FEAT: add SearchFilter for character-sheet Inventory and DomainCards

FEAT: simplify the preparetion of inventory context
This commit is contained in:
Joaquin Pereyra 2025-06-28 19:14:38 -03:00
parent 15b696398c
commit 11f6ee3a7f
7 changed files with 169 additions and 46 deletions

View file

@ -56,7 +56,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
resizable: true resizable: true
}, },
form: { form: {
handler: this.updateForm,
submitOnChange: true, submitOnChange: true,
closeOnSubmit: false closeOnSubmit: false
}, },
@ -218,6 +217,15 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
this._createContextMenues(); this._createContextMenues();
} }
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
this._createSearchFilter();
}
/* -------------------------------------------- */
_createContextMenues() { _createContextMenues() {
const allOptions = { const allOptions = {
useItem: { useItem: {
@ -402,49 +410,156 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
})) }))
}; };
context.inventory = { return context;
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')
}
};
if (context.inventory.length === 0) { /**@inheritdoc */
context.inventory = Array(1).fill(Array(5).fill([])); async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case "inventory":
context.inventory = this._prepareInventoryContext();
break;
} }
return context; return context;
} }
static async updateForm(event, _, formData) { /**
await this.document.update(formData.object); * Prepare the inventory context, grouping items by type
this.render(); * 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<string>, loadout: Set<string> }}
*/
#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) { async mapFeatureType(data, configType) {
return await Promise.all( return await Promise.all(
data.map(async x => { 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 }, { title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value },
event.shiftKey event.shiftKey
); );
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const systemContent = new DHDualityRoll({ const systemContent = new DHDualityRoll({
title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', {
ability: game.i18n.localize(abilities[button.dataset.attribute].label) ability: game.i18n.localize(abilities[button.dataset.attribute].label)
@ -502,7 +617,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
advantage: advantage, advantage: advantage,
disadvantage: disadvantage disadvantage: disadvantage
}); });
await cls.create({ await cls.create({
type: 'dualityRoll', type: 'dualityRoll',
sound: CONFIG.sounds.dice, sound: CONFIG.sounds.dice,
@ -771,9 +886,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const systemData = { const systemData = {
name: game.i18n.localize('DAGGERHEART.General.Experience.Single'), name: game.i18n.localize('DAGGERHEART.General.Experience.Single'),
description: `${experience.description} ${ description: `${experience.description} ${experience.total < 0 ? experience.total : `+${experience.total}`
experience.total < 0 ? experience.total : `+${experience.total}` }`
}`
}; };
const msg = new cls({ const msg = new cls({
type: 'abilityUse', type: 'abilityUse',

View file

@ -3920,6 +3920,10 @@ div.daggerheart.views.multiclass {
.application.sheet.daggerheart.actor.dh-style.character .tab.inventory .search-section .search-bar input:placeholder { .application.sheet.daggerheart.actor.dh-style.character .tab.inventory .search-section .search-bar input:placeholder {
color: light-dark(#18162e50, #efe6d850); 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 { .application.sheet.daggerheart.actor.dh-style.character .tab.inventory .search-section .search-bar .icon {
align-content: center; align-content: center;
height: 32px; height: 32px;

View file

@ -30,6 +30,11 @@
&:placeholder { &:placeholder {
color: light-dark(@dark-blue-50, @beige-50); color: light-dark(@dark-blue-50, @beige-50);
} }
&::-webkit-search-cancel-button {
-webkit-appearance: none;
display: none;
}
} }
.icon { .icon {

View file

@ -8,7 +8,7 @@
<div class="icon"> <div class="icon">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
</div> </div>
<input type="text" name="" id="" placeholder="Search..."> <input type="search" name="search" class="search-inventory" placeholder="Search...">
</div> </div>
<a><i class="fa-solid fa-filter"></i></a> <a><i class="fa-solid fa-filter"></i></a>
</div> </div>

View file

@ -8,7 +8,7 @@
<div class="icon"> <div class="icon">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
</div> </div>
<input type="text" name="" id="" placeholder="Search..."> <input type="search" name="search" class="search-loadout" placeholder="Search...">
</div> </div>
<a><i class="fa-solid fa-filter"></i></a> <a><i class="fa-solid fa-filter"></i></a>
<button class="btn-toggle-view" data-action="toggleLoadoutView" data-value="{{this.abilities.loadout.listView}}"> <button class="btn-toggle-view" data-action="toggleLoadoutView" data-value="{{this.abilities.loadout.listView}}">

View file

@ -1,4 +1,4 @@
<li class="card-item"> <li class="card-item" data-item-id="{{item.id}}">
<img src="{{item.img}}" data-action="viewObject" data-uuid="{{item.uuid}}" class="card-img" /> <img src="{{item.img}}" data-action="viewObject" data-uuid="{{item.uuid}}" class="card-img" />
<div class="card-label"> <div class="card-label">
<div class="controls"> <div class="controls">

View file

@ -1,4 +1,4 @@
<li class="inventory-item"> <li class="inventory-item" data-item-id="{{item.id}}">
<img src="{{item.img}}" data-action="viewObject" data-uuid="{{item.uuid}}" class="item-img" /> <img src="{{item.img}}" data-action="viewObject" data-uuid="{{item.uuid}}" class="item-img" />
<div class="item-label"> <div class="item-label">
<div class="item-name">{{item.name}}</div> <div class="item-name">{{item.name}}</div>