feat(BlockEditor): add rich paste support with html-to-blocks parsing

- add clipboard.js with htmlToBlocks, blocksToHtml, and blocksToPlainText helpers
- handle single-paragraph html paste as inline splice preserving block type
- handle multi-block html paste by splitting current block and merging head/tail paragraphs
- add onPasteInline and onPasteBlocks props to Block component
- implement handlePasteInline and handlePasteBlocks in BlockEditor
- fallback to plain text insertion when html is absent or yields no blocks
- update README to document clipboard behaviour and new paste handlers
This commit is contained in:
2026-04-25 20:19:32 -04:00
parent 547b975c01
commit 085a779c74
6 changed files with 540 additions and 11 deletions
+34 -3
View File
@@ -78,6 +78,7 @@ import {
sliceInline, concatInline, applyMark, toggleMark, removeAllMarks,
marksAtOffset, marksInRange, INLINE_COLORS,
isHexColor, collectUsedColors,
blocksToHtml, blocksToPlainText, htmlToBlocks,
} from '@zen/core/shared/components/BlockEditor';
```
@@ -142,7 +143,8 @@ 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é (texte plat)
- `Ctrl/Cmd + C` / `Ctrl/Cmd + X` → copie/coupe les blocs sélectionnés au format **HTML structuré** (titres, gras, listes, citations, …) + fallback `text/plain`. Collable dans Word, Google Docs, Slack, ou un autre `BlockEditor`.
- `Ctrl/Cmd + V` → remplace les blocs sélectionnés par le contenu HTML du presse-papier (parsé par `htmlToBlocks`).
- 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
@@ -201,11 +203,40 @@ utils/ids.js UUID pour les blocs
utils/caret.js gestion du caret (multi-Text-nodes)
```
## Copier-coller avec formatage
Le presse-papier transporte deux MIME en parallèle : `text/html` (structure
+ formatage) et `text/plain` (fallback). Côté éditeur :
- **Copy / Cut** : `blocksToHtml(selected)` produit un HTML standard
(`<h1>…<h6>`, `<p>`, `<ul>/<ol><li>`, `<blockquote>`, `<pre><code>`,
`<hr>`, `<figure><img>`). Pour les sélections multi-blocs (focus
défocus), on passe par l'API async `navigator.clipboard.write` avec un
`ClipboardItem` qui inclut les deux MIME.
- **Paste dans un bloc** : `Block.handlePaste` lit `text/html` en
priorité et appelle `htmlToBlocks`. Si un seul paragraphe est produit,
son contenu inline est splicé au caret. Sinon le bloc courant est
coupé en deux et les blocs collés sont insérés entre les deux moitiés
(avec fusion tête/queue si extrémités = paragraphe).
- **Paste en sélection multi-blocs** : `navigator.clipboard.read()` lit
le HTML du presse-papier, le convertit, et remplace les blocs
sélectionnés.
Tags HTML reconnus en entrée : `<h1>``<h6>`, `<p>`, `<ul>/<ol><li>`,
`<ul data-checklist>` ou `<li>` contenant `<input type="checkbox">`,
`<blockquote>`, `<pre>`, `<code>`, `<hr>`, `<figure>`, `<img>`, plus
toutes les marks de `domToInline` (`<strong>/<b>`, `<em>/<i>`, `<u>`,
`<s>/<strike>/<del>`, `<code>`, `<a href>`, `<span data-color>` /
`<span data-highlight>`).
Les wrappers de Word / Google Docs (`<b id=...>`, `<div>` de mise en
page) sont traversés pour atteindre les éléments block-level
descendants. Les styles CSS inline (`font-weight`, `font-style`,
`text-decoration`) sont également lus en plus des tags sémantiques.
## Limitations connues
- Pas d'imbrication de listes.
- 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.