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,
|
sliceInline,
|
||||||
concatInline,
|
concatInline,
|
||||||
toggleMark,
|
toggleMark,
|
||||||
|
removeAllMarks,
|
||||||
marksInRange,
|
marksInRange,
|
||||||
collectUsedColors,
|
collectUsedColors,
|
||||||
} from './inline/types.js';
|
} from './inline/types.js';
|
||||||
@@ -509,7 +510,20 @@ export default function BlockEditor({
|
|||||||
const next = toggleMark(block.content ?? [], start, end, mark);
|
const next = toggleMark(block.content ?? [], start, end, mark);
|
||||||
const nextBlocks = blocks.map(b => (b.id === blockId ? { ...b, content: next } : b));
|
const nextBlocks = blocks.map(b => (b.id === blockId ? { ...b, content: next } : b));
|
||||||
commitChange(nextBlocks, { immediate: true });
|
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(() => {
|
requestAnimationFrame(() => {
|
||||||
const ref = blockRefs.current.get(blockId);
|
const ref = blockRefs.current.get(blockId);
|
||||||
ref?.setCaretRange?.(start, end);
|
ref?.setCaretRange?.(start, end);
|
||||||
@@ -851,6 +865,7 @@ export default function BlockEditor({
|
|||||||
activeMarks={marks}
|
activeMarks={marks}
|
||||||
usedColors={usedColors}
|
usedColors={usedColors}
|
||||||
onToggleMark={applyToggleMark}
|
onToggleMark={applyToggleMark}
|
||||||
|
onClearMarks={applyRemoveAllMarks}
|
||||||
onPinChange={(p) => { toolbarPinnedRef.current = p; }}
|
onPinChange={(p) => { toolbarPinnedRef.current = p; }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ Le contenu vide est `[]` (jamais `[{type:'text', text:''}]`).
|
|||||||
```js
|
```js
|
||||||
import {
|
import {
|
||||||
inlineLength, inlineToPlainText, inlineFromText,
|
inlineLength, inlineToPlainText, inlineFromText,
|
||||||
sliceInline, concatInline, applyMark, toggleMark,
|
sliceInline, concatInline, applyMark, toggleMark, removeAllMarks,
|
||||||
marksAtOffset, marksInRange, INLINE_COLORS,
|
marksAtOffset, marksInRange, INLINE_COLORS,
|
||||||
isHexColor, collectUsedColors,
|
isHexColor, collectUsedColors,
|
||||||
} from '@zen/core/shared/components/BlockEditor';
|
} from '@zen/core/shared/components/BlockEditor';
|
||||||
@@ -126,6 +126,7 @@ apparaît au-dessus. Il propose :
|
|||||||
`<input type="color">`)
|
`<input type="color">`)
|
||||||
- **◐** — surlignage (même structure de popover)
|
- **◐** — surlignage (même structure de popover)
|
||||||
- **🔗** — lien (popover avec input URL ; ✕ pour retirer)
|
- **🔗** — 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**
|
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.
|
(via `marksInRange`). Toggle off si toute la plage est déjà marquée.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
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';
|
import { INLINE_COLORS, INLINE_COLOR_KEYS, markKey } from './types.js';
|
||||||
|
|
||||||
// Toolbar flottant de formatage. Affiché tant qu'une sélection non-vide
|
// 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: '' },
|
{ 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 ref = useRef(null);
|
||||||
const [pos, setPos] = useState({ top: 0, left: 0, flipped: false });
|
const [pos, setPos] = useState({ top: 0, left: 0, flipped: false });
|
||||||
const [popover, setPopover] = useState(null); // 'color' | 'highlight' | 'link' | null
|
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} />
|
<Link02Icon width={16} height={16} />
|
||||||
</button>
|
</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' && (
|
{popover === 'color' && (
|
||||||
<ColorGrid
|
<ColorGrid
|
||||||
mode="text"
|
mode="text"
|
||||||
|
|||||||
@@ -296,6 +296,10 @@ export function applyMark(nodes, start, end, mark) {
|
|||||||
return mapRange(nodes, start, end, n => addMarkToNode(n, 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) {
|
export function removeMark(nodes, start, end, type) {
|
||||||
return mapRange(nodes, start, end, n => removeMarkFromNode(n, type));
|
return mapRange(nodes, start, end, n => removeMarkFromNode(n, type));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -692,4 +692,11 @@ export const CodeSimpleIcon = (props) => (
|
|||||||
<path d="M1 12C1 11.2305 1.37561 10.6269 1.83105 10.1123C2.27334 9.61267 2.91679 9.06153 3.66797 8.41407L7.34668 5.24219C7.76499 4.88164 8.39723 4.92842 8.75781 5.34669C9.11836 5.76499 9.07158 6.39723 8.65332 6.75782L4.97363 9.92872C4.17815 10.6144 3.66029 11.0634 3.3291 11.4375C3.01113 11.7967 3 11.9411 3 12C3 12.0589 3.01114 12.2033 3.3291 12.5625C3.66029 12.9366 4.17815 13.3856 4.97363 14.0713L8.65332 17.2422C9.07158 17.6028 9.11836 18.235 8.75781 18.6533C8.39723 19.0716 7.76499 19.1184 7.34668 18.7578L3.66797 15.5859C2.91679 14.9385 2.27333 14.3873 1.83105 13.8877C1.37561 13.3732 1 12.7695 1 12Z" fill="currentColor"></path>
|
<path d="M1 12C1 11.2305 1.37561 10.6269 1.83105 10.1123C2.27334 9.61267 2.91679 9.06153 3.66797 8.41407L7.34668 5.24219C7.76499 4.88164 8.39723 4.92842 8.75781 5.34669C9.11836 5.76499 9.07158 6.39723 8.65332 6.75782L4.97363 9.92872C4.17815 10.6144 3.66029 11.0634 3.3291 11.4375C3.01113 11.7967 3 11.9411 3 12C3 12.0589 3.01114 12.2033 3.3291 12.5625C3.66029 12.9366 4.17815 13.3856 4.97363 14.0713L8.65332 17.2422C9.07158 17.6028 9.11836 18.235 8.75781 18.6533C8.39723 19.0716 7.76499 19.1184 7.34668 18.7578L3.66797 15.5859C2.91679 14.9385 2.27333 14.3873 1.83105 13.8877C1.37561 13.3732 1 12.7695 1 12Z" fill="currentColor"></path>
|
||||||
<path d="M21 12C21 11.9411 20.9889 11.7967 20.6709 11.4375C20.3397 11.0634 19.8219 10.6144 19.0264 9.92872L15.3467 6.75782C14.9284 6.39723 14.8816 5.76499 15.2422 5.34669C15.6028 4.92842 16.235 4.88164 16.6533 5.24219L20.332 8.41407C21.0832 9.06153 21.7267 9.61267 22.169 10.1123C22.6244 10.6269 23 11.2305 23 12C23 12.7695 22.6244 13.3732 22.169 13.8877C21.7267 14.3873 21.0832 14.9385 20.332 15.5859L16.6533 18.7578C16.235 19.1184 15.6028 19.0716 15.2422 18.6533C14.8816 18.235 14.9284 17.6028 15.3467 17.2422L19.0264 14.0713C19.8219 13.3856 20.3397 12.9366 20.6709 12.5625C20.9889 12.2033 21 12.0589 21 12Z" fill="currentColor"></path>
|
<path d="M21 12C21 11.9411 20.9889 11.7967 20.6709 11.4375C20.3397 11.0634 19.8219 10.6144 19.0264 9.92872L15.3467 6.75782C14.9284 6.39723 14.8816 5.76499 15.2422 5.34669C15.6028 4.92842 16.235 4.88164 16.6533 5.24219L20.332 8.41407C21.0832 9.06153 21.7267 9.61267 22.169 10.1123C22.6244 10.6269 23 11.2305 23 12C23 12.7695 22.6244 13.3732 22.169 13.8877C21.7267 14.3873 21.0832 14.9385 20.332 15.5859L16.6533 18.7578C16.235 19.1184 15.6028 19.0716 15.2422 18.6533C14.8816 18.235 14.9284 17.6028 15.3467 17.2422L19.0264 14.0713C19.8219 13.3856 20.3397 12.9366 20.6709 12.5625C20.9889 12.2033 21 12.0589 21 12Z" fill="currentColor"></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const TextClearIcon = (props) => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={24} height={24} color={"currentColor"} fill={"none"} {...props}>
|
||||||
|
<path d="M7.30935 3.18172C8.78256 3.0302 10.6081 3 12 3C13.392 3 15.2175 3.0302 16.6907 3.18173C17.1881 3.2323 17.7305 3.28749 18.1333 3.45256C18.9695 3.79524 19.6275 4.53058 19.8796 5.39749C20.0012 5.81535 20.0006 6.37987 20 6.90214C20 7.45442 19.5523 7.90214 19 7.90214C18.4478 7.90214 18 7.45442 18 6.90214C18 6.20662 17.9904 6.06315 17.9592 5.9561C17.8762 5.67072 17.6435 5.41327 17.3748 5.30316C17.2743 5.26194 17.1461 5.2391 16.4861 5.17123C15.3681 5.05624 13.9988 5.01473 12.78 5.00349L9.28077 19.001H11C11.5523 19.001 12 19.4487 12 20.001C12 20.5533 11.5523 21.001 11 21.001H8.01539C8.00568 21.0011 7.99594 21.0011 7.98619 21.001H5.00004C4.44775 21.001 4.00004 20.5533 4.00004 20.001C4.00004 19.4487 4.44775 19.001 5.00004 19.001H7.21922L10.7168 5.00984C9.6344 5.02734 8.48034 5.07184 7.51396 5.17123C6.85408 5.23909 6.72581 5.26194 6.62511 5.3032C6.35638 5.41333 6.12381 5.67077 6.04082 5.95608C6.00967 6.06318 6.00004 6.20666 6.00004 6.90214C6.00004 7.45442 5.55232 7.90214 5.00004 7.90214C4.44775 7.90214 4.00004 7.45442 4.00004 6.90214C3.99946 6.37983 3.99887 5.81534 4.1204 5.39751C4.37259 4.53047 5.03061 3.79521 5.86671 3.45258C6.26953 3.2875 6.81183 3.23231 7.30935 3.18172Z" fill="currentColor"></path>
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M13.2929 14.2929C13.6834 13.9024 14.3166 13.9024 14.7071 14.2929L16.5 16.0858L18.2929 14.2929C18.6834 13.9024 19.3166 13.9024 19.7071 14.2929C20.0976 14.6834 20.0976 15.3166 19.7071 15.7071L17.9142 17.5L19.7071 19.2929C20.0976 19.6834 20.0976 20.3166 19.7071 20.7071C19.3166 21.0976 18.6834 21.0976 18.2929 20.7071L16.5 18.9142L14.7071 20.7071C14.3166 21.0976 13.6834 21.0976 13.2929 20.7071C12.9024 20.3166 12.9024 19.6834 13.2929 19.2929L15.0858 17.5L13.2929 15.7071C12.9024 15.3166 12.9024 14.6834 13.2929 14.2929Z" fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user