mirror of
https://github.com/fzumpe/foundry-usernotes.git
synced 2026-06-06 21:00:03 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c19bbd1910 | ||
|
|
81410869f4 | ||
|
|
d4671086a4 | ||
|
|
0b89998c23 | ||
|
|
b6fd4d5f4c | ||
|
|
6a37be1ce8 | ||
|
|
00cbcecbfd | ||
|
|
3d09e19c52 | ||
|
|
1045e66262 |
16
module.json
16
module.json
@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"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.3",
|
||||||
"version": "1.0.1",
|
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "ChatGPT"
|
"name": "Florian Zumpe"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
@ -13,13 +12,18 @@
|
|||||||
"verified": "14"
|
"verified": "14"
|
||||||
},
|
},
|
||||||
"scripts": [
|
"scripts": [
|
||||||
|
"vendor/jodit/jodit.min.js"
|
||||||
|
],
|
||||||
|
"esmodules": [
|
||||||
"scripts/user-notes.js"
|
"scripts/user-notes.js"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"styles/user-notes.css"
|
"vendor/jodit/jodit.min.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",
|
||||||
"manifest": "https://raw.githubusercontent.com/fzumpe/foundry-usernotes/main/module.json",
|
"manifest": "https://raw.githubusercontent.com/fzumpe/foundry-usernotes/V1.0.3/module.json",
|
||||||
"download": "https://github.com/fzumpe/foundry-usernotes/releases/download/v1.0.1/user-notes.zip"
|
"download": "https://codeload.github.com/fzumpe/foundry-usernotes/zip/refs/tags/V1.0.3"
|
||||||
}
|
}
|
||||||
|
|||||||
474
scripts/user-notes-backup.js
Normal file
474
scripts/user-notes-backup.js
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
import {
|
||||||
|
USER_NOTES_MODULE_ID
|
||||||
|
} from "./user-notes-constants.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
userNotesDecodeBase64,
|
||||||
|
userNotesEncodeBase64,
|
||||||
|
userNotesLoadAppearance,
|
||||||
|
userNotesLoadNotes,
|
||||||
|
userNotesLoadPosition,
|
||||||
|
userNotesSaveAppearance,
|
||||||
|
userNotesSaveNotes,
|
||||||
|
userNotesSavePositionData
|
||||||
|
} from "./user-notes-storage.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
userNotesEscapePlainTextAsHtml,
|
||||||
|
userNotesSanitizeHtml
|
||||||
|
} from "./user-notes-sanitize.js";
|
||||||
|
|
||||||
|
const USER_NOTES_EXPORT_SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
|
const USER_NOTES_APPEARANCE_DEFAULTS_FOR_BACKUP = {
|
||||||
|
windowBackgroundColor: "#191813",
|
||||||
|
windowBackgroundAlpha: 0.96,
|
||||||
|
windowTextColor: "#f0f0e0",
|
||||||
|
textareaBackgroundColor: "#ffffff",
|
||||||
|
textareaBackgroundAlpha: 0.92,
|
||||||
|
textareaTextColor: "#111111"
|
||||||
|
};
|
||||||
|
|
||||||
|
function userNotesDownloadJson(filename, data) {
|
||||||
|
const json = JSON.stringify(data, null, 2);
|
||||||
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesReadJsonFile(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(String(reader.result ?? "")));
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => reject(reader.error);
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesBuildExportData(options) {
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
if (options.includeNotes) {
|
||||||
|
data.notes = {
|
||||||
|
format: "html",
|
||||||
|
encoding: "base64",
|
||||||
|
data: userNotesEncodeBase64(
|
||||||
|
userNotesSanitizeHtml(userNotesLoadNotes())
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeAppearance) {
|
||||||
|
data.appearance = userNotesLoadAppearance(
|
||||||
|
USER_NOTES_APPEARANCE_DEFAULTS_FOR_BACKUP
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includePosition) {
|
||||||
|
const position = userNotesLoadPosition(null);
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
data.position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
module: USER_NOTES_MODULE_ID,
|
||||||
|
schemaVersion: USER_NOTES_EXPORT_SCHEMA_VERSION,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesNormalizeImportedNotes(notes) {
|
||||||
|
if (typeof notes === "string") {
|
||||||
|
return userNotesEscapePlainTextAsHtml(notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notes || typeof notes !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.encoding === "base64") {
|
||||||
|
return userNotesSanitizeHtml(
|
||||||
|
userNotesDecodeBase64(notes.data ?? notes.content ?? "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.format === "html") {
|
||||||
|
return userNotesSanitizeHtml(notes.content ?? notes.data ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.format === "text") {
|
||||||
|
return userNotesEscapePlainTextAsHtml(notes.content ?? notes.data ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return userNotesSanitizeHtml(notes.content ?? notes.data ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesValidateImportData(importData) {
|
||||||
|
if (!importData || typeof importData !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importData.module !== USER_NOTES_MODULE_ID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!importData.data || typeof importData.data !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesApplyImportData(importData, options) {
|
||||||
|
const data = importData?.data ?? {};
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
if (options.importNotes && data.notes !== undefined) {
|
||||||
|
const importedNotes = userNotesNormalizeImportedNotes(data.notes);
|
||||||
|
|
||||||
|
if (options.noteMode === "append") {
|
||||||
|
const existing = userNotesSanitizeHtml(userNotesLoadNotes());
|
||||||
|
const separator = existing.trim() ? "<hr />" : "";
|
||||||
|
|
||||||
|
userNotesSaveNotes(`${existing}${separator}${importedNotes}`);
|
||||||
|
} else {
|
||||||
|
userNotesSaveNotes(importedNotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.importAppearance && data.appearance) {
|
||||||
|
userNotesSaveAppearance(data.appearance);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.importPosition && data.position) {
|
||||||
|
userNotesSavePositionData(data.position);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
globalThis.UserNotes?.refresh?.({
|
||||||
|
saveCurrentContent: false
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
"User Notes | Import was saved, but refreshing the open window failed",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserNotesExportSettings extends FormApplication {
|
||||||
|
static get defaultOptions() {
|
||||||
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
|
id: "user-notes-export-settings",
|
||||||
|
title: "User Notes Export",
|
||||||
|
template: null,
|
||||||
|
width: 480,
|
||||||
|
height: "auto",
|
||||||
|
closeOnSubmit: false,
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnClose: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _renderInner() {
|
||||||
|
return $(`
|
||||||
|
<div class="user-notes-backup-settings">
|
||||||
|
<p class="notes">
|
||||||
|
Exportiert ausgewählte lokale Browserdaten als JSON-Datei.
|
||||||
|
Die Datei enthält keine Welt- oder Benutzerdaten und wird beim Import
|
||||||
|
immer in den aktuell geöffneten Scope übernommen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-export-notes">Notizen exportieren</label>
|
||||||
|
<input id="user-notes-export-notes" type="checkbox" name="includeNotes" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-export-appearance">Farben und Transparenz exportieren</label>
|
||||||
|
<input id="user-notes-export-appearance" type="checkbox" name="includeAppearance" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-export-position">Fensterposition und Größe exportieren</label>
|
||||||
|
<input id="user-notes-export-position" type="checkbox" name="includePosition" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="sheet-footer user-notes-backup-footer">
|
||||||
|
<button type="button" class="user-notes-export-now">
|
||||||
|
<i class="fas fa-file-export"></i>
|
||||||
|
Exportieren
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
activateListeners(html) {
|
||||||
|
super.activateListeners(html);
|
||||||
|
|
||||||
|
html.on("click", ".user-notes-export-now", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const root = html[0];
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
console.warn("User Notes | export settings root element not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
includeNotes: root.querySelector('[name="includeNotes"]')?.checked ?? false,
|
||||||
|
includeAppearance: root.querySelector('[name="includeAppearance"]')?.checked ?? false,
|
||||||
|
includePosition: root.querySelector('[name="includePosition"]')?.checked ?? false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options.includeNotes && !options.includeAppearance && !options.includePosition) {
|
||||||
|
ui.notifications?.warn("User Notes: Bitte mindestens einen Export-Inhalt auswählen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportData = userNotesBuildExportData(options);
|
||||||
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
userNotesDownloadJson(`user-notes-${date}.json`, exportData);
|
||||||
|
|
||||||
|
ui.notifications?.info("User Notes: Export wurde erstellt.");
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateObject(_event, _formData) {
|
||||||
|
// Nicht verwendet. Buttons verarbeiten den Export direkt.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserNotesImportSettings extends FormApplication {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.importData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultOptions() {
|
||||||
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
|
id: "user-notes-import-settings",
|
||||||
|
title: "User Notes Import",
|
||||||
|
template: null,
|
||||||
|
width: 560,
|
||||||
|
height: "auto",
|
||||||
|
closeOnSubmit: false,
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnClose: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _renderInner() {
|
||||||
|
const hasData = Boolean(this.importData?.data);
|
||||||
|
const hasNotes = hasData && this.importData.data.notes !== undefined;
|
||||||
|
const hasAppearance = hasData && Boolean(this.importData.data.appearance);
|
||||||
|
const hasPosition = hasData && Boolean(this.importData.data.position);
|
||||||
|
|
||||||
|
return $(`
|
||||||
|
<div class="user-notes-backup-settings">
|
||||||
|
<p class="notes">
|
||||||
|
Importiert eine User-Notes-JSON-Datei in die aktuell geöffnete Welt
|
||||||
|
und für den aktuell eingeloggten Benutzer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="notes warning">
|
||||||
|
Sicherheits-Hinweis: Importiertes HTML wird streng bereinigt.
|
||||||
|
Skripte, Eventhandler, eingebettete aktive Inhalte, unsichere URLs und
|
||||||
|
nicht erlaubte HTML-Elemente werden entfernt. Dadurch können
|
||||||
|
Formatierungen verloren gehen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>JSON-Datei</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input type="file" name="importFile" accept="application/json,.json">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-import-notes">Notizen importieren</label>
|
||||||
|
<input id="user-notes-import-notes" type="checkbox" name="importNotes" ${hasNotes ? "checked" : ""} ${hasNotes ? "" : "disabled"}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Notizmodus</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<select name="noteMode" ${hasNotes ? "" : "disabled"}>
|
||||||
|
<option value="overwrite">Vorhandene Notizen überschreiben</option>
|
||||||
|
<option value="append">Importierte Notizen anhängen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-import-appearance">Farben und Transparenz importieren</label>
|
||||||
|
<input id="user-notes-import-appearance" type="checkbox" name="importAppearance" ${hasAppearance ? "checked" : ""} ${hasAppearance ? "" : "disabled"}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-import-position">Fensterposition und Größe importieren</label>
|
||||||
|
<input id="user-notes-import-position" type="checkbox" name="importPosition" ${hasPosition ? "checked" : ""} ${hasPosition ? "" : "disabled"}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="sheet-footer user-notes-backup-footer">
|
||||||
|
<button type="button" class="user-notes-import-now" ${hasData ? "" : "disabled"}>
|
||||||
|
<i class="fas fa-file-import"></i>
|
||||||
|
Importieren
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
activateListeners(html) {
|
||||||
|
super.activateListeners(html);
|
||||||
|
|
||||||
|
html.on("change", 'input[name="importFile"]', async event => {
|
||||||
|
const fileInput = event.currentTarget;
|
||||||
|
const file = fileInput.files?.[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = await userNotesReadJsonFile(file);
|
||||||
|
|
||||||
|
if (!userNotesValidateImportData(json)) {
|
||||||
|
ui.notifications?.error("User Notes: Diese JSON-Datei ist kein gültiger User-Notes-Export.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importData = json;
|
||||||
|
|
||||||
|
console.log("User Notes | import file loaded", {
|
||||||
|
hasNotes: json.data?.notes !== undefined,
|
||||||
|
hasAppearance: Boolean(json.data?.appearance),
|
||||||
|
hasPosition: Boolean(json.data?.position)
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.notifications?.info("User Notes: Importdatei wurde gelesen.");
|
||||||
|
this.render(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("User Notes | Import failed while reading JSON", err);
|
||||||
|
ui.notifications?.error("User Notes: JSON-Datei konnte nicht gelesen werden.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
html.on("click", ".user-notes-import-now", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const root = html[0];
|
||||||
|
|
||||||
|
console.log("User Notes | import button clicked", {
|
||||||
|
hasImportData: Boolean(this.importData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
console.warn("User Notes | import settings root element not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.importData) {
|
||||||
|
ui.notifications?.warn("User Notes: Bitte zuerst eine JSON-Datei auswählen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
importNotes: root.querySelector('[name="importNotes"]')?.checked ?? false,
|
||||||
|
noteMode: root.querySelector('[name="noteMode"]')?.value ?? "overwrite",
|
||||||
|
importAppearance: root.querySelector('[name="importAppearance"]')?.checked ?? false,
|
||||||
|
importPosition: root.querySelector('[name="importPosition"]')?.checked ?? false
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("User Notes | import options", options);
|
||||||
|
|
||||||
|
if (!options.importNotes && !options.importAppearance && !options.importPosition) {
|
||||||
|
ui.notifications?.warn("User Notes: Bitte mindestens einen Import-Inhalt auswählen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const changed = userNotesApplyImportData(this.importData, options);
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
ui.notifications?.warn("User Notes: Es wurden keine importierbaren Daten angewendet.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.notifications?.info("User Notes: Import wurde angewendet.");
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("User Notes | Import failed while applying data", err);
|
||||||
|
ui.notifications?.error("User Notes: Import konnte nicht angewendet werden. Details stehen in der Browser-Konsole.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateObject(_event, _formData) {
|
||||||
|
// Nicht verwendet. Buttons verarbeiten den Import direkt.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesRegisterBackupSettings() {
|
||||||
|
game.settings.registerMenu(USER_NOTES_MODULE_ID, "exportData", {
|
||||||
|
name: "Export",
|
||||||
|
label: "Notizen exportieren",
|
||||||
|
hint: "Exportiert ausgewählte lokale User-Notes-Daten als JSON-Datei.",
|
||||||
|
icon: "fas fa-file-export",
|
||||||
|
type: UserNotesExportSettings,
|
||||||
|
restricted: false
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.registerMenu(USER_NOTES_MODULE_ID, "importData", {
|
||||||
|
name: "Import",
|
||||||
|
label: "Notizen importieren",
|
||||||
|
hint: "Importiert Notizen, Darstellung und/oder Fensterposition aus einer JSON-Datei. Importiertes HTML wird aus Sicherheitsgründen streng bereinigt.",
|
||||||
|
icon: "fas fa-file-import",
|
||||||
|
type: UserNotesImportSettings,
|
||||||
|
restricted: false
|
||||||
|
});
|
||||||
|
}
|
||||||
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";
|
||||||
40
scripts/user-notes-controls.js
Normal file
40
scripts/user-notes-controls.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
USER_NOTES_TOOL_ID
|
||||||
|
} from "./user-notes-constants.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
userNotesOpenNotes
|
||||||
|
} from "./user-notes-window.js";
|
||||||
|
|
||||||
|
export function userNotesRegisterTokenControl(controls) {
|
||||||
|
const tokenControl = controls?.tokens;
|
||||||
|
|
||||||
|
if (!tokenControl) {
|
||||||
|
console.warn("User Notes | controls.tokens is not available; token control was not registered");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenControl.tools) {
|
||||||
|
console.warn("User Notes | controls.tokens.tools is not available; token control was not registered");
|
||||||
|
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: () => {
|
||||||
|
userNotesOpenNotes();
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: () => {
|
||||||
|
userNotesOpenNotes();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("User Notes | token control button registered");
|
||||||
|
}
|
||||||
211
scripts/user-notes-sanitize.js
Normal file
211
scripts/user-notes-sanitize.js
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
const USER_NOTES_ALLOWED_TAGS = new Set([
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"BLOCKQUOTE",
|
||||||
|
"BR",
|
||||||
|
"CAPTION",
|
||||||
|
"CODE",
|
||||||
|
"COL",
|
||||||
|
"COLGROUP",
|
||||||
|
"DIV",
|
||||||
|
"EM",
|
||||||
|
"H1",
|
||||||
|
"H2",
|
||||||
|
"H3",
|
||||||
|
"H4",
|
||||||
|
"H5",
|
||||||
|
"H6",
|
||||||
|
"HR",
|
||||||
|
"I",
|
||||||
|
"LI",
|
||||||
|
"OL",
|
||||||
|
"P",
|
||||||
|
"PRE",
|
||||||
|
"S",
|
||||||
|
"SPAN",
|
||||||
|
"STRONG",
|
||||||
|
"SUB",
|
||||||
|
"SUP",
|
||||||
|
"TABLE",
|
||||||
|
"TBODY",
|
||||||
|
"TD",
|
||||||
|
"TFOOT",
|
||||||
|
"TH",
|
||||||
|
"THEAD",
|
||||||
|
"TR",
|
||||||
|
"U",
|
||||||
|
"UL"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const USER_NOTES_ALLOWED_ATTRIBUTES = new Set([
|
||||||
|
"class",
|
||||||
|
"colspan",
|
||||||
|
"href",
|
||||||
|
"rel",
|
||||||
|
"rowspan",
|
||||||
|
"style",
|
||||||
|
"target",
|
||||||
|
"title"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const USER_NOTES_ALLOWED_CSS_PROPERTIES = new Set([
|
||||||
|
"background-color",
|
||||||
|
"border",
|
||||||
|
"border-bottom",
|
||||||
|
"border-color",
|
||||||
|
"border-left",
|
||||||
|
"border-radius",
|
||||||
|
"border-right",
|
||||||
|
"border-style",
|
||||||
|
"border-top",
|
||||||
|
"border-width",
|
||||||
|
"color",
|
||||||
|
"font-family",
|
||||||
|
"font-size",
|
||||||
|
"font-style",
|
||||||
|
"font-weight",
|
||||||
|
"margin-left",
|
||||||
|
"padding",
|
||||||
|
"text-align",
|
||||||
|
"text-decoration"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function userNotesSanitizeStyle(styleValue) {
|
||||||
|
const safeRules = [];
|
||||||
|
|
||||||
|
for (const rule of String(styleValue ?? "").split(";")) {
|
||||||
|
const [rawProperty, ...rawValueParts] = rule.split(":");
|
||||||
|
|
||||||
|
if (!rawProperty || rawValueParts.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const property = rawProperty.trim().toLowerCase();
|
||||||
|
const value = rawValueParts.join(":").trim();
|
||||||
|
|
||||||
|
if (!USER_NOTES_ALLOWED_CSS_PROPERTIES.has(property)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowered = value.toLowerCase();
|
||||||
|
|
||||||
|
if (
|
||||||
|
lowered.includes("url(") ||
|
||||||
|
lowered.includes("expression(") ||
|
||||||
|
lowered.includes("javascript:") ||
|
||||||
|
lowered.includes("data:") ||
|
||||||
|
lowered.includes("@import") ||
|
||||||
|
lowered.includes("behavior:")
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
safeRules.push(`${property}: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeRules.join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesIsSafeHref(value) {
|
||||||
|
const href = String(value ?? "").trim();
|
||||||
|
|
||||||
|
if (!href) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (href.startsWith("#")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(href, window.location.origin);
|
||||||
|
return ["http:", "https:", "mailto:"].includes(url.protocol);
|
||||||
|
} catch (_err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesSanitizeElement(element) {
|
||||||
|
if (!USER_NOTES_ALLOWED_TAGS.has(element.tagName)) {
|
||||||
|
const text = document.createTextNode(element.textContent ?? "");
|
||||||
|
element.replaceWith(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attribute of [...element.attributes]) {
|
||||||
|
const name = attribute.name.toLowerCase();
|
||||||
|
const value = attribute.value;
|
||||||
|
|
||||||
|
if (name.startsWith("on")) {
|
||||||
|
element.removeAttribute(attribute.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!USER_NOTES_ALLOWED_ATTRIBUTES.has(name)) {
|
||||||
|
element.removeAttribute(attribute.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "href") {
|
||||||
|
if (!userNotesIsSafeHref(value)) {
|
||||||
|
element.removeAttribute(attribute.name);
|
||||||
|
} else {
|
||||||
|
element.setAttribute("rel", "noopener noreferrer");
|
||||||
|
element.setAttribute("target", "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "target" && value !== "_blank") {
|
||||||
|
element.removeAttribute(attribute.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "style") {
|
||||||
|
const sanitizedStyle = userNotesSanitizeStyle(value);
|
||||||
|
|
||||||
|
if (sanitizedStyle) {
|
||||||
|
element.setAttribute("style", sanitizedStyle);
|
||||||
|
} else {
|
||||||
|
element.removeAttribute(attribute.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesSanitizeHtml(html) {
|
||||||
|
const template = document.createElement("template");
|
||||||
|
template.innerHTML = String(html ?? "");
|
||||||
|
|
||||||
|
for (const element of [...template.content.querySelectorAll("*")]) {
|
||||||
|
userNotesSanitizeElement(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.innerHTML.replace(/<br>/gi, "<br />");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesEscapePlainTextAsHtml(text) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.textContent = String(text ?? "");
|
||||||
|
|
||||||
|
return div.innerHTML.replace(/\n/g, "<br />");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesLooksLikeHtml(value) {
|
||||||
|
return /<\/?[a-z][\s\S]*>/i.test(String(value ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesNormalizeStoredNotesForEditor(value) {
|
||||||
|
const content = String(value ?? "");
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userNotesLooksLikeHtml(content)) {
|
||||||
|
return userNotesSanitizeHtml(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userNotesEscapePlainTextAsHtml(content);
|
||||||
|
}
|
||||||
432
scripts/user-notes-settings.js
Normal file
432
scripts/user-notes-settings.js
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
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-solid",
|
||||||
|
appearance.windowBackgroundColor
|
||||||
|
);
|
||||||
|
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
271
scripts/user-notes-storage.js
Normal file
271
scripts/user-notes-storage.js
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import {
|
||||||
|
USER_NOTES_MODULE_ID
|
||||||
|
} from "./user-notes-constants.js";
|
||||||
|
|
||||||
|
const USER_NOTES_STORAGE_SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
|
function userNotesBaseKey() {
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesContentKey() {
|
||||||
|
return `${userNotesBaseKey()}.content`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesStateKey() {
|
||||||
|
return `${userNotesBaseKey()}.state`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy keys from older versions.
|
||||||
|
* Kept only for migration.
|
||||||
|
*/
|
||||||
|
export function userNotesStorageKey() {
|
||||||
|
return `${userNotesBaseKey()}.notes`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesPositionKey() {
|
||||||
|
return `${userNotesBaseKey()}.position`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesAppearanceKey() {
|
||||||
|
return `${userNotesBaseKey()}.appearance`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesEncodeBase64(value) {
|
||||||
|
const bytes = new TextEncoder().encode(String(value ?? ""));
|
||||||
|
const chunkSize = 0x8000;
|
||||||
|
let binary = "";
|
||||||
|
|
||||||
|
for (let index = 0; index < bytes.length; index += chunkSize) {
|
||||||
|
const chunk = bytes.subarray(index, index + chunkSize);
|
||||||
|
binary += String.fromCharCode(...chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesDecodeBase64(value) {
|
||||||
|
try {
|
||||||
|
const binary = window.atob(String(value ?? ""));
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
|
||||||
|
for (let index = 0; index < binary.length; index++) {
|
||||||
|
bytes[index] = binary.charCodeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextDecoder().decode(bytes);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("User Notes | Could not decode base64 content", err);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesRemoveLegacyStorageKeys() {
|
||||||
|
window.localStorage.removeItem(userNotesStorageKey());
|
||||||
|
window.localStorage.removeItem(userNotesPositionKey());
|
||||||
|
window.localStorage.removeItem(userNotesAppearanceKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesReadJson(key, fallback = null) {
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (!raw) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`User Notes | Could not parse localStorage key: ${key}`, err);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesWriteJson(key, value) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
key,
|
||||||
|
JSON.stringify(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesCreateEmptyContent() {
|
||||||
|
return {
|
||||||
|
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
notes: {
|
||||||
|
format: "html",
|
||||||
|
encoding: "base64",
|
||||||
|
data: ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesCreateEmptyState() {
|
||||||
|
return {
|
||||||
|
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
appearance: null,
|
||||||
|
position: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesLoadContentObject() {
|
||||||
|
const content = userNotesReadJson(
|
||||||
|
userNotesContentKey(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (content?.notes?.encoding === "base64") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyNotes = window.localStorage.getItem(userNotesStorageKey());
|
||||||
|
|
||||||
|
if (legacyNotes !== null) {
|
||||||
|
const migrated = userNotesCreateEmptyContent();
|
||||||
|
migrated.notes.data = userNotesEncodeBase64(legacyNotes);
|
||||||
|
|
||||||
|
userNotesWriteJson(userNotesContentKey(), migrated);
|
||||||
|
window.localStorage.removeItem(userNotesStorageKey());
|
||||||
|
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userNotesCreateEmptyContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesLoadStateObject() {
|
||||||
|
const state = userNotesReadJson(
|
||||||
|
userNotesStateKey(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state && typeof state === "object") {
|
||||||
|
return {
|
||||||
|
...userNotesCreateEmptyState(),
|
||||||
|
...state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrated = userNotesCreateEmptyState();
|
||||||
|
|
||||||
|
const legacyPosition = userNotesReadJson(
|
||||||
|
userNotesPositionKey(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const legacyAppearance = userNotesReadJson(
|
||||||
|
userNotesAppearanceKey(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (legacyPosition) {
|
||||||
|
migrated.position = legacyPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyAppearance) {
|
||||||
|
migrated.appearance = legacyAppearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyPosition || legacyAppearance) {
|
||||||
|
userNotesWriteJson(userNotesStateKey(), migrated);
|
||||||
|
window.localStorage.removeItem(userNotesPositionKey());
|
||||||
|
window.localStorage.removeItem(userNotesAppearanceKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesSaveStateObject(state) {
|
||||||
|
userNotesWriteJson(
|
||||||
|
userNotesStateKey(),
|
||||||
|
{
|
||||||
|
...userNotesCreateEmptyState(),
|
||||||
|
...state,
|
||||||
|
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
userNotesRemoveLegacyStorageKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesLoadNotes() {
|
||||||
|
const content = userNotesLoadContentObject();
|
||||||
|
|
||||||
|
if (content?.notes?.encoding === "base64") {
|
||||||
|
return userNotesDecodeBase64(content.notes.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesSaveNotes(value) {
|
||||||
|
const content = {
|
||||||
|
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
notes: {
|
||||||
|
format: "html",
|
||||||
|
encoding: "base64",
|
||||||
|
data: userNotesEncodeBase64(value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
userNotesWriteJson(userNotesContentKey(), content);
|
||||||
|
userNotesRemoveLegacyStorageKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesLoadPosition(defaultValue = null) {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
return state.position ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesSavePositionData(position) {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
state.position = position;
|
||||||
|
|
||||||
|
userNotesSaveStateObject(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesRemoveSavedPosition() {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
state.position = null;
|
||||||
|
|
||||||
|
userNotesSaveStateObject(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesLoadAppearance(defaults) {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
if (!state.appearance || typeof state.appearance !== "object") {
|
||||||
|
return { ...defaults };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
...state.appearance
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesSaveAppearance(values) {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
state.appearance = values;
|
||||||
|
|
||||||
|
userNotesSaveStateObject(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesRemoveSavedAppearance() {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
state.appearance = null;
|
||||||
|
|
||||||
|
userNotesSaveStateObject(state);
|
||||||
|
}
|
||||||
550
scripts/user-notes-window.js
Normal file
550
scripts/user-notes-window.js
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
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,
|
||||||
|
userNotesLoadPosition,
|
||||||
|
userNotesSaveNotes,
|
||||||
|
userNotesSavePositionData,
|
||||||
|
userNotesRemoveSavedPosition
|
||||||
|
} from "./user-notes-storage.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
userNotesApplyWindowSettings
|
||||||
|
} from "./user-notes-settings.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
userNotesNormalizeStoredNotesForEditor,
|
||||||
|
userNotesSanitizeHtml
|
||||||
|
} from "./user-notes-sanitize.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(userNotesSanitizeHtml(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 pos = userNotesLoadPosition(null);
|
||||||
|
|
||||||
|
if (!pos) {
|
||||||
|
userNotesApplyPosition(
|
||||||
|
win,
|
||||||
|
userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
|
||||||
|
userNotesSavePositionData(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesResetPositionAndSize() {
|
||||||
|
userNotesRemoveSavedPosition();
|
||||||
|
|
||||||
|
const win = document.getElementById(USER_NOTES_WINDOW_ID);
|
||||||
|
|
||||||
|
if (win) {
|
||||||
|
userNotesApplyPosition(
|
||||||
|
win,
|
||||||
|
userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
|
||||||
|
);
|
||||||
|
|
||||||
|
userNotesSavePosition(win);
|
||||||
|
|
||||||
|
const editor = win.__userNotesEditor;
|
||||||
|
|
||||||
|
if (editor?.events) {
|
||||||
|
editor.events.fire("resize");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const editor = win.__userNotesEditor;
|
||||||
|
|
||||||
|
if (editor?.events) {
|
||||||
|
editor.events.fire("resize");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
handle.addEventListener("pointercancel", event => {
|
||||||
|
if (!drag || drag.pointerId !== event.pointerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drag = null;
|
||||||
|
userNotesSavePosition(win);
|
||||||
|
|
||||||
|
const editor = win.__userNotesEditor;
|
||||||
|
|
||||||
|
if (editor?.events) {
|
||||||
|
editor.events.fire("resize");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesGetEditorValue(win) {
|
||||||
|
const editor = win.__userNotesEditor;
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
return userNotesSanitizeHtml(editor.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = win.querySelector(".user-notes-editor");
|
||||||
|
|
||||||
|
if (textarea instanceof HTMLTextAreaElement) {
|
||||||
|
return userNotesSanitizeHtml(textarea.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesDestroyEditor(win) {
|
||||||
|
const editor = win.__userNotesEditor;
|
||||||
|
|
||||||
|
if (editor && typeof editor.destruct === "function") {
|
||||||
|
editor.destruct();
|
||||||
|
}
|
||||||
|
|
||||||
|
win.__userNotesEditor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesInsertSymbol(editor, symbol) {
|
||||||
|
editor.s.insertHTML(`${symbol} `);
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesCreateEditor(win, editorElement) {
|
||||||
|
const initialContent = userNotesNormalizeStoredNotesForEditor(userNotesLoadNotes());
|
||||||
|
|
||||||
|
if (!globalThis.Jodit) {
|
||||||
|
console.warn("User Notes | Jodit is not available. Falling back to plain textarea.");
|
||||||
|
|
||||||
|
editorElement.value = initialContent
|
||||||
|
.replace(/<br\s*\/?>/gi, "\n")
|
||||||
|
.replace(/<\/p>/gi, "\n")
|
||||||
|
.replace(/<[^>]+>/g, "");
|
||||||
|
|
||||||
|
editorElement.addEventListener("input", event => {
|
||||||
|
userNotesDebouncedSave(event.currentTarget.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = globalThis.Jodit.make(editorElement, {
|
||||||
|
height: "100%",
|
||||||
|
minHeight: 150,
|
||||||
|
|
||||||
|
allowResizeX: false,
|
||||||
|
allowResizeY: false,
|
||||||
|
|
||||||
|
toolbarAdaptive: false,
|
||||||
|
askBeforePasteHTML: false,
|
||||||
|
askBeforePasteFromWord: false,
|
||||||
|
defaultActionOnPaste: "insert_clear_html",
|
||||||
|
|
||||||
|
disablePlugins: [
|
||||||
|
"about",
|
||||||
|
"add-new-line",
|
||||||
|
"ai-assistant",
|
||||||
|
"file",
|
||||||
|
"image",
|
||||||
|
"media",
|
||||||
|
"paste-storage",
|
||||||
|
"powered-by-jodit",
|
||||||
|
"preview",
|
||||||
|
"print",
|
||||||
|
"source",
|
||||||
|
"speech-recognize",
|
||||||
|
"video"
|
||||||
|
],
|
||||||
|
|
||||||
|
buttons: [
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"underline",
|
||||||
|
"strikethrough",
|
||||||
|
"|",
|
||||||
|
"ul",
|
||||||
|
"ol",
|
||||||
|
"|",
|
||||||
|
"font",
|
||||||
|
"fontsize",
|
||||||
|
"brush",
|
||||||
|
"|",
|
||||||
|
"table",
|
||||||
|
"|",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
|
"|",
|
||||||
|
{
|
||||||
|
name: "checkbox-empty",
|
||||||
|
tooltip: "Kästchen einfügen",
|
||||||
|
icon: "☐",
|
||||||
|
exec: editorInstance => userNotesInsertSymbol(editorInstance, "☐")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "checkbox-checked",
|
||||||
|
tooltip: "Angehaktes Kästchen einfügen",
|
||||||
|
icon: "☑",
|
||||||
|
exec: editorInstance => userNotesInsertSymbol(editorInstance, "☑")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "checkmark",
|
||||||
|
tooltip: "Haken einfügen",
|
||||||
|
icon: "✓",
|
||||||
|
exec: editorInstance => userNotesInsertSymbol(editorInstance, "✓")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "crossmark",
|
||||||
|
tooltip: "Kreuz einfügen",
|
||||||
|
icon: "✗",
|
||||||
|
exec: editorInstance => userNotesInsertSymbol(editorInstance, "✗")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bordered-text",
|
||||||
|
tooltip: "Rahmen um markierten Text",
|
||||||
|
icon: "▣",
|
||||||
|
exec: editorInstance => {
|
||||||
|
const selected = editorInstance.s.html || " ";
|
||||||
|
|
||||||
|
editorInstance.s.insertHTML(
|
||||||
|
`<span style="border: 1px solid currentColor; padding: 2px 4px;">${selected}</span>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.value = initialContent;
|
||||||
|
|
||||||
|
editor.events.on("change", () => {
|
||||||
|
userNotesDebouncedSave(editor.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
win.__userNotesEditor = editor;
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesOpenNotes() {
|
||||||
|
let win = document.getElementById(USER_NOTES_WINDOW_ID);
|
||||||
|
|
||||||
|
if (win) {
|
||||||
|
win.hidden = false;
|
||||||
|
userNotesRestorePosition(win);
|
||||||
|
userNotesApplyWindowSettings(win);
|
||||||
|
userNotesBringToFront(win);
|
||||||
|
|
||||||
|
const editor = win.__userNotesEditor;
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
editor.focus();
|
||||||
|
|
||||||
|
if (editor.events) {
|
||||||
|
editor.events.fire("resize");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
win.querySelector(".user-notes-editor")?.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-editor" spellcheck="true"></textarea>
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(win);
|
||||||
|
|
||||||
|
userNotesApplyWindowSettings(win);
|
||||||
|
userNotesRestorePosition(win);
|
||||||
|
|
||||||
|
const editorElement = win.querySelector(".user-notes-editor");
|
||||||
|
|
||||||
|
if (!(editorElement instanceof HTMLTextAreaElement)) {
|
||||||
|
console.error(`${USER_NOTES_MODULE_ID} | Notes editor could not be created.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userNotesCreateEditor(win, editorElement);
|
||||||
|
|
||||||
|
win.querySelector(".user-notes-save")?.addEventListener("click", () => {
|
||||||
|
userNotesSaveNotes(userNotesGetEditorValue(win));
|
||||||
|
userNotesSetStatus("Gespeichert");
|
||||||
|
userNotesSavePosition(win);
|
||||||
|
|
||||||
|
const editor = win.__userNotesEditor;
|
||||||
|
|
||||||
|
if (editor?.events) {
|
||||||
|
editor.events.fire("resize");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
win.querySelector(".user-notes-close")?.addEventListener("click", () => {
|
||||||
|
userNotesSaveNotes(userNotesGetEditorValue(win));
|
||||||
|
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);
|
||||||
|
|
||||||
|
const editor = win.__userNotesEditor;
|
||||||
|
|
||||||
|
if (editor?.events) {
|
||||||
|
editor.events.fire("resize");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(win);
|
||||||
|
|
||||||
|
userNotesMakeDraggable(win);
|
||||||
|
userNotesBringToFront(win);
|
||||||
|
|
||||||
|
if (win.__userNotesEditor) {
|
||||||
|
win.__userNotesEditor.focus();
|
||||||
|
|
||||||
|
if (win.__userNotesEditor.events) {
|
||||||
|
win.__userNotesEditor.events.fire("resize");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editorElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesRefreshOpenWindow(options = {}) {
|
||||||
|
const saveCurrentContent = options.saveCurrentContent ?? true;
|
||||||
|
|
||||||
|
const oldWin = document.getElementById(USER_NOTES_WINDOW_ID);
|
||||||
|
|
||||||
|
if (!oldWin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveCurrentContent) {
|
||||||
|
userNotesSaveNotes(userNotesGetEditorValue(oldWin));
|
||||||
|
}
|
||||||
|
|
||||||
|
userNotesSavePosition(oldWin);
|
||||||
|
userNotesDestroyEditor(oldWin);
|
||||||
|
oldWin.remove();
|
||||||
|
|
||||||
|
userNotesOpenNotes();
|
||||||
|
}
|
||||||
@ -1,236 +1,53 @@
|
|||||||
/*
|
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";
|
userNotesRegisterBackupSettings
|
||||||
const LBN_BUTTON_ID = "lbn-open-notes";
|
} from "./user-notes-backup.js";
|
||||||
|
|
||||||
let lbnSaveTimer = null;
|
import {
|
||||||
let lbnObserverTimer = null;
|
userNotesRegisterTokenControl
|
||||||
|
} from "./user-notes-controls.js";
|
||||||
|
|
||||||
function lbnStorageKey() {
|
import {
|
||||||
const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
|
userNotesOpenNotes,
|
||||||
const userId = game.user?.id ?? "unknown-user";
|
userNotesRefreshOpenWindow,
|
||||||
return `${LBN_MODULE_ID}.${worldId}.${userId}.notes`;
|
userNotesResetPositionAndSize
|
||||||
}
|
} from "./user-notes-window.js";
|
||||||
|
|
||||||
function lbnPositionKey() {
|
globalThis.UserNotes = {
|
||||||
const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
|
open: userNotesOpenNotes,
|
||||||
const userId = game.user?.id ?? "unknown-user";
|
refresh: userNotesRefreshOpenWindow,
|
||||||
return `${LBN_MODULE_ID}.${worldId}.${userId}.position`;
|
resetPosition: userNotesResetPositionAndSize
|
||||||
}
|
};
|
||||||
|
|
||||||
function lbnLoadNotes() {
|
Hooks.once("init", () => {
|
||||||
return window.localStorage.getItem(lbnStorageKey()) ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
console.log("User Notes | application registered");
|
||||||
if (!raw) return;
|
|
||||||
const pos = JSON.parse(raw);
|
userNotesRegisterSettings(userNotesResetPositionAndSize);
|
||||||
if (Number.isFinite(pos.left)) win.style.left = `${pos.left}px`;
|
console.log("User Notes | settings registered");
|
||||||
if (Number.isFinite(pos.top)) win.style.top = `${pos.top}px`;
|
|
||||||
if (Number.isFinite(pos.width)) win.style.width = `${pos.width}px`;
|
userNotesRegisterBackupSettings();
|
||||||
if (Number.isFinite(pos.height)) win.style.height = `${pos.height}px`;
|
console.log("User Notes | export/import settings registered");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`${LBN_MODULE_ID} | Could not restore note window position`, err);
|
console.error("User Notes | error during init", err);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function lbnSavePosition(win) {
|
ui.notifications?.error(
|
||||||
const rect = win.getBoundingClientRect();
|
"User Notes: Fehler beim Initialisieren. Details stehen in der Browser-Konsole."
|
||||||
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) => {
|
try {
|
||||||
const element = lbnAsHTMLElement(html);
|
userNotesRegisterTokenControl(controls);
|
||||||
window.queueMicrotask(() => lbnInjectButton(element));
|
} catch (err) {
|
||||||
|
console.error("User Notes | error while registering token control", err);
|
||||||
|
|
||||||
|
ui.notifications?.error(
|
||||||
|
"User Notes: Fehler beim Registrieren des Token-Controls."
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
162
styles/user-notes-settings.css
Normal file
162
styles/user-notes-settings.css
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
.user-notes-appearance-settings,
|
||||||
|
.user-notes-backup-settings {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-appearance-settings .notes,
|
||||||
|
.user-notes-backup-settings .notes {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-backup-settings .warning {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--color-border-highlight, #ff6400);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 100, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,
|
||||||
|
.user-notes-backup-settings .form-group {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-appearance-settings .form-fields,
|
||||||
|
.user-notes-backup-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Dialog footer:
|
||||||
|
* Row 1: Apply + Save side-by-side.
|
||||||
|
* Row 2: Reset colors full width.
|
||||||
|
*/
|
||||||
|
.user-notes-appearance-settings .sheet-footer,
|
||||||
|
.user-notes-appearance-footer {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-primary-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-primary-actions button,
|
||||||
|
.user-notes-appearance-footer > .user-notes-reset-appearance {
|
||||||
|
height: 2rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-primary-actions button {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-appearance-footer > .user-notes-reset-appearance {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-backup-footer {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-backup-footer button {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 2rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-checkbox-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2rem;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.75rem;
|
||||||
|
|
||||||
|
margin: 0 0 0.45rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-checkbox-row label {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-checkbox-row input[type="checkbox"] {
|
||||||
|
justify-self: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
302
styles/user-notes-window.css
Normal file
302
styles/user-notes-window.css
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fixed inner padding:
|
||||||
|
* The editor is pinned to the window content area and grows/shrinks with it.
|
||||||
|
*/
|
||||||
|
.user-notes-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Original textarea fallback.
|
||||||
|
* Jodit replaces/wraps this element when available.
|
||||||
|
*/
|
||||||
|
.user-notes-editor {
|
||||||
|
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-editor:focus {
|
||||||
|
outline: 1px solid var(--color-border-highlight, #ff6400);
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Jodit layout.
|
||||||
|
* These rules force Jodit to fill the padded content area and inherit
|
||||||
|
* the configured User Notes colors.
|
||||||
|
*/
|
||||||
|
.user-notes-content .jodit-container,
|
||||||
|
.user-notes-content .jodit {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
|
||||||
|
min-width: 0 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
background: var(--user-notes-textarea-background, rgba(255, 255, 255, 0.92)) !important;
|
||||||
|
color: var(--user-notes-textarea-color, #111111) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Toolbar remains at the top and uses the window colors.
|
||||||
|
*/
|
||||||
|
.user-notes-content .jodit-toolbar__box,
|
||||||
|
.user-notes-content .jodit-toolbar__box:not(:empty) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
opacity: 1 !important;
|
||||||
|
|
||||||
|
background: var(--user-notes-window-background-solid, #191813) !important;
|
||||||
|
color: var(--user-notes-window-text-color, #f0f0e0) !important;
|
||||||
|
|
||||||
|
border-color: var(--color-border-light-primary, #7a695a) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-content .jodit-toolbar-button,
|
||||||
|
.user-notes-content .jodit-toolbar-button__button,
|
||||||
|
.user-notes-content .jodit-toolbar-button__trigger,
|
||||||
|
.user-notes-content .jodit-toolbar-button__text,
|
||||||
|
.user-notes-content .jodit-icon {
|
||||||
|
opacity: 1 !important;
|
||||||
|
color: var(--user-notes-window-text-color, #f0f0e0) !important;
|
||||||
|
fill: var(--user-notes-window-text-color, #f0f0e0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-content .jodit-toolbar-button__button,
|
||||||
|
.user-notes-content .jodit-toolbar-button__trigger {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-content .jodit-toolbar-button__button:hover,
|
||||||
|
.user-notes-content .jodit-toolbar-button__trigger:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.16) !important;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Toolbar buttons.
|
||||||
|
*/
|
||||||
|
.user-notes-content .jodit-toolbar-button,
|
||||||
|
.user-notes-content .jodit-toolbar-button__button,
|
||||||
|
.user-notes-content .jodit-toolbar-button__trigger {
|
||||||
|
color: var(--user-notes-window-text-color, #f0f0e0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-content .jodit-toolbar-button__button:hover,
|
||||||
|
.user-notes-content .jodit-toolbar-button__trigger:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.16) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Dropdown / popup content should remain readable.
|
||||||
|
*/
|
||||||
|
.user-notes-content .jodit-popup,
|
||||||
|
.user-notes-content .jodit-dialog,
|
||||||
|
.user-notes-content .jodit-ui-group,
|
||||||
|
.user-notes-content .jodit-toolbar-editor-collection {
|
||||||
|
color: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Main editor workplace fills all remaining height below the toolbar.
|
||||||
|
*/
|
||||||
|
.user-notes-content .jodit-workplace {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
|
||||||
|
min-width: 0 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
|
||||||
|
overflow: hidden !important;
|
||||||
|
|
||||||
|
background: var(--user-notes-textarea-background, rgba(255, 255, 255, 0.92)) !important;
|
||||||
|
color: var(--user-notes-textarea-color, #111111) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actual editable area.
|
||||||
|
*/
|
||||||
|
.user-notes-content .jodit-wysiwyg {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
|
||||||
|
min-width: 0 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
overflow: auto !important;
|
||||||
|
|
||||||
|
background: var(--user-notes-textarea-background, rgba(255, 255, 255, 0.92)) !important;
|
||||||
|
color: var(--user-notes-textarea-color, #111111) !important;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Do not let Jodit reset child text colors away from the configured editor color.
|
||||||
|
* Inline styles generated by the editor may still override this when users
|
||||||
|
* explicitly choose colors inside the editor.
|
||||||
|
*/
|
||||||
|
.user-notes-content .jodit-wysiwyg * {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hide Jodit's status bar to preserve vertical space.
|
||||||
|
*/
|
||||||
|
.user-notes-content .jodit-status-bar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Tables inside notes.
|
||||||
|
*/
|
||||||
|
.user-notes-window table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-window th,
|
||||||
|
.user-notes-window td {
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
padding: 0.25rem 0.4rem;
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
5568
vendor/jodit/CHANGELOG.md
vendored
Normal file
5568
vendor/jodit/CHANGELOG.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
19
vendor/jodit/LICENSE.txt
vendored
Normal file
19
vendor/jodit/LICENSE.txt
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2013-2026 https://xdsoft.net
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
3
vendor/jodit/VERSION.txt
vendored
Normal file
3
vendor/jodit/VERSION.txt
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Jodit Editor 4.12.2
|
||||||
|
Source: https://github.com/xdan/jodit/tree/4.12.2
|
||||||
|
License: MIT
|
||||||
1
vendor/jodit/jodit.min.css
vendored
Normal file
1
vendor/jodit/jodit.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
272
vendor/jodit/jodit.min.js
vendored
Normal file
272
vendor/jodit/jodit.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user