Refactor notepad toolbar helpers
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
This commit is contained in:
parent
61176ba9b7
commit
c2e37ad3f2
5 changed files with 234 additions and 173 deletions
|
|
@ -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 moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8");
|
||||||
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "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 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 combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.jsx", "utf8");
|
||||||
const combosEditor = await readFile("website/src/features/toolboxes/modules/CombosEditor.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");
|
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, /export function NotepadModule/);
|
||||||
assert.match(notepadModule, /TextExchangeActions/);
|
assert.match(notepadModule, /TextExchangeActions/);
|
||||||
assert.match(notepadModule, /type="notepad"/);
|
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(combosModule, /export function CombosModule/);
|
||||||
assert.match(combosUtils, /keyboardMouse/);
|
assert.match(combosUtils, /keyboardMouse/);
|
||||||
assert.match(combosUtils, /Nintendo 64/);
|
assert.match(combosUtils, /Nintendo 64/);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { calculateExpression, createStoredFormula, evaluateCalculatorEntries, ge
|
||||||
import { createDefaultDraft, getDateSpanDays, getDraftSelection, normalizeDraftAlertOffsets, normalizeDraftMonthlyRepeatBy } from "../website/src/features/toolboxes/modules/calendarDraft.js";
|
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 { 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 { 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 { 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 { 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";
|
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]]);
|
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("<p><br></p>", ""), true);
|
||||||
|
assert.equal(isHtmlEmpty("<p> </p>", ""), true);
|
||||||
|
assert.equal(isHtmlEmpty("<p>Hello</p>", "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", () => {
|
test("text exchange imports table TSV and calculator hierarchy", () => {
|
||||||
const context = createTextImportContext();
|
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);
|
const tableImport = importModuleText("table", "@size: 2x2\n@columns: Item\tTotal\n@rows: One\tTwo\n@tsv\nPotion\t=A1\nMega\t5", context);
|
||||||
|
|
|
||||||
|
|
@ -1,149 +1,21 @@
|
||||||
// Rôle : fournit l'outil bloc-notes riche avec annotations dessinées.
|
// Rôle : fournit l'outil bloc-notes riche avec annotations dessinées.
|
||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
import { DrawingControls } from "./DrawingControls.jsx";
|
||||||
import { DrawingControls, FormattingColorDropdown } from "./DrawingControls.jsx";
|
|
||||||
import { DrawingOverlay } from "./DrawingOverlay.jsx";
|
import { DrawingOverlay } from "./DrawingOverlay.jsx";
|
||||||
|
import { NotepadToolbar } from "./NotepadToolbar.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
|
import {
|
||||||
const TEXT_COLORS = ["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef"];
|
DRAWING_WIDTHS,
|
||||||
const HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"];
|
HIGHLIGHT_COLORS,
|
||||||
const DRAWING_WIDTHS = [2, 4, 8, 12];
|
TEXT_COLORS,
|
||||||
|
formatUpdatedAt,
|
||||||
function getPlainText(element) {
|
getPlainText,
|
||||||
return (element?.innerText || "").replace(/\n{3,}/g, "\n\n").trim();
|
getSelectionTextOffset,
|
||||||
}
|
isHtmlEmpty,
|
||||||
|
queryBlockFormat,
|
||||||
function getSelectionTextOffset(container) {
|
queryCommandState,
|
||||||
const selection = document.getSelection();
|
restoreSelectionTextOffset
|
||||||
if (!container || !selection?.rangeCount || !container.contains(selection.anchorNode)) return null;
|
} from "./notepadUtils.js";
|
||||||
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(/<br\s*\/?>/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 (
|
|
||||||
<button
|
|
||||||
className={`notepad-toolbar-button ${active ? "active" : ""}`}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={active}
|
|
||||||
title={title}
|
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
|
|
||||||
return (
|
|
||||||
<CompactDropdown
|
|
||||||
className="notepad-list-dropdown"
|
|
||||||
menuClassName="notepad-list-menu"
|
|
||||||
label={textContent.listGroupLabel || "Listes"}
|
|
||||||
closeOnMouseLeave
|
|
||||||
preventMouseDown
|
|
||||||
renderTrigger={({ toggle }) => (
|
|
||||||
<button
|
|
||||||
className={`notepad-toolbar-button ${activeFormats.bulletList || activeFormats.numberList ? "active" : ""}`}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={activeFormats.bulletList || activeFormats.numberList}
|
|
||||||
title={textContent.listGroupLabel || "Listes"}
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="notepad-list-icon" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{({ close }) => (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className={`notepad-list-option ${activeFormats.bulletList ? "active" : ""}`}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={activeFormats.bulletList}
|
|
||||||
onClick={() => {
|
|
||||||
onCommand("insertUnorderedList");
|
|
||||||
close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
•
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`notepad-list-option ${activeFormats.numberList ? "active" : ""}`}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={activeFormats.numberList}
|
|
||||||
onClick={() => {
|
|
||||||
onCommand("insertOrderedList");
|
|
||||||
close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
1.
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CompactDropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
||||||
const storedData = context.getModuleData(toolboxId, moduleId, { text: "" });
|
const storedData = context.getModuleData(toolboxId, moduleId, { text: "" });
|
||||||
|
|
@ -365,37 +237,18 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
||||||
<TextExchangeActions type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
|
<TextExchangeActions type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="notepad-toolbar" aria-label={textContent.toolbarLabel || "Mise en page du bloc notes"}>
|
<NotepadToolbar
|
||||||
<div className="notepad-format-controls">
|
textContent={textContent}
|
||||||
<div className="notepad-toolbar-group" role="group" aria-label={textContent.textFormatLabel || "Style du texte"}>
|
activeFormats={activeFormats}
|
||||||
<ToolbarButton active={activeFormats.bold} title={textContent.boldTitle || "Gras"} onClick={() => runCommand("bold")}>B</ToolbarButton>
|
textColor={textColor}
|
||||||
<ToolbarButton active={activeFormats.italic} title={textContent.italicTitle || "Italique"} onClick={() => runCommand("italic")}><em>I</em></ToolbarButton>
|
highlightColor={highlightColor}
|
||||||
<ToolbarButton active={activeFormats.underline} title={textContent.underlineTitle || "Souligné"} onClick={() => runCommand("underline")}><u>U</u></ToolbarButton>
|
textColors={TEXT_COLORS}
|
||||||
<ToolbarButton active={activeFormats.strikeThrough} title={textContent.strikeTitle || "Barré"} onClick={() => runCommand("strikeThrough")}><s>S</s></ToolbarButton>
|
highlightColors={HIGHLIGHT_COLORS}
|
||||||
<ToolbarButton active={activeFormats.heading} title={textContent.headingTitle || "Titre"} onClick={toggleHeading}>T</ToolbarButton>
|
onCommand={runCommand}
|
||||||
</div>
|
onHeadingToggle={toggleHeading}
|
||||||
<div className="notepad-toolbar-group" role="group" aria-label={textContent.colorGroupLabel || "Couleurs"}>
|
onTextColorApply={applyTextColor}
|
||||||
<ListTypeDropdown textContent={textContent} activeFormats={activeFormats} onCommand={runCommand} />
|
onHighlightColorApply={applyHighlightColor}
|
||||||
<FormattingColorDropdown
|
/>
|
||||||
label={textContent.colorGroupLabel || "Couleurs"}
|
|
||||||
sections={[
|
|
||||||
{
|
|
||||||
label: textContent.textColorLabel || "Couleur du texte",
|
|
||||||
colors: TEXT_COLORS,
|
|
||||||
value: textColor,
|
|
||||||
onSelect: applyTextColor
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: textContent.highlightColorLabel || "Surlignage",
|
|
||||||
colors: HIGHLIGHT_COLORS,
|
|
||||||
value: highlightColor,
|
|
||||||
onSelect: applyHighlightColor
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`notepad-surface ${drawingActive ? "is-drawing" : ""}`} ref={surfaceRef}>
|
<div className={`notepad-surface ${drawingActive ? "is-drawing" : ""}`} ref={surfaceRef}>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
115
website/src/features/toolboxes/modules/NotepadToolbar.jsx
Normal file
115
website/src/features/toolboxes/modules/NotepadToolbar.jsx
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Rôle : rend la barre d'outils de mise en forme du bloc-notes.
|
||||||
|
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
||||||
|
import { FormattingColorDropdown } from "./DrawingControls.jsx";
|
||||||
|
|
||||||
|
export function NotepadToolbar({
|
||||||
|
textContent,
|
||||||
|
activeFormats,
|
||||||
|
textColor,
|
||||||
|
highlightColor,
|
||||||
|
textColors,
|
||||||
|
highlightColors,
|
||||||
|
onCommand,
|
||||||
|
onHeadingToggle,
|
||||||
|
onTextColorApply,
|
||||||
|
onHighlightColorApply
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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"}>
|
||||||
|
<ToolbarButton active={activeFormats.bold} title={textContent.boldTitle || "Gras"} onClick={() => onCommand("bold")}>B</ToolbarButton>
|
||||||
|
<ToolbarButton active={activeFormats.italic} title={textContent.italicTitle || "Italique"} onClick={() => onCommand("italic")}><em>I</em></ToolbarButton>
|
||||||
|
<ToolbarButton active={activeFormats.underline} title={textContent.underlineTitle || "Souligné"} onClick={() => onCommand("underline")}><u>U</u></ToolbarButton>
|
||||||
|
<ToolbarButton active={activeFormats.strikeThrough} title={textContent.strikeTitle || "Barré"} onClick={() => onCommand("strikeThrough")}><s>S</s></ToolbarButton>
|
||||||
|
<ToolbarButton active={activeFormats.heading} title={textContent.headingTitle || "Titre"} onClick={onHeadingToggle}>T</ToolbarButton>
|
||||||
|
</div>
|
||||||
|
<div className="notepad-toolbar-group" role="group" aria-label={textContent.colorGroupLabel || "Couleurs"}>
|
||||||
|
<ListTypeDropdown textContent={textContent} activeFormats={activeFormats} onCommand={onCommand} />
|
||||||
|
<FormattingColorDropdown
|
||||||
|
label={textContent.colorGroupLabel || "Couleurs"}
|
||||||
|
sections={[
|
||||||
|
{
|
||||||
|
label: textContent.textColorLabel || "Couleur du texte",
|
||||||
|
colors: textColors,
|
||||||
|
value: textColor,
|
||||||
|
onSelect: onTextColorApply
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: textContent.highlightColorLabel || "Surlignage",
|
||||||
|
colors: highlightColors,
|
||||||
|
value: highlightColor,
|
||||||
|
onSelect: onHighlightColorApply
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolbarButton({ active, title, children, onClick }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`notepad-toolbar-button ${active ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={active}
|
||||||
|
title={title}
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
|
||||||
|
return (
|
||||||
|
<CompactDropdown
|
||||||
|
className="notepad-list-dropdown"
|
||||||
|
menuClassName="notepad-list-menu"
|
||||||
|
label={textContent.listGroupLabel || "Listes"}
|
||||||
|
closeOnMouseLeave
|
||||||
|
preventMouseDown
|
||||||
|
renderTrigger={({ toggle }) => (
|
||||||
|
<button
|
||||||
|
className={`notepad-toolbar-button ${activeFormats.bulletList || activeFormats.numberList ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={activeFormats.bulletList || activeFormats.numberList}
|
||||||
|
title={textContent.listGroupLabel || "Listes"}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<span className="notepad-list-icon" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={`notepad-list-option ${activeFormats.bulletList ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={activeFormats.bulletList}
|
||||||
|
onClick={() => {
|
||||||
|
onCommand("insertUnorderedList");
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`notepad-list-option ${activeFormats.numberList ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={activeFormats.numberList}
|
||||||
|
onClick={() => {
|
||||||
|
onCommand("insertOrderedList");
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
1.
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CompactDropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
website/src/features/toolboxes/modules/notepadUtils.js
Normal file
75
website/src/features/toolboxes/modules/notepadUtils.js
Normal file
|
|
@ -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(/<br\s*\/?>/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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue