From 3d09e19c52a6ad7e668d8d3cafa16b7aff3c60cb Mon Sep 17 00:00:00 2001 From: Florian Zumpe Date: Tue, 19 May 2026 16:28:16 +0200 Subject: [PATCH] Replaced Scripts by ESModules and added cool stuff --- module.json | 10 +- scripts/user-notes-constants.js | 19 ++ scripts/user-notes-controls.js | 48 ++++ scripts/user-notes-settings.js | 427 ++++++++++++++++++++++++++++++++ scripts/user-notes-storage.js | 67 +++++ scripts/user-notes-window.js | 358 ++++++++++++++++++++++++++ scripts/user-notes.js | 256 +++---------------- styles/user-notes-settings.css | 90 +++++++ styles/user-notes-window.css | 133 ++++++++++ styles/user-notes.css | 100 -------- 10 files changed, 1179 insertions(+), 329 deletions(-) create mode 100644 scripts/user-notes-constants.js create mode 100644 scripts/user-notes-controls.js create mode 100644 scripts/user-notes-settings.js create mode 100644 scripts/user-notes-storage.js create mode 100644 scripts/user-notes-window.js create mode 100644 styles/user-notes-settings.css create mode 100644 styles/user-notes-window.css delete mode 100644 styles/user-notes.css diff --git a/module.json b/module.json index 75f5ee9..3c5352b 100644 --- a/module.json +++ b/module.json @@ -1,22 +1,22 @@ { "id": "user-notes", "title": "User Notes", - "description": "User Notes: ein einfaches lokales Notizfenster für Foundry VTT v14. Notizen werden im Browser-localStorage pro Welt und Benutzer gespeichert.", - "version": "1.0.1", + "version": "1.0.0", "authors": [ { - "name": "ChatGPT" + "name": "Florian Zumpe" } ], "compatibility": { "minimum": "14", "verified": "14" }, - "scripts": [ + "esmodules": [ "scripts/user-notes.js" ], "styles": [ - "styles/user-notes.css" + "styles/user-notes-window.css", + "styles/user-notes-settings.css" ], "languages": [], "url": "https://github.com/fzumpe/foundry-usernotes", diff --git a/scripts/user-notes-constants.js b/scripts/user-notes-constants.js new file mode 100644 index 0000000..5232591 --- /dev/null +++ b/scripts/user-notes-constants.js @@ -0,0 +1,19 @@ +export const USER_NOTES_MODULE_ID = "user-notes"; +export const USER_NOTES_WINDOW_ID = "user-notes-window"; +export const USER_NOTES_TOOL_ID = "user-notes-open"; + +export const USER_NOTES_DEFAULT_POSITION = { + left: 200, + top: 150, + width: 500, + height: 400 +}; + +export const USER_NOTES_MIN_WIDTH = 280; +export const USER_NOTES_MIN_HEIGHT = 180; +export const USER_NOTES_VIEWPORT_MARGIN = 40; + +export const USER_NOTES_DEFAULT_BACKGROUND = "rgba(25, 24, 19, 0.96)"; +export const USER_NOTES_DEFAULT_TEXT_COLOR = "#f0f0e0"; +export const USER_NOTES_DEFAULT_TEXTAREA_BACKGROUND = "rgba(255, 255, 255, 0.92)"; +export const USER_NOTES_DEFAULT_TEXTAREA_COLOR = "#111111"; diff --git a/scripts/user-notes-controls.js b/scripts/user-notes-controls.js new file mode 100644 index 0000000..7f8d377 --- /dev/null +++ b/scripts/user-notes-controls.js @@ -0,0 +1,48 @@ +import { + USER_NOTES_MODULE_ID, + USER_NOTES_TOOL_ID +} from "./user-notes-constants.js"; + +import { + userNotesOpenNotes +} from "./user-notes-window.js"; + +export function userNotesRegisterTokenControl(controls) { + console.log("User Notes | registering token control", controls); + + const tokenControl = controls?.tokens; + + if (!tokenControl) { + console.warn("User Notes | controls.tokens fehlt", controls); + return; + } + + if (!tokenControl.tools) { + console.warn("User Notes | controls.tokens.tools fehlt", tokenControl); + return; + } + + tokenControl.tools[USER_NOTES_TOOL_ID] = { + name: USER_NOTES_TOOL_ID, + title: "User Notes öffnen", + icon: "fa-solid fa-note-sticky", + order: Object.keys(tokenControl.tools).length + 1, + button: true, + visible: true, + + onChange: (event, active) => { + console.log("User Notes | onChange", { event, active }); + userNotesOpenNotes(); + }, + + onClick: event => { + console.log("User Notes | onClick", { event }); + userNotesOpenNotes(); + } + }; + + console.log( + "User Notes | token control registered", + tokenControl.tools[USER_NOTES_TOOL_ID] + ); +} diff --git a/scripts/user-notes-settings.js b/scripts/user-notes-settings.js new file mode 100644 index 0000000..9fec390 --- /dev/null +++ b/scripts/user-notes-settings.js @@ -0,0 +1,427 @@ +import { + USER_NOTES_MODULE_ID, + USER_NOTES_WINDOW_ID +} from "./user-notes-constants.js"; + +import { + userNotesLoadAppearance, + userNotesSaveAppearance, + userNotesRemoveSavedAppearance +} from "./user-notes-storage.js"; + +let resetPositionCallback = null; + +const USER_NOTES_APPEARANCE_DEFAULTS = { + windowBackgroundColor: "#191813", + windowBackgroundAlpha: 0.96, + windowTextColor: "#f0f0e0", + textareaBackgroundColor: "#ffffff", + textareaBackgroundAlpha: 0.92, + textareaTextColor: "#111111" +}; + +function userNotesClampAlpha(value, fallback = 1) { + const number = Number(value); + + if (!Number.isFinite(number)) { + return fallback; + } + + return Math.max(0, Math.min(1, number)); +} + +function userNotesNormalizeHexColor(value, fallback) { + const normalized = String(value ?? "").trim(); + + if (/^#[0-9a-f]{6}$/i.test(normalized)) { + return normalized; + } + + return fallback; +} + +function userNotesHexToRgba(hex, alpha) { + const normalized = userNotesNormalizeHexColor(hex, "#000000"); + const match = normalized.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i); + + if (!match) { + return `rgba(0, 0, 0, ${userNotesClampAlpha(alpha)})`; + } + + const red = parseInt(match[1], 16); + const green = parseInt(match[2], 16); + const blue = parseInt(match[3], 16); + + return `rgba(${red}, ${green}, ${blue}, ${userNotesClampAlpha(alpha)})`; +} + +function userNotesLoadValidatedAppearance() { + const values = userNotesLoadAppearance(USER_NOTES_APPEARANCE_DEFAULTS); + + return { + windowBackgroundColor: userNotesNormalizeHexColor( + values.windowBackgroundColor, + USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundColor + ), + windowBackgroundAlpha: userNotesClampAlpha( + values.windowBackgroundAlpha, + USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundAlpha + ), + windowTextColor: userNotesNormalizeHexColor( + values.windowTextColor, + USER_NOTES_APPEARANCE_DEFAULTS.windowTextColor + ), + textareaBackgroundColor: userNotesNormalizeHexColor( + values.textareaBackgroundColor, + USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundColor + ), + textareaBackgroundAlpha: userNotesClampAlpha( + values.textareaBackgroundAlpha, + USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundAlpha + ), + textareaTextColor: userNotesNormalizeHexColor( + values.textareaTextColor, + USER_NOTES_APPEARANCE_DEFAULTS.textareaTextColor + ) + }; +} + +export function userNotesRegisterSettings(onResetPosition) { + resetPositionCallback = onResetPosition; + + game.settings.registerMenu(USER_NOTES_MODULE_ID, "appearanceSettings", { + name: "Darstellung", + label: "Farben und Transparenz einstellen", + hint: "Öffnet lokale Colorpicker für Fenster, Notizfeld und Schriftfarben. Die Werte werden nur im localStorage dieses Browsers gespeichert.", + icon: "fas fa-palette", + type: UserNotesAppearanceSettings, + restricted: false + }); + + game.settings.registerMenu(USER_NOTES_MODULE_ID, "resetWindowPosition", { + name: "Position und Größe zurücksetzen", + label: "Jetzt zurücksetzen", + hint: "Setzt nur Position und Größe des User-Notes-Fensters für diesen Browser zurück. Notizen, Farben und Transparenzwerte bleiben unverändert.", + icon: "fas fa-undo", + type: UserNotesDirectResetWindowPosition, + restricted: false + }); +} + +export function userNotesApplySettingsToOpenWindow() { + const win = document.getElementById(USER_NOTES_WINDOW_ID); + + if (!win) { + return; + } + + userNotesApplyWindowSettings(win); +} + +export function userNotesApplyWindowSettings(win) { + const appearance = userNotesLoadValidatedAppearance(); + + win.style.setProperty( + "--user-notes-window-background", + userNotesHexToRgba( + appearance.windowBackgroundColor, + appearance.windowBackgroundAlpha + ) + ); + + win.style.setProperty( + "--user-notes-window-text-color", + appearance.windowTextColor + ); + + win.style.setProperty( + "--user-notes-textarea-background", + userNotesHexToRgba( + appearance.textareaBackgroundColor, + appearance.textareaBackgroundAlpha + ) + ); + + win.style.setProperty( + "--user-notes-textarea-color", + appearance.textareaTextColor + ); +} + +class UserNotesDirectResetWindowPosition extends FormApplication { + render(_force, _options) { + if (typeof resetPositionCallback === "function") { + resetPositionCallback(); + ui.notifications?.info("User Notes: Position und Größe wurden zurückgesetzt."); + } else { + console.warn("User Notes | resetPositionCallback is not available"); + ui.notifications?.warn("User Notes: Reset-Funktion ist nicht verfügbar."); + } + + return this; + } +} + +class UserNotesAppearanceSettings extends FormApplication { + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: "user-notes-appearance-settings", + title: "User Notes Darstellung", + template: null, + width: 520, + height: "auto", + closeOnSubmit: false, + submitOnChange: false, + submitOnClose: false + }); + } + + getData() { + const appearance = userNotesLoadValidatedAppearance(); + + return { + ...appearance, + windowBackgroundAlphaPercent: Math.round(appearance.windowBackgroundAlpha * 100), + textareaBackgroundAlphaPercent: Math.round(appearance.textareaBackgroundAlpha * 100) + }; + } + + async _renderInner(data) { + const html = ` +
+

+ Diese Werte werden lokal im Browser gespeichert. + Foundry-Settings werden dadurch nicht überschrieben und die Seite wird nicht neu geladen. +

+ +
+ Fenster + +
+ +
+ + +
+

Farbe des äußeren Notizfensters.

+
+ +
+ +
+ + ${data.windowBackgroundAlphaPercent}% +
+

0% ist vollständig transparent, 100% ist vollständig deckend.

+
+ +
+ +
+ + +
+

Farbe für Titelleiste, Status und Buttons.

+
+
+ +
+ Notizfeld + +
+ +
+ + +
+

Farbe des eigentlichen Textfeldes.

+
+ +
+ +
+ + ${data.textareaBackgroundAlphaPercent}% +
+

0% ist vollständig transparent, 100% ist vollständig deckend.

+
+ +
+ +
+ + +
+

Farbe des Textes innerhalb des Notizfeldes.

+
+
+ +
+
+ + + +
+ + +
+
+ `; + + return $(html); + } + + activateListeners(html) { + super.activateListeners(html); + + const root = html[0]; + + if (!root) { + console.warn("User Notes | appearance settings root element not found"); + return; + } + + html.on("submit", event => { + event.preventDefault(); + event.stopPropagation(); + return false; + }); + + const syncColorPair = (colorName, textName) => { + const colorInput = root.querySelector(`input[name="${colorName}"]`); + const textInput = root.querySelector(`input[name="${textName}"]`); + + if (!colorInput || !textInput) { + return; + } + + colorInput.addEventListener("input", () => { + textInput.value = colorInput.value; + }); + + textInput.addEventListener("input", () => { + if (/^#[0-9a-f]{6}$/i.test(textInput.value)) { + colorInput.value = textInput.value; + } + }); + }; + + syncColorPair("windowBackgroundColor", "windowBackgroundColorText"); + syncColorPair("windowTextColor", "windowTextColorText"); + syncColorPair("textareaBackgroundColor", "textareaBackgroundColorText"); + syncColorPair("textareaTextColor", "textareaTextColorText"); + + for (const range of root.querySelectorAll('input[type="range"]')) { + const output = range + .closest(".user-notes-range-row") + ?.querySelector("output"); + + const updateOutput = () => { + if (output) { + output.textContent = `${Math.round(Number(range.value) * 100)}%`; + } + }; + + range.addEventListener("input", updateOutput); + range.addEventListener("change", updateOutput); + updateOutput(); + } + + root.querySelector(".user-notes-apply-appearance")?.addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + + this.userNotesSaveAppearanceFromDialog(root, { + closeDialog: false, + notify: true + }); + }); + + root.querySelector(".user-notes-save-appearance")?.addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + + this.userNotesSaveAppearanceFromDialog(root, { + closeDialog: true, + notify: true + }); + }); + + root.querySelector(".user-notes-reset-appearance")?.addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + + userNotesRemoveSavedAppearance(); + userNotesApplySettingsToOpenWindow(); + + ui.notifications?.info("User Notes: Standardfarben wurden wiederhergestellt."); + this.render(true); + }); + } + + userNotesSaveAppearanceFromDialog(root, options = {}) { + const closeDialog = options.closeDialog ?? true; + const notify = options.notify ?? true; + + const getInputValue = name => { + const input = root.querySelector(`[name="${name}"]`); + return input?.value; + }; + + const appearance = { + windowBackgroundColor: userNotesNormalizeHexColor( + getInputValue("windowBackgroundColorText") || getInputValue("windowBackgroundColor"), + USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundColor + ), + windowBackgroundAlpha: userNotesClampAlpha( + getInputValue("windowBackgroundAlpha"), + USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundAlpha + ), + windowTextColor: userNotesNormalizeHexColor( + getInputValue("windowTextColorText") || getInputValue("windowTextColor"), + USER_NOTES_APPEARANCE_DEFAULTS.windowTextColor + ), + textareaBackgroundColor: userNotesNormalizeHexColor( + getInputValue("textareaBackgroundColorText") || getInputValue("textareaBackgroundColor"), + USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundColor + ), + textareaBackgroundAlpha: userNotesClampAlpha( + getInputValue("textareaBackgroundAlpha"), + USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundAlpha + ), + textareaTextColor: userNotesNormalizeHexColor( + getInputValue("textareaTextColorText") || getInputValue("textareaTextColor"), + USER_NOTES_APPEARANCE_DEFAULTS.textareaTextColor + ) + }; + + userNotesSaveAppearance(appearance); + userNotesApplySettingsToOpenWindow(); + + if (notify) { + ui.notifications?.info( + closeDialog + ? "User Notes: Darstellung wurde gespeichert." + : "User Notes: Darstellung wurde angewendet." + ); + } + + if (closeDialog) { + this.close(); + } + } + + async _updateObject(_event, _formData) { + // Wird absichtlich nicht verwendet. + // Die Buttons speichern direkt in localStorage, damit kein nativer GET-Submit stattfindet. + } +} diff --git a/scripts/user-notes-storage.js b/scripts/user-notes-storage.js new file mode 100644 index 0000000..8ec28e8 --- /dev/null +++ b/scripts/user-notes-storage.js @@ -0,0 +1,67 @@ +import { + USER_NOTES_MODULE_ID +} from "./user-notes-constants.js"; + +export function userNotesStorageKey() { + const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world"; + const userId = game.user?.id ?? "unknown-user"; + + return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.notes`; +} + +export function userNotesPositionKey() { + const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world"; + const userId = game.user?.id ?? "unknown-user"; + + return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.position`; +} + +export function userNotesAppearanceKey() { + const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world"; + const userId = game.user?.id ?? "unknown-user"; + + return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.appearance`; +} + +export function userNotesLoadNotes() { + return window.localStorage.getItem(userNotesStorageKey()) ?? ""; +} + +export function userNotesSaveNotes(value) { + window.localStorage.setItem(userNotesStorageKey(), value); +} + +export function userNotesRemoveSavedPosition() { + window.localStorage.removeItem(userNotesPositionKey()); +} + +export function userNotesLoadAppearance(defaults) { + try { + const raw = window.localStorage.getItem(userNotesAppearanceKey()); + + if (!raw) { + return { ...defaults }; + } + + const parsed = JSON.parse(raw); + + return { + ...defaults, + ...parsed + }; + } catch (err) { + console.warn("User Notes | Could not load appearance settings", err); + return { ...defaults }; + } +} + +export function userNotesSaveAppearance(values) { + window.localStorage.setItem( + userNotesAppearanceKey(), + JSON.stringify(values) + ); +} + +export function userNotesRemoveSavedAppearance() { + window.localStorage.removeItem(userNotesAppearanceKey()); +} diff --git a/scripts/user-notes-window.js b/scripts/user-notes-window.js new file mode 100644 index 0000000..2090b60 --- /dev/null +++ b/scripts/user-notes-window.js @@ -0,0 +1,358 @@ +import { + USER_NOTES_MODULE_ID, + USER_NOTES_WINDOW_ID, + USER_NOTES_DEFAULT_POSITION, + USER_NOTES_MIN_WIDTH, + USER_NOTES_MIN_HEIGHT, + USER_NOTES_VIEWPORT_MARGIN +} from "./user-notes-constants.js"; + +import { + userNotesLoadNotes, + userNotesSaveNotes, + userNotesPositionKey, + userNotesRemoveSavedPosition +} from "./user-notes-storage.js"; + +import { + userNotesApplyWindowSettings +} from "./user-notes-settings.js"; + +let userNotesSaveTimer = null; + +export function userNotesSetStatus(text) { + const status = document.querySelector( + `#${USER_NOTES_WINDOW_ID} .user-notes-status` + ); + + if (status) { + status.textContent = text; + } +} + +export function userNotesDebouncedSave(value) { + window.clearTimeout(userNotesSaveTimer); + userNotesSetStatus("Ungespeichert …"); + + userNotesSaveTimer = window.setTimeout(() => { + userNotesSaveNotes(value); + userNotesSetStatus("Gespeichert"); + }, 250); +} + +export function userNotesApplyPosition(win, position) { + win.style.left = `${position.left}px`; + win.style.top = `${position.top}px`; + win.style.width = `${position.width}px`; + win.style.height = `${position.height}px`; +} + +export function userNotesClampPosition(position) { + const viewportWidth = Math.max( + window.innerWidth, + USER_NOTES_MIN_WIDTH + USER_NOTES_VIEWPORT_MARGIN + ); + + const viewportHeight = Math.max( + window.innerHeight, + USER_NOTES_MIN_HEIGHT + USER_NOTES_VIEWPORT_MARGIN + ); + + const maxWidth = Math.max( + USER_NOTES_MIN_WIDTH, + viewportWidth - USER_NOTES_VIEWPORT_MARGIN + ); + + const maxHeight = Math.max( + USER_NOTES_MIN_HEIGHT, + viewportHeight - USER_NOTES_VIEWPORT_MARGIN + ); + + const width = Math.max( + USER_NOTES_MIN_WIDTH, + Math.min(position.width, maxWidth) + ); + + const height = Math.max( + USER_NOTES_MIN_HEIGHT, + Math.min(position.height, maxHeight) + ); + + const maxLeft = Math.max(0, viewportWidth - width - 20); + const maxTop = Math.max(0, viewportHeight - height - 20); + + const left = Math.max(0, Math.min(position.left, maxLeft)); + const top = Math.max(0, Math.min(position.top, maxTop)); + + return { + left, + top, + width, + height + }; +} + +export function userNotesRestorePosition(win) { + try { + const raw = window.localStorage.getItem(userNotesPositionKey()); + + if (!raw) { + userNotesApplyPosition( + win, + userNotesClampPosition(USER_NOTES_DEFAULT_POSITION) + ); + return; + } + + const pos = JSON.parse(raw); + + const restoredPosition = { + left: Number.isFinite(pos.left) ? pos.left : USER_NOTES_DEFAULT_POSITION.left, + top: Number.isFinite(pos.top) ? pos.top : USER_NOTES_DEFAULT_POSITION.top, + width: Number.isFinite(pos.width) ? pos.width : USER_NOTES_DEFAULT_POSITION.width, + height: Number.isFinite(pos.height) ? pos.height : USER_NOTES_DEFAULT_POSITION.height + }; + + userNotesApplyPosition( + win, + userNotesClampPosition(restoredPosition) + ); + } catch (err) { + console.warn( + `${USER_NOTES_MODULE_ID} | Could not restore note window position`, + err + ); + + userNotesApplyPosition( + win, + userNotesClampPosition(USER_NOTES_DEFAULT_POSITION) + ); + } +} + +export function userNotesSavePosition(win) { + if (!win || win.hidden) { + return; + } + + const rect = win.getBoundingClientRect(); + + if (rect.width < USER_NOTES_MIN_WIDTH || rect.height < USER_NOTES_MIN_HEIGHT) { + return; + } + + const position = userNotesClampPosition({ + left: Math.round(rect.left), + top: Math.round(rect.top), + width: Math.round(rect.width), + height: Math.round(rect.height) + }); + + window.localStorage.setItem( + userNotesPositionKey(), + JSON.stringify(position) + ); +} + +export function userNotesResetPositionAndSize() { + userNotesRemoveSavedPosition(); + + const win = document.getElementById(USER_NOTES_WINDOW_ID); + + if (win) { + userNotesApplyPosition( + win, + userNotesClampPosition(USER_NOTES_DEFAULT_POSITION) + ); + + userNotesSavePosition(win); + } +} + +export function userNotesBringToFront(win) { + const currentTop = Number.parseInt(win.style.zIndex || "100000", 10); + win.style.zIndex = String(Math.max(currentTop + 1, 100000)); +} + +export function userNotesMakeDraggable(win) { + const handle = win.querySelector(".user-notes-titlebar"); + + if (!handle) { + return; + } + + let drag = null; + + handle.addEventListener("pointerdown", event => { + const target = event.target; + + if (target instanceof HTMLElement && target.closest("button")) { + return; + } + + const rect = win.getBoundingClientRect(); + + drag = { + pointerId: event.pointerId, + startX: event.clientX, + startY: event.clientY, + left: rect.left, + top: rect.top + }; + + userNotesBringToFront(win); + handle.setPointerCapture(event.pointerId); + }); + + handle.addEventListener("pointermove", event => { + if (!drag || drag.pointerId !== event.pointerId) { + return; + } + + const rect = win.getBoundingClientRect(); + + const clamped = userNotesClampPosition({ + left: drag.left + event.clientX - drag.startX, + top: drag.top + event.clientY - drag.startY, + width: rect.width, + height: rect.height + }); + + win.style.left = `${clamped.left}px`; + win.style.top = `${clamped.top}px`; + }); + + handle.addEventListener("pointerup", event => { + if (!drag || drag.pointerId !== event.pointerId) { + return; + } + + drag = null; + userNotesSavePosition(win); + }); + + handle.addEventListener("pointercancel", event => { + if (!drag || drag.pointerId !== event.pointerId) { + return; + } + + drag = null; + userNotesSavePosition(win); + }); +} + +export function userNotesOpenNotes() { + let win = document.getElementById(USER_NOTES_WINDOW_ID); + + if (win) { + win.hidden = false; + userNotesRestorePosition(win); + userNotesApplyWindowSettings(win); + userNotesBringToFront(win); + win.querySelector("textarea")?.focus(); + return; + } + + win = document.createElement("section"); + win.id = USER_NOTES_WINDOW_ID; + win.className = "user-notes-window"; + + win.innerHTML = ` +
+
+ + User Notes +
+ +
+ Gespeichert + + + + +
+
+ +
+ +
+ `; + + document.body.appendChild(win); + + userNotesApplyWindowSettings(win); + userNotesRestorePosition(win); + + const textarea = win.querySelector(".user-notes-textarea"); + + if (!(textarea instanceof HTMLTextAreaElement)) { + console.error(`${USER_NOTES_MODULE_ID} | Notes textarea could not be created.`); + return; + } + + textarea.value = userNotesLoadNotes(); + + textarea.addEventListener("input", event => { + userNotesDebouncedSave(event.currentTarget.value); + }); + + win.querySelector(".user-notes-save")?.addEventListener("click", () => { + userNotesSaveNotes(textarea.value); + userNotesSetStatus("Gespeichert"); + userNotesSavePosition(win); + }); + + win.querySelector(".user-notes-close")?.addEventListener("click", () => { + userNotesSaveNotes(textarea.value); + userNotesSavePosition(win); + win.hidden = true; + }); + + win.addEventListener("pointerdown", () => { + userNotesBringToFront(win); + }); + + const resizeObserver = new ResizeObserver(() => { + if (win.hidden) { + return; + } + + const rect = win.getBoundingClientRect(); + + if (rect.width < USER_NOTES_MIN_WIDTH || rect.height < USER_NOTES_MIN_HEIGHT) { + return; + } + + userNotesSavePosition(win); + }); + + resizeObserver.observe(win); + + userNotesMakeDraggable(win); + userNotesBringToFront(win); + textarea.focus(); +} + +export function userNotesRefreshOpenWindow() { + const oldWin = document.getElementById(USER_NOTES_WINDOW_ID); + + if (!oldWin) { + return; + } + + const textarea = oldWin.querySelector(".user-notes-textarea"); + + if (textarea instanceof HTMLTextAreaElement) { + userNotesSaveNotes(textarea.value); + } + + userNotesSavePosition(oldWin); + + oldWin.remove(); + + userNotesOpenNotes(); +} diff --git a/scripts/user-notes.js b/scripts/user-notes.js index c55e7bc..572acb9 100644 --- a/scripts/user-notes.js +++ b/scripts/user-notes.js @@ -1,236 +1,44 @@ -/* - * User Notes for Foundry VTT v14 - * Stores notes in window.localStorage per world and per user. - */ +import { + userNotesRegisterSettings +} from "./user-notes-settings.js"; -const LBN_MODULE_ID = "user-notes"; -const LBN_WINDOW_ID = "lbn-notes-window"; -const LBN_BUTTON_ID = "lbn-open-notes"; +import { + userNotesRegisterTokenControl +} from "./user-notes-controls.js"; -let lbnSaveTimer = null; -let lbnObserverTimer = null; +import { + userNotesOpenNotes, + userNotesResetPositionAndSize, + userNotesRefreshOpenWindow +} from "./user-notes-window.js"; -function lbnStorageKey() { - const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world"; - const userId = game.user?.id ?? "unknown-user"; - return `${LBN_MODULE_ID}.${worldId}.${userId}.notes`; -} +console.log("User Notes | ES module loaded"); -function lbnPositionKey() { - const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world"; - const userId = game.user?.id ?? "unknown-user"; - return `${LBN_MODULE_ID}.${worldId}.${userId}.position`; -} +globalThis.UserNotes = { + open: userNotesOpenNotes, + resetPosition: userNotesResetPositionAndSize, + refresh: userNotesRefreshOpenWindow +}; -function lbnLoadNotes() { - return window.localStorage.getItem(lbnStorageKey()) ?? ""; -} +Hooks.once("init", () => { + console.log("User Notes | init hook fired"); -function lbnSaveNotes(value) { - window.localStorage.setItem(lbnStorageKey(), value); -} - -function lbnSetStatus(text) { - const status = document.querySelector(`#${LBN_WINDOW_ID} .lbn-status`); - if (status) status.textContent = text; -} - -function lbnDebouncedSave(value) { - window.clearTimeout(lbnSaveTimer); - lbnSetStatus("Ungespeichert …"); - lbnSaveTimer = window.setTimeout(() => { - lbnSaveNotes(value); - lbnSetStatus("Gespeichert"); - }, 250); -} - -function lbnRestorePosition(win) { try { - const raw = window.localStorage.getItem(lbnPositionKey()); - if (!raw) return; - const pos = JSON.parse(raw); - if (Number.isFinite(pos.left)) win.style.left = `${pos.left}px`; - if (Number.isFinite(pos.top)) win.style.top = `${pos.top}px`; - if (Number.isFinite(pos.width)) win.style.width = `${pos.width}px`; - if (Number.isFinite(pos.height)) win.style.height = `${pos.height}px`; + userNotesRegisterSettings(userNotesResetPositionAndSize); + console.log("User Notes | settings registered"); } catch (err) { - console.warn(`${LBN_MODULE_ID} | Could not restore note window position`, err); - } -} - -function lbnSavePosition(win) { - const rect = win.getBoundingClientRect(); - window.localStorage.setItem(lbnPositionKey(), JSON.stringify({ - left: Math.round(rect.left), - top: Math.round(rect.top), - width: Math.round(rect.width), - height: Math.round(rect.height) - })); -} - -function lbnMakeDraggable(win) { - const handle = win.querySelector(".lbn-titlebar"); - if (!handle) return; - - let drag = null; - - handle.addEventListener("pointerdown", event => { - if (event.target.closest("button")) return; - - const rect = win.getBoundingClientRect(); - drag = { - pointerId: event.pointerId, - startX: event.clientX, - startY: event.clientY, - left: rect.left, - top: rect.top - }; - - handle.setPointerCapture(event.pointerId); - event.preventDefault(); - }); - - handle.addEventListener("pointermove", event => { - if (!drag || drag.pointerId !== event.pointerId) return; - - const nextLeft = Math.max(0, Math.min(window.innerWidth - 80, drag.left + event.clientX - drag.startX)); - const nextTop = Math.max(0, Math.min(window.innerHeight - 40, drag.top + event.clientY - drag.startY)); - - win.style.left = `${nextLeft}px`; - win.style.top = `${nextTop}px`; - }); - - handle.addEventListener("pointerup", event => { - if (!drag || drag.pointerId !== event.pointerId) return; - drag = null; - lbnSavePosition(win); - }); -} - -function lbnOpenNotes() { - let win = document.getElementById(LBN_WINDOW_ID); - - if (win) { - win.hidden = false; - win.classList.add("active"); - win.querySelector("textarea")?.focus(); - return; - } - - win = document.createElement("section"); - win.id = LBN_WINDOW_ID; - win.className = "lbn-notes-window"; - win.innerHTML = ` -
-
- - User Notes -
-
- Gespeichert - - -
-
- - `; - - document.body.appendChild(win); - - const textarea = win.querySelector(".lbn-textarea"); - textarea.value = lbnLoadNotes(); - - textarea.addEventListener("input", event => { - lbnDebouncedSave(event.currentTarget.value); - }); - - win.querySelector(".lbn-save").addEventListener("click", () => { - lbnSaveNotes(textarea.value); - lbnSetStatus("Gespeichert"); - }); - - win.querySelector(".lbn-close").addEventListener("click", () => { - lbnSaveNotes(textarea.value); - lbnSavePosition(win); - win.hidden = true; - }); - - new ResizeObserver(() => lbnSavePosition(win)).observe(win); - - lbnRestorePosition(win); - lbnMakeDraggable(win); - textarea.focus(); -} - -function lbnAsHTMLElement(html) { - if (html instanceof HTMLElement) return html; - if (html?.[0] instanceof HTMLElement) return html[0]; // legacy jQuery-style hook argument - if (html?.element instanceof HTMLElement) return html.element; - return null; -} - -function lbnFindPlayersElement(renderedElement = null) { - return renderedElement?.id === "players" - ? renderedElement - : renderedElement?.querySelector?.("#players") - ?? document.querySelector("#players") - ?? document.querySelector("[data-application-id='players']") - ?? document.querySelector(".players"); -} - -function lbnInjectButton(renderedElement = null) { - const players = lbnFindPlayersElement(renderedElement); - if (!players || players.querySelector(`#${LBN_BUTTON_ID}`)) return; - - const header = - players.querySelector(".window-header") - ?? players.querySelector("header") - ?? players.querySelector("h3") - ?? players.querySelector("h2") - ?? players; - - const button = document.createElement("button"); - button.id = LBN_BUTTON_ID; - button.type = "button"; - button.className = "lbn-player-note-button"; - button.title = "User Notes öffnen"; - button.setAttribute("aria-label", "User Notes öffnen"); - button.innerHTML = ``; - - button.addEventListener("click", event => { - event.preventDefault(); - event.stopPropagation(); - lbnOpenNotes(); - }); - - header.appendChild(button); -} - -Hooks.once("ready", () => { - lbnInjectButton(); - - // Fallback: Die Spieler-/Benutzerliste kann bei Theme-, Layout- oder Popout-Änderungen neu gerendert werden. - const observer = new MutationObserver(() => { - window.clearTimeout(lbnObserverTimer); - lbnObserverTimer = window.setTimeout(() => lbnInjectButton(), 100); - }); - - observer.observe(document.body, { childList: true, subtree: true }); -}); - -Hooks.on("renderApplicationV2", (app, html) => { - const element = lbnAsHTMLElement(html) ?? app?.element ?? null; - if (app?.constructor?.name === "Players" || lbnFindPlayersElement(element)) { - window.queueMicrotask(() => lbnInjectButton(element)); + console.error("User Notes | error during init", err); + ui.notifications?.error("User Notes: Fehler beim Initialisieren. Details stehen in der Browser-Konsole."); } }); -// Kompatibilitäts-Fallback für Installationen/Module, die noch einen spezifischen PlayerList-Hook auslösen. -Hooks.on("renderPlayerList", (_app, html) => { - const element = lbnAsHTMLElement(html); - window.queueMicrotask(() => lbnInjectButton(element)); +Hooks.on("getSceneControlButtons", controls => { + console.log("User Notes | getSceneControlButtons fired"); + + try { + userNotesRegisterTokenControl(controls); + } catch (err) { + console.error("User Notes | error while registering token control", err); + ui.notifications?.error("User Notes: Fehler beim Registrieren des Token-Controls."); + } }); diff --git a/styles/user-notes-settings.css b/styles/user-notes-settings.css new file mode 100644 index 0000000..db1a3a9 --- /dev/null +++ b/styles/user-notes-settings.css @@ -0,0 +1,90 @@ +.user-notes-appearance-settings { + padding: 0.75rem; +} + +.user-notes-appearance-settings .notes { + margin-top: 0; + margin-bottom: 0.75rem; +} + +.user-notes-appearance-settings fieldset { + margin: 0 0 0.75rem 0; + padding: 0.75rem; + + border: 1px solid var(--color-border-light-primary, #7a695a); + border-radius: 4px; +} + +.user-notes-appearance-settings legend { + padding: 0 0.35rem; + font-weight: bold; +} + +.user-notes-appearance-settings .form-group { + align-items: center; +} + +.user-notes-appearance-settings .form-fields { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.user-notes-appearance-settings input[type="color"] { + width: 3.5rem; + min-width: 3.5rem; + height: 2rem; + + padding: 0; + border: 1px solid var(--color-border-light-primary, #7a695a); + border-radius: 4px; + + background: transparent; + cursor: pointer; +} + +.user-notes-appearance-settings input[type="text"] { + flex: 1 1 auto; + min-width: 0; +} + +.user-notes-range-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.user-notes-range-row input[type="range"] { + flex: 1 1 auto; +} + +.user-notes-range-row output { + width: 3rem; + text-align: right; + white-space: nowrap; +} + +.user-notes-appearance-settings .sheet-footer, +.user-notes-appearance-footer { + margin-top: 0.75rem; + + display: flex; + align-items: center; + gap: 0.5rem; +} + +.user-notes-primary-actions { + flex: 1 1 auto; + + display: flex; + align-items: center; + gap: 0.5rem; +} + +.user-notes-primary-actions button { + flex: 1 1 0; +} + +.user-notes-appearance-footer > .user-notes-reset-appearance { + flex: 0 0 auto; +} diff --git a/styles/user-notes-window.css b/styles/user-notes-window.css new file mode 100644 index 0000000..b59dd85 --- /dev/null +++ b/styles/user-notes-window.css @@ -0,0 +1,133 @@ +.user-notes-window { + position: fixed; + z-index: 100000; + left: 200px; + top: 150px; + + width: 500px; + height: 400px; + + min-width: 280px; + min-height: 180px; + + max-width: calc(100vw - 40px); + max-height: calc(100vh - 40px); + + display: flex; + flex-direction: column; + + resize: both; + overflow: hidden; + + box-sizing: border-box; + + border: 1px solid var(--color-border-dark, #000); + border-radius: 6px; + + background: var(--user-notes-window-background, rgba(25, 24, 19, 0.96)); + color: var(--user-notes-window-text-color, #f0f0e0); + + box-shadow: 0 0 16px rgba(0, 0, 0, 0.55); +} + +.user-notes-window[hidden] { + display: none; +} + +.user-notes-titlebar { + flex: 0 0 auto; + + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + + padding: 0.45rem 0.55rem; + + background: rgba(0, 0, 0, 0.35); + border-bottom: 1px solid var(--color-border-dark, #000); + + cursor: move; + user-select: none; +} + +.user-notes-title, +.user-notes-controls { + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.user-notes-title { + min-width: 0; + font-weight: bold; +} + +.user-notes-status { + opacity: 0.75; + font-size: 0.78rem; + white-space: nowrap; +} + +.user-notes-controls button { + width: 1.55rem; + height: 1.55rem; + + display: inline-flex; + align-items: center; + justify-content: center; + + padding: 0; + + border: 1px solid var(--color-border-light-primary, #7a695a); + border-radius: 4px; + + background: rgba(0, 0, 0, 0.18); + color: var(--user-notes-window-text-color, #f0f0e0); + + cursor: pointer; +} + +.user-notes-controls button:hover { + background: rgba(255, 255, 255, 0.16); +} + +.user-notes-content { + flex: 1 1 auto; + min-height: 0; + + display: flex; + padding: 0.5rem; + + box-sizing: border-box; +} + +.user-notes-textarea { + flex: 1 1 auto; + + width: 100%; + height: 100%; + + min-width: 0; + min-height: 0; + + resize: none; + + box-sizing: border-box; + padding: 0.55rem; + + border: 1px solid var(--color-border-light-primary, #7a695a); + border-radius: 4px; + + background: var(--user-notes-textarea-background, rgba(255, 255, 255, 0.92)); + color: var(--user-notes-textarea-color, #111111); + + font-family: inherit; + font-size: 0.95rem; + line-height: 1.35; +} + +.user-notes-textarea:focus { + outline: 1px solid var(--color-border-highlight, #ff6400); + outline-offset: 0; +} diff --git a/styles/user-notes.css b/styles/user-notes.css deleted file mode 100644 index f9bd872..0000000 --- a/styles/user-notes.css +++ /dev/null @@ -1,100 +0,0 @@ -.lbn-player-note-button { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.65rem; - height: 1.65rem; - margin-left: 0.35rem; - padding: 0; - border: 1px solid var(--color-border-light-primary, #7a695a); - border-radius: 4px; - background: rgba(0, 0, 0, 0.18); - color: var(--color-text-light-highlight, #f0f0e0); - cursor: pointer; -} - -.lbn-player-note-button:hover { - background: rgba(255, 255, 255, 0.12); -} - -.lbn-notes-window { - position: fixed; - z-index: 100000; - left: 140px; - top: 120px; - width: 420px; - height: 330px; - min-width: 260px; - min-height: 180px; - display: flex; - flex-direction: column; - resize: both; - overflow: hidden; - border: 1px solid var(--color-border-dark, #000); - border-radius: 6px; - background: var(--color-bg, #1f1f1f); - box-shadow: 0 0 16px rgba(0, 0, 0, 0.55); -} - -.lbn-notes-window[hidden] { - display: none; -} - -.lbn-titlebar { - flex: 0 0 auto; - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - min-height: 2rem; - padding: 0.25rem 0.4rem; - background: rgba(0, 0, 0, 0.34); - color: var(--color-text-light-highlight, #f0f0e0); - cursor: move; - user-select: none; -} - -.lbn-title, -.lbn-controls { - display: inline-flex; - align-items: center; - gap: 0.4rem; -} - -.lbn-status { - opacity: 0.75; - font-size: 0.78rem; - white-space: nowrap; -} - -.lbn-controls button { - width: 1.55rem; - height: 1.55rem; - padding: 0; - border: 1px solid var(--color-border-light-primary, #7a695a); - border-radius: 4px; - background: rgba(255, 255, 255, 0.08); - color: inherit; - cursor: pointer; -} - -.lbn-controls button:hover { - background: rgba(255, 255, 255, 0.16); -} - -.lbn-textarea { - flex: 1 1 auto; - width: 100%; - min-height: 0; - box-sizing: border-box; - resize: none; - border: 0; - border-top: 1px solid rgba(255, 255, 255, 0.1); - padding: 0.65rem; - font-family: var(--font-primary, sans-serif); - font-size: 0.95rem; - line-height: 1.4; - color: var(--color-text-light-highlight, #f0f0e0); - background: rgba(255, 255, 255, 0.045); - outline: none; -}