From c2e37ad3f23dee43efbc3fb4c0723d08bf30c65d Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Mon, 24 Aug 2026 23:31:40 +0200 Subject: [PATCH] Refactor notepad toolbar helpers --- tests/static-toolboxes.test.mjs | 7 + tests/toolbox-modules.test.mjs | 11 + .../toolboxes/modules/NotepadModule.jsx | 199 +++--------------- .../toolboxes/modules/NotepadToolbar.jsx | 115 ++++++++++ .../toolboxes/modules/notepadUtils.js | 75 +++++++ 5 files changed, 234 insertions(+), 173 deletions(-) create mode 100644 website/src/features/toolboxes/modules/NotepadToolbar.jsx create mode 100644 website/src/features/toolboxes/modules/notepadUtils.js diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index d4f374b..ffe4445 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -139,6 +139,8 @@ test("toolbox module registry and modules expose expected behavior", async () => const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8"); const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8"); const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8"); + const notepadToolbar = await readFile("website/src/features/toolboxes/modules/NotepadToolbar.jsx", "utf8"); + const notepadUtils = await readFile("website/src/features/toolboxes/modules/notepadUtils.js", "utf8"); const combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.jsx", "utf8"); const combosEditor = await readFile("website/src/features/toolboxes/modules/CombosEditor.jsx", "utf8"); const combosParts = await readFile("website/src/features/toolboxes/modules/CombosParts.jsx", "utf8"); @@ -252,6 +254,11 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(notepadModule, /export function NotepadModule/); assert.match(notepadModule, /TextExchangeActions/); assert.match(notepadModule, /type="notepad"/); + assert.match(notepadToolbar, /export function NotepadToolbar/); + assert.match(notepadToolbar, /FormattingColorDropdown/); + assert.match(notepadToolbar, /ListTypeDropdown/); + assert.match(notepadUtils, /export function getPlainText/); + assert.match(notepadUtils, /export function restoreSelectionTextOffset/); assert.match(combosModule, /export function CombosModule/); assert.match(combosUtils, /keyboardMouse/); assert.match(combosUtils, /Nintendo 64/); diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index 0706068..d9d1bc1 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -10,6 +10,7 @@ import { calculateExpression, createStoredFormula, evaluateCalculatorEntries, ge import { createDefaultDraft, getDateSpanDays, getDraftSelection, normalizeDraftAlertOffsets, normalizeDraftMonthlyRepeatBy } from "../website/src/features/toolboxes/modules/calendarDraft.js"; import { getMonthOccurrenceSegments, getWeekGridLayout, getWeekMinutePosition, layoutConcurrentOccurrences } from "../website/src/features/toolboxes/modules/calendarLayout.js"; import { appendChecklistItems, moveChecklistItem, moveChecklistItemToCategory, renameChecklistSection } from "../website/src/features/toolboxes/modules/checklistUtils.js"; +import { formatUpdatedAt, isHtmlEmpty, queryBlockFormat, queryCommandState } from "../website/src/features/toolboxes/modules/notepadUtils.js"; import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js"; import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js"; import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js"; @@ -125,6 +126,16 @@ test("text exchange round trips simple toolbox modules and resets checklist prog assert.deepEqual(countersImport.data.counters.map((counter) => [counter.label, counter.value]), [["Win", 12], ["Loss", -2]]); }); +test("notepad helpers keep empty html and invalid command states stable", () => { + assert.equal(isHtmlEmpty("


", ""), true); + assert.equal(isHtmlEmpty("

 

", ""), true); + assert.equal(isHtmlEmpty("

Hello

", "Hello"), false); + assert.equal(formatUpdatedAt("not-a-date"), ""); + assert.match(formatUpdatedAt("2026-08-14T10:30:00.000Z"), /\d/); + assert.equal(queryCommandState("bold"), false); + assert.equal(queryBlockFormat(), ""); +}); + test("text exchange imports table TSV and calculator hierarchy", () => { const context = createTextImportContext(); const tableImport = importModuleText("table", "@size: 2x2\n@columns: Item\tTotal\n@rows: One\tTwo\n@tsv\nPotion\t=A1\nMega\t5", context); diff --git a/website/src/features/toolboxes/modules/NotepadModule.jsx b/website/src/features/toolboxes/modules/NotepadModule.jsx index aab1864..5b2e52f 100644 --- a/website/src/features/toolboxes/modules/NotepadModule.jsx +++ b/website/src/features/toolboxes/modules/NotepadModule.jsx @@ -1,149 +1,21 @@ // Rôle : fournit l'outil bloc-notes riche avec annotations dessinées. import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; -import { CompactDropdown } from "../../../components/CompactDropdown.jsx"; -import { DrawingControls, FormattingColorDropdown } from "./DrawingControls.jsx"; +import { DrawingControls } from "./DrawingControls.jsx"; import { DrawingOverlay } from "./DrawingOverlay.jsx"; +import { NotepadToolbar } from "./NotepadToolbar.jsx"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; - -const TEXT_COLORS = ["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef"]; -const HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"]; -const DRAWING_WIDTHS = [2, 4, 8, 12]; - -function getPlainText(element) { - return (element?.innerText || "").replace(/\n{3,}/g, "\n\n").trim(); -} - -function getSelectionTextOffset(container) { - const selection = document.getSelection(); - if (!container || !selection?.rangeCount || !container.contains(selection.anchorNode)) return null; - const range = selection.getRangeAt(0); - const prefix = range.cloneRange(); - prefix.selectNodeContents(container); - prefix.setEnd(range.startContainer, range.startOffset); - return prefix.toString().length; -} - -function restoreSelectionTextOffset(container, offset) { - if (!container || offset == null) return; - const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT); - let remaining = offset; - let node = walker.nextNode(); - while (node) { - const length = node.textContent.length; - if (remaining <= length) { - const range = document.createRange(); - range.setStart(node, remaining); - range.collapse(true); - const selection = document.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - return; - } - remaining -= length; - node = walker.nextNode(); - } - const range = document.createRange(); - range.selectNodeContents(container); - range.collapse(false); - const selection = document.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); -} - -function isHtmlEmpty(html, text) { - return !text && !String(html || "").replace(//gi, "").replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim(); -} - -function formatUpdatedAt(value) { - if (!value) return ""; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return ""; - return new Intl.DateTimeFormat("fr-FR", { - dateStyle: "short", - timeStyle: "short" - }).format(date); -} - -function queryCommandState(command) { - try { - return document.queryCommandState(command); - } catch { - return false; - } -} - -function queryBlockFormat() { - try { - return String(document.queryCommandValue("formatBlock") || "").replace(/[<>]/g, "").toLowerCase(); - } catch { - return ""; - } -} - -function ToolbarButton({ active, title, children, onClick }) { - return ( - - ); -} - -function ListTypeDropdown({ textContent, activeFormats, onCommand }) { - return ( - ( - - )} - > - {({ close }) => ( - <> - - - - )} - - ); -} +import { + DRAWING_WIDTHS, + HIGHLIGHT_COLORS, + TEXT_COLORS, + formatUpdatedAt, + getPlainText, + getSelectionTextOffset, + isHtmlEmpty, + queryBlockFormat, + queryCommandState, + restoreSelectionTextOffset +} from "./notepadUtils.js"; export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }) { const storedData = context.getModuleData(toolboxId, moduleId, { text: "" }); @@ -365,37 +237,18 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen } context.setModuleData(toolboxId, moduleId, nextData, "notepad")} /> )} -
-
-
- runCommand("bold")}>B - runCommand("italic")}>I - runCommand("underline")}>U - runCommand("strikeThrough")}>S - T -
-
- - -
-
-
+
+
+
+ onCommand("bold")}>B + onCommand("italic")}>I + onCommand("underline")}>U + onCommand("strikeThrough")}>S + T +
+
+ + +
+
+
+ ); +} + +function ToolbarButton({ active, title, children, onClick }) { + return ( + + ); +} + +function ListTypeDropdown({ textContent, activeFormats, onCommand }) { + return ( + ( + + )} + > + {({ close }) => ( + <> + + + + )} + + ); +} diff --git a/website/src/features/toolboxes/modules/notepadUtils.js b/website/src/features/toolboxes/modules/notepadUtils.js new file mode 100644 index 0000000..e02e381 --- /dev/null +++ b/website/src/features/toolboxes/modules/notepadUtils.js @@ -0,0 +1,75 @@ +// Rôle : regroupe les constantes et helpers DOM du bloc-notes riche. +export const TEXT_COLORS = ["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef"]; +export const HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"]; +export const DRAWING_WIDTHS = [2, 4, 8, 12]; + +export function getPlainText(element) { + return (element?.innerText || "").replace(/\n{3,}/g, "\n\n").trim(); +} + +export function getSelectionTextOffset(container) { + const selection = document.getSelection(); + if (!container || !selection?.rangeCount || !container.contains(selection.anchorNode)) return null; + const range = selection.getRangeAt(0); + const prefix = range.cloneRange(); + prefix.selectNodeContents(container); + prefix.setEnd(range.startContainer, range.startOffset); + return prefix.toString().length; +} + +export function restoreSelectionTextOffset(container, offset) { + if (!container || offset == null) return; + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT); + let remaining = offset; + let node = walker.nextNode(); + while (node) { + const length = node.textContent.length; + if (remaining <= length) { + const range = document.createRange(); + range.setStart(node, remaining); + range.collapse(true); + const selection = document.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + return; + } + remaining -= length; + node = walker.nextNode(); + } + const range = document.createRange(); + range.selectNodeContents(container); + range.collapse(false); + const selection = document.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +} + +export function isHtmlEmpty(html, text) { + return !text && !String(html || "").replace(//gi, "").replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim(); +} + +export function formatUpdatedAt(value) { + if (!value) return ""; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ""; + return new Intl.DateTimeFormat("fr-FR", { + dateStyle: "short", + timeStyle: "short" + }).format(date); +} + +export function queryCommandState(command) { + try { + return document.queryCommandState(command); + } catch { + return false; + } +} + +export function queryBlockFormat() { + try { + return String(document.queryCommandValue("formatBlock") || "").replace(/[<>]/g, "").toLowerCase(); + } catch { + return ""; + } +}