feat: Implement a movable and detachable Daggerheart countdown tracker with UI and module integration.
This commit is contained in:
commit
8da4684488
4 changed files with 302 additions and 0 deletions
144
scripts/MovableCountdowns.js
Normal file
144
scripts/MovableCountdowns.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* Creates the MovableCountdowns class extending the provided base class.
|
||||
* @param {class} BaseCountdowns The DhCountdowns class from Daggerheart system.
|
||||
* @returns {class} The MovableCountdowns class.
|
||||
*/
|
||||
export function createMovableCountdownsClass(BaseCountdowns) {
|
||||
return class MovableCountdowns extends BaseCountdowns {
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: 'movable-countdowns',
|
||||
tag: 'div', // AppV2
|
||||
classes: ['daggerheart', 'dh-style', 'countdowns', 'movable-countdowns'],
|
||||
window: {
|
||||
icon: 'fa-solid fa-clock-rotate-left',
|
||||
frame: true,
|
||||
title: 'DAGGERHEART.UI.Countdowns.title',
|
||||
positioned: true,
|
||||
resizable: true,
|
||||
minimizable: true
|
||||
},
|
||||
actions: {
|
||||
// Override toggleViewMode to instance method
|
||||
toggleViewMode: MovableCountdowns.prototype._toggleViewMode,
|
||||
|
||||
// Inherited static actions
|
||||
editCountdowns: BaseCountdowns.DEFAULT_OPTIONS.actions.editCountdowns,
|
||||
loopCountdown: BaseCountdowns.DEFAULT_OPTIONS.actions.loopCountdown,
|
||||
decreaseCountdown: BaseCountdowns.DEFAULT_OPTIONS.actions.decreaseCountdown,
|
||||
increaseCountdown: BaseCountdowns.DEFAULT_OPTIONS.actions.increaseCountdown,
|
||||
|
||||
reattach: MovableCountdowns.reattach
|
||||
},
|
||||
position: {
|
||||
width: 320,
|
||||
height: 'auto',
|
||||
top: 100,
|
||||
left: 100
|
||||
}
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
resources: {
|
||||
root: true,
|
||||
template: 'systems/daggerheart/templates/ui/countdowns.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
get element() {
|
||||
return document.getElementById(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-implementation of toggleViewMode that works on this instance.
|
||||
*/
|
||||
async _toggleViewMode() {
|
||||
// Need access to system CONFIG constants.
|
||||
// CONFIG.DH is global.
|
||||
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
|
||||
const appMode = CONFIG.DH.GENERAL.countdownAppMode;
|
||||
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
|
||||
|
||||
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode);
|
||||
|
||||
// Update this instance directly
|
||||
if (newMode === appMode.iconOnly) this.element.classList.add('icon-only');
|
||||
else this.element.classList.remove('icon-only');
|
||||
|
||||
this.render();
|
||||
|
||||
// Note: System tracker won't update automatically here if it's hidden,
|
||||
// but next time it renders it will check flag.
|
||||
}
|
||||
|
||||
/**@inheritdoc */
|
||||
/**@inheritdoc */
|
||||
async _renderFrame(options) {
|
||||
const frame = await super._renderFrame(options);
|
||||
|
||||
const header = frame.querySelector('.window-header');
|
||||
if (!header) return frame;
|
||||
|
||||
// Re-add Close Button if missing
|
||||
// dh-countdownsplus: Fix invisible close button by using <a> tag pattern like system
|
||||
if (!header.querySelector('[data-action="close"]')) {
|
||||
const closeTitle = game.i18n.localize("CLOSE");
|
||||
// Use <a> with <i> child to match system header control styling
|
||||
const closeBtn = document.createElement('a');
|
||||
closeBtn.className = 'header-control';
|
||||
closeBtn.setAttribute('data-action', 'close');
|
||||
closeBtn.setAttribute('data-tooltip', closeTitle);
|
||||
closeBtn.setAttribute('aria-label', closeTitle);
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'fa-solid fa-xmark';
|
||||
closeBtn.appendChild(icon);
|
||||
|
||||
header.appendChild(closeBtn);
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _onRender(context, options) {
|
||||
// Call super to handle template rendering
|
||||
await super._onRender(context, options);
|
||||
|
||||
// Workaround system pinning: Move back to body if pinned
|
||||
if (this.element && this.element.parentElement && this.element.parentElement.id === 'ui-right-column-1') {
|
||||
document.body.appendChild(this.element);
|
||||
}
|
||||
|
||||
// Ensure visibility
|
||||
this.element.hidden = false;
|
||||
}
|
||||
|
||||
static async reattach() {
|
||||
const movableApp = Object.values(ui.windows).find(w => w instanceof MovableCountdowns);
|
||||
if (movableApp) await movableApp.close();
|
||||
|
||||
// Unsuppress system tracker
|
||||
if (ui.countdowns) {
|
||||
ui.countdowns._suppressed = false;
|
||||
await ui.countdowns.render({ force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
/** @override */
|
||||
async close(options = {}) {
|
||||
await super.close(options);
|
||||
// Re-attach on close
|
||||
if (ui.countdowns) {
|
||||
ui.countdowns._suppressed = false;
|
||||
await ui.countdowns.render({ force: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
101
scripts/module.js
Normal file
101
scripts/module.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { createMovableCountdownsClass } from './MovableCountdowns.js';
|
||||
|
||||
const MODULE_ID = 'dh-countdownsplus';
|
||||
let MovableCountdowns;
|
||||
|
||||
Hooks.once('init', () => {
|
||||
console.log(`${MODULE_ID} | Initializing Daggerheart Countdowns Plus`);
|
||||
|
||||
// Register settings or other init logic if needed
|
||||
});
|
||||
|
||||
Hooks.on('ready', () => {
|
||||
// Ensure the system's countdown class exists
|
||||
if (!CONFIG.ui.countdowns) {
|
||||
console.error(`${MODULE_ID} | CONFIG.ui.countdowns not found. Is Daggerheart system initialized?`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create our class extending the system's class
|
||||
MovableCountdowns = createMovableCountdownsClass(CONFIG.ui.countdowns);
|
||||
|
||||
// Initial check to see if we should inject button
|
||||
hookSystemCountdowns();
|
||||
});
|
||||
|
||||
function hookSystemCountdowns() {
|
||||
// We want to hook into the render of the system's countdown app.
|
||||
// The system app is likely a singleton at ui.countdowns.
|
||||
|
||||
// Hook on renderDhCountdowns (name of the class in system is DhCountdowns)
|
||||
Hooks.on('renderDhCountdowns', (app, html, data) => {
|
||||
// Only inject if this is the SYSTEM one, not our movable one (ours inherits, so it might trigger this hook too?)
|
||||
// DhCountdowns extends ApplicationV2. AppV2 emits 'renderApplicationV2' and 'renderMyClass'.
|
||||
// So 'renderDhCountdowns' will fire for both.
|
||||
|
||||
if (app instanceof MovableCountdowns) return;
|
||||
|
||||
// Find the header to inject button
|
||||
// html is the HTMLElement in AppV2 hooks
|
||||
const header = html.querySelector('.window-header');
|
||||
if (!header) return;
|
||||
|
||||
// Check if button already exists
|
||||
if (header.querySelector('[data-action="detach-countdowns"]')) return;
|
||||
|
||||
// Create Detach Button
|
||||
const detachTooltip = "Detach"; // TODO: Localize
|
||||
const detachBtn = document.createElement('a'); // System uses <a> for header controls
|
||||
detachBtn.className = 'header-control';
|
||||
detachBtn.dataset.tooltip = detachTooltip;
|
||||
detachBtn.dataset.action = 'detach-countdowns';
|
||||
detachBtn.innerHTML = '<i class="fa-solid fa-up-right-from-square"></i>';
|
||||
|
||||
// Add listener
|
||||
detachBtn.addEventListener('click', async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// Open Movable
|
||||
// We assume singleton pattern for movable too
|
||||
let movable = Object.values(ui.windows).find(w => w instanceof MovableCountdowns);
|
||||
if (!movable) {
|
||||
movable = new MovableCountdowns();
|
||||
}
|
||||
await movable.render({ force: true });
|
||||
|
||||
// Close/Hide System App
|
||||
// Suppress future renders
|
||||
if (app) {
|
||||
app._suppressed = true;
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Insert before the "Close" button? System one doesn't have close button.
|
||||
// Insert at end or specific position?
|
||||
// System header controls: Edit, Toggle Mode.
|
||||
// Let's prepend or append.
|
||||
header.insertAdjacentElement('beforeend', detachBtn);
|
||||
});
|
||||
|
||||
// Patch system render to respect suppression
|
||||
if (ui.countdowns && !ui.countdowns._patched) {
|
||||
const originalRender = ui.countdowns.render.bind(ui.countdowns);
|
||||
ui.countdowns.render = function (options, ...args) {
|
||||
if (this._suppressed) {
|
||||
// Update internal state but don't show
|
||||
// If data update, we might need to update OUR instance?
|
||||
// But socket hooks call render() on ui.countdowns.
|
||||
// We should notify Movable if it's open.
|
||||
|
||||
const movable = Object.values(ui.windows).find(w => w instanceof MovableCountdowns);
|
||||
if (movable) movable.render();
|
||||
|
||||
return this;
|
||||
}
|
||||
return originalRender(options, ...args);
|
||||
};
|
||||
ui.countdowns._patched = true;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue