Initial
This commit is contained in:
317
resources/app/client/apps/forms/actor.js
Normal file
317
resources/app/client/apps/forms/actor.js
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user