mirror of
https://github.com/fzumpe/foundry-usernotes.git
synced 2026-06-06 21:00:03 +02:00
Replaced Scripts by ESModules and added cool stuff
This commit is contained in:
parent
1045e66262
commit
3d09e19c52
10
module.json
10
module.json
@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"id": "user-notes",
|
"id": "user-notes",
|
||||||
"title": "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.0",
|
||||||
"version": "1.0.1",
|
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "ChatGPT"
|
"name": "Florian Zumpe"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "14",
|
"minimum": "14",
|
||||||
"verified": "14"
|
"verified": "14"
|
||||||
},
|
},
|
||||||
"scripts": [
|
"esmodules": [
|
||||||
"scripts/user-notes.js"
|
"scripts/user-notes.js"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"styles/user-notes.css"
|
"styles/user-notes-window.css",
|
||||||
|
"styles/user-notes-settings.css"
|
||||||
],
|
],
|
||||||
"languages": [],
|
"languages": [],
|
||||||
"url": "https://github.com/fzumpe/foundry-usernotes",
|
"url": "https://github.com/fzumpe/foundry-usernotes",
|
||||||
|
|||||||
19
scripts/user-notes-constants.js
Normal file
19
scripts/user-notes-constants.js
Normal file
@ -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";
|
||||||
48
scripts/user-notes-controls.js
Normal file
48
scripts/user-notes-controls.js
Normal file
@ -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]
|
||||||
|
);
|
||||||
|
}
|
||||||
427
scripts/user-notes-settings.js
Normal file
427
scripts/user-notes-settings.js
Normal file
@ -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 = `
|
||||||
|
<div class="user-notes-appearance-settings">
|
||||||
|
<p class="notes">
|
||||||
|
Diese Werte werden lokal im Browser gespeichert.
|
||||||
|
Foundry-Settings werden dadurch nicht überschrieben und die Seite wird nicht neu geladen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Fenster</legend>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Hintergrundfarbe</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input type="color" name="windowBackgroundColor" value="${data.windowBackgroundColor}">
|
||||||
|
<input type="text" name="windowBackgroundColorText" value="${data.windowBackgroundColor}">
|
||||||
|
</div>
|
||||||
|
<p class="hint">Farbe des äußeren Notizfensters.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Hintergrundtransparenz</label>
|
||||||
|
<div class="form-fields user-notes-range-row">
|
||||||
|
<input type="range" name="windowBackgroundAlpha" min="0" max="1" step="0.01" value="${data.windowBackgroundAlpha}">
|
||||||
|
<output>${data.windowBackgroundAlphaPercent}%</output>
|
||||||
|
</div>
|
||||||
|
<p class="hint">0% ist vollständig transparent, 100% ist vollständig deckend.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Schriftfarbe</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input type="color" name="windowTextColor" value="${data.windowTextColor}">
|
||||||
|
<input type="text" name="windowTextColorText" value="${data.windowTextColor}">
|
||||||
|
</div>
|
||||||
|
<p class="hint">Farbe für Titelleiste, Status und Buttons.</p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Notizfeld</legend>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Hintergrundfarbe</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input type="color" name="textareaBackgroundColor" value="${data.textareaBackgroundColor}">
|
||||||
|
<input type="text" name="textareaBackgroundColorText" value="${data.textareaBackgroundColor}">
|
||||||
|
</div>
|
||||||
|
<p class="hint">Farbe des eigentlichen Textfeldes.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Hintergrundtransparenz</label>
|
||||||
|
<div class="form-fields user-notes-range-row">
|
||||||
|
<input type="range" name="textareaBackgroundAlpha" min="0" max="1" step="0.01" value="${data.textareaBackgroundAlpha}">
|
||||||
|
<output>${data.textareaBackgroundAlphaPercent}%</output>
|
||||||
|
</div>
|
||||||
|
<p class="hint">0% ist vollständig transparent, 100% ist vollständig deckend.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Schriftfarbe</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input type="color" name="textareaTextColor" value="${data.textareaTextColor}">
|
||||||
|
<input type="text" name="textareaTextColorText" value="${data.textareaTextColor}">
|
||||||
|
</div>
|
||||||
|
<p class="hint">Farbe des Textes innerhalb des Notizfeldes.</p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<footer class="sheet-footer user-notes-appearance-footer">
|
||||||
|
<div class="user-notes-primary-actions">
|
||||||
|
<button type="button" class="user-notes-apply-appearance">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
Anwenden
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="user-notes-save-appearance">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="user-notes-reset-appearance">
|
||||||
|
<i class="fas fa-undo"></i>
|
||||||
|
Standardfarben
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
67
scripts/user-notes-storage.js
Normal file
67
scripts/user-notes-storage.js
Normal file
@ -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());
|
||||||
|
}
|
||||||
358
scripts/user-notes-window.js
Normal file
358
scripts/user-notes-window.js
Normal file
@ -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 = `
|
||||||
|
<header class="user-notes-titlebar">
|
||||||
|
<div class="user-notes-title">
|
||||||
|
<i class="fas fa-note-sticky" aria-hidden="true"></i>
|
||||||
|
<span>User Notes</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-notes-controls">
|
||||||
|
<span class="user-notes-status">Gespeichert</span>
|
||||||
|
|
||||||
|
<button type="button" class="user-notes-save" title="Jetzt speichern">
|
||||||
|
<i class="fas fa-save" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="user-notes-close" title="Schließen">
|
||||||
|
<i class="fas fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="user-notes-content">
|
||||||
|
<textarea class="user-notes-textarea" spellcheck="true" placeholder="Notizen für diese Welt und diesen Benutzer …"></textarea>
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@ -1,236 +1,44 @@
|
|||||||
/*
|
import {
|
||||||
* User Notes for Foundry VTT v14
|
userNotesRegisterSettings
|
||||||
* Stores notes in window.localStorage per world and per user.
|
} from "./user-notes-settings.js";
|
||||||
*/
|
|
||||||
|
|
||||||
const LBN_MODULE_ID = "user-notes";
|
import {
|
||||||
const LBN_WINDOW_ID = "lbn-notes-window";
|
userNotesRegisterTokenControl
|
||||||
const LBN_BUTTON_ID = "lbn-open-notes";
|
} from "./user-notes-controls.js";
|
||||||
|
|
||||||
let lbnSaveTimer = null;
|
import {
|
||||||
let lbnObserverTimer = null;
|
userNotesOpenNotes,
|
||||||
|
userNotesResetPositionAndSize,
|
||||||
|
userNotesRefreshOpenWindow
|
||||||
|
} from "./user-notes-window.js";
|
||||||
|
|
||||||
function lbnStorageKey() {
|
console.log("User Notes | ES module loaded");
|
||||||
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`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lbnPositionKey() {
|
globalThis.UserNotes = {
|
||||||
const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
|
open: userNotesOpenNotes,
|
||||||
const userId = game.user?.id ?? "unknown-user";
|
resetPosition: userNotesResetPositionAndSize,
|
||||||
return `${LBN_MODULE_ID}.${worldId}.${userId}.position`;
|
refresh: userNotesRefreshOpenWindow
|
||||||
}
|
};
|
||||||
|
|
||||||
function lbnLoadNotes() {
|
Hooks.once("init", () => {
|
||||||
return window.localStorage.getItem(lbnStorageKey()) ?? "";
|
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 {
|
try {
|
||||||
const raw = window.localStorage.getItem(lbnPositionKey());
|
userNotesRegisterSettings(userNotesResetPositionAndSize);
|
||||||
if (!raw) return;
|
console.log("User Notes | settings registered");
|
||||||
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`;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`${LBN_MODULE_ID} | Could not restore note window position`, err);
|
console.error("User Notes | error during init", err);
|
||||||
}
|
ui.notifications?.error("User Notes: Fehler beim Initialisieren. Details stehen in der Browser-Konsole.");
|
||||||
}
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<header class="lbn-titlebar">
|
|
||||||
<div class="lbn-title">
|
|
||||||
<i class="fas fa-note-sticky" aria-hidden="true"></i>
|
|
||||||
<span>User Notes</span>
|
|
||||||
</div>
|
|
||||||
<div class="lbn-controls">
|
|
||||||
<span class="lbn-status">Gespeichert</span>
|
|
||||||
<button type="button" class="lbn-save" title="Jetzt speichern">
|
|
||||||
<i class="fas fa-save" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="lbn-close" title="Schließen">
|
|
||||||
<i class="fas fa-times" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<textarea class="lbn-textarea" spellcheck="true" placeholder="Notizen für diese Welt und diesen Benutzer …"></textarea>
|
|
||||||
`;
|
|
||||||
|
|
||||||
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 = `<i class="fas fa-note-sticky" aria-hidden="true"></i>`;
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kompatibilitäts-Fallback für Installationen/Module, die noch einen spezifischen PlayerList-Hook auslösen.
|
Hooks.on("getSceneControlButtons", controls => {
|
||||||
Hooks.on("renderPlayerList", (_app, html) => {
|
console.log("User Notes | getSceneControlButtons fired");
|
||||||
const element = lbnAsHTMLElement(html);
|
|
||||||
window.queueMicrotask(() => lbnInjectButton(element));
|
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.");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
90
styles/user-notes-settings.css
Normal file
90
styles/user-notes-settings.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
133
styles/user-notes-window.css
Normal file
133
styles/user-notes-window.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user