feat(ui): add "open in new tab" option to block editor link toolbar
- add `linkNewTab` state (default true) in InlineToolbar - pass `newTab` flag through `onToggleMark` on link submit and remove - restore `newTab` value when reopening the link popover - add checkbox ui in link popover to toggle new tab behavior - update link serialization to render `target="_blank" rel="noopener noreferrer"` when `newTab` is set - add `newTab` field to link mark type definition
This commit is contained in:
@@ -27,6 +27,7 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
||||
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(true);
|
||||
|
||||
useEffect(() => {
|
||||
onPinChange?.(popover !== null);
|
||||
@@ -75,21 +76,22 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
||||
function handleLinkSubmit(e) {
|
||||
e.preventDefault();
|
||||
if (!linkUrl) return;
|
||||
onToggleMark?.({ type: 'link', href: linkUrl });
|
||||
onToggleMark?.({ type: 'link', href: linkUrl, newTab: linkNewTab });
|
||||
setLinkUrl('');
|
||||
setPopover(null);
|
||||
}
|
||||
|
||||
function handleLinkRemove() {
|
||||
// Trouver le href actif pour reproduire la même mark (toggle off).
|
||||
// Trouver la mark active pour reproduire la même clé (toggle off).
|
||||
const link = activeMarks.find(m => m.type === 'link');
|
||||
if (link) onToggleMark?.({ type: 'link', href: link.href });
|
||||
if (link) onToggleMark?.({ type: 'link', href: link.href, ...(link.newTab ? { newTab: true } : {}) });
|
||||
setPopover(null);
|
||||
}
|
||||
|
||||
function openLinkPopover() {
|
||||
const link = activeMarks.find(m => m.type === 'link');
|
||||
setLinkUrl(link?.href ?? '');
|
||||
setLinkNewTab(link ? !!link.newTab : true);
|
||||
setPopover(p => (p === 'link' ? null : 'link'));
|
||||
}
|
||||
|
||||
@@ -157,32 +159,43 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
||||
{popover === 'link' && (
|
||||
<form
|
||||
onSubmit={handleLinkSubmit}
|
||||
className="absolute top-full left-0 mt-1 flex items-center gap-1 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md px-2 py-1.5"
|
||||
className="absolute top-full left-0 mt-1 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"
|
||||
>
|
||||
<input
|
||||
autoFocus
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
value={linkUrl}
|
||||
onChange={(e) => setLinkUrl(e.target.value)}
|
||||
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="submit"
|
||||
className="px-2 py-1 text-xs rounded bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
{isActive('link') && (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
autoFocus
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
value={linkUrl}
|
||||
onChange={(e) => setLinkUrl(e.target.value)}
|
||||
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={handleLinkRemove}
|
||||
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"
|
||||
type="submit"
|
||||
className="px-2 py-1 text-xs rounded bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
✕
|
||||
OK
|
||||
</button>
|
||||
)}
|
||||
{isActive('link') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLinkRemove}
|
||||
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={linkNewTab}
|
||||
onChange={(e) => setLinkNewTab(e.target.checked)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
Ouvrir dans un nouvel onglet
|
||||
</label>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,14 +14,6 @@
|
||||
|
||||
import { INLINE_COLORS, normalize } from './types.js';
|
||||
|
||||
const SIMPLE_TAGS = {
|
||||
bold: 'STRONG',
|
||||
italic: 'EM',
|
||||
underline: 'U',
|
||||
strike: 'S',
|
||||
code: 'CODE',
|
||||
};
|
||||
|
||||
const TAG_TO_MARK = {
|
||||
STRONG: 'bold',
|
||||
B: 'bold',
|
||||
@@ -87,13 +79,14 @@ function buildNode(d, node) {
|
||||
// 4. Lien — toujours à l'extérieur.
|
||||
const link = findMark(marks, 'link');
|
||||
if (link) {
|
||||
const attrs = { href: link.href };
|
||||
if (link.newTab) {
|
||||
attrs.target = '_blank';
|
||||
attrs.rel = 'noopener noreferrer';
|
||||
}
|
||||
el = wrap(d, el, 'a', {
|
||||
className: 'text-blue-600 dark:text-blue-400 underline underline-offset-2',
|
||||
attrs: {
|
||||
href: link.href,
|
||||
rel: 'noopener noreferrer',
|
||||
target: '_blank',
|
||||
},
|
||||
attrs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,7 +142,10 @@ function walk(node, marks, out) {
|
||||
|
||||
if (tag === 'A') {
|
||||
const href = node.getAttribute('href') || '';
|
||||
if (href) added.push({ type: 'link', href });
|
||||
if (href) {
|
||||
const newTab = node.getAttribute('target') === '_blank';
|
||||
added.push({ type: 'link', href, ...(newTab ? { newTab: true } : {}) });
|
||||
}
|
||||
}
|
||||
|
||||
if (tag === 'SPAN') {
|
||||
|
||||
@@ -48,7 +48,7 @@ export function markKey(mark) {
|
||||
case 'highlight':
|
||||
return `${mark.type}:${mark.color}`;
|
||||
case 'link':
|
||||
return `link:${mark.href}`;
|
||||
return `link:${mark.newTab ? '1' : '0'}:${mark.href}`;
|
||||
default:
|
||||
return mark.type;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user