# Clipboard & Export

> Range selection, copy and paste as TSV, CSV export with quoting and formula protection, and downloadText.

Page: https://svelte-lean.vercel.app/table/clipboard-export · Markdown: https://svelte-lean.vercel.app/table/clipboard-export.md · Index: https://svelte-lean.vercel.app/llms.txt

A range is two cell positions in a prop; the text formats are pure functions in two entry points. Copy produces tab-separated text a spreadsheet understands; paste returns validated edits; CSV export quotes and protects formulas.

- **Source**: clipboard.tsexport.tsdata-table.svelte
- **Tests**: core.test.tsdata-table.dom.test.tstable.test.tstable.spec.ts (browser)
- **Fixture**: fixtures/table-basic
- **Benchmark**: table.bench.spec.ts

Range selection is controlled: `cellRange` is the highlighted rectangle and `onCellRangeChange` reports a click, a Shift+click or a Shift+arrow extension. With the callback set, cells become focusable in the grid pattern. The renderer only highlights; reading the cells is `copyRange()` from `@svelte-lean/table/clipboard` (895 B gzip), which quotes values containing tabs, line breaks or quotes.

`pasteRange()` goes the other way: it parses tab-separated text, walks the target rectangle, skips columns that are not editable, parses and validates each value with the column rules, and returns `EditEvent`s without touching the rows. An invalid cell throws with the column name, so nothing is half-applied. CSV export lives in `@svelte-lean/table/export` (741 B gzip) and reuses the column `format` functions, so the file shows what the user saw.

## Range selection and clipboard

Select a rectangular range and translate it to or from tab-delimited spreadsheet data.

RangeClipboard.svelte

```svelte
<script lang="ts">
	import { DataTable, type CellRange } from '@svelte-lean/table';
	import { applyEdit } from '@svelte-lean/table/editing';
	import { copyRange, pasteRange } from '@svelte-lean/table/clipboard';
	import { memberColumns, pick } from '$lib/demo/columns';
	import { people } from '$lib/demo/data';

	let rows = $state(people(6));
	const columns = pick(memberColumns(), ['name', 'team', 'region', 'revenue']).map((column) => ({
		...column,
		editable: column.id !== 'name'
	}));
	let range = $state<CellRange | null>({ start: { row: 1, column: 'team' }, end: { row: 3, column: 'revenue' } });
	let tsv = $state('');
	let notice = $state('');

	async function copy() {
		if (!range) return;
		tsv = copyRange(range, rows, columns, (row) => row.id);
		try {
			await navigator.clipboard.writeText(tsv);
			notice = 'Copied as tab-separated text.';
		} catch {
			notice = 'Clipboard unavailable; the text is shown below.';
		}
	}
	// pasteRange returns validated EditEvents; the application applies them.
	function paste() {
		if (!range) return;
		const start = {
			row: rows.findIndex((row) => row.id === range!.start.row),
			column: columns.findIndex((column) => column.id === range!.start.column)
		};
		const changes = pasteRange(tsv, start, rows, columns, (row) => row.id);
		rows = changes.reduce((next, change) => applyEdit(next, change, columns, (row) => row.id), rows);
		notice = `${changes.length} cells updated.`;
	}
</script>

<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" onclick={copy}>Copy range</button>
<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" onclick={paste} disabled={!tsv}>Paste at range start</button>
<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" onclick={() => (range = null)}>Clear range</button>

<!-- Click a cell, then Shift+click (or Shift+arrow) another cell to extend the rectangle. -->
<DataTable data={rows} {columns} getRowId={(row) => row.id} cellRange={range} onCellRangeChange={(next) => (range = next)} pagination={false} label="Members: range and clipboard" />

<textarea aria-label="Tab-separated text" bind:value={tsv}></textarea>
<p role="status">{notice}</p>
```

Click a cell, then Shift+click (or hold Shift with the arrow keys) to extend the rectangle. Copy range writes tab-separated text to the clipboard and into the textarea; edit the text and Paste at range start turns it into validated edits applied with applyEdit(). Member is not editable and is skipped on paste.

- Positions are row ids and column ids, so a range survives sorting and re-rendering.
- The clipboard write needs a secure context and permission; the textarea shows the text either way.
- Revenue is a number editor: pasting text into it throws and nothing is applied.

## Keyboard

| Key | When | Result |
| --- | --- | --- |
| `Tab` | outside the body | Enters the body on the last cell reached, or the first cell. |
| `ArrowLeft`/`ArrowRight`/`ArrowUp`/`ArrowDown` | on a cell | Moves to the neighbouring cell. |
| `Shift+Arrow` | on a cell | Moves and extends the range from its start to the new cell. |

## CSV export

Export visible data with correct quoting and spreadsheet-formula protection.

CsvExport.svelte

```svelte
<script lang="ts">
	import { DataTable } from '@svelte-lean/table';
	import { downloadText, toCsv } from '@svelte-lean/table/export';
	import { memberColumns, pick } from '$lib/demo/columns';
	import { people, type Person } from '$lib/demo/data';

	// One value starts with "=": the export prefixes it so a spreadsheet does not run it as a formula.
	const data: Person[] = people(5).map((row, index) =>
		index === 0 ? { ...row, name: '=HYPERLINK("https://example.com")' } : row
	);
	const columns = pick(memberColumns(), ['name', 'team', 'region', 'revenue']);
	const csv = toCsv(data, columns); // format() applies, header row included, BOM for spreadsheets
</script>

<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" onclick={() => downloadText(csv, 'members.csv')}>
	Download CSV
</button>

<DataTable {data} {columns} getRowId={(row) => row.id} pagination={false} label="Members: CSV export" />
<pre>{csv}</pre>
```

The first row's name starts with an equals sign; in the output it is prefixed with an apostrophe so a spreadsheet shows it as text instead of evaluating it. Revenue is exported through its format function. Download CSV calls downloadText(), which creates an object URL, clicks a link and revokes the URL.

`options`

```ts
toCsv(rows, columns, {
	delimiter: ';',        // ',' (default), ';' or '\t'
	headers: true,         // the header row from column.header
	protectFormulas: true, // prefixes values starting with = + - @ tab or CR with an apostrophe
	bom: true              // a UTF-8 byte-order mark so spreadsheets read the encoding
});
```

- `parseDelimited()` reads the format back, including quoted line breaks and doubled quotes.
- Export what the user sees: pass the visible columns and the filtered rows from the row model.

## API

| Name | Of | Type | Default | Description |
| --- | --- | --- | --- | --- |
| [`cellRange`](https://svelte-lean.vercel.app/table/api.md#prop-cellrange) | Events and composition | `CellRange \| null` | `undefined` | The highlighted rectangular range; with onCellRangeChange cells become focusable. |
| [`onCellRangeChange`](https://svelte-lean.vercel.app/table/api.md#prop-oncellrangechange) | Events and composition | `(range: CellRange \| null) => void` | `undefined` | Reports a click, Shift+click or Shift+arrow range. |
| [`CellRange`](https://svelte-lean.vercel.app/table/api.md#type-cellrange) | Events and positions | `{ start: CellPosition; end: CellPosition }` | – | An inclusive rectangle anchored by two positions. |
| [`CellPosition`](https://svelte-lean.vercel.app/table/api.md#type-cellposition) | Events and positions | `{ row: RowId; column: string }` | – | A cell by row id and column id. |
| [`editable`](https://svelte-lean.vercel.app/table/api.md#column-editable) | Editing | `boolean \| ((row: T) => boolean)` | `false` | Cell editing for the column or per row; needs onEdit on the table. |
| [`format`](https://svelte-lean.vercel.app/table/api.md#column-format) | Data and content | `(value: unknown, row: T) => string \| null` | `String(value)` | Formats the plain text of a cell and of a CSV export without a snippet; null or an empty string renders the placeholder. |
| [`@svelte-lean/table/clipboard`](https://svelte-lean.vercel.app/table/api.md#module--svelte-lean-table-clipboard) | Entry points | `copyRange · pasteRange · rangeBounds` | – | A rectangular range to tab-separated text and back to validated EditEvents. |
| [`@svelte-lean/table/export`](https://svelte-lean.vercel.app/table/api.md#module--svelte-lean-table-export) | Entry points | `toCsv · parseDelimited · downloadText` | – | CSV with quoting and formula protection, a parser, and a browser download. |
| [`@svelte-lean/table/editing`](https://svelte-lean.vercel.app/table/api.md#module--svelte-lean-table-editing) | Entry points | `applyEdit · setValue · parseEdit · createHistory · editEvent` | – | Immutable edit application and a caller-owned undo history. |

## Related

- [Editing](https://svelte-lean.vercel.app/table/editing.md)
- [Data Grid](https://svelte-lean.vercel.app/table/data-grid.md)
- [Selection](https://svelte-lean.vercel.app/table/selection.md)
