diff --git a/langs/en-ca.json b/langs/en-ca.json index e8fee74..8bd6b3f 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -90,6 +90,7 @@ } }, "apps": { + "discard-changes": "Discard Changes", "no-settings-to-display": "No settings to display", "make-global-reference": "Make Global Reference", "StatusEffectIconConfig": { @@ -99,7 +100,9 @@ "select-using-image-tagger": "Select using Image Tagger" }, "SidebarTabRearranger": { - "title": "Rearrange Sidebar Tabs" + "title": "Rearrange Sidebar Tabs", + "top": "Top", + "bottom": "Bottom" } }, "notifs": { diff --git a/module/apps/SidebarTabRearranger.mjs b/module/apps/SidebarTabRearranger.mjs index fa15d19..53ee0fa 100644 --- a/module/apps/SidebarTabRearranger.mjs +++ b/module/apps/SidebarTabRearranger.mjs @@ -1,6 +1,8 @@ import { __ID__, filePath } from "../consts.mjs"; +import { performArraySort } from "../utils/performArraySort.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +const { DragDrop } = foundry.applications.ux; const { getDocumentClass } = foundry.utils; export class SidebarTabRearranger extends HandlebarsApplicationMixin(ApplicationV2) { @@ -20,7 +22,8 @@ export class SidebarTabRearranger extends HandlebarsApplicationMixin(Application closeOnSubmit: true, submitOnChange: false, }, - actions: {}, + actions: { + }, }; static PARTS = { @@ -30,9 +33,33 @@ export class SidebarTabRearranger extends HandlebarsApplicationMixin(Application // #endregion Options // #region Instance Data + #order = []; + + constructor(...args) { + super(...args); + + // TODO: define this using the game settings + this.#order = Object.keys(ui.sidebar.constructor.TABS); + }; // #endregion Instance Data // #region Lifecycle + async _onRender(...args) { + await super._onRender(...args); + + new DragDrop.implementation({ + dragSelector: `.tab`, + dropSelector: `.drop-zone`, + callbacks: { + dragstart: this.#onDragStart.bind(this), + dragenter: this.#onDragEnter.bind(this), + dragleave: this.#onDragLeave.bind(this), + dragend: this.#onDragEnd.bind(this), + drop: this.#onDrop.bind(this), + }, + }).bind(this.element); + }; + /** @this {SidebarTabRearranger} */ static async #onSubmit() {}; // #endregion Lifecycle @@ -47,7 +74,11 @@ export class SidebarTabRearranger extends HandlebarsApplicationMixin(Application const tabs = ui.sidebar.constructor.TABS; ctx.tabs = []; - for (const [id, tab] of Object.entries(tabs)) { + for (let i = 0; i < this.#order.length; i++) { + const id = this.#order[i]; + const tab = tabs[id]; + if (!tab) { continue }; + let { documentName, gmOnly, tooltip, icon } = tab; if (gmOnly && !game.user.isGM) { continue }; @@ -60,6 +91,7 @@ export class SidebarTabRearranger extends HandlebarsApplicationMixin(Application id, name: game.i18n.localize(tooltip), icon, + nextIndex: i + 1, }); }; @@ -69,4 +101,64 @@ export class SidebarTabRearranger extends HandlebarsApplicationMixin(Application // #region Actions // #endregion Actions + + // #region Drag & Drop + #onDragStart(event) { + /** @type {HTMLLIElement|undefined} */ + const target = event.target.closest(`[data-tab-id]`); + if (!target) { return }; + + const tabID = target.dataset.tabId; + + target.classList.add(`no-hover-styles`); + event.dataTransfer.setDragImage(target, 0, 0); + event.dataTransfer.setData(`oft/tab`, tabID); + target.closest(`.tab-list`)?.classList.add(`dragging`); + + /* + This timeout is required to get the difference between the drag + image and the element in-DOM, because this puts the class removal + in a subsequent event cycle instead of being handled in the current + cycle. + */ + setTimeout(() => target.classList.remove(`no-hover-styles`), 0); + }; + + #onDragEnter(event) { + event.currentTarget.style.setProperty(`--colour`, `#00aa00`); + }; + + #onDragLeave(event) { + event.currentTarget.style.removeProperty(`--colour`); + }; + + #onDragEnd() { + this.element.querySelector(`.tab-list`)?.classList.remove(`dragging`); + this.element + .querySelectorAll(`[style="--colour: #00aa00"]`) + .forEach(el => el.style.removeProperty(`--colour`)); + }; + + #onDrop(event) { + const droppedID = event.dataTransfer.getData(`oft/tab`); + this.element.querySelector(`.tab-list`)?.classList.remove(`dragging`); + event.currentTarget?.style?.removeProperty(`--colour`); + if (!droppedID) { return }; + + const droppedIndex = this.#order.findIndex(t => t === droppedID); + const dropTarget = event.currentTarget; + const targetIndex = parseInt(dropTarget?.dataset.moveToIndex); + if ( + !dropTarget + || droppedIndex < 0 + || targetIndex === droppedIndex + ) { return }; + + this.#order = performArraySort( + droppedID, + { targetIndex, list: this.#order }, + ); + this.render({ parts: [`list`] }); + }; + // #endregion Drag & Drop }; diff --git a/module/utils/performArraySort.mjs b/module/utils/performArraySort.mjs new file mode 100644 index 0000000..04fa195 --- /dev/null +++ b/module/utils/performArraySort.mjs @@ -0,0 +1,31 @@ +export function performArraySort( + element, + { list, targetIndex }, +) { + + // Case: same position + if (list.indexOf(el => el === element) === targetIndex) { + return Array.from(list); + }; + + // Case: start of array + if (targetIndex === 0) { + list = list.filter(el => el !== element); + return [element, ...list]; + }; + + // Case: end of array + if (targetIndex === list.length - 1) { + list = list.filter(el => el !== element); + return [...list, element]; + }; + + // Case: middle of array + const front = list + .slice(0, targetIndex) + .filter(el => el !== element); + const back = list + .slice(targetIndex) + .filter(el => el !== element); + return [...front, element, ...back]; +}; diff --git a/styles/apps/SidebarTabRearranger.css b/styles/apps/SidebarTabRearranger.css index dc97660..efd1f19 100644 --- a/styles/apps/SidebarTabRearranger.css +++ b/styles/apps/SidebarTabRearranger.css @@ -1,19 +1,55 @@ .oft.SidebarTabRearranger { - ol { - display: flex; - flex-direction: column; + > .window-content { + gap: 16px; + } + + footer { + display: grid; + grid-template-columns: repeat(2, 1fr); gap: 8px; + } + + .drop-zone { + --colour: transparent; + background: color-mix(in srgb, var(--colour) 30%, transparent 70%); + border: 1px solid var(--colour); + border-radius: 4px; + transition: all 150ms ease-in-out; + width: 12px; + } + + .tab-list { + display: flex; + flex-direction: row; + row-gap: 8px; + column-gap: 2px; list-style-type: none; margin: 0; padding: 0; + + &.dragging > .drop-zone { + --colour: #c9593f; + } } - li { + .tab { + margin: 0; display: flex; - flex-direction: row; + flex-direction: column; align-items: center; - gap: 12px; cursor: var(--cursor-grab); + + > .drag-handle-icon { + display: none; + } + &:hover:not(.no-hover-styles) { + > .sidebar-icon { + display: none; + } + > .drag-handle-icon { + display: initial; + } + } } .emulate-button { @@ -26,4 +62,8 @@ border: 1px solid var(--color-light-5); border-radius: 4px; } + + .bottom-label { + text-align: right; + } } diff --git a/templates/SidebarTabRearranger/footer.hbs b/templates/SidebarTabRearranger/footer.hbs index 1353e78..f159af4 100644 --- a/templates/SidebarTabRearranger/footer.hbs +++ b/templates/SidebarTabRearranger/footer.hbs @@ -1,3 +1,14 @@ diff --git a/templates/SidebarTabRearranger/list.hbs b/templates/SidebarTabRearranger/list.hbs index 5ec612c..63dd785 100644 --- a/templates/SidebarTabRearranger/list.hbs +++ b/templates/SidebarTabRearranger/list.hbs @@ -1,15 +1,33 @@
-
    +
    + {{ localize "OFT.apps.SidebarTabRearranger.top" }} +
    +
    +
    {{#each tabs as | tab |}} -
  1. - - {{tab.name}} -
    +
    + -
  2. +
    +
    {{/each}} -
+ +
+ {{ localize "OFT.apps.SidebarTabRearranger.bottom" }} +