Overview
The insertFromPaste inputType is triggered when the user pastes content from the clipboard (typically via Ctrl/Cmd + V). The browser inserts the pasted content at the current selection position.
Basic Behavior
Scenario: Text pasted at cursor
Before (Cursor position)
After pasting "text "
Scenario: Rich text pasted (replaces selection)
Before (Text selected)
After pasting "<strong>bold</strong>"
Clipboard Data Access
The beforeinput event provides access to clipboard data via e.dataTransfer:
e.dataTransfer.getData('text/plain')- Plain texte.dataTransfer.getData('text/html')- HTML contente.dataTransfer.files- File list (for images, etc.)
Browser-Specific Behavior
Chrome/Edge
- Preserves HTML structure when pasting from rich text sources
- May sanitize certain HTML elements for security
- Supports pasting images as
<img>elements
Firefox
- May strip or modify HTML structure more aggressively
- Table structure may be lost when pasting from Excel/Sheets
Safari
- Preserves formatting by default
- No built-in "Paste as plain text" option
Mobile-Specific Behavior
⚠️ Samsung Keyboard: Clipboard History & Saved Text
Android + Samsung Keyboard: Samsung Keyboard's clipboard history and saved text features may cause unexpected behavior:
- Clipboard History: Selecting from clipboard history may fire
insertFromPaste, butbeforeinputmay not fire - Saved Text: Inserting saved text may fire
insertFromPasteorinsertReplacementTextdepending on context - Multiple Events: A single clipboard history selection may trigger multiple
inputevents - Event Timing: Events may fire with delays or in unexpected order
- Content Format: Clipboard history items may include HTML formatting that needs sanitization
Best Practice: Always monitor both beforeinput and input events, compare DOM state to detect unexpected insertions, and sanitize all clipboard content.
Gboard Clipboard Suggestions
- Gboard may show clipboard suggestions that interfere with paste operations
- Clipboard suggestions may trigger
insertFromPasteorinsertReplacementText - Multiple paste events may fire for a single clipboard selection
Security Considerations
⚠️ XSS Risk
Warning: Pasting HTML content can introduce XSS vulnerabilities. Always sanitize pasted HTML before inserting it into the DOM.
Use libraries like DOMPurify to sanitize HTML before insertion.
IME Composition + Paste
⚠️ Critical Issue
Pasting during IME composition may cancel the active composition. The composition text may be lost or the paste may occur in an unexpected location.
When handling paste operations, check for active composition state before inserting content to avoid interrupting the user's input.
Editor Internal Model & DOM Synchronization
⚠️ Exception: Programmatic DOM Updates Clear Undo Stack
When editors programmatically update DOM after paste:
- After converting pasted HTML to internal model and re-rendering DOM, browser's undo stack may be cleared
- This is especially common in Safari - programmatic DOM changes clear the undo stack
- Users may lose ability to undo previous operations after paste
- Editor's internal undo stack and browser's undo stack become out of sync
Workaround: Use document.execCommand('insertHTML', false, html) carefully, or fully manage your own undo stack when using preventDefault().
Editor-Specific Handling
Different editor frameworks handle paste operations differently, often converting pasted HTML to their internal model format. Here's how major editors implement insertFromPaste:
Paste Handling
Slate intercepts paste and converts HTML to Slate nodes:
import { Editor, Transforms } from 'slate';
import { jsx } from 'slate-hyperscript';
element.addEventListener('beforeinput', (e) => {
if (e.inputType === 'insertFromPaste' && e.dataTransfer) {
e.preventDefault();
const htmlString = e.dataTransfer.getData('text/html');
const plainText = e.dataTransfer.getData('text/plain');
// Parse HTML and convert to Slate nodes
const fragment = new DOMParser().parseFromString(htmlString || plainText, 'text/html');
const nodes = Array.from(fragment.body.childNodes).map(node => {
// Convert DOM node to Slate node structure
// This is simplified - real implementation would be more complex
return jsx('element', { type: 'paragraph' }, [{ text: node.textContent || '' }]);
});
// Insert nodes at selection
Transforms.insertNodes(editor, nodes);
}
});
- HTML parsing: Parses HTML string into DOM, then converts to Slate nodes.
- Model conversion: Converts browser HTML structure to Slate internal node model.
- Sanitization: Should sanitize HTML before parsing to prevent XSS.
Paste Handling
ProseMirror uses DOMParser to parse HTML into ProseMirror nodes:
import { DOMParser } from 'prosemirror-model';
import { schema } from './schema';
view.dom.addEventListener('beforeinput', (e) => {
if (e.inputType === 'insertFromPaste' && e.dataTransfer) {
e.preventDefault();
const { state, dispatch } = view;
const htmlString = e.dataTransfer.getData('text/html');
const dom = new DOMParser(window).parseFromString(htmlString, 'text/html');
// Parse HTML into ProseMirror nodes using schema
const fragment = DOMParser.fromSchema(schema).parse(dom.body);
// Insert fragment at selection
const tr = state.tr.replaceSelection(fragment);
dispatch(tr);
}
});
- DOMParser: Uses ProseMirror's
DOMParser.fromSchema()to parse HTML. - Schema-aware: HTML is parsed according to ProseMirror schema rules.
- Transaction-based: Creates a transaction for undo/redo support.
Paste Handling
Draft.js uses convertFromHTML to convert HTML to ContentState:
import { EditorState, ContentState } from 'draft-js';
import { stateFromHTML } from 'draft-js-import-html';
element.addEventListener('beforeinput', (e) => {
if (e.inputType === 'insertFromPaste' && e.dataTransfer) {
e.preventDefault();
const htmlString = e.dataTransfer.getData('text/html');
// Convert HTML to ContentState
const contentState = stateFromHTML(htmlString);
// Get current selection
const selection = editorState.getSelection();
// Insert content at selection
const newContentState = Modifier.replaceWithFragment(
editorState.getCurrentContent(),
selection,
contentState.getBlockMap()
);
const newState = EditorState.push(
editorState,
newContentState,
'insert-fragment'
);
setEditorState(newState);
}
});
- HTML import: Uses
stateFromHTMLto convert HTML to ContentState. - Block conversion: Converts HTML elements to Draft.js ContentBlocks.
- Modifier API: Uses
Modifier.replaceWithFragmentto insert content.