Skip to content

Link Popover

LinkPopover provides a floating URL input popover for creating and editing links. It listens for the linkEdit event (emitted by the Link mark’s Mod-K shortcut and toolbar button) and toggles a popover with a URL input, apply button, and remove button. It auto-prepends https:// for bare URLs and validates against allowed protocols.

This is a UI extension - it creates DOM elements for the popover. In headless setups, omit this extension and build your own link editing UI by listening for the linkEdit event directly.

Included in StarterKit by default.

Select text and press Cmd+K / Ctrl+K to open the URL popover. Type a URL and press Enter to apply, or click the remove button to unlink.

The editor includes the Link mark and LinkPopover with the default theme. Click the link toolbar button or press Cmd+K / Ctrl+K to open the URL popover. Enter a URL, press Enter to apply. Click an existing link and press Cmd+K to edit or remove it.

Click to try it out

LinkPopover is included in StarterKit, so it works out of the box. To use it standalone:

import { Editor, Document, Paragraph, Text, Link, LinkPopover } from '@domternal/core';
import '@domternal/theme';
const editor = new Editor({
element: document.getElementById('editor')!,
extensions: [Document, Paragraph, Text, Link, LinkPopover],
content: '<p>Select text and press <code>Cmd+K</code> / <code>Ctrl+K</code> to add a link.</p>',
});

To configure LinkPopover in StarterKit:

StarterKit.configure({
linkPopover: { protocols: ['http:', 'https:'] },
})

To disable LinkPopover in StarterKit:

StarterKit.configure({ linkPopover: false })
OptionTypeDefaultDescription
protocolsstring[]['http:', 'https:', 'mailto:', 'tel:']Allowed URL protocols for validation. Should match the Link mark’s protocols option.
import { LinkPopover } from '@domternal/core';
const editor = new Editor({
extensions: [
LinkPopover.configure({
protocols: ['http:', 'https:', 'mailto:'],
}),
],
});

LinkPopover does not register any commands. It uses the Link mark’s setLink and unsetLink commands internally.

LinkPopover does not register its own keyboard shortcuts. It is triggered by the Link mark’s Mod-K shortcut, which emits a linkEdit event.

The popover itself supports keyboard navigation:

KeyContextAction
EnterIn URL inputApply the link
EscapeAnywhere in popoverClose the popover and return focus to the editor
TabIn URL inputMove focus to the apply button
Shift-TabIn URL inputMove focus to the remove button (if visible) or apply button
Tab / Shift-TabOn buttonsCycle focus between input, apply, and remove
EnterOn a buttonClick the focused button

LinkPopover does not register any input rules.

LinkPopover does not register any toolbar items. The toolbar link button is registered by the Link mark with emitEvent: 'linkEdit', which triggers this popover.

LinkPopover listens for the linkEdit event on the editor. This event is emitted by:

  • The Link mark’s Mod-K keyboard shortcut
  • The Link toolbar button click (via emitEvent: 'linkEdit')

Each linkEdit event toggles the popover: if closed, it opens; if open, it closes and returns focus to the editor.

When the popover opens, it:

  1. Detects existing links - checks if the cursor or selection is inside a link mark. If so, pre-fills the input with the existing href and shows the remove button.
  2. Shows a selection decoration - when text is selected, the browser hides the native selection highlight when the input takes focus. The plugin renders an inline dm-link-pending decoration to keep the selected text visually highlighted.
  3. Positions with Floating UI - uses positionFloating() (which wraps @floating-ui/dom) to position the popover below the anchor element (toolbar button) or below the cursor position.
  4. Focuses the input - auto-focuses and selects the URL input content.

When the user presses Enter or clicks the apply button:

  1. Auto-prepends protocol - if the URL has no protocol (e.g., example.com), https:// is prepended automatically.
  2. Validates the URL - checks the URL against the configured protocols list using isValidUrl(). Invalid URLs are silently rejected and the popover closes.
  3. Handles existing links - if the cursor is inside an existing link with no selection, the plugin selects the full link range using getMarkRange() and updates the mark in a single transaction to avoid a visual flash.
  4. Applies the mark - calls editor.commands.setLink({ href }) to set the link.
  5. Closes and refocuses - hides the popover and returns focus to the editor.

The remove button calls editor.commands.unsetLink(), closes the popover, and returns focus to the editor. It is only visible when the cursor is inside an existing link.

Clicking outside the popover closes it. Clicks on the toggle anchor element (the toolbar button that opened the popover) are ignored so the button’s own click handler can toggle correctly.

The popover is appended to document.body (not inside .dm-editor) to avoid clipping by overflow: hidden on the editor container.

<div class="dm-link-popover" data-dm-editor-ui>
<input class="dm-link-popover-input" type="url" placeholder="Enter URL..." />
<button class="dm-link-popover-btn dm-link-popover-apply" aria-label="Apply link">
<!-- check icon -->
</button>
<button class="dm-link-popover-btn dm-link-popover-remove" aria-label="Remove link">
<!-- linkBreak icon -->
</button>
</div>

Icons used: check (apply) and linkBreak (remove) from defaultIcons.

While the popover is open with a text selection, the plugin maintains an inline decoration with the class dm-link-pending over the selected range. This provides a blue highlight (rgba(37, 99, 235, 0.12)) so the user can see which text will receive the link.

LinkPopover writes its open state to editor.storage.link.isOpen. This allows the Link toolbar button to show an “expanded” state while the popover is open.

The @domternal/theme package includes full styles for the link popover with light and dark theme support.

ClassDescription
.dm-link-popoverMain popover container (fixed position, z-index 60)
.dm-link-popover-inputURL text input (min-width 14rem)
.dm-link-popover-btnShared button styles (1.5rem square)
.dm-link-popover-applyApply button (blue on hover)
.dm-link-popover-removeRemove button (red on hover)
.dm-link-pendingInline decoration on selected text while popover is open
import { LinkPopover } from '@domternal/core';
import type { LinkPopoverOptions } from '@domternal/core';
ExportTypeDescription
LinkPopoverExtensionThe link popover extension
LinkPopoverOptionsTypeScript typeOptions for LinkPopover.configure()

@domternal/core - LinkPopover.ts