Lattice Grid Buy a licence

developer guide

Data Grid Clipboard Copy and Paste with Excel

A range copies out in the layout Excel and Sheets expect, and a block pasted back is applied cell by cell through the same validation as typing. A preview shows what will change and what was refused before anything is written, and text that would act as a formula is neutralised as it arrives.

Developer guideExport and clipboard › Data Grid Clipboard Copy and Paste with Excel

Clipboard

Copy produces the tab-separated form Excel, Numbers and Sheets all read, so a range pastes as cells rather than as one lump of text. Values go through each column's clipboard hook: a lookup copies its label, a date copies an unambiguous form.

Copy and paste

grid.export.rangeText();                          // the text, without writing it
grid.export.clipboard({ rows: 'range' });         // copy the range
grid.export.clipboard({ headers: true, rows: 'selected' });

grid.edit.pasteInto(text);                        // Excel's tiling rules

Guarding against formula injection on paste

By default a copied range round-trips verbatim, so a value beginning =, +, - or @ is carried unchanged - the right thing when the paste target is another grid or cell range. When the paste target is a spreadsheet, pass sanitise: true and a leading formula character is neutralised with an apostrophe, so the value cannot be executed as a formula in Excel or Sheets.

Opt in per copy

const { buildClipboardText } = await import('../packages/core/src/export/index.js');

// An explicit cell matrix; the formula guard applies to data fields.
const cells = [['=1+1']];

// Off by default: the value round-trips exactly as copied.
const plain = buildClipboardText({}, { rows: 'range', cells });

// sanitise:true prefixes an apostrophe so a spreadsheet stores it as text.
const guarded = buildClipboardText({}, { rows: 'range', cells, sanitise: true });

return `default:${plain} guarded:${guarded}`;

Pasting follows the spreadsheet convention: one cell into a range fills the range, one row into several rows repeats down, and a block larger than the target extends past it. People have twenty years of muscle memory for this and a grid that invents its own rules is a grid people fight.

Previewing a bulk paste

A paste is the one clipboard gesture that can rewrite dozens of cells with nothing to inspect first: a payload that lands a column to the left of where it was aimed, or over a range the user forgot was selected, looks exactly like one that worked. Turn on edit.pastePreview and a paste into more than one cell opens a confirm/cancel dialog before anything commits.

Opt in

createGrid(host, {
  // Off by default: an unconfigured grid pastes straight away, as before.
  edit: { enabled: true, pastePreview: true },
});

The dialog lists every cell that will change, old → new, and every cell a commit would reject - a read-only cell, a value the column's type or edit.validate refuses, a cell a permission policy forbids. Confirm commits precisely that set through the ordinary paste path; Cancel commits nothing. A single-cell paste skips the dialog - a preview for one cell is friction, not a safety net. The dialog is a modal role="dialog": Escape cancels, Tab stays inside it, focus moves in on open and back on close, and its opening is announced through the grid's live region. The same diff is available without any UI from grid.edit.previewPaste(anchor, text, extent?), which returns { changes, rejected } and changes nothing.

Why a per-edit flag, off by default. Paste preview lives under edit because a paste is a bulk edit and its accept/reject decisions are the edit model's - the preview cannot disagree with the commit because it runs the same checks. Off by default keeps every existing grid's paste behaviour exactly as it was.