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 { 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: any) => logEvent('cellClick', `row:${e.row} col:${e.col}`)), bus.on('cellChange', (e: any) => logEvent('cellChange', `[${e.row},${e.col}] "${e.oldValue}" → "${e.newValue}"`)), bus.on('selectionChange', (e: any) => logEvent('selectionChange', `row:${e.selection.activeRow} col:${e.selection.activeCol}`)), bus.on('scroll', (e: any) => logEvent('scroll', `top:${Math.round(e.scrollTop)} left:${Math.round(e.scrollLeft)}`)), bus.on('sortChange', (e: any) => 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 eventconst unsubscribe = engine.on('cellChange', (event) => { console.log(`Cell [${event.row}, ${event.col}] changed`); console.log(`Old: ${event.oldValue}, New: ${event.newValue}`);});
// Unsubscribe when doneunsubscribe();Event Reference
Section titled “Event Reference”Cell Events
Section titled “Cell Events”| Event | Payload | Description |
|---|---|---|
cellClick | { row, col, originalEvent } | Single click on a cell |
cellDoubleClick | { row, col, originalEvent } | Double-click on a cell (opens editor) |
cellChange | { row, col, oldValue, newValue } | Cell value changed after edit commit |
cellStatusChange | { row, col, status } | Cell status lifecycle: changed → saving → saved, error |
cellValidation | { row, col, valid, errors } | Validation result after cell edit |
Selection Events
Section titled “Selection Events”| Event | Payload | Description |
|---|---|---|
selectionChange | { activeCell, ranges, anchor } | Selection changed (click, keyboard, or programmatic) |
Scroll Events
Section titled “Scroll Events”| Event | Payload | Description |
|---|---|---|
scroll | { scrollTop, scrollLeft, viewport } | Scroll position changed |
Lifecycle Events
Section titled “Lifecycle Events”| Event | Payload | Description |
|---|---|---|
ready | { engine } | Engine initialization complete |
destroy | {} | Engine destroyed and resources cleaned up |
Command Events
Section titled “Command Events”| Event | Payload | Description |
|---|---|---|
commandExecute | { command } | A command was executed |
commandUndo | { command } | A command was undone |
commandRedo | { command } | A command was redone |
Clipboard Events
Section titled “Clipboard Events”| Event | Payload | Description |
|---|---|---|
clipboardCopy | { cells, text } | Cells copied to clipboard |
clipboardCut | { cells, text } | Cells cut to clipboard |
clipboardPaste | { cells, target } | Data pasted from clipboard |
Resize Events
Section titled “Resize Events”| Event | Payload | Description |
|---|---|---|
columnResizeStart | { col, width } | Column resize drag started |
columnResize | { col, width } | Column width changing during drag |
columnResizeEnd | { col, oldWidth, newWidth } | Column resize drag completed |
rowResizeStart | { row, height } | Row resize drag started |
rowResize | { row, height } | Row height changing during drag |
rowResizeEnd | { row, oldHeight, newHeight } | Row resize drag completed |
Autofill Events
Section titled “Autofill Events”| Event | Payload | Description |
|---|---|---|
autofillStart | { startCell, direction } | Fill handle drag started |
autofillPreview | { range, values } | Preview values during drag |
autofillComplete | { range, values } | Fill operation completed |
Sort & Filter Events
Section titled “Sort & Filter Events”| Event | Payload | Description |
|---|---|---|
sortChange | { columns } | Sort configuration changed |
sortRejected | { column, reason } | Sort request rejected |
filterChange | { filters } | Filter configuration changed |
Group Events
Section titled “Group Events”| Event | Payload | Description |
|---|---|---|
rowGroupToggle | { groupKey, expanded } | Row group expanded/collapsed |
rowGroupChange | { groups } | 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 and cellDoubleClick.
interface CellEvent { row: number; col: number; value: CellValue; column: ColumnDef;}CommandEvent
Section titled “CommandEvent”Fired on commandExecute, commandUndo, and commandRedo.
interface CommandEvent { description: string;}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;}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.columns }} onFilterChange={(event: FilterChangeEvent) => { // event.filters }} onReady={() => { // Table mounted and ready }}/>