ReoGrid ReoGrid Web

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.

Live Demo

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 SelectionRange state machine backs the handle, but it is @internal and is not part of the public API. Always use worksheet.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.

GestureResult
Ctrl/Cmd + click or dragAdds a range; it becomes the active one
Plain click, or an arrow keyCollapses back to a single range
Shift + click, Shift + ArrowExtends 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>
  )
}
Was this page helpful?
Stay Updated

Be first to know — get updates as they ship

Get notified of new releases, features, and announcements.
No spam — just updates that matter.