@dimarborda/xlsx-editor 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @dimarborda/xlsx-editor
2
2
 
3
- View and edit Excel (`.xlsx`) workbooks in the browser, drawn on canvas the way Excel shows them, and work with them from Node.js. A Rust and WebAssembly engine reads the file, recalculates formulas and saves losslessly: what it does not touch stays byte by byte. It works in any frontend, and nothing leaves the browser.
3
+ View and edit Excel (`.xlsx`) workbooks in the browser, drawn on canvas the way Excel shows them, and work with them from Node.js. A Rust and WebAssembly engine reads the file, recalculates formulas and saves losslessly: what it does not touch stays byte by byte. It works with React, Vue, Svelte, Angular or plain HTML, and in desktop apps such as Tauri. It needs no server: the library sends your files nowhere.
4
4
 
5
5
  For AI agents, `wb.area()` gives a table's columns, types and totals without reading the whole sheet, and a change that would break something is refused with the reason, leaving the workbook as it was.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dimarborda/xlsx-editor",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Excel (.xlsx) editor for the browser and engine for Node.js: Rust + WebAssembly that edits workbooks without losing what it does not touch, from a canvas grid or from code. An AI agent can read a table's structure without loading the whole sheet",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.d.ts CHANGED
@@ -78,7 +78,7 @@ export interface ChangeDetail {
78
78
  range: string;
79
79
  /** Every cell of `range` with its new value (`null` when empty); empty above 200 000 cells */
80
80
  cells: { ref: string; value: CellValue }[];
81
- source: 'edit' | 'clear' | 'paste' | 'cut' | 'move' | 'insert' | 'delete' | 'fill' | 'replace' | 'undo' | 'redo';
81
+ source: 'edit' | 'clear' | 'paste' | 'cut' | 'move' | 'insert' | 'delete' | 'resize' | 'hide' | 'show' | 'fill' | 'replace' | 'undo' | 'redo';
82
82
  }
83
83
 
84
84
  export interface XlsxEditorEventMap {
@@ -992,8 +992,9 @@ export class XlsxEditor extends EventTarget {
992
992
  /** Sets rows or columns to sizes in pixels, as one undo step: `[[line, px or null]]` */
993
993
  #applySizes(axis, sizes) {
994
994
  if (!sizes.length) return false;
995
+ const changed = this.#linesRange(axis, sizes[0][0], sizes[sizes.length - 1][0]);
995
996
  try {
996
- this.#workbook.checkpoint(this.#historyToken(null));
997
+ this.#workbook.checkpoint(this.#historyToken(changed));
997
998
  const sheet = this.#activeSheet();
998
999
  // Runs of lines of one size are one call
999
1000
  for (let i = 0; i < sizes.length;) {
@@ -1010,11 +1011,18 @@ export class XlsxEditor extends EventTarget {
1010
1011
  this.reload();
1011
1012
  return false;
1012
1013
  }
1013
- this.#workbook.setSelectionAfter(this.#historyToken(null));
1014
+ this.#workbook.setSelectionAfter(this.#historyToken(changed));
1014
1015
  this.reload();
1016
+ this.#emitChange(changed, 'resize');
1015
1017
  return true;
1016
1018
  }
1017
1019
 
1020
+ /** The lines `a..=b` as a range, only as far across as the sheet has cells */
1021
+ #linesRange(axis, a, b) {
1022
+ const { maxRow, maxCol } = this.#sheet.used;
1023
+ return axis === 'row' ? rangeOf({ row: a, col: 1 }, { row: b, col: Math.max(maxCol, 1) }) : rangeOf({ row: 1, col: a }, { row: Math.max(maxRow, 1), col: b });
1024
+ }
1025
+
1018
1026
  /** Double click: on the edge of a header fits its lines to their content, in a cell edits it */
1019
1027
  #onDoubleClick(e) {
1020
1028
  if (!this.#sheet) return;
@@ -1243,17 +1251,19 @@ export class XlsxEditor extends EventTarget {
1243
1251
  #setLinesHidden(axis, a, b, hidden) {
1244
1252
  if (this.#readOnly || !this.#sheet) return false;
1245
1253
  this.#commitEdit(null, { focus: false });
1254
+ const changed = this.#linesRange(axis, a, b);
1246
1255
  try {
1247
- this.#workbook.checkpoint(this.#historyToken(null));
1256
+ this.#workbook.checkpoint(this.#historyToken(changed));
1248
1257
  const sheet = this.#activeSheet();
1249
1258
  if (axis === 'row') sheet.setRowsHidden(`${a}:${b}`, hidden);
1250
1259
  else sheet.setColumnsHidden(`${columnName(a)}:${columnName(b)}`, hidden);
1251
- this.#workbook.setSelectionAfter(this.#historyToken(null));
1260
+ this.#workbook.setSelectionAfter(this.#historyToken(changed));
1252
1261
  } catch (error) {
1253
1262
  this.#fail(error);
1254
1263
  return false;
1255
1264
  }
1256
1265
  this.reload();
1266
+ this.#emitChange(changed, hidden ? 'hide' : 'show');
1257
1267
  return true;
1258
1268
  }
1259
1269
 
@@ -1328,9 +1338,11 @@ export class XlsxEditor extends EventTarget {
1328
1338
  return { index, first, last, found };
1329
1339
  });
1330
1340
  const only = blocks.length === 1 && blocks[0].index === this.#sheetIndex ? rangeOf(blocks[0].first, blocks[0].last) : null;
1341
+ // Other sheets too: undoing says what changed in each one
1342
+ const others = only ? null : blocks.map(({ index, first, last }) => [index, rangeOf(first, last)]);
1331
1343
  let written = false;
1332
1344
  try {
1333
- this.#workbook.checkpoint(this.#historyToken(only));
1345
+ this.#workbook.checkpoint(this.#historyToken(only, others));
1334
1346
  for (const { index, first, last, found } of blocks) {
1335
1347
  // Holes leave the cells between the hits as they are
1336
1348
  const rows = Array.from({ length: last.row - first.row + 1 }, () => new Array(last.col - first.col + 1));
@@ -1342,7 +1354,7 @@ export class XlsxEditor extends EventTarget {
1342
1354
  this.#workbook.sheet(index).setValues(refOf(first), rows);
1343
1355
  written = true;
1344
1356
  }
1345
- this.#workbook.setSelectionAfter(this.#historyToken(only));
1357
+ this.#workbook.setSelectionAfter(this.#historyToken(only, others));
1346
1358
  } catch (error) {
1347
1359
  // All or nothing, also when a later sheet refuses
1348
1360
  if (written) this.#workbook.undo();
@@ -2359,9 +2371,9 @@ export class XlsxEditor extends EventTarget {
2359
2371
  }
2360
2372
 
2361
2373
  /** What the undo history remembers about a change: the sheet, the selection, the cells */
2362
- #historyToken(changed) {
2374
+ #historyToken(changed, others = null) {
2363
2375
  const sel = this.selection;
2364
- return JSON.stringify({ sheet: this.#sheetIndex, range: sel?.range, active: sel?.ref, whole: sel?.whole ?? null, changed });
2376
+ return JSON.stringify({ sheet: this.#sheetIndex, range: sel?.range, active: sel?.ref, whole: sel?.whole ?? null, changed, others });
2365
2377
  }
2366
2378
 
2367
2379
  /** Once the selection has moved after a change, redo puts it back there */
@@ -2447,6 +2459,9 @@ export class XlsxEditor extends EventTarget {
2447
2459
  if (whole === 'col') this.#scrollIntoView({ col: active.col });
2448
2460
  }
2449
2461
  if (token?.changed) this.#emitChange(token.changed, direction);
2462
+ for (const [index, range] of token?.others ?? []) {
2463
+ if (tabs[index]?.kind === 'worksheet') this.#emitChange(range, direction, this.#workbook.sheet(index));
2464
+ }
2450
2465
  return true;
2451
2466
  }
2452
2467