Skip to main content
The Override system allows you to customize core editor behaviors like paste, copy, and drag-and-drop operations without modifying the editor’s internal code.
The Override system provides low-level control over editor behavior. Use it carefully as incorrect implementations may break core functionality.

Available Overrides

Canvas Editor provides four override points:
interface Override {
  paste?: (evt?: ClipboardEvent) => unknown | IOverrideResult
  pasteImage?: (file: File | Blob) => unknown | IOverrideResult
  copy?: () => unknown | IOverrideResult
  drop?: (evt: DragEvent) => unknown | IOverrideResult
}

interface IOverrideResult {
  preventDefault?: boolean
}
Source: /src/editor/core/override/Override.ts:1

Basic Usage

Access the override system via editor.override:
import Editor from 'canvas-editor'

const editor = new Editor(container, data, options)

// Override paste behavior
editor.override.paste = (evt) => {
  console.log('Custom paste handler')
  // Return preventDefault: true to stop default behavior
  return { preventDefault: true }
}

Paste Override

Customize how content is pasted into the editor:
editor.override.paste = (evt?: ClipboardEvent) => {
  if (!evt) return

  const text = evt.clipboardData?.getData('text/plain')
  
  if (text) {
    // Custom paste logic
    console.log('Pasting text:', text)
    
    // Transform the text before inserting
    const transformedText = text.toUpperCase()
    editor.command.executeInsertElementList([{
      value: transformedText
    }])
    
    // Prevent default paste behavior
    return { preventDefault: true }
  }
}

Paste Image Override

Customize how images are handled when pasted:
editor.override.pasteImage = async (file: File | Blob) => {
  console.log('Image pasted:', file)

  // Upload to custom image server
  const formData = new FormData()
  formData.append('image', file)

  try {
    const response = await fetch('/api/upload-image', {
      method: 'POST',
      body: formData
    })

    const { url } = await response.json()

    // Insert image with server URL
    editor.command.executeInsertElementList([{
      type: ElementType.IMAGE,
      value: url,
      width: 300,
      height: 200
    }])

    return { preventDefault: true }
  } catch (error) {
    console.error('Image upload failed:', error)
    alert('Failed to upload image')
    return { preventDefault: true }
  }
}
The pasteImage override is called specifically for image files. Use this to upload images to your server instead of embedding them as base64.

Copy Override

Customize what gets copied to the clipboard:
editor.override.copy = async () => {
  // Get selected text
  const rangeText = editor.command.getRangeText()
  
  if (!rangeText) return

  // Add source attribution
  const attribution = '\n\nSource: My Document Editor'
  const text = rangeText + attribution

  // Write to clipboard
  const plainText = new Blob([text], { type: 'text/plain' })
  const item = new ClipboardItem({
    'text/plain': plainText
  })

  await navigator.clipboard.write([item])

  // Prevent default copy behavior
  return { preventDefault: true }
}

Drop Override

Customize drag-and-drop behavior:
editor.override.drop = async (evt: DragEvent) => {
  console.log('Drop event:', evt)

  const files = evt.dataTransfer?.files
  
  if (!files || files.length === 0) return

  for (let i = 0; i < files.length; i++) {
    const file = files[i]

    if (file.type.startsWith('image/')) {
      // Handle image drop
      const formData = new FormData()
      formData.append('file', file)

      try {
        const response = await fetch('/api/upload', {
          method: 'POST',
          body: formData
        })

        const { url } = await response.json()

        editor.command.executeInsertElementList([{
          type: ElementType.IMAGE,
          value: url
        }])
      } catch (error) {
        console.error('Upload failed:', error)
      }
    } else if (file.type === 'application/pdf') {
      // Handle PDF drop
      alert('PDF import coming soon!')
    } else {
      // Handle other file types
      const text = await file.text()
      editor.command.executeInsertElementList([{
        value: text
      }])
    }
  }

  return { preventDefault: true }
}

Preventing Default Behavior

Control whether the default editor behavior should execute:
1

Prevent Default

Return { preventDefault: true } to stop default behavior:
editor.override.paste = (evt) => {
  // Custom logic here
  return { preventDefault: true } // Default paste blocked
}
2

Allow Default

Return { preventDefault: false } or nothing to allow default behavior:
editor.override.paste = (evt) => {
  // Custom logic here
  return { preventDefault: false } // Default paste continues
}

// Or simply return nothing
editor.override.copy = () => {
  console.log('Copy tracked')
  // Default copy behavior will execute
}
3

Conditional Prevention

Conditionally prevent default based on content:
editor.override.paste = (evt) => {
  const text = evt?.clipboardData?.getData('text/plain')
  
  // Block paste if content contains forbidden words
  if (text && containsForbiddenWords(text)) {
    alert('Content contains forbidden words')
    return { preventDefault: true }
  }

  // Allow default paste for clean content
  return { preventDefault: false }
}

Real-World Examples

Convert Markdown to editor elements when pasting:
import { markdownToElements } from './utils/markdown'

editor.override.paste = (evt) => {
  if (!evt) return

  const text = evt.clipboardData?.getData('text/plain')
  
  if (!text) return

  // Check if content looks like Markdown
  const hasMarkdown = /[#*_`\[\]]/g.test(text)

  if (hasMarkdown) {
    const elements = markdownToElements(text)
    editor.command.executeInsertElementList(elements)
    return { preventDefault: true }
  }

  // Let default paste handle non-Markdown
}
Validate image size before allowing paste:
const MAX_IMAGE_SIZE = 5 * 1024 * 1024 // 5MB

editor.override.pasteImage = async (file) => {
  if (file.size > MAX_IMAGE_SIZE) {
    alert(`Image too large. Max size: ${MAX_IMAGE_SIZE / 1024 / 1024}MB`)
    return { preventDefault: true }
  }

  // Compress image before inserting
  const compressed = await compressImage(file)
  const dataUrl = await blobToDataUrl(compressed)

  editor.command.executeInsertElementList([{
    type: ElementType.IMAGE,
    value: dataUrl
  }])

  return { preventDefault: true }
}

async function compressImage(file: File | Blob): Promise<Blob> {
  // Image compression logic
  return file // Simplified
}
Preserve rich formatting when copying:
editor.override.copy = async () => {
  const range = editor.command.getRange()
  const elements = editor.command.getElementList()
  
  const selected = elements.slice(range.startIndex, range.endIndex + 1)

  // Create HTML with full formatting
  const html = selected.map(el => {
    let styles = []
    if (el.bold) styles.push('font-weight: bold')
    if (el.italic) styles.push('font-style: italic')
    if (el.color) styles.push(`color: ${el.color}`)
    if (el.size) styles.push(`font-size: ${el.size}px`)

    const styleAttr = styles.length > 0 
      ? ` style="${styles.join('; ')}"` 
      : ''
    
    return `<span${styleAttr}>${el.value || ''}</span>`
  }).join('')

  const plainText = selected.map(el => el.value || '').join('')

  const htmlBlob = new Blob([html], { type: 'text/html' })
  const textBlob = new Blob([plainText], { type: 'text/plain' })

  await navigator.clipboard.write([new ClipboardItem({
    'text/html': htmlBlob,
    'text/plain': textBlob
  })])

  return { preventDefault: true }
}
Handle multiple file types intelligently:
editor.override.drop = async (evt) => {
  const files = evt.dataTransfer?.files
  if (!files) return

  for (const file of Array.from(files)) {
    if (file.type.startsWith('image/')) {
      await handleImageDrop(file)
    } else if (file.type === 'text/plain') {
      await handleTextDrop(file)
    } else if (file.type === 'application/json') {
      await handleJsonDrop(file)
    } else {
      console.warn('Unsupported file type:', file.type)
    }
  }

  return { preventDefault: true }
}

async function handleImageDrop(file: File) {
  const url = await uploadToServer(file)
  editor.command.executeInsertElementList([{
    type: ElementType.IMAGE,
    value: url
  }])
}

async function handleTextDrop(file: File) {
  const text = await file.text()
  editor.command.executeInsertElementList([{ value: text }])
}

async function handleJsonDrop(file: File) {
  const json = await file.text()
  const data = JSON.parse(json)
  // Insert structured data as table or formatted text
  editor.command.executeInsertElementList(formatJsonData(data))
}

Removing Overrides

To remove an override and restore default behavior:
// Remove paste override
editor.override.paste = undefined

// Remove all overrides
editor.override.paste = undefined
editor.override.pasteImage = undefined
editor.override.copy = undefined
editor.override.drop = undefined

Best Practices

Handle errors gracefully

Always wrap async operations in try-catch blocks and provide user feedback on errors.

Preserve default behavior when possible

Return preventDefault: false when you want to augment, not replace, default behavior.

Validate input

Always validate pasted content, file sizes, and types before processing.

Consider performance

Avoid heavy processing in override handlers. Use async operations for slow tasks.
Override handlers run on every paste/copy/drop event. Avoid expensive synchronous operations that could freeze the UI.

Debugging Overrides

Use console logging to debug override behavior:
editor.override.paste = (evt) => {
  console.group('Paste Override')
  console.log('Event:', evt)
  console.log('Plain text:', evt?.clipboardData?.getData('text/plain'))
  console.log('HTML:', evt?.clipboardData?.getData('text/html'))
  console.log('Types:', evt?.clipboardData?.types)
  console.groupEnd()

  // Your logic here
}

TypeScript Support

All override methods are fully typed:
import { Override, IOverrideResult } from 'canvas-editor'

const customOverride: Override = {
  paste: (evt?: ClipboardEvent): IOverrideResult => {
    // TypeScript will enforce correct types
    return { preventDefault: true }
  },
  pasteImage: async (file: File | Blob): Promise<IOverrideResult> => {
    // Async overrides work too
    return { preventDefault: true }
  }
}

editor.override.paste = customOverride.paste
editor.override.pasteImage = customOverride.pasteImage

Next Steps

Plugin System

Create reusable plugins with overrides

Commands API

Learn about available editor commands

Events

Listen to editor events

Custom Shortcuts

Register keyboard shortcuts

Build docs developers (and LLMs) love