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 [pos, setPos] = useState({ top: 0, left: 0, flipped: false });
|
||||||
const [popover, setPopover] = useState(null); // 'color' | 'highlight' | 'link' | null
|
const [popover, setPopover] = useState(null); // 'color' | 'highlight' | 'link' | null
|
||||||
const [linkUrl, setLinkUrl] = useState('');
|
const [linkUrl, setLinkUrl] = useState('');
|
||||||
|
const [linkNewTab, setLinkNewTab] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onPinChange?.(popover !== null);
|
onPinChange?.(popover !== null);
|
||||||
@@ -75,21 +76,22 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
|||||||
function handleLinkSubmit(e) {
|
function handleLinkSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!linkUrl) return;
|
if (!linkUrl) return;
|
||||||
onToggleMark?.({ type: 'link', href: linkUrl });
|
onToggleMark?.({ type: 'link', href: linkUrl, newTab: linkNewTab });
|
||||||
setLinkUrl('');
|
setLinkUrl('');
|
||||||
setPopover(null);
|
setPopover(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLinkRemove() {
|
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');
|
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);
|
setPopover(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLinkPopover() {
|
function openLinkPopover() {
|
||||||
const link = activeMarks.find(m => m.type === 'link');
|
const link = activeMarks.find(m => m.type === 'link');
|
||||||
setLinkUrl(link?.href ?? '');
|
setLinkUrl(link?.href ?? '');
|
||||||
|
setLinkNewTab(link ? !!link.newTab : true);
|
||||||
setPopover(p => (p === 'link' ? null : 'link'));
|
setPopover(p => (p === 'link' ? null : 'link'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,32 +159,43 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onPinCh
|
|||||||
{popover === 'link' && (
|
{popover === 'link' && (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleLinkSubmit}
|
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
|
<div className="flex items-center gap-1">
|
||||||
autoFocus
|
<input
|
||||||
type="url"
|
autoFocus
|
||||||
placeholder="https://..."
|
type="url"
|
||||||
value={linkUrl}
|
placeholder="https://..."
|
||||||
onChange={(e) => setLinkUrl(e.target.value)}
|
value={linkUrl}
|
||||||
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"
|
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') && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="submit"
|
||||||
onClick={handleLinkRemove}
|
className="px-2 py-1 text-xs rounded bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
✕
|
OK
|
||||||
</button>
|
</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>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,14 +14,6 @@
|
|||||||
|
|
||||||
import { INLINE_COLORS, normalize } from './types.js';
|
import { INLINE_COLORS, normalize } from './types.js';
|
||||||
|
|
||||||
const SIMPLE_TAGS = {
|
|
||||||
bold: 'STRONG',
|
|
||||||
italic: 'EM',
|
|
||||||
underline: 'U',
|
|
||||||
strike: 'S',
|
|
||||||
code: 'CODE',
|
|
||||||
};
|
|
||||||
|
|
||||||
const TAG_TO_MARK = {
|
const TAG_TO_MARK = {
|
||||||
STRONG: 'bold',
|
STRONG: 'bold',
|
||||||
B: 'bold',
|
B: 'bold',
|
||||||
@@ -87,13 +79,14 @@ function buildNode(d, node) {
|
|||||||
// 4. Lien — toujours à l'extérieur.
|
// 4. Lien — toujours à l'extérieur.
|
||||||
const link = findMark(marks, 'link');
|
const link = findMark(marks, 'link');
|
||||||
if (link) {
|
if (link) {
|
||||||
|
const attrs = { href: link.href };
|
||||||
|
if (link.newTab) {
|
||||||
|
attrs.target = '_blank';
|
||||||
|
attrs.rel = 'noopener noreferrer';
|
||||||
|
}
|
||||||
el = wrap(d, el, 'a', {
|
el = wrap(d, el, 'a', {
|
||||||
className: 'text-blue-600 dark:text-blue-400 underline underline-offset-2',
|
className: 'text-blue-600 dark:text-blue-400 underline underline-offset-2',
|
||||||
attrs: {
|
attrs,
|
||||||
href: link.href,
|
|
||||||
rel: 'noopener noreferrer',
|
|
||||||
target: '_blank',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +142,10 @@ function walk(node, marks, out) {
|
|||||||
|
|
||||||
if (tag === 'A') {
|
if (tag === 'A') {
|
||||||
const href = node.getAttribute('href') || '';
|
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') {
|
if (tag === 'SPAN') {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function markKey(mark) {
|
|||||||
case 'highlight':
|
case 'highlight':
|
||||||
return `${mark.type}:${mark.color}`;
|
return `${mark.type}:${mark.color}`;
|
||||||
case 'link':
|
case 'link':
|
||||||
return `link:${mark.href}`;
|
return `link:${mark.newTab ? '1' : '0'}:${mark.href}`;
|
||||||
default:
|
default:
|
||||||
return mark.type;
|
return mark.type;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user