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("
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(/