improve notepad features and add drawing canvas
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
e73d754082
commit
de1b8a5638
20 changed files with 1732 additions and 141 deletions
|
|
@ -1,6 +1,18 @@
|
|||
// Rôle : normalise, compacte et sérialise les données toolbox persistées.
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z", task: "a", relation: "e" };
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z", task: "a", relation: "e", stroke: "d" };
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const NOTEPAD_ALLOWED_COLORS = new Set(["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#101426", "#202745"]);
|
||||
const NOTEPAD_ALLOWED_TAGS = new Set(["p", "div", "br", "strong", "b", "em", "i", "u", "ul", "ol", "li", "h3", "h4", "span", "mark"]);
|
||||
const NOTEPAD_RGB_COLOR_MAP = {
|
||||
"rgb(245, 247, 255)": "#f5f7ff",
|
||||
"rgb(180, 189, 211)": "#b4bdd3",
|
||||
"rgb(246, 196, 83)": "#f6c453",
|
||||
"rgb(34, 211, 238)": "#22d3ee",
|
||||
"rgb(139, 92, 246)": "#8b5cf6",
|
||||
"rgb(217, 70, 239)": "#d946ef",
|
||||
"rgb(16, 20, 38)": "#101426",
|
||||
"rgb(32, 39, 69)": "#202745"
|
||||
};
|
||||
const TIMER_TABS = new Set(["stopwatch", "countdown"]);
|
||||
const COUNTDOWN_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
|
||||
const TIMER_ALERT_MODES = new Set(["off", "visible", "site"]);
|
||||
|
|
@ -119,6 +131,136 @@ function parsePositiveInt(value, fallback = 1) {
|
|||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function decodeBasicEntities(value) {
|
||||
return String(value || "")
|
||||
.replace(/ /gi, " ")
|
||||
.replace(/</gi, "<")
|
||||
.replace(/>/gi, ">")
|
||||
.replace(/"/gi, "\"")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/gi, "&");
|
||||
}
|
||||
|
||||
function normalizeStyleAttribute(value) {
|
||||
const declarations = String(value || "")
|
||||
.split(";")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
const allowed = [];
|
||||
declarations.forEach((declaration) => {
|
||||
const [property, rawValue] = declaration.split(":").map((item) => item?.trim().toLowerCase());
|
||||
if (!["color", "background-color"].includes(property)) return;
|
||||
const color = NOTEPAD_RGB_COLOR_MAP[rawValue] || rawValue;
|
||||
if (!NOTEPAD_ALLOWED_COLORS.has(color)) return;
|
||||
allowed.push(`${property}: ${color}`);
|
||||
});
|
||||
return allowed.join("; ");
|
||||
}
|
||||
|
||||
export function sanitizeNotepadHtml(value) {
|
||||
const source = String(value || "")
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, "");
|
||||
let html = "";
|
||||
let cursor = 0;
|
||||
source.replace(/<\/?([a-z0-9]+)([^>]*)>/gi, (match, rawTag, rawAttributes, offset) => {
|
||||
html += escapeHtml(source.slice(cursor, offset));
|
||||
cursor = offset + match.length;
|
||||
const tag = rawTag.toLowerCase();
|
||||
if (!NOTEPAD_ALLOWED_TAGS.has(tag)) return "";
|
||||
if (match.startsWith("</")) {
|
||||
if (tag !== "br") html += `</${tag}>`;
|
||||
return "";
|
||||
}
|
||||
if (tag === "br") {
|
||||
html += "<br>";
|
||||
return "";
|
||||
}
|
||||
const styleMatch = String(rawAttributes || "").match(/\sstyle=(?:"([^"]*)"|'([^']*)')/i);
|
||||
const style = normalizeStyleAttribute(styleMatch?.[1] || styleMatch?.[2] || "");
|
||||
html += style ? `<${tag} style="${style}">` : `<${tag}>`;
|
||||
return "";
|
||||
});
|
||||
html += escapeHtml(source.slice(cursor));
|
||||
return html.trim();
|
||||
}
|
||||
|
||||
function textToNotepadHtml(value) {
|
||||
return String(value || "")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line ? `<p>${escapeHtml(line)}</p>` : "<p><br></p>")
|
||||
.join("");
|
||||
}
|
||||
|
||||
function notepadHtmlToText(value) {
|
||||
const withBreaks = String(value || "")
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/<\/(p|div|h3|h4|li)>/gi, "\n")
|
||||
.replace(/<li[^>]*>/gi, "- ");
|
||||
return decodeBasicEntities(withBreaks.replace(/<[^>]*>/g, ""))
|
||||
.split("\n")
|
||||
.map((line) => line.trimEnd())
|
||||
.join("\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizeIsoDate(value) {
|
||||
const text = String(value || "").trim();
|
||||
if (!text) return "";
|
||||
const timestamp = Date.parse(text);
|
||||
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : "";
|
||||
}
|
||||
|
||||
function normalizeDrawingPoint(point, options = {}) {
|
||||
const x = Number(point?.x);
|
||||
const y = Number(point?.y);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
||||
const max = Number(options.max);
|
||||
return {
|
||||
x: Math.min(Number.isFinite(max) ? max : Number.POSITIVE_INFINITY, Math.max(0, Math.round(x * 100) / 100)),
|
||||
y: Math.min(Number.isFinite(max) ? max : Number.POSITIVE_INFINITY, Math.max(0, Math.round(y * 100) / 100))
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDrawingStroke(stroke, options = {}) {
|
||||
const points = (Array.isArray(stroke?.points) ? stroke.points : []).map((point) => normalizeDrawingPoint(point, options)).filter(Boolean);
|
||||
if (points.length < 2) return null;
|
||||
const color = NOTEPAD_ALLOWED_COLORS.has(String(stroke?.color || "").toLowerCase()) ? String(stroke.color).toLowerCase() : "#f6c453";
|
||||
const width = Math.min(24, Math.max(1, Number(stroke?.width) || 4));
|
||||
return {
|
||||
id: stroke?.id || uid("stroke"),
|
||||
color,
|
||||
width: Math.round(width * 10) / 10,
|
||||
points
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeDrawingData(data, options = {}) {
|
||||
return {
|
||||
strokes: (Array.isArray(data?.strokes) ? data.strokes : []).map((stroke) => normalizeDrawingStroke(stroke, options)).filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeNotepadData(data) {
|
||||
const source = data && typeof data === "object" ? data : {};
|
||||
const legacyText = String(source.text || "");
|
||||
const html = sanitizeNotepadHtml(source.html ? source.html : textToNotepadHtml(legacyText));
|
||||
const text = notepadHtmlToText(html) || legacyText.trim();
|
||||
const updatedAt = normalizeIsoDate(source.updatedAt);
|
||||
const drawingMode = source.drawingMode === "permanent" ? "permanent" : "temporary";
|
||||
const drawings = normalizeDrawingData(source.drawings);
|
||||
return { html, text, updatedAt, drawingMode, drawings };
|
||||
}
|
||||
|
||||
export function clampQty(value, target) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
const safeValue = Number.isFinite(parsed) ? parsed : 0;
|
||||
|
|
@ -413,7 +555,7 @@ export function normalizeImageAnnotationData(data) {
|
|||
y: clampPercent(marker?.y),
|
||||
label: String(marker?.label || "").trim()
|
||||
}));
|
||||
return { image, markers };
|
||||
return { image, markers, drawings: normalizeDrawingData(data?.drawings, { max: 100 }) };
|
||||
}
|
||||
|
||||
function compactChecklistItemForStorage(item) {
|
||||
|
|
@ -438,8 +580,14 @@ function compactChecklistSectionForStorage(section) {
|
|||
|
||||
export function compactModuleDataForStorage(type, value) {
|
||||
if (type === "notepad") {
|
||||
const text = String(value?.text || "");
|
||||
return text ? { text } : null;
|
||||
const normalized = normalizeNotepadData(value);
|
||||
const compact = {};
|
||||
if (normalized.html && normalized.text) compact.html = normalized.html;
|
||||
if (normalized.text) compact.text = normalized.text;
|
||||
if (normalized.updatedAt) compact.updatedAt = normalized.updatedAt;
|
||||
if (normalized.drawingMode === "permanent") compact.drawingMode = "permanent";
|
||||
if (normalized.drawingMode === "permanent" && normalized.drawings.strokes.length) compact.drawings = normalized.drawings;
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
}
|
||||
if (type === "checklist") {
|
||||
const normalized = normalizeChecklistData(value);
|
||||
|
|
@ -465,7 +613,7 @@ export function compactModuleDataForStorage(type, value) {
|
|||
if (type === "imageAnnotation") {
|
||||
const normalized = normalizeImageAnnotationData(value);
|
||||
if (!normalized.image) return null;
|
||||
return {
|
||||
const compact = {
|
||||
image: normalized.image,
|
||||
markers: normalized.markers.map((marker) => {
|
||||
const compact = { id: marker.id, x: marker.x, y: marker.y };
|
||||
|
|
@ -473,6 +621,8 @@ export function compactModuleDataForStorage(type, value) {
|
|||
return compact;
|
||||
})
|
||||
};
|
||||
if (normalized.drawings.strokes.length) compact.drawings = normalized.drawings;
|
||||
return compact;
|
||||
}
|
||||
if (type === "links") {
|
||||
const links = normalizeLinksData(value).links.map((link) => {
|
||||
|
|
@ -604,10 +754,16 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
}
|
||||
|
||||
if (type === "imageAnnotation") {
|
||||
return {
|
||||
const remapped = {
|
||||
...compact,
|
||||
markers: compact.markers.map((marker) => ({ ...marker, id: nextId("marker") }))
|
||||
};
|
||||
if (compact.drawings?.strokes) {
|
||||
remapped.drawings = {
|
||||
strokes: compact.drawings.strokes.map((stroke) => ({ ...stroke, id: nextId("stroke") }))
|
||||
};
|
||||
}
|
||||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "links") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue