feat(ui): add BlockEditor component with block types, slash menu, and drag-and-drop
- add BlockEditor orchestrator with controlled block list and keyboard navigation - add Block client component with contentEditable sync, drag handles, and markdown shortcuts - add SlashMenu for inserting block types via `/` command - add blockRegistry and block type definitions (paragraph, heading, bullet list, numbered list, quote, code, divider) - add caret and id utility helpers - export BlockEditor from shared components index - add BlockEditor demo to admin devkit ComponentsPage - add README documenting usage and architecture
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
// 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).
|
||||
|
||||
export function getCaretOffset(el) {
|
||||
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
||||
if (!sel || sel.rangeCount === 0) return 0;
|
||||
const range = sel.getRangeAt(0);
|
||||
if (!el.contains(range.startContainer)) return 0;
|
||||
const pre = range.cloneRange();
|
||||
pre.selectNodeContents(el);
|
||||
pre.setEnd(range.startContainer, range.startOffset);
|
||||
return pre.toString().length;
|
||||
}
|
||||
|
||||
export function setCaretOffset(el, offset) {
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
const sel = window.getSelection();
|
||||
if (!sel) 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);
|
||||
}
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
export function focusEnd(el) {
|
||||
if (!el) return;
|
||||
const len = (el.textContent ?? '').length;
|
||||
setCaretOffset(el, len);
|
||||
}
|
||||
|
||||
export function isCaretAtStart(el) {
|
||||
return getCaretOffset(el) === 0;
|
||||
}
|
||||
|
||||
export function isCaretAtEnd(el) {
|
||||
return getCaretOffset(el) === (el.textContent ?? '').length;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Génère un ID stable pour un bloc. randomUUID est dispo dans tous les navigateurs
|
||||
// modernes côté client ; en SSR on retombe sur un fallback simple.
|
||||
export function newBlockId() {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return `b_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user