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

Sheetworker: grab a range of repeating rows for calculations?

1710150768
vÍnce
Pro
Sheet Author
Trying to work out a repeating containers and equipment system.  Ideally, if we had nested repeating sections this would just work, but we don't... I have two repeating sections;  Container and Equipment.  I've worked out creating a new repeating "Container" that also creates a new repeating "Equipment".  These rows are synced. ie any changes made to one affects the other and vice versa.  Delete one and the other is deleted.  Cool beans.  simple mock-up; repeating_container 1. >> BACKPACK << 2. >> SATCHEL << 3. >> SMALL POUCH << repeating_equipment 1. Leather Armor 2. Small Shield 3. >> BACKPACK << 4. 50' rope 5. 3 torches 6. 1 weeks standard rations 7. waterskin 8. >> SATCHEL << 9. Potion of Healing 10. Small mirror 11. Chalk 12. >> SMALL POUCH << 13. 20 silver How would you suggest determining all items between two "Containers"?  Say, everything after BACKPACK but before SATCHEL.  Users will re-arrange their Equipment/Container rows as desired, so I'm not sure this is even possible without forcing them to fill-in a matching container name per Equipment row, which is not optimal. Other than the visual benefit of organizing one's gear with this system, I would ultimately like to handle weight calculations per container and even allow toggling carry/drop per container as well but I would need to grab a "range" of repeating rows to handle that.  Thoughts?  TIA
1710173650
GiGs
Pro
Sheet Author
API Scripter
vÍnce said: I'm not sure this is even possible without forcing them to fill-in a matching container name per Equipment row, I'm not sure this is possible either. It might be possible with roll20's version of jquery, but I havent done much with that yet, but I'm pretty sure you can't do it any other way. If you want your cintainer names to be created by the user, you have to rely on them spelling it correctly in the equipment list (which is far from ideal). It would be easier to have a preset list of names for containers, and if you really wanted some complex code, maybe present a choice of which ones are available.
1710173812
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
so, the way I'd do this is make it all one repeating section, then have different types of things (container and item) that can be added to that section (through custom add buttons). Then items are automatically included in the first container above them in the order.
1710186189

Edited 1710186380
vÍnce
Pro
Sheet Author
My fallback is requiring users to fill-in a "container" text field per item/row.  I'm sure I could do some validation/parsing on the field as well just to keep it "flexible/forgiving" to user's input but it would ultimately be up to the end-user to ensure they enter the appropriate container name.  I already have a "Location" field that is merely used to help differentiate item locations. I should be able to include name-matching to hook up container-based calculations, but I'm trying to avoid forcing per-row edits if possible. I am using action button substitution for the +add repcontrol to handle syncing between the two repeating sections. This works well with controlling what happens when someone adds a row. I thought about using a single repeating_equipment (and that is how the current sheet IS setup), but what happens when users re-arrange the rows?  Seems like this would be a problem regardless of how the "Containers" are added to the section...? For the record, I have seen a custom sheet that does use 2 repeating sections in this manner that somehow calcs the weight/cost per container and even updates these calcs as you re-arrange the rows. ie Sliding the "Container row" up/down the list automatically recalcs the container totals using only the rows after and before the next container row.  I tried to grok the code(major Kudos to the original author aka unknown to me...), but it's definitely beyond my level. ;-)  Kind of making it a personal challenge to see if I could come up with a similar system.
1710186567
GiGs
Pro
Sheet Author
API Scripter
vÍnce said: For the record, I have seen a custom sheet that does use 2 repeating sections in this manner that somehow calcs the weight/cost per container and even updates these calcs as you re-arrange the rows. which sheet was this? I'd like to have a look at it.
1710187130
vÍnce
Pro
Sheet Author
This is from someones personal sheet. It may have been commissioned. Not 100% sure. Definitely done by someone that has way more than my hobbyist-level understanding of js.  Not sure if it's appropriate for me to share publicly out of respect to the original author.
1710193353
GiGs
Pro
Sheet Author
API Scripter
Understood, I assumed it was one of the ones in the repo.
1710196881

Edited 1710197016
GiGs
Pro
Sheet Author
API Scripter
vÍnce said: For the record, I have seen a custom sheet that does use 2 repeating sections in this manner that somehow calcs the weight/cost per container and even updates these calcs as you re-arrange the rows. ie Sliding the "Container row" up/down the list automatically recalcs the container totals using only the rows after and before the next container row.  I tried to grok the code(major Kudos to the original author aka unknown to me...), but it's definitely beyond my level. ;-)  Kind of making it a personal challenge to see if I could come up with a similar system. This post is just ruminations, not solutions. You should be able to do this with one repeating section. What's happening here is that the row of the repeating section that contains the container name is being extracted, and everything after that row up to the row of the next container is being totalled. There's a function on the wiki about getting items in sorted order which should be able to be customised to get that. The trick as I see it would be to recognising which is a container. I guess thats where the second repeating section comes in. I still have no idea how you'd avoid manually typing the name, rather than having a dedicated container dropdown. I didn't understand initially what you were asking. This method of putting a container name in the repeating section and setting everything after that container to be inside that container is interesting, but alien to me. I was thinking of adding another column and a dropdown where the user would pick the container. So every item would have its own container, and then you'd go through the items and and add all those that matched a specific container. In your screenshot, the containers have a short name and a long name, and it looks like the long names are preset while the short names can be changed manually. I think you could probably make this completely userdefined like this: A repeating section for containers and another for equipment. Enter the names here of containers. Each time a new container row is created, a sheet worker is triggered, building an array of those names. It also builds an array of the container column in equipment. It then deletes any rows in equipment if it has a container that is not in the container array. Then it also adds a new row (containing the container name) for any container names that are not in the equipment list. You also have a second worker triggered on equipment changes (like moving a container), which calculates the appropriate capacity. This second worker would use the _reporder worker from the wiki (modified for this purpose) to properly calculate the capacity in area. It seems to me like that this method would be possible, but I havent created anything like that so I don't know how clunky it would be. That implementation is just thinking aloud - I'm not sure exactly how you;d want to do it. Making the container column read only and using CSS to make it look like that grey non-entry box yoiu have above would probably be a good idea.
1710197956

Edited 1710198002
GiGs
Pro
Sheet Author
API Scripter
vÍnce said: I've worked out creating a new repeating "Container" that also creates a new repeating "Equipment".  These rows are synced. ie any changes made to one affects the other and vice versa.  Are the rows synced beyond deletions. If you change the text in one section does it automatically get transferred to the other? I am thinking of a way to do this with a hidden column in each section used as an identifying key - I'm curious how you are doing it.
1710198463
vÍnce
Pro
Sheet Author
The dropdown in Containers does have some "predefined" options, that will auto-fill the row with weight and capacities, but there is also a "Custom" option in the same selector that just leaves everything blank.  Yes, the Name field syncs between the two repeating sections.  I've been able to work out using an action button to trigger creating "synced" rows between the two.  Updates and deletions work both-ways.  But, that seems to be the easy part. lol
1710198666
GiGs
Pro
Sheet Author
API Scripter
I'm interested in the method used to sync the name between the sections, imagining a user changing them on the fly. In my earlier cod method, I mentioned deleting a row when name doesn't match, but that step was before I'd thought of syncing the name between the sections (in my case, using a hidden column to properly identify the columns, something that users can't alter - I don't know how your method does it).
1710200434

Edited 1710200678
vÍnce
Pro
Sheet Author
html <div class="equipment"> <h1>Container</h1> <span class="hidden repcontrol-button"></span> <fieldset class="repeating_container"> <label> <span>Name:</span> <input type="text" name="attr_container_name" /> </label> </fieldset> <button type="action" name="act_add-container" class="repcontrol-button repcontrol-button-add btn">+Add</button> <h1>Items</h1> <span class="hidden repcontrol-button"></span> <fieldset class="repeating_equipment"> <label> <span>Name:</span> <input type="text" name="attr_equipment_name" /> </label> </fieldset> <button type="action" name="act_add-equipment" class="repcontrol-button repcontrol-button-add btn">+Add</button> </div> <input type="text" class="hidden" name="attr_container_array" value="" /> <input type="text" class="hidden" name="attr_equipment_array" value="" /> <script type="text/worker"> // text/javascript // text/worker // GiGs custom handling for number type and logs const int = (score, fallback = 0) => parseInt(score) || fallback; const float = (score, fallback = 0) => parseFloat(score) || fallback; const clog = (text, color = 'LawnGreen') => { const message = `%c ${text}`; console.log(message, `color: ${color}; font-weight:bold;`); }; let addDeleteContainer = {}; // generate a unique ID 100 % of the time const uniqueids = {}; const generateUniqueRowID = () => { let generated = generateRowID(); while (uniqueids[generated] === true) { // clog(`generateUniqueRowID: ${generated} is not a unique ID, trying again.`); generated = generateRowID(); } clog(`generateUniqueRowID: ${generated} verified as unique, returning.`); uniqueids[generated] = true; return generated; }; // replace repcontrol_add button with action Scott's method // Array of repeating fieldsets (used for custom +add button) const repeatingSections = ['repeating_container', 'repeating_equipment']; // do stuff when the +add action button is executed const addItem = (section) => { const output = {}; // adds new container row AND adds a new equipment row if (section === 'repeating_container') { clog(`container created`); const containerID = generateUniqueRowID(); const equipmentID = generateUniqueRowID(); const containerRow = `repeating_container_${containerID}`; const equipmentRow = `repeating_equipment_${equipmentID}`; output[`${containerRow}_id`] = containerID; output[`${equipmentRow}_id`] = equipmentID; addDeleteContainer(containerID, equipmentID, 'true'); } // adds a new equipment row only if (section === 'repeating_equipment') { clog(`equipment created`); const rowID = generateUniqueRowID(); const row = `${section}_${rowID}`; output[`${row}_id`] = rowID; } setAttrs(output, {silent: true}); }; // event trigger for all +add action buttons repeatingSections.forEach((section) => { const buttonName = section.replace(/repeating_/, ''); on(`clicked:add-${buttonName}`, () => addItem(section)); }); on('remove:repeating_container', (eventInfo) => { const id = eventInfo.sourceAttribute.split('_')[2]; addDeleteContainer(id, '', 'false'); }); on('remove:repeating_equipment', (eventInfo) => { const id = eventInfo.sourceAttribute.split('_')[2]; addDeleteContainer('', id, 'false'); }); // handles adding/removing containers from the 2 arrays addDeleteContainer = (containerID, equipmentID, newRow) => { const containerArray = []; const equipmentArray = []; const output = {}; getAttrs(['container_array', 'equipment_array'], (v) => { // clog(`containerID:${containerID} equipmentID:${equipmentID} newRow:${newRow}`); containerArray.push(...v.container_array); equipmentArray.push(...v.equipment_array); // container added if (newRow === 'true' && containerID !== '') { containerArray.push(containerID); } if (newRow === 'true' && equipmentID !== '') { equipmentArray.push(equipmentID); } // container removed if (newRow === 'false') { const containerMatchedIndex = containerArray.indexOf(containerID); if (containerMatchedIndex !== -1) { const equipmentMatchedID = equipmentArray[containerMatchedIndex]; removeRepeatingRow(`repeating_equipment_${equipmentMatchedID}`); containerArray.splice(containerMatchedIndex, 1); equipmentArray.splice(containerMatchedIndex, 1); } else { const equipmentMatchedIndex = equipmentArray.indexOf(equipmentID); if (equipmentMatchedIndex !== -1) { const containerMatchedID = containerArray[equipmentMatchedIndex]; removeRepeatingRow(`repeating_container_${containerMatchedID}`); equipmentArray.splice(equipmentMatchedIndex, 1); containerArray.splice(equipmentMatchedIndex, 1); } } } clog(` containerArray:[${containerArray}] equipmentArray:[${equipmentArray}] `); output.container_array = containerArray; output.equipment_array = equipmentArray; setAttrs(output, {silent: true}); }); }; // sync changes between the container row it's equipment row on('change:repeating_container:container_name change:repeating_equipment:equipment_name', (eventInfo) => { const sourceAttributeParts = eventInfo.sourceAttribute.split('_'); // clog(`sourceAttribute:${eventInfo.sourceAttribute} sourceType:${eventInfo.sourceType}`); const id = sourceAttributeParts[2]; // row id const section = sourceAttributeParts[1]; // repeating section name const attr = sourceAttributeParts.slice(3).join('_'); // full attr name const name = attr.split('_').slice(1).join('_'); // just the last portion of the attr name const output = {}; const containerArray = []; const equipmentArray = []; // clog(`Section:${section} ID:${id} Attribute:${attr}`); getAttrs(['container_array', 'equipment_array', `repeating_${section}_${id}_${attr}`], (v) => { containerArray.push(...v.container_array); equipmentArray.push(...v.equipment_array); const lowerCaseId = id.toLowerCase(); // check id against the appropriate array const matchedIndex = section === 'container' ? containerArray.findIndex((item) => item.toLowerCase() === lowerCaseId) : equipmentArray.findIndex((item) => item.toLowerCase() === lowerCaseId); clog(` containerArray:[${containerArray}] equipmentArray:[${equipmentArray}] `); if (matchedIndex !== -1) { // grab the id from the opposite array const matchedID = section === 'container' ? equipmentArray[matchedIndex] : containerArray[matchedIndex]; const attrValue = v[`repeating_${section}_${id}_${attr}`]; // clog(`matchedID:${matchedID}`); // sync the changes if (section === 'container') { output[`repeating_equipment_${matchedID}_equipment_${name}`] = attrValue; // clog(`Setting equipment row with attrValue: ${attrValue}`); } else if (section === 'equipment') { output[`repeating_container_${matchedID}_container_${name}`] = attrValue; // clog(`Setting container row with attrValue: ${attrValue}`); } } setAttrs(output, {silent: true}); }); }); </script> css .charsheet .repcontrol-button { /* Style the button as needed (add your styling rules here) */ outline: 2px solid lime; } .charsheet .repcontrol-button ~ .repcontrol > .repcontrol_add { display: none !important; } I only have this hooked up with "container_name" and "equipment_name" between the two sections at the moment, but it "should" work for any "shared" attribute names.  You have to look at how I'm extrapolating and concatenating the attr names in the last on(change) function.  Doing it like this because of how the html names are currently used.  This could be made much simpler if just using name. ie "repeating_container_id_name" and "repeating_equipment_id_name" instead of "repeating_container_id_container_name" and "repeating_equipment_id_equipment_name".  Regardless, this is a POC which seems to be working up to this point. ;-)
1710216135

Edited 1710216491
GiGs
Pro
Sheet Author
API Scripter
Vince, I'm happy you posted that code but I have only skimmed it so far but I'm hung up on this function. // generate a unique ID 100 % of the time const uniqueids = {}; const generateUniqueRowID = () => { let generated = generateRowID(); while (uniqueids[generated] === true) { // clog(`generateUniqueRowID: ${generated} is not a unique ID, trying again.`); generated = generateRowID(); } clog(`generateUniqueRowID: ${generated} verified as unique, returning.`); uniqueids[generated] = true; return generated; }; I'm wondering if I'm missing something, but I don't see how this is doing what it claims to. Let's follow the function (and preceding variable creation). the uniqueids variable is created. (I don't see why this is outside the function, btw, but that doesn't affect whether it works or not) generated is created. while uniqueids[generated] is tested, but at this point that is undefined so it is not true, and the while is skipped. uniqueids[generated]is set to true, after the while loop. This does nothing. (Also uniqueids[generated] is never set to false, and the structure suggests that it should be). The generated variable created in step 2 is returned. It's always this variable - all the other steps do nothing. Finish. It looks to me like this function could be attempting one of three things (and is doing none at present): checking that a generated value is actually created. For this, you'd need to rework that while loop so whether a value exists is checked. Checking that the value created is unique in a repeating section. For that, you'd need to pass the section's IDs into the function so the created id could be tested against them and recreated (e.g. a while loop) if it matches any Both 1 and 2: rerun the generateRowIf() function if the generated value is empty, or the generated value matches the repeating section. Am I missing something here? It's entirely possible that I am!
1710221083

Edited 1710230457
vÍnce
Pro
Sheet Author
I doubt you are missing anything GiGs. lol  I was working on something else(unrelated) that created many new repeating rows at once that synced with a fixed number of non-repeating rows and was occasionally experiencing issues with the generated row ids, ghosted rows, and repeats with a red box of death. (which I believe now may have been an issue of not handling mixed-case vs lowercase rowids). In my hunt to solve those issues I found some forum posts that suggested there might be issues with generateRowID(). This post (probably where a copied the function originally...); and a similar post here ; I also found other sheets on the repo that are using the same/similar custom function for generating rowids instead of generateRowID directly. Good example ; ( other sheets as well) I will claim ignorance. ;-P  I did look at the function when I first added it in and was also confused, but I'm often confused when working with js. It didn't break anything when I started calling it so I'm sure I moved on to something else and forgot about it. I grabbed that function for this experiment without consideration as well. How should it be fixed or more importantly, is it even necessary? Does "uniqueids" actually hold the previously generated ids? ie a placeholder. update/edit : I added an additional log to the function; clog(`uniqueids: ${JSON.stringify(uniqueids)}`); // generate a unique ID 100 % of the time const uniqueids = {}; const generateUniqueRowID = () => { let generated = generateRowID(); while (uniqueids[generated] === true) { // clog(`generateUniqueRowID: ${generated} is not a unique ID, trying again.`); generated = generateRowID(); } clog(`generateUniqueRowID: ${generated} verified as unique, returning.`); uniqueids[generated] = true;      clog(`uniqueids: ${JSON.stringify(uniqueids)}`); return generated; }; And it looks like uniqueids holds the generated keys for the session. Here's a log after adding a few rows; generateUniqueRowID: -Nsla16w3o80yx5jHLSD verified as unique, returning. uniqueids: {"-NslZzhgpCChJ-WEDVqF":true,"-NslZzhgpCChJ-WEDVqG":true,"-Nsla16w3o80yx5jHLSD":true} With this knowledge, does it look like it functions as expected?
1710262387
GiGs
Pro
Sheet Author
API Scripter
And it looks like uniqueids holds the generated keys for the session. Aha, that explains why the uniqueids variable is created outside the function (I was missing something!), and why this line is outside the while loop: uniqueids[generated] = true; uniqueids is a global variable whose value persists, and the function makes sure that the generated id doesnt match any other created that session, It doesn't compare the created id to any other already existing in the sheet, though, nor does it really check the first id created that session - only ones after that. But maybe that's enough to fix the problem it was created to handle. I don't think I've ever encountered the problem this function is meant to fix (though it occurs to me now there was a weird glitch I was having in one sheet that this might explain, so maybe I have). I didn't look closely enough in the linked discussion. :)
1710262908
vÍnce
Pro
Sheet Author
I believe the ids are generated using the time/date in some manner in order to prevent duplicates.  But for some "reason" it can glitch and produce a duplicate id, possibly exacerbated by creating multiple rows at once...? IDK
1710265656

Edited 1710448859
GiGs
Pro
Sheet Author
API Scripter
It does seem like it was created specifically in response to creating multiple rows at once (and incidentally, thats when the weird glitch I referred to happened), so that makes sense. So, yes, having access to the earlier IDs probably isn't necessary.
1710283643

Edited 1710283961
GiGs
Pro
Sheet Author
API Scripter
vÍnce said: How should it be fixed or more importantly, is it even necessary? I think a fix is not strictly neccesary. This issue with ids is addressed on the wiki, but the solution suggested there assumes you'll use getSectionIDs in the worker and you don't since you're saving the section ids to an array on the character sheet. Any solution I provided would involve a pretty big rewrite of the code (e.g. getting rid of those arrays because they already exist but are stored in two separate section arrays), because there'd be a knock on effect, and the resulting code would just do what you're already doing in a different way.
1710449293

Edited 1710613098
GiGs
Pro
Sheet Author
API Scripter
For Vince, I've created a version of the createUniqueIDs function and explained how to use it over here: <a href="https://cybersphere.me/create-unique-repeating-section-id/" rel="nofollow">https://cybersphere.me/create-unique-repeating-section-id/</a> You might not find it useful, because it does require that you pass the section ids to it, so you'd need to rewrite your code to use it. I'm considering creating a (hopefully shorter and simpler!) version of the synching sections code you have here that doesn't store the section IDs on the character sheet in a separate attribute. That code would use this function.
1710450185

Edited 1710450207
vÍnce
Pro
Sheet Author
Where there's a need...&nbsp; I'll for sure have a close look at your work. Thanks GiGs Isn't this something that could/should be addressed in roll20's function?
1710451814

Edited 1710452383
GiGs
Pro
Sheet Author
API Scripter
vÍnce said: Isn't this something that could/should be addressed in roll20's function? Roll20 does have a solution for it, described on the wiki (actually, I'm not sure if it's an official solution). They should defnitely update the function to avoid such a need, but it's not the only bug or idiosyncracy in their code that they've been informed about and left untouched.