- skip merge-with-previous-block trigger if selection is not collapsed - update README to document the collapsed-selection guard on Backspace
4.1 KiB
BlockEditor
Éditeur WYSIWYG par blocs, style Notion. Construit en interne (pas de
ProseMirror/Lexical/Tiptap). Un contentEditable par bloc, pas un seul
contentEditable global — c'est plus robuste et plus simple à étendre.
Utilisation
import { BlockEditor } from '@zen/core/shared/components';
const [blocks, setBlocks] = useState([]);
<BlockEditor value={blocks} onChange={setBlocks} label="Contenu" />
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)
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 |
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.
Props
<BlockEditor
value={blocks} // Block[]
onChange={setBlocks} // (Block[]) => void
label, error, placeholder, disabled, className
enabledBlocks={[...]} // optionnel : restreindre les types disponibles
/>
Interactions clavier
/→ ouvre le menu de commandes (filtrable au clavier, ↑ ↓ Entrée pour valider, Échap pour fermer)#→ titre 1,##→ 2, …,######→ 6-→ liste à puces1.→ liste numérotée>→ citation```→ bloc de code---→ séparateurBackspaceau 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èsCtrl+A)Entréesur un item de liste vide → sort de la listeCtrl/Cmd + Z/Ctrl/Cmd + Shift + Z→ undo / redo
Drag and drop
Chaque bloc affiche au survol :
- une poignée
+pour insérer un bloc en dessous (ouvre le slash menu) - une poignée
⋮⋮pour glisser-déposer (réordonner)
Le drag-and-drop utilise l'API HTML5 native, pas de dépendance externe.
Étendre — enregistrer un bloc custom
import { registerBlock, newBlockId } from '@zen/core/shared/components/BlockEditor';
registerBlock({
type: 'kpi',
label: 'KPI',
icon: '📊',
keywords: ['kpi', 'metric', 'stat'],
isText: false,
create: () => ({ id: newBlockId(), type: 'kpi', value: 0 }),
Component: ({ block, onChange, disabled }) => (
<input
type="number"
value={block.value}
disabled={disabled}
onChange={(e) => onChange({ value: Number(e.target.value) })}
/>
),
});
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).
Architecture interne
BlockEditor.client.js orchestrateur : value/onChange, undo, slash menu, drag-drop
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
utils/ids.js UUID pour les blocs
utils/caret.js gestion du caret dans un contentEditable
Limitations connues (Phase 1)
- 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).
- Tables : Phase 3.