diff --git a/module/applications/sheets-configs/environment-settings.mjs b/module/applications/sheets-configs/environment-settings.mjs index 7a91b272..7d4b9640 100644 --- a/module/applications/sheets-configs/environment-settings.mjs +++ b/module/applications/sheets-configs/environment-settings.mjs @@ -75,7 +75,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { * @returns */ static async #deleteAdversary(_event, target) { - const doc = getDocFromElement(target); + const doc = await getDocFromElement(target); const { category } = target.dataset; const path = `system.potentialAdversaries.${category}.adversaries`; diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 31755bca..d6c61718 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -4,7 +4,7 @@ import { abilities } from '../../../config/actorConfig.mjs'; import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import FilterMenu from '../../ux/filter-menu.mjs'; -import { getDocFromElement, itemAbleRollParse } from '../../../helpers/utils.mjs'; +import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs'; /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ @@ -258,19 +258,27 @@ export default class CharacterSheet extends DHBaseActorSheet { { name: 'toLoadout', icon: 'fa-solid fa-arrow-up', - condition: target => getDocFromElement(target).system.inVault, - callback: target => { - const doc = getDocFromElement(target), - actorLoadout = doc.actor.system.loadoutSlot; - if(actorLoadout.available) return doc.update({ 'system.inVault': false }); - ui.notifications.warn(game.i18n.format('DAGGERHEART.UI.Notifications.loadoutMaxReached', { max: actorLoadout.max })) + condition: target => { + const doc = getDocFromElementSync(target); + return doc && system.inVault; + }, + callback: async target => { + const doc = await getDocFromElement(target); + const actorLoadout = doc.actor.system.loadoutSlot; + if (actorLoadout.available) return doc.update({ 'system.inVault': false }); + ui.notifications.warn( + game.i18n.format('DAGGERHEART.UI.Notifications.loadoutMaxReached', { max: actorLoadout.max }) + ); } }, { name: 'toVault', icon: 'fa-solid fa-arrow-down', - condition: target => !getDocFromElement(target).system.inVault, - callback: target => getDocFromElement(target).update({ 'system.inVault': true }) + condition: target => { + const doc = getDocFromElementSync(target); + return doc && !doc.system.inVault; + }, + callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true }) } ].map(option => ({ ...option, @@ -292,13 +300,19 @@ export default class CharacterSheet extends DHBaseActorSheet { { name: 'equip', icon: 'fa-solid fa-hands', - condition: target => !getDocFromElement(target).system.equipped, + condition: target => { + const doc = getDocFromElementSync(target); + return doc && !doc.system.equipped; + }, callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target) }, { name: 'unequip', icon: 'fa-solid fa-hands', - condition: target => getDocFromElement(target).system.equipped, + condition: target => { + const doc = getDocFromElementSync(target); + return doc && system.equipped; + }, callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target) } ].map(option => ({ @@ -407,11 +421,11 @@ export default class CharacterSheet extends DHBaseActorSheet { * @param {HTMLElement} html The container to filter items from. * @protected */ - _onSearchFilterInventory(event, query, rgx, html) { + async _onSearchFilterInventory(_event, query, rgx, html) { this.#filteredItems.inventory.search.clear(); for (const li of html.querySelectorAll('.inventory-item')) { - const item = getDocFromElement(li); + const item = await getDocFromElement(li); const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); if (matchesSearch) this.#filteredItems.inventory.search.add(item.id); const { menu } = this.#filteredItems.inventory; @@ -427,11 +441,11 @@ export default class CharacterSheet extends DHBaseActorSheet { * @param {HTMLElement} html The container to filter items from. * @protected */ - _onSearchFilterCard(event, query, rgx, html) { + async _onSearchFilterCard(_event, query, rgx, html) { this.#filteredItems.loadout.search.clear(); for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { - const item = getDocFromElement(li); + const item = await getDocFromElement(li); const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); if (matchesSearch) this.#filteredItems.loadout.search.add(item.id); const { menu } = this.#filteredItems.loadout; @@ -478,11 +492,11 @@ export default class CharacterSheet extends DHBaseActorSheet { * @param {HTMLElement} html * @param {import('../ux/filter-menu.mjs').FilterItem[]} filters */ - _onMenuFilterInventory(event, html, filters) { + async _onMenuFilterInventory(_event, html, filters) { this.#filteredItems.inventory.menu.clear(); for (const li of html.querySelectorAll('.inventory-item')) { - const item = getDocFromElement(li); + const item = await getDocFromElement(li); const matchesMenu = filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); @@ -499,11 +513,11 @@ export default class CharacterSheet extends DHBaseActorSheet { * @param {HTMLElement} html * @param {import('../ux/filter-menu.mjs').FilterItem[]} filters */ - _onMenuFilterLoadout(event, html, filters) { + async _onMenuFilterLoadout(_event, html, filters) { this.#filteredItems.loadout.menu.clear(); for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { - const item = getDocFromElement(li); + const item = await getDocFromElement(li); const matchesMenu = filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); @@ -519,7 +533,7 @@ export default class CharacterSheet extends DHBaseActorSheet { /* -------------------------------------------- */ async updateItemResource(event) { - const item = getDocFromElement(event.currentTarget); + const item = await getDocFromElement(event.currentTarget); if (!item) return; const max = event.currentTarget.max ? Number(event.currentTarget.max) : null; @@ -529,7 +543,7 @@ export default class CharacterSheet extends DHBaseActorSheet { } async updateItemQuantity(event) { - const item = getDocFromElement(event.currentTarget); + const item = await getDocFromElement(event.currentTarget); if (!item) return; await item.update({ 'system.quantity': event.currentTarget.value }); @@ -609,7 +623,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * @type {ApplicationClickAction} */ static async #toggleEquipItem(_event, button) { - const item = getDocFromElement(button); + const item = await getDocFromElement(button); if (!item) return; if (item.system.equipped) { await item.update({ 'system.equipped': false }); @@ -664,7 +678,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * @type {ApplicationClickAction} */ static async #toggleVault(_event, button) { - const doc = getDocFromElement(button); + const doc = await getDocFromElement(button); await doc?.update({ 'system.inVault': !doc.system.inVault }); } @@ -673,7 +687,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * @type {ApplicationClickAction} */ static async #toggleResourceDice(event, target) { - const item = getDocFromElement(target); + const item = await getDocFromElement(target); const { dice } = event.target.closest('.item-resource').dataset; const diceState = item.system.resource.diceStates[dice]; @@ -688,7 +702,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * @type {ApplicationClickAction} */ static async #handleResourceDice(_, target) { - const item = getDocFromElement(target); + const item = await getDocFromElement(target); if (!item) return; const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document); @@ -709,7 +723,7 @@ export default class CharacterSheet extends DHBaseActorSheet { } async _onDragStart(event) { - const item = getDocFromElement(event.target); + const item = await getDocFromElement(event.target); const dragData = { type: item.documentName, diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 51328c8d..c5c9b46f 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -1,6 +1,5 @@ const { HandlebarsApplicationMixin } = foundry.applications.api; -import { getDocFromElement, tagifyElement } from '../../../helpers/utils.mjs'; -import DHActionConfig from '../../sheets-configs/action-config.mjs'; +import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs'; /** * @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction @@ -259,14 +258,20 @@ export default function DHApplicationMixin(Base) { { name: 'disableEffect', icon: 'fa-solid fa-lightbulb', - condition: target => !getDocFromElement(target).disabled, - callback: target => getDocFromElement(target).update({ disabled: true }) + condition: target => { + const doc = getDocFromElementSync(target); + return doc && !doc.disabled; + }, + callback: async target => (await getDocFromElement(target)).update({ disabled: true }) }, { name: 'enableEffect', icon: 'fa-regular fa-lightbulb', - condition: target => getDocFromElement(target).disabled, - callback: target => getDocFromElement(target).update({ disabled: false }) + condition: target => { + const doc = getDocFromElementSync(target); + return doc && doc.disabled; + }, + callback: async target => (await getDocFromElement(target)).update({ disabled: false }) } ].map(option => ({ ...option, @@ -299,10 +304,10 @@ export default function DHApplicationMixin(Base) { name: 'CONTROLS.CommonEdit', icon: 'fa-solid fa-pen-to-square', condition: target => { - const doc = getDocFromElement(target); - return !doc.hasOwnProperty('systemPath') || doc.inCollection; + const doc = getDocFromElementSync(target); + return doc && (!doc.hasOwnProperty('systemPath') || doc.inCollection); }, - callback: target => getDocFromElement(target).sheet.render({ force: true }) + callback: async target => (await getDocFromElement(target)).sheet.render({ force: true }) } ]; @@ -311,25 +316,25 @@ export default function DHApplicationMixin(Base) { name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', icon: 'fa-solid fa-burst', condition: target => { - const doc = getDocFromElement(target); - return !(doc.type === 'domainCard' && doc.system.inVault) + const doc = getDocFromElementSync(target); + return doc && !(doc.type === 'domainCard' && doc.system.inVault); }, - callback: (target, event) => getDocFromElement(target).use(event) + callback: async (target, event) => (await getDocFromElement(target)).use(event) }); if (toChat) options.unshift({ name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', icon: 'fa-solid fa-message', - callback: target => getDocFromElement(target).toChat(this.document.id) + callback: async target => (await getDocFromElement(target)).toChat(this.document.id) }); if (deletable) options.push({ name: 'CONTROLS.CommonDelete', icon: 'fa-solid fa-trash', - callback: (target, event) => { - const doc = getDocFromElement(target); + callback: async (target, event) => { + const doc = await getDocFromElement(target); if (event.shiftKey) return doc.delete(); else return doc.deleteDialog(); } @@ -371,7 +376,7 @@ export default function DHApplicationMixin(Base) { if (!actionId && !itemUuid) return; const doc = itemUuid - ? getDocFromElement(extensibleElement) + ? await getDocFromElement(extensibleElement) : this.document.system.attack?.id === actionId ? this.document.system.attack : this.document.system.actions?.get(actionId); @@ -429,8 +434,8 @@ export default function DHApplicationMixin(Base) { * Renders an embedded document. * @type {ApplicationClickAction} */ - static #editDoc(_event, target) { - const doc = getDocFromElement(target); + static async #editDoc(_event, target) { + const doc = await getDocFromElement(target); if (doc) return doc.sheet.render({ force: true }); } @@ -439,7 +444,7 @@ export default function DHApplicationMixin(Base) { * @type {ApplicationClickAction} */ static async #deleteDoc(event, target) { - const doc = getDocFromElement(target); + const doc = await getDocFromElement(target); if (doc) { if (event.shiftKey) return doc.delete(); else return await doc.deleteDialog(); @@ -451,7 +456,7 @@ export default function DHApplicationMixin(Base) { * @type {ApplicationClickAction} */ static async #toChat(_event, target) { - let doc = getDocFromElement(target); + let doc = await getDocFromElement(target); return doc.toChat(this.document.id); } @@ -460,7 +465,7 @@ export default function DHApplicationMixin(Base) { * @type {ApplicationClickAction} */ static async #useItem(event, target) { - let doc = getDocFromElement(target); + let doc = await getDocFromElement(target); await doc.use(event); } @@ -469,7 +474,7 @@ export default function DHApplicationMixin(Base) { * @type {ApplicationClickAction} */ static async #toggleEffect(_, target) { - const doc = getDocFromElement(target); + const doc = await getDocFromElement(target); await doc.update({ disabled: !doc.disabled }); } @@ -492,7 +497,7 @@ export default function DHApplicationMixin(Base) { const t = extensible?.classList.toggle('extended'); const descriptionElement = extensible?.querySelector('.invetory-description'); - if (t && !!descriptionElement) this.#prepareInventoryDescription(extensible, descriptionElement); + if (t && !!descriptionElement) await this.#prepareInventoryDescription(extensible, descriptionElement); } } diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 5fe4d681..22f7c880 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -117,7 +117,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { name: 'CONTROLS.CommonDelete', icon: '', callback: async target => { - const feature = getDocFromElement(target); + const feature = await getDocFromElement(target); if (!feature) return; const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { @@ -168,7 +168,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { */ static async #deleteFeature(_, element) { const target = element.closest('[data-item-uuid]'); - const feature = getDocFromElement(target); + const feature = await getDocFromElement(target); if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); await this.document.update({ 'system.features': this.document.system.features diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 749591ef..e2f30130 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -235,9 +235,25 @@ export const updateActorTokens = async (actor, update) => { * @param {HTMLElement} element - The DOM element to start the search from. * @returns {foundry.abstract.Document|null} The resolved document, or null if not found or invalid. */ -export function getDocFromElement(element) { +export async function getDocFromElement(element) { const target = element.closest('[data-item-uuid]'); - return foundry.utils.fromUuidSync(target.dataset.itemUuid) ?? null; + return (await foundry.utils.fromUuid(target.dataset.itemUuid)) ?? null; +} + +/** + * Retrieves a Foundry document associated with the nearest ancestor element + * that has a `data-item-uuid` attribute. + * @param {HTMLElement} element - The DOM element to start the search from. + * @returns {foundry.abstract.Document|null} The resolved document, or null if not found, invalid + * or in embedded compendium collection. + */ +export function getDocFromElementSync(element) { + const target = element.closest('[data-item-uuid]'); + try { + return foundry.utils.fromUuidSync(target.dataset.itemUuid) ?? null; + } catch (_) { + return null; + } } /**