Filtering a Repeating Section

Let’s say you have a big section, like a spell list, and want players to be able to filter down to see just level 1 spells, or just level 5 spells. You can do this. The Roll20 Wiki has a good example of a CSS Only method which is further defined by Rabulias here.

That thread also describes a sheet worker method by me, and I prefer that approach (not surprisingly!), because the CSS is much simpler. Since it is spread over several comments, this post brings the method together and both revises it to use modern code and expands on it to add extra features.

Version 1 – Filter By Level

In this approach, you need to first create the section with HTML, have a sheet worker that updates some hidden attributes, and CSS based n those attribute values to hide or display different rows.

In the HTML, we first create a radio button with values from -1 to 5. They would probably have labels too, and might use a select dropdown instead. (See versioon 2 for an example of this).

The fieldset here would likely include many more attributes. here’s we only include the spell level because that’s what we are demonstrating. The user selects a level for their spell.

Everything for the spell must be inside the spell-display div since that is what will be hidden or displayed.

<div class="spell_tabs">
    <input type="radio" name="attr_spell_tabs" value="-1">
    <input type="radio" name="attr_spell_tabs" value="0">
    <input type="radio" name="attr_spell_tabs" value="1">
    <input type="radio" name="attr_spell_tabs" value="2">
    <input type="radio" name="attr_spell_tabs" value="3">
    <input type="radio" name="attr_spell_tabs" value="4">
    <input type="radio" name="attr_spell_tabs" value="5">
</div>
<fieldset class="repeating_spells">
    <input type="hidden" class="toggle-show" name="attr_show" value="1">
    <div class="spell-display">
        <select name="attr_level">
            <option selected>-1</option>
            <option>0</option>
            <option>1</option>
            <option>2</option>
            <option>3</option>
            <option>4</option>
            <option>5</option>
        </select>
    </div>
</fieldset>Code language: HTML, XML (xml)

Then we have a sheet worker. This sets the hidden show attribute to 0 or 1.

on('change:spell_tabs', () => {
        getSectionIDs('repeating_spells', id_array => {
            const fields = [];
            id_array.forEach(id => {
                fields.push(section_name('spells',id,'level'));
            });
            getAttrs(['spell_tabs', ...fields], v => {
                const output = {};
                const level = +v.spell_tabs || 0;
                id_array.forEach(id => {
                    const thislevel = +v[section_name('spells',id,'level')] || 0;
                    output[section_name('spells',id,'show')] = (level === -1 || level === thislevel) ? 1 : 0;
                });
                setAttrs(output);
            });
        });
    });Code language: JavaScript (javascript)

Finally, in the CSS, we use the class of the show attribute to decide which spells to hide.

input.toggle-show[value="0"] ~ div.spell-display {
    display: none;
}Code language: CSS (css)

And that’s it. so with the radio buttons you select which spell level to display (from all, or one specific level), and everything else is hidden. If you prefer to use the value=”1″, this is the CSS to use.

input.toggle-show:not([value="1"]) ~ div.spell-display {
    display: none;
}Code language: CSS (css)

And that’s it! But we can do more.

Version 2 – Filter By Class and Level

Lets say your spell list shows class, and you want to narrow down by class and level. And you want to show all spells below a certain level, or all above a certain level. Any filtering you want is possible – it kiust takes a bit more logic to create.

First, lets create the HTML. This time, our filter section will use dropdowns. We’ll have a single test for Class, and two dropdowns for Level – to enter above, below, and equal.

<div class="spell_tabs">
    <select name="attr_class_select">
      <option selected>Any</option>
      <option>Wizard</option>
      <option>Warlock</option>
      <option>Sorcerer</option>
      <option>Druid</option>
      <option>Cleric</option>
    <select>
    <select name="attr_spell_type">
      <option value="-1">Less Than</option>
      <option value="0" selected>Equal</option>
      <option value="1">Greater Than</option>
    <select>
    <select name="attr_spell_level">
      <option value="-1" selected>Any</option>
      <option>0</option>
      <option>1</option>
      <option>2</option>
      <option>3</option>
      <option>4</option>
      <option>5</option>
    <select>
    <button type="action" name"act_spell_reset">Reset</button>
</div>
<fieldset class="repeating_spells">
    <input type="hidden" class="toggle-show" name="attr_show" value="1">
    <div class="spell-display">
        <select name="attr_class">
           <option value="Any" selected>Pick One</option>
           <option>Wizard</option>
           <option>Warlock</option>
           <option>Sorcerer</option>
           <option>Druid</option>
           <option>Cleric</option>
        <select>
        <select name="attr_level">
            <option selected>-1</option>
            <option>0</option>
            <option>1</option>
            <option>2</option>
            <option>3</option>
            <option>4</option>
            <option>5</option>
        </select>
    </div>
</fieldset>Code language: HTML, XML (xml)

The sheet worker needs to be a bit more complex. It has to cpmpare each spell detail against the initial dropsdowns, and return either 0 or 1.

   on('change:class_select change:spell_type change:spell_level', () => {
        getSectionIDs('repeating_spells', id_array => {
            const fields = [];
            id_array.forEach(id => {
                fields.push(section_name('spells',id,'level'));
                fields.push(section_name('spells',id,'class'));
            });
            getAttrs(['class_select', 'spell_type', 'spell_level', ...fields], v => {
                const output = {};
                const level = +v.spell_level || 0;
                const compare = +v.spell_type || 0;
                const class_chosen = v.class_select;

                id_array.forEach(id => {
                    const this_level = +v[section_name('spells',id,'level')] || 0;
                    const this_class = v[section_name('spells',id,'class')] ;
                    console.info({v, this_class, class_chosen,this_level});
                    let display = 0;
                    if(this_class === class_chosen || class_chosen==='Any') {
                       if(level === -1) {display = 1}
                       else if(compare === 1 && this_level >= level) {display = 1}
                       else if(compare === 0 && this_level === level) {display = 1}
                       else if(compare === -1 && this_level <= level) {display = 1}
                    }
                    output[section_name('spells',id,'show')] = display; 
                });
                setAttrs(output);
            });
        });
    });Code language: JavaScript (javascript)

Note that the filter test (the nested if statements) ould be more compact but I wanted to break down the steps so it’s easier to understand what’s happening

And finally, the CSS is exactly as above.

input.toggle-show:not([value="1"]) ~ div.spell-display {
    display: none;
}Code language: CSS (css)

And that’s it. build as complex a test as you want in the sheet worker, and let the filtering do its magic. Prepare for some frustration while building the filter though – it can be an ordeal!

Gotchas

Wit this version (and every version!), spells will be showin in the order they are created. Players can manually change the order, and really should. But the ways to do that programmatically are flawed and are a topic for another post. It might be best just to let players do it manually (they probably would, anyway).

Also, JavaScript doesnt like the word class. It has some reversed variable names. That’s why in the sheet worker, some variety was used there.

Finally, if you change a spells details, the filter doesn’t update immediately. Change the filter again to register the new changes.

But overall, you’ll find it an extremely capable technique.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.