From a014bb8e6ccd908adf6b0944647b92578193989e Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 15 Feb 2026 18:40:22 -0700 Subject: [PATCH 1/5] Add drag handle icon --- assets/icons/drag-handle.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/icons/drag-handle.svg diff --git a/assets/icons/drag-handle.svg b/assets/icons/drag-handle.svg new file mode 100644 index 0000000..b7c2d73 --- /dev/null +++ b/assets/icons/drag-handle.svg @@ -0,0 +1 @@ + \ No newline at end of file From 60034dcee28aecb79f78be9ddda7d2572aca71bb Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 15 Feb 2026 18:40:40 -0700 Subject: [PATCH 2/5] Add foundation of the application and templates --- langs/en-ca.json | 8 +++ module/apps/SidebarTabRearranger.mjs | 72 +++++++++++++++++++++++ module/hooks/setup.mjs | 2 + module/tweaks/rearrangeSidebarTabs.mjs | 35 +++++++++++ styles/apps/SidebarTabRearranger.css | 29 +++++++++ styles/main.css | 1 + templates/SidebarTabRearranger/footer.hbs | 3 + templates/SidebarTabRearranger/list.hbs | 15 +++++ 8 files changed, 165 insertions(+) create mode 100644 module/apps/SidebarTabRearranger.mjs create mode 100644 module/tweaks/rearrangeSidebarTabs.mjs create mode 100644 styles/apps/SidebarTabRearranger.css create mode 100644 templates/SidebarTabRearranger/footer.hbs create mode 100644 templates/SidebarTabRearranger/list.hbs diff --git a/langs/en-ca.json b/langs/en-ca.json index a64ce8d..e8fee74 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -76,6 +76,11 @@ "name": "Hotbar Settings", "hint": "Tweaks that modify Foundry's hotbar", "label": "Configure Hotbar" + }, + "rearrangeSidebarTabs": { + "name": "Rearrange Sidebar Tabs", + "hint": "(v13+) Allows you to customize the order the right-hand sidebar tabs appear in. Including module-added sidebar tabs.", + "label": "Change Tab Order" } }, "keybindings": { @@ -92,6 +97,9 @@ "no-status-effects": "No status effects detected, this is most likely due to your game system or other modules.", "remove-override": "Remove custom override", "select-using-image-tagger": "Select using Image Tagger" + }, + "SidebarTabRearranger": { + "title": "Rearrange Sidebar Tabs" } }, "notifs": { diff --git a/module/apps/SidebarTabRearranger.mjs b/module/apps/SidebarTabRearranger.mjs new file mode 100644 index 0000000..fa15d19 --- /dev/null +++ b/module/apps/SidebarTabRearranger.mjs @@ -0,0 +1,72 @@ +import { __ID__, filePath } from "../consts.mjs"; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +const { getDocumentClass } = foundry.utils; + +export class SidebarTabRearranger extends HandlebarsApplicationMixin(ApplicationV2) { + // #region Options + static DEFAULT_OPTIONS = { + tag: `form`, + classes: [ + __ID__, + `SidebarTabRearranger`, + ], + window: { + title: `OFT.apps.SidebarTabRearranger.title`, + }, + position: {}, + form: { + handler: this.#onSubmit, + closeOnSubmit: true, + submitOnChange: false, + }, + actions: {}, + }; + + static PARTS = { + list: { template: filePath(`templates/SidebarTabRearranger/list.hbs`) }, + footer: { template: filePath(`templates/SidebarTabRearranger/footer.hbs`) }, + }; + // #endregion Options + + // #region Instance Data + // #endregion Instance Data + + // #region Lifecycle + /** @this {SidebarTabRearranger} */ + static async #onSubmit() {}; + // #endregion Lifecycle + + // #region Data Prep + async _prepareContext() { + const ctx = { + meta: { + idp: this.id, + }, + }; + + const tabs = ui.sidebar.constructor.TABS; + ctx.tabs = []; + for (const [id, tab] of Object.entries(tabs)) { + let { documentName, gmOnly, tooltip, icon } = tab; + if (gmOnly && !game.user.isGM) { continue }; + + if (documentName) { + tooltip ??= getDocumentClass(documentName).metadata.labelPlural; + icon ??= CONFIG[documentName]?.sidebarIcon; + }; + + ctx.tabs.push({ + id, + name: game.i18n.localize(tooltip), + icon, + }); + }; + + return ctx; + }; + // #endregion Data Prep + + // #region Actions + // #endregion Actions +}; diff --git a/module/hooks/setup.mjs b/module/hooks/setup.mjs index b3d5fbf..12647b7 100644 --- a/module/hooks/setup.mjs +++ b/module/hooks/setup.mjs @@ -10,6 +10,7 @@ import { hotbarButtonGap } from "../tweaks/hotbarButtonGap.mjs"; import { hotbarButtonSize } from "../tweaks/hotbarButtonSize.mjs"; import { preventTokenRotation } from "../tweaks/preventTokenRotation.mjs"; import { preventUserConfigOpen } from "../tweaks/preventUserConfigOpen.mjs"; +import { rearrangeSidebarTabs } from "../tweaks/rearrangeSidebarTabs.mjs"; import { repositionHotbar } from "../tweaks/repositionHotbar.mjs"; import { startingSidebarTab } from "../tweaks/startingSidebarTab.mjs"; import { startSidebarExpanded } from "../tweaks/startSidebarExpanded.mjs"; @@ -50,6 +51,7 @@ Hooks.on(`setup`, () => { repositionHotbar(); customStatusIcons(); + rearrangeSidebarTabs(); chatImageLinks(); chatSidebarBackground(); startSidebarExpanded(); diff --git a/module/tweaks/rearrangeSidebarTabs.mjs b/module/tweaks/rearrangeSidebarTabs.mjs new file mode 100644 index 0000000..fe35869 --- /dev/null +++ b/module/tweaks/rearrangeSidebarTabs.mjs @@ -0,0 +1,35 @@ +import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; +import { __ID__ } from "../consts.mjs"; +import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; +import { SidebarTabRearranger } from "../apps/SidebarTabRearranger.mjs"; + +export const key = `rearrangeSidebarTabs`; + +export function rearrangeSidebarTabs() { + status[key] = SettingStatusEnum.Unknown; + if (preventTweakRegistration(key)) { return }; + + // #region Registration + Logger.log(`Registering tweak: ${key}`); + game.settings.registerMenu(__ID__, `${key}Menu`, { + name: `OFT.menu.${key}.name`, + hint: `OFT.menu.${key}.hint`, + label: `OFT.menu.${key}.label`, + restricted: false, + type: SidebarTabRearranger, + }); + game.settings.register(__ID__, key, { + scope: `user`, + config: false, + type: Array, + default: [], + }); + // #endregion Registration + + // #region Implementation + // TODO: do this + // #endregion Implementation + + status[key] = SettingStatusEnum.Registered; +}; diff --git a/styles/apps/SidebarTabRearranger.css b/styles/apps/SidebarTabRearranger.css new file mode 100644 index 0000000..dc97660 --- /dev/null +++ b/styles/apps/SidebarTabRearranger.css @@ -0,0 +1,29 @@ +.oft.SidebarTabRearranger { + ol { + display: flex; + flex-direction: column; + gap: 8px; + list-style-type: none; + margin: 0; + padding: 0; + } + + li { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + cursor: var(--cursor-grab); + } + + .emulate-button { + --size: 32px; + display: flex; + justify-content: center; + align-items: center; + height: var(--size); + aspect-ratio: 1; + border: 1px solid var(--color-light-5); + border-radius: 4px; + } +} diff --git a/styles/main.css b/styles/main.css index 63794a5..bcb2caf 100644 --- a/styles/main.css +++ b/styles/main.css @@ -6,6 +6,7 @@ @import url("./repositionHotbar.css") layer(tweaks); @import url("./apps/common.css") layer(apps); +@import url("./apps/SidebarTabRearranger.css") layer(apps); @import url("./apps/StatusEffectIconConfig.css") layer(apps); /* Make the chat sidebar the same width as all the other tabs */ diff --git a/templates/SidebarTabRearranger/footer.hbs b/templates/SidebarTabRearranger/footer.hbs new file mode 100644 index 0000000..1353e78 --- /dev/null +++ b/templates/SidebarTabRearranger/footer.hbs @@ -0,0 +1,3 @@ +
+ +
diff --git a/templates/SidebarTabRearranger/list.hbs b/templates/SidebarTabRearranger/list.hbs new file mode 100644 index 0000000..5ec612c --- /dev/null +++ b/templates/SidebarTabRearranger/list.hbs @@ -0,0 +1,15 @@ +
+
    + {{#each tabs as | tab |}} +
  1. + + {{tab.name}} +
    + +
  2. + {{/each}} +
+
From e28901dcf230b92f28d6570dc54e694c02d606a7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 22 Feb 2026 23:53:27 -0700 Subject: [PATCH 3/5] Update the sidebar tab rearranger to use Drag and Drop (again) --- langs/en-ca.json | 5 +- module/apps/SidebarTabRearranger.mjs | 96 ++++++++++++++++++++++- module/utils/performArraySort.mjs | 31 ++++++++ styles/apps/SidebarTabRearranger.css | 52 ++++++++++-- templates/SidebarTabRearranger/footer.hbs | 13 ++- templates/SidebarTabRearranger/list.hbs | 34 ++++++-- 6 files changed, 213 insertions(+), 18 deletions(-) create mode 100644 module/utils/performArraySort.mjs 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" }} +
From 621d2575acac22bae1ef4859d14cc11ef2b0965c Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 22 Feb 2026 23:53:56 -0700 Subject: [PATCH 4/5] Add a world and user setting variant for the tab order instead of just a user setting --- module/tweaks/rearrangeSidebarTabs.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/module/tweaks/rearrangeSidebarTabs.mjs b/module/tweaks/rearrangeSidebarTabs.mjs index fe35869..9d7e5a7 100644 --- a/module/tweaks/rearrangeSidebarTabs.mjs +++ b/module/tweaks/rearrangeSidebarTabs.mjs @@ -19,11 +19,15 @@ export function rearrangeSidebarTabs() { restricted: false, type: SidebarTabRearranger, }); - game.settings.register(__ID__, key, { + game.settings.register(__ID__, `${key}World`, { + scope: `world`, + config: false, + type: Array, + }); + game.settings.register(__ID__, `${key}User`, { scope: `user`, config: false, type: Array, - default: [], }); // #endregion Registration From 58803cb60f55bae809e0fbc44de278049f0eba23 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 22 Feb 2026 23:54:14 -0700 Subject: [PATCH 5/5] Use the icon I added instead of the font-awesome one --- templates/OFTSettingsMenu/footer.hbs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/templates/OFTSettingsMenu/footer.hbs b/templates/OFTSettingsMenu/footer.hbs index 41f2375..dd31400 100644 --- a/templates/OFTSettingsMenu/footer.hbs +++ b/templates/OFTSettingsMenu/footer.hbs @@ -2,7 +2,12 @@