From 11f6ee3a7fc3be0877e5f611f133e96aaa97660e Mon Sep 17 00:00:00 2001 From: Joaquin Pereyra Date: Sat, 28 Jun 2025 19:14:38 -0300 Subject: [PATCH] FEAT: add SearchFilter for character-sheet Inventory and DomainCards FEAT: simplify the preparetion of inventory context --- module/applications/sheets/character.mjs | 198 ++++++++++++++---- styles/daggerheart.css | 4 + styles/less/actors/character/inventory.less | 5 + .../sheets/actors/character/inventory.hbs | 2 +- templates/sheets/actors/character/loadout.hbs | 2 +- .../global/partials/domain-card-item.hbs | 2 +- .../sheets/global/partials/inventory-item.hbs | 2 +- 7 files changed, 169 insertions(+), 46 deletions(-) diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index 3b191b32..3eb35dfb 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: { @@ -402,49 +410,156 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { })) }; - context.inventory = { - consumable: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ConsumableTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'consumable') - }, - miscellaneous: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.MiscellaneousTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'miscellaneous') - }, - weapons: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.WeaponsTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'weapon') - }, - armor: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ArmorsTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'armor') - } - }; + return context; + } - if (context.inventory.length === 0) { - context.inventory = Array(1).fill(Array(5).fill([])); + /**@inheritdoc */ + async _preparePartContext(partId, context, options) { + context = await super._preparePartContext(partId, context, options); + + switch (partId) { + case "inventory": + context.inventory = this._prepareInventoryContext(); + break; } return context; } - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); + /** + * Prepare the inventory context, grouping items by type + * and providing localized titles for display in the inventory UI. + * + * @returns {Object} + */ + _prepareInventoryContext() { + const items = this.document.itemTypes; + const quantityTitle = game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle'); + + const inventoryConfig = { + consumable: 'ConsumableTitle', + miscellaneous: 'MiscellaneousTitle', + weapons: 'WeaponsTitle', + armor: 'ArmorsTitle' + }; + + return Object.fromEntries( + Object.entries(inventoryConfig).map(([key, nameKey]) => [ + key, + { + titles: { + name: game.i18n.localize(`DAGGERHEART.Sheets.PC.InventoryTab.${nameKey}`), + quantity: quantityTitle + }, + items: items[key] + } + ]) + ); } + + /* -------------------------------------------- */ + /* 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 => { @@ -487,9 +602,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { { title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value }, event.shiftKey ); - + const cls = getDocumentClass('ChatMessage'); - + const systemContent = new DHDualityRoll({ title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { ability: game.i18n.localize(abilities[button.dataset.attribute].label) @@ -502,7 +617,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { advantage: advantage, disadvantage: disadvantage }); - + await cls.create({ type: 'dualityRoll', sound: CONFIG.sounds.dice, @@ -771,9 +886,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { const cls = getDocumentClass('ChatMessage'); const systemData = { name: game.i18n.localize('DAGGERHEART.General.Experience.Single'), - description: `${experience.description} ${ - experience.total < 0 ? experience.total : `+${experience.total}` - }` + description: `${experience.description} ${experience.total < 0 ? experience.total : `+${experience.total}` + }` }; const msg = new cls({ type: 'abilityUse', diff --git a/styles/daggerheart.css b/styles/daggerheart.css index 395912ec..c8c47856 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -3920,6 +3920,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 1b2beaf8..c1583046 100644 --- a/styles/less/actors/character/inventory.less +++ b/styles/less/actors/character/inventory.less @@ -30,6 +30,11 @@ &:placeholder { color: light-dark(@dark-blue-50, @beige-50); } + + &::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; + } } .icon { diff --git a/templates/sheets/actors/character/inventory.hbs b/templates/sheets/actors/character/inventory.hbs index 0d9f312a..728f3510 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 @@
- +