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:
@@ -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; }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user