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:
@@ -1,5 +1,7 @@
|
||||
// Helpers de gestion du caret pour les contentEditable mono-bloc.
|
||||
// On reste sur du texte brut au MVP : un seul Text node enfant (ou aucun).
|
||||
// Helpers de gestion du caret pour les contentEditable.
|
||||
// Depuis Phase 2, l'arbre interne peut contenir plusieurs Text nodes
|
||||
// emballés dans des wrappers (<strong>, <em>, <a>, <span data-color>...).
|
||||
// Les fonctions ci-dessous calculent / posent un offset texte global.
|
||||
|
||||
export function getCaretOffset(el) {
|
||||
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
||||
@@ -12,22 +14,68 @@ export function getCaretOffset(el) {
|
||||
return pre.toString().length;
|
||||
}
|
||||
|
||||
export function getCaretRange(el) {
|
||||
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
||||
if (!sel || sel.rangeCount === 0) return null;
|
||||
const range = sel.getRangeAt(0);
|
||||
if (!el.contains(range.startContainer) || !el.contains(range.endContainer)) return null;
|
||||
const startPre = range.cloneRange();
|
||||
startPre.selectNodeContents(el);
|
||||
startPre.setEnd(range.startContainer, range.startOffset);
|
||||
const endPre = range.cloneRange();
|
||||
endPre.selectNodeContents(el);
|
||||
endPre.setEnd(range.endContainer, range.endOffset);
|
||||
return { start: startPre.toString().length, end: endPre.toString().length };
|
||||
}
|
||||
|
||||
// Trouve le Text node + offset local correspondant à un offset global.
|
||||
// Si l'arbre est vide, retourne `{ node: el, offset: 0 }` pour positionner
|
||||
// le caret directement dans l'élément racine.
|
||||
function locateOffset(el, offset) {
|
||||
if (!el) return null;
|
||||
const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
||||
let consumed = 0;
|
||||
let last = null;
|
||||
let textNode = walker.nextNode();
|
||||
while (textNode) {
|
||||
const len = textNode.nodeValue?.length ?? 0;
|
||||
if (offset <= consumed + len) {
|
||||
return { node: textNode, offset: Math.max(0, offset - consumed) };
|
||||
}
|
||||
consumed += len;
|
||||
last = textNode;
|
||||
textNode = walker.nextNode();
|
||||
}
|
||||
if (last) return { node: last, offset: last.nodeValue?.length ?? 0 };
|
||||
return { node: el, offset: 0 };
|
||||
}
|
||||
|
||||
export function setCaretOffset(el, offset) {
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
const sel = window.getSelection();
|
||||
if (!sel) return;
|
||||
const target = locateOffset(el, Math.max(0, offset || 0));
|
||||
if (!target) return;
|
||||
const range = document.createRange();
|
||||
const text = el.firstChild;
|
||||
if (!text) {
|
||||
range.setStart(el, 0);
|
||||
range.collapse(true);
|
||||
} else {
|
||||
const max = text.textContent?.length ?? 0;
|
||||
const pos = Math.max(0, Math.min(offset, max));
|
||||
range.setStart(text, pos);
|
||||
range.collapse(true);
|
||||
}
|
||||
range.setStart(target.node, target.offset);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
// Pose une sélection couvrant [start, end[ dans l'élément.
|
||||
export function setCaretRange(el, start, end) {
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
const sel = window.getSelection();
|
||||
if (!sel) return;
|
||||
const a = locateOffset(el, Math.max(0, start || 0));
|
||||
const b = locateOffset(el, Math.max(0, end || 0));
|
||||
if (!a || !b) return;
|
||||
const range = document.createRange();
|
||||
range.setStart(a.node, a.offset);
|
||||
range.setEnd(b.node, b.offset);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user