Compare commits

..

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

17 changed files with 330 additions and 8375 deletions

View File

@ -1,10 +1,11 @@
{ {
"id": "user-notes", "id": "user-notes",
"title": "User Notes", "title": "User Notes",
"version": "1.0.3", "description": "User Notes: ein einfaches lokales Notizfenster für Foundry VTT v14. Notizen werden im Browser-localStorage pro Welt und Benutzer gespeichert.",
"version": "1.0.1",
"authors": [ "authors": [
{ {
"name": "Florian Zumpe" "name": "ChatGPT"
} }
], ],
"compatibility": { "compatibility": {
@ -12,18 +13,13 @@
"verified": "14" "verified": "14"
}, },
"scripts": [ "scripts": [
"vendor/jodit/jodit.min.js"
],
"esmodules": [
"scripts/user-notes.js" "scripts/user-notes.js"
], ],
"styles": [ "styles": [
"vendor/jodit/jodit.min.css", "styles/user-notes.css"
"styles/user-notes-window.css",
"styles/user-notes-settings.css"
], ],
"languages": [], "languages": [],
"url": "https://github.com/fzumpe/foundry-usernotes", "url": "https://github.com/fzumpe/foundry-usernotes",
"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://github.com/fzumpe/foundry-usernotes/releases/download/v1.0.1/user-notes.zip"
} }

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,19 +0,0 @@
export const USER_NOTES_MODULE_ID = "user-notes";
export const USER_NOTES_WINDOW_ID = "user-notes-window";
export const USER_NOTES_TOOL_ID = "user-notes-open";
export const USER_NOTES_DEFAULT_POSITION = {
left: 200,
top: 150,
width: 500,
height: 400
};
export const USER_NOTES_MIN_WIDTH = 280;
export const USER_NOTES_MIN_HEIGHT = 180;
export const USER_NOTES_VIEWPORT_MARGIN = 40;
export const USER_NOTES_DEFAULT_BACKGROUND = "rgba(25, 24, 19, 0.96)";
export const USER_NOTES_DEFAULT_TEXT_COLOR = "#f0f0e0";
export const USER_NOTES_DEFAULT_TEXTAREA_BACKGROUND = "rgba(255, 255, 255, 0.92)";
export const USER_NOTES_DEFAULT_TEXTAREA_COLOR = "#111111";

View File

@ -1,40 +0,0 @@
import {
USER_NOTES_TOOL_ID
} from "./user-notes-constants.js";
import {
userNotesOpenNotes
} from "./user-notes-window.js";
export function userNotesRegisterTokenControl(controls) {
const tokenControl = controls?.tokens;
if (!tokenControl) {
console.warn("User Notes | controls.tokens is not available; token control was not registered");
return;
}
if (!tokenControl.tools) {
console.warn("User Notes | controls.tokens.tools is not available; token control was not registered");
return;
}
tokenControl.tools[USER_NOTES_TOOL_ID] = {
name: USER_NOTES_TOOL_ID,
title: "User Notes öffnen",
icon: "fa-solid fa-note-sticky",
order: Object.keys(tokenControl.tools).length + 1,
button: true,
visible: true,
onChange: () => {
userNotesOpenNotes();
},
onClick: () => {
userNotesOpenNotes();
}
};
console.log("User Notes | token control button registered");
}

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

@ -1,432 +0,0 @@
import {
USER_NOTES_MODULE_ID,
USER_NOTES_WINDOW_ID
} from "./user-notes-constants.js";
import {
userNotesLoadAppearance,
userNotesSaveAppearance,
userNotesRemoveSavedAppearance
} from "./user-notes-storage.js";
let resetPositionCallback = null;
const USER_NOTES_APPEARANCE_DEFAULTS = {
windowBackgroundColor: "#191813",
windowBackgroundAlpha: 0.96,
windowTextColor: "#f0f0e0",
textareaBackgroundColor: "#ffffff",
textareaBackgroundAlpha: 0.92,
textareaTextColor: "#111111"
};
function userNotesClampAlpha(value, fallback = 1) {
const number = Number(value);
if (!Number.isFinite(number)) {
return fallback;
}
return Math.max(0, Math.min(1, number));
}
function userNotesNormalizeHexColor(value, fallback) {
const normalized = String(value ?? "").trim();
if (/^#[0-9a-f]{6}$/i.test(normalized)) {
return normalized;
}
return fallback;
}
function userNotesHexToRgba(hex, alpha) {
const normalized = userNotesNormalizeHexColor(hex, "#000000");
const match = normalized.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
if (!match) {
return `rgba(0, 0, 0, ${userNotesClampAlpha(alpha)})`;
}
const red = parseInt(match[1], 16);
const green = parseInt(match[2], 16);
const blue = parseInt(match[3], 16);
return `rgba(${red}, ${green}, ${blue}, ${userNotesClampAlpha(alpha)})`;
}
function userNotesLoadValidatedAppearance() {
const values = userNotesLoadAppearance(USER_NOTES_APPEARANCE_DEFAULTS);
return {
windowBackgroundColor: userNotesNormalizeHexColor(
values.windowBackgroundColor,
USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundColor
),
windowBackgroundAlpha: userNotesClampAlpha(
values.windowBackgroundAlpha,
USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundAlpha
),
windowTextColor: userNotesNormalizeHexColor(
values.windowTextColor,
USER_NOTES_APPEARANCE_DEFAULTS.windowTextColor
),
textareaBackgroundColor: userNotesNormalizeHexColor(
values.textareaBackgroundColor,
USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundColor
),
textareaBackgroundAlpha: userNotesClampAlpha(
values.textareaBackgroundAlpha,
USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundAlpha
),
textareaTextColor: userNotesNormalizeHexColor(
values.textareaTextColor,
USER_NOTES_APPEARANCE_DEFAULTS.textareaTextColor
)
};
}
export function userNotesRegisterSettings(onResetPosition) {
resetPositionCallback = onResetPosition;
game.settings.registerMenu(USER_NOTES_MODULE_ID, "appearanceSettings", {
name: "Darstellung",
label: "Farben und Transparenz einstellen",
hint: "Öffnet lokale Colorpicker für Fenster, Notizfeld und Schriftfarben. Die Werte werden nur im localStorage dieses Browsers gespeichert.",
icon: "fas fa-palette",
type: UserNotesAppearanceSettings,
restricted: false
});
game.settings.registerMenu(USER_NOTES_MODULE_ID, "resetWindowPosition", {
name: "Position und Größe zurücksetzen",
label: "Jetzt zurücksetzen",
hint: "Setzt nur Position und Größe des User-Notes-Fensters für diesen Browser zurück. Notizen, Farben und Transparenzwerte bleiben unverändert.",
icon: "fas fa-undo",
type: UserNotesDirectResetWindowPosition,
restricted: false
});
}
export function userNotesApplySettingsToOpenWindow() {
const win = document.getElementById(USER_NOTES_WINDOW_ID);
if (!win) {
return;
}
userNotesApplyWindowSettings(win);
}
export function userNotesApplyWindowSettings(win) {
const appearance = userNotesLoadValidatedAppearance();
win.style.setProperty(
"--user-notes-window-background-solid",
appearance.windowBackgroundColor
);
win.style.setProperty(
"--user-notes-window-background",
userNotesHexToRgba(
appearance.windowBackgroundColor,
appearance.windowBackgroundAlpha
)
);
win.style.setProperty(
"--user-notes-window-text-color",
appearance.windowTextColor
);
win.style.setProperty(
"--user-notes-textarea-background",
userNotesHexToRgba(
appearance.textareaBackgroundColor,
appearance.textareaBackgroundAlpha
)
);
win.style.setProperty(
"--user-notes-textarea-color",
appearance.textareaTextColor
);
}
class UserNotesDirectResetWindowPosition extends FormApplication {
render(_force, _options) {
if (typeof resetPositionCallback === "function") {
resetPositionCallback();
ui.notifications?.info("User Notes: Position und Größe wurden zurückgesetzt.");
} else {
console.warn("User Notes | resetPositionCallback is not available");
ui.notifications?.warn("User Notes: Reset-Funktion ist nicht verfügbar.");
}
return this;
}
}
class UserNotesAppearanceSettings extends FormApplication {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "user-notes-appearance-settings",
title: "User Notes Darstellung",
template: null,
width: 520,
height: "auto",
closeOnSubmit: false,
submitOnChange: false,
submitOnClose: false
});
}
getData() {
const appearance = userNotesLoadValidatedAppearance();
return {
...appearance,
windowBackgroundAlphaPercent: Math.round(appearance.windowBackgroundAlpha * 100),
textareaBackgroundAlphaPercent: Math.round(appearance.textareaBackgroundAlpha * 100)
};
}
async _renderInner(data) {
const html = `
<div class="user-notes-appearance-settings">
<p class="notes">
Diese Werte werden lokal im Browser gespeichert.
Foundry-Settings werden dadurch nicht überschrieben und die Seite wird nicht neu geladen.
</p>
<fieldset>
<legend>Fenster</legend>
<div class="form-group">
<label>Hintergrundfarbe</label>
<div class="form-fields">
<input type="color" name="windowBackgroundColor" value="${data.windowBackgroundColor}">
<input type="text" name="windowBackgroundColorText" value="${data.windowBackgroundColor}">
</div>
<p class="hint">Farbe des äußeren Notizfensters.</p>
</div>
<div class="form-group">
<label>Hintergrundtransparenz</label>
<div class="form-fields user-notes-range-row">
<input type="range" name="windowBackgroundAlpha" min="0" max="1" step="0.01" value="${data.windowBackgroundAlpha}">
<output>${data.windowBackgroundAlphaPercent}%</output>
</div>
<p class="hint">0% ist vollständig transparent, 100% ist vollständig deckend.</p>
</div>
<div class="form-group">
<label>Schriftfarbe</label>
<div class="form-fields">
<input type="color" name="windowTextColor" value="${data.windowTextColor}">
<input type="text" name="windowTextColorText" value="${data.windowTextColor}">
</div>
<p class="hint">Farbe für Titelleiste, Status und Buttons.</p>
</div>
</fieldset>
<fieldset>
<legend>Notizfeld</legend>
<div class="form-group">
<label>Hintergrundfarbe</label>
<div class="form-fields">
<input type="color" name="textareaBackgroundColor" value="${data.textareaBackgroundColor}">
<input type="text" name="textareaBackgroundColorText" value="${data.textareaBackgroundColor}">
</div>
<p class="hint">Farbe des eigentlichen Textfeldes.</p>
</div>
<div class="form-group">
<label>Hintergrundtransparenz</label>
<div class="form-fields user-notes-range-row">
<input type="range" name="textareaBackgroundAlpha" min="0" max="1" step="0.01" value="${data.textareaBackgroundAlpha}">
<output>${data.textareaBackgroundAlphaPercent}%</output>
</div>
<p class="hint">0% ist vollständig transparent, 100% ist vollständig deckend.</p>
</div>
<div class="form-group">
<label>Schriftfarbe</label>
<div class="form-fields">
<input type="color" name="textareaTextColor" value="${data.textareaTextColor}">
<input type="text" name="textareaTextColorText" value="${data.textareaTextColor}">
</div>
<p class="hint">Farbe des Textes innerhalb des Notizfeldes.</p>
</div>
</fieldset>
<footer class="sheet-footer user-notes-appearance-footer">
<div class="user-notes-primary-actions">
<button type="button" class="user-notes-apply-appearance">
<i class="fas fa-check"></i>
Anwenden
</button>
<button type="button" class="user-notes-save-appearance">
<i class="fas fa-save"></i>
Speichern
</button>
</div>
<button type="button" class="user-notes-reset-appearance">
<i class="fas fa-undo"></i>
Standardfarben
</button>
</footer>
</div>
`;
return $(html);
}
activateListeners(html) {
super.activateListeners(html);
const root = html[0];
if (!root) {
console.warn("User Notes | appearance settings root element not found");
return;
}
html.on("submit", event => {
event.preventDefault();
event.stopPropagation();
return false;
});
const syncColorPair = (colorName, textName) => {
const colorInput = root.querySelector(`input[name="${colorName}"]`);
const textInput = root.querySelector(`input[name="${textName}"]`);
if (!colorInput || !textInput) {
return;
}
colorInput.addEventListener("input", () => {
textInput.value = colorInput.value;
});
textInput.addEventListener("input", () => {
if (/^#[0-9a-f]{6}$/i.test(textInput.value)) {
colorInput.value = textInput.value;
}
});
};
syncColorPair("windowBackgroundColor", "windowBackgroundColorText");
syncColorPair("windowTextColor", "windowTextColorText");
syncColorPair("textareaBackgroundColor", "textareaBackgroundColorText");
syncColorPair("textareaTextColor", "textareaTextColorText");
for (const range of root.querySelectorAll('input[type="range"]')) {
const output = range
.closest(".user-notes-range-row")
?.querySelector("output");
const updateOutput = () => {
if (output) {
output.textContent = `${Math.round(Number(range.value) * 100)}%`;
}
};
range.addEventListener("input", updateOutput);
range.addEventListener("change", updateOutput);
updateOutput();
}
root.querySelector(".user-notes-apply-appearance")?.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
this.userNotesSaveAppearanceFromDialog(root, {
closeDialog: false,
notify: true
});
});
root.querySelector(".user-notes-save-appearance")?.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
this.userNotesSaveAppearanceFromDialog(root, {
closeDialog: true,
notify: true
});
});
root.querySelector(".user-notes-reset-appearance")?.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
userNotesRemoveSavedAppearance();
userNotesApplySettingsToOpenWindow();
ui.notifications?.info("User Notes: Standardfarben wurden wiederhergestellt.");
this.render(true);
});
}
userNotesSaveAppearanceFromDialog(root, options = {}) {
const closeDialog = options.closeDialog ?? true;
const notify = options.notify ?? true;
const getInputValue = name => {
const input = root.querySelector(`[name="${name}"]`);
return input?.value;
};
const appearance = {
windowBackgroundColor: userNotesNormalizeHexColor(
getInputValue("windowBackgroundColorText") || getInputValue("windowBackgroundColor"),
USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundColor
),
windowBackgroundAlpha: userNotesClampAlpha(
getInputValue("windowBackgroundAlpha"),
USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundAlpha
),
windowTextColor: userNotesNormalizeHexColor(
getInputValue("windowTextColorText") || getInputValue("windowTextColor"),
USER_NOTES_APPEARANCE_DEFAULTS.windowTextColor
),
textareaBackgroundColor: userNotesNormalizeHexColor(
getInputValue("textareaBackgroundColorText") || getInputValue("textareaBackgroundColor"),
USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundColor
),
textareaBackgroundAlpha: userNotesClampAlpha(
getInputValue("textareaBackgroundAlpha"),
USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundAlpha
),
textareaTextColor: userNotesNormalizeHexColor(
getInputValue("textareaTextColorText") || getInputValue("textareaTextColor"),
USER_NOTES_APPEARANCE_DEFAULTS.textareaTextColor
)
};
userNotesSaveAppearance(appearance);
userNotesApplySettingsToOpenWindow();
if (notify) {
ui.notifications?.info(
closeDialog
? "User Notes: Darstellung wurde gespeichert."
: "User Notes: Darstellung wurde angewendet."
);
}
if (closeDialog) {
this.close();
}
}
async _updateObject(_event, _formData) {
// Wird absichtlich nicht verwendet.
// Die Buttons speichern direkt in localStorage, damit kein nativer GET-Submit stattfindet.
}
}

View File

@ -1,271 +0,0 @@
import {
USER_NOTES_MODULE_ID
} from "./user-notes-constants.js";
const USER_NOTES_STORAGE_SCHEMA_VERSION = 2;
function userNotesBaseKey() {
const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
const userId = game.user?.id ?? "unknown-user";
return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}`;
}
export function userNotesContentKey() {
return `${userNotesBaseKey()}.content`;
}
export function userNotesStateKey() {
return `${userNotesBaseKey()}.state`;
}
/**
* Legacy keys from older versions.
* Kept only for migration.
*/
export function userNotesStorageKey() {
return `${userNotesBaseKey()}.notes`;
}
export function userNotesPositionKey() {
return `${userNotesBaseKey()}.position`;
}
export function userNotesAppearanceKey() {
return `${userNotesBaseKey()}.appearance`;
}
export function userNotesEncodeBase64(value) {
const bytes = new TextEncoder().encode(String(value ?? ""));
const chunkSize = 0x8000;
let binary = "";
for (let index = 0; index < bytes.length; index += chunkSize) {
const chunk = bytes.subarray(index, index + chunkSize);
binary += String.fromCharCode(...chunk);
}
return window.btoa(binary);
}
export function userNotesDecodeBase64(value) {
try {
const binary = window.atob(String(value ?? ""));
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++) {
bytes[index] = binary.charCodeAt(index);
}
return new TextDecoder().decode(bytes);
} catch (err) {
console.warn("User Notes | Could not decode base64 content", err);
return "";
}
}
function userNotesRemoveLegacyStorageKeys() {
window.localStorage.removeItem(userNotesStorageKey());
window.localStorage.removeItem(userNotesPositionKey());
window.localStorage.removeItem(userNotesAppearanceKey());
}
function userNotesReadJson(key, fallback = null) {
try {
const raw = window.localStorage.getItem(key);
if (!raw) {
return fallback;
}
return JSON.parse(raw);
} catch (err) {
console.warn(`User Notes | Could not parse localStorage key: ${key}`, err);
return fallback;
}
}
function userNotesWriteJson(key, value) {
window.localStorage.setItem(
key,
JSON.stringify(value)
);
}
function userNotesCreateEmptyContent() {
return {
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
updatedAt: new Date().toISOString(),
notes: {
format: "html",
encoding: "base64",
data: ""
}
};
}
function userNotesCreateEmptyState() {
return {
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
updatedAt: new Date().toISOString(),
appearance: null,
position: null
};
}
function userNotesLoadContentObject() {
const content = userNotesReadJson(
userNotesContentKey(),
null
);
if (content?.notes?.encoding === "base64") {
return content;
}
const legacyNotes = window.localStorage.getItem(userNotesStorageKey());
if (legacyNotes !== null) {
const migrated = userNotesCreateEmptyContent();
migrated.notes.data = userNotesEncodeBase64(legacyNotes);
userNotesWriteJson(userNotesContentKey(), migrated);
window.localStorage.removeItem(userNotesStorageKey());
return migrated;
}
return userNotesCreateEmptyContent();
}
function userNotesLoadStateObject() {
const state = userNotesReadJson(
userNotesStateKey(),
null
);
if (state && typeof state === "object") {
return {
...userNotesCreateEmptyState(),
...state
};
}
const migrated = userNotesCreateEmptyState();
const legacyPosition = userNotesReadJson(
userNotesPositionKey(),
null
);
const legacyAppearance = userNotesReadJson(
userNotesAppearanceKey(),
null
);
if (legacyPosition) {
migrated.position = legacyPosition;
}
if (legacyAppearance) {
migrated.appearance = legacyAppearance;
}
if (legacyPosition || legacyAppearance) {
userNotesWriteJson(userNotesStateKey(), migrated);
window.localStorage.removeItem(userNotesPositionKey());
window.localStorage.removeItem(userNotesAppearanceKey());
}
return migrated;
}
function userNotesSaveStateObject(state) {
userNotesWriteJson(
userNotesStateKey(),
{
...userNotesCreateEmptyState(),
...state,
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
updatedAt: new Date().toISOString()
}
);
userNotesRemoveLegacyStorageKeys();
}
export function userNotesLoadNotes() {
const content = userNotesLoadContentObject();
if (content?.notes?.encoding === "base64") {
return userNotesDecodeBase64(content.notes.data);
}
return "";
}
export function userNotesSaveNotes(value) {
const content = {
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
updatedAt: new Date().toISOString(),
notes: {
format: "html",
encoding: "base64",
data: userNotesEncodeBase64(value)
}
};
userNotesWriteJson(userNotesContentKey(), content);
userNotesRemoveLegacyStorageKeys();
}
export function userNotesLoadPosition(defaultValue = null) {
const state = userNotesLoadStateObject();
return state.position ?? defaultValue;
}
export function userNotesSavePositionData(position) {
const state = userNotesLoadStateObject();
state.position = position;
userNotesSaveStateObject(state);
}
export function userNotesRemoveSavedPosition() {
const state = userNotesLoadStateObject();
state.position = null;
userNotesSaveStateObject(state);
}
export function userNotesLoadAppearance(defaults) {
const state = userNotesLoadStateObject();
if (!state.appearance || typeof state.appearance !== "object") {
return { ...defaults };
}
return {
...defaults,
...state.appearance
};
}
export function userNotesSaveAppearance(values) {
const state = userNotesLoadStateObject();
state.appearance = values;
userNotesSaveStateObject(state);
}
export function userNotesRemoveSavedAppearance() {
const state = userNotesLoadStateObject();
state.appearance = null;
userNotesSaveStateObject(state);
}

View File

@ -1,550 +0,0 @@
import {
USER_NOTES_MODULE_ID,
USER_NOTES_WINDOW_ID,
USER_NOTES_DEFAULT_POSITION,
USER_NOTES_MIN_WIDTH,
USER_NOTES_MIN_HEIGHT,
USER_NOTES_VIEWPORT_MARGIN
} from "./user-notes-constants.js";
import {
userNotesLoadNotes,
userNotesLoadPosition,
userNotesSaveNotes,
userNotesSavePositionData,
userNotesRemoveSavedPosition
} from "./user-notes-storage.js";
import {
userNotesApplyWindowSettings
} from "./user-notes-settings.js";
import {
userNotesNormalizeStoredNotesForEditor,
userNotesSanitizeHtml
} from "./user-notes-sanitize.js";
let userNotesSaveTimer = null;
export function userNotesSetStatus(text) {
const status = document.querySelector(
`#${USER_NOTES_WINDOW_ID} .user-notes-status`
);
if (status) {
status.textContent = text;
}
}
export function userNotesDebouncedSave(value) {
window.clearTimeout(userNotesSaveTimer);
userNotesSetStatus("Ungespeichert …");
userNotesSaveTimer = window.setTimeout(() => {
userNotesSaveNotes(userNotesSanitizeHtml(value));
userNotesSetStatus("Gespeichert");
}, 250);
}
export function userNotesApplyPosition(win, position) {
win.style.left = `${position.left}px`;
win.style.top = `${position.top}px`;
win.style.width = `${position.width}px`;
win.style.height = `${position.height}px`;
}
export function userNotesClampPosition(position) {
const viewportWidth = Math.max(
window.innerWidth,
USER_NOTES_MIN_WIDTH + USER_NOTES_VIEWPORT_MARGIN
);
const viewportHeight = Math.max(
window.innerHeight,
USER_NOTES_MIN_HEIGHT + USER_NOTES_VIEWPORT_MARGIN
);
const maxWidth = Math.max(
USER_NOTES_MIN_WIDTH,
viewportWidth - USER_NOTES_VIEWPORT_MARGIN
);
const maxHeight = Math.max(
USER_NOTES_MIN_HEIGHT,
viewportHeight - USER_NOTES_VIEWPORT_MARGIN
);
const width = Math.max(
USER_NOTES_MIN_WIDTH,
Math.min(position.width, maxWidth)
);
const height = Math.max(
USER_NOTES_MIN_HEIGHT,
Math.min(position.height, maxHeight)
);
const maxLeft = Math.max(0, viewportWidth - width - 20);
const maxTop = Math.max(0, viewportHeight - height - 20);
const left = Math.max(0, Math.min(position.left, maxLeft));
const top = Math.max(0, Math.min(position.top, maxTop));
return {
left,
top,
width,
height
};
}
export function userNotesRestorePosition(win) {
try {
const pos = userNotesLoadPosition(null);
if (!pos) {
userNotesApplyPosition(
win,
userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
);
return;
}
const restoredPosition = {
left: Number.isFinite(pos.left) ? pos.left : USER_NOTES_DEFAULT_POSITION.left,
top: Number.isFinite(pos.top) ? pos.top : USER_NOTES_DEFAULT_POSITION.top,
width: Number.isFinite(pos.width) ? pos.width : USER_NOTES_DEFAULT_POSITION.width,
height: Number.isFinite(pos.height) ? pos.height : USER_NOTES_DEFAULT_POSITION.height
};
userNotesApplyPosition(
win,
userNotesClampPosition(restoredPosition)
);
} catch (err) {
console.warn(
`${USER_NOTES_MODULE_ID} | Could not restore note window position`,
err
);
userNotesApplyPosition(
win,
userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
);
}
}
export function userNotesSavePosition(win) {
if (!win || win.hidden) {
return;
}
const rect = win.getBoundingClientRect();
if (rect.width < USER_NOTES_MIN_WIDTH || rect.height < USER_NOTES_MIN_HEIGHT) {
return;
}
const position = userNotesClampPosition({
left: Math.round(rect.left),
top: Math.round(rect.top),
width: Math.round(rect.width),
height: Math.round(rect.height)
});
userNotesSavePositionData(position);
}
export function userNotesResetPositionAndSize() {
userNotesRemoveSavedPosition();
const win = document.getElementById(USER_NOTES_WINDOW_ID);
if (win) {
userNotesApplyPosition(
win,
userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
);
userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
}
}
export function userNotesBringToFront(win) {
const currentTop = Number.parseInt(win.style.zIndex || "100000", 10);
win.style.zIndex = String(Math.max(currentTop + 1, 100000));
}
export function userNotesMakeDraggable(win) {
const handle = win.querySelector(".user-notes-titlebar");
if (!handle) {
return;
}
let drag = null;
handle.addEventListener("pointerdown", event => {
const target = event.target;
if (target instanceof HTMLElement && target.closest("button")) {
return;
}
const rect = win.getBoundingClientRect();
drag = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
left: rect.left,
top: rect.top
};
userNotesBringToFront(win);
handle.setPointerCapture(event.pointerId);
});
handle.addEventListener("pointermove", event => {
if (!drag || drag.pointerId !== event.pointerId) {
return;
}
const rect = win.getBoundingClientRect();
const clamped = userNotesClampPosition({
left: drag.left + event.clientX - drag.startX,
top: drag.top + event.clientY - drag.startY,
width: rect.width,
height: rect.height
});
win.style.left = `${clamped.left}px`;
win.style.top = `${clamped.top}px`;
});
handle.addEventListener("pointerup", event => {
if (!drag || drag.pointerId !== event.pointerId) {
return;
}
drag = null;
userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
});
handle.addEventListener("pointercancel", event => {
if (!drag || drag.pointerId !== event.pointerId) {
return;
}
drag = null;
userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
});
}
function userNotesGetEditorValue(win) {
const editor = win.__userNotesEditor;
if (editor) {
return userNotesSanitizeHtml(editor.value);
}
const textarea = win.querySelector(".user-notes-editor");
if (textarea instanceof HTMLTextAreaElement) {
return userNotesSanitizeHtml(textarea.value);
}
return "";
}
function userNotesDestroyEditor(win) {
const editor = win.__userNotesEditor;
if (editor && typeof editor.destruct === "function") {
editor.destruct();
}
win.__userNotesEditor = null;
}
function userNotesInsertSymbol(editor, symbol) {
editor.s.insertHTML(`${symbol} `);
}
function userNotesCreateEditor(win, editorElement) {
const initialContent = userNotesNormalizeStoredNotesForEditor(userNotesLoadNotes());
if (!globalThis.Jodit) {
console.warn("User Notes | Jodit is not available. Falling back to plain textarea.");
editorElement.value = initialContent
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<\/p>/gi, "\n")
.replace(/<[^>]+>/g, "");
editorElement.addEventListener("input", event => {
userNotesDebouncedSave(event.currentTarget.value);
});
return null;
}
const editor = globalThis.Jodit.make(editorElement, {
height: "100%",
minHeight: 150,
allowResizeX: false,
allowResizeY: false,
toolbarAdaptive: false,
askBeforePasteHTML: false,
askBeforePasteFromWord: false,
defaultActionOnPaste: "insert_clear_html",
disablePlugins: [
"about",
"add-new-line",
"ai-assistant",
"file",
"image",
"media",
"paste-storage",
"powered-by-jodit",
"preview",
"print",
"source",
"speech-recognize",
"video"
],
buttons: [
"bold",
"italic",
"underline",
"strikethrough",
"|",
"ul",
"ol",
"|",
"font",
"fontsize",
"brush",
"|",
"table",
"|",
"undo",
"redo",
"|",
{
name: "checkbox-empty",
tooltip: "Kästchen einfügen",
icon: "☐",
exec: editorInstance => userNotesInsertSymbol(editorInstance, "☐")
},
{
name: "checkbox-checked",
tooltip: "Angehaktes Kästchen einfügen",
icon: "☑",
exec: editorInstance => userNotesInsertSymbol(editorInstance, "☑")
},
{
name: "checkmark",
tooltip: "Haken einfügen",
icon: "✓",
exec: editorInstance => userNotesInsertSymbol(editorInstance, "✓")
},
{
name: "crossmark",
tooltip: "Kreuz einfügen",
icon: "✗",
exec: editorInstance => userNotesInsertSymbol(editorInstance, "✗")
},
{
name: "bordered-text",
tooltip: "Rahmen um markierten Text",
icon: "▣",
exec: editorInstance => {
const selected = editorInstance.s.html || "&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() {
let win = document.getElementById(USER_NOTES_WINDOW_ID);
if (win) {
win.hidden = false;
userNotesRestorePosition(win);
userNotesApplyWindowSettings(win);
userNotesBringToFront(win);
const editor = win.__userNotesEditor;
if (editor) {
editor.focus();
if (editor.events) {
editor.events.fire("resize");
}
} else {
win.querySelector(".user-notes-editor")?.focus();
}
return;
}
win = document.createElement("section");
win.id = USER_NOTES_WINDOW_ID;
win.className = "user-notes-window";
win.innerHTML = `
<header class="user-notes-titlebar">
<div class="user-notes-title">
<i class="fas fa-note-sticky" aria-hidden="true"></i>
<span>User Notes</span>
</div>
<div class="user-notes-controls">
<span class="user-notes-status">Gespeichert</span>
<button type="button" class="user-notes-save" title="Jetzt speichern">
<i class="fas fa-save" aria-hidden="true"></i>
</button>
<button type="button" class="user-notes-close" title="Schließen">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
</header>
<main class="user-notes-content">
<textarea class="user-notes-editor" spellcheck="true"></textarea>
</main>
`;
document.body.appendChild(win);
userNotesApplyWindowSettings(win);
userNotesRestorePosition(win);
const editorElement = win.querySelector(".user-notes-editor");
if (!(editorElement instanceof HTMLTextAreaElement)) {
console.error(`${USER_NOTES_MODULE_ID} | Notes editor could not be created.`);
return;
}
userNotesCreateEditor(win, editorElement);
win.querySelector(".user-notes-save")?.addEventListener("click", () => {
userNotesSaveNotes(userNotesGetEditorValue(win));
userNotesSetStatus("Gespeichert");
userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
});
win.querySelector(".user-notes-close")?.addEventListener("click", () => {
userNotesSaveNotes(userNotesGetEditorValue(win));
userNotesSavePosition(win);
win.hidden = true;
});
win.addEventListener("pointerdown", () => {
userNotesBringToFront(win);
});
const resizeObserver = new ResizeObserver(() => {
if (win.hidden) {
return;
}
const rect = win.getBoundingClientRect();
if (rect.width < USER_NOTES_MIN_WIDTH || rect.height < USER_NOTES_MIN_HEIGHT) {
return;
}
userNotesSavePosition(win);
const editor = win.__userNotesEditor;
if (editor?.events) {
editor.events.fire("resize");
}
});
resizeObserver.observe(win);
userNotesMakeDraggable(win);
userNotesBringToFront(win);
if (win.__userNotesEditor) {
win.__userNotesEditor.focus();
if (win.__userNotesEditor.events) {
win.__userNotesEditor.events.fire("resize");
}
} else {
editorElement.focus();
}
}
export function userNotesRefreshOpenWindow(options = {}) {
const saveCurrentContent = options.saveCurrentContent ?? true;
const oldWin = document.getElementById(USER_NOTES_WINDOW_ID);
if (!oldWin) {
return;
}
if (saveCurrentContent) {
userNotesSaveNotes(userNotesGetEditorValue(oldWin));
}
userNotesSavePosition(oldWin);
userNotesDestroyEditor(oldWin);
oldWin.remove();
userNotesOpenNotes();
}

View File

@ -1,53 +1,236 @@
import { /*
userNotesRegisterSettings * User Notes for Foundry VTT v14
} from "./user-notes-settings.js"; * Stores notes in window.localStorage per world and per user.
*/
import { const LBN_MODULE_ID = "user-notes";
userNotesRegisterBackupSettings const LBN_WINDOW_ID = "lbn-notes-window";
} from "./user-notes-backup.js"; const LBN_BUTTON_ID = "lbn-open-notes";
import { let lbnSaveTimer = null;
userNotesRegisterTokenControl let lbnObserverTimer = null;
} from "./user-notes-controls.js";
import { function lbnStorageKey() {
userNotesOpenNotes, const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
userNotesRefreshOpenWindow, const userId = game.user?.id ?? "unknown-user";
userNotesResetPositionAndSize return `${LBN_MODULE_ID}.${worldId}.${userId}.notes`;
} from "./user-notes-window.js"; }
globalThis.UserNotes = { function lbnPositionKey() {
open: userNotesOpenNotes, const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
refresh: userNotesRefreshOpenWindow, const userId = game.user?.id ?? "unknown-user";
resetPosition: userNotesResetPositionAndSize return `${LBN_MODULE_ID}.${worldId}.${userId}.position`;
}; }
Hooks.once("init", () => { function lbnLoadNotes() {
return window.localStorage.getItem(lbnStorageKey()) ?? "";
}
function lbnSaveNotes(value) {
window.localStorage.setItem(lbnStorageKey(), value);
}
function lbnSetStatus(text) {
const status = document.querySelector(`#${LBN_WINDOW_ID} .lbn-status`);
if (status) status.textContent = text;
}
function lbnDebouncedSave(value) {
window.clearTimeout(lbnSaveTimer);
lbnSetStatus("Ungespeichert …");
lbnSaveTimer = window.setTimeout(() => {
lbnSaveNotes(value);
lbnSetStatus("Gespeichert");
}, 250);
}
function lbnRestorePosition(win) {
try { try {
console.log("User Notes | application registered"); const raw = window.localStorage.getItem(lbnPositionKey());
if (!raw) return;
userNotesRegisterSettings(userNotesResetPositionAndSize); const pos = JSON.parse(raw);
console.log("User Notes | settings registered"); if (Number.isFinite(pos.left)) win.style.left = `${pos.left}px`;
if (Number.isFinite(pos.top)) win.style.top = `${pos.top}px`;
userNotesRegisterBackupSettings(); if (Number.isFinite(pos.width)) win.style.width = `${pos.width}px`;
console.log("User Notes | export/import settings registered"); if (Number.isFinite(pos.height)) win.style.height = `${pos.height}px`;
} catch (err) { } catch (err) {
console.error("User Notes | error during init", err); console.warn(`${LBN_MODULE_ID} | Could not restore note window position`, err);
}
}
ui.notifications?.error( function lbnSavePosition(win) {
"User Notes: Fehler beim Initialisieren. Details stehen in der Browser-Konsole." const rect = win.getBoundingClientRect();
); window.localStorage.setItem(lbnPositionKey(), JSON.stringify({
left: Math.round(rect.left),
top: Math.round(rect.top),
width: Math.round(rect.width),
height: Math.round(rect.height)
}));
}
function lbnMakeDraggable(win) {
const handle = win.querySelector(".lbn-titlebar");
if (!handle) return;
let drag = null;
handle.addEventListener("pointerdown", event => {
if (event.target.closest("button")) return;
const rect = win.getBoundingClientRect();
drag = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
left: rect.left,
top: rect.top
};
handle.setPointerCapture(event.pointerId);
event.preventDefault();
});
handle.addEventListener("pointermove", event => {
if (!drag || drag.pointerId !== event.pointerId) return;
const nextLeft = Math.max(0, Math.min(window.innerWidth - 80, drag.left + event.clientX - drag.startX));
const nextTop = Math.max(0, Math.min(window.innerHeight - 40, drag.top + event.clientY - drag.startY));
win.style.left = `${nextLeft}px`;
win.style.top = `${nextTop}px`;
});
handle.addEventListener("pointerup", event => {
if (!drag || drag.pointerId !== event.pointerId) return;
drag = null;
lbnSavePosition(win);
});
}
function lbnOpenNotes() {
let win = document.getElementById(LBN_WINDOW_ID);
if (win) {
win.hidden = false;
win.classList.add("active");
win.querySelector("textarea")?.focus();
return;
}
win = document.createElement("section");
win.id = LBN_WINDOW_ID;
win.className = "lbn-notes-window";
win.innerHTML = `
<header class="lbn-titlebar">
<div class="lbn-title">
<i class="fas fa-note-sticky" aria-hidden="true"></i>
<span>User Notes</span>
</div>
<div class="lbn-controls">
<span class="lbn-status">Gespeichert</span>
<button type="button" class="lbn-save" title="Jetzt speichern">
<i class="fas fa-save" aria-hidden="true"></i>
</button>
<button type="button" class="lbn-close" title="Schließen">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
</header>
<textarea class="lbn-textarea" spellcheck="true" placeholder="Notizen für diese Welt und diesen Benutzer …"></textarea>
`;
document.body.appendChild(win);
const textarea = win.querySelector(".lbn-textarea");
textarea.value = lbnLoadNotes();
textarea.addEventListener("input", event => {
lbnDebouncedSave(event.currentTarget.value);
});
win.querySelector(".lbn-save").addEventListener("click", () => {
lbnSaveNotes(textarea.value);
lbnSetStatus("Gespeichert");
});
win.querySelector(".lbn-close").addEventListener("click", () => {
lbnSaveNotes(textarea.value);
lbnSavePosition(win);
win.hidden = true;
});
new ResizeObserver(() => lbnSavePosition(win)).observe(win);
lbnRestorePosition(win);
lbnMakeDraggable(win);
textarea.focus();
}
function lbnAsHTMLElement(html) {
if (html instanceof HTMLElement) return html;
if (html?.[0] instanceof HTMLElement) return html[0]; // legacy jQuery-style hook argument
if (html?.element instanceof HTMLElement) return html.element;
return null;
}
function lbnFindPlayersElement(renderedElement = null) {
return renderedElement?.id === "players"
? renderedElement
: renderedElement?.querySelector?.("#players")
?? document.querySelector("#players")
?? document.querySelector("[data-application-id='players']")
?? document.querySelector(".players");
}
function lbnInjectButton(renderedElement = null) {
const players = lbnFindPlayersElement(renderedElement);
if (!players || players.querySelector(`#${LBN_BUTTON_ID}`)) return;
const header =
players.querySelector(".window-header")
?? players.querySelector("header")
?? players.querySelector("h3")
?? players.querySelector("h2")
?? players;
const button = document.createElement("button");
button.id = LBN_BUTTON_ID;
button.type = "button";
button.className = "lbn-player-note-button";
button.title = "User Notes öffnen";
button.setAttribute("aria-label", "User Notes öffnen");
button.innerHTML = `<i class="fas fa-note-sticky" aria-hidden="true"></i>`;
button.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
lbnOpenNotes();
});
header.appendChild(button);
}
Hooks.once("ready", () => {
lbnInjectButton();
// Fallback: Die Spieler-/Benutzerliste kann bei Theme-, Layout- oder Popout-Änderungen neu gerendert werden.
const observer = new MutationObserver(() => {
window.clearTimeout(lbnObserverTimer);
lbnObserverTimer = window.setTimeout(() => lbnInjectButton(), 100);
});
observer.observe(document.body, { childList: true, subtree: true });
});
Hooks.on("renderApplicationV2", (app, html) => {
const element = lbnAsHTMLElement(html) ?? app?.element ?? null;
if (app?.constructor?.name === "Players" || lbnFindPlayersElement(element)) {
window.queueMicrotask(() => lbnInjectButton(element));
} }
}); });
Hooks.on("getSceneControlButtons", controls => { // Kompatibilitäts-Fallback für Installationen/Module, die noch einen spezifischen PlayerList-Hook auslösen.
try { Hooks.on("renderPlayerList", (_app, html) => {
userNotesRegisterTokenControl(controls); const element = lbnAsHTMLElement(html);
} catch (err) { window.queueMicrotask(() => lbnInjectButton(element));
console.error("User Notes | error while registering token control", err);
ui.notifications?.error(
"User Notes: Fehler beim Registrieren des Token-Controls."
);
}
}); });

View File

@ -1,162 +0,0 @@
.user-notes-appearance-settings,
.user-notes-backup-settings {
padding: 0.75rem;
}
.user-notes-appearance-settings .notes,
.user-notes-backup-settings .notes {
margin-top: 0;
margin-bottom: 0.75rem;
}
.user-notes-backup-settings .warning {
padding: 0.5rem;
border: 1px solid var(--color-border-highlight, #ff6400);
border-radius: 4px;
background: rgba(255, 100, 0, 0.12);
}
.user-notes-appearance-settings fieldset {
margin: 0 0 0.75rem 0;
padding: 0.75rem;
border: 1px solid var(--color-border-light-primary, #7a695a);
border-radius: 4px;
}
.user-notes-appearance-settings legend {
padding: 0 0.35rem;
font-weight: bold;
}
.user-notes-appearance-settings .form-group,
.user-notes-backup-settings .form-group {
align-items: center;
}
.user-notes-appearance-settings .form-fields,
.user-notes-backup-settings .form-fields {
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-notes-appearance-settings input[type="color"] {
width: 3.5rem;
min-width: 3.5rem;
height: 2rem;
padding: 0;
border: 1px solid var(--color-border-light-primary, #7a695a);
border-radius: 4px;
background: transparent;
cursor: pointer;
}
.user-notes-appearance-settings input[type="text"] {
flex: 1 1 auto;
min-width: 0;
}
.user-notes-range-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-notes-range-row input[type="range"] {
flex: 1 1 auto;
}
.user-notes-range-row output {
width: 3rem;
text-align: right;
white-space: nowrap;
}
/*
* Dialog footer:
* Row 1: Apply + Save side-by-side.
* Row 2: Reset colors full width.
*/
.user-notes-appearance-settings .sheet-footer,
.user-notes-appearance-footer {
margin-top: 0.75rem;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.user-notes-primary-actions {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 0.5rem;
width: 100%;
}
.user-notes-primary-actions button,
.user-notes-appearance-footer > .user-notes-reset-appearance {
height: 2rem;
min-height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
box-sizing: border-box;
}
.user-notes-primary-actions button {
flex: 1 1 0;
}
.user-notes-appearance-footer > .user-notes-reset-appearance {
flex: 0 0 auto;
width: 100%;
}
.user-notes-backup-footer {
margin-top: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-notes-backup-footer button {
flex: 1 1 auto;
height: 2rem;
min-height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
box-sizing: border-box;
}
.user-notes-checkbox-row {
display: grid;
grid-template-columns: 1fr 2rem;
align-items: center;
column-gap: 0.75rem;
margin: 0 0 0.45rem 0;
}
.user-notes-checkbox-row label {
margin: 0;
line-height: 1.3;
}
.user-notes-checkbox-row input[type="checkbox"] {
justify-self: center;
margin: 0;
}

View File

@ -1,302 +0,0 @@
.user-notes-window {
position: fixed;
z-index: 100000;
left: 200px;
top: 150px;
width: 500px;
height: 400px;
min-width: 280px;
min-height: 180px;
max-width: calc(100vw - 40px);
max-height: calc(100vh - 40px);
display: flex;
flex-direction: column;
resize: both;
overflow: hidden;
box-sizing: border-box;
border: 1px solid var(--color-border-dark, #000);
border-radius: 6px;
background: var(--user-notes-window-background, rgba(25, 24, 19, 0.96));
color: var(--user-notes-window-text-color, #f0f0e0);
box-shadow: 0 0 16px rgba(0, 0, 0, 0.55);
}
.user-notes-window[hidden] {
display: none;
}
.user-notes-titlebar {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.45rem 0.55rem;
background: rgba(0, 0, 0, 0.35);
border-bottom: 1px solid var(--color-border-dark, #000);
cursor: move;
user-select: none;
}
.user-notes-title,
.user-notes-controls {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.user-notes-title {
min-width: 0;
font-weight: bold;
}
.user-notes-status {
opacity: 0.75;
font-size: 0.78rem;
white-space: nowrap;
}
.user-notes-controls button {
width: 1.55rem;
height: 1.55rem;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--color-border-light-primary, #7a695a);
border-radius: 4px;
background: rgba(0, 0, 0, 0.18);
color: var(--user-notes-window-text-color, #f0f0e0);
cursor: pointer;
}
.user-notes-controls button:hover {
background: rgba(255, 255, 255, 0.16);
}
/*
* Fixed inner padding:
* The editor is pinned to the window content area and grows/shrinks with it.
*/
.user-notes-content {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
display: flex;
padding: 0.5rem;
box-sizing: border-box;
overflow: hidden;
}
/*
* Original textarea fallback.
* Jodit replaces/wraps this element when available.
*/
.user-notes-editor {
flex: 1 1 auto;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
resize: none;
box-sizing: border-box;
padding: 0.55rem;
border: 1px solid var(--color-border-light-primary, #7a695a);
border-radius: 4px;
background: var(--user-notes-textarea-background, rgba(255, 255, 255, 0.92));
color: var(--user-notes-textarea-color, #111111);
font-family: inherit;
font-size: 0.95rem;
line-height: 1.35;
}
.user-notes-editor:focus {
outline: 1px solid var(--color-border-highlight, #ff6400);
outline-offset: 0;
}
/*
* Jodit layout.
* These rules force Jodit to fill the padded content area and inherit
* the configured User Notes colors.
*/
.user-notes-content .jodit-container,
.user-notes-content .jodit {
flex: 1 1 auto;
width: 100% !important;
height: 100% !important;
min-width: 0 !important;
min-height: 0 !important;
display: flex !important;
flex-direction: column !important;
box-sizing: border-box;
overflow: hidden;
background: var(--user-notes-textarea-background, rgba(255, 255, 255, 0.92)) !important;
color: var(--user-notes-textarea-color, #111111) !important;
}
/*
* Toolbar remains at the top and uses the window colors.
*/
.user-notes-content .jodit-toolbar__box,
.user-notes-content .jodit-toolbar__box:not(:empty) {
flex: 0 0 auto;
opacity: 1 !important;
background: var(--user-notes-window-background-solid, #191813) !important;
color: var(--user-notes-window-text-color, #f0f0e0) !important;
border-color: var(--color-border-light-primary, #7a695a) !important;
}
.user-notes-content .jodit-toolbar-button,
.user-notes-content .jodit-toolbar-button__button,
.user-notes-content .jodit-toolbar-button__trigger,
.user-notes-content .jodit-toolbar-button__text,
.user-notes-content .jodit-icon {
opacity: 1 !important;
color: var(--user-notes-window-text-color, #f0f0e0) !important;
fill: var(--user-notes-window-text-color, #f0f0e0) !important;
}
.user-notes-content .jodit-toolbar-button__button,
.user-notes-content .jodit-toolbar-button__trigger {
background: transparent !important;
}
.user-notes-content .jodit-toolbar-button__button:hover,
.user-notes-content .jodit-toolbar-button__trigger:hover {
background: rgba(255, 255, 255, 0.16) !important;
}
/*
* Toolbar buttons.
*/
.user-notes-content .jodit-toolbar-button,
.user-notes-content .jodit-toolbar-button__button,
.user-notes-content .jodit-toolbar-button__trigger {
color: var(--user-notes-window-text-color, #f0f0e0) !important;
}
.user-notes-content .jodit-toolbar-button__button:hover,
.user-notes-content .jodit-toolbar-button__trigger:hover {
background: rgba(255, 255, 255, 0.16) !important;
}
/*
* Dropdown / popup content should remain readable.
*/
.user-notes-content .jodit-popup,
.user-notes-content .jodit-dialog,
.user-notes-content .jodit-ui-group,
.user-notes-content .jodit-toolbar-editor-collection {
color: #111111;
}
/*
* Main editor workplace fills all remaining height below the toolbar.
*/
.user-notes-content .jodit-workplace {
flex: 1 1 auto !important;
width: 100% !important;
height: auto !important;
min-width: 0 !important;
min-height: 0 !important;
display: flex !important;
flex-direction: column !important;
overflow: hidden !important;
background: var(--user-notes-textarea-background, rgba(255, 255, 255, 0.92)) !important;
color: var(--user-notes-textarea-color, #111111) !important;
}
/*
* Actual editable area.
*/
.user-notes-content .jodit-wysiwyg {
flex: 1 1 auto !important;
width: 100% !important;
height: 100% !important;
min-width: 0 !important;
min-height: 0 !important;
box-sizing: border-box;
overflow: auto !important;
background: var(--user-notes-textarea-background, rgba(255, 255, 255, 0.92)) !important;
color: var(--user-notes-textarea-color, #111111) !important;
font-family: inherit;
font-size: 0.95rem;
line-height: 1.35;
}
/*
* Do not let Jodit reset child text colors away from the configured editor color.
* Inline styles generated by the editor may still override this when users
* explicitly choose colors inside the editor.
*/
.user-notes-content .jodit-wysiwyg * {
color: inherit;
}
/*
* Hide Jodit's status bar to preserve vertical space.
*/
.user-notes-content .jodit-status-bar {
display: none !important;
}
/*
* Tables inside notes.
*/
.user-notes-window table {
border-collapse: collapse;
width: auto;
}
.user-notes-window th,
.user-notes-window td {
border: 1px solid currentColor;
padding: 0.25rem 0.4rem;
}

100
styles/user-notes.css Normal file
View File

@ -0,0 +1,100 @@
.lbn-player-note-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.65rem;
height: 1.65rem;
margin-left: 0.35rem;
padding: 0;
border: 1px solid var(--color-border-light-primary, #7a695a);
border-radius: 4px;
background: rgba(0, 0, 0, 0.18);
color: var(--color-text-light-highlight, #f0f0e0);
cursor: pointer;
}
.lbn-player-note-button:hover {
background: rgba(255, 255, 255, 0.12);
}
.lbn-notes-window {
position: fixed;
z-index: 100000;
left: 140px;
top: 120px;
width: 420px;
height: 330px;
min-width: 260px;
min-height: 180px;
display: flex;
flex-direction: column;
resize: both;
overflow: hidden;
border: 1px solid var(--color-border-dark, #000);
border-radius: 6px;
background: var(--color-bg, #1f1f1f);
box-shadow: 0 0 16px rgba(0, 0, 0, 0.55);
}
.lbn-notes-window[hidden] {
display: none;
}
.lbn-titlebar {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
min-height: 2rem;
padding: 0.25rem 0.4rem;
background: rgba(0, 0, 0, 0.34);
color: var(--color-text-light-highlight, #f0f0e0);
cursor: move;
user-select: none;
}
.lbn-title,
.lbn-controls {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.lbn-status {
opacity: 0.75;
font-size: 0.78rem;
white-space: nowrap;
}
.lbn-controls button {
width: 1.55rem;
height: 1.55rem;
padding: 0;
border: 1px solid var(--color-border-light-primary, #7a695a);
border-radius: 4px;
background: rgba(255, 255, 255, 0.08);
color: inherit;
cursor: pointer;
}
.lbn-controls button:hover {
background: rgba(255, 255, 255, 0.16);
}
.lbn-textarea {
flex: 1 1 auto;
width: 100%;
min-height: 0;
box-sizing: border-box;
resize: none;
border: 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding: 0.65rem;
font-family: var(--font-primary, sans-serif);
font-size: 0.95rem;
line-height: 1.4;
color: var(--color-text-light-highlight, #f0f0e0);
background: rgba(255, 255, 255, 0.045);
outline: none;
}

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