Range Selection
Excel-style cell ranges: several at once, a fill handle that reads a series out of numbers, dates or text, writing a whole range at once, cut and move, and a clipboard that carries both plain text and HTML.
Basic Usage
Drag across the cells below to select a rectangle. Shift-click extends it, Shift with the arrow keys extends it from the keyboard, and holding Ctrl or Cmd while dragging starts a second range without losing the first.
import { createDataGrid, editing, rangeSelection } from '@sv5ui/datagrid';
const grid = createDataGrid<Week>({
columns,
data: weeks,
getRowId: (row) => String(row.id),
features: [
editing(), // ranges write through the edit stack, so undo works
rangeSelection({ multiple: true })
]
});The Fill Handle
The small square at the trailing corner of a range is the fill handle. Drag it and the grid continues whatever series it can read out of the selection. Select W1 on the first two rows below, which holds 40 and 53, and drag the handle down two rows: the grid reads the step as 13 and writes 66 and 79 over what was there. Or select W1 and W2 on one row and drag the handle right instead.
rangeSelection({ fill: true });
const range = getRangeSelection(grid)!;
range.fillWithin('down'); // Ctrl+D: copy the top row down the selection
range.fillWithin('right'); // Ctrl+R: copy the leftmost column across
// The handle is the other operation: it continues a series PAST the
// selection, reading the step from the cells in it.
continueSeries([1, 2], 3); // [3, 4, 5]
continueSeries([0, 5], 3); // [10, 15, 20]
continueSeries([3, 1], 2); // [-1, -3]
continueSeries([7], 3); // [7, 7, 7]
continueSeries([jan(1), jan(2)], 2); // [Jan 3, Jan 4]
continueSeries(['Item 1', 'Item 2'], 2); // ['Item 3', 'Item 4']What Counts as a Series
Read from the cells in the selection, in the direction the handle is dragged.
| Selection | What follows |
|---|---|
1, 2 | continues 3, 4, 5. The step is read from the selection, not assumed to be one |
0, 5 | continues 10, 15, 20 |
3, 1 | continues -1, -3, because the step may be negative |
7 | repeats 7, 7, 7. One value is not a series, so it is copied rather than counted |
Jan 1, Jan 2 | continues Jan 3, Jan 4, rolling into the next month and knowing which years are leap |
Item 1, Item 2 | continues Item 3, Item 4: a number inside text still counts |
Ctrl+D and Ctrl+R fill within it: they take the top row, or the leftmost column, and copy it
across the rest of the selection, the way a spreadsheet does. Selecting 40 and 53 and
pressing Ctrl+D gives 40 and 40, not 40 and 66. Writing a Whole Range
Select a rectangle, start typing, and Ctrl+Enter writes the value into every cell of it. It is one transaction, so one undo takes it all back
rather than one cell at a time.
rangeSelection({ rangeEdit: true });
const range = getRangeSelection(grid)!;
// Select a rectangle, type, and Ctrl+Enter writes it into every cell. One
// transaction, so one undo takes it all back.
range.applyToRange('pending');
grid.api.undo?.();
// A cell the column will not accept is skipped rather than failing the batch.Cut and Move
Ctrl+X marks the range with a dashed border rather than clearing it: the values stay where they are until
the move lands, so an interrupted cut loses nothing. Click another cell, Ctrl+V,
and the block moves, leaving blanks behind it.
rangeSelection({ cut: true });
const range = getRangeSelection(grid)!;
range.cutRange(); // Ctrl+X: a dashed border, values left in place
range.moveRange(4, 2); // Ctrl+V at a new anchor: the block moves
range.cancelCut(); // Escape
range.cutSource; // the CellRange lifted, or null
range.moveTarget; // where it would landThe Clipboard Carries Two Things
A copy writes TSV, which is what a spreadsheet pastes from, and HTML alongside it, so pasting into a document keeps the table shape rather than arriving as one line of text. The receiving application picks whichever it understands.
const range = getRangeSelection(grid)!;
await range.copyRange(); // TSV and HTML together
await range.copyRange({ headers: true });
await range.copyRange({ formatted: true }); // what the cell draws
await range.copyRange({ html: false }); // plain text only
// formatted is false by default, so a copy carries raw values and a number
// pastes into a spreadsheet as a number.
range.getRangeMatrix(); // string[][]
range.getRangeTsv(); // '40\t35\n53\t64'
await range.pasteFromClipboard();
range.pasteText('a\tb\nc\td');
// A block larger than the selection grows it to fit; one cell selected takes
// the whole block.The Range Status Bar
Count, shape, and the sum, average, minimum and maximum of whatever numbers the selection holds. Select some numbers below and it fills in; select the SKU column and it says nothing, because a column of text has no sum and reading zero would be worse than reading nothing.
<script lang="ts">
import { DataGrid, Grid } from '@sv5ui/datagrid';
const money = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
</script>
<DataGrid {grid} />
<Grid.RangeStatusBar {grid} />
<Grid.RangeStatusBar {grid} format={(value) => money.format(value)} />Alongside Row Selection
Ranges and row selection are different things, and both can be registered. selection() owns whole rows and the checkbox column; rangeSelection() owns rectangles of cells. Neither reads the other's state, so ticking rows below leaves the
range where it is.
const grid = createDataGrid<Week>({
columns,
data: weeks,
getRowId,
features: [
selection(), // whole rows, and the checkbox column
rangeSelection(), // rectangles of cells
editing() // what a range writes through
]
});
const rows = getSelection(grid)!.selectedIds;
const cells = getRangeSelection(grid)!.cellCount;
// Neither reads the other's state. With policy() registered, a masked cell
// copies masked and refuses to be written.Keyboard and Pointer
| Gesture | What it does |
|---|---|
Drag | Select a rectangle |
Shift + click | Extend the range to the cell clicked |
Shift + arrows | Extend it from the keyboard |
Ctrl / Cmd + drag | Start a second range without losing the first |
Ctrl / Cmd + C | Copy, as TSV and as HTML together |
Ctrl / Cmd + X | Mark the range for a move. The values stay until it lands |
Ctrl / Cmd + V | Paste, growing the selection to fit the block |
Ctrl / Cmd + Enter | Write the value being typed into every cell of the range |
Ctrl / Cmd + D | Copy the selection's top row down through the rest of it |
Ctrl / Cmd + R | Copy the selection's leftmost column across the rest of it |
Escape | Cancel a pending cut |
Options
Everything rangeSelection() accepts. All five are on by default, so turn one off to take a gesture away.
| Option | Description |
|---|---|
multiple | Several ranges at once, with Ctrl or Cmd |
fill | The fill handle at the trailing corner |
rangeEdit | Ctrl+Enter writes the whole range |
cut | Ctrl+X marks a range to be moved |
paste | Ctrl+V writes a block into the grid |
Ranges Over Rows the Server Owns
The grids above hold every row they draw. This one holds a hundred of five thousand, and a range is a rectangle of indices rather than a set of rows, so it can be drawn over rows that have not arrived. Both buttons select twenty salaries; only one of them selects twenty salaries that exist.
The status bar adds up what the cells hold, and a placeholder holds a zero, so the second
selection reads a sum of nothing over twenty people who are certainly paid. That is the
thing to know before wiring a range to anything that acts on it: keep the selection inside
what is loaded, or load the rest first with fetchAllServerRows.
No data
The last few requests
- Nothing yet. Scroll or select.
const grid = createDataGrid<Person>({
columns, data: [], getRowId,
rowModel: 'server',
features: [
rangeSelection(),
virtualization({ rowHeight: 40, overscan: 6 }),
serverRowModel(source, { mode: 'infinite', blockSize: 100, placeholder })
]
});
const range = getRangeSelection(grid)!;
range.startRange(0, 1);
range.extendTo(19, 1); // extendTo draws to an absolute cell; extendBy is the
// keyboard's, and measures from the focused cell
range.selectedValues; // the twenty salaries, because that block has arrived
range.startRange(1000, 1);
range.extendTo(1019, 1);
range.selectedValues; // twenty zeros: those rows have not been fetched
// A range is indices, not rows, so it can be drawn over rows that have not
// arrived. Those cells read as whatever the placeholder holds, and the sum
// in the status bar counts them, so a total can be quietly short.
const pending = countPlaceholders(grid, range.ranges);
// Which is worth knowing before wiring a range to anything that acts on it.
// The honest options are to keep the range inside what is loaded, or to
// fetch the rest first:
await grid.api.fetchAllServerRows?.({ batchSize: 500 });Driving It From Code
import { getRangeSelection } from '@sv5ui/datagrid';
const range = getRangeSelection(grid)!;
range.ranges; // CellRange[]: { top, left, bottom, right }
range.primary; // the one the keyboard is working in
range.bounds; // one rectangle covering them all
range.cellCount; // 9
range.shape; // { rows: 3, cols: 3 }
range.selectedValues; // [40, 35, 50, 53, 64, 57, 66, 93, 64]
range.selectAll();
range.clear();
range.isSelected(2, 1);
range.extendBy(1, 0);
range.startRange(2, 1, { additive: true });
// row and col are absolute indices within the filtered and sorted set, the
// same numbers data-dg-cell carries.