Compare commits
5 commits
main
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
| 58803cb60f | |||
| 621d2575ac | |||
| e28901dcf2 | |||
| 60034dcee2 | |||
| a014bb8e6c |
11 changed files with 371 additions and 1 deletions
1
assets/icons/drag-handle.svg
Normal file
1
assets/icons/drag-handle.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="M45.832 75c0 4.582-3.75 8.332-8.332 8.332s-8.332-3.75-8.332-8.332 3.75-8.332 8.332-8.332 8.332 3.75 8.332 8.332M37.5 41.668c-4.582 0-8.332 3.75-8.332 8.332s3.75 8.332 8.332 8.332 8.332-3.75 8.332-8.332-3.75-8.332-8.332-8.332m0-25c-4.582 0-8.332 3.75-8.332 8.332s3.75 8.332 8.332 8.332 8.332-3.75 8.332-8.332-3.75-8.332-8.332-8.332m25 16.664c4.582 0 8.332-3.75 8.332-8.332s-3.75-8.332-8.332-8.332-8.332 3.75-8.332 8.332 3.75 8.332 8.332 8.332m0 8.336c-4.582 0-8.332 3.75-8.332 8.332s3.75 8.332 8.332 8.332 8.332-3.75 8.332-8.332-3.75-8.332-8.332-8.332m0 25c-4.582 0-8.332 3.75-8.332 8.332s3.75 8.332 8.332 8.332 8.332-3.75 8.332-8.332-3.75-8.332-8.332-8.332"/></svg>
|
||||||
|
After Width: | Height: | Size: 736 B |
|
|
@ -76,6 +76,11 @@
|
||||||
"name": "Hotbar Settings",
|
"name": "Hotbar Settings",
|
||||||
"hint": "Tweaks that modify Foundry's hotbar",
|
"hint": "Tweaks that modify Foundry's hotbar",
|
||||||
"label": "Configure 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": {
|
"keybindings": {
|
||||||
|
|
@ -85,6 +90,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
|
"discard-changes": "Discard Changes",
|
||||||
"no-settings-to-display": "No settings to display",
|
"no-settings-to-display": "No settings to display",
|
||||||
"make-global-reference": "Make Global Reference",
|
"make-global-reference": "Make Global Reference",
|
||||||
"StatusEffectIconConfig": {
|
"StatusEffectIconConfig": {
|
||||||
|
|
@ -92,6 +98,11 @@
|
||||||
"no-status-effects": "No status effects detected, this is most likely due to your game system or other modules.",
|
"no-status-effects": "No status effects detected, this is most likely due to your game system or other modules.",
|
||||||
"remove-override": "Remove custom override",
|
"remove-override": "Remove custom override",
|
||||||
"select-using-image-tagger": "Select using Image Tagger"
|
"select-using-image-tagger": "Select using Image Tagger"
|
||||||
|
},
|
||||||
|
"SidebarTabRearranger": {
|
||||||
|
"title": "Rearrange Sidebar Tabs",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifs": {
|
"notifs": {
|
||||||
|
|
|
||||||
164
module/apps/SidebarTabRearranger.mjs
Normal file
164
module/apps/SidebarTabRearranger.mjs
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
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) {
|
||||||
|
// #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
|
||||||
|
#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
|
||||||
|
|
||||||
|
// #region Data Prep
|
||||||
|
async _prepareContext() {
|
||||||
|
const ctx = {
|
||||||
|
meta: {
|
||||||
|
idp: this.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = ui.sidebar.constructor.TABS;
|
||||||
|
ctx.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 };
|
||||||
|
|
||||||
|
if (documentName) {
|
||||||
|
tooltip ??= getDocumentClass(documentName).metadata.labelPlural;
|
||||||
|
icon ??= CONFIG[documentName]?.sidebarIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.tabs.push({
|
||||||
|
id,
|
||||||
|
name: game.i18n.localize(tooltip),
|
||||||
|
icon,
|
||||||
|
nextIndex: i + 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
// #endregion Data Prep
|
||||||
|
|
||||||
|
// #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
|
||||||
|
};
|
||||||
|
|
@ -10,6 +10,7 @@ import { hotbarButtonGap } from "../tweaks/hotbarButtonGap.mjs";
|
||||||
import { hotbarButtonSize } from "../tweaks/hotbarButtonSize.mjs";
|
import { hotbarButtonSize } from "../tweaks/hotbarButtonSize.mjs";
|
||||||
import { preventTokenRotation } from "../tweaks/preventTokenRotation.mjs";
|
import { preventTokenRotation } from "../tweaks/preventTokenRotation.mjs";
|
||||||
import { preventUserConfigOpen } from "../tweaks/preventUserConfigOpen.mjs";
|
import { preventUserConfigOpen } from "../tweaks/preventUserConfigOpen.mjs";
|
||||||
|
import { rearrangeSidebarTabs } from "../tweaks/rearrangeSidebarTabs.mjs";
|
||||||
import { repositionHotbar } from "../tweaks/repositionHotbar.mjs";
|
import { repositionHotbar } from "../tweaks/repositionHotbar.mjs";
|
||||||
import { startingSidebarTab } from "../tweaks/startingSidebarTab.mjs";
|
import { startingSidebarTab } from "../tweaks/startingSidebarTab.mjs";
|
||||||
import { startSidebarExpanded } from "../tweaks/startSidebarExpanded.mjs";
|
import { startSidebarExpanded } from "../tweaks/startSidebarExpanded.mjs";
|
||||||
|
|
@ -50,6 +51,7 @@ Hooks.on(`setup`, () => {
|
||||||
repositionHotbar();
|
repositionHotbar();
|
||||||
|
|
||||||
customStatusIcons();
|
customStatusIcons();
|
||||||
|
rearrangeSidebarTabs();
|
||||||
chatImageLinks();
|
chatImageLinks();
|
||||||
chatSidebarBackground();
|
chatSidebarBackground();
|
||||||
startSidebarExpanded();
|
startSidebarExpanded();
|
||||||
|
|
|
||||||
39
module/tweaks/rearrangeSidebarTabs.mjs
Normal file
39
module/tweaks/rearrangeSidebarTabs.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
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}World`, {
|
||||||
|
scope: `world`,
|
||||||
|
config: false,
|
||||||
|
type: Array,
|
||||||
|
});
|
||||||
|
game.settings.register(__ID__, `${key}User`, {
|
||||||
|
scope: `user`,
|
||||||
|
config: false,
|
||||||
|
type: Array,
|
||||||
|
});
|
||||||
|
// #endregion Registration
|
||||||
|
|
||||||
|
// #region Implementation
|
||||||
|
// TODO: do this
|
||||||
|
// #endregion Implementation
|
||||||
|
|
||||||
|
status[key] = SettingStatusEnum.Registered;
|
||||||
|
};
|
||||||
31
module/utils/performArraySort.mjs
Normal file
31
module/utils/performArraySort.mjs
Normal file
|
|
@ -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];
|
||||||
|
};
|
||||||
69
styles/apps/SidebarTabRearranger.css
Normal file
69
styles/apps/SidebarTabRearranger.css
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
.oft.SidebarTabRearranger {
|
||||||
|
> .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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
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 {
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-label {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
@import url("./repositionHotbar.css") layer(tweaks);
|
@import url("./repositionHotbar.css") layer(tweaks);
|
||||||
|
|
||||||
@import url("./apps/common.css") layer(apps);
|
@import url("./apps/common.css") layer(apps);
|
||||||
|
@import url("./apps/SidebarTabRearranger.css") layer(apps);
|
||||||
@import url("./apps/StatusEffectIconConfig.css") layer(apps);
|
@import url("./apps/StatusEffectIconConfig.css") layer(apps);
|
||||||
|
|
||||||
/* Make the chat sidebar the same width as all the other tabs */
|
/* Make the chat sidebar the same width as all the other tabs */
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-floppy-disk" inert aria-hidden="true"></i>
|
<oft-icon
|
||||||
|
name="icons/save"
|
||||||
|
aria-hidden="true"
|
||||||
|
var:fill="currentColor"
|
||||||
|
var:size="1.25rem"
|
||||||
|
></oft-icon>
|
||||||
{{localize "SETTINGS.Save"}}
|
{{localize "SETTINGS.Save"}}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
14
templates/SidebarTabRearranger/footer.hbs
Normal file
14
templates/SidebarTabRearranger/footer.hbs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<footer>
|
||||||
|
<button type="close">
|
||||||
|
{{ localize "OFT.apps.discard-changes" }}
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
<oft-icon
|
||||||
|
name="icons/save"
|
||||||
|
aria-hidden="true"
|
||||||
|
var:fill="currentColor"
|
||||||
|
var:size="1rem"
|
||||||
|
></oft-icon>
|
||||||
|
{{ localize "Save Changes" }}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
33
templates/SidebarTabRearranger/list.hbs
Normal file
33
templates/SidebarTabRearranger/list.hbs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<main>
|
||||||
|
<div class="top-label">
|
||||||
|
{{ localize "OFT.apps.SidebarTabRearranger.top" }}
|
||||||
|
</div>
|
||||||
|
<div class="tab-list" data-tooltip-direction="UP">
|
||||||
|
<div
|
||||||
|
class="drop-zone"
|
||||||
|
data-move-to-index="0"
|
||||||
|
></div>
|
||||||
|
{{#each tabs as | tab |}}
|
||||||
|
<div
|
||||||
|
class="emulate-button tab"
|
||||||
|
data-tab-id="{{tab.id}}"
|
||||||
|
data-tooltip="{{tab.name}}"
|
||||||
|
>
|
||||||
|
<span class="sidebar-icon {{tab.icon}}"></span>
|
||||||
|
<oft-icon
|
||||||
|
class="drag-handle-icon"
|
||||||
|
name="icons/drag-handle"
|
||||||
|
var:fill="color-mix(in srgb, currentColor 75%, black 25%)"
|
||||||
|
var:size="1rem"
|
||||||
|
></oft-icon>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="drop-zone"
|
||||||
|
data-move-to-index="{{tab.nextIndex}}"
|
||||||
|
></div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
<div class="bottom-label">
|
||||||
|
{{ localize "OFT.apps.SidebarTabRearranger.bottom" }}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue