318 lines
10 KiB
JavaScript
318 lines
10 KiB
JavaScript
|
|
/**
|
||
|
|
* 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<ActiveEffect|boolean>} 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<object|boolean>} 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<Item[]|boolean>} 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<Item[]>}
|
||
|
|
* @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<Item[]>}
|
||
|
|
* @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);
|
||
|
|
}
|
||
|
|
}
|