From b4ec9745e7cfb4b3c35c28fa46649dec422468bd Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sun, 13 Jul 2025 21:51:20 -0600 Subject: [PATCH] Add drag and drop support for reordering the attributes manually in the Attribute Manager --- module/apps/AttributeManager.mjs | 108 +++++++++++++++++- styles/Apps/AttributeManager.css | 14 ++- styles/Apps/common.css | 1 + templates/AttributeManager/attribute-list.hbs | 5 + 4 files changed, 123 insertions(+), 5 deletions(-) diff --git a/module/apps/AttributeManager.mjs b/module/apps/AttributeManager.mjs index 105b5bf..d3874ba 100644 --- a/module/apps/AttributeManager.mjs +++ b/module/apps/AttributeManager.mjs @@ -1,9 +1,11 @@ import { __ID__, filePath } from "../consts.mjs"; +import { attributeSorter } from "../utils/attributeSort.mjs"; import { Logger } from "../utils/Logger.mjs"; import { toID } from "../utils/toID.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; -const { deepClone, diffObject, randomID, setProperty } = foundry.utils; +const { deepClone, diffObject, mergeObject, performIntegerSort, randomID, setProperty } = foundry.utils; +const { DragDrop, TextEditor } = foundry.applications.ux; export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) { @@ -16,7 +18,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) ], position: { width: 400, - height: 350, + height: `auto`, }, window: { resizable: true, @@ -68,6 +70,18 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) for (const input of elements) { input.addEventListener(`change`, this.#bindListener.bind(this)); }; + + new DragDrop.implementation({ + dragSelector: `[data-attribute]`, + permissions: { + dragstart: this._canDragStart.bind(this), + drop: this._canDragDrop.bind(this), + }, + callbacks: { + dragstart: this._onDragStart.bind(this), + drop: this._onDrop.bind(this), + }, + }).bind(this.element); }; // #endregion Lifecycle @@ -93,11 +107,13 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) attrs.push({ id, name: data.name, + displayName: data.isNew ? `New Attribute` : data.name, + sort: data.sort, isRange: data.isRange, isNew: data.isNew ?? false, }); }; - ctx.attrs = attrs; + ctx.attrs = attrs.sort(attributeSorter); }; // #endregion Data Prep @@ -119,7 +135,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) Logger.debug(`Updating ${binding} value to ${value}`); setProperty(this.#attributes, binding, value); - await this.render(); + await this.render({ parts: [ `attributes` ]}); }; /** @this {AttributeManager} */ @@ -127,6 +143,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) const id = randomID(); this.#attributes[id] = { name: ``, + sort: Number.POSITIVE_INFINITY, isRange: false, isNew: true, }; @@ -168,4 +185,87 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) await this.#doc.update({ "system.attr": diff }); }; // #endregion Actions + + // #region Drag & Drop + _canDragStart() { + return this.#doc.isOwner; + }; + + _canDragDrop() { + return this.#doc.isOwner; + }; + + _onDragStart(event) { + const target = event.currentTarget; + if (`link` in event.target.dataset) { return }; + let dragData; + + if (target.dataset.attribute) { + const attributeID = target.dataset.attribute; + const attribute = this.#attributes[attributeID]; + dragData = { + _id: attributeID, + sort: attribute.sort, + }; + }; + + if (!dragData) { return }; + event.dataTransfer.setData(`text/plain`, JSON.stringify(dragData)); + }; + + _onDrop(event) { + const dropped = TextEditor.implementation.getDragEventData(event); + + const dropTarget = event.target.closest(`[data-attribute]`); + if (!dropTarget) { return }; + const targetID = dropTarget.dataset.attribute; + let target; + + // Not moving location, ignore drop event + if (targetID === dropped._id) { return }; + + // Determine all of the siblings and create sort data + const siblings = []; + for (const element of dropTarget.parentElement.children) { + const siblingID = element.dataset.attribute; + const attr = this.#attributes[siblingID]; + const sibling = { + _id: siblingID, + sort: attr.sort, + }; + if (siblingID && siblingID !== dropped._id) { + siblings.push(sibling); + }; + if (siblingID === targetID) { + target = sibling; + } + }; + + const sortUpdates = performIntegerSort( + dropped, + { + target, + siblings, + }, + ); + + const updateEntries = sortUpdates.map(({ target, update }) => { + return [ `${target._id}.sort`, update.sort ]; + }); + const update = Object.fromEntries(updateEntries); + + mergeObject( + this.#attributes, + update, + { + insertKeys: false, + insertValues: false, + inplace: true, + performDeletions: false, + }, + ); + + this.render({ parts: [ `attributes` ] }); + }; + // #endregion Drag & Drop }; diff --git a/styles/Apps/AttributeManager.css b/styles/Apps/AttributeManager.css index a36d35c..4a4bb0c 100644 --- a/styles/Apps/AttributeManager.css +++ b/styles/Apps/AttributeManager.css @@ -7,7 +7,7 @@ .attribute { display: grid; - grid-template-columns: 1fr auto auto; + grid-template-columns: min-content 1fr repeat(3, auto); align-items: center; gap: 8px; padding: 8px; @@ -18,6 +18,18 @@ display: flex; flex-direction: row; align-items: center; + + &.vertical { + flex-direction: column; + } + } + } + + taf-icon { + cursor: grab; + + &:active { + cursor: grabbing; } } diff --git a/styles/Apps/common.css b/styles/Apps/common.css index ce56fdd..bbf1212 100644 --- a/styles/Apps/common.css +++ b/styles/Apps/common.css @@ -4,5 +4,6 @@ display: flex; flex-direction: column; gap: 0.5rem; + overflow: auto; } } diff --git a/templates/AttributeManager/attribute-list.hbs b/templates/AttributeManager/attribute-list.hbs index f15d9a9..6bb3c33 100644 --- a/templates/AttributeManager/attribute-list.hbs +++ b/templates/AttributeManager/attribute-list.hbs @@ -4,6 +4,11 @@ class="attribute" data-attribute="{{ attr.id }}" > + {{#if attr.isNew}}