Range selection, fill handle and clipboard
Three behaviours, one switch. There is no rangeSelection, fillHandle or
clipboard prop — all of it is editing.range, and it rides on the editing
layer:
import { EnterpriseDataGrid } from '@kanunilabs/datagrid-react-enterprise';
<EnterpriseDataGrid
licenseKey={key}
dataSource={rows}
rowKey="id"
columns={columns}
editing={{ mode: 'cell', range: true }}
/>
range defaults to true, so enabling editing already gives you the
rectangle. Turning editing off turns range off with it: internally it resolves
to editingEnabled && (editing.range ?? true).
What the user can do
| Gesture | Result |
|---|---|
| Drag from a cell | Draws the rectangle |
| Shift-click | Extends it to the clicked cell |
| Ctrl-click or Ctrl-drag (Cmd on a Mac) | Adds another rectangle; the ones already selected stay |
| Shift+arrow | Grows it one cell from its anchor (from the focused cell when there is none) |
| Ctrl+Shift+arrow | Grows it to the first or last row or column |
| Drag the small square on the anchor cell | Fill handle — extends the values |
| Ctrl+D | Fills down: copies the rectangle's first row into the rows below it — with one cell, copies the cell above |
| Ctrl+R | Fills right: copies the first column into the columns to its right — with one cell, copies the cell to the left |
| Ctrl+C | Copies the rectangle as TSV |
| Ctrl+X | Copies, then clears the cells the user may write |
| Ctrl+V | Pastes from the rectangle's top-left cell — or from the focused cell when there is no rectangle. A rectangle bigger than the clipboard gets the clipboard repeated to fill it |
| Arrow / Home / End / PageUp / PageDown / Tab | Collapses the selection — every rectangle |
For the arrow keys, Home and PageUp/PageDown that last row is Excel's rule, and it surprises people who expect "select, then nudge with the arrows". (Tab differs from Excel, which walks the active cell through the selection without collapsing it.)
Several rectangles
Hold Ctrl (Cmd on a Mac, where Ctrl-click is the right-click) and click or drag to add a rectangle; a plain click starts over with one. What each gesture does with several:
| Gesture | With several rectangles |
|---|---|
| Statistics | Count and total every rectangle; a cell two of them share counts once |
| Ctrl+C | Rectangles over the same columns copy as one block, top to bottom; over the same rows, side by side. Rectangles that do not line up have no single block to paste back, so only the newest is copied |
| Ctrl+X | Clears every rectangle (the cells the user may write) |
| Ctrl+V, Shift+arrow, Ctrl+D / R, "Chart selected cells" | Act on the newest rectangle |
| Fill handle | Hidden — a fill copies one block, and a handle on one of several would hide which |
Whole columns from the header
Turn on columnSelection and a click on a column header selects that column
— every row, top to bottom — as one rectangle, the way clicking a column letter
does in a spreadsheet:
<EnterpriseDataGrid columnSelection />
The header is tinted while its column is selected, and everything above works on it: the statistics total the column, Ctrl+C copies it, Ctrl+X clears it, "Chart selected cells" charts it.
| Gesture | Does |
|---|---|
| Click a header | Selects that column; the other rectangles go |
| Shift+click | Widens the newest rectangle to every column between it and this one, all rows |
| Ctrl+click (Cmd on a Mac) | Adds the column and keeps the rest |
| Ctrl+Space | Selects the column of the focused cell, or of the focused header |
The header click stops sorting while this is on — one click cannot mean both. Sort from the column menu (right-click the header), or focus the header and press Enter. The grid's own columns (selection checkbox, row numbers, commands) and the group column still sort or do nothing, as before.
A column means the rows the grid shows now. Clear a filter afterwards and the rectangle still covers the rows it had; the header stops showing as selected, because the column now holds more than that.
Which cells may change
allowUpdating is the single gate for editing, fill, paste and cut:
editing={{
mode: 'cell',
allowUpdating: (row, columnId) => columnId !== 'id' && !row?.locked,
}}
A cell the predicate refuses — or a column with editable: false — is still
copied; it is just not cleared, pasted over or filled. The rest of the
gesture goes ahead: one read-only column inside a paste does not cancel it.
Validation rules run when the change is saved, not during the gesture. Cut
writes null, so a required-field rule refuses the save, the grid shows why,
and the change stays pending until it is corrected or undone with
Ctrl+Z.
Watching it happen
Gestures are published as a bracket — a Start and an End. Once a Start has
fired, its End always follows, even when the gesture wrote nothing or threw:
onReady={(controller) => {
controller.events.on('fillStart', (e) => console.log('filling', e.cellCount));
controller.events.on('fillEnd', (e) => console.log('done', e.performed));
controller.events.on('cellValueChanged', (e) => {
// 'edit' | 'cut' | 'fill' | 'paste' | 'undo' | 'redo'
if (e.source === 'paste') audit(e.rowKey, e.columnId, e.value);
});
}}
The pairs are cutStart/cutEnd, copyStart/copyEnd,
pasteStart/pasteEnd and fillStart/fillEnd. A whole paste is one undo
step, not one per cell.
Selection statistics
Set statusBar and the rectangle reports itself — cells, sum, average, min,
max:
statusBar={{ items: ['totalRows', 'selectedRows', 'rangeStats'] }}
Without a status bar the stats appear in their own strip instead, so the numbers show up exactly once either way. Non-numeric cells are skipped, not counted as zero.
Clipboard callbacks
By default a copy writes what the cells show — the lookup label and the
valueFormatter text; a renderCell drawing can be anything, so it is not
copied — and a paste reads the text as the column's type. The clipboard option lets your code change both — for
every copy in the grid: a cell range, a cut, the selected rows, "Copy with
headers".
<EnterpriseDataGrid
clipboard={{
// A price shown through a valueFormatter: copy the number, not "$1,234.50".
processCellForClipboard: ({ columnId, value, text }) =>
columnId === 'price' ? String(value) : text,
processHeaderForClipboard: ({ text }) => text.toUpperCase(),
// Drop empty lines and trim before anything is written.
processDataFromClipboard: ({ data }) =>
data.filter((row) => row.some((cell) => cell.trim() !== '')).map((row) => row.map((c) => c.trim())),
// Read "12,5" as 12.5 in a Turkish spreadsheet's paste.
processCellFromClipboard: ({ columnId, text, value }) =>
columnId === 'price' ? Number(text.replace(/\./g, '').replace(',', '.')) : value,
}}
/>
| Callback | Gets | Returns |
|---|---|---|
processCellForClipboard | row, rowKey, columnId, value, text (shown) | The text to copy |
processHeaderForClipboard | columnId, text | The header text to copy |
processDataFromClipboard | data — rows of cell texts | The table to paste, or null to paste nothing |
processCellFromClipboard | row, rowKey, columnId, text (pasted), value (what the grid would write) | The value to write |
sendToClipboard | text, kind ('copy' or 'cut') | Nothing — the grid leaves the system clipboard alone |
Tabs and line breaks in a copied cell become spaces, whatever the callback
returns: they would split the cell in a spreadsheet. A pasted cell still goes
through the permission check first (editable, allowUpdating) and validation
after; the callback chooses the value, not whether the cell may change. With
sendToClipboard the text stays in your app — keep it for an in-app paste, or
write HTML beside it. The copyStart / copyEnd events fire either way.
What this does not do
Written down because finding out mid-integration costs more than reading it here.
- The fill handle itself is mouse-only. It is
aria-hidden; from the keyboard, Ctrl+D and Ctrl+R (Cmd on a Mac) fill the way a spreadsheet does — they COPY the first row or column, they do not extend a numeric series the way a drag does. They write through the same path as the handle: the sameallowUpdatingcheck, one undo step. With nothing to copy from (one cell in the first row or column) the key is left to the browser. The rectangle is built with Shift+arrow, and the statistics strip is a polite live region, so a screen reader hears the cell count and sum as it grows. With range selection on, Shift+Up/Down extends the rectangle, not the row selection, and Ctrl+A still selects rows. - Several rectangles are a selection, not a shape. Paste, fill and the chart take the newest one; copy joins them only when they line up (see Several rectangles). There is no "add mode" key (Excel's Shift+F8), so adding a rectangle needs the mouse.
- Paste needs the async clipboard. It reads
navigator.clipboard.readText()with noclipboardDatafallback, so it needs a secure context and whatever permission or confirmation the browser asks for. Where the browser refuses, paste does nothing. - Paste crops, it never grows the grid. Copy 100 rows onto the last row and only the rows that fit are written.
- Type coercion covers numbers and booleans. A pasted date is written as text; so is a number the parser cannot read.
- Fill extrapolates evenly spaced numbers only. Two or more numbers with a
constant step become an arithmetic sequence. Unevenly spaced numbers, text,
dates, booleans and blanks repeat the pattern. No date series, no month
names, no
Item 1 → Item 2. Fill writes values, never formats. - Fill is single-axis, as in Excel. A diagonal drag resolves to whichever direction moved further.
- Copied text is what the user sees — the lookup label, then the
valueFormatter. That is one-way: pastingAntalyaback does not turn it into the stored code. - Sorting or filtering does not clear the rectangle. Range coordinates are
view indices and only
columnStateChangedclears them, so after a sort the rectangle sits on the same positions over different rows. Clear it yourself if that matters. - No event for the rectangle.
selectionChangedis row selection; there is norangeSelectionChanged. You can observe the gestures, not the shape. - Statistics stop at 100,000 cells and report
truncatedinstead. - Community has none of it — no rectangle, no fill, no cut, no paste. Community copies whole rows with Ctrl+C.
Without a licence key the features still run, watermarked. The boundary is distribution and the watermark, not a code path that refuses you.