Compare commits

..

No commits in common. "main" and "V1.0.2" have entirely different histories.
main ... V1.0.2

14 changed files with 75 additions and 7277 deletions

View File

@ -1,7 +1,7 @@
{ {
"id": "user-notes", "id": "user-notes",
"title": "User Notes", "title": "User Notes",
"version": "1.0.3", "version": "1.0.2",
"authors": [ "authors": [
{ {
"name": "Florian Zumpe" "name": "Florian Zumpe"
@ -11,19 +11,15 @@
"minimum": "14", "minimum": "14",
"verified": "14" "verified": "14"
}, },
"scripts": [
"vendor/jodit/jodit.min.js"
],
"esmodules": [ "esmodules": [
"scripts/user-notes.js" "scripts/user-notes.js"
], ],
"styles": [ "styles": [
"vendor/jodit/jodit.min.css",
"styles/user-notes-window.css", "styles/user-notes-window.css",
"styles/user-notes-settings.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/V1.0.3/module.json", "manifest": "https://raw.githubusercontent.com/fzumpe/foundry-usernotes/main/module.json",
"download": "https://codeload.github.com/fzumpe/foundry-usernotes/zip/refs/tags/V1.0.3" "download": "https://codeload.github.com/fzumpe/foundry-usernotes/zip/refs/tags/V1.0.2"
} }

View File

@ -1,474 +0,0 @@
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
});
}

View File

@ -1,211 +0,0 @@
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);
}

View File

@ -121,11 +121,6 @@ export function userNotesApplySettingsToOpenWindow() {
export function userNotesApplyWindowSettings(win) { export function userNotesApplyWindowSettings(win) {
const appearance = userNotesLoadValidatedAppearance(); const appearance = userNotesLoadValidatedAppearance();
win.style.setProperty(
"--user-notes-window-background-solid",
appearance.windowBackgroundColor
);
win.style.setProperty( win.style.setProperty(
"--user-notes-window-background", "--user-notes-window-background",
userNotesHexToRgba( userNotesHexToRgba(

View File

@ -2,270 +2,66 @@ import {
USER_NOTES_MODULE_ID USER_NOTES_MODULE_ID
} from "./user-notes-constants.js"; } from "./user-notes-constants.js";
const USER_NOTES_STORAGE_SCHEMA_VERSION = 2; export function userNotesStorageKey() {
function userNotesBaseKey() {
const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world"; const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
const userId = game.user?.id ?? "unknown-user"; const userId = game.user?.id ?? "unknown-user";
return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}`; return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.notes`;
}
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() { export function userNotesPositionKey() {
return `${userNotesBaseKey()}.position`; const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
const userId = game.user?.id ?? "unknown-user";
return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.position`;
} }
export function userNotesAppearanceKey() { export function userNotesAppearanceKey() {
return `${userNotesBaseKey()}.appearance`; const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
} const userId = game.user?.id ?? "unknown-user";
export function userNotesEncodeBase64(value) { return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.appearance`;
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() { export function userNotesLoadNotes() {
const content = userNotesLoadContentObject(); return window.localStorage.getItem(userNotesStorageKey()) ?? "";
if (content?.notes?.encoding === "base64") {
return userNotesDecodeBase64(content.notes.data);
}
return "";
} }
export function userNotesSaveNotes(value) { export function userNotesSaveNotes(value) {
const content = { window.localStorage.setItem(userNotesStorageKey(), value);
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() { export function userNotesRemoveSavedPosition() {
const state = userNotesLoadStateObject(); window.localStorage.removeItem(userNotesPositionKey());
state.position = null;
userNotesSaveStateObject(state);
} }
export function userNotesLoadAppearance(defaults) { export function userNotesLoadAppearance(defaults) {
const state = userNotesLoadStateObject(); try {
const raw = window.localStorage.getItem(userNotesAppearanceKey());
if (!state.appearance || typeof state.appearance !== "object") { if (!raw) {
return { ...defaults }; return { ...defaults };
} }
const parsed = JSON.parse(raw);
return { return {
...defaults, ...defaults,
...state.appearance ...parsed
}; };
} catch (err) {
console.warn("User Notes | Could not load appearance settings", err);
return { ...defaults };
}
} }
export function userNotesSaveAppearance(values) { export function userNotesSaveAppearance(values) {
const state = userNotesLoadStateObject(); window.localStorage.setItem(
userNotesAppearanceKey(),
state.appearance = values; JSON.stringify(values)
);
userNotesSaveStateObject(state);
} }
export function userNotesRemoveSavedAppearance() { export function userNotesRemoveSavedAppearance() {
const state = userNotesLoadStateObject(); window.localStorage.removeItem(userNotesAppearanceKey());
state.appearance = null;
userNotesSaveStateObject(state);
} }

View File

@ -9,9 +9,8 @@ import {
import { import {
userNotesLoadNotes, userNotesLoadNotes,
userNotesLoadPosition,
userNotesSaveNotes, userNotesSaveNotes,
userNotesSavePositionData, userNotesPositionKey,
userNotesRemoveSavedPosition userNotesRemoveSavedPosition
} from "./user-notes-storage.js"; } from "./user-notes-storage.js";
@ -19,11 +18,6 @@ import {
userNotesApplyWindowSettings userNotesApplyWindowSettings
} from "./user-notes-settings.js"; } from "./user-notes-settings.js";
import {
userNotesNormalizeStoredNotesForEditor,
userNotesSanitizeHtml
} from "./user-notes-sanitize.js";
let userNotesSaveTimer = null; let userNotesSaveTimer = null;
export function userNotesSetStatus(text) { export function userNotesSetStatus(text) {
@ -41,7 +35,7 @@ export function userNotesDebouncedSave(value) {
userNotesSetStatus("Ungespeichert …"); userNotesSetStatus("Ungespeichert …");
userNotesSaveTimer = window.setTimeout(() => { userNotesSaveTimer = window.setTimeout(() => {
userNotesSaveNotes(userNotesSanitizeHtml(value)); userNotesSaveNotes(value);
userNotesSetStatus("Gespeichert"); userNotesSetStatus("Gespeichert");
}, 250); }, 250);
} }
@ -100,9 +94,9 @@ export function userNotesClampPosition(position) {
export function userNotesRestorePosition(win) { export function userNotesRestorePosition(win) {
try { try {
const pos = userNotesLoadPosition(null); const raw = window.localStorage.getItem(userNotesPositionKey());
if (!pos) { if (!raw) {
userNotesApplyPosition( userNotesApplyPosition(
win, win,
userNotesClampPosition(USER_NOTES_DEFAULT_POSITION) userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
@ -110,6 +104,8 @@ export function userNotesRestorePosition(win) {
return; return;
} }
const pos = JSON.parse(raw);
const restoredPosition = { const restoredPosition = {
left: Number.isFinite(pos.left) ? pos.left : USER_NOTES_DEFAULT_POSITION.left, left: Number.isFinite(pos.left) ? pos.left : USER_NOTES_DEFAULT_POSITION.left,
top: Number.isFinite(pos.top) ? pos.top : USER_NOTES_DEFAULT_POSITION.top, top: Number.isFinite(pos.top) ? pos.top : USER_NOTES_DEFAULT_POSITION.top,
@ -152,7 +148,10 @@ export function userNotesSavePosition(win) {
height: Math.round(rect.height) height: Math.round(rect.height)
}); });
userNotesSavePositionData(position); window.localStorage.setItem(
userNotesPositionKey(),
JSON.stringify(position)
);
} }
export function userNotesResetPositionAndSize() { export function userNotesResetPositionAndSize() {
@ -167,12 +166,6 @@ export function userNotesResetPositionAndSize() {
); );
userNotesSavePosition(win); userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
} }
} }
@ -236,12 +229,6 @@ export function userNotesMakeDraggable(win) {
drag = null; drag = null;
userNotesSavePosition(win); userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
}); });
handle.addEventListener("pointercancel", event => { handle.addEventListener("pointercancel", event => {
@ -251,159 +238,9 @@ export function userNotesMakeDraggable(win) {
drag = null; drag = null;
userNotesSavePosition(win); 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 || "&nbsp;";
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() { export function userNotesOpenNotes() {
let win = document.getElementById(USER_NOTES_WINDOW_ID); let win = document.getElementById(USER_NOTES_WINDOW_ID);
@ -412,19 +249,7 @@ export function userNotesOpenNotes() {
userNotesRestorePosition(win); userNotesRestorePosition(win);
userNotesApplyWindowSettings(win); userNotesApplyWindowSettings(win);
userNotesBringToFront(win); userNotesBringToFront(win);
win.querySelector("textarea")?.focus();
const editor = win.__userNotesEditor;
if (editor) {
editor.focus();
if (editor.events) {
editor.events.fire("resize");
}
} else {
win.querySelector(".user-notes-editor")?.focus();
}
return; return;
} }
@ -453,7 +278,7 @@ export function userNotesOpenNotes() {
</header> </header>
<main class="user-notes-content"> <main class="user-notes-content">
<textarea class="user-notes-editor" spellcheck="true"></textarea> <textarea class="user-notes-textarea" spellcheck="true" placeholder="Notizen für diese Welt und diesen Benutzer …"></textarea>
</main> </main>
`; `;
@ -462,29 +287,27 @@ export function userNotesOpenNotes() {
userNotesApplyWindowSettings(win); userNotesApplyWindowSettings(win);
userNotesRestorePosition(win); userNotesRestorePosition(win);
const editorElement = win.querySelector(".user-notes-editor"); const textarea = win.querySelector(".user-notes-textarea");
if (!(editorElement instanceof HTMLTextAreaElement)) { if (!(textarea instanceof HTMLTextAreaElement)) {
console.error(`${USER_NOTES_MODULE_ID} | Notes editor could not be created.`); console.error(`${USER_NOTES_MODULE_ID} | Notes textarea could not be created.`);
return; return;
} }
userNotesCreateEditor(win, editorElement); textarea.value = userNotesLoadNotes();
textarea.addEventListener("input", event => {
userNotesDebouncedSave(event.currentTarget.value);
});
win.querySelector(".user-notes-save")?.addEventListener("click", () => { win.querySelector(".user-notes-save")?.addEventListener("click", () => {
userNotesSaveNotes(userNotesGetEditorValue(win)); userNotesSaveNotes(textarea.value);
userNotesSetStatus("Gespeichert"); userNotesSetStatus("Gespeichert");
userNotesSavePosition(win); userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
}); });
win.querySelector(".user-notes-close")?.addEventListener("click", () => { win.querySelector(".user-notes-close")?.addEventListener("click", () => {
userNotesSaveNotes(userNotesGetEditorValue(win)); userNotesSaveNotes(textarea.value);
userNotesSavePosition(win); userNotesSavePosition(win);
win.hidden = true; win.hidden = true;
}); });
@ -505,45 +328,30 @@ export function userNotesOpenNotes() {
} }
userNotesSavePosition(win); userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
}); });
resizeObserver.observe(win); resizeObserver.observe(win);
userNotesMakeDraggable(win); userNotesMakeDraggable(win);
userNotesBringToFront(win); userNotesBringToFront(win);
textarea.focus();
if (win.__userNotesEditor) {
win.__userNotesEditor.focus();
if (win.__userNotesEditor.events) {
win.__userNotesEditor.events.fire("resize");
}
} else {
editorElement.focus();
}
} }
export function userNotesRefreshOpenWindow(options = {}) { export function userNotesRefreshOpenWindow() {
const saveCurrentContent = options.saveCurrentContent ?? true;
const oldWin = document.getElementById(USER_NOTES_WINDOW_ID); const oldWin = document.getElementById(USER_NOTES_WINDOW_ID);
if (!oldWin) { if (!oldWin) {
return; return;
} }
if (saveCurrentContent) { const textarea = oldWin.querySelector(".user-notes-textarea");
userNotesSaveNotes(userNotesGetEditorValue(oldWin));
if (textarea instanceof HTMLTextAreaElement) {
userNotesSaveNotes(textarea.value);
} }
userNotesSavePosition(oldWin); userNotesSavePosition(oldWin);
userNotesDestroyEditor(oldWin);
oldWin.remove(); oldWin.remove();
userNotesOpenNotes(); userNotesOpenNotes();

View File

@ -2,23 +2,17 @@ import {
userNotesRegisterSettings userNotesRegisterSettings
} from "./user-notes-settings.js"; } from "./user-notes-settings.js";
import {
userNotesRegisterBackupSettings
} from "./user-notes-backup.js";
import { import {
userNotesRegisterTokenControl userNotesRegisterTokenControl
} from "./user-notes-controls.js"; } from "./user-notes-controls.js";
import { import {
userNotesOpenNotes, userNotesOpenNotes,
userNotesRefreshOpenWindow,
userNotesResetPositionAndSize userNotesResetPositionAndSize
} from "./user-notes-window.js"; } from "./user-notes-window.js";
globalThis.UserNotes = { globalThis.UserNotes = {
open: userNotesOpenNotes, open: userNotesOpenNotes,
refresh: userNotesRefreshOpenWindow,
resetPosition: userNotesResetPositionAndSize resetPosition: userNotesResetPositionAndSize
}; };
@ -27,10 +21,8 @@ Hooks.once("init", () => {
console.log("User Notes | application registered"); console.log("User Notes | application registered");
userNotesRegisterSettings(userNotesResetPositionAndSize); userNotesRegisterSettings(userNotesResetPositionAndSize);
console.log("User Notes | settings registered");
userNotesRegisterBackupSettings(); console.log("User Notes | settings registered");
console.log("User Notes | export/import settings registered");
} catch (err) { } catch (err) {
console.error("User Notes | error during init", err); console.error("User Notes | error during init", err);

View File

@ -1,21 +1,12 @@
.user-notes-appearance-settings, .user-notes-appearance-settings {
.user-notes-backup-settings {
padding: 0.75rem; padding: 0.75rem;
} }
.user-notes-appearance-settings .notes, .user-notes-appearance-settings .notes {
.user-notes-backup-settings .notes {
margin-top: 0; margin-top: 0;
margin-bottom: 0.75rem; 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 { .user-notes-appearance-settings fieldset {
margin: 0 0 0.75rem 0; margin: 0 0 0.75rem 0;
padding: 0.75rem; padding: 0.75rem;
@ -29,13 +20,11 @@
font-weight: bold; font-weight: bold;
} }
.user-notes-appearance-settings .form-group, .user-notes-appearance-settings .form-group {
.user-notes-backup-settings .form-group {
align-items: center; align-items: center;
} }
.user-notes-appearance-settings .form-fields, .user-notes-appearance-settings .form-fields {
.user-notes-backup-settings .form-fields {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@ -75,41 +64,21 @@
white-space: nowrap; 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-settings .sheet-footer,
.user-notes-appearance-footer { .user-notes-appearance-footer {
margin-top: 0.75rem; margin-top: 0.75rem;
display: flex; display: flex;
flex-direction: column; align-items: center;
align-items: stretch;
gap: 0.5rem; gap: 0.5rem;
} }
.user-notes-primary-actions { .user-notes-primary-actions {
flex: 1 1 auto;
display: flex; 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; align-items: center;
justify-content: center; gap: 0.5rem;
gap: 0.35rem;
box-sizing: border-box;
} }
.user-notes-primary-actions button { .user-notes-primary-actions button {
@ -118,45 +87,4 @@
.user-notes-appearance-footer > .user-notes-reset-appearance { .user-notes-appearance-footer > .user-notes-reset-appearance {
flex: 0 0 auto; 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;
} }

View File

@ -92,28 +92,17 @@
background: rgba(255, 255, 255, 0.16); 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 { .user-notes-content {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
min-width: 0;
display: flex; display: flex;
padding: 0.5rem; padding: 0.5rem;
box-sizing: border-box;
overflow: hidden; box-sizing: border-box;
} }
/* .user-notes-textarea {
* Original textarea fallback.
* Jodit replaces/wraps this element when available.
*/
.user-notes-editor {
flex: 1 1 auto; flex: 1 1 auto;
width: 100%; width: 100%;
@ -138,165 +127,7 @@
line-height: 1.35; line-height: 1.35;
} }
.user-notes-editor:focus { .user-notes-textarea:focus {
outline: 1px solid var(--color-border-highlight, #ff6400); outline: 1px solid var(--color-border-highlight, #ff6400);
outline-offset: 0; 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;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
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.

View File

@ -1,3 +0,0 @@
Jodit Editor 4.12.2
Source: https://github.com/xdan/jodit/tree/4.12.2
License: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long