Skip to main content
The Listener class provides a callback-based event system where you can attach single callback functions to various editor events.

Overview

Access the listener through the editor instance:
const editor = new Editor(container, data)
editor.listener.rangeStyleChange = (payload) => {
  console.log('Selection changed:', payload)
}

Available Listeners

rangeStyleChange

Fired when the selection style changes (format of selected text).
editor.listener.rangeStyleChange: IRangeStyleChange | null
Payload: IRangeStyle
{
  type: ElementType | null
  undo: boolean              // Can undo
  redo: boolean              // Can redo
  painter: boolean           // Format painter active
  font: string
  size: number
  bold: boolean
  italic: boolean
  underline: boolean
  strikeout: boolean
  color: string | null
  highlight: string | null
  rowFlex: RowFlex | null   // Alignment
  rowMargin: number
  dashArray: number[]
  level: TitleLevel | null  // Heading level
  listType: ListType | null
  listStyle: ListStyle | null
  groupIds: string[] | null
  textDecoration: ITextDecoration | null
  extension?: unknown | null
}
Example:
editor.listener.rangeStyleChange = (style) => {
  // Update toolbar UI
  document.querySelector('#bold-btn').classList.toggle('active', style.bold)
  document.querySelector('#italic-btn').classList.toggle('active', style.italic)
  document.querySelector('#font-size').value = style.size
  document.querySelector('#undo-btn').disabled = !style.undo
  document.querySelector('#redo-btn').disabled = !style.redo
}

visiblePageNoListChange

Fired when the list of visible pages changes during scrolling.
editor.listener.visiblePageNoListChange: IVisiblePageNoListChange | null
Payload: number[] - Array of visible page numbers Example:
editor.listener.visiblePageNoListChange = (pageNumbers) => {
  console.log('Visible pages:', pageNumbers)
  // Update page indicator UI
  document.querySelector('#page-info').textContent = 
    `Pages ${pageNumbers[0] + 1}-${pageNumbers[pageNumbers.length - 1] + 1}`
}

intersectionPageNoChange

Fired when the primary intersecting page changes.
editor.listener.intersectionPageNoChange: IIntersectionPageNoChange | null
Payload: number - Current page number (0-indexed) Example:
editor.listener.intersectionPageNoChange = (pageNo) => {
  console.log('Current page:', pageNo + 1)
  document.querySelector('#current-page').textContent = `Page ${pageNo + 1}`
}

pageSizeChange

Fired when the total number of pages changes.
editor.listener.pageSizeChange: IPageSizeChange | null
Payload: number - Total page count Example:
editor.listener.pageSizeChange = (totalPages) => {
  console.log('Total pages:', totalPages)
  document.querySelector('#total-pages').textContent = `of ${totalPages}`
}

pageScaleChange

Fired when the zoom level changes.
editor.listener.pageScaleChange: IPageScaleChange | null
Payload: number - Scale factor (e.g., 1 = 100%, 1.5 = 150%) Example:
editor.listener.pageScaleChange = (scale) => {
  console.log('Zoom:', Math.round(scale * 100) + '%')
  document.querySelector('#zoom-level').textContent = `${Math.round(scale * 100)}%`
}

saved

Fired when the document is saved (via save shortcut or command).
editor.listener.saved: ISaved | null
Payload: IEditorResult
{
  version: string
  data: IEditorData    // Full document data
  options: IEditorOption
}
Example:
editor.listener.saved = (result) => {
  console.log('Document saved')
  // Send to server
  fetch('/api/documents', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(result)
  })
}

contentChange

Fired whenever document content changes.
editor.listener.contentChange: IContentChange | null
Payload: void (no payload) Example:
let isDirty = false

editor.listener.contentChange = () => {
  isDirty = true
  console.log('Content changed')
  
  // Show unsaved indicator
  document.querySelector('#save-indicator').textContent = 'Unsaved changes'
}

controlChange

Fired when a control’s state changes (active/inactive).
editor.listener.controlChange: IControlChange | null
Payload: IControlChangeResult
{
  state: ControlState      // ACTIVE or INACTIVE
  control: IControl        // Control configuration
  controlId: string        // Control ID
}
Example:
editor.listener.controlChange = ({ state, control, controlId }) => {
  console.log(`Control ${controlId} is now ${state}`)
  
  if (state === 'active') {
    // Show control editing toolbar
    showControlToolbar(control)
  }
}

controlContentChange

Fired when a control’s content/value changes.
editor.listener.controlContentChange: IControlContentChange | null
Payload: IControlContentChangeResult
{
  control: IControl
  controlId: string
}
Example:
editor.listener.controlContentChange = ({ control, controlId }) => {
  console.log(`Control ${controlId} value changed:`, control.value)
  
  // Validate form field
  if (control.conceptId === 'email') {
    validateEmail(control.value)
  }
}

pageModeChange

Fired when page mode changes.
editor.listener.pageModeChange: IPageModeChange | null
Payload: PageMode - PAGING or CONTINUITY Example:
editor.listener.pageModeChange = (mode) => {
  console.log('Page mode:', mode)
  
  if (mode === PageMode.CONTINUITY) {
    // Hide page break indicators
  }
}

zoneChange

Fired when the cursor moves between zones (header/main/footer).
editor.listener.zoneChange: IZoneChange | null
Payload: EditorZone - HEADER, MAIN, or FOOTER Example:
editor.listener.zoneChange = (zone) => {
  console.log('Now in zone:', zone)
  
  // Show zone indicator
  document.querySelector('#zone-indicator').textContent = 
    `Editing ${zone.toLowerCase()}`
}

Complete Example

import Editor, { PageMode, ControlState } from '@hufe921/canvas-editor'

const editor = new Editor(container, data)

// Setup all listeners
editor.listener.rangeStyleChange = (style) => {
  // Update formatting toolbar
  updateToolbar(style)
}

editor.listener.contentChange = () => {
  // Mark document as dirty
  markDirty()
  
  // Auto-save after delay
  clearTimeout(autoSaveTimer)
  autoSaveTimer = setTimeout(() => {
    saveDocument()
  }, 2000)
}

editor.listener.pageSizeChange = (totalPages) => {
  // Update page count display
  document.querySelector('#page-count').textContent = totalPages
}

editor.listener.intersectionPageNoChange = (pageNo) => {
  // Update current page indicator
  document.querySelector('#current-page').textContent = pageNo + 1
}

editor.listener.pageScaleChange = (scale) => {
  // Update zoom slider
  document.querySelector('#zoom-slider').value = scale
  document.querySelector('#zoom-display').textContent = 
    `${Math.round(scale * 100)}%`
}

editor.listener.controlChange = ({ state, control }) => {
  if (state === ControlState.ACTIVE) {
    console.log('Control activated:', control.conceptId)
  }
}

editor.listener.saved = (result) => {
  // Send to server
  fetch('/api/save', {
    method: 'POST',
    body: JSON.stringify(result)
  }).then(() => {
    showNotification('Document saved successfully')
  })
}

Removing Listeners

To remove a listener, set it to null:
editor.listener.contentChange = null

Listener vs EventBus

Listener (callback-based):
  • Single callback per event
  • Simple assignment
  • Good for primary event handlers
EventBus (pub-sub based):
  • Multiple subscribers per event
  • Requires explicit subscription/unsubscription
  • Good for plugins and multiple components
See EventBus for the pub-sub alternative.

Build docs developers (and LLMs) love