feat(BlockEditor): add inline formatting with rich content model
- migrate block content from plain strings to InlineNode[] structure - add inline toolbar (bold, italic, code, color, link) on text selection - add checklist block type with toggle support - add image block type (URL-based, phase 2) - add inline serialization helpers (inlineToDom, domToInline) - add inline types and length utilities - extend caret utils with range get/set support - update block registry and all existing block types for new content model - update demo blocks in ComponentsPage to use rich inline content - update README to reflect new architecture
This commit is contained in:
@@ -16,23 +16,70 @@ const [blocks, setBlocks] = useState([]);
|
||||
`value` est un **tableau de blocs JSON**. C'est la source de vérité.
|
||||
`MarkdownEditor` reste disponible en parallèle pour les usages markdown.
|
||||
|
||||
## Format des blocs (Phase 1)
|
||||
## Format des blocs
|
||||
|
||||
Chaque bloc a un `id` (UUID) et un `type`. Selon le type :
|
||||
|
||||
| type | champs | description |
|
||||
|----------------|---------------|----------------------------|
|
||||
| `paragraph` | `content` | texte brut |
|
||||
| `heading_1..6` | `content` | titre niveau 1 à 6 |
|
||||
| `bullet_item` | `content` | élément de liste à puces |
|
||||
| `numbered_item`| `content` | élément de liste numérotée |
|
||||
| `quote` | `content` | citation |
|
||||
| `code` | `content` | bloc de code (monospace) |
|
||||
| `divider` | — | séparateur horizontal |
|
||||
| type | champs | description |
|
||||
|----------------|---------------------------------|----------------------------------|
|
||||
| `paragraph` | `content` | texte |
|
||||
| `heading_1..6` | `content` | titre niveau 1 à 6 |
|
||||
| `bullet_item` | `content` | élément de liste à puces |
|
||||
| `numbered_item`| `content` | élément de liste numérotée |
|
||||
| `checklist` | `content`, `checked` | case à cocher + texte |
|
||||
| `quote` | `content` | citation |
|
||||
| `code` | `content` | bloc de code (monospace) |
|
||||
| `divider` | — | séparateur horizontal |
|
||||
| `image` | `src`, `alt`, `caption` | image (URL uniquement) |
|
||||
|
||||
Phase 2 ajoutera : `checklist`, `image`, et le format de `content` passera de
|
||||
`string` à `InlineNode[]` pour supporter le formatting inline (gras, italique,
|
||||
couleur, lien). Phase 3 : `table`.
|
||||
`content` est un **tableau `InlineNode[]`** depuis Phase 2 — voir ci-dessous.
|
||||
|
||||
Phase 3 (à venir) : `table`.
|
||||
|
||||
## Format `InlineNode`
|
||||
|
||||
Le contenu inline d'un bloc texte est un tableau plat de nœuds `text`.
|
||||
Chaque nœud porte optionnellement des **marks** (formatage).
|
||||
|
||||
```js
|
||||
[
|
||||
{ type: 'text', text: 'Salut ' },
|
||||
{ type: 'text', text: 'monde', marks: [{ type: 'bold' }] },
|
||||
{ type: 'text', text: ' ' },
|
||||
{ type: 'text', text: 'lien', marks: [{ type: 'link', href: 'https://…' }] },
|
||||
]
|
||||
```
|
||||
|
||||
### Marks supportées
|
||||
|
||||
| type | payload | rendu |
|
||||
|---|---|---|
|
||||
| `bold` | — | `<strong>` |
|
||||
| `italic` | — | `<em>` |
|
||||
| `underline` | — | `<u>` |
|
||||
| `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 |
|
||||
|
||||
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 contenu vide est `[]` (jamais `[{type:'text', text:''}]`).
|
||||
|
||||
### Helpers exportés
|
||||
|
||||
```js
|
||||
import {
|
||||
inlineLength, inlineToPlainText, inlineFromText,
|
||||
sliceInline, concatInline, applyMark, toggleMark,
|
||||
marksAtOffset, marksInRange, INLINE_COLORS,
|
||||
} from '@zen/core/shared/components/BlockEditor';
|
||||
```
|
||||
|
||||
Tous les helpers sont **purs** : ils retournent un nouveau tableau normalisé
|
||||
(nœuds adjacents identiques fusionnés, vides supprimés).
|
||||
|
||||
## Props
|
||||
|
||||
@@ -47,18 +94,37 @@ couleur, lien). Phase 3 : `table`.
|
||||
|
||||
## Interactions clavier
|
||||
|
||||
- `/` → ouvre le menu de commandes (filtrable au clavier, ↑ ↓ Entrée pour valider, Échap pour fermer)
|
||||
- `/` → ouvre le menu de commandes (filtrable, ↑ ↓ Entrée pour valider, Échap pour fermer)
|
||||
- `# ` → titre 1, `## ` → 2, …, `###### ` → 6
|
||||
- `- ` → liste à puces
|
||||
- `1. ` → liste numérotée
|
||||
- `[] ` ou `[ ] ` → case à cocher
|
||||
- `> ` → citation
|
||||
- ` ``` ` → bloc de code
|
||||
- `---` → séparateur
|
||||
- `Ctrl/Cmd + B` → gras (sur sélection non vide)
|
||||
- `Ctrl/Cmd + I` → italique
|
||||
- `Ctrl/Cmd + U` → soulignement
|
||||
- `Ctrl/Cmd + E` → code inline
|
||||
- `Ctrl/Cmd + K` → lien (prompt pour l'URL)
|
||||
- `Backspace` au début d'un bloc typé → repasse en paragraphe ; au début d'un paragraphe, fusionne avec le bloc précédent (uniquement si la sélection est repliée — sinon le navigateur supprime le texte sélectionné, ex. après `Ctrl+A`)
|
||||
- `Entrée` sur un item de liste vide → sort de la liste
|
||||
- `Ctrl/Cmd + Z` / `Ctrl/Cmd + Shift + Z` → undo / redo
|
||||
- `Ctrl/Cmd + A` → 1er appui : sélectionne le contenu du bloc courant ; 2e appui : sélectionne **tous les blocs** (mode sélection multi-blocs)
|
||||
|
||||
## Toolbar de formatage
|
||||
|
||||
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)
|
||||
- **🔗** — lien (popover avec input URL ; ✕ pour retirer)
|
||||
|
||||
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.
|
||||
|
||||
## Sélection multi-blocs
|
||||
|
||||
Deux façons d'entrer en mode sélection multi-blocs :
|
||||
@@ -70,7 +136,7 @@ En mode sélection multi-blocs :
|
||||
- `Backspace` / `Delete` → supprime tous les blocs sélectionnés
|
||||
- `Escape` → quitte la sélection
|
||||
- `Ctrl/Cmd + A` → étend à tous les blocs (no-op si déjà tous sélectionnés)
|
||||
- `Ctrl/Cmd + C` / `Ctrl/Cmd + X` → copie/coupe le texte concaténé
|
||||
- `Ctrl/Cmd + C` / `Ctrl/Cmd + X` → copie/coupe le texte concaténé (texte plat)
|
||||
- frappe d'un caractère imprimable → remplace les blocs sélectionnés par un nouveau paragraphe contenant ce caractère
|
||||
- clic dans l'éditeur → quitte la sélection
|
||||
|
||||
@@ -109,24 +175,31 @@ registerBlock({
|
||||
|
||||
Le nouveau type apparaît automatiquement dans le slash menu de tout
|
||||
`<BlockEditor />` rendu après l'enregistrement. Pour les blocs **texte**, on
|
||||
fournit à la place `isText: true`, `textTag`, `textClassName`, et optionnellement
|
||||
`renderPrefix({ block, index, numberedIndex })` pour un préfixe (puce, numéro).
|
||||
fournit à la place `isText: true`, `textTag`, `textClassName`, et
|
||||
optionnellement `renderPrefix({ block, onPatch, index, numberedIndex, disabled })`
|
||||
pour un préfixe (puce, numéro, case à cocher). Le `content` initial doit
|
||||
être `[]` (un `InlineNode[]` vide).
|
||||
|
||||
## Architecture interne
|
||||
|
||||
```
|
||||
BlockEditor.client.js orchestrateur : value/onChange, undo, slash menu, drag-drop
|
||||
BlockEditor.client.js orchestrateur : value/onChange, undo, slash menu, drag-drop, toolbar
|
||||
Block.client.js wrapper d'un bloc : handles, contentEditable, paste sanitize
|
||||
SlashMenu.client.js menu flottant filtrable
|
||||
blockRegistry.js map type → définition, API publique d'extension
|
||||
blockTypes/ un fichier par type built-in
|
||||
inline/types.js InlineNode[] : palette, helpers purs (slice, concat, marks)
|
||||
inline/serialize.js DOM ↔ InlineNode[] (inlineToDom / domToInline)
|
||||
inline/Toolbar.client.js barre flottante de formatage
|
||||
utils/ids.js UUID pour les blocs
|
||||
utils/caret.js gestion du caret dans un contentEditable
|
||||
utils/caret.js gestion du caret (multi-Text-nodes)
|
||||
```
|
||||
|
||||
## Limitations connues (Phase 1)
|
||||
## Limitations connues
|
||||
|
||||
- Inline formatting (gras, italique, couleur, lien) **pas encore** : tout est texte brut. Phase 2.
|
||||
- Pas d'imbrication de listes.
|
||||
- Paste : seul le texte brut est conservé (sanitize HTML).
|
||||
- Paste : seul le texte brut est conservé (sanitize HTML). Le formatage
|
||||
inline copié depuis l'extérieur n'est pas préservé.
|
||||
- Image : URL uniquement, pas d'upload de fichier (Phase 2). La caption est
|
||||
une string plate (pas de formatage inline pour l'instant).
|
||||
- Tables : Phase 3.
|
||||
|
||||
Reference in New Issue
Block a user