refactor(BlockEditor): replace emoji icons with react icon components and add free color picker support
- update blockRegistry to accept ReactNode icons instead of emoji strings - replace emoji icons in all built-in block types with icon components from shared icons - add `isHexColor` and `collectUsedColors` helpers to inline/types.js - extend `color` and `highlight` marks to accept hex color strings in addition to palette keys - pass `usedColors` (collected from document) to InlineToolbar - update InlineToolbar color popover to show used colors and a free color input - add new icons to shared icons index - update README to reflect icon, color, and toolbar popover changes
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
concatInline,
|
||||
toggleMark,
|
||||
marksInRange,
|
||||
collectUsedColors,
|
||||
} from './inline/types.js';
|
||||
|
||||
registerBuiltInBlocks();
|
||||
@@ -443,6 +444,10 @@ export default function BlockEditor({
|
||||
// { blockId, start, end, rect, marks }
|
||||
const toolbarPinnedRef = useRef(false);
|
||||
|
||||
// Couleurs déjà utilisées dans le document (hors palette par défaut).
|
||||
// Présentées comme choix rapides dans le popover de couleur du toolbar.
|
||||
const usedColors = useMemo(() => collectUsedColors(blocks), [blocks]);
|
||||
|
||||
const updateToolbar = useCallback(() => {
|
||||
if (disabled) {
|
||||
setToolbar(null);
|
||||
@@ -841,6 +846,7 @@ export default function BlockEditor({
|
||||
<InlineToolbar
|
||||
rect={toolbar.rect}
|
||||
activeMarks={marks}
|
||||
usedColors={usedColors}
|
||||
onToggleMark={applyToggleMark}
|
||||
onPinChange={(p) => { toolbarPinnedRef.current = p; }}
|
||||
/>
|
||||
|
||||
@@ -60,11 +60,13 @@ Chaque nœud porte optionnellement des **marks** (formatage).
|
||||
| `strike` | — | `<s>` |
|
||||
| `code` | — | `<code>` (monospace, fond gris) |
|
||||
| `link` | `href: string` | `<a href>` (target="_blank") |
|
||||
| `color` | `color: 'blue' \| 'green' \| 'amber' \| 'red'` | couleur du texte |
|
||||
| `highlight` | `color: 'blue' \| 'green' \| 'amber' \| 'red'` | surlignage de fond |
|
||||
| `color` | `color: <key> \| '#rrggbb'` | couleur du texte |
|
||||
| `highlight` | `color: <key> \| '#rrggbb'` | surlignage de fond |
|
||||
|
||||
Les couleurs sont des **clés de palette** (pas de hex libre) — résolues vers
|
||||
les classes Tailwind du DESIGN system. Voir `inline/types.js:INLINE_COLORS`.
|
||||
Le champ `color` accepte **soit** une clé de palette (`blue`, `green`, `amber`,
|
||||
`red`) — résolue vers les classes Tailwind du DESIGN system —, **soit** une
|
||||
string hex `#rrggbb` choisie par l'utilisateur, appliquée via `style` inline.
|
||||
Voir `inline/types.js:INLINE_COLORS` et `isHexColor`.
|
||||
|
||||
Le contenu vide est `[]` (jamais `[{type:'text', text:''}]`).
|
||||
|
||||
@@ -75,6 +77,7 @@ import {
|
||||
inlineLength, inlineToPlainText, inlineFromText,
|
||||
sliceInline, concatInline, applyMark, toggleMark,
|
||||
marksAtOffset, marksInRange, INLINE_COLORS,
|
||||
isHexColor, collectUsedColors,
|
||||
} from '@zen/core/shared/components/BlockEditor';
|
||||
```
|
||||
|
||||
@@ -118,8 +121,10 @@ Quand une sélection non-vide existe dans un bloc, un toolbar flottant
|
||||
apparaît au-dessus. Il propose :
|
||||
|
||||
- **B I U S `</>`** — marks simples (toggle)
|
||||
- **A** — couleur du texte (popover de la palette)
|
||||
- **◐** — surlignage (popover)
|
||||
- **A** — couleur du texte (popover : palette par défaut + couleurs déjà
|
||||
utilisées dans le document + bouton `+` pour une couleur libre via
|
||||
`<input type="color">`)
|
||||
- **◐** — surlignage (même structure de popover)
|
||||
- **🔗** — lien (popover avec input URL ; ✕ pour retirer)
|
||||
|
||||
L'état actif est calculé à partir des marks **communes à toute la plage**
|
||||
@@ -158,7 +163,7 @@ import { registerBlock, newBlockId } from '@zen/core/shared/components/BlockEdit
|
||||
registerBlock({
|
||||
type: 'kpi',
|
||||
label: 'KPI',
|
||||
icon: '📊',
|
||||
icon: <ChartIcon width={18} height={18} />,
|
||||
keywords: ['kpi', 'metric', 'stat'],
|
||||
isText: false,
|
||||
create: () => ({ id: newBlockId(), type: 'kpi', value: 0 }),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// {
|
||||
// type: string, // id unique (ex: 'paragraph', 'my_custom')
|
||||
// label: string, // libellé affiché dans le slash menu
|
||||
// icon: string, // glyphe court (emoji ou caractère)
|
||||
// icon: ReactNode, // élément React rendu dans le slash menu (typiquement une icône de `@zen/core/shared/icons`)
|
||||
// keywords: string[], // termes de recherche pour le slash menu
|
||||
// shortcut?: string, // préfixe markdown qui convertit (ex: '# ', '- ')
|
||||
// shortcutTransform?: (block, match) => block, // optionnel : transforme un bloc existant
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LeftToRightListBulletIcon } from '@zen/core/shared/icons';
|
||||
import { newBlockId } from '../utils/ids.js';
|
||||
|
||||
const BulletItem = {
|
||||
type: 'bullet_item',
|
||||
label: 'Liste à puces',
|
||||
icon: '•',
|
||||
icon: <LeftToRightListBulletIcon width={18} height={18} />,
|
||||
keywords: ['liste', 'list', 'puce', 'bullet', 'ul'],
|
||||
shortcut: '- ',
|
||||
isText: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tick02Icon } from '@zen/core/shared/icons';
|
||||
import { Tick02Icon, CheckListIcon } from '@zen/core/shared/icons';
|
||||
import { newBlockId } from '../utils/ids.js';
|
||||
|
||||
// Préfixe : case à cocher cliquable. `onPatch({ checked })` mute l'état du
|
||||
@@ -33,7 +33,7 @@ function Checkbox({ block, onPatch, disabled }) {
|
||||
const Checklist = {
|
||||
type: 'checklist',
|
||||
label: 'Case à cocher',
|
||||
icon: '☐',
|
||||
icon: <CheckListIcon width={18} height={18} />,
|
||||
keywords: ['checklist', 'todo', 'tache', 'tâche', 'case', 'cocher', 'check'],
|
||||
shortcut: '[] ',
|
||||
isText: true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { SourceCodeIcon } from '@zen/core/shared/icons';
|
||||
import { newBlockId } from '../utils/ids.js';
|
||||
|
||||
const Code = {
|
||||
type: 'code',
|
||||
label: 'Bloc de code',
|
||||
icon: '</>',
|
||||
icon: <SourceCodeIcon width={18} height={18} />,
|
||||
keywords: ['code', 'pre', 'snippet'],
|
||||
shortcut: '``` ',
|
||||
isText: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SeparatorHorizontalIcon } from '@zen/core/shared/icons';
|
||||
import { newBlockId } from '../utils/ids.js';
|
||||
|
||||
function DividerComponent() {
|
||||
@@ -11,7 +12,7 @@ function DividerComponent() {
|
||||
const Divider = {
|
||||
type: 'divider',
|
||||
label: 'Séparateur',
|
||||
icon: '—',
|
||||
icon: <SeparatorHorizontalIcon width={18} height={18} />,
|
||||
keywords: ['separateur', 'divider', 'hr', 'ligne', 'line'],
|
||||
shortcut: '---',
|
||||
isText: false,
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import {
|
||||
Heading01Icon,
|
||||
Heading02Icon,
|
||||
Heading03Icon,
|
||||
Heading04Icon,
|
||||
Heading05Icon,
|
||||
Heading06Icon,
|
||||
} from '@zen/core/shared/icons';
|
||||
import { newBlockId } from '../utils/ids.js';
|
||||
|
||||
const HEADING_ICONS = {
|
||||
1: Heading01Icon,
|
||||
2: Heading02Icon,
|
||||
3: Heading03Icon,
|
||||
4: Heading04Icon,
|
||||
5: Heading05Icon,
|
||||
6: Heading06Icon,
|
||||
};
|
||||
|
||||
const HEADING_STYLES = {
|
||||
1: 'text-3xl font-bold leading-tight text-neutral-900 dark:text-white',
|
||||
2: 'text-2xl font-bold leading-tight text-neutral-900 dark:text-white',
|
||||
@@ -10,10 +27,11 @@ const HEADING_STYLES = {
|
||||
};
|
||||
|
||||
function makeHeading(level) {
|
||||
const Icon = HEADING_ICONS[level];
|
||||
return {
|
||||
type: `heading_${level}`,
|
||||
label: `Titre ${level}`,
|
||||
icon: `H${level}`,
|
||||
icon: <Icon width={18} height={18} />,
|
||||
keywords: [`titre ${level}`, `heading ${level}`, `h${level}`],
|
||||
isText: true,
|
||||
textTag: `h${level}`,
|
||||
|
||||
@@ -109,7 +109,7 @@ function ImageBlock({ block, onChange, disabled }) {
|
||||
const Image = {
|
||||
type: 'image',
|
||||
label: 'Image',
|
||||
icon: '🖼',
|
||||
icon: <Image01Icon width={18} height={18} />,
|
||||
keywords: ['image', 'photo', 'picture', 'img'],
|
||||
isText: false,
|
||||
create(init = {}) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LeftToRightListNumberIcon } from '@zen/core/shared/icons';
|
||||
import { newBlockId } from '../utils/ids.js';
|
||||
|
||||
const NumberedItem = {
|
||||
type: 'numbered_item',
|
||||
label: 'Liste numérotée',
|
||||
icon: '1.',
|
||||
icon: <LeftToRightListNumberIcon width={18} height={18} />,
|
||||
keywords: ['liste numerotee', 'numbered list', 'ordonnee', 'ordered', 'ol'],
|
||||
shortcut: '1. ',
|
||||
isText: true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { TextIcon } from '@zen/core/shared/icons';
|
||||
import { newBlockId } from '../utils/ids.js';
|
||||
|
||||
const Paragraph = {
|
||||
type: 'paragraph',
|
||||
label: 'Texte',
|
||||
icon: '¶',
|
||||
icon: <TextIcon width={18} height={18} />,
|
||||
keywords: ['paragraphe', 'paragraph', 'texte', 'text', 'p'],
|
||||
isText: true,
|
||||
textTag: 'p',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LeftToRightBlockQuoteIcon } from '@zen/core/shared/icons';
|
||||
import { newBlockId } from '../utils/ids.js';
|
||||
|
||||
const Quote = {
|
||||
type: 'quote',
|
||||
label: 'Citation',
|
||||
icon: '❝',
|
||||
icon: <LeftToRightBlockQuoteIcon width={18} height={18} />,
|
||||
keywords: ['citation', 'quote', 'blockquote'],
|
||||
shortcut: '> ',
|
||||
isText: true,
|
||||
|
||||
@@ -22,6 +22,8 @@ export { newBlockId } from './utils/ids.js';
|
||||
export {
|
||||
INLINE_COLORS,
|
||||
INLINE_COLOR_KEYS,
|
||||
isHexColor,
|
||||
collectUsedColors,
|
||||
inlineLength,
|
||||
inlineToPlainText,
|
||||
inlineFromText,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { TextColorIcon, HighlighterIcon, Link02Icon } 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
|
||||
@@ -22,7 +23,7 @@ const SIMPLE_BUTTONS = [
|
||||
{ type: 'code', label: '</>', title: 'Code (Ctrl+E)', className: 'font-mono text-[11px]' },
|
||||
];
|
||||
|
||||
export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinChange }) {
|
||||
export default function InlineToolbar({ rect, activeMarks, onToggleMark, 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
|
||||
@@ -124,7 +125,7 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
||||
onClick={() => setPopover(p => (p === 'color' ? null : 'color'))}
|
||||
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 ${isActive('color') ? 'bg-neutral-100 dark:bg-neutral-800' : ''}`}
|
||||
>
|
||||
<span className="font-semibold">A</span>
|
||||
<TextColorIcon width={16} height={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -133,7 +134,7 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
||||
onClick={() => setPopover(p => (p === 'highlight' ? null : 'highlight'))}
|
||||
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 ${isActive('highlight') ? 'bg-neutral-100 dark:bg-neutral-800' : ''}`}
|
||||
>
|
||||
<span aria-hidden>◐</span>
|
||||
<HighlighterIcon width={16} height={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -142,13 +143,14 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
||||
onClick={openLinkPopover}
|
||||
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 ${isActive('link') ? 'bg-neutral-100 dark:bg-neutral-800' : ''}`}
|
||||
>
|
||||
<span aria-hidden>🔗</span>
|
||||
<Link02Icon width={16} height={16} />
|
||||
</button>
|
||||
|
||||
{popover === 'color' && (
|
||||
<ColorGrid
|
||||
mode="text"
|
||||
activeKey={activeMarks.find(m => m.type === 'color')?.color}
|
||||
usedColors={usedColors?.color}
|
||||
onPick={handleColor}
|
||||
/>
|
||||
)}
|
||||
@@ -156,6 +158,7 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
||||
<ColorGrid
|
||||
mode="highlight"
|
||||
activeKey={activeMarks.find(m => m.type === 'highlight')?.color}
|
||||
usedColors={usedColors?.highlight}
|
||||
onPick={handleHighlight}
|
||||
/>
|
||||
)}
|
||||
@@ -205,12 +208,27 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
||||
);
|
||||
}
|
||||
|
||||
function ColorGrid({ mode, activeKey, onPick }) {
|
||||
const USED_COLORS_LIMIT = 8;
|
||||
|
||||
function ColorGrid({ mode, activeKey, usedColors, onPick }) {
|
||||
const pickerRef = useRef(null);
|
||||
const used = Array.isArray(usedColors) ? usedColors.slice(0, USED_COLORS_LIMIT) : [];
|
||||
const isText = mode === 'text';
|
||||
|
||||
function openCustomPicker() {
|
||||
pickerRef.current?.click();
|
||||
}
|
||||
|
||||
function handleCustomChange(e) {
|
||||
const value = e.target.value;
|
||||
if (value) onPick(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute top-full left-0 mt-1 flex items-center gap-1 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md px-2 py-1.5">
|
||||
{INLINE_COLOR_KEYS.map(key => {
|
||||
const palette = INLINE_COLORS[key];
|
||||
const tw = mode === 'text' ? palette.text : palette.highlight;
|
||||
const tw = isText ? palette.text : palette.highlight;
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
@@ -220,10 +238,45 @@ function ColorGrid({ mode, activeKey, onPick }) {
|
||||
onClick={() => onPick(key)}
|
||||
className={`w-6 h-6 flex items-center justify-center rounded ${tw} ${activeKey === key ? 'ring-2 ring-blue-500' : ''}`}
|
||||
>
|
||||
<span className={mode === 'text' ? 'font-semibold' : ''}>A</span>
|
||||
<span className={isText ? 'font-semibold' : ''}>A</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{used.length > 0 && (
|
||||
<span className="mx-1 w-px h-5 bg-neutral-200 dark:bg-neutral-700" aria-hidden />
|
||||
)}
|
||||
{used.map(value => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
title={`Utilisée : ${value}`}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onPick(value)}
|
||||
style={isText ? { color: value } : { backgroundColor: value }}
|
||||
className={`w-6 h-6 flex items-center justify-center rounded ${activeKey === value ? 'ring-2 ring-blue-500' : ''}`}
|
||||
>
|
||||
<span className={isText ? 'font-semibold' : ''}>A</span>
|
||||
</button>
|
||||
))}
|
||||
<span className="mx-1 w-px h-5 bg-neutral-200 dark:bg-neutral-700" aria-hidden />
|
||||
<button
|
||||
type="button"
|
||||
title="Couleur personnalisée"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={openCustomPicker}
|
||||
className="w-6 h-6 flex items-center justify-center rounded border border-dashed border-neutral-300 dark:border-neutral-600 text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
<span aria-hidden>+</span>
|
||||
</button>
|
||||
<input
|
||||
ref={pickerRef}
|
||||
type="color"
|
||||
onChange={handleCustomChange}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="absolute opacity-0 pointer-events-none w-0 h-0"
|
||||
tabIndex={-1}
|
||||
aria-hidden
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// Cet ordre est important pour que la sérialisation aller-retour soit
|
||||
// stable : `inlineToDom(domToInline(x))` produit le même HTML que `x`.
|
||||
|
||||
import { INLINE_COLORS, normalize } from './types.js';
|
||||
import { INLINE_COLORS, isHexColor, normalize } from './types.js';
|
||||
|
||||
const TAG_TO_MARK = {
|
||||
STRONG: 'bold',
|
||||
@@ -56,22 +56,26 @@ function buildNode(d, node) {
|
||||
if (findMark(marks, 'italic')) el = wrap(d, el, 'em');
|
||||
if (findMark(marks, 'bold')) el = wrap(d, el, 'strong');
|
||||
|
||||
// 2. Couleur du texte.
|
||||
// 2. Couleur du texte. Hex → style inline ; clé palette → classe Tailwind.
|
||||
const color = findMark(marks, 'color');
|
||||
if (color) {
|
||||
const tw = INLINE_COLORS[color.color]?.text;
|
||||
const hex = isHexColor(color.color);
|
||||
const tw = hex ? '' : (INLINE_COLORS[color.color]?.text || '');
|
||||
el = wrap(d, el, 'span', {
|
||||
className: tw || '',
|
||||
className: tw,
|
||||
style: hex ? { color: color.color } : null,
|
||||
attrs: { 'data-color': color.color },
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Surlignage.
|
||||
// 3. Surlignage. Hex → style inline ; clé palette → classe Tailwind.
|
||||
const highlight = findMark(marks, 'highlight');
|
||||
if (highlight) {
|
||||
const tw = INLINE_COLORS[highlight.color]?.highlight;
|
||||
const hex = isHexColor(highlight.color);
|
||||
const tw = hex ? '' : (INLINE_COLORS[highlight.color]?.highlight || '');
|
||||
el = wrap(d, el, 'span', {
|
||||
className: `rounded px-0.5 ${tw || ''}`,
|
||||
className: `rounded px-0.5${tw ? ` ${tw}` : ''}`,
|
||||
style: hex ? { backgroundColor: highlight.color } : null,
|
||||
attrs: { 'data-highlight': highlight.color },
|
||||
});
|
||||
}
|
||||
@@ -96,6 +100,9 @@ function buildNode(d, node) {
|
||||
function wrap(d, child, tagName, opts = {}) {
|
||||
const el = d.createElement(tagName);
|
||||
if (opts.className) el.className = opts.className;
|
||||
if (opts.style) {
|
||||
for (const [k, v] of Object.entries(opts.style)) el.style[k] = v;
|
||||
}
|
||||
if (opts.attrs) {
|
||||
for (const [k, v] of Object.entries(opts.attrs)) el.setAttribute(k, v);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
// Palette des couleurs sémantiques du DESIGN system (cf. docs/DESIGN.md).
|
||||
// Bleu / vert / ambre / rouge — utilisées avec parcimonie. Les classes
|
||||
// Tailwind sont appliquées au rendu via `inlineToDom`.
|
||||
//
|
||||
// Le champ `color` des marks `color`/`highlight` accepte **soit** une clé
|
||||
// de cette palette, **soit** une string `#rrggbb` (couleur libre choisie
|
||||
// par l'utilisateur). À la sérialisation, un hex bascule sur un `style`
|
||||
// inline ; une clé palette utilise les classes Tailwind ci-dessous.
|
||||
export const INLINE_COLORS = {
|
||||
blue: { text: 'text-blue-600 dark:text-blue-400', highlight: 'bg-blue-100 dark:bg-blue-900/40' },
|
||||
green: { text: 'text-green-600 dark:text-green-400', highlight: 'bg-green-100 dark:bg-green-900/40' },
|
||||
@@ -28,6 +33,39 @@ export const INLINE_COLORS = {
|
||||
|
||||
export const INLINE_COLOR_KEYS = Object.keys(INLINE_COLORS);
|
||||
|
||||
const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
||||
|
||||
export function isHexColor(c) {
|
||||
return typeof c === 'string' && HEX_RE.test(c);
|
||||
}
|
||||
|
||||
// Parcourt tous les blocs et collecte les valeurs de `color` et `highlight`
|
||||
// utilisées dans le contenu inline. Conserve l'ordre d'apparition, déduplique,
|
||||
// et exclut les clés de la palette par défaut (la palette est déjà affichée
|
||||
// au-dessus dans le toolbar).
|
||||
export function collectUsedColors(blocks) {
|
||||
const out = { color: [], highlight: [] };
|
||||
if (!Array.isArray(blocks)) return out;
|
||||
const seen = { color: new Set(), highlight: new Set() };
|
||||
const palette = new Set(INLINE_COLOR_KEYS);
|
||||
for (const block of blocks) {
|
||||
const content = block?.content;
|
||||
if (!Array.isArray(content)) continue;
|
||||
for (const node of content) {
|
||||
if (!node?.marks) continue;
|
||||
for (const mark of node.marks) {
|
||||
if (mark.type !== 'color' && mark.type !== 'highlight') continue;
|
||||
const value = mark.color;
|
||||
if (!value || palette.has(value)) continue;
|
||||
if (seen[mark.type].has(value)) continue;
|
||||
seen[mark.type].add(value);
|
||||
out[mark.type].push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const SIMPLE_MARK_TYPES = ['bold', 'italic', 'underline', 'strike', 'code'];
|
||||
|
||||
function marksEqual(a, b) {
|
||||
|
||||
Reference in New Issue
Block a user