Files
core/src/shared/components/BlockEditor/README.md
T
hykocx 14c2c3d6bf fix(BlockEditor): prevent block merge when backspace pressed with active selection
- skip merge-with-previous-block trigger if selection is not collapsed
- update README to document the collapsed-selection guard on Backspace
2026-04-25 17:56:29 -04:00

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 à puces
  • 1. → liste numérotée
  • > → citation
  • ``` → bloc de code
  • --- → séparateur
  • 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

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.