Overview
The formatCreateLink inputType is triggered when the user presses Ctrl/Cmd + K or uses the browser's link creation feature. The browser wraps selected text in an <a href="..."> element.
Basic Behavior
Scenario 1: Text selected, URL provided
Before (Text selected)
After Cmd/Ctrl + K (URL: https://google.com)
Scenario 2: Collapsed cursor (no text selected)
Before (Cursor position)
After Cmd/Ctrl + K (URL: https://google.com)
Browser-Specific Behavior
- Most browsers prompt for URL via a dialog when Cmd/Ctrl + K is pressed
- If no URL is provided, some browsers create an empty link or do nothing
- The
beforeinputevent may include the URL ine.dataore.dataTransfer
IME Composition + formatCreateLink
⚠️ Critical Issue
During IME composition, pressing Cmd/Ctrl + K may not work as expected. On macOS with Korean IME, formatCreateLink does not fire; insertCompositionText may fire instead.
See formatBold for detailed workarounds.
Editor-Specific Handling
Different editor frameworks handle link creation differently, as links are more complex than simple text formatting. Here's how major editors implement formatCreateLink:
Link Creation
Slate wraps selected text in a link element node:
import { Editor, Transforms, Element } from 'slate';
element.addEventListener('beforeinput', (e) => {
if (e.inputType === 'formatCreateLink') {
e.preventDefault();
const url = e.dataTransfer?.getData('text/uri-list') ||
prompt('Enter URL:') || '';
if (!url) return;
// Check if selection is already in a link
const [match] = Editor.nodes(editor, {
match: n => Element.isElement(n) && n.type === 'link',
});
if (match) {
// Update existing link URL
Transforms.setNodes(editor, { url }, { at: match[1] });
} else {
// Wrap selection in link
Transforms.wrapNodes(editor, {
type: 'link',
url,
children: [],
}, { split: true });
}
}
});
- Element node: Links are represented as element nodes, not marks.
- wrapNodes: Uses
Transforms.wrapNodes()to wrap selection in link element. - URL storage: URL is stored as a property on the link element node.
Link Creation
ProseMirror uses mark system for links:
import { toggleMark } from 'prosemirror-commands';
import { schema } from './schema';
view.dom.addEventListener('beforeinput', (e) => {
if (e.inputType === 'formatCreateLink') {
e.preventDefault();
const { state, dispatch } = view;
const url = e.dataTransfer?.getData('text/uri-list') ||
prompt('Enter URL:') || '';
if (!url) return;
// ProseMirror uses marks for links
const linkMark = schema.marks.link.create({ href: url });
const { from, to } = state.selection;
const tr = state.tr.addMark(from, to, linkMark);
dispatch(tr);
}
});
- Mark system: Links are represented as marks (e.g.,
schema.marks.link). - addMark: Uses
tr.addMark()to apply link mark to selection. - URL in mark: URL is stored as an attribute on the link mark.
Link Creation
Draft.js uses entity system for links:
import { EditorState, Modifier, RichUtils } from 'draft-js';
import { Entity } from 'draft-js';
element.addEventListener('beforeinput', (e) => {
if (e.inputType === 'formatCreateLink') {
e.preventDefault();
const url = e.dataTransfer?.getData('text/uri-list') ||
prompt('Enter URL:') || '';
if (!url) return;
// Create entity for link
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
'LINK',
'MUTABLE',
{ url }
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
// Apply entity to selection
const newContentState = Modifier.applyEntity(
contentStateWithEntity,
editorState.getSelection(),
entityKey
);
const newState = EditorState.push(
editorState,
newContentState,
'apply-entity'
);
setEditorState(newState);
}
});
- Entity system: Links are represented as entities (e.g.,
'LINK'entity type). - createEntity: Creates an entity with URL metadata.
- applyEntity: Uses
Modifier.applyEntity()to apply entity to selection.