feat(BlockEditor): auto-open link popover when clicking on existing link

- add `linkRangeAt` import to detect link span under caret
- handle `mouseup` on container to detect collapsed click inside a link mark
- set `autoOpenLink` flag via ref and expand selection to full link range
- pass `autoOpenLink` to toolbar state and use `initialPopover='link'` prop
- initialize toolbar popover state from `initialPopover` prop
- pre-fill link url and new-tab from existing mark when `initialPopover` is set
- add `linkRangeAt` helper in `types.js` to find enclosing link range at offset
This commit is contained in:
2026-04-26 15:11:00 -04:00
parent 33ee62e908
commit 8159b5316a
3 changed files with 60 additions and 6 deletions
@@ -23,12 +23,13 @@ const SIMPLE_BUTTONS = [
{ type: 'code', label: <CodeSimpleIcon width={15} height={15} />, title: 'Code (Ctrl+E)', className: '' },
];
export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMark, onClearMarks, onPinChange, usedColors }) {
export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMark, onClearMarks, onPinChange, usedColors, initialPopover }) {
const ref = useRef(null);
const [pos, setPos] = useState({ top: 0, left: 0, flipped: false });
const [popover, setPopover] = useState(null); // 'color' | 'highlight' | 'link' | null
const [linkUrl, setLinkUrl] = useState('');
const [linkNewTab, setLinkNewTab] = useState(false);
const [popover, setPopover] = useState(initialPopover ?? null);
const existingLink = initialPopover === 'link' ? activeMarks?.find(m => m.type === 'link') : null;
const [linkUrl, setLinkUrl] = useState(existingLink?.href ?? '');
const [linkNewTab, setLinkNewTab] = useState(existingLink ? !!existingLink.newTab : false);
useEffect(() => {
onPinChange?.(popover !== null);
@@ -205,6 +205,33 @@ export function marksAtOffset(nodes, offset) {
return last.marks ? last.marks.map(m => ({ ...m })) : [];
}
// Trouve le début et la fin continus du span qui porte un lien à `offset`.
// Retourne { start, end, mark } ou null si aucun lien à cet offset.
export function linkRangeAt(nodes, offset) {
if (!Array.isArray(nodes) || nodes.length === 0) return null;
const marks = marksAtOffset(nodes, offset);
const linkMark = marks.find(m => m.type === 'link');
if (!linkMark) return null;
const key = markKey(linkMark);
let pos = 0;
let start = null;
let end = null;
for (const node of nodes) {
const len = node.text?.length ?? 0;
const nodeEnd = pos + len;
const hasLink = (node.marks ?? []).some(m => markKey(m) === key);
if (hasLink) {
if (start === null) start = pos;
end = nodeEnd;
} else if (start !== null) {
break;
}
pos = nodeEnd;
}
if (start === null) return null;
return { start, end, mark: linkMark };
}
// Marks communes à toute la plage [start, end[. Si la plage est vide,
// retourne les marks à l'offset `start`. Utile pour afficher l'état actif
// du toolbar.