feat(ui): add link popover component for inline link editing
- add LinkPopover.client.js component for creating, editing, and removing links - replace autoOpenLink ref-based approach with dedicated linkPopover state - import and integrate removeMark utility in BlockEditor - wire up handleLinkPopoverSet and handleLinkPopoverRemove handlers - open link popover on link click instead of expanding caret range - close link popover on mousedown outside popover and toolbar - refactor InlineToolbar to delegate link editing to linkPopover
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
const GAP = 8;
|
||||
const VIEWPORT_MARGIN = 8;
|
||||
|
||||
export default function LinkPopover({ rect, mark, onSetLink, onRemoveLink, onClose }) {
|
||||
const ref = useRef(null);
|
||||
const [pos, setPos] = useState({ top: 0, left: 0 });
|
||||
const [url, setUrl] = useState(mark?.href ?? '');
|
||||
const [newTab, setNewTab] = useState(mark?.newTab ?? false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!rect || typeof window === 'undefined') return;
|
||||
const width = ref.current?.offsetWidth ?? 280;
|
||||
const height = ref.current?.offsetHeight ?? 80;
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
let top = rect.bottom + GAP;
|
||||
let left = rect.left;
|
||||
if (left + width + VIEWPORT_MARGIN > vw) left = vw - width - VIEWPORT_MARGIN;
|
||||
if (left < VIEWPORT_MARGIN) left = VIEWPORT_MARGIN;
|
||||
if (top + height + VIEWPORT_MARGIN > vh) top = rect.top - height - GAP;
|
||||
if (top < VIEWPORT_MARGIN) top = VIEWPORT_MARGIN;
|
||||
setPos({ top, left });
|
||||
}, [rect]);
|
||||
|
||||
useEffect(() => {
|
||||
function onKey(e) {
|
||||
if (e.key === 'Escape') { e.preventDefault(); onClose?.(); }
|
||||
}
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => document.removeEventListener('keydown', onKey);
|
||||
}, [onClose]);
|
||||
|
||||
function handleSubmit() {
|
||||
if (!url) return;
|
||||
onSetLink?.(url, newTab);
|
||||
onClose?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-link-popover
|
||||
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 50 }}
|
||||
className="flex flex-col gap-1.5 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md px-2 py-1.5"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
autoFocus
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleSubmit(); } }}
|
||||
className="w-56 px-2 py-1 text-sm rounded border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white outline-none focus:border-blue-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
className="px-2 py-1 text-xs rounded bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { onRemoveLink?.(); onClose?.(); }}
|
||||
title="Retirer le lien"
|
||||
className="px-2 py-1 text-xs rounded text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs text-neutral-700 dark:text-neutral-300 select-none cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newTab}
|
||||
onChange={(e) => setNewTab(e.target.checked)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
Ouvrir dans un nouvel onglet
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,13 +23,12 @@ 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, initialPopover }) {
|
||||
export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMark, onClearMarks, onPinChange, usedColors }) {
|
||||
const ref = useRef(null);
|
||||
const [pos, setPos] = useState({ top: 0, left: 0, flipped: 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);
|
||||
const [popover, setPopover] = useState(null);
|
||||
const [linkUrl, setLinkUrl] = useState('');
|
||||
const [linkNewTab, setLinkNewTab] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
onPinChange?.(popover !== null);
|
||||
|
||||
Reference in New Issue
Block a user