From f30435c945b3db6b60244b7b4f78c51e43012813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20B=C3=B6ttner?= Date: Tue, 6 Jul 2021 23:52:22 +0200 Subject: [PATCH 01/13] Refactoring --- .gitattributes | 2 +- compendium-browser.js | 1524 +---------------- item-packs.json => data/item-packs.json | 18 +- spell-classes.json => data/spell-classes.json | 962 +++++------ sub-classes.json => data/sub-classes.json | 28 +- hooks/mainHook.js | 72 + lang/de.json | 69 + module.json | 115 +- scripts/compendium-browser.js | 620 +++++++ scripts/modules/entities.mjs | 502 ++++++ scripts/modules/exporter.mjs | 46 + scripts/modules/filter.mjs | 294 ++++ scripts/modules/settings.mjs | 100 ++ .../compendium-browser.css | 164 +- .../compendium-browser.less | 568 +++--- template/entity-browser-list.html | 60 + template/feat-browser-list.html | 17 - template/feat-browser.html | 6 +- template/filter-container.html | 31 +- template/item-browser-list.html | 16 - template/item-browser.html | 6 +- template/loading.html | 12 +- template/npc-browser-list.html | 21 - template/npc-browser.html | 6 +- template/settings.html | 27 +- template/spell-browser-list.html | 25 - template/spell-browser.html | 6 +- template/template.html | 12 +- 28 files changed, 2825 insertions(+), 2504 deletions(-) rename item-packs.json => data/item-packs.json (99%) rename spell-classes.json => data/spell-classes.json (97%) rename sub-classes.json => data/sub-classes.json (98%) create mode 100644 hooks/mainHook.js create mode 100644 lang/de.json create mode 100644 scripts/compendium-browser.js create mode 100644 scripts/modules/entities.mjs create mode 100644 scripts/modules/exporter.mjs create mode 100644 scripts/modules/filter.mjs create mode 100644 scripts/modules/settings.mjs rename compendium-browser.css => styles/compendium-browser.css (59%) rename compendium-browser.less => styles/compendium-browser.less (95%) create mode 100644 template/entity-browser-list.html delete mode 100644 template/feat-browser-list.html delete mode 100644 template/item-browser-list.html delete mode 100644 template/npc-browser-list.html delete mode 100644 template/spell-browser-list.html diff --git a/.gitattributes b/.gitattributes index abcfe61..5c37172 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,5 +3,5 @@ FUNDING.yml export-ignore .gitattributes export-ignore README.md export-ignore preview.jpg export-ignore -compendium-browser.less export-ignore +styles/compendium-browser.less export-ignore Patchnotes.md export-ignore diff --git a/compendium-browser.js b/compendium-browser.js index 2ec0486..dbd6438 100644 --- a/compendium-browser.js +++ b/compendium-browser.js @@ -1,1522 +1,6 @@ -/* eslint-disable valid-jsdoc */ -/* eslint-disable complexity */ -/** - * @author Felix Müller aka syl3r86 - * @version 0.2.0 - */ -/** @author Jeffrey Pugh aka @spetzel2020 - * @version 0.4 - */ -/* -4-Feb-2020 0.4.0 Switch to not pre-loading the indexes, and instead do that at browsing time, to reduce server load and memory usage - Refactor some of the eslint warnings -5-Feb-2021 Don't do memory allocation - just browse compendia in real-time - After this, next step would be incremental (lazy) loading -7-Feb-2021 0.4.1 Move load back to "ready" hook, but limit number loaded -8-Feb-2021 0.4.1 Bug fix: initialize() was setting this.spells, not this.items so CB was using twice the memory (once loaded incorrectly into this.spells - and once loaded on first getData() into this.items) - 0.4.1b SpellBrowser -> CompendiumBrowser -9-Feb-2021 0.4.1b Call loadAndFilterItems instead of loadItems; filter as we go, limited by numToPreload - 0.4.1c Needed to pass specific spellFilters, itemFilters etc. - 0.4.1d: Fixed img observer on replaced spellData -11-Feb-2021 0.4.1e: Don't save the filter data (which is most of the memory) and remove the preload limit; instead just save the minimal amount of data - 0.4.1g: Generalize the spell list reload and confirm spells still working - 0.4.1h: Add the partials for npc, feat, item and the backing code -12-Feb-2021 0.4.1j: Correct compactItem for feats and items required display items - Rename itemType -> browserTab to differentiate candidate item's type from the tab it appears on (spell, feat/class, item, NPC) - Fixed: Was calling the wrong sort for feat and NPC - 0.4.1k: Don't call loadItems() during initalize; getData() just displays static elements - 0.4.1l: Display progress indicator for loading - for now just a static one -15-Feb-2021 0.4.2: Fix NPCs to use loadAndFilterNpcs - 0.4.2b: Add Loading... message for NPCs - 0.4.2c: Correct Loading... message on initial tab, but not on tab switch - 0.4.2d: Display the type of item being loaded -16-Dec-2021 0.4.2f: Change preload to maxLoaded and display a message to filter if you want more -10-Mar-2021 0.4.3: activateItemListListeners(): Remove spurious li.parents (wasn't being used anyway) -11-Mar-2021 0.4.3 Fixed: Reset Filters doesn't clear the on-screen filter fields (because it is not completely re-rendering like it used to) Issue #4 - Hack solution is to re-render whole dialog which unfortunately loses filter settings on other tabs as well - 0.4.3b: Clear all filters to match displayed -15-Mar-2021 0.4.5: Fix: Spells from non-system compendium show up in items tab. Issue#10 - loadAndFilterItems(): Changed tests to switch + more explicit tests - 0.4.5b Show compendium source in results issue#11 - Try showing compendium in the image mouseover -12-Jun-2021 0.5.0 Test for Foundry 0.8.x in which creature type is now data.details.type.value -*/ +import { moduleHooks } from './hooks/mainHook.js'; +//import { Settings } from './scripts/modules/settings.mjs'; -const CMPBrowser = { - MODULE_NAME : "compendium-browser", - MODULE_VERSION : "0.4.5", - MAXLOAD : 500, //Default for the maximum number to load before displaying a message that you need to filter to see more -} -class CompendiumBrowser extends Application { - - static get defaultOptions() { - const options = super.defaultOptions; - mergeObject(options, { - title: "CMPBrowser.compendiumBrowser", - tabs: [{navSelector: ".tabs", contentSelector: ".content", initial: "spell"}], - classes: options.classes.concat('compendium-browser'), - template: "modules/compendium-browser/template/template.html", - width: 800, - height: 700, - resizable: true, - minimizable: true - }); - return options; - } - - async initialize() { - // load settings - if (this.settings === undefined) { - this.initSettings(); - } - - await loadTemplates([ - "modules/compendium-browser/template/spell-browser.html", - "modules/compendium-browser/template/spell-browser-list.html", - "modules/compendium-browser/template/npc-browser.html", - "modules/compendium-browser/template/npc-browser-list.html", - "modules/compendium-browser/template/feat-browser.html", - "modules/compendium-browser/template/feat-browser-list.html", - "modules/compendium-browser/template/item-browser.html", - "modules/compendium-browser/template/item-browser-list.html", - "modules/compendium-browser/template/filter-container.html", - "modules/compendium-browser/template/settings.html", - "modules/compendium-browser/template/loading.html" - ]); - - this.hookCompendiumList(); - - //Reset the filters used in the dialog - this.spellFilters = { - registeredFilterCategorys: {}, - activeFilters: {} - }; - this.npcFilters = { - registeredFilterCategorys: {}, - activeFilters: {} - }; - this.featFilters = { - registeredFilterCategorys: {}, - activeFilters: {} - }; - this.itemFilters = { - registeredFilterCategorys: {}, - activeFilters: {} - }; - } - - - /** override */ - _onChangeTab(event, tabs, active) { - super._onChangeTab(event, tabs, active); - const html = this.element; - this.replaceList(html, active, {reload : false}) - } - - - /** override */ - async getData() { - - //0.4.1 Filter as we load to support new way of filtering - //Previously loaded all data and filtered in place; now loads minimal (preload) amount, filtered as we go - //First time (when you press Compendium Browser button) is called with filters unset - - //0.4.1k: Don't do any item/npc loading until tab is visible - let data = { - items : [], - npcs: [], - spellFilters : this.spellFilters, - showSpellBrowser : (game.user.isGM || this.settings.allowSpellBrowser), - featFilters : this.featFilters, - showFeatBrowser : (game.user.isGM || this.settings.allowFeatBrowser), - itemFilters : this.itemFilters, - showItemBrowser : (game.user.isGM || this.settings.allowItemBrowser), - npcFilters : this.npcFilters, - showNpcBrowser : (game.user.isGM || this.settings.allowNpcBrowser), - settings : this.settings, - isGM : game.user.isGM - }; - - - return data; - } - - activateItemListListeners(html) { - // show entity sheet - html.find('.item-edit').click(ev => { - let itemId = $(ev.currentTarget).parents("li").attr("data-entry-id"); - let compendium = $(ev.currentTarget).parents("li").attr("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === compendium); - pack.getEntity(itemId).then(entity => { - entity.sheet.render(true); - }); - }); - - // make draggable - //0.4.1: Avoid the game.packs lookup - html.find('.draggable').each((i, li) => { - li.setAttribute("draggable", true); - li.addEventListener('dragstart', event => { - let packName = li.getAttribute("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === packName); - if (!pack) { - event.preventDefault(); - return false; - } - event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.entity, - pack: pack.collection, - id: li.getAttribute("data-entry-id") - })); - }, false); - }); - } - - /** override */ - activateListeners(html) { - super.activateListeners(html); - - this.observer = new IntersectionObserver((entries, observer) => { - for (let e of entries) { - if (!e.isIntersecting) continue; - const img = e.target; - // Avatar image - //const img = li.querySelector("img"); - if (img && img.dataset.src) { - img.src = img.dataset.src; - delete img.dataset.src; - } - - // No longer observe the target - observer.unobserve(e.target); - } - }); - - this.activateItemListListeners(html); - - // toggle visibility of filter containers - html.find('.filtercontainer h3, .multiselect label').click(async ev => { - await $(ev.target.nextElementSibling).toggle(100); - - }); - html.find('.multiselect label').trigger('click'); - - // sort spell list - html.find('.spell-browser select[name=sortorder]').on('change', ev => { - let spellList = html.find('.spell-browser li'); - let byName = (ev.target.value == 'true'); - let sortedList = this.sortSpells(spellList, byName); - let ol = $(html.find('.spell-browser ul')); - ol[0].innerHTML = []; - for (let element of sortedList) { - ol[0].append(element); - } - }); - this.triggerSort(html, "spell"); - - // sort feat list in place - html.find('.feat-browser select[name=sortorder]').on('change', ev => { - let featList = html.find('.feat-browser li'); - let byName = (ev.target.value == 'true'); - let sortedList = this.sortFeats(featList, byName); - let ol = $(html.find('.feat-browser ul')); - ol[0].innerHTML = []; - for (let element of sortedList) { - ol[0].append(element); - } - }); - this.triggerSort(html, "feat"); - - // sort item list in place - html.find('.item-browser select[name=sortorder]').on('change', ev => { - let itemList = html.find('.item-browser li'); - let byName = (ev.target.value == 'true'); - let sortedList = this.sortItems(itemList, byName); - let ol = $(html.find('.item-browser ul')); - ol[0].innerHTML = []; - for (let element of sortedList) { - ol[0].append(element); - } - }); - this.triggerSort(html, "item"); - - // sort npc list in place - html.find('.npc-browser select[name=sortorder]').on('change', ev => { - let npcList = html.find('.npc-browser li'); - let orderBy = ev.target.value; - let sortedList = this.sortNpcs(npcList, orderBy); - let ol = $(html.find('.npc-browser ul')); - ol[0].innerHTML = []; - for (let element of sortedList) { - ol[0].append(element); - } - }); - this.triggerSort(html, "npc"); - - // reset filters and re-render - //0.4.3: Reset ALL filters because when we do a re-render it affects all tabs - html.find('#reset-spell-filter').click(ev => { - this.resetFilters(); - //v0.4.3: Re-render so that we display the filters correctly - this.refreshList = "spell"; - this.render(); - }); - - html.find('#reset-feat-filter').click(ev => { - this.resetFilters(); - //v0.4.3: Re-render so that we display the filters correctly - this.refreshList = "feat"; - this.render(); - }); - - html.find('#reset-item-filter').click(ev => { - this.resetFilters(); - //v0.4.3: Re-render so that we display the filters correctly - this.refreshList = "item"; - this.render(); - - }); - - html.find('#reset-npc-filter').click(ev => { - this.resetFilters(); - //v0.4.3: Re-render so that we display the filters correctly - this.refreshList = "npc"; - this.render(); - }); - - // settings - html.find('.settings input').on('change', ev => { - let setting = ev.target.dataset.setting; - let value = ev.target.checked; - if (setting === 'spell-compendium-setting') { - let key = ev.target.dataset.key; - this.settings.loadedSpellCompendium[key].load = value; - this.render(); - ui.notifications.info("Settings Saved. Item Compendiums are being reloaded."); - } else if (setting === 'npc-compendium-setting') { - let key = ev.target.dataset.key; - this.settings.loadedNpcCompendium[key].load = value; - this.render(); - ui.notifications.info("Settings Saved. NPC Compendiums are being reloaded."); - } - if (setting === 'allow-spell-browser') { - this.settings.allowSpellBrowser = value; - } - if (setting === 'allow-feat-browser') { - this.settings.allowFeatBrowser = value; - } - if (setting === 'allow-item-browser') { - this.settings.allowItemBrowser = value; - } - if (setting === 'allow-npc-browser') { - this.settings.allowNpcBrowser = value; - } - this.saveSettings(); - }); - - - // activating or deactivating filters - //0.4.1: Now does a re-load and updates just the data side - // text filters - html.find('.filter[data-type=text] input, .filter[data-type=text] select').on('keyup change paste', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const value = ev.target.value; - const browserTab = $(ev.target).parents('.tab').data('tab'); - - const filterTarget = `${browserTab}Filters`; - - if (value === '' || value === undefined) { - delete this[filterTarget].activeFilters[key]; - } else { - this[filterTarget].activeFilters[key] = { - path: path, - type: 'text', - valIsArray: false, - value: ev.target.value - } - } - - this.replaceList(html, browserTab); - }); - - // select filters - html.find('.filter[data-type=select] select, .filter[data-type=bool] select').on('change', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const filterType = $(ev.target).parents('.filter').data('type'); - const browserTab = $(ev.target).parents('.tab').data('tab'); - let valIsArray = $(ev.target).parents('.filter').data('valisarray'); - if (valIsArray === 'true') valIsArray = true; - let value = ev.target.value; - if (value === 'false') value = false; - if (value === 'true') value = true; - - const filterTarget = `${browserTab}Filters`; - - if (value === "null") { - delete this[filterTarget].activeFilters[key] - } else { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - value:value - } - } - this.replaceList(html, browserTab); - }); - - // multiselect filters - html.find('.filter[data-type=multiSelect] input').on('change', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const filterType = 'multiSelect'; - const browserTab = $(ev.target).parents('.tab').data('tab'); - let valIsArray = $(ev.target).parents('.filter').data('valisarray'); - if (valIsArray === 'true') valIsArray = true; - let value = $(ev.target).data('value'); - - const filterTarget = `${browserTab}Filters`; - const filter = this[filterTarget].activeFilters[key]; - - if (ev.target.checked === true) { - if (filter === undefined) { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - values: [value] - } - } else { - this[filterTarget].activeFilters[key].values.push(value); - } - } else { - delete this[filterTarget].activeFilters[key].values.splice(this[filterTarget].activeFilters[key].values.indexOf(value),1); - if (this[filterTarget].activeFilters[key].values.length === 0) { - delete this[filterTarget].activeFilters[key]; - } - } - - this.replaceList(html, browserTab); - }); - - - html.find('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').on('change keyup paste', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const filterType = 'numberCompare'; - const browserTab = $(ev.target).parents('.tab').data('tab'); - let valIsArray = false; - - const operator = $(ev.target).parents('.filter').find('select').val(); - const value = $(ev.target).parents('.filter').find('input').val(); - - const filterTarget = `${browserTab}Filters`; - - if (value === '' || operator === 'null') { - delete this[filterTarget].activeFilters[key] - } else { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - operator: operator, - value: value - } - } - - this.replaceList(html, browserTab); - }); - - //Just for the loading image - if (this.observer) { - html.find("img").each((i,img) => this.observer.observe(img)); - } - } - - async checkListsLoaded() { - //Provides extra info not in the standard SRD, like which classes can learn a spell - if (!this.classList) { - this.classList = await fetch('modules/compendium-browser/spell-classes.json').then(result => { - return result.json(); - }).then(obj => { - return this.classList = obj; - }); - } - - if (!this.packList) { - this.packList = await fetch('modules/compendium-browser/item-packs.json').then(result => { - return result.json(); - }).then(obj => { - return this.packList = obj; - }); - } - - if (!this.subClasses) { - this.subClasses = await fetch('modules/compendium-browser/sub-classes.json').then(result => { - return result.json(); - }).then(obj => { - return this.subClasses = obj; - }); - } - } - - async loadAndFilterItems(browserTab="spell",updateLoading=null) { - console.log(`Load and Filter Items | Started loading ${browserTab}s`); - console.time("loadAndFilterItems"); - await this.checkListsLoaded(); - - const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; - - //0.4.1: Load and filter just one of spells, feats, and items (specified by browserTab) - let unfoundSpells = ''; - let numItemsLoaded = 0; - let compactItems = {}; - - //Filter the full list, but only save the core compendium information + displayed info - for (let pack of game.packs) { - if (pack['metadata']['entity'] === "Item" && this.settings.loadedSpellCompendium[pack.collection].load) { -//FIXME: How much could we do with the loaded index rather than all content? -//OR filter the content up front for the decoratedItem.type?? - await pack.getContent().then(content => { - for (let item5e of content) { - let compactItem = null; - const decoratedItem = this.decorateItem(item5e); - if (decoratedItem) { - switch (browserTab) { - case "spell": - if ((decoratedItem.type === "spell") && this.passesFilter(decoratedItem, this.spellFilters.activeFilters)) { - compactItem = { - compendium : pack.collection, - name : decoratedItem.name, - img: decoratedItem.img, - data : { - level : decoratedItem.data?.level, - components : decoratedItem.data?.components - } - } - } - break; - - case "feat": - if (["feat","class"].includes(decoratedItem.type) && this.passesFilter(decoratedItem, this.featFilters.activeFilters)) { - compactItem = { - compendium : pack.collection, - name : decoratedItem.name, - img: decoratedItem.img, - classRequirementString : decoratedItem.classRequirementString - } - } - break; - - case "item": - //0.4.5: Itm type for true items could be many things (weapon, consumable, etc) so we just look for everything except spells, feats, classes - if (!["spell","feat","class"].includes(decoratedItem.type) && this.passesFilter(decoratedItem, this.itemFilters.activeFilters)) { - compactItem = { - compendium : pack.collection, - name : decoratedItem.name, - img: decoratedItem.img, - type : decoratedItem.type - } - } - break; - - default: - break; - } - - if (compactItem) { //Indicates it passed the filters - compactItems[decoratedItem._id] = compactItem; - if (numItemsLoaded++ >= maxLoad) break; - //0.4.2e: Update the UI (e.g. "Loading 142 spells") - if (updateLoading) {updateLoading(numItemsLoaded);} - } - } - }//for item5e of content - }); - }//end if pack entity === Item - if (numItemsLoaded >= maxLoad) break; - }//for packs - -/* - if (unfoundSpells !== '') { - console.log(`Load and Fliter Items | List of Spells that don't have a class associated to them:`); - console.log(unfoundSpells); - } -*/ - this.itemsLoaded = true; - console.timeEnd("loadAndFilterItems"); - console.log(`Load and Filter Items | Finished loading ${Object.keys(compactItems).length} ${browserTab}s`); - return compactItems; - } - - async loadItems(numToPreload=CMPBrowser.PRELOAD) { - console.log('Item Browser | Started loading items'); - console.time("loadItems"); - await this.checkListsLoaded(); - - this.itemsLoaded = false; - - - let unfoundSpells = ''; - let numSpellsLoaded = 0; - let numFeatsLoaded = 0; - let numItemsLoaded = 0; - let items = { - spells: {}, - feats: {}, - items: {} - }; - - - for (let pack of game.packs) { - if (pack['metadata']['entity'] === "Item" && this.settings.loadedSpellCompendium[pack.collection].load) { - await pack.getContent().then(content => { - for (let item5e of content) { - let item = item5e.data; - if (item.type === 'spell') { - //0.4.1 Only preload a limited number and fill more in as needed - if (numSpellsLoaded++ > numToPreload) continue; - - item.compendium = pack.collection; - - // determining classes that can use the spell - let cleanSpellName = item.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); - //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); - if (this.classList[cleanSpellName]) { - let classes = this.classList[cleanSpellName]; - item.data.classes = classes.split(','); - } else { - unfoundSpells += cleanSpellName + ','; - } - - // getting damage types - item.damageTypes = []; - if (item.data.damage && item.data.damage.parts.length > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (item.damageTypes.indexOf(type) === -1) { - item.damageTypes.push(type); - } - } - } - items.spells[(item._id)] = item; - } else if (item.type === 'feat' || item.type === 'class') { - //0.4.1 Only preload a limited number and fill more in as needed - if (numFeatsLoaded++ > numToPreload) continue; - - item.compendium = pack.collection; - // getting damage types - item.damageTypes = []; - if (item.data.damage && item.data.damage.parts.length > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (item.damageTypes.indexOf(type) === -1) { - item.damageTypes.push(type); - } - } - } - - // getting class - let reqString = item.data.requirements?.replace(/[0-9]/g, '').trim(); - let matchedClass = []; - for (let c in this.subClasses) { - if (reqString && reqString.toLowerCase().indexOf(c) !== -1) { - matchedClass.push(c); - } else { - for (let subClass of this.subClasses[c]) { - if (reqString && reqString.indexOf(subClass) !== -1) { - matchedClass.push(c); - break; - } - } - } - } - item.classRequirement = matchedClass; - item.classRequirementString = matchedClass.join(', '); - - // getting uses/ressources status - item.usesRessources = item5e.hasLimitedUses; - - item.hasSave = item5e.hasSave; - - items.feats[(item._id)] = item; - - } else { - //0.4.1 Only preload a limited number and fill more in as needed - if (numItemsLoaded++ > numToPreload) continue; - - item.compendium = pack.collection; - // getting damage types - item.damageTypes = []; - if (item.data.damage && item.data.damage.parts.size > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (item.damageTypes.indexOf(type) === -1) { - item.damageTypes.push(type); - } - } - } - - // getting pack - let matchedPacks = []; - for (let pack of Object.keys(this.packList)) { - for (let packItem of this.packList[pack]) { - if (item.name.toLowerCase() === packItem.toLowerCase()) { - matchedPacks.push(pack); - break; - } - } - } - item.matchedPacks = matchedPacks; - item.matchedPacksString = matchedPacks.join(', '); - - // getting uses/ressources status - item.usesRessources = item5e.hasLimitedUses - - items.items[(item._id)] = item; - } - - }//for item5e of content - }); - } - if ((numSpellsLoaded >= numToPreload) && (numFeatsLoaded >= numToPreload) && (numItemsLoaded >= numToPreload)) break; - }//for packs - if (unfoundSpells !== '') { - console.log(`Item Browser | List of Spells that don't have a class associated to them:`); - console.log(unfoundSpells); - } - this.itemsLoaded = true; - console.timeEnd("loadItems"); - console.log(`Item Browser | Finished loading items: ${Object.keys(items.spells).length} spells, ${Object.keys(items.feats).length} features, ${Object.keys(items.items).length} items `); - return items; - } - - async loadAndFilterNpcs(updateLoading=null) { - console.log('NPC Browser | Started loading NPCs'); - console.time("loadAndFilterNpcs"); - let npcs = {}; - - const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; - - let numNpcsLoaded = 0; - this.npcsLoaded = false; - for (let pack of game.packs) { - if (pack['metadata']['entity'] == "Actor" && this.settings.loadedNpcCompendium[pack.collection].load) { - await pack.getContent().then(async content => { - - for (let npc of content) { - let compactNpc = null; - const decoratedNpc = this.decorateNpc(npc); - if (decoratedNpc && this.passesFilter(decoratedNpc, this.npcFilters.activeFilters)) { - //0.4.2: Don't store all the details - just the display elements - compactNpc = { - compendium : pack.collection, - name : decoratedNpc.name, - img: decoratedNpc.img, - displayCR : decoratedNpc.displayCR, - displaySize : decoratedNpc.displaySize, - displayType: decoratedNpc.data?.details?.type, - orderCR : decoratedNpc.data.details.cr, - orderSize : decoratedNpc.filterSize - } - if (compactNpc) { - npcs[decoratedNpc._id] = compactNpc; - //0.4.2 Don't load more than maxLoad; display a message to filter - if (numNpcsLoaded++ > maxLoad) break; - //0.4.2e: Update the UI (e.g. "Loading 142 NPCs") - if (updateLoading) {updateLoading(numNpcsLoaded);} - } - } - } - }); - } - //0.4.1 Only preload a limited number and fill more in as needed - if (numNpcsLoaded >= maxLoad) break; - } - - this.npcsLoaded = true; - console.timeEnd("loadAndFilterNpcs"); - console.log(`NPC Browser | Finished loading NPCs: ${Object.keys(npcs).length} NPCs`); - return npcs; - } - - - - hookCompendiumList() { - Hooks.on('renderCompendiumDirectory', (app, html, data) => { - this.hookCompendiumList(); - }); - - let html = $('#compendium'); - if (this.settings === undefined) { - this.initSettings(); - } - if (game.user.isGM || this.settings.allowSpellBrowser || this.settings.allowNpcBrowser) { - const cbButton = $(``); - html.find('.compendium-browser-btn').remove(); - - // adding to directory-list since the footer doesn't exist if the user is not gm - html.find('.directory-footer').append(cbButton); - - // Handle button clicks - cbButton.click(ev => { - ev.preventDefault(); - //0.4.1: Reset filters when you click button - this.resetFilters(); - //0.4.3: Reset everything (including data) when you press the button - calls afterRender() hook - - if (game.user.isGM || this.settings.allowSpellBrowser) { - this.refreshList = "spell"; - } else if (this.settings.allowFeatBrowser) { - this.refreshList = "feat"; - } else if (this.settings.allowItemBrowser) { - this.refreshList = "item"; - } else if (this.settings.allowNPCBrowser) { - this.refreshList = "npc"; - } - this.render(true); - }); - } - } - - - /* Hook to load the first data */ - static afterRender(cb, html) { - //0.4.3: Because a render always resets ALL the displayed filters (on all tabs) to unselected , we have to blank all the lists as well - // (because the current HTML template doesn't set the selected filter values) - if (!cb?.refreshList) {return;} - - cb.replaceList(html, cb.refreshList); - - cb.refreshList = null; - } - - resetFilters() { - this.spellFilters.activeFilters = {}; - this.featFilters.activeFilters = {}; - this.itemFilters.activeFilters = {}; - this.npcFilters.activeFilters = {}; - } - - - - async replaceList(html, browserTab, options = {reload : true}) { - //After rendering the first time or re-rendering trigger the load/reload of visible data - - let elements = null; - //0.4.2 Display a Loading... message while the data is being loaded and filtered - let loadingMessage = null; - if (browserTab === 'spell') { - elements = html.find("ul#CBSpells"); - loadingMessage = html.find("#CBSpellsMessage"); - } else if (browserTab === 'npc') { - elements = html.find("ul#CBNPCs"); - loadingMessage = html.find("#CBNpcsMessage"); - } else if (browserTab === 'feat') { - elements = html.find("ul#CBFeats"); - loadingMessage = html.find("#CBFeatsMessage"); - } else if (browserTab === 'item') { - elements = html.find("ul#CBItems"); - loadingMessage = html.find("#CBItemsMessage"); - } - if (elements?.length) { - //0.4.2b: On a tab-switch, only reload if there isn't any data already - if (options?.reload || !elements[0].children.length) { - - const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; - const updateLoading = async numLoaded => { - if (loadingMessage.length) {this.renderLoading(loadingMessage[0], browserTab, numLoaded, numLoaded>=maxLoad);} - } - updateLoading(0); - - //Uses loadAndFilterItems to read compendia for items which pass the current filters and render on this tab - const newItemsHTML = await this.renderItemData(browserTab, updateLoading); - elements[0].innerHTML = newItemsHTML; - //Re-sort before setting up lazy loading - this.triggerSort(html, browserTab); - - //Lazy load images - if (this.observer) { - $(elements).find("img").each((i,img) => this.observer.observe(img)); - } - - //Reactivate listeners for clicking and dragging - this.activateItemListListeners($(elements)); - } - } - - } - - async renderLoading(messageElement, itemType, numLoaded, maxLoaded=false) { - if (!messageElement) return; - - let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.html", {numLoaded: numLoaded, itemType: itemType, maxLoaded: maxLoaded}); - messageElement.innerHTML = loadingHTML; - } - - async renderItemData(browserTab, updateLoading=null) { - let listItems; - if (browserTab === "npc") { - listItems = await this.loadAndFilterNpcs(updateLoading); - } else { - listItems = await this.loadAndFilterItems(browserTab, updateLoading); - } - const html = await renderTemplate(`modules/compendium-browser/template/${browserTab}-browser-list.html`, {listItems : listItems}) - - return html; - } - - //SORTING - triggerSort(html, browserTab) { - if (browserTab === 'spell') { - html.find('.spell-browser select[name=sortorder]').trigger('change'); - } else if (browserTab === 'feat') { - html.find('.feat-browser select[name=sortorder]').trigger('change'); - } else if (browserTab === 'npc') { - html.find('.npc-browser select[name=sortorder]').trigger('change') - } else if (browserTab === 'item') { - html.find('.item-browser select[name=sortorder]').trigger('change'); - } - } - - - - sortSpells(list, byName) { - if (byName) { - list.sort((a, b) => { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); - } else { - list.sort((a, b) => { - let aVal = $(a).find('input[name=level]').val(); - let bVal = $(b).find('input[name=level]').val(); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); - } - return list; - } - - sortFeats(list, byName) { - if (byName) { - list.sort((a, b) => { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); - } else { - list.sort((a, b) => { - let aVal = $(a).find('input[name=class]').val(); - let bVal = $(b).find('input[name=class]').val(); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); - } - return list; - } - - sortItems(list, byName) { - if (byName) { - list.sort((a, b) => { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); - } else { - list.sort((a, b) => { - let aVal = $(a).find('input[name=type]').val(); - let bVal = $(b).find('input[name=type]').val(); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); - } - return list; - } - - sortNpcs(list, orderBy) { - switch (orderBy) { - case 'name': - list.sort((a, b) => { - let aName = $(a).find('.npc-name a')[0].innerHTML; - let bName = $(b).find('.npc-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); break; - case 'cr': - list.sort((a, b) => { - let aVal = Number($(a).find('input[name="order.cr"]').val()); - let bVal = Number($(b).find('input[name="order.cr"]').val()); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.npc-name a')[0].innerHTML; - let bName = $(b).find('.npc-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); break; - case 'size': - list.sort((a, b) => { - let aVal = $(a).find('input[name="order.size"]').val(); - let bVal = $(b).find('input[name="order.size"]').val(); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.npc-name a')[0].innerHTML; - let bName = $(b).find('.npc-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); break; - } - return list; - } - - decorateItem(item5e) { - if (!item5e) return null; - //Decorate and then filter a compendium entry - returns null or the item - const item = item5e.data; - - // getting damage types (common to all Items, although some won't have any) - item.damageTypes = []; - if (item.data.damage && item.data.damage.parts.length > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (item.damageTypes.indexOf(type) === -1) { - item.damageTypes.push(type); - } - } - } - - if (item.type === 'spell') { - // determining classes that can use the spell - let cleanSpellName = item.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); - //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); - if (this.classList[cleanSpellName]) { - let classes = this.classList[cleanSpellName]; - item.data.classes = classes.split(','); - } else { -//FIXME: unfoundSpells += cleanSpellName + ','; - } - } else if (item.type === 'feat' || item.type === 'class') { - // getting class - let reqString = item.data.requirements?.replace(/[0-9]/g, '').trim(); - let matchedClass = []; - for (let c in this.subClasses) { - if (reqString && reqString.toLowerCase().indexOf(c) !== -1) { - matchedClass.push(c); - } else { - for (let subClass of this.subClasses[c]) { - if (reqString && reqString.indexOf(subClass) !== -1) { - matchedClass.push(c); - break; - } - } - } - } - item.classRequirement = matchedClass; - item.classRequirementString = matchedClass.join(', '); - - // getting uses/ressources status - item.usesRessources = item5e.hasLimitedUses; - item.hasSave = item5e.hasSave; - - } else { - // getting pack - let matchedPacks = []; - for (let pack of Object.keys(this.packList)) { - for (let packItem of this.packList[pack]) { - if (item.name.toLowerCase() === packItem.toLowerCase()) { - matchedPacks.push(pack); - break; - } - } - } - item.matchedPacks = matchedPacks; - item.matchedPacksString = matchedPacks.join(', '); - - // getting uses/ressources status - item.usesRessources = item5e.hasLimitedUses - } - return item; - } - - decorateNpc(npc) { - //console.log('%c '+npc.name, 'background: white; color: red') - const decoratedNpc = npc.data; - - // cr display - let cr = decoratedNpc.data.details.cr; - if (cr == undefined || cr == '') cr = 0; - else cr = Number(cr); - if (cr > 0 && cr < 1) cr = "1/" + (1 / cr); - decoratedNpc.displayCR = cr; - decoratedNpc.displaySize = 'unset'; - decoratedNpc.filterSize = 2; - if (CONFIG.DND5E.actorSizes[decoratedNpc.data.traits.size] !== undefined) { - decoratedNpc.displaySize = CONFIG.DND5E.actorSizes[decoratedNpc.data.traits.size]; - } - switch (decoratedNpc.data.traits.size) { - case 'grg': decoratedNpc.filterSize = 5; break; - case 'huge': decoratedNpc.filterSize = 4; break; - case 'lg': decoratedNpc.filterSize = 3; break; - case 'sm': decoratedNpc.filterSize = 1; break; - case 'tiny': decoratedNpc.filterSize = 0; break; - case 'med': - default: decoratedNpc.filterSize = 2; break; - } - - // getting value for HasSpells and damage types - decoratedNpc.hasSpells = false; - decoratedNpc.damageDealt = []; - for (let item of decoratedNpc.items) { - if (item.type == 'spell') { - decoratedNpc.hasSpells = true; - } - if (item.data.damage && item.data.damage.parts && item.data.damage.parts.length > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (decoratedNpc.damageDealt.indexOf(type) === -1) { - decoratedNpc.damageDealt.push(type); - } - } - } - } - - return decoratedNpc; - } - - filterElements(list, subjects, filters) { - for (let element of list) { - let subject = subjects[element.dataset.entryId]; - if (this.passesFilter(subject, filters) == false) { - $(element).hide(); - } else { - $(element).show(); - } - } - } - - passesFilter(subject, filters) { - for (let filter of Object.values(filters)) { - let prop = getProperty(subject, filter.path); - if (filter.type === 'numberCompare') { - - switch (filter.operator) { - case '=': if (prop != filter.value) { return false; } break; - case '<': if (prop >= filter.value) { return false; } break; - case '>': if (prop <= filter.value) { return false; } break; - } - - continue; - } - if (filter.valIsArray === false) { - if (filter.type === 'text') { - if (prop === undefined) return false; - if (prop.toLowerCase().indexOf(filter.value.toLowerCase()) === -1) { - return false; - } - } else { - if (filter.value !== undefined && prop !== undefined && prop != filter.value && !(filter.value === true && prop)) { - return false; - } - if (filter.values && filter.values.indexOf(prop) === -1) { - return false; - } - } - } else { - if (prop === undefined) return false; - if (typeof prop === 'object') { - if (filter.value) { - if (prop.indexOf(filter.value) === -1) { - return false; - } - } else if (filter.values) { - for (let val of filter.values) { - if (prop.indexOf(val) !== -1) { - continue; - } - return false; - } - } - } else { - for (let val of filter.values) { - if (prop === val) { - continue; - } - } - return false; - } - } - } - - return true; - } - - clearObject(obj) { - let newObj = {}; - for (let key in obj) { - if (obj[key] == true) { - newObj[key] = true; - } - } - return newObj; - } - - initSettings() { - let defaultSettings = { - loadedSpellCompendium: {}, - loadedNpcCompendium: {}, - }; - for (let compendium of game.packs) { - if (compendium['metadata']['entity'] === "Item") { - defaultSettings.loadedSpellCompendium[compendium.collection] = { - load: true, - name: `${compendium['metadata']['label']} (${compendium.collection})` - }; - } - if (compendium['metadata']['entity'] === "Actor") { - defaultSettings.loadedNpcCompendium[compendium.collection] = { - load: true, - name: `${compendium['metadata']['label']} (${compendium.collection})` - }; - } - } - // creating game setting container - game.settings.register(CMPBrowser.MODULE_NAME, "settings", { - name: "Compendium Browser Settings", - hint: "Settings to exclude packs from loading and visibility of the browser", - default: defaultSettings, - type: Object, - scope: 'world', - onChange: settings => { - this.settings = settings; - } - }); - game.settings.register(CMPBrowser.MODULE_NAME, "maxload", { - name: game.i18n.localize("CMPBrowser.SETTING.Maxload.NAME"), - hint: game.i18n.localize("CMPBrowser.SETTING.Maxload.HINT"), - scope: "world", - config: true, - default: CMPBrowser.MAXLOAD, - type: Number, - range: { // If range is specified, the resulting setting will be a range slider - min: 200, - max: 5000, - step: 100 - } - }); - - // load settings from container and apply to default settings (available compendie might have changed) - let settings = game.settings.get(CMPBrowser.MODULE_NAME, 'settings'); - for (let compKey in defaultSettings.loadedSpellCompendium) { - if (settings.loadedSpellCompendium[compKey] !== undefined) { - defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; - } - } - for (let compKey in defaultSettings.loadedNpcCompendium) { - if (settings.loadedNpcCompendium[compKey] !== undefined) { - defaultSettings.loadedNpcCompendium[compKey].load = settings.loadedNpcCompendium[compKey].load; - } - } - defaultSettings.allowSpellBrowser = settings.allowSpellBrowser ? true : false; - defaultSettings.allowFeatBrowser = settings.allowFeatBrowser ? true : false; - defaultSettings.allowItemBrowser = settings.allowItemBrowser ? true : false; - defaultSettings.allowNpcBrowser = settings.allowNpcBrowser ? true : false; - - if (game.user.isGM) { - game.settings.set(CMPBrowser.MODULE_NAME, 'settings', defaultSettings); - console.log("New default settings set"); - console.log(defaultSettings); - } - this.settings = defaultSettings; - } - - saveSettings() { - game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings); - } - - //FILTERS - Added on the Ready hook - //0.4.0 Make this async so filters can be added all at once - async addFilter(entityType, category, label, path, type, possibleValues = null, valIsArray = false) { - let target = `${entityType}Filters`; - let filter = {}; - filter.path = path; - filter.label = label; - filter.type = 'text'; - if (['text', 'bool', 'select', 'multiSelect', 'numberCompare'].indexOf(type) !== -1) { - filter[`is${type}`] = true; - filter.type = type; - } - if (possibleValues !== null) { - filter.possibleValues = possibleValues; - } - filter.valIsArray = valIsArray; - - let catId = category.replace(/\W/g, ''); - if (this[target].registeredFilterCategorys[catId] === undefined) { - this[target].registeredFilterCategorys[catId] = {label: category, filters: []}; - } - this[target].registeredFilterCategorys[catId].filters.push(filter); - - } - - async addSpellFilters() { - // Spellfilters - this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); - this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.lvl"), 'data.level', 'multiSelect', [game.i18n.localize("CMPBrowser.cantip"), 1, 2, 3, 4, 5, 6, 7, 8, 9]); - this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.school"), 'data.school', 'select', CONFIG.DND5E.spellSchools); - this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.castingTime"), 'data.activation.type', 'select', - { - action: game.i18n.localize("DND5E.Action"), - bonus: game.i18n.localize("CMPBrowser.bonusAction"), - reaction: game.i18n.localize("CMPBrowser.reaction"), - minute: game.i18n.localize("DND5E.TimeMinute"), - hour: game.i18n.localize("DND5E.TimeHour"), - day: game.i18n.localize("DND5E.TimeDay") - } - ); - this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.spellType"), 'data.actionType', 'select', CONFIG.DND5E.itemActionTypes); - this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'data.classes', 'select', - { - artificer: game.i18n.localize("CMPBrowser.artificer"), - bard: game.i18n.localize("CMPBrowser.bard"), - cleric: game.i18n.localize("CMPBrowser.cleric"), - druid: game.i18n.localize("CMPBrowser.druid"), - paladin: game.i18n.localize("CMPBrowser.paladin"), - ranger: game.i18n.localize("CMPBrowser.ranger"), - sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), - warlock: game.i18n.localize("CMPBrowser.warlock"), - wizard: game.i18n.localize("CMPBrowser.wizard"), - }, true - ); - this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.ritual"), 'data.components.ritual', 'bool'); - this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.concentration"), 'data.components.concentration', 'bool'); - this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.verbal"), 'data.components.vocal', 'bool'); - this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.somatic"), 'data.components.somatic', 'bool'); - this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.material"), 'data.components.material', 'bool'); - } - - async addItemFilters() { - // Item Filters - - this.addItemFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); - this.addItemFilter(game.i18n.localize("CMPBrowser.general"), "Item Type", 'type', 'select', { - consumable: game.i18n.localize("DND5E.ItemTypeConsumable"), - backpack: game.i18n.localize("DND5E.ItemTypeContainer"), - equipment: game.i18n.localize("DND5E.ItemTypeEquipment"), - loot: game.i18n.localize("DND5E.ItemTypeLoot"), - tool: game.i18n.localize("DND5E.ItemTypeTool"), - weapon: game.i18n.localize("DND5E.ItemTypeWeapon") - }); - this.addItemFilter(game.i18n.localize("CMPBrowser.general"), "Packs", 'matchedPacks', 'select', - { - burglar: "Burglar's Pack", - diplomat: "Diplomat's Pack", - dungeoneer: "Dungeoneer's Pack", - entertainer: "Entertainer's Pack", - explorer: "Explorer's Pack", - monsterhunter: "Monster Hunter's Pack", - priest: "Priest's Pack", - scholar: "Scholar's Pack", - }, true - ); - this.addItemFilter("Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); - this.addItemFilter("Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - this.addItemFilter("Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); - - this.addItemFilter("Item Subtype", "Weapon", 'data.weaponType', 'text', CONFIG.DND5E.weaponTypes); - this.addItemFilter("Item Subtype", "Equipment", 'data.armor.type', 'text', CONFIG.DND5E.equipmentTypes); - this.addItemFilter("Item Subtype", "Consumable", 'data.consumableType', 'text', CONFIG.DND5E.consumableTypes); - - this.addItemFilter("Magic Items", "Rarity", 'data.rarity', 'select', - { - Common: "Common", - Uncommon: "Uncommon", - Rare: "Rare", - "Very rare": "Very Rare", - Legendary: "Legendary" - }); - } - - async addFeatFilters() { - - // Feature Filters - - this.addFeatFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); - this.addFeatFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'classRequirement', 'select', - { - artificer: game.i18n.localize("CMPBrowser.artificer"), - barbarian: "Barbarian", - bard: game.i18n.localize("CMPBrowser.bard"), - cleric: game.i18n.localize("CMPBrowser.cleric"), - druid: game.i18n.localize("CMPBrowser.druid"), - fighter: "Fighter", - monk: "Monk", - paladin: game.i18n.localize("CMPBrowser.paladin"), - ranger: game.i18n.localize("CMPBrowser.ranger"), - rogue: "Rogue", - sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), - warlock: game.i18n.localize("CMPBrowser.warlock"), - wizard: game.i18n.localize("CMPBrowser.wizard") - }, true); - - this.addFeatFilter("Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); - this.addFeatFilter("Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - this.addFeatFilter("Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); - - - } - - async addNpcFilters() { - const isFoundryV8 = game.data.version.startsWith("0.8"); - // NPC Filters - - this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.details.source', 'text'); - this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.size"), 'data.traits.size', 'select', CONFIG.DND5E.actorSizes); - this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasSpells"), 'hasSpells', 'bool'); - this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegAct"), 'data.resources.legact.max', 'bool'); - this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegRes"), 'data.resources.legres.max', 'bool'); - this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.cr"), 'data.details.cr', 'numberCompare'); - - //Foundry 0.8.x: Creature type (data.details.type) is now a structure, so we check data.details.types.value instead - let npcDetailsPath; - if (isFoundryV8) { - npcDetailsPath = "data.details.type.value"; - } else {//0.7.x - npcDetailsPath = "data.details.type"; - } - - this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.creatureType"), npcDetailsPath, 'text', { - aberration: game.i18n.localize("CMPBrowser.aberration"), - beast: game.i18n.localize("CMPBrowser.beast"), - celestial: game.i18n.localize("CMPBrowser.celestial"), - construct: game.i18n.localize("CMPBrowser.construct"), - dragon: game.i18n.localize("CMPBrowser.dragon"), - elemental: game.i18n.localize("CMPBrowser.elemental"), - fey: game.i18n.localize("CMPBrowser.fey"), - fiend: game.i18n.localize("CMPBrowser.fiend"), - giant: game.i18n.localize("CMPBrowser.giant"), - humanoid: game.i18n.localize("CMPBrowser.humanoid"), - monstrosity: game.i18n.localize("CMPBrowser.monstrosity"), - ooze: game.i18n.localize("CMPBrowser.ooze"), - plant: game.i18n.localize("CMPBrowser.plant"), - undead: game.i18n.localize("CMPBrowser.undead") - }); - - this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityStr"), 'data.abilities.str.value', 'numberCompare'); - this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityDex"), 'data.abilities.dex.value', 'numberCompare'); - this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCon"), 'data.abilities.con.value', 'numberCompare'); - this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityInt"), 'data.abilities.int.value', 'numberCompare'); - this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityWis"), 'data.abilities.wis.value', 'numberCompare'); - this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCha"), 'data.abilities.cha.value', 'numberCompare'); - - this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamImm"), 'data.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamRes"), 'data.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamVuln"), 'data.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.ConImm"), 'data.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true); - this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("CMPBrowser.dmgDealt"), 'damageDealt', 'multiSelect', CONFIG.DND5E.damageTypes, true); - - } - - /** - * Used to add custom filters to the Spell-Browser - * @param {String} category - Title of the category - * @param {String} label - Title of the filter - * @param {String} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value - * @param {String} type - type of filter - * possible filter: - * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching - * bool: will see if the data at the path exists and not false. - * select: exactly matches the data with the chosen selector from possibleValues - * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data - * numberCompare: gives the option to compare numerical values, either with =, < or the > operator - * @param {Boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters - * @param {Boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden - */ - addSpellFilter(category, label, path, type, possibleValues = null, valIsArray = false) { - this.addFilter('spell', category, label, path, type, possibleValues, valIsArray); - } - - /** - * Used to add custom filters to the Spell-Browser - * @param {String} category - Title of the category - * @param {String} label - Title of the filter - * @param {String} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value - * @param {String} type - type of filter - * possible filter: - * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching - * bool: will see if the data at the path exists and not false. - * select: exactly matches the data with the chosen selector from possibleValues - * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data - * numberCompare: gives the option to compare numerical values, either with =, < or the > operator - * @param {Boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters - * @param {Boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden - */ - addNpcFilter(category, label, path, type, possibleValues = null, valIsArray = false) { - this.addFilter('npc', category, label, path, type, possibleValues, valIsArray); - } - - addFeatFilter(category, label, path, type, possibleValues = null, valIsArray = false) { - this.addFilter('feat', category, label, path, type, possibleValues, valIsArray); - } - - addItemFilter(category, label, path, type, possibleValues = null, valIsArray = false) { - this.addFilter('item', category, label, path, type, possibleValues, valIsArray); - } -} - -Hooks.on('ready', async () => { - - if (game.compendiumBrowser === undefined) { - game.compendiumBrowser = new CompendiumBrowser(); -//0.4.0 Defer loading content until we actually use the Compendium Browser - //A compromise approach would be better (periodic loading) except would still create the memory use problem - await game.compendiumBrowser.initialize(); - } - - game.compendiumBrowser.addSpellFilters(); - game.compendiumBrowser.addFeatFilters(); - game.compendiumBrowser.addItemFilters(); - game.compendiumBrowser.addNpcFilters(); - -}); - -Hooks.on("renderCompendiumBrowser", CompendiumBrowser.afterRender); \ No newline at end of file +moduleHooks.onInit(); +moduleHooks.onReady(); diff --git a/item-packs.json b/data/item-packs.json similarity index 99% rename from item-packs.json rename to data/item-packs.json index 6ef94ac..7892c7e 100644 --- a/item-packs.json +++ b/data/item-packs.json @@ -1,10 +1,10 @@ -{ - "burglar": ["Burglar's Pack", "Backpack","Ball Bearings", "String", "Bell", "Candle", "Crowbar", "Hammer", "Piton", "Hooded Lantern", "Oil Flask", "Rations", "Tinderbox","Waterskin","Hempen Rope (50 ft.)"], - "diplomat": ["Diplomat's Pack", "Chest", "Map or Scroll Case", "Fine Clothes", "Ink Bottle", "Ink Pen", "Lamp", "Oil Flask", "Paper", "Perfume", "Sealing Wax", "Soap"], - "dungeoneer": ["Dungeoneer's Pack", "Backpack", "Crowbar", "Hammer", "Piton", "Torch", "Tinderbox", "Rations", "Waterskin", "Hempen Rope (50 ft.)"], - "entertainer": ["Entertainer's Pack", "Backpack", "Bedroll", "Costume Clothes", "Candle", "Rations", "Waterskin", "Disguise Kit"], - "explorer": ["Explorer's Pack", "Backpack", "Bedroll", "Mess Kit", "Tinderbox", "Torch", "Rations", "Waterskin", "Hempen Rope (50 ft.)"], - "monsterhunter": ["Monster Hunter's Pack", "Chest", "Crowbar", "Hammer", "Wooden Stake", "Holy Symbol", "Flask of Holy Water", "Manacles", "Steel Mirror", "Oil Flask", "Tinderbox", "Torch"], - "priest": ["Priest's Pack", "Backpack", "Blanket", "Candle", "Tinderbox", "Alms Box", "Block of Incense", "Censor", "Vestments", "Rations", "Waterskin"], - "scholar": ["Scholar's Pack", "Backpack", "Book of Lore", "Ink Bottle", "Ink Pen", "Parchment", "Bag of Sand", "Small Knife"] +{ + "burglar": ["Burglar's Pack", "Backpack","Ball Bearings", "String", "Bell", "Candle", "Crowbar", "Hammer", "Piton", "Hooded Lantern", "Oil Flask", "Rations", "Tinderbox","Waterskin","Hempen Rope (50 ft.)"], + "diplomat": ["Diplomat's Pack", "Chest", "Map or Scroll Case", "Fine Clothes", "Ink Bottle", "Ink Pen", "Lamp", "Oil Flask", "Paper", "Perfume", "Sealing Wax", "Soap"], + "dungeoneer": ["Dungeoneer's Pack", "Backpack", "Crowbar", "Hammer", "Piton", "Torch", "Tinderbox", "Rations", "Waterskin", "Hempen Rope (50 ft.)"], + "entertainer": ["Entertainer's Pack", "Backpack", "Bedroll", "Costume Clothes", "Candle", "Rations", "Waterskin", "Disguise Kit"], + "explorer": ["Explorer's Pack", "Backpack", "Bedroll", "Mess Kit", "Tinderbox", "Torch", "Rations", "Waterskin", "Hempen Rope (50 ft.)"], + "monsterhunter": ["Monster Hunter's Pack", "Chest", "Crowbar", "Hammer", "Wooden Stake", "Holy Symbol", "Flask of Holy Water", "Manacles", "Steel Mirror", "Oil Flask", "Tinderbox", "Torch"], + "priest": ["Priest's Pack", "Backpack", "Blanket", "Candle", "Tinderbox", "Alms Box", "Block of Incense", "Censor", "Vestments", "Rations", "Waterskin"], + "scholar": ["Scholar's Pack", "Backpack", "Book of Lore", "Ink Bottle", "Ink Pen", "Parchment", "Bag of Sand", "Small Knife"] } \ No newline at end of file diff --git a/spell-classes.json b/data/spell-classes.json similarity index 97% rename from spell-classes.json rename to data/spell-classes.json index eac6303..e5addda 100644 --- a/spell-classes.json +++ b/data/spell-classes.json @@ -1,482 +1,482 @@ - -{ - "abidalzimshorridwilting": "sorcerer,wizard", - "absorbelements": "artificer,druid,ranger,sorcerer,wizard", - "arcaneweapon": "artificerrevisited", - "acidsplash": "artificer,sorcerer,wizard,artificerrevisited", - "aganazzarsscorcher": "sorcerer,wizard", - "aid": "artificer,cleric,paladin,artificer,artificerrevisited", - "alarm": "artificer,ranger,wizard,artificer,artificerrevisited", - "alterself": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "animalfriendship": "bard,druid,ranger", - "animalmessenger": "bard,druid,ranger", - "animalshapes": "druid", - "animatedead": "cleric,wizard", - "animateobjects": "artificer,bard,sorcerer,wizard,artificerrevisited", - "antilifeshell": "druid", - "antimagicfield": "cleric,wizard", - "antipathysympathy": "druid,wizard", - "arcaneeye": "artificer,wizard,artificer,artificerrevisited", - "arcanegate": "sorcerer,warlock,wizard", - "arcanelock": "artificer,wizard,artificer,artificerrevisited", - "armorofagathys": "warlock", - "armsofhadar": "warlock", - "astralprojection": "cleric,warlock,wizard", - "augury": "cleric", - "auraoflife": "paladin", - "auraofpurity": "paladin", - "auraofvitality": "paladin", - "awaken": "bard,druid", - "bane": "bard,cleric", - "banishingsmite": "paladin", - "banishment": "cleric,paladin,sorcerer,warlock,wizard", - "barkskin": "druid,ranger", - "beaconofhope": "cleric", - "beastbond": "druid,ranger", - "beastsense": "druid,ranger", - "bestowcurse": "bard,cleric,wizard", - "bigbyshand": "artificer,wizard,artificerrevisited", - "arcanehand": "wizard,artificerrevisited", - "bladebarrier": "cleric", - "bladeward": "bard,sorcerer,warlock,wizard", - "bless": "cleric,paladin", - "blight": "druid,sorcerer,warlock,wizard", - "blindingsmite": "paladin", - "blindnessdeafness": "bard,cleric,sorcerer,wizard", - "blink": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "blur": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "bonesoftheearth": "druid", - "boomingblade": "sorcerer,warlock,wizard", - "brandingsmite": "paladin", - "burninghands": "sorcerer,wizard", - "calllightning": "druid", - "calmemotions": "bard,cleric", - "catapult": "artificer,sorcerer,wizard", - "catnap": "artificer,bard,sorcerer,wizard", - "causefear": "warlock,wizard", - "ceremony": "cleric,paladin", - "chainlightning": "sorcerer,wizard", - "chaosbolt": "sorcerer", - "charmmonster": "bard,druid,sorcerer,warlock,wizard", - "charmperson": "bard,druid,sorcerer,warlock,wizard", - "chilltouch": "sorcerer,warlock,wizard", - "chromaticorb": "sorcerer,wizard", - "circleofdeath": "sorcerer,warlock,wizard", - "circleofpower": "paladin", - "clairvoyance": "bard,cleric,sorcerer,wizard", - "clone": "wizard", - "cloudofdaggers": "bard,sorcerer,warlock,wizard", - "cloudkill": "sorcerer,wizard", - "colorspray": "sorcerer,wizard", - "command": "cleric,paladin", - "commune": "cleric", - "communewithnature": "druid,ranger", - "compelledduel": "paladin", - "comprehendlanguages": "bard,sorcerer,warlock,wizard", - "compulsion": "bard", - "coneofcold": "sorcerer,wizard", - "confusion": "bard,druid,sorcerer,wizard", - "conjureanimals": "druid,ranger", - "conjurebarrage": "ranger", - "conjurecelestial": "cleric", - "conjureelemental": "druid,wizard", - "conjurefey": "druid,warlock", - "conjureminorelementals": "druid,wizard", - "conjurevolley": "ranger", - "conjurewoodlandbeings": "druid,ranger", - "contactotherplane": "warlock,wizard", - "contagion": "cleric,druid", - "contingency": "wizard", - "continualflame": "artificer,cleric,wizard,artificer,artificerrevisited", - "controlflames": "druid,sorcerer,wizard", - "controlwater": "cleric,druid,wizard", - "controlweather": "cleric,druid,wizard", - "controlwinds": "druid,sorcerer,wizard", - "cordonofarrows": "ranger", - "counterspell": "sorcerer,warlock,wizard", - "createbonfire": "artificer,druid,sorcerer,warlock,wizard", - "createfoodandwater": "artificer,cleric,paladin", - "createhomunculus": "wizard", - "createundead": "cleric,warlock,wizard", - "createordestroywater": "cleric,druid", - "creation": "artificer,sorcerer,wizard,artificerrevisited", - "crownofmadness": "bard,sorcerer,warlock,wizard", - "crownofstars": "sorcerer,warlock,wizard", - "crusadersmantle": "paladin", - "curewounds": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", - "dancinglights": "artificer,bard,sorcerer,wizard,artificerrevisited", - "dansemacabre": "warlock,wizard", - "darkness": "sorcerer,warlock,wizard", - "darkvision": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "dawn": "cleric,wizard", - "daylight": "cleric,druid,paladin,ranger,sorcerer", - "deathward": "cleric,paladin,artificer", - "delayedblastfireball": "sorcerer,wizard", - "demiplane": "warlock,wizard", - "destructivewave": "paladin", - "detectevilandgood": "cleric,paladin", - "detectmagic": "artificer,bard,cleric,druid,paladin,ranger,sorcerer,wizard,artificerrevisited", - "detectpoisonanddisease": "cleric,druid,paladin,ranger", - "detectthoughts": "bard,sorcerer,wizard", - "dimensiondoor": "bard,sorcerer,warlock,wizard", - "disguiseself": "artificer,bard,sorcerer,wizard,artificer,artificerrevisited", - "disintegrate": "sorcerer,wizard", - "dispelevilandgood": "cleric,paladin", - "dispelmagic": "artificer,bard,cleric,druid,paladin,sorcerer,warlock,wizard,artificerrevisited", - "dissonantwhispers": "bard", - "distortvalue": "bard,sorcerer,warlock,wizard", - "divination": "cleric", - "divinefavor": "paladin", - "divineword": "cleric", - "dominatebeast": "druid,sorcerer", - "dominatemonster": "bard,sorcerer,warlock,wizard", - "dominateperson": "bard,sorcerer,wizard", - "dragonsbreath": "sorcerer,wizard", - "drawmijsinstantsummons": "wizard", - "instantsummons": "wizard", - "dream": "bard,warlock,wizard", - "druidgrove": "druid", - "druidcraft": "druid", - "dustdevil": "druid,sorcerer,wizard", - "earthtremor": "bard,druid,sorcerer,wizard", - "earthbind": "druid,sorcerer,warlock,wizard", - "earthquake": "cleric,druid,sorcerer", - "eldritchblast": "warlock", - "elementalbane": "artificer,druid,warlock,wizard", - "elementalweapon": "artificer,paladin,artificerrevisited", - "enemiesabound": "bard,sorcerer,warlock,wizard", - "enervation": "sorcerer,warlock,wizard", - "enhanceability": "artificer,bard,cleric,druid,sorcerer,artificer,artificerrevisited", - "enlargereduce": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "ensnaringstrike": "ranger", - "entangle": "druid", - "enthrall": "bard,warlock", - "eruptingearth": "druid,sorcerer,wizard", - "etherealness": "bard,cleric,sorcerer,warlock,wizard", - "evardsblacktentacles": "wizard", - "blacktentacles": "wizard", - "expeditiousretreat": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", - "eyebite": "bard,sorcerer,warlock,wizard", - "fabricate": "artificer,wizard,artificer,artificerrevisited", - "faeriefire": "artificer,bard,druid", - "falselife": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "farstep": "sorcerer,warlock,wizard", - "fastfriends": "bard,cleric,wizard", - "fear": "bard,sorcerer,warlock,wizard", - "featherfall": "artificer,bard,sorcerer,wizard", - "feeblemind": "bard,druid,warlock,wizard", - "feigndeath": "bard,cleric,druid,wizard", - "findfamiliar": "wizard", - "findgreatersteed": "paladin", - "findsteed": "paladin", - "findtraps": "cleric,druid,ranger", - "findthepath": "bard,cleric,druid", - "fingerofdeath": "sorcerer,warlock,wizard", - "firebolt": "artificer,sorcerer,wizard,artificerrevisited", - "fireshield": "wizard", - "firestorm": "cleric,druid,sorcerer", - "fireball": "sorcerer,wizard", - "flamearrows": "artificer,druid,ranger,sorcerer,wizard", - "flameblade": "druid", - "flamestrike": "cleric", - "flamingsphere": "druid,wizard", - "fleshtostone": "warlock,wizard", - "fly": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", - "fogcloud": "druid,ranger,sorcerer,wizard", - "forbiddance": "cleric", - "forcecage": "bard,warlock,wizard", - "foresight": "bard,druid,warlock,wizard", - "freedomofmovement": "artificer,bard,cleric,druid,ranger,artificer,artificerrevisited", - "friends": "bard,sorcerer,warlock,wizard", - "frostbite": "artificer,druid,sorcerer,warlock,wizard", - "gaseousform": "sorcerer,warlock,wizard,artificer,artificerrevisited", - "gate": "cleric,sorcerer,wizard", - "geas": "bard,cleric,druid,paladin,wizard", - "gentlerepose": "cleric,wizard", - "giantinsect": "druid", - "giftofgab": "bard,wizard", - "glibness": "bard,warlock", - "globeofinvulnerability": "sorcerer,wizard", - "glyphofwarding": "artificer,bard,cleric,wizard,artificer,artificerrevisited", - "goodberry": "druid,ranger", - "graspingvine": "druid,ranger", - "grease": "artificer,wizard,artificerrevisited", - "greaterinvisibility": "bard,sorcerer,wizard", - "greaterrestoration": "artificer,bard,cleric,druid,artificerrevisited", - "greenflameblade": "sorcerer,warlock,wizard", - "guardianoffaith": "cleric", - "guardianofnature": "druid,ranger", - "guardsandwards": "bard,wizard", - "guidance": "artificer,cleric,druid,artificerrevisited", - "guidingbolt": "cleric", - "gust": "druid,sorcerer,wizard", - "gustofwind": "druid,sorcerer,wizard", - "hailofthorns": "ranger", - "hallow": "cleric", - "hallucinatoryterrain": "bard,druid,warlock,wizard", - "harm": "cleric", - "haste": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "heal": "cleric,druid", - "healingspirit": "druid,ranger", - "healingword": "bard,cleric,druid", - "heatmetal": "artificer,bard,druid,artificerrevisited", - "hellishrebuke": "warlock", - "heroesfeast": "cleric,druid", - "heroism": "bard,paladin", - "hex": "warlock", - "holdmonster": "bard,sorcerer,warlock,wizard", - "holdperson": "bard,cleric,druid,sorcerer,warlock,wizard", - "holyaura": "cleric", - "holyweapon": "cleric,paladin", - "hungerofhadar": "warlock", - "huntersmark": "ranger", - "hypnoticpattern": "bard,sorcerer,warlock,wizard", - "iceknife": "druid,sorcerer,wizard", - "icestorm": "druid,sorcerer,wizard", - "identify": "artificer,bard,wizard,artificerrevisited", - "illusorydragon": "wizard", - "illusoryscript": "bard,warlock,wizard", - "immolation": "sorcerer,wizard", - "imprisonment": "warlock,wizard", - "incendiarycloud": "sorcerer,wizard", - "incitegreed": "cleric,warlock,wizard", - "infernalcalling": "warlock,wizard", - "infestation": "druid,sorcerer,warlock,wizard", - "inflictwounds": "cleric", - "insectplague": "cleric,druid,sorcerer", - "investitureofflame": "druid,sorcerer,warlock,wizard", - "investitureofice": "druid,sorcerer,warlock,wizard", - "investitureofstone": "druid,sorcerer,warlock,wizard", - "investitureofwind": "druid,sorcerer,warlock,wizard", - "invisibility": "artificer,bard,sorcerer,warlock,wizard,artificer,artificerrevisited", - "invulnerability": "wizard", - "jimsglowingcoin": "wizard", - "jimsmagicmissile": "wizard", - "jump": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "knock": "bard,sorcerer,wizard", - "legendlore": "bard,cleric,wizard", - "leomundssecretchest": "artificer,wizard,artificer,artificerrevisited", - "leomundstinyhut": "bard,wizard", - "lesserrestoration": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", - "levitate": "artificer,sorcerer,wizard,artificerrevisited", - "lifetransference": "cleric,wizard", - "light": "artificer,bard,cleric,sorcerer,wizard,artificerrevisited", - "lightningarrow": "ranger", - "lightningbolt": "sorcerer,wizard", - "lightninglure": "sorcerer,warlock,wizard", - "locateanimalsorplants": "bard,druid,ranger", - "locatecreature": "bard,cleric,druid,paladin,ranger,wizard", - "locateobject": "bard,cleric,druid,paladin,ranger,wizard", - "longstrider": "artificer,bard,druid,ranger,wizard,artificer,artificerrevisited", - "maddeningdarkness": "warlock,wizard", - "maelstrom": "druid", - "magearmor": "sorcerer,wizard", - "magehand": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", - "magiccircle": "cleric,paladin,warlock,wizard", - "magicjar": "wizard", - "magicmissile": "sorcerer,wizard", - "magicmouth": "artificer,bard,wizard,artificerrevisited", - "magicstone": "artificer,druid,warlock", - "magicweapon": "artificer,paladin,wizard,artificer,artificerrevisited", - "majorimage": "bard,sorcerer,warlock,wizard", - "masscurewounds": "bard,cleric,druid", - "massheal": "cleric", - "masshealingword": "cleric", - "masspolymorph": "bard,sorcerer,wizard", - "masssuggestion": "bard,sorcerer,warlock,wizard", - "maximiliansearthengrasp": "sorcerer,wizard", - "maze": "wizard", - "meldintostone": "cleric,druid", - "melfsacidarrow": "wizard", - "melfsminutemeteors": "sorcerer,wizard", - "acidarrow": "wizard", - "mending": "artificer,bard,cleric,druid,sorcerer,wizard,artificerrevisited", - "mentalprison": "sorcerer,warlock,wizard", - "message": "artificer,bard,sorcerer,wizard,artificerrevisited", - "meteorswarm": "sorcerer,wizard", - "mightyfortress": "wizard", - "mindblank": "bard,wizard", - "mindspike": "sorcerer,warlock,wizard", - "minorillusion": "bard,sorcerer,warlock,wizard", - "miragearcane": "bard,druid,wizard", - "mirrorimage": "sorcerer,warlock,wizard", - "mislead": "bard,wizard", - "mistystep": "sorcerer,warlock,wizard", - "modifymemory": "bard,wizard", - "moldearth": "druid,sorcerer,wizard", - "moonbeam": "druid", - "mordenkainensfaithfulhound": "artificer,wizard,artificer,artificerrevisited", - "motivationalspeech": "bard,cleric", - "faithfulhound": "wizard,artificer,artificerrevisited", - "mordenkainensmagnificentmansion": "bard,wizard", - "magnificentmansion": "bard,wizard", - "mordenkainensprivatesanctum": "artificer,wizard,artificer,artificerrevisited", - "mordenkainenssword": "bard,wizard", - "arcanesword": "bard,wizard", - "moveearth": "druid,sorcerer,wizard", - "negativeenergyflood": "warlock,wizard", - "nondetection": "bard,ranger,wizard", - "nystulsmagicaura": "wizard", - "arcanistsmagicaura": "wizard", - "otilukesfreezingsphere": "wizard", - "otilukesresilientsphere": "artificer,wizard,artificer,artificerrevisited", - "ottosirresistibledance": "bard,wizard", - "passwithouttrace": "druid,ranger", - "passwall": "wizard", - "phantasmalforce": "bard,sorcerer,wizard", - "phantasmalkiller": "wizard", - "phantomsteed": "wizard", - "planarally": "cleric", - "planarbinding": "bard,cleric,druid,wizard", - "planeshift": "cleric,druid,sorcerer,warlock,wizard", - "plantgrowth": "bard,druid,ranger", - "poisonspray": "artificer,druid,sorcerer,warlock,wizard,artificerrevisited", - "polymorph": "bard,druid,sorcerer,wizard", - "powerwordheal": "bard", - "powerwordkill": "bard,sorcerer,warlock,wizard", - "powerwordpain": "sorcerer,warlock,wizard", - "powerwordstun": "bard,sorcerer,warlock,wizard", - "prayerofhealing": "cleric", - "prestidigitation": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", - "primalsavagery": "druid", - "primordialward": "druid", - "prismaticspray": "sorcerer,wizard", - "prismaticwall": "wizard", - "produceflame": "druid", - "programmedillusion": "bard,wizard", - "projectimage": "bard,wizard", - "protectionfromenergy": "artificer,cleric,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "protectionfromevilandgood": "cleric,paladin,warlock,wizard", - "protectionfrompoison": "artificer,cleric,druid,paladin,ranger,artificer,artificerrevisited", - "psychicscream": "bard,sorcerer,warlock,wizard", - "purifyfoodanddrink": "artificer,cleric,druid,paladin", - "pyrotechnics": "artificer,bard,sorcerer,wizard", - "raisedead": "bard,cleric,paladin", - "rarystelepathicbond": "wizard", - "rayofenfeeblement": "warlock,wizard", - "rayoffrost": "artificer,sorcerer,wizard,artificerrevisited", - "rayofsickness": "sorcerer,wizard", - "regenerate": "bard,cleric,druid", - "reincarnate": "druid", - "removecurse": "cleric,paladin,warlock,wizard", - "resistance": "artificer,cleric,druid,artificerrevisited", - "resurrection": "bard,cleric", - "reversegravity": "druid,sorcerer,wizard", - "revivify": "artificer,cleric,paladin,artificer,artificerrevisited", - "ropetrick": "artificer,wizard,artificer,artificerrevisited", - "sacredflame": "cleric", - "sanctuary": "artificer,cleric,artificer,artificerrevisited", - "scatter": "sorcerer,warlock,wizard", - "scorchingray": "sorcerer,wizard", - "scrying": "bard,cleric,druid,warlock,wizard", - "searingsmite": "paladin", - "seeinvisibility": "artificer,bard,sorcerer,wizard,artificerrevisited", - "seeming": "bard,sorcerer,wizard", - "sending": "bard,cleric,wizard", - "sequester": "wizard", - "shadowblade": "sorcerer,warlock,wizard", - "shadowofmoil": "warlock", - "shapewater": "druid,sorcerer,wizard", - "shapechange": "druid,wizard", - "shatter": "bard,sorcerer,warlock,wizard", - "shield": "sorcerer,wizard", - "shieldoffaith": "cleric,paladin,artificer,artificerrevisited", - "shillelagh": "druid", - "shockinggrasp": "artificer,sorcerer,wizard,artificerrevisited", - "sickeningradiance": "sorcerer,warlock,wizard", - "silence": "bard,cleric,ranger", - "silentimage": "bard,sorcerer,wizard", - "simulacrum": "wizard", - "skillempowerment": "artificer,bard,sorcerer,wizard", - "skywrite": "artificer,bard,druid,wizard", - "sleep": "bard,sorcerer,wizard", - "sleetstorm": "druid,sorcerer,wizard", - "slow": "sorcerer,wizard", - "snare": "artificer,druid,ranger,wizard", - "snillocssnowballswarm": "sorcerer,wizard", - "soulcage": "warlock,wizard", - "sparethedying": "artificer,cleric,artificerrevisited", - "speakwithanimals": "bard,druid,ranger", - "speakwithdead": "bard,cleric", - "speakwithplants": "bard,druid,ranger", - "spiderclimb": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", - "spikegrowth": "druid,ranger", - "spiritguardians": "cleric", - "spiritualweapon": "cleric", - "staggeringsmite": "paladin", - "steelwindstrike": "ranger,wizard", - "stinkingcloud": "bard,sorcerer,wizard", - "stoneshape": "artificer,cleric,druid,wizard,artificer,artificerrevisited", - "stoneskin": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "stormofvengeance": "druid", - "stormsphere": "sorcerer,wizard", - "suggestion": "bard,sorcerer,warlock,wizard", - "summongreaterdemon": "warlock,wizard", - "summonlesserdemons": "warlock,wizard", - "sunbeam": "druid,sorcerer,wizard", - "sunburst": "druid,sorcerer,wizard", - "swiftquiver": "ranger", - "swordburst": "sorcerer,warlock,wizard", - "synapticstatic": "bard,sorcerer,warlock,wizard", - "symbol": "bard,cleric,wizard", - "tashashideouslaughter": "bard,wizard", - "hideouslaughter": "bard,wizard", - "telekinesis": "sorcerer,wizard", - "telepathy": "wizard", - "teleport": "bard,sorcerer,wizard", - "teleportationcircle": "bard,sorcerer,wizard", - "templeofthegods": "cleric", - "tensersfloatingdisk": "wizard", - "tensersfloatingdisc": "wizard", - "tenserstransformation": "wizard", - "floatingdisc": "wizard", - "thaumaturgy": "cleric", - "thornwhip": "artificer,druid,artificerrevisited", - "thunderstep": "sorcerer,warlock,wizard", - "thunderclap": "artificer,bard,druid,sorcerer,warlock,wizard", - "thunderoussmite": "paladin", - "thunderwave": "bard,druid,sorcerer,wizard", - "tidalwave": "druid,sorcerer,wizard", - "timestop": "sorcerer,wizard", - "tinyservant": "artificer,wizard", - "tollthedead": "cleric,warlock,wizard", - "tongues": "bard,cleric,sorcerer,warlock,wizard", - "transmuterock": "artificer,druid,wizard", - "transportviaplants": "druid", - "treestride": "druid,ranger", - "truepolymorph": "bard,warlock,wizard", - "trueresurrection": "cleric,druid", - "trueseeing": "bard,cleric,sorcerer,warlock,wizard", - "truestrike": "bard,sorcerer,warlock,wizard", - "tsunami": "druid", - "unseenservant": "bard,warlock,wizard", - "vampirictouch": "warlock,wizard", - "viciousmockery": "bard", - "vitriolicsphere": "sorcerer,wizard", - "walloffire": "druid,sorcerer,wizard", - "wallofforce": "wizard", - "wallofice": "wizard", - "walloflight": "sorcerer,warlock,wizard", - "wallofsand": "wizard", - "wallofstone": "artificer,druid,sorcerer,wizard,artificerrevisited", - "wallofthorns": "druid", - "wallofwater": "druid,sorcerer,wizard", - "wardingbond": "cleric", - "wardingwind": "bard,druid,sorcerer,wizard", - "waterbreathing": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "waterwalk": "artificer,cleric,druid,ranger,sorcerer,artificer,artificerrevisited", - "waterysphere": "druid,sorcerer,wizard", - "web": "artificer,sorcerer,wizard", - "weird": "wizard", - "whirlwind": "druid,sorcerer,wizard", - "windwalk": "druid", - "windwall": "druid,ranger", - "wish": "sorcerer,wizard", - "witchbolt": "sorcerer,warlock,wizard", - "wordofradiance": "cleric", - "wordofrecall": "cleric", - "wrathofnature": "druid,ranger", - "wrathfulsmite": "paladin", - "zephyrstrike": "ranger", - "zoneoftruth": "bard,cleric,paladin" + +{ + "abidalzimshorridwilting": "sorcerer,wizard", + "absorbelements": "artificer,druid,ranger,sorcerer,wizard", + "arcaneweapon": "artificerrevisited", + "acidsplash": "artificer,sorcerer,wizard,artificerrevisited", + "aganazzarsscorcher": "sorcerer,wizard", + "aid": "artificer,cleric,paladin,artificer,artificerrevisited", + "alarm": "artificer,ranger,wizard,artificer,artificerrevisited", + "alterself": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "animalfriendship": "bard,druid,ranger", + "animalmessenger": "bard,druid,ranger", + "animalshapes": "druid", + "animatedead": "cleric,wizard", + "animateobjects": "artificer,bard,sorcerer,wizard,artificerrevisited", + "antilifeshell": "druid", + "antimagicfield": "cleric,wizard", + "antipathysympathy": "druid,wizard", + "arcaneeye": "artificer,wizard,artificer,artificerrevisited", + "arcanegate": "sorcerer,warlock,wizard", + "arcanelock": "artificer,wizard,artificer,artificerrevisited", + "armorofagathys": "warlock", + "armsofhadar": "warlock", + "astralprojection": "cleric,warlock,wizard", + "augury": "cleric", + "auraoflife": "paladin", + "auraofpurity": "paladin", + "auraofvitality": "paladin", + "awaken": "bard,druid", + "bane": "bard,cleric", + "banishingsmite": "paladin", + "banishment": "cleric,paladin,sorcerer,warlock,wizard", + "barkskin": "druid,ranger", + "beaconofhope": "cleric", + "beastbond": "druid,ranger", + "beastsense": "druid,ranger", + "bestowcurse": "bard,cleric,wizard", + "bigbyshand": "artificer,wizard,artificerrevisited", + "arcanehand": "wizard,artificerrevisited", + "bladebarrier": "cleric", + "bladeward": "bard,sorcerer,warlock,wizard", + "bless": "cleric,paladin", + "blight": "druid,sorcerer,warlock,wizard", + "blindingsmite": "paladin", + "blindnessdeafness": "bard,cleric,sorcerer,wizard", + "blink": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "blur": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "bonesoftheearth": "druid", + "boomingblade": "sorcerer,warlock,wizard", + "brandingsmite": "paladin", + "burninghands": "sorcerer,wizard", + "calllightning": "druid", + "calmemotions": "bard,cleric", + "catapult": "artificer,sorcerer,wizard", + "catnap": "artificer,bard,sorcerer,wizard", + "causefear": "warlock,wizard", + "ceremony": "cleric,paladin", + "chainlightning": "sorcerer,wizard", + "chaosbolt": "sorcerer", + "charmmonster": "bard,druid,sorcerer,warlock,wizard", + "charmperson": "bard,druid,sorcerer,warlock,wizard", + "chilltouch": "sorcerer,warlock,wizard", + "chromaticorb": "sorcerer,wizard", + "circleofdeath": "sorcerer,warlock,wizard", + "circleofpower": "paladin", + "clairvoyance": "bard,cleric,sorcerer,wizard", + "clone": "wizard", + "cloudofdaggers": "bard,sorcerer,warlock,wizard", + "cloudkill": "sorcerer,wizard", + "colorspray": "sorcerer,wizard", + "command": "cleric,paladin", + "commune": "cleric", + "communewithnature": "druid,ranger", + "compelledduel": "paladin", + "comprehendlanguages": "bard,sorcerer,warlock,wizard", + "compulsion": "bard", + "coneofcold": "sorcerer,wizard", + "confusion": "bard,druid,sorcerer,wizard", + "conjureanimals": "druid,ranger", + "conjurebarrage": "ranger", + "conjurecelestial": "cleric", + "conjureelemental": "druid,wizard", + "conjurefey": "druid,warlock", + "conjureminorelementals": "druid,wizard", + "conjurevolley": "ranger", + "conjurewoodlandbeings": "druid,ranger", + "contactotherplane": "warlock,wizard", + "contagion": "cleric,druid", + "contingency": "wizard", + "continualflame": "artificer,cleric,wizard,artificer,artificerrevisited", + "controlflames": "druid,sorcerer,wizard", + "controlwater": "cleric,druid,wizard", + "controlweather": "cleric,druid,wizard", + "controlwinds": "druid,sorcerer,wizard", + "cordonofarrows": "ranger", + "counterspell": "sorcerer,warlock,wizard", + "createbonfire": "artificer,druid,sorcerer,warlock,wizard", + "createfoodandwater": "artificer,cleric,paladin", + "createhomunculus": "wizard", + "createundead": "cleric,warlock,wizard", + "createordestroywater": "cleric,druid", + "creation": "artificer,sorcerer,wizard,artificerrevisited", + "crownofmadness": "bard,sorcerer,warlock,wizard", + "crownofstars": "sorcerer,warlock,wizard", + "crusadersmantle": "paladin", + "curewounds": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", + "dancinglights": "artificer,bard,sorcerer,wizard,artificerrevisited", + "dansemacabre": "warlock,wizard", + "darkness": "sorcerer,warlock,wizard", + "darkvision": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "dawn": "cleric,wizard", + "daylight": "cleric,druid,paladin,ranger,sorcerer", + "deathward": "cleric,paladin,artificer", + "delayedblastfireball": "sorcerer,wizard", + "demiplane": "warlock,wizard", + "destructivewave": "paladin", + "detectevilandgood": "cleric,paladin", + "detectmagic": "artificer,bard,cleric,druid,paladin,ranger,sorcerer,wizard,artificerrevisited", + "detectpoisonanddisease": "cleric,druid,paladin,ranger", + "detectthoughts": "bard,sorcerer,wizard", + "dimensiondoor": "bard,sorcerer,warlock,wizard", + "disguiseself": "artificer,bard,sorcerer,wizard,artificer,artificerrevisited", + "disintegrate": "sorcerer,wizard", + "dispelevilandgood": "cleric,paladin", + "dispelmagic": "artificer,bard,cleric,druid,paladin,sorcerer,warlock,wizard,artificerrevisited", + "dissonantwhispers": "bard", + "distortvalue": "bard,sorcerer,warlock,wizard", + "divination": "cleric", + "divinefavor": "paladin", + "divineword": "cleric", + "dominatebeast": "druid,sorcerer", + "dominatemonster": "bard,sorcerer,warlock,wizard", + "dominateperson": "bard,sorcerer,wizard", + "dragonsbreath": "sorcerer,wizard", + "drawmijsinstantsummons": "wizard", + "instantsummons": "wizard", + "dream": "bard,warlock,wizard", + "druidgrove": "druid", + "druidcraft": "druid", + "dustdevil": "druid,sorcerer,wizard", + "earthtremor": "bard,druid,sorcerer,wizard", + "earthbind": "druid,sorcerer,warlock,wizard", + "earthquake": "cleric,druid,sorcerer", + "eldritchblast": "warlock", + "elementalbane": "artificer,druid,warlock,wizard", + "elementalweapon": "artificer,paladin,artificerrevisited", + "enemiesabound": "bard,sorcerer,warlock,wizard", + "enervation": "sorcerer,warlock,wizard", + "enhanceability": "artificer,bard,cleric,druid,sorcerer,artificer,artificerrevisited", + "enlargereduce": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "ensnaringstrike": "ranger", + "entangle": "druid", + "enthrall": "bard,warlock", + "eruptingearth": "druid,sorcerer,wizard", + "etherealness": "bard,cleric,sorcerer,warlock,wizard", + "evardsblacktentacles": "wizard", + "blacktentacles": "wizard", + "expeditiousretreat": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", + "eyebite": "bard,sorcerer,warlock,wizard", + "fabricate": "artificer,wizard,artificer,artificerrevisited", + "faeriefire": "artificer,bard,druid", + "falselife": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "farstep": "sorcerer,warlock,wizard", + "fastfriends": "bard,cleric,wizard", + "fear": "bard,sorcerer,warlock,wizard", + "featherfall": "artificer,bard,sorcerer,wizard", + "feeblemind": "bard,druid,warlock,wizard", + "feigndeath": "bard,cleric,druid,wizard", + "findfamiliar": "wizard", + "findgreatersteed": "paladin", + "findsteed": "paladin", + "findtraps": "cleric,druid,ranger", + "findthepath": "bard,cleric,druid", + "fingerofdeath": "sorcerer,warlock,wizard", + "firebolt": "artificer,sorcerer,wizard,artificerrevisited", + "fireshield": "wizard", + "firestorm": "cleric,druid,sorcerer", + "fireball": "sorcerer,wizard", + "flamearrows": "artificer,druid,ranger,sorcerer,wizard", + "flameblade": "druid", + "flamestrike": "cleric", + "flamingsphere": "druid,wizard", + "fleshtostone": "warlock,wizard", + "fly": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", + "fogcloud": "druid,ranger,sorcerer,wizard", + "forbiddance": "cleric", + "forcecage": "bard,warlock,wizard", + "foresight": "bard,druid,warlock,wizard", + "freedomofmovement": "artificer,bard,cleric,druid,ranger,artificer,artificerrevisited", + "friends": "bard,sorcerer,warlock,wizard", + "frostbite": "artificer,druid,sorcerer,warlock,wizard", + "gaseousform": "sorcerer,warlock,wizard,artificer,artificerrevisited", + "gate": "cleric,sorcerer,wizard", + "geas": "bard,cleric,druid,paladin,wizard", + "gentlerepose": "cleric,wizard", + "giantinsect": "druid", + "giftofgab": "bard,wizard", + "glibness": "bard,warlock", + "globeofinvulnerability": "sorcerer,wizard", + "glyphofwarding": "artificer,bard,cleric,wizard,artificer,artificerrevisited", + "goodberry": "druid,ranger", + "graspingvine": "druid,ranger", + "grease": "artificer,wizard,artificerrevisited", + "greaterinvisibility": "bard,sorcerer,wizard", + "greaterrestoration": "artificer,bard,cleric,druid,artificerrevisited", + "greenflameblade": "sorcerer,warlock,wizard", + "guardianoffaith": "cleric", + "guardianofnature": "druid,ranger", + "guardsandwards": "bard,wizard", + "guidance": "artificer,cleric,druid,artificerrevisited", + "guidingbolt": "cleric", + "gust": "druid,sorcerer,wizard", + "gustofwind": "druid,sorcerer,wizard", + "hailofthorns": "ranger", + "hallow": "cleric", + "hallucinatoryterrain": "bard,druid,warlock,wizard", + "harm": "cleric", + "haste": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "heal": "cleric,druid", + "healingspirit": "druid,ranger", + "healingword": "bard,cleric,druid", + "heatmetal": "artificer,bard,druid,artificerrevisited", + "hellishrebuke": "warlock", + "heroesfeast": "cleric,druid", + "heroism": "bard,paladin", + "hex": "warlock", + "holdmonster": "bard,sorcerer,warlock,wizard", + "holdperson": "bard,cleric,druid,sorcerer,warlock,wizard", + "holyaura": "cleric", + "holyweapon": "cleric,paladin", + "hungerofhadar": "warlock", + "huntersmark": "ranger", + "hypnoticpattern": "bard,sorcerer,warlock,wizard", + "iceknife": "druid,sorcerer,wizard", + "icestorm": "druid,sorcerer,wizard", + "identify": "artificer,bard,wizard,artificerrevisited", + "illusorydragon": "wizard", + "illusoryscript": "bard,warlock,wizard", + "immolation": "sorcerer,wizard", + "imprisonment": "warlock,wizard", + "incendiarycloud": "sorcerer,wizard", + "incitegreed": "cleric,warlock,wizard", + "infernalcalling": "warlock,wizard", + "infestation": "druid,sorcerer,warlock,wizard", + "inflictwounds": "cleric", + "insectplague": "cleric,druid,sorcerer", + "investitureofflame": "druid,sorcerer,warlock,wizard", + "investitureofice": "druid,sorcerer,warlock,wizard", + "investitureofstone": "druid,sorcerer,warlock,wizard", + "investitureofwind": "druid,sorcerer,warlock,wizard", + "invisibility": "artificer,bard,sorcerer,warlock,wizard,artificer,artificerrevisited", + "invulnerability": "wizard", + "jimsglowingcoin": "wizard", + "jimsmagicmissile": "wizard", + "jump": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "knock": "bard,sorcerer,wizard", + "legendlore": "bard,cleric,wizard", + "leomundssecretchest": "artificer,wizard,artificer,artificerrevisited", + "leomundstinyhut": "bard,wizard", + "lesserrestoration": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", + "levitate": "artificer,sorcerer,wizard,artificerrevisited", + "lifetransference": "cleric,wizard", + "light": "artificer,bard,cleric,sorcerer,wizard,artificerrevisited", + "lightningarrow": "ranger", + "lightningbolt": "sorcerer,wizard", + "lightninglure": "sorcerer,warlock,wizard", + "locateanimalsorplants": "bard,druid,ranger", + "locatecreature": "bard,cleric,druid,paladin,ranger,wizard", + "locateobject": "bard,cleric,druid,paladin,ranger,wizard", + "longstrider": "artificer,bard,druid,ranger,wizard,artificer,artificerrevisited", + "maddeningdarkness": "warlock,wizard", + "maelstrom": "druid", + "magearmor": "sorcerer,wizard", + "magehand": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", + "magiccircle": "cleric,paladin,warlock,wizard", + "magicjar": "wizard", + "magicmissile": "sorcerer,wizard", + "magicmouth": "artificer,bard,wizard,artificerrevisited", + "magicstone": "artificer,druid,warlock", + "magicweapon": "artificer,paladin,wizard,artificer,artificerrevisited", + "majorimage": "bard,sorcerer,warlock,wizard", + "masscurewounds": "bard,cleric,druid", + "massheal": "cleric", + "masshealingword": "cleric", + "masspolymorph": "bard,sorcerer,wizard", + "masssuggestion": "bard,sorcerer,warlock,wizard", + "maximiliansearthengrasp": "sorcerer,wizard", + "maze": "wizard", + "meldintostone": "cleric,druid", + "melfsacidarrow": "wizard", + "melfsminutemeteors": "sorcerer,wizard", + "acidarrow": "wizard", + "mending": "artificer,bard,cleric,druid,sorcerer,wizard,artificerrevisited", + "mentalprison": "sorcerer,warlock,wizard", + "message": "artificer,bard,sorcerer,wizard,artificerrevisited", + "meteorswarm": "sorcerer,wizard", + "mightyfortress": "wizard", + "mindblank": "bard,wizard", + "mindspike": "sorcerer,warlock,wizard", + "minorillusion": "bard,sorcerer,warlock,wizard", + "miragearcane": "bard,druid,wizard", + "mirrorimage": "sorcerer,warlock,wizard", + "mislead": "bard,wizard", + "mistystep": "sorcerer,warlock,wizard", + "modifymemory": "bard,wizard", + "moldearth": "druid,sorcerer,wizard", + "moonbeam": "druid", + "mordenkainensfaithfulhound": "artificer,wizard,artificer,artificerrevisited", + "motivationalspeech": "bard,cleric", + "faithfulhound": "wizard,artificer,artificerrevisited", + "mordenkainensmagnificentmansion": "bard,wizard", + "magnificentmansion": "bard,wizard", + "mordenkainensprivatesanctum": "artificer,wizard,artificer,artificerrevisited", + "mordenkainenssword": "bard,wizard", + "arcanesword": "bard,wizard", + "moveearth": "druid,sorcerer,wizard", + "negativeenergyflood": "warlock,wizard", + "nondetection": "bard,ranger,wizard", + "nystulsmagicaura": "wizard", + "arcanistsmagicaura": "wizard", + "otilukesfreezingsphere": "wizard", + "otilukesresilientsphere": "artificer,wizard,artificer,artificerrevisited", + "ottosirresistibledance": "bard,wizard", + "passwithouttrace": "druid,ranger", + "passwall": "wizard", + "phantasmalforce": "bard,sorcerer,wizard", + "phantasmalkiller": "wizard", + "phantomsteed": "wizard", + "planarally": "cleric", + "planarbinding": "bard,cleric,druid,wizard", + "planeshift": "cleric,druid,sorcerer,warlock,wizard", + "plantgrowth": "bard,druid,ranger", + "poisonspray": "artificer,druid,sorcerer,warlock,wizard,artificerrevisited", + "polymorph": "bard,druid,sorcerer,wizard", + "powerwordheal": "bard", + "powerwordkill": "bard,sorcerer,warlock,wizard", + "powerwordpain": "sorcerer,warlock,wizard", + "powerwordstun": "bard,sorcerer,warlock,wizard", + "prayerofhealing": "cleric", + "prestidigitation": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", + "primalsavagery": "druid", + "primordialward": "druid", + "prismaticspray": "sorcerer,wizard", + "prismaticwall": "wizard", + "produceflame": "druid", + "programmedillusion": "bard,wizard", + "projectimage": "bard,wizard", + "protectionfromenergy": "artificer,cleric,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "protectionfromevilandgood": "cleric,paladin,warlock,wizard", + "protectionfrompoison": "artificer,cleric,druid,paladin,ranger,artificer,artificerrevisited", + "psychicscream": "bard,sorcerer,warlock,wizard", + "purifyfoodanddrink": "artificer,cleric,druid,paladin", + "pyrotechnics": "artificer,bard,sorcerer,wizard", + "raisedead": "bard,cleric,paladin", + "rarystelepathicbond": "wizard", + "rayofenfeeblement": "warlock,wizard", + "rayoffrost": "artificer,sorcerer,wizard,artificerrevisited", + "rayofsickness": "sorcerer,wizard", + "regenerate": "bard,cleric,druid", + "reincarnate": "druid", + "removecurse": "cleric,paladin,warlock,wizard", + "resistance": "artificer,cleric,druid,artificerrevisited", + "resurrection": "bard,cleric", + "reversegravity": "druid,sorcerer,wizard", + "revivify": "artificer,cleric,paladin,artificer,artificerrevisited", + "ropetrick": "artificer,wizard,artificer,artificerrevisited", + "sacredflame": "cleric", + "sanctuary": "artificer,cleric,artificer,artificerrevisited", + "scatter": "sorcerer,warlock,wizard", + "scorchingray": "sorcerer,wizard", + "scrying": "bard,cleric,druid,warlock,wizard", + "searingsmite": "paladin", + "seeinvisibility": "artificer,bard,sorcerer,wizard,artificerrevisited", + "seeming": "bard,sorcerer,wizard", + "sending": "bard,cleric,wizard", + "sequester": "wizard", + "shadowblade": "sorcerer,warlock,wizard", + "shadowofmoil": "warlock", + "shapewater": "druid,sorcerer,wizard", + "shapechange": "druid,wizard", + "shatter": "bard,sorcerer,warlock,wizard", + "shield": "sorcerer,wizard", + "shieldoffaith": "cleric,paladin,artificer,artificerrevisited", + "shillelagh": "druid", + "shockinggrasp": "artificer,sorcerer,wizard,artificerrevisited", + "sickeningradiance": "sorcerer,warlock,wizard", + "silence": "bard,cleric,ranger", + "silentimage": "bard,sorcerer,wizard", + "simulacrum": "wizard", + "skillempowerment": "artificer,bard,sorcerer,wizard", + "skywrite": "artificer,bard,druid,wizard", + "sleep": "bard,sorcerer,wizard", + "sleetstorm": "druid,sorcerer,wizard", + "slow": "sorcerer,wizard", + "snare": "artificer,druid,ranger,wizard", + "snillocssnowballswarm": "sorcerer,wizard", + "soulcage": "warlock,wizard", + "sparethedying": "artificer,cleric,artificerrevisited", + "speakwithanimals": "bard,druid,ranger", + "speakwithdead": "bard,cleric", + "speakwithplants": "bard,druid,ranger", + "spiderclimb": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", + "spikegrowth": "druid,ranger", + "spiritguardians": "cleric", + "spiritualweapon": "cleric", + "staggeringsmite": "paladin", + "steelwindstrike": "ranger,wizard", + "stinkingcloud": "bard,sorcerer,wizard", + "stoneshape": "artificer,cleric,druid,wizard,artificer,artificerrevisited", + "stoneskin": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "stormofvengeance": "druid", + "stormsphere": "sorcerer,wizard", + "suggestion": "bard,sorcerer,warlock,wizard", + "summongreaterdemon": "warlock,wizard", + "summonlesserdemons": "warlock,wizard", + "sunbeam": "druid,sorcerer,wizard", + "sunburst": "druid,sorcerer,wizard", + "swiftquiver": "ranger", + "swordburst": "sorcerer,warlock,wizard", + "synapticstatic": "bard,sorcerer,warlock,wizard", + "symbol": "bard,cleric,wizard", + "tashashideouslaughter": "bard,wizard", + "hideouslaughter": "bard,wizard", + "telekinesis": "sorcerer,wizard", + "telepathy": "wizard", + "teleport": "bard,sorcerer,wizard", + "teleportationcircle": "bard,sorcerer,wizard", + "templeofthegods": "cleric", + "tensersfloatingdisk": "wizard", + "tensersfloatingdisc": "wizard", + "tenserstransformation": "wizard", + "floatingdisc": "wizard", + "thaumaturgy": "cleric", + "thornwhip": "artificer,druid,artificerrevisited", + "thunderstep": "sorcerer,warlock,wizard", + "thunderclap": "artificer,bard,druid,sorcerer,warlock,wizard", + "thunderoussmite": "paladin", + "thunderwave": "bard,druid,sorcerer,wizard", + "tidalwave": "druid,sorcerer,wizard", + "timestop": "sorcerer,wizard", + "tinyservant": "artificer,wizard", + "tollthedead": "cleric,warlock,wizard", + "tongues": "bard,cleric,sorcerer,warlock,wizard", + "transmuterock": "artificer,druid,wizard", + "transportviaplants": "druid", + "treestride": "druid,ranger", + "truepolymorph": "bard,warlock,wizard", + "trueresurrection": "cleric,druid", + "trueseeing": "bard,cleric,sorcerer,warlock,wizard", + "truestrike": "bard,sorcerer,warlock,wizard", + "tsunami": "druid", + "unseenservant": "bard,warlock,wizard", + "vampirictouch": "warlock,wizard", + "viciousmockery": "bard", + "vitriolicsphere": "sorcerer,wizard", + "walloffire": "druid,sorcerer,wizard", + "wallofforce": "wizard", + "wallofice": "wizard", + "walloflight": "sorcerer,warlock,wizard", + "wallofsand": "wizard", + "wallofstone": "artificer,druid,sorcerer,wizard,artificerrevisited", + "wallofthorns": "druid", + "wallofwater": "druid,sorcerer,wizard", + "wardingbond": "cleric", + "wardingwind": "bard,druid,sorcerer,wizard", + "waterbreathing": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "waterwalk": "artificer,cleric,druid,ranger,sorcerer,artificer,artificerrevisited", + "waterysphere": "druid,sorcerer,wizard", + "web": "artificer,sorcerer,wizard", + "weird": "wizard", + "whirlwind": "druid,sorcerer,wizard", + "windwalk": "druid", + "windwall": "druid,ranger", + "wish": "sorcerer,wizard", + "witchbolt": "sorcerer,warlock,wizard", + "wordofradiance": "cleric", + "wordofrecall": "cleric", + "wrathofnature": "druid,ranger", + "wrathfulsmite": "paladin", + "zephyrstrike": "ranger", + "zoneoftruth": "bard,cleric,paladin" } \ No newline at end of file diff --git a/sub-classes.json b/data/sub-classes.json similarity index 98% rename from sub-classes.json rename to data/sub-classes.json index 072d52c..91841e0 100644 --- a/sub-classes.json +++ b/data/sub-classes.json @@ -1,15 +1,15 @@ -{ - "artificer":["Alchemist","Gunsmith"], - "barbarian":["Ancesstrial Guardian", "Battlerager", "Berserker", "Storm Herald", "Totem Warriro", "Zealot"], - "bard":["Eloquence", "Glamour", "Lore", "Swords", "Valor", "Whispers"], - "cleric":["Arcana", "Death", "Forge", "Grave", "Knowledge", "Life", "Light", "Nature", "Order", "Tempest", "Trickery", "War Domain"], - "druid":["Dreams", "Land", "Moon", "Sheapherd", "Spores"], - "fighter":["Arcane Archer", "Battle Master", "Cavalier", "Champion", "Echo Knight", "Eldritch Knight", "Samurai"], - "monk":["Drunken Master", "Four Elements", "Kensei", "Long Death", "Open Hand", "Shadow", "Sun Soul"], - "paladin":["Ancients", "Conquest", "Crown", "Devotion", "Glory", "Oathbreaker", "Redemption", "Vengeance"], - "ranger":["Beast Master", "Gloom Stalker", "Horizon Walker", "Hunter", "Monster Slayer"], - "rogue":["Arcane Trickster", "Assassin", "Inquisitive", "Mastermind", "Scout", "Swashbuckler", "Thief"], - "sorcerer":["Devine Soul", "Draconic", "Shadow", "Storm", "Wild"], - "warlock":["Archfey", "Celestial", "Fiend","Great Old One", "Hexblade", "Undying"], - "wizard":["Abjuration", "Bladesinging", "Chronurgy", "Conjuration", "Divination", "Énchantment", "Evocation", "Graviturgy", "Illusion", "Necromancy", "Transmutation", "War Magic"] +{ + "artificer":["Alchemist","Gunsmith"], + "barbarian":["Ancesstrial Guardian", "Battlerager", "Berserker", "Storm Herald", "Totem Warriro", "Zealot"], + "bard":["Eloquence", "Glamour", "Lore", "Swords", "Valor", "Whispers"], + "cleric":["Arcana", "Death", "Forge", "Grave", "Knowledge", "Life", "Light", "Nature", "Order", "Tempest", "Trickery", "War Domain"], + "druid":["Dreams", "Land", "Moon", "Sheapherd", "Spores"], + "fighter":["Arcane Archer", "Battle Master", "Cavalier", "Champion", "Echo Knight", "Eldritch Knight", "Samurai"], + "monk":["Drunken Master", "Four Elements", "Kensei", "Long Death", "Open Hand", "Shadow", "Sun Soul"], + "paladin":["Ancients", "Conquest", "Crown", "Devotion", "Glory", "Oathbreaker", "Redemption", "Vengeance"], + "ranger":["Beast Master", "Gloom Stalker", "Horizon Walker", "Hunter", "Monster Slayer"], + "rogue":["Arcane Trickster", "Assassin", "Inquisitive", "Mastermind", "Scout", "Swashbuckler", "Thief"], + "sorcerer":["Devine Soul", "Draconic", "Shadow", "Storm", "Wild"], + "warlock":["Archfey", "Celestial", "Fiend","Great Old One", "Hexblade", "Undying"], + "wizard":["Abjuration", "Bladesinging", "Chronurgy", "Conjuration", "Divination", "Énchantment", "Evocation", "Graviturgy", "Illusion", "Necromancy", "Transmutation", "War Magic"] } \ No newline at end of file diff --git a/hooks/mainHook.js b/hooks/mainHook.js new file mode 100644 index 0000000..61a914f --- /dev/null +++ b/hooks/mainHook.js @@ -0,0 +1,72 @@ +import { CompendiumBrowser } from '../scripts/compendium-browser.js'; + +export class moduleHooks { + + static onInit() { + Hooks.on('init', async () => { + Handlebars.registerHelper('switch', function (value, options) { + this.switch_value = value; + this.switch_break = false; + return options.fn(this); + }); + + Handlebars.registerHelper('case', function (value, options) { + if (value == this.switch_value) { + this.switch_break = true; + return options.fn(this); + } + }); + + Handlebars.registerHelper('default', function (value, options) { + if (this.switch_break == false) { + return value; + } + }); + + Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) { + switch (operator) { + case '==': + return (v1 == v2) ? options.fn(this) : options.inverse(this); + case '===': + return (v1 === v2) ? options.fn(this) : options.inverse(this); + case '!=': + return (v1 != v2) ? options.fn(this) : options.inverse(this); + case '!==': + return (v1 !== v2) ? options.fn(this) : options.inverse(this); + case '<': + return (v1 < v2) ? options.fn(this) : options.inverse(this); + case '<=': + return (v1 <= v2) ? options.fn(this) : options.inverse(this); + case '>': + return (v1 > v2) ? options.fn(this) : options.inverse(this); + case '>=': + return (v1 >= v2) ? options.fn(this) : options.inverse(this); + case '&&': + return (v1 && v2) ? options.fn(this) : options.inverse(this); + case '||': + return (v1 || v2) ? options.fn(this) : options.inverse(this); + default: + return options.inverse(this); + } + }); + }); + } + + static onReady() { + Hooks.on('ready', async () => { + if (game.compendiumBrowser === undefined) { + game.compendiumBrowser = new CompendiumBrowser(); + //0.4.0 Defer loading content until we actually use the Compendium Browser + //A compromise approach would be better (periodic loading) except would still create the memory use problem + await game.compendiumBrowser.initialize(); + } + + game.compendiumBrowser.addFilters(); + }); + } + + static onRenderComplete() { + Hooks.on("renderCompendiumBrowser", CompendiumBrowser.afterRender); + } + +} \ No newline at end of file diff --git a/lang/de.json b/lang/de.json new file mode 100644 index 0000000..0e6986e --- /dev/null +++ b/lang/de.json @@ -0,0 +1,69 @@ +{ + "CMPBrowser.compendiumBrowser":"Compendium Browser", + "CMPBrowser.sortBy":"Sortieren nach", + "CMPBrowser.cr":"Challenge Rating", + "CMPBrowser.generalSettings":"Allgemeine Einstellungen", + "CMPBrowser.allowSpellAcc":"Erlaube Spielern Zauber zu durchsuchen.", + "CMPBrowser.allowNpcAcc":"Erlaube Spielern NPCs zu durchsuchen.", + "CMPBrowser.compSettingsSpell":"Gegenstand Kompendium Einstellungen", + "CMPBrowser.compSettingsNpc":"NPC Kompendium Einstellungen", + "CMPBrowser.load":"Laden", + "CMPBrowser.lvl":"Level", + "CMPBrowser.ritual":"Ritual", + "CMPBrowser.concentration":"Konzentration", + "CMPBrowser.verbal":"Verbal", + "CMPBrowser.somatic":"Somatisch", + "CMPBrowser.material":"Material", + "CMPBrowser.cantip":"Cantrip", + "CMPBrowser.school":"Schule", + "CMPBrowser.castingTime":"Zauberzeit", + "CMPBrowser.bonusAction":"Bonus Aktion", + "CMPBrowser.reaction":"Reaktion", + "CMPBrowser.spellType":"Zaubertyp", + "CMPBrowser.damageType":"Schadensart", + "CMPBrowser.class":"Klasse", + "CMPBrowser.artificer":"Artificer", + "CMPBrowser.bard":"Barde", + "CMPBrowser.cleric":"Kleriker", + "CMPBrowser.druid":"Druide", + "CMPBrowser.paladin":"Paladin", + "CMPBrowser.ranger":"Ranger", + "CMPBrowser.sorcerer":"Zauberer", + "CMPBrowser.warlock":"Hexenmeister", + "CMPBrowser.wizard":"Magier", + "CMPBrowser.general":"Allgemein", + "CMPBrowser.components":"Komponenten", + "CMPBrowser.hasSpells":"Hat Zauber", + "CMPBrowser.hasLegAct":"Hat Legendäre Aktion(en)", + "CMPBrowser.hasLegRes":"Hat Legendäre Resistenz(en)", + "CMPBrowser.creatureType":"Kreaturentyp", + "CMPBrowser.aberration": "Aberration", + "CMPBrowser.beast": "Beast", + "CMPBrowser.celestial": "Göttlich", + "CMPBrowser.construct": "construct", + "CMPBrowser.dragon": "Drachen", + "CMPBrowser.elemental": "Elemental", + "CMPBrowser.fey": "Fey", + "CMPBrowser.fiend": "Fiend", + "CMPBrowser.giant": "Giant", + "CMPBrowser.humanoid": "Humanoid", + "CMPBrowser.monstrosity": "Monstrosity", + "CMPBrowser.ooze": "Ooze", + "CMPBrowser.plant": "Plant", + "CMPBrowser.undead": "Undead", + "CMPBrowser.abilities": "Fähigkeiten", + "CMPBrowser.dmgInteraction": "Damage Interaction", + "CMPBrowser.dmgDealt": "Damage Dealt", + "CMPBrowser.size": "Größe", + "CMPBrowser.Tab.SpellBrowser":"Zauber Browser", + "CMPBrowser.Tab.FeatBrowser": "Feat Browser", + "CMPBrowser.Tab.ItemBrowser": "Gegenstand Browser", + "CMPBrowser.Tab.NPCBrowser":"NPC Browser", + "CMPBrowser.Tab.Settings":"Einstellungen", + "CMPBrowser.SETTING.Maxload.NAME" : "Maximum load", + "CMPBrowser.SETTING.Maxload.HINT" : "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", + "CMPBrowser.LOADING.Message" : "Loaded...{numLoaded} {itemType}s", + "CMPBrowser.LOADING.MaxLoaded" : "(maximum displayed; to see more, use the filters)", + "CMPBrowser.Filters.ResetFilters" : "Reset Filters" + +} \ No newline at end of file diff --git a/module.json b/module.json index d89e4f7..0a45ba0 100644 --- a/module.json +++ b/module.json @@ -1,51 +1,66 @@ { - "name": "compendium-browser", - "title": "Compendium Browser", - "description": "

Easily browse and filter spells, feats, items, and npcs loaded from compendia!

NEW! Compendium Browser is faster and better-behaved; it no longer loads all the compendia into memory on start-up (which sometimes hung servers because of memory or CPU requirements). Instead, it filters and loads on-demand, as well as giving you a Module Setting to control how many rows are loaded at a time.
Changes in v0.5.0:Fixed: Issue #17: Error in Foundry 0.8.x when filtering NPC by Creature Type
Changes in v0.4.5:", - "version": "0.5.0", - "author": "Spetzel#0103", - "authors": [ - { - "name": "Spetzel#0103", - "url": "https://github.com/spetzel2020" - }, - { - "name": "Felix#6196" - } - ], - "systems": ["dnd5e"], - "scripts": ["./compendium-browser.js"], - "styles": ["./compendium-browser.css"], - "packs": [], - "languages": [ - { - "lang": "en", - "name": "English", - "path": "lang/en.json" - }, - { - "lang": "ja", - "name": "Japanese", - "path": "lang/ja.json" - }, - { - "lang": "fr", - "name": "French (FRANCE)", - "path": "lang/fr.json" - }, - { - "lang": "pt-BR", - "name": "Português (Brasil)", - "path": "lang/pt-BR.json" - } - ], - "url": "https://github.com/League-of-Foundry-Developers/compendium-browser", - "manifest": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/latest/download/module.json", - "download": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/download/v0.5.0/compendium-browser.zip", - "minimumCoreVersion": "0.6.2", - "compatibleCoreVersion": "0.8.6", - "allowBugReporter": true, - "bugs": "https://github.com/League-of-Foundry-Developers/compendium-browser/issues", - "readme": "https://github.com/League-of-Foundry-Developers/compendium-browser/blob/master/README.md", - "changelog": "https://github.com/League-of-Foundry-Developers/compendium-browser/blob/master/CHANGELOG.md" -} + "name": "compendium-browser", + "title": "Compendium Browser", + "description": "

Easily browse and filter spells, feats, items, and npcs loaded from compendia!

NEW! Compendium Browser is faster and better-behaved; it no longer loads all the compendia into memory on start-up (which sometimes hung servers because of memory or CPU requirements). Instead, it filters and loads on-demand, as well as giving you a Module Setting to control how many rows are loaded at a time.
Changes in v0.5.0:Fixed: Issue #17: Error in Foundry 0.8.x when filtering NPC by Creature Type
Changes in v0.4.5:", + "version": "0.5.2", + "minimumCoreVersion": "0.6.2", + "compatibleCoreVersion": "0.8.8", + "allowBugReporter": true, + "author": "Spetzel#0103", + "authors": [ + { + "name": "Spetzel#0103", + "url": "https://github.com/spetzel2020" + }, + { + "name": "Felix#6196" + }, + { + "name": "JackPrince#0494", + "url": "https://github.com/DanielBoettner/" + } + ], + "systems": [ + "dnd5e" + ], + "includes": [ + "./hooks/**", + "./scripts/**" + ], + "esmodules": [ + "/compendium-browser.js" + ], + "styles": [ + "./styles/compendium-browser.css", + "./styles/compendium-browser.less" + ], + "packs": [], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + }, + { + "lang": "ja", + "name": "Japanese", + "path": "lang/ja.json" + }, + { + "lang": "fr", + "name": "French (FRANCE)", + "path": "lang/fr.json" + }, + { + "lang": "pt-BR", + "name": "Português (Brasil)", + "path": "lang/pt-BR.json" + } + ], + "url": "https://github.com/League-of-Foundry-Developers/compendium-browser", + "manifest": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/latest/download/module.json", + "download": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/download/v0.5.2/compendium-browser.zip", + "bugs": "https://github.com/League-of-Foundry-Developers/compendium-browser/issues", + "readme": "https://github.com/League-of-Foundry-Developers/compendium-browser/blob/master/README.md", + "changelog": "https://github.com/League-of-Foundry-Developers/compendium-browser/blob/master/CHANGELOG.md" +} \ No newline at end of file diff --git a/scripts/compendium-browser.js b/scripts/compendium-browser.js new file mode 100644 index 0000000..aa216c5 --- /dev/null +++ b/scripts/compendium-browser.js @@ -0,0 +1,620 @@ +import { ModuleSettings , CMPBrowser } from './modules/settings.mjs'; +import Exporter from './modules/exporter.mjs'; +import Entities from './modules/entities.mjs'; +import { Filter } from './modules/filter.mjs'; + +//import Exporter from './modules/exporter.mjs'; +/* eslint-disable valid-jsdoc */ +/* eslint-disable complexity */ + +export class CompendiumBrowser extends Application { + + constructor() { + super(); + let moduleSettings = new ModuleSettings(); + this.settings = moduleSettings.initSettings(); + + this.filters = new Filter(); + + this.currentLists = {}; + } + + /** + * + */ + static get defaultOptions() { + const options = super.defaultOptions; + mergeObject(options, { + title: "CMPBrowser.compendiumBrowser", + tabs: [{ navSelector: ".tabs", contentSelector: ".content", initial: "spell" }], + classes: options.classes.concat('compendium-browser'), + template: "modules/compendium-browser/template/template.html", + width: 800, + height: 700, + resizable: true, + minimizable: true + }); + return options; + } + + async initialize() { + // load settings + if (this.settings === undefined) { + ModuleSettings.initSettings(); + } + + await loadTemplates([ + "modules/compendium-browser/template/spell-browser.html", + "modules/compendium-browser/template/npc-browser.html", + "modules/compendium-browser/template/feat-browser.html", + "modules/compendium-browser/template/item-browser.html", + "modules/compendium-browser/template/entity-browser-list.html", + "modules/compendium-browser/template/filter-container.html", + "modules/compendium-browser/template/settings.html", + "modules/compendium-browser/template/loading.html" + ]); + + this.hookCompendiumList(); + } + + + /** override */ + _onChangeTab(event, tabs, active) { + super._onChangeTab(event, tabs, active); + const html = this.element; + this.replaceList(html, active, { reload: false }) + } + + /** + * + * @returns {Obejct} data + */ + async getData() { + + //0.4.1 Filter as we load to support new way of filtering + //Previously loaded all data and filtered in place; now loads minimal (preload) amount, filtered as we go + //First time (when you press Compendium Browser button) is called with filters unset + + //0.4.1k: Don't do any item/npc loading until tab is visible + let data = { + items: [], + npcs: [], + spellFilters: this.filters.getByName('spellFilters'), + showSpellBrowser: (game.user.isGM || this.settings.allowSpellBrowser), + featFilters: this.filters.getByName('featFilters'), + showFeatBrowser: (game.user.isGM || this.settings.allowFeatBrowser), + itemFilters: this.filters.getByName('itemFilters'), + showItemBrowser: (game.user.isGM || this.settings.allowItemBrowser), + npcFilters: this.filters.getByName('npcFilters'), + showNpcBrowser: (game.user.isGM || this.settings.allowNpcBrowser), + settings: this.settings, + isGM: game.user.isGM + }; + + + return data; + } + + activateItemListListeners(html) { + // show entity sheet + html.find('*[data-action="openSheet"]').click(async e => { + let itemId = e.currentTarget.parentNode.dataset.entryId; + let compendium = e.currentTarget.parentNode.dataset.entryCompendium; + let pack = game.packs.find(p => p.collection === compendium); + await pack.getEntity(itemId).then(entity => { + entity.sheet.render(true); + }); + }); + + // make draggable + //0.4.1: Avoid the game.packs lookup + html.find('.draggable').each((i, li) => { + li.setAttribute("draggable", true); + li.addEventListener('dragstart', event => { + let packName = li.getAttribute("data-entry-compendium"); + let pack = game.packs.find(p => p.collection === packName); + if (!pack) { + event.preventDefault(); + return false; + } + event.dataTransfer.setData("text/plain", JSON.stringify({ + type: pack.entity, + pack: pack.collection, + id: li.getAttribute("data-entry-id") + })); + }, false); + }); + } + + /** override */ + activateListeners(html) { + super.activateListeners(html); + + this.observer = new IntersectionObserver((entries, observer) => { + for (let e of entries) { + if (!e.isIntersecting) continue; + const img = e.target; + // Avatar image + //const img = li.querySelector("img"); + if (img && img.dataset.src) { + img.src = img.dataset.src; + delete img.dataset.src; + } + + // No longer observe the target + observer.unobserve(e.target); + } + }); + + this.activateItemListListeners(html); + + // toggle visibility of filter containers + html.find('.filtercontainer h3, .multiselect label').click(async ev => { + await $(ev.target.nextElementSibling).toggle(100); + }); + + html.find('.multiselect label').trigger('click'); + + // reset filters and re-rendes + html.find('button[data-action="reset-filters"]').click(ev => { + this.filters.resetFilters(); + this.refreshList = ev.target.closest('.tab').dataset.tab; + this.render(); + }); + + html.find('button[data-action="export"]').click(e => { + let items = document.querySelectorAll('.content .tab.active .cb_entities .item'), + tableItems = []; + + items.forEach(item => { + let obj = {}; + Object.assign(obj, item.dataset); + tableItems.push(obj); + }); + + Exporter.createTableFromSelection('testtable',tableItems); + }); + + // settings + html.find('.settings input').on('change', e => { + let setting = e.target.dataset.setting, + value = e.target.checked, + key = e.target.dataset.key, + category = (e.dataset.type === 'npc')? 'Actor' : 'Item'; + + this.settings.loadedCompendium[category][key].load = value; + this.render(); + + ui.notifications.info("Settings Saved. Compendiums are being reloaded."); + + if (setting === 'allow-spell-browser') { + this.settings.allowSpellBrowser = value; + } + if (setting === 'allow-feat-browser') { + this.settings.allowFeatBrowser = value; + } + if (setting === 'allow-item-browser') { + this.settings.allowItemBrowser = value; + } + if (setting === 'allow-npc-browser') { + this.settings.allowNpcBrowser = value; + } + ModuleSettings.saveSettings(); + }); + + + // activating or deactivating filters + //0.4.1: Now does a re-load and updates just the data side + // text filters + html.find('.filter[data-type=text] input, .filter[data-type=text] select').on('keyup change paste', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const value = ev.target.value; + const entityType = $(ev.target).parents('.tab').data('tab'); + + const filterTarget = `${entityType}Filters`; + + if (value === '' || value === undefined) { + delete this[filterTarget].activeFilters[key]; + } else { + this[filterTarget].activeFilters[key] = { + path: path, + type: 'text', + valIsArray: false, + value: ev.target.value + } + } + + this.replaceList(html, entityType); + }); + + // select filters + html.find('.filter[data-type=select] select, .filter[data-type=bool] select').on('change', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const filterType = $(ev.target).parents('.filter').data('type'); + const browserTab = $(ev.target).parents('.tab').data('tab'); + let valIsArray = $(ev.target).parents('.filter').data('valisarray'); + if (valIsArray === 'true') valIsArray = true; + let value = ev.target.value; + if (value === 'false') value = false; + if (value === 'true') value = true; + + const filterTarget = `${browserTab}Filters`; + + if (value === "null") { + delete this.filters[filterTarget].activeFilters[key]; + } else { + this.filters[filterTarget].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + value: value + } + } + this.replaceList(html, browserTab); + }); + + // multiselect filters + html.find('.filter[data-type=multiSelect] input').on('change', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const filterType = 'multiSelect'; + const browserTab = $(ev.target).parents('.tab').data('tab'); + let valIsArray = $(ev.target).parents('.filter').data('valisarray'); + if (valIsArray === 'true') valIsArray = true; + let value = $(ev.target).data('value'); + + const filterTarget = `${browserTab}Filters`; + const filter = this[filterTarget].activeFilters[key]; + + if (ev.target.checked === true) { + if (filter === undefined) { + this[filterTarget].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + values: [value] + } + } else { + this[filterTarget].activeFilters[key].values.push(value); + } + } else { + delete this[filterTarget].activeFilters[key].values.splice(this[filterTarget].activeFilters[key].values.indexOf(value), 1); + if (this[filterTarget].activeFilters[key].values.length === 0) { + delete this[filterTarget].activeFilters[key]; + } + } + + this.replaceList(html, browserTab); + }); + + + html.find('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').on('change keyup paste', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const filterType = 'numberCompare'; + const browserTab = $(ev.target).parents('.tab').data('tab'); + let valIsArray = false; + + const operator = $(ev.target).parents('.filter').find('select').val(); + const value = $(ev.target).parents('.filter').find('input').val(); + + const filterTarget = `${browserTab}Filters`; + + if (value === '' || operator === 'null') { + delete this[filterTarget].activeFilters[key] + } else { + this[filterTarget].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + operator: operator, + value: value + } + } + + this.replaceList(html, browserTab); + }); + + //Just for the loading image + if (this.observer) { + html.find("img").each((i, img) => this.observer.observe(img)); + } + } + + hookCompendiumList() { + Hooks.on('renderCompendiumDirectory', (app, html, data) => { + this.hookCompendiumList(); + }); + + let html = $('#compendium'); + if (this.settings === undefined) { + this.initSettings(); + } + if (game.user.isGM || this.settings.allowSpellBrowser || this.settings.allowNpcBrowser) { + const cbButton = $(``); + html.find('.compendium-browser-btn').remove(); + + // adding to directory-list since the footer doesn't exist if the user is not gm + html.find('.directory-footer').append(cbButton); + + // Handle button clicks + cbButton.click(ev => { + ev.preventDefault(); + //0.4.1: Reset filters when you click button + this.filters.resetFilters(); + //0.4.3: Reset everything (including data) when you press the button - calls afterRender() hook + + if (game.user.isGM || this.settings.allowSpellBrowser) { + this.refreshList = "spell"; + } else if (this.settings.allowFeatBrowser) { + this.refreshList = "feat"; + } else if (this.settings.allowItemBrowser) { + this.refreshList = "item"; + } else if (this.settings.allowNPCBrowser) { + this.refreshList = "npc"; + } + this.render(true); + }); + } + } + + + /* Hook to load the first data */ + static afterRender(cb, html) { + //0.4.3: Because a render always resets ALL the displayed filters (on all tabs) to unselected , we have to blank all the lists as well + // (because the current HTML template doesn't set the selected filter values) + if (!cb?.refreshList) { return; } + + cb.replaceList(html, cb.refreshList); + + cb.refreshList = null; + } + + async replaceList(html, browserTab, options = { reload: true }) { + //After rendering the first time or re-rendering trigger the load/reload of visible data + + let elements = null; + //0.4.2 Display a Loading... message while the data is being loaded and filtered + let loadingMessage = null; + if (browserTab === 'spell') { + elements = html.find("ul#CBSpells"); + loadingMessage = html.find("#CBSpellsMessage"); + } else if (browserTab === 'npc') { + elements = html.find("ul#CBNPCs"); + loadingMessage = html.find("#CBNpcsMessage"); + } else if (browserTab === 'feat') { + elements = html.find("ul#CBFeats"); + loadingMessage = html.find("#CBFeatsMessage"); + } else if (browserTab === 'item') { + elements = html.find("ul#CBItems"); + loadingMessage = html.find("#CBItemsMessage"); + } + if (elements?.length) { + //0.4.2b: On a tab-switch, only reload if there isn't any data already + if (options?.reload || !elements[0].children.length) { + + const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; + const updateLoading = async numLoaded => { + if (loadingMessage.length) { this.renderLoading(loadingMessage[0], browserTab, numLoaded, numLoaded >= maxLoad); } + }; + updateLoading(0); + + //Uses loadAndFilterItems to read compendia for items which pass the current filters and render on this tab + const newItemsHTML = await this.renderItemData(browserTab, updateLoading); + elements[0].innerHTML = newItemsHTML; + + //Lazy load images + if (this.observer) { + $(elements).find("img").each((i, img) => this.observer.observe(img)); + } + + //Reactivate listeners for clicking and dragging + this.activateItemListListeners($(elements)); + } + } + + } + + async renderLoading(messageElement, itemType, numLoaded, maxLoaded = false) { + if (!messageElement) return; + + let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.html", { numLoaded: numLoaded, itemType: itemType, maxLoaded: maxLoaded }); + messageElement.innerHTML = loadingHTML; + } + + async renderItemData(entityType, updateLoading = null) { + const entityHelper = new Entities(this.settings, this.filters); + let entityList = await entityHelper.loadAndFilter(entityType, updateLoading); + + this.currentLists[entityType] = Entities._sortList(entityList, entityType); + + const html = await renderTemplate(`modules/compendium-browser/template/entity-browser-list.html`, { listItems: this.currentLists[entityType] }); + + return html; + } + + //SORTING + triggerSort(html, entityType) { + html.find('.' + entityType + '-browser select[name=sortorder]').trigger('change'); + } + + _sortEntities(html, entityType) { + html.find('.' + entityType + '-browser select[name=sortorder]').on('change', ev => { + let entityList = html.find('.' + entityType + '-browser .cb_entities li'); + let byName = ev.target.value; + let sortedList = (entityType != 'npc')? this.sortEntity(entityList, entityType, byName) : this.sortNpcs(entityList, byName); + let ol = $(html.find('.' + entityType + '-browser .cb_entities')); + ol[0].innerHTML = []; + for (let element of sortedList) { + ol[0].append(element); + } + }); + this.triggerSort(html, entityType); + } + + decorateItem(item5e) { + if (!item5e) return null; + //Decorate and then filter a compendium entry - returns null or the item + const item = item5e.data; + + // getting damage types (common to all Items, although some won't have any) + item.damageTypes = []; + if (item.data.damage && item.data.damage.parts.length > 0) { + for (let part of item.data.damage.parts) { + let type = part[1]; + if (item.damageTypes.indexOf(type) === -1) { + item.damageTypes.push(type); + } + } + } + + if (item.type === 'spell') { + // determining classes that can use the spell + let cleanSpellName = item.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); + //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); + if (this.classList[cleanSpellName]) { + let classes = this.classList[cleanSpellName]; + item.data.classes = classes.split(','); + } else { + //FIXME: unfoundSpells += cleanSpellName + ','; + } + } else if (item.type === 'feat' || item.type === 'class') { + // getting class + let reqString = item.data.requirements?.replace(/[0-9]/g, '').trim(); + let matchedClass = []; + for (let c in this.subClasses) { + if (reqString && reqString.toLowerCase().indexOf(c) !== -1) { + matchedClass.push(c); + } else { + for (let subClass of this.subClasses[c]) { + if (reqString && reqString.indexOf(subClass) !== -1) { + matchedClass.push(c); + break; + } + } + } + } + item.classRequirement = matchedClass; + item.classRequirementString = matchedClass.join(', '); + + // getting uses/ressources status + item.usesRessources = item5e.hasLimitedUses; + item.hasSave = item5e.hasSave; + + } else { + // getting pack + let matchedPacks = []; + for (let pack of Object.keys(this.packList)) { + for (let packItem of this.packList[pack]) { + if (item.name.toLowerCase() === packItem.toLowerCase()) { + matchedPacks.push(pack); + break; + } + } + } + item.matchedPacks = matchedPacks; + item.matchedPacksString = matchedPacks.join(', '); + + // getting uses/ressources status + item.usesRessources = item5e.hasLimitedUses + } + return item; + } + + decorateNpc(npc) { + //console.log('%c '+npc.name, 'background: white; color: red') + const entityData = npc.data; + + + } + + filterElements(list, subjects, filters) { + for (let element of list) { + let subject = subjects[element.dataset.entryId]; + if (this.passesFilter(subject, filters) == false) { + $(element).hide(); + } else { + $(element).show(); + } + } + } + + passesFilter(subject, filters) { + for (let filter of Object.values(filters)) { + let prop = getProperty(subject, filter.path); + if (filter.type === 'numberCompare') { + + switch (filter.operator) { + case '=': if (prop != filter.value) { return false; } break; + case '<': if (prop >= filter.value) { return false; } break; + case '>': if (prop <= filter.value) { return false; } break; + } + + continue; + } + if (filter.valIsArray === false) { + if (filter.type === 'text') { + if (prop === undefined) return false; + if (prop.toLowerCase().indexOf(filter.value.toLowerCase()) === -1) { + return false; + } + } else { + if (filter.value !== undefined && prop !== undefined && prop != filter.value && !(filter.value === true && prop)) { + return false; + } + if (filter.values && filter.values.indexOf(prop) === -1) { + return false; + } + } + } else { + if (prop === undefined) return false; + if (typeof prop === 'object') { + if (filter.value) { + if (prop.indexOf(filter.value) === -1) { + return false; + } + } else if (filter.values) { + for (let val of filter.values) { + if (prop.indexOf(val) !== -1) { + continue; + } + return false; + } + } + } else { + for (let val of filter.values) { + if (prop === val) { + continue; + } + } + return false; + } + } + } + + return true; + } + + clearObject(obj) { + let newObj = {}; + for (let key in obj) { + if (obj[key] == true) { + newObj[key] = true; + } + } + return newObj; + } + + saveSettings() { + game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings); + } + + async addFilters(){ + await this.filters.addSpellFilters(); + await this.filters.addFeatFilters(); + await this.filters.addItemFilters(); + await this.filters.addNpcFilters(); + } +} \ No newline at end of file diff --git a/scripts/modules/entities.mjs b/scripts/modules/entities.mjs new file mode 100644 index 0000000..30a3717 --- /dev/null +++ b/scripts/modules/entities.mjs @@ -0,0 +1,502 @@ +import { Filter } from './filter.mjs'; +import { CMPBrowser } from './settings.mjs'; + +export default class Entities { + + constructor(settings, filters) { + this.settings = settings; + this.filters = filters; + } + + /** + * + * @param {String} entityType + * @param {*} updateLoading + * @returns + */ + async loadAndFilter(entityType = "spell", updateLoading = null) { + console.log(`Load and Filter Items | Started loading ${entityType}s`); + console.time("loadAndFilterItems"); + await this.checkListsLoaded(); + const Category = (entityType === 'npc') ? 'Actor' : 'Item'; + const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; + const ActiveFilters = this.filters.getByType(entityType).activeFilters; + const packs = game.packs.filter((pack) => pack.metadata.entity === Category); + //0.4.1: Load and filter just one of spells, feats, and items (specified by browserTab) + let unfoundSpells = ''; + let numItemsLoaded = 0; + let compactItems = []; + + //Filter the full list, but only save the core compendium information + displayed info + for (let pack of packs) { + if (this.settings.loadedCompendium[pack.metadata.entity][pack.collection].load) { + //FIXME: How much could we do with the loaded index rather than all content? + //OR filter the content up front for the decoratedItem.type?? + await pack.getContent().then(content => { + for (let item5e of content) { + let compactItem = null; + const decoratedEntity = Entities.decorateEntity(item5e, entityType, this.packList, this.classlist, this.subClasses); + if (decoratedEntity) { + switch (entityType) { + case "spell": + if (Filter.passesFilter(decoratedEntity, ActiveFilters)) { + compactItem = { + _id: decoratedEntity._id, + compendium: pack.collection, + name: decoratedEntity.name, + img: decoratedEntity.img, + type: decoratedEntity.type, + data: { + level: decoratedEntity.data?.level, + components: decoratedEntity.data?.components + } + }; + } + break; + + case "feat": + if (["feat", "class"].includes(decoratedEntity.type) && Filter.passesFilter(decoratedEntity, ActiveFilters)) { + compactItem = { + _id: decoratedEntity._id, + compendium: pack.collection, + name: decoratedEntity.name, + img: decoratedEntity.img, + type: decoratedEntity.type, + classRequirement: decoratedEntity.classRequirement + }; + } + break; + + case "item": + //0.4.5: Itm type for true items could be many things (weapon, consumable, etc) so we just look for everything except spells, feats, classes + if (!["spell", "feat", "class"].includes(decoratedEntity.type) && Filter.passesFilter(decoratedEntity, ActiveFilters)) { + compactItem = { + _id: decoratedEntity._id, + compendium: pack.collection, + name: decoratedEntity.name, + img: decoratedEntity.img, + type: decoratedEntity.type, + rarity: decoratedEntity.data?.rarity.toLowerCase().replace(/ /g, ''), + dae: decoratedEntity.effects.size || false, + ac: (decoratedEntity.data?.armor?.type) ? decoratedEntity.data?.armor?.value || false : false, + }; + + if (compactItem.type == 'weapon' && (decoratedEntity.data?.range?.value)) { + setProperty(compactItem, `tags.range`, decoratedEntity.data?.range?.value + decoratedEntity.data?.range?.units); + } + + for (let filter of Object.values(ActiveFilters)) { + setProperty(compactItem, `tags.${filter.path}`, filter.value); + } + } + break; + case "npc": + if (Filter.passesFilter(decoratedEntity, ActiveFilters)) { + //0.4.2: Don't store all the details - just the display elements + compactItem = { + _id: decoratedEntity._id, + compendium: pack.collection, + name: decoratedEntity.name, + img: decoratedEntity.img, + displayCR: decoratedEntity.displayCR, + displaySize: decoratedEntity.displaySize, + displayType: decoratedEntity.data?.details?.type, + orderCR: decoratedEntity.data.details.cr, + orderSize: decoratedEntity.filterSize + } + } + break; + default: + break; + } + + if (compactItem) { //Indicates it passed the filters + compactItems.push(compactItem); + if (numItemsLoaded++ >= maxLoad) break; + //0.4.2e: Update the UI (e.g. "Loading 142 spells") + if (updateLoading) { updateLoading(numItemsLoaded); } + } + } + }//for item5e of content + }); + }//end if pack entity === Item + if (numItemsLoaded >= maxLoad) break; + }//for packs + + /* + if (unfoundSpells !== '') { + console.log(`Load and Fliter Items | List of Spells that don't have a class associated to them:`); + console.log(unfoundSpells); + } + */ + this.itemsLoaded = true; + console.timeEnd("loadAndFilterItems"); + console.log(`Load and Filter Items | Finished loading ${compactItems.length} ${entityType}s`); + return compactItems; + } + + async loadEntity(numToPreload = CMPBrowser.PRELOAD) { + console.log('Item Browser | Started loading items'); + console.time("loadItems"); + await this.checkListsLoaded(); + + this.itemsLoaded = false; + + + let unfoundSpells = ''; + let numSpellsLoaded = 0; + let numFeatsLoaded = 0; + let numItemsLoaded = 0; + let items = { + spells: {}, + feats: {}, + items: {} + }; + + + for (let pack of game.packs) { + if (pack['metadata']['entity'] === "Item" && this.settings.loadedSpellCompendium[pack.collection].load) { + await pack.getContent().then(content => { + for (let item5e of content) { + let item = item5e.data; + if (item.type === 'spell') { + //0.4.1 Only preload a limited number and fill more in as needed + if (numSpellsLoaded++ > numToPreload) continue; + + item.compendium = pack.collection; + + // determining classes that can use the spell + let cleanSpellName = item.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); + //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); + if (this.classList[cleanSpellName]) { + let classes = this.classList[cleanSpellName]; + item.data.classes = classes.split(','); + } else { + unfoundSpells += cleanSpellName + ','; + } + + // getting damage types + item.damageTypes = []; + if (item.data.damage && item.data.damage.parts.length > 0) { + for (let part of item.data.damage.parts) { + let type = part[1]; + if (item.damageTypes.indexOf(type) === -1) { + item.damageTypes.push(type); + } + } + } + items.spells[(item._id)] = item; + } else if (item.type === 'feat' || item.type === 'class') { + //0.4.1 Only preload a limited number and fill more in as needed + if (numFeatsLoaded++ > numToPreload) continue; + + item.compendium = pack.collection; + // getting damage types + item.damageTypes = []; + if (item.data.damage && item.data.damage.parts.length > 0) { + for (let part of item.data.damage.parts) { + let type = part[1]; + if (item.damageTypes.indexOf(type) === -1) { + item.damageTypes.push(type); + } + } + } + + // getting class + let reqString = item.data.requirements?.replace(/[0-9]/g, '').trim(); + let matchedClass = []; + for (let c in this.subClasses) { + if (reqString && reqString.toLowerCase().indexOf(c) !== -1) { + matchedClass.push(c); + } else { + for (let subClass of this.subClasses[c]) { + if (reqString && reqString.indexOf(subClass) !== -1) { + matchedClass.push(c); + break; + } + } + } + } + item.classRequirement = matchedClass; + item.classRequirementString = matchedClass.join(', '); + + // getting uses/ressources status + item.usesRessources = item5e.hasLimitedUses; + + item.hasSave = item5e.hasSave; + + items.feats[(item._id)] = item; + + } else { + //0.4.1 Only preload a limited number and fill more in as needed + if (numItemsLoaded++ > numToPreload) continue; + + item.compendium = pack.collection; + // getting damage types + item.damageTypes = []; + if (item.data.damage && item.data.damage.parts.size > 0) { + for (let part of item.data.damage.parts) { + let type = part[1]; + if (item.damageTypes.indexOf(type) === -1) { + item.damageTypes.push(type); + } + } + } + + // getting pack + let matchedPacks = []; + for (let pack of Object.keys(this.packList)) { + for (let packItem of this.packList[pack]) { + if (item.name.toLowerCase() === packItem.toLowerCase()) { + matchedPacks.push(pack); + break; + } + } + } + item.matchedPacks = matchedPacks; + item.matchedPacksString = matchedPacks.join(', '); + + // getting uses/ressources status + item.usesRessources = item5e.hasLimitedUses + + items.items[(item._id)] = item; + } + + }//for item5e of content + }); + } + if ((numSpellsLoaded >= numToPreload) && (numFeatsLoaded >= numToPreload) && (numItemsLoaded >= numToPreload)) break; + }//for packs + if (unfoundSpells !== '') { + console.log(`Item Browser | List of Spells that don't have a class associated to them:`); + console.log(unfoundSpells); + } + this.itemsLoaded = true; + console.timeEnd("loadItems"); + console.log(`Item Browser | Finished loading items: ${Object.keys(items.spells).length} spells, ${Object.keys(items.feats).length} features, ${Object.keys(items.items).length} items `); + return items; + } + + static _sortList(list, entityType, orderBy) { + if(entityType == 'npc'){ + switch (orderBy) { + case 'name': + list.sort((a, b) => { + let aName = a.name; + let bName = b.name; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + }); break; + case 'cr': + list.sort((a, b) => { + let aVal = Number(a.orderCR); + let bVal = Number(b.orderCR); + if (aVal < bVal) return -1; + if (aVal > bVal) return 1; + if (aVal == bVal) { + let aName = a.name; + let bName = b.name; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + } + }); break; + case 'size': + list.sort((a, b) => { + let aVal = a.orderSize; + let bVal = b.orderSize; + if (aVal < bVal) return -1; + if (aVal > bVal) return 1; + if (aVal == bVal) { + let aName = a.name; + let bName = b.name; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + } + }); break; + } + } else { + if (orderBy) { + list.sort((a, b) => { + let aName = a.name; + let bName = b.name; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + }); + } else { + let defaultSort = new Map([ + ['spell', 'level'], + ['feats', 'class'], + ['items', 'type'], + ]); + + list.sort((a, b) => { + let sort = defaultSort.get(entityType); + let aVal = a[sort]; + let bVal = b[sort]; + if (aVal < bVal) return -1; + if (aVal > bVal) return 1; + if (aVal == bVal) { + let aName = a.name; + let bName = b.name; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + } + }); + } + } + return list; + } + + /** + * + * @param {item5e|Object}entity + * @param {Object|Iterable} packList + * @param {Object|Iterable} classList + * @param {Object|Iterable} subClasses + * + * @returns {Object} decoratedItem + */ + static decorateEntity(entity, entityType, packList = {}, classList = {}, subClasses = {}) { + if (!entity) return null; + //Decorate and then filter a compendium entry - returns null or the item + const entityData = entity.data; + + if (entityType === 'npc') { + // cr display + let cr = entityData.data.details.cr; + if (cr == undefined || cr == '') cr = 0; + else cr = Number(cr); + if (cr > 0 && cr < 1) cr = "1/" + (1 / cr); + entityData.displayCR = cr; + entityData.displaySize = 'unset'; + entityData.filterSize = 2; + if (CONFIG.DND5E.actorSizes[entityData.data.traits.size] !== undefined) { + entityData.displaySize = CONFIG.DND5E.actorSizes[entityData.data.traits.size]; + } + switch (entityData.data.traits.size) { + case 'grg': entityData.filterSize = 5; break; + case 'huge': entityData.filterSize = 4; break; + case 'lg': entityData.filterSize = 3; break; + case 'sm': entityData.filterSize = 1; break; + case 'tiny': entityData.filterSize = 0; break; + case 'med': + default: entityData.filterSize = 2; break; + } + + // getting value for HasSpells and damage types + entityData.hasSpells = false; + entityData.damageDealt = []; + for (let item of entityData.items) { + if (item.type == 'spell') { + entityData.hasSpells = true; + } + if (item.data.damage && item.data.damage.parts && item.data.damage.parts.length > 0) { + for (let part of item.data.damage.parts) { + let type = part[1]; + if (entityData.damageDealt.indexOf(type) === -1) { + entityData.damageDealt.push(type); + } + } + } + } + } else { + // getting damage types (common to all Items, although some won't have any) + entityData.damageTypes = []; + if (entityData.data.damage && entityData.data.damage.parts.length > 0) { + for (let part of entityData.data.damage.parts) { + let type = part[1]; + if (entityData.damageTypes.indexOf(type) === -1) { + entityData.damageTypes.push(type); + } + } + } + + if (entityData.type === 'spell') { + // determining classes that can use the spell + let cleanSpellName = entityData.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); + //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); + if (classList[cleanSpellName]) { + let classes = classList[cleanSpellName]; + entityData.data.classes = classes.split(','); + } else { + //FIXME: unfoundSpells += cleanSpellName + ','; + } + } else if (entityData.type === 'feat' || entityData.type === 'class') { + // getting class + let reqString = entityData.data.requirements?.replace(/[0-9]/g, '').trim(); + let matchedClass = []; + for (let c in subClasses) { + if (reqString && reqString.toLowerCase().indexOf(c) !== -1) { + matchedClass.push(c); + } else { + for (let subClass of subClasses[c]) { + if (reqString && reqString.indexOf(subClass) !== -1) { + matchedClass.push(c); + break; + } + } + } + } + entityData.classRequirement = matchedClass; + entityData.classRequirementString = matchedClass.join(', '); + + // getting uses/ressources status + entityData.usesRessources = entity.hasLimitedUses; + entityData.hasSave = entity.hasSave; + + } else { + // getting pack + let matchedPacks = []; + for (let pack of Object.keys(packList)) { + for (let packItem of packList[pack]) { + if (entityData.name.toLowerCase() === packItem.toLowerCase()) { + matchedPacks.push(pack); + break; + } + } + } + entityData.matchedPacks = matchedPacks; + entityData.matchedPacksString = matchedPacks.join(', '); + + // getting uses/ressources status + entityData.usesRessources = entity.hasLimitedUses; + } + } + + return entityData; + + } + + async checkListsLoaded() { + const dataPath = '/modules/compendium-browser/data/'; + //Provides extra info not in the standard SRD, like which classes can learn a spell + if (!this.classList) { + this.classList = await fetch(dataPath + 'spell-classes.json').then(result => { + return result.json(); + }).then(obj => { + return this.classList = obj; + }); + } + + if (!this.packList) { + this.packList = await fetch(dataPath + 'item-packs.json').then(result => { + return result.json(); + }).then(obj => { + return this.packList = obj; + }); + } + + if (!this.subClasses) { + this.subClasses = await fetch(dataPath + 'sub-classes.json').then(result => { + return result.json(); + }).then(obj => { + return this.subClasses = obj; + }); + } + } +} \ No newline at end of file diff --git a/scripts/modules/exporter.mjs b/scripts/modules/exporter.mjs new file mode 100644 index 0000000..d5b0e89 --- /dev/null +++ b/scripts/modules/exporter.mjs @@ -0,0 +1,46 @@ +export default class Exporter { + + /** + * Create a new RollTable by extracting entries from a compendium. + * + * @param {string} tableName the name of the table entity that will be created + * @param {string} compendiumName the name of the compendium to use for the table generation + * @param {function(Entity)} weightPredicate a function that returns a weight (number) that will be used + * for the tableResult weight for that given entity. returning 0 will exclude the entity from appearing in the table + */ + static async createTableFromSelection(tableName, itemSubset, { weightPredicate = null } = {}) { + let data = { name: tableName }, + tableArray = []; + + if (itemSubset && (itemSubset.length > 0)) { + const newTable = await RollTable.create(data); + + ui.notifications.info(`Starting generation of a rolltable with ${itemSubset.length} entries.`); + + for (let item of itemSubset) { + let weight = 1; + if (weightPredicate) { + weight = weightPredicate(item); + } + if (weight == 0) continue; + + let tableEntryData = {}; + tableEntryData.type = 2; + tableEntryData.collection = item.entryCompendium; + tableEntryData.text = item.entryName; + tableEntryData.img = item.entryImage; + tableEntryData.weight = weight; + tableEntryData.range = [1, 1]; + + tableArray.push(tableEntryData); + } + + await newTable.createEmbeddedDocuments("TableResult", tableArray); + await newTable.normalize(); + + ui.notifications.info(`Rolltable ${tableName} with ${tableArray.length} entries was generated.`); + } else { + ui.notifications.warn(`Compendium named ${item.packName} not found`); + } + } +} \ No newline at end of file diff --git a/scripts/modules/filter.mjs b/scripts/modules/filter.mjs new file mode 100644 index 0000000..e05d3ee --- /dev/null +++ b/scripts/modules/filter.mjs @@ -0,0 +1,294 @@ +export class Filter { + + constructor() { + //Reset the filters used in the dialog + this.spellFilters = this._getInitialFilters(); + this.npcFilters = this._getInitialFilters(); + this.featFilters = this._getInitialFilters(); + this.itemFilters = this._getInitialFilters(); + } + + /** + * + * @param {Object} subject + * @param {Obj} filters + * @returns + */ + static passesFilter(subject, filters) { + for (let filter of Object.values(filters)) { + let prop = getProperty(subject, filter.path); + if (filter.type === 'numberCompare') { + + switch (filter.operator) { + case '=': if (prop != filter.value) { return false; } break; + case '<': if (prop >= filter.value) { return false; } break; + case '>': if (prop <= filter.value) { return false; } break; + } + + continue; + } + if (filter.valIsArray === false) { + if (filter.type === 'text') { + if (prop === undefined) return false; + if (prop.toLowerCase().indexOf(filter.value.toLowerCase()) === -1) { + return false; + } + } else { + if (filter.value !== undefined && prop !== undefined && prop != filter.value && !(filter.value === true && prop)) { + return false; + } + if (filter.values && filter.values.indexOf(prop) === -1) { + return false; + } + } + } else { + if (prop === undefined) return false; + if (typeof prop === 'object') { + if (filter.value) { + if (prop.indexOf(filter.value) === -1) { + return false; + } + } else if (filter.values) { + for (let val of filter.values) { + if (prop.indexOf(val) !== -1) { + continue; + } + return false; + } + } + } else { + for (let val of filter.values) { + if (prop === val) { + continue; + } + } + return false; + } + } + } + + return true; + } + + getByName(name) { + return getProperty(this, name); + } + + getByType(type){ + return getProperty(this, type+'Filters'); + } + + resetFilters() { + this.spellFilters.activeFilters = {}; + this.featFilters.activeFilters = {}; + this.itemFilters.activeFilters = {}; + this.npcFilters.activeFilters = {}; + } + + _getInitialFilters() { + return { + registeredFilterCategorys: {}, + activeFilters: {} + }; + } + + /** + * Used to add custom filters to the Spell-Browser + * @param {String} entityType type of entity for the filter + * @param {String} category - Title of the category + * @param {String} label - Title of the filter + * @param {String} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value + * @param {String} type - type of filter + * possible filter: + * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching + * bool: will see if the data at the path exists and not false. + * select: exactly matches the data with the chosen selector from possibleValues + * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data + * numberCompare: gives the option to compare numerical values, either with =, < or the > operator + * @param {Boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters + * @param {Boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden + */ + async addFilter(entityType, category, label, path, type, possibleValues = null, valIsArray = false) { + let target = `${entityType}Filters`; + let filter = {}; + filter.path = path; + filter.label = label; + filter.type = 'text'; + if (['text', 'bool', 'select', 'multiSelect', 'numberCompare'].indexOf(type) !== -1) { + filter[`is${type}`] = true; + filter.type = type; + } + if (possibleValues !== null) { + filter.possibleValues = possibleValues; + } + filter.valIsArray = valIsArray; + + let catId = category.replace(/\W/g, ''); + if (this[target].registeredFilterCategorys[catId] === undefined) { + this[target].registeredFilterCategorys[catId] = { label: category, filters: [] }; + } + this[target].registeredFilterCategorys[catId].filters.push(filter); + + } + + async addSpellFilters() { + const SPELL = 'spell'; + // Spellfilters + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.lvl"), 'data.level', 'multiSelect', [game.i18n.localize("CMPBrowser.cantip"), 1, 2, 3, 4, 5, 6, 7, 8, 9]); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.school"), 'data.school', 'select', CONFIG.DND5E.spellSchools); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.castingTime"), 'data.activation.type', 'select', + { + action: game.i18n.localize("DND5E.Action"), + bonus: game.i18n.localize("CMPBrowser.bonusAction"), + reaction: game.i18n.localize("CMPBrowser.reaction"), + minute: game.i18n.localize("DND5E.TimeMinute"), + hour: game.i18n.localize("DND5E.TimeHour"), + day: game.i18n.localize("DND5E.TimeDay") + } + ); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.spellType"), 'data.actionType', 'select', CONFIG.DND5E.itemActionTypes); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'data.classes', 'select', + { + artificer: game.i18n.localize("CMPBrowser.artificer"), + bard: game.i18n.localize("CMPBrowser.bard"), + cleric: game.i18n.localize("CMPBrowser.cleric"), + druid: game.i18n.localize("CMPBrowser.druid"), + paladin: game.i18n.localize("CMPBrowser.paladin"), + ranger: game.i18n.localize("CMPBrowser.ranger"), + sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), + warlock: game.i18n.localize("CMPBrowser.warlock"), + wizard: game.i18n.localize("CMPBrowser.wizard"), + }, true + ); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.ritual"), 'data.components.ritual', 'bool'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.concentration"), 'data.components.concentration', 'bool'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.verbal"), 'data.components.vocal', 'bool'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.somatic"), 'data.components.somatic', 'bool'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.material"), 'data.components.material', 'bool'); + } + + async addItemFilters() { + let ITEM = 'item'; + // Item Filters + + await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); + await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), "Item Type", 'type', 'select', { + consumable: game.i18n.localize("DND5E.ItemTypeConsumable"), + backpack: game.i18n.localize("DND5E.ItemTypeContainer"), + equipment: game.i18n.localize("DND5E.ItemTypeEquipment"), + loot: game.i18n.localize("DND5E.ItemTypeLoot"), + tool: game.i18n.localize("DND5E.ItemTypeTool"), + weapon: game.i18n.localize("DND5E.ItemTypeWeapon") + }); + await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), "Packs", 'matchedPacks', 'select', + { + burglar: "Burglar's Pack", + diplomat: "Diplomat's Pack", + dungeoneer: "Dungeoneer's Pack", + entertainer: "Entertainer's Pack", + explorer: "Explorer's Pack", + monsterhunter: "Monster Hunter's Pack", + priest: "Priest's Pack", + scholar: "Scholar's Pack", + }, true + ); + await this.addFilter(ITEM, "Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); + await this.addFilter(ITEM, "Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); + await this.addFilter(ITEM, "Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); + + await this.addFilter(ITEM, "Item Subtype", "Weapon", 'data.weaponType', 'text', CONFIG.DND5E.weaponTypes); + await this.addFilter(ITEM, "Item Subtype", "Equipment", 'data.armor.type', 'text', CONFIG.DND5E.equipmentTypes); + await this.addFilter(ITEM, "Item Subtype", "Consumable", 'data.consumableType', 'text', CONFIG.DND5E.consumableTypes); + + await this.addFilter(ITEM, "Magic Items", "Rarity", 'data.rarity', 'select', + { + Common: "Common", + Uncommon: "Uncommon", + Rare: "Rare", + "Very rare": "Very Rare", + Legendary: "Legendary" + }); + } + + async addFeatFilters() { + const FEAT = 'feat'; + // Feature Filters + + this.addFilter(FEAT, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); + this.addFilter(FEAT,game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'classRequirement', 'select', + { + artificer: game.i18n.localize("CMPBrowser.artificer"), + barbarian: "Barbarian", + bard: game.i18n.localize("CMPBrowser.bard"), + cleric: game.i18n.localize("CMPBrowser.cleric"), + druid: game.i18n.localize("CMPBrowser.druid"), + fighter: "Fighter", + monk: "Monk", + paladin: game.i18n.localize("CMPBrowser.paladin"), + ranger: game.i18n.localize("CMPBrowser.ranger"), + rogue: "Rogue", + sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), + warlock: game.i18n.localize("CMPBrowser.warlock"), + wizard: game.i18n.localize("CMPBrowser.wizard") + }, true); + + this.addFilter(FEAT,"Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); + this.addFilter(FEAT,"Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); + this.addFilter(FEAT,"Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); + + } + + async addNpcFilters() { + const isFoundryV8 = game.data.version.startsWith("0.8"), + NPC = 'npc'; + // NPC Filters + + this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.details.source', 'text'); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.size"), 'data.traits.size', 'select', CONFIG.DND5E.actorSizes); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasSpells"), 'hasSpells', 'bool'); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegAct"), 'data.resources.legact.max', 'bool'); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegRes"), 'data.resources.legres.max', 'bool'); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.cr"), 'data.details.cr', 'numberCompare'); + + //Foundry 0.8.x: Creature type (data.details.type) is now a structure, so we check data.details.types.value instead + let npcDetailsPath; + if (isFoundryV8) { + npcDetailsPath = "data.details.type.value"; + } else {//0.7.x + npcDetailsPath = "data.details.type"; + } + + this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.creatureType"), npcDetailsPath, 'text', { + aberration: game.i18n.localize("CMPBrowser.aberration"), + beast: game.i18n.localize("CMPBrowser.beast"), + celestial: game.i18n.localize("CMPBrowser.celestial"), + construct: game.i18n.localize("CMPBrowser.construct"), + dragon: game.i18n.localize("CMPBrowser.dragon"), + elemental: game.i18n.localize("CMPBrowser.elemental"), + fey: game.i18n.localize("CMPBrowser.fey"), + fiend: game.i18n.localize("CMPBrowser.fiend"), + giant: game.i18n.localize("CMPBrowser.giant"), + humanoid: game.i18n.localize("CMPBrowser.humanoid"), + monstrosity: game.i18n.localize("CMPBrowser.monstrosity"), + ooze: game.i18n.localize("CMPBrowser.ooze"), + plant: game.i18n.localize("CMPBrowser.plant"), + undead: game.i18n.localize("CMPBrowser.undead") + }); + + this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityStr"), 'data.abilities.str.value', 'numberCompare'); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityDex"), 'data.abilities.dex.value', 'numberCompare'); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCon"), 'data.abilities.con.value', 'numberCompare'); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityInt"), 'data.abilities.int.value', 'numberCompare'); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityWis"), 'data.abilities.wis.value', 'numberCompare'); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCha"), 'data.abilities.cha.value', 'numberCompare'); + + this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamImm"), 'data.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamRes"), 'data.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamVuln"), 'data.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.ConImm"), 'data.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true); + this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("CMPBrowser.dmgDealt"), 'damageDealt', 'multiSelect', CONFIG.DND5E.damageTypes, true); + + } +} \ No newline at end of file diff --git a/scripts/modules/settings.mjs b/scripts/modules/settings.mjs new file mode 100644 index 0000000..855ba10 --- /dev/null +++ b/scripts/modules/settings.mjs @@ -0,0 +1,100 @@ +export const CMPBrowser = { + MODULE_NAME: "compendium-browser", + MODULE_VERSION: "0.5.1", + MAXLOAD: 500, //Default for the maximum number to load before displaying a message that you need to filter to see more +}; + +const SETTINGS = 'settings'; + +export class ModuleSettings { + + constructor() { + this.gs = game.settings; + } + + /** + * constructs and returns defaults settings + */ + static _getDefaults() { + let defaultSettings = { + loadedCompendium: { + Actor: {}, + Item: {} + } + }; + + for (let compendium of game.packs) { + if(defaultSettings.loadedCompendium[compendium.metadata.entity]){ + defaultSettings.loadedCompendium[compendium.metadata.entity][compendium.collection] = { + load: true, + name: `${compendium.metadata.label} (${compendium.collection})` + }; + } + } + + return defaultSettings; + } + + /** + * + * @returns {Array} Settings + */ + initSettings() { + let defaultSettings = ModuleSettings._getDefaults(); + + // creating game setting container + this.gs.register(CMPBrowser.MODULE_NAME, SETTINGS, { + name: "Compendium Browser Settings", + hint: "Settings to exclude packs from loading and visibility of the browser", + default: defaultSettings, + type: Object, + scope: 'world', + onChange: settings => { + this.settings = settings; + } + }); + + this.gs.register(CMPBrowser.MODULE_NAME, "maxload", { + name: game.i18n.localize("CMPBrowser.SETTING.Maxload.NAME"), + hint: game.i18n.localize("CMPBrowser.SETTING.Maxload.HINT"), + scope: "world", + config: true, + default: CMPBrowser.MAXLOAD, + type: Number, + range: { // If range is specified, the resulting setting will be a range slider + min: 200, + max: 5000, + step: 100 + } + }); + + // load settings from container and apply to default settings (available compendia might have changed) + let settings = this.gs.get(CMPBrowser.MODULE_NAME, SETTINGS); + for (let compKey in defaultSettings.loadedSpellCompendium) { + if (settings.loadedSpellCompendium[compKey] !== undefined) { + defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; + } + } + for (let compKey in defaultSettings.loadedNpcCompendium) { + if (settings.loadedNpcCompendium[compKey] !== undefined) { + defaultSettings.loadedNpcCompendium[compKey].load = settings.loadedNpcCompendium[compKey].load; + } + } + defaultSettings.allowSpellBrowser = settings.allowSpellBrowser ? true : false; + defaultSettings.allowFeatBrowser = settings.allowFeatBrowser ? true : false; + defaultSettings.allowItemBrowser = settings.allowItemBrowser ? true : false; + defaultSettings.allowNpcBrowser = settings.allowNpcBrowser ? true : false; + + if (game.user.isGM) { + game.settings.set(CMPBrowser.MODULE_NAME, SETTINGS, defaultSettings); + console.log("New default settings set"); + console.log(defaultSettings); + } + + return defaultSettings; + } + + static saveSettings() { + game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings); + } +} \ No newline at end of file diff --git a/compendium-browser.css b/styles/compendium-browser.css similarity index 59% rename from compendium-browser.css rename to styles/compendium-browser.css index 96021bc..49190a0 100644 --- a/compendium-browser.css +++ b/styles/compendium-browser.css @@ -7,14 +7,25 @@ .compendium-browser { overflow-y: hidden!important; max-width: 1100px; - max-height: 90vh; } + +.compendium-browser .info { + padding: 0.25em 0.6em; + text-align: center; + border: 1px solid #acacac; + display: inline-block; + margin: auto; + background: rgba(0,0,0,0.3); + border-radius: 0.3em; + color: #cecece; +} + .compendium-browser .window-content { overflow-y: hidden!important; height: 100%; + padding: 0.5em 0.5em 0 0.5em; } .compendium-browser .window-content .parent { - overflow-y: hidden!important; height: 100%; } .compendium-browser .window-content .parent .content { @@ -53,21 +64,21 @@ position: sticky; display: block; min-width: 250px; - max-width: 45%; + max-width: 8vw; width: 350px; height: 100%; padding-right: 5px; - overflow: scroll; + overflow-y: scroll; + flex-grow: 1; } .compendium-browser .list-area { position: sticky; display: flex; min-width: 250px; - max-width: 55%; - width: 400px; height: 100%; padding-right: 5px; - overflow-y: scroll; + /**overflow-y: scroll;**/ + flex-grow: 2; } .compendium-browser .control-area button { background: rgba(0, 0, 0, 0.05); @@ -135,7 +146,7 @@ .compendium-browser .browser .window-content { overflow-y: hidden!important; } -.compendium-browser .browser ul { +.compendium-browser .browser .list-area ul { float: right; display: block; min-width: 335px; @@ -159,24 +170,20 @@ display: inline-block; min-width: 15px; } -.compendium-browser .item-browser li, -.compendium-browser .feat-browser li, -.compendium-browser .spell-browser li { +.compendium-browser .item-browser > li, +.compendium-browser .feat-browser > li, +.compendium-browser .spell-browser > li { cursor: default; vertical-align: middle; line-height: 32px; margin: 2px 0; } -.compendium-browser .item-browser li .item-image, -.compendium-browser .feat-browser li .item-image, -.compendium-browser .spell-browser li .item-image { - max-width: 32px; - height: 32px; -} -.compendium-browser .item-browser li .item-name, -.compendium-browser .feat-browser li .item-name, -.compendium-browser .spell-browser li .item-name { - height: 32px; + +.compendium-browser .browser li:nth-child(odd) { + background: rgba(0, 0, 0, 0.05); + } + +.compendium-browser .browser li .item-name { padding-left: 5px; } .compendium-browser .item-browser li .feat-tags, @@ -185,11 +192,10 @@ .compendium-browser .item-browser li .item-tags, .compendium-browser .feat-browser li .item-tags, .compendium-browser .spell-browser li .item-tags { - text-align: right; + text-align: justify; margin-right: 3px; margin-left: 3px; text-transform: capitalize; - height: 32px; } .compendium-browser .spell-browser .spell .spell-level { text-align: center; @@ -257,3 +263,115 @@ vertical-align: middle; height: 100%; } + +.compendium-browser .actions { + flex: 0 0 32px; + box-sizing: content-box; + flex: 0 0 32px; + /* margin: 0 0 5px; */ + background: rgb(86 85 78 / 18%); + border-radius: 0 0 0.5em 0.5em; + padding: 0 0.7em; +} +.compendium-browser .actions ul { + list-style: none; + display: flex; + flex-wrap: nowrap; + flex-direction: row-reverse; +} +.compendium-browser .actions li { + box-sizing: content-box; +} +.compendium-browser .actions li button { + cursor: pointer; + color: #ededed; + background-color: #444; + border-left: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 0; +} + +.compendium-browser .cb_entities .item { + border-left: 0.3em solid #ccc; + border-bottom: 1px dotted #ccc; + line-height: 1em; + display: grid; + grid-template-columns: 0.2fr 0.8fr 1fr; + grid-template-rows: 1fr 1fr; + gap: 0px 0px; + grid-template-areas: + "Image Name Tags" + "Image ItemTags Tags"; +} + +.compendium-browser .cb_entities ul.tags { + list-style: none; + min-width: initial; + box-sizing: content-box; + display: inline-block; +} +.compendium-browser .cb_entities .item-tags{ + padding-left: 0.5em; + grid-area: ItemTags; +} + +.compendium-browser .cb_entities .spell-tags, +.compendium-browser .cb_entities .misc-tags { + grid-area: Tags; +} + +.compendium-browser .cb_entities .tags li{ + font-size: 0.8em; + line-height: initial; + display: inline-block; + border: 1px solid #acacac; + padding: 0.2em; + border-radius: 0.2em; + line-height: initial; +} + +.compendium-browser .cb_entities .item-image { + grid-area: Image; + width: 100%; + height: 100%; + object-fit: cover; + margin: 0; + padding: 0; +} +.compendium-browser .cb_entities .item-name { + grid-area: Name; + font-size: 1.2em; + line-height: initial; + display: inline; +} + +.compendium-browser .cb_entities .item.r_common { + border-left: 0.3em solid #fff; +} +.compendium-browser .cb_entities .item.r_uncommon { + border-color: rgb(30,255,0); +} +.compendium-browser .cb_entities .item.r_uncommon .item-name, +.compendium-browser .cb_entities .item.r_uncommon .tags { + background: linear-gradient(90deg, rgba(30,255,0,1) 0%, rgba(30,255,0,0) 35%); +} +.compendium-browser .cb_entities .item.r_rare {border-color: darkblue;} + +.compendium-browser .cb_entities .item.r_rare .item-name, +.compendium-browser .cb_entities .item.r_rare .item-info { + background: linear-gradient(90deg, rgba(0,112,221,1) 0%, rgba(0,112,221,0) 35%); +} +.compendium-browser .cb_entities .item.r_veryrare {border-color: #a335ee;} + +.compendium-browser .cb_entities .item.r_veryrare .item-name, +.compendium-browser .cb_entities .item.r_veryrare .item-info { + background: linear-gradient(90deg, rgba(163,53,238,1) 0%, rgba(163,53,238,0) 35%); +} + +.compendium-browser .cb_entities .item.r_legendary {border-color: rgb(255,128,0);} + +.compendium-browser .cb_entities .item.r_legendary .item-name, +.compendium-browser .cb_entities .item.r_legendary .item-info { + background: linear-gradient(90deg, rgba(255,128,0,1) 0%, rgba(255,128,0,0) 35%); +} + +.compendium-browser .cb_entities .item.r_rare .item-name{color: inherit;} diff --git a/compendium-browser.less b/styles/compendium-browser.less similarity index 95% rename from compendium-browser.less rename to styles/compendium-browser.less index 4b690fb..205ebdf 100644 --- a/compendium-browser.less +++ b/styles/compendium-browser.less @@ -1,285 +1,285 @@ -#compendium .directory-footer .compendium-browser-btn { - margin-top:5px; -} - -#compendium .directory-footer { - display:block; -} - -.compendium-browser { - overflow-y: hidden!important; - max-width:1100px; - max-height:90vh; - - .window-content { - overflow-y: hidden!important; - height: 100%; - .parent { - overflow-y: hidden!important; - height: 100%; - .content { - overflow-y: hidden!important; - height: calc(100% - 2em); - .tab { - overflow-y: hidden!important; - height: 100%; - .browser { - overflow-y: hidden!important; - height: 100%; - ul { - overflow-y:auto; - height:100%; - } - } - .settings { - overflow-y:auto; - height:100%; - } - } - } - } - } - - .tabs { - max-height:2em; - border-bottom: solid #782e22; - a { - } - } - .tabContainer { - height:calc(100% - 2em); - .tab { - width: 100%; - height: 100%; - overflow:scroll; - } - } - - .control-area { - position:sticky; - display: block; - min-width: 250px; - max-width: 400px; - width: 300px; - height:100%; - padding-right:5px; - overflow:scroll; - - button { - background: rgba(0, 0, 0, 0.05); - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding:2px; - } - - .filtercontainer { - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding:2px; - - h3 { - margin:0; - cursor:pointer; - } - - dl, div { - margin: 5px 0; - } - - dt { - display:inline-block; - width:40%; - padding-left:5px; - } - - dd { - display:inline-block; - width:58%; - margin-left:0; - - select { - width:100%; - } - } - .multiselect { - border: 1px solid #bbb; - border-radius: 3px; - vertical-align: middle; - line-height:32px; - margin:2px 0; - - label { - padding:5px; - } - input { - vertical-align: middle; - } - } - .small-input { - width: calc(100% - 44px); - height: 27px; - background: rgba(0, 0, 0, 0.05); - border: 1px solid #444; - border-radius: 3px; - padding: 0 3px; - text-overflow: ellipsis; - } - .small-select { - width: 40px; - } - } - } - - .browser { - height: 100%; - overflow-y: hidden!important; - - .window-content { - overflow-y: hidden!important; - } - - ul { - float:right; - display: block; - min-width: 335px; - width: 785px; - margin:0; - height: 100%; - overflow:auto; - padding-left: 5px; - - .filter-tags { - display:none; - } - - li { - span { - white-space: nowrap; - overflow:hidden; - } - } - - } - - .spacer { - display:inline-block; - min-width:5px; - } - .spacer-large { - display:inline-block; - min-width:15px; - } - } - - .item-browser, .feat-browser, .spell-browser { - li { - cursor:default; - vertical-align: middle; - line-height:32px; - margin:2px 0; - - .item-image { - max-width:32px; - height:32px; - } - .item-name { - height:32px; - padding-left:5px; - } - - .feat-tags, .item-tags { - text-align:right; - margin-right:3px; - margin-left: 3px; - text-transform:capitalize; - height:32px; - } - } - } - - .spell-browser { - .spell { - - .spell-level { - text-align:center; - font-weight:900; - max-width:18px; - height:32px; - } - .spell-tags { - text-align:right; - margin-right:3px; - font-weight:900; - max-width:100px; - height:32px; - } - } - } - - .npc-browser { - .npc { - cursor:default; - vertical-align: middle; - line-height:64px; - margin:4px 0; - .npc-image { - max-width: 64px; - height: 64px; - } - .npc-image img { - width: 64px; - height: 64px; - border: none; - object-fit: contain; - } - - .npc-line { - line-height: 25px; - padding: 9px 0 5px 5px; - } - .npc-name { - font-weight:bold; - font-size:16px; - } - .cr { - display: inline-block; - width: 55px; - } - .size { - display: inline-block; - width: 75px; - } - .type { - display: inline-block; - } - } - } - - .settings { - .settings-group { - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding:2px; - - h3 { - margin:0; - cursor:pointer; - } - - label { - display:block; - } - input { - - } - h4 { - display:inline-block; - vertical-align: middle; - height:100%; - } - } - } +#compendium .directory-footer .compendium-browser-btn { + margin-top:5px; +} + +#compendium .directory-footer { + display:block; +} + +.compendium-browser { + overflow-y: hidden!important; + max-width:1100px; + max-height:90vh; + + .window-content { + overflow-y: hidden!important; + height: 100%; + .parent { + overflow-y: hidden!important; + height: 100%; + .content { + overflow-y: hidden!important; + height: calc(100% - 2em); + .tab { + overflow-y: hidden!important; + height: 100%; + .browser { + overflow-y: hidden!important; + height: 100%; + ul { + overflow-y:auto; + height:100%; + } + } + .settings { + overflow-y:auto; + height:100%; + } + } + } + } + } + + .tabs { + max-height:2em; + border-bottom: solid #782e22; + a { + } + } + .tabContainer { + height:calc(100% - 2em); + .tab { + width: 100%; + height: 100%; + overflow:scroll; + } + } + + .control-area { + position:sticky; + display: block; + min-width: 250px; + max-width: 400px; + width: 300px; + height:100%; + padding-right:5px; + overflow:scroll; + + button { + background: rgba(0, 0, 0, 0.05); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding:2px; + } + + .filtercontainer { + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding:2px; + + h3 { + margin:0; + cursor:pointer; + } + + dl, div { + margin: 5px 0; + } + + dt { + display:inline-block; + width:40%; + padding-left:5px; + } + + dd { + display:inline-block; + width:58%; + margin-left:0; + + select { + width:100%; + } + } + .multiselect { + border: 1px solid #bbb; + border-radius: 3px; + vertical-align: middle; + line-height:32px; + margin:2px 0; + + label { + padding:5px; + } + input { + vertical-align: middle; + } + } + .small-input { + width: calc(100% - 44px); + height: 27px; + background: rgba(0, 0, 0, 0.05); + border: 1px solid #444; + border-radius: 3px; + padding: 0 3px; + text-overflow: ellipsis; + } + .small-select { + width: 40px; + } + } + } + + .browser { + height: 100%; + overflow-y: hidden!important; + + .window-content { + overflow-y: hidden!important; + } + + ul { + float:right; + display: block; + min-width: 335px; + width: 785px; + margin:0; + height: 100%; + overflow:auto; + padding-left: 5px; + + .filter-tags { + display:none; + } + + li { + span { + white-space: nowrap; + overflow:hidden; + } + } + + } + + .spacer { + display:inline-block; + min-width:5px; + } + .spacer-large { + display:inline-block; + min-width:15px; + } + } + + .item-browser, .feat-browser, .spell-browser { + li { + cursor:default; + vertical-align: middle; + line-height:32px; + margin:2px 0; + + .item-image { + max-width:32px; + height:32px; + } + .item-name { + height:32px; + padding-left:5px; + } + + .feat-tags, .item-tags { + text-align:right; + margin-right:3px; + margin-left: 3px; + text-transform:capitalize; + height:32px; + } + } + } + + .spell-browser { + .spell { + + .spell-level { + text-align:center; + font-weight:900; + max-width:18px; + height:32px; + } + .spell-tags { + text-align:right; + margin-right:3px; + font-weight:900; + max-width:100px; + height:32px; + } + } + } + + .npc-browser { + .npc { + cursor:default; + vertical-align: middle; + line-height:64px; + margin:4px 0; + .npc-image { + max-width: 64px; + height: 64px; + } + .npc-image img { + width: 64px; + height: 64px; + border: none; + object-fit: contain; + } + + .npc-line { + line-height: 25px; + padding: 9px 0 5px 5px; + } + .npc-name { + font-weight:bold; + font-size:16px; + } + .cr { + display: inline-block; + width: 55px; + } + .size { + display: inline-block; + width: 75px; + } + .type { + display: inline-block; + } + } + } + + .settings { + .settings-group { + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding:2px; + + h3 { + margin:0; + cursor:pointer; + } + + label { + display:block; + } + input { + + } + h4 { + display:inline-block; + vertical-align: middle; + height:100%; + } + } + } } \ No newline at end of file diff --git a/template/entity-browser-list.html b/template/entity-browser-list.html new file mode 100644 index 0000000..faa0569 --- /dev/null +++ b/template/entity-browser-list.html @@ -0,0 +1,60 @@ +{{#each listItems as |entity|}} +
  • + +
    + +
    + + + + + {{#if entity.classRequirement}} + + {{/if}} + {{#ifCond entity.type '==' 'spell'}} + + {{/ifCond}} + +
  • +{{/each}} diff --git a/template/feat-browser-list.html b/template/feat-browser-list.html deleted file mode 100644 index 55be8f8..0000000 --- a/template/feat-browser-list.html +++ /dev/null @@ -1,17 +0,0 @@ -{{#each listItems as |feat id|}} -
  • -
    - -
    - -
    - {{feat.classRequirementString}} - -
    -
    - -
    -
  • -{{/each}} diff --git a/template/feat-browser.html b/template/feat-browser.html index 0960e31..6f4c330 100644 --- a/template/feat-browser.html +++ b/template/feat-browser.html @@ -11,14 +11,14 @@ - + {{> "modules/compendium-browser/template/filter-container.html" filters=featFilters}}
    -
    diff --git a/template/filter-container.html b/template/filter-container.html index 282863f..49f0ac1 100644 --- a/template/filter-container.html +++ b/template/filter-container.html @@ -3,21 +3,22 @@

    {{cat.label}}

    {{#each cat.filters as |filter key|}} -
    +
    {{#if filter.istext}} -
    {{filter.label}}
    -
    - {{#if filter.possibleValues}} - - {{else}} - - {{/if}} -
    +
    {{filter.label}}
    +
    + {{#if filter.possibleValues}} + + {{else}} + + {{/if}} +
    {{/if}} {{#if filter.isbool}}
    @@ -65,7 +66,7 @@

    {{cat.label}}

    - +
    diff --git a/template/item-browser-list.html b/template/item-browser-list.html deleted file mode 100644 index baff6f1..0000000 --- a/template/item-browser-list.html +++ /dev/null @@ -1,16 +0,0 @@ -{{#each listItems as |item id|}} -
  • -
    - -
    - -
    - {{item.type}} -
    -
    - -
    -
  • -{{/each}} diff --git a/template/item-browser.html b/template/item-browser.html index 7b8b217..b5ba4a9 100644 --- a/template/item-browser.html +++ b/template/item-browser.html @@ -11,14 +11,14 @@ - +
    {{> "modules/compendium-browser/template/filter-container.html" filters=itemFilters}}
    -
    \ No newline at end of file diff --git a/template/loading.html b/template/loading.html index d558fab..c59a9cf 100644 --- a/template/loading.html +++ b/template/loading.html @@ -1,6 +1,8 @@ - - - {{localize "CMPBrowser.LOADING.Message" numLoaded=numLoaded itemType=itemType}} - {{#if maxLoaded}}{{localize "CMPBrowser.LOADING.MaxLoaded"}}{{/if}} - + \ No newline at end of file diff --git a/template/npc-browser-list.html b/template/npc-browser-list.html deleted file mode 100644 index e065292..0000000 --- a/template/npc-browser-list.html +++ /dev/null @@ -1,21 +0,0 @@ -{{#each listItems as |npc id|}} -
  • -
    - -
    -
    - -
    - CR {{npc.displayCR}} - {{npc.displaySize}} - {{npc.displayType}} -
    -
    - - -
    -
    -
  • -{{/each}} diff --git a/template/npc-browser.html b/template/npc-browser.html index 16167a3..fcb51b8 100644 --- a/template/npc-browser.html +++ b/template/npc-browser.html @@ -12,14 +12,14 @@ - + {{> "modules/compendium-browser/template/filter-container.html" filters=npcFilters}}
    -
    \ No newline at end of file diff --git a/template/settings.html b/template/settings.html index 9a442be..5f71c39 100644 --- a/template/settings.html +++ b/template/settings.html @@ -1,5 +1,5 @@
    -
    +

    {{localize "CMPBrowser.generalSettings"}}

    -
    +

    {{localize "CMPBrowser.compSettingsSpell"}}

    - {{#each settings.loadedSpellCompendium as |spellComp key|}} + {{#each settings.loadedCompendium.Item as |Comp key|}} {{/each}}
    -
    +

    {{localize "CMPBrowser.compSettingsNpc"}}

    - {{#each settings.loadedNpcCompendium as |npcComp key|}} + {{#each settings.loadedCompendium.Actor as |Comp key|}} + {{/each}} +
    +
    +

    {{localize "CMPBrowser.compSettingsNpc"}}

    + {{#each settings.loadedCompendium.JournalEntry as |Comp key|}} + {{/each}}
    diff --git a/template/spell-browser-list.html b/template/spell-browser-list.html deleted file mode 100644 index d797f06..0000000 --- a/template/spell-browser-list.html +++ /dev/null @@ -1,25 +0,0 @@ - -{{#each listItems as |spell id|}} -
  • -
    - -
    - -
    - {{#if spell.data.level}}{{spell.data.level}}{{else}}C{{/if}} -
    - R - C -
    - V - S - M -
    -
    - -
    -
  • -{{/each}} - \ No newline at end of file diff --git a/template/spell-browser.html b/template/spell-browser.html index 66c3613..00fa34d 100644 --- a/template/spell-browser.html +++ b/template/spell-browser.html @@ -11,14 +11,14 @@ - +
    {{> "modules/compendium-browser/template/filter-container.html" filters=spellFilters}}
    -
      - {{> "modules/compendium-browser/template/spell-browser-list.html" spells=items}} +
        + {{> "modules/compendium-browser/template/entity-browser-list.html" spells=items}}
    \ No newline at end of file diff --git a/template/template.html b/template/template.html index ec5f6dc..8d40acc 100644 --- a/template/template.html +++ b/template/template.html @@ -6,12 +6,20 @@ {{#if showNpcBrowser}}{{localize "CMPBrowser.Tab.NPCBrowser"}}{{/if}} {{#if isGM}}{{localize "CMPBrowser.Tab.Settings"}}{{/if}}
    - +
    {{#if showSpellBrowser}}{{> "modules/compendium-browser/template/spell-browser.html"}}{{/if}}
    {{#if showFeatBrowser}}{{> "modules/compendium-browser/template/feat-browser.html"}}{{/if}}
    {{#if showItemBrowser}}{{> "modules/compendium-browser/template/item-browser.html"}}{{/if}}
    {{#if showNpcBrowser}} {{> "modules/compendium-browser/template/npc-browser.html"}}{{/if}}
    -
    {{#if isGM}} {{> "modules/compendium-browser/template/settings.html"}}{{/if}}
    + {{#if isGM}} +
    {{> "modules/compendium-browser/template/settings.html"}}
    + {{/if}}
    \ No newline at end of file From 7f10130f7a4273e14eed83ab36d0dc498ef6148a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20B=C3=B6ttner?= Date: Fri, 9 Jul 2021 12:57:36 +0200 Subject: [PATCH 02/13] New Structure and working Export --- hooks/events.mjs | 232 ++++++++++++++ hooks/mainHook.js | 2 - lang/en.json | 2 +- scripts/compendium-browser.js | 485 ++++-------------------------- scripts/modules/entities.mjs | 224 +++----------- scripts/modules/filter.mjs | 18 ++ scripts/modules/renderer.mjs | 40 +++ scripts/modules/settings.mjs | 73 +++-- styles/compendium-browser.css | 110 +++---- template/entity-browser-list.html | 14 +- template/entity-browser.html | 22 ++ template/feat-browser.html | 1 - template/item-browser.html | 3 +- template/loading.html | 7 +- template/npc-browser.html | 1 - template/spell-browser.html | 3 +- template/template.html | 19 +- 17 files changed, 514 insertions(+), 742 deletions(-) create mode 100644 hooks/events.mjs create mode 100644 scripts/modules/renderer.mjs create mode 100644 template/entity-browser.html diff --git a/hooks/events.mjs b/hooks/events.mjs new file mode 100644 index 0000000..d89699f --- /dev/null +++ b/hooks/events.mjs @@ -0,0 +1,232 @@ +import Exporter from "../scripts/modules/exporter.mjs"; + +export class Events { + constructor(){ + return this; + } + + static async activateActionListener(app = document.getElementsByClassName('window-app')){ + app = app.get(0); + app.querySelector('button[data-action="export"]').addEventListener('click', async (e)=> { + let items = app.querySelectorAll('.content .tab.active .cb_entities .entity'), + entityType = app.querySelector('.content .tab.active').dataset.tab, + filters = JSON.parse(app.querySelector('.tab.active .cb_entities').dataset.activeFilters), + tableItems = [], + tableName = entityType + 'table: '; + + items.forEach(item => { + let obj = {}; + Object.assign(obj, item.dataset); + tableItems.push(obj); + }); + + let filterKeys = Object.keys(filters); + if (filterKeys.length > 0){ + for (let key of filterKeys) { + tableName = tableName + '_' + filters[key].path + '-' + filters[key].value; + } + } + + Exporter.createTableFromSelection(tableName,tableItems); + }); + } + + static async activateItemListListeners(app = document.getElementsByClassName('window-app')) { + app = app.get(0); + + // open entity sheet on click + app.querySelectorAll('*[data-action="openSheet"]').forEach(async el => { + el.addEventListener('click', async (e) => { + let itemId = e.currentTarget.parentNode.dataset.entryId; + let compendium = e.currentTarget.parentNode.dataset.entryCompendium; + let pack = game.packs.find(p => p.collection === compendium); + await pack.getEntity(itemId).then(entity => { + entity.sheet.render(true); + }); + }); + }); + + // make draggable + //0.4.1: Avoid the game.packs lookup + app.querySelectorAll('.draggable').forEach(async li => { + li.setAttribute("draggable", true); + li.addEventListener('dragstart', event => { + let packName = li.getAttribute("data-entry-compendium"); + let pack = game.packs.find(p => p.collection === packName); + if (!pack) { + event.preventDefault(); + return false; + } + event.dataTransfer.setData("text/plain", JSON.stringify({ + type: pack.entity, + pack: pack.collection, + id: li.getAttribute("data-entry-id") + })); + }, false); + }); + } + + static async observeListElement(list,tag) { + for (let img of list.getElementsByTagName(tag)) { + game.compendiumBrowser.observer.observe(img); + } + } + + static async activateFilterListeners(html){ + // toggle visibility of filter containers + html.find('.filtercontainer h3, .multiselect label').click(async ev => { + await $(ev.target.nextElementSibling).toggle(100); + }); + + html.find('.multiselect label').trigger('click'); + + // reset filters and re-rendes + html.find('button[data-action="reset-filters"]').click(ev => { + this.filters.resetFilters(); + this.refreshList = ev.target.closest('.tab').dataset.tab; + this.render(); + }); + + // settings + html.find('.settings input').on('change', e => { + let setting = e.target.dataset.setting, + value = e.target.checked, + key = e.target.dataset.key, + category = (e.dataset.type === 'npc')? 'Actor' : 'Item'; + + this.settings.loadedCompendium[category][key].load = value; + this.render(); + + ui.notifications.info("Settings Saved. Compendiums are being reloaded."); + + if (setting === 'allow-spell-browser') { + this.settings.allowSpellBrowser = value; + } + if (setting === 'allow-feat-browser') { + this.settings.allowFeatBrowser = value; + } + if (setting === 'allow-item-browser') { + this.settings.allowItemBrowser = value; + } + if (setting === 'allow-npc-browser') { + this.settings.allowNpcBrowser = value; + } + ModuleSettings.saveSettings(); + }); + + + // activating or deactivating filters + //0.4.1: Now does a re-load and updates just the data side + // text filters + html.find('.filter[data-type=text] input, .filter[data-type=text] select').on('keyup change paste', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const value = ev.target.value; + const entityType = $(ev.target).parents('.tab').data('tab'); + + const filterTarget = `${entityType}Filters`; + + if (value === '' || value === undefined) { + delete game.compendiumBrowser.filters[filterTarget].activeFilters[key]; + } else { + game.compendiumBrowser.filters[filterTarget].activeFilters[key] = { + path: path, + type: 'text', + valIsArray: false, + value: ev.target.value + } + } + + game.compendiumBrowser.replaceList(html, entityType); + }); + + // select filters + html.find('.filter[data-type=select] select, .filter[data-type=bool] select').on('change', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const filterType = $(ev.target).parents('.filter').data('type'); + const browserTab = $(ev.target).parents('.tab').data('tab'); + let valIsArray = $(ev.target).parents('.filter').data('valisarray'); + if (valIsArray === 'true') valIsArray = true; + let value = ev.target.value; + if (value === 'false') value = false; + if (value === 'true') value = true; + + const filterTarget = `${browserTab}Filters`; + + if (value === "null") { + delete game.compendiumBrowser.filters[filterTarget].activeFilters[key]; + } else { + game.compendiumBrowser.filters[filterTarget].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + value: value + } + } + game.compendiumBrowser.replaceList(html, browserTab); + }); + + // multiselect filters + html.find('.filter[data-type=multiSelect] input').on('change', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const filterType = 'multiSelect'; + const browserTab = $(ev.target).parents('.tab').data('tab'); + let valIsArray = $(ev.target).parents('.filter').data('valisarray'); + if (valIsArray === 'true') valIsArray = true; + let value = $(ev.target).data('value'); + + const filterTarget = `${browserTab}Filters`; + const filter = this[filterTarget].activeFilters[key]; + + if (ev.target.checked === true) { + if (filter === undefined) { + this[filterTarget].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + values: [value] + } + } else { + this[filterTarget].activeFilters[key].values.push(value); + } + } else { + delete this[filterTarget].activeFilters[key].values.splice(this[filterTarget].activeFilters[key].values.indexOf(value), 1); + if (this[filterTarget].activeFilters[key].values.length === 0) { + delete this[filterTarget].activeFilters[key]; + } + } + + game.compendiumBrowser.replaceList(html, browserTab); + }); + + + html.find('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').on('change keyup paste', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const filterType = 'numberCompare'; + const browserTab = $(ev.target).parents('.tab').data('tab'); + let valIsArray = false; + + const operator = $(ev.target).parents('.filter').find('select').val(); + const value = $(ev.target).parents('.filter').find('input').val(); + + const filterTarget = `${browserTab}Filters`; + + if (value === '' || operator === 'null') { + delete this[filterTarget].activeFilters[key] + } else { + this[filterTarget].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + operator: operator, + value: value + } + } + + game.compendiumBrowser.replaceList(html, browserTab); + }); + } +} \ No newline at end of file diff --git a/hooks/mainHook.js b/hooks/mainHook.js index 61a914f..bcc7d57 100644 --- a/hooks/mainHook.js +++ b/hooks/mainHook.js @@ -60,8 +60,6 @@ export class moduleHooks { //A compromise approach would be better (periodic loading) except would still create the memory use problem await game.compendiumBrowser.initialize(); } - - game.compendiumBrowser.addFilters(); }); } diff --git a/lang/en.json b/lang/en.json index 501edcc..762eb72 100644 --- a/lang/en.json +++ b/lang/en.json @@ -62,7 +62,7 @@ "CMPBrowser.Tab.Settings":"Settings", "CMPBrowser.SETTING.Maxload.NAME" : "Maximum load", "CMPBrowser.SETTING.Maxload.HINT" : "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", - "CMPBrowser.LOADING.Message" : "Loaded...{numLoaded} {itemType}s", + "CMPBrowser.LOADING.Message" : "Loaded: {numLoaded} {itemType}s", "CMPBrowser.LOADING.MaxLoaded" : "(maximum displayed; to see more, use the filters)", "CMPBrowser.Filters.ResetFilters" : "Reset Filters" diff --git a/scripts/compendium-browser.js b/scripts/compendium-browser.js index aa216c5..dd65242 100644 --- a/scripts/compendium-browser.js +++ b/scripts/compendium-browser.js @@ -1,7 +1,9 @@ import { ModuleSettings , CMPBrowser } from './modules/settings.mjs'; import Exporter from './modules/exporter.mjs'; import Entities from './modules/entities.mjs'; +import { Events } from '../hooks/events.mjs'; import { Filter } from './modules/filter.mjs'; +import { Renderer } from './modules/renderer.mjs'; //import Exporter from './modules/exporter.mjs'; /* eslint-disable valid-jsdoc */ @@ -11,11 +13,11 @@ export class CompendiumBrowser extends Application { constructor() { super(); - let moduleSettings = new ModuleSettings(); - this.settings = moduleSettings.initSettings(); - this.filters = new Filter(); + ModuleSettings.registerGameSettings(); + this.settings = ModuleSettings.initModuleSettings(); + this.filters = new Filter(); this.currentLists = {}; } @@ -40,20 +42,18 @@ export class CompendiumBrowser extends Application { async initialize() { // load settings if (this.settings === undefined) { - ModuleSettings.initSettings(); + this.settings = ModuleSettings.initModuleSettings(); } await loadTemplates([ "modules/compendium-browser/template/spell-browser.html", - "modules/compendium-browser/template/npc-browser.html", - "modules/compendium-browser/template/feat-browser.html", - "modules/compendium-browser/template/item-browser.html", + "modules/compendium-browser/template/entity-browser.html", "modules/compendium-browser/template/entity-browser-list.html", "modules/compendium-browser/template/filter-container.html", "modules/compendium-browser/template/settings.html", "modules/compendium-browser/template/loading.html" ]); - + this.filters.addEntityFilters(); this.hookCompendiumList(); } @@ -62,7 +62,15 @@ export class CompendiumBrowser extends Application { _onChangeTab(event, tabs, active) { super._onChangeTab(event, tabs, active); const html = this.element; - this.replaceList(html, active, { reload: false }) + this.refreshFilters(html,active); + this.replaceList(html, active, { reload: false }); + } + + async refreshFilters(html,entityType){ + let entityBrowserHtml = await Renderer.renderFilters(game.compendiumBrowser.filters[entityType +'Filters']), + filterWrapper = document.querySelector('.tab.active .browser .control-area'); + filterWrapper.innerHTML = filterWrapper.firstElementChild.outerHTML + entityBrowserHtml; + Events.activateFilterListeners(html); } /** @@ -95,44 +103,13 @@ export class CompendiumBrowser extends Application { return data; } - activateItemListListeners(html) { - // show entity sheet - html.find('*[data-action="openSheet"]').click(async e => { - let itemId = e.currentTarget.parentNode.dataset.entryId; - let compendium = e.currentTarget.parentNode.dataset.entryCompendium; - let pack = game.packs.find(p => p.collection === compendium); - await pack.getEntity(itemId).then(entity => { - entity.sheet.render(true); - }); - }); - - // make draggable - //0.4.1: Avoid the game.packs lookup - html.find('.draggable').each((i, li) => { - li.setAttribute("draggable", true); - li.addEventListener('dragstart', event => { - let packName = li.getAttribute("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === packName); - if (!pack) { - event.preventDefault(); - return false; - } - event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.entity, - pack: pack.collection, - id: li.getAttribute("data-entry-id") - })); - }, false); - }); - } - /** override */ activateListeners(html) { super.activateListeners(html); this.observer = new IntersectionObserver((entries, observer) => { - for (let e of entries) { - if (!e.isIntersecting) continue; + for (let [i, e] of entries.entries()) { + if (!e.isIntersecting) break; const img = e.target; // Avatar image //const img = li.querySelector("img"); @@ -141,181 +118,28 @@ export class CompendiumBrowser extends Application { delete img.dataset.src; } - // No longer observe the target - observer.unobserve(e.target); - } - }); - - this.activateItemListListeners(html); - - // toggle visibility of filter containers - html.find('.filtercontainer h3, .multiselect label').click(async ev => { - await $(ev.target.nextElementSibling).toggle(100); - }); - - html.find('.multiselect label').trigger('click'); - - // reset filters and re-rendes - html.find('button[data-action="reset-filters"]').click(ev => { - this.filters.resetFilters(); - this.refreshList = ev.target.closest('.tab').dataset.tab; - this.render(); - }); - - html.find('button[data-action="export"]').click(e => { - let items = document.querySelectorAll('.content .tab.active .cb_entities .item'), - tableItems = []; - - items.forEach(item => { - let obj = {}; - Object.assign(obj, item.dataset); - tableItems.push(obj); - }); - - Exporter.createTableFromSelection('testtable',tableItems); - }); - - // settings - html.find('.settings input').on('change', e => { - let setting = e.target.dataset.setting, - value = e.target.checked, - key = e.target.dataset.key, - category = (e.dataset.type === 'npc')? 'Actor' : 'Item'; - - this.settings.loadedCompendium[category][key].load = value; - this.render(); - - ui.notifications.info("Settings Saved. Compendiums are being reloaded."); - - if (setting === 'allow-spell-browser') { - this.settings.allowSpellBrowser = value; - } - if (setting === 'allow-feat-browser') { - this.settings.allowFeatBrowser = value; - } - if (setting === 'allow-item-browser') { - this.settings.allowItemBrowser = value; - } - if (setting === 'allow-npc-browser') { - this.settings.allowNpcBrowser = value; - } - ModuleSettings.saveSettings(); - }); - - - // activating or deactivating filters - //0.4.1: Now does a re-load and updates just the data side - // text filters - html.find('.filter[data-type=text] input, .filter[data-type=text] select').on('keyup change paste', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const value = ev.target.value; - const entityType = $(ev.target).parents('.tab').data('tab'); - - const filterTarget = `${entityType}Filters`; - - if (value === '' || value === undefined) { - delete this[filterTarget].activeFilters[key]; - } else { - this[filterTarget].activeFilters[key] = { - path: path, - type: 'text', - valIsArray: false, - value: ev.target.value + // pre load more elements if there are any + if (i + 2 < entries.length) { + let next = entries[i+1]?.target, + evenmore = entries[i+2].target; + if((next && next.dataset.src) && (evenmore && evenmore.dataset.src)){ + next.src = next.dataset.src; + evenmore.src = evenmore.dataset.src; + delete next.dataset.src; + delete evenmore.dataset.src; + observer.unobserve(next); + observer.unobserve(evenmore); + } } - } - - this.replaceList(html, entityType); - }); - // select filters - html.find('.filter[data-type=select] select, .filter[data-type=bool] select').on('change', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const filterType = $(ev.target).parents('.filter').data('type'); - const browserTab = $(ev.target).parents('.tab').data('tab'); - let valIsArray = $(ev.target).parents('.filter').data('valisarray'); - if (valIsArray === 'true') valIsArray = true; - let value = ev.target.value; - if (value === 'false') value = false; - if (value === 'true') value = true; - - const filterTarget = `${browserTab}Filters`; - - if (value === "null") { - delete this.filters[filterTarget].activeFilters[key]; - } else { - this.filters[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - value: value - } - } - this.replaceList(html, browserTab); - }); - - // multiselect filters - html.find('.filter[data-type=multiSelect] input').on('change', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const filterType = 'multiSelect'; - const browserTab = $(ev.target).parents('.tab').data('tab'); - let valIsArray = $(ev.target).parents('.filter').data('valisarray'); - if (valIsArray === 'true') valIsArray = true; - let value = $(ev.target).data('value'); - - const filterTarget = `${browserTab}Filters`; - const filter = this[filterTarget].activeFilters[key]; - - if (ev.target.checked === true) { - if (filter === undefined) { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - values: [value] - } - } else { - this[filterTarget].activeFilters[key].values.push(value); - } - } else { - delete this[filterTarget].activeFilters[key].values.splice(this[filterTarget].activeFilters[key].values.indexOf(value), 1); - if (this[filterTarget].activeFilters[key].values.length === 0) { - delete this[filterTarget].activeFilters[key]; - } + // No longer observe the target + observer.unobserve(e.target); } - - this.replaceList(html, browserTab); }); - - html.find('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').on('change keyup paste', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const filterType = 'numberCompare'; - const browserTab = $(ev.target).parents('.tab').data('tab'); - let valIsArray = false; - - const operator = $(ev.target).parents('.filter').find('select').val(); - const value = $(ev.target).parents('.filter').find('input').val(); - - const filterTarget = `${browserTab}Filters`; - - if (value === '' || operator === 'null') { - delete this[filterTarget].activeFilters[key] - } else { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - operator: operator, - value: value - } - } - - this.replaceList(html, browserTab); - }); + Events.activateActionListener(html); + Events.activateItemListListeners(html); + Events.activateFilterListeners(html); //Just for the loading image if (this.observer) { @@ -372,229 +196,34 @@ export class CompendiumBrowser extends Application { cb.refreshList = null; } - async replaceList(html, browserTab, options = { reload: true }) { + async replaceList(html, entityType, options = { reload: true }) { //After rendering the first time or re-rendering trigger the load/reload of visible data + let entityListElement = document.querySelector('.tab.active .browser .cb_entities'); - let elements = null; - //0.4.2 Display a Loading... message while the data is being loaded and filtered - let loadingMessage = null; - if (browserTab === 'spell') { - elements = html.find("ul#CBSpells"); - loadingMessage = html.find("#CBSpellsMessage"); - } else if (browserTab === 'npc') { - elements = html.find("ul#CBNPCs"); - loadingMessage = html.find("#CBNpcsMessage"); - } else if (browserTab === 'feat') { - elements = html.find("ul#CBFeats"); - loadingMessage = html.find("#CBFeatsMessage"); - } else if (browserTab === 'item') { - elements = html.find("ul#CBItems"); - loadingMessage = html.find("#CBItemsMessage"); - } - if (elements?.length) { + if (entityListElement.childElementCount !== undefined) { //0.4.2b: On a tab-switch, only reload if there isn't any data already - if (options?.reload || !elements[0].children.length) { + if (options?.reload || entityListElement.childElementCount < 1) { const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; - const updateLoading = async numLoaded => { - if (loadingMessage.length) { this.renderLoading(loadingMessage[0], browserTab, numLoaded, numLoaded >= maxLoad); } - }; - updateLoading(0); + await Renderer.updateLoading(0,maxLoad,entityType); + // loadItems + const entityHelper = new Entities(this.settings, this.filters); + let entityList = await entityHelper.loadAndFilter(entityType,true); + // + this.currentLists[entityType] = entityList = Entities._sortList(entityList, entityType); //Uses loadAndFilterItems to read compendia for items which pass the current filters and render on this tab - const newItemsHTML = await this.renderItemData(browserTab, updateLoading); - elements[0].innerHTML = newItemsHTML; + const newEntitiesHTML = await Renderer.renderEntityList(entityList.entities, true); + entityListElement.setAttribute('data-active-filters', entityList.filters); + entityListElement.innerHTML = newEntitiesHTML; - //Lazy load images - if (this.observer) { - $(elements).find("img").each((i, img) => this.observer.observe(img)); - } + await Events.observeListElement(entityListElement,'img'); //Reactivate listeners for clicking and dragging - this.activateItemListListeners($(elements)); - } - } - - } - - async renderLoading(messageElement, itemType, numLoaded, maxLoaded = false) { - if (!messageElement) return; - - let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.html", { numLoaded: numLoaded, itemType: itemType, maxLoaded: maxLoaded }); - messageElement.innerHTML = loadingHTML; - } - - async renderItemData(entityType, updateLoading = null) { - const entityHelper = new Entities(this.settings, this.filters); - let entityList = await entityHelper.loadAndFilter(entityType, updateLoading); - - this.currentLists[entityType] = Entities._sortList(entityList, entityType); - - const html = await renderTemplate(`modules/compendium-browser/template/entity-browser-list.html`, { listItems: this.currentLists[entityType] }); - - return html; - } - - //SORTING - triggerSort(html, entityType) { - html.find('.' + entityType + '-browser select[name=sortorder]').trigger('change'); - } - - _sortEntities(html, entityType) { - html.find('.' + entityType + '-browser select[name=sortorder]').on('change', ev => { - let entityList = html.find('.' + entityType + '-browser .cb_entities li'); - let byName = ev.target.value; - let sortedList = (entityType != 'npc')? this.sortEntity(entityList, entityType, byName) : this.sortNpcs(entityList, byName); - let ol = $(html.find('.' + entityType + '-browser .cb_entities')); - ol[0].innerHTML = []; - for (let element of sortedList) { - ol[0].append(element); - } - }); - this.triggerSort(html, entityType); - } - - decorateItem(item5e) { - if (!item5e) return null; - //Decorate and then filter a compendium entry - returns null or the item - const item = item5e.data; - - // getting damage types (common to all Items, although some won't have any) - item.damageTypes = []; - if (item.data.damage && item.data.damage.parts.length > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (item.damageTypes.indexOf(type) === -1) { - item.damageTypes.push(type); - } - } - } - - if (item.type === 'spell') { - // determining classes that can use the spell - let cleanSpellName = item.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); - //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); - if (this.classList[cleanSpellName]) { - let classes = this.classList[cleanSpellName]; - item.data.classes = classes.split(','); - } else { - //FIXME: unfoundSpells += cleanSpellName + ','; + Events.activateItemListListeners(html); } - } else if (item.type === 'feat' || item.type === 'class') { - // getting class - let reqString = item.data.requirements?.replace(/[0-9]/g, '').trim(); - let matchedClass = []; - for (let c in this.subClasses) { - if (reqString && reqString.toLowerCase().indexOf(c) !== -1) { - matchedClass.push(c); - } else { - for (let subClass of this.subClasses[c]) { - if (reqString && reqString.indexOf(subClass) !== -1) { - matchedClass.push(c); - break; - } - } - } - } - item.classRequirement = matchedClass; - item.classRequirementString = matchedClass.join(', '); - - // getting uses/ressources status - item.usesRessources = item5e.hasLimitedUses; - item.hasSave = item5e.hasSave; - - } else { - // getting pack - let matchedPacks = []; - for (let pack of Object.keys(this.packList)) { - for (let packItem of this.packList[pack]) { - if (item.name.toLowerCase() === packItem.toLowerCase()) { - matchedPacks.push(pack); - break; - } - } - } - item.matchedPacks = matchedPacks; - item.matchedPacksString = matchedPacks.join(', '); - - // getting uses/ressources status - item.usesRessources = item5e.hasLimitedUses } - return item; - } - - decorateNpc(npc) { - //console.log('%c '+npc.name, 'background: white; color: red') - const entityData = npc.data; - - } - - filterElements(list, subjects, filters) { - for (let element of list) { - let subject = subjects[element.dataset.entryId]; - if (this.passesFilter(subject, filters) == false) { - $(element).hide(); - } else { - $(element).show(); - } - } - } - - passesFilter(subject, filters) { - for (let filter of Object.values(filters)) { - let prop = getProperty(subject, filter.path); - if (filter.type === 'numberCompare') { - - switch (filter.operator) { - case '=': if (prop != filter.value) { return false; } break; - case '<': if (prop >= filter.value) { return false; } break; - case '>': if (prop <= filter.value) { return false; } break; - } - - continue; - } - if (filter.valIsArray === false) { - if (filter.type === 'text') { - if (prop === undefined) return false; - if (prop.toLowerCase().indexOf(filter.value.toLowerCase()) === -1) { - return false; - } - } else { - if (filter.value !== undefined && prop !== undefined && prop != filter.value && !(filter.value === true && prop)) { - return false; - } - if (filter.values && filter.values.indexOf(prop) === -1) { - return false; - } - } - } else { - if (prop === undefined) return false; - if (typeof prop === 'object') { - if (filter.value) { - if (prop.indexOf(filter.value) === -1) { - return false; - } - } else if (filter.values) { - for (let val of filter.values) { - if (prop.indexOf(val) !== -1) { - continue; - } - return false; - } - } - } else { - for (let val of filter.values) { - if (prop === val) { - continue; - } - } - return false; - } - } - } - - return true; } clearObject(obj) { @@ -606,15 +235,5 @@ export class CompendiumBrowser extends Application { } return newObj; } - - saveSettings() { - game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings); - } - - async addFilters(){ - await this.filters.addSpellFilters(); - await this.filters.addFeatFilters(); - await this.filters.addItemFilters(); - await this.filters.addNpcFilters(); - } + } \ No newline at end of file diff --git a/scripts/modules/entities.mjs b/scripts/modules/entities.mjs index 30a3717..c82688d 100644 --- a/scripts/modules/entities.mjs +++ b/scripts/modules/entities.mjs @@ -1,4 +1,5 @@ import { Filter } from './filter.mjs'; +import { Renderer } from './renderer.mjs'; import { CMPBrowser } from './settings.mjs'; export default class Entities { @@ -20,32 +21,41 @@ export default class Entities { await this.checkListsLoaded(); const Category = (entityType === 'npc') ? 'Actor' : 'Item'; const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; - const ActiveFilters = this.filters.getByType(entityType).activeFilters; + const ActiveFilters = game.compendiumBrowser.filters.getByType(entityType).activeFilters; const packs = game.packs.filter((pack) => pack.metadata.entity === Category); //0.4.1: Load and filter just one of spells, feats, and items (specified by browserTab) let unfoundSpells = ''; let numItemsLoaded = 0; - let compactItems = []; + let compactList = { + filters: JSON.stringify(ActiveFilters), + entities: [] + }; //Filter the full list, but only save the core compendium information + displayed info for (let pack of packs) { - if (this.settings.loadedCompendium[pack.metadata.entity][pack.collection].load) { + if (game.compendiumBrowser.settings.loadedCompendium[pack.metadata.entity][pack.collection].load) { //FIXME: How much could we do with the loaded index rather than all content? //OR filter the content up front for the decoratedItem.type?? await pack.getContent().then(content => { - for (let item5e of content) { - let compactItem = null; - const decoratedEntity = Entities.decorateEntity(item5e, entityType, this.packList, this.classlist, this.subClasses); - if (decoratedEntity) { + for (let currentEntity of content) { + + if (currentEntity.data.type != entityType && entityType != Category.toLowerCase()) continue; + + let compactEntity = null, + decoratedEntity = false; + switch (entityType) { case "spell": + decoratedEntity = Entities.decorateEntity(currentEntity, entityType, this.packList, this.classList, this.subClasses); if (Filter.passesFilter(decoratedEntity, ActiveFilters)) { - compactItem = { + compactEntity = { _id: decoratedEntity._id, compendium: pack.collection, name: decoratedEntity.name, img: decoratedEntity.img, type: decoratedEntity.type, + dae: decoratedEntity.effects.size || false, + classRequirement: decoratedEntity.classRequirement, data: { level: decoratedEntity.data?.level, components: decoratedEntity.data?.components @@ -55,22 +65,25 @@ export default class Entities { break; case "feat": + decoratedEntity = Entities.decorateEntity(currentEntity, entityType, this.packList, this.classList, this.subClasses); if (["feat", "class"].includes(decoratedEntity.type) && Filter.passesFilter(decoratedEntity, ActiveFilters)) { - compactItem = { + compactEntity = { _id: decoratedEntity._id, compendium: pack.collection, name: decoratedEntity.name, img: decoratedEntity.img, type: decoratedEntity.type, + dae: decoratedEntity.effects.size || false, classRequirement: decoratedEntity.classRequirement }; } break; case "item": + decoratedEntity = Entities.decorateEntity(currentEntity, entityType, this.packList, this.classList, this.subClasses); //0.4.5: Itm type for true items could be many things (weapon, consumable, etc) so we just look for everything except spells, feats, classes if (!["spell", "feat", "class"].includes(decoratedEntity.type) && Filter.passesFilter(decoratedEntity, ActiveFilters)) { - compactItem = { + compactEntity = { _id: decoratedEntity._id, compendium: pack.collection, name: decoratedEntity.name, @@ -81,19 +94,20 @@ export default class Entities { ac: (decoratedEntity.data?.armor?.type) ? decoratedEntity.data?.armor?.value || false : false, }; - if (compactItem.type == 'weapon' && (decoratedEntity.data?.range?.value)) { - setProperty(compactItem, `tags.range`, decoratedEntity.data?.range?.value + decoratedEntity.data?.range?.units); + if (compactEntity.type == 'weapon' && (decoratedEntity.data?.range?.value)) { + setProperty(compactEntity, `tags.range`, decoratedEntity.data?.range?.value + decoratedEntity.data?.range?.units); } for (let filter of Object.values(ActiveFilters)) { - setProperty(compactItem, `tags.${filter.path}`, filter.value); + setProperty(compactEntity, `tags.${filter.path}`, filter.value); } } break; case "npc": + decoratedEntity = Entities.decorateEntity(currentEntity, entityType, this.packList, this.classList, this.subClasses); if (Filter.passesFilter(decoratedEntity, ActiveFilters)) { //0.4.2: Don't store all the details - just the display elements - compactItem = { + compactEntity = { _id: decoratedEntity._id, compendium: pack.collection, name: decoratedEntity.name, @@ -110,178 +124,29 @@ export default class Entities { break; } - if (compactItem) { //Indicates it passed the filters - compactItems.push(compactItem); + if (compactEntity) { //Indicates it passed the filters + compactList.entities.push(compactEntity); if (numItemsLoaded++ >= maxLoad) break; //0.4.2e: Update the UI (e.g. "Loading 142 spells") - if (updateLoading) { updateLoading(numItemsLoaded); } + if (updateLoading) { Renderer.updateLoading(numItemsLoaded,500,entityType); } } - } - }//for item5e of content + } //for item5e of content }); - }//end if pack entity === Item + } if (numItemsLoaded >= maxLoad) break; }//for packs - /* - if (unfoundSpells !== '') { - console.log(`Load and Fliter Items | List of Spells that don't have a class associated to them:`); - console.log(unfoundSpells); - } - */ this.itemsLoaded = true; console.timeEnd("loadAndFilterItems"); - console.log(`Load and Filter Items | Finished loading ${compactItems.length} ${entityType}s`); - return compactItems; - } - - async loadEntity(numToPreload = CMPBrowser.PRELOAD) { - console.log('Item Browser | Started loading items'); - console.time("loadItems"); - await this.checkListsLoaded(); - - this.itemsLoaded = false; - - - let unfoundSpells = ''; - let numSpellsLoaded = 0; - let numFeatsLoaded = 0; - let numItemsLoaded = 0; - let items = { - spells: {}, - feats: {}, - items: {} - }; - - - for (let pack of game.packs) { - if (pack['metadata']['entity'] === "Item" && this.settings.loadedSpellCompendium[pack.collection].load) { - await pack.getContent().then(content => { - for (let item5e of content) { - let item = item5e.data; - if (item.type === 'spell') { - //0.4.1 Only preload a limited number and fill more in as needed - if (numSpellsLoaded++ > numToPreload) continue; - - item.compendium = pack.collection; - - // determining classes that can use the spell - let cleanSpellName = item.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); - //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); - if (this.classList[cleanSpellName]) { - let classes = this.classList[cleanSpellName]; - item.data.classes = classes.split(','); - } else { - unfoundSpells += cleanSpellName + ','; - } - - // getting damage types - item.damageTypes = []; - if (item.data.damage && item.data.damage.parts.length > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (item.damageTypes.indexOf(type) === -1) { - item.damageTypes.push(type); - } - } - } - items.spells[(item._id)] = item; - } else if (item.type === 'feat' || item.type === 'class') { - //0.4.1 Only preload a limited number and fill more in as needed - if (numFeatsLoaded++ > numToPreload) continue; - - item.compendium = pack.collection; - // getting damage types - item.damageTypes = []; - if (item.data.damage && item.data.damage.parts.length > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (item.damageTypes.indexOf(type) === -1) { - item.damageTypes.push(type); - } - } - } - - // getting class - let reqString = item.data.requirements?.replace(/[0-9]/g, '').trim(); - let matchedClass = []; - for (let c in this.subClasses) { - if (reqString && reqString.toLowerCase().indexOf(c) !== -1) { - matchedClass.push(c); - } else { - for (let subClass of this.subClasses[c]) { - if (reqString && reqString.indexOf(subClass) !== -1) { - matchedClass.push(c); - break; - } - } - } - } - item.classRequirement = matchedClass; - item.classRequirementString = matchedClass.join(', '); - - // getting uses/ressources status - item.usesRessources = item5e.hasLimitedUses; - - item.hasSave = item5e.hasSave; - - items.feats[(item._id)] = item; - - } else { - //0.4.1 Only preload a limited number and fill more in as needed - if (numItemsLoaded++ > numToPreload) continue; - - item.compendium = pack.collection; - // getting damage types - item.damageTypes = []; - if (item.data.damage && item.data.damage.parts.size > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (item.damageTypes.indexOf(type) === -1) { - item.damageTypes.push(type); - } - } - } - - // getting pack - let matchedPacks = []; - for (let pack of Object.keys(this.packList)) { - for (let packItem of this.packList[pack]) { - if (item.name.toLowerCase() === packItem.toLowerCase()) { - matchedPacks.push(pack); - break; - } - } - } - item.matchedPacks = matchedPacks; - item.matchedPacksString = matchedPacks.join(', '); - - // getting uses/ressources status - item.usesRessources = item5e.hasLimitedUses - - items.items[(item._id)] = item; - } - - }//for item5e of content - }); - } - if ((numSpellsLoaded >= numToPreload) && (numFeatsLoaded >= numToPreload) && (numItemsLoaded >= numToPreload)) break; - }//for packs - if (unfoundSpells !== '') { - console.log(`Item Browser | List of Spells that don't have a class associated to them:`); - console.log(unfoundSpells); - } - this.itemsLoaded = true; - console.timeEnd("loadItems"); - console.log(`Item Browser | Finished loading items: ${Object.keys(items.spells).length} spells, ${Object.keys(items.feats).length} features, ${Object.keys(items.items).length} items `); - return items; + console.log(`Load and Filter Items | Finished loading ${compactList.length} ${entityType}s`); + return compactList; } static _sortList(list, entityType, orderBy) { if(entityType == 'npc'){ switch (orderBy) { case 'name': - list.sort((a, b) => { + list.entities.sort((a, b) => { let aName = a.name; let bName = b.name; if (aName < bName) return -1; @@ -289,7 +154,7 @@ export default class Entities { return 0; }); break; case 'cr': - list.sort((a, b) => { + list.entities.sort((a, b) => { let aVal = Number(a.orderCR); let bVal = Number(b.orderCR); if (aVal < bVal) return -1; @@ -303,7 +168,7 @@ export default class Entities { } }); break; case 'size': - list.sort((a, b) => { + list.entities.sort((a, b) => { let aVal = a.orderSize; let bVal = b.orderSize; if (aVal < bVal) return -1; @@ -319,7 +184,7 @@ export default class Entities { } } else { if (orderBy) { - list.sort((a, b) => { + list.entities.sort((a, b) => { let aName = a.name; let bName = b.name; if (aName < bName) return -1; @@ -333,7 +198,7 @@ export default class Entities { ['items', 'type'], ]); - list.sort((a, b) => { + list.entities.sort((a, b) => { let sort = defaultSort.get(entityType); let aVal = a[sort]; let bVal = b[sort]; @@ -375,6 +240,11 @@ export default class Entities { entityData.displayCR = cr; entityData.displaySize = 'unset'; entityData.filterSize = 2; + if (!entityData.data.details.type.value){ + let temp = entityData.data.details.type; + entityData.data.details.type = {value: temp}; + setProperty(entityData,'data.details.type', entityData.data.details.type); + } if (CONFIG.DND5E.actorSizes[entityData.data.traits.size] !== undefined) { entityData.displaySize = CONFIG.DND5E.actorSizes[entityData.data.traits.size]; } @@ -422,9 +292,7 @@ export default class Entities { //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); if (classList[cleanSpellName]) { let classes = classList[cleanSpellName]; - entityData.data.classes = classes.split(','); - } else { - //FIXME: unfoundSpells += cleanSpellName + ','; + entityData.classRequirement = classes.split(','); } } else if (entityData.type === 'feat' || entityData.type === 'class') { // getting class diff --git a/scripts/modules/filter.mjs b/scripts/modules/filter.mjs index e05d3ee..968cce5 100644 --- a/scripts/modules/filter.mjs +++ b/scripts/modules/filter.mjs @@ -70,6 +70,17 @@ export class Filter { return true; } + filterElements(list, subjects, filters) { + for (let element of list) { + let subject = subjects[element.dataset.entryId]; + if (this.passesFilter(subject, filters) == false) { + $(element).hide(); + } else { + $(element).show(); + } + } + } + getByName(name) { return getProperty(this, name); } @@ -92,6 +103,13 @@ export class Filter { }; } + async addEntityFilters(){ + await this.addSpellFilters(); + await this.addFeatFilters(); + await this.addItemFilters(); + await this.addNpcFilters(); + } + /** * Used to add custom filters to the Spell-Browser * @param {String} entityType type of entity for the filter diff --git a/scripts/modules/renderer.mjs b/scripts/modules/renderer.mjs new file mode 100644 index 0000000..2c54bc1 --- /dev/null +++ b/scripts/modules/renderer.mjs @@ -0,0 +1,40 @@ +export class Renderer { + constructor() { + this.currentLists = {}; + } + + /** + * Render the Information section + * + * @param messageElement + * @param itemType + * @param numLoaded + * @param maxLoaded + * @returns + */ + static async renderLoading(messageElement, itemType, numLoaded, maxLoaded = false) { + if (!messageElement) return; + + let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.html", { numLoaded: numLoaded, itemType: itemType, maxLoaded: maxLoaded }); + messageElement.innerHTML = loadingHTML; + } + + static async updateLoading(numLoaded = 0, maxLoad = 500, entityType) { + let loader = document.getElementById('CBInfoMessage'); + if (loader) { Renderer.renderLoading(loader, entityType, numLoaded, numLoaded >= maxLoad); } + } + + /** + * + * @param entityType + * @param updateLoading + * @returns {html} html rendered List of entities + */ + static async renderEntityList(entityList, updateLoading = null) { + return await renderTemplate(`modules/compendium-browser/template/entity-browser-list.html`, { listItems: entityList, }); + } + + static async renderFilters(filters){ + return await renderTemplate(`modules/compendium-browser/template/filter-container.html`, {filters: filters}); + } +} \ No newline at end of file diff --git a/scripts/modules/settings.mjs b/scripts/modules/settings.mjs index 855ba10..b25be45 100644 --- a/scripts/modules/settings.mjs +++ b/scripts/modules/settings.mjs @@ -19,7 +19,8 @@ export class ModuleSettings { let defaultSettings = { loadedCompendium: { Actor: {}, - Item: {} + Item: {}, + JournalEntry: {} } }; @@ -39,37 +40,10 @@ export class ModuleSettings { * * @returns {Array} Settings */ - initSettings() { + static initModuleSettings() { let defaultSettings = ModuleSettings._getDefaults(); - - // creating game setting container - this.gs.register(CMPBrowser.MODULE_NAME, SETTINGS, { - name: "Compendium Browser Settings", - hint: "Settings to exclude packs from loading and visibility of the browser", - default: defaultSettings, - type: Object, - scope: 'world', - onChange: settings => { - this.settings = settings; - } - }); - - this.gs.register(CMPBrowser.MODULE_NAME, "maxload", { - name: game.i18n.localize("CMPBrowser.SETTING.Maxload.NAME"), - hint: game.i18n.localize("CMPBrowser.SETTING.Maxload.HINT"), - scope: "world", - config: true, - default: CMPBrowser.MAXLOAD, - type: Number, - range: { // If range is specified, the resulting setting will be a range slider - min: 200, - max: 5000, - step: 100 - } - }); - // load settings from container and apply to default settings (available compendia might have changed) - let settings = this.gs.get(CMPBrowser.MODULE_NAME, SETTINGS); + let settings = game.settings.get(CMPBrowser.MODULE_NAME, SETTINGS); for (let compKey in defaultSettings.loadedSpellCompendium) { if (settings.loadedSpellCompendium[compKey] !== undefined) { defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; @@ -94,7 +68,46 @@ export class ModuleSettings { return defaultSettings; } + static registerGameSettings(){ + // creating game setting container + game.settings.register(CMPBrowser.MODULE_NAME, SETTINGS, { + name: "Compendium Browser Settings", + hint: "Settings to exclude packs from loading and visibility of the browser", + default: ModuleSettings._getDefaults(), + type: Object, + scope: 'world', + onChange: settings => { + this.settings = settings; + } + }); + + game.settings.register(CMPBrowser.MODULE_NAME, "maxload", { + name: game.i18n.localize("CMPBrowser.SETTING.Maxload.NAME"), + hint: game.i18n.localize("CMPBrowser.SETTING.Maxload.HINT"), + scope: "world", + config: true, + default: CMPBrowser.MAXLOAD, + type: Number, + range: { // If range is specified, the resulting setting will be a range slider + min: 200, + max: 5000, + step: 100 + } + }); + } + static saveSettings() { game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings); } + + filterElements(list, subjects, filters) { + for (let element of list) { + let subject = subjects[element.dataset.entryId]; + if (this.passesFilter(subject, filters) == false) { + $(element).hide(); + } else { + $(element).show(); + } + } + } } \ No newline at end of file diff --git a/styles/compendium-browser.css b/styles/compendium-browser.css index 49190a0..1f12f89 100644 --- a/styles/compendium-browser.css +++ b/styles/compendium-browser.css @@ -170,9 +170,7 @@ display: inline-block; min-width: 15px; } -.compendium-browser .item-browser > li, -.compendium-browser .feat-browser > li, -.compendium-browser .spell-browser > li { +.compendium-browser .entity-browser > li { cursor: default; vertical-align: middle; line-height: 32px; @@ -183,73 +181,49 @@ background: rgba(0, 0, 0, 0.05); } -.compendium-browser .browser li .item-name { +.compendium-browser .browser li .entity-name { padding-left: 5px; } -.compendium-browser .item-browser li .feat-tags, -.compendium-browser .feat-browser li .feat-tags, -.compendium-browser .spell-browser li .feat-tags, -.compendium-browser .item-browser li .item-tags, -.compendium-browser .feat-browser li .item-tags, -.compendium-browser .spell-browser li .item-tags { +.compendium-browser .entity-browser li .feat-tags, +.compendium-browser .entity-browser li .item-tags { text-align: justify; margin-right: 3px; margin-left: 3px; text-transform: capitalize; } -.compendium-browser .spell-browser .spell .spell-level { +.compendium-browser .spell-browser .entity .spell-level { text-align: center; font-weight: 900; max-width: 18px; height: 32px; } -.compendium-browser .spell-browser .spell .spell-tags { +.compendium-browser .spell-browser .entity .spell-tags { text-align: right; margin-right: 3px; font-weight: 900; max-width: 100px; height: 32px; } -.compendium-browser .npc-browser .npc { - cursor: default; - vertical-align: middle; - line-height: 64px; - margin: 4px 0; -} -.compendium-browser .npc-browser .npc .npc-image { + +.compendium-browser .entity-browser .entity .npc-image { max-width: 64px; height: 64px; } -.compendium-browser .npc-browser .npc .npc-image img { +.compendium-browser .entity-browser .npc .npc-image img { width: 64px; height: 64px; border: none; object-fit: contain; } -.compendium-browser .npc-browser .npc .npc-line { - line-height: 25px; - padding: 9px 0 5px 5px; -} -.compendium-browser .npc-browser .npc .npc-name { - font-weight: bold; - font-size: 16px; -} -.compendium-browser .npc-browser .npc .cr { - display: inline-block; - width: 55px; -} -.compendium-browser .npc-browser .npc .size { - display: inline-block; - width: 75px; -} -.compendium-browser .npc-browser .npc .type { - display: inline-block; -} + .compendium-browser .settings .settings-group { border: 1px solid #bbb; border-radius: 5px; margin-top: 5px; padding: 2px; + font-size: 0.9em; + max-width: fit-content; + float: left; } .compendium-browser .settings .settings-group h3 { margin: 0; @@ -290,7 +264,7 @@ border-radius: 0; } -.compendium-browser .cb_entities .item { +.compendium-browser .cb_entities .entity { border-left: 0.3em solid #ccc; border-bottom: 1px dotted #ccc; line-height: 1em; @@ -299,8 +273,8 @@ grid-template-rows: 1fr 1fr; gap: 0px 0px; grid-template-areas: - "Image Name Tags" - "Image ItemTags Tags"; + "Image Name SpellTags" + "Image EntityTags MiscTags"; } .compendium-browser .cb_entities ul.tags { @@ -309,16 +283,13 @@ box-sizing: content-box; display: inline-block; } -.compendium-browser .cb_entities .item-tags{ +.compendium-browser .cb_entities .entity-tags{ padding-left: 0.5em; - grid-area: ItemTags; -} - -.compendium-browser .cb_entities .spell-tags, -.compendium-browser .cb_entities .misc-tags { - grid-area: Tags; + grid-area: EntityTags; } +.compendium-browser .cb_entities .spell-tags { grid-area: SpellTags; } +.compendium-browser .cb_entities .misc-tags { grid-area: MiscTags;} .compendium-browser .cb_entities .tags li{ font-size: 0.8em; line-height: initial; @@ -329,49 +300,46 @@ line-height: initial; } -.compendium-browser .cb_entities .item-image { +.compendium-browser .cb_entities .entity-image { grid-area: Image; - width: 100%; - height: 100%; + width: 48px; + height: 48px; object-fit: cover; margin: 0; padding: 0; } -.compendium-browser .cb_entities .item-name { +.compendium-browser .cb_entities .entity-name { grid-area: Name; font-size: 1.2em; line-height: initial; display: inline; } -.compendium-browser .cb_entities .item.r_common { - border-left: 0.3em solid #fff; -} -.compendium-browser .cb_entities .item.r_uncommon { - border-color: rgb(30,255,0); -} -.compendium-browser .cb_entities .item.r_uncommon .item-name, -.compendium-browser .cb_entities .item.r_uncommon .tags { +.compendium-browser .cb_entities .entity.r_common {border-left: 0.3em solid #fff;} + +.compendium-browser .cb_entities .entity.r_uncommon {border-color: rgb(30,255,0);} +.compendium-browser .cb_entities .entity.r_uncommon .entity-name, +.compendium-browser .cb_entities .entity.r_uncommon .tags { background: linear-gradient(90deg, rgba(30,255,0,1) 0%, rgba(30,255,0,0) 35%); } -.compendium-browser .cb_entities .item.r_rare {border-color: darkblue;} +.compendium-browser .cb_entities .entity.r_rare {border-color: darkblue;} -.compendium-browser .cb_entities .item.r_rare .item-name, -.compendium-browser .cb_entities .item.r_rare .item-info { +.compendium-browser .cb_entities .entity.r_rare .entity-name, +.compendium-browser .cb_entities .entity.r_rare .entity-info { background: linear-gradient(90deg, rgba(0,112,221,1) 0%, rgba(0,112,221,0) 35%); } -.compendium-browser .cb_entities .item.r_veryrare {border-color: #a335ee;} +.compendium-browser .cb_entities .entity.r_veryrare {border-color: #a335ee;} -.compendium-browser .cb_entities .item.r_veryrare .item-name, -.compendium-browser .cb_entities .item.r_veryrare .item-info { +.compendium-browser .cb_entities .entity.r_veryrare .item-name, +.compendium-browser .cb_entities .entity.r_veryrare entity-info { background: linear-gradient(90deg, rgba(163,53,238,1) 0%, rgba(163,53,238,0) 35%); } -.compendium-browser .cb_entities .item.r_legendary {border-color: rgb(255,128,0);} +.compendium-browser .cb_entities .entity.r_legendary {border-color: rgb(255,128,0);} -.compendium-browser .cb_entities .item.r_legendary .item-name, -.compendium-browser .cb_entities .item.r_legendary .item-info { +.compendium-browser .cb_entities .entity.r_legendary .entity-name, +.compendium-browser .cb_entities .entity.r_legendary .entity-info { background: linear-gradient(90deg, rgba(255,128,0,1) 0%, rgba(255,128,0,0) 35%); } -.compendium-browser .cb_entities .item.r_rare .item-name{color: inherit;} +.compendium-browser .cb_entities .entity.r_rare .entity-name{color: inherit;} diff --git a/template/entity-browser-list.html b/template/entity-browser-list.html index faa0569..7b736c9 100644 --- a/template/entity-browser-list.html +++ b/template/entity-browser-list.html @@ -1,20 +1,20 @@ {{#each listItems as |entity|}} -
  • -
    - +
    +
    -
    - {{entity.name}} + -
      +
      • {{#if entity.type}}
      • @@ -25,7 +25,7 @@ {{#case 'tool'}}{{/case}} {{#case 'loot'}}{{/case}} {{#case 'weapon'}}{{/case}} - {{#default entity.type}}{{entity.type}}{{/default}} + {{#default ''}}{{entity.type}}{{/default}} {{/switch}}
      • {{/if}} diff --git a/template/entity-browser.html b/template/entity-browser.html new file mode 100644 index 0000000..b9a8916 --- /dev/null +++ b/template/entity-browser.html @@ -0,0 +1,22 @@ +
        +
        +
        +
        + +
        +
        +
        {{localize "CMPBrowser.sortBy"}}:
        +
        +
        + +
        + {{> "modules/compendium-browser/template/filter-container.html" filters=filters}} +
        +
        +
          +
          +
          \ No newline at end of file diff --git a/template/feat-browser.html b/template/feat-browser.html index 6f4c330..7084668 100644 --- a/template/feat-browser.html +++ b/template/feat-browser.html @@ -16,7 +16,6 @@ {{> "modules/compendium-browser/template/filter-container.html" filters=featFilters}}
      -
        {{> "modules/compendium-browser/template/entity-browser-list.html" feats=items}}
      diff --git a/template/item-browser.html b/template/item-browser.html index b5ba4a9..3e34b22 100644 --- a/template/item-browser.html +++ b/template/item-browser.html @@ -16,8 +16,7 @@ {{> "modules/compendium-browser/template/filter-container.html" filters=itemFilters}}
      - -
        +
          {{> "modules/compendium-browser/template/entity-browser-list.html" items=items}}
      diff --git a/template/loading.html b/template/loading.html index c59a9cf..f52f126 100644 --- a/template/loading.html +++ b/template/loading.html @@ -1,8 +1,5 @@ \ No newline at end of file diff --git a/template/npc-browser.html b/template/npc-browser.html index fcb51b8..be0f8bf 100644 --- a/template/npc-browser.html +++ b/template/npc-browser.html @@ -17,7 +17,6 @@ {{> "modules/compendium-browser/template/filter-container.html" filters=npcFilters}}
      -
        {{> "modules/compendium-browser/template/entity-browser-list.html" npcs=npcs}}
      diff --git a/template/spell-browser.html b/template/spell-browser.html index 00fa34d..df05ee4 100644 --- a/template/spell-browser.html +++ b/template/spell-browser.html @@ -16,9 +16,8 @@ {{> "modules/compendium-browser/template/filter-container.html" filters=spellFilters}}
      -
        - {{> "modules/compendium-browser/template/entity-browser-list.html" spells=items}} + {{> "modules/compendium-browser/template/entity-browser-list.html" listItems=items}}
      \ No newline at end of file diff --git a/template/template.html b/template/template.html index 8d40acc..a0810ef 100644 --- a/template/template.html +++ b/template/template.html @@ -1,23 +1,24 @@
      - {{#if showSpellBrowser}}{{localize "CMPBrowser.Tab.SpellBrowser"}}{{/if}} + {{#if showSpellBrowser}}🧚{{/if}} {{#if showFeatBrowser}}{{localize "CMPBrowser.Tab.FeatBrowser"}}{{/if}} - {{#if showItemBrowser}}{{localize "CMPBrowser.Tab.ItemBrowser"}}{{/if}} - {{#if showNpcBrowser}}{{localize "CMPBrowser.Tab.NPCBrowser"}}{{/if}} - {{#if isGM}}{{localize "CMPBrowser.Tab.Settings"}}{{/if}} + {{#if showItemBrowser}}🔮{{/if}} + {{#if showNpcBrowser}}👥{{/if}} + {{#if isGM}}⚙️{{/if}}
      -
      {{#if showSpellBrowser}}{{> "modules/compendium-browser/template/spell-browser.html"}}{{/if}}
      -
      {{#if showFeatBrowser}}{{> "modules/compendium-browser/template/feat-browser.html"}}{{/if}}
      -
      {{#if showItemBrowser}}{{> "modules/compendium-browser/template/item-browser.html"}}{{/if}}
      -
      {{#if showNpcBrowser}} {{> "modules/compendium-browser/template/npc-browser.html"}}{{/if}}
      +
      {{#if showSpellBrowser}}{{> "modules/compendium-browser/template/entity-browser.html"}}{{/if}}
      +
      {{#if showFeatBrowser}}{{> "modules/compendium-browser/template/entity-browser.html"}}{{/if}}
      +
      {{#if showItemBrowser}}{{> "modules/compendium-browser/template/entity-browser.html"}}{{/if}}
      +
      {{#if showNpcBrowser}} {{> "modules/compendium-browser/template/entity-browser.html"}}{{/if}}
      {{#if isGM}}
      {{> "modules/compendium-browser/template/settings.html"}}
      {{/if}} From 67131e2c87cb242503360ce296ff75078d8282a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20B=C3=B6ttner?= Date: Wed, 28 Jul 2021 11:20:15 +0200 Subject: [PATCH 03/13] WIP: module refactoring --- .eslintrc | 49 + .gitattributes | 2 + .github/FUNDING.yml | 4 +- .github/ISSUE_TEMPLATE/bug_report.md | 38 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/workflows/release.yml | 38 + .jshintrc | 3 + .vscode/settings.json | 21 + compendium-browser.js | 6 +- data/filter/spellfilter.json | 15 + dist/compendium-browser.js | 5 + dist/data/item-packs.json | 10 + dist/data/spell-classes.json | 481 +++ dist/data/sub-classes.json | 15 + dist/hooks/events.js | 210 ++ .../mainHook.js => dist/hooks/moduleHooks.js | 41 +- dist/lang/de.json | 74 + dist/lang/en.json | 73 + dist/lang/fr.json | 65 + dist/lang/ja.json | 60 + dist/lang/pt-BR.json | 61 + dist/module.json | 66 + dist/scripts/classes/compactEntity.js | 18 + dist/scripts/classes/compactList.js | 43 + dist/scripts/classes/decoratedEntity.js | 164 + dist/scripts/classes/filter.js | 44 + dist/scripts/compendium-browser.js | 189 ++ dist/scripts/modules/entities.js | 181 ++ dist/scripts/modules/exporter.js | 34 + dist/scripts/modules/filter.js | 304 ++ dist/scripts/modules/renderer.js | 69 + dist/scripts/modules/settings.js | 93 + dist/scripts/versioning/version-check.js | 32 + dist/styles/compendium-browser.css | 382 +++ dist/styles/compendium-browser.min.css | 1 + dist/template/entity-browser.hbs | 9 + dist/template/entity-list.hbs | 60 + dist/template/filter-container.hbs | 111 + .../loading.html => dist/template/loading.hbs | 2 +- dist/template/settings.hbs | 66 + dist/template/template.hbs | 28 + docs/Filter.html | 1388 +++++++++ docs/VersionCheck.html | 301 ++ docs/assets/css/main.css | 2660 +++++++++++++++++ docs/assets/images/icons.png | Bin 0 -> 9615 bytes docs/assets/images/icons@2x.png | Bin 0 -> 28144 bytes docs/assets/images/widgets.png | Bin 0 -> 480 bytes docs/assets/images/widgets@2x.png | Bin 0 -> 855 bytes docs/assets/js/main.js | 248 ++ docs/assets/js/search.js | 1 + docs/dist_hooks - Kopie_events.js.html | 251 ++ docs/dist_hooks - Kopie_moduleHooks.js.html | 140 + docs/dist_hooks_events.js.html | 261 ++ docs/dist_hooks_moduleHooks.js.html | 140 + docs/dist_scripts_classes_compactList.js.html | 94 + ...st_scripts_classes_decoratedEntity.js.html | 215 ++ docs/dist_scripts_classes_filter.js.html | 95 + docs/dist_scripts_compendium-browser.js.html | 240 ++ docs/dist_scripts_modules_entities.js.html | 232 ++ docs/dist_scripts_modules_exporter.js.html | 85 + docs/dist_scripts_modules_filter.js.html | 355 +++ docs/dist_scripts_modules_renderer.js.html | 120 + docs/dist_scripts_modules_settings.js.html | 144 + ...t_scripts_versioning_version-check.js.html | 83 + docs/fonts/OpenSans-Bold-webfont.eot | Bin 0 -> 19544 bytes docs/fonts/OpenSans-Bold-webfont.svg | 1830 ++++++++++++ docs/fonts/OpenSans-Bold-webfont.woff | Bin 0 -> 22432 bytes docs/fonts/OpenSans-BoldItalic-webfont.eot | Bin 0 -> 20133 bytes docs/fonts/OpenSans-BoldItalic-webfont.svg | 1830 ++++++++++++ docs/fonts/OpenSans-BoldItalic-webfont.woff | Bin 0 -> 23048 bytes docs/fonts/OpenSans-Italic-webfont.eot | Bin 0 -> 20265 bytes docs/fonts/OpenSans-Italic-webfont.svg | 1830 ++++++++++++ docs/fonts/OpenSans-Italic-webfont.woff | Bin 0 -> 23188 bytes docs/fonts/OpenSans-Light-webfont.eot | Bin 0 -> 19514 bytes docs/fonts/OpenSans-Light-webfont.svg | 1831 ++++++++++++ docs/fonts/OpenSans-Light-webfont.woff | Bin 0 -> 22248 bytes docs/fonts/OpenSans-LightItalic-webfont.eot | Bin 0 -> 20535 bytes docs/fonts/OpenSans-LightItalic-webfont.svg | 1835 ++++++++++++ docs/fonts/OpenSans-LightItalic-webfont.woff | Bin 0 -> 23400 bytes docs/fonts/OpenSans-Regular-webfont.eot | Bin 0 -> 19836 bytes docs/fonts/OpenSans-Regular-webfont.svg | 1831 ++++++++++++ docs/fonts/OpenSans-Regular-webfont.woff | Bin 0 -> 22660 bytes docs/hooks_events.js.html | 314 ++ docs/hooks_moduleHooks.js.html | 152 + docs/index.html | 271 ++ docs/module.exports.html | 301 ++ docs/modules.html | 89 + docs/scripts/linenumber.js | 25 + docs/scripts/prettify/Apache-License-2.0.txt | 202 ++ docs/scripts/prettify/lang-css.js | 2 + docs/scripts/prettify/prettify.js | 28 + docs/scripts_compendium-browser.js.html | 265 ++ .../scripts_modules_exporter.js.html | 56 +- docs/scripts_modules_settings.js.html | 155 + docs/scripts_versioning_version-check.js.html | 89 + docs/styles/jsdoc-default.css | 358 +++ docs/styles/prettify-jsdoc.css | 111 + docs/styles/prettify-tomorrow.css | 132 + hooks/events.js | 264 ++ hooks/events.mjs | 232 -- hooks/moduleHooks.js | 102 + jsdoc.json | 26 + lang/de.json | 35 +- lang/en.json | 28 +- lang/fr.json | 121 +- lang/ja.json | 2 +- lang/pt-BR.json | 2 +- module.json | 2 +- package.json | 32 + scripts/classes/compactEntity.ts | 26 + scripts/classes/compactList.ts | 53 + scripts/classes/decoratedEntity.ts | 177 ++ scripts/classes/filter.ts | 47 + scripts/compendium-browser.js | 156 +- scripts/modules/entities.mjs | 370 --- scripts/modules/entities.ts | 195 ++ scripts/modules/exporter.ts | 48 + scripts/modules/{filter.mjs => filter.ts} | 193 +- scripts/modules/renderer.mjs | 40 - scripts/modules/renderer.ts | 73 + scripts/modules/{settings.mjs => settings.js} | 36 +- scripts/versioning/version-check.js | 39 + styles/app.css | 103 + styles/app.less | 114 + styles/compendium-browser.css | 344 +-- styles/compendium-browser.less | 293 +- styles/components/entity-browser.css | 45 + styles/components/entity-browser.less | 183 ++ styles/filters/filters.css | 113 + styles/filters/filters.less | 119 + styles/mixins.less | 0 styles/themes/README.md | 23 + styles/themes/dark.css | 29 + styles/themes/dark.less | 31 + styles/themes/default.css | 63 + styles/themes/default.less | 63 + styles/variables.css | 95 + styles/variables.less | 44 + template/entity-browser-list.html | 60 - template/entity-browser.hbs | 9 + template/entity-browser.html | 22 - template/entity-list.hbs | 60 + template/feat-browser.html | 24 - template/filter-container.hbs | 111 + template/filter-container.html | 79 - template/item-browser.html | 23 - template/loading.hbs | 5 + template/npc-browser.html | 24 - template/settings.hbs | 66 + template/settings.html | 49 - template/spell-browser.html | 23 - template/template.hbs | 28 + template/template.html | 26 - tsconfig.json | 47 + 154 files changed, 26488 insertions(+), 1906 deletions(-) create mode 100644 .eslintrc create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/release.yml create mode 100644 .jshintrc create mode 100644 .vscode/settings.json create mode 100644 data/filter/spellfilter.json create mode 100644 dist/compendium-browser.js create mode 100644 dist/data/item-packs.json create mode 100644 dist/data/spell-classes.json create mode 100644 dist/data/sub-classes.json create mode 100644 dist/hooks/events.js rename hooks/mainHook.js => dist/hooks/moduleHooks.js (71%) create mode 100644 dist/lang/de.json create mode 100644 dist/lang/en.json create mode 100644 dist/lang/fr.json create mode 100644 dist/lang/ja.json create mode 100644 dist/lang/pt-BR.json create mode 100644 dist/module.json create mode 100644 dist/scripts/classes/compactEntity.js create mode 100644 dist/scripts/classes/compactList.js create mode 100644 dist/scripts/classes/decoratedEntity.js create mode 100644 dist/scripts/classes/filter.js create mode 100644 dist/scripts/compendium-browser.js create mode 100644 dist/scripts/modules/entities.js create mode 100644 dist/scripts/modules/exporter.js create mode 100644 dist/scripts/modules/filter.js create mode 100644 dist/scripts/modules/renderer.js create mode 100644 dist/scripts/modules/settings.js create mode 100644 dist/scripts/versioning/version-check.js create mode 100644 dist/styles/compendium-browser.css create mode 100644 dist/styles/compendium-browser.min.css create mode 100644 dist/template/entity-browser.hbs create mode 100644 dist/template/entity-list.hbs create mode 100644 dist/template/filter-container.hbs rename template/loading.html => dist/template/loading.hbs (80%) create mode 100644 dist/template/settings.hbs create mode 100644 dist/template/template.hbs create mode 100644 docs/Filter.html create mode 100644 docs/VersionCheck.html create mode 100644 docs/assets/css/main.css create mode 100644 docs/assets/images/icons.png create mode 100644 docs/assets/images/icons@2x.png create mode 100644 docs/assets/images/widgets.png create mode 100644 docs/assets/images/widgets@2x.png create mode 100644 docs/assets/js/main.js create mode 100644 docs/assets/js/search.js create mode 100644 docs/dist_hooks - Kopie_events.js.html create mode 100644 docs/dist_hooks - Kopie_moduleHooks.js.html create mode 100644 docs/dist_hooks_events.js.html create mode 100644 docs/dist_hooks_moduleHooks.js.html create mode 100644 docs/dist_scripts_classes_compactList.js.html create mode 100644 docs/dist_scripts_classes_decoratedEntity.js.html create mode 100644 docs/dist_scripts_classes_filter.js.html create mode 100644 docs/dist_scripts_compendium-browser.js.html create mode 100644 docs/dist_scripts_modules_entities.js.html create mode 100644 docs/dist_scripts_modules_exporter.js.html create mode 100644 docs/dist_scripts_modules_filter.js.html create mode 100644 docs/dist_scripts_modules_renderer.js.html create mode 100644 docs/dist_scripts_modules_settings.js.html create mode 100644 docs/dist_scripts_versioning_version-check.js.html create mode 100644 docs/fonts/OpenSans-Bold-webfont.eot create mode 100644 docs/fonts/OpenSans-Bold-webfont.svg create mode 100644 docs/fonts/OpenSans-Bold-webfont.woff create mode 100644 docs/fonts/OpenSans-BoldItalic-webfont.eot create mode 100644 docs/fonts/OpenSans-BoldItalic-webfont.svg create mode 100644 docs/fonts/OpenSans-BoldItalic-webfont.woff create mode 100644 docs/fonts/OpenSans-Italic-webfont.eot create mode 100644 docs/fonts/OpenSans-Italic-webfont.svg create mode 100644 docs/fonts/OpenSans-Italic-webfont.woff create mode 100644 docs/fonts/OpenSans-Light-webfont.eot create mode 100644 docs/fonts/OpenSans-Light-webfont.svg create mode 100644 docs/fonts/OpenSans-Light-webfont.woff create mode 100644 docs/fonts/OpenSans-LightItalic-webfont.eot create mode 100644 docs/fonts/OpenSans-LightItalic-webfont.svg create mode 100644 docs/fonts/OpenSans-LightItalic-webfont.woff create mode 100644 docs/fonts/OpenSans-Regular-webfont.eot create mode 100644 docs/fonts/OpenSans-Regular-webfont.svg create mode 100644 docs/fonts/OpenSans-Regular-webfont.woff create mode 100644 docs/hooks_events.js.html create mode 100644 docs/hooks_moduleHooks.js.html create mode 100644 docs/index.html create mode 100644 docs/module.exports.html create mode 100644 docs/modules.html create mode 100644 docs/scripts/linenumber.js create mode 100644 docs/scripts/prettify/Apache-License-2.0.txt create mode 100644 docs/scripts/prettify/lang-css.js create mode 100644 docs/scripts/prettify/prettify.js create mode 100644 docs/scripts_compendium-browser.js.html rename scripts/modules/exporter.mjs => docs/scripts_modules_exporter.js.html (56%) create mode 100644 docs/scripts_modules_settings.js.html create mode 100644 docs/scripts_versioning_version-check.js.html create mode 100644 docs/styles/jsdoc-default.css create mode 100644 docs/styles/prettify-jsdoc.css create mode 100644 docs/styles/prettify-tomorrow.css create mode 100644 hooks/events.js delete mode 100644 hooks/events.mjs create mode 100644 hooks/moduleHooks.js create mode 100644 jsdoc.json create mode 100644 package.json create mode 100644 scripts/classes/compactEntity.ts create mode 100644 scripts/classes/compactList.ts create mode 100644 scripts/classes/decoratedEntity.ts create mode 100644 scripts/classes/filter.ts delete mode 100644 scripts/modules/entities.mjs create mode 100644 scripts/modules/entities.ts create mode 100644 scripts/modules/exporter.ts rename scripts/modules/{filter.mjs => filter.ts} (64%) delete mode 100644 scripts/modules/renderer.mjs create mode 100644 scripts/modules/renderer.ts rename scripts/modules/{settings.mjs => settings.js} (80%) create mode 100644 scripts/versioning/version-check.js create mode 100644 styles/app.css create mode 100644 styles/app.less create mode 100644 styles/components/entity-browser.css create mode 100644 styles/components/entity-browser.less create mode 100644 styles/filters/filters.css create mode 100644 styles/filters/filters.less create mode 100644 styles/mixins.less create mode 100644 styles/themes/README.md create mode 100644 styles/themes/dark.css create mode 100644 styles/themes/dark.less create mode 100644 styles/themes/default.css create mode 100644 styles/themes/default.less create mode 100644 styles/variables.css create mode 100644 styles/variables.less delete mode 100644 template/entity-browser-list.html create mode 100644 template/entity-browser.hbs delete mode 100644 template/entity-browser.html create mode 100644 template/entity-list.hbs delete mode 100644 template/feat-browser.html create mode 100644 template/filter-container.hbs delete mode 100644 template/filter-container.html delete mode 100644 template/item-browser.html create mode 100644 template/loading.hbs delete mode 100644 template/npc-browser.html create mode 100644 template/settings.hbs delete mode 100644 template/settings.html delete mode 100644 template/spell-browser.html create mode 100644 template/template.hbs delete mode 100644 template/template.html create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..eaacda4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,49 @@ +{ + "env": { + "es6": true, + "es2017": true, + "es2020": true, + "node": true + }, + "extends": ["airbnb-base"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 8, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": { + "arrow-body-style": 0, + "comma-dangle": 0, + "linebreak-style": ["error", "windows"], + "indent": 0, + "import/extensions": ["error", "never"], + "no-async-promise-executor": 0, + "no-await-in-loop": 0, + "no-console": 0, + "no-empty-function": 0, + "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], + "no-restricted-syntax": 0, + "no-tabs": 0, + "no-trailing-spaces": 0, + "no-unused-vars": 0, + "no-useless-constructor": 0, + "@typescript-eslint/indent": ["error", "tab"], + "@typescript-eslint/no-empty-function": ["error"], + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-useless-constructor": ["error"] + }, + "settings": { + "import/resolver": { + "node": { + "paths": ["src"], + "extensions": [".ts",".mjs"] + } + } + } +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 5c37172..5a8e98b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,5 @@ README.md export-ignore preview.jpg export-ignore styles/compendium-browser.less export-ignore Patchnotes.md export-ignore +.vscode export-ignore +docs export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 09e34d7..bcde3ee 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -github: syl3r86 -custom: ["paypal.me/syl3r86"] +github: DanielBoettner +custom: ["https://paypal.me/pools/c/8B3yZd2Z2f"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..032ba01 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +on: + push: + # Sequence of patterns matched against refs/tags + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +name: Upload Release Asset + +jobs: + build: + name: Upload Release Asset + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Zip files + run: | + zip -r compendium-browser ./dist/ * -x "img/*" ".github/*" ".idea/*" "tsconfig.json" + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./compendium-browser.zip + asset_name: compendium-browser.zip + asset_content_type: application/zip diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..56e07c6 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "esversion": 11 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9941887 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "typescript.tsdk": "C:\\Users\\jackp\\AppData\\Local\\FoundryVTT\\Data\\systems\\dnd5e\\module", + "debug.javascript.usePreview": false, + "triggerTaskOnSave.tasks": { + "watch: reload": [ + "dist/**/*.js", + "dist/templates/**/*.hbs" + ], + "watch: less": [ + "/styles/*.less" + ] + }, + "triggerTaskOnSave.showStatusBarToggle": true, + "launches": { + "fvtt-debug": "FoundryVTT" + }, + "includePath": [ + "C:\\Program Files\\FoundryVTT\\resources\\app", + "C:\\Users\\jackp\\AppData\\Local\\FoundryVTT\\Data\\systems\\dnd5e\\module" + ] +} \ No newline at end of file diff --git a/compendium-browser.js b/compendium-browser.js index dbd6438..ac47d97 100644 --- a/compendium-browser.js +++ b/compendium-browser.js @@ -1,6 +1,6 @@ -import { moduleHooks } from './hooks/mainHook.js'; -//import { Settings } from './scripts/modules/settings.mjs'; - +'use strict'; +/* jshint node: true */ +import moduleHooks from './hooks/moduleHooks.js'; moduleHooks.onInit(); moduleHooks.onReady(); diff --git a/data/filter/spellfilter.json b/data/filter/spellfilter.json new file mode 100644 index 0000000..82be27a --- /dev/null +++ b/data/filter/spellfilter.json @@ -0,0 +1,15 @@ +{ + "filters": { + "general": [ + {"label": "DND5E.Source","path": "data.source","type": "text"} + {"label": "CMPBrowser.lvl" ,"path": "data.level","type": "multiSelect", "value": "['CMPBrowser.cantrip', 1, 2, 3, 4, 5, 6, 7, 8, 9]"} + ], + "components": [ + {"label": "CMPBrowser.ritual", "path": "data.components.ritual", "type": "boolean"}, + {"label": "CMPBrowser.concentration", "path": "data.components.concentration", "type": "boolean"}, + {"label": "CMPBrowser.vocal", "path": "data.components.vocal", "type": "boolean"}, + {"label": "CMPBrowser.somatic", "path": "data.components.somatic", "type": "boolean"}, + {"label": "CMPBrowser.material", "path": "data.components.material", "type": "boolean"} + ] + } +} \ No newline at end of file diff --git a/dist/compendium-browser.js b/dist/compendium-browser.js new file mode 100644 index 0000000..98f1759 --- /dev/null +++ b/dist/compendium-browser.js @@ -0,0 +1,5 @@ +'use strict'; +/* jshint node: true */ +import moduleHooks from './hooks/moduleHooks.js'; +moduleHooks.onInit(); +moduleHooks.onReady(); diff --git a/dist/data/item-packs.json b/dist/data/item-packs.json new file mode 100644 index 0000000..1a161e3 --- /dev/null +++ b/dist/data/item-packs.json @@ -0,0 +1,10 @@ +{ + "burglar": ["Burglar's Pack", "Backpack", "Ball Bearings", "String", "Bell", "Candle", "Crowbar", "Hammer", "Piton", "Hooded Lantern", "Oil Flask", "Rations", "Tinderbox", "Waterskin", "Hempen Rope (50 ft.)"], + "diplomat": ["Diplomat's Pack", "Chest", "Map or Scroll Case", "Fine Clothes", "Ink Bottle", "Ink Pen", "Lamp", "Oil Flask", "Paper", "Perfume", "Sealing Wax", "Soap"], + "dungeoneer": ["Dungeoneer's Pack", "Backpack", "Crowbar", "Hammer", "Piton", "Torch", "Tinderbox", "Rations", "Waterskin", "Hempen Rope (50 ft.)"], + "entertainer": ["Entertainer's Pack", "Backpack", "Bedroll", "Costume Clothes", "Candle", "Rations", "Waterskin", "Disguise Kit"], + "explorer": ["Explorer's Pack", "Backpack", "Bedroll", "Mess Kit", "Tinderbox", "Torch", "Rations", "Waterskin", "Hempen Rope (50 ft.)"], + "monsterhunter": ["Monster Hunter's Pack", "Chest", "Crowbar", "Hammer", "Wooden Stake", "Holy Symbol", "Flask of Holy Water", "Manacles", "Steel Mirror", "Oil Flask", "Tinderbox", "Torch"], + "priest": ["Priest's Pack", "Backpack", "Blanket", "Candle", "Tinderbox", "Alms Box", "Block of Incense", "Censor", "Vestments", "Rations", "Waterskin"], + "scholar": ["Scholar's Pack", "Backpack", "Book of Lore", "Ink Bottle", "Ink Pen", "Parchment", "Bag of Sand", "Small Knife"] +} diff --git a/dist/data/spell-classes.json b/dist/data/spell-classes.json new file mode 100644 index 0000000..ca575e7 --- /dev/null +++ b/dist/data/spell-classes.json @@ -0,0 +1,481 @@ +{ + "abidalzimshorridwilting": "sorcerer,wizard", + "absorbelements": "artificer,druid,ranger,sorcerer,wizard", + "arcaneweapon": "artificerrevisited", + "acidsplash": "artificer,sorcerer,wizard,artificerrevisited", + "aganazzarsscorcher": "sorcerer,wizard", + "aid": "artificer,cleric,paladin,artificer,artificerrevisited", + "alarm": "artificer,ranger,wizard,artificer,artificerrevisited", + "alterself": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "animalfriendship": "bard,druid,ranger", + "animalmessenger": "bard,druid,ranger", + "animalshapes": "druid", + "animatedead": "cleric,wizard", + "animateobjects": "artificer,bard,sorcerer,wizard,artificerrevisited", + "antilifeshell": "druid", + "antimagicfield": "cleric,wizard", + "antipathysympathy": "druid,wizard", + "arcaneeye": "artificer,wizard,artificer,artificerrevisited", + "arcanegate": "sorcerer,warlock,wizard", + "arcanelock": "artificer,wizard,artificer,artificerrevisited", + "armorofagathys": "warlock", + "armsofhadar": "warlock", + "astralprojection": "cleric,warlock,wizard", + "augury": "cleric", + "auraoflife": "paladin", + "auraofpurity": "paladin", + "auraofvitality": "paladin", + "awaken": "bard,druid", + "bane": "bard,cleric", + "banishingsmite": "paladin", + "banishment": "cleric,paladin,sorcerer,warlock,wizard", + "barkskin": "druid,ranger", + "beaconofhope": "cleric", + "beastbond": "druid,ranger", + "beastsense": "druid,ranger", + "bestowcurse": "bard,cleric,wizard", + "bigbyshand": "artificer,wizard,artificerrevisited", + "arcanehand": "wizard,artificerrevisited", + "bladebarrier": "cleric", + "bladeward": "bard,sorcerer,warlock,wizard", + "bless": "cleric,paladin", + "blight": "druid,sorcerer,warlock,wizard", + "blindingsmite": "paladin", + "blindnessdeafness": "bard,cleric,sorcerer,wizard", + "blink": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "blur": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "bonesoftheearth": "druid", + "boomingblade": "sorcerer,warlock,wizard", + "brandingsmite": "paladin", + "burninghands": "sorcerer,wizard", + "calllightning": "druid", + "calmemotions": "bard,cleric", + "catapult": "artificer,sorcerer,wizard", + "catnap": "artificer,bard,sorcerer,wizard", + "causefear": "warlock,wizard", + "ceremony": "cleric,paladin", + "chainlightning": "sorcerer,wizard", + "chaosbolt": "sorcerer", + "charmmonster": "bard,druid,sorcerer,warlock,wizard", + "charmperson": "bard,druid,sorcerer,warlock,wizard", + "chilltouch": "sorcerer,warlock,wizard", + "chromaticorb": "sorcerer,wizard", + "circleofdeath": "sorcerer,warlock,wizard", + "circleofpower": "paladin", + "clairvoyance": "bard,cleric,sorcerer,wizard", + "clone": "wizard", + "cloudofdaggers": "bard,sorcerer,warlock,wizard", + "cloudkill": "sorcerer,wizard", + "colorspray": "sorcerer,wizard", + "command": "cleric,paladin", + "commune": "cleric", + "communewithnature": "druid,ranger", + "compelledduel": "paladin", + "comprehendlanguages": "bard,sorcerer,warlock,wizard", + "compulsion": "bard", + "coneofcold": "sorcerer,wizard", + "confusion": "bard,druid,sorcerer,wizard", + "conjureanimals": "druid,ranger", + "conjurebarrage": "ranger", + "conjurecelestial": "cleric", + "conjureelemental": "druid,wizard", + "conjurefey": "druid,warlock", + "conjureminorelementals": "druid,wizard", + "conjurevolley": "ranger", + "conjurewoodlandbeings": "druid,ranger", + "contactotherplane": "warlock,wizard", + "contagion": "cleric,druid", + "contingency": "wizard", + "continualflame": "artificer,cleric,wizard,artificer,artificerrevisited", + "controlflames": "druid,sorcerer,wizard", + "controlwater": "cleric,druid,wizard", + "controlweather": "cleric,druid,wizard", + "controlwinds": "druid,sorcerer,wizard", + "cordonofarrows": "ranger", + "counterspell": "sorcerer,warlock,wizard", + "createbonfire": "artificer,druid,sorcerer,warlock,wizard", + "createfoodandwater": "artificer,cleric,paladin", + "createhomunculus": "wizard", + "createundead": "cleric,warlock,wizard", + "createordestroywater": "cleric,druid", + "creation": "artificer,sorcerer,wizard,artificerrevisited", + "crownofmadness": "bard,sorcerer,warlock,wizard", + "crownofstars": "sorcerer,warlock,wizard", + "crusadersmantle": "paladin", + "curewounds": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", + "dancinglights": "artificer,bard,sorcerer,wizard,artificerrevisited", + "dansemacabre": "warlock,wizard", + "darkness": "sorcerer,warlock,wizard", + "darkvision": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "dawn": "cleric,wizard", + "daylight": "cleric,druid,paladin,ranger,sorcerer", + "deathward": "cleric,paladin,artificer", + "delayedblastfireball": "sorcerer,wizard", + "demiplane": "warlock,wizard", + "destructivewave": "paladin", + "detectevilandgood": "cleric,paladin", + "detectmagic": "artificer,bard,cleric,druid,paladin,ranger,sorcerer,wizard,artificerrevisited", + "detectpoisonanddisease": "cleric,druid,paladin,ranger", + "detectthoughts": "bard,sorcerer,wizard", + "dimensiondoor": "bard,sorcerer,warlock,wizard", + "disguiseself": "artificer,bard,sorcerer,wizard,artificer,artificerrevisited", + "disintegrate": "sorcerer,wizard", + "dispelevilandgood": "cleric,paladin", + "dispelmagic": "artificer,bard,cleric,druid,paladin,sorcerer,warlock,wizard,artificerrevisited", + "dissonantwhispers": "bard", + "distortvalue": "bard,sorcerer,warlock,wizard", + "divination": "cleric", + "divinefavor": "paladin", + "divineword": "cleric", + "dominatebeast": "druid,sorcerer", + "dominatemonster": "bard,sorcerer,warlock,wizard", + "dominateperson": "bard,sorcerer,wizard", + "dragonsbreath": "sorcerer,wizard", + "drawmijsinstantsummons": "wizard", + "instantsummons": "wizard", + "dream": "bard,warlock,wizard", + "druidgrove": "druid", + "druidcraft": "druid", + "dustdevil": "druid,sorcerer,wizard", + "earthtremor": "bard,druid,sorcerer,wizard", + "earthbind": "druid,sorcerer,warlock,wizard", + "earthquake": "cleric,druid,sorcerer", + "eldritchblast": "warlock", + "elementalbane": "artificer,druid,warlock,wizard", + "elementalweapon": "artificer,paladin,artificerrevisited", + "enemiesabound": "bard,sorcerer,warlock,wizard", + "enervation": "sorcerer,warlock,wizard", + "enhanceability": "artificer,bard,cleric,druid,sorcerer,artificer,artificerrevisited", + "enlargereduce": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "ensnaringstrike": "ranger", + "entangle": "druid", + "enthrall": "bard,warlock", + "eruptingearth": "druid,sorcerer,wizard", + "etherealness": "bard,cleric,sorcerer,warlock,wizard", + "evardsblacktentacles": "wizard", + "blacktentacles": "wizard", + "expeditiousretreat": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", + "eyebite": "bard,sorcerer,warlock,wizard", + "fabricate": "artificer,wizard,artificer,artificerrevisited", + "faeriefire": "artificer,bard,druid", + "falselife": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "farstep": "sorcerer,warlock,wizard", + "fastfriends": "bard,cleric,wizard", + "fear": "bard,sorcerer,warlock,wizard", + "featherfall": "artificer,bard,sorcerer,wizard", + "feeblemind": "bard,druid,warlock,wizard", + "feigndeath": "bard,cleric,druid,wizard", + "findfamiliar": "wizard", + "findgreatersteed": "paladin", + "findsteed": "paladin", + "findtraps": "cleric,druid,ranger", + "findthepath": "bard,cleric,druid", + "fingerofdeath": "sorcerer,warlock,wizard", + "firebolt": "artificer,sorcerer,wizard,artificerrevisited", + "fireshield": "wizard", + "firestorm": "cleric,druid,sorcerer", + "fireball": "sorcerer,wizard", + "flamearrows": "artificer,druid,ranger,sorcerer,wizard", + "flameblade": "druid", + "flamestrike": "cleric", + "flamingsphere": "druid,wizard", + "fleshtostone": "warlock,wizard", + "fly": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", + "fogcloud": "druid,ranger,sorcerer,wizard", + "forbiddance": "cleric", + "forcecage": "bard,warlock,wizard", + "foresight": "bard,druid,warlock,wizard", + "freedomofmovement": "artificer,bard,cleric,druid,ranger,artificer,artificerrevisited", + "friends": "bard,sorcerer,warlock,wizard", + "frostbite": "artificer,druid,sorcerer,warlock,wizard", + "gaseousform": "sorcerer,warlock,wizard,artificer,artificerrevisited", + "gate": "cleric,sorcerer,wizard", + "geas": "bard,cleric,druid,paladin,wizard", + "gentlerepose": "cleric,wizard", + "giantinsect": "druid", + "giftofgab": "bard,wizard", + "glibness": "bard,warlock", + "globeofinvulnerability": "sorcerer,wizard", + "glyphofwarding": "artificer,bard,cleric,wizard,artificer,artificerrevisited", + "goodberry": "druid,ranger", + "graspingvine": "druid,ranger", + "grease": "artificer,wizard,artificerrevisited", + "greaterinvisibility": "bard,sorcerer,wizard", + "greaterrestoration": "artificer,bard,cleric,druid,artificerrevisited", + "greenflameblade": "sorcerer,warlock,wizard", + "guardianoffaith": "cleric", + "guardianofnature": "druid,ranger", + "guardsandwards": "bard,wizard", + "guidance": "artificer,cleric,druid,artificerrevisited", + "guidingbolt": "cleric", + "gust": "druid,sorcerer,wizard", + "gustofwind": "druid,sorcerer,wizard", + "hailofthorns": "ranger", + "hallow": "cleric", + "hallucinatoryterrain": "bard,druid,warlock,wizard", + "harm": "cleric", + "haste": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "heal": "cleric,druid", + "healingspirit": "druid,ranger", + "healingword": "bard,cleric,druid", + "heatmetal": "artificer,bard,druid,artificerrevisited", + "hellishrebuke": "warlock", + "heroesfeast": "cleric,druid", + "heroism": "bard,paladin", + "hex": "warlock", + "holdmonster": "bard,sorcerer,warlock,wizard", + "holdperson": "bard,cleric,druid,sorcerer,warlock,wizard", + "holyaura": "cleric", + "holyweapon": "cleric,paladin", + "hungerofhadar": "warlock", + "huntersmark": "ranger", + "hypnoticpattern": "bard,sorcerer,warlock,wizard", + "iceknife": "druid,sorcerer,wizard", + "icestorm": "druid,sorcerer,wizard", + "identify": "artificer,bard,wizard,artificerrevisited", + "illusorydragon": "wizard", + "illusoryscript": "bard,warlock,wizard", + "immolation": "sorcerer,wizard", + "imprisonment": "warlock,wizard", + "incendiarycloud": "sorcerer,wizard", + "incitegreed": "cleric,warlock,wizard", + "infernalcalling": "warlock,wizard", + "infestation": "druid,sorcerer,warlock,wizard", + "inflictwounds": "cleric", + "insectplague": "cleric,druid,sorcerer", + "investitureofflame": "druid,sorcerer,warlock,wizard", + "investitureofice": "druid,sorcerer,warlock,wizard", + "investitureofstone": "druid,sorcerer,warlock,wizard", + "investitureofwind": "druid,sorcerer,warlock,wizard", + "invisibility": "artificer,bard,sorcerer,warlock,wizard,artificer,artificerrevisited", + "invulnerability": "wizard", + "jimsglowingcoin": "wizard", + "jimsmagicmissile": "wizard", + "jump": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "knock": "bard,sorcerer,wizard", + "legendlore": "bard,cleric,wizard", + "leomundssecretchest": "artificer,wizard,artificer,artificerrevisited", + "leomundstinyhut": "bard,wizard", + "lesserrestoration": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", + "levitate": "artificer,sorcerer,wizard,artificerrevisited", + "lifetransference": "cleric,wizard", + "light": "artificer,bard,cleric,sorcerer,wizard,artificerrevisited", + "lightningarrow": "ranger", + "lightningbolt": "sorcerer,wizard", + "lightninglure": "sorcerer,warlock,wizard", + "locateanimalsorplants": "bard,druid,ranger", + "locatecreature": "bard,cleric,druid,paladin,ranger,wizard", + "locateobject": "bard,cleric,druid,paladin,ranger,wizard", + "longstrider": "artificer,bard,druid,ranger,wizard,artificer,artificerrevisited", + "maddeningdarkness": "warlock,wizard", + "maelstrom": "druid", + "magearmor": "sorcerer,wizard", + "magehand": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", + "magiccircle": "cleric,paladin,warlock,wizard", + "magicjar": "wizard", + "magicmissile": "sorcerer,wizard", + "magicmouth": "artificer,bard,wizard,artificerrevisited", + "magicstone": "artificer,druid,warlock", + "magicweapon": "artificer,paladin,wizard,artificer,artificerrevisited", + "majorimage": "bard,sorcerer,warlock,wizard", + "masscurewounds": "bard,cleric,druid", + "massheal": "cleric", + "masshealingword": "cleric", + "masspolymorph": "bard,sorcerer,wizard", + "masssuggestion": "bard,sorcerer,warlock,wizard", + "maximiliansearthengrasp": "sorcerer,wizard", + "maze": "wizard", + "meldintostone": "cleric,druid", + "melfsacidarrow": "wizard", + "melfsminutemeteors": "sorcerer,wizard", + "acidarrow": "wizard", + "mending": "artificer,bard,cleric,druid,sorcerer,wizard,artificerrevisited", + "mentalprison": "sorcerer,warlock,wizard", + "message": "artificer,bard,sorcerer,wizard,artificerrevisited", + "meteorswarm": "sorcerer,wizard", + "mightyfortress": "wizard", + "mindblank": "bard,wizard", + "mindspike": "sorcerer,warlock,wizard", + "minorillusion": "bard,sorcerer,warlock,wizard", + "miragearcane": "bard,druid,wizard", + "mirrorimage": "sorcerer,warlock,wizard", + "mislead": "bard,wizard", + "mistystep": "sorcerer,warlock,wizard", + "modifymemory": "bard,wizard", + "moldearth": "druid,sorcerer,wizard", + "moonbeam": "druid", + "mordenkainensfaithfulhound": "artificer,wizard,artificer,artificerrevisited", + "motivationalspeech": "bard,cleric", + "faithfulhound": "wizard,artificer,artificerrevisited", + "mordenkainensmagnificentmansion": "bard,wizard", + "magnificentmansion": "bard,wizard", + "mordenkainensprivatesanctum": "artificer,wizard,artificer,artificerrevisited", + "mordenkainenssword": "bard,wizard", + "arcanesword": "bard,wizard", + "moveearth": "druid,sorcerer,wizard", + "negativeenergyflood": "warlock,wizard", + "nondetection": "bard,ranger,wizard", + "nystulsmagicaura": "wizard", + "arcanistsmagicaura": "wizard", + "otilukesfreezingsphere": "wizard", + "otilukesresilientsphere": "artificer,wizard,artificer,artificerrevisited", + "ottosirresistibledance": "bard,wizard", + "passwithouttrace": "druid,ranger", + "passwall": "wizard", + "phantasmalforce": "bard,sorcerer,wizard", + "phantasmalkiller": "wizard", + "phantomsteed": "wizard", + "planarally": "cleric", + "planarbinding": "bard,cleric,druid,wizard", + "planeshift": "cleric,druid,sorcerer,warlock,wizard", + "plantgrowth": "bard,druid,ranger", + "poisonspray": "artificer,druid,sorcerer,warlock,wizard,artificerrevisited", + "polymorph": "bard,druid,sorcerer,wizard", + "powerwordheal": "bard", + "powerwordkill": "bard,sorcerer,warlock,wizard", + "powerwordpain": "sorcerer,warlock,wizard", + "powerwordstun": "bard,sorcerer,warlock,wizard", + "prayerofhealing": "cleric", + "prestidigitation": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", + "primalsavagery": "druid", + "primordialward": "druid", + "prismaticspray": "sorcerer,wizard", + "prismaticwall": "wizard", + "produceflame": "druid", + "programmedillusion": "bard,wizard", + "projectimage": "bard,wizard", + "protectionfromenergy": "artificer,cleric,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "protectionfromevilandgood": "cleric,paladin,warlock,wizard", + "protectionfrompoison": "artificer,cleric,druid,paladin,ranger,artificer,artificerrevisited", + "psychicscream": "bard,sorcerer,warlock,wizard", + "purifyfoodanddrink": "artificer,cleric,druid,paladin", + "pyrotechnics": "artificer,bard,sorcerer,wizard", + "raisedead": "bard,cleric,paladin", + "rarystelepathicbond": "wizard", + "rayofenfeeblement": "warlock,wizard", + "rayoffrost": "artificer,sorcerer,wizard,artificerrevisited", + "rayofsickness": "sorcerer,wizard", + "regenerate": "bard,cleric,druid", + "reincarnate": "druid", + "removecurse": "cleric,paladin,warlock,wizard", + "resistance": "artificer,cleric,druid,artificerrevisited", + "resurrection": "bard,cleric", + "reversegravity": "druid,sorcerer,wizard", + "revivify": "artificer,cleric,paladin,artificer,artificerrevisited", + "ropetrick": "artificer,wizard,artificer,artificerrevisited", + "sacredflame": "cleric", + "sanctuary": "artificer,cleric,artificer,artificerrevisited", + "scatter": "sorcerer,warlock,wizard", + "scorchingray": "sorcerer,wizard", + "scrying": "bard,cleric,druid,warlock,wizard", + "searingsmite": "paladin", + "seeinvisibility": "artificer,bard,sorcerer,wizard,artificerrevisited", + "seeming": "bard,sorcerer,wizard", + "sending": "bard,cleric,wizard", + "sequester": "wizard", + "shadowblade": "sorcerer,warlock,wizard", + "shadowofmoil": "warlock", + "shapewater": "druid,sorcerer,wizard", + "shapechange": "druid,wizard", + "shatter": "bard,sorcerer,warlock,wizard", + "shield": "sorcerer,wizard", + "shieldoffaith": "cleric,paladin,artificer,artificerrevisited", + "shillelagh": "druid", + "shockinggrasp": "artificer,sorcerer,wizard,artificerrevisited", + "sickeningradiance": "sorcerer,warlock,wizard", + "silence": "bard,cleric,ranger", + "silentimage": "bard,sorcerer,wizard", + "simulacrum": "wizard", + "skillempowerment": "artificer,bard,sorcerer,wizard", + "skywrite": "artificer,bard,druid,wizard", + "sleep": "bard,sorcerer,wizard", + "sleetstorm": "druid,sorcerer,wizard", + "slow": "sorcerer,wizard", + "snare": "artificer,druid,ranger,wizard", + "snillocssnowballswarm": "sorcerer,wizard", + "soulcage": "warlock,wizard", + "sparethedying": "artificer,cleric,artificerrevisited", + "speakwithanimals": "bard,druid,ranger", + "speakwithdead": "bard,cleric", + "speakwithplants": "bard,druid,ranger", + "spiderclimb": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", + "spikegrowth": "druid,ranger", + "spiritguardians": "cleric", + "spiritualweapon": "cleric", + "staggeringsmite": "paladin", + "steelwindstrike": "ranger,wizard", + "stinkingcloud": "bard,sorcerer,wizard", + "stoneshape": "artificer,cleric,druid,wizard,artificer,artificerrevisited", + "stoneskin": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "stormofvengeance": "druid", + "stormsphere": "sorcerer,wizard", + "suggestion": "bard,sorcerer,warlock,wizard", + "summongreaterdemon": "warlock,wizard", + "summonlesserdemons": "warlock,wizard", + "sunbeam": "druid,sorcerer,wizard", + "sunburst": "druid,sorcerer,wizard", + "swiftquiver": "ranger", + "swordburst": "sorcerer,warlock,wizard", + "synapticstatic": "bard,sorcerer,warlock,wizard", + "symbol": "bard,cleric,wizard", + "tashashideouslaughter": "bard,wizard", + "hideouslaughter": "bard,wizard", + "telekinesis": "sorcerer,wizard", + "telepathy": "wizard", + "teleport": "bard,sorcerer,wizard", + "teleportationcircle": "bard,sorcerer,wizard", + "templeofthegods": "cleric", + "tensersfloatingdisk": "wizard", + "tensersfloatingdisc": "wizard", + "tenserstransformation": "wizard", + "floatingdisc": "wizard", + "thaumaturgy": "cleric", + "thornwhip": "artificer,druid,artificerrevisited", + "thunderstep": "sorcerer,warlock,wizard", + "thunderclap": "artificer,bard,druid,sorcerer,warlock,wizard", + "thunderoussmite": "paladin", + "thunderwave": "bard,druid,sorcerer,wizard", + "tidalwave": "druid,sorcerer,wizard", + "timestop": "sorcerer,wizard", + "tinyservant": "artificer,wizard", + "tollthedead": "cleric,warlock,wizard", + "tongues": "bard,cleric,sorcerer,warlock,wizard", + "transmuterock": "artificer,druid,wizard", + "transportviaplants": "druid", + "treestride": "druid,ranger", + "truepolymorph": "bard,warlock,wizard", + "trueresurrection": "cleric,druid", + "trueseeing": "bard,cleric,sorcerer,warlock,wizard", + "truestrike": "bard,sorcerer,warlock,wizard", + "tsunami": "druid", + "unseenservant": "bard,warlock,wizard", + "vampirictouch": "warlock,wizard", + "viciousmockery": "bard", + "vitriolicsphere": "sorcerer,wizard", + "walloffire": "druid,sorcerer,wizard", + "wallofforce": "wizard", + "wallofice": "wizard", + "walloflight": "sorcerer,warlock,wizard", + "wallofsand": "wizard", + "wallofstone": "artificer,druid,sorcerer,wizard,artificerrevisited", + "wallofthorns": "druid", + "wallofwater": "druid,sorcerer,wizard", + "wardingbond": "cleric", + "wardingwind": "bard,druid,sorcerer,wizard", + "waterbreathing": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "waterwalk": "artificer,cleric,druid,ranger,sorcerer,artificer,artificerrevisited", + "waterysphere": "druid,sorcerer,wizard", + "web": "artificer,sorcerer,wizard", + "weird": "wizard", + "whirlwind": "druid,sorcerer,wizard", + "windwalk": "druid", + "windwall": "druid,ranger", + "wish": "sorcerer,wizard", + "witchbolt": "sorcerer,warlock,wizard", + "wordofradiance": "cleric", + "wordofrecall": "cleric", + "wrathofnature": "druid,ranger", + "wrathfulsmite": "paladin", + "zephyrstrike": "ranger", + "zoneoftruth": "bard,cleric,paladin" +} diff --git a/dist/data/sub-classes.json b/dist/data/sub-classes.json new file mode 100644 index 0000000..b55396d --- /dev/null +++ b/dist/data/sub-classes.json @@ -0,0 +1,15 @@ +{ + "artificer": ["Alchemist", "Gunsmith"], + "barbarian": ["Ancesstrial Guardian", "Battlerager", "Berserker", "Storm Herald", "Totem Warriro", "Zealot"], + "bard": ["Eloquence", "Glamour", "Lore", "Swords", "Valor", "Whispers"], + "cleric": ["Arcana", "Death", "Forge", "Grave", "Knowledge", "Life", "Light", "Nature", "Order", "Tempest", "Trickery", "War Domain"], + "druid": ["Dreams", "Land", "Moon", "Sheapherd", "Spores"], + "fighter": ["Arcane Archer", "Battle Master", "Cavalier", "Champion", "Echo Knight", "Eldritch Knight", "Samurai"], + "monk": ["Drunken Master", "Four Elements", "Kensei", "Long Death", "Open Hand", "Shadow", "Sun Soul"], + "paladin": ["Ancients", "Conquest", "Crown", "Devotion", "Glory", "Oathbreaker", "Redemption", "Vengeance"], + "ranger": ["Beast Master", "Gloom Stalker", "Horizon Walker", "Hunter", "Monster Slayer"], + "rogue": ["Arcane Trickster", "Assassin", "Inquisitive", "Mastermind", "Scout", "Swashbuckler", "Thief"], + "sorcerer": ["Devine Soul", "Draconic", "Shadow", "Storm", "Wild"], + "warlock": ["Archfey", "Celestial", "Fiend", "Great Old One", "Hexblade", "Undying"], + "wizard": ["Abjuration", "Bladesinging", "Chronurgy", "Conjuration", "Divination", "Énchantment", "Evocation", "Graviturgy", "Illusion", "Necromancy", "Transmutation", "War Magic"] +} diff --git a/dist/hooks/events.js b/dist/hooks/events.js new file mode 100644 index 0000000..f040d7e --- /dev/null +++ b/dist/hooks/events.js @@ -0,0 +1,210 @@ +import { Exporter } from "../scripts/modules/exporter.js"; +import { Renderer } from "../scripts/modules/renderer.js"; +import { ModuleSettings } from "../scripts/modules/settings.js"; +export class Events { + constructor() { + return this; + } + /** + * Reload Filters for a given entity + * + * @param {HTMLCollection} html + * @param {String} entityType + */ + static async reloadFilters(html, entityType) { + let entityBrowserHtml = await Renderer.renderFilters(game.compendiumBrowser.filters[entityType], entityType), filterWrapper = document.querySelector('.tab.active .browser .control-area'); + filterWrapper.innerHTML = entityBrowserHtml; + Events.activateFilterListeners(html); + } + /** + * + * @param {HTMLCollection} app + */ + static async activateActionListener(app = document.getElementsByClassName('window-app')) { + app = app.get(0); + app.querySelector('button[data-action="export"]').addEventListener('click', async (e) => { + let items = app.querySelectorAll('.content .tab.active .cb_entities .entity'), entityType = app.querySelector('.content .tab.active').dataset.tab, filters = JSON.parse(app.querySelector('.tab.active .cb_entities').dataset.activeFilters), tableItems = [], tableName = entityType + 'table: '; + items.forEach(item => { + let obj = {}; + Object.assign(obj, item.dataset); + tableItems.push(obj); + }); + let filterKeys = Object.keys(filters); + if (filterKeys.length > 0) { + for (let key of filterKeys) { + tableName = tableName + '_' + filters[key].path + '-' + filters[key].value; + } + } + let d = Dialog.confirm({ + title: "Export subset to table", + content: "

      Choose wisely.

      ", + yes: () => Exporter.createTableFromSelection(tableName, tableItems), + no: () => console.log("You chose ... poorly"), + defaultYes: false + }); + }); + } + static async activateItemListListeners(app = document.getElementsByClassName('window-app')) { + app = app[0]; + // open entity sheet on click + app.querySelectorAll('*[data-action="openSheet"]').forEach(async (el) => { + el.addEventListener('click', async (e) => { + let itemId = e.currentTarget.parentNode.dataset.entryId; + let compendium = e.currentTarget.parentNode.dataset.entryCompendium; + let pack = game.packs.find(p => p.collection === compendium); + await pack.getEntity(itemId).then(entity => { + entity.sheet.render(true); + }); + }); + }); + // make draggable + //0.4.1: Avoid the game.packs lookup + app.querySelectorAll('.draggable').forEach(async (li) => { + li.setAttribute("draggable", true); + li.addEventListener('dragstart', event => { + let packName = li.getAttribute("data-entry-compendium"); + let pack = game.packs.find(p => p.collection === packName); + if (!pack) { + event.preventDefault(); + return false; + } + event.dataTransfer.setData("text/plain", JSON.stringify({ + type: pack.entity, + pack: pack.collection, + id: li.getAttribute("data-entry-id") + })); + }, false); + }); + } + static async observeListElement(list, tag) { + for (let element of list.getElementsByTagName(tag)) { + game.compendiumBrowser.observer.observe(element); + } + } + static async activateFilterListeners(html, app = document.getElementsByClassName('window-app')) { + app = app[0]; + // toggle visibility of filter containers + html.find('.filtercontainer h3, .multiselect label').click(async (ev) => { + await $(ev.target.nextElementSibling).toggle(100); + }); + html.find('.multiselect label').trigger('click'); + // reset filters and re-rendes + app.querySelectorAll('.button[data-action="reset-filters').forEach(async (el) => { + el.addEventListener('click', async (e) => { + game.compendiumBrowser.filters.resetFilters(); + game.compendiumBrowser.refreshList = e.target.closest('.tab').dataset.tab; + game.compendiumBrowser.render(); + }); + }); + // select filters + app.querySelectorAll('.settings input').forEach(async (el) => { + el.addEventListener('keyup change paste', async (e) => { + let target = e.target, setting = target.dataset.setting, value = target.checked, key = target.dataset.key, category = (target.dataset.type === 'Spell' || target.dataset.type === 'Feat') ? 'Item' : target.dataset.type; + if (key) + game.compendiumBrowser.settings.loadedCompendium[category][key].load = value; + ui.notifications.info("Settings Saved. Compendiums are being reloaded."); + switch (setting) { + case 'allow-spell-browser': + game.compendiumBrowser.settings.allowSpellBrowser = value; + break; + case 'allow-feat-browser': + game.compendiumBrowser.settings.allowFeatBrowser = value; + break; + case 'allow-item-browser': + game.compendiumBrowser.settings.allowItemBrowser = value; + break; + case 'allow-actor-browser': + game.compendiumBrowser.settings.allowActorBrowser = value; + break; + case 'allow-rolltable-browser': + game.compendiumBrowser.settings.allowRollTableBrowser = value; + break; + case 'allow-journalentry-browser': + game.compendiumBrowser.settings.allowJournalEntryBrowser = value; + break; + default: + break; + } + ModuleSettings.saveSettings(); + game.compendiumBrowser.render(); + }); + }); + // select filters + app.querySelectorAll('.filter[data-type=text] input, .filter[data-type=text] select').forEach(async (el) => { + el.addEventListener('keyup change paste', async (e) => { + const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), entityType = app.querySelector('.content .tab.active').dataset.tab; + if (e.target.value === '' || e.target.value === undefined) { + delete game.compendiumBrowser.filters[entityType].activeFilters[key]; + } + else { + game.compendiumBrowser.filters[entityType].activeFilters[key] = { + path: path, + type: 'text', + valIsArray: false, + value: e.target.value + }; + } + game.compendiumBrowser.replaceList(html, entityType); + }); + }); + // select filters + app.querySelectorAll('.filter[data-type=select] select, .filter[data-type=bool] select').forEach(async (el) => { + el.addEventListener('change', async (e) => { + const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), filterType = e.target.closest('.filter').dataset.type, entityType = app.querySelector('.content .tab.active').dataset.tab; + let valIsArray = e.target.closest('.filter').dataset.valIsArray === 'true', value = e.target.options[e.target.selectedIndex].value; + if (value === "null") { + delete game.compendiumBrowser.filters[entityType].activeFilters[key]; + } + else { + game.compendiumBrowser.filters[entityType].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + value: value + }; + } + game.compendiumBrowser.replaceList(html, entityType); + }); + }); + // multiselect + app.querySelectorAll('.filter[data-type=multiSelect] input').forEach(async (el) => { + el.addEventListener('change', async (e) => { + const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), filterType = 'multiSelect', entityType = app.querySelector('.content .tab.active').dataset.tab, filter = game.compendiumBrowser.filters[entityType].activeFilters[key]; + let valIsArray = e.target.closest('.filter').dataset.valIsArray, value = e.target.dataset.value; + if (e.target.checked === true) { + if (filter === undefined) { + game.compendiumBrowser.filters[entityType].activeFilters[key] = + { path: path, type: filterType, valIsArray: valIsArray, values: [value] }; + } + else { + game.compendiumBrowser.filters[entityType].activeFilters[key].values.push(value); + } + } + else { + delete game.compendiumBrowser.filters[entityType].activeFilters[key].values.splice(game.compendiumBrowser.filters[entityType].activeFilters[key].values.indexOf(value), 1); + if (game.compendiumBrowser.filters[entityType].activeFilters[key].values.length === 0) + delete game.compendiumBrowser.filters[entityType].activeFilters[key]; + } + game.compendiumBrowser.replaceList(html, entityType); + }); + }); + app.querySelectorAll('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').forEach(async (el) => { + el.addEventListener('change keyup paste', async (e) => { + const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), filterType = 'numberCompare', entityType = app.querySelector('.content .tab.active').dataset.tab, operator = e.target.closest('.filter').getElementsByTagName('select').val, value = e.target.closest('.filter').getElementsByTagName('input').val; + if (value === '' || operator === 'null') { + delete game.compendiumBrowser.filters[entityType].activeFilters[key]; + } + else { + game.compendiumBrowser.filters[entityType].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: false, + operator: operator, + value: value + }; + } + game.compendiumBrowser.replaceList(html, browserTab); + }); + }); + } +} diff --git a/hooks/mainHook.js b/dist/hooks/moduleHooks.js similarity index 71% rename from hooks/mainHook.js rename to dist/hooks/moduleHooks.js index bcc7d57..275737e 100644 --- a/hooks/mainHook.js +++ b/dist/hooks/moduleHooks.js @@ -1,7 +1,19 @@ +/** + * @fileOverview Holding the modules Hooks + * + * @author Daniel Böttner + * @version 1.0.0 + */ +/* jshint node: true */ +'use strict'; +import { CMPBrowser } from '../scripts/modules/settings.js'; import { CompendiumBrowser } from '../scripts/compendium-browser.js'; - -export class moduleHooks { - +import { VersionCheck } from '../scripts/versioning/version-check.js'; +import { Renderer } from '../scripts/modules/renderer.js'; +/** + * Holds the static methods to be called + */ +export default class moduleHooks { static onInit() { Hooks.on('init', async () => { Handlebars.registerHelper('switch', function (value, options) { @@ -9,20 +21,17 @@ export class moduleHooks { this.switch_break = false; return options.fn(this); }); - Handlebars.registerHelper('case', function (value, options) { if (value == this.switch_value) { this.switch_break = true; return options.fn(this); } }); - Handlebars.registerHelper('default', function (value, options) { if (this.switch_break == false) { return value; } }); - Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) { switch (operator) { case '==': @@ -49,9 +58,18 @@ export class moduleHooks { return options.inverse(this); } }); + Handlebars.registerHelper("debug", function (optionalValue) { + console.log("Current Context"); + console.log("===================="); + console.log(this); + if (optionalValue) { + console.log("Value"); + console.log("===================="); + console.log(optionalValue); + } + }); }); } - static onReady() { Hooks.on('ready', async () => { if (game.compendiumBrowser === undefined) { @@ -60,11 +78,12 @@ export class moduleHooks { //A compromise approach would be better (periodic loading) except would still create the memory use problem await game.compendiumBrowser.initialize(); } + if (VersionCheck.check(CMPBrowser.MODULE_NAME) && game.user.isGM) { + console.log('version check'); + } }); } - static onRenderComplete() { - Hooks.on("renderCompendiumBrowser", CompendiumBrowser.afterRender); + Hooks.on("renderCompendiumBrowser", Renderer.afterRender); } - -} \ No newline at end of file +} diff --git a/dist/lang/de.json b/dist/lang/de.json new file mode 100644 index 0000000..27e8aac --- /dev/null +++ b/dist/lang/de.json @@ -0,0 +1,74 @@ +{ + "CMPBrowser.compendiumBrowser": "Compendium Browser", + "CMPBrowser.sortBy": "Sortieren nach", + "CMPBrowser.cr": "Challenge Rating", + "CMPBrowser.generalSettings": "Allgemeine Einstellungen", + "CMPBrowser.allowSpellAcc": "Erlaube Spielern Zauber zu durchsuchen.", + "CMPBrowser.allowActorAcc": "Player access to actor browser", + "CMPBrowser.allowRolltable": "Player access to rolltable browser", + "CMPBrowser.allowJournalEntry": "Player Access to the JournalEntry browser", + "CMPBrowser.compSettingsSpell": "Gegenstand Kompendium Einstellungen", + "CMPBrowser.compSettingsNpc": "Actor Kompendium Einstellungen", + "CMPBrowser.Filters.ResetFilters": "Reset Filters", + "CMPBrowser.Tab.SpellBrowser": "Zauber Browser", + "CMPBrowser.Tab.FeatBrowser": "Feat Browser", + "CMPBrowser.Tab.ItemBrowser": "Gegenstand Browser", + "CMPBrowser.Tab.NPCBrowser": "NPC Browser", + "CMPBrowser.Tab.ActorBrowser": "Actor Browser", + "CMPBrowser.Tab.RolltableBrowser": "Rolltable Browser", + "CMPBrowser.Tab.JournalEntryBrowser": "JournalEntry Browser", + "CMPBrowser.Tab.Settings": "Einstellungen", + "CMPBrowser.SETTING.Maxload.NAME": "Maximum load", + "CMPBrowser.SETTING.Maxload.HINT": "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", + "CMPBrowser.LOADING.Message": "Geladen: {numLoaded} {entityType}s ({numPacks} Kompendia)", + "CMPBrowser.LOADING.MaxLoaded": "(maximum displayed; to see more, use the filters)", + "CMPBrowser.load": "Laden", + "CMPBrowser.lvl": "Level", + "CMPBrowser.ritual": "Ritual", + "CMPBrowser.concentration": "Konzentration", + "CMPBrowser.vocal": "Verbal", + "CMPBrowser.somatic": "Somatisch", + "CMPBrowser.material": "Material", + "CMPBrowser.cantip": "Cantrip", + "CMPBrowser.school": "Schule", + "CMPBrowser.castingTime": "Zauberzeit", + "CMPBrowser.bonusAction": "Bonus Aktion", + "CMPBrowser.reaction": "Reaktion", + "CMPBrowser.spellType": "Zaubertyp", + "CMPBrowser.damageType": "Schadensart", + "CMPBrowser.class": "Klasse", + "CMPBrowser.artificer": "Artificer", + "CMPBrowser.bard": "Barde", + "CMPBrowser.cleric": "Kleriker", + "CMPBrowser.druid": "Druide", + "CMPBrowser.paladin": "Paladin", + "CMPBrowser.ranger": "Ranger", + "CMPBrowser.sorcerer": "Zauberer", + "CMPBrowser.warlock": "Hexenmeister", + "CMPBrowser.wizard": "Magier", + "CMPBrowser.general": "Allgemein", + "CMPBrowser.components": "Komponenten", + "CMPBrowser.hasSpells": "Hat Zauber", + "CMPBrowser.hasLegAct": "Hat Legendäre Aktion(en)", + "CMPBrowser.hasLegRes": "Hat Legendäre Resistenz(en)", + "CMPBrowser.creatureType": "Kreaturentyp", + "CMPBrowser.aberration": "Aberration", + "CMPBrowser.beast": "Beast", + "CMPBrowser.celestial": "Göttlich", + "CMPBrowser.construct": "construct", + "CMPBrowser.dragon": "Drachen", + "CMPBrowser.elemental": "Elemental", + "CMPBrowser.fey": "Fey", + "CMPBrowser.fiend": "Fiend", + "CMPBrowser.giant": "Giant", + "CMPBrowser.humanoid": "Humanoid", + "CMPBrowser.monstrosity": "Monstrosity", + "CMPBrowser.ooze": "Ooze", + "CMPBrowser.plant": "Plant", + "CMPBrowser.undead": "Undead", + "CMPBrowser.abilities": "Fähigkeiten", + "CMPBrowser.dmgInteraction": "Damage Interaction", + "CMPBrowser.dmgDealt": "Damage Dealt", + "CMPBrowser.size": "Größe", + "CMPBrowser.TableType": "RollTable Typ" +} diff --git a/dist/lang/en.json b/dist/lang/en.json new file mode 100644 index 0000000..748d7d2 --- /dev/null +++ b/dist/lang/en.json @@ -0,0 +1,73 @@ +{ + "CMPBrowser.compendiumBrowser": "Compendium Browser", + "CMPBrowser.sortBy": "Sort by", + "CMPBrowser.cr": "Challenge Rating", + "CMPBrowser.generalSettings": "General Settings", + "CMPBrowser.allowSpellAcc": "Players access to the Spell browser", + "CMPBrowser.allowActorAcc": "Player access to Actor browser", + "CMPBrowser.allowRolltable": "Player access to RollTable browser", + "CMPBrowser.allowJournalEntry": "Player Access to the JournalEntry browser", + "CMPBrowser.compSettingsSpell": "Item Compendium Settings", + "CMPBrowser.compSettingsNpc": "Actor Compendium Settings", + "CMPBrowser.load": "Load", + "CMPBrowser.lvl": "Level", + "CMPBrowser.ritual": "Ritual", + "CMPBrowser.concentration": "Concentration", + "CMPBrowser.vocal": "Verbal", + "CMPBrowser.somatic": "Somatic", + "CMPBrowser.material": "Material", + "CMPBrowser.cantrip": "Cantrip", + "CMPBrowser.school": "School", + "CMPBrowser.castingTime": "Casting Time", + "CMPBrowser.bonusAction": "Bonus Action", + "CMPBrowser.reaction": "Reaction", + "CMPBrowser.spellType": "Spell Type", + "CMPBrowser.damageType": "Damage Type", + "CMPBrowser.class": "Class", + "CMPBrowser.artificer": "Artificer", + "CMPBrowser.bard": "Bard", + "CMPBrowser.cleric": "Cleric", + "CMPBrowser.druid": "Druid", + "CMPBrowser.paladin": "Paladin", + "CMPBrowser.ranger": "Ranger", + "CMPBrowser.sorcerer": "Sorcerer", + "CMPBrowser.warlock": "Warlock", + "CMPBrowser.wizard": "Wizard", + "CMPBrowser.general": "General", + "CMPBrowser.components": "Components", + "CMPBrowser.hasSpells": "Has Spells", + "CMPBrowser.hasLegAct": "Has Legendary Actions", + "CMPBrowser.hasLegRes": "Has Legendary Resistance", + "CMPBrowser.creatureType": "Creature Type", + "CMPBrowser.aberration": "Aberration", + "CMPBrowser.beast": "Beast", + "CMPBrowser.celestial": "Celestial", + "CMPBrowser.construct": "construct", + "CMPBrowser.dragon": "Dragon", + "CMPBrowser.elemental": "Elemental", + "CMPBrowser.fey": "Fey", + "CMPBrowser.fiend": "Fiend", + "CMPBrowser.giant": "Giant", + "CMPBrowser.humanoid": "Humanoid", + "CMPBrowser.monstrosity": "Monstrosity", + "CMPBrowser.ooze": "Ooze", + "CMPBrowser.plant": "Plant", + "CMPBrowser.undead": "Undead", + "CMPBrowser.abilities": "Abilities", + "CMPBrowser.dmgInteraction": "Damage Interaction", + "CMPBrowser.dmgDealt": "Damage Dealt", + "CMPBrowser.size": "Size", + "CMPBrowser.Tab.ActorBrowser": "Actors", + "CMPBrowser.Tab.SpellBrowser": "Spells", + "CMPBrowser.Tab.FeatBrowser": "Feats", + "CMPBrowser.Tab.ItemBrowser": "Items", + "CMPBrowser.Tab.RollTableBrowser": "Rolltables", + "CMPBrowser.Tab.JournalEntryBrowser": "JournalEntries", + "CMPBrowser.Tab.Settings": "Settings", + "CMPBrowser.SETTING.Maxload.NAME": "Maximum load", + "CMPBrowser.SETTING.Maxload.HINT": "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", + "CMPBrowser.LOADING.Message": "Loaded: {numLoaded} {entityType}s ({numPacks} Compendia)", + "CMPBrowser.LOADING.MaxLoaded": "(maximum displayed; to see more, use the filters)", + "CMPBrowser.Filters.ResetFilters": "Reset Filters", + "CMPBrowser.TableType": "RollTable Type" +} diff --git a/dist/lang/fr.json b/dist/lang/fr.json new file mode 100644 index 0000000..c1206ba --- /dev/null +++ b/dist/lang/fr.json @@ -0,0 +1,65 @@ +{ + "CMPBrowser.compendiumBrowser": "Recherche dans les Compendium", + "CMPBrowser.sortBy": "Trié par", + "CMPBrowser.cr": "Niveau de la rencontre", + "CMPBrowser.generalSettings": "Paramètres généraux", + "CMPBrowser.allowSpellAcc": "Autoriser les joueurs à accéder aux listes de sorts", + "CMPBrowser.allowNpcAcc": "Autoriser les joueurs à accéder aux listes de PNJ", + "CMPBrowser.compSettingsSpell": "Paramètres de compendium de sorts", + "CMPBrowser.compSettingsNpc": "Paramètres de compendium de PNJ", + "CMPBrowser.load": "Charger", + "CMPBrowser.lvl": "Level", + "CMPBrowser.ritual": "Rituel", + "CMPBrowser.concentration": "Concentration", + "CMPBrowser.vocal": "Verbales", + "CMPBrowser.somatic": "Somatiques", + "CMPBrowser.material": "Matérielles", + "CMPBrowser.cantip": "Tours de magie", + "CMPBrowser.school": "Ecole", + "CMPBrowser.castingTime": "Durée d'incantation", + "CMPBrowser.bonusAction": "Action Bonus", + "CMPBrowser.reaction": "Réaction", + "CMPBrowser.spellType": "Type de sort", + "CMPBrowser.damageType": "Type de dégâts", + "CMPBrowser.class": "Classe", + "CMPBrowser.bard": "Barde", + "CMPBrowser.cleric": "Clerc", + "CMPBrowser.druid": "Druide", + "CMPBrowser.paladin": "Paladin", + "CMPBrowser.ranger": "Rôdeur", + "CMPBrowser.sorcerer": "Sorcier", + "CMPBrowser.warlock": "Ensorceleur", + "CMPBrowser.wizard": "Magicien", + "CMPBrowser.general": "Général", + "CMPBrowser.components": "Composants", + "CMPBrowser.hasSpells": "à des Sorts", + "CMPBrowser.hasLegAct": "à des Actions Légendaires", + "CMPBrowser.hasLegRes": "à des Resistances Légendaires", + "CMPBrowser.creatureType": "Type de Créature", + "CMPBrowser.aberration": "Aberration", + "CMPBrowser.beast": "Bête", + "CMPBrowser.celestial": "Céleste", + "CMPBrowser.construct": "Artificielles", + "CMPBrowser.dragon": "Dragon", + "CMPBrowser.elemental": "Elementaire", + "CMPBrowser.fey": "Fée", + "CMPBrowser.fiend": "Fiélon", + "CMPBrowser.giant": "Géant", + "CMPBrowser.humanoid": "Humanoïde", + "CMPBrowser.monstrosity": "Monstrueuse", + "CMPBrowser.ooze": "Vase", + "CMPBrowser.plant": "Plante", + "CMPBrowser.undead": "Morts-vivants", + "CMPBrowser.abilities": "Capacités", + "CMPBrowser.dmgInteraction": "Spécificité des dégâts", + "CMPBrowser.dmgDealt": "Type de dégats", + "CMPBrowser.size": "Taille", + "CMPBrowser.spellBrowser": "Recherche de sorts", + "CMPBrowser.npcBrowser": "Recherche de PNJ", + "CMPBrowser.settings": "Paramétrages", + "CMPBrowser.SETTING.Maxload.NAME": "Charge maximale", + "CMPBrowser.SETTING.Maxload.HINT": "Nombre maximum de sorts, dons, objets ou PNJ à afficher ; pour en voir plus, utilisez les filtres. Ce paramètre permet de gérer la mémoire et la charge du serveur.", + "CMPBrowser.LOADING.Message": "{numLoaded} {entityType}s chargées", + "CMPBrowser.LOADING.MaxLoaded": "(maximum affiché ; pour en voir plus, utilisez les filtres)", + "CMPBrowser.Filters.ResetFilters": "Réinitialiser les filtres" +} diff --git a/dist/lang/ja.json b/dist/lang/ja.json new file mode 100644 index 0000000..b92ca92 --- /dev/null +++ b/dist/lang/ja.json @@ -0,0 +1,60 @@ +{ + "CMPBrowser.compendiumBrowser": "辞典ブラウザ", + "CMPBrowser.sortBy": "並び替え", + "CMPBrowser.cr": "難易度", + "CMPBrowser.generalSettings": "一般設定", + "CMPBrowser.allowSpellAcc": "プレイヤーに呪文辞典の使用を許可する。", + "CMPBrowser.allowNpcAcc": "プレイヤーにNPC辞典の使用を許可する。", + "CMPBrowser.compSettingsSpell": "呪文辞典設定", + "CMPBrowser.compSettingsNpc": "NPC辞典設定", + "CMPBrowser.load": "追加", + "CMPBrowser.lvl": "レベル", + "CMPBrowser.ritual": "儀式", + "CMPBrowser.concentration": "集中", + "CMPBrowser.vocal": "音声", + "CMPBrowser.somatic": "動作", + "CMPBrowser.material": "物質", + "CMPBrowser.cantip": "初級", + "CMPBrowser.school": "系統", + "CMPBrowser.castingTime": "発動時間", + "CMPBrowser.bonusAction": "ボーナスアクション", + "CMPBrowser.reaction": "リアクション", + "CMPBrowser.spellType": "呪文種別", + "CMPBrowser.damageType": "ダメージ種別", + "CMPBrowser.class": "クラス", + "CMPBrowser.bard": "バード", + "CMPBrowser.cleric": "クレリック", + "CMPBrowser.druid": "ドルイド", + "CMPBrowser.paladin": "パラディン", + "CMPBrowser.ranger": "レンジャー", + "CMPBrowser.sorcerer": "ソーサラー", + "CMPBrowser.warlock": "ウォーロック", + "CMPBrowser.wizard": "ウィザード", + "CMPBrowser.general": "一般", + "CMPBrowser.components": "物質構成要素", + "CMPBrowser.hasSpells": "術者", + "CMPBrowser.hasLegAct": "伝説的アクション所持", + "CMPBrowser.hasLegRes": "伝説的抵抗所持", + "CMPBrowser.creatureType": "クリーチャー種別", + "CMPBrowser.aberration": "異形", + "CMPBrowser.beast": "野獣", + "CMPBrowser.celestial": "セレスチャル", + "CMPBrowser.construct": "人造", + "CMPBrowser.dragon": "ドラゴン", + "CMPBrowser.elemental": "エレメンタル", + "CMPBrowser.fey": "フェイ", + "CMPBrowser.fiend": "フィーンド", + "CMPBrowser.giant": "巨人", + "CMPBrowser.humanoid": "ヒューマノイド", + "CMPBrowser.monstrosity": "怪物", + "CMPBrowser.ooze": "粘体", + "CMPBrowser.plant": "植物", + "CMPBrowser.undead": "アンデッド", + "CMPBrowser.abilities": "能力値", + "CMPBrowser.dmgInteraction": "ダメージ関連", + "CMPBrowser.dmgDealt": "与えるダメージ種別", + "CMPBrowser.size": "サイズ", + "CMPBrowser.spellBrowser": "呪文ブラウザ", + "CMPBrowser.npcBrowser": "NPCブラウザ", + "CMPBrowser.settings": "設定" +} diff --git a/dist/lang/pt-BR.json b/dist/lang/pt-BR.json new file mode 100644 index 0000000..89386d5 --- /dev/null +++ b/dist/lang/pt-BR.json @@ -0,0 +1,61 @@ +{ + "CMPBrowser.compendiumBrowser": "Navegador de Compêndio", + "CMPBrowser.sortBy": "Classificar por", + "CMPBrowser.cr": "Nivel de Desafio", + "CMPBrowser.generalSettings": "Configurações Gerais", + "CMPBrowser.allowSpellAcc": "Permite o acesso dos jogadores ao navegador de magias", + "CMPBrowser.allowNpcAcc": "Permite o acesso dos jogadores ao navegador de NPCs", + "CMPBrowser.compSettingsSpell": "Configuração de Compêndio de Itens", + "CMPBrowser.compSettingsNpc": "Configuração de Compêndio de NPCs", + "CMPBrowser.load": "Carregar", + "CMPBrowser.lvl": "Nivel", + "CMPBrowser.ritual": "Ritual", + "CMPBrowser.concentration": "Concentraçãon", + "CMPBrowser.vocal": "Verbal", + "CMPBrowser.somatic": "Somático", + "CMPBrowser.material": "Material", + "CMPBrowser.cantip": "Truque", + "CMPBrowser.school": "Escola", + "CMPBrowser.castingTime": "Custo de Ativação", + "CMPBrowser.bonusAction": "Ação bonus", + "CMPBrowser.reaction": "Reação", + "CMPBrowser.spellType": "Tipo da Magia", + "CMPBrowser.damageType": "Tipo de Dano", + "CMPBrowser.class": "Classe", + "CMPBrowser.artificer": "Artífice", + "CMPBrowser.bard": "Bardo", + "CMPBrowser.cleric": "Clerigo", + "CMPBrowser.druid": "Druida", + "CMPBrowser.paladin": "Paladino", + "CMPBrowser.ranger": "Guardião", + "CMPBrowser.sorcerer": "Geiticeiro", + "CMPBrowser.warlock": "Bruxo", + "CMPBrowser.wizard": "Mago", + "CMPBrowser.general": "Geral", + "CMPBrowser.components": "Componentes", + "CMPBrowser.hasSpells": "Tem Magias", + "CMPBrowser.hasLegAct": "Tem Ações Lendárias", + "CMPBrowser.hasLegRes": "Tem Resistência Lendária", + "CMPBrowser.creatureType": "Tipo de Criatura", + "CMPBrowser.aberration": "Aberração", + "CMPBrowser.beast": "Besta", + "CMPBrowser.celestial": "Celestial", + "CMPBrowser.construct": "Constructo", + "CMPBrowser.dragon": "Dragão", + "CMPBrowser.elemental": "Elemental", + "CMPBrowser.fey": "Feérico", + "CMPBrowser.fiend": "Ínfero", + "CMPBrowser.giant": "Giante", + "CMPBrowser.humanoid": "Humanoide", + "CMPBrowser.monstrosity": "Monstruosidade", + "CMPBrowser.ooze": "Gosma", + "CMPBrowser.plant": "Planta", + "CMPBrowser.undead": "Morto-Vivo", + "CMPBrowser.abilities": "Atributos", + "CMPBrowser.dmgInteraction": "Interação do Dano", + "CMPBrowser.dmgDealt": "Dano Causado", + "CMPBrowser.size": "Tamanho", + "CMPBrowser.spellBrowser": "Navegador de Magias", + "CMPBrowser.npcBrowser": "Navegador de NPC", + "CMPBrowser.settings": "Configurações" +} diff --git a/dist/module.json b/dist/module.json new file mode 100644 index 0000000..51d77a4 --- /dev/null +++ b/dist/module.json @@ -0,0 +1,66 @@ +{ + "name": "compendium-browser", + "title": "Compendium Browser", + "description": "

      Easily browse and filter spells, feats, items, and npcs loaded from compendia!

      NEW! Compendium Browser is faster and better-behaved; it no longer loads all the compendia into memory on start-up (which sometimes hung servers because of memory or CPU requirements). Instead, it filters and loads on-demand, as well as giving you a Module Setting to control how many rows are loaded at a time.
      Changes in v0.5.0:Fixed: Issue #17: Error in Foundry 0.8.x when filtering NPC by Creature Type
      Changes in v0.4.5:
      • Fixed: Spells from non-system compendium show up in items tab.(Issue #10)
      • Added: Show compendium source in results (Issue #11)
      ", + "version": "0.5.2", + "minimumCoreVersion": "0.6.2", + "compatibleCoreVersion": "0.8.8", + "allowBugReporter": true, + "author": "Spetzel#0103", + "authors": [ + { + "name": "Spetzel#0103", + "url": "https://github.com/spetzel2020" + }, + { + "name": "Felix#6196" + }, + { + "name": "JackPrince#0494", + "url": "https://github.com/DanielBoettner/" + } + ], + "systems": [ + "dnd5e" + ], + "includes": [ + "./hooks/**", + "./scripts/**" + ], + "esmodules": [ + "./compendium-browser.js" + ], + "styles": [ + "./styles/compendium-browser.css", + "./styles/compendium-browser.less" + ], + "packs": [], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + }, + { + "lang": "ja", + "name": "Japanese", + "path": "lang/ja.json" + }, + { + "lang": "fr", + "name": "French (FRANCE)", + "path": "lang/fr.json" + }, + { + "lang": "pt-BR", + "name": "Português (Brasil)", + "path": "lang/pt-BR.json" + } + ], + "url": "https://github.com/League-of-Foundry-Developers/compendium-browser", + "manifest": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/latest/download/module.json", + "download": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/download/v0.5.2/compendium-browser.zip", + "bugs": "https://github.com/League-of-Foundry-Developers/compendium-browser/issues", + "readme": "https://github.com/League-of-Foundry-Developers/compendium-browser/blob/master/README.md", + "changelog": "https://github.com/League-of-Foundry-Developers/compendium-browser/blob/master/CHANGELOG.md" +} diff --git a/dist/scripts/classes/compactEntity.js b/dist/scripts/classes/compactEntity.js new file mode 100644 index 0000000..d5fb171 --- /dev/null +++ b/dist/scripts/classes/compactEntity.js @@ -0,0 +1,18 @@ +export class compactEntity { + constructor() { + this._id = ''; + this.name = ''; + this.compendium = ''; + this.classRequirementString = ''; + this.data = {}; + this.flags = {}; + this.dae = false; + this.img = 'icons/svg/daze.svg'; + this.type = ''; + this.usesRessources = false; + this.hasSave = false; + } + hasDAE() { + return this.dae; + } +} diff --git a/dist/scripts/classes/compactList.js b/dist/scripts/classes/compactList.js new file mode 100644 index 0000000..b38360e --- /dev/null +++ b/dist/scripts/classes/compactList.js @@ -0,0 +1,43 @@ +export class compactList { + constructor() { + this.entities = []; + this.activeFilters = ''; + } + /** + * + * @returns {number} + */ + size() { + return this.entities.length; + } + /** + * + * @param {compactEntity} entity + * @returns {void} + */ + addEntity(entity) { + this.entities.push(entity); + } + /** + * + * @param {*} index + * @returns {compactEntity} + */ + getEntity(index) { + return this.entities[index]; + } + /** + * + * @param {string} filters + */ + addActiveFilters(filters) { + this.activeFilters = filters; + } + /** + * + * @returns {boolean} + */ + hasFilters() { + return this.activeFilters.length != 0; + } +} diff --git a/dist/scripts/classes/decoratedEntity.js b/dist/scripts/classes/decoratedEntity.js new file mode 100644 index 0000000..d9a9253 --- /dev/null +++ b/dist/scripts/classes/decoratedEntity.js @@ -0,0 +1,164 @@ +import { compactEntity } from "./compactEntity.js"; +export class decoratedEntity extends compactEntity { + constructor() { + super(); + this.damageDealt = []; + this.damageTypes = []; + this.matchedPacks = []; + this.matchedPacksString = ''; + this.classes = []; + this.hasSpells = false; + } + /** + * + * @param {Item|Actor5e|JournalEntry|RollTable} entityData + * @param {string} entityType + * @param {Compendium} packList + * @param {object} classList + * @param {object} subClasses + * + * @returns {object} decoratedItem + */ + static decorate(entity, entityType, packList = null, classList = null, subClasses = null) { + let decorated = new decoratedEntity(), entityData = entity.data; + decorated._id = entity.id; + decorated.name = entity.name || entityData.name; + decorated.img = entity.img; + decorated.type = entity.type || ''; + decorated.data.details = {}; + decorated.flags = entityData.flags; + // getting pack(s) + let matchedPacks = []; + for (let pack in packList) { + for (let packItem of packList[pack]) { + if (entity.name.toLowerCase() === packItem.toLowerCase()) { + matchedPacks.push(pack); + break; + } + } + } + decorated.matchedPacks = matchedPacks; + decorated.matchedPacksString = matchedPacks.join(', '); + // handle common stuff + switch (entityType) { + case 'Feat': + case 'Item': + case 'Spell': + // getting damage types (common to all Items, although some won't have any) + decorated.damageTypes = []; + if (entityData.damage && entityData.damage.parts.length > 0) { + for (let part of entityData.damage.parts) { + let type = part[1]; + if (decorated.damageTypes.indexOf(type) === -1) { + decorated.damageTypes.push(type); + } + } + } + // getting uses/ressources status + decorated.usesRessources = entityData.hasLimitedUses; + decorated.hasSave = entityData.hasSave; + } + // handle inidividual stuff + switch (entityType) { + // no break is intentional, specific entitytype cases are handled below + case 'Actor': + // challengeRating display + let challengeRating = () => { + let cr = Number(entityData.data.details.cr) || 0; + cr = (cr > 0 && cr < 1) ? "1/" + (1 / cr) : cr; + return cr; + }; + decorated.displayCR = challengeRating(); + decorated.orderCR = Number(entityData.data.details?.cr) || 0; + decorated.displaySize = 'unset'; + decorated.filterSize = 2; + if (entityData.data.details?.type instanceof String) { + let temp = entityData.data.details.type; + decorated.data.details.type = { value: temp }; + } + setProperty(decorated, 'data.details.type', entityData.data.details.type); + if (CONFIG.DND5E.actorSizes[entityData.data.traits.size] !== undefined) { + entityData.displaySize = CONFIG.DND5E.actorSizes[entityData.data.traits.size]; + } + switch (entityData.data.traits.size) { + case 'grg': + decorated.filterSize = 5; + break; + case 'huge': + decorated.filterSize = 4; + break; + case 'lg': + decorated.filterSize = 3; + break; + case 'sm': + decorated.filterSize = 1; + break; + case 'tiny': + decorated.filterSize = 0; + break; + case 'med': + default: + decorated.filterSize = 2; + break; + } + // getting value for HasSpells and damage types + decorated.hasSpells = false; + for (let item of entityData.items) { + if (item.type == 'spell') { + decorated.hasSpells = true; + } + if (item.data.damage && item.data.damage.parts && item.data.damage.parts.length > 0) { + for (let part of item.data.damage.parts) { + let type = part[1]; + if (decorated.damageDealt.indexOf(type) === -1) { + decorated.damageDealt.push(type); + } + } + } + } + break; + case 'Feat': + let reqString = entityData.requirements?.replace(/[0-9]/g, '').trim(); + let matchedClass = []; + for (let subClass in subClasses) { + if (reqString && reqString.toLowerCase().indexOf(subClass) !== -1) { + matchedClass.push(subClass); + } + else { + for (let sub of subClasses[subClass]) { + if (reqString && reqString.indexOf(sub) !== -1) { + matchedClass.push(sub); + break; + } + } + } + } + decorated.classRequirement = matchedClass; + decorated.classRequirementString = matchedClass.join(', '); + // getting uses/ressources status + decorated.usesRessources = entityData.hasLimitedUses; + decorated.hasSave = entityData.hasSave; + break; + case 'Item': + decorated.data.rarity = entityData.data.rarity.toLowerCase().replace(/ /g, '') || 'common'; + break; + case 'RollTable': + decorated.data.displayRoll = entityData.displayRoll; + decorated.data.formula = entityData.formula; + decorated.type = 'RollTable'; + decorated.data.details.type = entityData.flags?.['better-rolltables']?.['table-type'] || 'none'; + break; + case 'Spell': + decorated.data.components = entityData.data.components; + decorated.data.level = entityData.data.level; + // determining classes that can use the spell + let cleanSpellName = entity.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); + if (classList && classList[cleanSpellName]) { + let classes = classList[cleanSpellName]; + decorated.classRequirement = classes.split(','); + } + break; + } + return decorated; + } +} diff --git a/dist/scripts/classes/filter.js b/dist/scripts/classes/filter.js new file mode 100644 index 0000000..7a57063 --- /dev/null +++ b/dist/scripts/classes/filter.js @@ -0,0 +1,44 @@ +var FilterTypes; +(function (FilterTypes) { + FilterTypes[FilterTypes["text"] = 0] = "text"; + FilterTypes[FilterTypes["bool"] = 1] = "bool"; + FilterTypes[FilterTypes["select"] = 2] = "select"; + FilterTypes[FilterTypes["multiSelect"] = 3] = "multiSelect"; + FilterTypes[FilterTypes["numberCompare"] = 4] = "numberCompare"; +})(FilterTypes || (FilterTypes = {})); +/** + * Used to add custom filters to the Spell-Browser + * @param {string} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value + * @param {string} label - Title of the filter + * @param {string} type - type of filter + * possible types: + * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching + * bool: will see if the data at the path exists and not false. + * select: exactly matches the data with the chosen selector from possibleValues + * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data + * numberCompare: gives the option to compare numerical values, either with =, < or the > operator + * @param {null|boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters + * @param {boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden + */ +export class Filter { + constructor(path, label, type, possibleValues = null, valIsArray = false) { + this.path = ''; + this.label = ''; + this.type = ''; + this.valIsArray = false; + this.possibleValues = null; + this.is_text = false; + this.is_bool = false; + this.is_select = false; + this.is_multiSelect = false; + this.is_numberCompare = false; + this.path = path; + this.label = label; + if (type in FilterTypes) { + this.type = type; + setProperty(this, `is_${type}`, true); + } + this.possibleValues = possibleValues; + this.valIsArray = valIsArray; + } +} diff --git a/dist/scripts/compendium-browser.js b/dist/scripts/compendium-browser.js new file mode 100644 index 0000000..d725a1d --- /dev/null +++ b/dist/scripts/compendium-browser.js @@ -0,0 +1,189 @@ +import { ModuleSettings, CMPBrowser } from './modules/settings.js'; +import { Entities } from './modules/entities.js'; +import { Events } from '../hooks/events.js'; +import { Filter } from './modules/filter.js'; +import { Renderer } from './modules/renderer.js'; +//import Exporter from './modules/exporter.mjs'; +/* eslint-disable valid-jsdoc */ +/* eslint-disable complexity */ +export class CompendiumBrowser extends Application { + constructor() { + super(); + ModuleSettings.registerGameSettings(); + this.settings = ModuleSettings.initModuleSettings(); + this.filters = new Filter(); + this.currentLists = {}; + } + /** + * + */ + static get defaultOptions() { + const options = super.defaultOptions; + mergeObject(options, { + title: "CMPBrowser.compendiumBrowser", + tabs: [{ navSelector: ".tabs", contentSelector: ".content", initial: "Item" }], + classes: options.classes.concat('compendium-browser'), + template: "modules/compendium-browser/template/template.hbs", + width: 900, + height: 800, + resizable: true, + minimizable: true + }); + return options; + } + async initialize() { + // load settings + if (this.settings === undefined) { + this.settings = ModuleSettings.initModuleSettings(); + } + Renderer.loadTemplates([ + "modules/compendium-browser/template/template.hbs", + "modules/compendium-browser/template/entity-browser.hbs", + "modules/compendium-browser/template/entity-list.hbs", + "modules/compendium-browser/template/filter-container.hbs", + "modules/compendium-browser/template/settings.hbs", + "modules/compendium-browser/template/loading.hbs" + ]); + this.filters.addEntityFilters(); + this.hookCompendiumList(); + } + /** override */ + _onChangeTab(event, tabs, active) { + super._onChangeTab(event, tabs, active); + const html = this.element; + if (active != 'setting') { + Events.reloadFilters(html, active); + this.replaceList(html, active, { reload: false }); + } + } + /** + * + * @returns {Obejct} data + */ + async getData() { + let data = { + items: [], + actors: [], + filters: { + Spell: this.filters.getByName('Spell'), + Item: this.filters.getByName('Item'), + Actor: this.filters.getByName('Actor'), + RollTable: this.filters.getByName('RollTable'), + JournalEntry: this.filters.getByName('RollTable') + }, + showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, + showFeatBrowser: (game.user.isGM) || this.settings.allowFeatBrowser, + showItemBrowser: (game.user.isGM) || this.settings.allowItemBrowser, + showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, + showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser, + showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser, + settings: this.settings, + isGM: game.user.isGM + }; + return data; + } + /** override */ + activateListeners(html) { + super.activateListeners(html); + this.observer = new IntersectionObserver((entries, observer) => { + for (let [i, e] of entries.entries()) { + if (!e.isIntersecting) + break; + const el = e.target; + // Avatar image + //const img = li.querySelector("img"); + if (el && el.dataset.src) { + el.style['background-image'] = `url(${el.dataset.src})`; + delete el.dataset.src; + } + // No longer observe the target + observer.unobserve(e.target); + } + }); + Events.activateActionListener(html); + Events.activateItemListListeners(html); + Events.activateFilterListeners(html); + //Just for the loading image + if (this.observer) { + html.find(".entity-image").each((i, imageElement) => this.observer.observe(imageElement)); + } + } + hookCompendiumList() { + Hooks.on('renderCompendiumDirectory', (app, html, data) => { + this.hookCompendiumList(); + }); + let html = $('#compendium'); + if (this.settings === undefined) { + this.initSettings(); + } + if (game.user.isGM || this.settings.allowItemBrowser || this.settings.allowSpellBrowser || this.settings.allowActorBrowser) { + const cbButton = $(``); + html.find('.compendium-browser-btn').remove(); + // adding to directory-list since the footer doesn't exist if the user is not gm + html.find('.directory-footer').append(cbButton); + // Handle button clicks + cbButton.click(ev => { + ev.preventDefault(); + //0.4.1: Reset filters when you click button + this.filters.resetFilters(); + //0.4.3: Reset everything (including data) when you press the button - calls afterRender() hook + if (game.user.isGM || this.settings.allowSpellBrowser) { + this.refreshList = "Spell"; + } + else if (this.settings.allowFeatBrowser) { + this.refreshList = "Feat"; + } + else if (this.settings.allowItemBrowser) { + this.refreshList = "Item"; + } + else if (this.settings.allowActorBrowser) { + this.refreshList = "Actor"; + } + else if (this.settings.allowJournalEntryBrowser) { + this.refreshList = "JournalEntry"; + } + else if (this.settings.allowRollTableBrowser) { + this.refreshList = "RollTable"; + } + this.render(true); + }); + } + } + /** + * + * @param {*} html + * @param {*} entityType + * @param {*} options + */ + async replaceList(html, entityType, options = { reload: true }) { + //After rendering the first time or re-rendering trigger the load/reload of visible data + let entityListElement = document.querySelector('.tab.active .browser .cb_entities'); + if (entityListElement.childElementCount !== undefined) { + //0.4.2b: On a tab-switch, only reload if there isn't any data already + if (options?.reload || entityListElement.childElementCount < 1) { + const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; + await Renderer.updateLoading(entityType, 0, maxLoad); + // loadItems + const entityHelper = new Entities(); + let entityList = await entityHelper.loadAndFilter(entityType, true); + this.currentLists[entityType] = entityList = Entities._sortList(entityList, entityType); + //Uses loadAndFilterItems to read compendia for items which pass the current filters and render on this tab + const newEntitiesHTML = await Renderer.renderEntityList(entityList.entities, entityType, true); + entityListElement.setAttribute('data-active-filters', entityList.activeFilters); + entityListElement.innerHTML = newEntitiesHTML; + await Events.observeListElement(entityListElement, 'aside'); + //Reactivate listeners for clicking and dragging + Events.activateItemListListeners(html); + } + } + } + clearObject(obj) { + let newObj = {}; + for (let key in obj) { + if (obj[key] == true) { + newObj[key] = true; + } + } + return newObj; + } +} diff --git a/dist/scripts/modules/entities.js b/dist/scripts/modules/entities.js new file mode 100644 index 0000000..ecc0523 --- /dev/null +++ b/dist/scripts/modules/entities.js @@ -0,0 +1,181 @@ +import { Filter } from './filter.js'; +import { Renderer } from './renderer.js'; +import { CMPBrowser } from './settings.js'; +import { compactEntity } from '../classes/compactEntity.js'; +import { decoratedEntity } from '../classes/decoratedEntity.js'; +import { compactList } from '../classes/compactList.js'; +export class Entities { + constructor() { + this.packList = null; + this.classList = null; + this.subClassList = null; + this.itemsLoaded = false; + } + /** + * + * @param {string} entityType + * @param {any} updateLoading + * + * @returns {Promise} + */ + async loadAndFilter(entityType = "Item", updateLoading = null) { + console.log(`Load and Filter Items | Started loading ${entityType}s`); + console.time("loadAndFilterItems"); + await this.checkListsLoaded(); + const Category = (entityType === 'Spell' || entityType === 'Feat') ? 'Item' : entityType; + const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; + const ActiveFilters = game.compendiumBrowser.filters.getByName(entityType).activeFilters; + const packs = game.packs.filter((pack) => pack.metadata.entity === Category); + //0.4.1: Load and filter just one of spells, feats, and items (specified by browserTab) + let unfoundSpells = ''; + let numItemsLoaded = 0, numPacks = packs.length, comp_list = new compactList(); + comp_list.activeFilters = JSON.stringify(ActiveFilters); + //Filter the full list, but only save the core compendium information + displayed info + for (let pack of packs) { + if (game.compendiumBrowser.settings.loadedCompendium[pack.metadata.entity][pack.collection].load) { + //FIXME: How much could we do with the loaded index rather than all content? + //OR filter the content up front for the decoratedItem.type?? + await pack.getDocuments().then((content) => { + for (let currentEntity of content) { + // fail fast + if (currentEntity.data.type != entityType.toLowerCase() && entityType != Category) + continue; + if (!Filter.passesFilter(currentEntity, ActiveFilters)) + continue; + let decorated = decoratedEntity.decorate(currentEntity, entityType, this.packList, this.classList, this.subClassList); + let compact = new compactEntity(); + // set common properties for all entities + compact._id = decorated._id; + compact.name = decorated.name; + compact.compendium = pack.collection; + compact.img = decorated.img; + compact.type = decorated.type; + compact.data = decorated.data; + compact.flags = decorated.flags; + if (ActiveFilters) { + for (let index in ActiveFilters) { + setProperty(compact, `tags.${ActiveFilters[index].path}`, ActiveFilters[index].value); + } + } + if (Category == 'Item') { + compact.dae = (decorated.effects?.size) || false; + } + switch (entityType) { + case "Spell": + case "Feat": + if (entityType === 'Spell' || ["feat", "class"].includes(decorated.type)) { + compact.classRequirement = decorated.classRequirement; + } + break; + case "Item": + //0.4.5: Itm type for true items could be many things (weapon, consumable, etc) so we just look for everything except spells, feats, classes + if (!["spell", "feat", "class"].includes(decorated.type)) { + compact.rarity = decorated.data.rarity; + compact.ac = (decorated.data?.armor?.type) ? decorated.data?.armor?.value || false : false; + if (compact.type == 'weapon' && (decorated.data?.range?.value)) { + setProperty(compact, `tags.range`, decorated.data?.range?.value + decorated.data?.range?.units); + } + } + break; + case "Actor": + compact.displayCR = decorated.displayCR; + compact.displaySize = decorated.displaySize; + compact.displayType = decorated.data?.details?.type; + compact.orderCR = decorated.orderCR; + compact.orderSize = decorated.filterSize; + compact.data.details = decorated.data.details; + break; + default: + break; + } + comp_list.addEntity(compact); + if (updateLoading) { + Renderer.updateLoading(entityType, numItemsLoaded, numPacks, 500); + } + if (numItemsLoaded++ >= maxLoad) + break; + } + }); // get Entities + } + if (numItemsLoaded >= maxLoad) + break; + } //for packs + console.timeEnd("loadAndFilterItems"); + console.log(`Load and Filter Items | Finished loading ${comp_list.size()} ${entityType}s`); + return comp_list; + } + /** + * + * @param {compactList} list + * @param {string} entityType + * @param {string} orderBy + * @returns {compactList} + */ + static _sortList(list, entityType, orderBy) { + const SortCollator = new Intl.Collator(game.i18n.lang, { numeric: true, sensitivity: 'base' }); + if (entityType === 'Actor') { + switch (orderBy) { + case 'name': + list.entities.sort((left, right) => SortCollator.compare(left.name, right.name)); + break; + case 'cr': + list.entities.sort((left, right) => { + return left.displayCR - right.displayCR || SortCollator.compare(left.name, right.name); + }); + break; + case 'size': + list.entities.sort((left, right) => { + return left.orderSize - right.orderSize || SortCollator.compare(left.name, right.name); + }); + break; + } + } + else { + if (orderBy) { + list.entities.sort((left, right) => left.name.localeCompare(right.name)); + } + else { + let defaultSort = new Map([ + ['Item', 'type'], + ['Spell', 'data.level'], + ['Feats', 'data.class'], + ['RollTable', 'compendium'], + ['JounralEntry', 'name'], + ]); + list.entities.sort((left, right) => { + let sort = defaultSort.get(entityType) || '', result = SortCollator.compare(getProperty(left, sort), getProperty(right, sort)); + return result || SortCollator.compare(left.name, right.name); + }); + } + } + return list; + } + /** + * Check all the prepared list with extra info are loaded + */ + async checkListsLoaded() { + const dataPath = '/modules/compendium-browser/data/'; + //Provides extra info not in the standard SRD, like which classes can learn a spell + if (!this.classList) { + this.classList = await fetch(dataPath + 'spell-classes.json').then(result => { + return result.json(); + }).then(list => { + return list; + }); + } + if (!this.packList) { + this.packList = await fetch(dataPath + 'item-packs.json').then(result => { + return result.json(); + }).then(list => { + return list; + }); + } + if (!this.subClassList) { + this.subClassList = await fetch(dataPath + 'sub-classes.json').then(result => { + return result.json(); + }).then(list => { + return list; + }); + } + } +} diff --git a/dist/scripts/modules/exporter.js b/dist/scripts/modules/exporter.js new file mode 100644 index 0000000..1416018 --- /dev/null +++ b/dist/scripts/modules/exporter.js @@ -0,0 +1,34 @@ +export class Exporter { + /** + * Create a new RollTable from a given Set of entitites. + * + * @param {string} tableName the name of the table entity that will be created + * @param {Array} entities a set of + * @param {function(Entity)} weightPredicate a function that returns a weight (number) that will be used + * for the tableResult weight for that given entity. returning 0 will exclude the entity from appearing in the table + */ + static async createTableFromSelection(tableName = new Date().valueOf().toString(), entities, weightPredicate = null, options = null) { + let data = { name: tableName }, tableArray = []; + if (!entities || !(entities.length > 0)) + return false; + const newTable = await RollTable.create(data); + ui.notifications.info(`Starting generation of a rolltable with ${entities.length} entries.`); + for (let entity of entities) { + let weight = weightPredicate != null ? weightPredicate(entity) : 1; + if (weight <= 0) + continue; + let tableRowData = {}; + tableRowData.type = 2; + tableRowData.collection = entity.entryCompendium; + tableRowData.text = entity.entryName; + tableRowData.img = entity.entryImage; + tableRowData.weight = weight; + tableRowData.range = [1, 1]; + tableArray.push(tableRowData); + } + await newTable.createEmbeddedDocuments('TableResult', tableArray); + await newTable.normalize(); + ui.notifications.info(`Rolltable ${tableName} with ${tableArray.length} entries was generated.`); + return true; + } +} diff --git a/dist/scripts/modules/filter.js b/dist/scripts/modules/filter.js new file mode 100644 index 0000000..0f2d027 --- /dev/null +++ b/dist/scripts/modules/filter.js @@ -0,0 +1,304 @@ +import { Filter as FilterEntity } from "../classes/filter.js"; +export class Filter { + constructor() { + this.Spell = this._getInitialFilters(); + this.Actor = this._getInitialFilters(); + this.Feat = this._getInitialFilters(); + this.Item = this._getInitialFilters(); + this.RollTable = this._getInitialFilters(); + this.JournalEntry = this._getInitialFilters(); + } + /** + * + * @param {any} subject + * @param {any} filters + * + * @returns {boolean} + */ + static passesFilter(subject, filters) { + for (let filter of Object.values(filters)) { + let prop = getProperty(subject, `data.${filter.path}`) || getProperty(subject, filter.path); + if (prop === undefined) + return false; + if (filter.type === 'numberCompare') { + switch (filter.operator) { + case '=': + if (prop != filter.value) { + return false; + } + break; + case '<': + if (prop >= filter.value) { + return false; + } + break; + case '>': + if (prop <= filter.value) { + return false; + } + break; + } + continue; + } + if (filter.valIsArray === false) { + if (filter.type === 'text') { + if (prop.toLowerCase().indexOf(filter.value.toLowerCase()) === -1) { + return false; + } + } + else { + if (filter.value !== undefined && prop !== undefined && prop != filter.value && !(filter.value === true && prop)) { + return false; + } + if (filter.values && filter.values.indexOf(prop) === -1) { + return false; + } + } + } + else { + if (prop === undefined) + return false; + if (typeof prop === 'object') { + if (filter.value) { + if (prop.indexOf(filter.value) === -1) { + return false; + } + } + else if (filter.values) { + for (let val of filter.values) { + if (prop.indexOf(val) !== -1) { + continue; + } + return false; + } + } + } + else { + for (let val of filter.values) { + if (prop === val) { + continue; + } + } + return false; + } + } + } + return true; + } + /** + * + * @param name + * @returns any + */ + getByName(name) { + return getProperty(this, name); + } + /** + * + * @param {string} entityType + * @returns {Iterable|undefined} + */ + getByEntityType(entityType) { + return getProperty(this, entityType); + } + resetFilters() { + this.Spell.activeFilters = {}; + this.Feat.activeFilters = {}; + this.Item.activeFilters = {}; + this.Actor.activeFilters = {}; + this.RollTable.activeFilters = {}; + this.JournalEntry.activeFilters = {}; + } + /** + * + * @returns {Object} + */ + _getInitialFilters() { + return { + registeredFilterCategorys: {}, + activeFilters: {} + }; + } + /** + * add entityfilters + */ + async addEntityFilters() { + await this.addSpellFilters(); + await this.addFeatFilters(); + await this.addItemFilters(); + await this.addActorFilters(); + await this.addRollTableFilters(); + } + /** + * Used to add custom filters to the Spell-Browser + * @param {string} entityType type of entity for the filter + * @param {string} category - Title of the category + * @param {string} label - Title of the filter + * @param {string} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value + * @param {string} type - type of filter + * possible filter: + * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching + * bool: will see if the data at the path exists and not false. + * select: exactly matches the data with the chosen selector from possibleValues + * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data + * numberCompare: gives the option to compare numerical values, either with =, < or the > operator + * @param {null|boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters + * @param {boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden + */ + async addFilter(entityType, category, label, path, type, possibleValues = null, valIsArray = false) { + let filter = new FilterEntity(path, label, type, possibleValues, valIsArray), catId = category.replace(/\W/g, ''), target = this.getByName(entityType).registeredFilterCategorys; + if (target[catId] === undefined) { + target[catId] = { label: category, filters: [] }; + } + target[catId].filters.push(filter); + } + /** + * Add all spellfilters + * + * @todo convert this to read and use an iteratable object that can be stored in an extra file + */ + async addSpellFilters() { + const SPELL = 'Spell'; + // Spellfilters + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.lvl"), 'data.level', 'multiSelect', [game.i18n.localize("CMPBrowser.cantrip"), 1, 2, 3, 4, 5, 6, 7, 8, 9]); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.school"), 'data.school', 'select', CONFIG.DND5E.spellSchools); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.castingTime"), 'data.activation.type', 'select', { + action: game.i18n.localize("DND5E.Action"), + bonus: game.i18n.localize("CMPBrowser.bonusAction"), + reaction: game.i18n.localize("CMPBrowser.reaction"), + minute: game.i18n.localize("DND5E.TimeMinute"), + hour: game.i18n.localize("DND5E.TimeHour"), + day: game.i18n.localize("DND5E.TimeDay") + }); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.spellType"), 'data.actionType', 'select', CONFIG.DND5E.itemActionTypes); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'data.classes', 'select', { + artificer: game.i18n.localize("CMPBrowser.artificer"), + bard: game.i18n.localize("CMPBrowser.bard"), + cleric: game.i18n.localize("CMPBrowser.cleric"), + druid: game.i18n.localize("CMPBrowser.druid"), + paladin: game.i18n.localize("CMPBrowser.paladin"), + ranger: game.i18n.localize("CMPBrowser.ranger"), + sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), + warlock: game.i18n.localize("CMPBrowser.warlock"), + wizard: game.i18n.localize("CMPBrowser.wizard"), + }, true); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.ritual"), 'data.components.ritual', 'bool'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.concentration"), 'data.components.concentration', 'bool'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.vocal"), 'data.components.vocal', 'bool'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.somatic"), 'data.components.somatic', 'bool'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.material"), 'data.components.material', 'bool'); + } + async addItemFilters() { + const ITEM = 'Item'; + // Item Filters + await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); + await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), "Item Type", 'type', 'select', { + consumable: game.i18n.localize("DND5E.ItemTypeConsumable"), + backpack: game.i18n.localize("DND5E.ItemTypeContainer"), + equipment: game.i18n.localize("DND5E.ItemTypeEquipment"), + loot: game.i18n.localize("DND5E.ItemTypeLoot"), + tool: game.i18n.localize("DND5E.ItemTypeTool"), + weapon: game.i18n.localize("DND5E.ItemTypeWeapon") + }); + await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), "Packs", 'matchedPacks', 'select', { + burglar: "Burglar's Pack", + diplomat: "Diplomat's Pack", + dungeoneer: "Dungeoneer's Pack", + entertainer: "Entertainer's Pack", + explorer: "Explorer's Pack", + monsterhunter: "Monster Hunter's Pack", + priest: "Priest's Pack", + scholar: "Scholar's Pack", + }, true); + await this.addFilter(ITEM, "Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); + await this.addFilter(ITEM, "Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); + await this.addFilter(ITEM, "Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); + await this.addFilter(ITEM, "Item Subtype", "Weapon", 'data.weaponType', 'text', CONFIG.DND5E.weaponTypes); + await this.addFilter(ITEM, "Item Subtype", "Equipment", 'data.armor.type', 'text', CONFIG.DND5E.equipmentTypes); + await this.addFilter(ITEM, "Item Subtype", "Consumable", 'data.consumableType', 'text', CONFIG.DND5E.consumableTypes); + await this.addFilter(ITEM, "Magic Items", "Rarity", 'data.rarity', 'select', { + Common: "Common", + Uncommon: "Uncommon", + Rare: "Rare", + "Very rare": "Very Rare", + Legendary: "Legendary" + }); + } + async addFeatFilters() { + const FEAT = 'Feat'; + this.addFilter(FEAT, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); + this.addFilter(FEAT, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'classRequirement', 'select', { + artificer: game.i18n.localize("CMPBrowser.artificer"), + barbarian: "Barbarian", + bard: game.i18n.localize("CMPBrowser.bard"), + cleric: game.i18n.localize("CMPBrowser.cleric"), + druid: game.i18n.localize("CMPBrowser.druid"), + fighter: "Fighter", + monk: "Monk", + paladin: game.i18n.localize("CMPBrowser.paladin"), + ranger: game.i18n.localize("CMPBrowser.ranger"), + rogue: "Rogue", + sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), + warlock: game.i18n.localize("CMPBrowser.warlock"), + wizard: game.i18n.localize("CMPBrowser.wizard") + }, true); + this.addFilter(FEAT, "Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); + this.addFilter(FEAT, "Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); + this.addFilter(FEAT, "Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); + } + async addActorFilters() { + const isFoundryV8 = game.data.version.startsWith("0.8"), ACTOR = 'Actor'; + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.details.source', 'text'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.size"), 'data.traits.size', 'select', CONFIG.DND5E.actorSizes); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasSpells"), 'hasSpells', 'bool'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegAct"), 'data.resources.legact.max', 'bool'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegRes"), 'data.resources.legres.max', 'bool'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.cr"), 'data.details.cr', 'numberCompare'); + //Foundry 0.8.x: Creature type (data.details.type) is now a structure, so we check data.details.types.value instead + let actorDetailsPath; + if (isFoundryV8) { + actorDetailsPath = "data.details.type.value"; + } + else { //0.7.x + actorDetailsPath = "data.details.type"; + } + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.creatureType"), actorDetailsPath, 'text', { + aberration: game.i18n.localize("CMPBrowser.aberration"), + beast: game.i18n.localize("CMPBrowser.beast"), + celestial: game.i18n.localize("CMPBrowser.celestial"), + construct: game.i18n.localize("CMPBrowser.construct"), + dragon: game.i18n.localize("CMPBrowser.dragon"), + elemental: game.i18n.localize("CMPBrowser.elemental"), + fey: game.i18n.localize("CMPBrowser.fey"), + fiend: game.i18n.localize("CMPBrowser.fiend"), + giant: game.i18n.localize("CMPBrowser.giant"), + humanoid: game.i18n.localize("CMPBrowser.humanoid"), + monstrosity: game.i18n.localize("CMPBrowser.monstrosity"), + ooze: game.i18n.localize("CMPBrowser.ooze"), + plant: game.i18n.localize("CMPBrowser.plant"), + undead: game.i18n.localize("CMPBrowser.undead") + }); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityStr"), 'data.abilities.str.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityDex"), 'data.abilities.dex.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCon"), 'data.abilities.con.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityInt"), 'data.abilities.int.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityWis"), 'data.abilities.wis.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCha"), 'data.abilities.cha.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamImm"), 'data.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamRes"), 'data.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamVuln"), 'data.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.ConImm"), 'data.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("CMPBrowser.dmgDealt"), 'damageDealt', 'multiSelect', CONFIG.DND5E.damageTypes, true); + } + async addRollTableFilters() { + const RT = 'RollTable'; + this.addFilter(RT, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.TableType"), 'flags.better-rolltables.table-type', 'select', { + none: "FoundryVTT default", + better: "Better", + loot: "Loot", + story: "Story", + }); + } +} diff --git a/dist/scripts/modules/renderer.js b/dist/scripts/modules/renderer.js new file mode 100644 index 0000000..e552b2c --- /dev/null +++ b/dist/scripts/modules/renderer.js @@ -0,0 +1,69 @@ +export class Renderer { + /** + * + * @param {Array} templateStrings + */ + static async loadTemplates(templateStrings) { + return await loadTemplates(templateStrings); + } + /** + * Render the Information section + * + * @param {HTMLElement} messageElement + * @param entityType + * @param numLoaded + * @param maxLoaded + * + */ + static async renderLoading(messageElement, entityType, numLoaded, numPacks, maxLoaded = false) { + if (!messageElement) + return; + let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.hbs", { numLoaded: numLoaded, entityType: entityType, numPacks: numPacks, maxLoaded: maxLoaded }); + messageElement.innerHTML = "" + loadingHTML; + } + /** + * Update Loading Message for the given entity + * + * @param {string} entityType + * @param {number} numLoaded + * @param {number} maxLoad + */ + static async updateLoading(entityType, numLoaded = 0, numPacks = 1, maxLoad = 500) { + let loader = document.getElementById('CBInfoMessage'); + if (loader) { + Renderer.renderLoading(loader, entityType, numLoaded, numPacks, numLoaded >= maxLoad); + } + } + /** + * + * @param {String} entityType + * @param {Boolean|null} updateLoading + * + */ + static async renderEntityList(entityList, entityType, updateLoading = null) { + return await renderTemplate(`modules/compendium-browser/template/entity-list.hbs`, { entityItems: entityList, entityType: entityType }); + } + /** + * + * @param {Object} filters + * @param {String} entityType + * @returns {String} hmtl + */ + static async renderFilters(filters, entityType) { + return await renderTemplate(`modules/compendium-browser/template/filter-container.hbs`, { entityType: entityType, filters: filters }); + } + /* Hook to load the first data */ + /** + * + * @param CompendiumBrowserApp + * @param html + * @returns {Promise} + */ + static async afterRender(CompendiumBrowserApp, html) { + if (!CompendiumBrowserApp?.refreshList) { + return; + } + await CompendiumBrowserApp.replaceList(html, CompendiumBrowserApp.refreshList); + CompendiumBrowserApp.refreshList = undefined; + } +} diff --git a/dist/scripts/modules/settings.js b/dist/scripts/modules/settings.js new file mode 100644 index 0000000..703222a --- /dev/null +++ b/dist/scripts/modules/settings.js @@ -0,0 +1,93 @@ +export const CMPBrowser = { + MODULE_NAME: "compendium-browser", + MODULE_VERSION: "0.5.1", + MAXLOAD: 500, //Default for the maximum number to load before displaying a message that you need to filter to see more +}; +const SETTINGS = 'settings'; +export class ModuleSettings { + /** + * constructs and returns defaults settings + */ + static _getDefaults() { + let defaultSettings = { + loadedCompendium: { + Actor: {}, + Item: {}, + JournalEntry: {}, + RollTable: {}, + } + }; + for (let compendium of game.packs) { + if (defaultSettings.loadedCompendium[compendium.metadata.entity]) { + defaultSettings.loadedCompendium[compendium.metadata.entity][compendium.collection] = { + load: true, + name: `${compendium.metadata.label} (${compendium.collection})` + }; + } + } + return defaultSettings; + } + /** + * + * @returns {Array} Settings + */ + static initModuleSettings() { + let defaultSettings = ModuleSettings._getDefaults(); + // load settings from container and apply to default settings (available compendia might have changed) + let settings = game.settings.get(CMPBrowser.MODULE_NAME, SETTINGS); + for (let compKey in defaultSettings.loadedSpellCompendium) { + if (settings.loadedSpellCompendium[compKey] !== undefined) { + defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; + } + } + for (let compKey in defaultSettings.loadedActorCompendium) { + if (settings.loadedActorCompendium[compKey] !== undefined) { + defaultSettings.loadedActorCompendium[compKey].load = settings.loadedActorCompendium[compKey].load; + } + } + defaultSettings.allowSpellBrowser = settings.allowSpellBrowser ? true : false; + defaultSettings.allowFeatBrowser = settings.allowFeatBrowser ? true : false; + defaultSettings.allowItemBrowser = settings.allowItemBrowser ? true : false; + defaultSettings.allowActorBrowser = settings.allowActorBrowser ? true : false; + defaultSettings.allowJournalEntryBrowser = settings.allowJournalEntryBrowser ? true : false; + defaultSettings.allowRollTableBrowser = settings.allowRollTableBrowser ? true : false; + if (game.user.isGM) { + game.settings.set(CMPBrowser.MODULE_NAME, SETTINGS, defaultSettings); + console.log("New default settings set"); + console.log(defaultSettings); + } + return defaultSettings; + } + /** + * Registe ther very basic settings in the game world. + */ + static registerGameSettings() { + // creating game setting container + game.settings.register(CMPBrowser.MODULE_NAME, SETTINGS, { + name: "Compendium Browser Settings", + hint: "Settings to exclude packs from loading and visibility of the browser", + default: ModuleSettings._getDefaults(), + type: Object, + scope: 'world', + onChange: settings => { + game.compendiumBrowser.settings = settings; + } + }); + game.settings.register(CMPBrowser.MODULE_NAME, "maxload", { + name: game.i18n.localize("CMPBrowser.SETTING.Maxload.NAME"), + hint: game.i18n.localize("CMPBrowser.SETTING.Maxload.HINT"), + scope: "world", + config: true, + default: CMPBrowser.MAXLOAD, + type: Number, + range: { + min: 200, + max: 5000, + step: 100 + } + }); + } + static saveSettings() { + game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings); + } +} diff --git a/dist/scripts/versioning/version-check.js b/dist/scripts/versioning/version-check.js new file mode 100644 index 0000000..c09946b --- /dev/null +++ b/dist/scripts/versioning/version-check.js @@ -0,0 +1,32 @@ +/** + * Version check function from Forien Unedintified item module + */ +export class VersionCheck { + static _reg(mN) { + if (this._r) + return; + game.settings.register(mN, 'version', { + name: `${mN} Version`, + default: "0.0.0", + type: String, + scope: 'client', + }); + this._r = true; + } + static check(mN) { + if (!this._r) + this._reg(mN); + let mV = this.get(mN); + let oV = game.settings.get(mN, "version"); + return isNewerVersion(mV, oV); + } + static set(mN, v) { + if (!this._r) + this._reg(mN); + game.settings.set(mN, "version", v); + } + static get(mN) { + return game.modules.get(mN).data.version; + } +} +VersionCheck._r = false; diff --git a/dist/styles/compendium-browser.css b/dist/styles/compendium-browser.css new file mode 100644 index 0000000..a58b71b --- /dev/null +++ b/dist/styles/compendium-browser.css @@ -0,0 +1,382 @@ +#compendium .directory-footer { + display: block; +} +#compendium .directory-footer .compendium-browser-btn { + margin-top: 5px; +} +.compendium-browser { + overflow-y: hidden !important; + max-width: 90%; +} +.compendium-browser .info { + padding: 0.25em 0.6em; + text-align: center; + border: 1px solid #acacac; + display: inline-block; + margin: auto; + background: rgba(0, 0, 0, 0.3); + border-radius: 0.3em; + color: #cecece; +} +.compendium-browser .window-content { + overflow-y: hidden !important; + background: var(--background); + height: 100%; + padding: 0.5em 0.5em 0 0.5em; +} +.compendium-browser .window-content .parent { + height: 100%; +} +.compendium-browser .window-content .parent .content { + overflow-y: hidden !important; + height: calc(100% - 2em); +} +.compendium-browser .window-content .parent .content .tab { + overflow-y: hidden !important; + height: 100%; +} +.compendium-browser .window-content .parent .content .tab .browser { + overflow-y: hidden !important; + height: 100%; +} +.compendium-browser .window-content .parent .content .tab .browser ul { + overflow-y: auto; + height: 100%; +} +.compendium-browser .window-content .parent .content .tab .settings { + overflow-y: auto; + height: 100%; +} +.compendium-browser .tabs { + max-height: 2em; + color: var(--primary-font); + background: var(--header-background); +} +.item .compendium-browser .tabs .active { + background: var(--background); +} +.compendium-browser .tabContainer { + height: calc(100% - 2em); +} +.compendium-browser .tabContainer .tab { + width: 100%; + height: 100%; + overflow: scroll; +} +.compendium-browser .control-area { + display: grid; + grid-template-rows: 2em 1fr; + height: 100%; + grid-template-areas: "TOGGLER" "FILTERS"; + grid-gap: 0; + width: max-content; + border-radius: 0.5em; + position: relative; + background: var(--header-background); + /* Toggler Functionality */ +} +.compendium-browser .control-area button { + background: rgba(0, 0, 0, 0.05); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; +} +.compendium-browser .control-area .toggler { + display: none; + grid-area: TOGGLER; + font-size: 0; + width: 0; + height: 0; +} +.compendium-browser .control-area .toggler:after { + display: inline-block; + height: 16px; + width: 16px; + border-radius: 3px; + background: none; + color: darkgrey; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + text-rendering: auto; + content: "\f0b0"; + font-variant: normal; +} +.compendium-browser .control-area .toggler ~ #cb_filtercontainer { + display: none; +} +.compendium-browser .control-area .toggler:checked ~ #cb_filtercontainer { + background: rgba(0, 0, 0, 0.05); + display: initial; + grid-area: FILTERS; + width: max-content; + border-radius: 0.5em 0.5em 0 0; + position: relative; +} +.compendium-browser .control-area .filtercontainer { + color: var(--primary-font); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + background: rgba(0, 0, 0, 0.75); +} +.compendium-browser .control-area .filtercontainer h3 { + margin: 0; + cursor: pointer; +} +.compendium-browser .control-area .filtercontainer dl { + margin: 0; +} +.compendium-browser .control-area .filtercontainer div { + margin: 5px 0; +} +.compendium-browser .control-area .filtercontainer dt { + display: inline-block; + width: 40%; + padding-left: 5px; + font-weight: 400; +} +.compendium-browser .control-area .filtercontainer dd { + display: inline-block; + width: 58%; + margin-left: 0; +} +.compendium-browser .control-area .filtercontainer dd select { + width: 100%; +} +.compendium-browser .control-area .filtercontainer input, +.compendium-browser .control-area .filtercontainer select { + height: 1.4em; + font-size: 0.9em; + font-weight: 400; +} +.compendium-browser .control-area .filtercontainer .multiselect { + border: 1px solid #bbb; + border-radius: 3px; + vertical-align: middle; + line-height: 32px; + margin: 2px 0; +} +.compendium-browser .control-area .filtercontainer .multiselect label { + padding: 5px; +} +.compendium-browser .control-area .filtercontainer .multiselect input { + vertical-align: middle; +} +.compendium-browser .control-area .filtercontainer .small-input { + width: calc(100% - 44px); + height: 27px; + background: rgba(0, 0, 0, 0.05); + border: 1px solid #444; + border-radius: 3px; + padding: 0 3px; + text-overflow: ellipsis; +} +.compendium-browser .control-area .filtercontainer .small-select { + width: 40px; +} +.compendium-browser .browser { + display: grid; + grid-template-columns: 2em auto 2em; + grid-template-areas: "L_SIDEBAR CONTENT R_SIDEBAR"; + height: 100%; + overflow: hidden !important; +} +.compendium-browser .browser .window-content { + overflow-y: hidden !important; +} +.compendium-browser .browser ul .filter-tags { + display: none; +} +.compendium-browser .browser ul li span { + white-space: nowrap; + overflow: hidden; +} +.compendium-browser .browser .spacer { + display: inline-block; + min-width: 5px; +} +.compendium-browser .browser .spacer-large { + display: inline-block; + min-width: 15px; +} +.compendium-browser.entity-browser > li { + cursor: default; +} +.compendium-browser.entity-browser li .feat-tags { + text-align: justify; + margin-right: 3px; + margin-left: 3px; + text-transform: capitalize; +} +.compendium-browser.entity-browser li .item-tags { + text-align: justify; + margin-right: 3px; + margin-left: 3px; + text-transform: capitalize; +} +.compendium-browser .list-area { + grid-template-rows: auto; + grid-template-columns: auto; + grid-area: CONTENT; +} +.compendium-browser .settings .settings-group { + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + font-size: 0.9em; + max-width: fit-content; + float: left; +} +.compendium-browser .settings .settings-group h3 { + margin: 0; + cursor: pointer; +} +.compendium-browser .settings .settings-group label { + display: block; +} +.compendium-browser .settings .settings-group h4 { + display: inline-block; + vertical-align: middle; + height: 100%; +} +.compendium-browser .actions { + box-sizing: content-box; + flex: 0 0 32px; + background: rgba(86, 85, 78, 0.18); + border-radius: 0 0 0.5em 0.5em; + padding: 0 0.7em; +} +.compendium-browser .actions ul { + list-style: none; + display: flex; + flex-wrap: nowrap; + flex-direction: row-reverse; +} +.compendium-browser .actions li { + box-sizing: content-box; +} +.compendium-browser .actions li button { + cursor: pointer; + color: #ededed; + background-color: #444; + border-left: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 0; +} +.compendium-browser .cb_entities { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(15vw, 100%), 1fr)); +} +.compendium-browser .cb_entities .entity-header { + display: grid; + grid-template-columns: 0.55fr 0.1fr 1fr; + gap: 0px 2.5em; + text-align: center; + border: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 3px; +} +.compendium-browser .cb_entities .entity { + background: var(--background); + color: darkgrey; + border-radius: 5px; + margin: 0.2em; + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 0.1em; + height: clamp(5vh, 6vh, 7vh); + box-shadow: 0 0 3px inset var(--light-color); + display: grid; + grid-template-columns: 3em 1fr; + grid-template-areas: "Image Name" "Image Tags"; +} +.compendium-browser .cb_entities .entity:hover { + box-shadow: 0 0 3px inset var(--icon-hover); +} +.compendium-browser .cb_entities .entity .entity-image { + cursor: pointer; + background-repeat: no-repeat; + border: 1px solid rgba(83, 75, 75, 0.3); + padding: 0.1em; + border-radius: 0.5em; + justify-content: center; + align-items: center; + background-clip: border-box; + background-image: url('/icons/svg/daze.svg'); + background-size: cover; + width: 3.2em; + grid-area: Image; + box-shadow: 0 0 0.2em 0.1em inset rgba(43, 45, 46, 0.863); +} +.compendium-browser .cb_entities .entity .entity-name { + grid-area: Name; + color: rgba(236, 229, 229, 0.932); + font-size: 12px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 0.9vw; + font-weight: 400; + overflow: hidden; + height: 100%; +} +.compendium-browser .cb_entities .entity ul.tags { + list-style: none; + min-width: initial; + box-sizing: content-box; + grid-area: Tags; + display: grid; + grid-template-columns: repeat(auto-fit, 16px); + margin: 0; +} +.compendium-browser .cb_entities .entity .tags .spell-tag { + color: var(--background); +} +.compendium-browser .cb_entities .entity .tags .spell-tag.active { + color: var(--primary-font); +} +.compendium-browser .cb_entities .entity .tags li { + cursor: help; + color: darkgray; + justify-content: center; + align-items: center; + height: 1.6em; + width: 1.6em; + font-size: 0.8em; + line-height: 1.6em; + display: inline-block; + padding: 0.2em; + border-radius: 0.1em; +} +.compendium-browser .cb_entities .entity.r_common { + border-left: 0.3em solid fff; +} +.compendium-browser .cb_entities .entity.r_common .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(fff, 0.75); +} +.compendium-browser .cb_entities .entity.r_uncommon { + border-color: #3dbb2d; + background: linear-gradient(90deg, rgba(rgba(30, 255, 0), 1) 0%, rgba(163, 53, 238, 0) 35%); +} +.compendium-browser .cb_entities .entity.r_uncommon .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(rgba(30, 255, 0), 0.75); +} +.compendium-browser .cb_entities .entity.r_rare { + border-color: darkblue; + background: linear-gradient(90deg, #0070dd 0%, rgba(0, 0, 0, 0.2) 35%); +} +.compendium-browser .cb_entities .entity.r_rare .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(0, 112, 221, 0.75); +} +.compendium-browser .cb_entities .entity.r_veryrare { + border-color: #a335ee; + background: linear-gradient(90deg, #a335ee 0%, rgba(163, 53, 238, 0) 35%); +} +.compendium-browser .cb_entities .entity.r_veryrare .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(163, 53, 238, 0.75); +} +.compendium-browser .cb_entities .entity.r_legendary { + border-color: #ff8000; + background: linear-gradient(90deg, #ff8000 0%, rgba(255, 128, 0, 0) 35%); +} +.compendium-browser .cb_entities .entity.r_legendary .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(255, 128, 0, 0.75); +} diff --git a/dist/styles/compendium-browser.min.css b/dist/styles/compendium-browser.min.css new file mode 100644 index 0000000..1686105 --- /dev/null +++ b/dist/styles/compendium-browser.min.css @@ -0,0 +1 @@ +#compendium .directory-footer{display:block}#compendium .directory-footer .compendium-browser-btn{margin-top:5px}.compendium-browser{overflow-y:hidden!important;max-width:90%}.compendium-browser .info{padding:.25em .6em;text-align:center;border:1px solid #acacac;display:inline-block;margin:auto;background:rgba(0,0,0,.3);border-radius:.3em;color:#cecece}.compendium-browser .window-content{overflow-y:hidden!important;background:var(--background);height:100%;padding:.5em .5em 0 .5em}.compendium-browser .window-content .parent{height:100%}.compendium-browser .window-content .parent .content{overflow-y:hidden!important;height:calc(100% - 2em)}.compendium-browser .window-content .parent .content .tab{overflow-y:hidden!important;height:100%}.compendium-browser .window-content .parent .content .tab .browser{overflow-y:hidden!important;height:100%}.compendium-browser .window-content .parent .content .tab .browser ul{overflow-y:auto;height:100%}.compendium-browser .window-content .parent .content .tab .settings{overflow-y:auto;height:100%}.compendium-browser .tabs{max-height:2em;color:var(--primary-font);background:var(--header-background)}.compendium-browser .tabs.item.active{background:var(--background)}.compendium-browser .tabContainer{height:calc(100% - 2em)}.compendium-browser .tabContainer .tab{width:100%;height:100%;overflow:scroll}.compendium-browser .control-area{display:grid;grid-template-rows:2em 1fr;height:100%;grid-template-areas:"TOGGLER" "FILTERS";grid-gap:0;width:max-content;border-radius:.5em;position:relative;background:var(--header-background)}.compendium-browser .control-area button{background:rgba(0,0,0,.05);border:1px solid #bbb;border-radius:5px;margin-top:5px;padding:2px}.compendium-browser .control-area .toggler{display:none;grid-area:TOGGLER;font-size:0;width:0;height:0}.compendium-browser .control-area .toggler:after{display:inline-block;height:16px;width:16px;border-radius:3px;background:0 0;color:#a9a9a9;font-family:"Font Awesome 5 Free";font-weight:900;text-rendering:auto;content:"\f0b0";font-variant:normal}.compendium-browser .control-area .toggler~#cb_filtercontainer{display:none}.compendium-browser .control-area .toggler:checked~#cb_filtercontainer{background:rgba(0,0,0,.05);display:initial;grid-area:FILTERS;width:max-content;border-radius:.5em .5em 0 0;position:relative}.compendium-browser .control-area .filtercontainer{color:var(--primary-font);border:1px solid #bbb;border-radius:5px;margin-top:5px;padding:2px;background:rgba(0,0,0,.75)}.compendium-browser .control-area .filtercontainer h3{margin:0;cursor:pointer}.compendium-browser .control-area .filtercontainer dl{margin:0}.compendium-browser .control-area .filtercontainer div{margin:5px 0}.compendium-browser .control-area .filtercontainer dt{display:inline-block;width:40%;padding-left:5px;font-weight:400}.compendium-browser .control-area .filtercontainer dd{display:inline-block;width:58%;margin-left:0}.compendium-browser .control-area .filtercontainer dd select{width:100%}.compendium-browser .control-area .filtercontainer input,.compendium-browser .control-area .filtercontainer select{height:1.4em;font-size:.9em;font-weight:400}.compendium-browser .control-area .filtercontainer .multiselect{border:1px solid #bbb;border-radius:3px;vertical-align:middle;line-height:32px;margin:2px 0}.compendium-browser .control-area .filtercontainer .multiselect label{padding:5px}.compendium-browser .control-area .filtercontainer .multiselect input{vertical-align:middle}.compendium-browser .control-area .filtercontainer .small-input{width:calc(100% - 44px);height:27px;background:rgba(0,0,0,.05);border:1px solid #444;border-radius:3px;padding:0 3px;text-overflow:ellipsis}.compendium-browser .control-area .filtercontainer .small-select{width:40px}.compendium-browser .browser{display:grid;grid-template-columns:2em auto 2em;grid-template-areas:"L_SIDEBAR CONTENT R_SIDEBAR";height:100%;overflow:hidden!important}.compendium-browser .browser .window-content{overflow-y:hidden!important}.compendium-browser .browser ul .filter-tags{display:none}.compendium-browser .browser ul li span{white-space:nowrap;overflow:hidden}.compendium-browser .browser .spacer{display:inline-block;min-width:5px}.compendium-browser .browser .spacer-large{display:inline-block;min-width:15px}.compendium-browser.entity-browser>li{cursor:default}.compendium-browser.entity-browser li .feat-tags{text-align:justify;margin-right:3px;margin-left:3px;text-transform:capitalize}.compendium-browser.entity-browser li .item-tags{text-align:justify;margin-right:3px;margin-left:3px;text-transform:capitalize}.compendium-browser .list-area{grid-template-rows:auto;grid-template-columns:auto;grid-area:CONTENT}.compendium-browser .settings .settings-group{border:1px solid #bbb;border-radius:5px;margin-top:5px;padding:2px;font-size:.9em;max-width:fit-content;float:left}.compendium-browser .settings .settings-group h3{margin:0;cursor:pointer}.compendium-browser .settings .settings-group label{display:block}.compendium-browser .settings .settings-group h4{display:inline-block;vertical-align:middle;height:100%}.compendium-browser .actions{box-sizing:content-box;flex:0 0 32px;background:rgba(86,85,78,.18);border-radius:0 0 .5em .5em;padding:0 .7em}.compendium-browser .actions ul{list-style:none;display:flex;flex-wrap:nowrap;flex-direction:row-reverse}.compendium-browser .actions li{box-sizing:content-box}.compendium-browser .actions li button{cursor:pointer;color:#ededed;background-color:#444;border-left:1px dotted rgba(0,0,0,.05);border-radius:0}.compendium-browser .cb_entities{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(15vw,100%),1fr));align-items:center}.compendium-browser .cb_entities .entity-header{display:grid;grid-template-columns:0.55fr 0.1fr 1fr;gap:0 2.5em;text-align:center;border:1px dotted rgba(0,0,0,.05);border-radius:3px}.compendium-browser .cb_entities .entity{background:var(--background);color:#a9a9a9;border-radius:5px;margin:.2em;border:1px solid rgba(0,0,0,.2);padding:.1em;height:clamp(5vh,6vh,7vh);box-shadow:0 0 3px inset var(--light-color);display:grid;grid-template-columns:3em 1fr;grid-template-areas:"Image Name" "Image Tags"}.compendium-browser .cb_entities .entity:hover{box-shadow:0 0 3px inset var(--icon-hover)}.compendium-browser .cb_entities .entity .entity-image{cursor:pointer;background-repeat:no-repeat;border:1px solid rgba(83,75,75,.3);padding:.1em;border-radius:.5em;justify-content:center;align-items:center;background-clip:border-box;background-image:url('/icons/svg/daze.svg');background-size:cover;width:3.2em;grid-area:Image;box-shadow:0 0 .2em .1em inset rgba(43,45,46,.863)}.compendium-browser .cb_entities .entity .entity-name{grid-area:Name;color:rgba(236,229,229,.932);font-size:12px;font-family:"Modesto Condensed","Palatino Linotype",serif;font-size:.9vw;font-weight:400;overflow:hidden;height:100%}.compendium-browser .cb_entities .entity ul.tags{list-style:none;min-width:initial;box-sizing:content-box;grid-area:Tags;display:grid;grid-template-columns:repeat(auto-fit,16px);margin:0}.compendium-browser .cb_entities .entity .tags .spell-tag{color:var(--background)}.compendium-browser .cb_entities .entity .tags .spell-tag.active{color:var(--primary-font)}.compendium-browser .cb_entities .entity .tags li{cursor:help;color:#a9a9a9;justify-content:center;align-items:center;height:1.6em;width:1.6em;font-size:.8em;line-height:1.6em;display:inline-block;padding:.2em;border-radius:.1em}.compendium-browser .cb_entities .entity.r_common{border-left:.3em solid fff}.compendium-browser .cb_entities .entity.r_common .entity-image{box-shadow:0 0 .2em .1em inset rgba(fff,.75)}.compendium-browser .cb_entities .entity.r_uncommon{border-color:#3dbb2d;background:linear-gradient(90deg,rgba(rgba(30,255,0),1) 0,rgba(163,53,238,0) 35%)}.compendium-browser .cb_entities .entity.r_uncommon .entity-image{box-shadow:0 0 .2em .1em inset rgba(rgba(30,255,0),.75)}.compendium-browser .cb_entities .entity.r_rare{border-color:#00008b;background:linear-gradient(90deg,#0070dd 0,rgba(0,0,0,.2) 35%)}.compendium-browser .cb_entities .entity.r_rare .entity-image{box-shadow:0 0 .2em .1em inset rgba(0,112,221,.75)}.compendium-browser .cb_entities .entity.r_veryrare{border-color:#a335ee;background:linear-gradient(90deg,#a335ee 0,rgba(163,53,238,0) 35%)}.compendium-browser .cb_entities .entity.r_veryrare .entity-image{box-shadow:0 0 .2em .1em inset rgba(163,53,238,.75)}.compendium-browser .cb_entities .entity.r_legendary{border-color:#ff8000;background:linear-gradient(90deg,#ff8000 0,rgba(255,128,0,0) 35%)}.compendium-browser .cb_entities .entity.r_legendary .entity-image{box-shadow:0 0 .2em .1em inset rgba(255,128,0,.75)} \ No newline at end of file diff --git a/dist/template/entity-browser.hbs b/dist/template/entity-browser.hbs new file mode 100644 index 0000000..ceb6501 --- /dev/null +++ b/dist/template/entity-browser.hbs @@ -0,0 +1,9 @@ +
      + +
        + +
        \ No newline at end of file diff --git a/dist/template/entity-list.hbs b/dist/template/entity-list.hbs new file mode 100644 index 0000000..a3dd858 --- /dev/null +++ b/dist/template/entity-list.hbs @@ -0,0 +1,60 @@ +{{#each entityItems as |entity|}} +
      • + + + + + +
          +
        • + {{#if entity.type}} +
        • + {{#switch entity.type}} + {{#case 'backpack'}}{{/case}} + {{#case 'consumable'}}{{/case}} + {{#case 'equipment'}}{{/case}} + {{#case 'tool'}}{{/case}} + {{#case 'loot'}}{{/case}} + {{#case 'weapon'}}{{/case}} + {{#case 'RollTable'}}{{/case}} + {{#default ''}}{{entity.type}}{{/default}} + {{/switch}} +
        • + {{/if}} + {{#if entity.dae}}
        • {{/if}} + {{#if entity.ac}}
        • {{entity.ac}}
        • {{/if}} + + {{#each entity.tags as |value name|}} +
        • + {{#if (eq name "range")}}{{value}}{{/if}} +
        • + {{/each}} +
        + {{#if entity.classRequirement}} +
          + {{#each entity.classRequirement as | class |}} +
        • + {{/each}} +
        + {{/if}} + {{#if (eq entity.type 'spell')}} +
          +
        • {{#if entity.data.level}}{{entity.data.level}}{{else}}C{{/if}}
        • +
        • R
        • +
        • C
        • +
        • V
        • +
        • S
        • +
        • M
        • +
        + {{/if}} + +
      • +{{/each}} \ No newline at end of file diff --git a/dist/template/filter-container.hbs b/dist/template/filter-container.hbs new file mode 100644 index 0000000..34fb781 --- /dev/null +++ b/dist/template/filter-container.hbs @@ -0,0 +1,111 @@ + +
        +
        +
        + +
        +
        +
        {{localize "CMPBrowser.sortBy"}}:
        +
        + +
        +
        + +
        + + {{#each filters.registeredFilterCategorys as |cat key|}} +
        +

        {{cat.label}}

        +
        + {{#each cat.filters as |filter key|}} +
        + {{#if filter.is_text}} +
        {{filter.label}}
        +
        + {{#if filter.possibleValues}} + + {{else}} + + {{/if}} +
        + {{/if}} + {{#if filter.is_bool}} +
        +
        {{filter.label}}
        +
        + +
        +
        + {{/if}} + {{#if filter.is_select}} +
        +
        {{filter.label}}
        +
        + +
        +
        + {{/if}} + {{#if filter.is_multiSelect}} +
        + +
        + {{#each filter.possibleValues as |label val|}} + +
        +
        + {{/each}} +
        +
        + {{/if}} + {{#if filter.is_numberCompare}} +
        +
        {{filter.label}}
        +
        + + +
        +
        + {{/if}} +
        + {{/each}} +
        +
        + {{/each}} +
        \ No newline at end of file diff --git a/template/loading.html b/dist/template/loading.hbs similarity index 80% rename from template/loading.html rename to dist/template/loading.hbs index f52f126..a7bed74 100644 --- a/template/loading.html +++ b/dist/template/loading.hbs @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/dist/template/settings.hbs b/dist/template/settings.hbs new file mode 100644 index 0000000..4678139 --- /dev/null +++ b/dist/template/settings.hbs @@ -0,0 +1,66 @@ +
        +
        +

        {{localize "CMPBrowser.generalSettings"}}

        + + + + + + +
        + +
        +

        {{localize "CMPBrowser.compSettingsSpell"}}

        + {{#each settings.loadedCompendium.Item as |Comp key|}} + + {{/each}} +
        +
        +

        {{localize "CMPBrowser.compSettingsActor"}}

        + {{#each settings.loadedCompendium.Actor as |Comp key|}} + + {{/each}} +
        +
        +

        {{localize "CMPBrowser.compSettingsJournalEntry"}}

        + {{#each settings.loadedCompendium.JournalEntry as |Comp key|}} + + {{/each}} +
        +
        +

        {{localize "CMPBrowser.compSettingsRolltable"}}

        + {{#each settings.loadedCompendium.Rolltable as |Comp key|}} + + {{/each}} +
        +
        \ No newline at end of file diff --git a/dist/template/template.hbs b/dist/template/template.hbs new file mode 100644 index 0000000..e4e1c61 --- /dev/null +++ b/dist/template/template.hbs @@ -0,0 +1,28 @@ +
        +
        + {{#if showItemBrowser}}{{/if}} + {{#if showSpellBrowser}}{{/if}} + {{#if showFeatBrowser}}{{/if}} + {{#if showActorBrowser}}{{/if}} + {{#if showJournalEntryBrowser}}{{/if}} + {{#if showRollTableBrowser}}{{/if}} + {{#if isGM}}{{/if}} +
        + +
        + {{#if showSpellBrowser}}
        {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Spell}}
        {{/if}} + {{#if showFeatBrowser}}
        {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Feat}}
        {{/if}} + {{#if showItemBrowser}}
        {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Item}}
        {{/if}} + {{#if showActorBrowser}}
        {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Actor}}
        {{/if}} + {{#if showJournalEntryBrowser}}
        {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.JournalEntry}}
        {{/if}} + {{#if showRollTableBrowser}}
        {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.RollTable}}
        {{/if}} + {{#if isGM}}
        {{> "modules/compendium-browser/template/settings.hbs"}}
        {{/if}} +
        +
        \ No newline at end of file diff --git a/docs/Filter.html b/docs/Filter.html new file mode 100644 index 0000000..048a869 --- /dev/null +++ b/docs/Filter.html @@ -0,0 +1,1388 @@ + + + + + JSDoc: Class: Filter + + + + + + + + + + +
        + +

        Class: Filter

        + + + + + + +
        + +
        + +

        Filter(path, label, type, possibleValues, valIsArray)

        + +
        Used to add custom filters to the Spell-Browser
        + + +
        + +
        +
        + + + + +

        Constructor

        + + + +

        new Filter(path, label, type, possibleValues, valIsArray)

        + + + + + + + + + + + + + + +
        Parameters:
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        NameTypeDescription
        path + + +string + + + + path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value
        label + + +string + + + + Title of the filter
        type + + +string + + + + type of filter possible types: text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching bool: will see if the data at the path exists and not false. select: exactly matches the data with the chosen selector from possibleValues multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data numberCompare: gives the option to compare numerical values, either with =, < or the > operator
        possibleValues + + +null +| + +boolean + + + + predetermined values to choose from. needed for select and multiSelect, can be used in text filters
        valIsArray + + +boolean + + + + if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden
        + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + +
        + + + + + + +

        Classes

        + +
        +
        Filter
        +
        +
        + + + + + + + + + + + +

        Methods

        + + + + + + + +

        (static) passesFilter(subject, filters) → {boolean}

        + + + + + + + + + + + + + + +
        Parameters:
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        NameTypeDescription
        subject + + +any + + + +
        filters + + +any + + + +
        + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + +
        Returns:
        + + + + +
        +
        + Type +
        +
        + +boolean + + +
        +
        + + + + + + + + + + + + + +

        _getInitialFilters() → {Object}

        + + + + + + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + +
        Returns:
        + + + + +
        +
        + Type +
        +
        + +Object + + +
        +
        + + + + + + + + + + + + + +

        (async) addEntityFilters()

        + + + + + + +
        + add entityfilters +
        + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +

        (async) addFilter(entityType, category, label, path, type, possibleValues, valIsArray)

        + + + + + + +
        + Used to add custom filters to the Spell-Browser +
        + + + + + + + + + +
        Parameters:
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        NameTypeDefaultDescription
        entityType + + +string + + + + + + type of entity for the filter
        category + + +string + + + + + + Title of the category
        label + + +string + + + + + + Title of the filter
        path + + +string + + + + + + path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value
        type + + +string + + + + + + type of filter possible filter: text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching bool: will see if the data at the path exists and not false. select: exactly matches the data with the chosen selector from possibleValues multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data numberCompare: gives the option to compare numerical values, either with =, < or the > operator
        possibleValues + + +null +| + +boolean + + + + + + null + + predetermined values to choose from. needed for select and multiSelect, can be used in text filters
        valIsArray + + +boolean + + + + + + false + + if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden
        + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +

        (async) addSpellFilters()

        + + + + + + +
        + Add all spellfilters +
        + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        To Do:
        +
        +
          +
        • convert this to read and use an iteratable object that can be stored in an extra file
        • +
        +
        + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +

        getByEntityType(entityType) → {Iterable|undefined}

        + + + + + + + + + + + + + + +
        Parameters:
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        NameTypeDescription
        entityType + + +string + + + +
        + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + +
        Returns:
        + + + + +
        +
        + Type +
        +
        + +Iterable +| + +undefined + + +
        +
        + + + + + + + + + + + + + +

        getByName(name)

        + + + + + + + + + + + + + + +
        Parameters:
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        NameTypeDescription
        name + +
        + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + +
        Returns:
        + + +
        + any +
        + + + + + + + + + + + + + + + +
        + +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + \ No newline at end of file diff --git a/docs/VersionCheck.html b/docs/VersionCheck.html new file mode 100644 index 0000000..8c2b9c1 --- /dev/null +++ b/docs/VersionCheck.html @@ -0,0 +1,301 @@ + + + + + JSDoc: Class: VersionCheck + + + + + + + + + + +
        + +

        Class: VersionCheck

        + + + + + + +
        + +
        + +

        VersionCheck()

        + +
        Version check function from Forien Unedintified item module
        + + +
        + +
        +
        + + + + +

        Constructor

        + + + +

        new VersionCheck()

        + + + + + + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + + + + + +
        + +
        + +

        VersionCheck()

        + +
        Version check function from Forien Unedintified item module
        + + +
        + +
        +
        + + + + +

        Constructor

        + + + +

        new VersionCheck()

        + + + + + + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + \ No newline at end of file diff --git a/docs/assets/css/main.css b/docs/assets/css/main.css new file mode 100644 index 0000000..46571c2 --- /dev/null +++ b/docs/assets/css/main.css @@ -0,0 +1,2660 @@ +:root { + --color-background: #fdfdfd; + --color-text: #222; + --color-text-aside: #707070; + --color-link: #4da6ff; + --color-menu-divider: #eee; + --color-menu-divider-focus: #000; + --color-menu-label: #707070; + --color-panel: #fff; + --color-panel-divider: #eee; + --color-comment-tag: #707070; + --color-comment-tag-text: #fff; + --color-code-background: rgba(0, 0, 0, 0.04); + --color-ts: #9600ff; + --color-ts-interface: #647f1b; + --color-ts-enum: #937210; + --color-ts-class: #0672de; + --color-ts-private: #707070; + --color-toolbar: #fff; + --color-toolbar-text: #333; +} + +/*! normalize.css v1.1.3 | MIT License | git.io/normalize */ +/* ========================================================================== + * * HTML5 display definitions + * * ========================================================================== */ +/** + * * Correct `block` display not defined in IE 6/7/8/9 and Firefox 3. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { + display: block; +} + +/** + * * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. */ +audio, canvas, video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/** + * * Prevent modern browsers from displaying `audio` without controls. + * * Remove excess height in iOS 5 devices. */ +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4. + * * Known issue: no IE 6 support. */ +[hidden] { + display: none; +} + +/* ========================================================================== + * * Base + * * ========================================================================== */ +/** + * * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using + * * `em` units. + * * 2. Prevent iOS text size adjust after orientation change, without disabling + * * user zoom. */ +html { + font-size: 100%; + /* 1 */ + -ms-text-size-adjust: 100%; + /* 2 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + font-family: sans-serif; +} + +/** + * * Address `font-family` inconsistency between `textarea` and other form + * * elements. */ +button, input, select, textarea { + font-family: sans-serif; +} + +/** + * * Address margins handled incorrectly in IE 6/7. */ +body { + margin: 0; +} + +/* ========================================================================== + * * Links + * * ========================================================================== */ +/** + * * Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { + outline: thin dotted; +} +a:active, a:hover { + outline: 0; +} + +/** + * * Improve readability when focused and also mouse hovered in all browsers. */ +/* ========================================================================== + * * Typography + * * ========================================================================== */ +/** + * * Address font sizes and margins set differently in IE 6/7. + * * Address font sizes within `section` and `article` in Firefox 4+, Safari 5, + * * and Chrome. */ +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4, .tsd-index-panel h3 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.67em; + margin: 2.33em 0; +} + +/** + * * Address styling not present in IE 7/8/9, Safari 5, and Chrome. */ +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * * Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. */ +b, strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/** + * * Address styling not present in Safari 5 and Chrome. */ +dfn { + font-style: italic; +} + +/** + * * Address differences between Firefox and other browsers. + * * Known issue: no IE 6/7 normalization. */ +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * * Address styling not present in IE 6/7/8/9. */ +mark { + background: #ff0; + color: #000; +} + +/** + * * Address margins set differently in IE 6/7. */ +p, pre { + margin: 1em 0; +} + +/** + * * Correct font family set oddly in IE 6, Safari 4/5, and Chrome. */ +code, kbd, pre, samp { + font-family: monospace, serif; + _font-family: "courier new", monospace; + font-size: 1em; +} + +/** + * * Improve readability of pre-formatted text in all browsers. */ +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +/** + * * Address CSS quotes not supported in IE 6/7. */ +q { + quotes: none; +} +q:before, q:after { + content: ""; + content: none; +} + +/** + * * Address `quotes` property not supported in Safari 4. */ +/** + * * Address inconsistent and variable font size in all browsers. */ +small { + font-size: 80%; +} + +/** + * * Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ========================================================================== + * * Lists + * * ========================================================================== */ +/** + * * Address margins set differently in IE 6/7. */ +dl, menu, ol, ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/** + * * Address paddings set differently in IE 6/7. */ +menu, ol, ul { + padding: 0 0 0 40px; +} + +/** + * * Correct list images handled incorrectly in IE 7. */ +nav ul, nav ol { + list-style: none; + list-style-image: none; +} + +/* ========================================================================== + * * Embedded content + * * ========================================================================== */ +/** + * * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3. + * * 2. Improve image quality when scaled in IE 7. */ +img { + border: 0; + /* 1 */ + -ms-interpolation-mode: bicubic; +} + +/* 2 */ +/** + * * Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { + overflow: hidden; +} + +/* ========================================================================== + * * Figures + * * ========================================================================== */ +/** + * * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11. */ +figure, form { + margin: 0; +} + +/* ========================================================================== + * * Forms + * * ========================================================================== */ +/** + * * Correct margin displayed oddly in IE 6/7. */ +/** + * * Define consistent border, margin, and padding. */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * * 1. Correct color not being inherited in IE 6/7/8/9. + * * 2. Correct text not wrapping in Firefox 3. + * * 3. Correct alignment displayed oddly in IE 6/7. */ +legend { + border: 0; + /* 1 */ + padding: 0; + white-space: normal; + /* 2 */ + *margin-left: -7px; +} + +/* 3 */ +/** + * * 1. Correct font size not being inherited in all browsers. + * * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5, + * * and Chrome. + * * 3. Improve appearance and consistency in all browsers. */ +button, input, select, textarea { + font-size: 100%; + /* 1 */ + margin: 0; + /* 2 */ + vertical-align: baseline; + /* 3 */ + *vertical-align: middle; +} + +/* 3 */ +/** + * * Address Firefox 3+ setting `line-height` on `input` using `!important` in + * * the UA stylesheet. */ +button, input { + line-height: normal; +} + +/** + * * Address inconsistent `text-transform` inheritance for `button` and `select`. + * * All other form control elements do not inherit `text-transform` values. + * * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+. + * * Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { + text-transform: none; +} + +/** + * * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * * and `video` controls. + * * 2. Correct inability to style clickable `input` types in iOS. + * * 3. Improve usability and consistency of cursor style between image-type + * * `input` and others. + * * 4. Remove inner spacing in IE 7 without affecting normal text inputs. + * * Known issue: inner spacing remains in IE 6. */ +button, html input[type=button] { + -webkit-appearance: button; + /* 2 */ + cursor: pointer; + /* 3 */ + *overflow: visible; +} + +/* 4 */ +input[type=reset], input[type=submit] { + -webkit-appearance: button; + /* 2 */ + cursor: pointer; + /* 3 */ + *overflow: visible; +} + +/* 4 */ +/** + * * Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { + cursor: default; +} + +/** + * * 1. Address box sizing set to content-box in IE 8/9. + * * 2. Remove excess padding in IE 8/9. + * * 3. Remove excess padding in IE 7. + * * Known issue: excess padding remains in IE 6. */ +input { + /* 3 */ +} +input[type=checkbox], input[type=radio] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ + *height: 13px; + /* 3 */ + *width: 13px; +} +input[type=search] { + -webkit-appearance: textfield; + /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + /* 2 */ + box-sizing: content-box; +} +input[type=search]::-webkit-search-cancel-button, input[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. + * * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome + * * (include `-moz` to future-proof). */ +/** + * * Remove inner padding and search cancel button in Safari 5 and Chrome + * * on OS X. */ +/** + * * Remove inner padding and border in Firefox 3+. */ +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * * 1. Remove default vertical scrollbar in IE 6/7/8/9. + * * 2. Improve readability and alignment in all browsers. */ +textarea { + overflow: auto; + /* 1 */ + vertical-align: top; +} + +/* 2 */ +/* ========================================================================== + * * Tables + * * ========================================================================== */ +/** + * * Remove most spacing between table cells. */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +ul.tsd-descriptions > li > :first-child, .tsd-panel > :first-child, .col > :first-child, .col-11 > :first-child, .col-10 > :first-child, .col-9 > :first-child, .col-8 > :first-child, .col-7 > :first-child, .col-6 > :first-child, .col-5 > :first-child, .col-4 > :first-child, .col-3 > :first-child, .col-2 > :first-child, .col-1 > :first-child, +ul.tsd-descriptions > li > :first-child > :first-child, +.tsd-panel > :first-child > :first-child, +.col > :first-child > :first-child, +.col-11 > :first-child > :first-child, +.col-10 > :first-child > :first-child, +.col-9 > :first-child > :first-child, +.col-8 > :first-child > :first-child, +.col-7 > :first-child > :first-child, +.col-6 > :first-child > :first-child, +.col-5 > :first-child > :first-child, +.col-4 > :first-child > :first-child, +.col-3 > :first-child > :first-child, +.col-2 > :first-child > :first-child, +.col-1 > :first-child > :first-child, +ul.tsd-descriptions > li > :first-child > :first-child > :first-child, +.tsd-panel > :first-child > :first-child > :first-child, +.col > :first-child > :first-child > :first-child, +.col-11 > :first-child > :first-child > :first-child, +.col-10 > :first-child > :first-child > :first-child, +.col-9 > :first-child > :first-child > :first-child, +.col-8 > :first-child > :first-child > :first-child, +.col-7 > :first-child > :first-child > :first-child, +.col-6 > :first-child > :first-child > :first-child, +.col-5 > :first-child > :first-child > :first-child, +.col-4 > :first-child > :first-child > :first-child, +.col-3 > :first-child > :first-child > :first-child, +.col-2 > :first-child > :first-child > :first-child, +.col-1 > :first-child > :first-child > :first-child { + margin-top: 0; +} +ul.tsd-descriptions > li > :last-child, .tsd-panel > :last-child, .col > :last-child, .col-11 > :last-child, .col-10 > :last-child, .col-9 > :last-child, .col-8 > :last-child, .col-7 > :last-child, .col-6 > :last-child, .col-5 > :last-child, .col-4 > :last-child, .col-3 > :last-child, .col-2 > :last-child, .col-1 > :last-child, +ul.tsd-descriptions > li > :last-child > :last-child, +.tsd-panel > :last-child > :last-child, +.col > :last-child > :last-child, +.col-11 > :last-child > :last-child, +.col-10 > :last-child > :last-child, +.col-9 > :last-child > :last-child, +.col-8 > :last-child > :last-child, +.col-7 > :last-child > :last-child, +.col-6 > :last-child > :last-child, +.col-5 > :last-child > :last-child, +.col-4 > :last-child > :last-child, +.col-3 > :last-child > :last-child, +.col-2 > :last-child > :last-child, +.col-1 > :last-child > :last-child, +ul.tsd-descriptions > li > :last-child > :last-child > :last-child, +.tsd-panel > :last-child > :last-child > :last-child, +.col > :last-child > :last-child > :last-child, +.col-11 > :last-child > :last-child > :last-child, +.col-10 > :last-child > :last-child > :last-child, +.col-9 > :last-child > :last-child > :last-child, +.col-8 > :last-child > :last-child > :last-child, +.col-7 > :last-child > :last-child > :last-child, +.col-6 > :last-child > :last-child > :last-child, +.col-5 > :last-child > :last-child > :last-child, +.col-4 > :last-child > :last-child > :last-child, +.col-3 > :last-child > :last-child > :last-child, +.col-2 > :last-child > :last-child > :last-child, +.col-1 > :last-child > :last-child > :last-child { + margin-bottom: 0; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 40px; +} +@media (max-width: 640px) { + .container { + padding: 0 20px; + } +} + +.container-main { + padding-bottom: 200px; +} + +.row { + display: flex; + position: relative; + margin: 0 -10px; +} +.row:after { + visibility: hidden; + display: block; + content: ""; + clear: both; + height: 0; +} + +.col, .col-11, .col-10, .col-9, .col-8, .col-7, .col-6, .col-5, .col-4, .col-3, .col-2, .col-1 { + box-sizing: border-box; + float: left; + padding: 0 10px; +} + +.col-1 { + width: 8.3333333333%; +} + +.offset-1 { + margin-left: 8.3333333333%; +} + +.col-2 { + width: 16.6666666667%; +} + +.offset-2 { + margin-left: 16.6666666667%; +} + +.col-3 { + width: 25%; +} + +.offset-3 { + margin-left: 25%; +} + +.col-4 { + width: 33.3333333333%; +} + +.offset-4 { + margin-left: 33.3333333333%; +} + +.col-5 { + width: 41.6666666667%; +} + +.offset-5 { + margin-left: 41.6666666667%; +} + +.col-6 { + width: 50%; +} + +.offset-6 { + margin-left: 50%; +} + +.col-7 { + width: 58.3333333333%; +} + +.offset-7 { + margin-left: 58.3333333333%; +} + +.col-8 { + width: 66.6666666667%; +} + +.offset-8 { + margin-left: 66.6666666667%; +} + +.col-9 { + width: 75%; +} + +.offset-9 { + margin-left: 75%; +} + +.col-10 { + width: 83.3333333333%; +} + +.offset-10 { + margin-left: 83.3333333333%; +} + +.col-11 { + width: 91.6666666667%; +} + +.offset-11 { + margin-left: 91.6666666667%; +} + +.tsd-kind-icon { + display: block; + position: relative; + padding-left: 20px; + text-indent: -20px; +} +.tsd-kind-icon:before { + content: ""; + display: inline-block; + vertical-align: middle; + width: 17px; + height: 17px; + margin: 0 3px 2px 0; + background-image: url(../images/icons.png); +} +@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + .tsd-kind-icon:before { + background-image: url(../images/icons@2x.png); + background-size: 238px 204px; + } +} + +.tsd-signature.tsd-kind-icon:before { + background-position: 0 -153px; +} + +.tsd-kind-object-literal > .tsd-kind-icon:before { + background-position: 0px -17px; +} +.tsd-kind-object-literal.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -17px; +} +.tsd-kind-object-literal.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -17px; +} + +.tsd-kind-class > .tsd-kind-icon:before { + background-position: 0px -34px; +} +.tsd-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -34px; +} +.tsd-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -34px; +} + +.tsd-kind-class.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -51px; +} +.tsd-kind-class.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -51px; +} +.tsd-kind-class.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -51px; +} + +.tsd-kind-interface > .tsd-kind-icon:before { + background-position: 0px -68px; +} +.tsd-kind-interface.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -68px; +} +.tsd-kind-interface.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -68px; +} + +.tsd-kind-interface.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -85px; +} +.tsd-kind-interface.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -85px; +} +.tsd-kind-interface.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -85px; +} + +.tsd-kind-namespace > .tsd-kind-icon:before { + background-position: 0px -102px; +} +.tsd-kind-namespace.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -102px; +} +.tsd-kind-namespace.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -102px; +} + +.tsd-kind-module > .tsd-kind-icon:before { + background-position: 0px -102px; +} +.tsd-kind-module.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -102px; +} +.tsd-kind-module.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -102px; +} + +.tsd-kind-enum > .tsd-kind-icon:before { + background-position: 0px -119px; +} +.tsd-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -119px; +} +.tsd-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -119px; +} + +.tsd-kind-enum-member > .tsd-kind-icon:before { + background-position: 0px -136px; +} +.tsd-kind-enum-member.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -136px; +} +.tsd-kind-enum-member.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -136px; +} + +.tsd-kind-signature > .tsd-kind-icon:before { + background-position: 0px -153px; +} +.tsd-kind-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -153px; +} +.tsd-kind-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -153px; +} + +.tsd-kind-type-alias > .tsd-kind-icon:before { + background-position: 0px -170px; +} +.tsd-kind-type-alias.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -170px; +} +.tsd-kind-type-alias.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -170px; +} + +.tsd-kind-type-alias.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: 0px -187px; +} +.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -17px -187px; +} +.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -34px -187px; +} + +.tsd-kind-variable > .tsd-kind-icon:before { + background-position: -136px -0px; +} +.tsd-kind-variable.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -0px; +} +.tsd-kind-variable.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -0px; +} +.tsd-kind-variable.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -0px; +} +.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-variable.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -0px; +} +.tsd-kind-variable.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -0px; +} + +.tsd-kind-property > .tsd-kind-icon:before { + background-position: -136px -0px; +} +.tsd-kind-property.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -0px; +} +.tsd-kind-property.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -0px; +} +.tsd-kind-property.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -0px; +} +.tsd-kind-property.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -0px; +} +.tsd-kind-property.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -0px; +} +.tsd-kind-property.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -0px; +} + +.tsd-kind-get-signature > .tsd-kind-icon:before { + background-position: -136px -17px; +} +.tsd-kind-get-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -17px; +} +.tsd-kind-get-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -17px; +} +.tsd-kind-get-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -17px; +} + +.tsd-kind-set-signature > .tsd-kind-icon:before { + background-position: -136px -34px; +} +.tsd-kind-set-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -34px; +} +.tsd-kind-set-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -34px; +} +.tsd-kind-set-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -34px; +} + +.tsd-kind-accessor > .tsd-kind-icon:before { + background-position: -136px -51px; +} +.tsd-kind-accessor.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -51px; +} +.tsd-kind-accessor.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -51px; +} +.tsd-kind-accessor.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -51px; +} + +.tsd-kind-function > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-function.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-method > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-call-signature > .tsd-kind-icon:before { + background-position: -136px -68px; +} +.tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -68px; +} +.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -68px; +} +.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -68px; +} + +.tsd-kind-function.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: -136px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -85px; +} +.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -85px; +} + +.tsd-kind-method.tsd-has-type-parameter > .tsd-kind-icon:before { + background-position: -136px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -85px; +} +.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -85px; +} + +.tsd-kind-constructor > .tsd-kind-icon:before { + background-position: -136px -102px; +} +.tsd-kind-constructor.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -102px; +} +.tsd-kind-constructor.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -102px; +} +.tsd-kind-constructor.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -102px; +} + +.tsd-kind-constructor-signature > .tsd-kind-icon:before { + background-position: -136px -102px; +} +.tsd-kind-constructor-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -102px; +} +.tsd-kind-constructor-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -102px; +} +.tsd-kind-constructor-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -102px; +} + +.tsd-kind-index-signature > .tsd-kind-icon:before { + background-position: -136px -119px; +} +.tsd-kind-index-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -119px; +} +.tsd-kind-index-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -119px; +} +.tsd-kind-index-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -119px; +} + +.tsd-kind-event > .tsd-kind-icon:before { + background-position: -136px -136px; +} +.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -136px; +} +.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -136px; +} +.tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -136px; +} +.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -136px; +} +.tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -136px; +} +.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -136px; +} + +.tsd-is-static > .tsd-kind-icon:before { + background-position: -136px -153px; +} +.tsd-is-static.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -153px; +} +.tsd-is-static.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -153px; +} +.tsd-is-static.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -153px; +} +.tsd-is-static.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -153px; +} +.tsd-is-static.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -153px; +} +.tsd-is-static.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -153px; +} +.tsd-is-static.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -153px; +} + +.tsd-is-static.tsd-kind-function > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-method > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-call-signature > .tsd-kind-icon:before { + background-position: -136px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -170px; +} +.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -170px; +} + +.tsd-is-static.tsd-kind-event > .tsd-kind-icon:before { + background-position: -136px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { + background-position: -153px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { + background-position: -51px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -68px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { + background-position: -85px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -102px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { + background-position: -170px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { + background-position: -187px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { + background-position: -119px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { + background-position: -204px -187px; +} +.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited > .tsd-kind-icon:before { + background-position: -221px -187px; +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes fade-out { + from { + opacity: 1; + visibility: visible; + } + to { + opacity: 0; + } +} +@keyframes fade-in-delayed { + 0% { + opacity: 0; + } + 33% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes fade-out-delayed { + 0% { + opacity: 1; + visibility: visible; + } + 66% { + opacity: 0; + } + 100% { + opacity: 0; + } +} +@keyframes shift-to-left { + from { + transform: translate(0, 0); + } + to { + transform: translate(-25%, 0); + } +} +@keyframes unshift-to-left { + from { + transform: translate(-25%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-in-from-right { + from { + transform: translate(100%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-out-to-right { + from { + transform: translate(0, 0); + visibility: visible; + } + to { + transform: translate(100%, 0); + } +} +body { + background: var(--color-background); + font-family: "Segoe UI", sans-serif; + font-size: 16px; + color: var(--color-text); +} + +a { + color: var(--color-link); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +code, pre { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + padding: 0.2em; + margin: 0; + font-size: 14px; + background-color: var(--color-code-background); +} + +pre { + padding: 10px; +} +pre code { + padding: 0; + font-size: 100%; + background-color: transparent; +} + +blockquote { + margin: 1em 0; + padding-left: 1em; + border-left: 4px solid gray; +} + +.tsd-typography { + line-height: 1.333em; +} +.tsd-typography ul { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-typography h4, .tsd-typography .tsd-index-panel h3, .tsd-index-panel .tsd-typography h3, .tsd-typography h5, .tsd-typography h6 { + font-size: 1em; + margin: 0; +} +.tsd-typography h5, .tsd-typography h6 { + font-weight: normal; +} +.tsd-typography p, .tsd-typography ul, .tsd-typography ol { + margin: 1em 0; +} + +@media (min-width: 901px) and (max-width: 1024px) { + html.default .col-content { + width: 72%; + } + html.default .col-menu { + width: 28%; + } + html.default .tsd-navigation { + padding-left: 10px; + } +} +@media (max-width: 900px) { + html.default .col-content { + float: none; + width: 100%; + } + html.default .col-menu { + position: fixed !important; + overflow: auto; + -webkit-overflow-scrolling: touch; + z-index: 1024; + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + width: 100%; + padding: 20px 20px 0 0; + max-width: 450px; + visibility: hidden; + background-color: var(--color-panel); + transform: translate(100%, 0); + } + html.default .col-menu > *:last-child { + padding-bottom: 20px; + } + html.default .overlay { + content: ""; + display: block; + position: fixed; + z-index: 1023; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + visibility: hidden; + } + html.default.to-has-menu .overlay { + animation: fade-in 0.4s; + } + html.default.to-has-menu header, +html.default.to-has-menu footer, +html.default.to-has-menu .col-content { + animation: shift-to-left 0.4s; + } + html.default.to-has-menu .col-menu { + animation: pop-in-from-right 0.4s; + } + html.default.from-has-menu .overlay { + animation: fade-out 0.4s; + } + html.default.from-has-menu header, +html.default.from-has-menu footer, +html.default.from-has-menu .col-content { + animation: unshift-to-left 0.4s; + } + html.default.from-has-menu .col-menu { + animation: pop-out-to-right 0.4s; + } + html.default.has-menu body { + overflow: hidden; + } + html.default.has-menu .overlay { + visibility: visible; + } + html.default.has-menu header, +html.default.has-menu footer, +html.default.has-menu .col-content { + transform: translate(-25%, 0); + } + html.default.has-menu .col-menu { + visibility: visible; + transform: translate(0, 0); + } +} + +.tsd-page-title { + padding: 70px 0 20px 0; + margin: 0 0 40px 0; + background: var(--color-panel); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.35); +} +.tsd-page-title h1 { + margin: 0; +} + +.tsd-breadcrumb { + margin: 0; + padding: 0; + color: var(--color-text-aside); +} +.tsd-breadcrumb a { + color: var(--color-text-aside); + text-decoration: none; +} +.tsd-breadcrumb a:hover { + text-decoration: underline; +} +.tsd-breadcrumb li { + display: inline; +} +.tsd-breadcrumb li:after { + content: " / "; +} + +html.minimal .container { + margin: 0; +} +html.minimal .container-main { + padding-top: 50px; + padding-bottom: 0; +} +html.minimal .content-wrap { + padding-left: 300px; +} +html.minimal .tsd-navigation { + position: fixed !important; + overflow: auto; + -webkit-overflow-scrolling: touch; + box-sizing: border-box; + z-index: 1; + left: 0; + top: 40px; + bottom: 0; + width: 300px; + padding: 20px; + margin: 0; +} +html.minimal .tsd-member .tsd-member { + margin-left: 0; +} +html.minimal .tsd-page-toolbar { + position: fixed; + z-index: 2; +} +html.minimal #tsd-filter .tsd-filter-group { + right: 0; + transform: none; +} +html.minimal footer { + background-color: transparent; +} +html.minimal footer .container { + padding: 0; +} +html.minimal .tsd-generator { + padding: 0; +} +@media (max-width: 900px) { + html.minimal .tsd-navigation { + display: none; + } + html.minimal .content-wrap { + padding-left: 0; + } +} + +dl.tsd-comment-tags { + overflow: hidden; +} +dl.tsd-comment-tags dt { + float: left; + padding: 1px 5px; + margin: 0 10px 0 0; + border-radius: 4px; + border: 1px solid var(--color-comment-tag); + color: var(--color-comment-tag); + font-size: 0.8em; + font-weight: normal; +} +dl.tsd-comment-tags dd { + margin: 0 0 10px 0; +} +dl.tsd-comment-tags dd:before, dl.tsd-comment-tags dd:after { + display: table; + content: " "; +} +dl.tsd-comment-tags dd pre, dl.tsd-comment-tags dd:after { + clear: both; +} +dl.tsd-comment-tags p { + margin: 0; +} + +.tsd-panel.tsd-comment .lead { + font-size: 1.1em; + line-height: 1.333em; + margin-bottom: 2em; +} +.tsd-panel.tsd-comment .lead:last-child { + margin-bottom: 0; +} + +.toggle-protected .tsd-is-private { + display: none; +} + +.toggle-public .tsd-is-private, +.toggle-public .tsd-is-protected, +.toggle-public .tsd-is-private-protected { + display: none; +} + +.toggle-inherited .tsd-is-inherited { + display: none; +} + +.toggle-externals .tsd-is-external { + display: none; +} + +#tsd-filter { + position: relative; + display: inline-block; + height: 40px; + vertical-align: bottom; +} +.no-filter #tsd-filter { + display: none; +} +#tsd-filter .tsd-filter-group { + display: inline-block; + height: 40px; + vertical-align: bottom; + white-space: nowrap; +} +#tsd-filter input { + display: none; +} +@media (max-width: 900px) { + #tsd-filter .tsd-filter-group { + display: block; + position: absolute; + top: 40px; + right: 20px; + height: auto; + background-color: var(--color-panel); + visibility: hidden; + transform: translate(50%, 0); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); + } + .has-options #tsd-filter .tsd-filter-group { + visibility: visible; + } + .to-has-options #tsd-filter .tsd-filter-group { + animation: fade-in 0.2s; + } + .from-has-options #tsd-filter .tsd-filter-group { + animation: fade-out 0.2s; + } + #tsd-filter label, +#tsd-filter .tsd-select { + display: block; + padding-right: 20px; + } +} + +footer { + border-top: 1px solid var(--color-panel-divider); + background-color: var(--color-panel); +} +footer.with-border-bottom { + border-bottom: 1px solid var(--color-panel-divider); +} +footer .tsd-legend-group { + font-size: 0; +} +footer .tsd-legend { + display: inline-block; + width: 25%; + padding: 0; + font-size: 16px; + list-style: none; + line-height: 1.333em; + vertical-align: top; +} +@media (max-width: 900px) { + footer .tsd-legend { + width: 50%; + } +} + +.tsd-hierarchy { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-hierarchy .target { + font-weight: bold; +} + +.tsd-index-panel .tsd-index-content { + margin-bottom: -30px !important; +} +.tsd-index-panel .tsd-index-section { + margin-bottom: 30px !important; +} +.tsd-index-panel h3 { + margin: 0 -20px 10px -20px; + padding: 0 20px 10px 20px; + border-bottom: 1px solid var(--color-panel-divider); +} +.tsd-index-panel ul.tsd-index-list { + -webkit-column-count: 3; + -moz-column-count: 3; + -ms-column-count: 3; + -o-column-count: 3; + column-count: 3; + -webkit-column-gap: 20px; + -moz-column-gap: 20px; + -ms-column-gap: 20px; + -o-column-gap: 20px; + column-gap: 20px; + padding: 0; + list-style: none; + line-height: 1.333em; +} +@media (max-width: 900px) { + .tsd-index-panel ul.tsd-index-list { + -webkit-column-count: 1; + -moz-column-count: 1; + -ms-column-count: 1; + -o-column-count: 1; + column-count: 1; + } +} +@media (min-width: 901px) and (max-width: 1024px) { + .tsd-index-panel ul.tsd-index-list { + -webkit-column-count: 2; + -moz-column-count: 2; + -ms-column-count: 2; + -o-column-count: 2; + column-count: 2; + } +} +.tsd-index-panel ul.tsd-index-list li { + -webkit-page-break-inside: avoid; + -moz-page-break-inside: avoid; + -ms-page-break-inside: avoid; + -o-page-break-inside: avoid; + page-break-inside: avoid; +} +.tsd-index-panel a, +.tsd-index-panel .tsd-parent-kind-module a { + color: var(--color-ts); +} +.tsd-index-panel .tsd-parent-kind-interface a { + color: var(--color-ts-interface); +} +.tsd-index-panel .tsd-parent-kind-enum a { + color: var(--color-ts-enum); +} +.tsd-index-panel .tsd-parent-kind-class a { + color: var(--color-ts-class); +} +.tsd-index-panel .tsd-kind-module a { + color: var(--color-ts); +} +.tsd-index-panel .tsd-kind-interface a { + color: var(--color-ts-interface); +} +.tsd-index-panel .tsd-kind-enum a { + color: var(--color-ts-enum); +} +.tsd-index-panel .tsd-kind-class a { + color: var(--color-ts-class); +} +.tsd-index-panel .tsd-is-private a { + color: var(--color-ts-private); +} + +.tsd-flag { + display: inline-block; + padding: 1px 5px; + border-radius: 4px; + color: var(--color-comment-tag-text); + background-color: var(--color-comment-tag); + text-indent: 0; + font-size: 14px; + font-weight: normal; +} + +.tsd-anchor { + position: absolute; + top: -100px; +} + +.tsd-member { + position: relative; +} +.tsd-member .tsd-anchor + h3 { + margin-top: 0; + margin-bottom: 0; + border-bottom: none; +} +.tsd-member a[data-tsd-kind] { + color: var(--color-ts); +} +.tsd-member a[data-tsd-kind=Interface] { + color: var(--color-ts-interface); +} +.tsd-member a[data-tsd-kind=Enum] { + color: var(--color-ts-enum); +} +.tsd-member a[data-tsd-kind=Class] { + color: var(--color-ts-class); +} +.tsd-member a[data-tsd-kind=Private] { + color: var(--color-ts-private); +} + +.tsd-navigation { + margin: 0 0 0 40px; +} +.tsd-navigation a { + display: block; + padding-top: 2px; + padding-bottom: 2px; + border-left: 2px solid transparent; + color: var(--color-text); + text-decoration: none; + transition: border-left-color 0.1s; +} +.tsd-navigation a:hover { + text-decoration: underline; +} +.tsd-navigation ul { + margin: 0; + padding: 0; + list-style: none; +} +.tsd-navigation li { + padding: 0; +} + +.tsd-navigation.primary { + padding-bottom: 40px; +} +.tsd-navigation.primary a { + display: block; + padding-top: 6px; + padding-bottom: 6px; +} +.tsd-navigation.primary ul li a { + padding-left: 5px; +} +.tsd-navigation.primary ul li li a { + padding-left: 25px; +} +.tsd-navigation.primary ul li li li a { + padding-left: 45px; +} +.tsd-navigation.primary ul li li li li a { + padding-left: 65px; +} +.tsd-navigation.primary ul li li li li li a { + padding-left: 85px; +} +.tsd-navigation.primary ul li li li li li li a { + padding-left: 105px; +} +.tsd-navigation.primary > ul { + border-bottom: 1px solid var(--color-panel-divider); +} +.tsd-navigation.primary li { + border-top: 1px solid var(--color-panel-divider); +} +.tsd-navigation.primary li.current > a { + font-weight: bold; +} +.tsd-navigation.primary li.label span { + display: block; + padding: 20px 0 6px 5px; + color: var(--color-menu-label); +} +.tsd-navigation.primary li.globals + li > span, .tsd-navigation.primary li.globals + li > a { + padding-top: 20px; +} + +.tsd-navigation.secondary { + max-height: calc(100vh - 1rem - 40px); + overflow: auto; + position: -webkit-sticky; + position: sticky; + top: calc(.5rem + 40px); + transition: 0.3s; +} +.tsd-navigation.secondary.tsd-navigation--toolbar-hide { + max-height: calc(100vh - 1rem); + top: 0.5rem; +} +.tsd-navigation.secondary ul { + transition: opacity 0.2s; +} +.tsd-navigation.secondary ul li a { + padding-left: 25px; +} +.tsd-navigation.secondary ul li li a { + padding-left: 45px; +} +.tsd-navigation.secondary ul li li li a { + padding-left: 65px; +} +.tsd-navigation.secondary ul li li li li a { + padding-left: 85px; +} +.tsd-navigation.secondary ul li li li li li a { + padding-left: 105px; +} +.tsd-navigation.secondary ul li li li li li li a { + padding-left: 125px; +} +.tsd-navigation.secondary ul.current a { + border-left-color: var(--color-panel-divider); +} +.tsd-navigation.secondary li.focus > a, +.tsd-navigation.secondary ul.current li.focus > a { + border-left-color: var(--color-menu-divider-focus); +} +.tsd-navigation.secondary li.current { + margin-top: 20px; + margin-bottom: 20px; + border-left-color: var(--color-panel-divider); +} +.tsd-navigation.secondary li.current > a { + font-weight: bold; +} + +@media (min-width: 901px) { + .menu-sticky-wrap { + position: static; + } +} + +.tsd-panel { + margin: 20px 0; + padding: 20px; + background-color: var(--color-panel); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +.tsd-panel:empty { + display: none; +} +.tsd-panel > h1, .tsd-panel > h2, .tsd-panel > h3 { + margin: 1.5em -20px 10px -20px; + padding: 0 20px 10px 20px; + border-bottom: 1px solid var(--color-panel-divider); +} +.tsd-panel > h1.tsd-before-signature, .tsd-panel > h2.tsd-before-signature, .tsd-panel > h3.tsd-before-signature { + margin-bottom: 0; + border-bottom: 0; +} +.tsd-panel table { + display: block; + width: 100%; + overflow: auto; + margin-top: 10px; + word-break: normal; + word-break: keep-all; +} +.tsd-panel table th { + font-weight: bold; +} +.tsd-panel table th, .tsd-panel table td { + padding: 6px 13px; + border: 1px solid #ddd; +} +.tsd-panel table tr { + background-color: #fff; + border-top: 1px solid #ccc; +} +.tsd-panel table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +.tsd-panel-group { + margin: 60px 0; +} +.tsd-panel-group > h1, .tsd-panel-group > h2, .tsd-panel-group > h3 { + padding-left: 20px; + padding-right: 20px; +} + +#tsd-search { + transition: background-color 0.2s; +} +#tsd-search .title { + position: relative; + z-index: 2; +} +#tsd-search .field { + position: absolute; + left: 0; + top: 0; + right: 40px; + height: 40px; +} +#tsd-search .field input { + box-sizing: border-box; + position: relative; + top: -50px; + z-index: 1; + width: 100%; + padding: 0 10px; + opacity: 0; + outline: 0; + border: 0; + background: transparent; + color: var(--color-text); +} +#tsd-search .field label { + position: absolute; + overflow: hidden; + right: -40px; +} +#tsd-search .field input, +#tsd-search .title { + transition: opacity 0.2s; +} +#tsd-search .results { + position: absolute; + visibility: hidden; + top: 40px; + width: 100%; + margin: 0; + padding: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +#tsd-search .results li { + padding: 0 10px; + background-color: var(--color-background); +} +#tsd-search .results li:nth-child(even) { + background-color: var(--color-panel); +} +#tsd-search .results li.state { + display: none; +} +#tsd-search .results li.current, +#tsd-search .results li:hover { + background-color: var(--color-panel-divider); +} +#tsd-search .results a { + display: block; +} +#tsd-search .results a:before { + top: 10px; +} +#tsd-search .results span.parent { + color: var(--color-text-aside); + font-weight: normal; +} +#tsd-search.has-focus { + background-color: var(--color-panel-divider); +} +#tsd-search.has-focus .field input { + top: 0; + opacity: 1; +} +#tsd-search.has-focus .title { + z-index: 0; + opacity: 0; +} +#tsd-search.has-focus .results { + visibility: visible; +} +#tsd-search.loading .results li.state.loading { + display: block; +} +#tsd-search.failure .results li.state.failure { + display: block; +} + +.tsd-signature { + margin: 0 0 1em 0; + padding: 10px; + border: 1px solid var(--color-panel-divider); + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 14px; + overflow-x: auto; +} +.tsd-signature.tsd-kind-icon { + padding-left: 30px; +} +.tsd-signature.tsd-kind-icon:before { + top: 10px; + left: 10px; +} +.tsd-panel > .tsd-signature { + margin-left: -20px; + margin-right: -20px; + border-width: 1px 0; +} +.tsd-panel > .tsd-signature.tsd-kind-icon { + padding-left: 40px; +} +.tsd-panel > .tsd-signature.tsd-kind-icon:before { + left: 20px; +} + +.tsd-signature-symbol { + color: var(--color-text-aside); + font-weight: normal; +} + +.tsd-signature-type { + font-style: italic; + font-weight: normal; +} + +.tsd-signatures { + padding: 0; + margin: 0 0 1em 0; + border: 1px solid var(--color-panel-divider); +} +.tsd-signatures .tsd-signature { + margin: 0; + border-width: 1px 0 0 0; + transition: background-color 0.1s; +} +.tsd-signatures .tsd-signature:first-child { + border-top-width: 0; +} +.tsd-signatures .tsd-signature.current { + background-color: var(--color-panel-divider); +} +.tsd-signatures.active > .tsd-signature { + cursor: pointer; +} +.tsd-panel > .tsd-signatures { + margin-left: -20px; + margin-right: -20px; + border-width: 1px 0; +} +.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon { + padding-left: 40px; +} +.tsd-panel > .tsd-signatures .tsd-signature.tsd-kind-icon:before { + left: 20px; +} +.tsd-panel > a.anchor + .tsd-signatures { + border-top-width: 0; + margin-top: -20px; +} + +ul.tsd-descriptions { + position: relative; + overflow: hidden; + padding: 0; + list-style: none; +} +ul.tsd-descriptions.active > .tsd-description { + display: none; +} +ul.tsd-descriptions.active > .tsd-description.current { + display: block; +} +ul.tsd-descriptions.active > .tsd-description.fade-in { + animation: fade-in-delayed 0.3s; +} +ul.tsd-descriptions.active > .tsd-description.fade-out { + animation: fade-out-delayed 0.3s; + position: absolute; + display: block; + top: 0; + left: 0; + right: 0; + opacity: 0; + visibility: hidden; +} +ul.tsd-descriptions h4, ul.tsd-descriptions .tsd-index-panel h3, .tsd-index-panel ul.tsd-descriptions h3 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} + +ul.tsd-parameters, +ul.tsd-type-parameters { + list-style: square; + margin: 0; + padding-left: 20px; +} +ul.tsd-parameters > li.tsd-parameter-signature, +ul.tsd-type-parameters > li.tsd-parameter-signature { + list-style: none; + margin-left: -20px; +} +ul.tsd-parameters h5, +ul.tsd-type-parameters h5 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} +ul.tsd-parameters .tsd-comment, +ul.tsd-type-parameters .tsd-comment { + margin-top: -0.5em; +} + +.tsd-sources { + font-size: 14px; + color: var(--color-text-aside); + margin: 0 0 1em 0; +} +.tsd-sources a { + color: var(--color-text-aside); + text-decoration: underline; +} +.tsd-sources ul, .tsd-sources p { + margin: 0 !important; +} +.tsd-sources ul { + list-style: none; + padding: 0; +} + +.tsd-page-toolbar { + position: fixed; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 40px; + color: var(--color-toolbar-text); + background: var(--color-toolbar); + border-bottom: 1px solid var(--color-panel-divider); + transition: transform 0.3s linear; +} +.tsd-page-toolbar a { + color: var(--color-toolbar-text); + text-decoration: none; +} +.tsd-page-toolbar a.title { + font-weight: bold; +} +.tsd-page-toolbar a.title:hover { + text-decoration: underline; +} +.tsd-page-toolbar .table-wrap { + display: table; + width: 100%; + height: 40px; +} +.tsd-page-toolbar .table-cell { + display: table-cell; + position: relative; + white-space: nowrap; + line-height: 40px; +} +.tsd-page-toolbar .table-cell:first-child { + width: 100%; +} + +.tsd-page-toolbar--hide { + transform: translateY(-100%); +} + +.tsd-select .tsd-select-list li:before, .tsd-select .tsd-select-label:before, .tsd-widget:before { + content: ""; + display: inline-block; + width: 40px; + height: 40px; + margin: 0 -8px 0 0; + background-image: url(../images/widgets.png); + background-repeat: no-repeat; + text-indent: -1024px; + vertical-align: bottom; +} +@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + .tsd-select .tsd-select-list li:before, .tsd-select .tsd-select-label:before, .tsd-widget:before { + background-image: url(../images/widgets@2x.png); + background-size: 320px 40px; + } +} + +.tsd-widget { + display: inline-block; + overflow: hidden; + opacity: 0.6; + height: 40px; + transition: opacity 0.1s, background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-widget:hover { + opacity: 0.8; +} +.tsd-widget.active { + opacity: 1; + background-color: var(--color-panel-divider); +} +.tsd-widget.no-caption { + width: 40px; +} +.tsd-widget.no-caption:before { + margin: 0; +} +.tsd-widget.search:before { + background-position: 0 0; +} +.tsd-widget.menu:before { + background-position: -40px 0; +} +.tsd-widget.options:before { + background-position: -80px 0; +} +.tsd-widget.options, .tsd-widget.menu { + display: none; +} +@media (max-width: 900px) { + .tsd-widget.options, .tsd-widget.menu { + display: inline-block; + } +} +input[type=checkbox] + .tsd-widget:before { + background-position: -120px 0; +} +input[type=checkbox]:checked + .tsd-widget:before { + background-position: -160px 0; +} + +.tsd-select { + position: relative; + display: inline-block; + height: 40px; + transition: opacity 0.1s, background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-select .tsd-select-label { + opacity: 0.6; + transition: opacity 0.2s; +} +.tsd-select .tsd-select-label:before { + background-position: -240px 0; +} +.tsd-select.active .tsd-select-label { + opacity: 0.8; +} +.tsd-select.active .tsd-select-list { + visibility: visible; + opacity: 1; + transition-delay: 0s; +} +.tsd-select .tsd-select-list { + position: absolute; + visibility: hidden; + top: 40px; + left: 0; + margin: 0; + padding: 0; + opacity: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); + transition: visibility 0s 0.2s, opacity 0.2s; +} +.tsd-select .tsd-select-list li { + padding: 0 20px 0 0; + background-color: var(--color-background); +} +.tsd-select .tsd-select-list li:before { + background-position: 40px 0; +} +.tsd-select .tsd-select-list li:nth-child(even) { + background-color: var(--color-panel); +} +.tsd-select .tsd-select-list li:hover { + background-color: var(--color-panel-divider); +} +.tsd-select .tsd-select-list li.selected:before { + background-position: -200px 0; +} +@media (max-width: 900px) { + .tsd-select .tsd-select-list { + top: 0; + left: auto; + right: 100%; + margin-right: -5px; + } + .tsd-select .tsd-select-label:before { + background-position: -280px 0; + } +} + +img { + max-width: 100%; +} diff --git a/docs/assets/images/icons.png b/docs/assets/images/icons.png new file mode 100644 index 0000000000000000000000000000000000000000..3836d5fe46e48bbe186116855aae879c23935327 GIT binary patch literal 9615 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 NZmR35l_G4O{0}dcmS_L~ literal 0 HcmV?d00001 diff --git a/docs/assets/images/icons@2x.png b/docs/assets/images/icons@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5a209e2f6d7f915cc9cb6fe7a4264c8be4db87b0 GIT binary patch literal 28144 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 U&wLp6h6BX&boFyt=akR{0G%$)mH+?% literal 0 HcmV?d00001 diff --git a/docs/assets/images/widgets@2x.png b/docs/assets/images/widgets@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4bbbd57272f3b28f47527d4951ad10f950b8ad43 GIT binary patch literal 855 zcmeAS@N?(olHy`uVBq!ia0y~yU}^xe12~w0Jcmn z@(X6T|9^jgLcx21{)7exgY)a>N6m2F0<`Rqr;B4q1>>88jUdw-7W`c)zLE*mq8W2H z-<&Jl_Hco5BuC5n@AbF5GD82~-e8-v=#zCyUX0F-o}8pPfAv`!GN$ff+TL<~@kgt} z62eO?_|&+>xBmM$@p|z`tIKEdpPf8%qI>4r7@jn<=eta*{3~?g(zz{Ke9zc-G^gr? z-7foa?LcS!hmbwzru}ICvbWLlW8;+l-}!^=c32!^nV`+`C*;0-*Y%l94pC;Cb3GXz zzSf%a!{gVr{Y_lVuUj+a)*Ca+!-Hu%xmP&&X-2CuANY8^i{D7Kg6qzP zXz_ps9+lN8ESH{K4`yu&b~I>N9xGlE&;2u*b?+Go!AhN?m-bxlLvtC#MzDF2kFzfHJ1W7ybqdefSqVhbOykd*Yi%EDuhs z4wF{ft^bv2+DDnKb8gj1FuvcV`M}luS>lO<^)8x>y1#R;a=-ZKwWTQQb)ioBbi;zh zD!f5V)8581to1LL7c9!l^PSC$NBPYif!_vAZhmL4)v4U)4UsrLYiH_9rmQDd?)(e5 z^pcH>qvBg*i0dus2r*mp4;zKvu=P#s-ti;2obl`NjjwoYd>e(oo#j_uyRb<7Pv^If zzZ|mGHmV)8^tbO%^>eqMw(@7(&3g{jEp-Najo7V75xI_ZHK*FA`elF{r5}E*d7+j_R literal 0 HcmV?d00001 diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js new file mode 100644 index 0000000..dc257a8 --- /dev/null +++ b/docs/assets/js/main.js @@ -0,0 +1,248 @@ +/* + * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). + * This devtool is not neither made for production nor for readable output files. + * It uses "eval()" calls to create a separate source file in the browser devtools. + * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) + * or disable the default devtool with "devtool: false". + * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). + */ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ "../node_modules/lunr/lunr.js": +/*!************************************!*\ + !*** ../node_modules/lunr/lunr.js ***! + \************************************/ +/***/ ((module, exports, __webpack_require__) => { + +eval("var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;/**\n * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9\n * Copyright (C) 2020 Oliver Nightingale\n * @license MIT\n */\n\n;(function(){\n\n/**\n * A convenience function for configuring and constructing\n * a new lunr Index.\n *\n * A lunr.Builder instance is created and the pipeline setup\n * with a trimmer, stop word filter and stemmer.\n *\n * This builder object is yielded to the configuration function\n * that is passed as a parameter, allowing the list of fields\n * and other builder parameters to be customised.\n *\n * All documents _must_ be added within the passed config function.\n *\n * @example\n * var idx = lunr(function () {\n * this.field('title')\n * this.field('body')\n * this.ref('id')\n *\n * documents.forEach(function (doc) {\n * this.add(doc)\n * }, this)\n * })\n *\n * @see {@link lunr.Builder}\n * @see {@link lunr.Pipeline}\n * @see {@link lunr.trimmer}\n * @see {@link lunr.stopWordFilter}\n * @see {@link lunr.stemmer}\n * @namespace {function} lunr\n */\nvar lunr = function (config) {\n var builder = new lunr.Builder\n\n builder.pipeline.add(\n lunr.trimmer,\n lunr.stopWordFilter,\n lunr.stemmer\n )\n\n builder.searchPipeline.add(\n lunr.stemmer\n )\n\n config.call(builder, builder)\n return builder.build()\n}\n\nlunr.version = \"2.3.9\"\n/*!\n * lunr.utils\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A namespace containing utils for the rest of the lunr library\n * @namespace lunr.utils\n */\nlunr.utils = {}\n\n/**\n * Print a warning message to the console.\n *\n * @param {String} message The message to be printed.\n * @memberOf lunr.utils\n * @function\n */\nlunr.utils.warn = (function (global) {\n /* eslint-disable no-console */\n return function (message) {\n if (global.console && console.warn) {\n console.warn(message)\n }\n }\n /* eslint-enable no-console */\n})(this)\n\n/**\n * Convert an object to a string.\n *\n * In the case of `null` and `undefined` the function returns\n * the empty string, in all other cases the result of calling\n * `toString` on the passed object is returned.\n *\n * @param {Any} obj The object to convert to a string.\n * @return {String} string representation of the passed object.\n * @memberOf lunr.utils\n */\nlunr.utils.asString = function (obj) {\n if (obj === void 0 || obj === null) {\n return \"\"\n } else {\n return obj.toString()\n }\n}\n\n/**\n * Clones an object.\n *\n * Will create a copy of an existing object such that any mutations\n * on the copy cannot affect the original.\n *\n * Only shallow objects are supported, passing a nested object to this\n * function will cause a TypeError.\n *\n * Objects with primitives, and arrays of primitives are supported.\n *\n * @param {Object} obj The object to clone.\n * @return {Object} a clone of the passed object.\n * @throws {TypeError} when a nested object is passed.\n * @memberOf Utils\n */\nlunr.utils.clone = function (obj) {\n if (obj === null || obj === undefined) {\n return obj\n }\n\n var clone = Object.create(null),\n keys = Object.keys(obj)\n\n for (var i = 0; i < keys.length; i++) {\n var key = keys[i],\n val = obj[key]\n\n if (Array.isArray(val)) {\n clone[key] = val.slice()\n continue\n }\n\n if (typeof val === 'string' ||\n typeof val === 'number' ||\n typeof val === 'boolean') {\n clone[key] = val\n continue\n }\n\n throw new TypeError(\"clone is not deep and does not support nested objects\")\n }\n\n return clone\n}\nlunr.FieldRef = function (docRef, fieldName, stringValue) {\n this.docRef = docRef\n this.fieldName = fieldName\n this._stringValue = stringValue\n}\n\nlunr.FieldRef.joiner = \"/\"\n\nlunr.FieldRef.fromString = function (s) {\n var n = s.indexOf(lunr.FieldRef.joiner)\n\n if (n === -1) {\n throw \"malformed field ref string\"\n }\n\n var fieldRef = s.slice(0, n),\n docRef = s.slice(n + 1)\n\n return new lunr.FieldRef (docRef, fieldRef, s)\n}\n\nlunr.FieldRef.prototype.toString = function () {\n if (this._stringValue == undefined) {\n this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef\n }\n\n return this._stringValue\n}\n/*!\n * lunr.Set\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A lunr set.\n *\n * @constructor\n */\nlunr.Set = function (elements) {\n this.elements = Object.create(null)\n\n if (elements) {\n this.length = elements.length\n\n for (var i = 0; i < this.length; i++) {\n this.elements[elements[i]] = true\n }\n } else {\n this.length = 0\n }\n}\n\n/**\n * A complete set that contains all elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.complete = {\n intersect: function (other) {\n return other\n },\n\n union: function () {\n return this\n },\n\n contains: function () {\n return true\n }\n}\n\n/**\n * An empty set that contains no elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.empty = {\n intersect: function () {\n return this\n },\n\n union: function (other) {\n return other\n },\n\n contains: function () {\n return false\n }\n}\n\n/**\n * Returns true if this set contains the specified object.\n *\n * @param {object} object - Object whose presence in this set is to be tested.\n * @returns {boolean} - True if this set contains the specified object.\n */\nlunr.Set.prototype.contains = function (object) {\n return !!this.elements[object]\n}\n\n/**\n * Returns a new set containing only the elements that are present in both\n * this set and the specified set.\n *\n * @param {lunr.Set} other - set to intersect with this set.\n * @returns {lunr.Set} a new set that is the intersection of this and the specified set.\n */\n\nlunr.Set.prototype.intersect = function (other) {\n var a, b, elements, intersection = []\n\n if (other === lunr.Set.complete) {\n return this\n }\n\n if (other === lunr.Set.empty) {\n return other\n }\n\n if (this.length < other.length) {\n a = this\n b = other\n } else {\n a = other\n b = this\n }\n\n elements = Object.keys(a.elements)\n\n for (var i = 0; i < elements.length; i++) {\n var element = elements[i]\n if (element in b.elements) {\n intersection.push(element)\n }\n }\n\n return new lunr.Set (intersection)\n}\n\n/**\n * Returns a new set combining the elements of this and the specified set.\n *\n * @param {lunr.Set} other - set to union with this set.\n * @return {lunr.Set} a new set that is the union of this and the specified set.\n */\n\nlunr.Set.prototype.union = function (other) {\n if (other === lunr.Set.complete) {\n return lunr.Set.complete\n }\n\n if (other === lunr.Set.empty) {\n return this\n }\n\n return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements)))\n}\n/**\n * A function to calculate the inverse document frequency for\n * a posting. This is shared between the builder and the index\n *\n * @private\n * @param {object} posting - The posting for a given term\n * @param {number} documentCount - The total number of documents.\n */\nlunr.idf = function (posting, documentCount) {\n var documentsWithTerm = 0\n\n for (var fieldName in posting) {\n if (fieldName == '_index') continue // Ignore the term index, its not a field\n documentsWithTerm += Object.keys(posting[fieldName]).length\n }\n\n var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5)\n\n return Math.log(1 + Math.abs(x))\n}\n\n/**\n * A token wraps a string representation of a token\n * as it is passed through the text processing pipeline.\n *\n * @constructor\n * @param {string} [str=''] - The string token being wrapped.\n * @param {object} [metadata={}] - Metadata associated with this token.\n */\nlunr.Token = function (str, metadata) {\n this.str = str || \"\"\n this.metadata = metadata || {}\n}\n\n/**\n * Returns the token string that is being wrapped by this object.\n *\n * @returns {string}\n */\nlunr.Token.prototype.toString = function () {\n return this.str\n}\n\n/**\n * A token update function is used when updating or optionally\n * when cloning a token.\n *\n * @callback lunr.Token~updateFunction\n * @param {string} str - The string representation of the token.\n * @param {Object} metadata - All metadata associated with this token.\n */\n\n/**\n * Applies the given function to the wrapped string token.\n *\n * @example\n * token.update(function (str, metadata) {\n * return str.toUpperCase()\n * })\n *\n * @param {lunr.Token~updateFunction} fn - A function to apply to the token string.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.update = function (fn) {\n this.str = fn(this.str, this.metadata)\n return this\n}\n\n/**\n * Creates a clone of this token. Optionally a function can be\n * applied to the cloned token.\n *\n * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.clone = function (fn) {\n fn = fn || function (s) { return s }\n return new lunr.Token (fn(this.str, this.metadata), this.metadata)\n}\n/*!\n * lunr.tokenizer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A function for splitting a string into tokens ready to be inserted into\n * the search index. Uses `lunr.tokenizer.separator` to split strings, change\n * the value of this property to change how strings are split into tokens.\n *\n * This tokenizer will convert its parameter to a string by calling `toString` and\n * then will split this string on the character in `lunr.tokenizer.separator`.\n * Arrays will have their elements converted to strings and wrapped in a lunr.Token.\n *\n * Optional metadata can be passed to the tokenizer, this metadata will be cloned and\n * added as metadata to every token that is created from the object to be tokenized.\n *\n * @static\n * @param {?(string|object|object[])} obj - The object to convert into tokens\n * @param {?object} metadata - Optional metadata to associate with every token\n * @returns {lunr.Token[]}\n * @see {@link lunr.Pipeline}\n */\nlunr.tokenizer = function (obj, metadata) {\n if (obj == null || obj == undefined) {\n return []\n }\n\n if (Array.isArray(obj)) {\n return obj.map(function (t) {\n return new lunr.Token(\n lunr.utils.asString(t).toLowerCase(),\n lunr.utils.clone(metadata)\n )\n })\n }\n\n var str = obj.toString().toLowerCase(),\n len = str.length,\n tokens = []\n\n for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {\n var char = str.charAt(sliceEnd),\n sliceLength = sliceEnd - sliceStart\n\n if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) {\n\n if (sliceLength > 0) {\n var tokenMetadata = lunr.utils.clone(metadata) || {}\n tokenMetadata[\"position\"] = [sliceStart, sliceLength]\n tokenMetadata[\"index\"] = tokens.length\n\n tokens.push(\n new lunr.Token (\n str.slice(sliceStart, sliceEnd),\n tokenMetadata\n )\n )\n }\n\n sliceStart = sliceEnd + 1\n }\n\n }\n\n return tokens\n}\n\n/**\n * The separator used to split a string into tokens. Override this property to change the behaviour of\n * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens.\n *\n * @static\n * @see lunr.tokenizer\n */\nlunr.tokenizer.separator = /[\\s\\-]+/\n/*!\n * lunr.Pipeline\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Pipelines maintain an ordered list of functions to be applied to all\n * tokens in documents entering the search index and queries being ran against\n * the index.\n *\n * An instance of lunr.Index created with the lunr shortcut will contain a\n * pipeline with a stop word filter and an English language stemmer. Extra\n * functions can be added before or after either of these functions or these\n * default functions can be removed.\n *\n * When run the pipeline will call each function in turn, passing a token, the\n * index of that token in the original list of all tokens and finally a list of\n * all the original tokens.\n *\n * The output of functions in the pipeline will be passed to the next function\n * in the pipeline. To exclude a token from entering the index the function\n * should return undefined, the rest of the pipeline will not be called with\n * this token.\n *\n * For serialisation of pipelines to work, all functions used in an instance of\n * a pipeline should be registered with lunr.Pipeline. Registered functions can\n * then be loaded. If trying to load a serialised pipeline that uses functions\n * that are not registered an error will be thrown.\n *\n * If not planning on serialising the pipeline then registering pipeline functions\n * is not necessary.\n *\n * @constructor\n */\nlunr.Pipeline = function () {\n this._stack = []\n}\n\nlunr.Pipeline.registeredFunctions = Object.create(null)\n\n/**\n * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token\n * string as well as all known metadata. A pipeline function can mutate the token string\n * or mutate (or add) metadata for a given token.\n *\n * A pipeline function can indicate that the passed token should be discarded by returning\n * null, undefined or an empty string. This token will not be passed to any downstream pipeline\n * functions and will not be added to the index.\n *\n * Multiple tokens can be returned by returning an array of tokens. Each token will be passed\n * to any downstream pipeline functions and all will returned tokens will be added to the index.\n *\n * Any number of pipeline functions may be chained together using a lunr.Pipeline.\n *\n * @interface lunr.PipelineFunction\n * @param {lunr.Token} token - A token from the document being processed.\n * @param {number} i - The index of this token in the complete list of tokens for this document/field.\n * @param {lunr.Token[]} tokens - All tokens for this document/field.\n * @returns {(?lunr.Token|lunr.Token[])}\n */\n\n/**\n * Register a function with the pipeline.\n *\n * Functions that are used in the pipeline should be registered if the pipeline\n * needs to be serialised, or a serialised pipeline needs to be loaded.\n *\n * Registering a function does not add it to a pipeline, functions must still be\n * added to instances of the pipeline for them to be used when running a pipeline.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @param {String} label - The label to register this function with\n */\nlunr.Pipeline.registerFunction = function (fn, label) {\n if (label in this.registeredFunctions) {\n lunr.utils.warn('Overwriting existing registered function: ' + label)\n }\n\n fn.label = label\n lunr.Pipeline.registeredFunctions[fn.label] = fn\n}\n\n/**\n * Warns if the function is not registered as a Pipeline function.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @private\n */\nlunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {\n var isRegistered = fn.label && (fn.label in this.registeredFunctions)\n\n if (!isRegistered) {\n lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\\n', fn)\n }\n}\n\n/**\n * Loads a previously serialised pipeline.\n *\n * All functions to be loaded must already be registered with lunr.Pipeline.\n * If any function from the serialised data has not been registered then an\n * error will be thrown.\n *\n * @param {Object} serialised - The serialised pipeline to load.\n * @returns {lunr.Pipeline}\n */\nlunr.Pipeline.load = function (serialised) {\n var pipeline = new lunr.Pipeline\n\n serialised.forEach(function (fnName) {\n var fn = lunr.Pipeline.registeredFunctions[fnName]\n\n if (fn) {\n pipeline.add(fn)\n } else {\n throw new Error('Cannot load unregistered function: ' + fnName)\n }\n })\n\n return pipeline\n}\n\n/**\n * Adds new functions to the end of the pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline.\n */\nlunr.Pipeline.prototype.add = function () {\n var fns = Array.prototype.slice.call(arguments)\n\n fns.forEach(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n this._stack.push(fn)\n }, this)\n}\n\n/**\n * Adds a single function after a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.after = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n pos = pos + 1\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Adds a single function before a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.before = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Removes a function from the pipeline.\n *\n * @param {lunr.PipelineFunction} fn The function to remove from the pipeline.\n */\nlunr.Pipeline.prototype.remove = function (fn) {\n var pos = this._stack.indexOf(fn)\n if (pos == -1) {\n return\n }\n\n this._stack.splice(pos, 1)\n}\n\n/**\n * Runs the current list of functions that make up the pipeline against the\n * passed tokens.\n *\n * @param {Array} tokens The tokens to run through the pipeline.\n * @returns {Array}\n */\nlunr.Pipeline.prototype.run = function (tokens) {\n var stackLength = this._stack.length\n\n for (var i = 0; i < stackLength; i++) {\n var fn = this._stack[i]\n var memo = []\n\n for (var j = 0; j < tokens.length; j++) {\n var result = fn(tokens[j], j, tokens)\n\n if (result === null || result === void 0 || result === '') continue\n\n if (Array.isArray(result)) {\n for (var k = 0; k < result.length; k++) {\n memo.push(result[k])\n }\n } else {\n memo.push(result)\n }\n }\n\n tokens = memo\n }\n\n return tokens\n}\n\n/**\n * Convenience method for passing a string through a pipeline and getting\n * strings out. This method takes care of wrapping the passed string in a\n * token and mapping the resulting tokens back to strings.\n *\n * @param {string} str - The string to pass through the pipeline.\n * @param {?object} metadata - Optional metadata to associate with the token\n * passed to the pipeline.\n * @returns {string[]}\n */\nlunr.Pipeline.prototype.runString = function (str, metadata) {\n var token = new lunr.Token (str, metadata)\n\n return this.run([token]).map(function (t) {\n return t.toString()\n })\n}\n\n/**\n * Resets the pipeline by removing any existing processors.\n *\n */\nlunr.Pipeline.prototype.reset = function () {\n this._stack = []\n}\n\n/**\n * Returns a representation of the pipeline ready for serialisation.\n *\n * Logs a warning if the function has not been registered.\n *\n * @returns {Array}\n */\nlunr.Pipeline.prototype.toJSON = function () {\n return this._stack.map(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n\n return fn.label\n })\n}\n/*!\n * lunr.Vector\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A vector is used to construct the vector space of documents and queries. These\n * vectors support operations to determine the similarity between two documents or\n * a document and a query.\n *\n * Normally no parameters are required for initializing a vector, but in the case of\n * loading a previously dumped vector the raw elements can be provided to the constructor.\n *\n * For performance reasons vectors are implemented with a flat array, where an elements\n * index is immediately followed by its value. E.g. [index, value, index, value]. This\n * allows the underlying array to be as sparse as possible and still offer decent\n * performance when being used for vector calculations.\n *\n * @constructor\n * @param {Number[]} [elements] - The flat list of element index and element value pairs.\n */\nlunr.Vector = function (elements) {\n this._magnitude = 0\n this.elements = elements || []\n}\n\n\n/**\n * Calculates the position within the vector to insert a given index.\n *\n * This is used internally by insert and upsert. If there are duplicate indexes then\n * the position is returned as if the value for that index were to be updated, but it\n * is the callers responsibility to check whether there is a duplicate at that index\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @returns {Number}\n */\nlunr.Vector.prototype.positionForIndex = function (index) {\n // For an empty vector the tuple can be inserted at the beginning\n if (this.elements.length == 0) {\n return 0\n }\n\n var start = 0,\n end = this.elements.length / 2,\n sliceLength = end - start,\n pivotPoint = Math.floor(sliceLength / 2),\n pivotIndex = this.elements[pivotPoint * 2]\n\n while (sliceLength > 1) {\n if (pivotIndex < index) {\n start = pivotPoint\n }\n\n if (pivotIndex > index) {\n end = pivotPoint\n }\n\n if (pivotIndex == index) {\n break\n }\n\n sliceLength = end - start\n pivotPoint = start + Math.floor(sliceLength / 2)\n pivotIndex = this.elements[pivotPoint * 2]\n }\n\n if (pivotIndex == index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex > index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex < index) {\n return (pivotPoint + 1) * 2\n }\n}\n\n/**\n * Inserts an element at an index within the vector.\n *\n * Does not allow duplicates, will throw an error if there is already an entry\n * for this index.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n */\nlunr.Vector.prototype.insert = function (insertIdx, val) {\n this.upsert(insertIdx, val, function () {\n throw \"duplicate index\"\n })\n}\n\n/**\n * Inserts or updates an existing index within the vector.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n * @param {function} fn - A function that is called for updates, the existing value and the\n * requested value are passed as arguments\n */\nlunr.Vector.prototype.upsert = function (insertIdx, val, fn) {\n this._magnitude = 0\n var position = this.positionForIndex(insertIdx)\n\n if (this.elements[position] == insertIdx) {\n this.elements[position + 1] = fn(this.elements[position + 1], val)\n } else {\n this.elements.splice(position, 0, insertIdx, val)\n }\n}\n\n/**\n * Calculates the magnitude of this vector.\n *\n * @returns {Number}\n */\nlunr.Vector.prototype.magnitude = function () {\n if (this._magnitude) return this._magnitude\n\n var sumOfSquares = 0,\n elementsLength = this.elements.length\n\n for (var i = 1; i < elementsLength; i += 2) {\n var val = this.elements[i]\n sumOfSquares += val * val\n }\n\n return this._magnitude = Math.sqrt(sumOfSquares)\n}\n\n/**\n * Calculates the dot product of this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The vector to compute the dot product with.\n * @returns {Number}\n */\nlunr.Vector.prototype.dot = function (otherVector) {\n var dotProduct = 0,\n a = this.elements, b = otherVector.elements,\n aLen = a.length, bLen = b.length,\n aVal = 0, bVal = 0,\n i = 0, j = 0\n\n while (i < aLen && j < bLen) {\n aVal = a[i], bVal = b[j]\n if (aVal < bVal) {\n i += 2\n } else if (aVal > bVal) {\n j += 2\n } else if (aVal == bVal) {\n dotProduct += a[i + 1] * b[j + 1]\n i += 2\n j += 2\n }\n }\n\n return dotProduct\n}\n\n/**\n * Calculates the similarity between this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The other vector to calculate the\n * similarity with.\n * @returns {Number}\n */\nlunr.Vector.prototype.similarity = function (otherVector) {\n return this.dot(otherVector) / this.magnitude() || 0\n}\n\n/**\n * Converts the vector to an array of the elements within the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toArray = function () {\n var output = new Array (this.elements.length / 2)\n\n for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) {\n output[j] = this.elements[i]\n }\n\n return output\n}\n\n/**\n * A JSON serializable representation of the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toJSON = function () {\n return this.elements\n}\n/* eslint-disable */\n/*!\n * lunr.stemmer\n * Copyright (C) 2020 Oliver Nightingale\n * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt\n */\n\n/**\n * lunr.stemmer is an english language stemmer, this is a JavaScript\n * implementation of the PorterStemmer taken from http://tartarus.org/~martin\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token - The string to stem\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n * @function\n */\nlunr.stemmer = (function(){\n var step2list = {\n \"ational\" : \"ate\",\n \"tional\" : \"tion\",\n \"enci\" : \"ence\",\n \"anci\" : \"ance\",\n \"izer\" : \"ize\",\n \"bli\" : \"ble\",\n \"alli\" : \"al\",\n \"entli\" : \"ent\",\n \"eli\" : \"e\",\n \"ousli\" : \"ous\",\n \"ization\" : \"ize\",\n \"ation\" : \"ate\",\n \"ator\" : \"ate\",\n \"alism\" : \"al\",\n \"iveness\" : \"ive\",\n \"fulness\" : \"ful\",\n \"ousness\" : \"ous\",\n \"aliti\" : \"al\",\n \"iviti\" : \"ive\",\n \"biliti\" : \"ble\",\n \"logi\" : \"log\"\n },\n\n step3list = {\n \"icate\" : \"ic\",\n \"ative\" : \"\",\n \"alize\" : \"al\",\n \"iciti\" : \"ic\",\n \"ical\" : \"ic\",\n \"ful\" : \"\",\n \"ness\" : \"\"\n },\n\n c = \"[^aeiou]\", // consonant\n v = \"[aeiouy]\", // vowel\n C = c + \"[^aeiouy]*\", // consonant sequence\n V = v + \"[aeiou]*\", // vowel sequence\n\n mgr0 = \"^(\" + C + \")?\" + V + C, // [C]VC... is m>0\n meq1 = \"^(\" + C + \")?\" + V + C + \"(\" + V + \")?$\", // [C]VC[V] is m=1\n mgr1 = \"^(\" + C + \")?\" + V + C + V + C, // [C]VCVC... is m>1\n s_v = \"^(\" + C + \")?\" + v; // vowel in stem\n\n var re_mgr0 = new RegExp(mgr0);\n var re_mgr1 = new RegExp(mgr1);\n var re_meq1 = new RegExp(meq1);\n var re_s_v = new RegExp(s_v);\n\n var re_1a = /^(.+?)(ss|i)es$/;\n var re2_1a = /^(.+?)([^s])s$/;\n var re_1b = /^(.+?)eed$/;\n var re2_1b = /^(.+?)(ed|ing)$/;\n var re_1b_2 = /.$/;\n var re2_1b_2 = /(at|bl|iz)$/;\n var re3_1b_2 = new RegExp(\"([^aeiouylsz])\\\\1$\");\n var re4_1b_2 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var re_1c = /^(.+?[^aeiou])y$/;\n var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;\n\n var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;\n\n var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;\n var re2_4 = /^(.+?)(s|t)(ion)$/;\n\n var re_5 = /^(.+?)e$/;\n var re_5_1 = /ll$/;\n var re3_5 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var porterStemmer = function porterStemmer(w) {\n var stem,\n suffix,\n firstch,\n re,\n re2,\n re3,\n re4;\n\n if (w.length < 3) { return w; }\n\n firstch = w.substr(0,1);\n if (firstch == \"y\") {\n w = firstch.toUpperCase() + w.substr(1);\n }\n\n // Step 1a\n re = re_1a\n re2 = re2_1a;\n\n if (re.test(w)) { w = w.replace(re,\"$1$2\"); }\n else if (re2.test(w)) { w = w.replace(re2,\"$1$2\"); }\n\n // Step 1b\n re = re_1b;\n re2 = re2_1b;\n if (re.test(w)) {\n var fp = re.exec(w);\n re = re_mgr0;\n if (re.test(fp[1])) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1];\n re2 = re_s_v;\n if (re2.test(stem)) {\n w = stem;\n re2 = re2_1b_2;\n re3 = re3_1b_2;\n re4 = re4_1b_2;\n if (re2.test(w)) { w = w + \"e\"; }\n else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,\"\"); }\n else if (re4.test(w)) { w = w + \"e\"; }\n }\n }\n\n // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say)\n re = re_1c;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n w = stem + \"i\";\n }\n\n // Step 2\n re = re_2;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step2list[suffix];\n }\n }\n\n // Step 3\n re = re_3;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step3list[suffix];\n }\n }\n\n // Step 4\n re = re_4;\n re2 = re2_4;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n if (re.test(stem)) {\n w = stem;\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1] + fp[2];\n re2 = re_mgr1;\n if (re2.test(stem)) {\n w = stem;\n }\n }\n\n // Step 5\n re = re_5;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n re2 = re_meq1;\n re3 = re3_5;\n if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {\n w = stem;\n }\n }\n\n re = re_5_1;\n re2 = re_mgr1;\n if (re.test(w) && re2.test(w)) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n\n // and turn initial Y back to y\n\n if (firstch == \"y\") {\n w = firstch.toLowerCase() + w.substr(1);\n }\n\n return w;\n };\n\n return function (token) {\n return token.update(porterStemmer);\n }\n})();\n\nlunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')\n/*!\n * lunr.stopWordFilter\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.generateStopWordFilter builds a stopWordFilter function from the provided\n * list of stop words.\n *\n * The built in lunr.stopWordFilter is built using this generator and can be used\n * to generate custom stopWordFilters for applications or non English languages.\n *\n * @function\n * @param {Array} token The token to pass through the filter\n * @returns {lunr.PipelineFunction}\n * @see lunr.Pipeline\n * @see lunr.stopWordFilter\n */\nlunr.generateStopWordFilter = function (stopWords) {\n var words = stopWords.reduce(function (memo, stopWord) {\n memo[stopWord] = stopWord\n return memo\n }, {})\n\n return function (token) {\n if (token && words[token.toString()] !== token.toString()) return token\n }\n}\n\n/**\n * lunr.stopWordFilter is an English language stop word list filter, any words\n * contained in the list will not be passed through the filter.\n *\n * This is intended to be used in the Pipeline. If the token does not pass the\n * filter then undefined will be returned.\n *\n * @function\n * @implements {lunr.PipelineFunction}\n * @params {lunr.Token} token - A token to check for being a stop word.\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n */\nlunr.stopWordFilter = lunr.generateStopWordFilter([\n 'a',\n 'able',\n 'about',\n 'across',\n 'after',\n 'all',\n 'almost',\n 'also',\n 'am',\n 'among',\n 'an',\n 'and',\n 'any',\n 'are',\n 'as',\n 'at',\n 'be',\n 'because',\n 'been',\n 'but',\n 'by',\n 'can',\n 'cannot',\n 'could',\n 'dear',\n 'did',\n 'do',\n 'does',\n 'either',\n 'else',\n 'ever',\n 'every',\n 'for',\n 'from',\n 'get',\n 'got',\n 'had',\n 'has',\n 'have',\n 'he',\n 'her',\n 'hers',\n 'him',\n 'his',\n 'how',\n 'however',\n 'i',\n 'if',\n 'in',\n 'into',\n 'is',\n 'it',\n 'its',\n 'just',\n 'least',\n 'let',\n 'like',\n 'likely',\n 'may',\n 'me',\n 'might',\n 'most',\n 'must',\n 'my',\n 'neither',\n 'no',\n 'nor',\n 'not',\n 'of',\n 'off',\n 'often',\n 'on',\n 'only',\n 'or',\n 'other',\n 'our',\n 'own',\n 'rather',\n 'said',\n 'say',\n 'says',\n 'she',\n 'should',\n 'since',\n 'so',\n 'some',\n 'than',\n 'that',\n 'the',\n 'their',\n 'them',\n 'then',\n 'there',\n 'these',\n 'they',\n 'this',\n 'tis',\n 'to',\n 'too',\n 'twas',\n 'us',\n 'wants',\n 'was',\n 'we',\n 'were',\n 'what',\n 'when',\n 'where',\n 'which',\n 'while',\n 'who',\n 'whom',\n 'why',\n 'will',\n 'with',\n 'would',\n 'yet',\n 'you',\n 'your'\n])\n\nlunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')\n/*!\n * lunr.trimmer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.trimmer is a pipeline function for trimming non word\n * characters from the beginning and end of tokens before they\n * enter the index.\n *\n * This implementation may not work correctly for non latin\n * characters and should either be removed or adapted for use\n * with languages with non-latin characters.\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token The token to pass through the filter\n * @returns {lunr.Token}\n * @see lunr.Pipeline\n */\nlunr.trimmer = function (token) {\n return token.update(function (s) {\n return s.replace(/^\\W+/, '').replace(/\\W+$/, '')\n })\n}\n\nlunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer')\n/*!\n * lunr.TokenSet\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A token set is used to store the unique list of all tokens\n * within an index. Token sets are also used to represent an\n * incoming query to the index, this query token set and index\n * token set are then intersected to find which tokens to look\n * up in the inverted index.\n *\n * A token set can hold multiple tokens, as in the case of the\n * index token set, or it can hold a single token as in the\n * case of a simple query token set.\n *\n * Additionally token sets are used to perform wildcard matching.\n * Leading, contained and trailing wildcards are supported, and\n * from this edit distance matching can also be provided.\n *\n * Token sets are implemented as a minimal finite state automata,\n * where both common prefixes and suffixes are shared between tokens.\n * This helps to reduce the space used for storing the token set.\n *\n * @constructor\n */\nlunr.TokenSet = function () {\n this.final = false\n this.edges = {}\n this.id = lunr.TokenSet._nextId\n lunr.TokenSet._nextId += 1\n}\n\n/**\n * Keeps track of the next, auto increment, identifier to assign\n * to a new tokenSet.\n *\n * TokenSets require a unique identifier to be correctly minimised.\n *\n * @private\n */\nlunr.TokenSet._nextId = 1\n\n/**\n * Creates a TokenSet instance from the given sorted array of words.\n *\n * @param {String[]} arr - A sorted array of strings to create the set from.\n * @returns {lunr.TokenSet}\n * @throws Will throw an error if the input array is not sorted.\n */\nlunr.TokenSet.fromArray = function (arr) {\n var builder = new lunr.TokenSet.Builder\n\n for (var i = 0, len = arr.length; i < len; i++) {\n builder.insert(arr[i])\n }\n\n builder.finish()\n return builder.root\n}\n\n/**\n * Creates a token set from a query clause.\n *\n * @private\n * @param {Object} clause - A single clause from lunr.Query.\n * @param {string} clause.term - The query clause term.\n * @param {number} [clause.editDistance] - The optional edit distance for the term.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromClause = function (clause) {\n if ('editDistance' in clause) {\n return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance)\n } else {\n return lunr.TokenSet.fromString(clause.term)\n }\n}\n\n/**\n * Creates a token set representing a single string with a specified\n * edit distance.\n *\n * Insertions, deletions, substitutions and transpositions are each\n * treated as an edit distance of 1.\n *\n * Increasing the allowed edit distance will have a dramatic impact\n * on the performance of both creating and intersecting these TokenSets.\n * It is advised to keep the edit distance less than 3.\n *\n * @param {string} str - The string to create the token set from.\n * @param {number} editDistance - The allowed edit distance to match.\n * @returns {lunr.Vector}\n */\nlunr.TokenSet.fromFuzzyString = function (str, editDistance) {\n var root = new lunr.TokenSet\n\n var stack = [{\n node: root,\n editsRemaining: editDistance,\n str: str\n }]\n\n while (stack.length) {\n var frame = stack.pop()\n\n // no edit\n if (frame.str.length > 0) {\n var char = frame.str.charAt(0),\n noEditNode\n\n if (char in frame.node.edges) {\n noEditNode = frame.node.edges[char]\n } else {\n noEditNode = new lunr.TokenSet\n frame.node.edges[char] = noEditNode\n }\n\n if (frame.str.length == 1) {\n noEditNode.final = true\n }\n\n stack.push({\n node: noEditNode,\n editsRemaining: frame.editsRemaining,\n str: frame.str.slice(1)\n })\n }\n\n if (frame.editsRemaining == 0) {\n continue\n }\n\n // insertion\n if (\"*\" in frame.node.edges) {\n var insertionNode = frame.node.edges[\"*\"]\n } else {\n var insertionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = insertionNode\n }\n\n if (frame.str.length == 0) {\n insertionNode.final = true\n }\n\n stack.push({\n node: insertionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str\n })\n\n // deletion\n // can only do a deletion if we have enough edits remaining\n // and if there are characters left to delete in the string\n if (frame.str.length > 1) {\n stack.push({\n node: frame.node,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // deletion\n // just removing the last character from the str\n if (frame.str.length == 1) {\n frame.node.final = true\n }\n\n // substitution\n // can only do a substitution if we have enough edits remaining\n // and if there are characters left to substitute\n if (frame.str.length >= 1) {\n if (\"*\" in frame.node.edges) {\n var substitutionNode = frame.node.edges[\"*\"]\n } else {\n var substitutionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = substitutionNode\n }\n\n if (frame.str.length == 1) {\n substitutionNode.final = true\n }\n\n stack.push({\n node: substitutionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // transposition\n // can only do a transposition if there are edits remaining\n // and there are enough characters to transpose\n if (frame.str.length > 1) {\n var charA = frame.str.charAt(0),\n charB = frame.str.charAt(1),\n transposeNode\n\n if (charB in frame.node.edges) {\n transposeNode = frame.node.edges[charB]\n } else {\n transposeNode = new lunr.TokenSet\n frame.node.edges[charB] = transposeNode\n }\n\n if (frame.str.length == 1) {\n transposeNode.final = true\n }\n\n stack.push({\n node: transposeNode,\n editsRemaining: frame.editsRemaining - 1,\n str: charA + frame.str.slice(2)\n })\n }\n }\n\n return root\n}\n\n/**\n * Creates a TokenSet from a string.\n *\n * The string may contain one or more wildcard characters (*)\n * that will allow wildcard matching when intersecting with\n * another TokenSet.\n *\n * @param {string} str - The string to create a TokenSet from.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromString = function (str) {\n var node = new lunr.TokenSet,\n root = node\n\n /*\n * Iterates through all characters within the passed string\n * appending a node for each character.\n *\n * When a wildcard character is found then a self\n * referencing edge is introduced to continually match\n * any number of any characters.\n */\n for (var i = 0, len = str.length; i < len; i++) {\n var char = str[i],\n final = (i == len - 1)\n\n if (char == \"*\") {\n node.edges[char] = node\n node.final = final\n\n } else {\n var next = new lunr.TokenSet\n next.final = final\n\n node.edges[char] = next\n node = next\n }\n }\n\n return root\n}\n\n/**\n * Converts this TokenSet into an array of strings\n * contained within the TokenSet.\n *\n * This is not intended to be used on a TokenSet that\n * contains wildcards, in these cases the results are\n * undefined and are likely to cause an infinite loop.\n *\n * @returns {string[]}\n */\nlunr.TokenSet.prototype.toArray = function () {\n var words = []\n\n var stack = [{\n prefix: \"\",\n node: this\n }]\n\n while (stack.length) {\n var frame = stack.pop(),\n edges = Object.keys(frame.node.edges),\n len = edges.length\n\n if (frame.node.final) {\n /* In Safari, at this point the prefix is sometimes corrupted, see:\n * https://github.com/olivernn/lunr.js/issues/279 Calling any\n * String.prototype method forces Safari to \"cast\" this string to what\n * it's supposed to be, fixing the bug. */\n frame.prefix.charAt(0)\n words.push(frame.prefix)\n }\n\n for (var i = 0; i < len; i++) {\n var edge = edges[i]\n\n stack.push({\n prefix: frame.prefix.concat(edge),\n node: frame.node.edges[edge]\n })\n }\n }\n\n return words\n}\n\n/**\n * Generates a string representation of a TokenSet.\n *\n * This is intended to allow TokenSets to be used as keys\n * in objects, largely to aid the construction and minimisation\n * of a TokenSet. As such it is not designed to be a human\n * friendly representation of the TokenSet.\n *\n * @returns {string}\n */\nlunr.TokenSet.prototype.toString = function () {\n // NOTE: Using Object.keys here as this.edges is very likely\n // to enter 'hash-mode' with many keys being added\n //\n // avoiding a for-in loop here as it leads to the function\n // being de-optimised (at least in V8). From some simple\n // benchmarks the performance is comparable, but allowing\n // V8 to optimize may mean easy performance wins in the future.\n\n if (this._str) {\n return this._str\n }\n\n var str = this.final ? '1' : '0',\n labels = Object.keys(this.edges).sort(),\n len = labels.length\n\n for (var i = 0; i < len; i++) {\n var label = labels[i],\n node = this.edges[label]\n\n str = str + label + node.id\n }\n\n return str\n}\n\n/**\n * Returns a new TokenSet that is the intersection of\n * this TokenSet and the passed TokenSet.\n *\n * This intersection will take into account any wildcards\n * contained within the TokenSet.\n *\n * @param {lunr.TokenSet} b - An other TokenSet to intersect with.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.prototype.intersect = function (b) {\n var output = new lunr.TokenSet,\n frame = undefined\n\n var stack = [{\n qNode: b,\n output: output,\n node: this\n }]\n\n while (stack.length) {\n frame = stack.pop()\n\n // NOTE: As with the #toString method, we are using\n // Object.keys and a for loop instead of a for-in loop\n // as both of these objects enter 'hash' mode, causing\n // the function to be de-optimised in V8\n var qEdges = Object.keys(frame.qNode.edges),\n qLen = qEdges.length,\n nEdges = Object.keys(frame.node.edges),\n nLen = nEdges.length\n\n for (var q = 0; q < qLen; q++) {\n var qEdge = qEdges[q]\n\n for (var n = 0; n < nLen; n++) {\n var nEdge = nEdges[n]\n\n if (nEdge == qEdge || qEdge == '*') {\n var node = frame.node.edges[nEdge],\n qNode = frame.qNode.edges[qEdge],\n final = node.final && qNode.final,\n next = undefined\n\n if (nEdge in frame.output.edges) {\n // an edge already exists for this character\n // no need to create a new node, just set the finality\n // bit unless this node is already final\n next = frame.output.edges[nEdge]\n next.final = next.final || final\n\n } else {\n // no edge exists yet, must create one\n // set the finality bit and insert it\n // into the output\n next = new lunr.TokenSet\n next.final = final\n frame.output.edges[nEdge] = next\n }\n\n stack.push({\n qNode: qNode,\n output: next,\n node: node\n })\n }\n }\n }\n }\n\n return output\n}\nlunr.TokenSet.Builder = function () {\n this.previousWord = \"\"\n this.root = new lunr.TokenSet\n this.uncheckedNodes = []\n this.minimizedNodes = {}\n}\n\nlunr.TokenSet.Builder.prototype.insert = function (word) {\n var node,\n commonPrefix = 0\n\n if (word < this.previousWord) {\n throw new Error (\"Out of order word insertion\")\n }\n\n for (var i = 0; i < word.length && i < this.previousWord.length; i++) {\n if (word[i] != this.previousWord[i]) break\n commonPrefix++\n }\n\n this.minimize(commonPrefix)\n\n if (this.uncheckedNodes.length == 0) {\n node = this.root\n } else {\n node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child\n }\n\n for (var i = commonPrefix; i < word.length; i++) {\n var nextNode = new lunr.TokenSet,\n char = word[i]\n\n node.edges[char] = nextNode\n\n this.uncheckedNodes.push({\n parent: node,\n char: char,\n child: nextNode\n })\n\n node = nextNode\n }\n\n node.final = true\n this.previousWord = word\n}\n\nlunr.TokenSet.Builder.prototype.finish = function () {\n this.minimize(0)\n}\n\nlunr.TokenSet.Builder.prototype.minimize = function (downTo) {\n for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) {\n var node = this.uncheckedNodes[i],\n childKey = node.child.toString()\n\n if (childKey in this.minimizedNodes) {\n node.parent.edges[node.char] = this.minimizedNodes[childKey]\n } else {\n // Cache the key for this node since\n // we know it can't change anymore\n node.child._str = childKey\n\n this.minimizedNodes[childKey] = node.child\n }\n\n this.uncheckedNodes.pop()\n }\n}\n/*!\n * lunr.Index\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * An index contains the built index of all documents and provides a query interface\n * to the index.\n *\n * Usually instances of lunr.Index will not be created using this constructor, instead\n * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be\n * used to load previously built and serialized indexes.\n *\n * @constructor\n * @param {Object} attrs - The attributes of the built search index.\n * @param {Object} attrs.invertedIndex - An index of term/field to document reference.\n * @param {Object} attrs.fieldVectors - Field vectors\n * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens.\n * @param {string[]} attrs.fields - The names of indexed document fields.\n * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms.\n */\nlunr.Index = function (attrs) {\n this.invertedIndex = attrs.invertedIndex\n this.fieldVectors = attrs.fieldVectors\n this.tokenSet = attrs.tokenSet\n this.fields = attrs.fields\n this.pipeline = attrs.pipeline\n}\n\n/**\n * A result contains details of a document matching a search query.\n * @typedef {Object} lunr.Index~Result\n * @property {string} ref - The reference of the document this result represents.\n * @property {number} score - A number between 0 and 1 representing how similar this document is to the query.\n * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match.\n */\n\n/**\n * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple\n * query language which itself is parsed into an instance of lunr.Query.\n *\n * For programmatically building queries it is advised to directly use lunr.Query, the query language\n * is best used for human entered text rather than program generated text.\n *\n * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported\n * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello'\n * or 'world', though those that contain both will rank higher in the results.\n *\n * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can\n * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding\n * wildcards will increase the number of documents that will be found but can also have a negative\n * impact on query performance, especially with wildcards at the beginning of a term.\n *\n * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term\n * hello in the title field will match this query. Using a field not present in the index will lead\n * to an error being thrown.\n *\n * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term\n * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported\n * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2.\n * Avoid large values for edit distance to improve query performance.\n *\n * Each term also supports a presence modifier. By default a term's presence in document is optional, however\n * this can be changed to either required or prohibited. For a term's presence to be required in a document the\n * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and\n * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not\n * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'.\n *\n * To escape special characters the backslash character '\\' can be used, this allows searches to include\n * characters that would normally be considered modifiers, e.g. `foo\\~2` will search for a term \"foo~2\" instead\n * of attempting to apply a boost of 2 to the search term \"foo\".\n *\n * @typedef {string} lunr.Index~QueryString\n * @example Simple single term query\n * hello\n * @example Multiple term query\n * hello world\n * @example term scoped to a field\n * title:hello\n * @example term with a boost of 10\n * hello^10\n * @example term with an edit distance of 2\n * hello~2\n * @example terms with presence modifiers\n * -foo +bar baz\n */\n\n/**\n * Performs a search against the index using lunr query syntax.\n *\n * Results will be returned sorted by their score, the most relevant results\n * will be returned first. For details on how the score is calculated, please see\n * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}.\n *\n * For more programmatic querying use lunr.Index#query.\n *\n * @param {lunr.Index~QueryString} queryString - A string containing a lunr query.\n * @throws {lunr.QueryParseError} If the passed query string cannot be parsed.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.search = function (queryString) {\n return this.query(function (query) {\n var parser = new lunr.QueryParser(queryString, query)\n parser.parse()\n })\n}\n\n/**\n * A query builder callback provides a query object to be used to express\n * the query to perform on the index.\n *\n * @callback lunr.Index~queryBuilder\n * @param {lunr.Query} query - The query object to build up.\n * @this lunr.Query\n */\n\n/**\n * Performs a query against the index using the yielded lunr.Query object.\n *\n * If performing programmatic queries against the index, this method is preferred\n * over lunr.Index#search so as to avoid the additional query parsing overhead.\n *\n * A query object is yielded to the supplied function which should be used to\n * express the query to be run against the index.\n *\n * Note that although this function takes a callback parameter it is _not_ an\n * asynchronous operation, the callback is just yielded a query object to be\n * customized.\n *\n * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.query = function (fn) {\n // for each query clause\n // * process terms\n // * expand terms from token set\n // * find matching documents and metadata\n // * get document vectors\n // * score documents\n\n var query = new lunr.Query(this.fields),\n matchingFields = Object.create(null),\n queryVectors = Object.create(null),\n termFieldCache = Object.create(null),\n requiredMatches = Object.create(null),\n prohibitedMatches = Object.create(null)\n\n /*\n * To support field level boosts a query vector is created per\n * field. An empty vector is eagerly created to support negated\n * queries.\n */\n for (var i = 0; i < this.fields.length; i++) {\n queryVectors[this.fields[i]] = new lunr.Vector\n }\n\n fn.call(query, query)\n\n for (var i = 0; i < query.clauses.length; i++) {\n /*\n * Unless the pipeline has been disabled for this term, which is\n * the case for terms with wildcards, we need to pass the clause\n * term through the search pipeline. A pipeline returns an array\n * of processed terms. Pipeline functions may expand the passed\n * term, which means we may end up performing multiple index lookups\n * for a single query term.\n */\n var clause = query.clauses[i],\n terms = null,\n clauseMatches = lunr.Set.empty\n\n if (clause.usePipeline) {\n terms = this.pipeline.runString(clause.term, {\n fields: clause.fields\n })\n } else {\n terms = [clause.term]\n }\n\n for (var m = 0; m < terms.length; m++) {\n var term = terms[m]\n\n /*\n * Each term returned from the pipeline needs to use the same query\n * clause object, e.g. the same boost and or edit distance. The\n * simplest way to do this is to re-use the clause object but mutate\n * its term property.\n */\n clause.term = term\n\n /*\n * From the term in the clause we create a token set which will then\n * be used to intersect the indexes token set to get a list of terms\n * to lookup in the inverted index\n */\n var termTokenSet = lunr.TokenSet.fromClause(clause),\n expandedTerms = this.tokenSet.intersect(termTokenSet).toArray()\n\n /*\n * If a term marked as required does not exist in the tokenSet it is\n * impossible for the search to return any matches. We set all the field\n * scoped required matches set to empty and stop examining any further\n * clauses.\n */\n if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = lunr.Set.empty\n }\n\n break\n }\n\n for (var j = 0; j < expandedTerms.length; j++) {\n /*\n * For each term get the posting and termIndex, this is required for\n * building the query vector.\n */\n var expandedTerm = expandedTerms[j],\n posting = this.invertedIndex[expandedTerm],\n termIndex = posting._index\n\n for (var k = 0; k < clause.fields.length; k++) {\n /*\n * For each field that this query term is scoped by (by default\n * all fields are in scope) we need to get all the document refs\n * that have this term in that field.\n *\n * The posting is the entry in the invertedIndex for the matching\n * term from above.\n */\n var field = clause.fields[k],\n fieldPosting = posting[field],\n matchingDocumentRefs = Object.keys(fieldPosting),\n termField = expandedTerm + \"/\" + field,\n matchingDocumentsSet = new lunr.Set(matchingDocumentRefs)\n\n /*\n * if the presence of this term is required ensure that the matching\n * documents are added to the set of required matches for this clause.\n *\n */\n if (clause.presence == lunr.Query.presence.REQUIRED) {\n clauseMatches = clauseMatches.union(matchingDocumentsSet)\n\n if (requiredMatches[field] === undefined) {\n requiredMatches[field] = lunr.Set.complete\n }\n }\n\n /*\n * if the presence of this term is prohibited ensure that the matching\n * documents are added to the set of prohibited matches for this field,\n * creating that set if it does not yet exist.\n */\n if (clause.presence == lunr.Query.presence.PROHIBITED) {\n if (prohibitedMatches[field] === undefined) {\n prohibitedMatches[field] = lunr.Set.empty\n }\n\n prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet)\n\n /*\n * Prohibited matches should not be part of the query vector used for\n * similarity scoring and no metadata should be extracted so we continue\n * to the next field\n */\n continue\n }\n\n /*\n * The query field vector is populated using the termIndex found for\n * the term and a unit value with the appropriate boost applied.\n * Using upsert because there could already be an entry in the vector\n * for the term we are working with. In that case we just add the scores\n * together.\n */\n queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b })\n\n /**\n * If we've already seen this term, field combo then we've already collected\n * the matching documents and metadata, no need to go through all that again\n */\n if (termFieldCache[termField]) {\n continue\n }\n\n for (var l = 0; l < matchingDocumentRefs.length; l++) {\n /*\n * All metadata for this term/field/document triple\n * are then extracted and collected into an instance\n * of lunr.MatchData ready to be returned in the query\n * results\n */\n var matchingDocumentRef = matchingDocumentRefs[l],\n matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field),\n metadata = fieldPosting[matchingDocumentRef],\n fieldMatch\n\n if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) {\n matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata)\n } else {\n fieldMatch.add(expandedTerm, field, metadata)\n }\n\n }\n\n termFieldCache[termField] = true\n }\n }\n }\n\n /**\n * If the presence was required we need to update the requiredMatches field sets.\n * We do this after all fields for the term have collected their matches because\n * the clause terms presence is required in _any_ of the fields not _all_ of the\n * fields.\n */\n if (clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = requiredMatches[field].intersect(clauseMatches)\n }\n }\n }\n\n /**\n * Need to combine the field scoped required and prohibited\n * matching documents into a global set of required and prohibited\n * matches\n */\n var allRequiredMatches = lunr.Set.complete,\n allProhibitedMatches = lunr.Set.empty\n\n for (var i = 0; i < this.fields.length; i++) {\n var field = this.fields[i]\n\n if (requiredMatches[field]) {\n allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field])\n }\n\n if (prohibitedMatches[field]) {\n allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field])\n }\n }\n\n var matchingFieldRefs = Object.keys(matchingFields),\n results = [],\n matches = Object.create(null)\n\n /*\n * If the query is negated (contains only prohibited terms)\n * we need to get _all_ fieldRefs currently existing in the\n * index. This is only done when we know that the query is\n * entirely prohibited terms to avoid any cost of getting all\n * fieldRefs unnecessarily.\n *\n * Additionally, blank MatchData must be created to correctly\n * populate the results.\n */\n if (query.isNegated()) {\n matchingFieldRefs = Object.keys(this.fieldVectors)\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n var matchingFieldRef = matchingFieldRefs[i]\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRef)\n matchingFields[matchingFieldRef] = new lunr.MatchData\n }\n }\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n /*\n * Currently we have document fields that match the query, but we\n * need to return documents. The matchData and scores are combined\n * from multiple fields belonging to the same document.\n *\n * Scores are calculated by field, using the query vectors created\n * above, and combined into a final document score using addition.\n */\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]),\n docRef = fieldRef.docRef\n\n if (!allRequiredMatches.contains(docRef)) {\n continue\n }\n\n if (allProhibitedMatches.contains(docRef)) {\n continue\n }\n\n var fieldVector = this.fieldVectors[fieldRef],\n score = queryVectors[fieldRef.fieldName].similarity(fieldVector),\n docMatch\n\n if ((docMatch = matches[docRef]) !== undefined) {\n docMatch.score += score\n docMatch.matchData.combine(matchingFields[fieldRef])\n } else {\n var match = {\n ref: docRef,\n score: score,\n matchData: matchingFields[fieldRef]\n }\n matches[docRef] = match\n results.push(match)\n }\n }\n\n /*\n * Sort the results objects by score, highest first.\n */\n return results.sort(function (a, b) {\n return b.score - a.score\n })\n}\n\n/**\n * Prepares the index for JSON serialization.\n *\n * The schema for this JSON blob will be described in a\n * separate JSON schema file.\n *\n * @returns {Object}\n */\nlunr.Index.prototype.toJSON = function () {\n var invertedIndex = Object.keys(this.invertedIndex)\n .sort()\n .map(function (term) {\n return [term, this.invertedIndex[term]]\n }, this)\n\n var fieldVectors = Object.keys(this.fieldVectors)\n .map(function (ref) {\n return [ref, this.fieldVectors[ref].toJSON()]\n }, this)\n\n return {\n version: lunr.version,\n fields: this.fields,\n fieldVectors: fieldVectors,\n invertedIndex: invertedIndex,\n pipeline: this.pipeline.toJSON()\n }\n}\n\n/**\n * Loads a previously serialized lunr.Index\n *\n * @param {Object} serializedIndex - A previously serialized lunr.Index\n * @returns {lunr.Index}\n */\nlunr.Index.load = function (serializedIndex) {\n var attrs = {},\n fieldVectors = {},\n serializedVectors = serializedIndex.fieldVectors,\n invertedIndex = Object.create(null),\n serializedInvertedIndex = serializedIndex.invertedIndex,\n tokenSetBuilder = new lunr.TokenSet.Builder,\n pipeline = lunr.Pipeline.load(serializedIndex.pipeline)\n\n if (serializedIndex.version != lunr.version) {\n lunr.utils.warn(\"Version mismatch when loading serialised index. Current version of lunr '\" + lunr.version + \"' does not match serialized index '\" + serializedIndex.version + \"'\")\n }\n\n for (var i = 0; i < serializedVectors.length; i++) {\n var tuple = serializedVectors[i],\n ref = tuple[0],\n elements = tuple[1]\n\n fieldVectors[ref] = new lunr.Vector(elements)\n }\n\n for (var i = 0; i < serializedInvertedIndex.length; i++) {\n var tuple = serializedInvertedIndex[i],\n term = tuple[0],\n posting = tuple[1]\n\n tokenSetBuilder.insert(term)\n invertedIndex[term] = posting\n }\n\n tokenSetBuilder.finish()\n\n attrs.fields = serializedIndex.fields\n\n attrs.fieldVectors = fieldVectors\n attrs.invertedIndex = invertedIndex\n attrs.tokenSet = tokenSetBuilder.root\n attrs.pipeline = pipeline\n\n return new lunr.Index(attrs)\n}\n/*!\n * lunr.Builder\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Builder performs indexing on a set of documents and\n * returns instances of lunr.Index ready for querying.\n *\n * All configuration of the index is done via the builder, the\n * fields to index, the document reference, the text processing\n * pipeline and document scoring parameters are all set on the\n * builder before indexing.\n *\n * @constructor\n * @property {string} _ref - Internal reference to the document reference field.\n * @property {string[]} _fields - Internal reference to the document fields to index.\n * @property {object} invertedIndex - The inverted index maps terms to document fields.\n * @property {object} documentTermFrequencies - Keeps track of document term frequencies.\n * @property {object} documentLengths - Keeps track of the length of documents added to the index.\n * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing.\n * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing.\n * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index.\n * @property {number} documentCount - Keeps track of the total number of documents indexed.\n * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75.\n * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2.\n * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space.\n * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index.\n */\nlunr.Builder = function () {\n this._ref = \"id\"\n this._fields = Object.create(null)\n this._documents = Object.create(null)\n this.invertedIndex = Object.create(null)\n this.fieldTermFrequencies = {}\n this.fieldLengths = {}\n this.tokenizer = lunr.tokenizer\n this.pipeline = new lunr.Pipeline\n this.searchPipeline = new lunr.Pipeline\n this.documentCount = 0\n this._b = 0.75\n this._k1 = 1.2\n this.termIndex = 0\n this.metadataWhitelist = []\n}\n\n/**\n * Sets the document field used as the document reference. Every document must have this field.\n * The type of this field in the document should be a string, if it is not a string it will be\n * coerced into a string by calling toString.\n *\n * The default ref is 'id'.\n *\n * The ref should _not_ be changed during indexing, it should be set before any documents are\n * added to the index. Changing it during indexing can lead to inconsistent results.\n *\n * @param {string} ref - The name of the reference field in the document.\n */\nlunr.Builder.prototype.ref = function (ref) {\n this._ref = ref\n}\n\n/**\n * A function that is used to extract a field from a document.\n *\n * Lunr expects a field to be at the top level of a document, if however the field\n * is deeply nested within a document an extractor function can be used to extract\n * the right field for indexing.\n *\n * @callback fieldExtractor\n * @param {object} doc - The document being added to the index.\n * @returns {?(string|object|object[])} obj - The object that will be indexed for this field.\n * @example Extracting a nested field\n * function (doc) { return doc.nested.field }\n */\n\n/**\n * Adds a field to the list of document fields that will be indexed. Every document being\n * indexed should have this field. Null values for this field in indexed documents will\n * not cause errors but will limit the chance of that document being retrieved by searches.\n *\n * All fields should be added before adding documents to the index. Adding fields after\n * a document has been indexed will have no effect on already indexed documents.\n *\n * Fields can be boosted at build time. This allows terms within that field to have more\n * importance when ranking search results. Use a field boost to specify that matches within\n * one field are more important than other fields.\n *\n * @param {string} fieldName - The name of a field to index in all documents.\n * @param {object} attributes - Optional attributes associated with this field.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this field.\n * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document.\n * @throws {RangeError} fieldName cannot contain unsupported characters '/'\n */\nlunr.Builder.prototype.field = function (fieldName, attributes) {\n if (/\\//.test(fieldName)) {\n throw new RangeError (\"Field '\" + fieldName + \"' contains illegal character '/'\")\n }\n\n this._fields[fieldName] = attributes || {}\n}\n\n/**\n * A parameter to tune the amount of field length normalisation that is applied when\n * calculating relevance scores. A value of 0 will completely disable any normalisation\n * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b\n * will be clamped to the range 0 - 1.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.b = function (number) {\n if (number < 0) {\n this._b = 0\n } else if (number > 1) {\n this._b = 1\n } else {\n this._b = number\n }\n}\n\n/**\n * A parameter that controls the speed at which a rise in term frequency results in term\n * frequency saturation. The default value is 1.2. Setting this to a higher value will give\n * slower saturation levels, a lower value will result in quicker saturation.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.k1 = function (number) {\n this._k1 = number\n}\n\n/**\n * Adds a document to the index.\n *\n * Before adding fields to the index the index should have been fully setup, with the document\n * ref and all fields to index already having been specified.\n *\n * The document must have a field name as specified by the ref (by default this is 'id') and\n * it should have all fields defined for indexing, though null or undefined values will not\n * cause errors.\n *\n * Entire documents can be boosted at build time. Applying a boost to a document indicates that\n * this document should rank higher in search results than other documents.\n *\n * @param {object} doc - The document to add to the index.\n * @param {object} attributes - Optional attributes associated with this document.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this document.\n */\nlunr.Builder.prototype.add = function (doc, attributes) {\n var docRef = doc[this._ref],\n fields = Object.keys(this._fields)\n\n this._documents[docRef] = attributes || {}\n this.documentCount += 1\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i],\n extractor = this._fields[fieldName].extractor,\n field = extractor ? extractor(doc) : doc[fieldName],\n tokens = this.tokenizer(field, {\n fields: [fieldName]\n }),\n terms = this.pipeline.run(tokens),\n fieldRef = new lunr.FieldRef (docRef, fieldName),\n fieldTerms = Object.create(null)\n\n this.fieldTermFrequencies[fieldRef] = fieldTerms\n this.fieldLengths[fieldRef] = 0\n\n // store the length of this field for this document\n this.fieldLengths[fieldRef] += terms.length\n\n // calculate term frequencies for this field\n for (var j = 0; j < terms.length; j++) {\n var term = terms[j]\n\n if (fieldTerms[term] == undefined) {\n fieldTerms[term] = 0\n }\n\n fieldTerms[term] += 1\n\n // add to inverted index\n // create an initial posting if one doesn't exist\n if (this.invertedIndex[term] == undefined) {\n var posting = Object.create(null)\n posting[\"_index\"] = this.termIndex\n this.termIndex += 1\n\n for (var k = 0; k < fields.length; k++) {\n posting[fields[k]] = Object.create(null)\n }\n\n this.invertedIndex[term] = posting\n }\n\n // add an entry for this term/fieldName/docRef to the invertedIndex\n if (this.invertedIndex[term][fieldName][docRef] == undefined) {\n this.invertedIndex[term][fieldName][docRef] = Object.create(null)\n }\n\n // store all whitelisted metadata about this token in the\n // inverted index\n for (var l = 0; l < this.metadataWhitelist.length; l++) {\n var metadataKey = this.metadataWhitelist[l],\n metadata = term.metadata[metadataKey]\n\n if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) {\n this.invertedIndex[term][fieldName][docRef][metadataKey] = []\n }\n\n this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata)\n }\n }\n\n }\n}\n\n/**\n * Calculates the average document length for this index\n *\n * @private\n */\nlunr.Builder.prototype.calculateAverageFieldLengths = function () {\n\n var fieldRefs = Object.keys(this.fieldLengths),\n numberOfFields = fieldRefs.length,\n accumulator = {},\n documentsWithField = {}\n\n for (var i = 0; i < numberOfFields; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n field = fieldRef.fieldName\n\n documentsWithField[field] || (documentsWithField[field] = 0)\n documentsWithField[field] += 1\n\n accumulator[field] || (accumulator[field] = 0)\n accumulator[field] += this.fieldLengths[fieldRef]\n }\n\n var fields = Object.keys(this._fields)\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i]\n accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName]\n }\n\n this.averageFieldLength = accumulator\n}\n\n/**\n * Builds a vector space model of every document using lunr.Vector\n *\n * @private\n */\nlunr.Builder.prototype.createFieldVectors = function () {\n var fieldVectors = {},\n fieldRefs = Object.keys(this.fieldTermFrequencies),\n fieldRefsLength = fieldRefs.length,\n termIdfCache = Object.create(null)\n\n for (var i = 0; i < fieldRefsLength; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n fieldName = fieldRef.fieldName,\n fieldLength = this.fieldLengths[fieldRef],\n fieldVector = new lunr.Vector,\n termFrequencies = this.fieldTermFrequencies[fieldRef],\n terms = Object.keys(termFrequencies),\n termsLength = terms.length\n\n\n var fieldBoost = this._fields[fieldName].boost || 1,\n docBoost = this._documents[fieldRef.docRef].boost || 1\n\n for (var j = 0; j < termsLength; j++) {\n var term = terms[j],\n tf = termFrequencies[term],\n termIndex = this.invertedIndex[term]._index,\n idf, score, scoreWithPrecision\n\n if (termIdfCache[term] === undefined) {\n idf = lunr.idf(this.invertedIndex[term], this.documentCount)\n termIdfCache[term] = idf\n } else {\n idf = termIdfCache[term]\n }\n\n score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf)\n score *= fieldBoost\n score *= docBoost\n scoreWithPrecision = Math.round(score * 1000) / 1000\n // Converts 1.23456789 to 1.234.\n // Reducing the precision so that the vectors take up less\n // space when serialised. Doing it now so that they behave\n // the same before and after serialisation. Also, this is\n // the fastest approach to reducing a number's precision in\n // JavaScript.\n\n fieldVector.insert(termIndex, scoreWithPrecision)\n }\n\n fieldVectors[fieldRef] = fieldVector\n }\n\n this.fieldVectors = fieldVectors\n}\n\n/**\n * Creates a token set of all tokens in the index using lunr.TokenSet\n *\n * @private\n */\nlunr.Builder.prototype.createTokenSet = function () {\n this.tokenSet = lunr.TokenSet.fromArray(\n Object.keys(this.invertedIndex).sort()\n )\n}\n\n/**\n * Builds the index, creating an instance of lunr.Index.\n *\n * This completes the indexing process and should only be called\n * once all documents have been added to the index.\n *\n * @returns {lunr.Index}\n */\nlunr.Builder.prototype.build = function () {\n this.calculateAverageFieldLengths()\n this.createFieldVectors()\n this.createTokenSet()\n\n return new lunr.Index({\n invertedIndex: this.invertedIndex,\n fieldVectors: this.fieldVectors,\n tokenSet: this.tokenSet,\n fields: Object.keys(this._fields),\n pipeline: this.searchPipeline\n })\n}\n\n/**\n * Applies a plugin to the index builder.\n *\n * A plugin is a function that is called with the index builder as its context.\n * Plugins can be used to customise or extend the behaviour of the index\n * in some way. A plugin is just a function, that encapsulated the custom\n * behaviour that should be applied when building the index.\n *\n * The plugin function will be called with the index builder as its argument, additional\n * arguments can also be passed when calling use. The function will be called\n * with the index builder as its context.\n *\n * @param {Function} plugin The plugin to apply.\n */\nlunr.Builder.prototype.use = function (fn) {\n var args = Array.prototype.slice.call(arguments, 1)\n args.unshift(this)\n fn.apply(this, args)\n}\n/**\n * Contains and collects metadata about a matching document.\n * A single instance of lunr.MatchData is returned as part of every\n * lunr.Index~Result.\n *\n * @constructor\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n * @property {object} metadata - A cloned collection of metadata associated with this document.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData = function (term, field, metadata) {\n var clonedMetadata = Object.create(null),\n metadataKeys = Object.keys(metadata || {})\n\n // Cloning the metadata to prevent the original\n // being mutated during match data combination.\n // Metadata is kept in an array within the inverted\n // index so cloning the data can be done with\n // Array#slice\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n clonedMetadata[key] = metadata[key].slice()\n }\n\n this.metadata = Object.create(null)\n\n if (term !== undefined) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = clonedMetadata\n }\n}\n\n/**\n * An instance of lunr.MatchData will be created for every term that matches a\n * document. However only one instance is required in a lunr.Index~Result. This\n * method combines metadata from another instance of lunr.MatchData with this\n * objects metadata.\n *\n * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData.prototype.combine = function (otherMatchData) {\n var terms = Object.keys(otherMatchData.metadata)\n\n for (var i = 0; i < terms.length; i++) {\n var term = terms[i],\n fields = Object.keys(otherMatchData.metadata[term])\n\n if (this.metadata[term] == undefined) {\n this.metadata[term] = Object.create(null)\n }\n\n for (var j = 0; j < fields.length; j++) {\n var field = fields[j],\n keys = Object.keys(otherMatchData.metadata[term][field])\n\n if (this.metadata[term][field] == undefined) {\n this.metadata[term][field] = Object.create(null)\n }\n\n for (var k = 0; k < keys.length; k++) {\n var key = keys[k]\n\n if (this.metadata[term][field][key] == undefined) {\n this.metadata[term][field][key] = otherMatchData.metadata[term][field][key]\n } else {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key])\n }\n\n }\n }\n }\n}\n\n/**\n * Add metadata for a term/field pair to this instance of match data.\n *\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n */\nlunr.MatchData.prototype.add = function (term, field, metadata) {\n if (!(term in this.metadata)) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = metadata\n return\n }\n\n if (!(field in this.metadata[term])) {\n this.metadata[term][field] = metadata\n return\n }\n\n var metadataKeys = Object.keys(metadata)\n\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n\n if (key in this.metadata[term][field]) {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key])\n } else {\n this.metadata[term][field][key] = metadata[key]\n }\n }\n}\n/**\n * A lunr.Query provides a programmatic way of defining queries to be performed\n * against a {@link lunr.Index}.\n *\n * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method\n * so the query object is pre-initialized with the right index fields.\n *\n * @constructor\n * @property {lunr.Query~Clause[]} clauses - An array of query clauses.\n * @property {string[]} allFields - An array of all available fields in a lunr.Index.\n */\nlunr.Query = function (allFields) {\n this.clauses = []\n this.allFields = allFields\n}\n\n/**\n * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause.\n *\n * This allows wildcards to be added to the beginning and end of a term without having to manually do any string\n * concatenation.\n *\n * The wildcard constants can be bitwise combined to select both leading and trailing wildcards.\n *\n * @constant\n * @default\n * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour\n * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists\n * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with trailing wildcard\n * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING })\n * @example query term with leading and trailing wildcard\n * query.term('foo', {\n * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING\n * })\n */\n\nlunr.Query.wildcard = new String (\"*\")\nlunr.Query.wildcard.NONE = 0\nlunr.Query.wildcard.LEADING = 1\nlunr.Query.wildcard.TRAILING = 2\n\n/**\n * Constants for indicating what kind of presence a term must have in matching documents.\n *\n * @constant\n * @enum {number}\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with required presence\n * query.term('foo', { presence: lunr.Query.presence.REQUIRED })\n */\nlunr.Query.presence = {\n /**\n * Term's presence in a document is optional, this is the default value.\n */\n OPTIONAL: 1,\n\n /**\n * Term's presence in a document is required, documents that do not contain\n * this term will not be returned.\n */\n REQUIRED: 2,\n\n /**\n * Term's presence in a document is prohibited, documents that do contain\n * this term will not be returned.\n */\n PROHIBITED: 3\n}\n\n/**\n * A single clause in a {@link lunr.Query} contains a term and details on how to\n * match that term against a {@link lunr.Index}.\n *\n * @typedef {Object} lunr.Query~Clause\n * @property {string[]} fields - The fields in an index this clause should be matched against.\n * @property {number} [boost=1] - Any boost that should be applied when matching this clause.\n * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be.\n * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline.\n * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended.\n * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents.\n */\n\n/**\n * Adds a {@link lunr.Query~Clause} to this query.\n *\n * Unless the clause contains the fields to be matched all fields will be matched. In addition\n * a default boost of 1 is applied to the clause.\n *\n * @param {lunr.Query~Clause} clause - The clause to add to this query.\n * @see lunr.Query~Clause\n * @returns {lunr.Query}\n */\nlunr.Query.prototype.clause = function (clause) {\n if (!('fields' in clause)) {\n clause.fields = this.allFields\n }\n\n if (!('boost' in clause)) {\n clause.boost = 1\n }\n\n if (!('usePipeline' in clause)) {\n clause.usePipeline = true\n }\n\n if (!('wildcard' in clause)) {\n clause.wildcard = lunr.Query.wildcard.NONE\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) {\n clause.term = \"*\" + clause.term\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) {\n clause.term = \"\" + clause.term + \"*\"\n }\n\n if (!('presence' in clause)) {\n clause.presence = lunr.Query.presence.OPTIONAL\n }\n\n this.clauses.push(clause)\n\n return this\n}\n\n/**\n * A negated query is one in which every clause has a presence of\n * prohibited. These queries require some special processing to return\n * the expected results.\n *\n * @returns boolean\n */\nlunr.Query.prototype.isNegated = function () {\n for (var i = 0; i < this.clauses.length; i++) {\n if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause}\n * to the list of clauses that make up this query.\n *\n * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion\n * to a token or token-like string should be done before calling this method.\n *\n * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an\n * array, each term in the array will share the same options.\n *\n * @param {object|object[]} term - The term(s) to add to the query.\n * @param {object} [options] - Any additional properties to add to the query clause.\n * @returns {lunr.Query}\n * @see lunr.Query#clause\n * @see lunr.Query~Clause\n * @example adding a single term to a query\n * query.term(\"foo\")\n * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard\n * query.term(\"foo\", {\n * fields: [\"title\"],\n * boost: 10,\n * wildcard: lunr.Query.wildcard.TRAILING\n * })\n * @example using lunr.tokenizer to convert a string to tokens before using them as terms\n * query.term(lunr.tokenizer(\"foo bar\"))\n */\nlunr.Query.prototype.term = function (term, options) {\n if (Array.isArray(term)) {\n term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this)\n return this\n }\n\n var clause = options || {}\n clause.term = term.toString()\n\n this.clause(clause)\n\n return this\n}\nlunr.QueryParseError = function (message, start, end) {\n this.name = \"QueryParseError\"\n this.message = message\n this.start = start\n this.end = end\n}\n\nlunr.QueryParseError.prototype = new Error\nlunr.QueryLexer = function (str) {\n this.lexemes = []\n this.str = str\n this.length = str.length\n this.pos = 0\n this.start = 0\n this.escapeCharPositions = []\n}\n\nlunr.QueryLexer.prototype.run = function () {\n var state = lunr.QueryLexer.lexText\n\n while (state) {\n state = state(this)\n }\n}\n\nlunr.QueryLexer.prototype.sliceString = function () {\n var subSlices = [],\n sliceStart = this.start,\n sliceEnd = this.pos\n\n for (var i = 0; i < this.escapeCharPositions.length; i++) {\n sliceEnd = this.escapeCharPositions[i]\n subSlices.push(this.str.slice(sliceStart, sliceEnd))\n sliceStart = sliceEnd + 1\n }\n\n subSlices.push(this.str.slice(sliceStart, this.pos))\n this.escapeCharPositions.length = 0\n\n return subSlices.join('')\n}\n\nlunr.QueryLexer.prototype.emit = function (type) {\n this.lexemes.push({\n type: type,\n str: this.sliceString(),\n start: this.start,\n end: this.pos\n })\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.escapeCharacter = function () {\n this.escapeCharPositions.push(this.pos - 1)\n this.pos += 1\n}\n\nlunr.QueryLexer.prototype.next = function () {\n if (this.pos >= this.length) {\n return lunr.QueryLexer.EOS\n }\n\n var char = this.str.charAt(this.pos)\n this.pos += 1\n return char\n}\n\nlunr.QueryLexer.prototype.width = function () {\n return this.pos - this.start\n}\n\nlunr.QueryLexer.prototype.ignore = function () {\n if (this.start == this.pos) {\n this.pos += 1\n }\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.backup = function () {\n this.pos -= 1\n}\n\nlunr.QueryLexer.prototype.acceptDigitRun = function () {\n var char, charCode\n\n do {\n char = this.next()\n charCode = char.charCodeAt(0)\n } while (charCode > 47 && charCode < 58)\n\n if (char != lunr.QueryLexer.EOS) {\n this.backup()\n }\n}\n\nlunr.QueryLexer.prototype.more = function () {\n return this.pos < this.length\n}\n\nlunr.QueryLexer.EOS = 'EOS'\nlunr.QueryLexer.FIELD = 'FIELD'\nlunr.QueryLexer.TERM = 'TERM'\nlunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE'\nlunr.QueryLexer.BOOST = 'BOOST'\nlunr.QueryLexer.PRESENCE = 'PRESENCE'\n\nlunr.QueryLexer.lexField = function (lexer) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.FIELD)\n lexer.ignore()\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexTerm = function (lexer) {\n if (lexer.width() > 1) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.TERM)\n }\n\n lexer.ignore()\n\n if (lexer.more()) {\n return lunr.QueryLexer.lexText\n }\n}\n\nlunr.QueryLexer.lexEditDistance = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.EDIT_DISTANCE)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexBoost = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.BOOST)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexEOS = function (lexer) {\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n}\n\n// This matches the separator used when tokenising fields\n// within a document. These should match otherwise it is\n// not possible to search for some tokens within a document.\n//\n// It is possible for the user to change the separator on the\n// tokenizer so it _might_ clash with any other of the special\n// characters already used within the search string, e.g. :.\n//\n// This means that it is possible to change the separator in\n// such a way that makes some words unsearchable using a search\n// string.\nlunr.QueryLexer.termSeparator = lunr.tokenizer.separator\n\nlunr.QueryLexer.lexText = function (lexer) {\n while (true) {\n var char = lexer.next()\n\n if (char == lunr.QueryLexer.EOS) {\n return lunr.QueryLexer.lexEOS\n }\n\n // Escape character is '\\'\n if (char.charCodeAt(0) == 92) {\n lexer.escapeCharacter()\n continue\n }\n\n if (char == \":\") {\n return lunr.QueryLexer.lexField\n }\n\n if (char == \"~\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexEditDistance\n }\n\n if (char == \"^\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexBoost\n }\n\n // \"+\" indicates term presence is required\n // checking for length to ensure that only\n // leading \"+\" are considered\n if (char == \"+\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n // \"-\" indicates term presence is prohibited\n // checking for length to ensure that only\n // leading \"-\" are considered\n if (char == \"-\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n if (char.match(lunr.QueryLexer.termSeparator)) {\n return lunr.QueryLexer.lexTerm\n }\n }\n}\n\nlunr.QueryParser = function (str, query) {\n this.lexer = new lunr.QueryLexer (str)\n this.query = query\n this.currentClause = {}\n this.lexemeIdx = 0\n}\n\nlunr.QueryParser.prototype.parse = function () {\n this.lexer.run()\n this.lexemes = this.lexer.lexemes\n\n var state = lunr.QueryParser.parseClause\n\n while (state) {\n state = state(this)\n }\n\n return this.query\n}\n\nlunr.QueryParser.prototype.peekLexeme = function () {\n return this.lexemes[this.lexemeIdx]\n}\n\nlunr.QueryParser.prototype.consumeLexeme = function () {\n var lexeme = this.peekLexeme()\n this.lexemeIdx += 1\n return lexeme\n}\n\nlunr.QueryParser.prototype.nextClause = function () {\n var completedClause = this.currentClause\n this.query.clause(completedClause)\n this.currentClause = {}\n}\n\nlunr.QueryParser.parseClause = function (parser) {\n var lexeme = parser.peekLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.type) {\n case lunr.QueryLexer.PRESENCE:\n return lunr.QueryParser.parsePresence\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expected either a field or a term, found \" + lexeme.type\n\n if (lexeme.str.length >= 1) {\n errorMessage += \" with value '\" + lexeme.str + \"'\"\n }\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n}\n\nlunr.QueryParser.parsePresence = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.str) {\n case \"-\":\n parser.currentClause.presence = lunr.Query.presence.PROHIBITED\n break\n case \"+\":\n parser.currentClause.presence = lunr.Query.presence.REQUIRED\n break\n default:\n var errorMessage = \"unrecognised presence operator'\" + lexeme.str + \"'\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term or field, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term or field, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseField = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n if (parser.query.allFields.indexOf(lexeme.str) == -1) {\n var possibleFields = parser.query.allFields.map(function (f) { return \"'\" + f + \"'\" }).join(', '),\n errorMessage = \"unrecognised field '\" + lexeme.str + \"', possible fields: \" + possibleFields\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.fields = [lexeme.str]\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseTerm = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n parser.currentClause.term = lexeme.str.toLowerCase()\n\n if (lexeme.str.indexOf(\"*\") != -1) {\n parser.currentClause.usePipeline = false\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseEditDistance = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var editDistance = parseInt(lexeme.str, 10)\n\n if (isNaN(editDistance)) {\n var errorMessage = \"edit distance must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.editDistance = editDistance\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseBoost = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var boost = parseInt(lexeme.str, 10)\n\n if (isNaN(boost)) {\n var errorMessage = \"boost must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.boost = boost\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\n /**\n * export the module via AMD, CommonJS or as a browser global\n * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js\n */\n ;(function (root, factory) {\n if (true) {\n // AMD. Register as an anonymous module.\n !(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),\n\t\t__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?\n\t\t(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :\n\t\t__WEBPACK_AMD_DEFINE_FACTORY__),\n\t\t__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__))\n } else {}\n }(this, function () {\n /**\n * Just return a value to define the module export.\n * This example returns an object, but the module\n * can return a function as the exported value.\n */\n return lunr\n }))\n})();\n\n\n//# sourceURL=webpack:///../node_modules/lunr/lunr.js?"); + +/***/ }), + +/***/ "./default/assets/css/main.sass": +/*!**************************************!*\ + !*** ./default/assets/css/main.sass ***! + \**************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extract-plugin\n\n\n//# sourceURL=webpack:///./default/assets/css/main.sass?"); + +/***/ }), + +/***/ "./default/assets/js/src/bootstrap.ts": +/*!********************************************!*\ + !*** ./default/assets/js/src/bootstrap.ts ***! + \********************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _typedoc_Application__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./typedoc/Application */ \"./default/assets/js/src/typedoc/Application.ts\");\n/* harmony import */ var _typedoc_components_MenuHighlight__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./typedoc/components/MenuHighlight */ \"./default/assets/js/src/typedoc/components/MenuHighlight.ts\");\n/* harmony import */ var _typedoc_components_Search__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./typedoc/components/Search */ \"./default/assets/js/src/typedoc/components/Search.ts\");\n/* harmony import */ var _typedoc_components_Signature__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./typedoc/components/Signature */ \"./default/assets/js/src/typedoc/components/Signature.ts\");\n/* harmony import */ var _typedoc_components_Toggle__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./typedoc/components/Toggle */ \"./default/assets/js/src/typedoc/components/Toggle.ts\");\n/* harmony import */ var _typedoc_components_Filter__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./typedoc/components/Filter */ \"./default/assets/js/src/typedoc/components/Filter.ts\");\n/* harmony import */ var _css_main_sass__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../css/main.sass */ \"./default/assets/css/main.sass\");\n\n\n\n\n\n\n\n(0,_typedoc_components_Search__WEBPACK_IMPORTED_MODULE_2__.initSearch)();\n(0,_typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.registerComponent)(_typedoc_components_MenuHighlight__WEBPACK_IMPORTED_MODULE_1__.MenuHighlight, \".menu-highlight\");\n(0,_typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.registerComponent)(_typedoc_components_Signature__WEBPACK_IMPORTED_MODULE_3__.Signature, \".tsd-signatures\");\n(0,_typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.registerComponent)(_typedoc_components_Toggle__WEBPACK_IMPORTED_MODULE_4__.Toggle, \"a[data-toggle]\");\nif (_typedoc_components_Filter__WEBPACK_IMPORTED_MODULE_5__.Filter.isSupported()) {\n (0,_typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.registerComponent)(_typedoc_components_Filter__WEBPACK_IMPORTED_MODULE_5__.Filter, \"#tsd-filter\");\n}\nelse {\n document.documentElement.classList.add(\"no-filter\");\n}\nvar app = new _typedoc_Application__WEBPACK_IMPORTED_MODULE_0__.Application();\nObject.defineProperty(window, \"app\", { value: app });\n\n\n//# sourceURL=webpack:///./default/assets/js/src/bootstrap.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/Application.ts": +/*!******************************************************!*\ + !*** ./default/assets/js/src/typedoc/Application.ts ***! + \******************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"registerComponent\": () => /* binding */ registerComponent,\n/* harmony export */ \"Application\": () => /* binding */ Application\n/* harmony export */ });\n/**\n * List of all known components.\n */\nvar components = [];\n/**\n * Register a new component.\n */\nfunction registerComponent(constructor, selector) {\n components.push({\n selector: selector,\n constructor: constructor,\n });\n}\n/**\n * TypeDoc application class.\n */\nvar Application = /** @class */ (function () {\n /**\n * Create a new Application instance.\n */\n function Application() {\n this.createComponents(document.body);\n }\n /**\n * Create all components beneath the given jQuery element.\n */\n Application.prototype.createComponents = function (context) {\n components.forEach(function (c) {\n context.querySelectorAll(c.selector).forEach(function (el) {\n if (!el.dataset.hasInstance) {\n new c.constructor({ el: el });\n el.dataset.hasInstance = String(true);\n }\n });\n });\n };\n return Application;\n}());\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/Application.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/Component.ts": +/*!****************************************************!*\ + !*** ./default/assets/js/src/typedoc/Component.ts ***! + \****************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Component\": () => /* binding */ Component\n/* harmony export */ });\n/**\n * TypeDoc component class.\n */\nvar Component = /** @class */ (function () {\n function Component(options) {\n this.el = options.el;\n }\n return Component;\n}());\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/Component.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/EventTarget.ts": +/*!******************************************************!*\ + !*** ./default/assets/js/src/typedoc/EventTarget.ts ***! + \******************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"EventTarget\": () => /* binding */ EventTarget\n/* harmony export */ });\n/**\n * TypeDoc event target class.\n */\nvar EventTarget = /** @class */ (function () {\n function EventTarget() {\n this.listeners = {};\n }\n EventTarget.prototype.addEventListener = function (type, callback) {\n if (!(type in this.listeners)) {\n this.listeners[type] = [];\n }\n this.listeners[type].push(callback);\n };\n EventTarget.prototype.removeEventListener = function (type, callback) {\n if (!(type in this.listeners)) {\n return;\n }\n var stack = this.listeners[type];\n for (var i = 0, l = stack.length; i < l; i++) {\n if (stack[i] === callback) {\n stack.splice(i, 1);\n return;\n }\n }\n };\n EventTarget.prototype.dispatchEvent = function (event) {\n if (!(event.type in this.listeners)) {\n return true;\n }\n var stack = this.listeners[event.type].slice();\n for (var i = 0, l = stack.length; i < l; i++) {\n stack[i].call(this, event);\n }\n return !event.defaultPrevented;\n };\n return EventTarget;\n}());\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/EventTarget.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/Filter.ts": +/*!************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/Filter.ts ***! + \************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Filter\": () => /* binding */ Filter\n/* harmony export */ });\n/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ \"./default/assets/js/src/typedoc/Component.ts\");\n/* harmony import */ var _utils_pointer__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils/pointer */ \"./default/assets/js/src/typedoc/utils/pointer.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\nvar FilterItem = /** @class */ (function () {\n function FilterItem(key, value) {\n this.key = key;\n this.value = value;\n this.defaultValue = value;\n this.initialize();\n if (window.localStorage[this.key]) {\n this.setValue(this.fromLocalStorage(window.localStorage[this.key]));\n }\n }\n FilterItem.prototype.initialize = function () { };\n FilterItem.prototype.setValue = function (value) {\n if (this.value == value)\n return;\n var oldValue = this.value;\n this.value = value;\n window.localStorage[this.key] = this.toLocalStorage(value);\n this.handleValueChange(oldValue, value);\n };\n return FilterItem;\n}());\nvar FilterItemCheckbox = /** @class */ (function (_super) {\n __extends(FilterItemCheckbox, _super);\n function FilterItemCheckbox() {\n return _super !== null && _super.apply(this, arguments) || this;\n }\n FilterItemCheckbox.prototype.initialize = function () {\n var _this = this;\n var checkbox = document.querySelector(\"#tsd-filter-\" + this.key);\n if (!checkbox)\n return;\n this.checkbox = checkbox;\n this.checkbox.addEventListener(\"change\", function () {\n _this.setValue(_this.checkbox.checked);\n });\n };\n FilterItemCheckbox.prototype.handleValueChange = function (oldValue, newValue) {\n if (!this.checkbox)\n return;\n this.checkbox.checked = this.value;\n document.documentElement.classList.toggle(\"toggle-\" + this.key, this.value != this.defaultValue);\n };\n FilterItemCheckbox.prototype.fromLocalStorage = function (value) {\n return value == \"true\";\n };\n FilterItemCheckbox.prototype.toLocalStorage = function (value) {\n return value ? \"true\" : \"false\";\n };\n return FilterItemCheckbox;\n}(FilterItem));\nvar FilterItemSelect = /** @class */ (function (_super) {\n __extends(FilterItemSelect, _super);\n function FilterItemSelect() {\n return _super !== null && _super.apply(this, arguments) || this;\n }\n FilterItemSelect.prototype.initialize = function () {\n var _this = this;\n document.documentElement.classList.add(\"toggle-\" + this.key + this.value);\n var select = document.querySelector(\"#tsd-filter-\" + this.key);\n if (!select)\n return;\n this.select = select;\n var onActivate = function () {\n _this.select.classList.add(\"active\");\n };\n var onDeactivate = function () {\n _this.select.classList.remove(\"active\");\n };\n this.select.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerDown, onActivate);\n this.select.addEventListener(\"mouseover\", onActivate);\n this.select.addEventListener(\"mouseleave\", onDeactivate);\n this.select.querySelectorAll(\"li\").forEach(function (el) {\n el.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerUp, function (e) {\n select.classList.remove(\"active\");\n _this.setValue(e.target.dataset.value || \"\");\n });\n });\n document.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerDown, function (e) {\n if (_this.select.contains(e.target))\n return;\n _this.select.classList.remove(\"active\");\n });\n };\n FilterItemSelect.prototype.handleValueChange = function (oldValue, newValue) {\n this.select.querySelectorAll(\"li.selected\").forEach(function (el) {\n el.classList.remove(\"selected\");\n });\n var selected = this.select.querySelector('li[data-value=\"' + newValue + '\"]');\n var label = this.select.querySelector(\".tsd-select-label\");\n if (selected && label) {\n selected.classList.add(\"selected\");\n label.textContent = selected.textContent;\n }\n document.documentElement.classList.remove(\"toggle-\" + oldValue);\n document.documentElement.classList.add(\"toggle-\" + newValue);\n };\n FilterItemSelect.prototype.fromLocalStorage = function (value) {\n return value;\n };\n FilterItemSelect.prototype.toLocalStorage = function (value) {\n return value;\n };\n return FilterItemSelect;\n}(FilterItem));\nvar Filter = /** @class */ (function (_super) {\n __extends(Filter, _super);\n function Filter(options) {\n var _this = _super.call(this, options) || this;\n _this.optionVisibility = new FilterItemSelect(\"visibility\", \"private\");\n _this.optionInherited = new FilterItemCheckbox(\"inherited\", true);\n _this.optionExternals = new FilterItemCheckbox(\"externals\", true);\n return _this;\n }\n Filter.isSupported = function () {\n try {\n return typeof window.localStorage != \"undefined\";\n }\n catch (e) {\n return false;\n }\n };\n return Filter;\n}(_Component__WEBPACK_IMPORTED_MODULE_0__.Component));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/Filter.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/MenuHighlight.ts": +/*!*******************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/MenuHighlight.ts ***! + \*******************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"MenuHighlight\": () => /* binding */ MenuHighlight\n/* harmony export */ });\n/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ \"./default/assets/js/src/typedoc/Component.ts\");\n/* harmony import */ var _services_Viewport__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../services/Viewport */ \"./default/assets/js/src/typedoc/services/Viewport.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\n/**\n * Manages the sticky state of the navigation and moves the highlight\n * to the current navigation item.\n */\nvar MenuHighlight = /** @class */ (function (_super) {\n __extends(MenuHighlight, _super);\n /**\n * Create a new MenuHighlight instance.\n *\n * @param options Backbone view constructor options.\n */\n function MenuHighlight(options) {\n var _this = _super.call(this, options) || this;\n /**\n * List of all discovered anchors.\n */\n _this.anchors = [];\n /**\n * Index of the currently highlighted anchor.\n */\n _this.index = -1;\n _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.addEventListener(\"resize\", function () { return _this.onResize(); });\n _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.addEventListener(\"scroll\", function (e) { return _this.onScroll(e); });\n _this.createAnchors();\n return _this;\n }\n /**\n * Find all anchors on the current page.\n */\n MenuHighlight.prototype.createAnchors = function () {\n var _this = this;\n var base = window.location.href;\n if (base.indexOf(\"#\") != -1) {\n base = base.substr(0, base.indexOf(\"#\"));\n }\n this.el.querySelectorAll(\"a\").forEach(function (el) {\n var href = el.href;\n if (href.indexOf(\"#\") == -1)\n return;\n if (href.substr(0, base.length) != base)\n return;\n var hash = href.substr(href.indexOf(\"#\") + 1);\n var anchor = document.querySelector(\"a.tsd-anchor[name=\" + hash + \"]\");\n var link = el.parentNode;\n if (!anchor || !link)\n return;\n _this.anchors.push({\n link: link,\n anchor: anchor,\n position: 0,\n });\n });\n this.onResize();\n };\n /**\n * Triggered after the viewport was resized.\n */\n MenuHighlight.prototype.onResize = function () {\n var anchor;\n for (var index = 0, count = this.anchors.length; index < count; index++) {\n anchor = this.anchors[index];\n var rect = anchor.anchor.getBoundingClientRect();\n anchor.position = rect.top + document.body.scrollTop;\n }\n this.anchors.sort(function (a, b) {\n return a.position - b.position;\n });\n var event = new CustomEvent(\"scroll\", {\n detail: {\n scrollTop: _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.scrollTop,\n },\n });\n this.onScroll(event);\n };\n /**\n * Triggered after the viewport was scrolled.\n *\n * @param event The custom event with the current vertical scroll position.\n */\n MenuHighlight.prototype.onScroll = function (event) {\n var scrollTop = event.detail.scrollTop + 5;\n var anchors = this.anchors;\n var count = anchors.length - 1;\n var index = this.index;\n while (index > -1 && anchors[index].position > scrollTop) {\n index -= 1;\n }\n while (index < count && anchors[index + 1].position < scrollTop) {\n index += 1;\n }\n if (this.index != index) {\n if (this.index > -1)\n this.anchors[this.index].link.classList.remove(\"focus\");\n this.index = index;\n if (this.index > -1)\n this.anchors[this.index].link.classList.add(\"focus\");\n }\n };\n return MenuHighlight;\n}(_Component__WEBPACK_IMPORTED_MODULE_0__.Component));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/MenuHighlight.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/Search.ts": +/*!************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/Search.ts ***! + \************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"initSearch\": () => /* binding */ initSearch\n/* harmony export */ });\n/* harmony import */ var _utils_debounce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../utils/debounce */ \"./default/assets/js/src/typedoc/utils/debounce.ts\");\n/* harmony import */ var lunr__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! lunr */ \"../node_modules/lunr/lunr.js\");\n/* harmony import */ var lunr__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(lunr__WEBPACK_IMPORTED_MODULE_1__);\n\n\nfunction initSearch() {\n var searchEl = document.getElementById(\"tsd-search\");\n if (!searchEl)\n return;\n var searchScript = document.getElementById(\"search-script\");\n searchEl.classList.add(\"loading\");\n if (searchScript) {\n searchScript.addEventListener(\"error\", function () {\n searchEl.classList.remove(\"loading\");\n searchEl.classList.add(\"failure\");\n });\n searchScript.addEventListener(\"load\", function () {\n searchEl.classList.remove(\"loading\");\n searchEl.classList.add(\"ready\");\n });\n if (window.searchData) {\n searchEl.classList.remove(\"loading\");\n }\n }\n var field = document.querySelector(\"#tsd-search-field\");\n var results = document.querySelector(\".results\");\n if (!field || !results) {\n throw new Error(\"The input field or the result list wrapper was not found\");\n }\n var resultClicked = false;\n results.addEventListener(\"mousedown\", function () { return (resultClicked = true); });\n results.addEventListener(\"mouseup\", function () {\n resultClicked = false;\n searchEl.classList.remove(\"has-focus\");\n });\n field.addEventListener(\"focus\", function () { return searchEl.classList.add(\"has-focus\"); });\n field.addEventListener(\"blur\", function () {\n if (!resultClicked) {\n resultClicked = false;\n searchEl.classList.remove(\"has-focus\");\n }\n });\n var state = {\n base: searchEl.dataset.base + \"/\",\n };\n bindEvents(searchEl, results, field, state);\n}\nfunction bindEvents(searchEl, results, field, state) {\n field.addEventListener(\"input\", (0,_utils_debounce__WEBPACK_IMPORTED_MODULE_0__.debounce)(function () {\n updateResults(searchEl, results, field, state);\n }, 200));\n var preventPress = false;\n field.addEventListener(\"keydown\", function (e) {\n preventPress = true;\n if (e.key == \"Enter\") {\n gotoCurrentResult(results, field);\n }\n else if (e.key == \"Escape\") {\n field.blur();\n }\n else if (e.key == \"ArrowUp\") {\n setCurrentResult(results, -1);\n }\n else if (e.key === \"ArrowDown\") {\n setCurrentResult(results, 1);\n }\n else {\n preventPress = false;\n }\n });\n field.addEventListener(\"keypress\", function (e) {\n if (preventPress)\n e.preventDefault();\n });\n /**\n * Start searching by pressing slash.\n */\n document.body.addEventListener(\"keydown\", function (e) {\n if (e.altKey || e.ctrlKey || e.metaKey)\n return;\n if (!field.matches(\":focus\") && e.key === \"/\") {\n field.focus();\n e.preventDefault();\n }\n });\n}\nfunction checkIndex(state, searchEl) {\n if (state.index)\n return;\n if (window.searchData) {\n searchEl.classList.remove(\"loading\");\n searchEl.classList.add(\"ready\");\n state.data = window.searchData;\n state.index = lunr__WEBPACK_IMPORTED_MODULE_1__.Index.load(window.searchData.index);\n }\n}\nfunction updateResults(searchEl, results, query, state) {\n checkIndex(state, searchEl);\n // Don't clear results if loading state is not ready,\n // because loading or error message can be removed.\n if (!state.index || !state.data)\n return;\n results.textContent = \"\";\n var searchText = query.value.trim();\n // Perform a wildcard search\n var res = state.index.search(\"*\" + searchText + \"*\");\n for (var i = 0, c = Math.min(10, res.length); i < c; i++) {\n var row = state.data.rows[Number(res[i].ref)];\n // Bold the matched part of the query in the search results\n var name_1 = boldMatches(row.name, searchText);\n if (row.parent) {\n name_1 = \"\" + boldMatches(row.parent, searchText) + \".\" + name_1;\n }\n var item = document.createElement(\"li\");\n item.classList.value = row.classes;\n var anchor = document.createElement(\"a\");\n anchor.href = state.base + row.url;\n anchor.classList.add(\"tsd-kind-icon\");\n anchor.innerHTML = name_1;\n item.append(anchor);\n results.appendChild(item);\n }\n}\n/**\n * Move the highlight within the result set.\n */\nfunction setCurrentResult(results, dir) {\n var current = results.querySelector(\".current\");\n if (!current) {\n current = results.querySelector(dir == 1 ? \"li:first-child\" : \"li:last-child\");\n if (current) {\n current.classList.add(\"current\");\n }\n }\n else {\n var rel = dir == 1\n ? current.nextElementSibling\n : current.previousElementSibling;\n if (rel) {\n current.classList.remove(\"current\");\n rel.classList.add(\"current\");\n }\n }\n}\n/**\n * Navigate to the highlighted result.\n */\nfunction gotoCurrentResult(results, field) {\n var current = results.querySelector(\".current\");\n if (!current) {\n current = results.querySelector(\"li:first-child\");\n }\n if (current) {\n var link = current.querySelector(\"a\");\n if (link) {\n window.location.href = link.href;\n }\n field.blur();\n }\n}\nfunction boldMatches(text, search) {\n if (search === \"\") {\n return text;\n }\n var lowerText = text.toLocaleLowerCase();\n var lowerSearch = search.toLocaleLowerCase();\n var parts = [];\n var lastIndex = 0;\n var index = lowerText.indexOf(lowerSearch);\n while (index != -1) {\n parts.push(escapeHtml(text.substring(lastIndex, index)), \"\" + escapeHtml(text.substring(index, index + lowerSearch.length)) + \"\");\n lastIndex = index + lowerSearch.length;\n index = lowerText.indexOf(lowerSearch, lastIndex);\n }\n parts.push(escapeHtml(text.substring(lastIndex)));\n return parts.join(\"\");\n}\nvar SPECIAL_HTML = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \"'\": \"'\",\n '\"': \""\",\n};\nfunction escapeHtml(text) {\n return text.replace(/[&<>\"'\"]/g, function (match) { return SPECIAL_HTML[match]; });\n}\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/Search.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/Signature.ts": +/*!***************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/Signature.ts ***! + \***************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Signature\": () => /* binding */ Signature\n/* harmony export */ });\n/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ \"./default/assets/js/src/typedoc/Component.ts\");\n/* harmony import */ var _services_Viewport__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../services/Viewport */ \"./default/assets/js/src/typedoc/services/Viewport.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\n/**\n * Holds a signature and its description.\n */\nvar SignatureGroup = /** @class */ (function () {\n /**\n * Create a new SignatureGroup instance.\n *\n * @param signature The target signature.\n * @param description The description for the signature.\n */\n function SignatureGroup(signature, description) {\n this.signature = signature;\n this.description = description;\n }\n /**\n * Add the given class to all elements of the group.\n *\n * @param className The class name to add.\n */\n SignatureGroup.prototype.addClass = function (className) {\n this.signature.classList.add(className);\n this.description.classList.add(className);\n return this;\n };\n /**\n * Remove the given class from all elements of the group.\n *\n * @param className The class name to remove.\n */\n SignatureGroup.prototype.removeClass = function (className) {\n this.signature.classList.remove(className);\n this.description.classList.remove(className);\n return this;\n };\n return SignatureGroup;\n}());\n/**\n * Controls the tab like behaviour of methods and functions with multiple signatures.\n */\nvar Signature = /** @class */ (function (_super) {\n __extends(Signature, _super);\n /**\n * Create a new Signature instance.\n *\n * @param options Backbone view constructor options.\n */\n function Signature(options) {\n var _this = _super.call(this, options) || this;\n /**\n * List of found signature groups.\n */\n _this.groups = [];\n /**\n * The index of the currently displayed signature.\n */\n _this.index = -1;\n _this.createGroups();\n if (_this.container) {\n _this.el.classList.add(\"active\");\n Array.from(_this.el.children).forEach(function (signature) {\n signature.addEventListener(\"touchstart\", function (event) {\n return _this.onClick(event);\n });\n signature.addEventListener(\"click\", function (event) {\n return _this.onClick(event);\n });\n });\n _this.container.classList.add(\"active\");\n _this.setIndex(0);\n }\n return _this;\n }\n /**\n * Set the index of the active signature.\n *\n * @param index The index of the signature to activate.\n */\n Signature.prototype.setIndex = function (index) {\n if (index < 0)\n index = 0;\n if (index > this.groups.length - 1)\n index = this.groups.length - 1;\n if (this.index == index)\n return;\n var to = this.groups[index];\n if (this.index > -1) {\n var from_1 = this.groups[this.index];\n from_1.removeClass(\"current\").addClass(\"fade-out\");\n to.addClass(\"current\");\n to.addClass(\"fade-in\");\n _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.triggerResize();\n setTimeout(function () {\n from_1.removeClass(\"fade-out\");\n to.removeClass(\"fade-in\");\n }, 300);\n }\n else {\n to.addClass(\"current\");\n _services_Viewport__WEBPACK_IMPORTED_MODULE_1__.Viewport.instance.triggerResize();\n }\n this.index = index;\n };\n /**\n * Find all signature/description groups.\n */\n Signature.prototype.createGroups = function () {\n var signatures = this.el.children;\n if (signatures.length < 2)\n return;\n this.container = this.el.nextElementSibling;\n var descriptions = this.container.children;\n this.groups = [];\n for (var index = 0; index < signatures.length; index++) {\n this.groups.push(new SignatureGroup(signatures[index], descriptions[index]));\n }\n };\n /**\n * Triggered when the user clicks onto a signature header.\n *\n * @param e The related event object.\n */\n Signature.prototype.onClick = function (e) {\n var _this = this;\n this.groups.forEach(function (group, index) {\n if (group.signature === e.currentTarget) {\n _this.setIndex(index);\n }\n });\n };\n return Signature;\n}(_Component__WEBPACK_IMPORTED_MODULE_0__.Component));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/Signature.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/components/Toggle.ts": +/*!************************************************************!*\ + !*** ./default/assets/js/src/typedoc/components/Toggle.ts ***! + \************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Toggle\": () => /* binding */ Toggle\n/* harmony export */ });\n/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ \"./default/assets/js/src/typedoc/Component.ts\");\n/* harmony import */ var _utils_pointer__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils/pointer */ \"./default/assets/js/src/typedoc/utils/pointer.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\nvar Toggle = /** @class */ (function (_super) {\n __extends(Toggle, _super);\n function Toggle(options) {\n var _this = _super.call(this, options) || this;\n _this.className = _this.el.dataset.toggle || \"\";\n _this.el.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerUp, function (e) { return _this.onPointerUp(e); });\n _this.el.addEventListener(\"click\", function (e) { return e.preventDefault(); });\n document.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerDown, function (e) {\n return _this.onDocumentPointerDown(e);\n });\n document.addEventListener(_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.pointerUp, function (e) {\n return _this.onDocumentPointerUp(e);\n });\n return _this;\n }\n Toggle.prototype.setActive = function (value) {\n if (this.active == value)\n return;\n this.active = value;\n document.documentElement.classList.toggle(\"has-\" + this.className, value);\n this.el.classList.toggle(\"active\", value);\n var transition = (this.active ? \"to-has-\" : \"from-has-\") + this.className;\n document.documentElement.classList.add(transition);\n setTimeout(function () { return document.documentElement.classList.remove(transition); }, 500);\n };\n Toggle.prototype.onPointerUp = function (event) {\n if (_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.hasPointerMoved)\n return;\n this.setActive(true);\n event.preventDefault();\n };\n Toggle.prototype.onDocumentPointerDown = function (e) {\n if (this.active) {\n if (e.target.closest(\".col-menu, .tsd-filter-group\")) {\n return;\n }\n this.setActive(false);\n }\n };\n Toggle.prototype.onDocumentPointerUp = function (e) {\n var _this = this;\n if (_utils_pointer__WEBPACK_IMPORTED_MODULE_1__.hasPointerMoved)\n return;\n if (this.active) {\n if (e.target.closest(\".col-menu\")) {\n var link = e.target.closest(\"a\");\n if (link) {\n var href = window.location.href;\n if (href.indexOf(\"#\") != -1) {\n href = href.substr(0, href.indexOf(\"#\"));\n }\n if (link.href.substr(0, href.length) == href) {\n setTimeout(function () { return _this.setActive(false); }, 250);\n }\n }\n }\n }\n };\n return Toggle;\n}(_Component__WEBPACK_IMPORTED_MODULE_0__.Component));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/components/Toggle.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/services/Viewport.ts": +/*!************************************************************!*\ + !*** ./default/assets/js/src/typedoc/services/Viewport.ts ***! + \************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Viewport\": () => /* binding */ Viewport\n/* harmony export */ });\n/* harmony import */ var _EventTarget__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../EventTarget */ \"./default/assets/js/src/typedoc/EventTarget.ts\");\n/* harmony import */ var _utils_trottle__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils/trottle */ \"./default/assets/js/src/typedoc/utils/trottle.ts\");\nvar __extends = (undefined && undefined.__extends) || (function () {\n var extendStatics = function (d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n };\n return function (d, b) {\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n };\n})();\n\n\n/**\n * A global service that monitors the window size and scroll position.\n */\nvar Viewport = /** @class */ (function (_super) {\n __extends(Viewport, _super);\n /**\n * Create new Viewport instance.\n */\n function Viewport() {\n var _this = _super.call(this) || this;\n /**\n * The current scroll position.\n */\n _this.scrollTop = 0;\n /**\n * The previous scrollTop.\n */\n _this.lastY = 0;\n /**\n * The width of the window.\n */\n _this.width = 0;\n /**\n * The height of the window.\n */\n _this.height = 0;\n /**\n * Boolean indicating whether the toolbar is shown.\n */\n _this.showToolbar = true;\n _this.toolbar = (document.querySelector(\".tsd-page-toolbar\"));\n _this.secondaryNav = (document.querySelector(\".tsd-navigation.secondary\"));\n window.addEventListener(\"scroll\", (0,_utils_trottle__WEBPACK_IMPORTED_MODULE_1__.throttle)(function () { return _this.onScroll(); }, 10));\n window.addEventListener(\"resize\", (0,_utils_trottle__WEBPACK_IMPORTED_MODULE_1__.throttle)(function () { return _this.onResize(); }, 10));\n _this.onResize();\n _this.onScroll();\n return _this;\n }\n /**\n * Trigger a resize event.\n */\n Viewport.prototype.triggerResize = function () {\n var event = new CustomEvent(\"resize\", {\n detail: {\n width: this.width,\n height: this.height,\n },\n });\n this.dispatchEvent(event);\n };\n /**\n * Triggered when the size of the window has changed.\n */\n Viewport.prototype.onResize = function () {\n this.width = window.innerWidth || 0;\n this.height = window.innerHeight || 0;\n var event = new CustomEvent(\"resize\", {\n detail: {\n width: this.width,\n height: this.height,\n },\n });\n this.dispatchEvent(event);\n };\n /**\n * Triggered when the user scrolled the viewport.\n */\n Viewport.prototype.onScroll = function () {\n this.scrollTop = window.scrollY || 0;\n var event = new CustomEvent(\"scroll\", {\n detail: {\n scrollTop: this.scrollTop,\n },\n });\n this.dispatchEvent(event);\n this.hideShowToolbar();\n };\n /**\n * Handle hiding/showing of the toolbar.\n */\n Viewport.prototype.hideShowToolbar = function () {\n var isShown = this.showToolbar;\n this.showToolbar = this.lastY >= this.scrollTop || this.scrollTop <= 0;\n if (isShown !== this.showToolbar) {\n this.toolbar.classList.toggle(\"tsd-page-toolbar--hide\");\n this.secondaryNav.classList.toggle(\"tsd-navigation--toolbar-hide\");\n }\n this.lastY = this.scrollTop;\n };\n Viewport.instance = new Viewport();\n return Viewport;\n}(_EventTarget__WEBPACK_IMPORTED_MODULE_0__.EventTarget));\n\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/services/Viewport.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/utils/debounce.ts": +/*!*********************************************************!*\ + !*** ./default/assets/js/src/typedoc/utils/debounce.ts ***! + \*********************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"debounce\": () => /* binding */ debounce\n/* harmony export */ });\nvar debounce = function (fn, wait) {\n if (wait === void 0) { wait = 100; }\n var timeout;\n return function () {\n var args = [];\n for (var _i = 0; _i < arguments.length; _i++) {\n args[_i] = arguments[_i];\n }\n clearTimeout(timeout);\n timeout = setTimeout(function () { return fn(args); }, wait);\n };\n};\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/utils/debounce.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/utils/pointer.ts": +/*!********************************************************!*\ + !*** ./default/assets/js/src/typedoc/utils/pointer.ts ***! + \********************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"pointerDown\": () => /* binding */ pointerDown,\n/* harmony export */ \"pointerMove\": () => /* binding */ pointerMove,\n/* harmony export */ \"pointerUp\": () => /* binding */ pointerUp,\n/* harmony export */ \"pointerDownPosition\": () => /* binding */ pointerDownPosition,\n/* harmony export */ \"preventNextClick\": () => /* binding */ preventNextClick,\n/* harmony export */ \"isPointerDown\": () => /* binding */ isPointerDown,\n/* harmony export */ \"isPointerTouch\": () => /* binding */ isPointerTouch,\n/* harmony export */ \"hasPointerMoved\": () => /* binding */ hasPointerMoved,\n/* harmony export */ \"isMobile\": () => /* binding */ isMobile\n/* harmony export */ });\n/**\n * Event name of the pointer down event.\n */\nvar pointerDown = \"mousedown\";\n/**\n * Event name of the pointer move event.\n */\nvar pointerMove = \"mousemove\";\n/**\n * Event name of the pointer up event.\n */\nvar pointerUp = \"mouseup\";\n/**\n * Position the pointer was pressed at.\n */\nvar pointerDownPosition = { x: 0, y: 0 };\n/**\n * Should the next click on the document be supressed?\n */\nvar preventNextClick = false;\n/**\n * Is the pointer down?\n */\nvar isPointerDown = false;\n/**\n * Is the pointer a touch point?\n */\nvar isPointerTouch = false;\n/**\n * Did the pointer move since the last down event?\n */\nvar hasPointerMoved = false;\n/**\n * Is the user agent a mobile agent?\n */\nvar isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\ndocument.documentElement.classList.add(isMobile ? \"is-mobile\" : \"not-mobile\");\nif (isMobile && \"ontouchstart\" in document.documentElement) {\n isPointerTouch = true;\n pointerDown = \"touchstart\";\n pointerMove = \"touchmove\";\n pointerUp = \"touchend\";\n}\ndocument.addEventListener(pointerDown, function (e) {\n isPointerDown = true;\n hasPointerMoved = false;\n var t = pointerDown == \"touchstart\"\n ? e.targetTouches[0]\n : e;\n pointerDownPosition.y = t.pageY || 0;\n pointerDownPosition.x = t.pageX || 0;\n});\ndocument.addEventListener(pointerMove, function (e) {\n if (!isPointerDown)\n return;\n if (!hasPointerMoved) {\n var t = pointerDown == \"touchstart\"\n ? e.targetTouches[0]\n : e;\n var x = pointerDownPosition.x - (t.pageX || 0);\n var y = pointerDownPosition.y - (t.pageY || 0);\n hasPointerMoved = Math.sqrt(x * x + y * y) > 10;\n }\n});\ndocument.addEventListener(pointerUp, function () {\n isPointerDown = false;\n});\ndocument.addEventListener(\"click\", function (e) {\n if (preventNextClick) {\n e.preventDefault();\n e.stopImmediatePropagation();\n preventNextClick = false;\n }\n});\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/utils/pointer.ts?"); + +/***/ }), + +/***/ "./default/assets/js/src/typedoc/utils/trottle.ts": +/*!********************************************************!*\ + !*** ./default/assets/js/src/typedoc/utils/trottle.ts ***! + \********************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"throttle\": () => /* binding */ throttle\n/* harmony export */ });\nvar throttle = function (fn, wait) {\n if (wait === void 0) { wait = 100; }\n var time = Date.now();\n return function () {\n var args = [];\n for (var _i = 0; _i < arguments.length; _i++) {\n args[_i] = arguments[_i];\n }\n if (time + wait - Date.now() < 0) {\n fn.apply(void 0, args);\n time = Date.now();\n }\n };\n};\n\n\n//# sourceURL=webpack:///./default/assets/js/src/typedoc/utils/trottle.ts?"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ if(__webpack_module_cache__[moduleId]) { +/******/ return __webpack_module_cache__[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => module['default'] : +/******/ () => module; +/******/ __webpack_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +/******/ // startup +/******/ // Load entry module +/******/ __webpack_require__("./default/assets/js/src/bootstrap.ts"); +/******/ // This entry module used 'exports' so it can't be inlined +/******/ })() +; \ No newline at end of file diff --git a/docs/assets/js/search.js b/docs/assets/js/search.js new file mode 100644 index 0000000..d94ce50 --- /dev/null +++ b/docs/assets/js/search.js @@ -0,0 +1 @@ +window.searchData = {"kinds":{},"rows":[],"index":{"version":"2.3.9","fields":["name","parent"],"fieldVectors":[],"invertedIndex":[],"pipeline":[]}} \ No newline at end of file diff --git a/docs/dist_hooks - Kopie_events.js.html b/docs/dist_hooks - Kopie_events.js.html new file mode 100644 index 0000000..4e16052 --- /dev/null +++ b/docs/dist_hooks - Kopie_events.js.html @@ -0,0 +1,251 @@ + + + + + JSDoc: Source: dist/hooks - Kopie/events.js + + + + + + + + + + +
        + +

        Source: dist/hooks - Kopie/events.js

        + + + + + + +
        +
        +
        import { Exporter } from "../scripts/modules/exporter.js";
        +import { Renderer } from "../scripts/modules/renderer.js";
        +import { ModuleSettings } from "../scripts/modules/settings.js";
        +export class Events {
        +    constructor() {
        +        return this;
        +    }
        +    /**
        +     * Reload Filters for a given entity
        +     *
        +     * @param {HTMLCollection} html
        +     * @param {String} entityType
        +     */
        +    static async reloadFilters(html, entityType) {
        +        let entityBrowserHtml = await Renderer.renderFilters(game.compendiumBrowser.filters[entityType], entityType), filterWrapper = document.querySelector('.tab.active .browser .control-area');
        +        filterWrapper.innerHTML = entityBrowserHtml;
        +        Events.activateFilterListeners(html);
        +    }
        +    static async activateActionListener(app = document.getElementsByClassName('window-app')) {
        +        app = app.get(0);
        +        app.querySelector('button[data-action="export"]').addEventListener('click', async (e) => {
        +            let items = app.querySelectorAll('.content .tab.active .cb_entities .entity'), entityType = app.querySelector('.content .tab.active').dataset.tab, filters = JSON.parse(app.querySelector('.tab.active .cb_entities').dataset.activeFilters), tableItems = [], tableName = entityType + 'table: ';
        +            items.forEach(item => {
        +                let obj = {};
        +                Object.assign(obj, item.dataset);
        +                tableItems.push(obj);
        +            });
        +            let filterKeys = Object.keys(filters);
        +            if (filterKeys.length > 0) {
        +                for (let key of filterKeys) {
        +                    tableName = tableName + '_' + filters[key].path + '-' + filters[key].value;
        +                }
        +            }
        +            Exporter.createTableFromSelection(tableName, tableItems);
        +        });
        +    }
        +    static async activateItemListListeners(app = document.getElementsByClassName('window-app')) {
        +        app = app[0];
        +        // open entity sheet on click
        +        app.querySelectorAll('*[data-action="openSheet"]').forEach(async (el) => {
        +            el.addEventListener('click', async (e) => {
        +                let itemId = e.currentTarget.parentNode.dataset.entryId;
        +                let compendium = e.currentTarget.parentNode.dataset.entryCompendium;
        +                let pack = game.packs.find(p => p.collection === compendium);
        +                await pack.getEntity(itemId).then(entity => {
        +                    entity.sheet.render(true);
        +                });
        +            });
        +        });
        +        // make draggable
        +        //0.4.1: Avoid the game.packs lookup
        +        app.querySelectorAll('.draggable').forEach(async (li) => {
        +            li.setAttribute("draggable", true);
        +            li.addEventListener('dragstart', event => {
        +                let packName = li.getAttribute("data-entry-compendium");
        +                let pack = game.packs.find(p => p.collection === packName);
        +                if (!pack) {
        +                    event.preventDefault();
        +                    return false;
        +                }
        +                event.dataTransfer.setData("text/plain", JSON.stringify({
        +                    type: pack.entity,
        +                    pack: pack.collection,
        +                    id: li.getAttribute("data-entry-id")
        +                }));
        +            }, false);
        +        });
        +    }
        +    static async observeListElement(list, tag) {
        +        for (let element of list.getElementsByTagName(tag)) {
        +            game.compendiumBrowser.observer.observe(element);
        +        }
        +    }
        +    static async activateFilterListeners(html, app = document.getElementsByClassName('window-app')) {
        +        app = app[0];
        +        // toggle visibility of filter containers
        +        html.find('.filtercontainer h3, .multiselect label').click(async (ev) => {
        +            await $(ev.target.nextElementSibling).toggle(100);
        +        });
        +        html.find('.multiselect label').trigger('click');
        +        // reset filters and re-rendes
        +        app.querySelectorAll('.button[data-action="reset-filters').forEach(async (el) => {
        +            el.addEventListener('click', async (e) => {
        +                game.compendiumBrowser.filters.resetFilters();
        +                game.compendiumBrowser.refreshList = e.target.closest('.tab').dataset.tab;
        +                game.compendiumBrowser.render();
        +            });
        +        });
        +        // select filters
        +        app.querySelectorAll('.settings input').forEach(async (el) => {
        +            el.addEventListener('keyup change paste', async (e) => {
        +                let target = e.target, setting = target.dataset.setting, value = target.checked, key = target.dataset.key, category = (target.dataset.type === 'Spell' || target.dataset.type === 'Feat') ? 'Item' : target.dataset.type;
        +                if (key)
        +                    game.compendiumBrowser.settings.loadedCompendium[category][key].load = value;
        +                ui.notifications.info("Settings Saved. Compendiums are being reloaded.");
        +                switch (setting) {
        +                    case 'allow-spell-browser':
        +                        game.compendiumBrowser.settings.allowSpellBrowser = value;
        +                        break;
        +                    case 'allow-feat-browser':
        +                        game.compendiumBrowser.settings.allowFeatBrowser = value;
        +                        break;
        +                    case 'allow-item-browser':
        +                        game.compendiumBrowser.settings.allowItemBrowser = value;
        +                        break;
        +                    case 'allow-actor-browser':
        +                        game.compendiumBrowser.settings.allowActorBrowser = value;
        +                        break;
        +                    case 'allow-rolltable-browser':
        +                        game.compendiumBrowser.settings.allowRollTableBrowser = value;
        +                        break;
        +                    case 'allow-journalentry-browser':
        +                        game.compendiumBrowser.settings.allowJournalEntryBrowser = value;
        +                        break;
        +                    default:
        +                        break;
        +                }
        +                ModuleSettings.saveSettings();
        +                game.compendiumBrowser.render();
        +            });
        +        });
        +        // select filters
        +        app.querySelectorAll('.filter[data-type=text] input, .filter[data-type=text] select').forEach(async (el) => {
        +            el.addEventListener('keyup change paste', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), entityType = app.querySelector('.content .tab.active').dataset.tab;
        +                if (e.target.value === '' || e.target.value === undefined) {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                }
        +                else {
        +                    game.compendiumBrowser.filters[entityType].activeFilters[key] = {
        +                        path: path,
        +                        type: 'text',
        +                        valIsArray: false,
        +                        value: e.target.value
        +                    };
        +                }
        +                game.compendiumBrowser.replaceList(html, entityType);
        +            });
        +        });
        +        // select filters
        +        app.querySelectorAll('.filter[data-type=select] select, .filter[data-type=bool] select').forEach(async (el) => {
        +            el.addEventListener('change', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), filterType = e.target.closest('.filter').dataset.type, entityType = app.querySelector('.content .tab.active').dataset.tab;
        +                let valIsArray = e.target.closest('.filter').dataset.valIsArray === 'true', value = e.target.options[e.target.selectedIndex].value;
        +                if (value === "null") {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                }
        +                else {
        +                    game.compendiumBrowser.filters[entityType].activeFilters[key] = {
        +                        path: path,
        +                        type: filterType,
        +                        valIsArray: valIsArray,
        +                        value: value
        +                    };
        +                }
        +                game.compendiumBrowser.replaceList(html, entityType);
        +            });
        +        });
        +        // multiselect
        +        app.querySelectorAll('.filter[data-type=multiSelect] input').forEach(async (el) => {
        +            el.addEventListener('change', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), filterType = 'multiSelect', entityType = app.querySelector('.content .tab.active').dataset.tab, filter = game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                let valIsArray = e.target.closest('.filter').dataset.valIsArray, value = e.target.dataset.value;
        +                if (e.target.checked === true) {
        +                    if (filter === undefined) {
        +                        game.compendiumBrowser.filters[entityType].activeFilters[key] =
        +                            { path: path, type: filterType, valIsArray: valIsArray, values: [value] };
        +                    }
        +                    else {
        +                        game.compendiumBrowser.filters[entityType].activeFilters[key].values.push(value);
        +                    }
        +                }
        +                else {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key].values.splice(game.compendiumBrowser.filters[entityType].activeFilters[key].values.indexOf(value), 1);
        +                    if (game.compendiumBrowser.filters[entityType].activeFilters[key].values.length === 0)
        +                        delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                }
        +                game.compendiumBrowser.replaceList(html, entityType);
        +            });
        +        });
        +        app.querySelectorAll('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').forEach(async (el) => {
        +            el.addEventListener('change keyup paste', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), filterType = 'numberCompare', entityType = app.querySelector('.content .tab.active').dataset.tab, operator = e.target.closest('.filter').getElementsByTagName('select').val, value = e.target.closest('.filter').getElementsByTagName('input').val;
        +                if (value === '' || operator === 'null') {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                }
        +                else {
        +                    game.compendiumBrowser.filters[entityType].activeFilters[key] = {
        +                        path: path,
        +                        type: filterType,
        +                        valIsArray: false,
        +                        operator: operator,
        +                        value: value
        +                    };
        +                }
        +                game.compendiumBrowser.replaceList(html, browserTab);
        +            });
        +        });
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Fri Jul 23 2021 16:03:38 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_hooks - Kopie_moduleHooks.js.html b/docs/dist_hooks - Kopie_moduleHooks.js.html new file mode 100644 index 0000000..8e3dfae --- /dev/null +++ b/docs/dist_hooks - Kopie_moduleHooks.js.html @@ -0,0 +1,140 @@ + + + + + JSDoc: Source: dist/hooks - Kopie/moduleHooks.js + + + + + + + + + + +
        + +

        Source: dist/hooks - Kopie/moduleHooks.js

        + + + + + + +
        +
        +
        /**
        + * @fileOverview Holding the modules Hooks
        + *
        + * @author Daniel Böttner <jackprince1983@gmail.com>
        + * @version 1.0.0
        + */
        +/* jshint node: true */
        +'use strict';
        +import { CMPBrowser } from '../scripts/modules/settings.js';
        +import { CompendiumBrowser } from '../scripts/compendium-browser.js';
        +import { VersionCheck } from '../scripts/versioning/version-check.js';
        +import { Renderer } from '../scripts/modules/renderer.js';
        +/**
        + * Holds the static methods to be called
        + */
        +export default class moduleHooks {
        +    static onInit() {
        +        Hooks.on('init', async () => {
        +            Handlebars.registerHelper('switch', function (value, options) {
        +                this.switch_value = value;
        +                this.switch_break = false;
        +                return options.fn(this);
        +            });
        +            Handlebars.registerHelper('case', function (value, options) {
        +                if (value == this.switch_value) {
        +                    this.switch_break = true;
        +                    return options.fn(this);
        +                }
        +            });
        +            Handlebars.registerHelper('default', function (value, options) {
        +                if (this.switch_break == false) {
        +                    return value;
        +                }
        +            });
        +            Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
        +                switch (operator) {
        +                    case '==':
        +                        return (v1 == v2) ? options.fn(this) : options.inverse(this);
        +                    case '===':
        +                        return (v1 === v2) ? options.fn(this) : options.inverse(this);
        +                    case '!=':
        +                        return (v1 != v2) ? options.fn(this) : options.inverse(this);
        +                    case '!==':
        +                        return (v1 !== v2) ? options.fn(this) : options.inverse(this);
        +                    case '<':
        +                        return (v1 < v2) ? options.fn(this) : options.inverse(this);
        +                    case '<=':
        +                        return (v1 <= v2) ? options.fn(this) : options.inverse(this);
        +                    case '>':
        +                        return (v1 > v2) ? options.fn(this) : options.inverse(this);
        +                    case '>=':
        +                        return (v1 >= v2) ? options.fn(this) : options.inverse(this);
        +                    case '&&':
        +                        return (v1 && v2) ? options.fn(this) : options.inverse(this);
        +                    case '||':
        +                        return (v1 || v2) ? options.fn(this) : options.inverse(this);
        +                    default:
        +                        return options.inverse(this);
        +                }
        +            });
        +            Handlebars.registerHelper("debug", function (optionalValue) {
        +                console.log("Current Context");
        +                console.log("====================");
        +                console.log(this);
        +                if (optionalValue) {
        +                    console.log("Value");
        +                    console.log("====================");
        +                    console.log(optionalValue);
        +                }
        +            });
        +        });
        +    }
        +    static onReady() {
        +        Hooks.on('ready', async () => {
        +            if (game.compendiumBrowser === undefined) {
        +                game.compendiumBrowser = new CompendiumBrowser();
        +                //0.4.0 Defer loading content until we actually use the Compendium Browser
        +                //A compromise approach would be better (periodic loading) except would still create the memory use problem
        +                await game.compendiumBrowser.initialize();
        +            }
        +            if (VersionCheck.check(CMPBrowser.MODULE_NAME) && game.user.isGM) {
        +                console.log('version check');
        +            }
        +        });
        +    }
        +    static onRenderComplete() {
        +        Hooks.on("renderCompendiumBrowser", Renderer.afterRender);
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Fri Jul 23 2021 16:03:38 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_hooks_events.js.html b/docs/dist_hooks_events.js.html new file mode 100644 index 0000000..ebfc2c4 --- /dev/null +++ b/docs/dist_hooks_events.js.html @@ -0,0 +1,261 @@ + + + + + JSDoc: Source: dist/hooks/events.js + + + + + + + + + + +
        + +

        Source: dist/hooks/events.js

        + + + + + + +
        +
        +
        import { Exporter } from "../scripts/modules/exporter.js";
        +import { Renderer } from "../scripts/modules/renderer.js";
        +import { ModuleSettings } from "../scripts/modules/settings.js";
        +export class Events {
        +    constructor() {
        +        return this;
        +    }
        +    /**
        +     * Reload Filters for a given entity
        +     *
        +     * @param {HTMLCollection} html
        +     * @param {String} entityType
        +     */
        +    static async reloadFilters(html, entityType) {
        +        let entityBrowserHtml = await Renderer.renderFilters(game.compendiumBrowser.filters[entityType], entityType), filterWrapper = document.querySelector('.tab.active .browser .control-area');
        +        filterWrapper.innerHTML = entityBrowserHtml;
        +        Events.activateFilterListeners(html);
        +    }
        +    /**
        +     *
        +     * @param {HTMLCollection} app
        +     */
        +    static async activateActionListener(app = document.getElementsByClassName('window-app')) {
        +        app = app.get(0);
        +        app.querySelector('button[data-action="export"]').addEventListener('click', async (e) => {
        +            let items = app.querySelectorAll('.content .tab.active .cb_entities .entity'), entityType = app.querySelector('.content .tab.active').dataset.tab, filters = JSON.parse(app.querySelector('.tab.active .cb_entities').dataset.activeFilters), tableItems = [], tableName = entityType + 'table: ';
        +            items.forEach(item => {
        +                let obj = {};
        +                Object.assign(obj, item.dataset);
        +                tableItems.push(obj);
        +            });
        +            let filterKeys = Object.keys(filters);
        +            if (filterKeys.length > 0) {
        +                for (let key of filterKeys) {
        +                    tableName = tableName + '_' + filters[key].path + '-' + filters[key].value;
        +                }
        +            }
        +            let d = Dialog.confirm({
        +                title: "Export subset to table",
        +                content: "<p>Choose wisely.</p>",
        +                yes: () => Exporter.createTableFromSelection(tableName, tableItems),
        +                no: () => console.log("You chose ... poorly"),
        +                defaultYes: false
        +            });
        +        });
        +    }
        +    static async activateItemListListeners(app = document.getElementsByClassName('window-app')) {
        +        app = app[0];
        +        // open entity sheet on click
        +        app.querySelectorAll('*[data-action="openSheet"]').forEach(async (el) => {
        +            el.addEventListener('click', async (e) => {
        +                let itemId = e.currentTarget.parentNode.dataset.entryId;
        +                let compendium = e.currentTarget.parentNode.dataset.entryCompendium;
        +                let pack = game.packs.find(p => p.collection === compendium);
        +                await pack.getEntity(itemId).then(entity => {
        +                    entity.sheet.render(true);
        +                });
        +            });
        +        });
        +        // make draggable
        +        //0.4.1: Avoid the game.packs lookup
        +        app.querySelectorAll('.draggable').forEach(async (li) => {
        +            li.setAttribute("draggable", true);
        +            li.addEventListener('dragstart', event => {
        +                let packName = li.getAttribute("data-entry-compendium");
        +                let pack = game.packs.find(p => p.collection === packName);
        +                if (!pack) {
        +                    event.preventDefault();
        +                    return false;
        +                }
        +                event.dataTransfer.setData("text/plain", JSON.stringify({
        +                    type: pack.entity,
        +                    pack: pack.collection,
        +                    id: li.getAttribute("data-entry-id")
        +                }));
        +            }, false);
        +        });
        +    }
        +    static async observeListElement(list, tag) {
        +        for (let element of list.getElementsByTagName(tag)) {
        +            game.compendiumBrowser.observer.observe(element);
        +        }
        +    }
        +    static async activateFilterListeners(html, app = document.getElementsByClassName('window-app')) {
        +        app = app[0];
        +        // toggle visibility of filter containers
        +        html.find('.filtercontainer h3, .multiselect label').click(async (ev) => {
        +            await $(ev.target.nextElementSibling).toggle(100);
        +        });
        +        html.find('.multiselect label').trigger('click');
        +        // reset filters and re-rendes
        +        app.querySelectorAll('.button[data-action="reset-filters').forEach(async (el) => {
        +            el.addEventListener('click', async (e) => {
        +                game.compendiumBrowser.filters.resetFilters();
        +                game.compendiumBrowser.refreshList = e.target.closest('.tab').dataset.tab;
        +                game.compendiumBrowser.render();
        +            });
        +        });
        +        // select filters
        +        app.querySelectorAll('.settings input').forEach(async (el) => {
        +            el.addEventListener('keyup change paste', async (e) => {
        +                let target = e.target, setting = target.dataset.setting, value = target.checked, key = target.dataset.key, category = (target.dataset.type === 'Spell' || target.dataset.type === 'Feat') ? 'Item' : target.dataset.type;
        +                if (key)
        +                    game.compendiumBrowser.settings.loadedCompendium[category][key].load = value;
        +                ui.notifications.info("Settings Saved. Compendiums are being reloaded.");
        +                switch (setting) {
        +                    case 'allow-spell-browser':
        +                        game.compendiumBrowser.settings.allowSpellBrowser = value;
        +                        break;
        +                    case 'allow-feat-browser':
        +                        game.compendiumBrowser.settings.allowFeatBrowser = value;
        +                        break;
        +                    case 'allow-item-browser':
        +                        game.compendiumBrowser.settings.allowItemBrowser = value;
        +                        break;
        +                    case 'allow-actor-browser':
        +                        game.compendiumBrowser.settings.allowActorBrowser = value;
        +                        break;
        +                    case 'allow-rolltable-browser':
        +                        game.compendiumBrowser.settings.allowRollTableBrowser = value;
        +                        break;
        +                    case 'allow-journalentry-browser':
        +                        game.compendiumBrowser.settings.allowJournalEntryBrowser = value;
        +                        break;
        +                    default:
        +                        break;
        +                }
        +                ModuleSettings.saveSettings();
        +                game.compendiumBrowser.render();
        +            });
        +        });
        +        // select filters
        +        app.querySelectorAll('.filter[data-type=text] input, .filter[data-type=text] select').forEach(async (el) => {
        +            el.addEventListener('keyup change paste', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), entityType = app.querySelector('.content .tab.active').dataset.tab;
        +                if (e.target.value === '' || e.target.value === undefined) {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                }
        +                else {
        +                    game.compendiumBrowser.filters[entityType].activeFilters[key] = {
        +                        path: path,
        +                        type: 'text',
        +                        valIsArray: false,
        +                        value: e.target.value
        +                    };
        +                }
        +                game.compendiumBrowser.replaceList(html, entityType);
        +            });
        +        });
        +        // select filters
        +        app.querySelectorAll('.filter[data-type=select] select, .filter[data-type=bool] select').forEach(async (el) => {
        +            el.addEventListener('change', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), filterType = e.target.closest('.filter').dataset.type, entityType = app.querySelector('.content .tab.active').dataset.tab;
        +                let valIsArray = e.target.closest('.filter').dataset.valIsArray === 'true', value = e.target.options[e.target.selectedIndex].value;
        +                if (value === "null") {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                }
        +                else {
        +                    game.compendiumBrowser.filters[entityType].activeFilters[key] = {
        +                        path: path,
        +                        type: filterType,
        +                        valIsArray: valIsArray,
        +                        value: value
        +                    };
        +                }
        +                game.compendiumBrowser.replaceList(html, entityType);
        +            });
        +        });
        +        // multiselect
        +        app.querySelectorAll('.filter[data-type=multiSelect] input').forEach(async (el) => {
        +            el.addEventListener('change', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), filterType = 'multiSelect', entityType = app.querySelector('.content .tab.active').dataset.tab, filter = game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                let valIsArray = e.target.closest('.filter').dataset.valIsArray, value = e.target.dataset.value;
        +                if (e.target.checked === true) {
        +                    if (filter === undefined) {
        +                        game.compendiumBrowser.filters[entityType].activeFilters[key] =
        +                            { path: path, type: filterType, valIsArray: valIsArray, values: [value] };
        +                    }
        +                    else {
        +                        game.compendiumBrowser.filters[entityType].activeFilters[key].values.push(value);
        +                    }
        +                }
        +                else {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key].values.splice(game.compendiumBrowser.filters[entityType].activeFilters[key].values.indexOf(value), 1);
        +                    if (game.compendiumBrowser.filters[entityType].activeFilters[key].values.length === 0)
        +                        delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                }
        +                game.compendiumBrowser.replaceList(html, entityType);
        +            });
        +        });
        +        app.querySelectorAll('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').forEach(async (el) => {
        +            el.addEventListener('change keyup paste', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path, key = path.replace(/\./g, ''), filterType = 'numberCompare', entityType = app.querySelector('.content .tab.active').dataset.tab, operator = e.target.closest('.filter').getElementsByTagName('select').val, value = e.target.closest('.filter').getElementsByTagName('input').val;
        +                if (value === '' || operator === 'null') {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                }
        +                else {
        +                    game.compendiumBrowser.filters[entityType].activeFilters[key] = {
        +                        path: path,
        +                        type: filterType,
        +                        valIsArray: false,
        +                        operator: operator,
        +                        value: value
        +                    };
        +                }
        +                game.compendiumBrowser.replaceList(html, browserTab);
        +            });
        +        });
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_hooks_moduleHooks.js.html b/docs/dist_hooks_moduleHooks.js.html new file mode 100644 index 0000000..6a5a1b4 --- /dev/null +++ b/docs/dist_hooks_moduleHooks.js.html @@ -0,0 +1,140 @@ + + + + + JSDoc: Source: dist/hooks/moduleHooks.js + + + + + + + + + + +
        + +

        Source: dist/hooks/moduleHooks.js

        + + + + + + +
        +
        +
        /**
        + * @fileOverview Holding the modules Hooks
        + *
        + * @author Daniel Böttner <jackprince1983@gmail.com>
        + * @version 1.0.0
        + */
        +/* jshint node: true */
        +'use strict';
        +import { CMPBrowser } from '../scripts/modules/settings.js';
        +import { CompendiumBrowser } from '../scripts/compendium-browser.js';
        +import { VersionCheck } from '../scripts/versioning/version-check.js';
        +import { Renderer } from '../scripts/modules/renderer.js';
        +/**
        + * Holds the static methods to be called
        + */
        +export default class moduleHooks {
        +    static onInit() {
        +        Hooks.on('init', async () => {
        +            Handlebars.registerHelper('switch', function (value, options) {
        +                this.switch_value = value;
        +                this.switch_break = false;
        +                return options.fn(this);
        +            });
        +            Handlebars.registerHelper('case', function (value, options) {
        +                if (value == this.switch_value) {
        +                    this.switch_break = true;
        +                    return options.fn(this);
        +                }
        +            });
        +            Handlebars.registerHelper('default', function (value, options) {
        +                if (this.switch_break == false) {
        +                    return value;
        +                }
        +            });
        +            Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
        +                switch (operator) {
        +                    case '==':
        +                        return (v1 == v2) ? options.fn(this) : options.inverse(this);
        +                    case '===':
        +                        return (v1 === v2) ? options.fn(this) : options.inverse(this);
        +                    case '!=':
        +                        return (v1 != v2) ? options.fn(this) : options.inverse(this);
        +                    case '!==':
        +                        return (v1 !== v2) ? options.fn(this) : options.inverse(this);
        +                    case '<':
        +                        return (v1 < v2) ? options.fn(this) : options.inverse(this);
        +                    case '<=':
        +                        return (v1 <= v2) ? options.fn(this) : options.inverse(this);
        +                    case '>':
        +                        return (v1 > v2) ? options.fn(this) : options.inverse(this);
        +                    case '>=':
        +                        return (v1 >= v2) ? options.fn(this) : options.inverse(this);
        +                    case '&&':
        +                        return (v1 && v2) ? options.fn(this) : options.inverse(this);
        +                    case '||':
        +                        return (v1 || v2) ? options.fn(this) : options.inverse(this);
        +                    default:
        +                        return options.inverse(this);
        +                }
        +            });
        +            Handlebars.registerHelper("debug", function (optionalValue) {
        +                console.log("Current Context");
        +                console.log("====================");
        +                console.log(this);
        +                if (optionalValue) {
        +                    console.log("Value");
        +                    console.log("====================");
        +                    console.log(optionalValue);
        +                }
        +            });
        +        });
        +    }
        +    static onReady() {
        +        Hooks.on('ready', async () => {
        +            if (game.compendiumBrowser === undefined) {
        +                game.compendiumBrowser = new CompendiumBrowser();
        +                //0.4.0 Defer loading content until we actually use the Compendium Browser
        +                //A compromise approach would be better (periodic loading) except would still create the memory use problem
        +                await game.compendiumBrowser.initialize();
        +            }
        +            if (VersionCheck.check(CMPBrowser.MODULE_NAME) && game.user.isGM) {
        +                console.log('version check');
        +            }
        +        });
        +    }
        +    static onRenderComplete() {
        +        Hooks.on("renderCompendiumBrowser", Renderer.afterRender);
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_classes_compactList.js.html b/docs/dist_scripts_classes_compactList.js.html new file mode 100644 index 0000000..bb450d7 --- /dev/null +++ b/docs/dist_scripts_classes_compactList.js.html @@ -0,0 +1,94 @@ + + + + + JSDoc: Source: dist/scripts/classes/compactList.js + + + + + + + + + + +
        + +

        Source: dist/scripts/classes/compactList.js

        + + + + + + +
        +
        +
        export class compactList {
        +    constructor() {
        +        this.entities = [];
        +        this.activeFilters = '';
        +    }
        +    /**
        +     *
        +     * @returns {number}
        +     */
        +    size() {
        +        return this.entities.length;
        +    }
        +    /**
        +     *
        +     * @param {compactEntity} entity
        +     * @returns {void}
        +     */
        +    addEntity(entity) {
        +        this.entities.push(entity);
        +    }
        +    /**
        +     *
        +     * @param {*} index
        +     * @returns {compactEntity}
        +     */
        +    getEntity(index) {
        +        return this.entities[index];
        +    }
        +    /**
        +     *
        +     * @param {string} filters
        +     */
        +    addActiveFilters(filters) {
        +        this.activeFilters = filters;
        +    }
        +    /**
        +     *
        +     * @returns {boolean}
        +     */
        +    hasFilters() {
        +        return this.activeFilters.length != 0;
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_classes_decoratedEntity.js.html b/docs/dist_scripts_classes_decoratedEntity.js.html new file mode 100644 index 0000000..e001138 --- /dev/null +++ b/docs/dist_scripts_classes_decoratedEntity.js.html @@ -0,0 +1,215 @@ + + + + + JSDoc: Source: dist/scripts/classes/decoratedEntity.js + + + + + + + + + + +
        + +

        Source: dist/scripts/classes/decoratedEntity.js

        + + + + + + +
        +
        +
        import { compactEntity } from "./compactEntity.js";
        +export class decoratedEntity extends compactEntity {
        +    constructor() {
        +        super();
        +        this.damageDealt = [];
        +        this.damageTypes = [];
        +        this.matchedPacks = [];
        +        this.matchedPacksString = '';
        +        this.classes = [];
        +        this.hasSpells = false;
        +    }
        +    /**
        +    *
        +    * @param {Item|Actor5e|JournalEntry|RollTable} entityData
        +    * @param {string} entityType
        +    * @param {Compendium} packList
        +    * @param {object} classList
        +    * @param {object} subClasses
        +    *
        +    * @returns {object} decoratedItem
        +    */
        +    static decorate(entity, entityType, packList = null, classList = null, subClasses = null) {
        +        let decorated = new decoratedEntity(), entityData = entity.data;
        +        decorated._id = entity.id;
        +        decorated.name = entity.name || entityData.name;
        +        decorated.img = entity.img;
        +        decorated.type = entity.type || '';
        +        decorated.data.details = {};
        +        decorated.flags = entityData.flags;
        +        // getting pack(s)
        +        let matchedPacks = [];
        +        for (let pack in packList) {
        +            for (let packItem of packList[pack]) {
        +                if (entity.name.toLowerCase() === packItem.toLowerCase()) {
        +                    matchedPacks.push(pack);
        +                    break;
        +                }
        +            }
        +        }
        +        decorated.matchedPacks = matchedPacks;
        +        decorated.matchedPacksString = matchedPacks.join(', ');
        +        // handle common stuff
        +        switch (entityType) {
        +            case 'Feat':
        +            case 'Item':
        +            case 'Spell':
        +                // getting damage types (common to all Items, although some won't have any)
        +                decorated.damageTypes = [];
        +                if (entityData.damage && entityData.damage.parts.length > 0) {
        +                    for (let part of entityData.damage.parts) {
        +                        let type = part[1];
        +                        if (decorated.damageTypes.indexOf(type) === -1) {
        +                            decorated.damageTypes.push(type);
        +                        }
        +                    }
        +                }
        +                // getting uses/ressources status
        +                decorated.usesRessources = entityData.hasLimitedUses;
        +                decorated.hasSave = entityData.hasSave;
        +        }
        +        // handle inidividual stuff
        +        switch (entityType) {
        +            // no break is intentional, specific entitytype cases are handled below
        +            case 'Actor':
        +                // challengeRating display
        +                let challengeRating = () => {
        +                    let cr = Number(entityData.data.details.cr) || 0;
        +                    cr = (cr > 0 && cr < 1) ? "1/" + (1 / cr) : cr;
        +                    return cr;
        +                };
        +                decorated.displayCR = challengeRating();
        +                decorated.orderCR = Number(entityData.data.details?.cr) || 0;
        +                decorated.displaySize = 'unset';
        +                decorated.filterSize = 2;
        +                if (entityData.data.details?.type instanceof String) {
        +                    let temp = entityData.data.details.type;
        +                    decorated.data.details.type = { value: temp };
        +                }
        +                setProperty(decorated, 'data.details.type', entityData.data.details.type);
        +                if (CONFIG.DND5E.actorSizes[entityData.data.traits.size] !== undefined) {
        +                    entityData.displaySize = CONFIG.DND5E.actorSizes[entityData.data.traits.size];
        +                }
        +                switch (entityData.data.traits.size) {
        +                    case 'grg':
        +                        decorated.filterSize = 5;
        +                        break;
        +                    case 'huge':
        +                        decorated.filterSize = 4;
        +                        break;
        +                    case 'lg':
        +                        decorated.filterSize = 3;
        +                        break;
        +                    case 'sm':
        +                        decorated.filterSize = 1;
        +                        break;
        +                    case 'tiny':
        +                        decorated.filterSize = 0;
        +                        break;
        +                    case 'med':
        +                    default:
        +                        decorated.filterSize = 2;
        +                        break;
        +                }
        +                // getting value for HasSpells and damage types
        +                decorated.hasSpells = false;
        +                for (let item of entityData.items) {
        +                    if (item.type == 'spell') {
        +                        decorated.hasSpells = true;
        +                    }
        +                    if (item.data.damage && item.data.damage.parts && item.data.damage.parts.length > 0) {
        +                        for (let part of item.data.damage.parts) {
        +                            let type = part[1];
        +                            if (decorated.damageDealt.indexOf(type) === -1) {
        +                                decorated.damageDealt.push(type);
        +                            }
        +                        }
        +                    }
        +                }
        +                break;
        +            case 'Feat':
        +                let reqString = entityData.requirements?.replace(/[0-9]/g, '').trim();
        +                let matchedClass = [];
        +                for (let subClass in subClasses) {
        +                    if (reqString && reqString.toLowerCase().indexOf(subClass) !== -1) {
        +                        matchedClass.push(subClass);
        +                    }
        +                    else {
        +                        for (let sub of subClasses[subClass]) {
        +                            if (reqString && reqString.indexOf(sub) !== -1) {
        +                                matchedClass.push(sub);
        +                                break;
        +                            }
        +                        }
        +                    }
        +                }
        +                decorated.classRequirement = matchedClass;
        +                decorated.classRequirementString = matchedClass.join(', ');
        +                // getting uses/ressources status
        +                decorated.usesRessources = entityData.hasLimitedUses;
        +                decorated.hasSave = entityData.hasSave;
        +                break;
        +            case 'Item':
        +                decorated.data.rarity = entityData.data.rarity.toLowerCase().replace(/ /g, '') || 'common';
        +                break;
        +            case 'RollTable':
        +                decorated.data.displayRoll = entityData.displayRoll;
        +                decorated.data.formula = entityData.formula;
        +                decorated.type = 'RollTable';
        +                decorated.data.details.type = entityData.flags?.['better-rolltables']?.['table-type'] || 'none';
        +                break;
        +            case 'Spell':
        +                decorated.data.components = entityData.data.components;
        +                decorated.data.level = entityData.data.level;
        +                // determining classes that can use the spell
        +                let cleanSpellName = entity.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, '');
        +                if (classList && classList[cleanSpellName]) {
        +                    let classes = classList[cleanSpellName];
        +                    decorated.classRequirement = classes.split(',');
        +                }
        +                break;
        +        }
        +        return decorated;
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_classes_filter.js.html b/docs/dist_scripts_classes_filter.js.html new file mode 100644 index 0000000..dab4a9d --- /dev/null +++ b/docs/dist_scripts_classes_filter.js.html @@ -0,0 +1,95 @@ + + + + + JSDoc: Source: dist/scripts/classes/filter.js + + + + + + + + + + +
        + +

        Source: dist/scripts/classes/filter.js

        + + + + + + +
        +
        +
        var FilterTypes;
        +(function (FilterTypes) {
        +    FilterTypes[FilterTypes["text"] = 0] = "text";
        +    FilterTypes[FilterTypes["bool"] = 1] = "bool";
        +    FilterTypes[FilterTypes["select"] = 2] = "select";
        +    FilterTypes[FilterTypes["multiSelect"] = 3] = "multiSelect";
        +    FilterTypes[FilterTypes["numberCompare"] = 4] = "numberCompare";
        +})(FilterTypes || (FilterTypes = {}));
        +/**
        + * Used to add custom filters to the Spell-Browser
        + * @param {string} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value
        + * @param {string} label - Title of the filter
        + * @param {string} type - type of filter
        + *                      possible types:
        + *                          text:           will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching
        + *                          bool:           will see if the data at the path exists and not false.
        + *                          select:         exactly matches the data with the chosen selector from possibleValues
        + *                          multiSelect:    enables selecting multiple values from possibleValues, any of witch has to match the objects data
        + *                          numberCompare:  gives the option to compare numerical values, either with =, < or the > operator
        + * @param {null|boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters
        + * @param {boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden
        + */
        +export class Filter {
        +    constructor(path, label, type, possibleValues = null, valIsArray = false) {
        +        this.path = '';
        +        this.label = '';
        +        this.type = '';
        +        this.valIsArray = false;
        +        this.possibleValues = null;
        +        this.is_text = false;
        +        this.is_bool = false;
        +        this.is_select = false;
        +        this.is_multiSelect = false;
        +        this.is_numberCompare = false;
        +        this.path = path;
        +        this.label = label;
        +        if (type in FilterTypes) {
        +            this.type = type;
        +            setProperty(this, `is_${type}`, true);
        +        }
        +        this.possibleValues = possibleValues;
        +        this.valIsArray = valIsArray;
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_compendium-browser.js.html b/docs/dist_scripts_compendium-browser.js.html new file mode 100644 index 0000000..6bc8283 --- /dev/null +++ b/docs/dist_scripts_compendium-browser.js.html @@ -0,0 +1,240 @@ + + + + + JSDoc: Source: dist/scripts/compendium-browser.js + + + + + + + + + + +
        + +

        Source: dist/scripts/compendium-browser.js

        + + + + + + +
        +
        +
        import { ModuleSettings, CMPBrowser } from './modules/settings.js';
        +import { Entities } from './modules/entities.js';
        +import { Events } from '../hooks/events.js';
        +import { Filter } from './modules/filter.js';
        +import { Renderer } from './modules/renderer.js';
        +//import Exporter from './modules/exporter.mjs';
        +/* eslint-disable valid-jsdoc */
        +/* eslint-disable complexity */
        +export class CompendiumBrowser extends Application {
        +    constructor() {
        +        super();
        +        ModuleSettings.registerGameSettings();
        +        this.settings = ModuleSettings.initModuleSettings();
        +        this.filters = new Filter();
        +        this.currentLists = {};
        +    }
        +    /**
        +     *
        +     */
        +    static get defaultOptions() {
        +        const options = super.defaultOptions;
        +        mergeObject(options, {
        +            title: "CMPBrowser.compendiumBrowser",
        +            tabs: [{ navSelector: ".tabs", contentSelector: ".content", initial: "Item" }],
        +            classes: options.classes.concat('compendium-browser'),
        +            template: "modules/compendium-browser/template/template.hbs",
        +            width: 900,
        +            height: 800,
        +            resizable: true,
        +            minimizable: true
        +        });
        +        return options;
        +    }
        +    async initialize() {
        +        // load settings
        +        if (this.settings === undefined) {
        +            this.settings = ModuleSettings.initModuleSettings();
        +        }
        +        Renderer.loadTemplates([
        +            "modules/compendium-browser/template/template.hbs",
        +            "modules/compendium-browser/template/entity-browser.hbs",
        +            "modules/compendium-browser/template/entity-list.hbs",
        +            "modules/compendium-browser/template/filter-container.hbs",
        +            "modules/compendium-browser/template/settings.hbs",
        +            "modules/compendium-browser/template/loading.hbs"
        +        ]);
        +        this.filters.addEntityFilters();
        +        this.hookCompendiumList();
        +    }
        +    /** override */
        +    _onChangeTab(event, tabs, active) {
        +        super._onChangeTab(event, tabs, active);
        +        const html = this.element;
        +        if (active != 'setting') {
        +            Events.reloadFilters(html, active);
        +            this.replaceList(html, active, { reload: false });
        +        }
        +    }
        +    /**
        +     *
        +     * @returns {Obejct} data
        +     */
        +    async getData() {
        +        let data = {
        +            items: [],
        +            actors: [],
        +            filters: {
        +                Spell: this.filters.getByName('Spell'),
        +                Item: this.filters.getByName('Item'),
        +                Actor: this.filters.getByName('Actor'),
        +                RollTable: this.filters.getByName('RollTable'),
        +                JournalEntry: this.filters.getByName('RollTable')
        +            },
        +            showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser,
        +            showFeatBrowser: (game.user.isGM) || this.settings.allowFeatBrowser,
        +            showItemBrowser: (game.user.isGM) || this.settings.allowItemBrowser,
        +            showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser,
        +            showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser,
        +            showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser,
        +            settings: this.settings,
        +            isGM: game.user.isGM
        +        };
        +        return data;
        +    }
        +    /** override */
        +    activateListeners(html) {
        +        super.activateListeners(html);
        +        this.observer = new IntersectionObserver((entries, observer) => {
        +            for (let [i, e] of entries.entries()) {
        +                if (!e.isIntersecting)
        +                    break;
        +                const el = e.target;
        +                // Avatar image
        +                //const img = li.querySelector("img");
        +                if (el && el.dataset.src) {
        +                    el.style['background-image'] = `url(${el.dataset.src})`;
        +                    delete el.dataset.src;
        +                }
        +                // No longer observe the target
        +                observer.unobserve(e.target);
        +            }
        +        });
        +        Events.activateActionListener(html);
        +        Events.activateItemListListeners(html);
        +        Events.activateFilterListeners(html);
        +        //Just for the loading image
        +        if (this.observer) {
        +            html.find(".entity-image").each((i, imageElement) => this.observer.observe(imageElement));
        +        }
        +    }
        +    hookCompendiumList() {
        +        Hooks.on('renderCompendiumDirectory', (app, html, data) => {
        +            this.hookCompendiumList();
        +        });
        +        let html = $('#compendium');
        +        if (this.settings === undefined) {
        +            this.initSettings();
        +        }
        +        if (game.user.isGM || this.settings.allowItemBrowser || this.settings.allowSpellBrowser || this.settings.allowActorBrowser) {
        +            const cbButton = $(`<button class="compendium-browser-btn"><i class="fas fa-fire"></i> ${game.i18n.localize("CMPBrowser.compendiumBrowser")}</button>`);
        +            html.find('.compendium-browser-btn').remove();
        +            // adding to directory-list since the footer doesn't exist if the user is not gm
        +            html.find('.directory-footer').append(cbButton);
        +            // Handle button clicks
        +            cbButton.click(ev => {
        +                ev.preventDefault();
        +                //0.4.1: Reset filters when you click button
        +                this.filters.resetFilters();
        +                //0.4.3: Reset everything (including data) when you press the button - calls afterRender() hook
        +                if (game.user.isGM || this.settings.allowSpellBrowser) {
        +                    this.refreshList = "Spell";
        +                }
        +                else if (this.settings.allowFeatBrowser) {
        +                    this.refreshList = "Feat";
        +                }
        +                else if (this.settings.allowItemBrowser) {
        +                    this.refreshList = "Item";
        +                }
        +                else if (this.settings.allowActorBrowser) {
        +                    this.refreshList = "Actor";
        +                }
        +                else if (this.settings.allowJournalEntryBrowser) {
        +                    this.refreshList = "JournalEntry";
        +                }
        +                else if (this.settings.allowRollTableBrowser) {
        +                    this.refreshList = "RollTable";
        +                }
        +                this.render(true);
        +            });
        +        }
        +    }
        +    /**
        +     *
        +     * @param {*} html
        +     * @param {*} entityType
        +     * @param {*} options
        +     */
        +    async replaceList(html, entityType, options = { reload: true }) {
        +        //After rendering the first time or re-rendering trigger the load/reload of visible data
        +        let entityListElement = document.querySelector('.tab.active .browser .cb_entities');
        +        if (entityListElement.childElementCount !== undefined) {
        +            //0.4.2b: On a tab-switch, only reload if there isn't any data already 
        +            if (options?.reload || entityListElement.childElementCount < 1) {
        +                const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD;
        +                await Renderer.updateLoading(entityType, 0, maxLoad);
        +                // loadItems
        +                const entityHelper = new Entities();
        +                let entityList = await entityHelper.loadAndFilter(entityType, true);
        +                this.currentLists[entityType] = entityList = Entities._sortList(entityList, entityType);
        +                //Uses loadAndFilterItems to read compendia for items which pass the current filters and render on this tab
        +                const newEntitiesHTML = await Renderer.renderEntityList(entityList.entities, entityType, true);
        +                entityListElement.setAttribute('data-active-filters', entityList.activeFilters);
        +                entityListElement.innerHTML = newEntitiesHTML;
        +                await Events.observeListElement(entityListElement, 'aside');
        +                //Reactivate listeners for clicking and dragging
        +                Events.activateItemListListeners(html);
        +            }
        +        }
        +    }
        +    clearObject(obj) {
        +        let newObj = {};
        +        for (let key in obj) {
        +            if (obj[key] == true) {
        +                newObj[key] = true;
        +            }
        +        }
        +        return newObj;
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_modules_entities.js.html b/docs/dist_scripts_modules_entities.js.html new file mode 100644 index 0000000..95551cb --- /dev/null +++ b/docs/dist_scripts_modules_entities.js.html @@ -0,0 +1,232 @@ + + + + + JSDoc: Source: dist/scripts/modules/entities.js + + + + + + + + + + +
        + +

        Source: dist/scripts/modules/entities.js

        + + + + + + +
        +
        +
        import { Filter } from './filter.js';
        +import { Renderer } from './renderer.js';
        +import { CMPBrowser } from './settings.js';
        +import { compactEntity } from '../classes/compactEntity.js';
        +import { decoratedEntity } from '../classes/decoratedEntity.js';
        +import { compactList } from '../classes/compactList.js';
        +export class Entities {
        +    constructor() {
        +        this.packList = null;
        +        this.classList = null;
        +        this.subClassList = null;
        +        this.itemsLoaded = false;
        +    }
        +    /**
        +     *
        +     * @param {string} entityType
        +     * @param {any} updateLoading
        +     *
        +     * @returns {Promise<compactList>}
        +     */
        +    async loadAndFilter(entityType = "Item", updateLoading = null) {
        +        console.log(`Load and Filter Items | Started loading ${entityType}s`);
        +        console.time("loadAndFilterItems");
        +        await this.checkListsLoaded();
        +        const Category = (entityType === 'Spell' || entityType === 'Feat') ? 'Item' : entityType;
        +        const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD;
        +        const ActiveFilters = game.compendiumBrowser.filters.getByName(entityType).activeFilters;
        +        const packs = game.packs.filter((pack) => pack.metadata.entity === Category);
        +        //0.4.1: Load and filter just one of spells, feats, and items (specified by browserTab)
        +        let unfoundSpells = '';
        +        let numItemsLoaded = 0, numPacks = packs.length, comp_list = new compactList();
        +        comp_list.activeFilters = JSON.stringify(ActiveFilters);
        +        //Filter the full list, but only save the core compendium information + displayed info 
        +        for (let pack of packs) {
        +            if (game.compendiumBrowser.settings.loadedCompendium[pack.metadata.entity][pack.collection].load) {
        +                //FIXME: How much could we do with the loaded index rather than all content? 
        +                //OR filter the content up front for the decoratedItem.type??               
        +                await pack.getDocuments().then((content) => {
        +                    for (let currentEntity of content) {
        +                        // fail fast
        +                        if (currentEntity.data.type != entityType.toLowerCase() && entityType != Category)
        +                            continue;
        +                        if (!Filter.passesFilter(currentEntity, ActiveFilters))
        +                            continue;
        +                        let decorated = decoratedEntity.decorate(currentEntity, entityType, this.packList, this.classList, this.subClassList);
        +                        let compact = new compactEntity();
        +                        // set common properties for all entities
        +                        compact._id = decorated._id;
        +                        compact.name = decorated.name;
        +                        compact.compendium = pack.collection;
        +                        compact.img = decorated.img;
        +                        compact.type = decorated.type;
        +                        compact.data = decorated.data;
        +                        compact.flags = decorated.flags;
        +                        if (ActiveFilters) {
        +                            for (let index in ActiveFilters) {
        +                                setProperty(compact, `tags.${ActiveFilters[index].path}`, ActiveFilters[index].value);
        +                            }
        +                        }
        +                        if (Category == 'Item') {
        +                            compact.dae = (decorated.effects?.size) || false;
        +                        }
        +                        switch (entityType) {
        +                            case "Spell":
        +                            case "Feat":
        +                                if (entityType === 'Spell' || ["feat", "class"].includes(decorated.type)) {
        +                                    compact.classRequirement = decorated.classRequirement;
        +                                }
        +                                break;
        +                            case "Item":
        +                                //0.4.5: Itm type for true items could be many things (weapon, consumable, etc) so we just look for everything except spells, feats, classes
        +                                if (!["spell", "feat", "class"].includes(decorated.type)) {
        +                                    compact.rarity = decorated.data.rarity;
        +                                    compact.ac = (decorated.data?.armor?.type) ? decorated.data?.armor?.value || false : false;
        +                                    if (compact.type == 'weapon' && (decorated.data?.range?.value)) {
        +                                        setProperty(compact, `tags.range`, decorated.data?.range?.value + decorated.data?.range?.units);
        +                                    }
        +                                }
        +                                break;
        +                            case "Actor":
        +                                compact.displayCR = decorated.displayCR;
        +                                compact.displaySize = decorated.displaySize;
        +                                compact.displayType = decorated.data?.details?.type;
        +                                compact.orderCR = decorated.orderCR;
        +                                compact.orderSize = decorated.filterSize;
        +                                compact.data.details = decorated.data.details;
        +                                break;
        +                            default:
        +                                break;
        +                        }
        +                        comp_list.addEntity(compact);
        +                        if (updateLoading) {
        +                            Renderer.updateLoading(entityType, numItemsLoaded, numPacks, 500);
        +                        }
        +                        if (numItemsLoaded++ >= maxLoad)
        +                            break;
        +                    }
        +                }); // get Entities
        +            }
        +            if (numItemsLoaded >= maxLoad)
        +                break;
        +        } //for packs
        +        console.timeEnd("loadAndFilterItems");
        +        console.log(`Load and Filter Items | Finished loading ${comp_list.size()} ${entityType}s`);
        +        return comp_list;
        +    }
        +    /**
        +     *
        +     * @param {compactList} list
        +     * @param {string} entityType
        +     * @param {string} orderBy
        +     * @returns {compactList}
        +     */
        +    static _sortList(list, entityType, orderBy) {
        +        const SortCollator = new Intl.Collator(game.i18n.lang, { numeric: true, sensitivity: 'base' });
        +        if (entityType === 'Actor') {
        +            switch (orderBy) {
        +                case 'name':
        +                    list.entities.sort((left, right) => SortCollator.compare(left.name, right.name));
        +                    break;
        +                case 'cr':
        +                    list.entities.sort((left, right) => {
        +                        return left.displayCR - right.displayCR || SortCollator.compare(left.name, right.name);
        +                    });
        +                    break;
        +                case 'size':
        +                    list.entities.sort((left, right) => {
        +                        return left.orderSize - right.orderSize || SortCollator.compare(left.name, right.name);
        +                    });
        +                    break;
        +            }
        +        }
        +        else {
        +            if (orderBy) {
        +                list.entities.sort((left, right) => left.name.localeCompare(right.name));
        +            }
        +            else {
        +                let defaultSort = new Map([
        +                    ['Item', 'type'],
        +                    ['Spell', 'data.level'],
        +                    ['Feats', 'data.class'],
        +                    ['RollTable', 'compendium'],
        +                    ['JounralEntry', 'name'],
        +                ]);
        +                list.entities.sort((left, right) => {
        +                    let sort = defaultSort.get(entityType) || '', result = SortCollator.compare(getProperty(left, sort), getProperty(right, sort));
        +                    return result || SortCollator.compare(left.name, right.name);
        +                });
        +            }
        +        }
        +        return list;
        +    }
        +    /**
        +     * Check all the prepared list with extra info are loaded
        +     */
        +    async checkListsLoaded() {
        +        const dataPath = '/modules/compendium-browser/data/';
        +        //Provides extra info not in the standard SRD, like which classes can learn a spell
        +        if (!this.classList) {
        +            this.classList = await fetch(dataPath + 'spell-classes.json').then(result => {
        +                return result.json();
        +            }).then(list => {
        +                return list;
        +            });
        +        }
        +        if (!this.packList) {
        +            this.packList = await fetch(dataPath + 'item-packs.json').then(result => {
        +                return result.json();
        +            }).then(list => {
        +                return list;
        +            });
        +        }
        +        if (!this.subClassList) {
        +            this.subClassList = await fetch(dataPath + 'sub-classes.json').then(result => {
        +                return result.json();
        +            }).then(list => {
        +                return list;
        +            });
        +        }
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_modules_exporter.js.html b/docs/dist_scripts_modules_exporter.js.html new file mode 100644 index 0000000..19461aa --- /dev/null +++ b/docs/dist_scripts_modules_exporter.js.html @@ -0,0 +1,85 @@ + + + + + JSDoc: Source: dist/scripts/modules/exporter.js + + + + + + + + + + +
        + +

        Source: dist/scripts/modules/exporter.js

        + + + + + + +
        +
        +
        export class Exporter {
        +    /**
        +     * Create a new RollTable from a given Set of entitites.
        +     *
        +     * @param {string} tableName the name of the table entity that will be created
        +     * @param {Array<any>} entities a set of
        +     * @param {function(Entity)} weightPredicate a function that returns a weight (number) that will be used
        +     * for the tableResult weight for that given entity. returning 0 will exclude the entity from appearing in the table
        +     */
        +    static async createTableFromSelection(tableName = new Date().valueOf().toString(), entities, weightPredicate = null, options = null) {
        +        let data = { name: tableName }, tableArray = [];
        +        if (!entities || !(entities.length > 0))
        +            return false;
        +        const newTable = await RollTable.create(data);
        +        ui.notifications.info(`Starting generation of a rolltable with ${entities.length} entries.`);
        +        for (let entity of entities) {
        +            let weight = weightPredicate != null ? weightPredicate(entity) : 1;
        +            if (weight <= 0)
        +                continue;
        +            let tableRowData = {};
        +            tableRowData.type = 2;
        +            tableRowData.collection = entity.entryCompendium;
        +            tableRowData.text = entity.entryName;
        +            tableRowData.img = entity.entryImage;
        +            tableRowData.weight = weight;
        +            tableRowData.range = [1, 1];
        +            tableArray.push(tableRowData);
        +        }
        +        await newTable.createEmbeddedDocuments('TableResult', tableArray);
        +        await newTable.normalize();
        +        ui.notifications.info(`Rolltable ${tableName} with ${tableArray.length} entries was generated.`);
        +        return true;
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_modules_filter.js.html b/docs/dist_scripts_modules_filter.js.html new file mode 100644 index 0000000..67be102 --- /dev/null +++ b/docs/dist_scripts_modules_filter.js.html @@ -0,0 +1,355 @@ + + + + + JSDoc: Source: dist/scripts/modules/filter.js + + + + + + + + + + +
        + +

        Source: dist/scripts/modules/filter.js

        + + + + + + +
        +
        +
        import { Filter as FilterEntity } from "../classes/filter.js";
        +export class Filter {
        +    constructor() {
        +        this.Spell = this._getInitialFilters();
        +        this.Actor = this._getInitialFilters();
        +        this.Feat = this._getInitialFilters();
        +        this.Item = this._getInitialFilters();
        +        this.RollTable = this._getInitialFilters();
        +        this.JournalEntry = this._getInitialFilters();
        +    }
        +    /**
        +     *
        +     * @param {any} subject
        +     * @param {any} filters
        +     *
        +     * @returns {boolean}
        +     */
        +    static passesFilter(subject, filters) {
        +        for (let filter of Object.values(filters)) {
        +            let prop = getProperty(subject, `data.${filter.path}`) || getProperty(subject, filter.path);
        +            if (prop === undefined)
        +                return false;
        +            if (filter.type === 'numberCompare') {
        +                switch (filter.operator) {
        +                    case '=':
        +                        if (prop != filter.value) {
        +                            return false;
        +                        }
        +                        break;
        +                    case '<':
        +                        if (prop >= filter.value) {
        +                            return false;
        +                        }
        +                        break;
        +                    case '>':
        +                        if (prop <= filter.value) {
        +                            return false;
        +                        }
        +                        break;
        +                }
        +                continue;
        +            }
        +            if (filter.valIsArray === false) {
        +                if (filter.type === 'text') {
        +                    if (prop.toLowerCase().indexOf(filter.value.toLowerCase()) === -1) {
        +                        return false;
        +                    }
        +                }
        +                else {
        +                    if (filter.value !== undefined && prop !== undefined && prop != filter.value && !(filter.value === true && prop)) {
        +                        return false;
        +                    }
        +                    if (filter.values && filter.values.indexOf(prop) === -1) {
        +                        return false;
        +                    }
        +                }
        +            }
        +            else {
        +                if (prop === undefined)
        +                    return false;
        +                if (typeof prop === 'object') {
        +                    if (filter.value) {
        +                        if (prop.indexOf(filter.value) === -1) {
        +                            return false;
        +                        }
        +                    }
        +                    else if (filter.values) {
        +                        for (let val of filter.values) {
        +                            if (prop.indexOf(val) !== -1) {
        +                                continue;
        +                            }
        +                            return false;
        +                        }
        +                    }
        +                }
        +                else {
        +                    for (let val of filter.values) {
        +                        if (prop === val) {
        +                            continue;
        +                        }
        +                    }
        +                    return false;
        +                }
        +            }
        +        }
        +        return true;
        +    }
        +    /**
        +     *
        +     * @param name
        +     * @returns any
        +     */
        +    getByName(name) {
        +        return getProperty(this, name);
        +    }
        +    /**
        +     *
        +     * @param {string} entityType
        +     * @returns {Iterable|undefined}
        +     */
        +    getByEntityType(entityType) {
        +        return getProperty(this, entityType);
        +    }
        +    resetFilters() {
        +        this.Spell.activeFilters = {};
        +        this.Feat.activeFilters = {};
        +        this.Item.activeFilters = {};
        +        this.Actor.activeFilters = {};
        +        this.RollTable.activeFilters = {};
        +        this.JournalEntry.activeFilters = {};
        +    }
        +    /**
        +     *
        +     * @returns {Object}
        +     */
        +    _getInitialFilters() {
        +        return {
        +            registeredFilterCategorys: {},
        +            activeFilters: {}
        +        };
        +    }
        +    /**
        +     * add entityfilters
        +     */
        +    async addEntityFilters() {
        +        await this.addSpellFilters();
        +        await this.addFeatFilters();
        +        await this.addItemFilters();
        +        await this.addActorFilters();
        +        await this.addRollTableFilters();
        +    }
        +    /**
        +     * Used to add custom filters to the Spell-Browser
        +     * @param {string} entityType type of entity for the filter
        +     * @param {string} category - Title of the category
        +     * @param {string} label - Title of the filter
        +     * @param {string} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value
        +     * @param {string} type - type of filter
        +     *                      possible filter:
        +     *                          text:           will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching
        +     *                          bool:           will see if the data at the path exists and not false.
        +     *                          select:         exactly matches the data with the chosen selector from possibleValues
        +     *                          multiSelect:    enables selecting multiple values from possibleValues, any of witch has to match the objects data
        +     *                          numberCompare:  gives the option to compare numerical values, either with =, < or the > operator
        +     * @param {null|boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters
        +     * @param {boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden
        +     */
        +    async addFilter(entityType, category, label, path, type, possibleValues = null, valIsArray = false) {
        +        let filter = new FilterEntity(path, label, type, possibleValues, valIsArray), catId = category.replace(/\W/g, ''), target = this.getByName(entityType).registeredFilterCategorys;
        +        if (target[catId] === undefined) {
        +            target[catId] = { label: category, filters: [] };
        +        }
        +        target[catId].filters.push(filter);
        +    }
        +    /**
        +     * Add all spellfilters
        +     *
        +     * @todo convert this to read and use an iteratable object that can be stored in an extra file
        +     */
        +    async addSpellFilters() {
        +        const SPELL = 'Spell';
        +        // Spellfilters
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text');
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.lvl"), 'data.level', 'multiSelect', [game.i18n.localize("CMPBrowser.cantrip"), 1, 2, 3, 4, 5, 6, 7, 8, 9]);
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.school"), 'data.school', 'select', CONFIG.DND5E.spellSchools);
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.castingTime"), 'data.activation.type', 'select', {
        +            action: game.i18n.localize("DND5E.Action"),
        +            bonus: game.i18n.localize("CMPBrowser.bonusAction"),
        +            reaction: game.i18n.localize("CMPBrowser.reaction"),
        +            minute: game.i18n.localize("DND5E.TimeMinute"),
        +            hour: game.i18n.localize("DND5E.TimeHour"),
        +            day: game.i18n.localize("DND5E.TimeDay")
        +        });
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.spellType"), 'data.actionType', 'select', CONFIG.DND5E.itemActionTypes);
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes);
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'data.classes', 'select', {
        +            artificer: game.i18n.localize("CMPBrowser.artificer"),
        +            bard: game.i18n.localize("CMPBrowser.bard"),
        +            cleric: game.i18n.localize("CMPBrowser.cleric"),
        +            druid: game.i18n.localize("CMPBrowser.druid"),
        +            paladin: game.i18n.localize("CMPBrowser.paladin"),
        +            ranger: game.i18n.localize("CMPBrowser.ranger"),
        +            sorcerer: game.i18n.localize("CMPBrowser.sorcerer"),
        +            warlock: game.i18n.localize("CMPBrowser.warlock"),
        +            wizard: game.i18n.localize("CMPBrowser.wizard"),
        +        }, true);
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.ritual"), 'data.components.ritual', 'bool');
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.concentration"), 'data.components.concentration', 'bool');
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.vocal"), 'data.components.vocal', 'bool');
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.somatic"), 'data.components.somatic', 'bool');
        +        this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.material"), 'data.components.material', 'bool');
        +    }
        +    async addItemFilters() {
        +        const ITEM = 'Item';
        +        // Item Filters
        +        await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text');
        +        await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), "Item Type", 'type', 'select', {
        +            consumable: game.i18n.localize("DND5E.ItemTypeConsumable"),
        +            backpack: game.i18n.localize("DND5E.ItemTypeContainer"),
        +            equipment: game.i18n.localize("DND5E.ItemTypeEquipment"),
        +            loot: game.i18n.localize("DND5E.ItemTypeLoot"),
        +            tool: game.i18n.localize("DND5E.ItemTypeTool"),
        +            weapon: game.i18n.localize("DND5E.ItemTypeWeapon")
        +        });
        +        await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), "Packs", 'matchedPacks', 'select', {
        +            burglar: "Burglar's Pack",
        +            diplomat: "Diplomat's Pack",
        +            dungeoneer: "Dungeoneer's Pack",
        +            entertainer: "Entertainer's Pack",
        +            explorer: "Explorer's Pack",
        +            monsterhunter: "Monster Hunter's Pack",
        +            priest: "Priest's Pack",
        +            scholar: "Scholar's Pack",
        +        }, true);
        +        await this.addFilter(ITEM, "Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes);
        +        await this.addFilter(ITEM, "Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes);
        +        await this.addFilter(ITEM, "Game Mechanics", "Uses Resources", 'usesRessources', 'bool');
        +        await this.addFilter(ITEM, "Item Subtype", "Weapon", 'data.weaponType', 'text', CONFIG.DND5E.weaponTypes);
        +        await this.addFilter(ITEM, "Item Subtype", "Equipment", 'data.armor.type', 'text', CONFIG.DND5E.equipmentTypes);
        +        await this.addFilter(ITEM, "Item Subtype", "Consumable", 'data.consumableType', 'text', CONFIG.DND5E.consumableTypes);
        +        await this.addFilter(ITEM, "Magic Items", "Rarity", 'data.rarity', 'select', {
        +            Common: "Common",
        +            Uncommon: "Uncommon",
        +            Rare: "Rare",
        +            "Very rare": "Very Rare",
        +            Legendary: "Legendary"
        +        });
        +    }
        +    async addFeatFilters() {
        +        const FEAT = 'Feat';
        +        this.addFilter(FEAT, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text');
        +        this.addFilter(FEAT, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'classRequirement', 'select', {
        +            artificer: game.i18n.localize("CMPBrowser.artificer"),
        +            barbarian: "Barbarian",
        +            bard: game.i18n.localize("CMPBrowser.bard"),
        +            cleric: game.i18n.localize("CMPBrowser.cleric"),
        +            druid: game.i18n.localize("CMPBrowser.druid"),
        +            fighter: "Fighter",
        +            monk: "Monk",
        +            paladin: game.i18n.localize("CMPBrowser.paladin"),
        +            ranger: game.i18n.localize("CMPBrowser.ranger"),
        +            rogue: "Rogue",
        +            sorcerer: game.i18n.localize("CMPBrowser.sorcerer"),
        +            warlock: game.i18n.localize("CMPBrowser.warlock"),
        +            wizard: game.i18n.localize("CMPBrowser.wizard")
        +        }, true);
        +        this.addFilter(FEAT, "Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes);
        +        this.addFilter(FEAT, "Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes);
        +        this.addFilter(FEAT, "Game Mechanics", "Uses Resources", 'usesRessources', 'bool');
        +    }
        +    async addActorFilters() {
        +        const isFoundryV8 = game.data.version.startsWith("0.8"), ACTOR = 'Actor';
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.details.source', 'text');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.size"), 'data.traits.size', 'select', CONFIG.DND5E.actorSizes);
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasSpells"), 'hasSpells', 'bool');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegAct"), 'data.resources.legact.max', 'bool');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegRes"), 'data.resources.legres.max', 'bool');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.cr"), 'data.details.cr', 'numberCompare');
        +        //Foundry 0.8.x: Creature type (data.details.type) is now a structure, so we check data.details.types.value instead
        +        let actorDetailsPath;
        +        if (isFoundryV8) {
        +            actorDetailsPath = "data.details.type.value";
        +        }
        +        else { //0.7.x
        +            actorDetailsPath = "data.details.type";
        +        }
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.creatureType"), actorDetailsPath, 'text', {
        +            aberration: game.i18n.localize("CMPBrowser.aberration"),
        +            beast: game.i18n.localize("CMPBrowser.beast"),
        +            celestial: game.i18n.localize("CMPBrowser.celestial"),
        +            construct: game.i18n.localize("CMPBrowser.construct"),
        +            dragon: game.i18n.localize("CMPBrowser.dragon"),
        +            elemental: game.i18n.localize("CMPBrowser.elemental"),
        +            fey: game.i18n.localize("CMPBrowser.fey"),
        +            fiend: game.i18n.localize("CMPBrowser.fiend"),
        +            giant: game.i18n.localize("CMPBrowser.giant"),
        +            humanoid: game.i18n.localize("CMPBrowser.humanoid"),
        +            monstrosity: game.i18n.localize("CMPBrowser.monstrosity"),
        +            ooze: game.i18n.localize("CMPBrowser.ooze"),
        +            plant: game.i18n.localize("CMPBrowser.plant"),
        +            undead: game.i18n.localize("CMPBrowser.undead")
        +        });
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityStr"), 'data.abilities.str.value', 'numberCompare');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityDex"), 'data.abilities.dex.value', 'numberCompare');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCon"), 'data.abilities.con.value', 'numberCompare');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityInt"), 'data.abilities.int.value', 'numberCompare');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityWis"), 'data.abilities.wis.value', 'numberCompare');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCha"), 'data.abilities.cha.value', 'numberCompare');
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamImm"), 'data.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true);
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamRes"), 'data.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true);
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamVuln"), 'data.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true);
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.ConImm"), 'data.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true);
        +        this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("CMPBrowser.dmgDealt"), 'damageDealt', 'multiSelect', CONFIG.DND5E.damageTypes, true);
        +    }
        +    async addRollTableFilters() {
        +        const RT = 'RollTable';
        +        this.addFilter(RT, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.TableType"), 'flags.better-rolltables.table-type', 'select', {
        +            none: "FoundryVTT default",
        +            better: "Better",
        +            loot: "Loot",
        +            story: "Story",
        +        });
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_modules_renderer.js.html b/docs/dist_scripts_modules_renderer.js.html new file mode 100644 index 0000000..2ede631 --- /dev/null +++ b/docs/dist_scripts_modules_renderer.js.html @@ -0,0 +1,120 @@ + + + + + JSDoc: Source: dist/scripts/modules/renderer.js + + + + + + + + + + +
        + +

        Source: dist/scripts/modules/renderer.js

        + + + + + + +
        +
        +
        export class Renderer {
        +    /**
        +     *
        +     * @param {Array<string>} templateStrings
        +     */
        +    static async loadTemplates(templateStrings) {
        +        return await loadTemplates(templateStrings);
        +    }
        +    /**
        +     * Render the Information section
        +     *
        +     * @param {HTMLElement} messageElement
        +     * @param entityType
        +     * @param numLoaded
        +     * @param maxLoaded
        +     *
        +     */
        +    static async renderLoading(messageElement, entityType, numLoaded, numPacks, maxLoaded = false) {
        +        if (!messageElement)
        +            return;
        +        let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.hbs", { numLoaded: numLoaded, entityType: entityType, numPacks: numPacks, maxLoaded: maxLoaded });
        +        messageElement.innerHTML = "" + loadingHTML;
        +    }
        +    /**
        +     * Update Loading Message for the given entity
        +     *
        +     * @param {string} entityType
        +     * @param {number} numLoaded
        +     * @param {number} maxLoad
        +     */
        +    static async updateLoading(entityType, numLoaded = 0, numPacks = 1, maxLoad = 500) {
        +        let loader = document.getElementById('CBInfoMessage');
        +        if (loader) {
        +            Renderer.renderLoading(loader, entityType, numLoaded, numPacks, numLoaded >= maxLoad);
        +        }
        +    }
        +    /**
        +     *
        +     * @param {String} entityType
        +     * @param {Boolean|null} updateLoading
        +     *
        +     */
        +    static async renderEntityList(entityList, entityType, updateLoading = null) {
        +        return await renderTemplate(`modules/compendium-browser/template/entity-list.hbs`, { entityItems: entityList, entityType: entityType });
        +    }
        +    /**
        +     *
        +     * @param {Object} filters
        +     * @param {String} entityType
        +     * @returns {String} hmtl
        +     */
        +    static async renderFilters(filters, entityType) {
        +        return await renderTemplate(`modules/compendium-browser/template/filter-container.hbs`, { entityType: entityType, filters: filters });
        +    }
        +    /* Hook to load the first data */
        +    /**
        +     *
        +     * @param CompendiumBrowserApp
        +     * @param html
        +     * @returns {Promise<void>}
        +     */
        +    static async afterRender(CompendiumBrowserApp, html) {
        +        if (!CompendiumBrowserApp?.refreshList) {
        +            return;
        +        }
        +        await CompendiumBrowserApp.replaceList(html, CompendiumBrowserApp.refreshList);
        +        CompendiumBrowserApp.refreshList = undefined;
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_modules_settings.js.html b/docs/dist_scripts_modules_settings.js.html new file mode 100644 index 0000000..cc0638b --- /dev/null +++ b/docs/dist_scripts_modules_settings.js.html @@ -0,0 +1,144 @@ + + + + + JSDoc: Source: dist/scripts/modules/settings.js + + + + + + + + + + +
        + +

        Source: dist/scripts/modules/settings.js

        + + + + + + +
        +
        +
        export const CMPBrowser = {
        +    MODULE_NAME: "compendium-browser",
        +    MODULE_VERSION: "0.5.1",
        +    MAXLOAD: 500, //Default for the maximum number to load before displaying a message that you need to filter to see more    
        +};
        +const SETTINGS = 'settings';
        +export class ModuleSettings {
        +    /**
        +     * constructs and returns defaults settings
        +     */
        +    static _getDefaults() {
        +        let defaultSettings = {
        +            loadedCompendium: {
        +                Actor: {},
        +                Item: {},
        +                JournalEntry: {},
        +                RollTable: {},
        +            }
        +        };
        +        for (let compendium of game.packs) {
        +            if (defaultSettings.loadedCompendium[compendium.metadata.entity]) {
        +                defaultSettings.loadedCompendium[compendium.metadata.entity][compendium.collection] = {
        +                    load: true,
        +                    name: `${compendium.metadata.label} (${compendium.collection})`
        +                };
        +            }
        +        }
        +        return defaultSettings;
        +    }
        +    /**
        +     *
        +     * @returns {Array} Settings
        +     */
        +    static initModuleSettings() {
        +        let defaultSettings = ModuleSettings._getDefaults();
        +        // load settings from container and apply to default settings (available compendia might have changed)
        +        let settings = game.settings.get(CMPBrowser.MODULE_NAME, SETTINGS);
        +        for (let compKey in defaultSettings.loadedSpellCompendium) {
        +            if (settings.loadedSpellCompendium[compKey] !== undefined) {
        +                defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load;
        +            }
        +        }
        +        for (let compKey in defaultSettings.loadedActorCompendium) {
        +            if (settings.loadedActorCompendium[compKey] !== undefined) {
        +                defaultSettings.loadedActorCompendium[compKey].load = settings.loadedActorCompendium[compKey].load;
        +            }
        +        }
        +        defaultSettings.allowSpellBrowser = settings.allowSpellBrowser ? true : false;
        +        defaultSettings.allowFeatBrowser = settings.allowFeatBrowser ? true : false;
        +        defaultSettings.allowItemBrowser = settings.allowItemBrowser ? true : false;
        +        defaultSettings.allowActorBrowser = settings.allowActorBrowser ? true : false;
        +        defaultSettings.allowJournalEntryBrowser = settings.allowJournalEntryBrowser ? true : false;
        +        defaultSettings.allowRollTableBrowser = settings.allowRollTableBrowser ? true : false;
        +        if (game.user.isGM) {
        +            game.settings.set(CMPBrowser.MODULE_NAME, SETTINGS, defaultSettings);
        +            console.log("New default settings set");
        +            console.log(defaultSettings);
        +        }
        +        return defaultSettings;
        +    }
        +    /**
        +     * Registe ther very basic settings in the game world.
        +     */
        +    static registerGameSettings() {
        +        // creating game setting container
        +        game.settings.register(CMPBrowser.MODULE_NAME, SETTINGS, {
        +            name: "Compendium Browser Settings",
        +            hint: "Settings to exclude packs from loading and visibility of the browser",
        +            default: ModuleSettings._getDefaults(),
        +            type: Object,
        +            scope: 'world',
        +            onChange: settings => {
        +                game.compendiumBrowser.settings = settings;
        +            }
        +        });
        +        game.settings.register(CMPBrowser.MODULE_NAME, "maxload", {
        +            name: game.i18n.localize("CMPBrowser.SETTING.Maxload.NAME"),
        +            hint: game.i18n.localize("CMPBrowser.SETTING.Maxload.HINT"),
        +            scope: "world",
        +            config: true,
        +            default: CMPBrowser.MAXLOAD,
        +            type: Number,
        +            range: {
        +                min: 200,
        +                max: 5000,
        +                step: 100
        +            }
        +        });
        +    }
        +    static saveSettings() {
        +        game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings);
        +    }
        +}
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/dist_scripts_versioning_version-check.js.html b/docs/dist_scripts_versioning_version-check.js.html new file mode 100644 index 0000000..d444638 --- /dev/null +++ b/docs/dist_scripts_versioning_version-check.js.html @@ -0,0 +1,83 @@ + + + + + JSDoc: Source: dist/scripts/versioning/version-check.js + + + + + + + + + + +
        + +

        Source: dist/scripts/versioning/version-check.js

        + + + + + + +
        +
        +
        /**
        + * Version check function from Forien Unedintified item module
        + */
        +export class VersionCheck {
        +    static _reg(mN) {
        +        if (this._r)
        +            return;
        +        game.settings.register(mN, 'version', {
        +            name: `${mN} Version`,
        +            default: "0.0.0",
        +            type: String,
        +            scope: 'client',
        +        });
        +        this._r = true;
        +    }
        +    static check(mN) {
        +        if (!this._r)
        +            this._reg(mN);
        +        let mV = this.get(mN);
        +        let oV = game.settings.get(mN, "version");
        +        return isNewerVersion(mV, oV);
        +    }
        +    static set(mN, v) {
        +        if (!this._r)
        +            this._reg(mN);
        +        game.settings.set(mN, "version", v);
        +    }
        +    static get(mN) {
        +        return game.modules.get(mN).data.version;
        +    }
        +}
        +VersionCheck._r = false;
        +
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/fonts/OpenSans-Bold-webfont.eot b/docs/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..5d20d916338a5890a033952e2e07ba7380f5a7d3 GIT binary patch literal 19544 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 literal 0 HcmV?d00001 diff --git a/docs/fonts/OpenSans-Bold-webfont.svg b/docs/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000..3ed7be4 --- /dev/null +++ b/docs/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Bold-webfont.woff b/docs/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..1205787b0ed50db71ebd4f8a7f85d106721ff258 GIT binary patch literal 22432 zcmZsB1B@t5ubU^O|H%}V|IzIVNI zUovCM*w)bDm$Uix&jbJf0&20h={9zAA^05!;@9Ta9)O418En_g!QA$j%|T zg7y+LH+25>h2!|O`Oo%0Aeh^Dn*DMD0007R000ge0Uny~7N&+K0045Wzx^z~U;{Kx zUbpxqf4R$F{l9sTz@vgjSlGIF007AU#s~B}CU7TXuFRs1z45P|qR4N2OTXCll}{hH zHT3wsuJV8Pgy25_69Vzr8QPlua=-Bb&i}^9U_Kjd;b8CV0sx?j@XNjYjt5W_dcEY} zWcur?{$H$r|HFd_(WSeo(QnM^|9*9_|6rl7So13Ze*rMbn?LiP91}v%{ZCFUVQhP> z8ylDy80-QYL4qL|7#V={y9-PL9W(yUI~b4<0Kj9tDn(W%NgQM3r-SAi%{IQ-av{#b zm?Dp*nUWE(`7{EcC}s)ta^1+9Uj`lvS<-m^uZMv8f-v%ehSe}U)}pB5vjGC6Uy~pm zo)<1qh;kgVTrs$D``1)&z8ke|;_(>$1Je!j%!vOnt{S4G>G`aABr9vrN*+4@PrG+q zdH3aZlXjCg-utrN?)PA6A(Aic*r{P)fItNfh`QJTc? z3wgp|$4hT`N(iVlzs(@58kfEk!62o^Q$flqq@=t{xl6XxO=$TCkbN0bkG!jwEbQN4 zG2V(|AGxWwXsuk-^?T%XAZ@~-ovUcv=&a}s0@$uWPKYo9;IKW2M`U||9p*tE=o13y zAO}3UTRRB4eo~B3#8#jJ2h?E$oa*=!uFZf9hm1DKeep&;V=p~b&jPH{5LgBA@Apns zU_VKVVEcdkU^~M2p8z9$y^ucg{gfQAU$62E{9_n|TCq4qgET=@+bg~A5}0o^Z#JVV z0qRI-PMZJEiE6Zg;GOQ;a2q|YsR@`&xDGOhGncu2d?Pj-GduAh$N_@M0V6IXBF<8R zxjfTXUW5hxM5`WGGjy>!(C%ba9^je@u0M9bG`-6VPM;@*UhaZwS{dYJWn~}}ibs}G zwGYxwzK4<->i3DRk}gn0r*b}@NcD5zt|~z4eUPlFFr-kBCng*diUrGxHMPqQK9yIo zB)B7F{t676O}rd4M%_4i?(Wg!N5}Pcv!4?>x{ffiV@XWmaoy{%8Wm5Ska0TN1*tUF4 zR};ELu9o%iR=|sY^G~PFaL86`dKghU?-lE#d&z}pZ+O3EY*1UyOcxQKcc*>kZrR#Zgl0UbrqyO(KU-@)HSW=yLIKuRVv{d z)L3=2Hasz^73ld^tUTeWl^AnXdtrW!p5f0DAcnD2vgr=9S&I~S<@~f7FLK8=U8MLO zub`KNmnLdxsr4ZF!hIad$A;=O|K_Ow$zev}MxzD>j*btIhJU51X~qo|BvFieSwmA2T)~V@&E$JN5n$?FPQ>^cms6; zfC7Mkrh_v7CS3ggk-&2RW`Lg%KtRwCV8EatKtLe706;ea00i21Z!|FQ0gaGB zKz~VrOzxN#89&WgOkm6^4Y-C~qRwK0QUk*SlL9jX69Ur%y91L0ql7wzBKomJi@;%e zG{1kqGe)2ndjLwQA*!PU1qB3!1i{KDkVMgm70?fUYJTv4_#gfEfBJvAe=xqgzdnxp z#=yn#aC{tg`?kS5@NB$l@B0G5ZQ&#FG#fHg>&5qGh z)Rx(r-JaoM<)-PX?XK~%^|txC{k{SJ2=)=?8SWv*E6y?2Io?4=z}Q}8Z6%sdYIjZ!tQ;*e zRIV=l%LF$%S>}_lvdZ#%9eu)fzuxX_O5EF>BcH+N^?ORsyMN{lP02pquKtEZ{wS6+ z{>Nl~eJMO5hr+~wQv+lL0&obKy!YR;5de)ohS3-N=ZXysoB<(?13bWw7`xpATWS8& zW0+`8`TYadZ|-1-3If172LD?bc&ulsTDmWYp(J;b#3s&?LW8Z=#HgW{LQb+<(Vuo-en}s5k&k>}Q!XMicO zVLg=&(uGl9(Oo$-PVIkRw7^8@GMS=KQ@O$qUR{@LG>4z%E!?>(RP5ICNkw(ERwIDN#rrPuiBq|9tPRn(cB5|zN0 z+L9lPC|rbz!sI*m2=9PF9G?=@X;lErA)3sio}aE{WzoYnwr`zLmy*4ZoE5_#dQm=g zC(_*GfX1p4-?zc*sJ1@h3(_jz>ROHG#4Sg0^v}t0&(b7^d1(As^L{`1LYMo-F2HjD zeqT(fv)&@3nD4uRV!95htYU$lM|G7zS!|Ii%P8x;jKaF^F2gA7JuNZyliD^z{KDCJ zK*)a8F)I6k=d{orx7mnKz+NR}w+`mCpeJCb6|>n$E#`U&!2&x!T|yO@YiaT{&{|c= z3Z%(8|5y|;))7v4QGtx>y1Y!~kMgq=L60+96p?*hucL$PZn@QbyLaZMzoo@|9$Gcb z9-9<)$1r~|8$5k)5BJl|?%JW@oT`v42w!TT1OP^14UY70c}YUOf&0zbeJbDwiU zc1g)Mn~}wre&(Y+E)n_0n`et-f_6n$OC-fLX!9TMr*@=_>sLW%QS$j=xa*OLc2g*0 zVSiNq1+}DSY_r<|I;pDKcGSGpn-9{x$%=!p#l$i%j9W0JtY>)GiVCF^d{a`vB|=yW ziYcDMco4K!=wK_HE4-EU;8~s*1~xQdXkKF%LahX)F6vI>xcePmh4uQW$A09k3o&Oz zxV&TX7llW8MS-6SxUF7;U74X&^7$Fxf%4@=v#*L8R@uSj5baVQ>r}g#+|VQPTe`*; zHk{Ur06Z$b?5u?96k|K%I7W=A>{~_v-SD_QMwOOLPuNFUVq>JLJ7S`*^FCgtTZ_JF zPm1%zX#3B4ZcB{LoioXCi|8N!6M@T=%0Mr3CIn+ZPH3!w)&4`c0aqCMi(7vgxt|_b z=%_=@D~rr2W&G;+XsWh}lo4IK`iW4yCeCuV`BiZX8%qzPSX{i=kQ5A@zg7OX{?XpO zx;lRWI9Qx8$@1BBOG~_3+efTyu&0wn0(6}(IdB8;0;FfzN2;HEfDCwFM%$nra&Q81 zognx~!*-dS>;Qe_;QG)H5nx6MS4mIcdV!rF@DhY;#o_vho!9`oNy2uiogj>yAdsBw zfO*Kmb|E=I^b>_|W8y22(|V4C*aEs6PRSIkO2DGn(9+_qk)Qd{Q+y2&*TT@^y-W_@ zgWr>&rN6d`l>BSM7x7~@|0($I_bd4~hcD{W5Iv>c6}gcdCHFaR&-LY88&+BTzRv&w z0Dpb};62u-e603-?>W9ym$SMD!*6Uxk4IhITVfXue^lrzwEI6A4uh1-DI^VaSIDCN!Bx#_}2`m_w3&xgi4^FsaE+qj- zQ4%UsktG=;O@8Za=2(jd)*A!vf(m-OqboU|8Vznb31Ud8!sc#oZ?3j7!OcvF)%kQd zJY`fJu(sy79GVv^6X{(JXHSy*1FTM>DfC(>lL8sfs;P{ML$J2kit`r%xO+G4@@wsp z^;3Fn?HxAefF6z>9p7LaE z{j~1BVfTCvDBEx(47Zd+?M~MEJcD;TDb(+d&pJ@`^XVI1d{>e!ttZy!4)k7$$e4~k zc|wI-l02;t`wad33Pf}K?EIyun1pl~Lso_DR#Tc(B&C#OL97rNB1G%kh4g+$YTPD5 zE<@SzI6!$xXFG5*pbEOx_RqD#Y(;G;!D*zs^(S-r<2Xz!R3GLIox)N53>-ag&qeXg za5CQN?HRYUe3#PCf&9yLLyN;jb>aGPpmxYxMRCms+UP#0cm{uRPFFnsNjEF>%zc4z9w!+P%u^7nX z{c$W-i|4HxWx>n&D3VKLAyNqqNu}jFwg8&3@e>JQHqw1}TU>GMfAVuz?@C5dXM(-H z4;^qua~M^SgZfM)zl6P<4nV2RsWA6Gs1NF9HR1uwY5KhM8 zUV_kZ)IWgU50B%pQ*)sGH@i&-;7UFBNZYH9g6s=3hqCxn#{!R2q8>8%KRz$ycV}1p zyELjVZSvmDOZa}?jX$Fy(n{NX#7IX6RFWci=24s;85AY&Je9ZZprinEDUwcQo)ARy zmReEc`6P*!0<tE_`L^9G#rd~^DcPNZe)+yc zTf8mwN4&_GaC@cpR|Q2$hkY5jY)ua3bk@1djL!A6dp=e4XfvAo!*cU_uOPX3_UF$f zz6*M`I6nRf^vmNjPWRfL^aRuq?`0MeCkfUO`cObP7j%%Smu%NUpb}gGdv{i~Vb6-1 z8A9-;K!Zee(axpW7PRGzI``f)MG)2ZdnK|!SAR&j1W)NJ?veLt9&WebvXTa zxc$!FY2XQF4Tw!qRwb`X$W%~^9+D9hG$17_07T7_0(0<+CDDplB9wUSKn*hs z4H(c5wzAP?n|!XN#rJ=ooM$FqT?UYuP|LcU8%_anv!O$25OyZuJ~JYoMCim2=1Yz` z`Wlq^%!66Pg~AP`QUl8eC=={cpo$Pmz6cpVFapR1ii52RoG^aqcU*>viX9+Y_Q_oh3X z*uG)GfQ#7RF-X>hMK{cP%tOWW@)nn%ME z{;oZQH;LrW+SnCg*>IR{;pEAKse?C$I4|ZPn)%Bia`-@(vPIMZwm6Rsa#y!;}VlCCIS}Xz=8T%q? z3yW-Q9#XDdJPBNVLqCCOM4IO2sJSrUV+p7bu*IKmmVY~-I&##5ffK}W7I_R`ZJ~B8 zDzRGL3&mw|HdZ?CsoZuNZQks*d|(aP`X1Ujj0MzS_?6h{TeSzV5%k^dN1_$~pzj+& zP7)-+g5S*oDhYN>Ra{ge`_eQN5R#B|P@s^sU^Ugs6$?1qtn7_jR}LOboyU&Q{>n={ zn>bL1^Nf@o3;gjQF4j36OErBNR;9l-xoPmv++sc73N69gXtaKxoa%Xh*iCMl*a2E8 z$sJor{T?eB{&5?cTNn_WptQ+!y*RD0F1EW|I|&kZchnz<`plqQ?iYj-dZVH;)q%e5 zq;M)IR>IVTWU`}|L{g&w8=o|57`Sv;yKJ3+;ZUc4*Ubj%tvcSrT8WBO%WjMLDtc0E zM^I|1gGn^GeK9)81Lp?fjg{QcBGW(hA68WDD?Vk~4Dg}uO z0?kB>r--+T*K{JSmu!hh<!R6BTSVNYfECYc{7hM+!$yzZQmgC6~uW zZnb|Cc!)OUTkUIwBgCsN8{e@yl@NlT!0SPkIQ&!=sfdUBDJ*9u7ZUA9xT|eA-EW~+ z#yJO{!@XROpy7Drp-u|pf`cNhxTIXs;I7FONh62E8j7XCz^?Z*c|o4xb!t zMtJ4H4-Ob_A_g#9^IQr105w8Hj~}5!wB|<~@K5)YmbB+Sbkak4{TPRdpyWc1(hAiV zivRkdi7ORE@DcVWP7?y$KNz=G>=KU^=@ec_O&p(L2pn z4GHD$C3yl|LlL-Phh|Zw+e^n|cOa_VZIKed*`65LOG66lZXG zjaF}J(?v;!VdWR@_i)+Ai!^wgU6k;l*XmVtl0F$&i`GF=PrefV95h8Gfw zzk8?5y$aX-b{cp@J~>06@6p?$u@;knBJ36FG?nSq$W6iViWOCFLU}~U-r@@eOc;tG z3=_LFJF$4li3fAUyUPe9xll}Ox;1BGUs@^x7F>P z78>|xSe-A9jUJ6wifg3^EQTr^O%;KHN!3aeXVCYn83TNdoQ$lPyx8=Whw}^z3sJsZ zp}4(d_o=ZBGUAV5^e>11yzs-?2)dTMz+SAk*|h%W=ElpkG41#?`U}mv33HLH z-t#i~d}U-EvAxaK3|dT1YvN51XDM-9uFgnezryUF>m+62c!pea(qso-{0OlDx|FDV z%I1-@7z&mFeN$XFkT$~>zA zpYSh_^tQ0N6v9&$wl82iueaqC0ed1BynCs%m`|hV~9|(NI%33RI)SkS>YL3YZ755sj4KR*1X7uCzQ*QWxOudkw z4nC$X0iLo*y+|aIBf&;LbnNKSoIaE78f9`z_8;d-u`GzRuD(?y-0DGu>Ua|akSGU9 z@m5=c0~B) zk;VpQF0ST}PQDsElr@Kp{R9Yjk%1WTkQl0Z&(o4do3*%?y3|$YS|mGO&%@=W9`47h zZgqQ0gOZ{^HDz~xn$R)^JUl#aLy(VWd~31XL*BQZ77 z>QoR$% zf=;0@rnhUCS@lFpOJoAt)0WVp7&7`>8r|&!>7Gwhw8s)Ma6DT8Jqr>qis4O3ysFjg zfJp9w#{*-GQ55r3wL@Ho+}z8reIjNs0gTX$G%W{Zo}t#{Z2_g|0x#Pu+HP4?|Dg0{ zI?u+Qe8QepC|-)~1VIXn)pjF8ZOSMZR4joA#uc$JraoxMJbdEOYwhlsOOVO`h=QZ{ zx6`I-?vI-nakT0j?A9n>3XNE^NcPO~lpSu+zm>5k^og_BPVYWXOG$2jILNHw17}ST zxELO1)ips39Gp5jn5$Asx<5|gTWelD0v*BAD@J{^>U9TGRih8mH3H{ZE@9R1uY9jM zgVoj6!_}DatH~ZNn&Qa;M%i{z10DiznN?;Rw=-7%V3J?W_lw~5d_m3Xj%qH8$ycS= z;PC=1U(E^6W68Ta0Q3je@HbrIJ2g*0*r>E)y2hluKB>WAV@;v{m06=8>_y;^e1i)|*Puw%qp=B}PseK!q6F)8{W?K;CZfE}9m?!r=Q%Ei@e zLaS$w;y-db|JWMMNVXl2v&ULyZFp&{z3oMWghi$uD5j5SD#SgH#k4c@9(@HzVB8?4rie}u5<)+K#$rzQ+`;DAm7BKvs9f- zP2hVNfLQ2n`gxcQT$YTFESjtFe{EZ7xbET`6Lb~U8fnN`{?r4ySGKv{>_9zyuQ4~2 zlXU1izP*0=WUo=s^Z1wC>3~-g%u4MkG*bHM>Yif7XB*l#Xx>BkTmg(@@b#dYcH!l; zIB$(77Qe@f22*`*$X)7%$=96(OqGqdp6jHYDTc|G>Gw^4$NLU%2L^)sH({aLNDs9? zy!<&yXlydwgP!^JYFMni(XBQN6bd`wiP_wu-`ikCdN|-A9o$9q|0^6KIxk9LR%b&U z6=dYl`k>-0Ay3y-iTSLjwq?#GW6RzzbL1=^uIh1K5PTxM{$v`sk&>&;N0|u5fOg!S z6a?-s3Ks{A7{PvS@O%M$45WF5*?{kQCj9qhq|<|S@^y?#Q4_nmeliG^=!A3haoAYtydfBFgB{4)+H?Y3@?9 z8T98eK)I4VI+PCsMWq%feakD_PkP7ZD@9A&x&PLb>{(ojLQzzDDJ{{h1D12_&py+i zFuDMq;H1fI(=i62@&aRRv?jbl-ojeBDd-dP=uP@Lmkct+_;n~~C2y+^pHjA#U@;KoUP1oIX(P(p zIC(z9j-@DZdb_?8+E)jFj z0e+2f8Pmf#d{st!VAj#Eq!mUw!8E1dOsW3q2c3j$xwu0n9E;gbF^1l0@x4vX$FJ^O zFiUf3PTj?In$HllX6^D;9*mP+I8JVJA6p*CG3HSv(FwJ($Sc2p{J_FT@I|KO;4A1y z;s;?EKAr=wRX{y|Ffw^oV#bSlk#F4Qe1WG^`%VG158*qm=pAK!pm{Zzu%6WMJ)1eS zt>Drw3C7rRTkGHdNC33JS%ADUrj;u;u_19A<ZcSR~zNw^YI(s69dZI!?x? zzuJ25l}3KakVb~@Sr$hOd`eNQ3mV6*q{D?PTY_VM4(uy1NFqna=trpsiH--v3G zIDuP=(4vajEL%7h*AFGXv35vURw6E?Dq|yf87OolrKFfRJ}9h+6~^9(uO=ZMrWlKe zWid~ur5iRnK0$!03)&h~mUGjQS$x-v(KaYSqj51eSVS3{lvoDN@$qx`fl+^1E;j<^|xP`Ol3u2zY-0(J%`T0FuJfXtjod9%f^u-i^ygAtZ?~; z5H#9*B^uYq{infvq!LT%yD;%NNM#h)i)<;5%UwOr$E_?3{w>P+uX*U(#|YuZ{$K<# zXlBf^1j;7!IEP>B`Y^5gzxet;=VLU!vQ7m#im1Qk`IT^9XX#yi`DoTil=Ap9>43Qv z7p+ny>o8K2gcMlQ&>Eu{jG5EN5v<1&Kz#u%y42ZsVhJ2>mYtLEx4N$pR)(3paxuGn zx@QOSJt3MyO^rPse4-yugV8__o)2BU7?=NW6ptFy%oC}BLly*vE?|WFx~*DNij71H>7#=RaGaIuRFGojZB^hK2`W#2GKJG#yKK)98?a4Y z3wpi%S`Oh||B8XdRUVJm&LHlA_+`@aWDcjZpET+_I~!hZgZ&Jj zbNcTRrY4DI{l1K&U8G9>A0XiPJfoDm{-|SeT`8N@e2&iVQBU*}9l>~xJCwYv$cIFk zOCat}%Z2NKndzF+3XD~3nEA~V()rDiit_E%<%7gULtpT-H{E2;Bg@eW8zl)LlLk6W zH~>GV8qE2aBn!#hK%E2{zGQA+tpfhPG3{Bo*X6`uK`ORMWd^hXTCyrjs#u&uO^PT5 zo1+@UV6_tP{((BqKCp2h!e1XK=!fn%p$(I8ufAPOvZtx7Eb&AafD}}|gMa~-h*+}x zKepVUZo(!D56LdUKYLSuOTM~KisGW2yluRESMZ*pynib2uhUkH72a|gTe5lQjPtTU zkL9#~&TSjAaXFp6o=WG4+3XT7a;9;e9%6+P_Ak`#FO}`TpV~&q`Tm_(!iI{On%lL1 z9ktlplX~{<)}aD>!KH>Sv9T_7(_XG!5qq7-o|>{n}-p~FYJ?j+5U96thH#rH2FoXTjltltv>y@ z23+ipAl{9HF9d)kj7S@ntd6TH)4Y%wxAwhw&E9f(fj)@V$4|^3V6&^K+XsK+bk`dk zjbn%EJ54+h!L@HrW&)YPM3Aq9K;`FO)#hq(8W852khC8S4mas{E}&sU_NXHIp^Nm} zmr#j1z^C&%&BhGa1$4fchhs9B@3Y6w5g$#Z*0 zJe8ji^h-tjT`fKQldNG2*P$zVQY_(q{V1Uu^c6Lih&wR8i}C)ihJIgVWX>_ekVM)} z7wCh$;i2whK|=E7+4|eU84%*B{`J_r+z9_n*_BbDj3Zl zhim=!S9PZcN%LZWT^EJx?2BURErCVnd#Qrh20&e`PmEiuj<;rM*0Hvpo~tL{%dhba zGntZ!9ZwmV*pJgs^mUBX34)ME4jpe~+A;NLU} zQr`YJVjdky`rxxH5}tzcL%p1)N0dvx%no6}#T%NSQlNjU@6Lu#c@Hl^vA(A7BLU<_ z_|m=%DPt!;krqS`tU3GFo{x}-|Ls1e-*uuSbSq?B%fP|H@k|Dj>vv~aLO-8js{g~+ z7Y2poYtXUn=4bx{HoKiic9!uC9q<5Kt?*3Pn&=*W-t^X=R@}L7MUIf+EAwDt3$20T zMwWb@2I7PMiJEdm*m+NybiGt$38@6;sbsUIE@IXEK|nY|FW~K0h82aXRa?1oDMWBc zPpYyH^TDCI0d%KIYiA`G>T0Y9luZVi%p)6c;;xgO(kCg1Nm%KJa^ za=12L%{7FW11~SeM)%9O`kiw<2bj&S3&YMBr$c+=FIbFDZ*kmvL4L|q;>~ABmT>o! zu{6jiJtA#D)RMzFNZ%qIR&(q~`qz#^z6IJeIEHy08|+FNSGt`0<1r%Ts22DEIN`uX zsM*ZrCmi9(=1q2G1F;GF@8%s}pmDq-aQ@lY8yBLUDe+%hjaHHuf^B~8Uo=S15iJC? ze%Yy#AQ5DFaw&^&o|x`o>0vlM-F2^Jin#&a%C??q{RXS-$0vQdrHx0MYo6Mn(eJrV z#w}&W=+m_CpFP`t1$KwV!l|2&ulb%`hNmgG*^eoe{f^z6`;-0coa|LTc9Y`W*X(95 zSIP?RsnZvD96dy)6h?Rm=hk3~I|6fFh;iJi=4z}o85OuC-@sIX80%#LF|5)Uo5ZV)GVHRh0NyiP1#th z`Z*(5i<}p;|G36<-=`&n2zxD~4kJ`Kva77Ulu% ziR{FdXGhqPz}Sa)%xh3c0M0q>LzCFi*H$TQ<-*~XB)uwY%*W7m#|l7TXwD?jN{%0f zy|%a4|J&?!HvdnuGxO!>OIW$trk1q1zSE~)#nr|?NLbPMbVN(${T{Jt%4aQ3a=+^9 zc(xXr0xIbwsegac-DY|9@hqwq&!mhy&cMgz8eL95xNupNEW-L6X%mV^$7K;w4dcgc zD4RVpvcgzPy`b-*KLF{CdO0Rcg*Q-gpmeZ16nqG66(4wCu6X$k!{6g-#<8bwKrdun zPli=6bAObl$cqF`FN3x)(Qcx|o(0zk&TgixJ@8HlE(BM~)RH!O|JwR(>Y8m4gGEm} zu%{6hrKoLk`p-HG3TB|g;qg~%{cfGLVkQNiPbBnt!zjOEXd7<3Yx%ak0eL`=i zm&ASW9N4o^k4-Sb;}toTP>1aVmMlpQZMHT1oGup2qwX42s-FwkreP)awal&(T^=w2 zmq)4=fIt-oXn{b=m3f;l8R4v(gO_Z#ThfAt9D3ko7C6!dN@Ns?K3AnMou;6)sN->= z%ua_>@8HwN8-koe*Jgc5)ZW~9`(Sx?CYrZDQ$qSyvoIrR)^Oy2Vj8}(agoNy0$4zF z8D11`T=rg4y zb`C2XPu98jcgtmRqt5b7YsLhcT@;z(iidD%G&zQ+Vgc|LRyKStl{$n{3_}4}*SS=R zs1krVXs|cqrd~*uCsiR<2y0v+$gCPCt6t*@{(Bw;Sp1XAOSdokkCobx#J_d1m6aoG0IeS;zpQC4F z@>_Z@tT(hGZ;Cp^>y+RCI>Ei2A`v__mh z@buXc&0MoY9VgtDTr!_#272N-nldE0tn=hLBh-CqVkmTB9DR6wfl6^hMYE(E(#SiH zkO+$P18U@>Lcr?3+DTWMhS$4(QT*F&p7N?|^^xQEkS+Wz#ce+U&SBf0mG`~5UEg)Y zdf!JQFI$R?j&(f(_wf2jtWHPy=HlJic$eGEH9YK({f+1q4P>eOcOQFU4N>OcUSQ1Q z{!a>)#xMKn_3u2?aW9muN6_= zXa%Ldgb9B>>Vv60HbYAhS!k7rFyMN1e4xP|oa(!>4@Ig~T~p^M8m&aAMNsgrB@u=g z>$i>yJ4q7IIIo--c1EP{d^>HVv>c=txQAZQcU*ruaxytu@6+znXs7H2zcxObQmZ~5 z44dtCh%X3Dx4b0$?07#$+Mg~Lo#$KRX^iw;Bz+5B_aoxED^?dXd?~XHFSfU5*uLKw zqIrA6M0tyE&hQ?w+od_fai0HvgxO4ptu+qkO%CSYfyc+n#C`*?L&wR#)}nNGpeQJ^ zTeV&!yB(Yy0*0#(^mPgp)%oI_u|NeO2=Q1_N``M=J-l{;>C6dyoCR}aLXcC7po4RP zrb|7{J6+S|Y<2D>Lqb#G(@?%W1s73kYQ8)gvLdU^rfhhHnX$`em?fFNXeVUT{zTHp6^ODJZaSNG zcBW_rv%8oLrD(Ek11?Y`(aPd^D_1RG>0q%V(0x^zc`m8OsiKG{kz92Cp(Mgf0(oF! zc6{)%VGD~uN3`mcgk{CPk&HaF^0$f_jY{>OYJTAW4NcWEfS#9%tm)uua@~}-PbkU& zuf@S&Qrw_STJg2iW)+)j%d12)xr>Q zwaDDl^Hq6(u}+bjcO79&PxH^DHNcPR*Nm>PBPW%o)tI!@o$5t15%lF4j3HFi%eCMc3c$;XNVRfqnks*||+K=ajdiSiaXw zS-wNGN!d|pod5X38nCV%;JSOvX2MxKg3#9@!k_mU@A z6PKl=P}{8TNH*=E8Tb97=jm42%Q_t^nxi6U7!NLt3ma;O2~gmz+b;Oc@KzO3t#@ti^BH!e;2RfpHRg!NNzLc1n4-;mumVqQmd`l&At-_*btueY` z8T<-&B)LczCcZb#x~{|XmYz2xKA->Im!$`qNoJ+BJNob4+b*ng#@VQ2o3+^AxIO>2 zkpm}<`^DY<-lqR|%S5|7_7n9pd6Q1%iOez)y?Pc!6NdLa9JC)F5lwZtH@P@eRqNQy zYz5gLYv>x;8xtBBufwCBwbtsN(Vp&y9sOCZ<^0%J#|)H4{Z0@k4tM?xvjN5E_(`Lm z`zmf8okH1NusM&TQyn^bqxga=$I+vMNyrP4rx^Ofh$z9CNHH&n0JaEacp^C7%x)N! zC#l8*6bh((deDn(pXPj;Ha5rG;Yi-GBV)R4?+)ukvn&0q)?)pBk$C9=Ue?!0zOv_T z-Z}D+#S34hZvtE&HKhb^HJPAIb_>oMyiRwD%H>t9Qx9i%s|WC-`rFW$m-f z#bW`{AtR}z`#f^}?;A-i2R4FHfxUI=K8o{nliTj@?DiPIHf`DoRu79U$k=gS4Qqaiz7){j+low z?ntSU$3G#1pria0R_YmIe2LkXzG*6pfL8xOV}WjEa=c8IU?*g~~r3>0WX>x6W* zSl0y&Q;-@os}9X!8F`lUe3DNTtS$2`x*F=QZf#^Ks%jY!C@$4kYjV{Ydd%al+qRs5 zbb)nog^0~ZJe`6!pN*Z1j7u*(qBSv~hI3bJho(s1sY$jmmP<>}hDFBpj69DS7gD!F zTKYdkokO;z^H#i3+K8`B5aIm_hO+R=)3~Z$i_`bGhh?#Tgcrn9?KHomfJUw4MU&$E zO*Dr70S+B?b!4|*zw^?|__{HHA@~}&h|ueFSH2)wG`zOwIgOI=)#+hi3!q}+wDWDt zsSX7KMMMfICX*e4sb;|7dcih2)Ck&CA_^~PxL0nRF=)l8JyyW5Wo#v-JInI8ClGVt znQ#7p#0`8i-{BAxAkNIr#*EQr6qXu_l;^Xhd0+#NpvR2OA}UMSNC}CjPb#(!yY@e& z^s;iP*dqF3GPd@xm~t@w`%4m}WqlR^`Q-{rHD&1I2$ZvuxJ*hqcIC8c%zVI9P^&fI zEjz;9j=W9wr-g(?V5H)YkwA2$mi2i!V|0}9z4wBW=XC+GsUn9Au0!eJ?j_@XD0ml~ z04bJg6Wc3m{$n2iKXTNm@!V(r_j;ea{(~qkW;uRP{&KE4VEUgN%6z=i#STu^7?tL% z#$%*{%F$uREPMiW+&I6E0lcw@;F)Ame3?Q*pjp(}Pg;4V6{_YOx>WV1Zt<$Bo%!7& zm47V)E`z}tB(p6Qvrm^ekJhmiHx77HdpzSP7YuR5`z!EaNLi<{?T->VAvFHzl6hsL z9H3qJi3F$zQmDh0id&TBQsPLC)97}G4R_pV^&)r>i^DlsTF6dH5GH1YB_y0SJls%r z=WHa7ny6nyt@Iw5&C-x}=PZjMW&a(&nXz z$vZuLj^t$vj;mEaz&O)z9DZ>enT9w$as7_F_wL~ZG%O5rh}30RL~|-tV-~qorTh`3 zlw@OwWJ5`L6FqVhr_>gf?VrT^lu%FoQ$s6z~)W@CyzM%+n&1;jT@tz_4-&=!mZ4gU_REi8&ky}`46~!}8 zPSn#+EsF2bVH+g7Zm^&x*Xj3agIa*HOL>4K--c>Xhx-QVB)cI4I z#7eS-sS+>x;9i&ix@>~$NTdh%YWNg|KeHk!{gbACoqk}E5kj|r#NL@siEt9mobMfK83uPWm4 z87eLY$;B0J8LeB_Ebdx9VB^IpDbBX7?)?O~c2fQR04q<44)A|{AzIu^M>EnXAhq*H zrI77+z~9pU`r73P%dE}*K|kQ?^ONosvkl@#kxk4WZxUhN&t#n|^dLP2ahG!=SV)ae zNzXjI&YsOGU~q^0nCFU}%W`0W#G$Z1t$1(}f5Xc4<&oNB7OMg>A=EhJ@Pr*^Ime%+ zyX7btrEqe?aOg#Q?z0*V=`3N`ozxwJYbdBVRUFkF;0wr9eVrkGrG*o;Wj?tVJ91VP zt4Nb!lE|5Lb3XsF5jI|l;qAqCfa76vy873Z%GU}<7n}JxZuhSFS2L8&h=t_+ zFBo0g`>vkGAhshID?8o#1fItMoEP8A$c@{iT@&cvoP2(g%97^DE+<`$KxdZ-3AYyM zbTSfI+Z!UxvYG8O5htZg$_U6^fUuQ4b_oAVt=b!q3OMe$rw2pwR)4fhU=!H>Rooo*V3L1(kTZ~by$HFn(dq{gdM=*)2s0L9p8av zkG$$0<0+LCmNa+lNGy>gEX^6Ma5`AS35C0K8M2PC>&A^MtJF+5UQ-_T49a@?_({qY zrzWqAFb}mtNoJ8|s!h3LsN)G+OC?X{k0f26NOvqda|26SYmK|nK=7NC(=zDG*7}D< z&1LudPRf}4V~Dqf(&Bg^CQW(hG#!9NN+pc3c>miE+J4opI}YeQw4sY3Zlqx9zQp`) z1k<;xB3@QP>6%ZxE$4dVt!ECu(#ytiFVeV+NUNMvI1fdK#i*9B3G$B6abaC(DZC7v z&-(?)xM$i`g!LpnRlk{6!JyD5{aJ?*-`2J-ff?cA&)>Dnye@CI82RgDRc=4Mp_HmJ z%$@i96LatnH(Z_)ro|+6mVED>@v#HCsuXkF_eW73`MIDxuUD_w;|onPpZoa}h&7DJ zDM*EazCVTyx|#pZbSM~t<_NH(oeogHFu{VF8kG}6%c?j^INsZ0x3F+?n043c<4+#| zU)$f>P0jBL5G8^|w%ZL`3XgOWL%B;JvFg8mdglJ3wvxe~Wm$0C4w&9=DCo>orzP~Q zriBanQD!R+L+VO~%z1#K9A`Txm|hW?)bkrr<0E9YL+Hg_X2nT@7ebTJIF*-(3p zZmjnC_i3B|Pd@n{(tuV0X;7Iw8zZNDv}P+q&IBiwWCu>%51N`OQKHG=qX54dDEez0 zV~mM%oM@0_x5$r>YOqB5c)Aiat%l(^T1>Cz-wdt^W%LRHDJ%$H*Xz2TsMUQL>1jN# zVviHIFJ(cNl@}9d2BO=^B4;~petZ&Xm*L$q?cHUN!CPvSyrm}xkKh07Z}xrr&o^p@ zJ-lJUYhQjktK@fgodD9Bt2}z&o4bbZY8^Q9?zQPu%y|m@|Pank36N)h?Vj5xzMy<8EDs>zI@GY;ifL<8m-a&oRIv zJ;%T=xNsOz5}cq)0bi=5kd$za!6I@D5>-`cTvT_Ls*;hKUTfVk$ABZLq&EK4P?2NE z^n22h6ZLDXAfCqSIR??Yr0aGu*TK4ddV!FeLt}mE82cxJA}3*ZCzY5`0x(XO8Y6v8 zh|MZWouiwZjCylZYAOcukm^tMXLv+jEXI&xOhH#pqnbHM?3b(KzH^qqozdlg1Ggvr zKf-;$K*%kj`fP6+;%Y~3Hc&*36KKb-X}n#qBX&~<>|Im4W?qGMOEiAD6aFSU;aSKC z=JpOUzD?9>+-*p-sS{eWj+P@0=H=$_OFFND6l3_O(JA{#r&;)xd&4;lelpcPloQTj zpmWJDQRPaNiekmsaNCK(E0tngHk%U8H?Ba(@-GOF`@buqAl`ZTdL3dofAJF#odP1x z?*W8&`il7-VDIASyioT@?n03%{y>n8k*=mFcy`6k(?V)E7QFl^!d#*AISOWzfSD0W z<59eRG}!@=Pb7fUblrCry&I}moDcK}b#wEgl#=A6M1Bn=Dnt{6h$!%;wNcTUFWZ;P zqqWRHQM`!J?5;TC%^>2^B6m?HMsSh4LHU^hun~hNK6?AfhRx4B!TxsnJNDlopLlPO zp|tt425O%-W$yI5X3TF=+y#Mc1BX7erg1r2`33ue9R&O7FTplmUN`5FXIdMl-naCz zhaXvwYoqsoS;g9{6_i)%UIN<8{ks0{8Say?0Ke%~H-Bc7Gh;R3cm7_pnIEy;GuLRn2_?AWyJltjy`C;9Nr~~f?p)D}qo-CP`)GC4KCaUB*KY`q9Z`qy*pc6M zgmE73Uf$$;)z+Kj7l7 zCsq^*!SmLVYs1b;&T@!p^8`y9Y-=ajZz1gKL#RY$Iif|3=o*L;8OzmSrzH2t%|X`l zla1v3lze|U!_tOB?u4VsBKEv~pB+ZN*J23nEx$jUUy;ZdazZYa59&3%{EjMK+)Q|G zhNw}utqpIlA|@m$!D+Wz463*UK+`W!R|Kk{inh4jfWmQaYIbqz%W9 zpBp-);>JN$6_Pw;Smh0aDl7E<)Vj+%^zP8f0U=mFO*mFHm-Z7maZvV z%{#g7zoTe%??+lLIiO$8fO%8lJqvp$vvA%Nn#bF^awkr1cm|xjv#VFt)R9lKOZ9`{ zxO>C%m3>)$>qsNMtk*KkTtMrYy;^P70yTo@%PQp)Iynn=Q3h$Sz)5Le*b7;1aTmulay`Z{s+?7P7`-OqNZrdzGWaofN2XmiDh_eGG)ny=!nqd)FmtI`qEh*sJ$F;|Ot2mo`FqkHix%1Vbhd8sv1oNpb7AQF=1?QM0C~ zH7Ml#J}cfj<%|TK9lV;{P9w$LPU3y|Xu9)5Ng{~kit8mM1eG$z^-kHmHXF{qFZl4Q)s5yEbmwvVP#aOz&c&8GZ?qVG1m=8uep$>77ge zI{%}~EDj3-3UQw085}6rQ#gGhi##=W$dhR^LwZ>~J7f*S$q4Kp$liJ$DzpB662z%*l=hII= z42Bm`1agNDdxqZ!Vpy=OYj>WwxIWx5zIWE#>CKV)5t&7u@%9a$X4v&JUj5iXT*S;T zE|uik=sTx)$Yi(MHBnOq1YIZgH8Uco5Kf^i_PE0ib|mFkfj`(sFq!ztT%kfdr} zUXR)Z+%9S4uZC4T`Oa&lFfr|^!SaVUS6BWb`L!9n{xB$6=uH?YACt<}?V`@mqxVng z!512U;bBKiA~#&6+E9y%xTNw&X3ThS$;{gxeYUV`*TSAXyA~=3r`~_>ZBrNCKRGuT z%+2l9ORwcTEFY6Csui*2hPsOT4#N?n0+GAuc=xW;9v2&9HmI`1@1fT81~;!LwWfSg zgFI)|ox-8C;+U1@<#%QeA6D)Y?^oQx-zy~rg)7#30_nZP4^O8%|4GMd{r?}ntAZWU zR=VbA{T_iTsSb90_F3dP?PouywLh0A?Sb{;KCUjIWC-8;*8XcIcu5h__;pr}K%u=T zNVR}9eqzD#60fu;z7`xa*>_)cfTQYg+A3Asf6E2GBAS;r>sLg>Dr^2d$FEOQcE;~# zpF!4p|0}A@1$d4 z8lz}!$H8k{5eL6z0Q5`Vpi&7kL*1Hqcv=iN^bMCc$;o@0nIsIPQO-#hj`!K8^^UDy>`%;zm->txFR&-5eHk<8c zyZF@#{Ju=D%Uj?nfS~x*3Pt?4Q_%05&$5NE@JusXsTvDn7toVWKDmYtY<+M2=+X1`JyyRRLO~rGfIv+6GAx%zb8+7!Ucc)(g9N+J$;_CwjfcCR0Q{ax~*We;rg_V8@~SMg=i2TZ58 zy8{K=zJ(B$WSSiAX~O|rU`o}ztMu55ji+NL8PjxY+WwFj)8+j_43K811e zxUgR>oN)c(P3~9oC_x@~X)S-DFTn2-OFBO^ST6M^y;q{G~mE9b6t`ZPTER52e7I^B+@M&|1gG4oY# zP*Wo_HSyFXpC(Uz>GL#LJI*sMKyKvoqO~|Ep3v?jJ>dlGlqws&)b_JB{$Cc#~@_zyK<12Ll0C?JCU}Rum zV3eFS*=-wVJipCX26+w!5IB2P;vS6tSN>0ggO9zKfsuiOfe9oE0AQ93W_a3TU}Rw6 z=>6LOBp3WE|5wSu#{d*T0q+5m+y<@y0C?JMlTT<9K^Vo~&c6*MNDc)FQi_O3kQ$^& z5eb3dAp|KBN)QR9NRTLa2qK}B9(sr%BBAtFp)5hvlX@y^>DeM4L_|d5tp_i`gNTQs zS>LzWLeL(5yxDK&o1J}cM-6Z}1;9)KN~qwT-b2Tp#f(|UHU9#N4ydY==%{V#HVUSW zqRgo(ifRJ|Rc6mTj!nxrI7EMd^Jj3=b^yDC&}PxL1B7OU zH2C}uZ8wcjJr$y+y~=tAq5lw}TO*5H?-DI@u8Bp{L(Zk~!p;KzF88hRJBOr)^W3M) zGpDJuri7HPM88enyJ9|}W-|!P6zbHv*+E@rk>k6ZEg?`XY^YYWYJSDz!0#iFy7?Ke z52Q!;5a-uH1(PPggpBn!%;__jHcfAjT8+I-yyv(}q}C!XUbBzeJlk>i z91Wd8-VBl+dM`DD=s@4$S;fZ`^5l|y3w;P|0WI;{dlL0ouj>=IDE)pK=Mt{d`$Fvd z5%^nFW)bHw;-x4vcth`=Q3LXaS>+FN_!pjQEgmzAaU=`L%)X+3^!+IO8g*)v!#K>~ zG5ues-Y5I9|49!2A^+HDesdhjBF>r`XZaRw|0CDSKhnpJ+42^s@AYf?aF@9ys#XB+ zD=Cb?cj_wj7U$$XBpBWs-mR*)i>#m)P}E&y1#_BXg&XcOvth6L!MjDgiD6szW>#sr zD|U#CS>ib#ASa}P5j;2k0_XDC9(dYgU|`UJ!YGC&hC7TdjL(>Im^zr&F~(9Lo-tU#vc?D_GC58L>@ZJHqydU4-3%J%W85hZRQ&#}Q60P8-e) z&OXjtTr6C2Tz*_NTywbYaSL$=aJO+^;1S`;;OXGm!}E;SfH#4+gLez>72Xeg0(@qC z0emHVFZjdwX9#Er)ClYoED&5JctuD|C`2er=z*}6aE0(Qkt&e~q6VTRqF2P2#Dc_{ z#14tQ6E_hL6JH?yMEr?_fJBSLHAw@>BFRNkd{Pcl2c#{elcXD@=g0)fprnE!pjk1)o zi*lawEad|#Oez*CDJm0G_NjbO6;riRouPV6^^2N{nx9&g+7@*)^%?5FG!itX&upK(st6W(O#l`M*EwNgievpGhHEF2i-i~1-i%d`1JDhZs6xQ7{QIX)xJja>Y~v2#rjAOf!IR zk(q#5joBo#59TiBJ1i6|bO5tMjI#g$00031008d*K>!5+J^%#(0swjdhX8H>00BDz zGXMkt0eIS-Q@c*XKoA_q;U!)Y1wx3z1qB5$CIJc2@kkITf&v5$jpKw6NHDUE5L6VD zd1Hxh4{-(;JG51Z9PHA5h8U~#)OqR(aUi}jbwoyn(#dyP5ei)}v&O0-?@#`| zh(+Ck-k-3~NVsL{pf%5!9dypE`|Q>ICA2PMj_XpEOMiQGU}9ZC4Kn{5m$27! z>8c_#uac|h?@G=Fr&E+}D$gD~s*DO!)ey#f}mn$__ z>8-crjAU}Am#%Ui&|BgSt8)_bg0xlDz9rQ=T#Mq%^6VU!(hIHsCie+l z9H@l=0C?JM&{b^HaS*`q?`>V%xx3>||Npk@hPSN6-JQW!fw7H_0>cTefspV9!Crvi z8uS4OZox_58HWep6}t7u8~5_bU2>PZBZ`*zt-O6H6TNB#=lF z$)u1<8tG(^Nfz1UkV_u<6i`SJ#gtG=D_YZrwzQ)?9q33WI@5)&bfY^KG<2-kuv3PE zaw_OSPkPatKJ=v@PF(b-5;qsKztm7)X`M`R%vxPkz=8(j&nYXNAml(ywHZil28@!iT_Hu+@{Ny(WIL2LW zbDUYsW(U>Wr-nP+<1r6-$Rj?6zxRwMJmmyFez235Jm&>|KJ%4L%pt&B=21%>`>1C= z4FqW29mJ%s7`f8gR{F*6L z7qD0?l@Xm5rOI8p(yFv8E1K2AjY>_aE3HbK(ylC1I+W$gfAgFXH8oe$;=BQ0C|FZn z)##6ubWcRP(qS{WL&5sy#I5%6xFY+6)s7ufE&OT;PRhH2VnIddj2OM1V{s10Zss$|FTK|umAE+ z00+SP{}^I`{(owZ|5OhDDgL*L8^H13xaY^Wba0tuzK3D; z0ErQCzXZeM3TYlbE0TB5=(wu9TEA0F0kV#_O-WHCYTINIaR<$uwQZ0Nxpu)}8+Xo# zK351TFF*2;cWszI0}81#x8Q>{OVh4Si;T2Wv^e2w`sPYKj03-h9dWHnKQyvJen3)F zQ~t5j^`_lSa&+Yq%P4F5DN_8OQT(#@Wew<6RLxDriBt+yG!hL5f7G$dP_2E^!85s{ za-U*IG14NkRvK^dm}bzHW9EgVAg}x$aS{7xe8i zxe7lK)YqKme+>x>K!5r~Qe!D}VTJ_@BO`_h{)KQg4DM8fEUL|RDj1I%u|g%wDCb;$ zUUJN~PePEveHKOjdVJRo^@_-DANoF$_W{}Tb$k|#8<)F8J*nLGDr_Ot7<_~!`Uoln z2)7B;!;APxn4v>PBdeH-_)z-6$Ndp zcG5TnXz3?T(fA#+%(LQ7(dR44wb#cP5jGD}$9XcJsEDsbDPb%(rCSXfa9(cKZ}NUNM!cMtquo3vqA5mV)*Yq^kfT~Z|~ClbvjoKOd#GZ z&ai0seQDaME7-YPDqXASvNO)1aq34?P0vLe`h+OLucG_+j6!ML%sj|P!uO;F&u3j~ zy~*#K^AjF-_x&ilh`aSp2eR#$tE)ySL9RNfy{fZ+g=T#13$MF^i?z{&sga=(F)T`{ z>Z!3TO2#U9lk}6E_~D55v~nbuk9`hA!$X-V^o>93wsrsPf43t@C(lifQI1ejP9Gl{ z3X+E*zT)~GVt%dglSn&yNsS4T-u1RwfIWiokR7gB#RZpC4SXPM<`At zRNpRJV^hs4vS3Td3xZLK6e@h!(EcbyZfZCyWF{(tpEZmO@_k?*E5=7TLOf@g zq3G9kDdYLqP!PJ@B-NRR!8D**rY`O4J!V+^Z>)i)%cPpGrQ=@T-Z)dZy;3K+HTgpl z&7Fp3*$y<=?mx1F7TIZ**`+nvwb$4^oH#%_X$@0lmn*QmZ7ZRpiNc4$z@wDJKFo_> zjIpXJZhPqboJ73)t~+u;!=o9QEa%{9-%inEZw6KVtM)`HuOMxLI#`W%FuM1cmMA zF@Mz=Chin#OFa60HnMn&6IKa_+r+u&;kwI5N5B+_s-N5$c@OTQO7j~OaTN+WJe{d~{Q zAZYbleP*?JjIn&l=rLET33_DibdFnC|0i{r+|AdL&05D9tq|cDSxU8sMn)Mc={Q>R zu0%|cJS=%#j#gLTBhM$`nIgCz*LR_q?~BI09k#xEPNuc@Y7t`EU!XV+{LN72=jr9b z{nt4eR-BM`5)zn8a|G|a0-AKi(a+Ub@YXcx2Q$Sk9y^*vSx5R2&{0ME??+WqE11*0 z9k|F6Ns)A<1%spcm1SsqE5Cp|g|KmTD@o{xu9u>gfD~c|iP!cp7!Cb6l*Hh$Y?pSY z2Ld=3q#|ck4PX|&W3ZwQzz@0)Ez}fZ?eVy9AriS;p%6J3W~n*QpPyLB=Bu}fDpZbN zfpqQ26=}wVW=r5oOgN=0<)FGv$aG;3l-DktOWGT4{NZ4O46#ksO z-rMS7!+@TtHojltg?9NC2b%_`dmOTLUs>Vn_ST;+d`hLKO3Jcs${5F@0rEx&p>2Q3 zKKhNBDq$T3gOrR#v6@cgjMnpgD9W*lgaw3(NHN<9E zO8Yq!9^%*cU;`LEfWSYY$e=K&lGyQ-NR^qh=wpnNCmHhW3gIQaM~Ue7G;C+NEpzY7 zRNzD3+x>=3jCm1LO16SO{<9oPwVP1&$?sn4XAF|(Q)E>P3Nq~^DE3&C#33SA=Posx z_9;!B#%(N#SKg~uX=+Ui(}=l)SFshb0`Ewc$y=(lFE?)Q*@C3-8VRn_*K(vy5H^4; zwoTGN912$G>xR2^=Nx^bECevueQ1;+Hvq8^Ak%Q+#e^SUoNGaxU2S|Pru#B&1k*iR z*XfdUD+Cwgs7<{qMmk!Ui%|{kDau_V=n~7`zT^|-v41BFT4)HQI}#Ty`EnIefH-~& zPzYDc#VhY(qG8L%PJrg=Vs9)o?<3U60)NCfYp*Y|*$lVM{P>YILeKa7;mkpdtOJE% zhQY?yUYL*_*d`(%wI)Yd*TcfSL^J_p0cd9O=%w?`bu`3W3baZSs39`XEiRH2RiWaW zQe;oGNUP3H;@|I$I{{67(ZdTv)#D5ZOAz94{0odOpc@3qj{V3L9mpwM{7@QA0!UN zaYW9Fbwjz8^|M}~cLpf|G1kzp!iO+afWPxwf@ktXSR7!cNd4(-)1aThWd}Dyb;_6Y)$eD}Z!Lis)%1#Fr z7K4r#KJa51W#NHOxbp-&nYZ+%dg^EN5je42Qtv)Ns(77v8o^BVy-g|dRrLrSwPvkn ztxW#=ubRJQ6HjqlKASn3%>cX*tMnH#{y~{}PZVkXEjK)2*p8(=_Nx z#becxK;YMmKj`LvsY5v`1IT8Ynh8){>}o%;vT2MC^H1%1Mp@W@K7IO7Vz^=L61GWMLK=gPB5ogyt-qySy8*Fv zGTZEu6^IhWh)$#1;Cc3kTj_Z1jb#g@1UM*2Yck_+D2_nnvF{Ohe@(zIlQfVYiAr*6 zWOk>X^zekQ(**kPfMG2cW-`^a;24T(CkmT-mslQ6_#+ZKdtQ8znIq?iZyXwlWtT8? zOGnr)RyCNKRrkakhcDgPDZK8_)uhn4jBdD&*wNQmEO0-YA{e=Q3m5A6!u+!nigBQ`@7jBs6e zp*i~_sOD$C0p{yc0-uVtrDIf))Qdyr>3*EBB@sLigUb8}`_SC}`d-0@C!6~<%WND_D6|BHm>Ke>@OE@yOrKR_=7dJ7+Prg9FP3UMwrnH=M+!EJTIkNS zf~a_bbpn87Zj#;111TdA!)d?>a3{UkS@u9tHFO~#(+sv+Df+eqEi$EHW7_)kP}1z| zbo=?wL)w-3*&%j67v@jg`oZuO1Sw3&3*0m(a;Z640PvCZn0JhJOeUNzuy?%xEVgC( z(`U{U$!}NY?iTKxtbrtDw}`ic2ji~aP9~>rHA6e9#XZ7Rq?&BZT4(gHWUQE$&Lt)N zdAUTaC=0@Mu$sZ0KDt1)VmcanBy=zDn#axv%VykIlI>i9yiKBMm-v#Ga?1)}~*7+2gSOdQaWBCN3tJ&k-T(A{2b z9vA_F%>g-;kEItbq`?`3!J@VuBo0an{Ja6KZ#&9kDZYEn^moi$L*Ed?&9l{T&;-i! zilaIV%{@8y4kCPDY#Gt=@gH@x@9g_?0=s^8oZScA#CckOpL}@?$KmJ~ zRa^)@uG1`oE)Yi_Tv)$Zy3xje|0P;2h>2A83*dXy9ik&X3P}6)h5q}3@|fYc@f3|= zjMfsA#yLLs_k-%ghuoyY8Or-#$wnS*D;IcYn)bU0t{tePlfCeN`t_3v#6-d9_n)OE zp)N6u&9+eIm4~j4;-gT_7>lz6szlQ{$qe8CJYzS&nCaU<;#LAT?$KvzL?dL&cHu4> z_^@C{d>OSoN1$x5JD1Mhm3fhR!`rMa7a9SnmJ$(cJWTER7}2T6VIXm7EKne<`D1(t znHGHwHMjH@^Y2}Ay5mFU+(K1&x^csgB(cTnau$C_2yLi6&>&))A<$V(Y56z~i-ssF zb{&oPmXOY(sk!G=J_SVmJ%}rXEXzijl@=}3UBEAcx@m#WH2=&{BPh$EUMdF+mQ=#Q zRV&eJK-uG}sI@L6paV;uhn`w;O^h%Wq7zV&sjopFGiBYVnlp^1DwW->aecPRd8k$W zduGf~++;`yjko4LNYNT5Ae%E=5$}4 z8l|hIHp!yYO7u7Uz6@m+TFJ|;pzN?GWc`5Y7WEx>MHe+yjh{_>MPq=98tO4@>4F;9 z0bAs$n`1Ze#PuFrJ)u5we(y^jLns)TC23PTL3BddyMvV~+e*7erxg#AYz84D;pyGrkT6T zS;#tub~f9DBh3w2vwv(|32_a`FcZ7vr<##|JAw}H5N4ra>fS)&Y$WR=wP<2uao)0i zib|6 zfr62&nW+zo(q{^vgyxRSEB=u(IHP$|yQHsdUrU;+*^<+3X1Cto3doJQjg1RgKZT_+ zPR>WRtqm+$*j!EoswYv6%hJq|MO)>q$YRhdO$Hf~G0qY|3F@;AnJBTyUGScQIi<}X z6->Le{E%OaUIW-PdN{KI0B0t0tNl%Kc|&7ndsN)rd%+?OsztRt2 zU$eK&8UtU!BL*T@s1A>8slKhS7YhDzKB1edY#phVKsMER-DoU@73h13>lC#_Ub}rWuzV&ijCAj5CR+i;|W*t#v&47fTw}FWh8G# zJmDysau2egF# z?8}QHv(_nw&aFsRKY&l!##vq;{*0=|T6yMdb!${h;S*o*YeIQ|k5T$}hAXaG9}EKy z;kKe7y`}+Jg5bX)qFDHdQByc6W9?%w}{O7=%g=R z)^O=cM)huK(SN|?V8J^FtM9GE{ZZ;l#kxXdO}9;&h<3B)y(vgIRzK7O>M@>uKZI}( z(Xnbgxb?{zA6wyaXVL^Y_dyL#jT>9(b8Ta6^Y`Ph7fF1$%6(#Jb<`z=RO-h=F8A4u zx%^0z2g)I6d&26D-g7X1OVzmjlvaFWIxL`26Y?Yq7yX$gjEWjr?j4q#JF7jpi3Fy!V>L_)F4R|z4nO? zH3zXD-J{eOWsd=u=wD~d>;gH`L9gL^NYKOn{k%h4+|b|pr1@Wyb3(9lvA9D;jwTD` zaG=2^q$KDt&7^Bwbo?Ob#@sQhGV2e}nwbBWPYPnb7L?Q#GeLBkMFOc*^E zZq;^ZvFg|0Qi6sOeUP6#O>-ewV#r5!#C>am=h=E<>e7Ty*|II$NDcyY*wv9-t2zr{VOP4`mT6aSNY)_R?_eI*y;5`jLlx$bI+QH42tL;8G6% zJxk_O9bRFXfWUXOJ}Vc5|Ju6fn#93cb-2I2L1hJKlYA!~Z9`N&*&Vh}=e!__u^Yja zo~j~)3gI=hLt4H|Ank$A0FL~S1kOO%0;t0Gli`|kC=-jm$|e4#cyY74oqy;2-p4W4 z{T_PMjYJ~Q#Y3aafS`@enS?afYql8)eTIx_yd0k*HaNK*)V^0;PrhV5mK{2*3=@GahsF3AtAKi; z)&BMO++|4iQDCtswDy>X7j0KMAlZ?|JgSgff_6>+pOM@4*2ZWqZQ$nIKTqsI$-Q2# z*jp=BMZBDOx04jbw`*->tWSSJlv7YsyRr zFwKaYj1K&uG+g|u1KU&;6}oh1#t4E&f9!>`CjnU#DXVNWVf7QOymx9?GOcK?wRUro zu(=V9%TzoWxv-gPeA%i8mp91>>r=L=W3vc`qH z;{yXTBjx1scd0PC(m;$Vo~4;c-BvGbkBq2ZqvG3kquBb7Hh&v7%sg=Dw$M@pU z9QsrIJv6%!=prWn5Rl)&5E^a7sZ?t&r!dhIa)(o)&wn ztqCegFx;>lp%R)Fi%itR#q#~+Q2-B$dDgyfkA1}tvKI;8w2}`MrVIxqh84M=$&Qx! zEFBYUP!B3vM=|-x6r-8+0=xk?)RS2XeqW?NWaPP|u14%grvQzl@u$?F{xIE~=Z_U? zVb6=#_z!ifp45Qi27GTdr;^@@T;RKi-fPuiw72 zSXaZ98WK3})&FA=Q2ZTpXl`CWT07_bhq6GGY-5SVl&ZhL?1^qzxCiW`(o3$!g5}%;6V!w zX=Xs8ei;fchqO3_qbHQO`%e}KPBi*iY9BV)k;qWok9<4I2D4zG7S+aK6g-WS^kw9F zehA^u1Y8JU=IM|8OW0qfRo#elmB*5kieoOXXSlBM4nL&t$7<1X!D$3?vzs@k8V}BSD7dfv%^EBTCI!N3-zqQ?p}+xFb0!>NjN-&C^bRlbdah+k1jgk-RJ5;)YFP5BFni4 zQquq0O>N?Xn?EF(i-LAhBRHV4h|<%ZC32^)i;bEd2A1v;==?O> ztnH24e$o%UE7B!FGWv`Y*WAhN5x^i{7at_SLe%-FLYT=)5@_BX8Db{IomC3zAghW0 z;2e_#*Y?nHtJSd`dg+2MJ4Z@L(#<&ynC*3yPg%vch|O`d$Tv@yex1WpH%Di=UpCN4KBuoLWr^X{f z0G_x8mDdf(Rw(;X7|N6N3e0sVPnom5ZYY!@u1P&3OVuhExD&bK{w_|u(+U?2)9JmN zVBZxRRvTho?tZ`h_h6c$JcP_jU}y(VH*BASLbFlSpqbN2dh{Ik``Z3>qs7FSgaLG7 zeE|Vl>o-O3X294vz%rT4YLq+5qEmk@d1e1~;}_1WMKSonVf@W3{$NjafB?NUG*6ja zv&Cl}*V400&(t7l#!Q{i1=Yfxc#i(h({FrtY9sE<9~XNNP5DWOwk@5S!Te~ySY1;> zeqyB1C(*J|(+1pS#Hu|e_i~~@AvUpDFzVz;vO1a+hwq3*`$5QNZCFO=El>BVu`m;7 z^`x#89tlrL%>M0rt0YDIlKL{AtxmHs78g(k2ID|BG$For+REvxww3_K%X?%UabYD} zF|xPnw=cNb7S#ST5u9q{=Sk}+um=JAYXl>GX|j?;^UlG4a@{wGkW4dTA_6^Jp?+vE z%?Z0??@B;N8%L-fnS&0xLia+qn`$bw-J>xa{M(H{wuc+!hGjwpx_homQ5Dlz@Z!cc zv}$V1>QM}{nPWs!wF}tb(fcm9Qrc9xn}56M5CBcxdLdl5Q^f47-b5ZHHUs|2b0_m4 z0gcMp0KZcbmL8rF(a>GbKv}auWy)SDSzWUwnTlYO8xl#A;YqE{H__SVo zz0`>R=05p8Qbgu*I{7EKPV=1y9s!odIK15H&rTHCwPX5U0GDN5h zOAo*!=cj_+t&q}OjMU+ayiARJ*^3=1CpaTDA%a=Y=&D?#cOspMlDKa7s8^`S$>4}I z_2JWY!d6UOCr+C&0zg1;hoa#j+A`55207p$yy;ZDtF>hH65r^Jx)-E@`J)gGu6`l) z&BgZ!TLssxUjC!y^`#^eD>+jIH)C*i3m^P@R*0&ci8;#Q0e5Cb>C#oal3v>{2D;oy z)4Q~)IAA}v$Ky0o3r;*Fe1Q92bhT&hp}kX70U1>J?G1pjx(Eiuk)$l#tb zx01ZDyl^l{{3XiRPdnfo>;%Lj<^ zbc9rj2qjDg1zvI};j((E20nRzD11>Lzbs)EbZLHhvE63&zJDBU~6Xa&Wh0#}-ToaHi}7}Bo3a#s@R zfKI`FX8LDCK6SPquUu{UN~gh|b~<(018R|<&evi;=9N7Pp+G_>YY`~^Xu(X-$PymH zneQCEtb&v==X|W~L?kv%sikb$#Woyxej?){VY}!V%za^wLG_%}xiwBSy;UYVu30V# z2w+FlT~JCiz4jrn3q@Z|?C4MB=8AFb#L*w{@O4Q>&m2@|CjY)u`+_BTA{MI}2krT1 z2oDo_*4VV7dEh2wWJ{Q4)MJ1LKmLdu^Nc~)5*c`lgU;i-N0EXBwInQQUHc;Q3I*2Y zmngG8Y7(-2fgfe3Pryj&6E%H2K63Erk(>d_d13>`6{`ytgOExh+F)2v@<7r-7P!X>gORv(U?9_(8W@`Y2U19 z1xAoco9KPfV@Oy37paH2sGfXsyUr_&yMs)38(c>kg=B=c?Y(?UUQy&4bUChIkkMd) zDCjHy0p-WEh%u%(eFZTeP>t)|dK-Fe)Z9tU2YyKWGp!VAiy%Jv!2UgD^X^H^5!q2C zH4P$JA$p67mXLOhW1G0NfV$qDG_@r>B?62-TiN8uM@4rjAC1&*<7Q11DR(WN8WRnf zO=r*slqK7wcDzJXhYe6SWre#EACyek*9|V|q9nx$-|<>5%Wo?mIzjmDeswP2&p6@| z@wHUU-pV{g=T3)2hB)W3wjY1>PMXLht)h_>-n5JfIoeQ?IK?;;nl(vDCpOelMCRHb z&qy(PB!EWJ{me`}Dr3NGO=8|Z;TLIO756O@xdK`vWlOugX=vsC2bAu^PO%WzvS;^G3GqIFGBQzeu}A_#V*fF@kP z%9YxC45E|>aQ6z+Km62F1<0wIHhu%v7y3;h)cmTlw4R+{y;F%Yh4ttnm8U_sbv~a; zCcvN2(#=uVjKK8veTjOG>S5wQfZ@rR(1U9UF)ZVS10PwindU8DxZBE%%u(zyG-QG) z0u4%GBgAYY%!9G}etyZF*t?8c!>86(zLc}udk^*T)49i_Wf@VDWVuz|Xrbu<^0v!n zi6H(h6RGSX6$Xpy@RYa=UcJ}T2vPb0yKaVacyq+x%mG{gcs!T4xSW~oFJ@=Q=h>7l zw*|6g11FX;l|d?1fpu9%#aCTtC-K>)TnI=hXt|jQFwNQ1*Efh8CGFUwBg3Nc^XUpt zvCfT|maJ}mY5K#zLB&{zs*JxX8>9J~E*|a#u6ba_-=!8H9lka3q?X;+%#9icL}E*^ z5}xCgK1tjf0K*2}7`p3q??#U=Yw@Vu1Oe5Ra%puAy2=FAbi#JY48D?5(STk8thJeykzRyV3)P-|!xKjBEln5x<3Q^Z~Ef`{^5z zTG%1e=7<|<=ebv2&%6jCIqA=e2wMttHbe;D4?K)B{bfaioR)~455ADx;d4*VMW=y1 z2WpM!wuZJ7tFwwWM)ig>Z`?>5t%k4s~QOWU; z!jL_8sHWF6iXMxNM0?|bABK<_J14;A>7HaJ@P3j zm!}zDWIN`UIa5K0p_yzCy}}-AkM;K_0Zelsv#2>DrkH?4I!p{@7OAt`k@0CHs=C7^YM&YsEi9YPu@Rd~? zlJ?2Lkd1h8le4Kv36Py06g7X)n&DTNz3rtJVPY(?zHbcL#nI!K{3Uwy2lt%w+XZsr zHUh6}N}7V0z;s-Tx?*y8gJ&bP4(JWd&^dtJ5F7UIOA?FboCkjT}<@B^!FeCw|)>3Y$s9q%i4Y>iS1pg*~?9TGanZcch{nkE%+xTct*9BB7q7ajLdqqLC=WD!4+ttCf`~ba^-U`j_diD#<0xTOgt}HR{D)a#|uyYFZ%pcTmxhtmi1QpL=c6{mK zgQ{0sVt__enH+BCAiGw;*X#&z1i$ix%T6p31A^|+5Q?=3?{CW^-a;;5$)O_KVnODo z>NYAi8DTJWy~RNsf%E$f@GoLc*?!B2lEsuA6wsP8&n1WHU5cb_T5EB zRAg*^8_$UwMjt;On@son$Q$n|xEPcDryh-2d$<{`Zeccx^Fu#_=DmE7ESlK#V;8=6 zy57~V7|D-u#gPHuxJF8uFWb_Ar&PdX9mB7?@E~o;>O~P&_D>$APjcAj2Zkhb(`kID z0vdhiO2%PXzkO00u=HY3l?nQp{Qw?%UGMdrJ-B`?^VAw!*{p!rkCB6A9ctR zb1#dDBe_T23W44Z)W9P`&hPt0P4_=NQHuKI%Pf<>%87rgk$TQ25WWPCxd_3Gcb-0| z?!s~_MO^S9V3fQCA0 zV?-~PdN0I^SXQ@8i~FMb!`rXZB@&T);xWaDirCm3MOG3`?qInr69o-Bu=h0oOK9zd z!dbet#DHmb(zIs=NRJM`Q>1Uv$?rTy3W=DorFAIEdPC-W;subH+s=-8FZCbU?6Y5QQeTPOV1ZsrLoNLXH79!C5;p{t z=T&g0dN}a(FL`&@{~Rhwi@GkdM|Ve1PVZFyOmVluGYHR=ICcfq#iRf9J6A~W|KQ{b zi1_eE+WhS&{Z*;H+TM7rYa+%LuIfwvYXXfd77LX*uSTI*rZZNDQ|Zx=G9@bSRQ>$SM=uG>j2Oo8BSl zLHvUXNSy@%WBG@U)9fg2fw`{9us!HfnV=Wou^uM+oEXY|Y* zEDuCce@p#S(wZY82nYYfMK@Yo)D+x5(Qg^Zh7^P^Zh(Da*%f}Da9dGbRL_-@{0(#r z!ZZwDm;SL|Fy~I5?)BG>LKqB%E|5k3a?`|*Zc<~lhm@n@>Q1%OH1{PC9VNfr~tGXxu4I5uj zq-6S>J0;{qE61S8HT|Ty+3;?qT9bA?DqOZ={g*M?i@|L1YpHtv! zpwCJa88(#D{Vj}zS_7v-1+JZ)Ut*3JAEfS%X{>0YBu-sP1gF+Q+Epqe)b@9_en8eF){FDs}D2UdYrn)&Asa z^-=i8YG1o-zeNlUo&LwV2)kaDmNY#*@B1fV@kBkddZNT*?p?EWf%MVW@o&7h(Nh7} z0fDlXUb|8?F?gZ~JE6)DRD3)#B!R;YUDSuSrKP?t#^VE4#XdoDME zHy4ZD4m#4d2}#7qnu_VRCH?#`SOtmhi;dZh0_{610Lh z+kM5}lcrqCegb0{NkB+N2@88)Q-cTT>qQ*_$Qy!5f2==F*GcBU*kDsmk{+w~ZsH!x z)87KIW|@a*W|UiSREewU^NCwk&AcvQbh_XH0~sp|<5)C;DIXOg<}T6?Z^7bt_r=j6 zdFx&gL}mV3ftJcnw@h<;!^_lOx|Gp7-sar3H|D{o`>s-z#yHq7uHO(%ZD1Lj&hJjb zBsM0LoH8~N!>=Qrey#+*FcxQ(hwZwoq81QWp1jA`oLBCP0WpxoIgGdd2IPs6qM_7K zhEpALQvFp&C6p+^d+@&p1^7p;wTQhGpBe0IaelJJcycFvxJ8o=_0BELOACgk@0qk# z4#(>AK30;MqqdZTXGU7>-2o=%uvL6TYCjwYGelWCi?@^{l#Pz7#Y$`6B00gA&o_ZX zKrZcPVmU1C0{OT_uQDWtsc-Mf6j?LWEhjmlS>;3+wtO(*Mj50jsSa zejET=$i0Wp<~kH%{+5O69bbqS%4PqSViwPZkPalZx#3$YO1viB+qd8ID#lS&4$$6VCBm-WCgAy$}R??5reN}ir8amzlZw* z1PiXIqZIH@A-VIPxuMA3chwHt0|AvkaJ`5p#ux_V-#^?%PN&c!niiLhQ=y1H=xgm?H_9XTdC zU~L>zLo>;M3~~;{k>9E81l91dE#^6OkO1kc8c!`xJ7IJ7<-k8%|8-*f^z+3?b9qi7 zMAGJb&bAX9?0en4FrNECVUn?xi>NnV?%Ix1Ki)7!iFf;XT>GHpb&w0*fSD9#M?HIs zC0VUU%$o@%N|^8F61uy?BMZS!F`}wdPWpLq>b02wIfb8+D8yx;ioYYx*`7(Y(Zmn7 zF$YdORXyfQh`KiW7yhuy)uRx_Oni7Lb}OxqjKZF%LHwf~pIIrgk#h_X>Npf%iuOg_ zBX9dDNuHXoNL5Ex%$L3|#j?i`L3SCWhHYyw0Yuuu6HCG^KQ@CU06>!X6)^WWwLVI< zBj_}H3&cot@;_4v9`iVKi&rg1$}wzBd6bd(GWnmkMPd7i3m$mxX z#Q)wv7K36`&bNpc)r-Yz1+_47UfX*SKAqe z|HH?}i@^Y-oCjgsdvRTKy8)aj6Ys}DVOp?sL!Wd^il(Ro4gpS#Bs6O^_{!n~;w)Wm z^&*nlx=7=GEe@C!TG^dHZv$a=f)nLe(~sWK$H$k94iO(t$;D6L|H0i9?up*EZgs+y z0!ma5{x(BJ-I%a6uvgSWEGc3Y#4N}%`HRf9DpDQ`ajT5fgj(g-vPcEOwR~buzgqF5 zEhsZ`@$B#ZK{Q5mmCq;$bL>}&j)=NpYb>`4Zm96v1ECzE`8;sHC@55_38fN-IFSZq z3knI)leRdlA!@>O#@s7|Ru;B}$bA`lZCzMWweOZXMQ$L`p`vDx4?fFXQRh5HRCx7{FKO#DTZfLbU{7)Fu z%%^PCQY><0Au@MBV8rc>n%si?0t&bD6hmKk&LpF9&=^HiCQ;bTd8k$Nh+3g*HdvtTzx9;(^QTRGU(| zNmESw0rlc}0bvF-U&OR8X)()6)i$)|=lO>^vZcypN$KLMUkE&Ks1@8Pyqdta3RrvZ zUYlQM!wmudnO|H2baO0%;6T~+1++AuoZ9`k(UBskdCuahFrb%JZsxK5S~AdRh__m5 z0GYBm7|xGoXa{+hkZnDWtreWxF+hwU%_v#GjIhuURE1kO)5If9<&cWHB*_jHV5(jtcm_i6s~-T zCG4(Df7l&i9yra?vJ-$I;2JByOLZ0@Lj})5Nu?0R{|O-u z-tpQgyTx^j3YN0-^02d^pezyb1IHTe*&YFG0%vo)VAgClK0gh#_M1%o6kI1~?kI1n zgK))gyis^ll<*W~wsR?)oX+VCssPdcddd({`T>JKq)U@Ebv1tYcMa))feI1*B$cxx zY=|vVnOB>j&d4`(>l0nYF=LDllI7M+PfZl-v~HVPYr##qU&mKfmtc?>*jIrLGGU1s zdjLa!B3L|zI9#bPwWvpm)Z!~AVidm=zHhH?Q3q{UU^pigV}yOv=w{oQsCuGVJ!;T9 z@L-G>A}Y z*ZXalv6=0?VHP>Ac7eotV}*huG|Upj@f)Re2h}4v2bd4w!0mUJSR*VOdC68@u$$?9 ztg}&8`c0Eap`wQ50xdUcv1BtupaGc^i8rK`v{Qpk6KeQk!Lb7i@o<;OGSXQnoEdo& zGc`!)s;@}Ku42;z&kUm0np^_nQN{%zJM~notkFV75b%aIY3?>LirC={#FP-+LRDB! zHo&hSxWXbM5>vcA{5{oVZfwtpJW&raAR+**ZN@xlJUTvfw-FY=Ocbwg3ECv`FMgY3 z`$cyG?s6sy76+Vph8oL*D)r4eJk@ZSOWu_}xNMV&5HuQ-g33u{w*}SGCsin|dR4nb zLMPGeFVWWEr3Pa>*>-$0o-SU}gM3x=jJ%puj*eYmk{C(>1R*L~=xj*wZZ631dK2m# zorz{sy(|v_v*=y~Wl(zWBjsfHk+K0# z%(3w6(?FW)(T!;qEV}88PSeyki>A(DmpUl|5OE98Qs@iB&9ILE6&L@u$z0G;Lj*y)*g)rh zpI^9;4j_SMfgZ=n`{c~i&!s&DUjb=y3e_15feUq~k`?K74^*V0L84Q`^l*V(whWq$ znj@NI`;>X-5{9R5sj6|f@>jjOb6bY4rL#ii1;!D*imtQSPTC_V9v5&SHXQo3$0_Ij3B=(I(F(lemD4C5oLqor< zMD(Lt+s`zu=-K-NJDj6i&2>Bwl=@=jon(jb?N)h|`3wNQ#MTvcBV$r8J)l__b7fSt z^hN3YZ)ICLfVoHOfL+EeYcl|8)Em+ek9~X9TV}J!pq&FQ zg5%6-3E=qJ!gU(sKB$I{SAj2zhWWz>OLXQ5@`~AeI~yer#X#2bYY3BGU#@=zM2)iu z;_`FDRG<#xU(KVXbq-&C>7!@s0p0n@!< z*wJ`e1^5oWlOkf||H7~9%EbkrKl;iuBLsZ*Mo6j=&?B^)TrTAd%rEF*#Rt#1L}52Mx3xc_0Bm|v+AM5n=OJdJ}9M_~FZO~H~%W@}U-gemSUQqIlAe6c@ ziMK(&Ropb>l1mbGn*dZr<+)GvP-oFGzMz!%!e0+iZ%GY-GJZ2*)&!Ll+pvijp%gUI zq)Y;LT*5IGH6qOzuu8Fbvb1`(`1iw#0AJ2u2pu&>NpWN+cYa(TdH`n;^FB|TQdFFR zi7^0RUyBq5RVD#j9xyA-rmm6+7*)OpKP|j+AX=duqBF^g77RZjqohWRmV?X+r0i;O zGZ-|<6xq>n{C6WTJxDLt5u#2=duJc2$#)vcyYx~Xk(OGNB+P?uVOGF<7csS04tW}o z!7f9)MOh}Ddon#Cz)ItRnM3F>sPm2leV`BSywZ-bFd!2PL}6}B9|AN38T0F?nkZg2 zyzw}KTvaFWbdpZjFQLqFHmy-y*dudB;Q1UcqST(o=Souq0*g^V#}+I77#l3iNRkaq zAOY)rrg+@pnkI5$c}qZoF)zue~9TD3i5T zC#B4rTa0Jnd^S+3-(OeKfCDcP1^kq=wjxGk3S%jy1ZzALoxY`PynGr(EUI#V(9n>! z78JHfIB!?_sfmFi-9mt((=#BEObAGL5D6~o)&6y|@&(D_H z0HBd;fW$Rs-c8XFl}efU5)6|TvnVdrR2AeU;E#}J@u zt3o(mtB&Lr_wK8Wq(2Hqwif7xx`q{2GXukjQ{W^8)%dOFBp9(&8qxK>|5|4BLg;-D*5V^bLaHha=EZkjz8oCx`BpT8riy5Fi6g2k`cqUu(-s==?WY)jd!r)&g5jC>H=-69rH^iFp&ev0`)UtRJ ztY&Qf7txD5n+2id0o({>6O4VPNzq3+n>U{lOfM%~a`O&dC(s z>WArpk|ru@D{7`Rrra{oAd0wJW~6Jq#gj6gK?rGp`eF@na#nofK*-jF2;uj-?tw2$ zK@);z)?}sn_{&Z8>)IVe!sOn9S(D&#%jRqnH3$fW86=Kl-MY?3U+Nlyy{By zOQxa+yBxB8p{?bi)T?Aag~SA0x#j7=9B-6?w3ok=D^Ui-20~!sxS2usVx}50sK{m^ ig3W + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.woff b/docs/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..ed760c0628b6a0026041f5b8bba466a0471fd2e0 GIT binary patch literal 23048 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Italic-webfont.woff b/docs/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..ff652e64356b538c001423b6aedefcf1ee66cd17 GIT binary patch literal 23188 zcmZsB1B@t5(Cyl`ZQHu*-MhAJ+qP}nwr%fS+qS*?_RF7_yqEkvIjOEQr@E_WlA2DY zU1dc@0RRDhn?@1<@_#l3=70SE`u~3u6;+Z3001oeWpVz4p$qV*n6QZGFE{k-`u;zaN}4#cm9;TJrV-(X@UcBa<99LMh*@4q%a z658XBslMZHEF8E7&@{N?(7eZpUmz@dN=nOQrz{c^wS0FnX#0PY&N6gaW6HT=~n{pJC<@{8T1$@+6^ zeYf9vRsNfg;6DIk0YTa5TO0p!6u+9~-y8)juwn@9Y#p5d0MvdZfN#I!0Tg>&FWEU5 z|Hi6+{*rP3;X#<_($(1DH)oCi@&o%1rdRT{zZUQp08_jLv;Wy~L-D@{>Jz!cCiN&yEV4`qxM9cFbYFoBwRPh0IQ;|D4fE`%?=h|lqJ;7JoM{9rYwt=vI{#0HXKY2! z<#w}XvnSt|MJ*d;NbJ44`;PAe&RTb+XD!k2!R=;EE^{LFESrNSh`nAZy zJdKpdNx@pe(!A3+AV&BXQYU^V{&dPr?JKPV%ePh+S55%E+dBOB&H1bBof1*H_{a-+ z!cgZ+Usy^o=wE)TAy^eIT?c|8O0}oLlvPLxS*Hr89LbxIiVq;$a;9EcXAf!ExFAv9 z$`UV`>9;72Jk<4jKOIkE5eE@faJ z39}&EG=8uhA^cB((f&S2FWCV~4%n|(SqA=b3_^_sJrN4?ceLlQ^nbEJeEQHU#H2z>}YNxKUs)6R0XaYM?<}-!OVDmq99p>I#LC# zn&y8e{%?p3T=wS~o0C=39sQ0_$>}1?-VzM$9F+AGZyWvezPCBr&7@Wvy=%}7mCy=i z$IP5_NDZ@7_FE{j!Rh*3bH1g}N=OZ?Hg*S_llA{XpllUGmk!coM<|PYbZqLlO&e?i z#c1~36?63{<)oTK^unXh81*MMn`weAFhKj1gr?(}c%+@pFT`e1`6h4$;Qd&)e$CVn zxQ7|xI0Pa4uv{~fH& zO5R*Js*nq(QtuSBJ(YH;RKb2kd08RbX0hMs&Qs|wOnstj5zVY`UN3OzE|95Gz}Ks_ z=xl3zVpJ*A@vdBX!c{3XIGIFyYE(Q5gvQU6oJ48jb?^z`iQA0YMPBx`6U^yMVzC8tg1CM9Ub z4eRvu04wxgfAGci3?Ug9-rheb7$892K7b_ZD8`gVvZfw|!Qc>}qtyF6F#L(4U_A6P zK+PHv0#O2i1~tJg&V#NPpwnV8&w016PXP=9Obe>s@wn`HI% zP4o?LMJ}cJ`^)1AGV2Ft{s8k!jE8yL9v^*wI;{~^SpC<7dV35n^Sfr*0Y z>Q!I;_g&1$U`N9EM#aD|13q5wR%ZjO00lDzAk7Dh@jv71>6!THVS!Sgasr8WCbJyWCZjCBnLzab_s?L zV2Koi!}O|u|A1$XLNE3Llu<*}ME?0B@JH|uSj8lg2s*JG`oT}_5B?ATqwoIDz)#N) z#&^%x$8rBSxELOem)&mvHh3qVl}Fuue*m~Od<34_4u8pQ!V~G@5ecv;8(5o)C>cS2 zPz?YE3r&^PB~F&sCQp~wCs2Uk08xR#K2n0hKc)tUd#DJ>391TJNcd!uA z5wa4KW3&{NWwsWVXSf)d8M+#qYrGttZN46#Z$SS){e=1Ydx-J!^NjWOcaY&Q)>qkE ziKbJUU1sAA#gnQvI?X0m@6On4HrpM>8!=a&E;n1Fa!Cmp?!5;3f1V>7XhLGtVTNH~ z&W`j}jusiJR+rMUzzt58`NS6(sfh<4(4k45G{(JWVz?PUE0%^|Jz`&Uhk>J3C{D?6{ zy_xE>-@d?yqo2OOd(3ThP(T3enDAz9>)FcYt_z|l$z3EdiF2gTpw5`g_IdMTL9`eQ z=2XKjgxWX|)ganMG)_m{_#f)M$COPckHq}dFEOb>DLD&lK!{$vdlwyBb@6ReAOvq&Jx;_yo}aRk0nNB~h{26H5vgdkPS6QoqY8B2!h6vl^T zf+?_JJ(Ud>bl_86Gfh z|EyAS%42~k3@e0cgclA<`D}?Xl~;i>8KY2BIl~WKU6*dOgq`It+&RlvvM4T1JB!X+ z#m0!?3cHW7$&eqF%(R5kuSm&Py9`ga0H-tBQIayxdm{llrHN-(f~zgnLlxO9;-i}8 z#sZThtWhYtLtV++5;U5a($ke}T^WfS$38v?98b;IbUoOeK4RU{tNnCQX0@NnYfVjy zh~rCc$qt1VEy6@%@}0Ydb;2M{O#jhplLN~on#!mCH&eyRqJwQ{+cv8zDSaU^CyGD( zqIl{`q`t=ija4nSZ-v)cV|m0Es8O-iy&BJnTY+Nlo15#JtxgW}(3DpDen0g>m-ogl zz;gh8UqY$1-YO+u;Jtxjybh|UWQLwkb(KI_VwNh+DDAn7!n*D%#VF)CBR>6;+CEGC z!r65|$bQv1CjEiuu+S5`*@REPUM*;|4(70+BVeNuz1c)9>U;^o0{d^Klqw+4+~{er zt-6X8NS*cHV{!O+XBgo{B{Ht_@-me#%Fj|bJ)b*&PPU? z%^{3M1Ca$6)DrG7EiMP>q{=GWk^d~-ypZmVR_uh#CYO0(T!JX2-NQmxlqeclCvQFodqT<`EIE!R)o_9Jec zh&jWe2$`3AwX_xw0r#nPth98mN zGSs%P;WS7LqEzBn zetKb{BM;TD%(A8x@oVCvsM;q}Mzw7kCPVO=IV)WLt%{jhnY$Up;Nryur(od3Rr}uh zMtSyWYsCR@usC3n6|iZSm3p*wj9OS>&m;@`X**tW;QHbD{hebUt$FeS(&K#@YlpVW z#RqkFCfEgoPB|U-b19pJGOAx9PgX<@DU<2$S3Eic3fG}`? zKyt7F<{=B+h2#X$O%%F~j;};c?>!P^^Xq9mC6lu#1&d@uOOLlie&$0@@zz6J3q_0f zFgkn>dQXD>`?XD^;9D2Ah#$R~Cg;09py1mQwx~-(^pt*A>_T#s-0!$O-=BM}Uv2jL zp#%f~{P_WZcUv#^hV)txd48Sps>PAcXgu2@GxtEqYdRZN7KEn=Ed~YguuHB?`Wxe* z@wXbaezUcTh{ymP5wX5t9}t3qhU%i>yo0Xew4>jm%mS@yple-5fjN zrYrsBcQ%G4cf`8ncJ4tiQm zv+g^}=eV1i8w@@=?n*sDxTz=3*4W9wb_zHdTOO$(yYjv}oT*?aH#|a}eNuTpaE?MV zJHr|CmO=RM`*?K`5`&W}qWq;7T*f*4j%Pp!NN+$Lln9}~t~Wxg0w~r~4#@H%hi>t> zK13-5x&?z~E|T2Qpi>9}By?y1~Jql5MMkc0eh zaa1^kiL*|^NXnJMG!P8=Q?pUrSDYV%s53+I{VbyP)HC^Fe3y1Q6Mz_9n?UUAOYIOosKNo5-dnMzDQ&lv8A+WcKwKCj;EKlCjk( z4A`!>4~pi}=H#g{Ue4mmj$2~3B&?*oJ~w{GPslCHlYdRNQdKK5y4&m^dOA+5R!>qN zyiji@nCu0lX)$r1#p^jDO#iYg%b3&O<8S%c~^M)T!)2ug)OyKPUPCndXI-Pr@xY292t>V!kuU%R2 z9t#D_jrehm9H%+T{d51|$?@_q|ikmn_Fi1ZYN|O7a z6Cs9iQR%ajYh)}e?!^#-w| zi78Sc`kU8rLHzVmyX&NE^j4#QkLwYycjjSij8@iN=}8M8yWRDO0*;FAB2)F#CU^7S zpN@{BD!DqR>wm$4k<=fX$}WS6s{XmNwH3Gu3wGv{tY(|A``6X3M9KG#P}|IDedKg{QdnvSD-Vq?4!J}Z zGGizB_1WLS!YQUKL#zebLg+Akgh?{=$+g(z9Wol~6%G5tW4^+wDY11) zy2k}qnfq|J`%Y{6Y>2d0>(h^|I+L!3QgL4QYqS~QE^*>sGJNs%hbS;Che09X^1NN* zNF7t*Tuf6?9;dK8R7FIOcf&C!GF|`RI3Mjp=OOz! z2^JcCHrQ%(i|O+C&iq?4qv>YF_fq&-kK+Tp)fMveIx&mglR)n4w0nyF+SkgFn?Qk@ zvO4ri_s>#MA`g>cMhKT82-^?LrF1O`wuA(->iHJf_9Q`$YVHk@K0DDh(L3{Q`_A%01tznh%(Z_Yd-lg>oBD>IK3A2J zDIJPMI*^s5&}VxaQfAA9@jzU&{^mxi6~2 zQ;{V8HmC*_L;|5rAx{%Ry9f^5tXZRR*@`hkpiHSwlH5_GF7#owQObn8826?}p~MIvnNJKs70^;2D!1JS5V1eZL(-&BrV>e>B_>5+p4ohla%~_W%(!Gm z5e;+UeUI$z{b5w~X6t7pm!18&f(qXwg2&?JON~FJveWK0{3bPemHTTN_{DlT_=OA{ zFFte?p->*VsvhT=70HEdmK(qdPC*|okw;kg4~Zb_Wu-VrJyBgITHW8e{rL##*cgW) zF;X$|P8>4RfQfxJQ{jCOSuPGi8Ss6c_Ov^^d_lS*#n!PiJ+KP%wN8%b(=Ni9fHU6& zdepLaKGntt@dflu&Dq^2WVTeF4A+|?ok_b%&`$~%n-*)B#2=a;D4XpUT^Va({R`K$h2P03e+P%m@)%?Jv7 z`qfr8-ChU|86d7Gz-&M);NpBKTaOp<#xZ2L6G)ETSG53F3QEMnp{61h&n&!0m>2|L zZW7SdOsrk2bDU#?VN@lTX(?EjwCK06!^uE$d|nmZ#>WTTTHnWaZsflwS<79YV}ma& zH1Ze?zp$nbP1GyI*+d(#Q~fzYYFj9-g4tzIl$b{|FVv(h#nEjtUlyf*55#@O!F z_Sa*cjqlaDIyyoxO;C3Bu9xLdhB81srJht_K!}z81UP8zP%Vjz+!rKOt=E(-W_Es8 zX$($nT67_i`_ZKL*Pc2F8*n^I54*gkwVtdwsABuqgCjW}Ux-eQU#W&a-=E#^k2UH#+piE%L*lO_{K;>sPOAOjrRy^( z_(oz`kdSb5F8wJ(Qo1_^N-n7|IXo76q4s+@9hC(hW3N(N@Qsm9c!-$t4J)9G7;0!y z6?=o}SBd}Rrt(%Q(yLL{t&Qi502?`n`BQhi5?nV*f%vpTYVN?k4WW)e>%hlt&}W8J zSdU??ncJ`UsNdePwpD}at&>+K#QedYUNLMBdX)BMYq8sK8dsqZ)mF7xKOnDG{HZP0svNo$3&P3jUO>pHu*68bCh3AUbd!80aY#QHy|JXGS(+<}x%N zt-ut3bR-B_VC`H6-IYnjI4cYGqrh=71L~c{Vbp=j!IAC z@=qhL>`K_KweNQqqdrs~rJg>+Vdm!F&UR%64m}MZ-cExTMC(9gEoGq_Iy0fkL!}7g zeLhg!&MG3RJk$X%_3i6n3*#vRsFTQJL0hP^LX|5KzOf`36S|jSc|GCzBZdXSGnCf6 z9_26EvYVP7Jx^k#@y;DNwIgZomIMooO)42AC>j+EndvVWVnHt)^|V0FPn{oJj5>x;~JZ zQ^NY;`yuXur-jIUO+!wm3(NYB>Df~bcWeTswS?;07#<>~NEW7e{Z z_D0u@Q!FPJJJx%Fo{i!zd#%O60)D^^d3ziS*_X$+WussMED5Scb0bn>n2lLiVkqR9 zO_LX!HuJJFYMZuzSu&5uyC}zuW(V^^*ft+M_5&VR1Ez=IbFy0*K)wH9KVr#Be_SZ6 zWvTwzTs%hDdv}!=amVi&5>GwW3~XvU*7Wa|DN% z^z$_|ZknNs^>DgrdA|gIyErRrP4A_4n-!<(`+i=$t$9#Tk4+YU+o{peA{P&wm#GKX zQQi+;fC%~;Q<&ylq{F!Iy31z4N)`x)L*UtmF4Mn?7i;GcAVC)t% zX{WW(XlnnSc$35Fm7Phv6L<3laq3Vn{e(pKeLE;?yIFXO*kY;T`C5Io2a}EQiTONe{C>%is1@;&T}_nF*kg+xCzbz%xYj-RGAnbtG`1IAcq?!E zdX)zo0P1xGU?c@6S6AQDdV(a>b))Hb_VJGRvyD2qJv^6%U`Gxa`~_SINpcu3hsFS& z;sOVZZRF6d1xJc-0MsB^tbQJzeZ_4Krght%jh~(9o50T*TFGC|tDEh*^1#}g+Pm%k zeL9mNaZgJ0;Q>GBV%P2TdW4_Qd1F_Uo7n30{jQsE%gA3dASgQNW(%Vi(T|a&xI#jb zyF0_u)To4ILdnwevvA?v$bLPV{((K7QiA3%rV6Ch89t?~rx4LHdV+$2oEh^v5y)G& zw?=!x)+9*y;=4*|C)w3S6nnc2a&D`VJT zYeHXd_qsR&ak)mHi%qy9X4SGti~6ifAD0Q_Nj0}w7Ng;v9a1VUg75}02aaF&XxvpA$EdXwHjc%Pw3}UHMjk&a5jUTXZ+3>ekLT!cNGPVzAK!~Q8Kbv0g2Vd7KWK%35(w(c441CjmRw}L#w;N7 zBHt^@R`0@NN))$jId9|Xe^+$L{tN+jeg@#E)7)6CTzy)UAXiarWCGe_%dSuX`McFb zalQCx-C%LfU;{`s+2OqGB0 z1wC~RdZUTg!G4la)8HSIqwoj@4R`rm0<=oDyxbhEcW6dv_3kuScn+{y1csqr8sriC z6k}6jqg1(UT{3otN@`*$2l>W@z$+b+AP5xvdb4`FkNtVoe6{@8f!Jue>%-ofg|4>t zKFsyL$)(Yrn6|d8z*O%%Z*SbBcH)!!7R1>wEM?CL%?3>js)T&Dq!-!hvk4d)Ork3> z&dwUeF&R#MmmN&qHv71V=lvkpl(FXM=aoS=vPRyv03%36NWcQHf#LSQzd({8P>Kx0 z0E&nQ)HYz$j52BbV+{PyE<8PNautLv@-V-#UupvSd*YiV8AG1Ll|QYMKgMjR!K>@3 zPBVIG(811-+VwnNT12+_OdphbMEUCb2FpfaV_U2x_WjbQ25v8tThEq`f#;xWUL#rH zwI*W6NP#VEP=-|sCe2|qMl0z+hp_M{7d~sSwr9Un{C8iF6@l}ZO^&xCXFTf{@+sk0 zEhxWjhbSMJj4t&jaeORYFCQ->`k03VNSE_kll!MH!S*@P@$jMrvuAQ>*xHD5{03mz zXi!>>H?J@gT&D#hMXpUEu*QguP zvS>4Q=(UZjzPKM{ztt*f#W4DWa~mA{h<1vsR!VI6%8E`aHHQxrRQ};iyMh(i1nryK z$*8{+Wp*#vajki7F0ZF6w+078FNjn!tfksL=d(`Cu=G9feRuUhaWj9U)3sCr5Z$YN zn2!J%NCwKxL7MLF>;|~8-c%HC{}&cBxFuT;@e2VZiy*1)N7aM}lpe38Em}X9l@2tw zUuPs$v;voGemt2prSf=JOJsePCSOYkUJl$Y|FKHA%jyn4 ze0gCJgodNadJ2caviT)@1eE8FCwW1^hqVVPDSYtfxq3$26V7-vW>I;>W4FIuGT0pA z0%TVI>Vy-f6R-BN*1jR;lZGjuhsxE^6?EGP)iZT{izyYJ2F{MPFKSAqd>qesQJ3hY za{E+eFnxDN=Am_S_-^@fJX&bajk6k@M}8ldZjKg1?%q1O-4(5dfFkD{FjUP}`5J<| z7Hn9US_T~SvMbH%h#ls%T`N(@O)U=`UNTe2KD-csF1D~x{k%S0=3pND{QF(A0rf7m zAE=$eH(EbX^9js!e@fCSxvh&i*wS7;ZO*06`5nECMyKTy{9WSA;!GyzQM$$Cqy2}- zBEtV6ZBb<`+x6NI?eS$1D^$Ap02z}|5$#4p#csHt6%9q%kdA| zgQ(X9-(^O(hY}p(o^{LMh@HzuEnyT!zKmB->sOeElCki2?1c_N+OEvxFkY>td%a!s zY6g`4cs&VfKWT#hM3v^4MY^MMx6W!lCVAbJPx@rF6GuJ6Wh6EQ*uy9mPy-^$5TN?O z;&%ZTGyumVCRq~U#KSc*B9K-BapxCByLBqw+XmqQFT7@Bcs-rsw|=)B#b@6mzGY?W z&NJkhPXxhYGV5HT-VghRs(m|rV$gXunvcgnkVa=Bdsv@eAM)`(KPJ4T2d3dgB+zOV zVt}vfmATeoK4gJHdl78!^-u1n)0cr8mg7u7=0~^^_jg1mIT{oc5}6$p*lZ2{el~f8dNdhTLFI4!PV>8yJGT#P)z<|5WpUlz9Cc8&Nz~ao2mxf}K zNy%L0htQlai-%g zWU=Qx50fADPW*7+t-#8n$kt-W-Ct1;4|)sT=&pJAJb%T~Ylja`{1v6aW3Vx@zY^#% zQ*pa4VyCNQic~C6danal!Q<_G>rdxyRFH%!Z9BLS&3+ws_zLZuxIjNbJA*}hu`lVI z6t%@;c91#~t-yW<8lWUdWTZe1n!hojGyu(=iz=bjMG@~ii1@<@S2>?RpuXwih{nAv zC&r}4S+?6Zc{+Xk{_fq_K3-YEq$y95q<@0g~ z(*qHD0z)^8mjkwIq}~#T;fEPuMKPL*iPHVio{nqx`lbePYo9iZQK3S)*R?t`xHub> zeUav(tgrIJ=WJ88PX3d2i-C9b6g7U6lh&{H%=0rIU1y4y8Unr?Aa9#jfqPmlhG$EE z%NrlYD60k*U&2t|IWMNy=tWHT>J}^2A+0yWG~@J=$Bp0pxwE zxYBF0i#j0{Do(*ZK-KyH*m&|J9jxXe;qPw)tc(jJ1ahSXAx}WrpWx7L%2uAyFj@R# zF?saOE@A$QbY7p4#^wk7uC+S=&W_538fkBaNjrWX1E$LAJ{s148X2&dKnH>J*9xghgxf+lUV0<~K_gvz;%Fy(Yra9hzl zh!9kIwhao`a8uMN7E=c9#;3sI>D>H81Yojb-) zjFg4EHRO!XL*SN%gGJT>6DErMu3i3FVnBEpQ;;<;WOJ{tT5O-stxVswM`W9-OxBaN z@Tb2OFVQEXUOwk(UTse|w%sveT?DhbZ9b8o56ICM?E1J5%(glpxLcX@@UJ?It#{pA zR^D;&=EVi(B&{#qg0{{}T(IrKFaLt&E_@?zic8%A^6ZxBUv)AQSb5O7Eb-~g!D1g? z&$Z!wclJD`X=S4*QaKq9296R#ze#SmmWE$|-hsCld#?{2x7T`AywE%NM|SoNT`?U@ za~Ez54ddc{+4@Lu4Vn!;EJ~ib5wAjZ{Y8$ z(R|}ZS-ux?E$;%_a|)MFo8$YPNqjzcP6A>r)<|j#)GBjGJP1GtF&&gI@RJ|0^m}^} z3VxuBx(rHvyC{sv1`y*U_LeW95o|zKT(`U_%RY)EYlbpQ2-4Mb7Dq-d;jp+HC|<~P zOw?HV@SNeGQnLY=9)(`%*2n#?2Czeu{W81=ugX4CYQJXkxvUsio)$aAWooC1vsJES zcMu0I13P;$g}&3j65%pOx7;ale{*{tK0?8+D7$Qr@l)37vGj4Jr^eA{cNurrB{Y_X-hEr_unQ%EBpL=*1`hjp8l zKAvN);uqkT`S3q~AiWS@2XH+Skx-SHmB*ZjF|TT~jXfG4N@?1Fp3Z9fb|eheU3*L zo}5=?U^|>7bbqHo9y9i9sDFo7*s4MPCB+o3o)dxp+*g2PdvWmGr~yaJjQ(bnpDu7r3lkVy=j%VAmyeaiNEs?Vz6TI%OO`*u#Qt zo_r;5WEf?O!?@yLc)r|(YubfGihrOGtdbP;?%`Na2th_gQ`dkTw@k} z=yUg82Q<1cyLw=vq5&qhquRZdgvDi)I|0ppdrFc##9%V&9d&Niin*JskR#=qDBT61_Zi7bqV_E1$h)+C<8MC$x(-)5m z?{^GnUacp_h{OB+f-eHyI!w>&7c?51f^A9_W?~9-4$Sc2(O^FnB35M{0{u*SF>sIk z++C)rW=$8-X1mO$*wN!8*)+%HXkUAmi_*4Yi=jx{+t6yGJ+GFfs%eVU`PE}PKkOef z)zn;97hDwdVprIIaC34cT^$N&6n*Ib>c)wHx{4JOCD7D|($+Ds<0a76k1@Z`Ea%H+ zWmx*JAW0${7<=KoiLU<-DtFD4g?R0{TANvvtAmG2py_!?!AC?$a-u5~bIWYFy@<$( zv2CVhY%F|f&n#;@rtSfGorkkW1f*iXrs7|8EsMlFVO9(!^lK#yrjt2OHD#_cPm{Ag z9reS$=)VD;ZpNa^yLWgRmM~nbA{?Ox^IJNFd?3%HR7rLuSV}x%z&k8*jeFnB`w^P6 zVTE1#Vd)5~gMGx8fek8=lc;}0WbGPOmlkzScPM{|hN@|eHP-EGgL+FxT{e4{zvcfe#oS8OEVbn~GHeI29DF>?pI_EAs2c%ZHT z9FoZn2p4hrQyU&D7c1r7@l3LuQs~Z$LNUnaFQx-q;s+NlUM=esjBYkHfPEVcMr5z$ zrL^aZxgJ`3>>79w>L5_oO2cBS3ev4_fQe<#N_lhNXYUOLxsI?zzqWo#evvCzZgH zEfXHkf8EV2_RRvueR=!w&?wtb2;6S&n)pe)+=maR#fem8Nz%J)+@Ui2?jwonj4%Ek zc+B|T48O#0%|G7J@>BnLCA*nw0236*$>IU#6;~R{D<~ukHwtXhI>(gOgWRzaKZRLF0Q(w(2-2i3~kCgY#)J?is4%N#HoSe>NGi!`)0}_|^rg z`?)ulkVPKCUY*JIwdZ+z8qd1Wk|dQi5btUM#=3Mvr8ZyN#8Ayp`Vm&XJ^tYUM!$V0 z^+OwTZS4Ajwbtm%Oc$-iXf_98`|<(x?k~0P3c~9u@(N(ymkRTcaR!MC0+RG(UY(oR zo`MSrt}6Gm#m&hZ`9a31cz2n#*m(+_Ut#Jaq4DR%=qOe}XwmDTLJgRU2!^zPM(GmQ z1kk>*LJy3!a`sOa6m{uj9*l4W3<;$i-den5u{Oq5|9o`JqvaR_PRa9&epBjI(*k;< z7o%-}S%51Sl6cGTkf)k9Y(55}jjQ&;7quAMq4eq3G5*i{`&Z=0Qj@hWwk(GyRBG=} z%;)3V%ONkhDc%q-9L~^I4mX9b+iBkC$%)%Ze|E3$KsV3&{gv*{PyWt7sW%E-N5Sof zZ~Vj3*`ClzS$=BY+si*$4rBaL6SqDy1Hllc1Zd$R&Vz8I4N4*>c~Aiqb|bvq4iIP%BYNVafMQjoDy2`kwsFtEF@0|#xoYic&_)3MQLpO( zB=f8#?FzHxvbYW_N%9*5@3Rz_Tb&Iu9L$BA?1gNmr~fkE;Zlr=`TA zg&x|`uAM>dxD~oF3V?Qq*Q`g_tWpRp^nFM6l!xy_!H<1|Gw-?>?^8REeZ?bg_Z8mC zv{FNK=MSob?@iogv2?Ichj)qkj3sW@*Zh%`XVP4ZD8Pd1u0sWuAi(UKP48P+t#=#| zdu;6wIx^XTyOF`j-$Q!XBAckbTD(!3NFg4`=pxWOS{^JYIC^>I$f$1NoDBX1Ka>p+ z0Yw9nf+#7g5}+cvp;F7;*Z$m(j~?DnBqEolCd&E*6DkkCa2|Q^NNi7UIp%&IE$_8Yg?79RO11_TrTMSI9p#S4B>>3Q9sNDyfz7X3YZ>Jqn(jNJ>oA0W3l zxk22<4nFVk#x#ebP!9DsL52zf5)u*?l9e)99ian+{bKHXb2kLn9kex&rDhm@{O`(y zGyD8{a}-|UnA|<_D>&Ql31Z-5X!(kVFY;l3G6XGzV<{Dxh(_&isttjYPz)%a578Y@ zwkiz{HqKVtx2Yay&6CCH%~whrG9k;JG%jN+i;~tNuk}wz#hfxvP96_?Njk&FFL5Yv1~6H&QRF+Fc2dsMX6 z>+($P*4@v&`?~N%bkyf;K0?o#189|=(NK(1biO*y(jK#)b9G|ymkV76pG{umSR=;X ztpVSuZlZNUpYYod$cc8JJZ-7iPg zW_&eZ26^I2g+u!i{$`nYQiT3Wf7=|zWvu<>L9$Q3gUPvrPrgehyRZt^#DSeUCyqy2 zMNcGTNCCmG#s3{Qct^*i%j%fJ!DIRso#Vx7SW>S?{?%wnt224npT!&W?X-XVY&e$~ zwmjrD2(c9>-Kb@Dz}|uK5uvDV23d&@A^kp*hvq__4-ry}%UPDBM2%0IXkQq+&kUi7 z&9>FHv)8{qjh*>A$}I}rBwPO49CMdivDMQFp%h5HA|JfPtI0ZJaGVLZlI3ou)>EaFu8M%je33E6;a6oeay(H$vzgx+$H?tCZ!={|Opdrha zwsqt*o6jUI^Wq-2{q}DjPd;&-(q;AdNLv5!Nz>u(vJ<5By^p?GURuh@_|V&QytwZ9 zc!T{&qpQyk)?#(-YV1}xAel1G)Skev(a=$dQiPl8C0d!l9@!n!e&8R`owyL)_v)h3 z#w$xbfgM34ifeJEA*rx zGr*XZs7KxhJA$Mty@fBss$EG&#lR#!oQhnmt9Hx&C902uijOMGotX5A!FoPr7A)MZ zf6bHTS#m+6?;5P%|lq9Y79uqo6P*n}01EDwV=WEKT_UImrlN4lO&&8-6Pa$V012AC>WTU~lU?_h{eCC3mOey3ThqkKx*HBpv3uGdn3#p)=icwg3W-(WX zC>w=fQuLxM<)gt!#+J(VBya^vvrklY97LVM!gLl3FIa7|8+B8Dx!{u^dUs=(n`u+arFX4TANeP6O<8q?!) zwo-t{((*>9KyqUCNJ%v@T3-=e#>;D@D1p|!{it-brHSwM6}VV`r%opGbCKqs!_W5J z;CX9Q?sd53Y4Y9UjOUK70;?%iNj5uXAi0Olw$eLTQLs}l0uyNgNQ>+nJO2Q&ysvGp z9W>$)!W6RJ-&+PtvqsBkr_L6jX09nHQC1~f$?8ffl|68NgUfk35HSa?R>(j6(BVT2DxxlaoS)6|FU4ot1A=0*K?3kUOKEHwkZQU zOl|)+r~Zd_(iPf=C59}5W!2-vvKL6W7`6N!UM9$xwls*$VHAK`^U~BmM6G>%!0WaC z*Wi6<0=kjnLCdJ}VI*ArvQl~7IN7_vH?^YTpGix?nP(dPD3KO_g4}dq5hJlu z0gv7UD#?S$i@z&G1N-&Z(xkr$b^zpkpx8F*8w)@DOdNyJbhVOsl)ev9T5~sSU$QeL zVdj5-lPA#VejU#{)c>ox54+qx{s4b{3-uzEBDYSYZ2}Kk8@GnJ5Ds~A*ar!yy%U{F zD75pi$R8%UPC=Q4B!Pn)AAANytIEW*!?2*EpvsVh0i~C(^Ozp^hIsuwZy zjuCV(Q;mbhFRcvsLO-Yzb&j%1h8r(D0f6L}T=z&_N81bdY|a9qr&zmWuqzyv7AL9X z5BK(z44zWs0=6*h4DBUCr`FwEHUgkp(MGK1sTHtL4zSDtd_h+H=i<6%PLmJX&eN^) zY%%CL`yY!H>=eLFH=x=oSca^`c$Y+@XYvXJOIx z>OzIE^EDup>)zn2k@edCS7C%eh9Lgnf1`tSgR)N>Mt|5=OXo#IJhmY3aAuW&>6aNy zfG~S_9}kOmn=1o$OI`eb*xr$L(cPi{IQf$$$N`@JfxfKTr)F&p#>X~fY#jpe)Bh2$H!8AOa8CF%S_~)EbYvB}#HjB|(}!pvQETrG z@s1K#)ugV;yQKGoc7tr#p!jDv1bG@$A`LZ;0#?A5f6i|99BciY>FBOt1XR0(I!wUqAecgrn zW(Um1OH1j{Hqa9*8@R2zTfJs=jLyp!dkoHVEqM)U{A`Z6g#x`u7RiZ^~MUWY9m_l0OfFh2Q6KA>4$Yabj*n5jmZ%SVHU&bb}c z{|TfSTju4S{=;djQrIE}${_pX(DM_W7G!7u9v}r3^J0Hl8bovSDkgT65_F2v6DKK` zKy-A!L$uXYnAJah;Ak5TcmMswo+I5#AD%lgb++f@qtA`^tjeALkhN#txI$O%_>x@5 z%(5j9M$6wM)AHZ-VH4*Hj<-**tLr_bV&X~d##qHqdr~RsXjf{3LYxeXqW+RGI)1 zS!%4(fKSkMH5yF-3oXMUq%#(|cOKY|hPDHZkWOgCQ#5*X|E0~)Mf!a@hKum&Ex5dG zLg*C*h5olLAVgyzDiors1g_AI(qXOE;>SeKFbVC9N#SoA-;R*J1EJ7P2z7HhC`wtG zp0u9b-QAKC9of$8+o5Lc*dyVCTkxv!A+%e;E8~`R(HkOEz!oZ10G$wqj;=F0{q8iZ z9gC0-EOec)P;kgdOQnkXcB|L><2i-L8g5ztnZF>^qO3osi;N4-LnHHkl)8l7f+%%Zuvt4u*I9 zm6TaX(CV~;t{Q=MQxSDF&9V}ms?rcbv|4@?y$*^8meUZm8ja$xp7S?1<^Iw@h^#~N z1EX1iHnmjk5cI^~>eQ`I@9u7la{Kkp>yzh6bLVu=p}t*I1ikvwWYDT9qNp40W>m^= zrQo(3k5ZQ^b?I#pU7cFMaC@T*zjpSM$#DxJRdb%2xcuR@*Vc`^FG-s}CvL@sC7b0J zh|N9SvEF(&qFFY{$^!|78^gm3Vcwp1M zhZeP-D{0(p_iP*1{1WcAZN~Cv<-hG+u#g+`+P>O({qrb)$rjp2)y`jolr6vV+T!|tYEh!btowFP8B;myBUwbqtyFu^LXwPma zvcMe)(ziv5-Mb&5ao)STClgT$!|gp_V3{QmR|i^>fQ@NaTj#zce?wbTB*EQMTnTY8 zkX=x}cmXH63&2WO>qhxRVoaomH`?eZjfAs^Hs~&UwP0OPL0|nCx{0aw+f&JUxF` zNk<0_&G_)KemLY`UEnOf*-L>F$f3~NZQC1zg5X$!;k?xa&T08wc+l-l4&+Wa48M80 zBA)L8$w-}LKdj>lJ%eD?$n;i52Wv**lrD?TT|q3}B*rWLb~)IB`JxM=zMk}KAd)UW zFFr1oDqD^q4ffK?TY|ZY_6uQv?hboOlD(&+r>iH8^b(V@!)z`ayV%U%(yr*KY*b%1w4Pt}?UtF3IK?4Djo0q^Y{BA(7rwXhzWb4%9(;-7 zZ!mh4D*lEYq4kQ&@73O6qEYEUb!fy&kYV*GYG~Pgw1K9SkoKmOjLt*&TZVM*R0(PC zREdd>!XORZyCu13ay_b7bT1r&2y%8C1HUi`8iC&7lBmBj^8T>$Q27tp9em?sJ_%uE9o8h1S7SUS8 zKz;_oNs(TDRn4>(n?dS2gOZ}@m_rpjM`n-@sm$@Vh|qBF5G6H(RNw;$f;5UM42v>_ z=GG}i=g=dh-d|%dqVh(`%Hj7h`N$K=FTjDPb@bae@Pvp2lR>Yeu@%qJQvN{0pK>V_h|n)yw@|euNux4O--i#iOiVVbryZKu+^Okr z`nc*MIZ}n>!Fvkos&C)-7od}}cR_Tjc@WVYe>;gfdS6rwDXNSuT`2^vO(LTaJ)vX0 zb@)7A)ZWV*+PRn4?4hmD@VWm^D=9@d59-a1erAElixKQxJBt2QV;VKm=)^%!kR?GZ zqy9G;#WC+nqark-#qC$-`!Cs7ovR+jdAscgytxYf+B4pZ)~^2hE6z;4^Y@64ewj~=VV zI08ONJVvzWM-9eN%~yn|v>d%&fD+oqt`-K&HA*DiE7j>>ci!jp%ITKu=;`bk6Q$Tp z@Hgz(t^;O{PwI%A<86Ls4vw1J@8dEVGZI}LLGxw#+L*%gD~^7&t?hSMUpDOglIBO{ zm*n?T_!SMq)|Bk=kvRt^-8=XBvrEY8x;MI;zWUB<`Fz%bFHRiC#m|2}XL;kYm(D_* zoaWp%jQbP}*zeYE!UM7P-Us>D_AOu3tFS$H?&^{|uVE+aDc(euHfJ{s(}F9GuLw?? zQ$OBhGEsE^Z>;A(=6)3I;9W#}BlHr-?!}`;K4=yVMhFBB2F~Qh&cq~9a%R%1$FMle z{Wzm{^@FqLY+Pd7<*Mk$f81;Bl0i{T4M|fT%47AcBnjYtDmEZ3Xd1gWHmD5-aU=Xb z0fz=BBy@Ck`ip@if3Y^DGxzDzDbp6;J8|0LYOg0PuWydWD;%1#Xkpca+69v{b8|DZ z`uAt&S-6D%m`@cxh3)MIYMTcq9pru-e4yl*EVK#RVm5|`C~YlPY-KHBJqgX5J58SS zSVH&JL%2c7!v^QaclU%%?elE+5rcE1x_ct0=JB66-Ok>9FiCJHWDStz&iB`&&R5j` z-#+6ulG@*RCq9=A19$IM#!1z`d7PvVj9bASCn|QwwQ|4HEtf0N8~n{lS!NHB8pNst z^_z3J<6$4*5c%mxm2<>87$3s!d5ZN$(c%6plGs&ItjSVBl7-$9WuwKirfkBilGlxE zc(71t4Xe1>gu9*lKYot@p*V0W7!EqxO{#ngjZ%^WO8`ZNB%P$wY8WW`T{H?pcI6NL zURCmD{hk!xg?0pA#NFhkCKrp83++wAnUH=tgTDpVC3qGec%9a!6K zBInEs!k+ZdOgK{CyEeL=3}Nre-`}oZhC|mVTjvIjC9g%;vhv30qc{jVA{- z9;m8Zdw2@+dS7i?W97I*^| z1wK!Mv6}Uwm8s|@?W~H3CeF2^5Ifrt1aTBZ0ag*zq9Z;wCOV3ive2uLSl=JL&L9yd z>XZgeFy`!+LAf~ELHg6qzpQNdWkSkjL)`8)Ukt6+FV_AL(pWOO32SkrJMH0OMb?&)FNJN& zeTpPkG&&&! zc4E#MW~DtSQLF_n1N0|uUG^5?&k*lxBER@Z>+$`|c<~hZlFY2G_H8Fg8HMsla>4fj z>ETPo2Z!|XeN1Ujefh!s;P$@WP`_nm{-M!swDW^+yi9+L8&mi3`&x8$`P_wIYK5lwMVyPR|1XM zqM09~)kp%i6T3e@!Pao7%NjtMBuh9JJ-=H-}UY-d-iRv;=-LTRU-Dm zS^cvL#zbD0}EA*X&dK!a^Hjrr%4i_Bz>uuhLtbvW6%(CsCV2>DyPN z{RsonK5tlti>PsCBGIU=65)^qB#fi?+fxSU5rWlfJW8t~^r|DhM0j3Ps>2$M5-Y(r z(;Tu8O8l40q_HcJLfFBi7E_k^wJ~L0hrs9d@7I@}==EUHGGz)-Q96x^A1Dko8VvNC zZm{S7v>(EEEqGYV^?&@Iwn4P~g#N#1ulPgiwN$ zLxv1aMI?lP1R6R?kyIo@$dm>oh=`OBf`b$h=_XPnLvaWhLdhVsghJ^MB!p6mWN9hE zp$H2nsYNq`M>^_KrlgW)8+lVhT)z%9udjICEf+D$ zZAn~B2*aWNiFuCa?Qg^-ZYq-RPJ@~l>sK+M4zR-cnrj+asQHcV(ZvdO*HfeEX$hoUSj$l&iK8+6W%FD zHhGsR({QJL0v-0d;T^e*>Um1NMV<9w{}N@gV5jj+7u|Kx_dBpVZb!TjAI1rM7=vD= zZ+y6o+=aR+UW^lXLC@GX1bx2)OT-KDVVsc<|DoqA|9rTO^s$13crlK6A)blK9=4Bt zd(M10SIK*2YAQ-y)bD`MI&h<^40zv2VgxR!73y=Y$$R*V?qe?0#GIE!nN))J@)>1P z(JSsyTXbv$F{xE4ER(P|IeaL4)59#!o%Dx%Bait$_xKNzPM3z+sWJz{2Kwqj0WZed=)e1Q25iyVs!OB>4rRt44~)+?;v*kaiB zv3+9KV0U28VQ*o-$I-`ej8lp;iE{zx162id|Z4+d|`Y=d{g*#@m=Bj#-GFgLO@4gnZQ562*Gbcc0w6K>x5nj zGYC%*ekP(NvP@J-v_bTon2uPJ*gCO);yU65;xoj*NN`CcNvr_EYm!EiZIX|qw4{8b zc1XRD&XB$#!yuz1V<)pq=87zrtdne=>;>6Ra$#~Ea*O0H$^DQwkdKm|A%96BL}8V} zEk!Ox8^sdEMT(b{WRyyj7Aaj&W>D5q4pFXAUZ#9TMMfn^r9ow#$~{#PRVURn)k~`X z)U?zh)SA>*sXbFqQ$L}hr7=O{k7kVK0j(abN7{1QQQ9-KFKK_%k%`x|}V6hMY02rv4asU7U z0002*08Ib|06G8#00IDd0EYl>0003r0Qmp}00DT~ol`qb!$1&yPQp(FkWwHjdoL0{O{tghI^$I0Ow>-~`Z9aRyF+D0n+w3rs*r$lBevv-4)( z%&Y+{;Q?_Ni8%lsM}Q5axC?L$N!(~0M+LVUCt%`5<0-7*P2*{-8YzuuaA(*W&tlDZ z)_5LU#=FKzoW}ARFA#_E7jYbW)%X$1@okNtV8?6NMH?*+pW_-$G^nNlhkJ*}MIQr< znS=5=r`5zgM;10R9BGX*Sf_Q5-hKLY7{^43*dtrbj>PYy2MdR^HHl0d(cZ%l`*K@{ z9xjU9yK>&(?9nUDG08C_EE78z5p_hrQfB|jsY(2y)}>gMFhgF*N=H~fMQzKh>g7wW zN_m&7hfCV}IGd=ABl(%)HRf6utH-$|(R|SsbfYb|xnfZ|g8c>a^~AR!y2APnnZ;xc zf9{3qr%!7E8~m>1vv?k5yP9hW>eBPSJfFD^B&(*>y+z-k2bRR_vN~1CrYV^O`H#Nj z;nPo5s>nDF{eoSTqh8|o-e!4&{j2WJSe9sR@w5|(Ii#h^cThqZ2kd-VUcQQX!qYlC ztnTskD+;Vidqvcn{5It*%e!-23&_(e{Eu=U3W%(T004N}ZO~P0({T{M@$YS2+qt{r zPXGV5>xQ?i#oe93R)MjNjsn98u7Qy72Ekr{;2QJ+2yVei;2DR9!7Ft1#~YViKDl3V zm-`)2@VhyjUcCG-zJo+bG|?D{!H5YnvBVKi0*NG%ObV%_kxmAgWRXn{x#W>g0fiJ% zObMm5qBU)3OFP=rfsS;dGhOIPH@ag%L&u5@J7qX1r-B~zq!+#ELtpyg#6^E9apPeC z0~y3%hA@<23}*x*8O3PEFqUzQX95$M#AK#0m1#_81~aJ=0|!~lI-d}1+6XksbLS;j^7 zvyv68Vl`j*#wA{Hl2csfHSc&MaS|^Hk|;@%EGd#IX_77(k||k|&1ueXo(tUMEa$kz z298P&*SO9V$(20GXR8!Qp%h86lt`)3SKHL!*G!?hfW=~|jOer|RqfK1R;688(V`x1 zRBB3HX;s>kc4e8;p)6Pao9B$EskxdK=MDHm!J6u-Mt|f<_e8WS9X5kI6s&J4+-e_> zE3!{mU1?R?%zwYF>-rx~rl?c^002w40LW5Uu>k>&S-A)R2moUsumK}PumdA-uop!j zAWOIa4pB?622)yCurwR6C|O`;Ac|F3umUAvumMG5BVw=uBSf+b0R}3v3qbXp#P^D03fHYtnC?oqAXB4pXEPtQ@F04-K3@(e4#g+%6N-G)7R69k;^X~m7J7wD zk*{&>0J#ZSzcl!MiK38*9VMW5cvM44v)>(BjH<8MrZYPjvwjpu&Q3pL>);RR*DKyH z@qDZ{afz8PV zCP0jeS2CRY(H&op+Dlk}ttn~UDB>NE>(cULR}Y&dUzbBYejAQx#)?Oezw-IVIUxx} z0!hZF>-judJZIiE)ZeEVXMMv(T(%->=n^Kv569oryCl(A=LgvcJUxl1%G%ZkAF1<*9iwq=Nfx(O=A zZkHd&7oBs-T@DQ@e196d*b0%0x<(DEi|Ig2fkKp0H8Y1)UHbT@hBxDCOnJGO2ObLF_FqZV8m4K$RwW8s9`Cp_dA8M3dBEq zq@H<=#9DU4bbd+lVfKUE9 z`^27fB90gWL5IJd4c3Ml*28-Vrz#(~lJtL|ktS<(oqaP3>27#%sYeyVE7o%O@)+Rq zd`N#cepv>10M28irei_PAk*ws*1=Zll%rL}oW7g7FEXUGtd#25=JXhd@@-lvV!Ca7 z*}I#fL+dXiBvl?X(&M$_Rl?u2jmXLzcZkSx9!|EABF>De2hpQ%KVumed$_&d{_?aL z)zFlqww|-Ay^dr)^3=*l=nC_OSiN}FZ(KM3;q2)4{1%6=aYO;u1o#~0@#T@#xlP%O zav%NZ;xPa5=+8jac=V-UrfNUCc(|&zJ#m}hQ)=UxmJ&N@_YH6kDFjs~BbvqJA&cjQ z#zq~zrSsL;R$h;)WE@`wdZ3U2PEoMu;Dk^!q{g$dDp_2=Gd}#2=P8d&U=(Q@P^({6 zXZroYg;vVyAO!R)-9w8mZQvImz#I})`qQ)?x3d;_h+L|R*l*pLOww#D5E)DO0qIUK z79%}@Y{8%ry;K(m#ui!GuWk*vMVpg}8>3VA2ZB(8RtaLgujj=JD zVEVp{dDMtkkNIU?>EdnFq=?Tq7ZKxmpZ*wjhaZlt{haex4L29`xFl)l>c<~Yb-2}F zTy|XDSs=70QFS1QbjZ|oByn*fNN~zDaVAM{A+&Lcs`|op^HoxNJmiD$LEeIK)*a(4 z6Y$5_J1PtvwFQf$5|0FAcf5qdtcV*bZas2>#L#@EO)B7SfTeSb<9)?iQe%IIn9&_b z9vNK_Wnv^P?;^m=?(J_Vt~FyLFCUr%?98G*x^akMeirRF;QfKW4RThpIwdOd!Ryf@ z;M@%-*H0ZgGGQz`o5LgaR-DrIH+78K=pr3eOJS`F&lSZ1)K(vjQEoZBbR56aj7&BX z$VrEwV&KT@XrPX6Gz;uV4pGG)h7kPt^ug7an79{0j70E!gC9%rR#C~+Xh~#Tc1>`K ziM3MiW!hm@DfWX9sW{O->ak2$jxaFM{)-5G3{#`S*#QDB2B;YTvA2LGNjoUX;3Oy^ zthCj_eev`v8vZmPy7ke|4$fRJ4g{$8IP4?}HNRQdvhV7)8?t4jgv2Nazt^kh_A?&B zIm27qCF{H13>!aR`*Wo1ZR^94J^5D33yAWagK-z2+%9@{(d17BtwS)KNQV z;G?C}Qo`F`h|xe;`wg!?lwlfFo>oP%$hfcJvy!N~yo zn_}W|MFSiqtR8PJ;kWFi&MwvR{1dthvFFXsY|GxFQYuql0k05t(C*OpTQYinldpNc z!rsPE1v(wK%0Y8c-9u>k0$oQMI)QM9YFzflfeOKaGD>v~Wh%IKud_RmJaR% zK%Wb3y~G16XgIQ8Tyoe6$Ak z*N`1G^P**h^EN1Z)a$2t%RATj{o>i5{-l&Tp?zFZv~3RmaKUqaq$2;01V9qeJ8fCh zfac3(6As@dO&=!st1$C(@|ZqebSmT@;F-4Y4iUpTos>WTeZDS|$Q6J?xdEmDA53z-svdbcQB%-6n@oR7mygnt1s6@_8| z(cs^6(3f9GPgT10FM&KrdPvVv!_qvaAhASpjdY6I3TS$uNf2J7rK9@KTqH`iCz z#dO1dgMUgOI92G$Q6ey(`kxEM<*;^+3N}+yeySp~)d1cIC!>8)`%XJUV{*wvN>SSVCIUf<8neJSsVKtXqB$Oh zyDkA>GU4bZj3HWtl(KKuC#XrcI8y?3FnjKpg=ppj$ZF?Wtb%AZU3T$Qg(oDJS6mOJ zw@E);-Xibt@8?96o=>>3Q?VhoZ^S1P`NSvCDfZD^Mx!*aT)zu~V$h&V;tjGC#X&Pb7K0PcOvn5DtnWqM)d}_`A0z_fuT=QX-e9 z5^E3#d)Bt1Z{+teR4#T{+*39R6nBIz;xdTT9FxLvP5)n$o8rU8SrP#zY1FXOVVAQ9 zEekG`%!y_~PLU%*TL|Z8H{7ZHhzqJ$#T4t=wJnLFjN7-`d+SpOylxGf_itIP z0v!_-d7hyn=Sj2-00xz(caJ?=I8knI6@X7oj!jllRQl);jM@QGda}<6d&5kfUtrY$ zSdmsoe65pHtEz9bnvDXH%+3Y&^pFnQE=4IEbwMNP_VRLy*TK4 z*voL~amDYl1?Rp?xVKmkV9*O3D=X6JmjBDebYg^<*gD9@B$~)A7b{5UWow}@rb|I1 zfnmCrUK-PaBB9WO44_LEbS3DHWRv+|h?Q(>8l^+-FD_49j#L}@8)PUVty6|@AAivr zyNQcFHZ^YTCCk0d2bb zhNVBMgAX-;$(Snr5|RDilrz?=gNeynSrqTjm?at2#GKNZzL!Yy3@yoO*ye29_9RrY zv7pRY)6_U8j|~87B73EKz6;#xjT!tsBonWQYBx=!_w(tNWXtW6Qy?MwG$wOwu#WsC z<#C?08di*H?ObplX`}PI2Ijg^7@+6?*fbA^HtJNLzEFqFBupKIQm=&?f~ij5R!g6J zE}p=HfXCRM=%~Wleq-eBhQ-cu!DR*~T3%saOzrA!*~S2}c}MNqVK@TdQQSbF1EzH; zgo8n~S^2;z)B7lAwxk~8LauX*iMWG;ab}pE_Z@~o#m0i|r*JyXO3%(n|T0DtBydU5q;imD4 zd{vqAFR>qWS-&dlKDfds{1&Ix951qr=>J zGnDbZW7KR^$o{PVfVH(@>N@p)$I9@?e6?ZL2^+^6dB6-?nf+M8o|qeM5Zk}K?EX0% zNnLuohUq$`h_HMEwn0@L0(14t?Q6`7b|>T=SZHt~30&KORwHM$ql(UdJABu)az0gx zc2Czbn>{dBCfBT($&$J{%kC{KH6zXZQ$F+A@X_~O zdZMn+rpGa6(`b6W>BFReqJKHfSD9ZKhD?VR6`V8Q%xLY3I~*@_y0s4ZW0NYCT$rz= zzU;k~yJtBnevLB90d&tNL+R}WREAt8_tC*k3mnQr9*0S#YeI`7*M1;!vrropLx2)C zl8A2v2a(!&;A#aQ{GPtuv3-~NbY!u|jwybneP0eYo`t%yvPqeiBhq=$d*R?VJwma5 zU*46Ops4*;a3SShW-4f&Sr~Vr&VLTOM8Q;u6fPuQ5p6F|0-D42Hb{`-4~@(SGqb4d zF1_cc)U-~?rjgH`hl-!4x!eOca&$Jvcu0PAl9pZqr#oQkf#n`Js@B<^2roZ%y0qhH zgnO?@dv-D$d-=S@J#kB=RU!hkO7ZQ3o+%>&&bLp-7IVi|4+I3jq=y^~hx3-Ii;)ll zsgX{)@6Vcmn+8VaS7R+Y0IvDSp9Oq$g>=Hgaqnk2u*PYXP!ZUclW)RIU67t^`-J?y?@*v#;Py3NaO>#IEDeN+ z7Z>sghK&B`ScjV`+5e%N6-h?t^@uVz_gfv&fo<-TZ47d>49KRLemgU_NAjlQ|!@++*??9{eCa6~AO$5WX*FaIXE-a}z z3H@DapFDV+{^uocyuMG=c+*=-XVBmmK;QqF0z$E`fb z_@#BMIpb^nf~KzYDo(M*BEu}XI*JD53OelwCN|mjrc1q$p!YoM`xR;tGw1vVWh3piQdumi07? zgOBG@Bp;Ud3YaR*+$8M6ebml~UvYnDf&`{$+;>WN8wn(lA zMK*^4cTt8L>!zb5!du_CAwns}s-eF*AAY!SpE;9K*B{JjS0kf93YfmOJrb)dHDUxV z4^cgLl`O6SJb2G({p(8|dz@Gv`!pbRNI#kbsoZ=yQImAjtO2=`mW|yI3$C-pnjZZ| z;&`2m4q57sBXUhxBaQRk$WQnmjSj?nfGU*PvFh1IV-~mE%M>YxOm7Dt(W@(;^!I6{ zJ7K`VA6QJzIv|B()|b$zc&##>r*NL|D}3B(hA8-Uo=+*$pQYq%ZA+9?l~mgj%D- z+OD95X@Fu-N%|}ibEX>f?pk#zZe}FB+qe`NWS&Z7t+4E8#H1_RuOb&RXOKEMfH3piOrG&|!9^ zCTJHQT%_t$y7PqVZqU}Y)$O2&zR=L9oj0AsY<2vcw^=pVh%dXOL+5LQ_V9u31|I4< z9M++IjdLw|Xu#AccW-f{j(g@e)yN#}(uE*EA$Oe)+<_(PMzrpNHoOYFv&*-ND((f5 z2JRWzr~gX2eOwn05(h0>kMV|OJu_c3k|6yR&KCH?JVEg;&6Aa>oQ(L1tj0tB8SGtz(bM|6bOf;wo=$LOL+-MVG39b3cEcHjZ-?3ZfL>bmSGRCS1KdiHH*?k}< z62WL-wx;9VQLrb9V@CX`0nQ_E?U4wg)!m zi^DRaU~p9o)_|(N<%39W#u^2l>k9OW`147hk{`Z{+zVMTWgs+8EH!~#S4ScTVS6_K_nvjP4D(aKnGXlil1T}EHe zj@M)ATFSiQJ^CPUmWoFm!81$Smeo@_7`E5?4aL}x+u%2ER&d1Tg`$JPE`MC4Q)G_@ zS{|L2Xc|8I=!f}YR4KK?hSmK5VmbiE;3o&1i!pBDkUHV-=)uE8S@J^Y)mh<}E^bZmDve~ntRYa3+508Ef>^E#ys$%Zd^7#>0+9|pS1bF9%*Qr7NR^AcM zmKzFRRLHfQPgv(&iZ4Clo2FZD5Rz_9YF9}THt_|1x5NxGZx9Qj@LNX42Fk>kA;ab| zxy-J=zeU%S%6IsPjy2l^Y6i}00g-0Z;ZCn`dJ*W$d-^{2+pk^vtI6#Zq=U=d8H&8s z7HwxEpFhbdq+1Y{2We<9$Tih-CPu~JLxQmw=BJubCvkQ5ro!xlYLSz08w-%Y^+$`q z2>vfr@5?YyTjE*@*}=S9n0xrjRwDbNB_ra$mDyH7!`1V4c4lJ?=vrIB1jurkBXY=* zyX+4c6u)J#Ro1vSvOjJn5ELlVr16`Vr_MqRT6LD!MJJrfn1k;zJ`yMtV}(*I7AkyB z-lmezWqFNd(y&3spo(bI)3Z#EAnDVy`^SUWyGdh!PK?=y!nX$eMyQ)C61)_VF2s$^ zwxUn_(fwx`_9q;?6ua+^-9@t%w+JPB$Bu0`w$-OMkyfNY(mK<&!pgqv<$&V1Bl{%o{QR)yVor1)51hh<4ezWFQwBJafo$S3g)lIp9&Gb^P0sGd6 zI=a8~7iALHo%ZMLv7j9E9*hwPmaOuivV6CBjJaK#do8IObHN$ar7uRYsD`Q!&^UKY zP=vV0shZwzqVKU`aM8H-E8`Qjl-unjuA7$N;_BR#YN_$_3`Xi|ObvZdE>*}T_gnxA z`NN!snbgqa%YzsK_$}i#Wx-g{6~pBXxG4DHQXeH>IJL8BJ_E9_&xvzAyABS>$pv{V z=GZow{f;_9FB*wl{^HMbGd33BP>&R^St*Mvr08lkTC-FQV=Cu6M9Yp0&-c<}847k9 z6L2^!CD zT~$mFzM;#0zU1&8mjnp~lNTzCKL}4So{LQ$y4f>35nrIJ!U}gq^H4$a=D{ewRKGKI z)_KiUT)AzHffJ=LXfwYQ?@Pdc^6aP=qD8$z0&_AL(#H$~KI`1VVAYd(1%UWJlI5^7$x-?=+{3n97$awDg1C zrgfYZOR3o_LW?gS%pyltOyI3Ynp#faDiTUiD2bwyUHGnOIP5_5R=}cdAydz#U4_exp<^!@JhlE>qxeSTp|-dIIK3bsi_i?mKN$`vfo|=Dcejp_1lDBGnP(#2Zd+6*Z!KaQv`2j4c<2(BtEgE7Dxwq*1{=uVJpE^+lZDCyW!_EQ%VD zu@7FCoIC&tjeH~NFMSE;Sz-)cYm))$ep)=Szc*!Ojag2;kIso3%&Se>+?x8(2wiQA zl?4^gIF1X7$V?LpDIdE2e$n~zgRc!is;o=Gk7g3L-j&Aj?pK$Ub1nj^NMYkY{1t>x z#T8}B^v3TBcb+Q_+?=yfGtFJbn@i7Z825v3S%?s<{(VlrWk(h$bjtL-%5NCZmQ-31xD|zXePwi9KCNaTXTtx{ffA#Nf+A_5`pt?p8wDmJ2vr4_7%InmC@Sy*WULVh@MF@}sF`~gM&J9G4z!@&7d z!Q-}Mjx-F|=1o{*jM>Mo^lTR!!o(y;wwRDxMvO(;ji*b1IRW6}{daCKQd0z~T z<{wk~ZBc}C&fSN%2aPA?`hT_(w~dc;fM7aljp-InF$L#{$&|ztSXoTo@Fc#8_V_7o6@}gC-cc6kO9;F z+NX(VN{Fn2NQWL0~shS5bmFaR+f)~m}VVVmf;_Ne#=2jm?Ryq5KDa_EtuOvh*&ZOOJV|@gf!?k*eau9g$3K^=21F+iuuvc)5L}<`|zwh*} z9XuE@%QNS6ej)yI;v$R36~^u!!-N7@P7vlUK4E6>!G)h~6*hfg z-R|~W%F5i7h_(i*@DF~Dd~ksUA;Awf?43gxD2?+t1%)j}ld3tx4LX{F-m#@>-w6Tk zSlT;lZF_xvmYglJ9&CH&Bj$&05nc1OzP_!XwbM2baFC5{dL;diycLYvPl-c;> ztbIvMN0{*SL0(Fb$<1FDBjp-!p)|erCQ0$lWhX@%6ctQcA8#sIA~d9(&O&#N7u*Ct z&k$PlkByZ1ckTV9Ko5hrB)dGeK0nT8JZ=rbw84qZ43&j{Y9A<5^te9MZ2=;rAu#?0 zW*?e}Z)6h5KNk&e^bc+Gkt3X_T~K{ZiWzA89{taEwkaYoGCme~Es3HcdLm7JXsPs^ zG_u6`l{YcW`c(>PY)6XKhCro@0cHKhAhaGJaS_eLzuy#G*)``@ZHu0MWxyB)jsT5P zJ6i6!*HGDFm(>?+L#I?3j#bNt_s0$#Q&e7vF>yK3ackUs(A#{z<1hOY$}e2jX#OQ3 z@*)161`~#4*sxEH*DiQ+T)|?!0G2<)D(3(DX5_A8&zhq-PJdL zor*uQ`#2JjPlvR7WvKtPjI83`&BR>~A@oYz;`(wxAOe2IL8FbQ+`ID0)9wzM%4b%7Zy>dbE}}!)n#>9J7?> zINhAkAgKV9cAi75;_zMHZSrxOH3nxYhu7p)7l?=%uQqa-4^u7XyYon%{6tA$7U*Gh z`Dg!=#VzCQciS^dGKj&m*;1HREGiFm>_CEX2FQ`88x z`M5)R?F2^Y5YBljjf1s*S47Y6ja5?f4WIpkq^oEZ>EO({E>E!~xHEN*VP^+dH@h zzBN)ProDHRI{qm%_H8sS)|si-LU6YBaRiP{*h;F)=*{bCch-Yt!=QLae4lWo=la~$ ztyw^~pz>?k81()G5YfWPR-QH2iq^fEdRmV%)PxXAONIhg@Dv00rKB}*2vHMuF&L9z zaWUiN9kvGnfVCbL@xUrpj>Q+{bYu65M`}i_Ph)>-3It1l`M329p)zqaSL*Ud)+v^%27TvOc zku9fgE;G!|6zjE*FJuC>sxW@S(|kbxlURU_-J*);gn!X0#l5UNaVAlmMam4GRA~k% z**)#){BRZ^K+dDW+>%m+kyzeMZ*B?anhJwd@h&#UVs0BFc&EVGoBFZ&C9TK6T&o+MS8P(EPak51t3G(63Q)(JVVJSIDimVgD_0ebdg z1N;^v1%|2$O1@5!xmQipa02;+k zg%JHs(kqLC^>!guhK-!gscDy+*kz1A=7QG9J>9_L~Cc0^BJ6RnC=- zGDbIy9ilSv2_Q-kiG3qaJc|3bXPv=ooL=X7Z}vf@k)@?+^NsaH0 zslKG3x~SINU)pOV<%0}ZH&$6}#Ie9wx3$ZJO3f^HRUY$g!9b@sSG9ORGaUw|f`3gz^>NZ}*K zEz5i;x^V~8avk?e$K8-<838+?`0CM7n(29|F{FBSj!gW-f9VS&3A+or`bv>>tW>8* z374bfNa3%m65hhjT(_z+Y{XQ-KasYF>Wo)yCJa}ua_@6!90x(vc2J_AkPN%YgM-fU zzknRFFV)zx%iFpK{3Hh4)Y!Ikn9S3BaE=dL=kK?sPX2r-;&Bk!Hc!&`hk3^WvL`A?~WUDddQwqpIrqD!RJt?J-1oL7HE`OIv!jrLN+zzpguB`PnD*IxX zVYXIyo3x^Lxg9OP&N4Cl0Db+WTSv!7??a8sgaU5mm(_L((U`I>-AOkiK$gSOlHN{*K$IRrS36w8)QAqLTFHa6) zTI|%i^>FOWqr&zg5scIRmT;LbR$;Ru6+^{_4)a)jFp`=avk7-D?wix_FnrIOp`Lbb zbk#iPX=>b$S>;%HQsStQVz%qZRgGi|0Aj}_(1N0?dtfemmOlI zFYA*-pY-}VBawYX4G`&m%nzn-XT#}@$|hhkodcK$`A1%7Hh*lYJ@c@2TtbK!SlcZY zfq8o@8*^Yf{5?WOG)yz$<|OO%M41y<@A322HT`ce;+eC_41;`|!?_X`MnU<(?y3@- zRykU1yJ>^ZqWVkEpyU*;#~a8zRY&xVtdijE8ujjyd1zxeXRYmi*Q2*WTG0m~CNRz9 zenBqz27}3@^$OFSm696wfXl8t8YWs+cTh!eDkeMMmh&MwVyE=0uSN}RsFiTIV$7a( z!(w|@=G2-=fJ!=my88?BFWjDYoiWvfJMphvh2T-N6cqFw4oa-{i6_eD4{^yFZnQ9* zA*7lVPln2=NbJia6bpjP??3Xq64apt&}G6sx-NzTg*Dg|jZ=r547A*p*@?Hm34A?y zX^N~Llu_+17Vrj3jZaAbrsc)^W+inaAhVjduH|$r`Rk$S)=y8)vzycRLgh!}4cpABENa9&U(boj3n?--f)nY3Sdg$-r1;c zW7tg|tytDwlX4s9jmBWi=ZsEyFMsDO>$@keP9_(t^<7jPA9K@uCHS%z$#HL9tWTRz z$opaBW#*J8J*=NCd;JV5r}gE@JOD|<+cEAS0&@rh%nr>b+~_QaBgTHc5(zZ)uiL83 zrmLkdM`7TT33=Y_yXKw-Od`|+Ouk3+pBK!eSWZ4=|26VM8GeENU54*^ zlC-B9bP&gsKJi2+j_yhFL-zr3;)#ZJ^F5Uw2l`QKZOux)B0(L|#Dn9TZx*V=T0c7w z8?%Z9@e}9O{9K-5t?0yczzjaho*neBJ>%ohXmU+sLzV(-_?Cv9ka1ZW%wR7Z{g`|?pdyv);#uLGI=^b)UVWXSkvG}LqU z=1Bmo0lG-$U_9b@7N6>)E5s1XYbHmS;T%$CucA~&gK(WEmwgLi)SiE87NT1(+EYF9 zkt1Px@%CYer9t#**fH!||m=*Rqy@Ji-c^2x4G zm8}d2@Bv;T)bo$=lfEN;XgQX7>64ap;db}p{t&|LPr1gLMR|%^W`kYWlB0JqlP3uV zBl5mSC3QV%9+-+6p6Po9(budYiX)j#tOZbv@?Ea5c$*C(Codq(9tF#tZAeN`bG{--l*Hn_)Yw^ovxMiQ(D{k zLg;d+_&z->!}PiPAnoHDAjUyPJe zSb%bfud! zzL~hw@sU@*lNm=OMk=1bkc(~xI!8rp2N-s(HCf!jNNp%asp@IQ~otJ^gY-Y9$^tL&CY;oD}o|iwSbW&@`}GBUwj*J`3V6#9|XW%$3m~k zdp6W!@5UVS8+wI7nDUFg4D{HEW1)!oJ*!b{blSiwb)cRJRq+Spq)<&CoD5|H6)C!^ znv^O%GY9&Di8#og_*5wi(z7S6*oC!bpWiP~j(SUf(h}!v3{}C<>rbl|Y@3 z!UKW;tu5Err_b$;i2`g)mINB?Sc1nUyz83%Rw<(zz}KI%Ty)eCp-8L5kNUcz9&sfN zX>Y@raLE|lxE|4%pC$)kC+%yN1uyUeiHE;_-Cv%$&oZZu3HKR` zgn?=6!X>b$Njdm{MW@Gd3uZ}m{-Lebf3dVPd8xhWsw5 z&%!U8_rZ~^v^;C8&_enKKNx3JK;b-;ZFtc1;z6O4ibr1{O6w})k=hfoO0$h=?A0$| zTh0oKYx)%vSgy6Jow|#oVV?MdZL*t3+b$-W8#8%T;ZwK$(2?=!u}0E7L=aJgc0OV+ z=qMp)yuWnL4PU3;%?MTSx7R_d$3a=?a=0|$z=+izMqKw1r^si7U{;JN#&;#hH1=OW z54U4)4hv-RSxO#uug3YMc*ftVxUGUrk73pvvE=@M2TI;8wx=b(cFNpe&3l_cZ3`vo zO#!v8!y0d38JvHln7{PcpFa(G|Gr_{Ap|CUFfhMhh;o1~$qnD24dfLfbs(mhQ~qnA z{9fe=CYETI66WPs17h0pp2+0$#=_yE`7@TjuR`PS=;1`+P20L(vhVOASb{?#kB~bY zWzn6@-5ux%Xap6UU@Gt>FR#0Z&Un5g8_z+IvOpFOT-q8$MZPCXNx6v|sVf$w6SL0~ z=8q~DSG~3;eBjOWA*a9!$Y&X#Z5=bFc0XlFUKFz+;gl-#PQm$6;SO@s^0Fer4GEP| z^d)DiB0^CAX@91eaE*aJXaIAeNQPuQmxhcvHQQIJYNenmG{baHqoBB+lvUbed>hlC z@{hyEe2OHo2`N}ki>()E&qZ|2RZK;S&WI`~CvHl@XL+^U?KeBaMQ#ZNSbC+w z78}nV#hJwAJovkny6I<}G!?&!=Q7OT+a9q)8frpu^J%uQW%8UCk_<6t)Jbj2wNw1J zK%4?=Y3Ln7%@TMw^Nip)odZmcrDN+(y$j^0<%{6)i!i`V2z1oY8_{hK|IS@6`*H1p8TpHz2V*%1(WZ zT`0YIL^>{3Hh4-dAv1$uq&Ci%e%pA?6li&vMnM)wK00Z0h;C()4T26;y@ggCl_V)t z^Tl2GnSfi}DSVjm$l`VG)3b(l`CK#_73IV}Uv2m61!Z&O4%qk`5{=r*Z?$(2Ds)9+ zdVU9u*#3ULtHazGC~R*_GUWT~wad)m8uxYN^vq4L!LHJg$OMG_l~{cEY^hGja#^BY zsJ&X)TbjcjFT>M8eT|U)+0+;GEiKtU({?824N-JwI(`nq7C=T60^DpI9UXRe;qUQU_Iw6f@BGOqI+uW zfU1A8h*25Vesd#Lr^jaL(3FKC99^zPP2(RfA2Z!ddy|;8p)Y`@-5ZppiBu`7kUk8d zFw&A#ogtxcK+G`Fp^ria?`gFnxI#z{mx^t*?5e{J+aC$FVuf;f#wxN*)fej z+g#HyV#dgwQ^B67oadqdM9Edm9R z`=p$O3{~#6(ngK=1b;32&zt$Oqvjg*n$X|q=JHD;<7v*e_oaVfv(o(}yJO*efz=eT zt1S?#y0YBTEf+C;l*j7`ikgBP?uo}K zWQ#P|v{={ht5u77G07cTqDSN$9-yTXv#Q_}i}xW*0*m*e*O#RrFtHBj+CzG3jFRzJ zkpRc?P2!$(Me~P(4(`mHTmW#wgQlEvwt(#SRzISiKkneiPJD*^pAw#^QzSX|$Vd#G z>==BZNt_abQd=1tGHIjkZsSUQ6qJ$6lyucfAE{#^5&0yEZGUELVMj7bF4rNDR|w9x z@r`ZSqes$|38F>EDKnH>3Q0K8->{R<$PX2N; zcs-H=MG1uj#^;(y>%<|7$MG?iF~+@|l3-A1l! zSL~>e=g1X{v|{?|D8(z`-s>`IZUqa(-Zh}goBx~(+DeWVvX^n2c7z`V?L?77%m~f- zi%nEhm+2fv($47{`8mu=sJqT3-TzZFX0I6_@pO5*-H+558F=Q(h)^ z^IKoQ`%G%dsklZ~jW+A@5%ZRdL_9g4iRCtJa-5}|-aU;p(=Uo8wP#1}k#1v6EYCf& zo9}ap(bDB8(Yw{bMt@KmI(`gMd63fjpQ9U1zqJmR`LjXwOf{YND53c}@AAsC@fN8Y z@&J!!7m-dX32>FY#Ixw$`O@MFOqbJbn)0h^6y>Xi42BZVlo}W!a?$?@ybDA0qnD?W zcEKy; z3kWO!DZJMf+jrl>mC!mVLx$|gS*-y;y})W?GJ$pYyFM99TbZF+awQK+HkPbDFh#}! zoi~6wrL5cBvG6QTvrhnQV=Swso{X+XOZJ?RpnRiXAoWMfs2fUwP;5}Ulr(730Y~f{abNYd9;Vqt|~lD`C4@$^u|#D%ZJ)NLIHk5L z(Zzn8yl9aJx7bwWm??8ZV@5k{&{7^+{GUx1rdFywh(egck}E^xGA$dqkhu&#KM2 zA7l*2d4f*YBpT@^o1APG>L+=1@fTjW?4LM{c?3AIQ3CPhdw3?F9bDw1Ft2a#gchLK zsLXqyiyEsMv@tXxUV@v}Uv(<{vjR1DiXkDiZBE9S3-&_)p2`EA7&k->O9Mo*?Ljzu$V~qIirmc!&uDZ++XX&7uAe`3Lr*EYEGPK4hlbK%F^O< zYd{e`l4?88^5NetjdG4@_Xn|}=BfK=D z3+rc#S#uRH(D3Ulhccq?mO-dyd92KIHqK}3qhTE=n69UinMT8aK}wzJ3-U?L0t8`@ z4g3>O*BqHb^wIU;4cI;N-^Wh~lK*>PgO3{mM!HP{chcvND5Ltd#&Hm$FY z2y$s~gItJ56$TZ8B2e8VQxN)CKpJd^N-{OmF2@ky@ zcKrlvbij^glKPgT2XKHw3eMb<4+m5%&J&r-6Q9Ki8Xk#w!YdJyY=odI(5EE`MH)y) zU_k+K^DM`aiX}%xO8<}sN50)4SN6(==GhhkD>LB0TsK%{0I`ktKopD+>LeOjV;skU zcq?=U)V9I+Q@X;sWSoi)pNh$tr^p~JBgDiau?bBg1Xo-X0ljz7`3Q2cL{Q`b(33dX zA=_0f;5E|si3&1Vw2{;ard+QNs<+ij*IQZg-((H`# zy}g#t!Luew=KV+VUgTY1!v+Q=0&AuhYH&&CI=N`mQm!uDu?D3O0^OM&$?4!j#s$Fk zhEa!c(w^r0C%7FB^hr3Rye3G{g}qq94a)SkP7pRMyJ@$*#5o%+Y);V~LO|~l0>&4`$NHEaQKZjlFH;j#P!=b0G_VuCgAC9$I?1ko z_=h4G=B`4v1NP!eV-r^x3HI=>Xj#;?@~9PI_6+o6273pS%5&F=h9m9r4l_t~x&eKd ztql>3{gtv95b-R*?xFNO%8*%+*Bw&PKS{vM=CSg)@^Dj))uC9tX}wpx+`*ro|I%0& zqEaxDCF$`+3gwd@qE#*Mej%jbuy9ING4jm+9IbjiJKS~60!RSt5u1<`s6}q>Px><^lesFt4+g+%U%EXedX8T)&H=k&#m>Y`XNPsFPu)|wh zd>l`rMo(FM5Cb3lYnzLMYwD=`%*gYJ3At^$%kkOy=X1c~L&nd6vgtPlEZqR3oD^Q* z&OU;tfS^V*y(<(xHdg`Y!>P2-#cfKYkx#C=kkaUSD`q?58E%PQ0RFjP;u>{ej4OH6 z7zFu`v0DSA+o@038!pniT`j%KOb({=Qpz_>Y-ZfyHZXxu(&I^1{*x;4lW;A)iNV5c zy9ClgqEv6SV61b1bfmhhqFg{+O`+s~P>R&=Gq9Lk-uSe6V|ryFi5T}7S5oD?6iDFw z;6*Z!L=6w=NDUTGM01v6T^BO>G0mjsGG&6=O!#SI0|bH5moS628sp<>+rsbNfC&le zR80;o@s~Vl@j47Od5T>wWHipGVusH>?p9M+LU2exf{@7(iO!s&@eD0=*;OdnkeAvA zz-t^q2)H$-$wWcmz$8@>CYCUfSXHcKb=+;5?4=KXC=zuVhIY3s%)wBDE3h@LfV~tJ zRXE7I<|9NoqqouB-NqZ*EKWz02uc?FCg^+>;E!L4mgn6D&E(&*XGDOErc{=`qqP4j zEvYYKvEJs?ao;2T3OgBV3rSxEj@v*li4IZ?^U2~~dCH;Hj8?(DQ~HE#Kr*5Qx?(2S2N850iFkzhxc~ka_}7QW<_H^>Ia<+7w`dt z(T12zWpKBs3%)W>H*dky2r*(WP62Zja3o%A*l3b`W!@V7VJ4mffDB6!;0(Om%r6|8 zUoa890HR1JEIJ4XiFk9V5t}8)~L_wpP literal 0 HcmV?d00001 diff --git a/docs/fonts/OpenSans-Light-webfont.svg b/docs/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000..11a472c --- /dev/null +++ b/docs/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Light-webfont.woff b/docs/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..e786074813a27d0a7a249047832988d5bf0fe756 GIT binary patch literal 22248 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 literal 0 HcmV?d00001 diff --git a/docs/fonts/OpenSans-LightItalic-webfont.eot b/docs/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..8f445929ffb03b50e98c2a2f7d831a0cb1b276a2 GIT binary patch literal 20535 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 Z%0#`NexefPFV?Xj51H#Y#AC7WXn+Jg($4?@ literal 0 HcmV?d00001 diff --git a/docs/fonts/OpenSans-LightItalic-webfont.svg b/docs/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000..431d7e3 --- /dev/null +++ b/docs/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-LightItalic-webfont.woff b/docs/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..43e8b9e6cc061ff17fd2903075cbde12715512b3 GIT binary patch literal 23400 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
        + +

        Source: hooks/events.js

        + + + + + + +
        +
        +
        import { Exporter } from "../scripts/modules/exporter.js";
        +import { Renderer } from "../scripts/modules/renderer.js";
        +import { ModuleSettings } from "../scripts/modules/settings.js";
        +
        +export class Events {
        +    constructor() {
        +        return this;
        +    }
        +
        +    /**
        +     * Reload Filters for a given entity
        +     * 
        +     * @param {HTMLCollection} html 
        +     * @param {String} entityType 
        +     */
        +    static async reloadFilters(html, entityType) {
        +        let entityBrowserHtml = await Renderer.renderFilters(game.compendiumBrowser.filters[entityType], entityType),
        +            filterWrapper = document.querySelector('.tab.active .browser .control-area');
        +        filterWrapper.innerHTML = entityBrowserHtml;
        +        Events.activateFilterListeners(html);
        +    }
        +
        +    /**
        +     * 
        +     * @param {HTMLCollection} app 
        +     */
        +    static async activateActionListener(app = document.getElementsByClassName('window-app')) {
        +        app = app.get(0);
        +        app.querySelector('button[data-action="export"]').addEventListener('click', async (e) => {
        +            let items = app.querySelectorAll('.content .tab.active .cb_entities .entity'),
        +                entityType = app.querySelector('.content .tab.active').dataset.tab,
        +                filters = JSON.parse(app.querySelector('.tab.active .cb_entities').dataset.activeFilters),
        +                tableItems = [],
        +                tableName = entityType + 'table: ';
        +
        +            items.forEach(item => {
        +                let obj = {};
        +                Object.assign(obj, item.dataset);
        +                tableItems.push(obj);
        +            });
        +
        +            let filterKeys = Object.keys(filters);
        +            if (filterKeys.length > 0) {
        +                for (let key of filterKeys) {
        +                    tableName = tableName + '_' + filters[key].path + '-' + filters[key].value;
        +                }
        +            }
        +
        +            let d = Dialog.confirm({
        +                title: "Export subset to table",
        +                content: "<p>Choose wisely.</p>",
        +                yes: () => Exporter.createTableFromSelection(tableName, tableItems),
        +                no: () => console.log("You chose ... poorly"),
        +                defaultYes: false
        +               });
        +
        +        });
        +    }
        +
        +    static async activateItemListListeners(app = document.getElementsByClassName('window-app')) {
        +        app = app[0];
        +
        +        // open entity sheet on click
        +        app.querySelectorAll('*[data-action="openSheet"]').forEach(async el => {
        +            el.addEventListener('click', async (e) => {
        +                let itemId = e.currentTarget.parentNode.dataset.entryId;
        +                let compendium = e.currentTarget.parentNode.dataset.entryCompendium;
        +                let pack = game.packs.find(p => p.collection === compendium);
        +                await pack.getEntity(itemId).then(entity => {
        +                    entity.sheet.render(true);
        +                });
        +            });
        +        });
        +
        +        // make draggable
        +        //0.4.1: Avoid the game.packs lookup
        +        app.querySelectorAll('.draggable').forEach(async li => {
        +            li.setAttribute("draggable", true);
        +            li.addEventListener('dragstart', event => {
        +                let packName = li.getAttribute("data-entry-compendium");
        +                let pack = game.packs.find(p => p.collection === packName);
        +                if (!pack) {
        +                    event.preventDefault();
        +                    return false;
        +                }
        +                event.dataTransfer.setData("text/plain", JSON.stringify({
        +                    type: pack.entity,
        +                    pack: pack.collection,
        +                    id: li.getAttribute("data-entry-id")
        +                }));
        +            }, false);
        +        });
        +    }
        +
        +    static async observeListElement(list, tag) {
        +        for (let element of list.getElementsByTagName(tag)) {
        +            game.compendiumBrowser.observer.observe(element);
        +        }
        +    }
        +
        +    static async activateFilterListeners(html, app = document.getElementsByClassName('window-app')) {
        +        app = app[0];
        +
        +        // toggle visibility of filter containers
        +        html.find('.filtercontainer h3, .multiselect label').click(async ev => {
        +            await $(ev.target.nextElementSibling).toggle(100);
        +        });
        +
        +        html.find('.multiselect label').trigger('click');
        +
        +        // reset filters and re-rendes
        +        app.querySelectorAll('.button[data-action="reset-filters').forEach(async el => {
        +            el.addEventListener('click', async (e) => {
        +                game.compendiumBrowser.filters.resetFilters();
        +                game.compendiumBrowser.refreshList = e.target.closest('.tab').dataset.tab;
        +                game.compendiumBrowser.render();
        +            });
        +        });
        +
        +        // select filters
        +        app.querySelectorAll('.settings input').forEach(async el => {
        +            el.addEventListener('keyup change paste', async (e) => {
        +                let target = e.target,
        +                setting = target.dataset.setting,
        +                value = target.checked,
        +                key = target.dataset.key,
        +                category = (target.dataset.type === 'Spell' || target.dataset.type === 'Feat') ? 'Item' : target.dataset.type;
        +
        +            if (key)
        +                game.compendiumBrowser.settings.loadedCompendium[category][key].load = value;
        +
        +            ui.notifications.info("Settings Saved. Compendiums are being reloaded.");
        +
        +            switch (setting) {
        +                case 'allow-spell-browser':
        +                    game.compendiumBrowser.settings.allowSpellBrowser = value;
        +                    break;
        +                case 'allow-feat-browser':
        +                    game.compendiumBrowser.settings.allowFeatBrowser = value;
        +                    break;
        +                case 'allow-item-browser':
        +                    game.compendiumBrowser.settings.allowItemBrowser = value;
        +                    break;
        +                case 'allow-actor-browser':
        +                    game.compendiumBrowser.settings.allowActorBrowser = value;
        +                    break;
        +                case 'allow-rolltable-browser':
        +                    game.compendiumBrowser.settings.allowRollTableBrowser = value;
        +                    break;
        +                case 'allow-journalentry-browser':
        +                    game.compendiumBrowser.settings.allowJournalEntryBrowser = value;
        +                    break;
        +                default:
        +                    break;
        +            }
        +            ModuleSettings.saveSettings();
        +            game.compendiumBrowser.render();
        +            });
        +        });
        +
        +        // select filters
        +        app.querySelectorAll('.filter[data-type=text] input, .filter[data-type=text] select').forEach(async el => {
        +            el.addEventListener('keyup change paste', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path,
        +                     key = path.replace(/\./g, ''),
        +                     entityType = app.querySelector('.content .tab.active').dataset.tab;
        +
        +                     if (e.target.value === '' || e.target.value === undefined) {
        +                        delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                    } else {
        +                        game.compendiumBrowser.filters[entityType].activeFilters[key] = {
        +                            path: path,
        +                            type: 'text',
        +                            valIsArray: false,
        +                            value: e.target.value
        +                        }
        +                    }
        +        
        +                    game.compendiumBrowser.replaceList(html, entityType);
        +            });
        +        });
        +
        +        // select filters
        +        app.querySelectorAll('.filter[data-type=select] select, .filter[data-type=bool] select').forEach(async el => {
        +            el.addEventListener('change', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path,
        +                    key = path.replace(/\./g, ''),
        +                    filterType = e.target.closest('.filter').dataset.type,
        +                    entityType = app.querySelector('.content .tab.active').dataset.tab;
        +
        +                let valIsArray = e.target.closest('.filter').dataset.valIsArray === 'true',
        +                    value = e.target.options[e.target.selectedIndex].value;
        +
        +                if (value === "null") {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                } else {
        +                    game.compendiumBrowser.filters[entityType].activeFilters[key] = {
        +                        path: path,
        +                        type: filterType,
        +                        valIsArray: valIsArray,
        +                        value: value
        +                    }
        +                }
        +                game.compendiumBrowser.replaceList(html, entityType);
        +
        +            });
        +        });
        +
        +
        +        // multiselect
        +        app.querySelectorAll('.filter[data-type=multiSelect] input').forEach(async el => {
        +            el.addEventListener('change', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path,
        +                    key = path.replace(/\./g, ''),
        +                    filterType = 'multiSelect',
        +                    entityType = app.querySelector('.content .tab.active').dataset.tab,
        +                    filter = game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                let valIsArray = e.target.closest('.filter').dataset.valIsArray,
        +                    value = e.target.dataset.value;
        +
        +                if (e.target.checked === true) {
        +                    if (filter === undefined) {
        +                        game.compendiumBrowser.filters[entityType].activeFilters[key] =
        +                            { path: path, type: filterType, valIsArray: valIsArray, values: [value] };
        +                    } else {
        +                        game.compendiumBrowser.filters[entityType].activeFilters[key].values.push(value);
        +                    }
        +                } else {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key].values.splice(game.compendiumBrowser.filters[entityType].activeFilters[key].values.indexOf(value), 1);
        +
        +                    if (game.compendiumBrowser.filters[entityType].activeFilters[key].values.length === 0)
        +                        delete game.compendiumBrowser.filters[entityType].activeFilters[key];
        +                }
        +
        +                game.compendiumBrowser.replaceList(html, entityType);
        +            });
        +        });
        +
        +        app.querySelectorAll('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').forEach(async el => {
        +            el.addEventListener('change keyup paste', async (e) => {
        +                const path = e.target.closest('.filter').dataset.path,
        +                    key = path.replace(/\./g, ''),
        +                    filterType = 'numberCompare',
        +                    entityType = app.querySelector('.content .tab.active').dataset.tab,
        +                    operator = e.target.closest('.filter').getElementsByTagName('select').val,
        +                    value = e.target.closest('.filter').getElementsByTagName('input').val;
        +
        +                if (value === '' || operator === 'null') {
        +                    delete game.compendiumBrowser.filters[entityType].activeFilters[key]
        +                } else {
        +                    game.compendiumBrowser.filters[entityType].activeFilters[key] = {
        +                        path: path,
        +                        type: filterType,
        +                        valIsArray: false,
        +                        operator: operator,
        +                        value: value
        +                    }
        +                }
        +
        +                game.compendiumBrowser.replaceList(html, browserTab);
        +            });
        +        });
        +    }
        +}
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/hooks_moduleHooks.js.html b/docs/hooks_moduleHooks.js.html new file mode 100644 index 0000000..dfe2fa1 --- /dev/null +++ b/docs/hooks_moduleHooks.js.html @@ -0,0 +1,152 @@ + + + + + JSDoc: Source: hooks/moduleHooks.js + + + + + + + + + + +
        + +

        Source: hooks/moduleHooks.js

        + + + + + + +
        +
        +
        /**
        + * @fileOverview Holding the modules Hooks
        + * 
        + * @author Daniel Böttner <jackprince1983@gmail.com>
        + * @version 1.0.0
        + */
        +/* jshint node: true */
        +
        +'use strict';
        +
        +import { CMPBrowser } from '../scripts/modules/settings.js';
        +import { CompendiumBrowser } from '../scripts/compendium-browser.js';
        +import { VersionCheck}  from '../scripts/versioning/version-check.js';
        +import { Renderer } from '../scripts/modules/renderer.js';
        +
        +/**
        + * Holds the static methods to be called 
        + */
        +export default class moduleHooks {
        +
        +    static onInit() {
        +        Hooks.on('init', async () => {
        +            Handlebars.registerHelper('switch', function (value, options) {
        +                this.switch_value = value;
        +                this.switch_break = false;
        +                return options.fn(this);
        +            });
        +
        +            Handlebars.registerHelper('case', function (value, options) {
        +                if (value == this.switch_value) {
        +                    this.switch_break = true;
        +                    return options.fn(this);
        +                }
        +            });
        +
        +            Handlebars.registerHelper('default', function (value, options) {
        +                if (this.switch_break == false) {
        +                    return value;
        +                }
        +            });
        +
        +            Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
        +                switch (operator) {
        +                    case '==':
        +                        return (v1 == v2) ? options.fn(this) : options.inverse(this);
        +                    case '===':
        +                        return (v1 === v2) ? options.fn(this) : options.inverse(this);
        +                    case '!=':
        +                        return (v1 != v2) ? options.fn(this) : options.inverse(this);
        +                    case '!==':
        +                        return (v1 !== v2) ? options.fn(this) : options.inverse(this);
        +                    case '<':
        +                        return (v1 < v2) ? options.fn(this) : options.inverse(this);
        +                    case '<=':
        +                        return (v1 <= v2) ? options.fn(this) : options.inverse(this);
        +                    case '>':
        +                        return (v1 > v2) ? options.fn(this) : options.inverse(this);
        +                    case '>=':
        +                        return (v1 >= v2) ? options.fn(this) : options.inverse(this);
        +                    case '&&':
        +                        return (v1 && v2) ? options.fn(this) : options.inverse(this);
        +                    case '||':
        +                        return (v1 || v2) ? options.fn(this) : options.inverse(this);
        +                    default:
        +                        return options.inverse(this);
        +                }
        +            });
        +
        +            Handlebars.registerHelper("debug", function (optionalValue) {
        +                console.log("Current Context");
        +                console.log("====================");
        +                console.log(this);
        +                if (optionalValue) {
        +                    console.log("Value");
        +                    console.log("====================");
        +                    console.log(optionalValue);
        +                }
        +            });
        +        });
        +    }
        +
        +    static onReady() {
        +        Hooks.on('ready', async () => {
        +            if (game.compendiumBrowser === undefined) {
        +                game.compendiumBrowser = new CompendiumBrowser();
        +                //0.4.0 Defer loading content until we actually use the Compendium Browser
        +                //A compromise approach would be better (periodic loading) except would still create the memory use problem
        +                await game.compendiumBrowser.initialize();
        +            }
        +
        +            if (VersionCheck.check(CMPBrowser.MODULE_NAME) && game.user.isGM) {
        +                console.log('version check');
        +              }
        +        });
        +    }
        +
        +    static onRenderComplete() {
        +        Hooks.on("renderCompendiumBrowser", Renderer.afterRender);
        +    }
        +
        +
        +}
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..0d0965e --- /dev/null +++ b/docs/index.html @@ -0,0 +1,271 @@ + + + + + JSDoc: Home + + + + + + + + + + +
        + +

        Home

        + + + + + + + + +

        + + + + + + + + + + + + + + + + + + + + + + + +
        + +
        + +

        dist/hooks/moduleHooks.js

        + + +
        + +
        +
        + + +
        Holding the modules Hooks
        + + + + + +
        + + +
        Version:
        +
        • 1.0.0
        + + + + + + + + + + + + + + + + + +
        Author:
        +
        + +
        + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + +
        + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + + + + + +
        + +
        + +

        hooks/moduleHooks.js

        + + +
        + +
        +
        + + +
        Holding the modules Hooks
        + + + + + +
        + + +
        Version:
        +
        • 1.0.0
        + + + + + + + + + + + + + + + + + +
        Author:
        +
        + +
        + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + +
        + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + \ No newline at end of file diff --git a/docs/module.exports.html b/docs/module.exports.html new file mode 100644 index 0000000..b596336 --- /dev/null +++ b/docs/module.exports.html @@ -0,0 +1,301 @@ + + + + + JSDoc: Class: exports + + + + + + + + + + +
        + +

        Class: exports

        + + + + + + +
        + +
        + +

        exports()

        + +
        Holds the static methods to be called
        + + +
        + +
        +
        + + + + +

        Constructor

        + + + +

        new exports()

        + + + + + + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + + + + + +
        + +
        + +

        exports()

        + +
        Holds the static methods to be called
        + + +
        + +
        +
        + + + + +

        Constructor

        + + + +

        new exports()

        + + + + + + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Source:
        +
        + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + + +
        + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + \ No newline at end of file diff --git a/docs/modules.html b/docs/modules.html new file mode 100644 index 0000000..f219961 --- /dev/null +++ b/docs/modules.html @@ -0,0 +1,89 @@ + + + + + + compendium-browser + + + + + + +
        +
        +
        +
        + +
        +
        + Options +
        +
        + All +
          +
        • Public
        • +
        • Public/Protected
        • +
        • All
        • +
        +
        + + + + +
        +
        + Menu +
        +
        +
        +
        +
        +
        +

        compendium-browser

        +
        +
        +
        +
        +
        +
        +
        + +
        +
        +
        +
        +

        Legend

        +
        +
        +
        +
        +
        +

        Generated using TypeDoc

        +
        +
        + + + \ No newline at end of file diff --git a/docs/scripts/linenumber.js b/docs/scripts/linenumber.js new file mode 100644 index 0000000..4354785 --- /dev/null +++ b/docs/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(() => { + const source = document.getElementsByClassName('prettyprint source linenums'); + let i = 0; + let lineNumber = 0; + let lineId; + let lines; + let totalLines; + let anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = `line${lineNumber}`; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/docs/scripts/prettify/Apache-License-2.0.txt b/docs/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/docs/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/scripts/prettify/lang-css.js b/docs/scripts/prettify/lang-css.js new file mode 100644 index 0000000..041e1f5 --- /dev/null +++ b/docs/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/docs/scripts/prettify/prettify.js b/docs/scripts/prettify/prettify.js new file mode 100644 index 0000000..eef5ad7 --- /dev/null +++ b/docs/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p + + + + JSDoc: Source: scripts/compendium-browser.js + + + + + + + + + + +
        + +

        Source: scripts/compendium-browser.js

        + + + + + + +
        +
        +
        import { ModuleSettings, CMPBrowser } from './modules/settings.js';
        +import { Entities } from './modules/entities.js';
        +import { Events } from '../hooks/events.js';
        +import { Filter } from './modules/filter.js';
        +import { Renderer } from './modules/renderer.js';
        +
        +//import Exporter from './modules/exporter.mjs';
        +/* eslint-disable valid-jsdoc */
        +/* eslint-disable complexity */
        +
        +export class CompendiumBrowser extends Application {
        +
        +    constructor() {
        +        super();
        +
        +        ModuleSettings.registerGameSettings();
        +        this.settings = ModuleSettings.initModuleSettings();
        +
        +        this.filters = new Filter();
        +        this.currentLists = {};
        +    }
        +
        +    /**
        +     * 
        +     */
        +    static get defaultOptions() {
        +        const options = super.defaultOptions;
        +        mergeObject(options, {
        +            title: "CMPBrowser.compendiumBrowser",
        +            tabs: [{ navSelector: ".tabs", contentSelector: ".content", initial: "Item" }],
        +            classes: options.classes.concat('compendium-browser'),
        +            template: "modules/compendium-browser/template/template.hbs",
        +            width: 900,
        +            height: 800,
        +            resizable: true,
        +            minimizable: true
        +        });
        +        return options;
        +    }
        +
        +    async initialize() {
        +        // load settings
        +        if (this.settings === undefined) {
        +            this.settings = ModuleSettings.initModuleSettings();
        +        }
        +
        +        Renderer.loadTemplates([
        +            "modules/compendium-browser/template/template.hbs",
        +            "modules/compendium-browser/template/entity-browser.hbs",
        +            "modules/compendium-browser/template/entity-list.hbs",
        +            "modules/compendium-browser/template/filter-container.hbs",
        +            "modules/compendium-browser/template/settings.hbs",
        +            "modules/compendium-browser/template/loading.hbs"
        +        ]);
        +
        +        this.filters.addEntityFilters();
        +        this.hookCompendiumList();
        +    }
        +
        +
        +    /** override */
        +    _onChangeTab(event, tabs, active) {
        +        super._onChangeTab(event, tabs, active);
        +        const html = this.element;
        +        if (active != 'setting') {
        +            Events.reloadFilters(html, active);
        +            this.replaceList(html, active, { reload: false });
        +        }
        +    }
        +
        +    /**
        +     * 
        +     * @returns {Obejct} data
        +     */
        +    async getData() {
        +        let data = {
        +            items: [],
        +            actors: [],
        +            filters: {
        +                Spell: this.filters.getByName('Spell'),
        +                Item: this.filters.getByName('Item'),
        +                Actor: this.filters.getByName('Actor'),
        +                RollTable: this.filters.getByName('RollTable'),
        +                JournalEntry: this.filters.getByName('RollTable')
        +            },
        +            showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser,
        +            showFeatBrowser: (game.user.isGM) || this.settings.allowFeatBrowser,
        +            showItemBrowser: (game.user.isGM) || this.settings.allowItemBrowser,
        +            showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser,
        +            showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser,
        +            showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser,            
        +            settings: this.settings,
        +            isGM: game.user.isGM
        +        };
        +
        +        return data;
        +    }
        +
        +    /** override */
        +    activateListeners(html) {
        +        super.activateListeners(html);
        +
        +        this.observer = new IntersectionObserver((entries, observer) => {
        +            for (let [i, e] of entries.entries()) {
        +                if (!e.isIntersecting) break;
        +                const el = e.target;
        +                // Avatar image
        +                //const img = li.querySelector("img");
        +                if (el && el.dataset.src) {
        +                    el.style['background-image'] = `url(${el.dataset.src})`;
        +                    delete el.dataset.src;
        +                }
        +
        +                // No longer observe the target
        +                observer.unobserve(e.target);
        +            }
        +        });
        +
        +        Events.activateActionListener(html);
        +        Events.activateItemListListeners(html);
        +        Events.activateFilterListeners(html);
        +
        +        //Just for the loading image
        +        if (this.observer) {
        +            html.find(".entity-image").each((i, imageElement) => this.observer.observe(imageElement));
        +        }
        +    }
        +
        +    hookCompendiumList() {
        +        Hooks.on('renderCompendiumDirectory', (app, html, data) => {
        +            this.hookCompendiumList();
        +        });
        +
        +        let html = $('#compendium');
        +        if (this.settings === undefined) {
        +            this.initSettings();
        +        }
        +        if (game.user.isGM || this.settings.allowItemBrowser || this.settings.allowSpellBrowser || this.settings.allowActorBrowser) {
        +            const cbButton = $(`<button class="compendium-browser-btn"><i class="fas fa-fire"></i> ${game.i18n.localize("CMPBrowser.compendiumBrowser")}</button>`);
        +            html.find('.compendium-browser-btn').remove();
        +
        +            // adding to directory-list since the footer doesn't exist if the user is not gm
        +            html.find('.directory-footer').append(cbButton);
        +
        +            // Handle button clicks
        +            cbButton.click(ev => {
        +                ev.preventDefault();
        +                //0.4.1: Reset filters when you click button
        +                this.filters.resetFilters();
        +                //0.4.3: Reset everything (including data) when you press the button - calls afterRender() hook
        +
        +                if (game.user.isGM || this.settings.allowSpellBrowser) {
        +                    this.refreshList = "Spell";
        +                } else if (this.settings.allowFeatBrowser) {
        +                    this.refreshList = "Feat";
        +                } else if (this.settings.allowItemBrowser) {
        +                    this.refreshList = "Item";
        +                } else if (this.settings.allowActorBrowser) {
        +                    this.refreshList = "Actor";
        +                } else if (this.settings.allowJournalEntryBrowser) {
        +                    this.refreshList = "JournalEntry";
        +                } else if (this.settings.allowRollTableBrowser) {
        +                    this.refreshList = "RollTable";
        +                }
        +                this.render(true);
        +            });
        +        }
        +    }
        +
        +    /**
        +     * 
        +     * @param {*} html 
        +     * @param {*} entityType 
        +     * @param {*} options 
        +     */
        +    async replaceList(html, entityType, options = { reload: true }) {
        +        //After rendering the first time or re-rendering trigger the load/reload of visible data
        +        let entityListElement = document.querySelector('.tab.active .browser .cb_entities');
        +
        +        if (entityListElement.childElementCount !== undefined) {
        +            //0.4.2b: On a tab-switch, only reload if there isn't any data already 
        +            if (options?.reload || entityListElement.childElementCount < 1) {
        +
        +                const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD;
        +                await Renderer.updateLoading(entityType, 0, maxLoad);
        +
        +                // loadItems
        +                const entityHelper = new Entities();
        +                let entityList = await entityHelper.loadAndFilter(entityType, true);
        +                this.currentLists[entityType] = entityList = Entities._sortList(entityList, entityType);
        +                //Uses loadAndFilterItems to read compendia for items which pass the current filters and render on this tab
        +                const newEntitiesHTML = await Renderer.renderEntityList(entityList.entities, entityType, true);
        +                entityListElement.setAttribute('data-active-filters', entityList.activeFilters);
        +                entityListElement.innerHTML = newEntitiesHTML;
        +
        +                await Events.observeListElement(entityListElement, 'aside');
        +
        +                //Reactivate listeners for clicking and dragging
        +                Events.activateItemListListeners(html);
        +            }
        +        }
        +
        +    }
        +
        +    clearObject(obj) {
        +        let newObj = {};
        +        for (let key in obj) {
        +            if (obj[key] == true) {
        +                newObj[key] = true;
        +            }
        +        }
        +        return newObj;
        +    }
        +
        +}
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/scripts/modules/exporter.mjs b/docs/scripts_modules_exporter.js.html similarity index 56% rename from scripts/modules/exporter.mjs rename to docs/scripts_modules_exporter.js.html index d5b0e89..a9d9651 100644 --- a/scripts/modules/exporter.mjs +++ b/docs/scripts_modules_exporter.js.html @@ -1,4 +1,32 @@ -export default class Exporter { + + + + + JSDoc: Source: scripts/modules/exporter.js + + + + + + + + + + +
        + +

        Source: scripts/modules/exporter.js

        + + + + + + +
        +
        +
        export class Exporter {
         
             /**
              * Create a new RollTable by extracting entries from a compendium.
        @@ -12,7 +40,7 @@
                 let data = { name: tableName },
                     tableArray = [];
         
        -        if (itemSubset && (itemSubset.length > 0)) {
        +        if (itemSubset && (itemSubset.length > 0)) {
                     const newTable = await RollTable.create(data);
         
                     ui.notifications.info(`Starting generation of a rolltable with ${itemSubset.length} entries.`);
        @@ -43,4 +71,26 @@
                     ui.notifications.warn(`Compendium named ${item.packName} not found`);
                 }
             }
        -}
        \ No newline at end of file
        +}
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 20 2021 16:43:22 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/scripts_modules_settings.js.html b/docs/scripts_modules_settings.js.html new file mode 100644 index 0000000..c920f91 --- /dev/null +++ b/docs/scripts_modules_settings.js.html @@ -0,0 +1,155 @@ + + + + + JSDoc: Source: scripts/modules/settings.js + + + + + + + + + + +
        + +

        Source: scripts/modules/settings.js

        + + + + + + +
        +
        +
        export const CMPBrowser = {
        +    MODULE_NAME: "compendium-browser",
        +    MODULE_VERSION: "0.5.1",
        +    MAXLOAD: 500,      //Default for the maximum number to load before displaying a message that you need to filter to see more    
        +};
        +
        +const SETTINGS = 'settings';
        +
        +export class ModuleSettings {
        +
        +    /**
        +     * constructs and returns defaults settings
        +     */
        +    static _getDefaults() {
        +        let defaultSettings = {
        +            loadedCompendium: {
        +                Actor: {},
        +                Item: {},
        +                JournalEntry: {},
        +                RollTable: {},
        +            }
        +        };
        +
        +        for (let compendium of game.packs) {
        +            if(defaultSettings.loadedCompendium[compendium.metadata.entity]){
        +                defaultSettings.loadedCompendium[compendium.metadata.entity][compendium.collection] = {
        +                    load: true,
        +                    name: `${compendium.metadata.label} (${compendium.collection})`
        +                };
        +            }
        +        }
        +
        +        return defaultSettings;
        +    }
        +
        +    /**
        +     * 
        +     * @returns {Array} Settings
        +     */
        +    static initModuleSettings() {
        +        let defaultSettings = ModuleSettings._getDefaults();
        +        // load settings from container and apply to default settings (available compendia might have changed)
        +        let settings = game.settings.get(CMPBrowser.MODULE_NAME, SETTINGS);
        +        for (let compKey in defaultSettings.loadedSpellCompendium) {
        +            if (settings.loadedSpellCompendium[compKey] !== undefined) {
        +                defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load;
        +            }
        +        }
        +        for (let compKey in defaultSettings.loadedActorCompendium) {
        +            if (settings.loadedActorCompendium[compKey] !== undefined) {
        +                defaultSettings.loadedActorCompendium[compKey].load = settings.loadedActorCompendium[compKey].load;
        +            }
        +        }
        +
        +        defaultSettings.allowSpellBrowser = settings.allowSpellBrowser ? true : false;
        +        defaultSettings.allowFeatBrowser = settings.allowFeatBrowser ? true : false;
        +        defaultSettings.allowItemBrowser = settings.allowItemBrowser ? true : false;
        +        defaultSettings.allowActorBrowser = settings.allowActorBrowser ? true : false;
        +        defaultSettings.allowJournalEntryBrowser = settings.allowJournalEntryBrowser ? true : false;
        +        defaultSettings.allowRollTableBrowser = settings.allowRollTableBrowser ? true : false;
        +        
        +        if (game.user.isGM) {
        +            game.settings.set(CMPBrowser.MODULE_NAME, SETTINGS, defaultSettings);
        +            console.log("New default settings set");
        +            console.log(defaultSettings);
        +        }
        +
        +        return defaultSettings;
        +    }
        +
        +    /**
        +     * Registe ther very basic settings in the game world.
        +     */
        +    static registerGameSettings(){
        +        // creating game setting container
        +        game.settings.register(CMPBrowser.MODULE_NAME, SETTINGS, {
        +            name: "Compendium Browser Settings",
        +            hint: "Settings to exclude packs from loading and visibility of the browser",
        +            default: ModuleSettings._getDefaults(),
        +            type: Object,
        +            scope: 'world',
        +            onChange: settings => {
        +                game.compendiumBrowser.settings = settings;
        +            }
        +        });
        +
        +        game.settings.register(CMPBrowser.MODULE_NAME, "maxload", {
        +            name: game.i18n.localize("CMPBrowser.SETTING.Maxload.NAME"),
        +            hint: game.i18n.localize("CMPBrowser.SETTING.Maxload.HINT"),
        +            scope: "world",
        +            config: true,
        +            default: CMPBrowser.MAXLOAD,
        +            type: Number,
        +            range: {             // If range is specified, the resulting setting will be a range slider
        +                min: 200,
        +                max: 5000,
        +                step: 100
        +            }
        +        });
        +    }
        +
        +    static saveSettings() {
        +        game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings);
        +    }
        +}
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/scripts_versioning_version-check.js.html b/docs/scripts_versioning_version-check.js.html new file mode 100644 index 0000000..8546c8b --- /dev/null +++ b/docs/scripts_versioning_version-check.js.html @@ -0,0 +1,89 @@ + + + + + JSDoc: Source: scripts/versioning/version-check.js + + + + + + + + + + +
        + +

        Source: scripts/versioning/version-check.js

        + + + + + + +
        +
        +
        /**
        + * Version check function from Forien Unedintified item module
        + */
        + export class VersionCheck {
        +    static _r = false;
        +  
        +    static _reg(mN) {
        +      if (this._r) return;
        +  
        +      game.settings.register(mN, 'version', {
        +        name: `${mN} Version`,
        +        default: "0.0.0",
        +        type: String,
        +        scope: 'client',
        +      });
        +  
        +      this._r = true;
        +    }
        +  
        +    static check(mN) {
        +      if (!this._r) this._reg(mN);
        +  
        +      let mV = this.get(mN);
        +      let oV = game.settings.get(mN, "version");
        +  
        +      return isNewerVersion(mV, oV);
        +    }
        +  
        +    static set(mN, v) {
        +      if (!this._r) this._reg(mN);
        +  
        +      game.settings.set(mN, "version", v);
        +    }
        +  
        +    static get(mN) {
        +      return game.modules.get(mN).data.version;
        +    }
        +  }
        +  
        +
        +
        + + + + +
        + + + +
        + +
        + Documentation generated by JSDoc 3.6.7 on Tue Jul 27 2021 19:37:39 GMT+0200 (Mitteleuropäische Sommerzeit) +
        + + + + + diff --git a/docs/styles/jsdoc-default.css b/docs/styles/jsdoc-default.css new file mode 100644 index 0000000..7d1729d --- /dev/null +++ b/docs/styles/jsdoc-default.css @@ -0,0 +1,358 @@ +@font-face { + font-family: 'Open Sans'; + font-weight: normal; + font-style: normal; + src: url('../fonts/OpenSans-Regular-webfont.eot'); + src: + local('Open Sans'), + local('OpenSans'), + url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), + url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); +} + +@font-face { + font-family: 'Open Sans Light'; + font-weight: normal; + font-style: normal; + src: url('../fonts/OpenSans-Light-webfont.eot'); + src: + local('Open Sans Light'), + local('OpenSans Light'), + url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-Light-webfont.woff') format('woff'), + url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg'); +} + +html +{ + overflow: auto; + background-color: #fff; + font-size: 14px; +} + +body +{ + font-family: 'Open Sans', sans-serif; + line-height: 1.5; + color: #4d4e53; + background-color: white; +} + +a, a:visited, a:active { + color: #0095dd; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +header +{ + display: block; + padding: 0px 4px; +} + +tt, code, kbd, samp { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.class-description { + font-size: 130%; + line-height: 140%; + margin-bottom: 1em; + margin-top: 1em; +} + +.class-description:empty { + margin: 0; +} + +#main { + float: left; + width: 70%; +} + +article dl { + margin-bottom: 40px; +} + +article img { + max-width: 100%; +} + +section +{ + display: block; + background-color: #fff; + padding: 12px 24px; + border-bottom: 1px solid #ccc; + margin-right: 30px; +} + +.variation { + display: none; +} + +.signature-attributes { + font-size: 60%; + color: #aaa; + font-style: italic; + font-weight: lighter; +} + +nav +{ + display: block; + float: right; + margin-top: 28px; + width: 30%; + box-sizing: border-box; + border-left: 1px solid #ccc; + padding-left: 16px; +} + +nav ul { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; + font-size: 100%; + line-height: 17px; + padding: 0; + margin: 0; + list-style-type: none; +} + +nav ul a, nav ul a:visited, nav ul a:active { + font-family: Consolas, Monaco, 'Andale Mono', monospace; + line-height: 18px; + color: #4D4E53; +} + +nav h3 { + margin-top: 12px; +} + +nav li { + margin-top: 6px; +} + +footer { + display: block; + padding: 6px; + margin-top: 12px; + font-style: italic; + font-size: 90%; +} + +h1, h2, h3, h4 { + font-weight: 200; + margin: 0; +} + +h1 +{ + font-family: 'Open Sans Light', sans-serif; + font-size: 48px; + letter-spacing: -2px; + margin: 12px 24px 20px; +} + +h2, h3.subsection-title +{ + font-size: 30px; + font-weight: 700; + letter-spacing: -1px; + margin-bottom: 12px; +} + +h3 +{ + font-size: 24px; + letter-spacing: -0.5px; + margin-bottom: 12px; +} + +h4 +{ + font-size: 18px; + letter-spacing: -0.33px; + margin-bottom: 12px; + color: #4d4e53; +} + +h5, .container-overview .subsection-title +{ + font-size: 120%; + font-weight: bold; + letter-spacing: -0.01em; + margin: 8px 0 3px 0; +} + +h6 +{ + font-size: 100%; + letter-spacing: -0.01em; + margin: 6px 0 3px 0; + font-style: italic; +} + +table +{ + border-spacing: 0; + border: 0; + border-collapse: collapse; +} + +td, th +{ + border: 1px solid #ddd; + margin: 0px; + text-align: left; + vertical-align: top; + padding: 4px 6px; + display: table-cell; +} + +thead tr +{ + background-color: #ddd; + font-weight: bold; +} + +th { border-right: 1px solid #aaa; } +tr > th:last-child { border-right: 1px solid #ddd; } + +.ancestors, .attribs { color: #999; } +.ancestors a, .attribs a +{ + color: #999 !important; + text-decoration: none; +} + +.clear +{ + clear: both; +} + +.important +{ + font-weight: bold; + color: #950B02; +} + +.yes-def { + text-indent: -1000px; +} + +.type-signature { + color: #aaa; +} + +.name, .signature { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.details { margin-top: 14px; border-left: 2px solid #DDD; } +.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } +.details dd { margin-left: 70px; } +.details ul { margin: 0; } +.details ul { list-style-type: none; } +.details li { margin-left: 30px; padding-top: 6px; } +.details pre.prettyprint { margin: 0 } +.details .object-value { padding-top: 0; } + +.description { + margin-bottom: 1em; + margin-top: 1em; +} + +.code-caption +{ + font-style: italic; + font-size: 107%; + margin: 0; +} + +.source +{ + border: 1px solid #ddd; + width: 80%; + overflow: auto; +} + +.prettyprint.source { + width: inherit; +} + +.source code +{ + font-size: 100%; + line-height: 18px; + display: block; + padding: 4px 12px; + margin: 0; + background-color: #fff; + color: #4D4E53; +} + +.prettyprint code span.line +{ + display: inline-block; +} + +.prettyprint.linenums +{ + padding-left: 70px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.prettyprint.linenums ol +{ + padding-left: 0; +} + +.prettyprint.linenums li +{ + border-left: 3px #ddd solid; +} + +.prettyprint.linenums li.selected, +.prettyprint.linenums li.selected * +{ + background-color: lightyellow; +} + +.prettyprint.linenums li * +{ + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.params .name, .props .name, .name code { + color: #4D4E53; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 100%; +} + +.params td.description > p:first-child, +.props td.description > p:first-child +{ + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, +.props td.description > p:last-child +{ + margin-bottom: 0; + padding-bottom: 0; +} + +.disabled { + color: #454545; +} diff --git a/docs/styles/prettify-jsdoc.css b/docs/styles/prettify-jsdoc.css new file mode 100644 index 0000000..5a2526e --- /dev/null +++ b/docs/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/docs/styles/prettify-tomorrow.css b/docs/styles/prettify-tomorrow.css new file mode 100644 index 0000000..b6f92a7 --- /dev/null +++ b/docs/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: #718c00; } + + /* a keyword */ + .kwd { + color: #8959a8; } + + /* a comment */ + .com { + color: #8e908c; } + + /* a type name */ + .typ { + color: #4271ae; } + + /* a literal value */ + .lit { + color: #f5871f; } + + /* punctuation */ + .pun { + color: #4d4d4c; } + + /* lisp open bracket */ + .opn { + color: #4d4d4c; } + + /* lisp close bracket */ + .clo { + color: #4d4d4c; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/hooks/events.js b/hooks/events.js new file mode 100644 index 0000000..7f9fb8c --- /dev/null +++ b/hooks/events.js @@ -0,0 +1,264 @@ +import { Exporter } from "../scripts/modules/exporter.js"; +import { Renderer } from "../scripts/modules/renderer.js"; +import { ModuleSettings } from "../scripts/modules/settings.js"; + +export class Events { + constructor() { + return this; + } + + /** + * Reload Filters for a given entity + * + * @param {HTMLCollection} html + * @param {String} entityType + */ + static async reloadFilters(html, entityType) { + let entityBrowserHtml = await Renderer.renderFilters(game.compendiumBrowser.filters[entityType], entityType), + filterWrapper = document.querySelector('.tab.active .browser .control-area'); + filterWrapper.innerHTML = entityBrowserHtml; + Events.activateFilterListeners(html); + } + + /** + * + * @param {HTMLCollection} app + */ + static async activateActionListener(app = document.getElementsByClassName('window-app')) { + app = app.get(0); + app.querySelector('button[data-action="export"]').addEventListener('click', async (e) => { + let items = app.querySelectorAll('.content .tab.active .cb_entities .entity'), + entityType = app.querySelector('.content .tab.active').dataset.tab, + filters = JSON.parse(app.querySelector('.tab.active .cb_entities').dataset.activeFilters), + tableItems = [], + tableName = entityType + 'table: '; + + items.forEach(item => { + let obj = {}; + Object.assign(obj, item.dataset); + tableItems.push(obj); + }); + + let filterKeys = Object.keys(filters); + if (filterKeys.length > 0) { + for (let key of filterKeys) { + tableName = tableName + '_' + filters[key].path + '-' + filters[key].value; + } + } + + let d = Dialog.confirm({ + title: "Export subset to table", + content: "

        Choose wisely.

        ", + yes: () => Exporter.createTableFromSelection(tableName, tableItems), + no: () => console.log("You chose ... poorly"), + defaultYes: false + }); + + }); + } + + static async activateItemListListeners(app = document.getElementsByClassName('window-app')) { + app = app[0]; + + // open entity sheet on click + app.querySelectorAll('*[data-action="openSheet"]').forEach(async el => { + el.addEventListener('click', async (e) => { + let itemId = e.currentTarget.parentNode.dataset.entryId; + let compendium = e.currentTarget.parentNode.dataset.entryCompendium; + let pack = game.packs.find(p => p.collection === compendium); + await pack.getEntity(itemId).then(entity => { + entity.sheet.render(true); + }); + }); + }); + + // make draggable + //0.4.1: Avoid the game.packs lookup + app.querySelectorAll('.draggable').forEach(async li => { + li.setAttribute("draggable", true); + li.addEventListener('dragstart', event => { + let packName = li.getAttribute("data-entry-compendium"); + let pack = game.packs.find(p => p.collection === packName); + if (!pack) { + event.preventDefault(); + return false; + } + event.dataTransfer.setData("text/plain", JSON.stringify({ + type: pack.entity, + pack: pack.collection, + id: li.getAttribute("data-entry-id") + })); + }, false); + }); + } + + static async observeListElement(list, tag) { + for (let element of list.getElementsByTagName(tag)) { + game.compendiumBrowser.observer.observe(element); + } + } + + static async activateFilterListeners(html, app = document.getElementsByClassName('window-app')) { + app = app[0]; + + // toggle visibility of filter containers + html.find('.filtercontainer h3, .multiselect label').click(async ev => { + await $(ev.target.nextElementSibling).toggle(100); + }); + + html.find('.multiselect label').trigger('click'); + + // reset filters and re-rendes + app.querySelectorAll('.button[data-action="reset-filters').forEach(async el => { + el.addEventListener('click', async (e) => { + game.compendiumBrowser.filters.resetFilters(); + game.compendiumBrowser.refreshList = e.target.closest('.tab').dataset.tab; + game.compendiumBrowser.render(); + }); + }); + + // select filters + app.querySelectorAll('.settings input').forEach(async el => { + el.addEventListener('keyup change paste', async (e) => { + let target = e.target, + setting = target.dataset.setting, + value = target.checked, + key = target.dataset.key, + category = (target.dataset.type === 'Spell' || target.dataset.type === 'Feat') ? 'Item' : target.dataset.type; + + if (key) + game.compendiumBrowser.settings.loadedCompendium[category][key].load = value; + + ui.notifications.info("Settings Saved. Compendiums are being reloaded."); + + switch (setting) { + case 'allow-spell-browser': + game.compendiumBrowser.settings.allowSpellBrowser = value; + break; + case 'allow-feat-browser': + game.compendiumBrowser.settings.allowFeatBrowser = value; + break; + case 'allow-item-browser': + game.compendiumBrowser.settings.allowItemBrowser = value; + break; + case 'allow-actor-browser': + game.compendiumBrowser.settings.allowActorBrowser = value; + break; + case 'allow-rolltable-browser': + game.compendiumBrowser.settings.allowRollTableBrowser = value; + break; + case 'allow-journalentry-browser': + game.compendiumBrowser.settings.allowJournalEntryBrowser = value; + break; + default: + break; + } + ModuleSettings.saveSettings(); + game.compendiumBrowser.render(); + }); + }); + + // select filters + app.querySelectorAll('.filter[data-type=text] input, .filter[data-type=text] select').forEach(async el => { + el.addEventListener('keyup change paste', async (e) => { + const path = e.target.closest('.filter').dataset.path, + key = path.replace(/\./g, ''), + entityType = app.querySelector('.content .tab.active').dataset.tab; + + if (e.target.value === '' || e.target.value === undefined) { + delete game.compendiumBrowser.filters[entityType].activeFilters[key]; + } else { + game.compendiumBrowser.filters[entityType].activeFilters[key] = { + path: path, + type: 'text', + valIsArray: false, + value: e.target.value + } + } + + game.compendiumBrowser.replaceList(html, entityType); + }); + }); + + // select filters + app.querySelectorAll('.filter[data-type=select] select, .filter[data-type=bool] select').forEach(async el => { + el.addEventListener('change', async (e) => { + const path = e.target.closest('.filter').dataset.path, + key = path.replace(/\./g, ''), + filterType = e.target.closest('.filter').dataset.type, + entityType = app.querySelector('.content .tab.active').dataset.tab; + + let valIsArray = e.target.closest('.filter').dataset.valIsArray === 'true', + value = e.target.options[e.target.selectedIndex].value; + + if (value === "null") { + delete game.compendiumBrowser.filters[entityType].activeFilters[key]; + } else { + game.compendiumBrowser.filters[entityType].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + value: value + } + } + game.compendiumBrowser.replaceList(html, entityType); + + }); + }); + + + // multiselect + app.querySelectorAll('.filter[data-type=multiSelect] input').forEach(async el => { + el.addEventListener('change', async (e) => { + const path = e.target.closest('.filter').dataset.path, + key = path.replace(/\./g, ''), + filterType = 'multiSelect', + entityType = app.querySelector('.content .tab.active').dataset.tab, + filter = game.compendiumBrowser.filters[entityType].activeFilters[key]; + let valIsArray = e.target.closest('.filter').dataset.valIsArray, + value = e.target.dataset.value; + + if (e.target.checked === true) { + if (filter === undefined) { + game.compendiumBrowser.filters[entityType].activeFilters[key] = + { path: path, type: filterType, valIsArray: valIsArray, values: [value] }; + } else { + game.compendiumBrowser.filters[entityType].activeFilters[key].values.push(value); + } + } else { + delete game.compendiumBrowser.filters[entityType].activeFilters[key].values.splice(game.compendiumBrowser.filters[entityType].activeFilters[key].values.indexOf(value), 1); + + if (game.compendiumBrowser.filters[entityType].activeFilters[key].values.length === 0) + delete game.compendiumBrowser.filters[entityType].activeFilters[key]; + } + + game.compendiumBrowser.replaceList(html, entityType); + }); + }); + + app.querySelectorAll('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').forEach(async el => { + el.addEventListener('change keyup paste', async (e) => { + const path = e.target.closest('.filter').dataset.path, + key = path.replace(/\./g, ''), + filterType = 'numberCompare', + entityType = app.querySelector('.content .tab.active').dataset.tab, + operator = e.target.closest('.filter').getElementsByTagName('select').val, + value = e.target.closest('.filter').getElementsByTagName('input').val; + + if (value === '' || operator === 'null') { + delete game.compendiumBrowser.filters[entityType].activeFilters[key] + } else { + game.compendiumBrowser.filters[entityType].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: false, + operator: operator, + value: value + } + } + + game.compendiumBrowser.replaceList(html, browserTab); + }); + }); + } +} \ No newline at end of file diff --git a/hooks/events.mjs b/hooks/events.mjs deleted file mode 100644 index d89699f..0000000 --- a/hooks/events.mjs +++ /dev/null @@ -1,232 +0,0 @@ -import Exporter from "../scripts/modules/exporter.mjs"; - -export class Events { - constructor(){ - return this; - } - - static async activateActionListener(app = document.getElementsByClassName('window-app')){ - app = app.get(0); - app.querySelector('button[data-action="export"]').addEventListener('click', async (e)=> { - let items = app.querySelectorAll('.content .tab.active .cb_entities .entity'), - entityType = app.querySelector('.content .tab.active').dataset.tab, - filters = JSON.parse(app.querySelector('.tab.active .cb_entities').dataset.activeFilters), - tableItems = [], - tableName = entityType + 'table: '; - - items.forEach(item => { - let obj = {}; - Object.assign(obj, item.dataset); - tableItems.push(obj); - }); - - let filterKeys = Object.keys(filters); - if (filterKeys.length > 0){ - for (let key of filterKeys) { - tableName = tableName + '_' + filters[key].path + '-' + filters[key].value; - } - } - - Exporter.createTableFromSelection(tableName,tableItems); - }); - } - - static async activateItemListListeners(app = document.getElementsByClassName('window-app')) { - app = app.get(0); - - // open entity sheet on click - app.querySelectorAll('*[data-action="openSheet"]').forEach(async el => { - el.addEventListener('click', async (e) => { - let itemId = e.currentTarget.parentNode.dataset.entryId; - let compendium = e.currentTarget.parentNode.dataset.entryCompendium; - let pack = game.packs.find(p => p.collection === compendium); - await pack.getEntity(itemId).then(entity => { - entity.sheet.render(true); - }); - }); - }); - - // make draggable - //0.4.1: Avoid the game.packs lookup - app.querySelectorAll('.draggable').forEach(async li => { - li.setAttribute("draggable", true); - li.addEventListener('dragstart', event => { - let packName = li.getAttribute("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === packName); - if (!pack) { - event.preventDefault(); - return false; - } - event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.entity, - pack: pack.collection, - id: li.getAttribute("data-entry-id") - })); - }, false); - }); - } - - static async observeListElement(list,tag) { - for (let img of list.getElementsByTagName(tag)) { - game.compendiumBrowser.observer.observe(img); - } - } - - static async activateFilterListeners(html){ - // toggle visibility of filter containers - html.find('.filtercontainer h3, .multiselect label').click(async ev => { - await $(ev.target.nextElementSibling).toggle(100); - }); - - html.find('.multiselect label').trigger('click'); - - // reset filters and re-rendes - html.find('button[data-action="reset-filters"]').click(ev => { - this.filters.resetFilters(); - this.refreshList = ev.target.closest('.tab').dataset.tab; - this.render(); - }); - - // settings - html.find('.settings input').on('change', e => { - let setting = e.target.dataset.setting, - value = e.target.checked, - key = e.target.dataset.key, - category = (e.dataset.type === 'npc')? 'Actor' : 'Item'; - - this.settings.loadedCompendium[category][key].load = value; - this.render(); - - ui.notifications.info("Settings Saved. Compendiums are being reloaded."); - - if (setting === 'allow-spell-browser') { - this.settings.allowSpellBrowser = value; - } - if (setting === 'allow-feat-browser') { - this.settings.allowFeatBrowser = value; - } - if (setting === 'allow-item-browser') { - this.settings.allowItemBrowser = value; - } - if (setting === 'allow-npc-browser') { - this.settings.allowNpcBrowser = value; - } - ModuleSettings.saveSettings(); - }); - - - // activating or deactivating filters - //0.4.1: Now does a re-load and updates just the data side - // text filters - html.find('.filter[data-type=text] input, .filter[data-type=text] select').on('keyup change paste', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const value = ev.target.value; - const entityType = $(ev.target).parents('.tab').data('tab'); - - const filterTarget = `${entityType}Filters`; - - if (value === '' || value === undefined) { - delete game.compendiumBrowser.filters[filterTarget].activeFilters[key]; - } else { - game.compendiumBrowser.filters[filterTarget].activeFilters[key] = { - path: path, - type: 'text', - valIsArray: false, - value: ev.target.value - } - } - - game.compendiumBrowser.replaceList(html, entityType); - }); - - // select filters - html.find('.filter[data-type=select] select, .filter[data-type=bool] select').on('change', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const filterType = $(ev.target).parents('.filter').data('type'); - const browserTab = $(ev.target).parents('.tab').data('tab'); - let valIsArray = $(ev.target).parents('.filter').data('valisarray'); - if (valIsArray === 'true') valIsArray = true; - let value = ev.target.value; - if (value === 'false') value = false; - if (value === 'true') value = true; - - const filterTarget = `${browserTab}Filters`; - - if (value === "null") { - delete game.compendiumBrowser.filters[filterTarget].activeFilters[key]; - } else { - game.compendiumBrowser.filters[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - value: value - } - } - game.compendiumBrowser.replaceList(html, browserTab); - }); - - // multiselect filters - html.find('.filter[data-type=multiSelect] input').on('change', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const filterType = 'multiSelect'; - const browserTab = $(ev.target).parents('.tab').data('tab'); - let valIsArray = $(ev.target).parents('.filter').data('valisarray'); - if (valIsArray === 'true') valIsArray = true; - let value = $(ev.target).data('value'); - - const filterTarget = `${browserTab}Filters`; - const filter = this[filterTarget].activeFilters[key]; - - if (ev.target.checked === true) { - if (filter === undefined) { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - values: [value] - } - } else { - this[filterTarget].activeFilters[key].values.push(value); - } - } else { - delete this[filterTarget].activeFilters[key].values.splice(this[filterTarget].activeFilters[key].values.indexOf(value), 1); - if (this[filterTarget].activeFilters[key].values.length === 0) { - delete this[filterTarget].activeFilters[key]; - } - } - - game.compendiumBrowser.replaceList(html, browserTab); - }); - - - html.find('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').on('change keyup paste', ev => { - const path = $(ev.target).parents('.filter').data('path'); - const key = path.replace(/\./g, ''); - const filterType = 'numberCompare'; - const browserTab = $(ev.target).parents('.tab').data('tab'); - let valIsArray = false; - - const operator = $(ev.target).parents('.filter').find('select').val(); - const value = $(ev.target).parents('.filter').find('input').val(); - - const filterTarget = `${browserTab}Filters`; - - if (value === '' || operator === 'null') { - delete this[filterTarget].activeFilters[key] - } else { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - operator: operator, - value: value - } - } - - game.compendiumBrowser.replaceList(html, browserTab); - }); - } -} \ No newline at end of file diff --git a/hooks/moduleHooks.js b/hooks/moduleHooks.js new file mode 100644 index 0000000..0bf5a4d --- /dev/null +++ b/hooks/moduleHooks.js @@ -0,0 +1,102 @@ +/** + * @fileOverview Holding the modules Hooks + * + * @author Daniel Böttner + * @version 1.0.0 + */ +/* jshint node: true */ + +'use strict'; + +import { CMPBrowser } from '../scripts/modules/settings.js'; +import { CompendiumBrowser } from '../scripts/compendium-browser.js'; +import { VersionCheck} from '../scripts/versioning/version-check.js'; +import { Renderer } from '../scripts/modules/renderer.js'; + +/** + * Holds the static methods to be called + */ +export default class moduleHooks { + + static onInit() { + Hooks.on('init', async () => { + Handlebars.registerHelper('switch', function (value, options) { + this.switch_value = value; + this.switch_break = false; + return options.fn(this); + }); + + Handlebars.registerHelper('case', function (value, options) { + if (value == this.switch_value) { + this.switch_break = true; + return options.fn(this); + } + }); + + Handlebars.registerHelper('default', function (value, options) { + if (this.switch_break == false) { + return value; + } + }); + + Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) { + switch (operator) { + case '==': + return (v1 == v2) ? options.fn(this) : options.inverse(this); + case '===': + return (v1 === v2) ? options.fn(this) : options.inverse(this); + case '!=': + return (v1 != v2) ? options.fn(this) : options.inverse(this); + case '!==': + return (v1 !== v2) ? options.fn(this) : options.inverse(this); + case '<': + return (v1 < v2) ? options.fn(this) : options.inverse(this); + case '<=': + return (v1 <= v2) ? options.fn(this) : options.inverse(this); + case '>': + return (v1 > v2) ? options.fn(this) : options.inverse(this); + case '>=': + return (v1 >= v2) ? options.fn(this) : options.inverse(this); + case '&&': + return (v1 && v2) ? options.fn(this) : options.inverse(this); + case '||': + return (v1 || v2) ? options.fn(this) : options.inverse(this); + default: + return options.inverse(this); + } + }); + + Handlebars.registerHelper("debug", function (optionalValue) { + console.log("Current Context"); + console.log("===================="); + console.log(this); + if (optionalValue) { + console.log("Value"); + console.log("===================="); + console.log(optionalValue); + } + }); + }); + } + + static onReady() { + Hooks.on('ready', async () => { + if (game.compendiumBrowser === undefined) { + game.compendiumBrowser = new CompendiumBrowser(); + //0.4.0 Defer loading content until we actually use the Compendium Browser + //A compromise approach would be better (periodic loading) except would still create the memory use problem + await game.compendiumBrowser.initialize(); + } + + if (VersionCheck.check(CMPBrowser.MODULE_NAME) && game.user.isGM) { + console.log('version check'); + } + }); + } + + static onRenderComplete() { + Hooks.on("renderCompendiumBrowser", Renderer.afterRender); + } + + +} \ No newline at end of file diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..1b2903f --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,26 @@ +{ + "plugins": [ + "plugins/markdown", + "plugins/summarize" + ], + "recurseDepth": 10, + "source": { + "includePattern": ".+\\.js(doc|x)?$", + "excludePattern": "(^|\\/|\\\\)_", + "exclude": ["./docs","./dist"], + "include": "." + }, + "sourceType": "module", + "tags": { + "allowUnknownTags": true, + "dictionaries": ["jsdoc","closure"] + }, + "templates": { + "cleverLinks": true, + "monospaceLinks": false + }, + "opts": { + "destination": "./docs/", + "recurse": true + } +} diff --git a/lang/de.json b/lang/de.json index 0e6986e..54912b9 100644 --- a/lang/de.json +++ b/lang/de.json @@ -4,14 +4,29 @@ "CMPBrowser.cr":"Challenge Rating", "CMPBrowser.generalSettings":"Allgemeine Einstellungen", "CMPBrowser.allowSpellAcc":"Erlaube Spielern Zauber zu durchsuchen.", - "CMPBrowser.allowNpcAcc":"Erlaube Spielern NPCs zu durchsuchen.", + "CMPBrowser.allowActorAcc":"Player access to actor browser", + "CMPBrowser.allowRolltable": "Player access to rolltable browser", + "CMPBrowser.allowJournalEntry": "Player Access to the JournalEntry browser", "CMPBrowser.compSettingsSpell":"Gegenstand Kompendium Einstellungen", - "CMPBrowser.compSettingsNpc":"NPC Kompendium Einstellungen", + "CMPBrowser.compSettingsNpc":"Actor Kompendium Einstellungen", + "CMPBrowser.Filters.ResetFilters" : "Reset Filters", + "CMPBrowser.Tab.SpellBrowser":"Zauber Browser", + "CMPBrowser.Tab.FeatBrowser": "Feat Browser", + "CMPBrowser.Tab.ItemBrowser": "Gegenstand Browser", + "CMPBrowser.Tab.NPCBrowser":"NPC Browser", + "CMPBrowser.Tab.ActorBrowser":"Actor Browser", + "CMPBrowser.Tab.RolltableBrowser": "Rolltable Browser", + "CMPBrowser.Tab.JournalEntryBrowser": "JournalEntry Browser", + "CMPBrowser.Tab.Settings":"Einstellungen", + "CMPBrowser.SETTING.Maxload.NAME" : "Maximum load", + "CMPBrowser.SETTING.Maxload.HINT" : "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", + "CMPBrowser.LOADING.Message" : "Geladen: {numLoaded} {entityType}s ({numPacks} Kompendia)", + "CMPBrowser.LOADING.MaxLoaded" : "(maximum displayed; to see more, use the filters)", "CMPBrowser.load":"Laden", "CMPBrowser.lvl":"Level", "CMPBrowser.ritual":"Ritual", "CMPBrowser.concentration":"Konzentration", - "CMPBrowser.verbal":"Verbal", + "CMPBrowser.vocal":"Verbal", "CMPBrowser.somatic":"Somatisch", "CMPBrowser.material":"Material", "CMPBrowser.cantip":"Cantrip", @@ -54,16 +69,6 @@ "CMPBrowser.abilities": "Fähigkeiten", "CMPBrowser.dmgInteraction": "Damage Interaction", "CMPBrowser.dmgDealt": "Damage Dealt", - "CMPBrowser.size": "Größe", - "CMPBrowser.Tab.SpellBrowser":"Zauber Browser", - "CMPBrowser.Tab.FeatBrowser": "Feat Browser", - "CMPBrowser.Tab.ItemBrowser": "Gegenstand Browser", - "CMPBrowser.Tab.NPCBrowser":"NPC Browser", - "CMPBrowser.Tab.Settings":"Einstellungen", - "CMPBrowser.SETTING.Maxload.NAME" : "Maximum load", - "CMPBrowser.SETTING.Maxload.HINT" : "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", - "CMPBrowser.LOADING.Message" : "Loaded...{numLoaded} {itemType}s", - "CMPBrowser.LOADING.MaxLoaded" : "(maximum displayed; to see more, use the filters)", - "CMPBrowser.Filters.ResetFilters" : "Reset Filters" - + "CMPBrowser.size": "Größe", + "CMPBrowser.TableType": "RollTable Typ" } \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 762eb72..d21f092 100644 --- a/lang/en.json +++ b/lang/en.json @@ -3,18 +3,20 @@ "CMPBrowser.sortBy":"Sort by", "CMPBrowser.cr":"Challenge Rating", "CMPBrowser.generalSettings":"General Settings", - "CMPBrowser.allowSpellAcc":"Allow Players Access to the spell browser", - "CMPBrowser.allowNpcAcc":"Allow Players Access to the npc browser", + "CMPBrowser.allowSpellAcc":"Players access to the Spell browser", + "CMPBrowser.allowActorAcc":"Player access to Actor browser", + "CMPBrowser.allowRolltable": "Player access to RollTable browser", + "CMPBrowser.allowJournalEntry": "Player Access to the JournalEntry browser", "CMPBrowser.compSettingsSpell":"Item Compendium Settings", - "CMPBrowser.compSettingsNpc":"NPC Compendium Settings", + "CMPBrowser.compSettingsNpc":"Actor Compendium Settings", "CMPBrowser.load":"Load", "CMPBrowser.lvl":"Level", "CMPBrowser.ritual":"Ritual", "CMPBrowser.concentration":"Concentration", - "CMPBrowser.verbal":"Verbal", + "CMPBrowser.vocal":"Verbal", "CMPBrowser.somatic":"Somatic", "CMPBrowser.material":"Material", - "CMPBrowser.cantip":"Cantrip", + "CMPBrowser.cantrip":"Cantrip", "CMPBrowser.school":"School", "CMPBrowser.castingTime":"Casting Time", "CMPBrowser.bonusAction":"Bonus Action", @@ -55,15 +57,17 @@ "CMPBrowser.dmgInteraction": "Damage Interaction", "CMPBrowser.dmgDealt": "Damage Dealt", "CMPBrowser.size": "Size", - "CMPBrowser.Tab.SpellBrowser":"Spell Browser", - "CMPBrowser.Tab.FeatBrowser": "Feat Browser", - "CMPBrowser.Tab.ItemBrowser": "Item Browser", - "CMPBrowser.Tab.NPCBrowser":"NPC Browser", + "CMPBrowser.Tab.ActorBrowser":"Actors", + "CMPBrowser.Tab.SpellBrowser":"Spells", + "CMPBrowser.Tab.FeatBrowser": "Feats", + "CMPBrowser.Tab.ItemBrowser": "Items", + "CMPBrowser.Tab.RollTableBrowser": "Rolltables", + "CMPBrowser.Tab.JournalEntryBrowser": "JournalEntries", "CMPBrowser.Tab.Settings":"Settings", "CMPBrowser.SETTING.Maxload.NAME" : "Maximum load", "CMPBrowser.SETTING.Maxload.HINT" : "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", - "CMPBrowser.LOADING.Message" : "Loaded: {numLoaded} {itemType}s", + "CMPBrowser.LOADING.Message" : "Loaded: {numLoaded} {entityType}s ({numPacks} Compendia)", "CMPBrowser.LOADING.MaxLoaded" : "(maximum displayed; to see more, use the filters)", - "CMPBrowser.Filters.ResetFilters" : "Reset Filters" - + "CMPBrowser.Filters.ResetFilters" : "Reset Filters", + "CMPBrowser.TableType": "RollTable Type" } \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json index fbf3402..8cefe50 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -1,60 +1,65 @@ { - "CMPBrowser.compendiumBrowser":"Recherche dans les Compendium", - "CMPBrowser.sortBy":"Trié par", - "CMPBrowser.cr":"Niveau de la rencontre", - "CMPBrowser.generalSettings":"Paramètres généraux", - "CMPBrowser.allowSpellAcc":"Autoriser les joueurs à accéder aux listes de sorts", - "CMPBrowser.allowNpcAcc":"Autoriser les joueurs à accéder aux listes de PNJ", - "CMPBrowser.compSettingsSpell":"Paramètres de compendium de sorts", - "CMPBrowser.compSettingsNpc":"Paramètres de compendium de PNJ", - "CMPBrowser.load":"Charger", - "CMPBrowser.lvl":"Level", - "CMPBrowser.ritual":"Rituel", - "CMPBrowser.concentration":"Concentration", - "CMPBrowser.verbal":"Verbales", - "CMPBrowser.somatic":"Somatiques", - "CMPBrowser.material":"Matérielles", - "CMPBrowser.cantip":"Tours de magie", - "CMPBrowser.school":"Ecole", - "CMPBrowser.castingTime":"Durée d'incantation", - "CMPBrowser.bonusAction":"Action Bonus", - "CMPBrowser.reaction":"Réaction", - "CMPBrowser.spellType":"Type de sort", - "CMPBrowser.damageType":"Type de dégâts", - "CMPBrowser.class":"Classe", - "CMPBrowser.bard":"Barde", - "CMPBrowser.cleric":"Clerc", - "CMPBrowser.druid":"Druide", - "CMPBrowser.paladin":"Paladin", - "CMPBrowser.ranger":"Rôdeur", - "CMPBrowser.sorcerer":"Sorcier", - "CMPBrowser.warlock":"Ensorceleur", - "CMPBrowser.wizard":"Magicien", - "CMPBrowser.general":"Général", - "CMPBrowser.components":"Composants", - "CMPBrowser.hasSpells":"à des Sorts", - "CMPBrowser.hasLegAct":"à des Actions Légendaires", - "CMPBrowser.hasLegRes":"à des Resistances Légendaires", - "CMPBrowser.creatureType":"Type de Créature", - "CMPBrowser.aberration": "Aberration", - "CMPBrowser.beast": "Bête", - "CMPBrowser.celestial": "Céleste", - "CMPBrowser.construct": "Artificielles", - "CMPBrowser.dragon": "Dragon", - "CMPBrowser.elemental": "Elementaire", - "CMPBrowser.fey": "Fée", - "CMPBrowser.fiend": "Fiélon", - "CMPBrowser.giant": "Géant", - "CMPBrowser.humanoid": "Humanoïde", - "CMPBrowser.monstrosity": "Monstrueuse", - "CMPBrowser.ooze": "Vase", - "CMPBrowser.plant": "Plante", - "CMPBrowser.undead": "Morts-vivants", - "CMPBrowser.abilities": "Capacités", - "CMPBrowser.dmgInteraction": "Spécificité des dégâts", - "CMPBrowser.dmgDealt": "Type de dégats", - "CMPBrowser.size": "Taille", - "CMPBrowser.spellBrowser":"Recherche de sorts", - "CMPBrowser.npcBrowser":"Recherche de PNJ", - "CMPBrowser.settings":"Paramétrages" + "CMPBrowser.compendiumBrowser": "Recherche dans les Compendium", + "CMPBrowser.sortBy": "Trié par", + "CMPBrowser.cr": "Niveau de la rencontre", + "CMPBrowser.generalSettings": "Paramètres généraux", + "CMPBrowser.allowSpellAcc": "Autoriser les joueurs à accéder aux listes de sorts", + "CMPBrowser.allowNpcAcc": "Autoriser les joueurs à accéder aux listes de PNJ", + "CMPBrowser.compSettingsSpell": "Paramètres de compendium de sorts", + "CMPBrowser.compSettingsNpc": "Paramètres de compendium de PNJ", + "CMPBrowser.load": "Charger", + "CMPBrowser.lvl": "Level", + "CMPBrowser.ritual": "Rituel", + "CMPBrowser.concentration": "Concentration", + "CMPBrowser.vocal": "Verbales", + "CMPBrowser.somatic": "Somatiques", + "CMPBrowser.material": "Matérielles", + "CMPBrowser.cantip": "Tours de magie", + "CMPBrowser.school": "Ecole", + "CMPBrowser.castingTime": "Durée d'incantation", + "CMPBrowser.bonusAction": "Action Bonus", + "CMPBrowser.reaction": "Réaction", + "CMPBrowser.spellType": "Type de sort", + "CMPBrowser.damageType": "Type de dégâts", + "CMPBrowser.class": "Classe", + "CMPBrowser.bard": "Barde", + "CMPBrowser.cleric": "Clerc", + "CMPBrowser.druid": "Druide", + "CMPBrowser.paladin": "Paladin", + "CMPBrowser.ranger": "Rôdeur", + "CMPBrowser.sorcerer": "Sorcier", + "CMPBrowser.warlock": "Ensorceleur", + "CMPBrowser.wizard": "Magicien", + "CMPBrowser.general": "Général", + "CMPBrowser.components": "Composants", + "CMPBrowser.hasSpells": "à des Sorts", + "CMPBrowser.hasLegAct": "à des Actions Légendaires", + "CMPBrowser.hasLegRes": "à des Resistances Légendaires", + "CMPBrowser.creatureType": "Type de Créature", + "CMPBrowser.aberration": "Aberration", + "CMPBrowser.beast": "Bête", + "CMPBrowser.celestial": "Céleste", + "CMPBrowser.construct": "Artificielles", + "CMPBrowser.dragon": "Dragon", + "CMPBrowser.elemental": "Elementaire", + "CMPBrowser.fey": "Fée", + "CMPBrowser.fiend": "Fiélon", + "CMPBrowser.giant": "Géant", + "CMPBrowser.humanoid": "Humanoïde", + "CMPBrowser.monstrosity": "Monstrueuse", + "CMPBrowser.ooze": "Vase", + "CMPBrowser.plant": "Plante", + "CMPBrowser.undead": "Morts-vivants", + "CMPBrowser.abilities": "Capacités", + "CMPBrowser.dmgInteraction": "Spécificité des dégâts", + "CMPBrowser.dmgDealt": "Type de dégats", + "CMPBrowser.size": "Taille", + "CMPBrowser.spellBrowser": "Recherche de sorts", + "CMPBrowser.npcBrowser": "Recherche de PNJ", + "CMPBrowser.settings": "Paramétrages", + "CMPBrowser.SETTING.Maxload.NAME": "Charge maximale", + "CMPBrowser.SETTING.Maxload.HINT": "Nombre maximum de sorts, dons, objets ou PNJ à afficher ; pour en voir plus, utilisez les filtres. Ce paramètre permet de gérer la mémoire et la charge du serveur.", + "CMPBrowser.LOADING.Message": "{numLoaded} {entityType}s chargées", + "CMPBrowser.LOADING.MaxLoaded": "(maximum affiché ; pour en voir plus, utilisez les filtres)", + "CMPBrowser.Filters.ResetFilters": "Réinitialiser les filtres" } \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index 5490318..0b3434c 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -11,7 +11,7 @@ "CMPBrowser.lvl":"レベル", "CMPBrowser.ritual":"儀式", "CMPBrowser.concentration":"集中", - "CMPBrowser.verbal":"音声", + "CMPBrowser.vocal":"音声", "CMPBrowser.somatic":"動作", "CMPBrowser.material":"物質", "CMPBrowser.cantip":"初級", diff --git a/lang/pt-BR.json b/lang/pt-BR.json index 5066be0..5c847bf 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -11,7 +11,7 @@ "CMPBrowser.lvl":"Nivel", "CMPBrowser.ritual":"Ritual", "CMPBrowser.concentration":"Concentraçãon", - "CMPBrowser.verbal":"Verbal", + "CMPBrowser.vocal":"Verbal", "CMPBrowser.somatic":"Somático", "CMPBrowser.material":"Material", "CMPBrowser.cantip":"Truque", diff --git a/module.json b/module.json index 0a45ba0..71d8515 100644 --- a/module.json +++ b/module.json @@ -28,7 +28,7 @@ "./scripts/**" ], "esmodules": [ - "/compendium-browser.js" + "./compendium-browser.js" ], "styles": [ "./styles/compendium-browser.css", diff --git a/package.json b/package.json new file mode 100644 index 0000000..e762f83 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "compendium-browser", + "version": "1.0.0", + "description": "A browser with simple UI to browse and filter entites from a compendium in FoundryVTT", + "main": "compendium-browser.js", + "directories": { + "doc": "docs" + }, + "scripts": { + "all": "npm run copyfiles && npm run less && npm run tsc && npm run cleancss && npm run jsdoc", + "copyfiles": "copyfiles ./template/*.hbs ./dist", + "less": "lessc ./styles/compendium-browser.less ./dist/styles/compendium-browser.css", + "tsc": "tsc", + "cleancss": "cleancss -o ./dist/styles/compendium-browser.min.css ./dist/styles/compendium-browser.css", + "jsdoc": "jsdoc -r . -d ./docs" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/DanielBoettner/compendium-browser.git" + }, + "keywords": [ + "FoundryVTT", + "node", + "javascript" + ], + "author": "Daniel Böttner", + "license": "MIT", + "bugs": { + "url": "https://github.com/DanielBoettner/compendium-browser/issues" + }, + "homepage": "https://github.com/DanielBoettner/compendium-browser#readme" +} diff --git a/scripts/classes/compactEntity.ts b/scripts/classes/compactEntity.ts new file mode 100644 index 0000000..f2112fe --- /dev/null +++ b/scripts/classes/compactEntity.ts @@ -0,0 +1,26 @@ +export class compactEntity { + public _id: string = ''; + public name: string = ''; + public compendium: string = ''; + public classRequirement: any; + public classRequirementString: string = ''; + public data: { [name: string]: any } = {}; + public flags: { [name: string]: any } = {}; + public dae: boolean = false; + public img: string = 'icons/svg/daze.svg'; + public rarity: string|undefined; + public type: string = ''; + public ac: string|undefined; + public displayCR: any; + public displaySize: any; + public displayType: any; + public orderCR!: number; + public orderSize!: number; + public effects: any; + public usesRessources: boolean = false; + public hasSave: boolean = false; + + hasDAE(): boolean { + return this.dae; + } +} \ No newline at end of file diff --git a/scripts/classes/compactList.ts b/scripts/classes/compactList.ts new file mode 100644 index 0000000..36b3560 --- /dev/null +++ b/scripts/classes/compactList.ts @@ -0,0 +1,53 @@ +import { compactEntity } from "./compactEntity"; + +export class compactList { + public entities: Array; + public activeFilters: String; + + constructor(){ + this.entities = []; + this.activeFilters = ''; + } + + /** + * + * @returns {number} + */ + size(): number { + return this.entities.length; + } + + /** + * + * @param {compactEntity} entity + * @returns {void} + */ + addEntity(entity: compactEntity): void { + this.entities.push(entity); + } + + /** + * + * @param {*} index + * @returns {compactEntity} + */ + getEntity(index: any): compactEntity { + return this.entities[index]; + } + + /** + * + * @param {string} filters + */ + addActiveFilters(filters: string){ + this.activeFilters = filters; + } + + /** + * + * @returns {boolean} + */ + hasFilters(): boolean { + return this.activeFilters.length != 0; + } +} \ No newline at end of file diff --git a/scripts/classes/decoratedEntity.ts b/scripts/classes/decoratedEntity.ts new file mode 100644 index 0000000..1d68f6e --- /dev/null +++ b/scripts/classes/decoratedEntity.ts @@ -0,0 +1,177 @@ +import { compactEntity } from "./compactEntity.js"; + +export class decoratedEntity extends compactEntity { + public effects: any; + public classes: Array; + public filterSize: any; + public hasSpells: boolean; + public damageDealt: Array = []; + public damageTypes: Array = []; + public matchedPacks: Array = []; + public matchedPacksString: string = ''; + + constructor() { + super(); + this.classes = []; + this.hasSpells = false; + } + + /** + * + * @param {Item|Actor5e|JournalEntry|RollTable} entityData + * @param {string} entityType + * @param {Compendium} packList + * @param {object} classList + * @param {object} subClasses + * + * @returns {object} decoratedItem + */ + public static decorate( + entity: any, + entityType: string, + packList: { [key: string]: any } | null = null, + classList: { [key: string]: any } | null = null, + subClasses: { [key: string]: any } | null = null + ): decoratedEntity { + let decorated = new decoratedEntity(), + entityData = entity.data; + + decorated._id = entity.id; + decorated.name = entity.name || entityData.name; + decorated.img = entity.img; + decorated.type = entity.type || ''; + decorated.data.details = {}; + decorated.flags = entityData.flags; + + // getting pack(s) + let matchedPacks = []; + for (let pack in packList) { + for (let packItem of packList[pack]) { + if (entity.name.toLowerCase() === packItem.toLowerCase()) { + matchedPacks.push(pack); + break; + } + } + } + + decorated.matchedPacks = matchedPacks; + decorated.matchedPacksString = matchedPacks.join(', '); + + // handle common stuff + switch(entityType) { + case 'Feat': + case 'Item': + case 'Spell': + // getting damage types (common to all Items, although some won't have any) + decorated.damageTypes = []; + if (entityData.damage && entityData.damage.parts.length > 0) { + for (let part of entityData.damage.parts) { + let type = part[1]; + if (decorated.damageTypes.indexOf(type) === -1) { + decorated.damageTypes.push(type); + } + } + } + // getting uses/ressources status + decorated.usesRessources = entityData.hasLimitedUses; + decorated.hasSave = entityData.hasSave; + } + + // handle inidividual stuff + switch (entityType) { + // no break is intentional, specific entitytype cases are handled below + case 'Actor': + // challengeRating display + let challengeRating = () => { + let cr: number | string = Number(entityData.data.details.cr) || 0; + cr = (cr > 0 && cr < 1) ? "1/" + (1 / cr) : cr; + return cr; + }; + + decorated.displayCR = challengeRating(); + decorated.orderCR = Number(entityData.data.details?.cr) || 0; + decorated.displaySize = 'unset'; + decorated.filterSize = 2; + if (entityData.data.details?.type instanceof String) { + let temp = entityData.data.details.type; + decorated.data.details.type = { value: temp }; + } + setProperty(decorated, 'data.details.type', entityData.data.details.type); + + if (CONFIG.DND5E.actorSizes[entityData.data.traits.size] !== undefined) { + entityData.displaySize = CONFIG.DND5E.actorSizes[entityData.data.traits.size]; + } + switch (entityData.data.traits.size) { + case 'grg': decorated.filterSize = 5; break; + case 'huge': decorated.filterSize = 4; break; + case 'lg': decorated.filterSize = 3; break; + case 'sm': decorated.filterSize = 1; break; + case 'tiny': decorated.filterSize = 0; break; + case 'med': + default: decorated.filterSize = 2; break; + } + + // getting value for HasSpells and damage types + decorated.hasSpells = false; + for (let item of entityData.items) { + if (item.type == 'spell') { + decorated.hasSpells = true; + } + if (item.data.damage && item.data.damage.parts && item.data.damage.parts.length > 0) { + for (let part of item.data.damage.parts) { + let type = part[1]; + if (decorated.damageDealt.indexOf(type) === -1) { + decorated.damageDealt.push(type); + } + } + } + } + break; + case 'Feat': + let reqString = entityData.requirements?.replace(/[0-9]/g, '').trim(); + let matchedClass = []; + for (let subClass in subClasses) { + if (reqString && reqString.toLowerCase().indexOf(subClass) !== -1) { + matchedClass.push(subClass); + } else { + for (let sub of subClasses[subClass]) { + if (reqString && reqString.indexOf(sub) !== -1) { + matchedClass.push(sub); + break; + } + } + } + } + decorated.classRequirement = matchedClass; + decorated.classRequirementString = matchedClass.join(', '); + + // getting uses/ressources status + decorated.usesRessources = entityData.hasLimitedUses; + decorated.hasSave = entityData.hasSave; + break; + case 'Item': + decorated.data.rarity = entityData.data.rarity.toLowerCase().replace(/ /g, '') || 'common'; + break; + case 'RollTable': + decorated.data.displayRoll = entityData.displayRoll; + decorated.data.formula = entityData.formula; + decorated.type = 'RollTable'; + decorated.data.details.type = entityData.flags?.['better-rolltables']?.['table-type'] || 'none'; + break; + case 'Spell': + decorated.data.components = entityData.data.components; + decorated.data.level = entityData.data.level; + + // determining classes that can use the spell + let cleanSpellName = entity.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); + + if (classList && classList[cleanSpellName]) { + let classes = classList[cleanSpellName]; + decorated.classRequirement = classes.split(','); + } + break; + } + + return decorated; + } +} \ No newline at end of file diff --git a/scripts/classes/filter.ts b/scripts/classes/filter.ts new file mode 100644 index 0000000..6df4afb --- /dev/null +++ b/scripts/classes/filter.ts @@ -0,0 +1,47 @@ +enum FilterTypes { + 'text', + 'bool', + 'select', + 'multiSelect', + 'numberCompare' +} + +/** + * Used to add custom filters to the Spell-Browser + * @param {string} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value + * @param {string} label - Title of the filter + * @param {string} type - type of filter + * possible types: + * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching + * bool: will see if the data at the path exists and not false. + * select: exactly matches the data with the chosen selector from possibleValues + * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data + * numberCompare: gives the option to compare numerical values, either with =, < or the > operator + * @param {null|boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters + * @param {boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden + */ +export class Filter { + public path: string = ''; + public label: string = ''; + public type: string = ''; + public valIsArray: boolean = false; + public possibleValues: any = null; + public is_text: boolean = false; + public is_bool: boolean = false; + public is_select: boolean = false; + public is_multiSelect: boolean = false; + public is_numberCompare: boolean = false; + + constructor(path: string, label: string, type: string, possibleValues: any = null, valIsArray: boolean = false) { + this.path = path; + this.label = label; + + if (type in FilterTypes) { + this.type = type; + setProperty(this,`is_${type}`,true); + } + + this.possibleValues = possibleValues; + this.valIsArray = valIsArray; + } +} diff --git a/scripts/compendium-browser.js b/scripts/compendium-browser.js index dd65242..1814fec 100644 --- a/scripts/compendium-browser.js +++ b/scripts/compendium-browser.js @@ -1,9 +1,8 @@ -import { ModuleSettings , CMPBrowser } from './modules/settings.mjs'; -import Exporter from './modules/exporter.mjs'; -import Entities from './modules/entities.mjs'; -import { Events } from '../hooks/events.mjs'; -import { Filter } from './modules/filter.mjs'; -import { Renderer } from './modules/renderer.mjs'; +import { ModuleSettings, CMPBrowser } from './modules/settings.js'; +import { Entities } from './modules/entities.js'; +import { Events } from '../hooks/events.js'; +import { Filter } from './modules/filter.js'; +import { Renderer } from './modules/renderer.js'; //import Exporter from './modules/exporter.mjs'; /* eslint-disable valid-jsdoc */ @@ -13,7 +12,7 @@ export class CompendiumBrowser extends Application { constructor() { super(); - + ModuleSettings.registerGameSettings(); this.settings = ModuleSettings.initModuleSettings(); @@ -28,11 +27,11 @@ export class CompendiumBrowser extends Application { const options = super.defaultOptions; mergeObject(options, { title: "CMPBrowser.compendiumBrowser", - tabs: [{ navSelector: ".tabs", contentSelector: ".content", initial: "spell" }], + tabs: [{ navSelector: ".tabs", contentSelector: ".content", initial: "Item" }], classes: options.classes.concat('compendium-browser'), - template: "modules/compendium-browser/template/template.html", - width: 800, - height: 700, + template: "modules/compendium-browser/template/template.hbs", + width: 900, + height: 800, resizable: true, minimizable: true }); @@ -45,14 +44,15 @@ export class CompendiumBrowser extends Application { this.settings = ModuleSettings.initModuleSettings(); } - await loadTemplates([ - "modules/compendium-browser/template/spell-browser.html", - "modules/compendium-browser/template/entity-browser.html", - "modules/compendium-browser/template/entity-browser-list.html", - "modules/compendium-browser/template/filter-container.html", - "modules/compendium-browser/template/settings.html", - "modules/compendium-browser/template/loading.html" + Renderer.loadTemplates([ + "modules/compendium-browser/template/template.hbs", + "modules/compendium-browser/template/entity-browser.hbs", + "modules/compendium-browser/template/entity-list.hbs", + "modules/compendium-browser/template/filter-container.hbs", + "modules/compendium-browser/template/settings.hbs", + "modules/compendium-browser/template/loading.hbs" ]); + this.filters.addEntityFilters(); this.hookCompendiumList(); } @@ -62,15 +62,10 @@ export class CompendiumBrowser extends Application { _onChangeTab(event, tabs, active) { super._onChangeTab(event, tabs, active); const html = this.element; - this.refreshFilters(html,active); - this.replaceList(html, active, { reload: false }); - } - - async refreshFilters(html,entityType){ - let entityBrowserHtml = await Renderer.renderFilters(game.compendiumBrowser.filters[entityType +'Filters']), - filterWrapper = document.querySelector('.tab.active .browser .control-area'); - filterWrapper.innerHTML = filterWrapper.firstElementChild.outerHTML + entityBrowserHtml; - Events.activateFilterListeners(html); + if (active != 'setting') { + Events.reloadFilters(html, active); + this.replaceList(html, active, { reload: false }); + } } /** @@ -78,28 +73,26 @@ export class CompendiumBrowser extends Application { * @returns {Obejct} data */ async getData() { - - //0.4.1 Filter as we load to support new way of filtering - //Previously loaded all data and filtered in place; now loads minimal (preload) amount, filtered as we go - //First time (when you press Compendium Browser button) is called with filters unset - - //0.4.1k: Don't do any item/npc loading until tab is visible let data = { items: [], - npcs: [], - spellFilters: this.filters.getByName('spellFilters'), - showSpellBrowser: (game.user.isGM || this.settings.allowSpellBrowser), - featFilters: this.filters.getByName('featFilters'), - showFeatBrowser: (game.user.isGM || this.settings.allowFeatBrowser), - itemFilters: this.filters.getByName('itemFilters'), - showItemBrowser: (game.user.isGM || this.settings.allowItemBrowser), - npcFilters: this.filters.getByName('npcFilters'), - showNpcBrowser: (game.user.isGM || this.settings.allowNpcBrowser), + actors: [], + filters: { + Spell: this.filters.getByName('Spell'), + Item: this.filters.getByName('Item'), + Actor: this.filters.getByName('Actor'), + RollTable: this.filters.getByName('RollTable'), + JournalEntry: this.filters.getByName('RollTable') + }, + showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, + showFeatBrowser: (game.user.isGM) || this.settings.allowFeatBrowser, + showItemBrowser: (game.user.isGM) || this.settings.allowItemBrowser, + showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, + showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser, + showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser, settings: this.settings, isGM: game.user.isGM }; - return data; } @@ -110,26 +103,12 @@ export class CompendiumBrowser extends Application { this.observer = new IntersectionObserver((entries, observer) => { for (let [i, e] of entries.entries()) { if (!e.isIntersecting) break; - const img = e.target; + const el = e.target; // Avatar image //const img = li.querySelector("img"); - if (img && img.dataset.src) { - img.src = img.dataset.src; - delete img.dataset.src; - } - - // pre load more elements if there are any - if (i + 2 < entries.length) { - let next = entries[i+1]?.target, - evenmore = entries[i+2].target; - if((next && next.dataset.src) && (evenmore && evenmore.dataset.src)){ - next.src = next.dataset.src; - evenmore.src = evenmore.dataset.src; - delete next.dataset.src; - delete evenmore.dataset.src; - observer.unobserve(next); - observer.unobserve(evenmore); - } + if (el && el.dataset.src) { + el.style['background-image'] = `url(${el.dataset.src})`; + delete el.dataset.src; } // No longer observe the target @@ -143,9 +122,9 @@ export class CompendiumBrowser extends Application { //Just for the loading image if (this.observer) { - html.find("img").each((i, img) => this.observer.observe(img)); + html.find(".entity-image").each((i, imageElement) => this.observer.observe(imageElement)); } - } + } hookCompendiumList() { Hooks.on('renderCompendiumDirectory', (app, html, data) => { @@ -156,7 +135,7 @@ export class CompendiumBrowser extends Application { if (this.settings === undefined) { this.initSettings(); } - if (game.user.isGM || this.settings.allowSpellBrowser || this.settings.allowNpcBrowser) { + if (game.user.isGM || this.settings.allowItemBrowser || this.settings.allowSpellBrowser || this.settings.allowActorBrowser) { const cbButton = $(``); html.find('.compendium-browser-btn').remove(); @@ -171,53 +150,50 @@ export class CompendiumBrowser extends Application { //0.4.3: Reset everything (including data) when you press the button - calls afterRender() hook if (game.user.isGM || this.settings.allowSpellBrowser) { - this.refreshList = "spell"; + this.refreshList = "Spell"; } else if (this.settings.allowFeatBrowser) { - this.refreshList = "feat"; + this.refreshList = "Feat"; } else if (this.settings.allowItemBrowser) { - this.refreshList = "item"; - } else if (this.settings.allowNPCBrowser) { - this.refreshList = "npc"; + this.refreshList = "Item"; + } else if (this.settings.allowActorBrowser) { + this.refreshList = "Actor"; + } else if (this.settings.allowJournalEntryBrowser) { + this.refreshList = "JournalEntry"; + } else if (this.settings.allowRollTableBrowser) { + this.refreshList = "RollTable"; } this.render(true); }); } } - - /* Hook to load the first data */ - static afterRender(cb, html) { - //0.4.3: Because a render always resets ALL the displayed filters (on all tabs) to unselected , we have to blank all the lists as well - // (because the current HTML template doesn't set the selected filter values) - if (!cb?.refreshList) { return; } - - cb.replaceList(html, cb.refreshList); - - cb.refreshList = null; - } - + /** + * + * @param {*} html + * @param {*} entityType + * @param {*} options + */ async replaceList(html, entityType, options = { reload: true }) { //After rendering the first time or re-rendering trigger the load/reload of visible data - let entityListElement = document.querySelector('.tab.active .browser .cb_entities'); + let entityListElement = document.querySelector('.tab.active .browser .cb_entities'); if (entityListElement.childElementCount !== undefined) { //0.4.2b: On a tab-switch, only reload if there isn't any data already if (options?.reload || entityListElement.childElementCount < 1) { const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; - await Renderer.updateLoading(0,maxLoad,entityType); + await Renderer.updateLoading(entityType, 0, maxLoad); // loadItems - const entityHelper = new Entities(this.settings, this.filters); - let entityList = await entityHelper.loadAndFilter(entityType,true); - // + const entityHelper = new Entities(); + let entityList = await entityHelper.loadAndFilter(entityType, true); this.currentLists[entityType] = entityList = Entities._sortList(entityList, entityType); //Uses loadAndFilterItems to read compendia for items which pass the current filters and render on this tab - const newEntitiesHTML = await Renderer.renderEntityList(entityList.entities, true); - entityListElement.setAttribute('data-active-filters', entityList.filters); + const newEntitiesHTML = await Renderer.renderEntityList(entityList.entities, entityType, true); + entityListElement.setAttribute('data-active-filters', entityList.activeFilters); entityListElement.innerHTML = newEntitiesHTML; - await Events.observeListElement(entityListElement,'img'); + await Events.observeListElement(entityListElement, 'aside'); //Reactivate listeners for clicking and dragging Events.activateItemListListeners(html); @@ -235,5 +211,5 @@ export class CompendiumBrowser extends Application { } return newObj; } - + } \ No newline at end of file diff --git a/scripts/modules/entities.mjs b/scripts/modules/entities.mjs deleted file mode 100644 index c82688d..0000000 --- a/scripts/modules/entities.mjs +++ /dev/null @@ -1,370 +0,0 @@ -import { Filter } from './filter.mjs'; -import { Renderer } from './renderer.mjs'; -import { CMPBrowser } from './settings.mjs'; - -export default class Entities { - - constructor(settings, filters) { - this.settings = settings; - this.filters = filters; - } - - /** - * - * @param {String} entityType - * @param {*} updateLoading - * @returns - */ - async loadAndFilter(entityType = "spell", updateLoading = null) { - console.log(`Load and Filter Items | Started loading ${entityType}s`); - console.time("loadAndFilterItems"); - await this.checkListsLoaded(); - const Category = (entityType === 'npc') ? 'Actor' : 'Item'; - const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; - const ActiveFilters = game.compendiumBrowser.filters.getByType(entityType).activeFilters; - const packs = game.packs.filter((pack) => pack.metadata.entity === Category); - //0.4.1: Load and filter just one of spells, feats, and items (specified by browserTab) - let unfoundSpells = ''; - let numItemsLoaded = 0; - let compactList = { - filters: JSON.stringify(ActiveFilters), - entities: [] - }; - - //Filter the full list, but only save the core compendium information + displayed info - for (let pack of packs) { - if (game.compendiumBrowser.settings.loadedCompendium[pack.metadata.entity][pack.collection].load) { - //FIXME: How much could we do with the loaded index rather than all content? - //OR filter the content up front for the decoratedItem.type?? - await pack.getContent().then(content => { - for (let currentEntity of content) { - - if (currentEntity.data.type != entityType && entityType != Category.toLowerCase()) continue; - - let compactEntity = null, - decoratedEntity = false; - - switch (entityType) { - case "spell": - decoratedEntity = Entities.decorateEntity(currentEntity, entityType, this.packList, this.classList, this.subClasses); - if (Filter.passesFilter(decoratedEntity, ActiveFilters)) { - compactEntity = { - _id: decoratedEntity._id, - compendium: pack.collection, - name: decoratedEntity.name, - img: decoratedEntity.img, - type: decoratedEntity.type, - dae: decoratedEntity.effects.size || false, - classRequirement: decoratedEntity.classRequirement, - data: { - level: decoratedEntity.data?.level, - components: decoratedEntity.data?.components - } - }; - } - break; - - case "feat": - decoratedEntity = Entities.decorateEntity(currentEntity, entityType, this.packList, this.classList, this.subClasses); - if (["feat", "class"].includes(decoratedEntity.type) && Filter.passesFilter(decoratedEntity, ActiveFilters)) { - compactEntity = { - _id: decoratedEntity._id, - compendium: pack.collection, - name: decoratedEntity.name, - img: decoratedEntity.img, - type: decoratedEntity.type, - dae: decoratedEntity.effects.size || false, - classRequirement: decoratedEntity.classRequirement - }; - } - break; - - case "item": - decoratedEntity = Entities.decorateEntity(currentEntity, entityType, this.packList, this.classList, this.subClasses); - //0.4.5: Itm type for true items could be many things (weapon, consumable, etc) so we just look for everything except spells, feats, classes - if (!["spell", "feat", "class"].includes(decoratedEntity.type) && Filter.passesFilter(decoratedEntity, ActiveFilters)) { - compactEntity = { - _id: decoratedEntity._id, - compendium: pack.collection, - name: decoratedEntity.name, - img: decoratedEntity.img, - type: decoratedEntity.type, - rarity: decoratedEntity.data?.rarity.toLowerCase().replace(/ /g, ''), - dae: decoratedEntity.effects.size || false, - ac: (decoratedEntity.data?.armor?.type) ? decoratedEntity.data?.armor?.value || false : false, - }; - - if (compactEntity.type == 'weapon' && (decoratedEntity.data?.range?.value)) { - setProperty(compactEntity, `tags.range`, decoratedEntity.data?.range?.value + decoratedEntity.data?.range?.units); - } - - for (let filter of Object.values(ActiveFilters)) { - setProperty(compactEntity, `tags.${filter.path}`, filter.value); - } - } - break; - case "npc": - decoratedEntity = Entities.decorateEntity(currentEntity, entityType, this.packList, this.classList, this.subClasses); - if (Filter.passesFilter(decoratedEntity, ActiveFilters)) { - //0.4.2: Don't store all the details - just the display elements - compactEntity = { - _id: decoratedEntity._id, - compendium: pack.collection, - name: decoratedEntity.name, - img: decoratedEntity.img, - displayCR: decoratedEntity.displayCR, - displaySize: decoratedEntity.displaySize, - displayType: decoratedEntity.data?.details?.type, - orderCR: decoratedEntity.data.details.cr, - orderSize: decoratedEntity.filterSize - } - } - break; - default: - break; - } - - if (compactEntity) { //Indicates it passed the filters - compactList.entities.push(compactEntity); - if (numItemsLoaded++ >= maxLoad) break; - //0.4.2e: Update the UI (e.g. "Loading 142 spells") - if (updateLoading) { Renderer.updateLoading(numItemsLoaded,500,entityType); } - } - } //for item5e of content - }); - } - if (numItemsLoaded >= maxLoad) break; - }//for packs - - this.itemsLoaded = true; - console.timeEnd("loadAndFilterItems"); - console.log(`Load and Filter Items | Finished loading ${compactList.length} ${entityType}s`); - return compactList; - } - - static _sortList(list, entityType, orderBy) { - if(entityType == 'npc'){ - switch (orderBy) { - case 'name': - list.entities.sort((a, b) => { - let aName = a.name; - let bName = b.name; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); break; - case 'cr': - list.entities.sort((a, b) => { - let aVal = Number(a.orderCR); - let bVal = Number(b.orderCR); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = a.name; - let bName = b.name; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); break; - case 'size': - list.entities.sort((a, b) => { - let aVal = a.orderSize; - let bVal = b.orderSize; - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = a.name; - let bName = b.name; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); break; - } - } else { - if (orderBy) { - list.entities.sort((a, b) => { - let aName = a.name; - let bName = b.name; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); - } else { - let defaultSort = new Map([ - ['spell', 'level'], - ['feats', 'class'], - ['items', 'type'], - ]); - - list.entities.sort((a, b) => { - let sort = defaultSort.get(entityType); - let aVal = a[sort]; - let bVal = b[sort]; - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = a.name; - let bName = b.name; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); - } - } - return list; - } - - /** - * - * @param {item5e|Object}entity - * @param {Object|Iterable} packList - * @param {Object|Iterable} classList - * @param {Object|Iterable} subClasses - * - * @returns {Object} decoratedItem - */ - static decorateEntity(entity, entityType, packList = {}, classList = {}, subClasses = {}) { - if (!entity) return null; - //Decorate and then filter a compendium entry - returns null or the item - const entityData = entity.data; - - if (entityType === 'npc') { - // cr display - let cr = entityData.data.details.cr; - if (cr == undefined || cr == '') cr = 0; - else cr = Number(cr); - if (cr > 0 && cr < 1) cr = "1/" + (1 / cr); - entityData.displayCR = cr; - entityData.displaySize = 'unset'; - entityData.filterSize = 2; - if (!entityData.data.details.type.value){ - let temp = entityData.data.details.type; - entityData.data.details.type = {value: temp}; - setProperty(entityData,'data.details.type', entityData.data.details.type); - } - if (CONFIG.DND5E.actorSizes[entityData.data.traits.size] !== undefined) { - entityData.displaySize = CONFIG.DND5E.actorSizes[entityData.data.traits.size]; - } - switch (entityData.data.traits.size) { - case 'grg': entityData.filterSize = 5; break; - case 'huge': entityData.filterSize = 4; break; - case 'lg': entityData.filterSize = 3; break; - case 'sm': entityData.filterSize = 1; break; - case 'tiny': entityData.filterSize = 0; break; - case 'med': - default: entityData.filterSize = 2; break; - } - - // getting value for HasSpells and damage types - entityData.hasSpells = false; - entityData.damageDealt = []; - for (let item of entityData.items) { - if (item.type == 'spell') { - entityData.hasSpells = true; - } - if (item.data.damage && item.data.damage.parts && item.data.damage.parts.length > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (entityData.damageDealt.indexOf(type) === -1) { - entityData.damageDealt.push(type); - } - } - } - } - } else { - // getting damage types (common to all Items, although some won't have any) - entityData.damageTypes = []; - if (entityData.data.damage && entityData.data.damage.parts.length > 0) { - for (let part of entityData.data.damage.parts) { - let type = part[1]; - if (entityData.damageTypes.indexOf(type) === -1) { - entityData.damageTypes.push(type); - } - } - } - - if (entityData.type === 'spell') { - // determining classes that can use the spell - let cleanSpellName = entityData.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); - //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); - if (classList[cleanSpellName]) { - let classes = classList[cleanSpellName]; - entityData.classRequirement = classes.split(','); - } - } else if (entityData.type === 'feat' || entityData.type === 'class') { - // getting class - let reqString = entityData.data.requirements?.replace(/[0-9]/g, '').trim(); - let matchedClass = []; - for (let c in subClasses) { - if (reqString && reqString.toLowerCase().indexOf(c) !== -1) { - matchedClass.push(c); - } else { - for (let subClass of subClasses[c]) { - if (reqString && reqString.indexOf(subClass) !== -1) { - matchedClass.push(c); - break; - } - } - } - } - entityData.classRequirement = matchedClass; - entityData.classRequirementString = matchedClass.join(', '); - - // getting uses/ressources status - entityData.usesRessources = entity.hasLimitedUses; - entityData.hasSave = entity.hasSave; - - } else { - // getting pack - let matchedPacks = []; - for (let pack of Object.keys(packList)) { - for (let packItem of packList[pack]) { - if (entityData.name.toLowerCase() === packItem.toLowerCase()) { - matchedPacks.push(pack); - break; - } - } - } - entityData.matchedPacks = matchedPacks; - entityData.matchedPacksString = matchedPacks.join(', '); - - // getting uses/ressources status - entityData.usesRessources = entity.hasLimitedUses; - } - } - - return entityData; - - } - - async checkListsLoaded() { - const dataPath = '/modules/compendium-browser/data/'; - //Provides extra info not in the standard SRD, like which classes can learn a spell - if (!this.classList) { - this.classList = await fetch(dataPath + 'spell-classes.json').then(result => { - return result.json(); - }).then(obj => { - return this.classList = obj; - }); - } - - if (!this.packList) { - this.packList = await fetch(dataPath + 'item-packs.json').then(result => { - return result.json(); - }).then(obj => { - return this.packList = obj; - }); - } - - if (!this.subClasses) { - this.subClasses = await fetch(dataPath + 'sub-classes.json').then(result => { - return result.json(); - }).then(obj => { - return this.subClasses = obj; - }); - } - } -} \ No newline at end of file diff --git a/scripts/modules/entities.ts b/scripts/modules/entities.ts new file mode 100644 index 0000000..e11bf62 --- /dev/null +++ b/scripts/modules/entities.ts @@ -0,0 +1,195 @@ +import { Filter } from './filter.js'; +import { Renderer } from './renderer.js'; +import { CMPBrowser, ModuleSettings } from './settings.js'; + +import { compactEntity } from '../classes/compactEntity.js'; +import { decoratedEntity } from '../classes/decoratedEntity.js'; +import { compactList } from '../classes/compactList.js'; + +export class Entities { + private packList: { [key: string]: Object } | null = null; + private classList: { [key: string]: Object } | null = null; + private subClassList: { [key: string]: Object } | null = null; + private itemsLoaded: boolean = false; + + /** + * + * @param {string} entityType + * @param {any} updateLoading + * + * @returns {Promise} + */ + async loadAndFilter(entityType: string = "Item", updateLoading: any = null): Promise { + console.log(`Load and Filter Items | Started loading ${entityType}s`); + console.time("loadAndFilterItems"); + + await this.checkListsLoaded(); + const Category = (entityType === 'Spell' || entityType === 'Feat') ? 'Item' : entityType; + const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; + const ActiveFilters = game.compendiumBrowser.filters.getByName(entityType).activeFilters; + const packs = game.packs.filter((pack) => pack.metadata.entity === Category); + //0.4.1: Load and filter just one of spells, feats, and items (specified by browserTab) + let unfoundSpells = ''; + let numItemsLoaded = 0, + numPacks = packs.length, + comp_list = new compactList(); + + comp_list.activeFilters = JSON.stringify(ActiveFilters); + + //Filter the full list, but only save the core compendium information + displayed info + for (let pack of packs) { + if (game.compendiumBrowser.settings.loadedCompendium[pack.metadata.entity][pack.collection].load) { + //FIXME: How much could we do with the loaded index rather than all content? + //OR filter the content up front for the decoratedItem.type?? + await pack.getDocuments().then((content: Collection) => { + for (let currentEntity of content) { + // fail fast + if (currentEntity.data.type != entityType.toLowerCase() && entityType != Category) continue; + if (!Filter.passesFilter(currentEntity, ActiveFilters)) continue; + + let decorated = decoratedEntity.decorate(currentEntity, entityType, this.packList, this.classList, this.subClassList); + let compact = new compactEntity(); + + // set common properties for all entities + + compact._id = decorated._id; + compact.name = decorated.name; + compact.compendium = pack.collection; + compact.img = decorated.img; + compact.type = decorated.type; + compact.data = decorated.data; + compact.flags = decorated.flags; + + if (ActiveFilters) { + for (let index in ActiveFilters) { + setProperty(compact, `tags.${ActiveFilters[index].path}`, ActiveFilters[index].value); + } + } + + if (Category == 'Item') { + compact.dae = (decorated.effects?.size) || false; + } + + switch (entityType) { + case "Spell": + case "Feat": + if (entityType === 'Spell' || ["feat", "class"].includes(decorated.type)) { + compact.classRequirement = decorated.classRequirement; + } + break; + case "Item": + //0.4.5: Itm type for true items could be many things (weapon, consumable, etc) so we just look for everything except spells, feats, classes + if (!["spell", "feat", "class"].includes(decorated.type)) { + compact.rarity = decorated.data.rarity; + compact.ac = (decorated.data?.armor?.type) ? decorated.data?.armor?.value || false : false; + + if (compact.type == 'weapon' && (decorated.data?.range?.value)) { + setProperty(compact, `tags.range`, decorated.data?.range?.value + decorated.data?.range?.units); + } + } + break; + case "Actor": + compact.displayCR = decorated.displayCR; + compact.displaySize = decorated.displaySize; + compact.displayType = decorated.data?.details?.type; + compact.orderCR = decorated.orderCR; + compact.orderSize = decorated.filterSize; + compact.data.details = decorated.data.details; + break; + default: + break; + } + + comp_list.addEntity(compact); + if (updateLoading) { Renderer.updateLoading(entityType, numItemsLoaded, numPacks, 500); } + if (numItemsLoaded++ >= maxLoad) break; + } + }); // get Entities + } + if (numItemsLoaded >= maxLoad) break; + }//for packs + + console.timeEnd("loadAndFilterItems"); + console.log(`Load and Filter Items | Finished loading ${comp_list.size()} ${entityType}s`); + return comp_list; + } + + /** + * + * @param {compactList} list + * @param {string} entityType + * @param {string} orderBy + * @returns {compactList} + */ + static _sortList(list: compactList, entityType: string, orderBy: string | undefined): compactList { + const SortCollator = new Intl.Collator(game.i18n.lang, {numeric: true, sensitivity: 'base'}); + if (entityType === 'Actor') { + switch (orderBy) { + case 'name': + list.entities.sort((left: compactEntity, right: compactEntity) => SortCollator.compare(left.name, right.name)); + break; + case 'cr': + list.entities.sort((left: compactEntity, right: compactEntity) => { + return left.displayCR - right.displayCR || SortCollator.compare(left.name, right.name); + }); + break; + case 'size': + list.entities.sort((left: compactEntity, right: compactEntity) => { + return left.orderSize - right.orderSize || SortCollator.compare(left.name, right.name); + }); + break; + } + } else { + if (orderBy) { + list.entities.sort((left: compactEntity, right: compactEntity) => left.name.localeCompare(right.name)); + } else { + let defaultSort = new Map([ + ['Item', 'type'], + ['Spell', 'data.level'], + ['Feats', 'data.class'], + ['RollTable', 'compendium'], + ['JounralEntry', 'name'], + ]); + + list.entities.sort((left: compactEntity, right: compactEntity) => { + let sort = defaultSort.get(entityType) || '', + result = SortCollator.compare(getProperty(left, sort),getProperty(right, sort)); + + return result || SortCollator.compare(left.name, right.name); + }); + } + } + return list; + } + + /** + * Check all the prepared list with extra info are loaded + */ + async checkListsLoaded() { + const dataPath = '/modules/compendium-browser/data/'; + //Provides extra info not in the standard SRD, like which classes can learn a spell + if (!this.classList) { + this.classList = await fetch(dataPath + 'spell-classes.json').then(result => { + return result.json(); + }).then(list => { + return list; + }); + } + + if (!this.packList) { + this.packList = await fetch(dataPath + 'item-packs.json').then(result => { + return result.json(); + }).then(list => { + return list; + }); + } + + if (!this.subClassList) { + this.subClassList = await fetch(dataPath + 'sub-classes.json').then(result => { + return result.json(); + }).then(list => { + return list; + }); + } + } +} \ No newline at end of file diff --git a/scripts/modules/exporter.ts b/scripts/modules/exporter.ts new file mode 100644 index 0000000..0e75a31 --- /dev/null +++ b/scripts/modules/exporter.ts @@ -0,0 +1,48 @@ +export class Exporter { + + /** + * Create a new RollTable from a given Set of entitites. + * + * @param {string} tableName the name of the table entity that will be created + * @param {Array} entities a set of + * @param {function(Entity)} weightPredicate a function that returns a weight (number) that will be used + * for the tableResult weight for that given entity. returning 0 will exclude the entity from appearing in the table + */ + static async createTableFromSelection( + tableName: string = new Date().valueOf().toString(), + entities: Array, + weightPredicate: Function|null = null, + options: Array | null = null + ): Promise { + let data = { name: tableName }, + tableArray = []; + + if (!entities || !(entities.length > 0)) return false; + + const newTable: any = await RollTable.create(data); + + ui.notifications.info(`Starting generation of a rolltable with ${entities.length} entries.`); + + for (let entity of entities) { + let weight = weightPredicate != null ? weightPredicate(entity) : 1; + if (weight <= 0) continue; + + let tableRowData: any = {}; + tableRowData.type = 2; + tableRowData.collection = entity.entryCompendium; + tableRowData.text = entity.entryName; + tableRowData.img = entity.entryImage; + tableRowData.weight = weight; + tableRowData.range = [1, 1]; + + tableArray.push(tableRowData); + } + + await newTable.createEmbeddedDocuments('TableResult', tableArray); + await newTable.normalize(); + + ui.notifications.info(`Rolltable ${tableName} with ${tableArray.length} entries was generated.`); + + return true; + } +} \ No newline at end of file diff --git a/scripts/modules/filter.mjs b/scripts/modules/filter.ts similarity index 64% rename from scripts/modules/filter.mjs rename to scripts/modules/filter.ts index 968cce5..5e82e8e 100644 --- a/scripts/modules/filter.mjs +++ b/scripts/modules/filter.ts @@ -1,35 +1,35 @@ -export class Filter { +import { Filter as FilterEntity } from "../classes/filter.js"; - constructor() { - //Reset the filters used in the dialog - this.spellFilters = this._getInitialFilters(); - this.npcFilters = this._getInitialFilters(); - this.featFilters = this._getInitialFilters(); - this.itemFilters = this._getInitialFilters(); - } +export class Filter { + public Spell: any = this._getInitialFilters(); + public Actor: any = this._getInitialFilters(); + public Feat: any = this._getInitialFilters(); + public Item: any = this._getInitialFilters(); + public RollTable: any = this._getInitialFilters(); + public JournalEntry: any = this._getInitialFilters(); /** * - * @param {Object} subject - * @param {Obj} filters - * @returns + * @param {any} subject + * @param {any} filters + * + * @returns {boolean} */ - static passesFilter(subject, filters) { + static passesFilter(subject: any, filters: Object): boolean { for (let filter of Object.values(filters)) { - let prop = getProperty(subject, filter.path); + let prop = getProperty(subject, `data.${filter.path}`) || getProperty(subject, filter.path); + if (prop === undefined) return false; if (filter.type === 'numberCompare') { - switch (filter.operator) { case '=': if (prop != filter.value) { return false; } break; case '<': if (prop >= filter.value) { return false; } break; case '>': if (prop <= filter.value) { return false; } break; } - continue; } + if (filter.valIsArray === false) { if (filter.type === 'text') { - if (prop === undefined) return false; if (prop.toLowerCase().indexOf(filter.value.toLowerCase()) === -1) { return false; } @@ -70,32 +70,37 @@ export class Filter { return true; } - filterElements(list, subjects, filters) { - for (let element of list) { - let subject = subjects[element.dataset.entryId]; - if (this.passesFilter(subject, filters) == false) { - $(element).hide(); - } else { - $(element).show(); - } - } - } - - getByName(name) { + /** + * + * @param name + * @returns any + */ + getByName(name: string) { return getProperty(this, name); } - getByType(type){ - return getProperty(this, type+'Filters'); + /** + * + * @param {string} entityType + * @returns {Iterable|undefined} + */ + getByEntityType(entityType: string){ + return getProperty(this, entityType); } resetFilters() { - this.spellFilters.activeFilters = {}; - this.featFilters.activeFilters = {}; - this.itemFilters.activeFilters = {}; - this.npcFilters.activeFilters = {}; + this.Spell.activeFilters = {}; + this.Feat.activeFilters = {}; + this.Item.activeFilters = {}; + this.Actor.activeFilters = {}; + this.RollTable.activeFilters = {}; + this.JournalEntry.activeFilters = {}; } + /** + * + * @returns {Object} + */ _getInitialFilters() { return { registeredFilterCategorys: {}, @@ -103,57 +108,55 @@ export class Filter { }; } + /** + * add entityfilters + */ async addEntityFilters(){ await this.addSpellFilters(); await this.addFeatFilters(); await this.addItemFilters(); - await this.addNpcFilters(); + await this.addActorFilters(); + await this.addRollTableFilters(); } /** * Used to add custom filters to the Spell-Browser - * @param {String} entityType type of entity for the filter - * @param {String} category - Title of the category - * @param {String} label - Title of the filter - * @param {String} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value - * @param {String} type - type of filter + * @param {string} entityType type of entity for the filter + * @param {string} category - Title of the category + * @param {string} label - Title of the filter + * @param {string} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value + * @param {string} type - type of filter * possible filter: * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching * bool: will see if the data at the path exists and not false. * select: exactly matches the data with the chosen selector from possibleValues * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data * numberCompare: gives the option to compare numerical values, either with =, < or the > operator - * @param {Boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters - * @param {Boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden + * @param {null|boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters + * @param {boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden */ - async addFilter(entityType, category, label, path, type, possibleValues = null, valIsArray = false) { - let target = `${entityType}Filters`; - let filter = {}; - filter.path = path; - filter.label = label; - filter.type = 'text'; - if (['text', 'bool', 'select', 'multiSelect', 'numberCompare'].indexOf(type) !== -1) { - filter[`is${type}`] = true; - filter.type = type; - } - if (possibleValues !== null) { - filter.possibleValues = possibleValues; - } - filter.valIsArray = valIsArray; + async addFilter(entityType: string, category: string, label: string, path: string, type: string, possibleValues: any = null, valIsArray: boolean = false) { + let filter = new FilterEntity(path, label, type, possibleValues, valIsArray), + catId = category.replace(/\W/g, ''), + target = this.getByName(entityType).registeredFilterCategorys; - let catId = category.replace(/\W/g, ''); - if (this[target].registeredFilterCategorys[catId] === undefined) { - this[target].registeredFilterCategorys[catId] = { label: category, filters: [] }; + if (target[catId] === undefined) { + target[catId] = { label: category, filters: [] }; } - this[target].registeredFilterCategorys[catId].filters.push(filter); - + + target[catId].filters.push(filter); } + /** + * Add all spellfilters + * + * @todo convert this to read and use an iteratable object that can be stored in an extra file + */ async addSpellFilters() { - const SPELL = 'spell'; + const SPELL = 'Spell'; // Spellfilters this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); - this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.lvl"), 'data.level', 'multiSelect', [game.i18n.localize("CMPBrowser.cantip"), 1, 2, 3, 4, 5, 6, 7, 8, 9]); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.lvl"), 'data.level', 'multiSelect', [game.i18n.localize("CMPBrowser.cantrip"), 1, 2, 3, 4, 5, 6, 7, 8, 9]); this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.school"), 'data.school', 'select', CONFIG.DND5E.spellSchools); this.addFilter(SPELL, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.castingTime"), 'data.activation.type', 'select', { @@ -182,13 +185,13 @@ export class Filter { ); this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.ritual"), 'data.components.ritual', 'bool'); this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.concentration"), 'data.components.concentration', 'bool'); - this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.verbal"), 'data.components.vocal', 'bool'); + this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.vocal"), 'data.components.vocal', 'bool'); this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.somatic"), 'data.components.somatic', 'bool'); this.addFilter(SPELL, game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.material"), 'data.components.material', 'bool'); } async addItemFilters() { - let ITEM = 'item'; + const ITEM = 'Item'; // Item Filters await this.addFilter(ITEM, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); @@ -231,8 +234,7 @@ export class Filter { } async addFeatFilters() { - const FEAT = 'feat'; - // Feature Filters + const FEAT = 'Feat'; this.addFilter(FEAT, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); this.addFilter(FEAT,game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'classRequirement', 'select', @@ -258,27 +260,26 @@ export class Filter { } - async addNpcFilters() { + async addActorFilters() { const isFoundryV8 = game.data.version.startsWith("0.8"), - NPC = 'npc'; - // NPC Filters + ACTOR = 'Actor'; - this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.details.source', 'text'); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.size"), 'data.traits.size', 'select', CONFIG.DND5E.actorSizes); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasSpells"), 'hasSpells', 'bool'); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegAct"), 'data.resources.legact.max', 'bool'); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegRes"), 'data.resources.legres.max', 'bool'); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.cr"), 'data.details.cr', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.details.source', 'text'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.size"), 'data.traits.size', 'select', CONFIG.DND5E.actorSizes); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasSpells"), 'hasSpells', 'bool'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegAct"), 'data.resources.legact.max', 'bool'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegRes"), 'data.resources.legres.max', 'bool'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.cr"), 'data.details.cr', 'numberCompare'); //Foundry 0.8.x: Creature type (data.details.type) is now a structure, so we check data.details.types.value instead - let npcDetailsPath; + let actorDetailsPath; if (isFoundryV8) { - npcDetailsPath = "data.details.type.value"; + actorDetailsPath = "data.details.type.value"; } else {//0.7.x - npcDetailsPath = "data.details.type"; + actorDetailsPath = "data.details.type"; } - this.addFilter(NPC, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.creatureType"), npcDetailsPath, 'text', { + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.creatureType"), actorDetailsPath, 'text', { aberration: game.i18n.localize("CMPBrowser.aberration"), beast: game.i18n.localize("CMPBrowser.beast"), celestial: game.i18n.localize("CMPBrowser.celestial"), @@ -295,18 +296,28 @@ export class Filter { undead: game.i18n.localize("CMPBrowser.undead") }); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityStr"), 'data.abilities.str.value', 'numberCompare'); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityDex"), 'data.abilities.dex.value', 'numberCompare'); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCon"), 'data.abilities.con.value', 'numberCompare'); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityInt"), 'data.abilities.int.value', 'numberCompare'); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityWis"), 'data.abilities.wis.value', 'numberCompare'); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCha"), 'data.abilities.cha.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityStr"), 'data.abilities.str.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityDex"), 'data.abilities.dex.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCon"), 'data.abilities.con.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityInt"), 'data.abilities.int.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityWis"), 'data.abilities.wis.value', 'numberCompare'); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCha"), 'data.abilities.cha.value', 'numberCompare'); + + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamImm"), 'data.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamRes"), 'data.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamVuln"), 'data.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.ConImm"), 'data.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true); + this.addFilter(ACTOR, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("CMPBrowser.dmgDealt"), 'damageDealt', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamImm"), 'data.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamRes"), 'data.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamVuln"), 'data.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.ConImm"), 'data.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true); - this.addFilter(NPC, game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("CMPBrowser.dmgDealt"), 'damageDealt', 'multiSelect', CONFIG.DND5E.damageTypes, true); + } + async addRollTableFilters() { + const RT = 'RollTable'; + this.addFilter(RT, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.TableType"), 'flags.better-rolltables.table-type', 'select', { + none: "FoundryVTT default", + better: "Better", + loot: "Loot", + story: "Story", + }) } } \ No newline at end of file diff --git a/scripts/modules/renderer.mjs b/scripts/modules/renderer.mjs deleted file mode 100644 index 2c54bc1..0000000 --- a/scripts/modules/renderer.mjs +++ /dev/null @@ -1,40 +0,0 @@ -export class Renderer { - constructor() { - this.currentLists = {}; - } - - /** - * Render the Information section - * - * @param messageElement - * @param itemType - * @param numLoaded - * @param maxLoaded - * @returns - */ - static async renderLoading(messageElement, itemType, numLoaded, maxLoaded = false) { - if (!messageElement) return; - - let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.html", { numLoaded: numLoaded, itemType: itemType, maxLoaded: maxLoaded }); - messageElement.innerHTML = loadingHTML; - } - - static async updateLoading(numLoaded = 0, maxLoad = 500, entityType) { - let loader = document.getElementById('CBInfoMessage'); - if (loader) { Renderer.renderLoading(loader, entityType, numLoaded, numLoaded >= maxLoad); } - } - - /** - * - * @param entityType - * @param updateLoading - * @returns {html} html rendered List of entities - */ - static async renderEntityList(entityList, updateLoading = null) { - return await renderTemplate(`modules/compendium-browser/template/entity-browser-list.html`, { listItems: entityList, }); - } - - static async renderFilters(filters){ - return await renderTemplate(`modules/compendium-browser/template/filter-container.html`, {filters: filters}); - } -} \ No newline at end of file diff --git a/scripts/modules/renderer.ts b/scripts/modules/renderer.ts new file mode 100644 index 0000000..a248bce --- /dev/null +++ b/scripts/modules/renderer.ts @@ -0,0 +1,73 @@ +import { compactEntity } from "../classes/compactEntity"; +import { CompendiumBrowser } from "../compendium-browser"; + +export class Renderer { + /** + * + * @param {Array} templateStrings + */ + static async loadTemplates(templateStrings: Array) { + return await loadTemplates(templateStrings); + } + + /** + * Render the Information section + * + * @param {HTMLElement} messageElement + * @param entityType + * @param numLoaded + * @param maxLoaded + * + */ + static async renderLoading(messageElement: HTMLElement, entityType: string, numLoaded: any, numPacks: number, maxLoaded = false): Promise { + if (!messageElement) return; + + let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.hbs", { numLoaded: numLoaded, entityType: entityType, numPacks: numPacks, maxLoaded: maxLoaded }); + messageElement.innerHTML = "" + loadingHTML; + } + + /** + * Update Loading Message for the given entity + * + * @param {string} entityType + * @param {number} numLoaded + * @param {number} maxLoad + */ + static async updateLoading(entityType: string, numLoaded: number = 0, numPacks = 1, maxLoad: number = 500): Promise { + let loader = document.getElementById('CBInfoMessage'); + if (loader) { Renderer.renderLoading(loader, entityType, numLoaded, numPacks, numLoaded >= maxLoad); } + } + + /** + * + * @param {String} entityType + * @param {Boolean|null} updateLoading + * + */ + static async renderEntityList(entityList: compactEntity[], entityType: string, updateLoading: boolean | null = null): Promise { + return await renderTemplate(`modules/compendium-browser/template/entity-list.hbs`, { entityItems: entityList, entityType: entityType }); + } + + /** + * + * @param {Object} filters + * @param {String} entityType + * @returns {String} hmtl + */ + static async renderFilters(filters: object, entityType: string): Promise { + return await renderTemplate(`modules/compendium-browser/template/filter-container.hbs`, { entityType: entityType, filters: filters }); + } + + /* Hook to load the first data */ + /** + * + * @param CompendiumBrowserApp + * @param html + * @returns {Promise} + */ + static async afterRender(CompendiumBrowserApp: CompendiumBrowser, html: HTMLDocument):Promise { + if (!CompendiumBrowserApp?.refreshList) { return; } + await CompendiumBrowserApp.replaceList(html, CompendiumBrowserApp.refreshList); + CompendiumBrowserApp.refreshList = undefined; + } +} \ No newline at end of file diff --git a/scripts/modules/settings.mjs b/scripts/modules/settings.js similarity index 80% rename from scripts/modules/settings.mjs rename to scripts/modules/settings.js index b25be45..71d8749 100644 --- a/scripts/modules/settings.mjs +++ b/scripts/modules/settings.js @@ -8,10 +8,6 @@ const SETTINGS = 'settings'; export class ModuleSettings { - constructor() { - this.gs = game.settings; - } - /** * constructs and returns defaults settings */ @@ -20,7 +16,8 @@ export class ModuleSettings { loadedCompendium: { Actor: {}, Item: {}, - JournalEntry: {} + JournalEntry: {}, + RollTable: {}, } }; @@ -49,16 +46,19 @@ export class ModuleSettings { defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; } } - for (let compKey in defaultSettings.loadedNpcCompendium) { - if (settings.loadedNpcCompendium[compKey] !== undefined) { - defaultSettings.loadedNpcCompendium[compKey].load = settings.loadedNpcCompendium[compKey].load; + for (let compKey in defaultSettings.loadedActorCompendium) { + if (settings.loadedActorCompendium[compKey] !== undefined) { + defaultSettings.loadedActorCompendium[compKey].load = settings.loadedActorCompendium[compKey].load; } } + defaultSettings.allowSpellBrowser = settings.allowSpellBrowser ? true : false; defaultSettings.allowFeatBrowser = settings.allowFeatBrowser ? true : false; defaultSettings.allowItemBrowser = settings.allowItemBrowser ? true : false; - defaultSettings.allowNpcBrowser = settings.allowNpcBrowser ? true : false; - + defaultSettings.allowActorBrowser = settings.allowActorBrowser ? true : false; + defaultSettings.allowJournalEntryBrowser = settings.allowJournalEntryBrowser ? true : false; + defaultSettings.allowRollTableBrowser = settings.allowRollTableBrowser ? true : false; + if (game.user.isGM) { game.settings.set(CMPBrowser.MODULE_NAME, SETTINGS, defaultSettings); console.log("New default settings set"); @@ -68,6 +68,9 @@ export class ModuleSettings { return defaultSettings; } + /** + * Registe ther very basic settings in the game world. + */ static registerGameSettings(){ // creating game setting container game.settings.register(CMPBrowser.MODULE_NAME, SETTINGS, { @@ -77,7 +80,7 @@ export class ModuleSettings { type: Object, scope: 'world', onChange: settings => { - this.settings = settings; + game.compendiumBrowser.settings = settings; } }); @@ -99,15 +102,4 @@ export class ModuleSettings { static saveSettings() { game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings); } - - filterElements(list, subjects, filters) { - for (let element of list) { - let subject = subjects[element.dataset.entryId]; - if (this.passesFilter(subject, filters) == false) { - $(element).hide(); - } else { - $(element).show(); - } - } - } } \ No newline at end of file diff --git a/scripts/versioning/version-check.js b/scripts/versioning/version-check.js new file mode 100644 index 0000000..73f00b6 --- /dev/null +++ b/scripts/versioning/version-check.js @@ -0,0 +1,39 @@ +/** + * Version check function from Forien Unedintified item module + */ + export class VersionCheck { + static _r = false; + + static _reg(mN) { + if (this._r) return; + + game.settings.register(mN, 'version', { + name: `${mN} Version`, + default: "0.0.0", + type: String, + scope: 'client', + }); + + this._r = true; + } + + static check(mN) { + if (!this._r) this._reg(mN); + + let mV = this.get(mN); + let oV = game.settings.get(mN, "version"); + + return isNewerVersion(mV, oV); + } + + static set(mN, v) { + if (!this._r) this._reg(mN); + + game.settings.set(mN, "version", v); + } + + static get(mN) { + return game.modules.get(mN).data.version; + } + } + \ No newline at end of file diff --git a/styles/app.css b/styles/app.css new file mode 100644 index 0000000..0d96a6f --- /dev/null +++ b/styles/app.css @@ -0,0 +1,103 @@ +.compendium-browser { + overflow-y: hidden !important; + max-width: 90%; +} +.compendium-browser .info { + padding: 0.25em 0.6em; + text-align: center; + border: 1px solid #acacac; + display: inline-block; + margin: auto; + background: rgba(0, 0, 0, 0.3); + border-radius: 0.3em; + color: #cecece; +} +.compendium-browser .window-content { + overflow-y: hidden !important; + background: var(--background); + height: 100%; + padding: 0.5em 0.5em 0 0.5em; +} +.compendium-browser .window-content .parent { + height: 100%; +} +.compendium-browser .window-content .parent .content { + overflow-y: hidden !important; + height: calc(100% - 2em); +} +.compendium-browser .window-content .parent .content .tab { + overflow-y: hidden !important; + height: 100%; +} +.compendium-browser .window-content .parent .content .tab .browser { + overflow-y: hidden !important; + height: 100%; +} +.compendium-browser .window-content .parent .content .tab .browser ul { + overflow-y: auto; + height: 100%; +} +.compendium-browser .window-content .parent .content .tab .settings { + overflow-y: auto; + height: 100%; +} +.compendium-browser .tabs { + max-height: 2em; + color: var(--primary-font); + background: var(--header-background); +} +.item .compendium-browser .tabs .active { + background: var(--background); +} +.compendium-browser .tabContainer { + height: calc(100% - 2em); +} +.compendium-browser .tabContainer .tab { + width: 100%; + height: 100%; + overflow: scroll; +} +.compendium-browser .settings .settings-group { + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + font-size: 0.9em; + max-width: fit-content; + float: left; +} +.compendium-browser .settings .settings-group h3 { + margin: 0; + cursor: pointer; +} +.compendium-browser .settings .settings-group label { + display: block; +} +.compendium-browser .settings .settings-group h4 { + display: inline-block; + vertical-align: middle; + height: 100%; +} +.compendium-browser .actions { + box-sizing: content-box; + flex: 0 0 32px; + background: rgba(86, 85, 78, 0.18); + border-radius: 0 0 0.5em 0.5em; + padding: 0 0.7em; +} +.compendium-browser .actions ul { + list-style: none; + display: flex; + flex-wrap: nowrap; + flex-direction: row-reverse; +} +.compendium-browser .actions li { + box-sizing: content-box; +} +.compendium-browser .actions li button { + cursor: pointer; + color: #ededed; + background-color: #444; + border-left: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 0; +} diff --git a/styles/app.less b/styles/app.less new file mode 100644 index 0000000..570c76a --- /dev/null +++ b/styles/app.less @@ -0,0 +1,114 @@ +@import (reference) 'variables.less'; + +.compendium-browser { + overflow-y: hidden !important; + max-width: 90%; + + .info { + padding: 0.25em 0.6em; + text-align: center; + border: 1px solid #acacac; + display: inline-block; + margin: auto; + background: rgba(0,0,0,0.3); + border-radius: 0.3em; + color: #cecece; + } + .window-content { + overflow-y: hidden !important; + background: var(--background); + height: 100%; + padding: 0.5em 0.5em 0 0.5em; + .parent { + height: 100%; + .content { + overflow-y: hidden !important; + height: calc(100% - 2em); + .tab { + overflow-y: hidden !important; + height: 100%; + .browser { + overflow-y: hidden !important; + height: 100%; + ul { + overflow-y: auto; + height: 100%; + } + } + .settings { + overflow-y: auto; + height: 100%; + } + } + } + } + } + .tabs { + max-height: 2em; + color: var(--primary-font); + background: var(--header-background); + + .item & .active { + background: var(--background); + } + } + .tabContainer { + height: calc(100% - 2em); + .tab { + width: 100%; + height: 100%; + overflow: scroll; + } + } + + + + .settings { + .settings-group { + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + font-size: 0.9em; + max-width: fit-content; + float: left; + h3 { + margin: 0; + cursor: pointer; + } + label { + display: block; + } + h4 { + display: inline-block; + vertical-align: middle; + height: 100%; + } + } + } + .actions { + flex: 0 0 32px; + box-sizing: content-box; + flex: 0 0 32px; + background: rgb(86 85 78 / 18%); + border-radius: 0 0 0.5em 0.5em; + padding: 0 0.7em; + ul { + list-style: none; + display: flex; + flex-wrap: nowrap; + flex-direction: row-reverse; + } + li { + box-sizing: content-box; + button { + cursor: pointer; + color: #ededed; + background-color: #444; + border-left: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 0; + } + } + } + +} \ No newline at end of file diff --git a/styles/compendium-browser.css b/styles/compendium-browser.css index 1f12f89..47b5547 100644 --- a/styles/compendium-browser.css +++ b/styles/compendium-browser.css @@ -1,345 +1,9 @@ -#compendium .directory-footer .compendium-browser-btn { - margin-top: 5px; -} +/** +* Entity rarity colors +*/ #compendium .directory-footer { display: block; } -.compendium-browser { - overflow-y: hidden!important; - max-width: 1100px; -} - -.compendium-browser .info { - padding: 0.25em 0.6em; - text-align: center; - border: 1px solid #acacac; - display: inline-block; - margin: auto; - background: rgba(0,0,0,0.3); - border-radius: 0.3em; - color: #cecece; -} - -.compendium-browser .window-content { - overflow-y: hidden!important; - height: 100%; - padding: 0.5em 0.5em 0 0.5em; -} -.compendium-browser .window-content .parent { - height: 100%; -} -.compendium-browser .window-content .parent .content { - overflow-y: hidden!important; - height: calc(100% - 2em); -} -.compendium-browser .window-content .parent .content .tab { - overflow-y: hidden!important; - height: 100%; -} -.compendium-browser .window-content .parent .content .tab .browser { - overflow-y: hidden!important; - height: 100%; -} -.compendium-browser .window-content .parent .content .tab .browser ul { - overflow-y: auto; - height: 100%; -} -.compendium-browser .window-content .parent .content .tab .settings { - overflow-y: auto; - height: 100%; -} -.compendium-browser .tabs { - max-height: 2em; - border-bottom: solid #782e22; -} -.compendium-browser .tabContainer { - height: calc(100% - 2em); -} -.compendium-browser .tabContainer .tab { - width: 100%; - height: 100%; - overflow: scroll; -} -.compendium-browser .control-area { - position: sticky; - display: block; - min-width: 250px; - max-width: 8vw; - width: 350px; - height: 100%; - padding-right: 5px; - overflow-y: scroll; - flex-grow: 1; -} -.compendium-browser .list-area { - position: sticky; - display: flex; - min-width: 250px; - height: 100%; - padding-right: 5px; - /**overflow-y: scroll;**/ - flex-grow: 2; -} -.compendium-browser .control-area button { - background: rgba(0, 0, 0, 0.05); - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding: 2px; -} -.compendium-browser .control-area .filtercontainer { - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding: 2px; -} -.compendium-browser .control-area .filtercontainer h3 { - margin: 0; - cursor: pointer; -} -.compendium-browser .control-area .filtercontainer dl, -.compendium-browser .control-area .filtercontainer div { - margin: 5px 0; -} -.compendium-browser .control-area .filtercontainer dt { - display: inline-block; - width: 40%; - padding-left: 5px; -} -.compendium-browser .control-area .filtercontainer dd { - display: inline-block; - width: 58%; - margin-left: 0; -} -.compendium-browser .control-area .filtercontainer dd select { - width: 100%; -} -.compendium-browser .control-area .filtercontainer .multiselect { - border: 1px solid #bbb; - border-radius: 3px; - vertical-align: middle; - line-height: 32px; - margin: 2px 0; -} -.compendium-browser .control-area .filtercontainer .multiselect label { - padding: 5px; -} -.compendium-browser .control-area .filtercontainer .multiselect input { - vertical-align: middle; -} -.compendium-browser .control-area .filtercontainer .small-input { - width: calc(100% - 44px); - height: 27px; - background: rgba(0, 0, 0, 0.05); - border: 1px solid #444; - border-radius: 3px; - padding: 0 3px; - text-overflow: ellipsis; -} -.compendium-browser .control-area .filtercontainer .small-select { - width: 40px; -} -.compendium-browser .browser { - height: 100%; - overflow-y: hidden!important; -} -.compendium-browser .browser .window-content { - overflow-y: hidden!important; -} -.compendium-browser .browser .list-area ul { - float: right; - display: block; - min-width: 335px; - margin: 0; - height: 100%; - overflow: auto; - padding-left: 5px; -} -.compendium-browser .browser ul .filter-tags { - display: none; -} -.compendium-browser .browser ul li span { - white-space: nowrap; - overflow: hidden; -} -.compendium-browser .browser .spacer { - display: inline-block; - min-width: 5px; -} -.compendium-browser .browser .spacer-large { - display: inline-block; - min-width: 15px; -} -.compendium-browser .entity-browser > li { - cursor: default; - vertical-align: middle; - line-height: 32px; - margin: 2px 0; -} - -.compendium-browser .browser li:nth-child(odd) { - background: rgba(0, 0, 0, 0.05); - } - -.compendium-browser .browser li .entity-name { - padding-left: 5px; -} -.compendium-browser .entity-browser li .feat-tags, -.compendium-browser .entity-browser li .item-tags { - text-align: justify; - margin-right: 3px; - margin-left: 3px; - text-transform: capitalize; -} -.compendium-browser .spell-browser .entity .spell-level { - text-align: center; - font-weight: 900; - max-width: 18px; - height: 32px; -} -.compendium-browser .spell-browser .entity .spell-tags { - text-align: right; - margin-right: 3px; - font-weight: 900; - max-width: 100px; - height: 32px; -} - -.compendium-browser .entity-browser .entity .npc-image { - max-width: 64px; - height: 64px; -} -.compendium-browser .entity-browser .npc .npc-image img { - width: 64px; - height: 64px; - border: none; - object-fit: contain; -} - -.compendium-browser .settings .settings-group { - border: 1px solid #bbb; - border-radius: 5px; +#compendium .directory-footer .compendium-browser-btn { margin-top: 5px; - padding: 2px; - font-size: 0.9em; - max-width: fit-content; - float: left; -} -.compendium-browser .settings .settings-group h3 { - margin: 0; - cursor: pointer; -} -.compendium-browser .settings .settings-group label { - display: block; -} -.compendium-browser .settings .settings-group h4 { - display: inline-block; - vertical-align: middle; - height: 100%; -} - -.compendium-browser .actions { - flex: 0 0 32px; - box-sizing: content-box; - flex: 0 0 32px; - /* margin: 0 0 5px; */ - background: rgb(86 85 78 / 18%); - border-radius: 0 0 0.5em 0.5em; - padding: 0 0.7em; -} -.compendium-browser .actions ul { - list-style: none; - display: flex; - flex-wrap: nowrap; - flex-direction: row-reverse; -} -.compendium-browser .actions li { - box-sizing: content-box; -} -.compendium-browser .actions li button { - cursor: pointer; - color: #ededed; - background-color: #444; - border-left: 1px dotted rgba(0, 0, 0, 0.05); - border-radius: 0; -} - -.compendium-browser .cb_entities .entity { - border-left: 0.3em solid #ccc; - border-bottom: 1px dotted #ccc; - line-height: 1em; - display: grid; - grid-template-columns: 0.2fr 0.8fr 1fr; - grid-template-rows: 1fr 1fr; - gap: 0px 0px; - grid-template-areas: - "Image Name SpellTags" - "Image EntityTags MiscTags"; -} - -.compendium-browser .cb_entities ul.tags { - list-style: none; - min-width: initial; - box-sizing: content-box; - display: inline-block; -} -.compendium-browser .cb_entities .entity-tags{ - padding-left: 0.5em; - grid-area: EntityTags; -} - -.compendium-browser .cb_entities .spell-tags { grid-area: SpellTags; } -.compendium-browser .cb_entities .misc-tags { grid-area: MiscTags;} -.compendium-browser .cb_entities .tags li{ - font-size: 0.8em; - line-height: initial; - display: inline-block; - border: 1px solid #acacac; - padding: 0.2em; - border-radius: 0.2em; - line-height: initial; -} - -.compendium-browser .cb_entities .entity-image { - grid-area: Image; - width: 48px; - height: 48px; - object-fit: cover; - margin: 0; - padding: 0; -} -.compendium-browser .cb_entities .entity-name { - grid-area: Name; - font-size: 1.2em; - line-height: initial; - display: inline; -} - -.compendium-browser .cb_entities .entity.r_common {border-left: 0.3em solid #fff;} - -.compendium-browser .cb_entities .entity.r_uncommon {border-color: rgb(30,255,0);} -.compendium-browser .cb_entities .entity.r_uncommon .entity-name, -.compendium-browser .cb_entities .entity.r_uncommon .tags { - background: linear-gradient(90deg, rgba(30,255,0,1) 0%, rgba(30,255,0,0) 35%); -} -.compendium-browser .cb_entities .entity.r_rare {border-color: darkblue;} - -.compendium-browser .cb_entities .entity.r_rare .entity-name, -.compendium-browser .cb_entities .entity.r_rare .entity-info { - background: linear-gradient(90deg, rgba(0,112,221,1) 0%, rgba(0,112,221,0) 35%); -} -.compendium-browser .cb_entities .entity.r_veryrare {border-color: #a335ee;} - -.compendium-browser .cb_entities .entity.r_veryrare .item-name, -.compendium-browser .cb_entities .entity.r_veryrare entity-info { - background: linear-gradient(90deg, rgba(163,53,238,1) 0%, rgba(163,53,238,0) 35%); -} - -.compendium-browser .cb_entities .entity.r_legendary {border-color: rgb(255,128,0);} - -.compendium-browser .cb_entities .entity.r_legendary .entity-name, -.compendium-browser .cb_entities .entity.r_legendary .entity-info { - background: linear-gradient(90deg, rgba(255,128,0,1) 0%, rgba(255,128,0,0) 35%); } - -.compendium-browser .cb_entities .entity.r_rare .entity-name{color: inherit;} diff --git a/styles/compendium-browser.less b/styles/compendium-browser.less index 205ebdf..c2c1f4f 100644 --- a/styles/compendium-browser.less +++ b/styles/compendium-browser.less @@ -1,285 +1,12 @@ -#compendium .directory-footer .compendium-browser-btn { - margin-top:5px; -} +@import "variables.less"; +@import "mixins.less"; +@import "app.less"; -#compendium .directory-footer { - display:block; +#compendium { + .directory-footer { + .compendium-browser-btn { + margin-top: 5px; + } + display: block; + } } - -.compendium-browser { - overflow-y: hidden!important; - max-width:1100px; - max-height:90vh; - - .window-content { - overflow-y: hidden!important; - height: 100%; - .parent { - overflow-y: hidden!important; - height: 100%; - .content { - overflow-y: hidden!important; - height: calc(100% - 2em); - .tab { - overflow-y: hidden!important; - height: 100%; - .browser { - overflow-y: hidden!important; - height: 100%; - ul { - overflow-y:auto; - height:100%; - } - } - .settings { - overflow-y:auto; - height:100%; - } - } - } - } - } - - .tabs { - max-height:2em; - border-bottom: solid #782e22; - a { - } - } - .tabContainer { - height:calc(100% - 2em); - .tab { - width: 100%; - height: 100%; - overflow:scroll; - } - } - - .control-area { - position:sticky; - display: block; - min-width: 250px; - max-width: 400px; - width: 300px; - height:100%; - padding-right:5px; - overflow:scroll; - - button { - background: rgba(0, 0, 0, 0.05); - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding:2px; - } - - .filtercontainer { - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding:2px; - - h3 { - margin:0; - cursor:pointer; - } - - dl, div { - margin: 5px 0; - } - - dt { - display:inline-block; - width:40%; - padding-left:5px; - } - - dd { - display:inline-block; - width:58%; - margin-left:0; - - select { - width:100%; - } - } - .multiselect { - border: 1px solid #bbb; - border-radius: 3px; - vertical-align: middle; - line-height:32px; - margin:2px 0; - - label { - padding:5px; - } - input { - vertical-align: middle; - } - } - .small-input { - width: calc(100% - 44px); - height: 27px; - background: rgba(0, 0, 0, 0.05); - border: 1px solid #444; - border-radius: 3px; - padding: 0 3px; - text-overflow: ellipsis; - } - .small-select { - width: 40px; - } - } - } - - .browser { - height: 100%; - overflow-y: hidden!important; - - .window-content { - overflow-y: hidden!important; - } - - ul { - float:right; - display: block; - min-width: 335px; - width: 785px; - margin:0; - height: 100%; - overflow:auto; - padding-left: 5px; - - .filter-tags { - display:none; - } - - li { - span { - white-space: nowrap; - overflow:hidden; - } - } - - } - - .spacer { - display:inline-block; - min-width:5px; - } - .spacer-large { - display:inline-block; - min-width:15px; - } - } - - .item-browser, .feat-browser, .spell-browser { - li { - cursor:default; - vertical-align: middle; - line-height:32px; - margin:2px 0; - - .item-image { - max-width:32px; - height:32px; - } - .item-name { - height:32px; - padding-left:5px; - } - - .feat-tags, .item-tags { - text-align:right; - margin-right:3px; - margin-left: 3px; - text-transform:capitalize; - height:32px; - } - } - } - - .spell-browser { - .spell { - - .spell-level { - text-align:center; - font-weight:900; - max-width:18px; - height:32px; - } - .spell-tags { - text-align:right; - margin-right:3px; - font-weight:900; - max-width:100px; - height:32px; - } - } - } - - .npc-browser { - .npc { - cursor:default; - vertical-align: middle; - line-height:64px; - margin:4px 0; - .npc-image { - max-width: 64px; - height: 64px; - } - .npc-image img { - width: 64px; - height: 64px; - border: none; - object-fit: contain; - } - - .npc-line { - line-height: 25px; - padding: 9px 0 5px 5px; - } - .npc-name { - font-weight:bold; - font-size:16px; - } - .cr { - display: inline-block; - width: 55px; - } - .size { - display: inline-block; - width: 75px; - } - .type { - display: inline-block; - } - } - } - - .settings { - .settings-group { - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding:2px; - - h3 { - margin:0; - cursor:pointer; - } - - label { - display:block; - } - input { - - } - h4 { - display:inline-block; - vertical-align: middle; - height:100%; - } - } - } -} \ No newline at end of file diff --git a/styles/components/entity-browser.css b/styles/components/entity-browser.css new file mode 100644 index 0000000..eae4573 --- /dev/null +++ b/styles/components/entity-browser.css @@ -0,0 +1,45 @@ +.compendium-browser .browser { + display: grid; + grid-template-columns: 2em auto 2em; + grid-template-areas: "L_SIDEBAR CONTENT R_SIDEBAR"; + height: 100%; + overflow: hidden !important; +} +.compendium-browser .browser .window-content { + overflow-y: hidden !important; +} +.compendium-browser .browser ul .filter-tags { + display: none; +} +.compendium-browser .browser ul li span { + white-space: nowrap; + overflow: hidden; +} +.compendium-browser .browser .spacer { + display: inline-block; + min-width: 5px; +} +.compendium-browser .browser .spacer-large { + display: inline-block; + min-width: 15px; +} +.compendium-browser .browser.entity-browser > li { + cursor: default; +} +.compendium-browser .browser.entity-browser li .feat-tags { + text-align: justify; + margin-right: 3px; + margin-left: 3px; + text-transform: capitalize; +} +.compendium-browser .browser.entity-browser li .item-tags { + text-align: justify; + margin-right: 3px; + margin-left: 3px; + text-transform: capitalize; +} +.compendium-browser .browser .list-area { + grid-template-rows: auto; + grid-template-columns: auto; + grid-area: CONTENT; +} diff --git a/styles/components/entity-browser.less b/styles/components/entity-browser.less new file mode 100644 index 0000000..d776827 --- /dev/null +++ b/styles/components/entity-browser.less @@ -0,0 +1,183 @@ +.compendium-browser { + .browser { + display: grid; + grid-template-columns: 2em auto 2em; + grid-template-areas: "L_SIDEBAR CONTENT R_SIDEBAR"; + height: 100%; + overflow: hidden !important; + + .window-content { + overflow-y: hidden !important; + } + ul { + .filter-tags { + display: none; + } + li { + span { + white-space: nowrap; + overflow: hidden; + } + } + } + .spacer { + display: inline-block; + min-width: 5px; + } + .spacer-large { + display: inline-block; + min-width: 15px; + } + + &.entity-browser { + >li { + cursor: default; + } + li { + .feat-tags { + text-align: justify; + margin-right: 3px; + margin-left: 3px; + text-transform: capitalize; + } + .item-tags { + text-align: justify; + margin-right: 3px; + margin-left: 3px; + text-transform: capitalize; + } + } + } + .list-area { + grid-template-rows: auto; + grid-template-columns: auto; + grid-area: CONTENT; + } + } + + .cb_entities { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(15vw, 100%), 1fr)); + + .entity-header { + display: grid; + grid-template-columns: 0.55fr 0.1fr 1fr; + gap: 0px 2.5em; + text-align: center; + border: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 3px; + } + .entity { + background: var(--background); + color: darkgrey; + border-radius: 5px; + margin: 0.2em; + border: 1px solid rgba(0,0,0,0.2); + padding: 0.1em; + height: clamp(5vh,6vh,7vh); + box-shadow: 0 0 3px inset var(--light-color); + + display: grid; + grid-template-columns: 3em 1fr; + grid-template-areas: + "Image Name" + "Image Tags"; + + &:hover { + box-shadow: 0 0 3px inset var(--icon-hover); + } + .entity-image { + cursor: pointer; + background-repeat: no-repeat; + background-size: cover; + border: 1px solid rgba(83, 75, 75, 0.3); + padding: 0.1em; + border-radius: 0.5em; + justify-content: center; + align-items: center; + background-clip: border-box; + background-image: url('/icons/svg/daze.svg'); + background-size: cover; + width: 3.2em; + grid-area: Image; + box-shadow: 0 0 0.2em 0.1em inset rgba(43, 45, 46, 0.863); + } + .entity-name { + grid-area: Name; + color:rgba(236, 229, 229, 0.932); + font-size: 12px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 0.9vw; + font-weight: 400; + overflow: hidden; + height: 100%; + } + + ul.tags { + list-style: none; + min-width: initial; + box-sizing: content-box; + grid-area: Tags; + display: grid; + grid-template-columns: repeat(auto-fit, 16px); + margin: 0; + } + .tags { + .spell-tag { + color: var(--background); + &.active { + color: var(--primary-font); + } + } + li { + cursor: help; + color: darkgray; + justify-content: center; + align-items: center; + height: 1.6em; + width: 1.6em; + font-size: 0.8em; + line-height: 1.6em; + display: inline-block; + padding: 0.2em; + border-radius: 0.1em; + } + } + + &.r_common { + border-left: 0.3em solid @r_common; + .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(@r_common, 0.75); + } + } + &.r_uncommon { + border-color: rgb(61, 187, 45); + background: linear-gradient(90deg, rgba(@r_uncommon,1) 0%, rgba(163,53,238,0) 35%); + .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(@r_uncommon, 0.75); + } + } + &.r_rare { + border-color: darkblue; + background: linear-gradient(90deg, rgba(@r_rare,1) 0%, rgba(0,0,0,0.2) 35%); + .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(@r_rare, 0.75); + } + } + &.r_veryrare { + border-color: #a335ee; + background: linear-gradient(90deg, rgba(@r_veryrare,1) 0%, rgba(163,53,238,0) 35%); + .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(@r_veryrare, 0.75); + } + } + &.r_legendary { + border-color: rgb(255,128,0); + background: linear-gradient(90deg, rgba(@r_legendary,1) 0%, rgba(255,128,0,0) 35%); + .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(@r_legendary, 0.75); + } + } + } + } +} \ No newline at end of file diff --git a/styles/filters/filters.css b/styles/filters/filters.css new file mode 100644 index 0000000..9cc3d76 --- /dev/null +++ b/styles/filters/filters.css @@ -0,0 +1,113 @@ +.compendium-browser .control-area { + display: grid; + grid-template-rows: 2em 1fr; + height: 100%; + grid-template-areas: "TOGGLER" "FILTERS"; + grid-gap: 0; + width: max-content; + border-radius: 0.5em; + position: relative; + background: var(--header-background); + /* Toggler Functionality */ +} +.compendium-browser .control-area button { + background: rgba(0, 0, 0, 0.05); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; +} +.compendium-browser .control-area .toggler { + display: none; + grid-area: TOGGLER; + font-size: 0; + width: 0; + height: 0; +} +.compendium-browser .control-area .toggler:after { + display: inline-block; + height: 16px; + width: 16px; + border-radius: 3px; + background: none; + color: darkgrey; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + text-rendering: auto; + content: "\f0b0"; + font-variant: normal; +} +.compendium-browser .control-area .toggler ~ #cb_filtercontainer { + display: none; +} +.compendium-browser .control-area .toggler:checked ~ #cb_filtercontainer { + background: rgba(0, 0, 0, 0.05); + display: initial; + grid-area: FILTERS; + width: max-content; + border-radius: 0.5em 0.5em 0 0; + position: relative; +} +.compendium-browser .control-area .filtercontainer { + color: var(--primary-font); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + background: rgba(0, 0, 0, 0.75); +} +.compendium-browser .control-area .filtercontainer h3 { + margin: 0; + cursor: pointer; +} +.compendium-browser .control-area .filtercontainer dl { + margin: 0; +} +.compendium-browser .control-area .filtercontainer div { + margin: 5px 0; +} +.compendium-browser .control-area .filtercontainer dt { + display: inline-block; + width: 40%; + padding-left: 5px; + font-weight: 400; +} +.compendium-browser .control-area .filtercontainer dd { + display: inline-block; + width: 58%; + margin-left: 0; +} +.compendium-browser .control-area .filtercontainer dd select { + width: 100%; +} +.compendium-browser .control-area .filtercontainer input, +.compendium-browser .control-area .filtercontainer select { + height: 1.4em; + font-size: 0.9em; + font-weight: 400; +} +.compendium-browser .control-area .filtercontainer .multiselect { + border: 1px solid #bbb; + border-radius: 3px; + vertical-align: middle; + line-height: 32px; + margin: 2px 0; +} +.compendium-browser .control-area .filtercontainer .multiselect label { + padding: 5px; +} +.compendium-browser .control-area .filtercontainer .multiselect input { + vertical-align: middle; +} +.compendium-browser .control-area .filtercontainer .small-input { + width: calc(100% - 44px); + height: 27px; + background: rgba(0, 0, 0, 0.05); + border: 1px solid #444; + border-radius: 3px; + padding: 0 3px; + text-overflow: ellipsis; +} +.compendium-browser .control-area .filtercontainer .small-select { + width: 40px; +} diff --git a/styles/filters/filters.less b/styles/filters/filters.less new file mode 100644 index 0000000..9440b83 --- /dev/null +++ b/styles/filters/filters.less @@ -0,0 +1,119 @@ +.compendium-browser { + .control-area { + display: grid; + grid-template-rows: 2em 1fr; + height: 100%; + grid-template-areas: + "TOGGLER" + "FILTERS"; + grid-gap:0; + width: max-content; + border-radius: 0.5em; + position: relative; + background: var(--header-background); + + button { + background: rgba(0, 0, 0, 0.05); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + } + /* Toggler Functionality */ + .toggler { + display: none; + grid-area: TOGGLER; + font-size: 0; + width: 0; + height: 0; + + &:after { + display: inline-block; + height: 16px; + width: 16px; + border-radius: 3px; + background: none; + color: darkgrey; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + text-rendering: auto; + content: "\f0b0"; + font-variant: normal; + } + } + .toggler ~ #cb_filtercontainer { + display: none; + } + .toggler:checked ~ #cb_filtercontainer { + background: rgba(0, 0, 0, 0.05); + display: initial; + grid-area: FILTERS; + width: max-content; + border-radius: 0.5em 0.5em 0 0; + position: relative; + } + .filtercontainer { + color: var(--primary-font); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + background: rgba(0,0,0,0.75); + + h3 { + margin: 0; + cursor: pointer; + } + dl { + margin: 0; + } + div { + margin: 5px 0; + } + dt { + display: inline-block; + width: 40%; + padding-left: 5px; + font-weight: 400; + } + dd { + display: inline-block; + width: 58%; + margin-left: 0; + select { + width: 100%; + } + } + input, select { + height: 1.4em; + font-size: 0.9em; + font-weight: 400; + } + .multiselect { + border: 1px solid #bbb; + border-radius: 3px; + vertical-align: middle; + line-height: 32px; + margin: 2px 0; + label { + padding: 5px; + } + input { + vertical-align: middle; + } + } + .small-input { + width: calc(100% - 44px); + height: 27px; + background: rgba(0, 0, 0, 0.05); + border: 1px solid #444; + border-radius: 3px; + padding: 0 3px; + text-overflow: ellipsis; + } + .small-select { + width: 40px; + } + } + } +} \ No newline at end of file diff --git a/styles/mixins.less b/styles/mixins.less new file mode 100644 index 0000000..e69de29 diff --git a/styles/themes/README.md b/styles/themes/README.md new file mode 100644 index 0000000..7b5f179 --- /dev/null +++ b/styles/themes/README.md @@ -0,0 +1,23 @@ +# Color Schemes + +> Contributor Note: Please update this readme accordingly if you change the underlying premise. + +## Files + +Every file in this themes folder represents a color scheme. +A given color scheme should be focused on only holding + +* color variables to be used by the module in general +* compatibillity fixes for 3rd party modules that maybe dynamically inject content where the modules styles have effect. + +## Files Structure + +Every + +```less + .baseSelector { + &.themeSelector{ + //style definitions + } + } +``` \ No newline at end of file diff --git a/styles/themes/dark.css b/styles/themes/dark.css new file mode 100644 index 0000000..d6bf61e --- /dev/null +++ b/styles/themes/dark.css @@ -0,0 +1,29 @@ +.system-dnd5e.tidy5eDark { + --primary-font: rgba(255, 255, 255, 0.8); + --background: #1e1e1e; + --white: #000000; + --primary-color: rgba(255, 255, 255, 0.8); + --secondary-color: rgba(255, 255, 255, 0.65); + --tertiary-color: rgba(255, 255, 255, 0.4); + --light-color: rgba(255, 255, 255, 0.25); + --faint-color: rgba(255, 255, 255, 0.1); + --faintest-color: rgba(255, 255, 255, 0.05); + --ability-accent: darkslategrey; + --header-background: rgba(255, 255, 255, 0.05); + --header-border: rgba(255, 255, 255, 0.25); + --primary-accent: #ff6400; + --prepared: rgba(0, 250, 180, 0.3); + --always-prepared: rgba(0, 100, 255, 0.3); + --icon-background: #1e1e1e; + --icon-shadow: rgba(0, 0, 0, 0.4); + --icon-outline: rgba(0, 0, 0, 0.4); + --icon-font: rgba(255, 255, 255, 0.4); + --icon-hover: rgba(255, 255, 255, 0.8); + --warning-accent: rgba(255, 30, 0, 0.65); + --check-default: url(../images/check-dark-unchecked.svg); + --check-checked: url(../images/check-light-checked.svg); + --checkbox-font: rgba(255, 255, 255, 0.8); + --checkbox-outline: #323232; + --checkbox-unchecked: #4b4b4b; + --checkbox-checked: rgba(0, 255, 0, 0.5); +} diff --git a/styles/themes/dark.less b/styles/themes/dark.less new file mode 100644 index 0000000..c99c48c --- /dev/null +++ b/styles/themes/dark.less @@ -0,0 +1,31 @@ +.system-dnd5e { + &.tidy5eDark { + --primary-font: rgba(255, 255, 255, 0.8); + --background:rgb(30, 30, 30); + --white: rgba(0, 0, 0, 1); + --primary-color: rgba(255, 255, 255, 0.8); + --secondary-color: rgba(255, 255, 255, .65); + --tertiary-color: rgba(255, 255, 255, .4); + --light-color: rgba(255, 255, 255, .25); + --faint-color: rgba(255, 255, 255, .1); + --faintest-color: rgba(255, 255, 255, .05); + --ability-accent: darkslategrey; + --header-background: rgba(255, 255, 255, .05); + --header-border: rgba(255, 255, 255, .25); + --primary-accent: rgba(255, 100, 0, 1); + --prepared: rgba(0, 250, 180, .3); + --always-prepared: rgba(0, 100, 255, .3); + --icon-background: rgb(30, 30, 30); + --icon-shadow: rgba(0, 0, 0, .4); + --icon-outline: rgba(0, 0, 0, .4); + --icon-font:rgba(255, 255, 255, .4); + --icon-hover: rgba(255, 255, 255, 0.8); + --warning-accent: rgba(255, 30, 0, .65); + --check-default: url(../images/check-dark-unchecked.svg); + --check-checked: url(../images/check-light-checked.svg); + --checkbox-font: rgba(255, 255, 255, .8); + --checkbox-outline: rgba(50, 50, 50, 1); + --checkbox-unchecked: rgba(75,75,75,1); + --checkbox-checked: rgba(0, 255, 0, 0.5); + } +} \ No newline at end of file diff --git a/styles/themes/default.css b/styles/themes/default.css new file mode 100644 index 0000000..9e8bb99 --- /dev/null +++ b/styles/themes/default.css @@ -0,0 +1,63 @@ +.system-dnd5e { + --modesto: "Modesto Condensed", "Palatino Linotype", serif; + --signika: "Signika", sans-serif; + --primary-font: rgba(0, 0, 0, 0.9); + --background: #ece9df; + --faintest-color: rgba(0, 0, 0, 0.05); + --faint-color: rgba(0, 0, 0, 0.1); + --light-color: rgba(0, 0, 0, 0.25); + --primary-color: rgba(0, 0, 0, 0.9); + --secondary-color: rgba(0, 0, 0, 0.65); + --tertiary-color: rgba(0, 0, 0, 0.4); + --primary-accent: #ff6400; + --white: #ffffff; + --faint-white: rgba(255, 255, 255, 0.2); + --linked-accent: rgba(0, 255, 0, 0.75); + --unlinked-accent: rgba(255, 0, 0, 0.75); + --linked-light: rgba(0, 255, 0, 0.4); + --unlinked-light: rgba(255, 0, 0, 0.4); + --safe-accent: rgba(0, 150, 100, 0.6); + --unsafe-accent: rgba(255, 0, 0, 0.6); + --header-background: rgba(255, 255, 255, 0.2); + --header-border: rgba(0, 0, 0, 0.25); + --stat-font: #ece9df; + --prepareable: #778899; + --prepared: rgba(50, 205, 50, 0.3); + --prepared-outline: #32cd32; + --prepared-accent: #adff2f; + --always-prepared: rgba(0, 0, 255, 0.15); + --always-prepared-outline: #4169e1; + --always-prepared-accent: #00bfff; + --magic-accent: #ffff00; + --faint-magic-accent: rgba(255, 255, 0, 0.6); + --magic-outline: #afff2f; + --attunement-required: #cd5c5c; + --icon-attuned: rgba(0, 0, 0, 0.4); + --xp-bar: #5ee192; + --encumbrance-bar: #6c8aa5; + --encumbrance-bar-outline: #cde4ff; + --encumbrance-outline: rgba(0, 0, 0, 0.9); + --warning-accent: rgba(255, 0, 0, 0.6); + --icon-background: #ece9df; + --icon-shadow: rgba(0, 0, 0, 0.4); + --icon-outline: rgba(0, 0, 0, 0.4); + --icon-font: rgba(0, 0, 0, 0.4); + --exhaustion-font: rgba(0, 0, 0, 0.4); + --icon-hover: rgba(0, 0, 0, 0.9); + --pc-border: 0px; + --npc-border: 0px; + --vehicle-border: 0px; + --note-background: rgba(0, 0, 0, 0.9); + --exhaustion-lvl1: #ffe600; + --exhaustion-lvl2: #ff8200; + --exhaustion-lvl3: #ff3200; + --ability-accent: darkslategrey; + --context-outline: rgba(0, 0, 0, 0.4); + --context-shadow: rgba(0, 0, 0, 0.65); + --check-default: url(../images/check-dark-unchecked.svg); + --check-checked: url(../images/check-dark-checked.svg); + --checkbox-font: rgba(0, 0, 0, 0.9); + --checkbox-outline: #969696; + --checkbox-unchecked: #D8D7D1; + --checkbox-checked: rgba(0, 255, 0, 0.3); +} diff --git a/styles/themes/default.less b/styles/themes/default.less new file mode 100644 index 0000000..2b431af --- /dev/null +++ b/styles/themes/default.less @@ -0,0 +1,63 @@ +.system-dnd5e { + --modesto: "Modesto Condensed", "Palatino Linotype", serif; + --signika: "Signika", sans-serif; + --primary-font: rgba(0, 0, 0, .9); + --background: rgba(236,233,223,1); + --faintest-color: rgba(0,0,0,.05); + --faint-color: rgba(0,0,0,.1); + --light-color: rgba(0, 0, 0, .25); + --primary-color: rgba(0,0,0,.9); + --secondary-color: rgba(0,0,0,.65); + --tertiary-color: rgba(0,0,0,.4); + --primary-accent: rgba(255,100,0,1); + --white: rgba(255,255,255,1); + --faint-white: rgba(255,255,255,.2); + --linked-accent: rgba(0,255,0,.75); + --unlinked-accent: rgba(255,0,0,.75); + --linked-light: rgba(0,255,0,.4); + --unlinked-light: rgba(255,0,0,.4); + --safe-accent: rgba(0,150,100,.6); + --unsafe-accent: rgba(255,0,0,.6); + --header-background: rgba(255, 255, 255, .2); + --header-border: rgba(0, 0, 0, .25); + --stat-font: rgba(236, 233, 223, 1); + --prepareable: rgba(119,136,153,1); + --prepared: rgba(50,205,50,.3); + --prepared-outline: rgba(50,205,50,1); + --prepared-accent: rgba(173,255,47,1); + --always-prepared: rgba(0,0,255,.15); + --always-prepared-outline: rgba(65,105,225,1); + --always-prepared-accent: rgba(0,191,255,1); + --magic-accent: rgba(255,255,0,1); + --faint-magic-accent: rgba(255,255,0,.6); + --magic-outline: rgba(175,255,47,1); + --attunement-required: rgba(205,92,92,1); + --icon-attuned: rgba(0,0,0,.4); + --xp-bar: rgba(94,225,146,1); + --encumbrance-bar: rgba(108,138,165,1); + --encumbrance-bar-outline: rgba(205, 228, 255, 1); + --encumbrance-outline: rgba(0, 0, 0, .9); + --warning-accent: rgba(255, 0, 0, .6); + --icon-background: rgba(236, 233, 223, 1); + --icon-shadow: rgba(0, 0, 0, .4); + --icon-outline: rgba(0, 0, 0, .4); + --icon-font: rgba(0, 0, 0, .4); + --exhaustion-font: rgba(0, 0, 0, .4); + --icon-hover: rgba(0, 0, 0, .9); + --pc-border: 0px; + --npc-border: 0px; + --vehicle-border: 0px; + --note-background: rgba(0,0,0,.9); + --exhaustion-lvl1: rgba(255, 230, 0, 1); + --exhaustion-lvl2: rgba(255, 130, 0, 1); + --exhaustion-lvl3: rgba(255, 50, 0, 1); + --ability-accent: darkslategrey; + --context-outline: rgba(0, 0, 0, .4); + --context-shadow: rgba(0,0,0,.65); + --check-default: url(../images/check-dark-unchecked.svg); + --check-checked: url(../images/check-dark-checked.svg); + --checkbox-font: rgba(0, 0, 0, .9); + --checkbox-outline: rgba(150, 150, 150, 1); + --checkbox-unchecked: #D8D7D1; + --checkbox-checked: rgba(0, 255, 0, 0.3); +} \ No newline at end of file diff --git a/styles/variables.css b/styles/variables.css new file mode 100644 index 0000000..5a59048 --- /dev/null +++ b/styles/variables.css @@ -0,0 +1,95 @@ +/** +* Entity rarity colors +*/ +.system-dnd5e { + --modesto: "Modesto Condensed", "Palatino Linotype", serif; + --signika: "Signika", sans-serif; + --primary-font: rgba(0, 0, 0, 0.9); + --background: #ece9df; + --faintest-color: rgba(0, 0, 0, 0.05); + --faint-color: rgba(0, 0, 0, 0.1); + --light-color: rgba(0, 0, 0, 0.25); + --primary-color: rgba(0, 0, 0, 0.9); + --secondary-color: rgba(0, 0, 0, 0.65); + --tertiary-color: rgba(0, 0, 0, 0.4); + --primary-accent: #ff6400; + --white: #ffffff; + --faint-white: rgba(255, 255, 255, 0.2); + --linked-accent: rgba(0, 255, 0, 0.75); + --unlinked-accent: rgba(255, 0, 0, 0.75); + --linked-light: rgba(0, 255, 0, 0.4); + --unlinked-light: rgba(255, 0, 0, 0.4); + --safe-accent: rgba(0, 150, 100, 0.6); + --unsafe-accent: rgba(255, 0, 0, 0.6); + --header-background: rgba(255, 255, 255, 0.2); + --header-border: rgba(0, 0, 0, 0.25); + --stat-font: #ece9df; + --prepareable: #778899; + --prepared: rgba(50, 205, 50, 0.3); + --prepared-outline: #32cd32; + --prepared-accent: #adff2f; + --always-prepared: rgba(0, 0, 255, 0.15); + --always-prepared-outline: #4169e1; + --always-prepared-accent: #00bfff; + --magic-accent: #ffff00; + --faint-magic-accent: rgba(255, 255, 0, 0.6); + --magic-outline: #afff2f; + --attunement-required: #cd5c5c; + --icon-attuned: rgba(0, 0, 0, 0.4); + --xp-bar: #5ee192; + --encumbrance-bar: #6c8aa5; + --encumbrance-bar-outline: #cde4ff; + --encumbrance-outline: rgba(0, 0, 0, 0.9); + --warning-accent: rgba(255, 0, 0, 0.6); + --icon-background: #ece9df; + --icon-shadow: rgba(0, 0, 0, 0.4); + --icon-outline: rgba(0, 0, 0, 0.4); + --icon-font: rgba(0, 0, 0, 0.4); + --exhaustion-font: rgba(0, 0, 0, 0.4); + --icon-hover: rgba(0, 0, 0, 0.9); + --pc-border: 0px; + --npc-border: 0px; + --vehicle-border: 0px; + --note-background: rgba(0, 0, 0, 0.9); + --exhaustion-lvl1: #ffe600; + --exhaustion-lvl2: #ff8200; + --exhaustion-lvl3: #ff3200; + --ability-accent: darkslategrey; + --context-outline: rgba(0, 0, 0, 0.4); + --context-shadow: rgba(0, 0, 0, 0.65); + --check-default: url(../images/check-dark-unchecked.svg); + --check-checked: url(../images/check-dark-checked.svg); + --checkbox-font: rgba(0, 0, 0, 0.9); + --checkbox-outline: #969696; + --checkbox-unchecked: #D8D7D1; + --checkbox-checked: rgba(0, 255, 0, 0.3); +} +.system-dnd5e.tidy5eDark { + --primary-font: rgba(255, 255, 255, 0.8); + --background: #1e1e1e; + --white: #000000; + --primary-color: rgba(255, 255, 255, 0.8); + --secondary-color: rgba(255, 255, 255, 0.65); + --tertiary-color: rgba(255, 255, 255, 0.4); + --light-color: rgba(255, 255, 255, 0.25); + --faint-color: rgba(255, 255, 255, 0.1); + --faintest-color: rgba(255, 255, 255, 0.05); + --ability-accent: darkslategrey; + --header-background: rgba(255, 255, 255, 0.05); + --header-border: rgba(255, 255, 255, 0.25); + --primary-accent: #ff6400; + --prepared: rgba(0, 250, 180, 0.3); + --always-prepared: rgba(0, 100, 255, 0.3); + --icon-background: #1e1e1e; + --icon-shadow: rgba(0, 0, 0, 0.4); + --icon-outline: rgba(0, 0, 0, 0.4); + --icon-font: rgba(255, 255, 255, 0.4); + --icon-hover: rgba(255, 255, 255, 0.8); + --warning-accent: rgba(255, 30, 0, 0.65); + --check-default: url(../images/check-dark-unchecked.svg); + --check-checked: url(../images/check-light-checked.svg); + --checkbox-font: rgba(255, 255, 255, 0.8); + --checkbox-outline: #323232; + --checkbox-unchecked: #4b4b4b; + --checkbox-checked: rgba(0, 255, 0, 0.5); +} diff --git a/styles/variables.less b/styles/variables.less new file mode 100644 index 0000000..e629048 --- /dev/null +++ b/styles/variables.less @@ -0,0 +1,44 @@ + +/** +* Entity rarity colors +*/ +@r_common: fff; +@r_uncommon: rgba(30,255,0); +@r_rare: rgb(0,112,221); +@r_veryrare: rgb(163,53,238); +@r_legendary: rgb(255,128,0); + +@import 'themes/default.less'; +@import 'themes/dark.less'; + +.system-dnd5e { + &.tidy5eDark { + --primary-font: rgba(255, 255, 255, 0.8); + --background:rgb(30, 30, 30); + --white: rgba(0, 0, 0, 1); + --primary-color: rgba(255, 255, 255, 0.8); + --secondary-color: rgba(255, 255, 255, .65); + --tertiary-color: rgba(255, 255, 255, .4); + --light-color: rgba(255, 255, 255, .25); + --faint-color: rgba(255, 255, 255, .1); + --faintest-color: rgba(255, 255, 255, .05); + --ability-accent: darkslategrey; + --header-background: rgba(255, 255, 255, .05); + --header-border: rgba(255, 255, 255, .25); + --primary-accent: rgba(255, 100, 0, 1); + --prepared: rgba(0, 250, 180, .3); + --always-prepared: rgba(0, 100, 255, .3); + --icon-background: rgb(30, 30, 30); + --icon-shadow: rgba(0, 0, 0, .4); + --icon-outline: rgba(0, 0, 0, .4); + --icon-font:rgba(255, 255, 255, .4); + --icon-hover: rgba(255, 255, 255, 0.8); + --warning-accent: rgba(255, 30, 0, .65); + --check-default: url(../images/check-dark-unchecked.svg); + --check-checked: url(../images/check-light-checked.svg); + --checkbox-font: rgba(255, 255, 255, .8); + --checkbox-outline: rgba(50, 50, 50, 1); + --checkbox-unchecked: rgba(75,75,75,1); + --checkbox-checked: rgba(0, 255, 0, 0.5); + } + } \ No newline at end of file diff --git a/template/entity-browser-list.html b/template/entity-browser-list.html deleted file mode 100644 index 7b736c9..0000000 --- a/template/entity-browser-list.html +++ /dev/null @@ -1,60 +0,0 @@ -{{#each listItems as |entity|}} -
      • - -
        - -
        - - - -
          -
        • - {{#if entity.type}} -
        • - {{#switch entity.type}} - {{#case 'backpack'}}{{/case}} - {{#case 'consumable'}}{{/case}} - {{#case 'equipment'}}{{/case}} - {{#case 'tool'}}{{/case}} - {{#case 'loot'}}{{/case}} - {{#case 'weapon'}}{{/case}} - {{#default ''}}{{entity.type}}{{/default}} - {{/switch}} -
        • - {{/if}} - {{#if entity.dae}}
        • {{/if}} - {{#if entity.ac}}
        • {{entity.ac}}
        • {{/if}} - - {{#each entity.tags as |value name|}} -
        • - {{#ifCond name "==" "range"}}{{value}}{{/ifCond}} -
        • - {{/each}} -
        - {{#if entity.classRequirement}} -
          - {{#each entity.classRequirement as | class |}} -
        • {{class}}
        • - {{/each}} -
        - {{/if}} - {{#ifCond entity.type '==' 'spell'}} -
          -
        • {{#if entity.data.level}}{{entity.data.level}}{{else}}C{{/if}}
        • -
        • R
        • -
        • C
        • -
        • V
        • -
        • S
        • -
        • M
        • -
        - {{/ifCond}} - -
      • -{{/each}} diff --git a/template/entity-browser.hbs b/template/entity-browser.hbs new file mode 100644 index 0000000..ceb6501 --- /dev/null +++ b/template/entity-browser.hbs @@ -0,0 +1,9 @@ +
        + +
          + +
          \ No newline at end of file diff --git a/template/entity-browser.html b/template/entity-browser.html deleted file mode 100644 index b9a8916..0000000 --- a/template/entity-browser.html +++ /dev/null @@ -1,22 +0,0 @@ -
          -
          -
          -
          - -
          -
          -
          {{localize "CMPBrowser.sortBy"}}:
          -
          -
          - -
          - {{> "modules/compendium-browser/template/filter-container.html" filters=filters}} -
          -
          -
            -
            -
            \ No newline at end of file diff --git a/template/entity-list.hbs b/template/entity-list.hbs new file mode 100644 index 0000000..a3dd858 --- /dev/null +++ b/template/entity-list.hbs @@ -0,0 +1,60 @@ +{{#each entityItems as |entity|}} +
          • + + + + + +
              +
            • + {{#if entity.type}} +
            • + {{#switch entity.type}} + {{#case 'backpack'}}{{/case}} + {{#case 'consumable'}}{{/case}} + {{#case 'equipment'}}{{/case}} + {{#case 'tool'}}{{/case}} + {{#case 'loot'}}{{/case}} + {{#case 'weapon'}}{{/case}} + {{#case 'RollTable'}}{{/case}} + {{#default ''}}{{entity.type}}{{/default}} + {{/switch}} +
            • + {{/if}} + {{#if entity.dae}}
            • {{/if}} + {{#if entity.ac}}
            • {{entity.ac}}
            • {{/if}} + + {{#each entity.tags as |value name|}} +
            • + {{#if (eq name "range")}}{{value}}{{/if}} +
            • + {{/each}} +
            + {{#if entity.classRequirement}} +
              + {{#each entity.classRequirement as | class |}} +
            • + {{/each}} +
            + {{/if}} + {{#if (eq entity.type 'spell')}} +
              +
            • {{#if entity.data.level}}{{entity.data.level}}{{else}}C{{/if}}
            • +
            • R
            • +
            • C
            • +
            • V
            • +
            • S
            • +
            • M
            • +
            + {{/if}} + +
          • +{{/each}} \ No newline at end of file diff --git a/template/feat-browser.html b/template/feat-browser.html deleted file mode 100644 index 7084668..0000000 --- a/template/feat-browser.html +++ /dev/null @@ -1,24 +0,0 @@ -
            -
            -
            -
            - -
            -
            -
            {{localize "CMPBrowser.sortBy"}}:
            -
            -
            - -
            - {{> "modules/compendium-browser/template/filter-container.html" filters=featFilters}} -
            -
            -
              - {{> "modules/compendium-browser/template/entity-browser-list.html" feats=items}} -
            -
            - -
            \ No newline at end of file diff --git a/template/filter-container.hbs b/template/filter-container.hbs new file mode 100644 index 0000000..34fb781 --- /dev/null +++ b/template/filter-container.hbs @@ -0,0 +1,111 @@ + +
            +
            +
            + +
            +
            +
            {{localize "CMPBrowser.sortBy"}}:
            +
            + +
            +
            + +
            + + {{#each filters.registeredFilterCategorys as |cat key|}} +
            +

            {{cat.label}}

            +
            + {{#each cat.filters as |filter key|}} +
            + {{#if filter.is_text}} +
            {{filter.label}}
            +
            + {{#if filter.possibleValues}} + + {{else}} + + {{/if}} +
            + {{/if}} + {{#if filter.is_bool}} +
            +
            {{filter.label}}
            +
            + +
            +
            + {{/if}} + {{#if filter.is_select}} +
            +
            {{filter.label}}
            +
            + +
            +
            + {{/if}} + {{#if filter.is_multiSelect}} +
            + +
            + {{#each filter.possibleValues as |label val|}} + +
            +
            + {{/each}} +
            +
            + {{/if}} + {{#if filter.is_numberCompare}} +
            +
            {{filter.label}}
            +
            + + +
            +
            + {{/if}} +
            + {{/each}} +
            +
            + {{/each}} +
            \ No newline at end of file diff --git a/template/filter-container.html b/template/filter-container.html deleted file mode 100644 index 49f0ac1..0000000 --- a/template/filter-container.html +++ /dev/null @@ -1,79 +0,0 @@ -{{#each filters.registeredFilterCategorys as |cat key|}} -
            -

            {{cat.label}}

            -
            - {{#each cat.filters as |filter key|}} -
            - {{#if filter.istext}} -
            {{filter.label}}
            -
            - {{#if filter.possibleValues}} - - {{else}} - - {{/if}} -
            - {{/if}} - {{#if filter.isbool}} -
            -
            {{filter.label}}
            -
            - -
            -
            - {{/if}} - {{#if filter.isselect}} -
            -
            {{filter.label}}
            -
            - -
            -
            - {{/if}} - {{#if filter.ismultiSelect}} -
            - -
            - {{#each filter.possibleValues as |label val|}} - -
            -
            - {{/each}} -
            -
            - {{/if}} - {{#if filter.isnumberCompare}} -
            -
            {{filter.label}}
            -
            - - -
            -
            - {{/if}} -
            - {{/each}} -
            - -
            -{{/each}} \ No newline at end of file diff --git a/template/item-browser.html b/template/item-browser.html deleted file mode 100644 index 3e34b22..0000000 --- a/template/item-browser.html +++ /dev/null @@ -1,23 +0,0 @@ -
            -
            -
            -
            - -
            -
            -
            {{localize "CMPBrowser.sortBy"}}:
            -
            -
            - -
            - {{> "modules/compendium-browser/template/filter-container.html" filters=itemFilters}} -
            -
            -
              - {{> "modules/compendium-browser/template/entity-browser-list.html" items=items}} -
            -
            -
            \ No newline at end of file diff --git a/template/loading.hbs b/template/loading.hbs new file mode 100644 index 0000000..a7bed74 --- /dev/null +++ b/template/loading.hbs @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/template/npc-browser.html b/template/npc-browser.html deleted file mode 100644 index be0f8bf..0000000 --- a/template/npc-browser.html +++ /dev/null @@ -1,24 +0,0 @@ -
            -
            -
            -
            - -
            -
            -
            {{localize "CMPBrowser.sortBy"}}:
            -
            -
            - -
            - {{> "modules/compendium-browser/template/filter-container.html" filters=npcFilters}} -
            -
            -
              - {{> "modules/compendium-browser/template/entity-browser-list.html" npcs=npcs}} -
            -
            -
            \ No newline at end of file diff --git a/template/settings.hbs b/template/settings.hbs new file mode 100644 index 0000000..4678139 --- /dev/null +++ b/template/settings.hbs @@ -0,0 +1,66 @@ +
            +
            +

            {{localize "CMPBrowser.generalSettings"}}

            + + + + + + +
            + +
            +

            {{localize "CMPBrowser.compSettingsSpell"}}

            + {{#each settings.loadedCompendium.Item as |Comp key|}} + + {{/each}} +
            +
            +

            {{localize "CMPBrowser.compSettingsActor"}}

            + {{#each settings.loadedCompendium.Actor as |Comp key|}} + + {{/each}} +
            +
            +

            {{localize "CMPBrowser.compSettingsJournalEntry"}}

            + {{#each settings.loadedCompendium.JournalEntry as |Comp key|}} + + {{/each}} +
            +
            +

            {{localize "CMPBrowser.compSettingsRolltable"}}

            + {{#each settings.loadedCompendium.Rolltable as |Comp key|}} + + {{/each}} +
            +
            \ No newline at end of file diff --git a/template/settings.html b/template/settings.html deleted file mode 100644 index 5f71c39..0000000 --- a/template/settings.html +++ /dev/null @@ -1,49 +0,0 @@ -
            -
            -

            {{localize "CMPBrowser.generalSettings"}}

            - - - - -
            - -
            -

            {{localize "CMPBrowser.compSettingsSpell"}}

            - {{#each settings.loadedCompendium.Item as |Comp key|}} - - {{/each}} -
            -
            -

            {{localize "CMPBrowser.compSettingsNpc"}}

            - {{#each settings.loadedCompendium.Actor as |Comp key|}} - - {{/each}} -
            -
            -

            {{localize "CMPBrowser.compSettingsNpc"}}

            - {{#each settings.loadedCompendium.JournalEntry as |Comp key|}} - - {{/each}} -
            -
            \ No newline at end of file diff --git a/template/spell-browser.html b/template/spell-browser.html deleted file mode 100644 index df05ee4..0000000 --- a/template/spell-browser.html +++ /dev/null @@ -1,23 +0,0 @@ -
            -
            -
            -
            - -
            -
            -
            {{localize "CMPBrowser.sortBy"}}:
            -
            -
            - -
            - {{> "modules/compendium-browser/template/filter-container.html" filters=spellFilters}} -
            -
            -
              - {{> "modules/compendium-browser/template/entity-browser-list.html" listItems=items}} -
            -
            -
            \ No newline at end of file diff --git a/template/template.hbs b/template/template.hbs new file mode 100644 index 0000000..e4e1c61 --- /dev/null +++ b/template/template.hbs @@ -0,0 +1,28 @@ +
            +
            + {{#if showItemBrowser}}{{/if}} + {{#if showSpellBrowser}}{{/if}} + {{#if showFeatBrowser}}{{/if}} + {{#if showActorBrowser}}{{/if}} + {{#if showJournalEntryBrowser}}{{/if}} + {{#if showRollTableBrowser}}{{/if}} + {{#if isGM}}{{/if}} +
            + +
            + {{#if showSpellBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Spell}}
            {{/if}} + {{#if showFeatBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Feat}}
            {{/if}} + {{#if showItemBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Item}}
            {{/if}} + {{#if showActorBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Actor}}
            {{/if}} + {{#if showJournalEntryBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.JournalEntry}}
            {{/if}} + {{#if showRollTableBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.RollTable}}
            {{/if}} + {{#if isGM}}
            {{> "modules/compendium-browser/template/settings.hbs"}}
            {{/if}} +
            +
            \ No newline at end of file diff --git a/template/template.html b/template/template.html deleted file mode 100644 index a0810ef..0000000 --- a/template/template.html +++ /dev/null @@ -1,26 +0,0 @@ -
            -
            - {{#if showSpellBrowser}}🧚{{/if}} - {{#if showFeatBrowser}}{{localize "CMPBrowser.Tab.FeatBrowser"}}{{/if}} - {{#if showItemBrowser}}🔮{{/if}} - {{#if showNpcBrowser}}👥{{/if}} - {{#if isGM}}⚙️{{/if}} -
            - -
            -
            {{#if showSpellBrowser}}{{> "modules/compendium-browser/template/entity-browser.html"}}{{/if}}
            -
            {{#if showFeatBrowser}}{{> "modules/compendium-browser/template/entity-browser.html"}}{{/if}}
            -
            {{#if showItemBrowser}}{{> "modules/compendium-browser/template/entity-browser.html"}}{{/if}}
            -
            {{#if showNpcBrowser}} {{> "modules/compendium-browser/template/entity-browser.html"}}{{/if}}
            - {{#if isGM}} -
            {{> "modules/compendium-browser/template/settings.html"}}
            - {{/if}} -
            -
            \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..906acfa --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,47 @@ +{ + "compilerOptions": { + "module": "es2020", + "moduleResolution": "node", + "target": "es2020", + "lib": [ + "es2020", + "es6", + "es5", + "dom" + ], + "outDir": "dist", + "rootDir": "./", + "sourceMap": false, + "experimentalDecorators": true, + "strict": true, + "strictNullChecks": true, + "esModuleInterop": false, + "types": [ + "foundry-pc-types", + "node", + ], + "typeRoots": [ + "C:\\Users\\jackp\\AppData\\Local\\FoundryVTT\\Data\\systems\\dnd5e\\module" + ], + "allowJs": true, + "resolveJsonModule": true + }, + "typedocOptions": { + "entryPoints": ["./compendium-browser.js"], + "out": "docs" + }, + "include": [ + "compendium-browser.js", + "module.json", + "*/**/*", + "./*/*.json", + "./template/*" + ], + "exclude": [ + "node_modules", + ".vscode", + "dist", + "docs", + "out" + ] +} \ No newline at end of file From c5548c8c95cf5d513e23b75b8e16d0b9904fffbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20B=C3=B6ttner?= Date: Sun, 10 Oct 2021 13:51:14 +0200 Subject: [PATCH 04/13] Module Refactored with a more streamlined process. --- Patchnotes.md | 19 + data/filter/spellfilter.json | 15 - dist/hooks/events.js | 50 ++- dist/lang/de.json | 50 ++- dist/lang/en.json | 15 +- dist/module.json | 8 +- dist/scripts/classes/decoratedEntity.js | 2 + dist/scripts/compendium-browser.js | 23 +- dist/scripts/modules/entities.js | 6 +- dist/scripts/modules/filter.js | 23 +- dist/scripts/modules/settings.js | 13 +- dist/styles/compendium-browser.css | 423 ++++++++++++------ dist/styles/compendium-browser.min.css | 2 +- dist/template/entity-browser.hbs | 6 +- dist/template/entity-list.hbs | 6 +- dist/template/export.hbs | 18 + dist/template/filter-container.hbs | 2 +- dist/template/settings.hbs | 56 +-- dist/template/template.hbs | 64 ++- docs/Filter.html | 16 +- docs/VersionCheck.html | 2 +- docs/dist_hooks_events.js.html | 52 ++- docs/dist_hooks_moduleHooks.js.html | 2 +- docs/dist_scripts_classes_compactList.js.html | 2 +- ...st_scripts_classes_decoratedEntity.js.html | 4 +- docs/dist_scripts_classes_filter.js.html | 2 +- docs/dist_scripts_compendium-browser.js.html | 25 +- docs/dist_scripts_modules_entities.js.html | 8 +- docs/dist_scripts_modules_exporter.js.html | 2 +- docs/dist_scripts_modules_filter.js.html | 25 +- docs/dist_scripts_modules_renderer.js.html | 2 +- docs/dist_scripts_modules_settings.js.html | 15 +- ...t_scripts_versioning_version-check.js.html | 2 +- docs/hooks_events.js.html | 63 ++- docs/hooks_moduleHooks.js.html | 2 +- docs/index.html | 2 +- docs/module.exports.html | 2 +- docs/scripts_compendium-browser.js.html | 26 +- docs/scripts_modules_settings.js.html | 15 +- docs/scripts_versioning_version-check.js.html | 2 +- hooks/events.js | 61 ++- lang/de.json | 50 ++- lang/en.json | 15 +- module.json | 8 +- package.json | 1 + scripts/classes/decoratedEntity.ts | 2 + scripts/compendium-browser.js | 24 +- scripts/modules/entities.ts | 5 +- scripts/modules/filter.ts | 29 +- scripts/modules/settings.js | 13 +- styles/app.css | 311 ++++++++++++- styles/app.less | 15 +- styles/components/entity-browser.css | 130 +++++- styles/components/entity-browser.less | 253 ++++++----- styles/components/sidebars.css | 166 +++++++ .../filters.less => components/sidebars.less} | 113 +++-- styles/filters/filters.css | 113 ----- styles/mixins.css | 0 styles/mixins.less | 3 + styles/themes/dark.css | 3 + styles/themes/dark.less | 8 + styles/themes/default.less | 68 +-- styles/variables.css | 3 + styles/variables.less | 39 +- template/entity-browser.hbs | 6 +- template/entity-list.hbs | 6 +- template/export.hbs | 18 + template/filter-container.hbs | 2 +- template/settings.hbs | 56 +-- template/template.hbs | 64 ++- 70 files changed, 1808 insertions(+), 849 deletions(-) delete mode 100644 data/filter/spellfilter.json create mode 100644 dist/template/export.hbs create mode 100644 styles/components/sidebars.css rename styles/{filters/filters.less => components/sidebars.less} (50%) delete mode 100644 styles/filters/filters.css create mode 100644 styles/mixins.css create mode 100644 template/export.hbs diff --git a/Patchnotes.md b/Patchnotes.md index 8336b40..373f868 100644 --- a/Patchnotes.md +++ b/Patchnotes.md @@ -1,3 +1,22 @@ +#v0.6.0 +- new: Export to a world/compendium rolltable +- new: UI inspired by TidySheet5e +- new: Light (default) and dark mode color scheme +- new: the source files are structured to give developers a cleaner environment + - most parts of the module are now TypeScript + - `hooks` for event related js + - `scripts` as the base folder for app related js + - `data` holding json files with data used to filter results + +- added: JournalEntry Browser +- added: RollTable Browser +- added: code documentation +- added: user documentation +- added: german translation + +- changed: Used native JS calls instead of jQuery wherever possible +- changed: The style definitions are now written in less + #v0.5.0 - Fixed: Issue #17 (error in filtering NPCs by Creature Type) diff --git a/data/filter/spellfilter.json b/data/filter/spellfilter.json deleted file mode 100644 index 82be27a..0000000 --- a/data/filter/spellfilter.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "filters": { - "general": [ - {"label": "DND5E.Source","path": "data.source","type": "text"} - {"label": "CMPBrowser.lvl" ,"path": "data.level","type": "multiSelect", "value": "['CMPBrowser.cantrip', 1, 2, 3, 4, 5, 6, 7, 8, 9]"} - ], - "components": [ - {"label": "CMPBrowser.ritual", "path": "data.components.ritual", "type": "boolean"}, - {"label": "CMPBrowser.concentration", "path": "data.components.concentration", "type": "boolean"}, - {"label": "CMPBrowser.vocal", "path": "data.components.vocal", "type": "boolean"}, - {"label": "CMPBrowser.somatic", "path": "data.components.somatic", "type": "boolean"}, - {"label": "CMPBrowser.material", "path": "data.components.material", "type": "boolean"} - ] - } -} \ No newline at end of file diff --git a/dist/hooks/events.js b/dist/hooks/events.js index f040d7e..e466f90 100644 --- a/dist/hooks/events.js +++ b/dist/hooks/events.js @@ -44,13 +44,17 @@ export class Events { }); }); } + /** + * + * @param {HTMLCollection} app + */ static async activateItemListListeners(app = document.getElementsByClassName('window-app')) { app = app[0]; // open entity sheet on click app.querySelectorAll('*[data-action="openSheet"]').forEach(async (el) => { el.addEventListener('click', async (e) => { - let itemId = e.currentTarget.parentNode.dataset.entryId; - let compendium = e.currentTarget.parentNode.dataset.entryCompendium; + let itemId = e.currentTarget.parentNode.dataset.entityId; + let compendium = e.currentTarget.parentNode.dataset.entityCompendium; let pack = game.packs.find(p => p.collection === compendium); await pack.getEntity(itemId).then(entity => { entity.sheet.render(true); @@ -58,24 +62,42 @@ export class Events { }); }); // make draggable - //0.4.1: Avoid the game.packs lookup app.querySelectorAll('.draggable').forEach(async (li) => { li.setAttribute("draggable", true); li.addEventListener('dragstart', event => { - let packName = li.getAttribute("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === packName); - if (!pack) { - event.preventDefault(); - return false; - } event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.entity, - pack: pack.collection, - id: li.getAttribute("data-entry-id") + type: li.dataset.entityType, + pack: li.dataset.entityCompendium, + id: li.dataset.entityId, + name: li.dataset.entityName })); }, false); }); } + /** + * + * @param {HTML|undefined} app + */ + static async registerDropTarget(app = document.getElementsByClassName('window-app')) { + app = app[0]; + let dropTarget = app.querySelector('.droptarget'); + dropTarget.addEventListener('dragover', async (e) => { + e.preventDefault(); + dropTarget.style.border = '1px solid red'; + }); + dropTarget.addEventListener('drop', async (e) => { + e.preventDefault(); + let newEntry = JSON.parse(e.dataTransfer.getData("text/plain")); + let row = dropTarget.insertRow(); + let compendium = row.insertCell(0); + let name = row.insertCell(1); + name.appendChild(document.createTextNode(newEntry.name)); + compendium.appendChild(document.createTextNode(newEntry.pack)); + row.dataset.id = newEntry.id; + row.dataset.pack = newEntry.pack; + row.dataset.type = newEntry.type; + }); + } static async observeListElement(list, tag) { for (let element of list.getElementsByTagName(tag)) { game.compendiumBrowser.observer.observe(element); @@ -85,6 +107,7 @@ export class Events { app = app[0]; // toggle visibility of filter containers html.find('.filtercontainer h3, .multiselect label').click(async (ev) => { + ev.target.classList.toggle('opened'); await $(ev.target.nextElementSibling).toggle(100); }); html.find('.multiselect label').trigger('click'); @@ -119,6 +142,9 @@ export class Events { case 'allow-rolltable-browser': game.compendiumBrowser.settings.allowRollTableBrowser = value; break; + case 'allow-scene-browser': + game.compendiumBrowser.settings.allowSceneBrowser = value; + break; case 'allow-journalentry-browser': game.compendiumBrowser.settings.allowJournalEntryBrowser = value; break; diff --git a/dist/lang/de.json b/dist/lang/de.json index 27e8aac..970c76c 100644 --- a/dist/lang/de.json +++ b/dist/lang/de.json @@ -3,25 +3,27 @@ "CMPBrowser.sortBy": "Sortieren nach", "CMPBrowser.cr": "Challenge Rating", "CMPBrowser.generalSettings": "Allgemeine Einstellungen", - "CMPBrowser.allowSpellAcc": "Erlaube Spielern Zauber zu durchsuchen.", - "CMPBrowser.allowActorAcc": "Player access to actor browser", - "CMPBrowser.allowRolltable": "Player access to rolltable browser", - "CMPBrowser.allowJournalEntry": "Player Access to the JournalEntry browser", + "CMPBrowser.allowSpellAcc": "Zauber", + "CMPBrowser.allowFeatAcc": "Feats", + "CMPBrowser.allowActorAcc": "Akteure", + "CMPBrowser.allowRolltable": "Rolltables", + "CMPBrowser.allowScene": "Szenen", + "CMPBrowser.allowJournalEntry": "Journal Einträge", "CMPBrowser.compSettingsSpell": "Gegenstand Kompendium Einstellungen", - "CMPBrowser.compSettingsNpc": "Actor Kompendium Einstellungen", - "CMPBrowser.Filters.ResetFilters": "Reset Filters", - "CMPBrowser.Tab.SpellBrowser": "Zauber Browser", - "CMPBrowser.Tab.FeatBrowser": "Feat Browser", - "CMPBrowser.Tab.ItemBrowser": "Gegenstand Browser", - "CMPBrowser.Tab.NPCBrowser": "NPC Browser", - "CMPBrowser.Tab.ActorBrowser": "Actor Browser", - "CMPBrowser.Tab.RolltableBrowser": "Rolltable Browser", - "CMPBrowser.Tab.JournalEntryBrowser": "JournalEntry Browser", + "CMPBrowser.compSettingsNpc": "Akteur Kompendium Einstellungen", + "CMPBrowser.Filters.ResetFilters": "Filter zurücksetzen", + "CMPBrowser.Tab.SpellBrowser": "Zauber", + "CMPBrowser.Tab.FeatBrowser": "Feats", + "CMPBrowser.Tab.ItemBrowser": "Gegenstände", + "CMPBrowser.Tab.ActorBrowser": "Akteure", + "CMPBrowser.Tab.RolltableBrowser": "Rolltables", + "CMPBrowser.Tab.SceneBrowser": "Szenen", + "CMPBrowser.Tab.JournalEntryBrowser": "Journal Einträge", "CMPBrowser.Tab.Settings": "Einstellungen", - "CMPBrowser.SETTING.Maxload.NAME": "Maximum load", - "CMPBrowser.SETTING.Maxload.HINT": "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", - "CMPBrowser.LOADING.Message": "Geladen: {numLoaded} {entityType}s ({numPacks} Kompendia)", - "CMPBrowser.LOADING.MaxLoaded": "(maximum displayed; to see more, use the filters)", + "CMPBrowser.SETTING.Maxload.NAME": "Maximum Resultate", + "CMPBrowser.SETTING.Maxload.HINT": "Maximale Anzahl an Resultaten für jeden Typ. Bitte über Filter die Suche eingrenzen. Diese Einstellung erlaubt Speicher und Server performance zu optimieren.", + "CMPBrowser.LOADING.Message": "Geladen: {numLoaded} {entityType}s (aus {numPacks} Kompendia)", + "CMPBrowser.LOADING.MaxLoaded": "(Maximale Resultate, Bitte die Suche eingrenzen.)", "CMPBrowser.load": "Laden", "CMPBrowser.lvl": "Level", "CMPBrowser.ritual": "Ritual", @@ -55,17 +57,17 @@ "CMPBrowser.aberration": "Aberration", "CMPBrowser.beast": "Beast", "CMPBrowser.celestial": "Göttlich", - "CMPBrowser.construct": "construct", + "CMPBrowser.construct": "Konstrukt", "CMPBrowser.dragon": "Drachen", - "CMPBrowser.elemental": "Elemental", - "CMPBrowser.fey": "Fey", + "CMPBrowser.elemental": "Elementar", + "CMPBrowser.fey": "Fee", "CMPBrowser.fiend": "Fiend", - "CMPBrowser.giant": "Giant", + "CMPBrowser.giant": "Riesen", "CMPBrowser.humanoid": "Humanoid", - "CMPBrowser.monstrosity": "Monstrosity", + "CMPBrowser.monstrosity": "Monstrosität", "CMPBrowser.ooze": "Ooze", - "CMPBrowser.plant": "Plant", - "CMPBrowser.undead": "Undead", + "CMPBrowser.plant": "Pflanze", + "CMPBrowser.undead": "Untot", "CMPBrowser.abilities": "Fähigkeiten", "CMPBrowser.dmgInteraction": "Damage Interaction", "CMPBrowser.dmgDealt": "Damage Dealt", diff --git a/dist/lang/en.json b/dist/lang/en.json index 748d7d2..a51f5d7 100644 --- a/dist/lang/en.json +++ b/dist/lang/en.json @@ -3,12 +3,14 @@ "CMPBrowser.sortBy": "Sort by", "CMPBrowser.cr": "Challenge Rating", "CMPBrowser.generalSettings": "General Settings", - "CMPBrowser.allowSpellAcc": "Players access to the Spell browser", - "CMPBrowser.allowActorAcc": "Player access to Actor browser", - "CMPBrowser.allowRolltable": "Player access to RollTable browser", - "CMPBrowser.allowJournalEntry": "Player Access to the JournalEntry browser", - "CMPBrowser.compSettingsSpell": "Item Compendium Settings", - "CMPBrowser.compSettingsNpc": "Actor Compendium Settings", + "CMPBrowser.allowSpellAcc": "Spells", + "CMPBrowser.allowActorAcc": "Actors", + "CMPBrowser.allowFeatAcc": "Feats", + "CMPBrowser.allowRolltable": "RollTable", + "CMPBrowser.allowJournalEntry": "JournalEntry", + "CMPBrowser.allowSceneBrowser": "Scenes", + "CMPBrowser.compSettingsSpell": "Items", + "CMPBrowser.compSettingsNpc": "Actors", "CMPBrowser.load": "Load", "CMPBrowser.lvl": "Level", "CMPBrowser.ritual": "Ritual", @@ -62,6 +64,7 @@ "CMPBrowser.Tab.FeatBrowser": "Feats", "CMPBrowser.Tab.ItemBrowser": "Items", "CMPBrowser.Tab.RollTableBrowser": "Rolltables", + "CMPBrowser.Tab.SceneBrowser": "Scenes", "CMPBrowser.Tab.JournalEntryBrowser": "JournalEntries", "CMPBrowser.Tab.Settings": "Settings", "CMPBrowser.SETTING.Maxload.NAME": "Maximum load", diff --git a/dist/module.json b/dist/module.json index 51d77a4..0983c84 100644 --- a/dist/module.json +++ b/dist/module.json @@ -2,9 +2,9 @@ "name": "compendium-browser", "title": "Compendium Browser", "description": "

            Easily browse and filter spells, feats, items, and npcs loaded from compendia!

            NEW! Compendium Browser is faster and better-behaved; it no longer loads all the compendia into memory on start-up (which sometimes hung servers because of memory or CPU requirements). Instead, it filters and loads on-demand, as well as giving you a Module Setting to control how many rows are loaded at a time.
            Changes in v0.5.0:Fixed: Issue #17: Error in Foundry 0.8.x when filtering NPC by Creature Type
            Changes in v0.4.5:
            • Fixed: Spells from non-system compendium show up in items tab.(Issue #10)
            • Added: Show compendium source in results (Issue #11)
            ", - "version": "0.5.2", + "version": "0.7.9", "minimumCoreVersion": "0.6.2", - "compatibleCoreVersion": "0.8.8", + "compatibleCoreVersion": "0.8.9", "allowBugReporter": true, "author": "Spetzel#0103", "authors": [ @@ -24,8 +24,8 @@ "dnd5e" ], "includes": [ - "./hooks/**", - "./scripts/**" + "./hooks/*.js", + "./scripts/*.js" ], "esmodules": [ "./compendium-browser.js" diff --git a/dist/scripts/classes/decoratedEntity.js b/dist/scripts/classes/decoratedEntity.js index d9a9253..d34a387 100644 --- a/dist/scripts/classes/decoratedEntity.js +++ b/dist/scripts/classes/decoratedEntity.js @@ -158,6 +158,8 @@ export class decoratedEntity extends compactEntity { decorated.classRequirement = classes.split(','); } break; + case 'Scene': + decorated.data.dimensions = entityData.height + ' * ' + entityData.width; } return decorated; } diff --git a/dist/scripts/compendium-browser.js b/dist/scripts/compendium-browser.js index d725a1d..d59f65c 100644 --- a/dist/scripts/compendium-browser.js +++ b/dist/scripts/compendium-browser.js @@ -42,7 +42,8 @@ export class CompendiumBrowser extends Application { "modules/compendium-browser/template/entity-list.hbs", "modules/compendium-browser/template/filter-container.hbs", "modules/compendium-browser/template/settings.hbs", - "modules/compendium-browser/template/loading.hbs" + "modules/compendium-browser/template/loading.hbs", + "modules/compendium-browser/template/export.hbs" ]); this.filters.addEntityFilters(); this.hookCompendiumList(); @@ -65,18 +66,20 @@ export class CompendiumBrowser extends Application { items: [], actors: [], filters: { - Spell: this.filters.getByName('Spell'), - Item: this.filters.getByName('Item'), Actor: this.filters.getByName('Actor'), + Item: this.filters.getByName('Item'), + JournalEntry: this.filters.getByName('JounralEntry'), RollTable: this.filters.getByName('RollTable'), - JournalEntry: this.filters.getByName('RollTable') + Spell: this.filters.getByName('Spell'), + Scene: this.filters.getByName('Scene'), }, - showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, + showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, showFeatBrowser: (game.user.isGM) || this.settings.allowFeatBrowser, showItemBrowser: (game.user.isGM) || this.settings.allowItemBrowser, - showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, - showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser, showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser, + showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser, + showSceneBrowser: (game.user.isGM) || this.settings.allowSceneBrowser, + showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, settings: this.settings, isGM: game.user.isGM }; @@ -103,6 +106,7 @@ export class CompendiumBrowser extends Application { Events.activateActionListener(html); Events.activateItemListListeners(html); Events.activateFilterListeners(html); + Events.registerDropTarget(html); //Just for the loading image if (this.observer) { html.find(".entity-image").each((i, imageElement) => this.observer.observe(imageElement)); @@ -145,6 +149,9 @@ export class CompendiumBrowser extends Application { else if (this.settings.allowRollTableBrowser) { this.refreshList = "RollTable"; } + else if (this.settings.allowRollTableBrowser) { + this.refreshList = "Scene"; + } this.render(true); }); } @@ -158,7 +165,7 @@ export class CompendiumBrowser extends Application { async replaceList(html, entityType, options = { reload: true }) { //After rendering the first time or re-rendering trigger the load/reload of visible data let entityListElement = document.querySelector('.tab.active .browser .cb_entities'); - if (entityListElement.childElementCount !== undefined) { + if (entityListElement && entityListElement.childElementCount !== undefined) { //0.4.2b: On a tab-switch, only reload if there isn't any data already if (options?.reload || entityListElement.childElementCount < 1) { const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; diff --git a/dist/scripts/modules/entities.js b/dist/scripts/modules/entities.js index ecc0523..784d734 100644 --- a/dist/scripts/modules/entities.js +++ b/dist/scripts/modules/entities.js @@ -1,5 +1,4 @@ import { Filter } from './filter.js'; -import { Renderer } from './renderer.js'; import { CMPBrowser } from './settings.js'; import { compactEntity } from '../classes/compactEntity.js'; import { decoratedEntity } from '../classes/decoratedEntity.js'; @@ -85,12 +84,14 @@ export class Entities { compact.orderSize = decorated.filterSize; compact.data.details = decorated.data.details; break; + case "Scene": + compact.img = currentEntity.data.thumb; default: break; } comp_list.addEntity(compact); if (updateLoading) { - Renderer.updateLoading(entityType, numItemsLoaded, numPacks, 500); + ui.notifications.info(`Loaded ${numItemsLoaded} ${entityType}s from ${numPacks} Compendia.`); } if (numItemsLoaded++ >= maxLoad) break; @@ -141,6 +142,7 @@ export class Entities { ['Feats', 'data.class'], ['RollTable', 'compendium'], ['JounralEntry', 'name'], + ['Scene', 'name'], ]); list.entities.sort((left, right) => { let sort = defaultSort.get(entityType) || '', result = SortCollator.compare(getProperty(left, sort), getProperty(right, sort)); diff --git a/dist/scripts/modules/filter.js b/dist/scripts/modules/filter.js index 0f2d027..b84f760 100644 --- a/dist/scripts/modules/filter.js +++ b/dist/scripts/modules/filter.js @@ -1,12 +1,13 @@ import { Filter as FilterEntity } from "../classes/filter.js"; export class Filter { constructor() { - this.Spell = this._getInitialFilters(); this.Actor = this._getInitialFilters(); this.Feat = this._getInitialFilters(); this.Item = this._getInitialFilters(); - this.RollTable = this._getInitialFilters(); this.JournalEntry = this._getInitialFilters(); + this.RollTable = this._getInitialFilters(); + this.Scene = this._getInitialFilters(); + this.Spell = this._getInitialFilters(); } /** * @@ -102,12 +103,13 @@ export class Filter { return getProperty(this, entityType); } resetFilters() { - this.Spell.activeFilters = {}; + this.Actor.activeFilters = {}; this.Feat.activeFilters = {}; this.Item.activeFilters = {}; - this.Actor.activeFilters = {}; - this.RollTable.activeFilters = {}; this.JournalEntry.activeFilters = {}; + this.RollTable.activeFilters = {}; + this.Scene.activeFilters = {}; + this.Spell.activeFilters = {}; } /** * @@ -128,6 +130,7 @@ export class Filter { await this.addItemFilters(); await this.addActorFilters(); await this.addRollTableFilters(); + await this.addSceneFilters(); } /** * Used to add custom filters to the Spell-Browser @@ -301,4 +304,14 @@ export class Filter { story: "Story", }); } + async addSceneFilters() { + const SCENE = 'Scene'; + this.addFilter(SCENE, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.Scene.Dimensions"), 'data', 'select', { + none: "FoundryVTT default", + small: "small", + medium: "medium", + large: "large", + kkkk: "4K" + }); + } } diff --git a/dist/scripts/modules/settings.js b/dist/scripts/modules/settings.js index 703222a..23a0e1b 100644 --- a/dist/scripts/modules/settings.js +++ b/dist/scripts/modules/settings.js @@ -15,6 +15,7 @@ export class ModuleSettings { Item: {}, JournalEntry: {}, RollTable: {}, + Scene: {} } }; for (let compendium of game.packs) { @@ -35,14 +36,9 @@ export class ModuleSettings { let defaultSettings = ModuleSettings._getDefaults(); // load settings from container and apply to default settings (available compendia might have changed) let settings = game.settings.get(CMPBrowser.MODULE_NAME, SETTINGS); - for (let compKey in defaultSettings.loadedSpellCompendium) { - if (settings.loadedSpellCompendium[compKey] !== undefined) { - defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; - } - } - for (let compKey in defaultSettings.loadedActorCompendium) { - if (settings.loadedActorCompendium[compKey] !== undefined) { - defaultSettings.loadedActorCompendium[compKey].load = settings.loadedActorCompendium[compKey].load; + for (let compKey in defaultSettings.loadedCompendium) { + if (settings.loadedCompendium[compKey] !== undefined) { + defaultSettings.loadedCompendium[compKey].load = settings.loadedCompendium[compKey].load; } } defaultSettings.allowSpellBrowser = settings.allowSpellBrowser ? true : false; @@ -51,6 +47,7 @@ export class ModuleSettings { defaultSettings.allowActorBrowser = settings.allowActorBrowser ? true : false; defaultSettings.allowJournalEntryBrowser = settings.allowJournalEntryBrowser ? true : false; defaultSettings.allowRollTableBrowser = settings.allowRollTableBrowser ? true : false; + defaultSettings.allowRollTableBrowser = settings.allowSceneBrowser ? true : false; if (game.user.isGM) { game.settings.set(CMPBrowser.MODULE_NAME, SETTINGS, defaultSettings); console.log("New default settings set"); diff --git a/dist/styles/compendium-browser.css b/dist/styles/compendium-browser.css index a58b71b..a121fd7 100644 --- a/dist/styles/compendium-browser.css +++ b/dist/styles/compendium-browser.css @@ -1,8 +1,103 @@ -#compendium .directory-footer { - display: block; -} -#compendium .directory-footer .compendium-browser-btn { - margin-top: 5px; +/** +* Entity rarity colors +*/ +/** +* Color Themes +**/ +.system-dnd5e { + --modesto: "Modesto Condensed", "Palatino Linotype", serif; + --signika: "Signika", sans-serif; + --primary-font: rgba(0, 0, 0, 0.9); + --background: #ece9df; + --faintest-color: rgba(0, 0, 0, 0.05); + --faint-color: rgba(0, 0, 0, 0.1); + --light-color: rgba(0, 0, 0, 0.25); + --primary-color: rgba(0, 0, 0, 0.9); + --secondary-color: rgba(0, 0, 0, 0.65); + --tertiary-color: rgba(0, 0, 0, 0.4); + --primary-accent: #ff6400; + --white: #ffffff; + --faint-white: rgba(255, 255, 255, 0.2); + --linked-accent: rgba(0, 255, 0, 0.75); + --unlinked-accent: rgba(255, 0, 0, 0.75); + --linked-light: rgba(0, 255, 0, 0.4); + --unlinked-light: rgba(255, 0, 0, 0.4); + --safe-accent: rgba(0, 150, 100, 0.6); + --unsafe-accent: rgba(255, 0, 0, 0.6); + --header-background: rgba(255, 255, 255, 0.2); + --header-border: rgba(0, 0, 0, 0.25); + --stat-font: #ece9df; + --prepareable: #778899; + --prepared: rgba(50, 205, 50, 0.3); + --prepared-outline: #32cd32; + --prepared-accent: #adff2f; + --always-prepared: rgba(0, 0, 255, 0.15); + --always-prepared-outline: #4169e1; + --always-prepared-accent: #00bfff; + --magic-accent: #ffff00; + --faint-magic-accent: rgba(255, 255, 0, 0.6); + --magic-outline: #afff2f; + --attunement-required: #cd5c5c; + --icon-attuned: rgba(0, 0, 0, 0.4); + --xp-bar: #5ee192; + --encumbrance-bar: #6c8aa5; + --encumbrance-bar-outline: #cde4ff; + --encumbrance-outline: rgba(0, 0, 0, 0.9); + --warning-accent: rgba(255, 0, 0, 0.6); + --icon-background: #ece9df; + --icon-shadow: rgba(0, 0, 0, 0.4); + --icon-outline: rgba(0, 0, 0, 0.4); + --icon-font: rgba(0, 0, 0, 0.4); + --exhaustion-font: rgba(0, 0, 0, 0.4); + --icon-hover: rgba(0, 0, 0, 0.9); + --pc-border: 0px; + --npc-border: 0px; + --vehicle-border: 0px; + --note-background: rgba(0, 0, 0, 0.9); + --exhaustion-lvl1: #ffe600; + --exhaustion-lvl2: #ff8200; + --exhaustion-lvl3: #ff3200; + --ability-accent: darkslategrey; + --context-outline: rgba(0, 0, 0, 0.4); + --context-shadow: rgba(0, 0, 0, 0.65); + --check-default: url(../images/check-dark-unchecked.svg); + --check-checked: url(../images/check-dark-checked.svg); + --checkbox-font: rgba(0, 0, 0, 0.9); + --checkbox-outline: #969696; + --checkbox-unchecked: #D8D7D1; + --checkbox-checked: rgba(0, 255, 0, 0.3); +} +.system-dnd5e.tidy5eDark { + --primary-font: rgba(255, 255, 255, 0.8); + --background: #1e1e1e; + --white: #000000; + --primary-color: rgba(255, 255, 255, 0.8); + --secondary-color: rgba(255, 255, 255, 0.65); + --tertiary-color: rgba(255, 255, 255, 0.4); + --light-color: rgba(255, 255, 255, 0.25); + --faint-color: rgba(255, 255, 255, 0.1); + --faintest-color: rgba(255, 255, 255, 0.05); + --ability-accent: darkslategrey; + --header-background: rgba(255, 255, 255, 0.05); + --header-border: rgba(255, 255, 255, 0.25); + --primary-accent: #ff6400; + --prepared: rgba(0, 250, 180, 0.3); + --always-prepared: rgba(0, 100, 255, 0.3); + --icon-background: #1e1e1e; + --icon-shadow: rgba(0, 0, 0, 0.4); + --icon-outline: rgba(0, 0, 0, 0.4); + --icon-font: rgba(255, 255, 255, 0.4); + --icon-hover: rgba(255, 255, 255, 0.8); + --warning-accent: rgba(255, 30, 0, 0.65); + --check-default: url(../images/check-dark-unchecked.svg); + --check-checked: url(../images/check-light-checked.svg); + --checkbox-font: rgba(255, 255, 255, 0.8); + --checkbox-outline: #323232; + --checkbox-unchecked: #4b4b4b; + --checkbox-checked: rgba(0, 255, 0, 0.5); +} +.system-dnd5e.tidy5eDark .app.compendium-browser .window-content { + color: var(--primary-font); } .compendium-browser { overflow-y: hidden !important; @@ -14,7 +109,7 @@ border: 1px solid #acacac; display: inline-block; margin: auto; - background: rgba(0, 0, 0, 0.3); + background: var(--background); border-radius: 0.3em; color: #cecece; } @@ -63,108 +158,205 @@ height: 100%; overflow: scroll; } -.compendium-browser .control-area { - display: grid; - grid-template-rows: 2em 1fr; +.compendium-browser .settings .settings-group { + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + font-size: 0.9em; + max-width: fit-content; + float: left; +} +.compendium-browser .settings .settings-group h3 { + margin: 0; + cursor: pointer; +} +.compendium-browser .settings .settings-group label { + display: block; +} +.compendium-browser .settings .settings-group h4 { + display: inline-block; + vertical-align: middle; height: 100%; - grid-template-areas: "TOGGLER" "FILTERS"; +} +.compendium-browser .actions { + box-sizing: content-box; + flex: 0 0 32px; + background: rgba(86, 85, 78, 0.18); + border-radius: 0 0 0.5em 0.5em; + padding: 0 0.7em; +} +.compendium-browser .actions ul { + list-style: none; + display: flex; + flex-wrap: nowrap; + flex-direction: row-reverse; +} +.compendium-browser .actions li { + box-sizing: content-box; +} +.compendium-browser .actions li button { + cursor: pointer; + color: #ededed; + background-color: #444; + border-left: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 0; +} +.control-area { + background: var(--background); + box-shadow: 0 0 0.3em 0.3em var(--context-shadow); + border-color: var(--header-border); + border-style: solid; + border-width: 0 1px 0 1px; + color: var(--primary-font); + display: grid; grid-gap: 0; + height: calc(100% -40px); width: max-content; - border-radius: 0.5em; - position: relative; - background: var(--header-background); + padding: 0.3em; + position: sticky; + left: -1px; + right: -1px; /* Toggler Functionality */ } -.compendium-browser .control-area button { +.control-area.l_sidebar { + grid-template-columns: 1fr 2em; + grid-template-areas: "CONTENT TOGGLER"; + margin-left: -2em; +} +.control-area.r_sidebar { + grid-template-columns: 2em 1fr; + grid-template-areas: "TOGGLER CONTENT"; + position: absolute; + top: 12vh; + right: -1px; + left: auto; + height: calc(100% - 44px); +} +.control-area button { background: rgba(0, 0, 0, 0.05); border: 1px solid #bbb; border-radius: 5px; margin-top: 5px; padding: 2px; } -.compendium-browser .control-area .toggler { - display: none; +.control-area .toggler { + display: inline; grid-area: TOGGLER; font-size: 0; width: 0; height: 0; + color: var(--primary-color); } -.compendium-browser .control-area .toggler:after { +.control-area .toggler:after { + cursor: pointer; + content: "\f0b0"; display: inline-block; height: 16px; width: 16px; + font-size: 16px; + position: absolute; border-radius: 3px; background: none; color: darkgrey; font-family: "Font Awesome 5 Free"; font-weight: 900; text-rendering: auto; - content: "\f0b0"; font-variant: normal; } -.compendium-browser .control-area .toggler ~ #cb_filtercontainer { +.control-area .toggler:hover:before { + content: 'toggle sidebar'; + position: absolute; + display: block; + font-size: 14px; + background: #333; + padding: 5px; + left: 30px; + width: max-content; + border: 1px solid grey; + border-radius: 0 3px 3px 3px; + z-index: 999; +} +.r_sidebar.control-area .toggler:hover:before { + left: auto; + right: 30px; +} +.control-area .toggler:checked:after { + color: var(--checkbox-checked); +} +.control-area .toggler ~ .sidebar_content { display: none; } -.compendium-browser .control-area .toggler:checked ~ #cb_filtercontainer { - background: rgba(0, 0, 0, 0.05); +.control-area .toggler ~ .sidebar_content * { + color: var(--primary-font); +} +.control-area .toggler ~ .sidebar_content * option { + background: var(--background); +} +.control-area .toggler:checked ~ .sidebar_content { display: initial; - grid-area: FILTERS; + grid-area: CONTENT; width: max-content; - border-radius: 0.5em 0.5em 0 0; - position: relative; } -.compendium-browser .control-area .filtercontainer { - color: var(--primary-font); - border: 1px solid #bbb; - border-radius: 5px; +.control-area .filtercontainer { margin-top: 5px; padding: 2px; - background: rgba(0, 0, 0, 0.75); } -.compendium-browser .control-area .filtercontainer h3 { +.control-area .filtercontainer h3 { margin: 0; cursor: pointer; } -.compendium-browser .control-area .filtercontainer dl { +.control-area .filtercontainer h3 :after { + display: inline-block; + height: auto; + content: "\25BE"; + position: absolute; + right: 0.2em; + font-size: 1.2em; +} +.control-area .filtercontainer h3.opened:after { + content: "\25B4"; +} +.control-area .filtercontainer dl { margin: 0; } -.compendium-browser .control-area .filtercontainer div { +.control-area .filtercontainer div { margin: 5px 0; } -.compendium-browser .control-area .filtercontainer dt { +.control-area .filtercontainer dt { display: inline-block; width: 40%; padding-left: 5px; font-weight: 400; } -.compendium-browser .control-area .filtercontainer dd { +.control-area .filtercontainer dd { display: inline-block; width: 58%; margin-left: 0; } -.compendium-browser .control-area .filtercontainer dd select { +.control-area .filtercontainer dd select { width: 100%; } -.compendium-browser .control-area .filtercontainer input, -.compendium-browser .control-area .filtercontainer select { +.control-area .filtercontainer input, +.control-area .filtercontainer select { height: 1.4em; font-size: 0.9em; font-weight: 400; } -.compendium-browser .control-area .filtercontainer .multiselect { +.control-area .filtercontainer .multiselect { border: 1px solid #bbb; border-radius: 3px; vertical-align: middle; line-height: 32px; margin: 2px 0; } -.compendium-browser .control-area .filtercontainer .multiselect label { +.control-area .filtercontainer .multiselect label { padding: 5px; } -.compendium-browser .control-area .filtercontainer .multiselect input { +.control-area .filtercontainer .multiselect input { vertical-align: middle; } -.compendium-browser .control-area .filtercontainer .small-input { +.control-area .filtercontainer .small-input { width: calc(100% - 44px); height: 27px; background: rgba(0, 0, 0, 0.05); @@ -173,7 +365,7 @@ padding: 0 3px; text-overflow: ellipsis; } -.compendium-browser .control-area .filtercontainer .small-select { +.control-area .filtercontainer .small-select { width: 40px; } .compendium-browser .browser { @@ -201,75 +393,28 @@ display: inline-block; min-width: 15px; } -.compendium-browser.entity-browser > li { +.compendium-browser .browser.entity-browser > li { cursor: default; } -.compendium-browser.entity-browser li .feat-tags { +.compendium-browser .browser.entity-browser li .feat-tags { text-align: justify; margin-right: 3px; margin-left: 3px; text-transform: capitalize; } -.compendium-browser.entity-browser li .item-tags { +.compendium-browser .browser.entity-browser li .item-tags { text-align: justify; margin-right: 3px; margin-left: 3px; text-transform: capitalize; } -.compendium-browser .list-area { +.compendium-browser .browser .list-area { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(12vw, 100%), 1fr)); grid-template-rows: auto; - grid-template-columns: auto; grid-area: CONTENT; } -.compendium-browser .settings .settings-group { - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding: 2px; - font-size: 0.9em; - max-width: fit-content; - float: left; -} -.compendium-browser .settings .settings-group h3 { - margin: 0; - cursor: pointer; -} -.compendium-browser .settings .settings-group label { - display: block; -} -.compendium-browser .settings .settings-group h4 { - display: inline-block; - vertical-align: middle; - height: 100%; -} -.compendium-browser .actions { - box-sizing: content-box; - flex: 0 0 32px; - background: rgba(86, 85, 78, 0.18); - border-radius: 0 0 0.5em 0.5em; - padding: 0 0.7em; -} -.compendium-browser .actions ul { - list-style: none; - display: flex; - flex-wrap: nowrap; - flex-direction: row-reverse; -} -.compendium-browser .actions li { - box-sizing: content-box; -} -.compendium-browser .actions li button { - cursor: pointer; - color: #ededed; - background-color: #444; - border-left: 1px dotted rgba(0, 0, 0, 0.05); - border-radius: 0; -} -.compendium-browser .cb_entities { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(15vw, 100%), 1fr)); -} -.compendium-browser .cb_entities .entity-header { +.compendium-browser .browser .cb_entities .entity-header { display: grid; grid-template-columns: 0.55fr 0.1fr 1fr; gap: 0px 2.5em; @@ -277,7 +422,7 @@ border: 1px dotted rgba(0, 0, 0, 0.05); border-radius: 3px; } -.compendium-browser .cb_entities .entity { +.compendium-browser .browser .cb_entities .entity { background: var(--background); color: darkgrey; border-radius: 5px; @@ -287,13 +432,14 @@ height: clamp(5vh, 6vh, 7vh); box-shadow: 0 0 3px inset var(--light-color); display: grid; - grid-template-columns: 3em 1fr; + grid-template-columns: 0.2fr 1fr; + grid-column-gap: 5px; grid-template-areas: "Image Name" "Image Tags"; } -.compendium-browser .cb_entities .entity:hover { +.compendium-browser .browser .cb_entities .entity:hover { box-shadow: 0 0 3px inset var(--icon-hover); } -.compendium-browser .cb_entities .entity .entity-image { +.compendium-browser .browser .cb_entities .entity .entity-image { cursor: pointer; background-repeat: no-repeat; border: 1px solid rgba(83, 75, 75, 0.3); @@ -308,7 +454,7 @@ grid-area: Image; box-shadow: 0 0 0.2em 0.1em inset rgba(43, 45, 46, 0.863); } -.compendium-browser .cb_entities .entity .entity-name { +.compendium-browser .browser .cb_entities .entity .entity-name { grid-area: Name; color: rgba(236, 229, 229, 0.932); font-size: 12px; @@ -318,7 +464,7 @@ overflow: hidden; height: 100%; } -.compendium-browser .cb_entities .entity ul.tags { +.compendium-browser .browser .cb_entities .entity ul.tags { list-style: none; min-width: initial; box-sizing: content-box; @@ -327,15 +473,15 @@ grid-template-columns: repeat(auto-fit, 16px); margin: 0; } -.compendium-browser .cb_entities .entity .tags .spell-tag { +.compendium-browser .browser .cb_entities .entity .tags .spell-tag { color: var(--background); } -.compendium-browser .cb_entities .entity .tags .spell-tag.active { +.compendium-browser .browser .cb_entities .entity .tags .spell-tag.active { color: var(--primary-font); } -.compendium-browser .cb_entities .entity .tags li { +.compendium-browser .browser .cb_entities .entity .tags li { cursor: help; - color: darkgray; + color: var(--faint-color); justify-content: center; align-items: center; height: 1.6em; @@ -346,37 +492,58 @@ padding: 0.2em; border-radius: 0.1em; } -.compendium-browser .cb_entities .entity.r_common { - border-left: 0.3em solid fff; +.compendium-browser .browser .cb_entities .entity.r_common { + border-left: 0.3em solid #ffffff; } -.compendium-browser .cb_entities .entity.r_common .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(fff, 0.75); +.compendium-browser .browser .cb_entities .entity.r_common .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#ffffff), 0.75); } -.compendium-browser .cb_entities .entity.r_uncommon { - border-color: #3dbb2d; - background: linear-gradient(90deg, rgba(rgba(30, 255, 0), 1) 0%, rgba(163, 53, 238, 0) 35%); +.compendium-browser .browser .cb_entities .entity.r_uncommon { + border-color: #1eff00; + background: linear-gradient(90deg, #1eff00 0%, var(--background) 35%); } -.compendium-browser .cb_entities .entity.r_uncommon .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(rgba(30, 255, 0), 0.75); +.compendium-browser .browser .cb_entities .entity.r_uncommon .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#1eff00), 0.75); } -.compendium-browser .cb_entities .entity.r_rare { - border-color: darkblue; - background: linear-gradient(90deg, #0070dd 0%, rgba(0, 0, 0, 0.2) 35%); +.compendium-browser .browser .cb_entities .entity.r_rare { + border-color: #0070dd; + background: linear-gradient(90deg, #0070dd 0%, var(--background) 35%); } -.compendium-browser .cb_entities .entity.r_rare .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(0, 112, 221, 0.75); +.compendium-browser .browser .cb_entities .entity.r_rare .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#0070dd), 0.75); } -.compendium-browser .cb_entities .entity.r_veryrare { +.compendium-browser .browser .cb_entities .entity.r_veryrare { border-color: #a335ee; - background: linear-gradient(90deg, #a335ee 0%, rgba(163, 53, 238, 0) 35%); + background: linear-gradient(90deg, #a335ee 0%, var(--background) 35%); } -.compendium-browser .cb_entities .entity.r_veryrare .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(163, 53, 238, 0.75); +.compendium-browser .browser .cb_entities .entity.r_veryrare .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#a335ee), 0.75); } -.compendium-browser .cb_entities .entity.r_legendary { +.compendium-browser .browser .cb_entities .entity.r_legendary { border-color: #ff8000; - background: linear-gradient(90deg, #ff8000 0%, rgba(255, 128, 0, 0) 35%); + background: linear-gradient(90deg, #ff8000 0%, var(--background) 35%); +} +.compendium-browser .browser .cb_entities .entity.r_legendary .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#ff8000), 0.75); } -.compendium-browser .cb_entities .entity.r_legendary .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(255, 128, 0, 0.75); +.compendium-browser .tab.active[data-tab="Scene"] .cb_entities .entity { + grid-template-areas: "Name" "Image"; + grid-template-columns: 1fr; + grid-template-rows: 0.2fr 1fr; +} +.compendium-browser .tab.active[data-tab="Scene"] .cb_entities .entity .entity-image { + width: initial; + height: initial; +} +.compendium-browser .tab.active[data-tab="Scene"] .cb_entities .entity .entity-name { + position: absolute; + z-index: 999; + padding: 0.5em; + font-size: 1.4em; +} +#compendium .directory-footer { + display: block; +} +#compendium .directory-footer .compendium-browser-btn { + margin-top: 5px; } diff --git a/dist/styles/compendium-browser.min.css b/dist/styles/compendium-browser.min.css index 1686105..864db2e 100644 --- a/dist/styles/compendium-browser.min.css +++ b/dist/styles/compendium-browser.min.css @@ -1 +1 @@ -#compendium .directory-footer{display:block}#compendium .directory-footer .compendium-browser-btn{margin-top:5px}.compendium-browser{overflow-y:hidden!important;max-width:90%}.compendium-browser .info{padding:.25em .6em;text-align:center;border:1px solid #acacac;display:inline-block;margin:auto;background:rgba(0,0,0,.3);border-radius:.3em;color:#cecece}.compendium-browser .window-content{overflow-y:hidden!important;background:var(--background);height:100%;padding:.5em .5em 0 .5em}.compendium-browser .window-content .parent{height:100%}.compendium-browser .window-content .parent .content{overflow-y:hidden!important;height:calc(100% - 2em)}.compendium-browser .window-content .parent .content .tab{overflow-y:hidden!important;height:100%}.compendium-browser .window-content .parent .content .tab .browser{overflow-y:hidden!important;height:100%}.compendium-browser .window-content .parent .content .tab .browser ul{overflow-y:auto;height:100%}.compendium-browser .window-content .parent .content .tab .settings{overflow-y:auto;height:100%}.compendium-browser .tabs{max-height:2em;color:var(--primary-font);background:var(--header-background)}.compendium-browser .tabs.item.active{background:var(--background)}.compendium-browser .tabContainer{height:calc(100% - 2em)}.compendium-browser .tabContainer .tab{width:100%;height:100%;overflow:scroll}.compendium-browser .control-area{display:grid;grid-template-rows:2em 1fr;height:100%;grid-template-areas:"TOGGLER" "FILTERS";grid-gap:0;width:max-content;border-radius:.5em;position:relative;background:var(--header-background)}.compendium-browser .control-area button{background:rgba(0,0,0,.05);border:1px solid #bbb;border-radius:5px;margin-top:5px;padding:2px}.compendium-browser .control-area .toggler{display:none;grid-area:TOGGLER;font-size:0;width:0;height:0}.compendium-browser .control-area .toggler:after{display:inline-block;height:16px;width:16px;border-radius:3px;background:0 0;color:#a9a9a9;font-family:"Font Awesome 5 Free";font-weight:900;text-rendering:auto;content:"\f0b0";font-variant:normal}.compendium-browser .control-area .toggler~#cb_filtercontainer{display:none}.compendium-browser .control-area .toggler:checked~#cb_filtercontainer{background:rgba(0,0,0,.05);display:initial;grid-area:FILTERS;width:max-content;border-radius:.5em .5em 0 0;position:relative}.compendium-browser .control-area .filtercontainer{color:var(--primary-font);border:1px solid #bbb;border-radius:5px;margin-top:5px;padding:2px;background:rgba(0,0,0,.75)}.compendium-browser .control-area .filtercontainer h3{margin:0;cursor:pointer}.compendium-browser .control-area .filtercontainer dl{margin:0}.compendium-browser .control-area .filtercontainer div{margin:5px 0}.compendium-browser .control-area .filtercontainer dt{display:inline-block;width:40%;padding-left:5px;font-weight:400}.compendium-browser .control-area .filtercontainer dd{display:inline-block;width:58%;margin-left:0}.compendium-browser .control-area .filtercontainer dd select{width:100%}.compendium-browser .control-area .filtercontainer input,.compendium-browser .control-area .filtercontainer select{height:1.4em;font-size:.9em;font-weight:400}.compendium-browser .control-area .filtercontainer .multiselect{border:1px solid #bbb;border-radius:3px;vertical-align:middle;line-height:32px;margin:2px 0}.compendium-browser .control-area .filtercontainer .multiselect label{padding:5px}.compendium-browser .control-area .filtercontainer .multiselect input{vertical-align:middle}.compendium-browser .control-area .filtercontainer .small-input{width:calc(100% - 44px);height:27px;background:rgba(0,0,0,.05);border:1px solid #444;border-radius:3px;padding:0 3px;text-overflow:ellipsis}.compendium-browser .control-area .filtercontainer .small-select{width:40px}.compendium-browser .browser{display:grid;grid-template-columns:2em auto 2em;grid-template-areas:"L_SIDEBAR CONTENT R_SIDEBAR";height:100%;overflow:hidden!important}.compendium-browser .browser .window-content{overflow-y:hidden!important}.compendium-browser .browser ul .filter-tags{display:none}.compendium-browser .browser ul li span{white-space:nowrap;overflow:hidden}.compendium-browser .browser .spacer{display:inline-block;min-width:5px}.compendium-browser .browser .spacer-large{display:inline-block;min-width:15px}.compendium-browser.entity-browser>li{cursor:default}.compendium-browser.entity-browser li .feat-tags{text-align:justify;margin-right:3px;margin-left:3px;text-transform:capitalize}.compendium-browser.entity-browser li .item-tags{text-align:justify;margin-right:3px;margin-left:3px;text-transform:capitalize}.compendium-browser .list-area{grid-template-rows:auto;grid-template-columns:auto;grid-area:CONTENT}.compendium-browser .settings .settings-group{border:1px solid #bbb;border-radius:5px;margin-top:5px;padding:2px;font-size:.9em;max-width:fit-content;float:left}.compendium-browser .settings .settings-group h3{margin:0;cursor:pointer}.compendium-browser .settings .settings-group label{display:block}.compendium-browser .settings .settings-group h4{display:inline-block;vertical-align:middle;height:100%}.compendium-browser .actions{box-sizing:content-box;flex:0 0 32px;background:rgba(86,85,78,.18);border-radius:0 0 .5em .5em;padding:0 .7em}.compendium-browser .actions ul{list-style:none;display:flex;flex-wrap:nowrap;flex-direction:row-reverse}.compendium-browser .actions li{box-sizing:content-box}.compendium-browser .actions li button{cursor:pointer;color:#ededed;background-color:#444;border-left:1px dotted rgba(0,0,0,.05);border-radius:0}.compendium-browser .cb_entities{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(15vw,100%),1fr));align-items:center}.compendium-browser .cb_entities .entity-header{display:grid;grid-template-columns:0.55fr 0.1fr 1fr;gap:0 2.5em;text-align:center;border:1px dotted rgba(0,0,0,.05);border-radius:3px}.compendium-browser .cb_entities .entity{background:var(--background);color:#a9a9a9;border-radius:5px;margin:.2em;border:1px solid rgba(0,0,0,.2);padding:.1em;height:clamp(5vh,6vh,7vh);box-shadow:0 0 3px inset var(--light-color);display:grid;grid-template-columns:3em 1fr;grid-template-areas:"Image Name" "Image Tags"}.compendium-browser .cb_entities .entity:hover{box-shadow:0 0 3px inset var(--icon-hover)}.compendium-browser .cb_entities .entity .entity-image{cursor:pointer;background-repeat:no-repeat;border:1px solid rgba(83,75,75,.3);padding:.1em;border-radius:.5em;justify-content:center;align-items:center;background-clip:border-box;background-image:url('/icons/svg/daze.svg');background-size:cover;width:3.2em;grid-area:Image;box-shadow:0 0 .2em .1em inset rgba(43,45,46,.863)}.compendium-browser .cb_entities .entity .entity-name{grid-area:Name;color:rgba(236,229,229,.932);font-size:12px;font-family:"Modesto Condensed","Palatino Linotype",serif;font-size:.9vw;font-weight:400;overflow:hidden;height:100%}.compendium-browser .cb_entities .entity ul.tags{list-style:none;min-width:initial;box-sizing:content-box;grid-area:Tags;display:grid;grid-template-columns:repeat(auto-fit,16px);margin:0}.compendium-browser .cb_entities .entity .tags .spell-tag{color:var(--background)}.compendium-browser .cb_entities .entity .tags .spell-tag.active{color:var(--primary-font)}.compendium-browser .cb_entities .entity .tags li{cursor:help;color:#a9a9a9;justify-content:center;align-items:center;height:1.6em;width:1.6em;font-size:.8em;line-height:1.6em;display:inline-block;padding:.2em;border-radius:.1em}.compendium-browser .cb_entities .entity.r_common{border-left:.3em solid fff}.compendium-browser .cb_entities .entity.r_common .entity-image{box-shadow:0 0 .2em .1em inset rgba(fff,.75)}.compendium-browser .cb_entities .entity.r_uncommon{border-color:#3dbb2d;background:linear-gradient(90deg,rgba(rgba(30,255,0),1) 0,rgba(163,53,238,0) 35%)}.compendium-browser .cb_entities .entity.r_uncommon .entity-image{box-shadow:0 0 .2em .1em inset rgba(rgba(30,255,0),.75)}.compendium-browser .cb_entities .entity.r_rare{border-color:#00008b;background:linear-gradient(90deg,#0070dd 0,rgba(0,0,0,.2) 35%)}.compendium-browser .cb_entities .entity.r_rare .entity-image{box-shadow:0 0 .2em .1em inset rgba(0,112,221,.75)}.compendium-browser .cb_entities .entity.r_veryrare{border-color:#a335ee;background:linear-gradient(90deg,#a335ee 0,rgba(163,53,238,0) 35%)}.compendium-browser .cb_entities .entity.r_veryrare .entity-image{box-shadow:0 0 .2em .1em inset rgba(163,53,238,.75)}.compendium-browser .cb_entities .entity.r_legendary{border-color:#ff8000;background:linear-gradient(90deg,#ff8000 0,rgba(255,128,0,0) 35%)}.compendium-browser .cb_entities .entity.r_legendary .entity-image{box-shadow:0 0 .2em .1em inset rgba(255,128,0,.75)} \ No newline at end of file +.system-dnd5e{--modesto:"Modesto Condensed","Palatino Linotype",serif;--signika:"Signika",sans-serif;--primary-font:rgba(0, 0, 0, 0.9);--background:#ece9df;--faintest-color:rgba(0, 0, 0, 0.05);--faint-color:rgba(0, 0, 0, 0.1);--light-color:rgba(0, 0, 0, 0.25);--primary-color:rgba(0, 0, 0, 0.9);--secondary-color:rgba(0, 0, 0, 0.65);--tertiary-color:rgba(0, 0, 0, 0.4);--primary-accent:#ff6400;--white:#ffffff;--faint-white:rgba(255, 255, 255, 0.2);--linked-accent:rgba(0, 255, 0, 0.75);--unlinked-accent:rgba(255, 0, 0, 0.75);--linked-light:rgba(0, 255, 0, 0.4);--unlinked-light:rgba(255, 0, 0, 0.4);--safe-accent:rgba(0, 150, 100, 0.6);--unsafe-accent:rgba(255, 0, 0, 0.6);--header-background:rgba(255, 255, 255, 0.2);--header-border:rgba(0, 0, 0, 0.25);--stat-font:#ece9df;--prepareable:#778899;--prepared:rgba(50, 205, 50, 0.3);--prepared-outline:#32cd32;--prepared-accent:#adff2f;--always-prepared:rgba(0, 0, 255, 0.15);--always-prepared-outline:#4169e1;--always-prepared-accent:#00bfff;--magic-accent:#ffff00;--faint-magic-accent:rgba(255, 255, 0, 0.6);--magic-outline:#afff2f;--attunement-required:#cd5c5c;--icon-attuned:rgba(0, 0, 0, 0.4);--xp-bar:#5ee192;--encumbrance-bar:#6c8aa5;--encumbrance-bar-outline:#cde4ff;--encumbrance-outline:rgba(0, 0, 0, 0.9);--warning-accent:rgba(255, 0, 0, 0.6);--icon-background:#ece9df;--icon-shadow:rgba(0, 0, 0, 0.4);--icon-outline:rgba(0, 0, 0, 0.4);--icon-font:rgba(0, 0, 0, 0.4);--exhaustion-font:rgba(0, 0, 0, 0.4);--icon-hover:rgba(0, 0, 0, 0.9);--pc-border:0px;--npc-border:0px;--vehicle-border:0px;--note-background:rgba(0, 0, 0, 0.9);--exhaustion-lvl1:#ffe600;--exhaustion-lvl2:#ff8200;--exhaustion-lvl3:#ff3200;--ability-accent:darkslategrey;--context-outline:rgba(0, 0, 0, 0.4);--context-shadow:rgba(0, 0, 0, 0.65);--check-default:url(../images/check-dark-unchecked.svg);--check-checked:url(../images/check-dark-checked.svg);--checkbox-font:rgba(0, 0, 0, 0.9);--checkbox-outline:#969696;--checkbox-unchecked:#D8D7D1;--checkbox-checked:rgba(0, 255, 0, 0.3)}.system-dnd5e.tidy5eDark{--primary-font:rgba(255, 255, 255, 0.8);--background:#1e1e1e;--white:#000000;--primary-color:rgba(255, 255, 255, 0.8);--secondary-color:rgba(255, 255, 255, 0.65);--tertiary-color:rgba(255, 255, 255, 0.4);--light-color:rgba(255, 255, 255, 0.25);--faint-color:rgba(255, 255, 255, 0.1);--faintest-color:rgba(255, 255, 255, 0.05);--ability-accent:darkslategrey;--header-background:rgba(255, 255, 255, 0.05);--header-border:rgba(255, 255, 255, 0.25);--primary-accent:#ff6400;--prepared:rgba(0, 250, 180, 0.3);--always-prepared:rgba(0, 100, 255, 0.3);--icon-background:#1e1e1e;--icon-shadow:rgba(0, 0, 0, 0.4);--icon-outline:rgba(0, 0, 0, 0.4);--icon-font:rgba(255, 255, 255, 0.4);--icon-hover:rgba(255, 255, 255, 0.8);--warning-accent:rgba(255, 30, 0, 0.65);--check-default:url(../images/check-dark-unchecked.svg);--check-checked:url(../images/check-light-checked.svg);--checkbox-font:rgba(255, 255, 255, 0.8);--checkbox-outline:#323232;--checkbox-unchecked:#4b4b4b;--checkbox-checked:rgba(0, 255, 0, 0.5)}.system-dnd5e.tidy5eDark .app.compendium-browser .window-content{color:var(--primary-font)}.compendium-browser{overflow-y:hidden!important;max-width:90%}.compendium-browser .info{padding:.25em .6em;text-align:center;border:1px solid #acacac;display:inline-block;margin:auto;background:var(--background);border-radius:.3em;color:#cecece}.compendium-browser .window-content{overflow-y:hidden!important;background:var(--background);height:100%;padding:.5em .5em 0 .5em}.compendium-browser .window-content .parent{height:100%}.compendium-browser .window-content .parent .content{overflow-y:hidden!important;height:calc(100% - 2em)}.compendium-browser .window-content .parent .content .tab{overflow-y:hidden!important;height:100%}.compendium-browser .window-content .parent .content .tab .browser{overflow-y:hidden!important;height:100%}.compendium-browser .window-content .parent .content .tab .browser ul{overflow-y:auto;height:100%}.compendium-browser .window-content .parent .content .tab .settings{overflow-y:auto;height:100%}.compendium-browser .tabs{max-height:2em;color:var(--primary-font);background:var(--header-background)}.item .compendium-browser .tabs .active{background:var(--background)}.compendium-browser .tabContainer{height:calc(100% - 2em)}.compendium-browser .tabContainer .tab{width:100%;height:100%;overflow:scroll}.compendium-browser .settings .settings-group{border:1px solid #bbb;border-radius:5px;margin-top:5px;padding:2px;font-size:.9em;max-width:fit-content;float:left}.compendium-browser .settings .settings-group h3{margin:0;cursor:pointer}.compendium-browser .settings .settings-group label{display:block}.compendium-browser .settings .settings-group h4{display:inline-block;vertical-align:middle;height:100%}.compendium-browser .actions{box-sizing:content-box;flex:0 0 32px;background:rgba(86,85,78,.18);border-radius:0 0 .5em .5em;padding:0 .7em}.compendium-browser .actions ul{list-style:none;display:flex;flex-wrap:nowrap;flex-direction:row-reverse}.compendium-browser .actions li{box-sizing:content-box}.compendium-browser .actions li button{cursor:pointer;color:#ededed;background-color:#444;border-left:1px dotted rgba(0,0,0,.05);border-radius:0}.control-area{background:var(--background);box-shadow:0 0 .3em .3em var(--context-shadow);border-color:var(--header-border);border-style:solid;border-width:0 1px 0 1px;color:var(--primary-font);display:grid;grid-gap:0;height:calc(100% -40px);width:max-content;padding:.3em;position:sticky;left:-1px;right:-1px}.control-area.l_sidebar{grid-template-columns:1fr 2em;grid-template-areas:"CONTENT TOGGLER";margin-left:-2em}.control-area.r_sidebar{grid-template-columns:2em 1fr;grid-template-areas:"TOGGLER CONTENT";position:absolute;top:12vh;right:-1px;left:auto;height:calc(100% - 44px)}.control-area button{background:rgba(0,0,0,.05);border:1px solid #bbb;border-radius:5px;margin-top:5px;padding:2px}.control-area .toggler{display:inline;grid-area:TOGGLER;font-size:0;width:0;height:0;color:var(--primary-color)}.control-area .toggler:after{cursor:pointer;content:"\f0b0";display:inline-block;height:16px;width:16px;font-size:16px;position:absolute;border-radius:3px;background:0 0;color:#a9a9a9;font-family:"Font Awesome 5 Free";font-weight:900;text-rendering:auto;font-variant:normal}.control-area .toggler:hover:before{content:'toggle sidebar';position:absolute;display:block;font-size:14px;background:#333;padding:5px;left:30px;width:max-content;border:1px solid grey;border-radius:0 3px 3px 3px;z-index:999}.r_sidebar.control-area .toggler:hover:before{left:auto;right:30px}.control-area .toggler:checked:after{color:var(--checkbox-checked)}.control-area .toggler~.sidebar_content{display:none}.control-area .toggler~.sidebar_content *{color:var(--primary-font)}.control-area .toggler~.sidebar_content * option{background:var(--background)}.control-area .toggler:checked~.sidebar_content{display:initial;grid-area:CONTENT;width:max-content}.control-area .filtercontainer{margin-top:5px;padding:2px}.control-area .filtercontainer h3{margin:0;cursor:pointer}.control-area .filtercontainer h3 :after{display:inline-block;height:auto;content:"\25BE";position:absolute;right:.2em;font-size:1.2em}.control-area .filtercontainer h3.opened:after{content:"\25B4"}.control-area .filtercontainer dl{margin:0}.control-area .filtercontainer div{margin:5px 0}.control-area .filtercontainer dt{display:inline-block;width:40%;padding-left:5px;font-weight:400}.control-area .filtercontainer dd{display:inline-block;width:58%;margin-left:0}.control-area .filtercontainer dd select{width:100%}.control-area .filtercontainer input,.control-area .filtercontainer select{height:1.4em;font-size:.9em;font-weight:400}.control-area .filtercontainer .multiselect{border:1px solid #bbb;border-radius:3px;vertical-align:middle;line-height:32px;margin:2px 0}.control-area .filtercontainer .multiselect label{padding:5px}.control-area .filtercontainer .multiselect input{vertical-align:middle}.control-area .filtercontainer .small-input{width:calc(100% - 44px);height:27px;background:rgba(0,0,0,.05);border:1px solid #444;border-radius:3px;padding:0 3px;text-overflow:ellipsis}.control-area .filtercontainer .small-select{width:40px}.compendium-browser .browser{display:grid;grid-template-columns:2em auto 2em;grid-template-areas:"L_SIDEBAR CONTENT R_SIDEBAR";height:100%;overflow:hidden!important}.compendium-browser .browser .window-content{overflow-y:hidden!important}.compendium-browser .browser ul .filter-tags{display:none}.compendium-browser .browser ul li span{white-space:nowrap;overflow:hidden}.compendium-browser .browser .spacer{display:inline-block;min-width:5px}.compendium-browser .browser .spacer-large{display:inline-block;min-width:15px}.compendium-browser .browser.entity-browser>li{cursor:default}.compendium-browser .browser.entity-browser li .feat-tags{text-align:justify;margin-right:3px;margin-left:3px;text-transform:capitalize}.compendium-browser .browser.entity-browser li .item-tags{text-align:justify;margin-right:3px;margin-left:3px;text-transform:capitalize}.compendium-browser .browser .list-area{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(12vw,100%),1fr));grid-template-rows:auto;grid-area:CONTENT}.compendium-browser .browser .cb_entities .entity-header{display:grid;grid-template-columns:0.55fr 0.1fr 1fr;gap:0 2.5em;text-align:center;border:1px dotted rgba(0,0,0,.05);border-radius:3px}.compendium-browser .browser .cb_entities .entity{background:var(--background);color:#a9a9a9;border-radius:5px;margin:.2em;border:1px solid rgba(0,0,0,.2);padding:.1em;height:clamp(5vh,6vh,7vh);box-shadow:0 0 3px inset var(--light-color);display:grid;grid-template-columns:0.2fr 1fr;grid-column-gap:5px;grid-template-areas:"Image Name" "Image Tags"}.compendium-browser .browser .cb_entities .entity:hover{box-shadow:0 0 3px inset var(--icon-hover)}.compendium-browser .browser .cb_entities .entity .entity-image{cursor:pointer;background-repeat:no-repeat;border:1px solid rgba(83,75,75,.3);padding:.1em;border-radius:.5em;justify-content:center;align-items:center;background-clip:border-box;background-image:url('/icons/svg/daze.svg');background-size:cover;width:3.2em;grid-area:Image;box-shadow:0 0 .2em .1em inset rgba(43,45,46,.863)}.compendium-browser .browser .cb_entities .entity .entity-name{grid-area:Name;color:rgba(236,229,229,.932);font-size:12px;font-family:"Modesto Condensed","Palatino Linotype",serif;font-size:.9vw;font-weight:400;overflow:hidden;height:100%}.compendium-browser .browser .cb_entities .entity ul.tags{list-style:none;min-width:initial;box-sizing:content-box;grid-area:Tags;display:grid;grid-template-columns:repeat(auto-fit,16px);margin:0}.compendium-browser .browser .cb_entities .entity .tags .spell-tag{color:var(--background)}.compendium-browser .browser .cb_entities .entity .tags .spell-tag.active{color:var(--primary-font)}.compendium-browser .browser .cb_entities .entity .tags li{cursor:help;color:var(--faint-color);justify-content:center;align-items:center;height:1.6em;width:1.6em;font-size:.8em;line-height:1.6em;display:inline-block;padding:.2em;border-radius:.1em}.compendium-browser .browser .cb_entities .entity.r_common{border-left:.3em solid #fff}.compendium-browser .browser .cb_entities .entity.r_common .entity-image{box-shadow:0 0 .2em .1em inset rgba(var(#fff),.75)}.compendium-browser .browser .cb_entities .entity.r_uncommon{border-color:#1eff00;background:linear-gradient(90deg,#1eff00 0,var(--background) 35%)}.compendium-browser .browser .cb_entities .entity.r_uncommon .entity-image{box-shadow:0 0 .2em .1em inset rgba(var(#1eff00),.75)}.compendium-browser .browser .cb_entities .entity.r_rare{border-color:#0070dd;background:linear-gradient(90deg,#0070dd 0,var(--background) 35%)}.compendium-browser .browser .cb_entities .entity.r_rare .entity-image{box-shadow:0 0 .2em .1em inset rgba(var(#0070dd),.75)}.compendium-browser .browser .cb_entities .entity.r_veryrare{border-color:#a335ee;background:linear-gradient(90deg,#a335ee 0,var(--background) 35%)}.compendium-browser .browser .cb_entities .entity.r_veryrare .entity-image{box-shadow:0 0 .2em .1em inset rgba(var(#a335ee),.75)}.compendium-browser .browser .cb_entities .entity.r_legendary{border-color:#ff8000;background:linear-gradient(90deg,#ff8000 0,var(--background) 35%)}.compendium-browser .browser .cb_entities .entity.r_legendary .entity-image{box-shadow:0 0 .2em .1em inset rgba(var(#ff8000),.75)}.compendium-browser .tab.active[data-tab=Scene] .cb_entities .entity{grid-template-areas:"Name" "Image";grid-template-columns:1fr;grid-template-rows:0.2fr 1fr}.compendium-browser .tab.active[data-tab=Scene] .cb_entities .entity .entity-image{width:initial;height:initial}.compendium-browser .tab.active[data-tab=Scene] .cb_entities .entity .entity-name{position:absolute;z-index:999;padding:.5em;font-size:1.4em}#compendium .directory-footer{display:block}#compendium .directory-footer .compendium-browser-btn{margin-top:5px} \ No newline at end of file diff --git a/dist/template/entity-browser.hbs b/dist/template/entity-browser.hbs index ceb6501..0c96fa3 100644 --- a/dist/template/entity-browser.hbs +++ b/dist/template/entity-browser.hbs @@ -1,9 +1,7 @@
            -
            \ No newline at end of file diff --git a/dist/template/entity-list.hbs b/dist/template/entity-list.hbs index a3dd858..e3c6d35 100644 --- a/dist/template/entity-list.hbs +++ b/dist/template/entity-list.hbs @@ -1,7 +1,7 @@ {{#each entityItems as |entity|}} -
          • +
          • )CdQ3F!eKyF|9D2V!F-rhUpJ8J+l7g0OBBVM#THTI&O8)>EGR%u6Gd9D9pm5!S}%&6DxW}^f|7=Y zPoO3(pTZY#?(7(|!5}5Nn!D%DotZmlW)?smSMcEE<^aT$6gw#LlwubPI9BYTffL0! zyu-EPCnz{Y#ZR&1d{F!hr_NW!&#~mXis$jseXDo@U)-kR7sMBeUt-T&RQw9By@BF9 z3f?cpmw4m-R{RHncaC**(V--ipJ<~6LkW2fi6RVfh%vcYt9@z>&M0LBSf-Q|Et8wU zCt43_*JB)mHR71wb`K@~5Cizwp{`A2uuJ^_Bcl3k{7ree$8&@l?;^2nagS+NqCDBfkB?pJws=PbK~+A7|2 z{gCDJKI-i%m4LD$n{WIwWR|c+NRy`C1#)1sSBI7FiH6z-QkhY&Q_|%I3exQ zQ`X1M?cZH4^M&BSyr;2z$+^SZUMA*0001Z+HKHROw(}?!13=vX`$@Br+fGR zZ%e`5O6%Txi$Yrz0gF{}p>fY>OnlS0Uevf}oDXW;D{d2gcE<2)oFcV80@g$H)63L{HN*d{8kVzKVW(;E)$9N_%kx5Ku3R9WJbY?JW^G#k0Wdx>E$NBBVtKRLiL?sA*s%w`TdsNz1=+~FRNdB8&+@iBD0 zXFTC4C-8-Cwv(4U=LLQ~^Oa4^rG|OTr5?ItoaPMYxxh`%a*kVU z;HYGAjq6;IY{`*awo0DlOMw(hkrYdb(O28l;MYvSx*ChcQW4f^QL5UdE3HbqvbxB$pfSg`>Cj#;?~00;nMAg}==M6d%RaIhCe zARtS)01i=0um)3FSgr#ump{<1pq_<0a34Kp8x=7I1^|9 literal 0 HcmV?d00001 diff --git a/docs/fonts/OpenSans-Regular-webfont.eot b/docs/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..6bbc3cf58cb011a6b4bf3cb1612ce212608f7274 GIT binary patch literal 19836 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Regular-webfont.woff b/docs/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..e231183dce4c7b452afc9e7799586fd285e146f4 GIT binary patch literal 22660 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 ML;wL%uV=LY00kR;TmS$7 literal 0 HcmV?d00001 diff --git a/docs/hooks_events.js.html b/docs/hooks_events.js.html new file mode 100644 index 0000000..16f096c --- /dev/null +++ b/docs/hooks_events.js.html @@ -0,0 +1,314 @@ + + + + + JSDoc: Source: hooks/events.js + + + + + + + + + + +

            \ No newline at end of file diff --git a/dist/template/template.hbs b/dist/template/template.hbs index e4e1c61..eae8b7c 100644 --- a/dist/template/template.hbs +++ b/dist/template/template.hbs @@ -1,28 +1,46 @@
            - {{#if showItemBrowser}}{{/if}} - {{#if showSpellBrowser}}{{/if}} - {{#if showFeatBrowser}}{{/if}} - {{#if showActorBrowser}}{{/if}} - {{#if showJournalEntryBrowser}}{{/if}} - {{#if showRollTableBrowser}}{{/if}} - {{#if isGM}}{{/if}} -
            - + {{#if showItemBrowser}}{{/if}} + {{#if showSpellBrowser}}{{/if}} + {{#if showFeatBrowser}}{{/if}} + {{#if showActorBrowser}}{{/if}} + {{#if showJournalEntryBrowser}}{{/if}} + {{#if showRollTableBrowser}}{{/if}} + {{#if showSceneBrowser}}{{/if}} + {{#if isGM}}{{/if}} +
            - {{#if showSpellBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Spell}}
            {{/if}} - {{#if showFeatBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Feat}}
            {{/if}} - {{#if showItemBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Item}}
            {{/if}} - {{#if showActorBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Actor}}
            {{/if}} - {{#if showJournalEntryBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.JournalEntry}}
            {{/if}} - {{#if showRollTableBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.RollTable}}
            {{/if}} - {{#if isGM}}
            {{> "modules/compendium-browser/template/settings.hbs"}}
            {{/if}} + {{#if showItemBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Item}}
            {{/if}} + {{#if showSpellBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Spell}}
            {{/if}} + {{#if showActorBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Actor}}
            {{/if}} + {{#if showFeatBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Feat}}
            {{/if}} + {{#if showJournalEntryBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.JournalEntry}}
            {{/if}} + {{#if showRollTableBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.RollTable}}
            {{/if}} + {{#if showSceneBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Scene}}
            {{/if}} + {{#if isGM}} +
            {{> "modules/compendium-browser/template/settings.hbs"}}
            + + + {{/if}}
            \ No newline at end of file diff --git a/docs/Filter.html b/docs/Filter.html index 048a869..e44be10 100644 --- a/docs/Filter.html +++ b/docs/Filter.html @@ -420,7 +420,7 @@
            Parameters:
            Source:
            @@ -522,7 +522,7 @@

            _ge
            Source:
            @@ -628,7 +628,7 @@

            (async) Source:
            @@ -940,7 +940,7 @@
            Parameters:
            Source:
            @@ -1028,7 +1028,7 @@

            (async) Source:
            @@ -1168,7 +1168,7 @@
            Parameters:
            Source:
            @@ -1317,7 +1317,7 @@
            Parameters:
            Source:
            @@ -1379,7 +1379,7 @@

            Home

            Classes

            • diff --git a/docs/VersionCheck.html b/docs/VersionCheck.html index 8c2b9c1..c7e2de4 100644 --- a/docs/VersionCheck.html +++ b/docs/VersionCheck.html @@ -292,7 +292,7 @@

              Home

              Classes

              • diff --git a/docs/dist_hooks_events.js.html b/docs/dist_hooks_events.js.html index ebfc2c4..63904a4 100644 --- a/docs/dist_hooks_events.js.html +++ b/docs/dist_hooks_events.js.html @@ -72,13 +72,17 @@

                Source: dist/hooks/events.js

                }); }); } + /** + * + * @param {HTMLCollection} app + */ static async activateItemListListeners(app = document.getElementsByClassName('window-app')) { app = app[0]; // open entity sheet on click app.querySelectorAll('*[data-action="openSheet"]').forEach(async (el) => { el.addEventListener('click', async (e) => { - let itemId = e.currentTarget.parentNode.dataset.entryId; - let compendium = e.currentTarget.parentNode.dataset.entryCompendium; + let itemId = e.currentTarget.parentNode.dataset.entityId; + let compendium = e.currentTarget.parentNode.dataset.entityCompendium; let pack = game.packs.find(p => p.collection === compendium); await pack.getEntity(itemId).then(entity => { entity.sheet.render(true); @@ -86,24 +90,42 @@

                Source: dist/hooks/events.js

                }); }); // make draggable - //0.4.1: Avoid the game.packs lookup app.querySelectorAll('.draggable').forEach(async (li) => { li.setAttribute("draggable", true); li.addEventListener('dragstart', event => { - let packName = li.getAttribute("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === packName); - if (!pack) { - event.preventDefault(); - return false; - } event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.entity, - pack: pack.collection, - id: li.getAttribute("data-entry-id") + type: li.dataset.entityType, + pack: li.dataset.entityCompendium, + id: li.dataset.entityId, + name: li.dataset.entityName })); }, false); }); } + /** + * + * @param {HTML|undefined} app + */ + static async registerDropTarget(app = document.getElementsByClassName('window-app')) { + app = app[0]; + let dropTarget = app.querySelector('.droptarget'); + dropTarget.addEventListener('dragover', async (e) => { + e.preventDefault(); + dropTarget.style.border = '1px solid red'; + }); + dropTarget.addEventListener('drop', async (e) => { + e.preventDefault(); + let newEntry = JSON.parse(e.dataTransfer.getData("text/plain")); + let row = dropTarget.insertRow(); + let compendium = row.insertCell(0); + let name = row.insertCell(1); + name.appendChild(document.createTextNode(newEntry.name)); + compendium.appendChild(document.createTextNode(newEntry.pack)); + row.dataset.id = newEntry.id; + row.dataset.pack = newEntry.pack; + row.dataset.type = newEntry.type; + }); + } static async observeListElement(list, tag) { for (let element of list.getElementsByTagName(tag)) { game.compendiumBrowser.observer.observe(element); @@ -113,6 +135,7 @@

                Source: dist/hooks/events.js

                app = app[0]; // toggle visibility of filter containers html.find('.filtercontainer h3, .multiselect label').click(async (ev) => { + ev.target.classList.toggle('opened'); await $(ev.target.nextElementSibling).toggle(100); }); html.find('.multiselect label').trigger('click'); @@ -147,6 +170,9 @@

                Source: dist/hooks/events.js

                case 'allow-rolltable-browser': game.compendiumBrowser.settings.allowRollTableBrowser = value; break; + case 'allow-scene-browser': + game.compendiumBrowser.settings.allowSceneBrowser = value; + break; case 'allow-journalentry-browser': game.compendiumBrowser.settings.allowJournalEntryBrowser = value; break; @@ -252,7 +278,7 @@

                Home

                Classes

                • diff --git a/docs/dist_hooks_moduleHooks.js.html b/docs/dist_hooks_moduleHooks.js.html index 6a5a1b4..523b5f9 100644 --- a/docs/dist_hooks_moduleHooks.js.html +++ b/docs/dist_hooks_moduleHooks.js.html @@ -131,7 +131,7 @@

                  Home

                  Classes

                  • diff --git a/docs/dist_scripts_classes_compactList.js.html b/docs/dist_scripts_classes_compactList.js.html index bb450d7..341e37b 100644 --- a/docs/dist_scripts_classes_compactList.js.html +++ b/docs/dist_scripts_classes_compactList.js.html @@ -85,7 +85,7 @@

                    Home

                    Classes

                    • diff --git a/docs/dist_scripts_classes_decoratedEntity.js.html b/docs/dist_scripts_classes_decoratedEntity.js.html index e001138..d324a3f 100644 --- a/docs/dist_scripts_classes_decoratedEntity.js.html +++ b/docs/dist_scripts_classes_decoratedEntity.js.html @@ -186,6 +186,8 @@

                      Source: dist/scripts/classes/decoratedEntity.js

                      decorated.classRequirement = classes.split(','); } break; + case 'Scene': + decorated.data.dimensions = entityData.height + ' * ' + entityData.width; } return decorated; } @@ -206,7 +208,7 @@

                      Home

                      Classes

                      • diff --git a/docs/dist_scripts_classes_filter.js.html b/docs/dist_scripts_classes_filter.js.html index dab4a9d..269938a 100644 --- a/docs/dist_scripts_classes_filter.js.html +++ b/docs/dist_scripts_classes_filter.js.html @@ -86,7 +86,7 @@

                        Home

                        Classes

                        • diff --git a/docs/dist_scripts_compendium-browser.js.html b/docs/dist_scripts_compendium-browser.js.html index 6bc8283..ab633bc 100644 --- a/docs/dist_scripts_compendium-browser.js.html +++ b/docs/dist_scripts_compendium-browser.js.html @@ -70,7 +70,8 @@

                          Source: dist/scripts/compendium-browser.js

                          "modules/compendium-browser/template/entity-list.hbs", "modules/compendium-browser/template/filter-container.hbs", "modules/compendium-browser/template/settings.hbs", - "modules/compendium-browser/template/loading.hbs" + "modules/compendium-browser/template/loading.hbs", + "modules/compendium-browser/template/export.hbs" ]); this.filters.addEntityFilters(); this.hookCompendiumList(); @@ -93,18 +94,20 @@

                          Source: dist/scripts/compendium-browser.js

                          items: [], actors: [], filters: { - Spell: this.filters.getByName('Spell'), - Item: this.filters.getByName('Item'), Actor: this.filters.getByName('Actor'), + Item: this.filters.getByName('Item'), + JournalEntry: this.filters.getByName('JounralEntry'), RollTable: this.filters.getByName('RollTable'), - JournalEntry: this.filters.getByName('RollTable') + Spell: this.filters.getByName('Spell'), + Scene: this.filters.getByName('Scene'), }, - showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, + showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, showFeatBrowser: (game.user.isGM) || this.settings.allowFeatBrowser, showItemBrowser: (game.user.isGM) || this.settings.allowItemBrowser, - showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, - showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser, showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser, + showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser, + showSceneBrowser: (game.user.isGM) || this.settings.allowSceneBrowser, + showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, settings: this.settings, isGM: game.user.isGM }; @@ -131,6 +134,7 @@

                          Source: dist/scripts/compendium-browser.js

                          Events.activateActionListener(html); Events.activateItemListListeners(html); Events.activateFilterListeners(html); + Events.registerDropTarget(html); //Just for the loading image if (this.observer) { html.find(".entity-image").each((i, imageElement) => this.observer.observe(imageElement)); @@ -173,6 +177,9 @@

                          Source: dist/scripts/compendium-browser.js

                          else if (this.settings.allowRollTableBrowser) { this.refreshList = "RollTable"; } + else if (this.settings.allowRollTableBrowser) { + this.refreshList = "Scene"; + } this.render(true); }); } @@ -186,7 +193,7 @@

                          Source: dist/scripts/compendium-browser.js

                          async replaceList(html, entityType, options = { reload: true }) { //After rendering the first time or re-rendering trigger the load/reload of visible data let entityListElement = document.querySelector('.tab.active .browser .cb_entities'); - if (entityListElement.childElementCount !== undefined) { + if (entityListElement && entityListElement.childElementCount !== undefined) { //0.4.2b: On a tab-switch, only reload if there isn't any data already if (options?.reload || entityListElement.childElementCount < 1) { const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; @@ -231,7 +238,7 @@

                          Home

                          Classes

                          • diff --git a/docs/dist_scripts_modules_entities.js.html b/docs/dist_scripts_modules_entities.js.html index 95551cb..b8f3eb1 100644 --- a/docs/dist_scripts_modules_entities.js.html +++ b/docs/dist_scripts_modules_entities.js.html @@ -27,7 +27,6 @@

                            Source: dist/scripts/modules/entities.js

                            import { Filter } from './filter.js';
                            -import { Renderer } from './renderer.js';
                             import { CMPBrowser } from './settings.js';
                             import { compactEntity } from '../classes/compactEntity.js';
                             import { decoratedEntity } from '../classes/decoratedEntity.js';
                            @@ -113,12 +112,14 @@ 

                            Source: dist/scripts/modules/entities.js

                            compact.orderSize = decorated.filterSize; compact.data.details = decorated.data.details; break; + case "Scene": + compact.img = currentEntity.data.thumb; default: break; } comp_list.addEntity(compact); if (updateLoading) { - Renderer.updateLoading(entityType, numItemsLoaded, numPacks, 500); + ui.notifications.info(`Loaded ${numItemsLoaded} ${entityType}s from ${numPacks} Compendia.`); } if (numItemsLoaded++ >= maxLoad) break; @@ -169,6 +170,7 @@

                            Source: dist/scripts/modules/entities.js

                            ['Feats', 'data.class'], ['RollTable', 'compendium'], ['JounralEntry', 'name'], + ['Scene', 'name'], ]); list.entities.sort((left, right) => { let sort = defaultSort.get(entityType) || '', result = SortCollator.compare(getProperty(left, sort), getProperty(right, sort)); @@ -223,7 +225,7 @@

                            Home

                            Classes

                            • diff --git a/docs/dist_scripts_modules_exporter.js.html b/docs/dist_scripts_modules_exporter.js.html index 19461aa..5329e9c 100644 --- a/docs/dist_scripts_modules_exporter.js.html +++ b/docs/dist_scripts_modules_exporter.js.html @@ -76,7 +76,7 @@

                              Home

                              Classes

                              • diff --git a/docs/dist_scripts_modules_filter.js.html b/docs/dist_scripts_modules_filter.js.html index 67be102..783ddc5 100644 --- a/docs/dist_scripts_modules_filter.js.html +++ b/docs/dist_scripts_modules_filter.js.html @@ -29,12 +29,13 @@

                                Source: dist/scripts/modules/filter.js

                                import { Filter as FilterEntity } from "../classes/filter.js";
                                 export class Filter {
                                     constructor() {
                                -        this.Spell = this._getInitialFilters();
                                         this.Actor = this._getInitialFilters();
                                         this.Feat = this._getInitialFilters();
                                         this.Item = this._getInitialFilters();
                                -        this.RollTable = this._getInitialFilters();
                                         this.JournalEntry = this._getInitialFilters();
                                +        this.RollTable = this._getInitialFilters();
                                +        this.Scene = this._getInitialFilters();
                                +        this.Spell = this._getInitialFilters();
                                     }
                                     /**
                                      *
                                @@ -130,12 +131,13 @@ 

                                Source: dist/scripts/modules/filter.js

                                return getProperty(this, entityType); } resetFilters() { - this.Spell.activeFilters = {}; + this.Actor.activeFilters = {}; this.Feat.activeFilters = {}; this.Item.activeFilters = {}; - this.Actor.activeFilters = {}; - this.RollTable.activeFilters = {}; this.JournalEntry.activeFilters = {}; + this.RollTable.activeFilters = {}; + this.Scene.activeFilters = {}; + this.Spell.activeFilters = {}; } /** * @@ -156,6 +158,7 @@

                                Source: dist/scripts/modules/filter.js

                                await this.addItemFilters(); await this.addActorFilters(); await this.addRollTableFilters(); + await this.addSceneFilters(); } /** * Used to add custom filters to the Spell-Browser @@ -329,6 +332,16 @@

                                Source: dist/scripts/modules/filter.js

                                story: "Story", }); } + async addSceneFilters() { + const SCENE = 'Scene'; + this.addFilter(SCENE, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.Scene.Dimensions"), 'data', 'select', { + none: "FoundryVTT default", + small: "small", + medium: "medium", + large: "large", + kkkk: "4K" + }); + } }
                            @@ -346,7 +359,7 @@

                            Home

                            Classes

                            • diff --git a/docs/dist_scripts_modules_renderer.js.html b/docs/dist_scripts_modules_renderer.js.html index 2ede631..1989f9d 100644 --- a/docs/dist_scripts_modules_renderer.js.html +++ b/docs/dist_scripts_modules_renderer.js.html @@ -111,7 +111,7 @@

                              Home

                              Classes

                              • diff --git a/docs/dist_scripts_modules_settings.js.html b/docs/dist_scripts_modules_settings.js.html index cc0638b..148b321 100644 --- a/docs/dist_scripts_modules_settings.js.html +++ b/docs/dist_scripts_modules_settings.js.html @@ -43,6 +43,7 @@

                                Source: dist/scripts/modules/settings.js

                                Item: {}, JournalEntry: {}, RollTable: {}, + Scene: {} } }; for (let compendium of game.packs) { @@ -63,14 +64,9 @@

                                Source: dist/scripts/modules/settings.js

                                let defaultSettings = ModuleSettings._getDefaults(); // load settings from container and apply to default settings (available compendia might have changed) let settings = game.settings.get(CMPBrowser.MODULE_NAME, SETTINGS); - for (let compKey in defaultSettings.loadedSpellCompendium) { - if (settings.loadedSpellCompendium[compKey] !== undefined) { - defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; - } - } - for (let compKey in defaultSettings.loadedActorCompendium) { - if (settings.loadedActorCompendium[compKey] !== undefined) { - defaultSettings.loadedActorCompendium[compKey].load = settings.loadedActorCompendium[compKey].load; + for (let compKey in defaultSettings.loadedCompendium) { + if (settings.loadedCompendium[compKey] !== undefined) { + defaultSettings.loadedCompendium[compKey].load = settings.loadedCompendium[compKey].load; } } defaultSettings.allowSpellBrowser = settings.allowSpellBrowser ? true : false; @@ -79,6 +75,7 @@

                                Source: dist/scripts/modules/settings.js

                                defaultSettings.allowActorBrowser = settings.allowActorBrowser ? true : false; defaultSettings.allowJournalEntryBrowser = settings.allowJournalEntryBrowser ? true : false; defaultSettings.allowRollTableBrowser = settings.allowRollTableBrowser ? true : false; + defaultSettings.allowRollTableBrowser = settings.allowSceneBrowser ? true : false; if (game.user.isGM) { game.settings.set(CMPBrowser.MODULE_NAME, SETTINGS, defaultSettings); console.log("New default settings set"); @@ -135,7 +132,7 @@

                                Home

                                Classes

                                • diff --git a/docs/dist_scripts_versioning_version-check.js.html b/docs/dist_scripts_versioning_version-check.js.html index d444638..7465335 100644 --- a/docs/dist_scripts_versioning_version-check.js.html +++ b/docs/dist_scripts_versioning_version-check.js.html @@ -74,7 +74,7 @@

                                  Home

                                  Classes

                                  • diff --git a/docs/hooks_events.js.html b/docs/hooks_events.js.html index 16f096c..0e53160 100644 --- a/docs/hooks_events.js.html +++ b/docs/hooks_events.js.html @@ -85,14 +85,18 @@

                                    Source: hooks/events.js

                                    }); } + /** + * + * @param {HTMLCollection} app + */ static async activateItemListListeners(app = document.getElementsByClassName('window-app')) { app = app[0]; // open entity sheet on click app.querySelectorAll('*[data-action="openSheet"]').forEach(async el => { el.addEventListener('click', async (e) => { - let itemId = e.currentTarget.parentNode.dataset.entryId; - let compendium = e.currentTarget.parentNode.dataset.entryCompendium; + let itemId = e.currentTarget.parentNode.dataset.entityId; + let compendium = e.currentTarget.parentNode.dataset.entityCompendium; let pack = game.packs.find(p => p.collection === compendium); await pack.getEntity(itemId).then(entity => { entity.sheet.render(true); @@ -101,25 +105,46 @@

                                    Source: hooks/events.js

                                    }); // make draggable - //0.4.1: Avoid the game.packs lookup app.querySelectorAll('.draggable').forEach(async li => { li.setAttribute("draggable", true); li.addEventListener('dragstart', event => { - let packName = li.getAttribute("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === packName); - if (!pack) { - event.preventDefault(); - return false; - } event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.entity, - pack: pack.collection, - id: li.getAttribute("data-entry-id") + type: li.dataset.entityType, + pack: li.dataset.entityCompendium, + id: li.dataset.entityId, + name: li.dataset.entityName })); }, false); }); } + /** + * + * @param {HTML|undefined} app + */ + static async registerDropTarget(app = document.getElementsByClassName('window-app')){ + app = app[0]; + let dropTarget = app.querySelector('.droptarget'); + dropTarget.addEventListener('dragover', async (e) => { + e.preventDefault(); + dropTarget.style.border = '1px solid red'; + }); + dropTarget.addEventListener('drop', async (e) => { + e.preventDefault(); + let newEntry = JSON.parse(e.dataTransfer.getData("text/plain")); + + let row = dropTarget.insertRow(); + let compendium = row.insertCell(0); + let name = row.insertCell(1); + name.appendChild(document.createTextNode(newEntry.name)); + compendium.appendChild(document.createTextNode(newEntry.pack)); + + row.dataset.id = newEntry.id; + row.dataset.pack = newEntry.pack; + row.dataset.type = newEntry.type; + }); + } + static async observeListElement(list, tag) { for (let element of list.getElementsByTagName(tag)) { game.compendiumBrowser.observer.observe(element); @@ -131,6 +156,7 @@

                                    Source: hooks/events.js

                                    // toggle visibility of filter containers html.find('.filtercontainer h3, .multiselect label').click(async ev => { + ev.target.classList.toggle('opened'); await $(ev.target.nextElementSibling).toggle(100); }); @@ -175,6 +201,9 @@

                                    Source: hooks/events.js

                                    case 'allow-rolltable-browser': game.compendiumBrowser.settings.allowRollTableBrowser = value; break; + case 'allow-scene-browser': + game.compendiumBrowser.settings.allowSceneBrowser = value; + break; case 'allow-journalentry-browser': game.compendiumBrowser.settings.allowJournalEntryBrowser = value; break; @@ -201,7 +230,7 @@

                                    Source: hooks/events.js

                                    type: 'text', valIsArray: false, value: e.target.value - } + }; } game.compendiumBrowser.replaceList(html, entityType); @@ -227,7 +256,7 @@

                                    Source: hooks/events.js

                                    type: filterType, valIsArray: valIsArray, value: value - } + }; } game.compendiumBrowser.replaceList(html, entityType); @@ -274,7 +303,7 @@

                                    Source: hooks/events.js

                                    value = e.target.closest('.filter').getElementsByTagName('input').val; if (value === '' || operator === 'null') { - delete game.compendiumBrowser.filters[entityType].activeFilters[key] + delete game.compendiumBrowser.filters[entityType].activeFilters[key]; } else { game.compendiumBrowser.filters[entityType].activeFilters[key] = { path: path, @@ -282,7 +311,7 @@

                                    Source: hooks/events.js

                                    valIsArray: false, operator: operator, value: value - } + }; } game.compendiumBrowser.replaceList(html, browserTab); @@ -305,7 +334,7 @@

                                    Home

                                    Classes

                                    • diff --git a/docs/hooks_moduleHooks.js.html b/docs/hooks_moduleHooks.js.html index dfe2fa1..b822421 100644 --- a/docs/hooks_moduleHooks.js.html +++ b/docs/hooks_moduleHooks.js.html @@ -143,7 +143,7 @@

                                      Home

                                      Classes

                                      • diff --git a/docs/index.html b/docs/index.html index 0d0965e..f874154 100644 --- a/docs/index.html +++ b/docs/index.html @@ -262,7 +262,7 @@

                                        Home

                                        Classes

                                        • diff --git a/docs/module.exports.html b/docs/module.exports.html index b596336..b9b8986 100644 --- a/docs/module.exports.html +++ b/docs/module.exports.html @@ -292,7 +292,7 @@

                                          Home

                                          Classes

                                          • diff --git a/docs/scripts_compendium-browser.js.html b/docs/scripts_compendium-browser.js.html index e5703a3..5e0ab47 100644 --- a/docs/scripts_compendium-browser.js.html +++ b/docs/scripts_compendium-browser.js.html @@ -78,7 +78,8 @@

                                            Source: scripts/compendium-browser.js

                                            "modules/compendium-browser/template/entity-list.hbs", "modules/compendium-browser/template/filter-container.hbs", "modules/compendium-browser/template/settings.hbs", - "modules/compendium-browser/template/loading.hbs" + "modules/compendium-browser/template/loading.hbs", + "modules/compendium-browser/template/export.hbs" ]); this.filters.addEntityFilters(); @@ -105,18 +106,20 @@

                                            Source: scripts/compendium-browser.js

                                            items: [], actors: [], filters: { - Spell: this.filters.getByName('Spell'), - Item: this.filters.getByName('Item'), Actor: this.filters.getByName('Actor'), + Item: this.filters.getByName('Item'), + JournalEntry: this.filters.getByName('JounralEntry'), RollTable: this.filters.getByName('RollTable'), - JournalEntry: this.filters.getByName('RollTable') + Spell: this.filters.getByName('Spell'), + Scene: this.filters.getByName('Scene'), }, - showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, + showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, showFeatBrowser: (game.user.isGM) || this.settings.allowFeatBrowser, showItemBrowser: (game.user.isGM) || this.settings.allowItemBrowser, - showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, + showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser, showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser, - showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser, + showSceneBrowser: (game.user.isGM) || this.settings.allowSceneBrowser, + showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, settings: this.settings, isGM: game.user.isGM }; @@ -147,7 +150,8 @@

                                            Source: scripts/compendium-browser.js

                                            Events.activateActionListener(html); Events.activateItemListListeners(html); Events.activateFilterListeners(html); - + Events.registerDropTarget(html); + //Just for the loading image if (this.observer) { html.find(".entity-image").each((i, imageElement) => this.observer.observe(imageElement)); @@ -189,6 +193,8 @@

                                            Source: scripts/compendium-browser.js

                                            this.refreshList = "JournalEntry"; } else if (this.settings.allowRollTableBrowser) { this.refreshList = "RollTable"; + } else if (this.settings.allowRollTableBrowser) { + this.refreshList = "Scene"; } this.render(true); }); @@ -205,7 +211,7 @@

                                            Source: scripts/compendium-browser.js

                                            //After rendering the first time or re-rendering trigger the load/reload of visible data let entityListElement = document.querySelector('.tab.active .browser .cb_entities'); - if (entityListElement.childElementCount !== undefined) { + if (entityListElement && entityListElement.childElementCount !== undefined) { //0.4.2b: On a tab-switch, only reload if there isn't any data already if (options?.reload || entityListElement.childElementCount < 1) { @@ -256,7 +262,7 @@

                                            Home

                                            Classes

                                            • diff --git a/docs/scripts_modules_settings.js.html b/docs/scripts_modules_settings.js.html index c920f91..abab822 100644 --- a/docs/scripts_modules_settings.js.html +++ b/docs/scripts_modules_settings.js.html @@ -46,6 +46,7 @@

                                              Source: scripts/modules/settings.js

                                              Item: {}, JournalEntry: {}, RollTable: {}, + Scene: {} } }; @@ -69,14 +70,9 @@

                                              Source: scripts/modules/settings.js

                                              let defaultSettings = ModuleSettings._getDefaults(); // load settings from container and apply to default settings (available compendia might have changed) let settings = game.settings.get(CMPBrowser.MODULE_NAME, SETTINGS); - for (let compKey in defaultSettings.loadedSpellCompendium) { - if (settings.loadedSpellCompendium[compKey] !== undefined) { - defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; - } - } - for (let compKey in defaultSettings.loadedActorCompendium) { - if (settings.loadedActorCompendium[compKey] !== undefined) { - defaultSettings.loadedActorCompendium[compKey].load = settings.loadedActorCompendium[compKey].load; + for (let compKey in defaultSettings.loadedCompendium) { + if (settings.loadedCompendium[compKey] !== undefined) { + defaultSettings.loadedCompendium[compKey].load = settings.loadedCompendium[compKey].load; } } @@ -86,6 +82,7 @@

                                              Source: scripts/modules/settings.js

                                              defaultSettings.allowActorBrowser = settings.allowActorBrowser ? true : false; defaultSettings.allowJournalEntryBrowser = settings.allowJournalEntryBrowser ? true : false; defaultSettings.allowRollTableBrowser = settings.allowRollTableBrowser ? true : false; + defaultSettings.allowRollTableBrowser = settings.allowSceneBrowser ? true : false; if (game.user.isGM) { game.settings.set(CMPBrowser.MODULE_NAME, SETTINGS, defaultSettings); @@ -146,7 +143,7 @@

                                              Home

                                              Classes

                                              • diff --git a/docs/scripts_versioning_version-check.js.html b/docs/scripts_versioning_version-check.js.html index 8546c8b..e9b9641 100644 --- a/docs/scripts_versioning_version-check.js.html +++ b/docs/scripts_versioning_version-check.js.html @@ -80,7 +80,7 @@

                                                Home

                                                Classes

                                                • diff --git a/hooks/events.js b/hooks/events.js index 7f9fb8c..6661520 100644 --- a/hooks/events.js +++ b/hooks/events.js @@ -57,14 +57,18 @@ export class Events { }); } + /** + * + * @param {HTMLCollection} app + */ static async activateItemListListeners(app = document.getElementsByClassName('window-app')) { app = app[0]; // open entity sheet on click app.querySelectorAll('*[data-action="openSheet"]').forEach(async el => { el.addEventListener('click', async (e) => { - let itemId = e.currentTarget.parentNode.dataset.entryId; - let compendium = e.currentTarget.parentNode.dataset.entryCompendium; + let itemId = e.currentTarget.parentNode.dataset.entityId; + let compendium = e.currentTarget.parentNode.dataset.entityCompendium; let pack = game.packs.find(p => p.collection === compendium); await pack.getEntity(itemId).then(entity => { entity.sheet.render(true); @@ -73,25 +77,46 @@ export class Events { }); // make draggable - //0.4.1: Avoid the game.packs lookup app.querySelectorAll('.draggable').forEach(async li => { li.setAttribute("draggable", true); li.addEventListener('dragstart', event => { - let packName = li.getAttribute("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === packName); - if (!pack) { - event.preventDefault(); - return false; - } event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.entity, - pack: pack.collection, - id: li.getAttribute("data-entry-id") + type: li.dataset.entityType, + pack: li.dataset.entityCompendium, + id: li.dataset.entityId, + name: li.dataset.entityName })); }, false); }); } + /** + * + * @param {HTML|undefined} app + */ + static async registerDropTarget(app = document.getElementsByClassName('window-app')){ + app = app[0]; + let dropTarget = app.querySelector('.droptarget'); + dropTarget.addEventListener('dragover', async (e) => { + e.preventDefault(); + dropTarget.style.border = '1px solid red'; + }); + dropTarget.addEventListener('drop', async (e) => { + e.preventDefault(); + let newEntry = JSON.parse(e.dataTransfer.getData("text/plain")); + + let row = dropTarget.insertRow(); + let compendium = row.insertCell(0); + let name = row.insertCell(1); + name.appendChild(document.createTextNode(newEntry.name)); + compendium.appendChild(document.createTextNode(newEntry.pack)); + + row.dataset.id = newEntry.id; + row.dataset.pack = newEntry.pack; + row.dataset.type = newEntry.type; + }); + } + static async observeListElement(list, tag) { for (let element of list.getElementsByTagName(tag)) { game.compendiumBrowser.observer.observe(element); @@ -103,6 +128,7 @@ export class Events { // toggle visibility of filter containers html.find('.filtercontainer h3, .multiselect label').click(async ev => { + ev.target.classList.toggle('opened'); await $(ev.target.nextElementSibling).toggle(100); }); @@ -147,6 +173,9 @@ export class Events { case 'allow-rolltable-browser': game.compendiumBrowser.settings.allowRollTableBrowser = value; break; + case 'allow-scene-browser': + game.compendiumBrowser.settings.allowSceneBrowser = value; + break; case 'allow-journalentry-browser': game.compendiumBrowser.settings.allowJournalEntryBrowser = value; break; @@ -173,7 +202,7 @@ export class Events { type: 'text', valIsArray: false, value: e.target.value - } + }; } game.compendiumBrowser.replaceList(html, entityType); @@ -199,7 +228,7 @@ export class Events { type: filterType, valIsArray: valIsArray, value: value - } + }; } game.compendiumBrowser.replaceList(html, entityType); @@ -246,7 +275,7 @@ export class Events { value = e.target.closest('.filter').getElementsByTagName('input').val; if (value === '' || operator === 'null') { - delete game.compendiumBrowser.filters[entityType].activeFilters[key] + delete game.compendiumBrowser.filters[entityType].activeFilters[key]; } else { game.compendiumBrowser.filters[entityType].activeFilters[key] = { path: path, @@ -254,7 +283,7 @@ export class Events { valIsArray: false, operator: operator, value: value - } + }; } game.compendiumBrowser.replaceList(html, browserTab); diff --git a/lang/de.json b/lang/de.json index 54912b9..48731fd 100644 --- a/lang/de.json +++ b/lang/de.json @@ -3,25 +3,27 @@ "CMPBrowser.sortBy":"Sortieren nach", "CMPBrowser.cr":"Challenge Rating", "CMPBrowser.generalSettings":"Allgemeine Einstellungen", - "CMPBrowser.allowSpellAcc":"Erlaube Spielern Zauber zu durchsuchen.", - "CMPBrowser.allowActorAcc":"Player access to actor browser", - "CMPBrowser.allowRolltable": "Player access to rolltable browser", - "CMPBrowser.allowJournalEntry": "Player Access to the JournalEntry browser", + "CMPBrowser.allowSpellAcc":"Zauber", + "CMPBrowser.allowFeatAcc":"Feats", + "CMPBrowser.allowActorAcc":"Akteure", + "CMPBrowser.allowRolltable": "Rolltables", + "CMPBrowser.allowScene": "Szenen", + "CMPBrowser.allowJournalEntry": "Journal Einträge", "CMPBrowser.compSettingsSpell":"Gegenstand Kompendium Einstellungen", - "CMPBrowser.compSettingsNpc":"Actor Kompendium Einstellungen", - "CMPBrowser.Filters.ResetFilters" : "Reset Filters", - "CMPBrowser.Tab.SpellBrowser":"Zauber Browser", - "CMPBrowser.Tab.FeatBrowser": "Feat Browser", - "CMPBrowser.Tab.ItemBrowser": "Gegenstand Browser", - "CMPBrowser.Tab.NPCBrowser":"NPC Browser", - "CMPBrowser.Tab.ActorBrowser":"Actor Browser", - "CMPBrowser.Tab.RolltableBrowser": "Rolltable Browser", - "CMPBrowser.Tab.JournalEntryBrowser": "JournalEntry Browser", + "CMPBrowser.compSettingsNpc":"Akteur Kompendium Einstellungen", + "CMPBrowser.Filters.ResetFilters" : "Filter zurücksetzen", + "CMPBrowser.Tab.SpellBrowser":"Zauber", + "CMPBrowser.Tab.FeatBrowser": "Feats", + "CMPBrowser.Tab.ItemBrowser": "Gegenstände", + "CMPBrowser.Tab.ActorBrowser":"Akteure", + "CMPBrowser.Tab.RolltableBrowser": "Rolltables", + "CMPBrowser.Tab.SceneBrowser": "Szenen", + "CMPBrowser.Tab.JournalEntryBrowser": "Journal Einträge", "CMPBrowser.Tab.Settings":"Einstellungen", - "CMPBrowser.SETTING.Maxload.NAME" : "Maximum load", - "CMPBrowser.SETTING.Maxload.HINT" : "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", - "CMPBrowser.LOADING.Message" : "Geladen: {numLoaded} {entityType}s ({numPacks} Kompendia)", - "CMPBrowser.LOADING.MaxLoaded" : "(maximum displayed; to see more, use the filters)", + "CMPBrowser.SETTING.Maxload.NAME" : "Maximum Resultate", + "CMPBrowser.SETTING.Maxload.HINT" : "Maximale Anzahl an Resultaten für jeden Typ. Bitte über Filter die Suche eingrenzen. Diese Einstellung erlaubt Speicher und Server performance zu optimieren.", + "CMPBrowser.LOADING.Message" : "Geladen: {numLoaded} {entityType}s (aus {numPacks} Kompendia)", + "CMPBrowser.LOADING.MaxLoaded" : "(Maximale Resultate, Bitte die Suche eingrenzen.)", "CMPBrowser.load":"Laden", "CMPBrowser.lvl":"Level", "CMPBrowser.ritual":"Ritual", @@ -55,17 +57,17 @@ "CMPBrowser.aberration": "Aberration", "CMPBrowser.beast": "Beast", "CMPBrowser.celestial": "Göttlich", - "CMPBrowser.construct": "construct", + "CMPBrowser.construct": "Konstrukt", "CMPBrowser.dragon": "Drachen", - "CMPBrowser.elemental": "Elemental", - "CMPBrowser.fey": "Fey", + "CMPBrowser.elemental": "Elementar", + "CMPBrowser.fey": "Fee", "CMPBrowser.fiend": "Fiend", - "CMPBrowser.giant": "Giant", + "CMPBrowser.giant": "Riesen", "CMPBrowser.humanoid": "Humanoid", - "CMPBrowser.monstrosity": "Monstrosity", + "CMPBrowser.monstrosity": "Monstrosität", "CMPBrowser.ooze": "Ooze", - "CMPBrowser.plant": "Plant", - "CMPBrowser.undead": "Undead", + "CMPBrowser.plant": "Pflanze", + "CMPBrowser.undead": "Untot", "CMPBrowser.abilities": "Fähigkeiten", "CMPBrowser.dmgInteraction": "Damage Interaction", "CMPBrowser.dmgDealt": "Damage Dealt", diff --git a/lang/en.json b/lang/en.json index d21f092..316da78 100644 --- a/lang/en.json +++ b/lang/en.json @@ -3,12 +3,14 @@ "CMPBrowser.sortBy":"Sort by", "CMPBrowser.cr":"Challenge Rating", "CMPBrowser.generalSettings":"General Settings", - "CMPBrowser.allowSpellAcc":"Players access to the Spell browser", - "CMPBrowser.allowActorAcc":"Player access to Actor browser", - "CMPBrowser.allowRolltable": "Player access to RollTable browser", - "CMPBrowser.allowJournalEntry": "Player Access to the JournalEntry browser", - "CMPBrowser.compSettingsSpell":"Item Compendium Settings", - "CMPBrowser.compSettingsNpc":"Actor Compendium Settings", + "CMPBrowser.allowSpellAcc":"Spells", + "CMPBrowser.allowActorAcc":"Actors", + "CMPBrowser.allowFeatAcc":"Feats", + "CMPBrowser.allowRolltable": "RollTable", + "CMPBrowser.allowJournalEntry": "JournalEntry", + "CMPBrowser.allowSceneBrowser": "Scenes", + "CMPBrowser.compSettingsSpell":"Items", + "CMPBrowser.compSettingsNpc":"Actors", "CMPBrowser.load":"Load", "CMPBrowser.lvl":"Level", "CMPBrowser.ritual":"Ritual", @@ -62,6 +64,7 @@ "CMPBrowser.Tab.FeatBrowser": "Feats", "CMPBrowser.Tab.ItemBrowser": "Items", "CMPBrowser.Tab.RollTableBrowser": "Rolltables", + "CMPBrowser.Tab.SceneBrowser": "Scenes", "CMPBrowser.Tab.JournalEntryBrowser": "JournalEntries", "CMPBrowser.Tab.Settings":"Settings", "CMPBrowser.SETTING.Maxload.NAME" : "Maximum load", diff --git a/module.json b/module.json index 71d8515..4cca9fb 100644 --- a/module.json +++ b/module.json @@ -2,9 +2,9 @@ "name": "compendium-browser", "title": "Compendium Browser", "description": "

                                                  Easily browse and filter spells, feats, items, and npcs loaded from compendia!

                                                  NEW! Compendium Browser is faster and better-behaved; it no longer loads all the compendia into memory on start-up (which sometimes hung servers because of memory or CPU requirements). Instead, it filters and loads on-demand, as well as giving you a Module Setting to control how many rows are loaded at a time.
                                                  Changes in v0.5.0:Fixed: Issue #17: Error in Foundry 0.8.x when filtering NPC by Creature Type
                                                  Changes in v0.4.5:
                                                  • Fixed: Spells from non-system compendium show up in items tab.(Issue #10)
                                                  • Added: Show compendium source in results (Issue #11)
                                                  ", - "version": "0.5.2", + "version": "0.7.9", "minimumCoreVersion": "0.6.2", - "compatibleCoreVersion": "0.8.8", + "compatibleCoreVersion": "0.8.9", "allowBugReporter": true, "author": "Spetzel#0103", "authors": [ @@ -24,8 +24,8 @@ "dnd5e" ], "includes": [ - "./hooks/**", - "./scripts/**" + "./hooks/*.js", + "./scripts/*.js" ], "esmodules": [ "./compendium-browser.js" diff --git a/package.json b/package.json index e762f83..dcc1fd4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "A browser with simple UI to browse and filter entites from a compendium in FoundryVTT", "main": "compendium-browser.js", + "type": "module", "directories": { "doc": "docs" }, diff --git a/scripts/classes/decoratedEntity.ts b/scripts/classes/decoratedEntity.ts index 1d68f6e..a845ab7 100644 --- a/scripts/classes/decoratedEntity.ts +++ b/scripts/classes/decoratedEntity.ts @@ -170,6 +170,8 @@ export class decoratedEntity extends compactEntity { decorated.classRequirement = classes.split(','); } break; + case 'Scene': + decorated.data.dimensions = entityData.height + ' * ' + entityData.width; } return decorated; diff --git a/scripts/compendium-browser.js b/scripts/compendium-browser.js index 1814fec..30b2228 100644 --- a/scripts/compendium-browser.js +++ b/scripts/compendium-browser.js @@ -50,7 +50,8 @@ export class CompendiumBrowser extends Application { "modules/compendium-browser/template/entity-list.hbs", "modules/compendium-browser/template/filter-container.hbs", "modules/compendium-browser/template/settings.hbs", - "modules/compendium-browser/template/loading.hbs" + "modules/compendium-browser/template/loading.hbs", + "modules/compendium-browser/template/export.hbs" ]); this.filters.addEntityFilters(); @@ -77,18 +78,20 @@ export class CompendiumBrowser extends Application { items: [], actors: [], filters: { - Spell: this.filters.getByName('Spell'), - Item: this.filters.getByName('Item'), Actor: this.filters.getByName('Actor'), + Item: this.filters.getByName('Item'), + JournalEntry: this.filters.getByName('JounralEntry'), RollTable: this.filters.getByName('RollTable'), - JournalEntry: this.filters.getByName('RollTable') + Spell: this.filters.getByName('Spell'), + Scene: this.filters.getByName('Scene'), }, - showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, + showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, showFeatBrowser: (game.user.isGM) || this.settings.allowFeatBrowser, showItemBrowser: (game.user.isGM) || this.settings.allowItemBrowser, - showActorBrowser: (game.user.isGM) || this.settings.allowActorBrowser, + showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser, showRollTableBrowser: (game.user.isGM) || this.settings.allowRollTableBrowser, - showJournalEntryBrowser: (game.user.isGM) || this.settings.allowJournalEntryBrowser, + showSceneBrowser: (game.user.isGM) || this.settings.allowSceneBrowser, + showSpellBrowser: (game.user.isGM) || this.settings.allowSpellBrowser, settings: this.settings, isGM: game.user.isGM }; @@ -119,7 +122,8 @@ export class CompendiumBrowser extends Application { Events.activateActionListener(html); Events.activateItemListListeners(html); Events.activateFilterListeners(html); - + Events.registerDropTarget(html); + //Just for the loading image if (this.observer) { html.find(".entity-image").each((i, imageElement) => this.observer.observe(imageElement)); @@ -161,6 +165,8 @@ export class CompendiumBrowser extends Application { this.refreshList = "JournalEntry"; } else if (this.settings.allowRollTableBrowser) { this.refreshList = "RollTable"; + } else if (this.settings.allowRollTableBrowser) { + this.refreshList = "Scene"; } this.render(true); }); @@ -177,7 +183,7 @@ export class CompendiumBrowser extends Application { //After rendering the first time or re-rendering trigger the load/reload of visible data let entityListElement = document.querySelector('.tab.active .browser .cb_entities'); - if (entityListElement.childElementCount !== undefined) { + if (entityListElement && entityListElement.childElementCount !== undefined) { //0.4.2b: On a tab-switch, only reload if there isn't any data already if (options?.reload || entityListElement.childElementCount < 1) { diff --git a/scripts/modules/entities.ts b/scripts/modules/entities.ts index e11bf62..193c2d5 100644 --- a/scripts/modules/entities.ts +++ b/scripts/modules/entities.ts @@ -96,12 +96,14 @@ export class Entities { compact.orderSize = decorated.filterSize; compact.data.details = decorated.data.details; break; + case "Scene": + compact.img = currentEntity.data.thumb; default: break; } comp_list.addEntity(compact); - if (updateLoading) { Renderer.updateLoading(entityType, numItemsLoaded, numPacks, 500); } + if (updateLoading) {ui.notifications.info(`Loaded ${numItemsLoaded} ${entityType}s from ${numPacks} Compendia.`);} if (numItemsLoaded++ >= maxLoad) break; } }); // get Entities @@ -149,6 +151,7 @@ export class Entities { ['Feats', 'data.class'], ['RollTable', 'compendium'], ['JounralEntry', 'name'], + ['Scene', 'name'], ]); list.entities.sort((left: compactEntity, right: compactEntity) => { diff --git a/scripts/modules/filter.ts b/scripts/modules/filter.ts index 5e82e8e..609d3f7 100644 --- a/scripts/modules/filter.ts +++ b/scripts/modules/filter.ts @@ -1,12 +1,13 @@ import { Filter as FilterEntity } from "../classes/filter.js"; -export class Filter { - public Spell: any = this._getInitialFilters(); +export class Filter { public Actor: any = this._getInitialFilters(); public Feat: any = this._getInitialFilters(); public Item: any = this._getInitialFilters(); - public RollTable: any = this._getInitialFilters(); public JournalEntry: any = this._getInitialFilters(); + public RollTable: any = this._getInitialFilters(); + public Scene: any = this._getInitialFilters(); + public Spell: any = this._getInitialFilters(); /** * @@ -89,12 +90,14 @@ export class Filter { } resetFilters() { - this.Spell.activeFilters = {}; + this.Actor.activeFilters = {}; this.Feat.activeFilters = {}; this.Item.activeFilters = {}; - this.Actor.activeFilters = {}; - this.RollTable.activeFilters = {}; - this.JournalEntry.activeFilters = {}; + this.JournalEntry.activeFilters = {}; + this.RollTable.activeFilters = {}; + this.Scene.activeFilters = {}; + this.Spell.activeFilters = {}; + } /** @@ -117,6 +120,7 @@ export class Filter { await this.addItemFilters(); await this.addActorFilters(); await this.addRollTableFilters(); + await this.addSceneFilters(); } /** @@ -320,4 +324,15 @@ export class Filter { story: "Story", }) } + + async addSceneFilters() { + const SCENE = 'Scene'; + this.addFilter(SCENE, game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.Scene.Dimensions"), 'data', 'select', { + none: "FoundryVTT default", + small: "small", + medium: "medium", + large: "large", + kkkk: "4K" + }) + } } \ No newline at end of file diff --git a/scripts/modules/settings.js b/scripts/modules/settings.js index 71d8749..104580a 100644 --- a/scripts/modules/settings.js +++ b/scripts/modules/settings.js @@ -18,6 +18,7 @@ export class ModuleSettings { Item: {}, JournalEntry: {}, RollTable: {}, + Scene: {} } }; @@ -41,14 +42,9 @@ export class ModuleSettings { let defaultSettings = ModuleSettings._getDefaults(); // load settings from container and apply to default settings (available compendia might have changed) let settings = game.settings.get(CMPBrowser.MODULE_NAME, SETTINGS); - for (let compKey in defaultSettings.loadedSpellCompendium) { - if (settings.loadedSpellCompendium[compKey] !== undefined) { - defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; - } - } - for (let compKey in defaultSettings.loadedActorCompendium) { - if (settings.loadedActorCompendium[compKey] !== undefined) { - defaultSettings.loadedActorCompendium[compKey].load = settings.loadedActorCompendium[compKey].load; + for (let compKey in defaultSettings.loadedCompendium) { + if (settings.loadedCompendium[compKey] !== undefined) { + defaultSettings.loadedCompendium[compKey].load = settings.loadedCompendium[compKey].load; } } @@ -58,6 +54,7 @@ export class ModuleSettings { defaultSettings.allowActorBrowser = settings.allowActorBrowser ? true : false; defaultSettings.allowJournalEntryBrowser = settings.allowJournalEntryBrowser ? true : false; defaultSettings.allowRollTableBrowser = settings.allowRollTableBrowser ? true : false; + defaultSettings.allowRollTableBrowser = settings.allowSceneBrowser ? true : false; if (game.user.isGM) { game.settings.set(CMPBrowser.MODULE_NAME, SETTINGS, defaultSettings); diff --git a/styles/app.css b/styles/app.css index 0d96a6f..dffec64 100644 --- a/styles/app.css +++ b/styles/app.css @@ -8,7 +8,7 @@ border: 1px solid #acacac; display: inline-block; margin: auto; - background: rgba(0, 0, 0, 0.3); + background: var(--background); border-radius: 0.3em; color: #cecece; } @@ -101,3 +101,312 @@ border-left: 1px dotted rgba(0, 0, 0, 0.05); border-radius: 0; } +.compendium-browser .control-area { + color: var(--primary-font); + display: grid; + grid-gap: 0; + height: calc(100% -40px); + width: max-content; + border-color: var(--header-border); + border-style: solid; + border-width: 0 1px 0 1px; + padding: 0.3em; + left: -1px; + right: -1px; + background: var(--background); + box-shadow: 0 0 0.3em 0.3em var(--context-shadow); + position: relative; + /* Toggler Functionality */ +} +.compendium-browser .control-area.l_sidebar { + grid-template-columns: 1fr 2em; + grid-template-areas: "CONTENT TOGGLER"; +} +.compendium-browser .control-area.r_sidebar { + grid-template-columns: 2em 1fr; + grid-template-areas: "TOGGLER CONTENT"; +} +.compendium-browser .control-area button { + background: rgba(0, 0, 0, 0.05); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; +} +.compendium-browser .control-area .toggler { + display: inline; + grid-area: TOGGLER; + font-size: 0; + width: 0; + height: 0; + color: var(--primary-color); +} +.compendium-browser .control-area .toggler:after { + cursor: pointer; + content: "\f0b0"; + display: inline-block; + height: 16px; + width: 16px; + font-size: 16px; + position: absolute; + border-radius: 3px; + background: none; + color: darkgrey; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + text-rendering: auto; + font-variant: normal; +} +.compendium-browser .control-area .toggler:hover:before { + content: 'toggle sidebar'; + position: absolute; + display: block; + font-size: 14px; + background: #333; + padding: 5px; + left: 30px; + /* top: -20px; */ + width: max-content; + border: 1px solid grey; + border-radius: 0 3px 3px 3px; + z-index: 999; +} +.compendium-browser .control-area .toggler:checked:after { + color: var(--checkbox-checked); +} +.compendium-browser .control-area .toggler ~ .sidebar_content { + display: none; +} +.compendium-browser .control-area .toggler:checked ~ .sidebar_content { + display: initial; + grid-area: CONTENT; + width: max-content; + position: relative; +} +.compendium-browser .control-area .filtercontainer { + margin-top: 5px; + padding: 2px; +} +.compendium-browser .control-area .filtercontainer h3 { + margin: 0; + cursor: pointer; +} +.compendium-browser .control-area .filtercontainer h3 :after { + display: inline-block; + height: auto; + content: "\25BE"; + position: absolute; + right: 0.2em; + font-size: 1.2em; +} +.compendium-browser .control-area .filtercontainer h3.opened:after { + content: "\25B4"; +} +.compendium-browser .control-area .filtercontainer dl { + margin: 0; +} +.compendium-browser .control-area .filtercontainer div { + margin: 5px 0; +} +.compendium-browser .control-area .filtercontainer dt { + display: inline-block; + width: 40%; + padding-left: 5px; + font-weight: 400; +} +.compendium-browser .control-area .filtercontainer dd { + display: inline-block; + width: 58%; + margin-left: 0; +} +.compendium-browser .control-area .filtercontainer dd select { + width: 100%; +} +.compendium-browser .control-area .filtercontainer input, +.compendium-browser .control-area .filtercontainer select { + height: 1.4em; + font-size: 0.9em; + font-weight: 400; +} +.compendium-browser .control-area .filtercontainer .multiselect { + border: 1px solid #bbb; + border-radius: 3px; + vertical-align: middle; + line-height: 32px; + margin: 2px 0; +} +.compendium-browser .control-area .filtercontainer .multiselect label { + padding: 5px; +} +.compendium-browser .control-area .filtercontainer .multiselect input { + vertical-align: middle; +} +.compendium-browser .control-area .filtercontainer .small-input { + width: calc(100% - 44px); + height: 27px; + background: rgba(0, 0, 0, 0.05); + border: 1px solid #444; + border-radius: 3px; + padding: 0 3px; + text-overflow: ellipsis; +} +.compendium-browser .control-area .filtercontainer .small-select { + width: 40px; +} +.compendium-browser .browser { + display: grid; + grid-template-columns: 2em auto 2em; + grid-template-areas: "L_SIDEBAR CONTENT R_SIDEBAR"; + height: 100%; + overflow: hidden !important; +} +.compendium-browser .browser .window-content { + overflow-y: hidden !important; +} +.compendium-browser .browser ul .filter-tags { + display: none; +} +.compendium-browser .browser ul li span { + white-space: nowrap; + overflow: hidden; +} +.compendium-browser .browser .spacer { + display: inline-block; + min-width: 5px; +} +.compendium-browser .browser .spacer-large { + display: inline-block; + min-width: 15px; +} +.compendium-browser .browser.entity-browser > li { + cursor: default; +} +.compendium-browser .browser.entity-browser li .feat-tags { + text-align: justify; + margin-right: 3px; + margin-left: 3px; + text-transform: capitalize; +} +.compendium-browser .browser.entity-browser li .item-tags { + text-align: justify; + margin-right: 3px; + margin-left: 3px; + text-transform: capitalize; +} +.compendium-browser .browser .list-area { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(12vw, 100%), 1fr)); + grid-template-rows: auto; + grid-area: CONTENT; +} +.compendium-browser .cb_entities .entity-header { + display: grid; + grid-template-columns: 0.55fr 0.1fr 1fr; + gap: 0px 2.5em; + text-align: center; + border: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 3px; +} +.compendium-browser .cb_entities .entity { + background: var(--background); + color: darkgrey; + border-radius: 5px; + margin: 0.2em; + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 0.1em; + height: clamp(5vh, 6vh, 7vh); + box-shadow: 0 0 3px inset var(--light-color); + display: grid; + grid-template-columns: 3em 1fr; + grid-template-areas: "Image Name" "Image Tags"; +} +.compendium-browser .cb_entities .entity:hover { + box-shadow: 0 0 3px inset var(--icon-hover); +} +.compendium-browser .cb_entities .entity .entity-image { + cursor: pointer; + background-repeat: no-repeat; + border: 1px solid rgba(83, 75, 75, 0.3); + padding: 0.1em; + border-radius: 0.5em; + justify-content: center; + align-items: center; + background-clip: border-box; + background-image: url('/icons/svg/daze.svg'); + background-size: cover; + width: 3.2em; + grid-area: Image; + box-shadow: 0 0 0.2em 0.1em inset rgba(43, 45, 46, 0.863); +} +.compendium-browser .cb_entities .entity .entity-name { + grid-area: Name; + color: rgba(236, 229, 229, 0.932); + font-size: 12px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 0.9vw; + font-weight: 400; + overflow: hidden; + height: 100%; +} +.compendium-browser .cb_entities .entity ul.tags { + list-style: none; + min-width: initial; + box-sizing: content-box; + grid-area: Tags; + display: grid; + grid-template-columns: repeat(auto-fit, 16px); + margin: 0; +} +.compendium-browser .cb_entities .entity .tags .spell-tag { + color: var(--background); +} +.compendium-browser .cb_entities .entity .tags .spell-tag.active { + color: var(--primary-font); +} +.compendium-browser .cb_entities .entity .tags li { + cursor: help; + color: var(--faint-color); + justify-content: center; + align-items: center; + height: 1.6em; + width: 1.6em; + font-size: 0.8em; + line-height: 1.6em; + display: inline-block; + padding: 0.2em; + border-radius: 0.1em; +} +.compendium-browser .cb_entities .entity.r_common { + border-left: 0.3em solid #ffffff; +} +.compendium-browser .cb_entities .entity.r_common .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#ffffff), 0.75); +} +.compendium-browser .cb_entities .entity.r_uncommon { + border-color: #1eff00; + background: linear-gradient(90deg, #1eff00 0%, var(--background) 35%); +} +.compendium-browser .cb_entities .entity.r_uncommon .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#1eff00), 0.75); +} +.compendium-browser .cb_entities .entity.r_rare { + border-color: #0070dd; + background: linear-gradient(90deg, #0070dd 0%, var(--background) 35%); +} +.compendium-browser .cb_entities .entity.r_rare .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#0070dd), 0.75); +} +.compendium-browser .cb_entities .entity.r_veryrare { + border-color: #a335ee; + background: linear-gradient(90deg, #a335ee 0%, var(--background) 35%); +} +.compendium-browser .cb_entities .entity.r_veryrare .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#a335ee), 0.75); +} +.compendium-browser .cb_entities .entity.r_legendary { + border-color: #ff8000; + background: linear-gradient(90deg, #ff8000 0%, var(--background) 35%); +} +.compendium-browser .cb_entities .entity.r_legendary .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#ff8000), 0.75); +} diff --git a/styles/app.less b/styles/app.less index 570c76a..b0667d8 100644 --- a/styles/app.less +++ b/styles/app.less @@ -10,7 +10,7 @@ border: 1px solid #acacac; display: inline-block; margin: auto; - background: rgba(0,0,0,0.3); + background: var(--background); border-radius: 0.3em; color: #cecece; } @@ -60,9 +60,7 @@ overflow: scroll; } } - - - + .settings { .settings-group { border: 1px solid #bbb; @@ -86,6 +84,7 @@ } } } + .actions { flex: 0 0 32px; box-sizing: content-box; @@ -109,6 +108,8 @@ border-radius: 0; } } - } - -} \ No newline at end of file + } +} + +@import 'components/sidebars.less'; +@import 'components/entity-browser.less'; \ No newline at end of file diff --git a/styles/components/entity-browser.css b/styles/components/entity-browser.css index eae4573..d956206 100644 --- a/styles/components/entity-browser.css +++ b/styles/components/entity-browser.css @@ -39,7 +39,135 @@ text-transform: capitalize; } .compendium-browser .browser .list-area { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(12vw, 100%), 1fr)); grid-template-rows: auto; - grid-template-columns: auto; grid-area: CONTENT; } +.compendium-browser .browser .cb_entities .entity-header { + display: grid; + grid-template-columns: 0.55fr 0.1fr 1fr; + gap: 0px 2.5em; + text-align: center; + border: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 3px; +} +.compendium-browser .browser .cb_entities .entity { + background: var(--background); + color: darkgrey; + border-radius: 5px; + margin: 0.2em; + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 0.1em; + height: clamp(5vh, 6vh, 7vh); + box-shadow: 0 0 3px inset var(--light-color); + display: grid; + grid-template-columns: 0.2fr 1fr; + grid-column-gap: 5px; + grid-template-areas: "Image Name" "Image Tags"; +} +.compendium-browser .browser .cb_entities .entity:hover { + box-shadow: 0 0 3px inset var(--icon-hover); +} +.compendium-browser .browser .cb_entities .entity .entity-image { + cursor: pointer; + background-repeat: no-repeat; + border: 1px solid rgba(83, 75, 75, 0.3); + padding: 0.1em; + border-radius: 0.5em; + justify-content: center; + align-items: center; + background-clip: border-box; + background-image: url('/icons/svg/daze.svg'); + background-size: cover; + width: 3.2em; + grid-area: Image; + box-shadow: 0 0 0.2em 0.1em inset rgba(43, 45, 46, 0.863); +} +.compendium-browser .browser .cb_entities .entity .entity-name { + grid-area: Name; + color: rgba(236, 229, 229, 0.932); + font-size: 12px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 0.9vw; + font-weight: 400; + overflow: hidden; + height: 100%; +} +.compendium-browser .browser .cb_entities .entity ul.tags { + list-style: none; + min-width: initial; + box-sizing: content-box; + grid-area: Tags; + display: grid; + grid-template-columns: repeat(auto-fit, 16px); + margin: 0; +} +.compendium-browser .browser .cb_entities .entity .tags .spell-tag { + color: var(--background); +} +.compendium-browser .browser .cb_entities .entity .tags .spell-tag.active { + color: var(--primary-font); +} +.compendium-browser .browser .cb_entities .entity .tags li { + cursor: help; + color: var(--faint-color); + justify-content: center; + align-items: center; + height: 1.6em; + width: 1.6em; + font-size: 0.8em; + line-height: 1.6em; + display: inline-block; + padding: 0.2em; + border-radius: 0.1em; +} +.compendium-browser .browser .cb_entities .entity.r_common { + border-left: 0.3em solid #ffffff; +} +.compendium-browser .browser .cb_entities .entity.r_common .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#ffffff), 0.75); +} +.compendium-browser .browser .cb_entities .entity.r_uncommon { + border-color: #1eff00; + background: linear-gradient(90deg, #1eff00 0%, var(--background) 35%); +} +.compendium-browser .browser .cb_entities .entity.r_uncommon .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#1eff00), 0.75); +} +.compendium-browser .browser .cb_entities .entity.r_rare { + border-color: #0070dd; + background: linear-gradient(90deg, #0070dd 0%, var(--background) 35%); +} +.compendium-browser .browser .cb_entities .entity.r_rare .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#0070dd), 0.75); +} +.compendium-browser .browser .cb_entities .entity.r_veryrare { + border-color: #a335ee; + background: linear-gradient(90deg, #a335ee 0%, var(--background) 35%); +} +.compendium-browser .browser .cb_entities .entity.r_veryrare .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#a335ee), 0.75); +} +.compendium-browser .browser .cb_entities .entity.r_legendary { + border-color: #ff8000; + background: linear-gradient(90deg, #ff8000 0%, var(--background) 35%); +} +.compendium-browser .browser .cb_entities .entity.r_legendary .entity-image { + box-shadow: 0 0 0.2em 0.1em inset rgba(var(#ff8000), 0.75); +} +.compendium-browser .tab.active[data-tab="Scene"] .cb_entities .entity { + grid-template-areas: "Name" "Image"; + grid-template-columns: 1fr; + grid-template-rows: 0.2fr 1fr; +} +.compendium-browser .tab.active[data-tab="Scene"] .cb_entities .entity .entity-image { + width: initial; + height: initial; +} +.compendium-browser .tab.active[data-tab="Scene"] .cb_entities .entity .entity-name { + position: absolute; + z-index: 999; + padding: 0.5em; + font-size: 1.4em; +} diff --git a/styles/components/entity-browser.less b/styles/components/entity-browser.less index d776827..a5f53c8 100644 --- a/styles/components/entity-browser.less +++ b/styles/components/entity-browser.less @@ -1,3 +1,6 @@ +@import (reference) '../variables.less'; +@import '../mixins.less'; + .compendium-browser { .browser { display: grid; @@ -49,135 +52,161 @@ } } .list-area { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(12vw, 100%), 1fr)); grid-template-rows: auto; - grid-template-columns: auto; grid-area: CONTENT; } - } - - .cb_entities { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(15vw, 100%), 1fr)); - - .entity-header { - display: grid; - grid-template-columns: 0.55fr 0.1fr 1fr; - gap: 0px 2.5em; - text-align: center; - border: 1px dotted rgba(0, 0, 0, 0.05); - border-radius: 3px; - } - .entity { - background: var(--background); - color: darkgrey; - border-radius: 5px; - margin: 0.2em; - border: 1px solid rgba(0,0,0,0.2); - padding: 0.1em; - height: clamp(5vh,6vh,7vh); - box-shadow: 0 0 3px inset var(--light-color); - display: grid; - grid-template-columns: 3em 1fr; - grid-template-areas: - "Image Name" - "Image Tags"; - - &:hover { - box-shadow: 0 0 3px inset var(--icon-hover); - } - .entity-image { - cursor: pointer; - background-repeat: no-repeat; - background-size: cover; - border: 1px solid rgba(83, 75, 75, 0.3); - padding: 0.1em; - border-radius: 0.5em; - justify-content: center; - align-items: center; - background-clip: border-box; - background-image: url('/icons/svg/daze.svg'); - background-size: cover; - width: 3.2em; - grid-area: Image; - box-shadow: 0 0 0.2em 0.1em inset rgba(43, 45, 46, 0.863); + .cb_entities { + .entity-header { + display: grid; + grid-template-columns: 0.55fr 0.1fr 1fr; + gap: 0px 2.5em; + text-align: center; + border: 1px dotted rgba(0, 0, 0, 0.05); + border-radius: 3px; } - .entity-name { - grid-area: Name; - color:rgba(236, 229, 229, 0.932); - font-size: 12px; - font-family: "Modesto Condensed", "Palatino Linotype", serif; - font-size: 0.9vw; - font-weight: 400; - overflow: hidden; - height: 100%; - } - - ul.tags { - list-style: none; - min-width: initial; - box-sizing: content-box; - grid-area: Tags; + .entity { + background: var(--background); + color: darkgrey; + border-radius: 5px; + margin: 0.2em; + border: 1px solid rgba(0,0,0,0.2); + padding: 0.1em; + height: clamp(5vh,6vh,7vh); + box-shadow: 0 0 3px inset var(--light-color); + display: grid; - grid-template-columns: repeat(auto-fit, 16px); - margin: 0; - } - .tags { - .spell-tag { - color: var(--background); - &.active { - color: var(--primary-font); - } + grid-template-columns: 0.2fr 1fr; + grid-column-gap: 5px; + grid-template-areas: + "Image Name" + "Image Tags"; + + &:hover { + box-shadow: 0 0 3px inset var(--icon-hover); } - li { - cursor: help; - color: darkgray; + .entity-image { + cursor: pointer; + background-repeat: no-repeat; + background-size: cover; + border: 1px solid rgba(83, 75, 75, 0.3); + padding: 0.1em; + border-radius: 0.5em; justify-content: center; align-items: center; - height: 1.6em; - width: 1.6em; - font-size: 0.8em; - line-height: 1.6em; - display: inline-block; - padding: 0.2em; - border-radius: 0.1em; + background-clip: border-box; + background-image: url('/icons/svg/daze.svg'); + background-size: cover; + width: 3.2em; + grid-area: Image; + box-shadow: 0 0 0.2em 0.1em inset rgba(43, 45, 46, 0.863); } - } - - &.r_common { - border-left: 0.3em solid @r_common; - .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(@r_common, 0.75); + .entity-name { + grid-area: Name; + color:rgba(236, 229, 229, 0.932); + font-size: 12px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 0.9vw; + font-weight: 400; + overflow: hidden; + height: 100%; } - } - &.r_uncommon { - border-color: rgb(61, 187, 45); - background: linear-gradient(90deg, rgba(@r_uncommon,1) 0%, rgba(163,53,238,0) 35%); - .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(@r_uncommon, 0.75); + + ul.tags { + list-style: none; + min-width: initial; + box-sizing: content-box; + grid-area: Tags; + display: grid; + grid-template-columns: repeat(auto-fit, 16px); + margin: 0; } - } - &.r_rare { - border-color: darkblue; - background: linear-gradient(90deg, rgba(@r_rare,1) 0%, rgba(0,0,0,0.2) 35%); - .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(@r_rare, 0.75); + .tags { + .spell-tag { + color: var(--background); + &.active { + color: var(--primary-font); + } + } + li { + cursor: help; + color: var(--faint-color); + justify-content: center; + align-items: center; + height: 1.6em; + width: 1.6em; + font-size: 0.8em; + line-height: 1.6em; + display: inline-block; + padding: 0.2em; + border-radius: 0.1em; + } } - } - &.r_veryrare { - border-color: #a335ee; - background: linear-gradient(90deg, rgba(@r_veryrare,1) 0%, rgba(163,53,238,0) 35%); - .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(@r_veryrare, 0.75); + + &.r_common { + border-left: 0.3em solid @r_common; + .entity-image { + .inset-box-shadow(@r_common); + } + } + &.r_uncommon { + border-color: @r_uncommon; + background: linear-gradient(90deg, rgba(@r_uncommon,1) 0%, var(--background) 35%); + + .entity-image { + .inset-box-shadow(@r_uncommon); + } + } + &.r_rare { + border-color: @r_rare; + background: linear-gradient(90deg, rgba(@r_rare,1) 0%, var(--background) 35%); + .entity-image { + .inset-box-shadow(@r_rare); + } + } + &.r_veryrare { + border-color: @r_veryrare; + background: linear-gradient(90deg, rgba(@r_veryrare,1) 0%, var(--background) 35%); + .entity-image { + .inset-box-shadow(@r_veryrare); + } + } + &.r_legendary { + border-color: @r_legendary; + background: linear-gradient(90deg, rgba(@r_legendary,1) 0%, var(--background) 35%); + .entity-image { + .inset-box-shadow(@r_legendary); + } } } - &.r_legendary { - border-color: rgb(255,128,0); - background: linear-gradient(90deg, rgba(@r_legendary,1) 0%, rgba(255,128,0,0) 35%); + } + } + + .tab.active[data-tab="Scene"] { + .cb_entities { + .entity { + grid-template-areas: + "Name" + "Image"; + grid-template-columns: 1fr; + grid-template-rows: 0.2fr 1fr; + .entity-image { - box-shadow: 0 0 0.2em 0.1em inset rgba(@r_legendary, 0.75); + width: initial; + height: initial; + } + + .entity-name { + position: absolute; + z-index: 999; + padding: 0.5em; + font-size: 1.4em; } } } + } -} \ No newline at end of file +} + diff --git a/styles/components/sidebars.css b/styles/components/sidebars.css new file mode 100644 index 0000000..a4fd4be --- /dev/null +++ b/styles/components/sidebars.css @@ -0,0 +1,166 @@ +.control-area { + background: var(--background); + box-shadow: 0 0 0.3em 0.3em var(--context-shadow); + border-color: var(--header-border); + border-style: solid; + border-width: 0 1px 0 1px; + color: var(--primary-font); + display: grid; + grid-gap: 0; + height: calc(100% -40px); + width: max-content; + padding: 0.3em; + position: sticky; + left: -1px; + right: -1px; + /* Toggler Functionality */ +} +.control-area.l_sidebar { + grid-template-columns: 1fr 2em; + grid-template-areas: "CONTENT TOGGLER"; + margin-left: -2em; +} +.control-area.r_sidebar { + grid-template-columns: 2em 1fr; + grid-template-areas: "TOGGLER CONTENT"; + position: absolute; + top: 12vh; + right: -1px; + left: auto; + height: calc(100% - 44px); +} +.control-area button { + background: rgba(0, 0, 0, 0.05); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; +} +.control-area .toggler { + display: inline; + grid-area: TOGGLER; + font-size: 0; + width: 0; + height: 0; + color: var(--primary-color); +} +.control-area .toggler:after { + cursor: pointer; + content: "\f0b0"; + display: inline-block; + height: 16px; + width: 16px; + font-size: 16px; + position: absolute; + border-radius: 3px; + background: none; + color: darkgrey; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + text-rendering: auto; + font-variant: normal; +} +.control-area .toggler:hover:before { + content: 'toggle sidebar'; + position: absolute; + display: block; + font-size: 14px; + background: #333; + padding: 5px; + left: 30px; + width: max-content; + border: 1px solid grey; + border-radius: 0 3px 3px 3px; + z-index: 999; +} +.r_sidebar.control-area .toggler:hover:before { + left: auto; + right: 30px; +} +.control-area .toggler:checked:after { + color: var(--checkbox-checked); +} +.control-area .toggler ~ .sidebar_content { + display: none; +} +.control-area .toggler ~ .sidebar_content * { + color: var(--primary-font); +} +.control-area .toggler ~ .sidebar_content * option { + background: var(--background); +} +.control-area .toggler:checked ~ .sidebar_content { + display: initial; + grid-area: CONTENT; + width: max-content; +} +.control-area .filtercontainer { + margin-top: 5px; + padding: 2px; +} +.control-area .filtercontainer h3 { + margin: 0; + cursor: pointer; +} +.control-area .filtercontainer h3 :after { + display: inline-block; + height: auto; + content: "\25BE"; + position: absolute; + right: 0.2em; + font-size: 1.2em; +} +.control-area .filtercontainer h3.opened:after { + content: "\25B4"; +} +.control-area .filtercontainer dl { + margin: 0; +} +.control-area .filtercontainer div { + margin: 5px 0; +} +.control-area .filtercontainer dt { + display: inline-block; + width: 40%; + padding-left: 5px; + font-weight: 400; +} +.control-area .filtercontainer dd { + display: inline-block; + width: 58%; + margin-left: 0; +} +.control-area .filtercontainer dd select { + width: 100%; +} +.control-area .filtercontainer input, +.control-area .filtercontainer select { + height: 1.4em; + font-size: 0.9em; + font-weight: 400; +} +.control-area .filtercontainer .multiselect { + border: 1px solid #bbb; + border-radius: 3px; + vertical-align: middle; + line-height: 32px; + margin: 2px 0; +} +.control-area .filtercontainer .multiselect label { + padding: 5px; +} +.control-area .filtercontainer .multiselect input { + vertical-align: middle; +} +.control-area .filtercontainer .small-input { + width: calc(100% - 44px); + height: 27px; + background: rgba(0, 0, 0, 0.05); + border: 1px solid #444; + border-radius: 3px; + padding: 0 3px; + text-overflow: ellipsis; +} +.control-area .filtercontainer .small-select { + width: 40px; +} diff --git a/styles/filters/filters.less b/styles/components/sidebars.less similarity index 50% rename from styles/filters/filters.less rename to styles/components/sidebars.less index 9440b83..babd467 100644 --- a/styles/filters/filters.less +++ b/styles/components/sidebars.less @@ -1,17 +1,37 @@ -.compendium-browser { +@import (reference) '../variables.less'; + .control-area { + background: var(--background); + box-shadow: 0 0 0.3em 0.3em var(--context-shadow); + border-color: var(--header-border); + border-style: solid; + border-width: 0 1px 0 1px; + color: var(--primary-font); display: grid; - grid-template-rows: 2em 1fr; - height: 100%; - grid-template-areas: - "TOGGLER" - "FILTERS"; grid-gap:0; - width: max-content; - border-radius: 0.5em; - position: relative; - background: var(--header-background); - + height: calc(100% -40px); + width: max-content; + padding: 0.3em; + position: sticky; + left: -1px; + right: -1px; + + &.l_sidebar { + grid-template-columns: 1fr 2em; + grid-template-areas: + "CONTENT TOGGLER"; + margin-left: -2em; + } + &.r_sidebar { + grid-template-columns: 2em 1fr; + grid-template-areas: + "TOGGLER CONTENT"; + position: absolute; + top: 12vh; + right: -1px; + left: auto; + height: calc(100% - @menuheight); + } button { background: rgba(0, 0, 0, 0.05); border: 1px solid #bbb; @@ -21,48 +41,90 @@ } /* Toggler Functionality */ .toggler { - display: none; + display: inline; grid-area: TOGGLER; font-size: 0; width: 0; height: 0; + color: var(--primary-color); &:after { + cursor: pointer; + content: "\f0b0"; display: inline-block; height: 16px; width: 16px; + font-size: 16px; + position: absolute; + border-radius: 3px; background: none; color: darkgrey; font-family: "Font Awesome 5 Free"; font-weight: 900; - text-rendering: auto; - content: "\f0b0"; + text-rendering: auto; font-variant: normal; } + + &:hover:before { + content: 'toggle sidebar'; + position: absolute; + display: block; + font-size: 14px; + background: #333; + padding: 5px; + left: 30px; + width: max-content; + border: 1px solid grey; + border-radius: 0 3px 3px 3px; + z-index: 999; + + .r_sidebar& { + left: auto; + right: 30px; + } + } + + &:checked:after{ + color: var(--checkbox-checked); + } + } - .toggler ~ #cb_filtercontainer { + .toggler ~ .sidebar_content { display: none; + * { + color: var(--primary-font); + + option { + background: var(--background); + } + } } - .toggler:checked ~ #cb_filtercontainer { - background: rgba(0, 0, 0, 0.05); + .toggler:checked ~ .sidebar_content { display: initial; - grid-area: FILTERS; + grid-area: CONTENT; width: max-content; - border-radius: 0.5em 0.5em 0 0; - position: relative; } .filtercontainer { - color: var(--primary-font); - border: 1px solid #bbb; - border-radius: 5px; margin-top: 5px; padding: 2px; - background: rgba(0,0,0,0.75); h3 { margin: 0; cursor: pointer; + + :after { + display: inline-block; + height: auto; + content: "\25BE"; + position: absolute; + right: 0.2em; + font-size: 1.2em; + } + + &.opened:after { + content: "\25B4"; + } } dl { margin: 0; @@ -115,5 +177,4 @@ width: 40px; } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/styles/filters/filters.css b/styles/filters/filters.css deleted file mode 100644 index 9cc3d76..0000000 --- a/styles/filters/filters.css +++ /dev/null @@ -1,113 +0,0 @@ -.compendium-browser .control-area { - display: grid; - grid-template-rows: 2em 1fr; - height: 100%; - grid-template-areas: "TOGGLER" "FILTERS"; - grid-gap: 0; - width: max-content; - border-radius: 0.5em; - position: relative; - background: var(--header-background); - /* Toggler Functionality */ -} -.compendium-browser .control-area button { - background: rgba(0, 0, 0, 0.05); - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding: 2px; -} -.compendium-browser .control-area .toggler { - display: none; - grid-area: TOGGLER; - font-size: 0; - width: 0; - height: 0; -} -.compendium-browser .control-area .toggler:after { - display: inline-block; - height: 16px; - width: 16px; - border-radius: 3px; - background: none; - color: darkgrey; - font-family: "Font Awesome 5 Free"; - font-weight: 900; - text-rendering: auto; - content: "\f0b0"; - font-variant: normal; -} -.compendium-browser .control-area .toggler ~ #cb_filtercontainer { - display: none; -} -.compendium-browser .control-area .toggler:checked ~ #cb_filtercontainer { - background: rgba(0, 0, 0, 0.05); - display: initial; - grid-area: FILTERS; - width: max-content; - border-radius: 0.5em 0.5em 0 0; - position: relative; -} -.compendium-browser .control-area .filtercontainer { - color: var(--primary-font); - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding: 2px; - background: rgba(0, 0, 0, 0.75); -} -.compendium-browser .control-area .filtercontainer h3 { - margin: 0; - cursor: pointer; -} -.compendium-browser .control-area .filtercontainer dl { - margin: 0; -} -.compendium-browser .control-area .filtercontainer div { - margin: 5px 0; -} -.compendium-browser .control-area .filtercontainer dt { - display: inline-block; - width: 40%; - padding-left: 5px; - font-weight: 400; -} -.compendium-browser .control-area .filtercontainer dd { - display: inline-block; - width: 58%; - margin-left: 0; -} -.compendium-browser .control-area .filtercontainer dd select { - width: 100%; -} -.compendium-browser .control-area .filtercontainer input, -.compendium-browser .control-area .filtercontainer select { - height: 1.4em; - font-size: 0.9em; - font-weight: 400; -} -.compendium-browser .control-area .filtercontainer .multiselect { - border: 1px solid #bbb; - border-radius: 3px; - vertical-align: middle; - line-height: 32px; - margin: 2px 0; -} -.compendium-browser .control-area .filtercontainer .multiselect label { - padding: 5px; -} -.compendium-browser .control-area .filtercontainer .multiselect input { - vertical-align: middle; -} -.compendium-browser .control-area .filtercontainer .small-input { - width: calc(100% - 44px); - height: 27px; - background: rgba(0, 0, 0, 0.05); - border: 1px solid #444; - border-radius: 3px; - padding: 0 3px; - text-overflow: ellipsis; -} -.compendium-browser .control-area .filtercontainer .small-select { - width: 40px; -} diff --git a/styles/mixins.css b/styles/mixins.css new file mode 100644 index 0000000..e69de29 diff --git a/styles/mixins.less b/styles/mixins.less index e69de29..db0f618 100644 --- a/styles/mixins.less +++ b/styles/mixins.less @@ -0,0 +1,3 @@ +.inset-box-shadow(@rgb_var: --faintest-color){ + box-shadow: 0 0 0.2em 0.1em inset rgba(var(@rgb_var), 0.75); +} \ No newline at end of file diff --git a/styles/themes/dark.css b/styles/themes/dark.css index d6bf61e..a582a08 100644 --- a/styles/themes/dark.css +++ b/styles/themes/dark.css @@ -27,3 +27,6 @@ --checkbox-unchecked: #4b4b4b; --checkbox-checked: rgba(0, 255, 0, 0.5); } +.system-dnd5e.tidy5eDark .app.compendium-browser .window-content { + color: var(--primary-font); +} diff --git a/styles/themes/dark.less b/styles/themes/dark.less index c99c48c..0a91ec8 100644 --- a/styles/themes/dark.less +++ b/styles/themes/dark.less @@ -27,5 +27,13 @@ --checkbox-outline: rgba(50, 50, 50, 1); --checkbox-unchecked: rgba(75,75,75,1); --checkbox-checked: rgba(0, 255, 0, 0.5); + + .app { + &.compendium-browser { + .window-content { + color: var(--primary-font); + } + } + } } } \ No newline at end of file diff --git a/styles/themes/default.less b/styles/themes/default.less index 2b431af..976b2b6 100644 --- a/styles/themes/default.less +++ b/styles/themes/default.less @@ -1,43 +1,43 @@ .system-dnd5e { --modesto: "Modesto Condensed", "Palatino Linotype", serif; --signika: "Signika", sans-serif; - --primary-font: rgba(0, 0, 0, .9); - --background: rgba(236,233,223,1); - --faintest-color: rgba(0,0,0,.05); - --faint-color: rgba(0,0,0,.1); - --light-color: rgba(0, 0, 0, .25); - --primary-color: rgba(0,0,0,.9); - --secondary-color: rgba(0,0,0,.65); - --tertiary-color: rgba(0,0,0,.4); - --primary-accent: rgba(255,100,0,1); - --white: rgba(255,255,255,1); - --faint-white: rgba(255,255,255,.2); - --linked-accent: rgba(0,255,0,.75); - --unlinked-accent: rgba(255,0,0,.75); - --linked-light: rgba(0,255,0,.4); - --unlinked-light: rgba(255,0,0,.4); - --safe-accent: rgba(0,150,100,.6); - --unsafe-accent: rgba(255,0,0,.6); - --header-background: rgba(255, 255, 255, .2); - --header-border: rgba(0, 0, 0, .25); - --stat-font: rgba(236, 233, 223, 1); - --prepareable: rgba(119,136,153,1); - --prepared: rgba(50,205,50,.3); - --prepared-outline: rgba(50,205,50,1); - --prepared-accent: rgba(173,255,47,1); - --always-prepared: rgba(0,0,255,.15); + --primary-font: rgba(0, 0, 0, .9); + --background:rgba(236,233,223,1); + --faintest-color:rgba(0,0,0,.05); + --faint-color:rgba(0,0,0,.1); + --light-color:rgba(0, 0, 0, .25); + --primary-color:rgba(0,0,0,.9); + --secondary-color:rgba(0,0,0,.65); + --tertiary-color:rgba(0,0,0,.4); + --primary-accent:rgba(255,100,0,1); + --white:rgba(255,255,255,1); + --faint-white:rgba(255,255,255,.2); + --linked-accent:rgba(0,255,0,.75); + --unlinked-accent:rgba(255,0,0,.75); + --linked-light:rgba(0,255,0,.4); + --unlinked-light:rgba(255,0,0,.4); + --safe-accent:rgba(0,150,100,.6); + --unsafe-accent:rgba(255,0,0,.6); + --header-background:rgba(255, 255, 255, .2); + --header-border:rgba(0, 0, 0, .25); + --stat-font:rgba(236, 233, 223, 1); + --prepareable:rgba(119,136,153,1); + --prepared:rgba(50,205,50,.3); + --prepared-outline:rgba(50,205,50,1); + --prepared-accent:rgba(173,255,47,1); + --always-prepared:rgba(0,0,255,.15); --always-prepared-outline: rgba(65,105,225,1); --always-prepared-accent: rgba(0,191,255,1); - --magic-accent: rgba(255,255,0,1); - --faint-magic-accent: rgba(255,255,0,.6); - --magic-outline: rgba(175,255,47,1); - --attunement-required: rgba(205,92,92,1); - --icon-attuned: rgba(0,0,0,.4); - --xp-bar: rgba(94,225,146,1); - --encumbrance-bar: rgba(108,138,165,1); + --magic-accent:rgba(255,255,0,1); + --faint-magic-accent:rgba(255,255,0,.6); + --magic-outline:rgba(175,255,47,1); + --attunement-required:rgba(205,92,92,1); + --icon-attuned:rgba(0,0,0,.4); + --xp-bar:rgba(94,225,146,1); + --encumbrance-bar:rgba(108,138,165,1); --encumbrance-bar-outline: rgba(205, 228, 255, 1); - --encumbrance-outline: rgba(0, 0, 0, .9); - --warning-accent: rgba(255, 0, 0, .6); + --encumbrance-outline:rgba(0, 0, 0, .9); + --warning-accent:rgba(255, 0, 0, .6); --icon-background: rgba(236, 233, 223, 1); --icon-shadow: rgba(0, 0, 0, .4); --icon-outline: rgba(0, 0, 0, .4); diff --git a/styles/variables.css b/styles/variables.css index 5a59048..2566294 100644 --- a/styles/variables.css +++ b/styles/variables.css @@ -1,6 +1,9 @@ /** * Entity rarity colors */ +/** +* Color Themes +**/ .system-dnd5e { --modesto: "Modesto Condensed", "Palatino Linotype", serif; --signika: "Signika", sans-serif; diff --git a/styles/variables.less b/styles/variables.less index e629048..fa285a4 100644 --- a/styles/variables.less +++ b/styles/variables.less @@ -2,43 +2,16 @@ /** * Entity rarity colors */ -@r_common: fff; -@r_uncommon: rgba(30,255,0); +@r_common: rgb(255,255,255); +@r_uncommon: rgb(30,255,0); @r_rare: rgb(0,112,221); @r_veryrare: rgb(163,53,238); @r_legendary: rgb(255,128,0); +/** +* Color Themes +**/ @import 'themes/default.less'; @import 'themes/dark.less'; -.system-dnd5e { - &.tidy5eDark { - --primary-font: rgba(255, 255, 255, 0.8); - --background:rgb(30, 30, 30); - --white: rgba(0, 0, 0, 1); - --primary-color: rgba(255, 255, 255, 0.8); - --secondary-color: rgba(255, 255, 255, .65); - --tertiary-color: rgba(255, 255, 255, .4); - --light-color: rgba(255, 255, 255, .25); - --faint-color: rgba(255, 255, 255, .1); - --faintest-color: rgba(255, 255, 255, .05); - --ability-accent: darkslategrey; - --header-background: rgba(255, 255, 255, .05); - --header-border: rgba(255, 255, 255, .25); - --primary-accent: rgba(255, 100, 0, 1); - --prepared: rgba(0, 250, 180, .3); - --always-prepared: rgba(0, 100, 255, .3); - --icon-background: rgb(30, 30, 30); - --icon-shadow: rgba(0, 0, 0, .4); - --icon-outline: rgba(0, 0, 0, .4); - --icon-font:rgba(255, 255, 255, .4); - --icon-hover: rgba(255, 255, 255, 0.8); - --warning-accent: rgba(255, 30, 0, .65); - --check-default: url(../images/check-dark-unchecked.svg); - --check-checked: url(../images/check-light-checked.svg); - --checkbox-font: rgba(255, 255, 255, .8); - --checkbox-outline: rgba(50, 50, 50, 1); - --checkbox-unchecked: rgba(75,75,75,1); - --checkbox-checked: rgba(0, 255, 0, 0.5); - } - } \ No newline at end of file +@menuheight: 44px; \ No newline at end of file diff --git a/template/entity-browser.hbs b/template/entity-browser.hbs index ceb6501..0c96fa3 100644 --- a/template/entity-browser.hbs +++ b/template/entity-browser.hbs @@ -1,9 +1,7 @@
                                                  -
                                                  \ No newline at end of file diff --git a/template/entity-list.hbs b/template/entity-list.hbs index a3dd858..e3c6d35 100644 --- a/template/entity-list.hbs +++ b/template/entity-list.hbs @@ -1,7 +1,7 @@ {{#each entityItems as |entity|}} -
                                                • +

            + -
            -

            {{localize "CMPBrowser.compSettingsSpell"}}

            - {{#each settings.loadedCompendium.Item as |Comp key|}} - - {{/each}} -
            -
            -

            {{localize "CMPBrowser.compSettingsActor"}}

            - {{#each settings.loadedCompendium.Actor as |Comp key|}} - - {{/each}} -
            -
            -

            {{localize "CMPBrowser.compSettingsJournalEntry"}}

            - {{#each settings.loadedCompendium.JournalEntry as |Comp key|}} - - {{/each}} -
            -
            -

            {{localize "CMPBrowser.compSettingsRolltable"}}

            - {{#each settings.loadedCompendium.Rolltable as |Comp key|}} - - {{/each}} -
            + {{#each settings.loadedCompendium as |Comp key|}} +
            +

            {{localize "CMPBrowser.load"}} {{key}} Compendium

            + + {{#each Comp as |entityComp entityKey|}} + + {{/each}} +
            + {{/each}} \ No newline at end of file diff --git a/template/template.hbs b/template/template.hbs index e4e1c61..eae8b7c 100644 --- a/template/template.hbs +++ b/template/template.hbs @@ -1,28 +1,46 @@
            - {{#if showItemBrowser}}{{/if}} - {{#if showSpellBrowser}}{{/if}} - {{#if showFeatBrowser}}{{/if}} - {{#if showActorBrowser}}{{/if}} - {{#if showJournalEntryBrowser}}{{/if}} - {{#if showRollTableBrowser}}{{/if}} - {{#if isGM}}{{/if}} -
            - + {{#if showItemBrowser}}{{/if}} + {{#if showSpellBrowser}}{{/if}} + {{#if showFeatBrowser}}{{/if}} + {{#if showActorBrowser}}{{/if}} + {{#if showJournalEntryBrowser}}{{/if}} + {{#if showRollTableBrowser}}{{/if}} + {{#if showSceneBrowser}}{{/if}} + {{#if isGM}}{{/if}} +
            - {{#if showSpellBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Spell}}
            {{/if}} - {{#if showFeatBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Feat}}
            {{/if}} - {{#if showItemBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Item}}
            {{/if}} - {{#if showActorBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Actor}}
            {{/if}} - {{#if showJournalEntryBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.JournalEntry}}
            {{/if}} - {{#if showRollTableBrowser}}
            {{> "modules/compendium-browser/template/entity-browser.hbs" filters=filters.RollTable}}
            {{/if}} - {{#if isGM}}
            {{> "modules/compendium-browser/template/settings.hbs"}}
            {{/if}} + {{#if showItemBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Item}}
            {{/if}} + {{#if showSpellBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Spell}}
            {{/if}} + {{#if showActorBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Actor}}
            {{/if}} + {{#if showFeatBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Feat}}
            {{/if}} + {{#if showJournalEntryBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.JournalEntry}}
            {{/if}} + {{#if showRollTableBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.RollTable}}
            {{/if}} + {{#if showSceneBrowser}}
            {{> + "modules/compendium-browser/template/entity-browser.hbs" filters=filters.Scene}}
            {{/if}} + {{#if isGM}} +
            {{> "modules/compendium-browser/template/settings.hbs"}}
            + + + {{/if}}
            \ No newline at end of file From 08bc2186a534bf9916ea23427a15599237b7e805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20B=C3=B6ttner?= Date: Thu, 14 Oct 2021 13:27:51 +0200 Subject: [PATCH 05/13] gitignore, jsdoc, package.json --- .gitignore | 5 + README.md | 31 ++-- docs/Filter.html | 2 +- docs/VersionCheck.html | 137 +----------------- docs/dist_hooks_events.js.html | 2 +- docs/dist_hooks_moduleHooks.js.html | 2 +- docs/dist_scripts_classes_compactList.js.html | 2 +- ...st_scripts_classes_decoratedEntity.js.html | 2 +- docs/dist_scripts_classes_filter.js.html | 2 +- docs/dist_scripts_compendium-browser.js.html | 2 +- docs/dist_scripts_modules_entities.js.html | 2 +- docs/dist_scripts_modules_exporter.js.html | 2 +- docs/dist_scripts_modules_filter.js.html | 2 +- docs/dist_scripts_modules_renderer.js.html | 2 +- docs/dist_scripts_modules_settings.js.html | 2 +- ...t_scripts_versioning_version-check.js.html | 2 +- docs/hooks_events.js.html | 4 +- docs/hooks_moduleHooks.js.html | 4 +- docs/index.html | 109 +------------- docs/module.exports.html | 137 +----------------- docs/scripts_compendium-browser.js.html | 4 +- docs/scripts_modules_settings.js.html | 4 +- docs/scripts_versioning_version-check.js.html | 4 +- jsdoc.json | 8 +- package.json | 2 +- preview.jpg | Bin 423410 -> 0 bytes 26 files changed, 61 insertions(+), 414 deletions(-) create mode 100644 .gitignore delete mode 100644 preview.jpg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d95ed45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vscode +.jshintrc +.eslintrc +foundry.js +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index 0d2adfb..0de94bd 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,12 @@ Tired of scrolling compendia? Easily brows and filter for spells, feats, items, Compendium Browser is faster and better-behaved; **it no longer loads all the compendia into memory on start-up** (which sometimes hung servers because of memory or CPU requirements). Instead, it filters and loads on-demand, as well as giving you a Module Setting to control how many rows are loaded at a time. -## Summary -* **Authors**: Discord: Spetzel#0103; Felix (felix.mueller.86@web.de) -* **Version**: 0.5.0 -* **Foundry VTT Compatibility**: 0.7.2-0.8.6 -* **System Compatibility (If applicable)**: dnd5e -* **Translation Support**: en - ## Installation -1. Go to the Add-on Modules tab in Foundry Setup -2. Click Install Module and search for **Compendium Browser** OR paste this link: `https://github.com/League-of-Foundry-Developers/compendium-browser/releases/latest/download/module.json` -3. Open your world and go to Settings>Manage Modules and enable Compendium Browser +1. Go to the Setup of the FoundryVTT instance +1. Go to the Add-on Modules tab +2. Click _Install Module_ and search for **Compendium Browser** + _or_ paste this link: `https://github.com/League-of-Foundry-Developers/compendium-browser/releases/latest/download/module.json` +3. Open your world and go to **Settings** > **Manage Modules** and enable Compendium Browser ![example](preview.jpg) @@ -23,12 +18,22 @@ Compendium Browser is faster and better-behaved; **it no longer loads all the co ## Details Only the Gamemaster has access to the Settings, where they can enable or disable player access to the spell or npc-browser. It is **highly** recommended to disable any compendia that do not contain spell or should not be used in the NPC Browser. . -This application enables anyone to add their own custom spell or npc filters via the api. After initialization the app can be found under game.compendiumBrowser where either addSpellFilter or addNpcFilter can be used to add a filter. This does support any data that the spell or npc has, including flags. +All filters featured in the app are included in can be found in `scripts/modules/entities.js/`. -All filters featured in the app are included in this manner and can be found in the compendium-browser.js at around line 726. + +## Summary +* **Authors**: + * **Daniel Böttner:** [Github Profile]('https://github.com/DanielBoettner') | [Join FVTT League Developers Discord](https://discord.gg/PHmVQrG5) + * **Spetzel:** Discord Tag Spetzel#0103 | [Join FVTT League Developers Discord](https://discord.gg/PHmVQrG5) + * **Felix Müller:** [e-mail](felix.mueller.86@web.de) +* **Version**: 0.7.9 +* **Foundry VTT Compatibility**: >= 0.8.8 +* **System Compatibility (If applicable)**: dnd5e +* **Translation Support**: en, de ## Contribution -If you feel like supporting my work, feel free to leave a tip at my paypal felix.mueller.86@web.de +If you feel like supporting this work, feel free to leave a tip at the original creators paypal felix.mueller.86@web.de +and/or use github funding options. ## License Creative Commons Licence
            Compendium Browser - a module for Foundry VTT - by Felix Müller is licensed under a Creative Commons Attribution 4.0 International License. diff --git a/docs/Filter.html b/docs/Filter.html index e44be10..fb0b1fb 100644 --- a/docs/Filter.html +++ b/docs/Filter.html @@ -1379,7 +1379,7 @@

            Home

            Classes