/** * The Application responsible for displaying and editing a single Actor document. * This Application is responsible for rendering an actor's attributes and allowing the actor to be edited. * @extends {DocumentSheet} * @category - Applications * @param {Actor} actor The Actor instance being displayed within the sheet. * @param {DocumentSheetOptions} [options] Additional application configuration options. */ class ActorSheet extends DocumentSheet { /** @inheritdoc */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { height: 720, width: 800, template: "templates/sheets/actor-sheet.html", closeOnSubmit: false, submitOnClose: true, submitOnChange: true, resizable: true, baseApplication: "ActorSheet", dragDrop: [{dragSelector: ".item-list .item", dropSelector: null}], secrets: [{parentSelector: ".editor"}], token: null }); } /* -------------------------------------------- */ /** @inheritdoc */ get title() { if ( !this.actor.isToken ) return this.actor.name; return `[${game.i18n.localize(TokenDocument.metadata.label)}] ${this.actor.name}`; } /* -------------------------------------------- */ /** * A convenience reference to the Actor document * @type {Actor} */ get actor() { return this.object; } /* -------------------------------------------- */ /** * If this Actor Sheet represents a synthetic Token actor, reference the active Token * @type {Token|null} */ get token() { return this.object.token || this.options.token || null; } /* -------------------------------------------- */ /* Methods */ /* -------------------------------------------- */ /** @inheritdoc */ async close(options) { this.options.token = null; return super.close(options); } /* -------------------------------------------- */ /** @inheritdoc */ getData(options={}) { const context = super.getData(options); context.actor = this.object; context.items = context.data.items; context.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)); context.effects = context.data.effects; return context; } /* -------------------------------------------- */ /** @inheritdoc */ _getHeaderButtons() { let buttons = super._getHeaderButtons(); const canConfigure = game.user.isGM || (this.actor.isOwner && game.user.can("TOKEN_CONFIGURE")); if ( this.options.editable && canConfigure ) { const closeIndex = buttons.findIndex(btn => btn.label === "Close"); buttons.splice(closeIndex, 0, { label: this.token ? "Token" : "TOKEN.TitlePrototype", class: "configure-token", icon: "fas fa-user-circle", onclick: ev => this._onConfigureToken(ev) }); } return buttons; } /* -------------------------------------------- */ /** @inheritdoc */ _getSubmitData(updateData = {}) { const data = super._getSubmitData(updateData); // Prevent submitting overridden values const overrides = foundry.utils.flattenObject(this.actor.overrides); for ( let k of Object.keys(overrides) ) delete data[k]; return data; } /* -------------------------------------------- */ /* Event Listeners */ /* -------------------------------------------- */ /** * Handle requests to configure the Token for the Actor * @param {PointerEvent} event The originating click event * @private */ _onConfigureToken(event) { event.preventDefault(); const renderOptions = { left: Math.max(this.position.left - 560 - 10, 10), top: this.position.top }; if ( this.token ) return this.token.sheet.render(true, renderOptions); else new CONFIG.Token.prototypeSheetClass(this.actor.prototypeToken, renderOptions).render(true); } /* -------------------------------------------- */ /* Drag and Drop */ /* -------------------------------------------- */ /** @inheritdoc */ _canDragStart(selector) { return this.isEditable; } /* -------------------------------------------- */ /** @inheritdoc */ _canDragDrop(selector) { return this.isEditable; } /* -------------------------------------------- */ /** @inheritdoc */ _onDragStart(event) { const li = event.currentTarget; if ( "link" in event.target.dataset ) return; // Create drag data let dragData; // Owned Items if ( li.dataset.itemId ) { const item = this.actor.items.get(li.dataset.itemId); dragData = item.toDragData(); } // Active Effect if ( li.dataset.effectId ) { const effect = this.actor.effects.get(li.dataset.effectId); dragData = effect.toDragData(); } if ( !dragData ) return; // Set data transfer event.dataTransfer.setData("text/plain", JSON.stringify(dragData)); } /* -------------------------------------------- */ /** @inheritdoc */ async _onDrop(event) { const data = TextEditor.getDragEventData(event); const actor = this.actor; const allowed = Hooks.call("dropActorSheetData", actor, this, data); if ( allowed === false ) return; // Handle different data types switch ( data.type ) { case "ActiveEffect": return this._onDropActiveEffect(event, data); case "Actor": return this._onDropActor(event, data); case "Item": return this._onDropItem(event, data); case "Folder": return this._onDropFolder(event, data); } } /* -------------------------------------------- */ /** * Handle the dropping of ActiveEffect data onto an Actor Sheet * @param {DragEvent} event The concluding DragEvent which contains drop data * @param {object} data The data transfer extracted from the event * @returns {Promise} The created ActiveEffect object or false if it couldn't be created. * @protected */ async _onDropActiveEffect(event, data) { const effect = await ActiveEffect.implementation.fromDropData(data); if ( !this.actor.isOwner || !effect ) return false; if ( effect.target === this.actor ) return false; return ActiveEffect.create(effect.toObject(), {parent: this.actor}); } /* -------------------------------------------- */ /** * Handle dropping of an Actor data onto another Actor sheet * @param {DragEvent} event The concluding DragEvent which contains drop data * @param {object} data The data transfer extracted from the event * @returns {Promise} A data object which describes the result of the drop, or false if the drop was * not permitted. * @protected */ async _onDropActor(event, data) { if ( !this.actor.isOwner ) return false; } /* -------------------------------------------- */ /** * Handle dropping of an item reference or item data onto an Actor Sheet * @param {DragEvent} event The concluding DragEvent which contains drop data * @param {object} data The data transfer extracted from the event * @returns {Promise} The created or updated Item instances, or false if the drop was not permitted. * @protected */ async _onDropItem(event, data) { if ( !this.actor.isOwner ) return false; const item = await Item.implementation.fromDropData(data); const itemData = item.toObject(); // Handle item sorting within the same Actor if ( this.actor.uuid === item.parent?.uuid ) return this._onSortItem(event, itemData); // Create the owned item return this._onDropItemCreate(itemData, event); } /* -------------------------------------------- */ /** * Handle dropping of a Folder on an Actor Sheet. * The core sheet currently supports dropping a Folder of Items to create all items as owned items. * @param {DragEvent} event The concluding DragEvent which contains drop data * @param {object} data The data transfer extracted from the event * @returns {Promise} * @protected */ async _onDropFolder(event, data) { if ( !this.actor.isOwner ) return []; const folder = await Folder.implementation.fromDropData(data); if ( folder.type !== "Item" ) return []; const droppedItemData = await Promise.all(folder.contents.map(async item => { if ( !(document instanceof Item) ) item = await fromUuid(item.uuid); return item.toObject(); })); return this._onDropItemCreate(droppedItemData, event); } /* -------------------------------------------- */ /** * Handle the final creation of dropped Item data on the Actor. * This method is factored out to allow downstream classes the opportunity to override item creation behavior. * @param {object[]|object} itemData The item data requested for creation * @param {DragEvent} event The concluding DragEvent which provided the drop data * @returns {Promise} * @private */ async _onDropItemCreate(itemData, event) { itemData = itemData instanceof Array ? itemData : [itemData]; return this.actor.createEmbeddedDocuments("Item", itemData); } /* -------------------------------------------- */ /** * Handle a drop event for an existing embedded Item to sort that Item relative to its siblings * @param {Event} event * @param {Object} itemData * @private */ _onSortItem(event, itemData) { // Get the drag source and drop target const items = this.actor.items; const source = items.get(itemData._id); const dropTarget = event.target.closest("[data-item-id]"); if ( !dropTarget ) return; const target = items.get(dropTarget.dataset.itemId); // Don't sort on yourself if ( source.id === target.id ) return; // Identify sibling items based on adjacent HTML elements const siblings = []; for ( let el of dropTarget.parentElement.children ) { const siblingId = el.dataset.itemId; if ( siblingId && (siblingId !== source.id) ) siblings.push(items.get(el.dataset.itemId)); } // Perform the sort const sortUpdates = SortingHelpers.performIntegerSort(source, {target, siblings}); const updateData = sortUpdates.map(u => { const update = u.update; update._id = u.target._id; return update; }); // Perform the update return this.actor.updateEmbeddedDocuments("Item", updateData); } }