Skip to main content

Overview

The Image extension provides support for embedding images in your editor with features like drag-and-drop upload, paste handling, dimension management, and asynchronous upload with progress tracking.

Installation

import { defineImage } from '@prosekit/extensions'

Basic Usage

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

const editor = createEditor({
  extensions: [
    defineImage(),
    // ... other extensions
  ],
})

Image Attributes

Images support the following attributes:
interface ImageAttrs {
  /**
   * The image URL.
   */
  src?: string | null
  
  /**
   * The image width in pixels.
   */
  width?: number | null
  
  /**
   * The image height in pixels.
   */
  height?: number | null
}

Commands

insertImage

Inserts an image node at the current position or specified position.
editor.commands.insertImage({
  src: 'https://example.com/image.jpg',
  width: 640,
  height: 480,
})
Parameters:
  • src: The image URL (optional, can be null for placeholders)
  • width: Image width in pixels (optional)
  • height: Image height in pixels (optional)

uploadImage

Uploads an image file and inserts it with a temporary URL that updates once the upload completes.
editor.commands.uploadImage({
  file: imageFile,
  uploader: async ({ file }) => {
    // Upload logic here
    const url = await uploadToServer(file)
    return url
  },
})
Parameters:
  • file: The File object to upload
  • uploader: Function that handles the upload and returns the final URL
  • pos: Optional position to insert the image
  • replace: Whether to replace an existing image at pos (default: false)
  • onError: Optional error handler

Upload Handler

The extension supports automatic file handling for drag-and-drop and paste events.

defineImageUploadHandler

Sets up automatic image upload handling:
import { defineImageUploadHandler } from '@prosekit/extensions'

const extension = defineImageUploadHandler({
  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
  },
  onError: ({ error, file }) => {
    console.error('Upload failed:', error, file)
  },
})
Options:
  • uploader: Async function that uploads the file and returns the URL
  • canDrop: Optional predicate to determine if a dropped file should be handled
  • canPaste: Optional predicate to determine if a pasted file should be handled

Uploader Function

The uploader function receives options and should return a promise:
type Uploader<T> = (options: UploaderOptions) => Promise<T>

interface UploaderOptions {
  file: File
  uploadTask: UploadTask<T>
}

Upload Progress Tracking

Track upload progress using the UploadTask class:
import { UploadTask } from '@prosekit/extensions'

const uploadTask = new UploadTask({
  file: imageFile,
  uploader: async ({ file, uploadTask }) => {
    const xhr = new XMLHttpRequest()
    
    xhr.upload.addEventListener('progress', (event) => {
      if (event.lengthComputable) {
        const progress = event.loaded / event.total
        uploadTask.setProgress(progress)
      }
    })
    
    // Upload logic...
    return uploadedUrl
  },
})

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

Complete Upload Example

import { createEditor } from '@prosekit/core'
import { defineImage, defineImageUploadHandler } from '@prosekit/extensions'

const editor = createEditor({
  extensions: [
    defineImage(),
    defineImageUploadHandler({
      uploader: async ({ file, uploadTask }) => {
        // Create FormData
        const formData = new FormData()
        formData.append('file', file)
        
        // Upload with progress tracking
        const response = await fetch('/api/upload', {
          method: 'POST',
          body: formData,
        })
        
        if (!response.ok) {
          throw new Error('Upload failed')
        }
        
        const data = await response.json()
        return data.url // Return final URL
      },
      onError: ({ error, file, uploadTask }) => {
        console.error('Upload failed:', error)
        // Show error notification to user
        alert(`Failed to upload ${file.name}`)
      },
    }),
  ],
})

// Manual upload
const fileInput = document.querySelector('input[type="file"]')
fileInput?.addEventListener('change', (event) => {
  const file = event.target.files?.[0]
  if (file && file.type.startsWith('image/')) {
    editor.commands.uploadImage({
      file,
      uploader: async ({ file }) => {
        // Upload and return URL
        return uploadedUrl
      },
    })
  }
})

Filtering Files

Control which files can be dropped or pasted:
defineImageUploadHandler({
  uploader: async ({ file }) => {
    // Upload logic
  },
  canDrop: (file) => {
    // Only allow images under 5MB
    return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024
  },
  canPaste: (file) => {
    // Only allow PNG and JPEG
    return ['image/png', 'image/jpeg'].includes(file.type)
  },
})

Replacing Images

Replace an existing image by specifying position and replace option:
editor.commands.uploadImage({
  file: newImageFile,
  pos: imagePosition,
  replace: true,
  uploader: async ({ file }) => uploadedUrl,
})

Error Handling

Handle upload errors gracefully:
interface ImageUploadErrorHandlerOptions {
  file: File
  error: unknown
  uploadTask: UploadTask<string>
}

const errorHandler: ImageUploadErrorHandler = ({ file, error, uploadTask }) => {
  console.error('Upload failed:', error)
  
  // Show user-friendly error
  if (error instanceof Error) {
    if (error.message.includes('network')) {
      alert('Network error. Please check your connection.')
    } else if (error.message.includes('size')) {
      alert('File is too large.')
    } else {
      alert('Upload failed. Please try again.')
    }
  }
  
  // Cleanup
  uploadTask.abort()
}

editor.commands.uploadImage({
  file: imageFile,
  uploader: uploadFunction,
  onError: errorHandler,
})

Utilities

replaceImageURL

Replace temporary URLs with final URLs after upload:
import { replaceImageURL } from '@prosekit/extensions'

replaceImageURL(editor.view, tempURL, finalURL)

Styling Images

Images render as standard <img> elements and can be styled with CSS:
.ProseMirror img {
  max-width: 100%;
  height: auto;
  border-radius: 4px;
}

.ProseMirror img.ProseMirror-selectednode {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

Image Properties

Images are defined as:
  • Block-level nodes in the document structure
  • Draggable for repositioning
  • Defining (cannot be split or joined)

Advanced Features

Custom Upload Service

class ImageUploadService {
  async upload(file: File): Promise<string> {
    // Optimize image before upload
    const optimized = await this.optimizeImage(file)
    
    // Upload to CDN
    const url = await this.uploadToCDN(optimized)
    
    return url
  }
  
  private async optimizeImage(file: File): Promise<File> {
    // Compression logic
    return file
  }
  
  private async uploadToCDN(file: File): Promise<string> {
    // CDN upload logic
    return 'https://cdn.example.com/image.jpg'
  }
}

const uploadService = new ImageUploadService()

defineImageUploadHandler({
  uploader: async ({ file }) => await uploadService.upload(file),
})

Image Validation

function validateImage(file: File): boolean {
  // Check file type
  const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
  if (!validTypes.includes(file.type)) {
    return false
  }
  
  // Check file size (max 10MB)
  if (file.size > 10 * 1024 * 1024) {
    return false
  }
  
  return true
}

defineImageUploadHandler({
  canDrop: validateImage,
  canPaste: validateImage,
  uploader: async ({ file }) => uploadedUrl,
})
  • Use with defineFile() for general file handling
  • Combine with defineTable() to embed images in table cells
  • Works with drag-and-drop indicators for visual feedback