insertFromPaste

How insertFromPaste inputType handles paste operations and varies across browsers.

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)

HTML:
<p>Hello|world</p>

After pasting "text "

HTML:
<p>Hellotext world</p>
Pasted content inserted at cursor position

Scenario: Rich text pasted (replaces selection)

Before (Text selected)

HTML:
<p>Hello world text</p>

After pasting "<strong>bold</strong>"

HTML:
<p>Hello <strong>bold</strong> text</p>
Selected text replaced with pasted HTML

Clipboard Data Access

The beforeinput event provides access to clipboard data via e.dataTransfer:

  • e.dataTransfer.getData('text/plain') - Plain text
  • e.dataTransfer.getData('text/html') - HTML content
  • e.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, but beforeinput may not fire
  • Saved Text: Inserting saved text may fire insertFromPaste or insertReplacementText depending on context
  • Multiple Events: A single clipboard history selection may trigger multiple input events
  • 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 insertFromPaste or insertReplacementText
  • 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:

Slate.js

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.
ProseMirror

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.
Draft.js

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 stateFromHTML to convert HTML to ContentState.
  • Block conversion: Converts HTML elements to Draft.js ContentBlocks.
  • Modifier API: Uses Modifier.replaceWithFragment to insert content.

Related resources