Add notepad text import export
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s

This commit is contained in:
Shinuwa 2026-08-14 08:49:26 +02:00
parent 323e1b266d
commit f76cddcc95
8 changed files with 165 additions and 19 deletions

View file

@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
import { DrawingControls, FormattingColorDropdown } from "./DrawingControls.jsx";
import { DrawingOverlay } from "./DrawingOverlay.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
const TEXT_COLORS = ["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef"];
const HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"];
@ -107,7 +108,7 @@ function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
);
}
export function NotepadModule({ toolboxId, moduleId, context }) {
export function NotepadModule({ toolboxId, moduleId, context, editing = false }) {
const storedData = context.getModuleData(toolboxId, moduleId, { text: "" });
const normalizedData = useMemo(() => context.normalizeNotepadData(storedData), [context, storedData]);
const textContent = context.moduleText?.notepad || {};
@ -142,7 +143,7 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
setDrawingMode(normalizedData.drawingMode);
setDrawingActive(false);
setEraseActive(false);
}, [toolboxId, moduleId]);
}, [toolboxId, moduleId, normalizedData.html, normalizedData.text, normalizedData.updatedAt, normalizedData.drawingMode]);
useEffect(() => {
if (!editorRef.current || editorRef.current.innerHTML === html) return;
@ -302,6 +303,11 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
return (
<div className="notepad-module">
{editing && (
<div className="module-add-panel notepad-exchange-panel">
<TextExchangeActions type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
</div>
)}
<div className="notepad-toolbar" aria-label={textContent.toolbarLabel || "Mise en page du bloc notes"}>
<div className="notepad-format-controls">
<div className="notepad-toolbar-group" role="group" aria-label={textContent.textFormatLabel || "Style du texte"}>

View file

@ -20,7 +20,7 @@ import { TaskPlannerModule } from "./TaskPlannerModule.jsx";
import { TimerModule } from "./TimerModule.jsx";
const MODULE_COMPONENTS = {
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: false },
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: true },
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true, scrollable: true },
images: { label: "Images", icon: "picture", Component: ImagesModule, editable: true, scrollable: true },
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true, scrollable: true },

View file

@ -19,6 +19,7 @@ export function parseColonImportLines(text) {
}
const SUPPORTED_TEXT_EXCHANGE_TYPES = new Set([
"notepad",
"checklist",
"links",
"counters",
@ -42,6 +43,7 @@ export function supportsTextExchange(type) {
}
export function hasTextExchangeContent(type, data) {
if (type === "notepad") return Boolean(data?.text || data?.html || data?.drawings?.strokes?.length);
if (type === "checklist") return Boolean(data?.sections?.some((section) => section.items?.length));
if (type === "links") return Boolean(data?.links?.length);
if (type === "counters") return Boolean(data?.counters?.length);
@ -57,6 +59,7 @@ export function hasTextExchangeContent(type, data) {
}
export function exportModuleText(type, data) {
if (type === "notepad") return exportNotepadText(data);
if (type === "checklist") return exportChecklistText(data);
if (type === "links") return exportLinksText(data);
if (type === "counters") return exportCountersText(data);
@ -76,7 +79,8 @@ export function importModuleText(type, text, context) {
const value = String(text || "");
if (!value.trim()) return fail("Le texte à importer est vide.");
let data = null;
if (type === "checklist") data = importChecklistText(value, context);
if (type === "notepad") data = importNotepadText(value, context);
else if (type === "checklist") data = importChecklistText(value, context);
else if (type === "links") data = importLinksText(value, context);
else if (type === "counters") data = importCountersText(value, context);
else if (type === "combos") data = importCombosText(value, context);
@ -157,6 +161,97 @@ function cloneDrawingData(drawings) {
};
}
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
function textToNotepadExchangeHtml(value) {
return String(value || "")
.split(/\r?\n/)
.map((line) => line ? `<p>${escapeHtml(line)}</p>` : "<p><br></p>")
.join("");
}
function exportNotepadText(data) {
const html = String(data?.html || "").trim() || textToNotepadExchangeHtml(data?.text || "");
const lines = ["@notepad", "@html", html, "@endhtml"];
if (data?.drawingMode === "permanent") lines.push("@drawingMode: permanent");
if (data?.drawings?.strokes?.length) lines.push(`@drawings: ${JSON.stringify(cloneDrawingData(data.drawings))}`);
return lines.join("\n");
}
function cloneImportedNotepadDrawings(drawings, context) {
if (!drawings || typeof drawings !== "object" || !Array.isArray(drawings.strokes)) throw new Error("dessins invalides.");
return {
strokes: drawings.strokes.map((stroke) => {
if (!stroke || typeof stroke !== "object" || !Array.isArray(stroke.points) || stroke.points.length < 2) throw new Error("trait de dessin invalide.");
return {
...stroke,
id: context.uid("stroke"),
points: stroke.points.map((point) => {
const x = Number(point?.x);
const y = Number(point?.y);
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("point de dessin invalide.");
return { x, y };
})
};
})
};
}
function importNotepadText(text, context) {
const lines = splitLines(text);
const hasStructuredMarker = lines.some((line) => ["@notepad", "@html", "@endhtml"].includes(line.trim()) || parseMetaLine(line));
if (!hasStructuredMarker) return context.normalizeNotepadData({ text });
let readingHtml = false;
let htmlWasOpened = false;
let htmlWasClosed = false;
const htmlLines = [];
let drawingMode = "temporary";
let drawings = { strokes: [] };
lines.forEach((line, index) => {
const cleanLine = line.trim();
if (readingHtml) {
if (cleanLine === "@endhtml") {
readingHtml = false;
htmlWasClosed = true;
return;
}
htmlLines.push(line);
return;
}
if (!cleanLine || cleanLine === "@notepad") return;
if (cleanLine === "@html") {
if (htmlWasOpened) throw lineError(index, "bloc html dupliqué.");
readingHtml = true;
htmlWasOpened = true;
return;
}
const meta = parseMetaLine(cleanLine);
if (meta?.key === "drawingMode") {
if (!["temporary", "permanent"].includes(meta.value)) throw lineError(index, "mode de dessin invalide.");
drawingMode = meta.value;
return;
}
if (meta?.key === "drawings") {
try {
drawings = cloneImportedNotepadDrawings(JSON.parse(meta.value), context);
} catch (error) {
throw lineError(index, error?.message || "dessins invalides.");
}
if (drawings.strokes.length) drawingMode = "permanent";
return;
}
throw lineError(index, `ligne bloc notes invalide: ${cleanLine}`);
});
if (readingHtml || (htmlWasOpened && !htmlWasClosed)) throw new Error("Bloc HTML non terminé.");
return context.normalizeNotepadData({ html: htmlLines.join("\n").trim(), drawingMode, drawings });
}
function exportChecklistText(data) {
return (data.sections || []).flatMap((section) => [
section.title ? `# ${section.title}` : "",