Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Adding a row to repeating section at a certain position

1689111663

Edited 1689111737
Maike (GM)
Sheet Author
Hi, I'm working on a character sheet where I would like to add a new row to a repeating section at certain position. The row is created in a worker script, which is triggered by pressing a button that is in a row of the repeatable. My goal is to add the created row directly below the row that triggered the script. My first try was to Create a new row (using generateRowID) Then reorder the repeating section with setSectionOrder. Unfortunately I ran into the UI glitches that this function causes Here is my script: on("clicked:repeating_skills:addskill", function(eventInfo) { const rowid = eventInfo.sourceAttribute.split('_')[2]; getAttrs([`repeating_skills_${rowid}_attribute`], function(v) { var newrowid = generateRowID(); var newrowattrs = {}; // add some attributes here, then call setAttrs() setAttrs(newrowattrs, undefined, function() { getSectionIDs("skills", function(idArray) { // reorder the positions var originalRowPos = idArray.indexOf(rowid.toLowerCase()); var newRowPos = idArray.indexOf(newrowid.toLowerCase()); idArray.splice(newRowPos, 1); idArray.splice(originalRowPos + 1, 0, newrowid.toLowerCase()); setSectionOrder("skills", idArray); // This part causes UI glitches :-/ }); }); }); }); So is there any other way to achieve this?
1689124312

Edited 1689124359
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Unfortunately, there really isn't. It's a bug I've been hoping Roll20 would fix for several years now. The only "solution" is to destroy all the items in the section, and then remake them in the order you want them in. Which is less than ideal for a whole host of reasons.
1689153313

Edited 1689153332
GiGs
Pro
Sheet Author
API Scripter
Thanks, Scott. I wondered if there was a better solution. The only way I know if is to destroy them all and recreate them in the order you want. Definitely not recommended.
1689174597

Edited 1689174613
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
I mean technically, I've cobbled together a "solution" for the scion and they came from... sheets, but I'm not really even sure which part of it made it all work.
1689178907
GiGs
Pro
Sheet Author
API Scripter
I saw a post where you mentioned that. I wondered if you'd come up with another approach more recently. I have a deletion version on the forums, but I would never use it Also does setSectionorder work properly if you close the sheet afterwards? I have noticed some glitches go away if you close the sheet then reopen it.
1689189135
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
If I remember correctly, closing the sheet resets the display issue and fixes the problem
1689190021
GiGs
Pro
Sheet Author
API Scripter
So, having a very visible instruction to close the sheet if you sort anything could avoid any issues
Thanks for your replies! Too bad there is no working solution yet! It seems that the combination of programmatically creating a row and reordering the rows is completely broken. It does not work in any realiable way at all, not even when closing and opening the sheet between steps. The original idea for my approach was to workaround the fact that repeatables cannot be nested, so I'm trying to find a way to emulate a list in a list. Seems like I have to try the delete-all-and-recreate aproach next :-/ I mean technically, I've cobbled together a "solution" for the scion and they came from... sheets, but I'm not really even sure which part of it made it all work. If you could find, extract and provide the relevant parts, I'm sure you would be a hero for all character sheet developers :-)
1689199371

Edited 1689199441
GiGs
Pro
Sheet Author
API Scripter
Maike (GM) said: Seems like I have to try the delete-all-and-recreate aproach next :-/ This works, here's a tested version I created for sorting a spell list. If it's any use, extract anything you need. The fields array at the top has to include every attribute in the repeating section. const fields = [ 'name' , 'class' , 'level' ]; on ( 'change:spell_sort' , ( event ) => {     if ( ! fields . includes ( event . newValue )) {         return ;     }     getSectionIDs ( 'repeating_spells' , idarray => {         const fieldnames = [];         // get the name of EVERY attribute on every row.         idarray . forEach ( id => fields . forEach ( field => fieldnames . push ( `repeating_spells_ ${ id } _ ${ field } ` )));                 getAttrs ([ ... fieldnames , 'spell_sort' ], v => {             const spell_sort = v . spell_sort ;             let allrows = [];             // need to go throw every row and get the values, and store them with out the id             idarray . forEach ( id => {                 // get all the value of every attribute in a row, and story it in object with the field name                 const thisrow = {};                 fields . forEach ( field => thisrow [ field ] = v [ `repeating_spells_ ${ id } _ ${ field } ` ]);                 allrows = [ ... allrows , thisrow ];             });             /* at this point we have all the attributes from the repeating section in an array of objects that looks something like             [                 {name: 'a spell', class: 'cleric', level: 3},                 {name: 'another spell', class: 'cleric', level: 3},                 {name: 'yet another spell', class: 'wizard', level: 1},             ]             now we need to sort the array. The Underscore function _.sortBy is easiest here             */             allrows = _ . sortBy ( allrows , spell_sort );             // now we create a new output function             const output = {};             allrows . forEach ( row => {                 //need to create a new id for each row, and create new complete repeating section attribute names for each field                 const rowid = generateRowID ();                 fields . forEach ( field => output [ `repeating_spells_ ${ rowid } _ ${ field } ` ] = row [ field ]);             });             // now we have a sorted copy of the rows we can add to the repeating section. But we also need to delete the old set.             // finally update the sheet. If a connection error happens between the previous row and next row, the entire repeating section is lost forever.             setAttrs ( output , { silent : true }, function () {                 idarray . forEach ( id => removeRepeatingRow ( `repeating_spells_ ${ id } ` ));             });         });     }); }); I'm wary of relying on this apporoach for repeated use, but you can test how it works.
Hey, thank you so much for that template, I adapted it a bit and it works very well! I have now successfully created an emulation of a nested list. Each row has a button that adds a new row just below it. Each row has a hidden attribute that controls the styling of the row, so that newly created rows look like nested list entries. One modification I did was that I added a helper function to make sure the generated RowIDs are unique, since there is an issue where repeated calls to this function do not always generate a new ID (see <a href="https://app.roll20.net/forum/post/8879969/generaterowid-does-not-always-generate-a-star-unique-star-rowid" rel="nofollow">https://app.roll20.net/forum/post/8879969/generaterowid-does-not-always-generate-a-star-unique-star-rowid</a> ) This is my (simplified) code, based on yours: &lt; fieldset class ="repeating_ myrepeating " &gt; &lt;!-- other inputs.. --&gt; &lt; button type ="action" name ="act_addsublistrow" &gt;Add row&lt;/ button &gt; &lt;/ fieldset&gt; // helper function to make sure that all generated IDs are unique function generateUniqueRowIDs(numberOfIds) { let uniquevalues = {}; while (Object.keys(uniquevalues).length &lt; numberOfIds) { uniquevalues[generateRowID()] = true; } return Object.keys(uniquevalues); } // list with all attributes of the repeating const fields = ['name', 'skill', 'totalvalue', 'mod', 'sublist']; on('clicked:repeating_myrepeating: addsublistrow ', (eventInfo) =&gt; { // extract the id of the row which triggered the script. // For some reason the id is not lowercase, but the result of getSectionIDs() is lowercase // so to compare the IDs later, toLowerCase() is needed here const triggerRowId = eventInfo.sourceAttribute.split('_')[2].toLowerCase(); getSectionIDs('repeating_myrepeating', idarray =&gt; { const fieldnames = []; // get the name of EVERY attribute on every row. idarray.forEach(id =&gt; fields.forEach(field =&gt; fieldnames.push(`repeating_myrepeating_${id}_${field}`))); getAttrs(fieldnames, v =&gt; { let allrows = []; // need to go throw every row and get the values, and store them with out the id idarray.forEach(id =&gt; { // get all the value of every attribute in a row, and story it in object with the field name const thisrow = {}; fields.forEach(field =&gt; thisrow[field] = v[`repeating_myrepeating_${id}_${field}`]); allrows = [...allrows, thisrow]; // Create an additional row right after the row that triggered the script if (id === triggerRowId) { const newrow = {}; // copy values from existing row, and/or set new values fields.forEach(field =&gt; newrow[field] = v[`repeating_myrepeating_${id}_${field}`]); newrow["name"] = v[`repeating_myrepeating_${id}_name`] + " Sublist entry"; // Set the sublist attribute. CSS will render this row in a different style to display as a nested / sub list entry newrow["sublist"] = "1"; allrows = [...allrows, newrow]; } }); // generate new unique IDs let newRowIds = generateUniqueRowIDs(allrows.length); // now we create a new output function const output = {}; allrows.forEach(row =&gt; { // create new complete repeating section attribute names for each field const rowid = newRowIds.shift(); fields.forEach(field =&gt; output[`repeating_myrepeating_${rowid}_${field}`] = row[field]); }); // finally update the sheet. If a connection error happens between the previous row and next row, the entire repeating section is lost forever. setAttrs(output, {silent:true}, function() { idarray.forEach(id =&gt; removeRepeatingRow(`repeating_myrepeating_${id}`)); }); }); }); });
1690743672

Edited 1690743703
GiGs
Pro
Sheet Author
API Scripter
Great! I have wondered about including a helper function for row id creation. While the wiki mentions that danger, I have never seen it happen, but that doesn't mean it's not a real danger - just rare.
1690744970
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
The non unique row id issue is something that would only happen if you were making multiple rows at a time in a sheetworker. Even then, it's exceedingly rare. I typically don't ever worry about it, especially since in order to ensure it didn't happen, you'd have to always get all the IDs for all sections every time an operation is run in the sheetworkers. That's not an infrastructure that most people want (see the low adoption rate of K-scaffold).
1690751376

Edited 1690751459
Maike (GM)
Sheet Author
My understanding of the issue is that the IDs are generated based on the current time of the browser. Some browsers in some cases seem to limit the resolution/precision of the time, so that if you call the function twice in a very short time frame, it could happen that the same timestamp is used and so it generates the same ID. At least that is my assumption, I have never reproduced the issue. I have no idea how common the problem is, but I figured the small overhead is worth it to make sure it does not happen, because it would be really annoying to lose a row.
1690755175
GiGs
Pro
Sheet Author
API Scripter
Myt impression is its very rare, but for a function like this that has the potential to destroy data, its worth eliminating that possibility.