Compare commits

...

6 Commits
V1.0.1 ... main

Author SHA1 Message Date
Florian Zumpe
c19bbd1910 update module.json to Version 1.0.3 2026-05-20 15:23:15 +02:00
Florian Zumpe
81410869f4 add import and export and fix local save structure 2026-05-20 15:21:46 +02:00
Florian Zumpe
d4671086a4 set styling for jodit editor and settings 2026-05-20 14:03:19 +02:00
Florian Zumpe
0b89998c23 changed Module.json to include jodit 2026-05-20 12:14:55 +02:00
Florian Zumpe
b6fd4d5f4c Implement Jodit Wysiwyg editor and sanitize functions 2026-05-20 11:20:33 +02:00
Florian Zumpe
6a37be1ce8 Cleanup console logs and Update to Version 1.0.2 2026-05-19 17:10:31 +02:00
15 changed files with 7300 additions and 105 deletions

View File

@ -1,7 +1,7 @@
{ {
"id": "user-notes", "id": "user-notes",
"title": "User Notes", "title": "User Notes",
"version": "1.0.0", "version": "1.0.3",
"authors": [ "authors": [
{ {
"name": "Florian Zumpe" "name": "Florian Zumpe"
@ -11,15 +11,19 @@
"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/main/module.json", "manifest": "https://raw.githubusercontent.com/fzumpe/foundry-usernotes/V1.0.3/module.json",
"download": "https://codeload.github.com/fzumpe/foundry-usernotes/zip/refs/tags/V1.0.1" "download": "https://codeload.github.com/fzumpe/foundry-usernotes/zip/refs/tags/V1.0.3"
} }

View 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
});
}

View File

@ -1,5 +1,4 @@
import { import {
USER_NOTES_MODULE_ID,
USER_NOTES_TOOL_ID USER_NOTES_TOOL_ID
} from "./user-notes-constants.js"; } from "./user-notes-constants.js";
@ -8,17 +7,15 @@ import {
} from "./user-notes-window.js"; } from "./user-notes-window.js";
export function userNotesRegisterTokenControl(controls) { export function userNotesRegisterTokenControl(controls) {
console.log("User Notes | registering token control", controls);
const tokenControl = controls?.tokens; const tokenControl = controls?.tokens;
if (!tokenControl) { if (!tokenControl) {
console.warn("User Notes | controls.tokens fehlt", controls); console.warn("User Notes | controls.tokens is not available; token control was not registered");
return; return;
} }
if (!tokenControl.tools) { if (!tokenControl.tools) {
console.warn("User Notes | controls.tokens.tools fehlt", tokenControl); console.warn("User Notes | controls.tokens.tools is not available; token control was not registered");
return; return;
} }
@ -30,19 +27,14 @@ export function userNotesRegisterTokenControl(controls) {
button: true, button: true,
visible: true, visible: true,
onChange: (event, active) => { onChange: () => {
console.log("User Notes | onChange", { event, active });
userNotesOpenNotes(); userNotesOpenNotes();
}, },
onClick: event => { onClick: () => {
console.log("User Notes | onClick", { event });
userNotesOpenNotes(); userNotesOpenNotes();
} }
}; };
console.log( console.log("User Notes | token control button registered");
"User Notes | token control registered",
tokenControl.tools[USER_NOTES_TOOL_ID]
);
} }

View 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);
}

View File

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

View File

@ -9,8 +9,9 @@ import {
import { import {
userNotesLoadNotes, userNotesLoadNotes,
userNotesLoadPosition,
userNotesSaveNotes, userNotesSaveNotes,
userNotesPositionKey, userNotesSavePositionData,
userNotesRemoveSavedPosition userNotesRemoveSavedPosition
} from "./user-notes-storage.js"; } from "./user-notes-storage.js";
@ -18,6 +19,11 @@ 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) {
@ -35,7 +41,7 @@ export function userNotesDebouncedSave(value) {
userNotesSetStatus("Ungespeichert …"); userNotesSetStatus("Ungespeichert …");
userNotesSaveTimer = window.setTimeout(() => { userNotesSaveTimer = window.setTimeout(() => {
userNotesSaveNotes(value); userNotesSaveNotes(userNotesSanitizeHtml(value));
userNotesSetStatus("Gespeichert"); userNotesSetStatus("Gespeichert");
}, 250); }, 250);
} }
@ -94,9 +100,9 @@ export function userNotesClampPosition(position) {
export function userNotesRestorePosition(win) { export function userNotesRestorePosition(win) {
try { try {
const raw = window.localStorage.getItem(userNotesPositionKey()); const pos = userNotesLoadPosition(null);
if (!raw) { if (!pos) {
userNotesApplyPosition( userNotesApplyPosition(
win, win,
userNotesClampPosition(USER_NOTES_DEFAULT_POSITION) userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
@ -104,8 +110,6 @@ 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,
@ -148,10 +152,7 @@ export function userNotesSavePosition(win) {
height: Math.round(rect.height) height: Math.round(rect.height)
}); });
window.localStorage.setItem( userNotesSavePositionData(position);
userNotesPositionKey(),
JSON.stringify(position)
);
} }
export function userNotesResetPositionAndSize() { export function userNotesResetPositionAndSize() {
@ -166,6 +167,12 @@ export function userNotesResetPositionAndSize() {
); );
userNotesSavePosition(win); userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
} }
} }
@ -229,6 +236,12 @@ 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 => {
@ -238,9 +251,159 @@ 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);
@ -249,7 +412,19 @@ 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;
} }
@ -278,7 +453,7 @@ export function userNotesOpenNotes() {
</header> </header>
<main class="user-notes-content"> <main class="user-notes-content">
<textarea class="user-notes-textarea" spellcheck="true" placeholder="Notizen für diese Welt und diesen Benutzer …"></textarea> <textarea class="user-notes-editor" spellcheck="true"></textarea>
</main> </main>
`; `;
@ -287,27 +462,29 @@ export function userNotesOpenNotes() {
userNotesApplyWindowSettings(win); userNotesApplyWindowSettings(win);
userNotesRestorePosition(win); userNotesRestorePosition(win);
const textarea = win.querySelector(".user-notes-textarea"); const editorElement = win.querySelector(".user-notes-editor");
if (!(textarea instanceof HTMLTextAreaElement)) { if (!(editorElement instanceof HTMLTextAreaElement)) {
console.error(`${USER_NOTES_MODULE_ID} | Notes textarea could not be created.`); console.error(`${USER_NOTES_MODULE_ID} | Notes editor could not be created.`);
return; return;
} }
textarea.value = userNotesLoadNotes(); userNotesCreateEditor(win, editorElement);
textarea.addEventListener("input", event => {
userNotesDebouncedSave(event.currentTarget.value);
});
win.querySelector(".user-notes-save")?.addEventListener("click", () => { win.querySelector(".user-notes-save")?.addEventListener("click", () => {
userNotesSaveNotes(textarea.value); userNotesSaveNotes(userNotesGetEditorValue(win));
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(textarea.value); userNotesSaveNotes(userNotesGetEditorValue(win));
userNotesSavePosition(win); userNotesSavePosition(win);
win.hidden = true; win.hidden = true;
}); });
@ -328,30 +505,45 @@ 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() { export function userNotesRefreshOpenWindow(options = {}) {
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;
} }
const textarea = oldWin.querySelector(".user-notes-textarea"); if (saveCurrentContent) {
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,43 +2,52 @@ 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,
userNotesResetPositionAndSize, userNotesRefreshOpenWindow,
userNotesRefreshOpenWindow userNotesResetPositionAndSize
} from "./user-notes-window.js"; } from "./user-notes-window.js";
console.log("User Notes | ES module loaded");
globalThis.UserNotes = { globalThis.UserNotes = {
open: userNotesOpenNotes, open: userNotesOpenNotes,
resetPosition: userNotesResetPositionAndSize, refresh: userNotesRefreshOpenWindow,
refresh: userNotesRefreshOpenWindow resetPosition: userNotesResetPositionAndSize
}; };
Hooks.once("init", () => { Hooks.once("init", () => {
console.log("User Notes | init hook fired");
try { try {
console.log("User Notes | application registered");
userNotesRegisterSettings(userNotesResetPositionAndSize); userNotesRegisterSettings(userNotesResetPositionAndSize);
console.log("User Notes | settings registered"); console.log("User Notes | settings registered");
userNotesRegisterBackupSettings();
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);
ui.notifications?.error("User Notes: Fehler beim Initialisieren. Details stehen in der Browser-Konsole.");
ui.notifications?.error(
"User Notes: Fehler beim Initialisieren. Details stehen in der Browser-Konsole."
);
} }
}); });
Hooks.on("getSceneControlButtons", controls => { Hooks.on("getSceneControlButtons", controls => {
console.log("User Notes | getSceneControlButtons fired");
try { try {
userNotesRegisterTokenControl(controls); userNotesRegisterTokenControl(controls);
} catch (err) { } catch (err) {
console.error("User Notes | error while registering token control", err); console.error("User Notes | error while registering token control", err);
ui.notifications?.error("User Notes: Fehler beim Registrieren des Token-Controls.");
ui.notifications?.error(
"User Notes: Fehler beim Registrieren des Token-Controls."
);
} }
}); });

View File

@ -1,12 +1,21 @@
.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;
@ -20,11 +29,13 @@
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;
@ -64,21 +75,41 @@
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;
align-items: center; flex-direction: column;
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;
align-items: center; flex-direction: row;
align-items: stretch;
gap: 0.5rem; 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 { .user-notes-primary-actions button {
@ -87,4 +118,45 @@
.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,17 +92,28 @@
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; box-sizing: border-box;
overflow: hidden;
} }
.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%;
@ -127,7 +138,165 @@
line-height: 1.35; line-height: 1.35;
} }
.user-notes-textarea:focus { .user-notes-editor: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;
}

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
View 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
View 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

File diff suppressed because one or more lines are too long

272
vendor/jodit/jodit.min.js vendored Normal file

File diff suppressed because one or more lines are too long