Events
Events
Section titled “Events”EventBus
Section titled “EventBus”All inter-subsystem communication in @witqq/spreadsheet goes through a typed EventBus. It implements a publish/subscribe pattern with type-safe event names and payloads.
View source code
import { useRef, useEffect, useState, useCallback } from 'react';import { Spreadsheet } from '@witqq/spreadsheet-react';import type { SpreadsheetRef } from '@witqq/spreadsheet-react';import type { CellEvent, CellChangeEvent, SelectionChangeEvent, ScrollEvent, SortChangeEvent,} from '@witqq/spreadsheet';import { DemoWrapper } from './DemoWrapper';import { DemoButton } from './DemoButton';import { generateEmployees, employeeColumns } from './generate-data';import { useSiteTheme } from './useSiteTheme';
const data = generateEmployees(30);const sortableColumns = employeeColumns.map((col) => ({ ...col, sortable: true }));
const EVENT_COLORS: Record<string, string> = { cellClick: '#2563eb', cellChange: '#16a34a', selectionChange: '#9333ea', scroll: '#64748b', sortChange: '#ea580c',};
interface EventEntry { time: string; name: string; detail: string;}
export function EventBusDemo() { const { witTheme } = useSiteTheme(); const tableRef = useRef<SpreadsheetRef>(null); const logRef = useRef<HTMLDivElement>(null); const [events, setEvents] = useState<EventEntry[]>([]);
const clearLog = useCallback(() => setEvents([]), []);
useEffect(() => { const engine = tableRef.current?.getInstance(); if (!engine) return; const bus = engine.getEventBus();
const logEvent = (name: string, detail: string) => { const time = new Date().toLocaleTimeString('en', { hour12: false }); setEvents((prev) => [...prev.slice(-49), { time, name, detail }]); };
const unsubs = [ bus.on('cellClick', (e: CellEvent) => logEvent('cellClick', `row:${e.row} col:${e.col}`)), bus.on('cellChange', (e: CellChangeEvent) => logEvent('cellChange', `[${e.row},${e.col}] "${e.oldValue}" → "${e.newValue}"`), ), bus.on('selectionChange', (e: SelectionChangeEvent) => logEvent('selectionChange', `row:${e.selection.activeRow} col:${e.selection.activeCol}`), ), bus.on('scroll', (e: ScrollEvent) => logEvent('scroll', `top:${Math.round(e.scrollTop)} left:${Math.round(e.scrollLeft)}`), ), bus.on('sortChange', (e: SortChangeEvent) => logEvent('sortChange', `${e.sortColumns.length} column(s)`), ), ];
return () => unsubs.forEach((fn) => fn()); }, []);
useEffect(() => { if (logRef.current) { logRef.current.scrollTop = logRef.current.scrollHeight; } }, [events]);
return ( <DemoWrapper height={500} title="Live Demo" description="Interact with the table — click cells, edit values, sort columns, scroll. Events appear in the log below." > <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ flex: 1, minHeight: 0 }}> <Spreadsheet theme={witTheme} ref={tableRef} columns={sortableColumns} data={data} showRowNumbers editable style={{ width: '100%', height: '100%' }} /> </div> <div style={{ borderTop: '1px solid var(--sl-color-gray-5)' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 8px', background: 'var(--sl-color-gray-6)', fontSize: 11, }} > <span style={{ fontWeight: 600, color: 'var(--sl-color-white)' }}> Event Log ({events.length}) </span> <DemoButton onClick={clearLog} style={{ padding: '2px 10px', fontSize: '0.72rem' }}> Clear </DemoButton> </div> <div ref={logRef} style={{ height: 120, overflowY: 'auto', background: 'var(--sl-color-gray-7, var(--sl-color-gray-6))', padding: 8, fontFamily: 'monospace', fontSize: 11, }} > {events.length === 0 && ( <div style={{ color: 'var(--sl-color-gray-3)', fontStyle: 'italic' }}> No events yet. Click a cell, edit a value, or scroll the table. </div> )} {events.map((evt, i) => ( <div key={i} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', lineHeight: '18px', }} > <span style={{ color: 'var(--sl-color-gray-3)' }}>[{evt.time}]</span>{' '} <span style={{ color: EVENT_COLORS[evt.name] || 'var(--sl-color-white)', fontWeight: 600, }} > {evt.name} </span> {': '} <span style={{ color: 'var(--sl-color-gray-2)' }}>{evt.detail}</span> </div> ))} </div> </div> </div> </DemoWrapper> );}const engine = tableRef.current?.getInstance();
// Subscribe to an eventengine.on('cellChange', (event) => { console.log(`Cell [${event.row}, ${event.col}] changed`); console.log(`Old: ${event.oldValue}, New: ${event.newValue}`);});
// Unsubscribe when doneengine.off('cellChange', handler);Event Reference
Section titled “Event Reference”Cell Events
Section titled “Cell Events”| Event | Payload | Description |
|---|---|---|
cellClick | { row, col, value, column, hitZone? } | Single click on a cell |
cellDoubleClick | { row, col, value, column, hitZone? } | Double-click on a cell (opens editor) |
cellHover | { row, col, value, column, hitZone? } | Mouse moves over a cell (no buttons pressed) |
cellChange | { row, col, value, column, oldValue, newValue, source } | Cell value changed after edit commit |
cellStatusChange | { row, col, oldStatus, newStatus, errorMessage? } | Cell status lifecycle: changed → saving → saved, error |
cellValidation | { row, col, result } | Validation result after cell edit |
Selection Events
Section titled “Selection Events”| Event | Payload | Description |
|---|---|---|
selectionChange | { selection, previousSelection } | Selection changed (click, keyboard, or programmatic) |
Scroll Events
Section titled “Scroll Events”| Event | Payload | Description |
|---|---|---|
scroll | { scrollTop, scrollLeft } | Scroll position changed |
Lifecycle Events
Section titled “Lifecycle Events”| Event | Payload | Description |
|---|---|---|
ready | (none) | Engine initialization complete |
destroy | (none) | Engine destroyed and resources cleaned up |
Command Events
Section titled “Command Events”| Event | Payload | Description |
|---|---|---|
commandExecute | { description } | A command was executed |
commandUndo | { description } | A command was undone |
commandRedo | { description } | A command was redone |
Clipboard Events
Section titled “Clipboard Events”| Event | Payload | Description |
|---|---|---|
clipboardCopy | { rowCount, colCount } | Cells copied to clipboard |
clipboardCut | { rowCount, colCount } | Cells cut to clipboard |
clipboardPaste | { rowCount, colCount } | Data pasted from clipboard |
Resize Events
Section titled “Resize Events”| Event | Payload | Description |
|---|---|---|
columnResizeStart | { colIndex } | Column resize drag started |
columnResize | { colIndex, oldWidth, newWidth } | Column width changing during drag |
columnResizeEnd | { colIndex, oldWidth, newWidth } | Column resize drag completed |
rowResizeStart | { rowIndex } | Row resize drag started |
rowResize | { rowIndex, oldHeight, newHeight } | Row height changing during drag |
rowResizeEnd | { rowIndex, oldHeight, newHeight } | Row resize drag completed |
Autofill Events
Section titled “Autofill Events”| Event | Payload | Description |
|---|---|---|
autofillStart | { sourceRange } | Fill handle drag started |
autofillPreview | { sourceRange, fillRange, direction } | Preview values during drag |
autofillComplete | { sourceRange, fillRange, direction } | Fill operation completed |
Sort & Filter Events
Section titled “Sort & Filter Events”| Event | Payload | Description |
|---|---|---|
sortChange | { sortColumns } | Sort configuration changed |
sortRejected | { reason } | Sort request rejected |
filterChange | { visibleRowCount, totalRowCount } | Filter configuration changed |
Group Events
Section titled “Group Events”| Event | Payload | Description |
|---|---|---|
rowGroupToggle | { headerRow, expanded } | Row group expanded/collapsed |
rowGroupChange | { groupHeaders } | Row grouping configuration changed |
Theme Events
Section titled “Theme Events”| Event | Payload | Description |
|---|---|---|
themeChange | { theme } | Theme changed via setTheme() |
Event Types Reference
Section titled “Event Types Reference”TypeScript interfaces for all event payloads. Import from @witqq/spreadsheet.
CellEvent
Section titled “CellEvent”Fired on cellClick, cellDoubleClick, and cellHover. When a cell type renderer declares hit zones via getHitZones(), the hitZone field contains the matched zone ID.
interface CellEvent { row: number; col: number; value: CellValue; column: ColumnDef; hitZone?: string;}CellChangeEvent
Section titled “CellChangeEvent”Fired on cellChange. Extends CellEvent with old/new value tracking.
interface CellChangeEvent extends CellEvent { oldValue: CellValue; newValue: CellValue; source: string;}CommandEvent
Section titled “CommandEvent”Fired on commandExecute, commandUndo, and commandRedo.
interface CommandEvent { description: string;}SelectionChangeEvent
Section titled “SelectionChangeEvent”Fired on selectionChange.
interface SelectionChangeEvent { selection: Selection; previousSelection: Selection;}ClipboardDataEvent
Section titled “ClipboardDataEvent”Fired on clipboardCopy, clipboardCut, and clipboardPaste.
interface ClipboardDataEvent { rowCount: number; colCount: number;}ColumnResizeEvent
Section titled “ColumnResizeEvent”Fired on columnResize and columnResizeEnd.
interface ColumnResizeEvent { colIndex: number; oldWidth: number; newWidth: number;}RowResizeEvent
Section titled “RowResizeEvent”Fired on rowResize and rowResizeEnd.
interface RowResizeEvent { rowIndex: number; oldHeight: number; newHeight: number;}CellStatusChangeEvent
Section titled “CellStatusChangeEvent”Fired on cellStatusChange when a cell’s tracking status transitions (e.g. changed → saving → saved).
interface CellStatusChangeEvent { row: number; col: number; oldStatus: CellMetadata['status'] | undefined; newStatus: CellMetadata['status'] | undefined; errorMessage?: string;}CellValidationEvent
Section titled “CellValidationEvent”Fired on cellValidation after a cell edit is validated against column/cell rules.
interface CellValidationEvent { row: number; col: number; result: ValidationResult;}GridMouseEvent
Section titled “GridMouseEvent”Fired on internal gridMouseDown, gridMouseMove, gridMouseUp, gridMouseHover, and gridContextMenu.
interface GridMouseEvent extends HitTestResult { readonly originalEvent: MouseEvent; readonly shiftKey: boolean; readonly ctrlKey: boolean;}GridKeyboardEvent
Section titled “GridKeyboardEvent”Fired on internal gridKeyDown.
interface GridKeyboardEvent { readonly originalEvent: KeyboardEvent; readonly key: string; readonly shiftKey: boolean; readonly ctrlKey: boolean;}RowGroupToggleEvent
Section titled “RowGroupToggleEvent”Fired on rowGroupToggle when a row group is expanded or collapsed.
interface RowGroupToggleEvent { readonly headerRow: number; readonly expanded: boolean;}RowGroupChangeEvent
Section titled “RowGroupChangeEvent”Fired on rowGroupChange when the set of group headers changes.
interface RowGroupChangeEvent { readonly groupHeaders: readonly number[];}ScrollEvent
Section titled “ScrollEvent”Fired on scroll when the viewport scroll position changes.
interface ScrollEvent { scrollTop: number; scrollLeft: number;}SortRejectedEvent
Section titled “SortRejectedEvent”Fired on sortRejected when a sort request is blocked (e.g. merged regions exist).
interface SortRejectedEvent { readonly reason: 'merged-regions-exist';}FilterChangeEvent
Section titled “FilterChangeEvent”Fired on filterChange after filters are applied or cleared.
interface FilterChangeEvent { readonly visibleRowCount: number; readonly totalRowCount: number;}AutofillStartEvent
Section titled “AutofillStartEvent”Fired on autofillStart when the user begins dragging the fill handle.
interface AutofillStartEvent { sourceRange: CellRange;}AutofillPreviewEvent
Section titled “AutofillPreviewEvent”Fired on autofillPreview during fill handle drag.
interface AutofillPreviewEvent { sourceRange: CellRange; fillRange: CellRange | null; direction: FillDirection | null;}AutofillCompleteEvent
Section titled “AutofillCompleteEvent”Fired on autofillComplete when the fill operation is committed.
interface AutofillCompleteEvent { sourceRange: CellRange; fillRange: CellRange; direction: FillDirection;}SortChangeEvent
Section titled “SortChangeEvent”Fired on sortChange when sort configuration changes.
interface SortChangeEvent { readonly sortColumns: readonly { col: number; direction: 'asc' | 'desc' }[];}EventTranslator
Section titled “EventTranslator”The EventTranslator converts raw DOM events (mouse clicks, touch, keyboard) into cell-level events. It performs hit-testing to determine which cell or region was interacted with.
Regions identified by EventTranslator:
| Region | Area | Actions |
|---|---|---|
cell | Data cells in the grid body | Click, edit, select |
header | Column header row | Sort toggle, filter open, resize |
row-number | Row number column on the left | Row selection |
corner | Top-left corner (row numbers × header) | Select all |
Touch Events
Section titled “Touch Events”On touch devices, the EventTranslator maps gestures:
| Gesture | Action |
|---|---|
| Tap | Select cell |
| Double-tap | Open inline editor |
| Scroll | Native scroll (CSS touch-action: pan-x pan-y) |
React Event Callbacks
Section titled “React Event Callbacks”In the React wrapper, events are exposed as props:
<Spreadsheet<Row> columns={columns} data={data} onCellChange={(event: CellChangeEvent) => { // event.row, event.col, event.oldValue, event.newValue }} onSelectionChange={(event: SelectionChangeEvent) => { // event.selection }} onSortChange={(event: SortChangeEvent) => { // event.sortColumns }} onFilterChange={(event: FilterChangeEvent) => { // event.visibleRowCount, event.totalRowCount }} onReady={() => { // Table mounted and ready }}/>