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.
Live Playground
Section titled “Live Playground”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.
Vanilla JS preview - Angular components produce the same output
Vanilla JS preview - React components produce the same output
Without LinkPopover, the Link mark still works but there is no built-in UI for entering URLs. The buttons below call setLink(), unsetLink(), and toggleLink() with a hardcoded URL to demonstrate the commands directly.
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>',});import { Component, signal } from '@angular/core';import { DomternalEditorComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text, Link, LinkPopover } from '@domternal/core';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); extensions = [Document, Paragraph, Text, Link, LinkPopover]; content = '<p>Select text and press Cmd+K / Ctrl+K to add a link.</p>';}<domternal-editor [extensions]="extensions" [content]="content" (editorCreated)="editor.set($event)"/>import { Domternal } from '@domternal/react';import { Document, Paragraph, Text, Link, LinkPopover } from '@domternal/core';
export default function Editor() { return ( <Domternal extensions={[Document, Paragraph, Text, Link, LinkPopover]} content="<p>Select text and press Cmd+K / Ctrl+K to add a link.</p>" > <Domternal.Content /> </Domternal> );}In headless mode, you typically skip LinkPopover and handle the linkEdit event yourself:
import { Editor, Document, Paragraph, Text, Link } from '@domternal/core';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [Document, Paragraph, Text, Link], content: '<p>Select text and press Cmd+K / Ctrl+K.</p>',});
// Build your own link UIeditor.on('linkEdit', ({ anchorElement }) => { const url = prompt('URL:'); if (url) editor.commands.setLink({ href: url });});To configure LinkPopover in StarterKit:
StarterKit.configure({ linkPopover: { protocols: ['http:', 'https:'] },})To disable LinkPopover in StarterKit:
StarterKit.configure({ linkPopover: false })Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
protocols | string[] | ['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:'], }), ],});Commands
Section titled “Commands”LinkPopover does not register any commands. It uses the Link mark’s setLink and unsetLink commands internally.
Keyboard shortcuts
Section titled “Keyboard shortcuts”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:
| Key | Context | Action |
|---|---|---|
Enter | In URL input | Apply the link |
Escape | Anywhere in popover | Close the popover and return focus to the editor |
Tab | In URL input | Move focus to the apply button |
Shift-Tab | In URL input | Move focus to the remove button (if visible) or apply button |
Tab / Shift-Tab | On buttons | Cycle focus between input, apply, and remove |
Enter | On a button | Click the focused button |
Input rules
Section titled “Input rules”LinkPopover does not register any input rules.
Toolbar items
Section titled “Toolbar items”LinkPopover does not register any toolbar items. The toolbar link button is registered by the Link mark with emitEvent: 'linkEdit', which triggers this popover.
How it works
Section titled “How it works”Event-driven toggle
Section titled “Event-driven toggle”LinkPopover listens for the linkEdit event on the editor. This event is emitted by:
- The Link mark’s
Mod-Kkeyboard 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.
Opening behavior
Section titled “Opening behavior”When the popover opens, it:
- Detects existing links - checks if the cursor or selection is inside a link mark. If so, pre-fills the input with the existing
hrefand shows the remove button. - 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-pendingdecoration to keep the selected text visually highlighted. - 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. - Focuses the input - auto-focuses and selects the URL input content.
Applying a link
Section titled “Applying a link”When the user presses Enter or clicks the apply button:
- Auto-prepends protocol - if the URL has no protocol (e.g.,
example.com),https://is prepended automatically. - Validates the URL - checks the URL against the configured
protocolslist usingisValidUrl(). Invalid URLs are silently rejected and the popover closes. - 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. - Applies the mark - calls
editor.commands.setLink({ href })to set the link. - Closes and refocuses - hides the popover and returns focus to the editor.
Removing a link
Section titled “Removing a link”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.
Click outside
Section titled “Click outside”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.
DOM structure
Section titled “DOM structure”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.
Selection decoration
Section titled “Selection decoration”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.
Toolbar integration
Section titled “Toolbar integration”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.
Styling
Section titled “Styling”The @domternal/theme package includes full styles for the link popover with light and dark theme support.
| Class | Description |
|---|---|
.dm-link-popover | Main popover container (fixed position, z-index 60) |
.dm-link-popover-input | URL text input (min-width 14rem) |
.dm-link-popover-btn | Shared button styles (1.5rem square) |
.dm-link-popover-apply | Apply button (blue on hover) |
.dm-link-popover-remove | Remove button (red on hover) |
.dm-link-pending | Inline decoration on selected text while popover is open |
Exports
Section titled “Exports”import { LinkPopover } from '@domternal/core';import type { LinkPopoverOptions } from '@domternal/core';| Export | Type | Description |
|---|---|---|
LinkPopover | Extension | The link popover extension |
LinkPopoverOptions | TypeScript type | Options for LinkPopover.configure() |
Source
Section titled “Source”@domternal/core - LinkPopover.ts