From 66389ab848b8a9ef5d5d523f970a8ddb533804fd Mon Sep 17 00:00:00 2001 From: Anderson Lima Date: Wed, 1 Nov 2023 15:47:09 -0300 Subject: [PATCH] races autocomplete --- chrome/manifest.json | 1 + firefox/manifest.json | 1 + src/common/element-factory.js | 60 ++++++++++++++++++++- src/common/helpers.js | 26 +++++++++ src/common/types.js | 24 +++++++++ src/features/character-sheet.js | 33 +++++++----- src/features/equipments.js | 10 ++-- src/features/races.js | 95 +++++++++++++++++++++++++++++++++ src/index.js | 7 +-- 9 files changed, 233 insertions(+), 24 deletions(-) create mode 100644 src/features/races.js diff --git a/chrome/manifest.json b/chrome/manifest.json index 814c798..56319e6 100644 --- a/chrome/manifest.json +++ b/chrome/manifest.json @@ -31,6 +31,7 @@ "features/powers.js", "features/spells.js", "features/equipments.js", + "features/races.js", "features/character-sheet.js", "features/book.js", "features/enhancement.js", diff --git a/firefox/manifest.json b/firefox/manifest.json index bb3b9be..9c4469f 100644 --- a/firefox/manifest.json +++ b/firefox/manifest.json @@ -31,6 +31,7 @@ "features/powers.js", "features/spells.js", "features/equipments.js", + "features/races.js", "features/character-sheet.js", "features/book.js", "features/enhancement.js", diff --git a/src/common/element-factory.js b/src/common/element-factory.js index 94b08c9..0f450ee 100644 --- a/src/common/element-factory.js +++ b/src/common/element-factory.js @@ -2,7 +2,7 @@ /* common/constants vars */ /* global BOOK_BUTTON_ID,BOOK_DIALOG_ID,BOOK_LIST_ID */ /* common/helpers vars */ -/* global createElement,slugify,setInputValue,addEventObserver,normalize,clearChildren */ +/* global createElement,slugify,setInputValue,addEventObserver,normalize,clearChildren,generateUUID */ /* common/dialog-manager vars */ /* global openDialog */ @@ -359,3 +359,61 @@ function createBookButton({ cssText, bookItems }) { }; return button; } + +/** + * Add a repeatable item to the character sheet. + * + * @param {object} props + * @param {HTMLDocument} props.iframe - The character sheet iframe document. + * @param {string} props.groupName - The item group name. + * @param {object[]} props.attributes - The item attributes values. + * @param {object[]} props.attributes[].name - The input name. + * @param {object[]} props.attributes[].value - The input value. + * @returns {HTMLUListElement} + */ +// eslint-disable-next-line no-unused-vars +function addRepItem({ iframe, groupName, attributes }) { + const fieldset = iframe + .querySelector(`div.repcontrol[data-groupname="${groupName}"]`) + .parentNode.querySelector('fieldset'); + const itemsContainer = iframe.querySelector( + `div.repcontainer[data-groupname="${groupName}"]`, + ); + if (!fieldset) { + console.error(`fieldset for ${groupName} not found`); + return; + } + const repRowId = generateUUID().replace(/_/g, 'Z'); + const newItem = createElement('div', { + classes: 'repitem', + append: [ + createElement('div', { + classes: 'itemcontrol', + append: [ + createElement('button', { + classes: 'btn btn-danger pictos repcontrol_del', + innerHTML: '#', + }), + createElement('a', { + classes: 'btn repcontrol_move', + innerHTML: '≡', + }), + ], + }), + ...Array.from(fieldset.childNodes).map((child) => child.cloneNode(true)), + ], + }); + newItem.setAttribute('data-reprowid', repRowId); + for (const attr of attributes) { + const attrInput = newItem.querySelector( + `input[name="${attr.name}"],textarea[name="${attr.name}"]`, + ); + if (attrInput) { + attrInput.value = attr.value; + setTimeout(() => { + attrInput.dispatchEvent(new CustomEvent('blur')); + }, 300); + } + } + itemsContainer.append(newItem); +} diff --git a/src/common/helpers.js b/src/common/helpers.js index 6e4576b..dffcd61 100644 --- a/src/common/helpers.js +++ b/src/common/helpers.js @@ -158,3 +158,29 @@ function clearChildren({ el }) { el.removeChild(el.lastChild); } } + +/** + * Generate Roll20 UUID. + * + * @returns {string} + */ +// eslint-disable-next-line no-unused-vars +function generateUUID() { + const source = + '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'; + const getFirstPart = (part, seed) => { + if (seed === 0) return `-${part}`; + return getFirstPart( + `${source.charAt(seed % 64)}${part}`, + Math.floor(seed / 64), + ); + }; + const getSecondPart = (part, size) => { + if (part.length === size) return part; + return getSecondPart( + `${part}${source.charAt(Math.floor(64 * Math.random()))}`, + size, + ); + }; + return `${getFirstPart('', new Date().getTime())}${getSecondPart('', 12)}`; +} diff --git a/src/common/types.js b/src/common/types.js index a0c27a1..a0145c9 100644 --- a/src/common/types.js +++ b/src/common/types.js @@ -39,12 +39,20 @@ * @property {string} description - Eg.: Você se torna treinado em duas perícias a sua escolha... */ +/** + * @typedef Ability + * @type {object} + * @property {string} name - Eg.: Conhecimento das Rochas + * @property {string} description - Eg.: Você recebe visão no escuro e +2 em testes de Percepção e Sobrevivência realizados no subterrâneo. + */ + /** * @typedef T20Data * @type {object} * @property {SpellData} spells * @property {PowerData} abilitiesAndPowers * @property {EquipmentData[]} equipments + * @property {Race[]} races */ /** @@ -103,3 +111,19 @@ * @property {string} name - Eg.: Armas * @property {Weapon[]|Armor[]|Item[]} items */ + +/** + * @typedef Attribute + * @type {object} + * @property {string} attr - Eg.: con + * @property {number} mod - Eg.: 2 + */ + +/** + * @typedef Race + * @type {object} + * @property {string} name - Eg.: Anão + * @property {number} displacement - Eg.: 6 + * @property {Attribute[]} attributes + * @property {Ability[]} abilities + */ diff --git a/src/features/character-sheet.js b/src/features/character-sheet.js index 318df63..a245379 100644 --- a/src/features/character-sheet.js +++ b/src/features/character-sheet.js @@ -11,6 +11,8 @@ /* global calcCD,loadSpellsEnhancement */ /* features/equipments vars */ /* global loadEquipmentEnhancement */ +/* features/races vars */ +/* global loadRacesEnhancement */ /** * Returns the data configuration of the character saved in the local storage. @@ -128,20 +130,13 @@ function loadSheetExtraCSS({ iframe }) { * Load the sheet improvements. * * @param {object} props - * @param {SpellData} props.spells - The spells data. - * @param {PowerData} props.powers - The powers data. - * @param {EquipmentData} props.equipments - The equipments data. + * @param {T20Data} props.db - The Tormenta20 data. * @param {string} props.characterId - The character ID in the Roll20 game. */ // eslint-disable-next-line no-unused-vars -function loadSheetEnhancement({ - spells, - abilitiesAndPowers, - equipments, - characterId, -}) { +function loadSheetEnhancement({ db: data, characterId }) { // Tormenta20 data - const data = { spells, abilitiesAndPowers, equipments }; + // const data = { spells, abilitiesAndPowers, equipments, races }; // Load the functionalities const iframe = document.querySelector(`iframe[name="iframe_${characterId}"]`); if (!iframe) { @@ -165,14 +160,28 @@ function loadSheetEnhancement({ }); const equipmentsContainer = pathQuerySelector({ root: iframe.contentDocument, - path: ['div.sheet-right-container', 'div.sheet-equipment-container'], + path: [ + 'div.sheet-right-container', + 'div.sheet-equipment-container', + 'div[data-groupname="repeating_equipment"]', + ], }); - if (spellsContainer && powersContainer && equipmentsContainer) { + const headerContainer = pathQuerySelector({ + root: iframe.contentDocument, + path: ['div.sheet-left-container', 'div.sheet-header-info'], + }); + if ( + spellsContainer && + powersContainer && + equipmentsContainer && + headerContainer + ) { init({ iframe: iframe.contentDocument, characterId }); calcCD({ iframe: iframe.contentDocument }); loadSpellsEnhancement({ iframe: iframe.contentDocument, data }); loadPowersEnhancement({ iframe: iframe.contentDocument, data }); loadEquipmentEnhancement({ iframe: iframe.contentDocument, data }); + loadRacesEnhancement({ iframe: iframe.contentDocument, data }); // Observers const spellsObserver = new MutationObserver(() => { loadSpellsEnhancement({ iframe: iframe.contentDocument, data }); diff --git a/src/features/equipments.js b/src/features/equipments.js index 75d602a..9044ae4 100644 --- a/src/features/equipments.js +++ b/src/features/equipments.js @@ -11,10 +11,10 @@ * @param {Equipment[]} props.equipments - All available equipments. */ function loadEquipmentAutoComplete({ equipmentsContainer, equipments }) { - if (!equipmentsContainer.querySelector('#list-equipment')) { + if (!equipmentsContainer.querySelector('#equipment-list')) { equipmentsContainer.append( createElement('datalist', { - id: 'list-equipment', + id: 'equipment-list', append: equipments.map((equipment) => createElement('option', { value: equipment.name }), ), @@ -22,9 +22,9 @@ function loadEquipmentAutoComplete({ equipmentsContainer, equipments }) { ); } for (const input of equipmentsContainer.querySelectorAll( - 'input[name="attr_equipname"]:not([list="list-equipment"])', + 'input[name="attr_equipname"]:not([list="equipment-list"])', )) { - input.setAttribute('list', 'list-equipment'); + input.setAttribute('list', 'equipment-list'); input.autocomplete = 'off'; const updateSpacesValue = () => { const equipment = equipments.find( @@ -55,7 +55,7 @@ function loadEquipmentAutoComplete({ equipmentsContainer, equipments }) { } /** - * Add the equipment autocomplete. + * Load the equipment related enhancements. * * @param {object} props * @param {HTMLDocument} props.iframe - The character sheet iframe document. diff --git a/src/features/races.js b/src/features/races.js new file mode 100644 index 0000000..60f4e83 --- /dev/null +++ b/src/features/races.js @@ -0,0 +1,95 @@ +'use strict'; + +/* common/helpers vars */ +/* global pathQuerySelector,createElement */ + +/** + * Add the race autocomplete. + * + * @param {object} props + * @param {HTMLDocument} props.iframe - The character sheet iframe document. + * @param {Race[]} props.races - All available races. + */ +function loadRaceAutoComplete({ iframe, races }) { + const headerContainer = pathQuerySelector({ + root: iframe, + path: ['div.sheet-left-container', 'div.sheet-header-info'], + }); + // const abilitiesAndPowersContainer = pathQuerySelector({ + // root: iframe, + // path: ['div.sheet-left-container', 'div.sheet-powers-and-abilities'], + // }); + if (!headerContainer.querySelector('#race-list')) { + headerContainer.append( + createElement('datalist', { + id: 'race-list', + append: races.map((race) => + createElement('option', { value: race.name }), + ), + }), + ); + } + const input = headerContainer.querySelector('input[name="attr_trace"]'); + input.setAttribute('list', 'race-list'); + input.autocomplete = 'off'; + + // const updateAbilities = () => { + // const race = races.find((race) => race.name === input.value); + // if (race) { + // const toRemove = races + // .filter((r) => r.name !== race.name) + // .map((r) => r.abilities) + // .reduce( + // (acc, abilities) => [...acc, ...abilities.map((a) => a.name)], + // [], + // ); + // const allAbilitiesInputs = abilitiesAndPowersContainer.querySelectorAll( + // 'input[name="attr_nameability"],input[name="attr_namepower"]', + // ); + // const currentAbilities = Array.from(allAbilitiesInputs).map( + // (abilityInput) => abilityInput.value.trim(), + // ); + // // add the race abilities + // for (const ability of race.abilities) { + // if (!currentAbilities.includes(ability.name)) { + // addRepItem({ + // iframe, + // groupName: 'repeating_abilities', + // attributes: [ + // { name: 'attr_nameability', value: ability.name }, + // { name: 'attr_abilitydescription', value: ability.description }, + // ], + // }); + // } + // } + // // remove the other races abilities + // for (const abilityInput of allAbilitiesInputs) { + // if (toRemove.includes(abilityInput.value.trim())) { + // console.log(`remove ${abilityInput.value}`); + // } + // } + // } + // }; + // addEventObserver({ + // el: input, + // eventName: 'input', + // eventHandler: updateAbilities, + // }); + // addEventObserver({ + // el: input, + // eventName: 'change', + // eventHandler: updateAbilities, + // }); +} + +/** + * Load the race related enhancements. + * + * @param {object} props + * @param {HTMLDocument} props.iframe - The character sheet iframe document. + * @param {T20Data} props.data - The Tormenta20 data. + */ +// eslint-disable-next-line no-unused-vars +function loadRacesEnhancement({ iframe, data }) { + loadRaceAutoComplete({ iframe, races: data.races }); +} diff --git a/src/index.js b/src/index.js index f24241d..19e5426 100644 --- a/src/index.js +++ b/src/index.js @@ -19,12 +19,7 @@ $(document).ready(() => { const data = e.originalEvent.data; // only add the sheet improvements when a character sheet is opened if (data.type === 'loaded') - loadSheetEnhancement({ - spells: db.spells, - abilitiesAndPowers: db.abilities_and_powers, - equipments: db.equipments, - characterId: data.characterId, - }); + loadSheetEnhancement({ db, characterId: data.characterId }); }); }); });