Character Vault
Any Concept / Any System
Compendium
Your System Come To Life
Roll20 for Android
Streamlined for your Tablet
Roll20 for iPad
Streamlined for your Tablet

Difference between revisions of "Character Sheet Development/Repeating Section"

From Roll20 Wiki

Jump to: navigation, search
m (Tips)
m
Line 443: Line 443:
 
** {{fpl|10304798/ Referencing an attr from within a repeating section}}
 
** {{fpl|10304798/ Referencing an attr from within a repeating section}}
 
* {{fpl|10304806/ Calculated rolls with repeatable rows}}
 
* {{fpl|10304806/ Calculated rolls with repeatable rows}}
* [[RepeatSum]]
+
* {{repo|shdwjk/TheAaronSheet TheAaronSheet}} - A facade for Sheet Worker Tasks and Utility Functions. Contains a great function for sheet troubleshooting/debugging, and function for repeating sections
 
+
 
=Related Pages=
 
=Related Pages=
 
* [[Designing Character Sheet Layout]] - repeating sections vary in size depending on how many components it has, and will impact your general design
 
* [[Designing Character Sheet Layout]] - repeating sections vary in size depending on how many components it has, and will impact your general design
 
+
* [[RepeatingSum]] - sheetworker example useful for repeating section.
 +
<br><br>
 
[[Category:Character Sheet Creation]]
 
[[Category:Character Sheet Creation]]

Revision as of 08:47, 10 August 2021

Main Page: Building Character Sheets


This is a for how Repeating Sections are created, used & modified when creating & editing character sheets.

Repeating Sections

General

Sometimes you may have a type of object where there may be one or more of them, and it's not known ahead of time how many there are. A good example of this is the Skills listing for a Character in Savage Worlds. Roll20's sheets allow you to define a template for each item in the section, and the player can then add as many of these in the listing as they need.
Example:

<h3>Skills</h3>
<fieldset class="repeating_skills">
  <button type="roll" name="roll_skill" value="/em uses @{skillname}, and rolls [[@{dtype}]]"></button>
  <input type="text" name="attr_skillname" value="">
  <select name="attr_dtype" class="dtype"> 
    <option value="d4">d4</option>
    <option value="d6">d6</option>
    <option value="d8">d8</option>
    <option value="d10">d10</option>
    <option value="d12">d12</option>
  </select>
</fieldset>

Contents

Definition & Restrictions

To define a repeating section, create a <fieldset>-element. Then define give it a unique class with the repeating_ prefix and a name written in lowercase & without underscores. Inside the <fieldset>-element place the info fields that a instance of the section should have.

  • Each repeating section should have a unique name, and you
    cannot use underscores
    .
    Use repeating_melee, or repeating_meleeweapon,
    not
    repeating_melee_weapon
    • attribute names within one repeating section doesn't have to be unique compared to attribute names in another section. Both repeating_melee and repeating_ranged can both have an attribute named attr_name, as they don't affect eachother
  • Class names should be all lowercase. If it isn't, you'll have problems with launching buttons in a repeating section from macros and scripts
  • It's not possible to create repeating sections inside repeating sections Nesting them isn't possible, but you can vote on the suggestion(Forum) and comment why we want it.
  • referencing buttons in rep sections behaves slightly different than regular parts of a sheet.
  • All attributes in a repeating section should have a unique name that isn't already used by a "normal" attribute. If you have attr_spellname in your repeating section, you can not have a "normal" attr_spellname outside of the repeating section, but it's fine to have a attr_spellname inside another repeating section. Se example below:
Good Example Bad example
<fieldset class="repeating_spell-level1">
  <input type="text" name="attr_spellname">
</fieldset>
<fieldset class="repeating_spell-level2">
  <input type="text" name="attr_spellname">
</fieldset>
<input type="text" name="attr_spellname">
<fieldset class="repeating_spell-level1">
  <input type="text" name="attr_spellname">
</fieldset>
The attribute attr_spellname is only present inside repeating sections. The attribute attr_spellname can be reused in this way The attribute attr_spellname is present in both a "normal" attribute and a repeating section. The attribute attr_spellname should not be used in this way.
  • Repeating sections are hard or impossible to create around a HTML <table>, and is recommended to be placed within a single cell if you insist trying to use <table>.


When the sheet is displayed, Roll20 will automatically include:

  • Add , to allow the player to add as many of each item as needed.
  • Modify buttons, which when pressed will make it possible for players to drag&reorder items in the repeating section, along with showing the delete-button(#) for deleting individual items.


Each item will have its own set of fields (in the example above, each has its own attr_dtype and attr_skillname).

Internally, each repeating item is stored in an attribute like so: repeating_skills_-ABC123_dtype or repeating_skills_$0_skillname. The rowID (the -ABC123 part of the previous example) will never change for a repeating section row, so you can reference it in macros, abilities, and API scripts. New rows that you add will be randomly assigned a new unique ID. By default, Rows are ordered in the order in which they were created, the oldest at the top.

Styling Repeating Sections

Main Page: CSS Wizardry

It's possible to style your repeating sections in a variety of ways. However, you can't just write your CSS as though the <fieldset> that's in your HTML source is what the user is viewing. After writing the code for your repeating section, here is how it will look when rendered to the user:

<fieldset class="repeating_my-repeating-section" style="display: none;">
    <!-- my-repeating-section HTML -->
</fieldset>
<div class="repcontainer" data-groupname="repeating_my-repeating-section">
    <div class="repitem">
        <div class="itemcontrol">
            <button class="btn btn-danger pictos repcontrol_del">#</button>
            <a class="btn repcontrol_move">≡</a>
        </div>
        <!-- my-repeating-section HTML -->
    </div>
    <!-- there will be a div.repitem for each item the user has actually added to the sheet -->
</div>
<div class="repcontrol" data-groupname="repeating_my-repeating-section">
    <button class="btn repcontrol_edit">Modify</button>
    <button class="btn repcontrol_add">+Add</button>
</div>

When you click the Modify button, the Add button is is set to display: none and the text of the Modify button is changed to "Done". When you click Done, the Add button is set to display: inline-block and the text of the Done button is changed to "Modify". While modifying repitems, the repcontainer gains the class "editmode".

Armed with this knowledge, you can do numerous things to alter how your repeating sections are displayed on the final character sheet. For example, you can have multiple repeating items per row:

.repcontainer[data-groupname="repeating_skills"] > .repitem {
    display: inline-block;
}

Note: You do not prefix the rep* classes with sheet-!

Remember to use the [data-groupname="repeating_..."] attribute selector if you want to only apply the style to a single repeating section. Of course, if you want the style to affect all of your repeating sections, that's not needed.

What Can't You Do?

You cannot:

  • Change the "display" property of the original <fieldset>.
  • Change the text of the Add, Modify/Done, Delete, or Move buttons.
    • However, you could set their opacity to 0 and display something in their place, much like styling checkboxes and radios, as well as add ::before or ::after pseudo-elements to them.
  • Change the "display" property of the Add button after the user has pressed Modify once.

Counting Items

Live Demo

CSS can let you count things. This can be applied in multiple contexts for character sheets, but one option is to count how many repeating items are in your repeating section:

<div class="sheet-repeating-fields">
    <fieldset class="repeating_my-repeating-section">
        <span class="sheet-counter"></span>
        <input type="text" name="attr_example">
    </fieldset>
</div>
.sheet-repeating-fields {
  counter-reset: sheet-rep-items;
}

.sheet-counter::before {
  counter-increment: sheet-rep-items;
  content: counter(sheet-rep-items) '. ';
}

Filter Repeating section displays according to criteria within the repeating items itself

Assume you want a tabbed section to organize/display spells. All the 1st circle spells are displayed under tab 1, all the 2nd circle spells under tab 2, Etc. One way to do this is to duplicate the html code for each spell level. You have 10 lists, and the tabs choose which of the 10 lists to display. The following code shows how to have one list, but to filter the list so it only displays certain spells.

GiGs has a few forum posts that demonstrates an alternative a likely better method to do the same as this in the following forum threads:


The HTML looks something like this.

    <span class="sheet-fixed-width" style="width: 11em;"><b> Spell List </b></span><br>
    <input type="radio" name="attr_tab-spells" class="sheet-tab sheet-spells-tab0" value="0" title="All circles" checked style="margin-left:1px;"/>
    <span class="sheet-tab sheet-spells-tab0">All</span>
    <input type="radio" name="attr_tab-spells" class="sheet-tab sheet-spells-tab1" value="1" title="Circle 1"/>
    <span class="sheet-tab sheet-spells-tab1">Circle 1</span>
    <input type="radio" name="attr_tab-spells" class="sheet-tab sheet-spells-tab2" value="2" title="Circle 2"/>
    <span class="sheet-tab sheet-spells-tab2">Circle 2</span>
    <input type="radio" name="attr_tab-spells" class="sheet-tab sheet-spells-tab3" value="3" title="Circle 3"/>
    <span class="sheet-tab sheet-spells-tab3">Circle 3</span>

    <div class="sheet-section sheet-section-sp-list">
        <fieldset class="repeating_spell">
            <input type="radio" name="attr_SP_Circle" class="sheet-hidden sheet-spells-tab0" value="0" checked />
            <input type="radio" name="attr_SP_Circle" class="sheet-hidden sheet-spells-tab1" value="1"/>
            <input type="radio" name="attr_SP_Circle" class="sheet-hidden sheet-spells-tab2" value="2"/>
            <input type="radio" name="attr_SP_Circle" class="sheet-hidden sheet-spells-tab3" value="3"/>
            <div class="sheet-filtered-box">
                <span><select name="attr_SP_Circle" class="sheet-nowrap sheet-label-down" style="width:5em;">
                    <option value="0" selected title"Please choose the circle for this spell.">-</option>
                    <option value="1">1</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                </select>Circle</span>
                <span class="sheet-nowrap">
                        <input name="attr_SP_Name" type="text" style="width:12em;" placeholder="Spell Name" ></span>

More fields.
            </div>
        </fieldset>
    </div>

The relevant part of the css looks like this.

div.sheet-section-sp-list {
    max-height: 999999px;
    visibility: visible;
    opacity: 1;
    transition: opacity 0.5s linear 0s;
    overflow: hidden;
}

.charsheet .repcontainer .repitem,          /* A filterbox should look exactly like a repitem.  */
.sheet-filtered-box {
    padding: 5px 5px 2px 5px;
    margin: 5px;
    border: 2px solid #CCCCCC;
    border-radius: 3px;
}

.repcontainer .repitem {
    overflow: hidden;
}

.sheet-section-sp-list>fieldset.repeating_spell+.repcontainer>.repitem {     /* A repitem within a spellslist should functionally not exist, it's place is taken by a filterbox. */
    padding: 0px;
    margin: 0px;
    border: none;
    border-radius:;
}

.sheet-filtered-box {       /* By default, display no items of a filter box */
    display: none;
}
            /* Decide which specific repitems to display.  */
input.sheet-spells-tab0:checked~.sheet-filtered-box,      /* Always display all spells of circle zero.  */
.sheet-spells-tab0:checked~.sheet-section-sp-list>fieldset.repeating_spell+.repcontainer>.repitem .sheet-filtered-box,  /* When tab0 is checked, display everything no matter what which of the inner boxes are checked. */
.sheet-spells-tab1:checked~.sheet-section-sp-list>fieldset.repeating_spell+.repcontainer>.repitem input.sheet-spells-tab1:checked~.sheet-filtered-box,
.sheet-spells-tab2:checked~.sheet-section-sp-list>fieldset.repeating_spell+.repcontainer>.repitem input.sheet-spells-tab2:checked~.sheet-filtered-box,
.sheet-spells-tab3:checked~.sheet-section-sp-list>fieldset.repeating_spell+.repcontainer>.repitem input.sheet-spells-tab3:checked~.sheet-filtered-box {
    display: block;
} 

So basically what this does is it makes a filterbox look exactly like a repitem. By default, no filterboxes are displayed. The last css entries basically say where tab X above the repeating field is checked, and the same radio button inside a specific repitem is checked, then display that item.

Duplicate a repeating section name to display the same data on more than one tab or present a summary of the data elsewhere

I am not certain that this trick is recommended by the developers, but as of this writing, it works.

You can put the same repeating section name in your html more than once in order to display the same information in multiple styles. You can have individual items displayed one way in one instance of the section (for example as a list of select/options) and a different way (for example as a read only text field) elsewhere. It is extremely useful to combine this trick with the previous section (filtering), so that only a select subset are displayed in different places, but you can do this with the convenience of having the data stored once in one list.

For example, on one tab you can have lots of fiddly options and choices, but on another tab you can display the same list of data pared down to it's minimalist essentials. If the user needs the full list, he can go to the details tab. But the items he uses most frequently fit compactly in a section of another tab. In ether case he is looking at, and manipulating the same list of data, not a list and a separate copy.

Here are the rules:

  • You can repeat repeating section names. Each repeated section will share the same rows and items, but each can present the data in a different way. For example : <fieldset class="repeating_weapons sheet-filtershortlist sheet-filtersummarylist"> the additional class tags can be used to differentiate how this section is to be styled as opposed to other repeating_weapons lists display the data.
  • Not all items must be present or displayed in all lists. However each item must be present in every list in which it is used. For example, if you have an auto-calc field, or a button that uses a field, All the fields used in the autocalc, or passed by the button, must be present in the same instance of the repeating section, it will not go looking for it in other instances of the repeating section. The referenced field may be hidden, but must be present.
  • You can change how fields are presented. For example they can be editable in one instance, but readonly in another.
  • If you change repitem to inline-block, it will float "rows" up so that it can display more than one per line.
fieldset.sheet-filtersummarylist+.repcontainer .repitem {
  display: inline-block;
  margin-right: 2em;
}
  • If you want the user to only add or modify items in one place, you can get rid of the Add/Modify buttons in the other places by simply adding display: none: to a set of tags similar to this: fieldset.sheet-filtershortlist~.repcontrol where of course the .sheet tag is whatever tag you specify in your html.

Sheetworkers

Main Page: Sheetworkers

Referencing, creating, deleting attributes in repeating sections isn't as easy & straightforward as with regular attributes, and sometimes use separate sheetworker functions than normal parts of the sheet.

This section displays only the repeating-section examples, while linking to the original section in the main sheetworker documents.

  • RepeatingSum A great snippet making it easier to sum up values form a repeating section, such as total weight of an inventory.
  • TheAaronSheet collection of sheetworker automation & simplification, including things making repeating sections easier to handle.

Events

change:<attribute_name>

Allows you to listen to the changes of specific attributes, or in the case of a repeating sections, any changes in the entire section. It's very straightforward:

on("change:repeating_spells:spellname", function(eventInfo) {
   //Do something here
   // eventInfo.sourceAttribute is the full name (including repeating ID) of the attribute 
   // that originally triggered this event, 
   // however the entire string will have been translated into lowercase and thus might not be 
   // suitable for being fed directly
   // into getAttrs() or other uses without being devandalized first.
});

on("change:repeating_spells", function(eventInfo) {
   //Would be triggered when any attribute in the repeating_spells section is changed
   // eventInfo.sourceAttribute is the full name (lowercased)(including repeating ID) 
   // of the attribute that originally triggered this event. 
});


For attributes in repeating fields, all of the following would be triggered when the repeating_spells_SpellName attribute is updated: change:repeating_spells:spellname, change:repeating_spells, change:spellname, change:spellname_max. This gives you maximum flexibility in choosing what "level" of change event you want to bind your function to.

change:_reorder:<sectionname>

Sheetworkers can also listen for a change event of a special attribute that is modified whenever a repeating section is re-ordered.

on("change:_reporder:<sectionname>", function(eventInfo) {
   // Where <sectionname> above should be a repeating section name, such as skills or spells
});


remove:repeating_<groupname>

This is an event which will fire whenever a row is deleted from a repeating field section. You can also listen for a specific row to be deleted if you know its ID, such as on(remove:repeating_inventory:-ABC123)


on("remove:repeating_inventory", function(eventInfo) {
     //Fired whenever a row is removed from repeating_inventory
     // eventInfo.sourceAttribute is the full name (including ID) of the first attribute that triggered the event (you can use this to determined the ID of the row that was deleted)
});

The removed:repeating_<groupname> function's eventinfo includes a special property removedInfo, that displays all of the attributes of the now removed repeating section.

on("remove:repeating_inventory", function(eventinfo) {
   console.log(eventinfo.removedInfo);
});


clicked:<button_name>

This event will trigger when an action button is clicked. The button's name will need to start with act_. Example:


<fieldset class="repeating_skills"> 
  <button type="action" name="act_activate">Activate!</button> 
</fieldset>
<script type="text/worker">
on("clicked:repeating_skills:activate", function() {
  console.log("Activate button clicked");
});
</script>


Functions

getAttrs(attributeNameArray, callback)

[Asynchronous]

The getAttrs function allows you to get the values of a set of attributes from the sheet. The "_max" suffix is supported, so getAttrs( ["Strength", "Strength_max"], func) will get both the current and max values of Strength. Note that the function is asynchronous, which means that there is no guarantee that the order in which multiple getAttrs calls are made is the order in which the results will be returned. Rather, you pass a callback function which will be executed when the values have been calculated. The callback function receives a simple JavaScript object with a list of key-value pairs, one for each attribute that you requested.

Values in repeating sections require a little special handling. If the event that you are inside of is already inside of a repeating section, you can simply request the variable using its name prefaced by the repeating group name and you will receive the value in the same repeating section the event was triggered in. For example, if we have a repeating_spells section that has both SpellName, SpellLevel, and SpellDamage, then:


on("change:repeating_spells:spelllevel", function() {
   getAttrs([
      "repeating_spells_SpellDamage",
      "repeating_spells_SpellName"
    ], function(values) {
      //values.repeating_spells_SpellDamage and values.repeating_spells_SpellName 
//will both be from the same repeating section row that the SpellLevel that changed is in.
   });
});

On the other hand, if you aren't currently in a repeating section, you can specifically request that value of a field in a repeating section row by specifying its ID manually:


getAttrs(["repeating_spells_-ABC123_SpellDamage"]...

You can also request the _reporder_repeating_<sectionname> attribute with getAttrs() to get a list of all the IDs in the section that have been ordered. However note that this may not include the full listing of all IDs in a section. Any IDs not in the list that are in the section are assumed to come after the ordered IDs in lexographic order.

setAttrs(values,options,callback)

For repeating sections, you have the option of using the simple variable name if the original event is in a repeating section, or you can specify the full repeating section variable name including ID in any event.


on("change:repeating_spells:spelllevel", function() {
   getAttrs(["repeating_spells_SpellLevel", "repeating_spells_SpellName"], function(values) {
      setAttrs({
         repeating_spells_SpellDamage: Math.floor(values.repeating_spells_SpellLevel / 2) + 10
      });
   });
});

getSectionIDs(section_name,callback)

[Asynchronous]

This function allows you to get a list of the IDs which currently exist for a given repeating section. This is useful for calculating things such as inventory where there may be a variable number of rows.


on("change:repeating_inventory", function() {
   getSectionIDs("inventory", function(idarray) {
      for(var i=0; i < idarray.length; i++) {
         //Do something with the IDs
      }
   });
});

Note that you may use GetAttrs() (described above) to request the _reporder_repeating_<sectionname> attribute to get a list of all the IDs in the section that have been ordered. However note that this may not include the full listing of all IDs in a section. Any IDs not in the list that are in the section are assumed to come after the ordered IDs in lexographic order.
Which is to say that getSectionIDs() will get all IDs - but not in the order that they are displayed to the user. getAttrs( _reporder_repeating_<sectionname>, ... ) will return a list of all IDs that have been moved out of their normal lexographic order. You can use the following function as a replacement for getSectionIDs to get the IDs in the order that they are displayed in instead.

var getSectionIDsOrdered = function (sectionName, callback) {
  'use strict';
  getAttrs([`_reporder_${sectionName}`], function (v) {
    getSectionIDs(sectionName, function (idArray) {
      let reporderArray = v[`_reporder_${sectionName}`] ? v[`_reporder_${sectionName}`].toLowerCase().split(',') : [],
        ids = [...new Set(reporderArray.filter(x => idArray.includes(x)).concat(idArray))];
      callback(ids);
    });
  });
};

generateRowID()

A synchronous function which immediately returns a new random ID which you can use to create a new repeating section row. If you use setAttrs() and pass in the ID of a repeating section row that doesn't exist, one will be created with that ID.

Here's an example you can use to create a new row in a repeating section:


var newrowid = generateRowID();
var newrowattrs = {};
newrowattrs["repeating_inventory_" + newrowid + "_weight"] = "testnewrow";
setAttrs(newrowattrs);

removeRepeatingRow( RowID )

A synchronous function which will immediately remove all the attributes associated with a given RowID and then remove the row from the character sheet. The RowID should be of the format repeating_<sectionname>_<rowid>. For example, repeating_skills_-KbjuRmBRiJNeAJBJeh2.

Here is an example of clearing out a summary list when the original list changes:

on("change:repeating_inventory", function() {
   getSectionIDs("repeating_inventorysummary", function(idarray) {
      for(var i=0; i < idarray.length; i++) {
        removeRepeatingRow("repeating_inventorysummary_" + idarray[i]);
      }
   });
});

API

TODO

Tips

Related Pages