Skip to main content

Overview

The File extension provides low-level utilities for handling file drop and paste events in your editor. It serves as the foundation for extensions like Image, allowing you to implement custom file handling logic for any file type.

Installation

import { defineFileDropHandler, defineFilePasteHandler } from '@prosekit/extensions'

File Drop Handler

Handle files dropped into the editor.

Basic Usage

import { defineFileDropHandler } from '@prosekit/extensions'
import { createEditor } from '@prosekit/core'

const editor = createEditor({
  extensions: [
    defineFileDropHandler((options) => {
      const { view, event, file, pos } = options
      
      // Handle the dropped file
      console.log('File dropped:', file.name, 'at position:', pos)
      
      // Return true if handled, false otherwise
      return true
    }),
    // ... other extensions
  ],
})

FileDropHandlerOptions

interface FileDropHandlerOptions {
  /**
   * The editor view.
   */
  view: EditorView
  
  /**
   * The event that triggered the drop.
   */
  event: DragEvent
  
  /**
   * The file that was dropped.
   */
  file: File
  
  /**
   * The position of the document where the file was dropped.
   */
  pos: number
}

Return Value

Return true if the file was handled and should not be processed by other handlers. Return false or void to allow other handlers to process the file.

File Paste Handler

Handle files pasted into the editor.

Basic Usage

import { defineFilePasteHandler } from '@prosekit/extensions'
import { createEditor } from '@prosekit/core'

const editor = createEditor({
  extensions: [
    defineFilePasteHandler((options) => {
      const { view, event, file } = options
      
      // Handle the pasted file
      console.log('File pasted:', file.name)
      
      // Return true if handled, false otherwise
      return true
    }),
    // ... other extensions
  ],
})

FilePasteHandlerOptions

interface FilePasteHandlerOptions {
  /**
   * The editor view.
   */
  view: EditorView
  
  /**
   * The event that triggered the paste.
   */
  event: ClipboardEvent
  
  /**
   * The file that was pasted.
   */
  file: File
}

Upload System

The extension provides a robust upload system with progress tracking.

UploadTask

Manage file uploads with progress tracking:
import { UploadTask } from '@prosekit/extensions'

const uploadTask = new UploadTask({
  file: myFile,
  uploader: async ({ file }) => {
    // Upload the file and return result
    const result = await uploadToServer(file)
    return result
  },
})

// Access temporary object URL
console.log('Object URL:', uploadTask.objectURL)

// Wait for upload to complete
uploadTask.finished.then((result) => {
  console.log('Upload complete:', result)
})

// Listen to progress events
uploadTask.addEventListener('progress', (event) => {
  console.log('Progress:', event.detail.progress)
})

// Abort upload
uploadTask.abort()

Uploader Function

The uploader function handles the actual upload logic:
type Uploader<T> = (options: UploaderOptions<T>) => Promise<T>

interface UploaderOptions<T> {
  /**
   * The file being uploaded.
   */
  file: File
  
  /**
   * The upload task instance for progress tracking.
   */
  uploadTask: UploadTask<T>
}

Upload Progress

Track upload progress:
interface UploadProgress {
  /**
   * Progress value between 0 and 1.
   */
  progress: number
}

const uploadTask = new UploadTask({
  file: myFile,
  uploader: async ({ file, uploadTask }) => {
    const xhr = new XMLHttpRequest()
    
    // Track progress
    xhr.upload.addEventListener('progress', (event) => {
      if (event.lengthComputable) {
        uploadTask.setProgress(event.loaded / event.total)
      }
    })
    
    // Perform upload
    return new Promise((resolve, reject) => {
      xhr.onload = () => resolve(xhr.responseText)
      xhr.onerror = () => reject(new Error('Upload failed'))
      xhr.open('POST', '/api/upload')
      xhr.send(formData)
    })
  },
})

uploadTask.addEventListener('progress', (event) => {
  const percent = Math.round(event.detail.progress * 100)
  console.log(`Upload progress: ${percent}%`)
})

Complete Examples

Image Upload Handler

import { defineFileDropHandler, defineFilePasteHandler, UploadTask } from '@prosekit/extensions'

function createImageUploadHandler() {
  const handler = async (options) => {
    const { file, view, pos } = options
    
    // Only handle images
    if (!file.type.startsWith('image/')) {
      return false
    }
    
    // Create upload task
    const uploadTask = new UploadTask({
      file,
      uploader: async ({ file }) => {
        const formData = new FormData()
        formData.append('image', file)
        
        const response = await fetch('/api/upload', {
          method: 'POST',
          body: formData,
        })
        
        const data = await response.json()
        return data.url
      },
    })
    
    // Insert image with temporary URL
    const tempURL = uploadTask.objectURL
    const transaction = view.state.tr.insert(
      pos || view.state.selection.from,
      view.state.schema.nodes.image.create({ src: tempURL })
    )
    view.dispatch(transaction)
    
    // Replace with final URL when upload completes
    uploadTask.finished.then((finalURL) => {
      replaceImageURL(view, tempURL, finalURL)
    })
    
    return true
  }
  
  return [
    defineFileDropHandler(handler),
    defineFilePasteHandler(handler),
  ]
}

PDF Upload Handler

import { defineFileDropHandler, UploadTask } from '@prosekit/extensions'

const pdfHandler = defineFileDropHandler(async ({ file, view, pos }) => {
  // Only handle PDF files
  if (file.type !== 'application/pdf') {
    return false
  }
  
  const uploadTask = new UploadTask({
    file,
    uploader: async ({ file, uploadTask }) => {
      const formData = new FormData()
      formData.append('pdf', file)
      
      const response = await fetch('/api/upload-pdf', {
        method: 'POST',
        body: formData,
      })
      
      return await response.json()
    },
  })
  
  // Show upload progress
  uploadTask.addEventListener('progress', (event) => {
    console.log(`Uploading ${file.name}: ${Math.round(event.detail.progress * 100)}%`)
  })
  
  try {
    const result = await uploadTask.finished
    
    // Insert link to PDF
    const transaction = view.state.tr.insert(
      pos,
      view.state.schema.text(file.name, [
        view.state.schema.marks.link.create({ href: result.url })
      ])
    )
    view.dispatch(transaction)
    
    return true
  } catch (error) {
    console.error('Upload failed:', error)
    return false
  }
})

Multiple File Type Handler

import { defineFileDropHandler, UploadTask } from '@prosekit/extensions'

const multiFileHandler = defineFileDropHandler(({ file, view, pos }) => {
  const fileType = file.type
  
  // Handle images
  if (fileType.startsWith('image/')) {
    handleImageUpload(file, view, pos)
    return true
  }
  
  // Handle videos
  if (fileType.startsWith('video/')) {
    handleVideoUpload(file, view, pos)
    return true
  }
  
  // Handle documents
  if (fileType.includes('pdf') || fileType.includes('document')) {
    handleDocumentUpload(file, view, pos)
    return true
  }
  
  // Not handled
  return false
})

File Validation

function validateFile(file: File): { valid: boolean; error?: string } {
  // Check file size (max 10MB)
  if (file.size > 10 * 1024 * 1024) {
    return { valid: false, error: 'File size must be less than 10MB' }
  }
  
  // Check file type
  const allowedTypes = [
    'image/jpeg',
    'image/png',
    'image/gif',
    'application/pdf',
  ]
  
  if (!allowedTypes.includes(file.type)) {
    return { valid: false, error: 'File type not allowed' }
  }
  
  return { valid: true }
}

defineFileDropHandler(({ file, view, pos }) => {
  const validation = validateFile(file)
  
  if (!validation.valid) {
    alert(validation.error)
    return true // Handled (prevented)
  }
  
  // Process valid file
  return handleFileUpload(file, view, pos)
})

Handling Multiple Files

The handler is called once for each file in a drop or paste event:
let uploadCount = 0

defineFileDropHandler(({ file, view, pos }) => {
  uploadCount++
  
  console.log(`Processing file ${uploadCount}: ${file.name}`)
  
  // Handle file...
  return true
})

Error Handling

defineFileDropHandler(async ({ file, view, pos }) => {
  try {
    const uploadTask = new UploadTask({
      file,
      uploader: async ({ file }) => {
        const response = await fetch('/api/upload', {
          method: 'POST',
          body: file,
        })
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`)
        }
        
        return await response.json()
      },
    })
    
    const result = await uploadTask.finished
    // Handle success
    
    return true
  } catch (error) {
    console.error('Upload failed:', error)
    alert(`Failed to upload ${file.name}`)
    return true // Handled (prevented default)
  }
})

Utilities

Deleting Upload Tasks

Clean up object URLs after use:
const objectURL = uploadTask.objectURL

// After upload completes and URL is replaced
UploadTask.delete(objectURL)

Best Practices

  1. Return boolean values: Always return true if you’ve handled the file, false otherwise
  2. Validate files: Check file type and size before processing
  3. Handle errors: Provide user feedback when uploads fail
  4. Clean up resources: Revoke object URLs when no longer needed
  5. Track progress: Use the UploadTask system for better UX
  6. Type checking: Verify file types before processing
  • Build on top of this with defineImage() for image-specific handling
  • Create custom file upload extensions for other media types
  • Combine with progress indicators for better user experience