Character Sheet Tools & Styles

Adding a few personal character sheet tools and styles for organization

As of 13.02.2024. See ბოლო ვერსია.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Character Sheet Tools & Styles
// @namespace    https://roll20.net
// @version      1.2
// @description  Adding a few personal character sheet tools and styles for organization
// @author       AppEternal
// @match        https://app.roll20.net/editor/character/*
// @match        https://app.roll20.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=roll20.net
// @grant        GM_addStyle
// @grant        GM.setValue
// @grant        GM.getValue
// @license MIT
// @run-at       document-start
// @require      http://code.jquery.com/jquery-3.4.1.min.js
// ==/UserScript==

const $ = window.jQuery;
const configs = {}

async function pageLoad(){
    await moveApiDown()
}

async function charaterSheetLoad(){
    await setDrawerClasses()
    await addListener()
    await spellSideBySide()
    await mathBox()
    await renameItems()
    await loadDroorStatus()
}

/*
 *
 * Toggle move api class
 *
*/
async function moveApiDown(){
    if(await get("moveApiDown")){
        $("#secondary-toolbar").addClass("move2bottom");
    }else{
        $("#secondary-toolbar").removeClass("move2bottom");
    }
}

/*
 *
 * Allow Renaming Items
 *
*/
async function renameItems(){
    if(await get("collapsibleCharacterSheetRename")){
        $(".toggleItems.toggleItemsStyle .name").removeClass("blockRename");
    }else{
        $(".toggleItems.toggleItemsStyle .name").addClass("blockRename");
    }
}

/*
 *
 * Toggle move api class
 *
*/
async function spellSideBySide(){
    if(await get("spellSideBySide")){
        $(".container.pc .page.spells").addClass("spellSideBySide");
    }else{
        $(".container.pc .page.spells").removeClass("spellSideBySide");
    }
}

/*
 *
 * Add math to the input boxes
 *
*/
async function mathBox(){
    if(await get("handleMath")){
        $("input:not(.skipMe)").change(function(event){
            let value = $(this).val()
            if(!/^[0-9+\-*]+$/.test(value)) return
            if(!/[*\-+]+/.test(value)) return

            $(this).val(parse(value))

        })
        $("input[type=number]").prop("type", "text");
        $("input:not(.skipMe)").addClass("skipMe")
    }
}
function parse(str) {
    return Function(`'use strict'; return (${str})`)()
}

/*
 *
 * save & load droor status
 *
*/
async function saveDroorStatus(id, status) {
    let current = await get("savedIds") ?? "{}"
    let converted = JSON.parse(current)
    converted[id] = status
    await set("savedIds", JSON.stringify(converted))
}
async function loadDroorStatus() {
    let current = await get("savedIds") ?? "{}"
    let converted = JSON.parse(current)
    let reSave = {}
    for (const id in converted){
        if(converted[id]) {
            toggleHide(id);
            reSave[id] = converted[id]
        }
    }
    await set("savedIds", JSON.stringify(converted))
}
/*
 *
 * set the classes for the droor items
 *
*/
async function setDrawerClasses() {
    console.log("Character Sheet Tools: adding drawer classes")
    let repList = $(".licensecontainer .repcontainer .repitem");
    let hiddenText = await get("collapsibleCharacterSheetString")
    let collapseEnabled = await get("collapsibleCharacterSheet")
    let style1 = await get("styleOption1")
    let style1String = await get("styleOption1String")
    let style2 = await get("styleOption2")
    let style2String = await get("styleOption2String")
    let style3 = await get("styleOption3")
    let style3String = await get("styleOption3String")
    console.log(`Character Sheet Tools: checking options - ${style1} - ${style1String}`)
    let style = await get("collapsibleCharacterSheetStyle")
    for (let i = 0; i < repList.length; i++) {
        let element = repList.eq(i);
        let blockText = element.text();
        let itemText = $(element).find(".item").text();
        let itemValue = $(element).find(".name ").val();
        let text = blockText + itemText + itemValue;

        if (style1 && text.includes(style1String)) {
            $(element).closest(".repitem").addClass("custom_Style1");
        }
        if (style2 && text.includes(style2String)) {
            $(element).closest(".repitem").addClass("custom_Style2");
        }
        if (style3 && text.includes(style3String)) {
            $(element).closest(".repitem").addClass("custom_Style3");
        }

        if (collapseEnabled && text.includes(hiddenText)) {
            if(style){
                $(element).closest(".repitem").addClass("toggleItemsStyle");
            }
            $(element).closest(".repitem").addClass("toggleItemsOpen");
            $(element).closest(".repitem").addClass("toggleItems");
        }
    }
}

/*
 *
 * Add collapsed draws
 *
*/
async function addListener() {
    if(!await get("collapsibleCharacterSheet")) return

    let repList = $('.licensecontainer .repcontainer .repitem');
    repList.off("click");
    repList.on("click", async function () {
        if($(this).find(`.trait`)?.length) return
        let id = $(this).attr("data-reprowid");
        await toggleHide(id);
    });

    let repListTrait = $(".licensecontainer .textbox .repcontainer .repitem .display-flag");
    repListTrait.off("click");
    repListTrait.on("click", async function () {
        let id = $(this).closest(".repitem").attr("data-reprowid");
        await toggleHide(id);
    });
}
async function toggleHide(id) {
    if(!await get("collapsibleCharacterSheet")) return
    let repList = $(".licensecontainer .repcontainer .repitem");
    let hiddenText = await get("collapsibleCharacterSheetString")
    let styleDrop = await get("collapsibleCharacterSheetStyle")
    let allRep = {};
    for (let i = 0; i < repList.length; i++) {
        let element = repList.eq(i);
        let nearestContainer = $(element)
        .closest(".repcontainer")
        .attr("data-groupname");
        let reprowid = element.attr("data-reprowid");
        if (!allRep[nearestContainer]) allRep[nearestContainer] = [];
        allRep[nearestContainer].push({
            id: reprowid,
            rep: element,
        });
    }
    let collapseRep = {};
    for (let i = 0; i < repList.length; i++) {
        let element = repList.eq(i);
        let blockText = element.text();
        let itemText = $(element).find(".item").text();
        let itemValue = $(element).find(".name ").val();
        let text = blockText + itemText + itemValue;
        if (text.includes(hiddenText)) {
            let nearestRepItem = $(element)
            .closest(".repitem")
            .attr("data-reprowid");
            let nearestContainer = $(element)
            .closest(".repcontainer")
            .attr("data-groupname");
            if (!collapseRep[nearestContainer]){
                collapseRep[nearestContainer] = [];
            }
            collapseRep[nearestContainer].push({
                id: nearestRepItem,
                rep: $(element).closest(".repitem"),
            });
        }
    }

    function getRange(start) {
        let end = undefined;
        let group = undefined;

        outGroup: for (const g in collapseRep) {
            for (let i = 0; i < collapseRep[g].length; i++) {
                if (collapseRep[g][i].id == start) {
                    end = collapseRep[g]?.[i + 1]?.id;
                    group = g;
                    break outGroup;
                }
            }
        }
        if (!allRep?.[group]) return;
        let startIndex = allRep?.[group].findIndex((x) => x.id == start);
        let endIndex = allRep?.[group].findIndex((x) => x.id == end);
        if (endIndex < 0) endIndex = allRep?.[group].length;

        return {
            start,
            startIndex,
            end,
            endIndex,
            group,
        };
    }
    function hide(id) {
        let range = getRange(id);
        if (!range?.group) return;
        let group = allRep[range.group];
        let isOpen = $(group[range.startIndex]?.rep).hasClass("toggleStatusClosed") ? false : true
        saveDroorStatus(id, isOpen)
        if(styleDrop) $(group[range.startIndex]?.rep).toggleClass("toggleItemsClose");
        $(group[range.startIndex]?.rep).toggleClass("toggleStatusClosed");
        for (let i = range.startIndex + 1; i < range.endIndex; i++) {
            $(group[i]?.rep).toggleClass("hide");
        }
    }
    hide(id);
}



/*
 *
 * Load Handlers
 *
*/
$(async function(){
    console.log("Character Sheet Tools: started")
    await waitForElm('#settings-accordion')
    $("#settings-accordion").append(MENUHTML)
    await loadConfigs()
    await configHandler()
    console.log("Character Sheet Tools: settings page loaded")
    await pageLoad()

});

$(async function(){
     console.log("Character Sheet Tools: loading saved configs")
    await loadConfigs()
    console.log("Character Sheet Tools: loading colors")
    await loadColors()
    while(true){
        console.log("Character Sheet Tools: waiting for character sheet")
        await waitForElm('#dialog-window')
        await charaterSheetLoad()
        console.log("Character Sheet Tools: character loaded")
        await waitForNoElm('#dialog-window')
    }






});
function waitForNoElm(selector) {
    return new Promise(resolve => {
        if (!document.querySelector(selector)) {
            return resolve(true);
        }
        const observer = new MutationObserver(mutations => {
            if (!document.querySelector(selector)) {
                observer.disconnect();
                resolve(true);
            }
        });

        // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}
function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(true);
        }
        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                observer.disconnect();
                resolve(true);
            }
        });

        // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}


/*
 *
 * Configs Manager
 *
*/
async function configHandler(){
    $("#moveApiDown").change(async function(){
        await set("moveApiDown", $(this).is(":checked"));
        await moveApiDown()
    });
    $("#handleMath").change(async function(){
        await set("handleMath", $(this).is(":checked"));
    });
    $("#spellSideBySide").change(async function(){
        await set("spellSideBySide", $(this).is(":checked"));
        await spellSideBySide()
    });
    $("#collapsibleCharacterSheet").change(async function(){
        await set("collapsibleCharacterSheet", $(this).is(":checked"));
    });
    $("#collapsibleCharacterSheetStyle").change(async function(){
        await set("collapsibleCharacterSheetStyle", $(this).is(":checked"));
    });
    $("#collapsibleCharacterSheetString").change(async function(){
        await set("collapsibleCharacterSheetString", $(this).val());
    });
    $("#collapsibleCharacterSheetColor").change(async function(){
        await set("collapsibleCharacterSheetColor", $(this).val());
    });
    $("#collapsibleCharacterSheetRename").change(async function(){
        await set("collapsibleCharacterSheetRename", $(this).is(":checked"));
        await renameItems()
    });


    $("#styleOption1").change(async function(){
        await set("styleOption1", $(this).is(":checked"));
    });
    $("#styleOption1String").change(async function(){
        await set("styleOption1String", $(this).val());
    });
    $("#styleOption1Color").change(async function(){
        await set("styleOption1Color", $(this).val());
    });
    $("#styleOption2").change(async function(){
        await set("styleOption2", $(this).is(":checked"));
    });
    $("#styleOption2String").change(async function(){
        await set("styleOption2String", $(this).val());
    });
    $("#styleOption2Color").change(async function(){
        await set("styleOption2Color", $(this).val());
    });
    $("#styleOption3").change(async function(){
        await set("styleOption3", $(this).is(":checked"));
    });
    $("#styleOption3String").change(async function(){
        await set("styleOption3String", $(this).val());
    });
    $("#styleOption3Color").change(async function(){
        await set("styleOption3Color", $(this).val());
    });
}

async function set(location, value){
    await GM.setValue(location, value);
    configs[location] = value;
}
async function get(location, dfValue = undefined){
    if(configs[location] !== undefined) return configs[location]
    let value = await GM.getValue(location)
    if(value == undefined && dfValue !== undefined){
        await set(location, value ?? dfValue)
    }
    configs[location] = value ?? dfValue;
    return value ?? dfValue
}

async function loadConfigs(){
    $("#moveApiDown").prop('checked', await get("moveApiDown", false));
    $("#handleMath").prop('checked', await get("handleMath", false));
    $("#collapsibleCharacterSheet").prop('checked', await get("collapsibleCharacterSheet", false));
    $("#spellSideBySide").prop('checked', await get("spellSideBySide", false));
    $("#collapsibleCharacterSheetStyle").prop('checked', await get("collapsibleCharacterSheetStyle", false));
    $("#collapsibleCharacterSheetRename").prop('checked', await get("collapsibleCharacterSheetRename", false));
    $("#collapsibleCharacterSheetString").val(await get("collapsibleCharacterSheetString", "[H]"));
    $("#collapsibleCharacterSheetColor").val(await get("collapsibleCharacterSheetColor", "#4FC3F7"));
    $("#collapsibleCharacterSheetColorClosed").val(await get("collapsibleCharacterSheetColorClosed", "#0288D1"));

    $("#styleOption1").prop('checked', await get("styleOption1", false));
    $("#styleOption1String").val(await get("styleOption1String", "[A]"));
    $("#styleOption1Color").val(await get("styleOption1Color", "red"));

    $("#styleOption2").prop('checked', await get("styleOption2", false));
    $("#styleOption2String").val(await get("styleOption2String", "[BA]"));
    $("#styleOption2Color").val(await get("styleOption2Color", "blue"));

    $("#styleOption3").prop('checked', await get("styleOption3", false));
    $("#styleOption3String").val(await get("styleOption3String", "[R]"));
    $("#styleOption3Color").val(await get("styleOption3Color", "yellow"));
}


async function loadColors(){

    let root = document.documentElement;
    if(root.classList.contains("colorsLoaded")) return

    let collapsibleCharacterSheetColor = await get("collapsibleCharacterSheetColor");
    let collapsibleCharacterSheetColorClosed = await get("collapsibleCharacterSheetColorClosed");
    let styleOption1Color = await get("styleOption1Color");
    let styleOption2Color = await get("styleOption2Color");
    let styleOption3Color = await get("styleOption3Color");




    root.classList.add("colorsLoaded");
    root.style.setProperty('--custom-toggle-open', collapsibleCharacterSheetColor);
    root.style.setProperty('--custom-toggle-close',collapsibleCharacterSheetColorClosed);
    root.style.setProperty('--custom-style-1',styleOption1Color);
    root.style.setProperty('--custom-style-2',styleOption2Color);
    root.style.setProperty('--custom-style-3',styleOption3Color);
}

let MENUHTML = `<div data-v-9f30d89c="" class="panel panel-default"><div data-v-9f30d89c="" class="panel-heading collapsed" data-toggle="collapse" data-parent="#settings-accordion" href="#collapseExpandedOptions" aria-expanded="false"><h4 data-v-9f30d89c="" class="panel-title"><a data-v-9f30d89c="" class="accordion-toggle collapsed" data-toggle="collapse" data-parent="#settings-accordion" href="#collapseExpandedOptions" aria-expanded="false">Expanded Options</a></h4></div><div data-v-9f30d89c="" id="collapseExpandedOptions" class="panel-collapse collapse" aria-expanded="false" style="height: 0px;"><div data-v-9f30d89c="" class="panel-body">`
    +
`<i>* Close & re-open sheet for changes to take effect</i>
<i>** Color changes require page refresh to take effect.</i>`
    +
`<div>
  <input type="checkbox" id="moveApiDown" name="moveApiDown" value="true">
  <label for="moveApiDown" style="display: inline;padding-left: 5px;">Move API bar down</label><br>
</div>`
    +
`<div>
  <input type="checkbox" id="handleMath" name="handleMath" value="true">
  <label for="handleMath" style="display: inline;padding-left: 5px;">*Allow math inside input boxes</label><br>
</div>`
    +
`<div>
  <input type="checkbox" id="spellSideBySide" name="spellSideBySide" value="true">
  <label for="spellSideBySide" style="display: inline;padding-left: 5px;">*Spell sheet side by side</label><br>
</div>`
    +
`<div style="border-style: ridge;padding:5px;border-radius: 5px;">
  <div>
    <input type="checkbox" id="collapsibleCharacterSheet" name="collapsibleCharacterSheet" value="true">
    <label for="collapsibleCharacterSheet" style="display: inline;padding-left: 5px;">*Enable collapsible items and features</label><br>
  </div>
  <div>
    <input type="checkbox" id="collapsibleCharacterSheetStyle" name="collapsibleCharacterSheetStyle" value="true">
    <label for="collapsibleCharacterSheetStyle" style="display: inline;padding-left: 5px;">*Style the labeled items</label><br>
  </div>
  <div>
    <input type="checkbox" id="collapsibleCharacterSheetRename" name="collapsibleCharacterSheetRename" value="true">
    <label for="collapsibleCharacterSheetRename" style="display: inline;padding-left: 5px;">Allow Renaming/Editing Items</label><br>
  </div>
  <div>
    <input placeholder="Select the string that identifies an item as a header" type="text" id="collapsibleCharacterSheetString" name="collapsibleCharacterSheetString" value="">
    <label for="collapsibleCharacterSheetString" style="display: inline;padding-left: 5px;"></label><br>
  </div>
  <div>
    <input placeholder="Open Color - #4FC3F7" type="text" id="collapsibleCharacterSheetColor" name="collapsibleCharacterSheetColor" value="">
    <label for="collapsibleCharacterSheetColor" style="display: inline;padding-left: 5px;"></label><br>
  </div>
  <div>
    <input placeholder="Closed Color - #0288D1" type="text" id="collapsibleCharacterSheetColorClosed" name="collapsibleCharacterSheetColorClosed" value="">
    <label for="collapsibleCharacterSheetColorClosed" style="display: inline;padding-left: 5px;"></label><br>
  </div>
</div>`
    +
`<div style="border-style: ridge;padding:5px;border-radius: 5px;">
  <div>
    <input type="checkbox" id="styleOption1" name="styleOption1" value="true">
    <label for="styleOption1" style="display: inline;padding-left: 5px;">*Enable style 1</label><br>
  </div>
  <div>
    <input placeholder="Style option 1 String" type="text" id="styleOption1String" name="styleOption1String" value="">
    <label for="styleOption1String" style="display: inline;padding-left: 5px;"></label><br>
  </div>
  <div>
    <input placeholder="Style option 1 Color - red" type="text" id="styleOption1Color" name="styleOption1Color" value="">
    <label for="styleOption1Color" style="display: inline;padding-left: 5px;"></label><br>
  </div>
</div>`
    +
    `<div style="border-style: ridge;padding:5px;border-radius: 5px;">
  <div>
    <input type="checkbox" id="styleOption2" name="styleOption2" value="true">
    <label for="styleOption2" style="display: inline;padding-left: 5px;">*Enable style 2</label><br>
  </div>
  <div>
    <input placeholder="Style option 2 String" type="text" id="styleOption2String" name="styleOption2String" value="">
    <label for="styleOption2String" style="display: inline;padding-left: 5px;"></label><br>
  </div>
  <div>
    <input placeholder="Style option 2 Color - blue" type="text" id="styleOption2Color" name="styleOption2Color" value="">
    <label for="styleOption2Color" style="display: inline;padding-left: 5px;"></label><br>
  </div>
</div>`
+
    `<div style="border-style: ridge;padding:5px;border-radius: 5px;">
  <div>
    <input type="checkbox" id="styleOption3" name="styleOption3" value="true">
    <label for="styleOption3" style="display: inline;padding-left: 5px;">*Enable style 3</label><br>
  </div>
  <div>
    <input placeholder="Style option 3 String" type="text" id="styleOption3String" name="styleOption3String" value="">
    <label for="styleOption3String" style="display: inline;padding-left: 5px;"></label><br>
  </div>
  <div>
    <input placeholder="Style option 3 Color - yellow" type="text" id="styleOption3Color" name="styleOption3Color" value="">
    <label for="styleOption3Color" style="display: inline;padding-left: 5px;"></label><br>
  </div>
</div>`
    +
`</div></div></div>`

GM_addStyle ( `
    #secondary-toolbar.move2bottom {
        bottom: 20px !important;
        top: unset !important;
    }

    .container.pc {
        display: flex !important;
    }
    .container.pc .page.spells.spellSideBySide{
        display: block !important;
    }


    .toggleItems .output.btn.ui-draggable {
        display: none !important;
    }

    .textbox .toggleItems.toggleItemsStyle {
        border-radius: 8px;
    }
    .blockRename {
        pointer-events:none;
    }
    .toggleItems.toggleItemsStyle:has(.item .name),
    .toggleItems.toggleItemsStyle .item .name {
        border-radius: 25px;
    }
    .toggleItems.toggleItemsStyle .item .equipped,
    .toggleItems.toggleItemsStyle .item .weight {
      display: none !important;
    }
    .custom_Style1{
       background-color:var(--custom-style-1) !important;
    }
    .custom_Style2{
       background-color:var(--custom-style-2) !important;
    }
    .custom_Style2{
       background-color:var(--custom-style-3) !important;
    }

    .toggleItems.toggleItemsStyle.toggleItemsOpen .title,
    .toggleItems.toggleItemsStyle.toggleItemsOpen .name,
    .toggleItems.toggleItemsStyle.toggleItemsOpen {
        background-color:var(--custom-toggle-open) !important;
        color: #E0E0E0 !important;
    }
    .toggleItems.toggleItemsStyle.toggleItemsClose .title,
    .toggleItems.toggleItemsStyle.toggleItemsClose .name,
    .toggleItems.toggleItemsStyle.toggleItemsClose {
        background-color:var(--custom-toggle-close) !important;
        color: #E0E0E0 !important;
    }
` );