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:
2026-04-25 18:32:21 -04:00
parent 2c132b3a8a
commit fdb36c39e5
3 changed files with 49 additions and 40 deletions
@@ -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,8 +159,9 @@ 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"
>
<div className="flex items-center gap-1">
<input
autoFocus
type="url"
@@ -183,6 +186,16 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
</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;
}