Skip to main content

Overview

Canvas Editor provides two complementary event systems for reacting to changes:
  1. Listener - Legacy callback-based system (simple, direct assignment)
  2. EventBus - Modern pub/sub pattern (multiple subscribers, better decoupling)
Both systems are initialized when you create an Editor instance and are available via editor.listener and editor.eventBus.

Listener (Legacy)

The Listener system uses direct callback assignment. Each event type has a single handler.

Available Events

class Listener {
  rangeStyleChange: IRangeStyleChange | null
  visiblePageNoListChange: IVisiblePageNoListChange | null
  intersectionPageNoChange: IIntersectionPageNoChange | null
  pageSizeChange: IPageSizeChange | null
  pageScaleChange: IPageScaleChange | null
  saved: ISaved | null
  contentChange: IContentChange | null
  controlChange: IControlChange | null
  controlContentChange: IControlContentChange | null
  pageModeChange: IPageModeChange | null
  zoneChange: IZoneChange | null
}

Usage Example

// Assign event handlers
editor.listener.contentChange = () => {
  console.log('Content has changed')
  saveToBackend(editor.command.getValue())
}

editor.listener.rangeStyleChange = (payload) => {
  console.log('Selection style:', payload)
  updateToolbar(payload)
}

editor.listener.pageSizeChange = (pageCount) => {
  console.log('Total pages:', pageCount)
}

// Remove handler
editor.listener.contentChange = null
Listener only supports one handler per event. Assigning a new handler will replace the previous one.
The EventBus uses a publish/subscribe pattern, allowing multiple subscribers per event.

EventBus API

class EventBus<EventMap> {
  // Subscribe to an event
  on<K extends keyof EventMap>(eventName: K, callback: EventMap[K]): void

  // Emit an event (internal use)
  emit<K extends keyof EventMap>(eventName: K, payload?: any): void

  // Unsubscribe from an event
  off<K extends keyof EventMap>(eventName: K, callback: EventMap[K]): void

  // Check if event has subscribers
  isSubscribe<K extends keyof EventMap>(eventName: K): boolean

  // Clear all subscriptions (dangerous!)
  dangerouslyClearAll(): void
}

Available Events

The EventBus supports all Listener events plus additional low-level events:
// Content changed
editor.eventBus.on('contentChange', () => {
  console.log('Content modified')
})

// Selection/range style changed
editor.eventBus.on('rangeStyleChange', (payload) => {
  console.log('Bold:', payload.bold)
  console.log('Italic:', payload.italic)
  console.log('Font:', payload.font)
  console.log('Size:', payload.size)
  // ... all style properties
})

// Document saved
editor.eventBus.on('saved', (payload) => {
  console.log('Saved data:', payload.data)
  console.log('Version:', payload.version)
})

Subscribing to Events

// Define handler function
const handleContentChange = () => {
  console.log('Content changed')
  autoSave()
}

// Subscribe
editor.eventBus.on('contentChange', handleContentChange)

// Multiple subscribers are supported
editor.eventBus.on('contentChange', () => {
  updateWordCount()
})

editor.eventBus.on('contentChange', () => {
  markDocumentDirty()
})

Unsubscribing from Events

const handler = (payload) => {
  console.log('Range style:', payload)
}

// Subscribe
editor.eventBus.on('rangeStyleChange', handler)

// Unsubscribe (must use same function reference)
editor.eventBus.off('rangeStyleChange', handler)
You must use the same function reference when unsubscribing. Arrow functions defined inline cannot be unsubscribed.

Checking Subscriptions

if (editor.eventBus.isSubscribe('contentChange')) {
  console.log('Someone is listening to content changes')
}

Event Payloads

IRangeStyle

Emitted when selection style changes (toolbar state):
interface IRangeStyle {
  type: ElementType | null           // Selected element type
  undo: boolean                      // Can undo
  redo: boolean                      // Can redo
  painter: boolean                   // Format painter active
  font: string                       // Font family
  size: number                       // Font size
  bold: boolean                      // Bold active
  italic: boolean                    // Italic active
  underline: boolean                 // Underline active
  strikeout: boolean                 // Strikeout active
  color: string | null              // Text color
  highlight: string | null          // Highlight color
  rowFlex: RowFlex | null          // Alignment
  rowMargin: number                 // Row margin
  dashArray: number[]               // Dash pattern
  level: TitleLevel | null          // Title level
  listType: ListType | null         // List type
  listStyle: ListStyle | null       // List style
  groupIds: string[] | null         // Group IDs
  textDecoration: ITextDecoration | null
  extension?: unknown | null        // Custom data
}

IEditorResult (saved event)

interface IEditorResult {
  version: string          // Editor version
  data: IEditorData       // Document data
  options: IEditorOption  // Editor options
}

IControlChangeResult

interface IControlChangeResult {
  controlId: string       // Control identifier
  value: any             // New control value
}

IPositionContextChangePayload

interface IPositionContextChangePayload {
  value: IPositionContext      // New position context
  oldValue: IPositionContext   // Previous context
}

Common Patterns

Auto-save

let saveTimeout: number

editor.eventBus.on('contentChange', () => {
  clearTimeout(saveTimeout)
  saveTimeout = setTimeout(() => {
    const data = editor.command.getValue()
    saveToBackend(data)
  }, 2000) // Debounce 2 seconds
})

Update Toolbar

editor.eventBus.on('rangeStyleChange', (style) => {
  // Update button states
  document.querySelector('#bold-btn').classList.toggle('active', style.bold)
  document.querySelector('#italic-btn').classList.toggle('active', style.italic)
  
  // Update font selector
  document.querySelector('#font-select').value = style.font
  
  // Update size input
  document.querySelector('#size-input').value = style.size.toString()
  
  // Enable/disable undo/redo
  document.querySelector('#undo-btn').disabled = !style.undo
  document.querySelector('#redo-btn').disabled = !style.redo
})

Page Number Display

let currentPage = 1
let totalPages = 1

editor.eventBus.on('intersectionPageNoChange', (pageNo) => {
  currentPage = pageNo + 1 // 0-indexed
  updatePageDisplay()
})

editor.eventBus.on('pageSizeChange', (count) => {
  totalPages = count
  updatePageDisplay()
})

function updatePageDisplay() {
  document.querySelector('#page-info').textContent = 
    `Page ${currentPage} of ${totalPages}`
}

Track Changes

let changeLog = []

editor.eventBus.on('contentChange', () => {
  changeLog.push({
    timestamp: Date.now(),
    user: getCurrentUser(),
    action: 'edit'
  })
})

editor.eventBus.on('imageMousedown', (payload) => {
  changeLog.push({
    timestamp: Date.now(),
    user: getCurrentUser(),
    action: 'image-click',
    imageId: payload.element.id
  })
})

Form Validation

const controlValues = new Map()

editor.eventBus.on('controlChange', (payload) => {
  controlValues.set(payload.controlId, payload.value)
  validateForm()
})

function validateForm() {
  const requiredControls = ['name', 'email', 'phone']
  const isValid = requiredControls.every(id => 
    controlValues.has(id) && controlValues.get(id)
  )
  
  document.querySelector('#submit-btn').disabled = !isValid
}

Cleanup

Always clean up event subscriptions when destroying the editor:
// Method 1: Unsubscribe individually
const handler = () => { /* ... */ }
editor.eventBus.on('contentChange', handler)
// Later...
editor.eventBus.off('contentChange', handler)

// Method 2: Clear all (during destroy)
editor.destroy() // Automatically calls eventBus.dangerouslyClearAll()

// Method 3: Manual clear (use with caution)
editor.eventBus.dangerouslyClearAll()
dangerouslyClearAll() removes all subscriptions, including internal ones. Only use this when destroying the editor.

Listener vs EventBus

FeatureListenerEventBus
Multiple subscribers❌ No✅ Yes
Mouse events❌ No✅ Yes
Image events❌ No✅ Yes
Unsubscribe support✅ Set to nulloff() method
Easier to use✅ Simple assignmentRequires on()/off()
Recommended for new code❌ Legacy✅ Preferred
Use EventBus for new projects. It’s more flexible and supports multiple subscribers.

Best Practices

Store Handler References

Store handlers in variables for easy unsubscribing:
const handleChange = () => { /* ... */ }
editor.eventBus.on('contentChange', handleChange)
// Later: editor.eventBus.off('contentChange', handleChange)

Debounce Expensive Operations

Debounce auto-save and other expensive operations:
let timeout
editor.eventBus.on('contentChange', () => {
  clearTimeout(timeout)
  timeout = setTimeout(save, 1000)
})

Cleanup on Unmount

Always unsubscribe when component unmounts:
// React example
useEffect(() => {
  const handler = () => {}
  editor.eventBus.on('contentChange', handler)
  return () => editor.eventBus.off('contentChange', handler)
}, [])

Type Safety

Use TypeScript for full event type safety:
import type { EventBusMap } from 'canvas-editor'

editor.eventBus.on('rangeStyleChange', (payload) => {
  // payload is fully typed as IRangeStyle
})

FAQ

Use EventBus for all new code. Listener is maintained for backward compatibility but EventBus is more powerful and flexible.
No, you cannot emit custom events. The EventBus is for subscribing to internal editor events only. For custom application events, use a separate event system.
Some events may fire during editor initialization. If you need to ignore initial events, add a flag:
let initialized = false
editor.eventBus.on('contentChange', () => {
  if (!initialized) return
  // Handle change
})
setTimeout(() => { initialized = true }, 0)
Always unsubscribe from events when components unmount or the editor is destroyed. Use the same function reference that was used to subscribe.
No, there’s no concept of event propagation or stopping events. All subscribers will be notified.

Next Steps

Commands

Learn how to execute operations

Editor Instance

Understand the Editor lifecycle

Build docs developers (and LLMs) love