feat(BlockEditor): add clear formatting button to inline toolbar

- add `removeAllMarks` function to inline/types.js
- implement `applyRemoveAllMarks` handler in BlockEditor client
- add `TextClearIcon` button with separator in Toolbar component
- expose `onClearMarks` prop on InlineToolbar
- update README to document new clear formatting action
This commit is contained in:
2026-04-25 19:23:45 -04:00
parent 741bf39a39
commit 30cd0bbd81
5 changed files with 44 additions and 5 deletions
@@ -13,6 +13,7 @@ import {
sliceInline,
concatInline,
toggleMark,
removeAllMarks,
marksInRange,
collectUsedColors,
} from './inline/types.js';
@@ -509,7 +510,20 @@ export default function BlockEditor({
const next = toggleMark(block.content ?? [], start, end, mark);
const nextBlocks = blocks.map(b => (b.id === blockId ? { ...b, content: next } : b));
commitChange(nextBlocks, { immediate: true });
// Restaure la sélection après le re-render.
requestAnimationFrame(() => {
const ref = blockRefs.current.get(blockId);
ref?.setCaretRange?.(start, end);
});
}
function applyRemoveAllMarks() {
if (!toolbar) return;
const { blockId, start, end } = toolbar;
const block = blocks.find(b => b.id === blockId);
if (!block) return;
const next = removeAllMarks(block.content ?? [], start, end);
const nextBlocks = blocks.map(b => (b.id === blockId ? { ...b, content: next } : b));
commitChange(nextBlocks, { immediate: true });
requestAnimationFrame(() => {
const ref = blockRefs.current.get(blockId);
ref?.setCaretRange?.(start, end);
@@ -851,6 +865,7 @@ export default function BlockEditor({
activeMarks={marks}
usedColors={usedColors}
onToggleMark={applyToggleMark}
onClearMarks={applyRemoveAllMarks}
onPinChange={(p) => { toolbarPinnedRef.current = p; }}
/>
);
+2 -1
View File
@@ -75,7 +75,7 @@ Le contenu vide est `[]` (jamais `[{type:'text', text:''}]`).
```js
import {
inlineLength, inlineToPlainText, inlineFromText,
sliceInline, concatInline, applyMark, toggleMark,
sliceInline, concatInline, applyMark, toggleMark, removeAllMarks,
marksAtOffset, marksInRange, INLINE_COLORS,
isHexColor, collectUsedColors,
} from '@zen/core/shared/components/BlockEditor';
@@ -126,6 +126,7 @@ apparaît au-dessus. Il propose :
`<input type="color">`)
- **◐** — surlignage (même structure de popover)
- **🔗** — lien (popover avec input URL ; ✕ pour retirer)
- **T/** — effacer tout le formatage de la sélection (supprime toutes les marks)
L'état actif est calculé à partir des marks **communes à toute la plage**
(via `marksInRange`). Toggle off si toute la plage est déjà marquée.
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { TextColorIcon, HighlighterIcon, Link02Icon, CodeSimpleIcon } from '@zen/core/shared/icons';
import { TextColorIcon, HighlighterIcon, Link02Icon, CodeSimpleIcon, TextClearIcon } from '@zen/core/shared/icons';
import { INLINE_COLORS, INLINE_COLOR_KEYS, markKey } from './types.js';
// Toolbar flottant de formatage. Affiché tant qu'une sélection non-vide
@@ -23,7 +23,7 @@ const SIMPLE_BUTTONS = [
{ type: 'code', label: <CodeSimpleIcon width={15} height={15} />, title: 'Code (Ctrl+E)', className: '' },
];
export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinChange, usedColors }) {
export default function InlineToolbar({ rect, activeMarks, onToggleMark, onClearMarks, onPinChange, usedColors }) {
const ref = useRef(null);
const [pos, setPos] = useState({ top: 0, left: 0, flipped: false });
const [popover, setPopover] = useState(null); // 'color' | 'highlight' | 'link' | null
@@ -146,6 +146,18 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
<Link02Icon width={16} height={16} />
</button>
<span className="mx-1 w-px h-5 bg-neutral-200 dark:bg-neutral-700" aria-hidden />
<button
type="button"
title="Effacer le formatage"
onMouseDown={(e) => e.preventDefault()}
onClick={() => onClearMarks?.()}
className="w-7 h-7 flex items-center justify-center rounded text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800"
>
<TextClearIcon />
</button>
{popover === 'color' && (
<ColorGrid
mode="text"
@@ -296,6 +296,10 @@ export function applyMark(nodes, start, end, mark) {
return mapRange(nodes, start, end, n => addMarkToNode(n, mark));
}
export function removeAllMarks(nodes, start, end) {
return mapRange(nodes, start, end, n => makeNode(n.text));
}
export function removeMark(nodes, start, end, type) {
return mapRange(nodes, start, end, n => removeMarkFromNode(n, type));
}