From fdb36c39e57b3c47f646622ccf19327668624d9c Mon Sep 17 00:00:00 2001 From: Hyko Date: Sat, 25 Apr 2026 18:32:21 -0400 Subject: [PATCH] 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 --- .../BlockEditor/inline/Toolbar.client.js | 63 +++++++++++-------- .../BlockEditor/inline/serialize.js | 24 +++---- .../components/BlockEditor/inline/types.js | 2 +- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/shared/components/BlockEditor/inline/Toolbar.client.js b/src/shared/components/BlockEditor/inline/Toolbar.client.js index a03c305..c85f61e 100644 --- a/src/shared/components/BlockEditor/inline/Toolbar.client.js +++ b/src/shared/components/BlockEditor/inline/Toolbar.client.js @@ -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' && (
- 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" - /> - - {isActive('link') && ( +
+ 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" + /> - )} + {isActive('link') && ( + + )} +
+
)} diff --git a/src/shared/components/BlockEditor/inline/serialize.js b/src/shared/components/BlockEditor/inline/serialize.js index 44e59ba..dcff38e 100644 --- a/src/shared/components/BlockEditor/inline/serialize.js +++ b/src/shared/components/BlockEditor/inline/serialize.js @@ -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') { diff --git a/src/shared/components/BlockEditor/inline/types.js b/src/shared/components/BlockEditor/inline/types.js index daf3f47..b06ecfd 100644 --- a/src/shared/components/BlockEditor/inline/types.js +++ b/src/shared/components/BlockEditor/inline/types.js @@ -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; }