# Cell selection & clipboard

> Select ranges of cells, copy and paste them as spreadsheet text, fill with a drag handle, and see live totals for a selection or the whole grid.

Cells select like a spreadsheet: click a cell, then drag or Shift-click to select a rectangle. A selection copies as tab-separated text that pastes into a spreadsheet, and text from a spreadsheet pastes back into editable cells. A footer shows live statistics for the selected numbers.

Drag across Price and Qty to see the sum and average below the grid, copy with Ctrl/⌘+C, or drag the small square at the selection's corner down to fill. The pinned last row totals every column.

Example: Range selection, fill and totals (selection.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef } from 'bo-grid';
  import { quotes } from './data';

  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 100 },
    { type: 'price', key: 'price', header: 'Price', editable: true, groupAgg: 'avg' },
    { type: 'number', key: 'qty', header: 'Qty', decimals: 0, editable: true, groupAgg: 'sum' },
    { type: 'percent', key: 'change', header: 'Chg %', groupAgg: 'avg' },
    { type: 'volume', key: 'volume', header: 'Volume', flex: 1, groupAgg: 'sum' },
  ];
  let rows = $state(quotes().map((q, i) => ({ ...q, qty: (i + 1) * 50 })));
</script>

<Grid
  {rows}
  {columns}
  height={300}
  fillHandle
  footer
  aggregations={['sum', 'avg', 'count']}
  onCellEdit={(e) => (e.row[e.column.key] = e.value)}
/>
```

data.ts:

```ts
// Small, fixed data for the docs' examples: every example reads the same
// symbols, so a reader compares features, not datasets.
// A type, not an interface: it then fits GridRow's index signature.
export type Quote = {
  id: number;
  symbol: string;
  name: string;
  sector: string;
  price: number;
  change: number;
  volume: number;
};

const BASE: Omit<Quote, 'id'>[] = [
  { symbol: 'VNM', name: 'Vinamilk', sector: 'Consumer', price: 68.5, change: 1.2, volume: 1_204_000 },
  { symbol: 'FPT', name: 'FPT Corp', sector: 'Technology', price: 132, change: -0.4, volume: 860_500 },
  { symbol: 'HPG', name: 'Hoa Phat', sector: 'Materials', price: 27.35, change: 0.74, volume: 3_412_900 },
  { symbol: 'VCB', name: 'Vietcombank', sector: 'Financials', price: 91.5, change: 0.55, volume: 1_986_200 },
  { symbol: 'MWG', name: 'Mobile World', sector: 'Consumer', price: 51.4, change: -1.1, volume: 2_250_400 },
  { symbol: 'SSI', name: 'SSI Securities', sector: 'Financials', price: 33.3, change: -0.45, volume: 4_104_700 },
  { symbol: 'VIC', name: 'Vingroup', sector: 'Real estate', price: 44.15, change: -0.11, volume: 1_530_100 },
  { symbol: 'GAS', name: 'PV Gas', sector: 'Energy', price: 71.8, change: 0.3, volume: 645_800 },
  { symbol: 'MSN', name: 'Masan', sector: 'Consumer', price: 76.2, change: 2.05, volume: 1_120_300 },
  { symbol: 'TCB', name: 'Techcombank', sector: 'Financials', price: 23.9, change: 0.84, volume: 5_870_000 },
  { symbol: 'VHM', name: 'Vinhomes', sector: 'Real estate', price: 41.6, change: -0.72, volume: 2_904_600 },
  { symbol: 'POW', name: 'PV Power', sector: 'Energy', price: 12.25, change: 1.66, volume: 6_315_200 },
];

/** A fresh copy of the quotes, so one example's edits never reach another. */
export function quotes(): Quote[] {
  return BASE.map((q, i) => ({ id: i + 1, ...q }));
}
```

## Selecting

- Click a cell; drag or <kbd>Shift</kbd>-click to extend to a rectangle.
- <kbd>Shift</kbd>+arrows extend from the keyboard, and <kbd>Ctrl/⌘</kbd>+<kbd>A</kbd> selects everything.
- <kbd>Escape</kbd> clears the selection.

The full key map is in [Keyboard & hotkeys](https://bonguynvan.github.io/bo-grid/docs/keyboard/).

## Copy and paste

<kbd>Ctrl/⌘</kbd>+<kbd>C</kbd> copies the selection as tab-separated text, which a spreadsheet pastes as cells. <kbd>Ctrl/⌘</kbd>+<kbd>V</kbd> pastes such a block into editable cells, starting at the selection's top-left corner:

- A single copied value fills the whole selection.
- A block is cut off at the grid's edges.
- Values go through the same checks as typing: cells in columns that are not editable, and values that `parse` or `validate` refuse, are skipped.
- Every accepted cell calls `onCellEdit`, so paste does nothing until you handle that callback. See [Inline editing](https://bonguynvan.github.io/bo-grid/docs/editing/).

## Fill handle

`fillHandle` puts a small square on the selection's bottom-right corner. Drag it down or right to copy the selected values over the cells you drag across, as a spreadsheet does; a multi-cell selection repeats as a pattern. Only editable columns are filled, in-memory data only.

```svelte
<Grid {rows} {columns} height={480} fillHandle onCellEdit={(e) => (e.row[e.column.key] = e.value)} />
```

Edits, pastes and fills undo with <kbd>Ctrl/⌘</kbd>+<kbd>Z</kbd> and redo with <kbd>Ctrl/⌘</kbd>+<kbd>Y</kbd>; a paste or a fill undoes as one step.

## Totals for a selection

When more than one cell is selected, a bar under the grid shows **Sum**, **Avg**, **Count**, **Min** and **Max** of the numbers in the range, and keeps them current as a live feed ticks. `aggregations` picks which to show:

```svelte
<Grid {rows} {columns} height={480} aggregations={['sum', 'avg', 'count']} />
```

## A totals row

`footer` pins a totals row to the bottom: every column with a `groupAgg` shows that aggregate over all rows that pass the filters. In-memory data only.

```ts
const columns: ColumnDef[] = [
  { type: 'number', key: 'qty', header: 'Qty', groupAgg: 'sum' },
  { type: 'price', key: 'price', header: 'Price', groupAgg: 'avg' },
];
```

```svelte
<Grid {rows} {columns} height={480} footer />
```

The totals follow a live feed without rescanning every row: `api.patchRows` moves them by the rows it changed, and rows changed any other way (`$state` rows, an `onCellEdit` handler) update them too. A computed column must depend only on its own row for this; one that reads other rows or the clock gets a fresh total on the next view or `api.refresh()`.

## Read-only grids and an active row

For a display grid, `cellSelection={false}` drops the selection highlight and the fill handle, so clicks go straight to `onRowClick` and `onCellClick`:

```svelte
<Grid {rows} {columns} height={480} cellSelection={false} onRowClick={(row) => open(row)} />
```

For a list beside a detail view, drive `selectedRowId` (by `getRowId`) to highlight the active row. It is separate from the cell selection and from checkbox [row selection](https://bonguynvan.github.io/bo-grid/docs/row-selection/):

```svelte
<Grid {rows} {columns} height={480} selectedRowId={active?.id} onRowClick={(row) => (active = row)} />
```

## Pinned rows

Rows can stay above the scroll, whether rows of your own (`pinnedRows`) or rows readers pin from the row menu (`rowPinning`): see [Pinned rows](https://bonguynvan.github.io/bo-grid/docs/pinned-rows/).

## Reference

- `cellSelection`: `boolean` (default `true`) — Highlight cells/ranges on click-drag (the blue selection fill + focus ring + fill handle). Set false for a read-only/display grid where clicks should pass straight through to `onRowClick`/`onCellClick` without a selection highlight.
- `fillHandle`: `boolean` (default `false`) — Show an Excel-style fill handle at the selection's bottom-right corner; drag it to copy the selected value(s) across the extended range (editable columns only). In-memory mode only.
- `aggregations`: `AggKind[]` (default `['sum', 'avg', 'count', 'min', 'max']`) — Which stats the selection footer shows over a multi-cell range.

  ```svelte
  <Grid {rows} {columns} height={480} aggregations={['sum', 'count']} />
  ```

- `footer`: `boolean` (default `false`) — Show a pinned totals row: each column with a `groupAgg` shows that aggregate over all (filtered) rows. In-memory mode only.
- `selectedRowId`: `string | number | null` (default `null`) — Controlled "active row" highlight (keyed by `getRowId`) — for master-detail / list-detail where one row is selected. Independent of the checkbox `rowSelection` and the cell range selection. `null` = none.
- `onRowClick`: `(row: GridRow, event: MouseEvent | KeyboardEvent) => void` — Called when a data row is activated by click or Enter (open a detail view, navigate, …). Edit-input and checkbox clicks are excluded.

  ```svelte
  <Grid {rows} {columns} height={480} {selectedRowId} onRowClick={(row) => (selectedRowId = row.id)} />
  ```

- `onCellClick`: `( info: { row: GridRow; column: ColumnDef; value: unknown }, event: MouseEvent, ) => void` — Called when a cell is clicked, with its row, column and value. Fires in addition to `onRowClick`; excluded for the edit input.

  ```svelte
  <Grid {rows} {columns} height={480} onCellClick={({ row, column }) => column.key === 'sym' && openChart(row.sym)} />
  ```

- `onCellEdit`: `(e: CellEditEvent) => void` — Called when an editable cell is committed. Update your row data in here.

  ```svelte
  <Grid {rows} {columns} height={480} onCellEdit={({ row, column, value }) => (row[column.key] = value)} />
  ```


- `groupAgg`: `AggKind` (default `undefined`) — When grouping is active (or with `footer`), show this aggregate of the column on group headers and the totals row.

  ```ts
  { type: 'volume', key: 'qty', header: 'Qty', groupAgg: 'sum' }
  ```

- `editable`: `boolean` (default `false`) — Allow inline editing (double-click, Enter or F2 on the focused cell, or type to replace). Number types edit as numbers, `date` as a date. Display types (`badge`, `boolean`, `rating`, `progress`, `tags`, `link`, `avatar`, `relative`, `sparkline`, `custom`) edit only with an `editor`; a computed (`value`) column never does.

- `pinRow(key: string | number, pinned?: boolean): boolean` — Pin a row above the scroll (or unpin it with `pinned: false`), as the row menu does under `rowPinning`; `pinned` is true unless given. The row stays in the body too. Returns false when no row has that id.

  ```ts
  api.pinRow(row.id);
  ```

- `getPinnedRowIds(): (string | number)[]` — Ids of the rows pinned at runtime, in pin order.

  ```ts
  for (const id of api.getPinnedRowIds()) api.pinRow(id, false);
  ```

---
Page: https://bonguynvan.github.io/bo-grid/docs/selection/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
