From 7135716da9ac51576771033f01d9842f809515f2 Mon Sep 17 00:00:00 2001 From: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com> Date: Sat, 28 Jun 2025 23:44:57 -0300 Subject: [PATCH] Feature/178 searchbar logic to items in character sheet (#209) * REFACTOR: remove DhpApplicationMixin REFACTOR: remove getEmbeddedDocument method from Item class REFACTOR: remove prepareData method from Actor class REFACTOR: remove _preUpdate method from Actor class * REFACTOR: rename dhpItem to DHItem REFACTOR: improvement Item#isInventoryItem getter REFACTOR: simplify Item's createDialog static method. REFACTOR: remove documentCreate template * FEAT: add SearchFilter for character-sheet Inventory and DomainCards FEAT: simplify the preparetion of inventory context --------- Co-authored-by: Joaquin Pereyra --- daggerheart.mjs | 2 +- module/applications/daggerheart-sheet.mjs | 48 ------- module/applications/sheets/character.mjs | 110 +++++++++++++- module/data/item/armor.mjs | 3 +- module/data/item/base.mjs | 4 +- module/data/item/consumable.mjs | 3 +- module/data/item/miscellaneous.mjs | 3 +- module/data/item/weapon.mjs | 6 +- module/documents/_module.mjs | 2 +- module/documents/actor.mjs | 8 -- module/documents/item.mjs | 120 +++++++--------- styles/daggerheart.css | 4 + styles/less/actors/character/inventory.less | 135 +++++++++--------- .../sheets/actors/character/inventory.hbs | 2 +- templates/sheets/actors/character/loadout.hbs | 2 +- .../global/partials/domain-card-item.hbs | 2 +- templates/sidebar/documentCreate.hbs | 45 ------ 17 files changed, 245 insertions(+), 254 deletions(-) delete mode 100644 module/applications/daggerheart-sheet.mjs delete mode 100644 templates/sidebar/documentCreate.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index c08fed77..909324b4 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -62,7 +62,7 @@ Hooks.once('init', () => { CONFIG.Dice.rolls = [...CONFIG.Dice.rolls, ...[DHRoll, DualityRoll, D20Roll, DamageRoll]]; CONFIG.MeasuredTemplate.objectClass = DhMeasuredTemplate; - CONFIG.Item.documentClass = documents.DhpItem; + CONFIG.Item.documentClass = documents.DHItem; //Registering the Item DataModel CONFIG.Item.dataModels = models.items.config; diff --git a/module/applications/daggerheart-sheet.mjs b/module/applications/daggerheart-sheet.mjs deleted file mode 100644 index 32d5212e..00000000 --- a/module/applications/daggerheart-sheet.mjs +++ /dev/null @@ -1,48 +0,0 @@ -export default function DhpApplicationMixin(Base) { - return class DhpSheet extends Base { - static applicationType = 'sheets'; - static documentType = ''; - - static get defaultOptions() { - return Object.assign(super.defaultOptions, { - classes: ['daggerheart', 'sheet', this.documentType], - template: `systems/${SYSTEM.id}/templates/${this.applicationType}/${this.documentType}.hbs`, - height: 'auto', - submitOnChange: true, - submitOnClose: false, - width: 450 - }); - } - - /** @override */ - get title() { - const { documentName, type, name } = this.object; - // const typeLabel = game.i18n.localize(CONFIG[documentName].typeLabels[type]); - const typeLabel = documentName; - return `[${typeLabel}] ${name}`; - } - - // async _renderOuter() { - // const html = await super._renderOuter(); - // // const overlaySrc = "systems/amia/assets/ThePrimordial.png"; - // const overlay = `
` - // $(html).find('.window-header').prepend(overlay); - // return html; - // } - - activateListeners(html) { - super.activateListeners(html); - html.on('click', '[data-action]', this.#onClickAction.bind(this)); - } - - async #onClickAction(event) { - event.preventDefault(); - const button = event.currentTarget; - const action = button.dataset.action; - - return this._handleAction(action, event, button); - } - - async _handleAction(action, event, button) {} - }; -} diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index b3e68609..ad294aff 100644 --- a/module/applications/sheets/character.mjs +++ b/module/applications/sheets/character.mjs @@ -56,7 +56,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { resizable: true }, form: { - handler: this.updateForm, submitOnChange: true, closeOnSubmit: false }, @@ -218,6 +217,15 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { this._createContextMenues(); } + /** @inheritDoc */ + async _onRender(context, options) { + await super._onRender(context, options); + + this._createSearchFilter(); + } + + /* -------------------------------------------- */ + _createContextMenues() { const allOptions = { useItem: { @@ -431,11 +439,105 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { return context; } - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); + /* -------------------------------------------- */ + /* Search Filter */ + /* -------------------------------------------- */ + + /** + * The currently active search filter. + * @type {foundry.applications.ux.SearchFilter} + */ + #search = {}; + + /** + * Track which item IDs are currently displayed due to a search filter. + * @type {{ inventory: Set, loadout: Set }} + */ + #filteredItems = { + inventory: new Set(), + loadout: new Set() + }; + + /** + * Create and initialize search filter instances for the inventory and loadout sections. + * + * Sets up two {@link foundry.applications.ux.SearchFilter} instances: + * - One for the inventory, which filters items in the inventory grid. + * - One for the loadout, which filters items in the loadout/card grid. + * @private + */ + _createSearchFilter() { + //Filters could be a application option if needed + const filters = [ + { + key: 'inventory', + input: 'input[type="search"].search-inventory', + content: '[data-application-part="inventory"] .items-section', + callback: this._onSearchFilterInventory.bind(this) + }, + { + key: 'loadout', + input: 'input[type="search"].search-loadout', + content: '[data-application-part="loadout"] .items-section', + callback: this._onSearchFilterCard.bind(this) + } + ]; + + for (const { key, input, content, callback } of filters) { + const filter = new foundry.applications.ux.SearchFilter({ + inputSelector: input, + contentSelector: content, + callback + }); + filter.bind(this.element); + this.#search[key] = filter; + } } + /** + * Handle invetory items search and filtering. + * @param {KeyboardEvent} event The keyboard input event. + * @param {string} query The input search string. + * @param {RegExp} rgx The regular expression query that should be matched against. + * @param {HTMLElement} html The container to filter items from. + * @protected + */ + _onSearchFilterInventory(event, query, rgx, html) { + this.#filteredItems.inventory.clear(); + + for (const ul of html.querySelectorAll('.items-list')) { + for (const li of ul.querySelectorAll('.inventory-item')) { + const item = this.document.items.get(li.dataset.itemId); + const match = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); + if (match) this.#filteredItems.inventory.add(item.id); + li.hidden = !match; + } + } + } + + /** + * Handle card items search and filtering. + * @param {KeyboardEvent} event The keyboard input event. + * @param {string} query The input search string. + * @param {RegExp} rgx The regular expression query that should be matched against. + * @param {HTMLElement} html The container to filter items from. + * @protected + */ + _onSearchFilterCard(event, query, rgx, html) { + this.#filteredItems.loadout.clear(); + + const elements = html.querySelectorAll('.items-list .inventory-item, .card-list .card-item'); + + for (const li of elements) { + const item = this.document.items.get(li.dataset.itemId); + const match = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); + if (match) this.#filteredItems.loadout.add(item.id); + li.hidden = !match; + } + } + + /* -------------------------------------------- */ + async mapFeatureType(data, configType) { return await Promise.all( data.map(async x => { diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index c7f5af0b..ffd00a23 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -9,7 +9,8 @@ export default class DHArmor extends BaseDataItem { label: 'TYPES.Item.armor', type: 'armor', hasDescription: true, - isQuantifiable: true + isQuantifiable: true, + isInventoryItem: true, }); } diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 219b43aa..0df64a3e 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -7,6 +7,7 @@ import { actionsTypes } from '../action/_module.mjs'; * @property {string} type - The system type that this data model represents. * @property {boolean} hasDescription - Indicates whether items of this type have description field * @property {boolean} isQuantifiable - Indicates whether items of this type have quantity field + * @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item */ const fields = foundry.data.fields; @@ -18,7 +19,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { label: 'Base Item', type: 'base', hasDescription: false, - isQuantifiable: false + isQuantifiable: false, + isInventoryItem: false, }; } diff --git a/module/data/item/consumable.mjs b/module/data/item/consumable.mjs index 6c8df798..cb8a13b5 100644 --- a/module/data/item/consumable.mjs +++ b/module/data/item/consumable.mjs @@ -8,7 +8,8 @@ export default class DHConsumable extends BaseDataItem { label: 'TYPES.Item.consumable', type: 'consumable', hasDescription: true, - isQuantifiable: true + isQuantifiable: true, + isInventoryItem: true, }); } diff --git a/module/data/item/miscellaneous.mjs b/module/data/item/miscellaneous.mjs index d7687dc7..529cf9a9 100644 --- a/module/data/item/miscellaneous.mjs +++ b/module/data/item/miscellaneous.mjs @@ -8,7 +8,8 @@ export default class DHMiscellaneous extends BaseDataItem { label: 'TYPES.Item.miscellaneous', type: 'miscellaneous', hasDescription: true, - isQuantifiable: true + isQuantifiable: true, + isInventoryItem: true, }); } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index e7551a21..005f08af 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -12,10 +12,8 @@ export default class DHWeapon extends BaseDataItem { type: 'weapon', hasDescription: true, isQuantifiable: true, - embedded: { - feature: 'featureTest' - }, - hasInitialAction: true + isInventoryItem: true, + hasInitialAction: true, }); } diff --git a/module/documents/_module.mjs b/module/documents/_module.mjs index 03237ee5..e6099009 100644 --- a/module/documents/_module.mjs +++ b/module/documents/_module.mjs @@ -1,4 +1,4 @@ export { default as DhpActor } from './actor.mjs'; -export { default as DhpItem } from './item.mjs'; +export { default as DHItem } from './item.mjs'; export { default as DhpCombat } from './combat.mjs'; export { default as DhActiveEffect } from './activeEffect.mjs'; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index c7099b07..350c39a1 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -17,14 +17,6 @@ export default class DhpActor extends Actor { this.updateSource({ prototypeToken }); } - prepareData() { - super.prepareData(); - } - - async _preUpdate(changed, options, user) { - super._preUpdate(changed, options, user); - } - async updateLevel(newLevel) { if (this.type !== 'character' || newLevel === this.system.levelData.level.changed) return; diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 195b9c27..45b9df8e 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1,14 +1,8 @@ -export default class DhpItem extends Item { - /** @inheritdoc */ - getEmbeddedDocument(embeddedName, id, { invalid = false, strict = false } = {}) { - const systemEmbeds = this.system.constructor.metadata.embedded ?? {}; - if (embeddedName in systemEmbeds) { - const path = `system.${systemEmbeds[embeddedName]}`; - return foundry.utils.getProperty(this, path).get(id) ?? null; - } - return super.getEmbeddedDocument(embeddedName, id, { invalid, strict }); - } - +/** + * Override and extend the basic Item implementation. + * @extends {foundry.documents.Item} + */ +export default class DHItem extends foundry.documents.Item { /** @inheritDoc */ prepareEmbeddedDocuments() { super.prepareEmbeddedDocuments(); @@ -35,75 +29,59 @@ export default class DhpItem extends Item { return data; } - isInventoryItem() { - return ['weapon', 'armor', 'miscellaneous', 'consumable'].includes(this.type); + /** + * Determine if this item is classified as an inventory item based on its metadata. + * @returns {boolean} Returns `true` if the item is an inventory item. + */ + get isInventoryItem() { + return this.system.constructor.metadata.isInventoryItem ?? false; } - static async createDialog(data = {}, { parent = null, pack = null, ...options } = {}) { - const documentName = this.metadata.name; - const types = game.documentTypes[documentName].filter(t => t !== CONST.BASE_DOCUMENT_TYPE); - let collection; - if (!parent) { - if (pack) collection = game.packs.get(pack); - else collection = game.collections.get(documentName); - } - const folders = collection?._formatFolderSelectOptions() ?? []; - const label = game.i18n.localize(this.metadata.label); - const title = game.i18n.format('DOCUMENT.Create', { type: label }); - const typeObjects = types.reduce((obj, t) => { - const label = CONFIG[documentName]?.typeLabels?.[t] ?? t; - obj[t] = { value: t, label: game.i18n.has(label) ? game.i18n.localize(label) : t }; - return obj; - }, {}); + /** @inheritdoc */ + static async createDialog(data = {}, createOptions = {}, options = {}) { + const { folders, types, template, context = {}, ...dialogOptions } = options; - // Render the document creation form - const html = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/sidebar/documentCreate.hbs', - { - folders, - name: data.name || game.i18n.format('DOCUMENT.New', { type: label }), - folder: data.folder, - hasFolders: folders.length >= 1, - type: data.type || CONFIG[documentName]?.defaultType || typeObjects.armor, - types: { - Items: [typeObjects.armor, typeObjects.weapon, typeObjects.consumable, typeObjects.miscellaneous], - Character: [ - typeObjects.class, - typeObjects.subclass, - typeObjects.ancestry, - typeObjects.community, - typeObjects.feature, - typeObjects.domainCard - ] - }, - hasTypes: types.length > 1 + if (types?.length === 0) { + throw new Error('The array of sub-types to restrict to must not be empty.'); + } + + const documentTypes = this.TYPES.filter(type => type !== 'base' && (!types || types.includes(type))).map( + type => { + const labelKey = CONFIG.Item?.typeLabels?.[type]; + const label = labelKey && game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type; + + const isInventoryItem = CONFIG.Item.dataModels[type]?.metadata?.isInventoryItem; + const group = + isInventoryItem === true + ? 'Inventory Items' + : isInventoryItem === false + ? 'Character Items' + : 'Other'; + + return { value: type, label, group }; } ); - // Render the confirmation dialog window - return Dialog.prompt({ - title: title, - content: html, - label: title, - callback: html => { - const form = html[0].querySelector('form'); - const fd = new FormDataExtended(form); - foundry.utils.mergeObject(data, fd.object, { inplace: true }); - if (!data.folder) delete data.folder; - if (types.length === 1) data.type = types[0]; - if (!data.name?.trim()) data.name = this.defaultName(); - return this.create(data, { parent, pack, renderSheet: true }); - }, - rejectClose: false, - options + if (!documentTypes.length) { + throw new Error('No document types were permitted to be created.'); + } + + const sortedTypes = documentTypes.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang)); + + return await super.createDialog(data, createOptions, { + folders, + types, + template, + context: { types: sortedTypes, ...context }, + ...dialogOptions }); } async selectActionDialog() { const content = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/views/actionSelect.hbs', - { actions: this.system.actions } - ), + 'systems/daggerheart/templates/views/actionSelect.hbs', + { actions: this.system.actions } + ), title = 'Select Action', type = 'div', data = {}; @@ -142,8 +120,8 @@ export default class DhpItem extends Item { this.type === 'ancestry' ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle') : this.type === 'community' - ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') - : game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), + ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') + : game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), origin: origin, img: this.img, name: this.name, diff --git a/styles/daggerheart.css b/styles/daggerheart.css index cecf313c..4dbf6ac4 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -4080,6 +4080,10 @@ div.daggerheart.views.multiclass { .application.sheet.daggerheart.actor.dh-style.character .tab.inventory .search-section .search-bar input:placeholder { color: light-dark(#18162e50, #efe6d850); } +.application.sheet.daggerheart.actor.dh-style.character .tab.inventory .search-section .search-bar input::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; +} .application.sheet.daggerheart.actor.dh-style.character .tab.inventory .search-section .search-bar .icon { align-content: center; height: 32px; diff --git a/styles/less/actors/character/inventory.less b/styles/less/actors/character/inventory.less index a6caf22b..c1583046 100644 --- a/styles/less/actors/character/inventory.less +++ b/styles/less/actors/character/inventory.less @@ -1,65 +1,70 @@ -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .tab.inventory { - .search-section { - display: flex; - gap: 10px; - align-items: center; - - .search-bar { - position: relative; - color: light-dark(@dark-blue-50, @beige-50); - width: 100%; - padding-top: 5px; - - input { - border-radius: 50px; - font-family: @font-body; - background: light-dark(@dark-blue-10, @golden-10); - border: none; - outline: 2px solid transparent; - transition: all 0.3s ease; - padding: 0 20px; - - &:hover { - outline: 2px solid light-dark(@dark, @golden); - } - - &:placeholder { - color: light-dark(@dark-blue-50, @beige-50); - } - } - - .icon { - align-content: center; - height: 32px; - position: absolute; - right: 20px; - font-size: 16px; - z-index: 1; - color: light-dark(@dark-blue-50, @beige-50); - } - } - } - - .items-section { - display: flex; - flex-direction: column; - gap: 10px; - overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - padding: 20px 0; - height: 80%; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - } - - .currency-section { - display: flex; - gap: 10px; - } - } -} +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .tab.inventory { + .search-section { + display: flex; + gap: 10px; + align-items: center; + + .search-bar { + position: relative; + color: light-dark(@dark-blue-50, @beige-50); + width: 100%; + padding-top: 5px; + + input { + border-radius: 50px; + font-family: @font-body; + background: light-dark(@dark-blue-10, @golden-10); + border: none; + outline: 2px solid transparent; + transition: all 0.3s ease; + padding: 0 20px; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + + &:placeholder { + color: light-dark(@dark-blue-50, @beige-50); + } + + &::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; + } + } + + .icon { + align-content: center; + height: 32px; + position: absolute; + right: 20px; + font-size: 16px; + z-index: 1; + color: light-dark(@dark-blue-50, @beige-50); + } + } + } + + .items-section { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); + padding: 20px 0; + height: 80%; + + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + + .currency-section { + display: flex; + gap: 10px; + } + } +} diff --git a/templates/sheets/actors/character/inventory.hbs b/templates/sheets/actors/character/inventory.hbs index 22b32d3f..3f4b98be 100644 --- a/templates/sheets/actors/character/inventory.hbs +++ b/templates/sheets/actors/character/inventory.hbs @@ -8,7 +8,7 @@
- + diff --git a/templates/sheets/actors/character/loadout.hbs b/templates/sheets/actors/character/loadout.hbs index e80aaa80..de63323c 100644 --- a/templates/sheets/actors/character/loadout.hbs +++ b/templates/sheets/actors/character/loadout.hbs @@ -8,7 +8,7 @@
- +