Cells

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.

SKU
W1
W2
W3
W4
DG-100
40
35
62
51
DG-210
53
64
44
97
DG-315
61
78
88
46
DG-420
47
52
39
74
DG-560
92
81
70
58
DG-675
45
40
95
83
DG-780
58
69
57
42
DG-885
71
98
66
90
8 rows
0 cells in 0 ranges
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.

SKU
W1
W2
W3
W4
DG-100
40
35
62
51
DG-210
53
64
44
97
DG-315
61
78
88
46
DG-420
47
52
39
74
DG-560
92
81
70
58
DG-675
45
40
95
83
DG-780
58
69
57
42
DG-885
71
98
66
90
8 rows
These copy within the selection. They are not the handle.
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.

SelectionWhat follows
1, 2continues 3, 4, 5. The step is read from the selection, not assumed to be one
0, 5continues 10, 15, 20
3, 1continues -1, -3, because the step may be negative
7repeats 7, 7, 7. One value is not a series, so it is copied rather than counted
Jan 1, Jan 2continues Jan 3, Jan 4, rolling into the next month and knowing which years are leap
Item 1, Item 2continues Item 3, Item 4: a number inside text still counts

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.

SKU
W1
W2
W3
W4
DG-100
40
35
62
51
DG-210
53
64
44
97
DG-315
61
78
88
46
DG-420
47
52
39
74
DG-560
92
81
70
58
DG-675
45
40
95
83
DG-780
58
69
57
42
DG-885
71
98
66
90
8 rows
Select some cells first.
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.

SKU
W1
W2
W3
W4
DG-100
40
35
62
51
DG-210
53
64
44
97
DG-315
61
78
88
46
DG-420
47
52
39
74
DG-560
92
81
70
58
DG-675
45
40
95
83
DG-780
58
69
57
42
DG-885
71
98
66
90
8 rows
nothing cut
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 land

The 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.

SKU
W1
W2
W3
W4
DG-100
40
35
62
51
DG-210
53
64
44
97
DG-315
61
78
88
46
DG-420
47
52
39
74
DG-560
92
81
70
58
DG-675
45
40
95
83
DG-780
58
69
57
42
DG-885
71
98
66
90
8 rows
<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.

SKU
W1
W2
W3
W4
DG-100
40
35
62
51
DG-210
53
64
44
97
DG-315
61
78
88
46
DG-420
47
52
39
74
DG-560
92
81
70
58
DG-675
45
40
95
83
DG-780
58
69
57
42
DG-885
71
98
66
90
8 rows
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

GestureWhat it does
DragSelect a rectangle
Shift + clickExtend the range to the cell clicked
Shift + arrowsExtend it from the keyboard
Ctrl / Cmd + dragStart a second range without losing the first
Ctrl / Cmd + CCopy, as TSV and as HTML together
Ctrl / Cmd + XMark the range for a move. The values stay until it lands
Ctrl / Cmd + VPaste, growing the selection to fit the block
Ctrl / Cmd + EnterWrite the value being typed into every cell of the range
Ctrl / Cmd + DCopy the selection's top row down through the rest of it
Ctrl / Cmd + RCopy the selection's leftmost column across the rest of it
EscapeCancel a pending cut

Options

Everything rangeSelection() accepts. All five are on by default, so turn one off to take a gesture away.

OptionDescription
multipleSeveral ranges at once, with Ctrl or Cmd
fillThe fill handle at the trailing corner
rangeEditCtrl+Enter writes the whole range
cutCtrl+X marks a range to be moved
pasteCtrl+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.

POST /datagrid/api/people ? rows on the server 0 cells not loaded yet
Progress
Rating

No data

0 rows

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.