Selection & Events
ReoGrid Web provides a full set of APIs for working with cell selection, subscribing to events, controlling scroll position, and performing hit testing.
Selection Range Operations
worksheet.selection returns a SelectionHandle — a thin façade over the active selection that exposes both reads (bounds, active cell, range view) and mutations (style operations, value, move).
const ws = grid.worksheet
// Move the selection — numeric coordinates
ws.selection.moveTo(0, 0, 4, 3) // topRow, leftColumn, bottomRow, rightColumn
// Or by A1-style address
ws.selection.moveTo('A1:D5')
// Read current bounds (null when nothing is selected)
const b = ws.selection.bounds
if (b) {
console.log(b.topRow, b.leftColumn, b.bottomRow, b.rightColumn)
}
// Active (focused) cell within the selection
const active = ws.selection.activeCell // { row, column } | null
// Convert the selection to a RangeHandle for full range operations
const range = ws.selection.range
range?.setBold().setBackgroundColor('#fef3c7')
// Check whether a given cell is the active one
const isActive = ws.isActiveCell(0, 0)
An internal
SelectionRangestate machine backs the handle, but it is@internaland is not part of the public API. Always useworksheet.selection.
Multi-Range Selection
Since v1.6.0, holding Ctrl (Cmd on macOS, where Ctrl+click is a right click) while clicking or dragging adds a range to the selection instead of replacing it — on cells and on row and column headers alike. Available in both editions.
| Gesture | Result |
|---|---|
| Ctrl/Cmd + click or drag | Adds a range; it becomes the active one |
| Plain click, or an arrow key | Collapses back to a single range |
| Shift + click, Shift + Arrow | Extends the newest range, keeping the others |
Every operation that acts on “the selection” covers all of it: Delete clears every range as one undoable step, styles apply to all of them, and double-clicking the border of Ctrl+clicked columns auto-fits each one (and nothing in between).
const ws = grid.worksheet
ws.selection.moveTo('A1:B2')
ws.selection.add('D4:E6') // two ranges selected — the API form of Ctrl+click
ws.selection.count // 2
ws.selection.ranges // RangeHandle[] — active range last
// Apply an operation to everything the user selected
ws.selection.ranges.forEach(r => r.setBold())
bounds / range / activeCell / moveTo keep their single-range meaning — they refer to the active range, which is the last one selected. ranges is therefore the safe way to act on “what the user selected”, since a single-range selection yields one handle.
Copy and cut
Copy follows Excel’s rule: the ranges have to line up — stacked with the same columns, or side by side with the same rows — to travel as one block (values, styles and number formats included). Anything else is refused, with Excel’s own explanation shown on the cell.
While several ranges are selected
The fill handle and drag-to-move are switched off, and Ctrl+clicking an already-selected cell adds rather than removes it.
Try it on the multi-range selection demo.
Events
All event methods return an unsubscribe function.
Selection Change
// The listener receives the active selection range — { row, col, rows, columns }
// (top-left cell + extents) — or null when nothing is selected
const unsub = ws.onSelectionChange((range) => {
if (range) {
const col = String.fromCharCode(65 + range.col)
console.log(`Selected: ${col}${range.row + 1} (${range.rows} x ${range.columns})`)
}
})
// Unsubscribe
unsub()
Since v1.6.0 a second argument lists every selected range, so a listener can react to a multi-range selection. Existing single-argument listeners are unaffected — the first argument is still the active range.
ws.onSelectionChange((range, ranges) => {
console.log(`${ranges.length} range(s) selected`)
})
Cell Value Change
ws.onCellValueChange(({ row, column }) => {
console.log(`Cell changed: row=${row}, col=${column}`)
const value = ws.getCellInput(row, column)
console.log('New value:', value)
})
Context Menu
ws.onContextMenu((event) => {
// event.area: 'cell' | 'row-header' | 'column-header' | 'corner'
// event.row, event.column: click position
// event.originalEvent: browser MouseEvent
console.log('Context menu:', event.area, event.row, event.column)
})
Viewport Size Change
ws.onViewportSizeChange(({ width, height }) => {
console.log(`Viewport resized: ${width} x ${height}`)
})
Scroll Change
ws.onScrollChange(({ x, y }) => {
console.log(`Scroll position: x=${x}, y=${y}`)
})
Structure Change (Row/Column Insert/Delete)
ws.onStructureChange(() => {
console.log('Grid structure changed')
})
Image Change
ws.onImagesChange((images) => {
console.log('Images updated:', images.length)
})
Protected Cell Edit Attempt
ws.onProtectedCellEdit(({ row, column }) => {
alert(`Cell (${row}, ${column}) is protected`)
})
Scroll Operations
// Get scroll position
const offset = ws.getScrollOffset()
console.log(offset.x, offset.y)
// Set scroll position
ws.setScrollOffset(100, 200)
// Scroll content size
const size = ws.getScrollContentSize()
console.log(size.width, size.height)
// Viewport size
const bodyWidth = ws.getBodyViewportWidth()
const bodyHeight = ws.getBodyViewportHeight()
Hit Testing
Identify a cell position from screen coordinates.
// Simple hit test
const cell = ws.getCellFromPoint(x, y)
if (cell) {
console.log(`Row: ${cell.row}, Column: ${cell.column}`)
}
// Detailed hit test
const result = ws.hitTest(x, y)
x / y are canvas coordinates in 100% sheet px. When the sheet is zoomed (v1.7.0), divide a pointer position in CSS px by ws.getZoom() first.
Getting Cell Rectangles
// Get cell rectangle on screen
const rect = ws.getCellRect(0, 0)
// { x, y, width, height }
// Get range rectangle on screen
const rangeRect = ws.getRangeRect(0, 0, 4, 3)
The rectangles are in 100% sheet px. When the sheet is zoomed (v1.7.0), multiply them by ws.getZoom() to get CSS px on the canvas — see overlaying your own elements.
Example: Toolbar Integration
import { useState, useEffect } from 'react'
function Toolbar({ worksheet }) {
const [cellRef, setCellRef] = useState('A1')
const [cellValue, setCellValue] = useState('')
useEffect(() => {
if (!worksheet) return
return worksheet.onSelectionChange((range) => {
if (range) {
const col = String.fromCharCode(65 + range.col)
setCellRef(`${col}${range.row + 1}`)
setCellValue(worksheet.getCellInput(range.row, range.col) || '')
}
})
}, [worksheet])
return (
<div>
<span>{cellRef}</span>
<input
value={cellValue}
onChange={(e) => {
setCellValue(e.target.value)
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
const active = worksheet.selection.activeCell
if (active) {
worksheet.setCellInput(active.row, active.column, cellValue)
}
}
}}
/>
</div>
)
} Sorry to hear that. What could be improved?