@xlcalc/vue 0.0.0-stage → 0.21.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.
Files changed (125) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +75 -2
  4. package/dist/abort.d.ts +6 -0
  5. package/dist/components/CellDraft.vue.d.ts +7 -0
  6. package/dist/components/CellEditor.vue.d.ts +3 -0
  7. package/dist/components/ColourSwatches.vue.d.ts +13 -0
  8. package/dist/components/DialogShell.vue.d.ts +37 -0
  9. package/dist/components/ExplainPanel.vue.d.ts +3 -0
  10. package/dist/components/FilterPopover.vue.d.ts +3 -0
  11. package/dist/components/FindBar.vue.d.ts +3 -0
  12. package/dist/components/FormatBorders.vue.d.ts +14 -0
  13. package/dist/components/FormatColour.vue.d.ts +19 -0
  14. package/dist/components/FormatDialog.vue.d.ts +24 -0
  15. package/dist/components/FormulaBar.vue.d.ts +3 -0
  16. package/dist/components/FunctionList.vue.d.ts +28 -0
  17. package/dist/components/FunctionTip.vue.d.ts +14 -0
  18. package/dist/components/InsertFunction.vue.d.ts +9 -0
  19. package/dist/components/OutlineCell.vue.d.ts +8 -0
  20. package/dist/components/OutlineLevels.vue.d.ts +7 -0
  21. package/dist/components/SheetGrid.vue.d.ts +3 -0
  22. package/dist/components/SheetMarks.vue.d.ts +3 -0
  23. package/dist/components/SheetMenu.vue.d.ts +3 -0
  24. package/dist/components/SheetRow.vue.d.ts +18 -0
  25. package/dist/components/SheetTabs.vue.d.ts +3 -0
  26. package/dist/components/SheetToolbar.vue.d.ts +43 -0
  27. package/dist/components/SheetView.vue.d.ts +56 -0
  28. package/dist/components/ValidationAlert.vue.d.ts +16 -0
  29. package/dist/components/ValidationDropdown.vue.d.ts +33 -0
  30. package/dist/components/ValidationPrompt.vue.d.ts +16 -0
  31. package/dist/components/cf-icons.d.ts +6 -0
  32. package/dist/components/context.d.ts +4 -0
  33. package/dist/components/dom.d.ts +7 -0
  34. package/dist/components/icons.d.ts +33 -0
  35. package/dist/components/popup.d.ts +29 -0
  36. package/dist/components/toolbar-icons.d.ts +24 -0
  37. package/dist/contract/memory.d.ts +108 -0
  38. package/dist/contract/messages.d.ts +260 -0
  39. package/dist/contract/source.d.ts +433 -0
  40. package/dist/contract/view.d.ts +108 -0
  41. package/dist/contract.d.ts +9 -0
  42. package/dist/core/ask.d.ts +2 -0
  43. package/dist/core/autocomplete.d.ts +28 -0
  44. package/dist/core/cell-style.d.ts +33 -0
  45. package/dist/core/cell-view.d.ts +104 -0
  46. package/dist/core/clip.d.ts +43 -0
  47. package/dist/core/commands.d.ts +47 -0
  48. package/dist/core/conditional.d.ts +16 -0
  49. package/dist/core/editing.d.ts +85 -0
  50. package/dist/core/explain.d.ts +36 -0
  51. package/dist/core/fill.d.ts +42 -0
  52. package/dist/core/filter.d.ts +72 -0
  53. package/dist/core/find.d.ts +53 -0
  54. package/dist/core/formula.d.ts +41 -0
  55. package/dist/core/function-tip.d.ts +34 -0
  56. package/dist/core/geometry.d.ts +55 -0
  57. package/dist/core/keys.d.ts +8 -0
  58. package/dist/core/list-keys.d.ts +4 -0
  59. package/dist/core/list.d.ts +17 -0
  60. package/dist/core/menu.d.ts +155 -0
  61. package/dist/core/merge.d.ts +19 -0
  62. package/dist/core/navigation.d.ts +72 -0
  63. package/dist/core/outline.d.ts +34 -0
  64. package/dist/core/point.d.ts +34 -0
  65. package/dist/core/ref-paint.d.ts +21 -0
  66. package/dist/core/selection.d.ts +45 -0
  67. package/dist/core/sheet.d.ts +774 -0
  68. package/dist/core/state.d.ts +107 -0
  69. package/dist/core/storage.d.ts +6 -0
  70. package/dist/core/totals.d.ts +16 -0
  71. package/dist/core/validation.d.ts +95 -0
  72. package/dist/core/viewport.d.ts +83 -0
  73. package/dist/core/windows.d.ts +102 -0
  74. package/dist/entry.d.ts +3 -0
  75. package/dist/failure.d.ts +5 -0
  76. package/dist/filter-key.d.ts +3 -0
  77. package/dist/find-match.d.ts +24 -0
  78. package/dist/format-cells.d.ts +85 -0
  79. package/dist/frame.d.ts +5 -0
  80. package/dist/grid-ref.d.ts +3 -0
  81. package/dist/index.d.ts +6 -0
  82. package/dist/index.js +12767 -0
  83. package/dist/lines.d.ts +16 -0
  84. package/dist/local/find.d.ts +15 -0
  85. package/dist/local/index.d.ts +3 -0
  86. package/dist/local/locale.d.ts +5 -0
  87. package/dist/local/outcome.d.ts +21 -0
  88. package/dist/local/query.d.ts +23 -0
  89. package/dist/local/source.d.ts +4 -0
  90. package/dist/local/types.d.ts +37 -0
  91. package/dist/local/validation.d.ts +8 -0
  92. package/dist/local/window.d.ts +16 -0
  93. package/dist/local/xlsx.d.ts +23 -0
  94. package/dist/local.js +1025 -0
  95. package/dist/locale-ref.d.ts +7 -0
  96. package/dist/locales.d.ts +3 -0
  97. package/dist/memory-outline.d.ts +31 -0
  98. package/dist/memory-query.d.ts +26 -0
  99. package/dist/memory-source.d.ts +3 -0
  100. package/dist/memory-validation.d.ts +17 -0
  101. package/dist/messages.d.ts +6 -0
  102. package/dist/number-formats.d.ts +13 -0
  103. package/dist/rect.d.ts +9 -0
  104. package/dist/ru-RU-BVeg6JGM.js +931 -0
  105. package/dist/sheet-query.d.ts +22 -0
  106. package/dist/stats.d.ts +8 -0
  107. package/dist/style.css +2 -0
  108. package/dist/toolbar.d.ts +39 -0
  109. package/dist/typed-entry-2l9Ay-hV.js +4559 -0
  110. package/dist/window-size.d.ts +4 -0
  111. package/docs/api.md +520 -0
  112. package/docs/autocomplete.md +60 -0
  113. package/docs/checks.md +40 -0
  114. package/docs/clipboard.md +48 -0
  115. package/docs/editing.md +97 -0
  116. package/docs/example-app.md +26 -0
  117. package/docs/find-replace.md +26 -0
  118. package/docs/list-mode.md +86 -0
  119. package/docs/local.md +94 -0
  120. package/docs/quick-start.md +221 -0
  121. package/docs/sheets.md +61 -0
  122. package/docs/toolbar.md +72 -0
  123. package/docs/validation.md +41 -0
  124. package/docs/view.md +50 -0
  125. package/package.json +92 -4
package/docs/checks.md ADDED
@@ -0,0 +1,40 @@
1
+ # Browser tests
2
+
3
+ The checks build the example app ([example-app.md](example-app.md)) or the local page, drive them in Chromium through Playwright, and save screenshots under `test-results/screens/`. `npm run screens` takes screenshots of the example with every kind of formatting; along the way it checks that the formula bar's layer of colored references lines up with the input's text to the pixel (single-line and expanded, idle and with a long draft, at both widths) and drives the formula bar with the mouse on the desktop: Ctrl+click on a reference, dragging over the text, a click while the cell editor is open, and the reference underlined with a hand pointer while Cmd or Ctrl is held over it (in either order); on the phone, a long press on a reference goes to its range and a tap on it starts editing.
4
+
5
+ ## App scenarios: `check:spa`
6
+
7
+ `npm run check:spa` builds the example app, serves it and drives it in Chromium at 1920 × 1200 and on a phone at 390 × 844 with touch input, saving screenshots of each step in `test-results/screens/spa-*.png`:
8
+
9
+ 1. a ru-RU workbook: values and a formula, bold, percentage, outside borders, undo, redo, Save as, and the saved file opened again;
10
+ 2. the structure menu: a row inserted by a right click and by a long press, a column hidden and unhidden, a sheet added, renamed and deleted, a column fitted by a double click;
11
+ 3. the fill handle and cut, copy and paste: a formula dragged three rows down by its handle, a range with a formula copied and pasted, a formula cut and pasted, each undone; Ctrl+C, Ctrl+X and Ctrl+V on the desktop, the cell context menu on the phone;
12
+ 4. Formula AutoComplete: `=СУ` in a cell on the desktop and in the formula bar on the phone, СУММ chosen from the list by Tab or a tap, and the sum checked;
13
+ 5. Insert Function: fx, СУММ found with its syntax and description, the logical functions by category, OK, `1;2` and the formula bar's ✓ prompting about the unclosed parenthesis, Fix, `)` and ✓ giving 3 with A1 still active, and СУММ first among the recently used;
14
+ 6. a file with a circular reference opened from the file picker, which keeps its stored values through an edit elsewhere;
15
+ 7. a file opened on the sheet it saved as active, Save as after a move to another sheet recording that sheet, and a file without a saved active sheet opened on its first visible one.
16
+
17
+ `DEMO=1 npm run check:spa` runs the whole check on the demo build (`npm run build:demo`) served under `/demo/`, and also checks that `/demo/` opens the app and that the build contains no source maps.
18
+
19
+ ## Large workbooks: `check:big`
20
+
21
+ `npm run check:big` measures the app on large workbooks in Chromium at 1920 × 1200. The production build served by `vite preview` opens each file from its file input, and the check measures the first drawn cells of the sheet the app opens and Ready; the jump to the largest sheet; scrolling it down five screens and right three; a number typed into a cell whose dependent is on screen, until the dependent displays its new value; Ctrl+Z; Save as (time and size; the saved file is read back, must contain the workbook's sheets and formulas, and is then deleted); the heap of the page and of its Web Workers (CDP); the longest frame gap and long task of each step; and a change of the workbook's language in its settings. It fails when a file without a language does not open in ru-RU, when the app opens a sheet other than the one Excel opens (the saved active sheet, otherwise the first visible one), on a page error, or when no edit reaches the screen. Without `BIG_XLSX` it runs on xlcalc's synthetic workbook of 34,000 rows (510,757 cells, written to `test-results`). To run it on your own workbooks, pass their paths separated by `:`; they are read in place and nothing is copied:
22
+
23
+ ```sh
24
+ BIG_XLSX="$HOME/books/plan.xlsx:$HOME/books/budget.xlsx" npm run check:big
25
+ BIG_PROFILE=1 BIG_XLSX=... npm run check:big # also the page's CPU profiles of each step, test-results/big-*.cpuprofile
26
+ BIG_URL=https://xlcalc.pridelab.dev/demo/ npm run check:big # a published app instead of the build, the synthetic workbook only
27
+ SPA_URL=https://xlcalc.pridelab.dev/demo/ npm run check:spa # the app's scenarios on a published app
28
+ ```
29
+
30
+ The table is printed to the terminal, the numbers go to `test-results/big-check.json` and the screenshots to `test-results/screens/big-*.png`. On the synthetic workbook (Apple M5 Pro, 12.10.2026, with the machine busy with other builds) the first sheet is drawn 2.3 to 2.4 s after the file is chosen, the dependent displays an edit in 40 to 50 ms and its undo in 25 to 40 ms, Save as takes 4.1 to 4.4 s, and no frame of the page waits longer than 0.3 s.
31
+
32
+ ## Values against Excel: `check:excel-values` and `check:compare-values`
33
+
34
+ `npm run check:excel-values` compares the library's values for a workbook with the values Excel saved in it: each formula cell, after a session opened with the file's stored values has calculated the workbook (as the app opens a file, so a circular reference keeps them), against `readXlsx`'s `cached`, with numbers compared within a relative 1e-9. It prints how many cells differ and exits with 1 if any do (`XLSX=a.xlsx:b.xlsx`; a large workbook needs `NODE_OPTIONS=--max-old-space-size=12288`).
35
+
36
+ `npm run check:compare-values` compares the values the app saved in a file with those Excel saved in its copy after a full recalculation (`SAVED=app.xlsx EXCEL=excel.xlsx`): every cell with a value in either file, where a value present in only one file is a difference, excluding cells with volatile formulas; numbers within 1e-12 absolute or 1e-14 relative, dates as serial numbers, text exactly, errors by code. It exits with 1 if any cell differs.
37
+
38
+ ## The local page: `check:local`
39
+
40
+ `npm run check:local` opens the example's `local.html` (the synthetic workbook of 3,400 rows) in Chromium, measures it and takes screenshots; the page also opens an xlsx file from its file input and saves the workbook under the file's name.
@@ -0,0 +1,48 @@
1
+ # Clipboard and fill handle
2
+
3
+ ## Copy, cut and paste
4
+
5
+ Ctrl+C (Cmd+C on a Mac) copies the selection to the clipboard as plain text in Excel's format (xlcalc's `formatClipboard`): fields separated by tabs, each row ending with CRLF, the last one included (no trailing line break on a Mac), fields that contain a tab or a line break in quotes, and hidden rows and columns omitted. If the source has `copyText` (`./local` does), the copy event puts the displayed text on the clipboard immediately and then replaces it with the source's text through the browser's Clipboard API: values formatted by their number formats and not clipped to the column width, as Excel copies them (a date displayed as `##` in a narrow column is copied as the date). If the browser denies the Clipboard API, the displayed text stays on the clipboard. Pasting either text counts as pasting this copy. Without `copyText`, or under a display filter, each cell is copied as its displayed text.
6
+
7
+ The copied range is marked with Excel's moving dashed border; Ctrl+X does the same for a cut. If the source has `structure` and the sheet is editable, Ctrl+V with that same text on the clipboard, or a paste whose clipboard contents the browser does not provide, pastes the range itself, to another sheet of the workbook as well:
8
+
9
+ - a copy goes through `structure({ op: 'copyRange', from, to })`, tiled across the selection when the copied range divides the selection's size, otherwise pasted once at the selection's top-left cell;
10
+ - a cut goes through `moveRange` to the selection's top-left cell.
11
+
12
+ The source performs the operation, so formulas are adjusted as Excel adjusts them (`./local` passes it to xlcalc's `editStructure`). The pasted range is selected, and a rejection displays the source's message. After a paste, a copy's border remains until Esc, opening a cell editor, a change to cells (an edit, a fill, Delete, an undo) or another copy; a cut's border disappears once the cut is pasted. Once the border is gone, pasting its text does nothing, since Excel has nothing to paste at that point. Any other text is pasted as before (through `paste`, otherwise `edit`), and changes to formatting or to rows and columns leave the border in place. Without `structure` a copy is pasted as text. Under a display filter a copy contains the text of the visible rows, the range is not marked, and there is no cut.
13
+
14
+ Try them on a sheet over `localSource`, which has `structure` and `copyText`: select A1:D2 and drag the fill handle down to row 6 to continue the series and fill the formula down, or copy E2 and paste it into E3:E6.
15
+
16
+ ```vue
17
+ <script setup lang="ts">
18
+ import { onUnmounted } from 'vue'
19
+ import { SheetView } from '@xlcalc/vue'
20
+ import { localSource } from '@xlcalc/vue/local'
21
+ import '@xlcalc/vue/style.css'
22
+
23
+ const source = localSource({
24
+ sheets: [
25
+ {
26
+ name: 'Series',
27
+ cells: {
28
+ A1: { value: 1 }, B1: { value: 'Mon' }, C1: { value: 'Q1' }, D1: { value: 'Item1' }, E1: { formula: 'A1*10' },
29
+ A2: { value: 2 }, B2: { value: 'Tue' }, C2: { value: 'Q2' }, D2: { value: 'Item2' }, E2: { formula: 'A2*10' },
30
+ },
31
+ },
32
+ ],
33
+ })
34
+ onUnmounted(() => void source.close())
35
+ </script>
36
+
37
+ <template>
38
+ <SheetView :source="source" :locale="source.locale" style="height: 320px" />
39
+ </template>
40
+ ```
41
+
42
+ ## Context menu
43
+
44
+ Right-clicking a cell, or a long press on it on a touch screen, opens Excel's cell context menu for the selection (a cell outside the selection is selected first): Cut, Copy and Paste (44 px high on a touch screen), followed by the commands from [view.md](view.md) that the source supports. This menu and the context menus of row numbers, column letters and sheet tabs handle the keyboard as Excel's do: the first enabled item has focus, Down and Up cycle through the enabled items, Home and End go to the first and the last, Enter or Space chooses an item, and Esc or Tab closes the menu and returns focus. The menu's Cut, Copy and Paste use the browser's Clipboard API. Paste reads the clipboard, which the browser may ask the user to allow; where it does not allow it, Paste pastes only the sheet's own copy and otherwise tells the user to press Ctrl+V. On a read-only sheet the menu has only Copy and Trace Precedents.
45
+
46
+ ## Fill handle
47
+
48
+ Excel's fill handle is the small square at the bottom-right corner of the selection. It appears on an editable sheet whose source has `structure`, but not under a display filter, when entire columns or rows are selected, or while a cell editor is open. Dragging it down, up, right or left extends its outline cell by cell along the axis in which the pointer has moved farthest, scrolling the grid at its edge. On release, the outlined range is filled from the selection through `structure({ op: 'fillRange', sheet, range, to })`, Excel's AutoFill, as a single undo step, and the outlined range is selected. The source performs the fill (`./local` passes it to xlcalc's `editStructure`): series continue (1, 2 → 3, 4; dates by day; Item1 → Item2; Q1, Mon, Jan), other values are copied with formulas adjusted, a partial copy of the selection fills the end of the range, and dragging up or left fills backwards. Releasing with Ctrl held (Option on a Mac) sends `type: 'series'`, which makes a single number increase by 1 instead of being copied. Esc during the drag cancels it, as in Excel, and the release then fills nothing; dragging back inside the selection does nothing (Excel clears the cells left out). Double-clicking the fill handle fills down as far as the data in the column to the left of the selection extends, otherwise in the column to the right. On a touch screen its target is 44 px around the small square, and a tap on it selects the cell underneath. If the source rejects the fill (a merged range, a source whose library has no `fillRange`), its message is displayed. The colors of the copy border and the fill outline are `--xs-copy-line` and `--xs-fill-frame`.
@@ -0,0 +1,97 @@
1
+ # Cell editing and keyboard shortcuts
2
+
3
+ Editing uses Excel's keys:
4
+
5
+ - typing over a cell, F2 or a double click starts editing the cell;
6
+ - Enter, Tab, Shift+Enter and Shift+Tab commit the entry and move the active cell. Inside a selection of several cells, the selection stays and the active cell moves within it as in Excel: Enter goes down a column and on to the top of the next one, Tab goes along a row and on to the start of the next one, wrapping around at the end;
7
+ - Esc cancels the edit, and Alt+Enter inserts a line break;
8
+ - Ctrl+Shift+U (or the arrow at the right end of the formula bar) expands the formula bar to five lines and collapses it back to one, as in Excel; while it is collapsed, a multi-line cell shows its first line and the grid does not move;
9
+ - Delete and Backspace clear the selection;
10
+ - Ctrl+Page Down and Ctrl+Page Up go to the next and the previous visible sheet;
11
+ - Ctrl+arrow goes where the source's `end(sheet, cell, direction)` points, as Excel's Range.End, and Shift+Ctrl+arrow extends the selection there. Without `end`, or under a display filter, it goes to the edge of the data known to the sheet.
12
+
13
+ Without `edit` the sheet is read-only.
14
+
15
+ A workbook calculated in the browser to try the keys on: type into B2 or C3 and D4 updates, F2 edits a cell in place, Ctrl+Enter over B2:C3 enters the same text into all four cells, and `=(1+2` brings up Excel's typo dialog.
16
+
17
+ ```vue
18
+ <script setup lang="ts">
19
+ import { onUnmounted } from 'vue'
20
+ import { SheetView } from '@xlcalc/vue'
21
+ import { localSource } from '@xlcalc/vue/local'
22
+ import '@xlcalc/vue/style.css'
23
+
24
+ const source = localSource({
25
+ sheets: [
26
+ {
27
+ name: 'Prices',
28
+ cells: {
29
+ A1: { value: 'Item' }, B1: { value: 'Price' }, C1: { value: 'Qty' }, D1: { value: 'Sum' },
30
+ A2: { value: 'Pens' }, B2: { value: 1.5 }, C2: { value: 10 }, D2: { formula: 'B2*C2' },
31
+ A3: { value: 'Paper' }, B3: { value: 4.25 }, C3: { value: 3 }, D3: { formula: 'B3*C3' },
32
+ A4: { value: 'Total' }, D4: { formula: 'SUM(D2:D3)' },
33
+ },
34
+ },
35
+ ],
36
+ })
37
+ onUnmounted(() => void source.close())
38
+ </script>
39
+
40
+ <template>
41
+ <SheetView :source="source" :locale="source.locale" style="height: 360px" />
42
+ </template>
43
+ ```
44
+
45
+ ## The formula bar
46
+
47
+ The formula bar is an input that always contains the active cell's text, the same text the cell editor opens with (a formula with its `=`), as in Excel; while the cell editor is open, it shows the draft. Clicking in it places the caret at the click and starts editing there, as F2 does; dragging starts editing with the dragged-over text selected; and clicking it while the cell editor is open moves the draft to the formula bar with the caret at the click. Focusing the input also starts editing, so anything typed into it always goes to the cell.
48
+
49
+ A formula's references are colored like their range borders on the grid, including those of a draft as it is typed. Outside editing, Ctrl+click on a reference (Cmd+click on a Mac) selects its range, on another sheet as well, like Go to Definition in a code editor, and the input's tooltip says so (`SheetMessages`' `goRefHint`). While Ctrl or Cmd is held, the reference under the pointer is underlined in its color and the pointer becomes a hand, whichever came first, the key or the mouse; both go away when the key is released, the pointer leaves the reference, or the window loses focus. On a touch screen, a long press on a reference (half a second, like the long press that opens the context menus; moving the finger cancels it) goes to its range instead, starts no edit and suppresses the browser's own menu and text selection, while a tap places the caret and edits as before; the input has no tooltip there. On a read-only sheet (without `edit`), on a locked cell, or with nothing selected, the input is read-only: its text can be selected and copied, keys do not start editing, and Ctrl+click or a long press on a reference still goes to it.
50
+
51
+ To the left of the input are Excel's Cancel (✕, `SheetMessages`' `cancel`) and Enter (✓, `barEnter`) buttons, enabled only while the cell editor or the formula bar is being edited. Cancel works like Esc; Enter enters the draft and keeps the active cell in place, like Ctrl+Enter on a single cell, and a formula that cannot be parsed brings up the same prompt as with Enter. Pressing either button keeps focus in the input until the button acts. They are followed by fx, Excel's Insert Function ([autocomplete.md](autocomplete.md#insert-function)). The reference colors are a layer over the input's transparent text, drawn with the CSS Custom Highlight API, so the layer's text lines up with the input's to the pixel; a browser without the API gets colored spans, which may be off by a fraction of a pixel.
52
+
53
+ ## Point mode
54
+
55
+ While a formula is being typed in the cell editor or the formula bar, the mouse inserts references as in Excel's Point mode, wherever xlcalc's `refAt` (measured in Excel) allows a reference: after the formula's first character (`=`, or a leading `+` or `-`), an operator, `(`, the workbook's list separator (`;` in ru-RU), `:`, a sheet's `!`, a decimal point, or a space after an operand (Excel's intersection operator). Clicking a cell inserts its address at the caret (replacing any text selected there), dragging inserts a range (`B2:D5`), clicking a column letter inserts the column (`F:F`), and clicking a row number inserts the row (`3:3`). Another click before anything is typed replaces that reference, and Shift+click extends it from its first corner; the input keeps focus and the active cell stays in place. Elsewhere a click commits the entry as before. Touch input does not insert references.
56
+
57
+ In a cell editor opened by typing over a cell (Enter mode; F2 switches modes), an arrow key with no reference inserted yet does what Excel does in that position: where a reference can go, it inserts the address of the adjacent cell (Shift+arrow inserts a range from the edited cell, `=C3:D3`); after an operand of a formula that parses in the workbook's locale (`=5`, `=A1`, `=SUM(A1)`), and after any value, it enters the text and moves the selection in that direction, as Enter does; where the formula does not parse (`=A1)`, `=1e`) or inside a string, a quoted sheet name or a table's brackets, it does nothing. Once a reference is inserted, the keys move it as they move the selection on the sheet: an arrow moves it from its first cell, Shift+arrow moves its far corner, Cmd+arrow on a Mac (Ctrl+arrow elsewhere) moves it to the edge of the data by the source's `end` (Shift extends it there), Home moves it to the row's first column, and Page Down and Page Up move it a screen down or up. Once anything is typed after a reference, the next arrow starts from the edited cell again. In an editor opened with F2 and in the formula bar, the arrow keys move the caret.
58
+
59
+ ## Formulas that cannot be parsed
60
+
61
+ When the source rejects a formula as `invalid` (on Enter, Tab, Ctrl+Enter, the formula bar's Enter button or a click elsewhere), the sheet asks what to do and shows the source's reason: Fix, the default, reopens the editor at the error; Restore, or Esc, leaves the cell unchanged.
62
+
63
+ When the outcome carries `correction`, the formula Excel suggests for an unclosed formula (`=(1+2` corrected to `=(1+2)`), the sheet shows Excel's dialog instead: "We found a typo in your formula and tried to correct it to: =(1+2). Would you like to accept this correction?" with Yes, the default, and No (`SheetMessages`' `typoFound`, `validationYes`, `validationNo`). Yes sends the same call again with the correction, an edit or a Ctrl+Enter fill, and the selection moves as the key that entered it moves it (down after Enter, right after Tab, the range kept after Ctrl+Enter). No, or Esc, shows Excel's message "This formula is missing an opening or closing parenthesis." with OK (`missingParenthesis`), after which the editor reopens with the typed text and the caret at the error. The sheet never corrects a formula on its own: the correction comes only from the source.
64
+
65
+ ## What the optional source methods enable
66
+
67
+ The source's other optional methods enable the following:
68
+
69
+ - Ctrl+Enter (Cmd+Return on a Mac), in the cell editor or the formula bar, enters the text into every cell of the selection through `fill(sheet, range, text, active)`, as Excel enters it from the active cell; the selection stays, and the text is sent even if it did not change. Without `fill`, or with a single cell, the text goes to the active cell through `edit` if it changed, as with Enter, and the active cell does not move.
70
+ - A paste on the grid goes to `paste(sheet, cell, text)` as a single call at the selection's top-left cell, and the source parses the text as Excel pastes it. Without `paste`, the component splits the text as Excel pastes it (xlcalc's `parseClipboard`: a row at each line break, a field at each tab, and one more row of empty fields after a final line break) and sends the fields to `edit`; a range that would extend past the edge of the sheet is rejected. Pasting the sheet's own copy goes through `structure` instead ([clipboard.md](clipboard.md)).
71
+ - Ctrl+Z and Ctrl+Y (Cmd on a Mac; Ctrl+Shift+Z also redoes; with any keyboard layout) on the grid call `undo()` and `redo()`, one call at a time. In an editor they undo the typing. Without these methods there is no undo.
72
+ - A successful outcome's `select` sets where the selection goes after a paste, a fill, an undo or an edit; if it is on another sheet, the component switches to it as the Name Box does. If the selection was moved while the call was pending, it stays where it was moved.
73
+ - A window's `editText` is the text a cell opens with for editing, and the formula bar shows the same text, as Excel's does; a cell missing from it opens empty, and a `CellUpdate` with `editText` changes it. Without it the component produces the text itself: the formula with its `=`, otherwise `entryText` from `xlcalc/entry` for the value in the workbook's locale, according to the cell's code in `numberFormats` (General if there is none), as Excel's formula bar displays it: a number in full with the locale's decimal separator, a percentage cell's value as a percentage, a date or time cell's value in the locale's short date and long time formats, booleans and errors in the locale's words, and text with Excel's apostrophe where typing it in that locale would be parsed as something else (`'007`; `'1,5` in ru-RU, `1,5` in en-US). `xlcalc/entry` is a separate chunk, loaded when the first cell needs its text (the active cell, for the formula bar); until it has loaded, an editor opens with the displayed text right away, and the formula bar switches to the produced text once it is available.
74
+ - Columns are drawn with xlcalc's geometry, the one it uses to lay out `####`, overflow and wrapped text (Excel for Mac's): a stored width is `columnWidthToPoints(width)` points, times 4/3 px rounded to a whole pixel, and a column whose width is `null` in a window is 53 points (71 px). A width dragged with the mouse is converted back through `columnWidthFromPoints`, in whole points, and the cells whose display changes with it come back in the `changes` of `layout`'s outcome and are redrawn, as an edit's are (a date that no longer shows `####`). The Column Width box counts characters of the stored width as Excel for Mac's does (`columnWidthToChars` and `columnWidthFromChars`): the default column is 8, and typing 8 gives the default back.
75
+ - A window's `appearance` is the conditional formatting of its cells as xlcalc calculates it (`ConditionalAppearance`), and the grid draws it as Excel does: a rule's style over the cell's own (font color, bold, italic, underline, strikethrough, fill, borders; a fill hides gridlines as the cell's own fill does), a color scale's color as a fill, a data bar from its `start` to its `end` (from the right with `rightToLeft`; a gradient from the bar's color at its axis toward white, or a solid fill; a border; a dashed axis across the cell), an icon from any of the 20 icon sets at the left of the cell before its text, and with `hideValue` only the bar or the icon (an icon shown alone follows the cell's own alignment, left for General). The text and color produced by a rule's number format are the window's `text` and `colors`.
76
+ - `subscribe(listener)` notifies the sheet of changes made elsewhere. `changed` patches the loaded windows; `reload` reads the sheets and windows again and reapplies a display filter; `repaint` with ranges re-reads in place only the windows of the displayed sheet that overlap the ranges: each window stays drawn until its response arrives, and the sizes, the scroll position and the display filter's rows stay as they are (a row that no longer matches the filter after an edit remains until the filter is reapplied, as Excel keeps it until Reapply). Repaints from one task are read together, and a repaint that arrives within 80 ms of the previous one aborts its request and reads once the burst ends; a `changed` that arrives while a window is being read is applied over its response, and a `changed` for cells off screen draws nothing. In the example after this list, `memorySource` simulates an edit from another tab and a rejection by a server.
77
+ - Ctrl+B, Ctrl+I and Ctrl+U (Cmd on a Mac, with any keyboard layout) on the grid toggle bold, italic and underline over the selection according to the active cell's formatting, through `format`, as the toolbar buttons do. Without `format` the browser handles these keys.
78
+ - Clicking a column letter or a row number selects the entire column or row up to Excel's last row or column, so the `state` event and the Name Box report `B:D`, `2:5`, or `1:1048576` for the entire sheet, and formatting applies to entire rows or columns.
79
+
80
+ ```vue
81
+ <script setup lang="ts">
82
+ import { SheetView, memorySource } from '@xlcalc/vue'
83
+ import '@xlcalc/vue/style.css'
84
+
85
+ const source = memorySource({ sheets: [{ name: 'Sheet1', cells: { A1: 'Visitors', B1: 120 } }] })
86
+ // Another tab writes B1: the sheet gets a `changed` event and redraws the cell
87
+ const remoteEdit = () => source.setCells('Sheet1', { B1: String(Math.round(Math.random() * 1000)) })
88
+ // The next edit is rejected, as a server would reject an edit of a locked cell
89
+ const refuseNext = () => source.failNext({ ok: false, reason: 'refused', message: 'This cell is locked on the server' })
90
+ </script>
91
+
92
+ <template>
93
+ <button @click="remoteEdit">Edit B1 elsewhere</button>
94
+ <button @click="refuseNext">Refuse the next edit</button>
95
+ <SheetView :source="source" style="height: 300px" />
96
+ </template>
97
+ ```
@@ -0,0 +1,26 @@
1
+ # Example app
2
+
3
+ `example/spa.html` is an Excel-like app over the local source, running entirely in the browser. It has:
4
+
5
+ - New (an empty workbook in English or Russian), Open (the file picker, or an xlsx file dropped on the page) and Save as (the workbook with its values as `<name>.xlsx`, under the name in the top bar);
6
+ - a notice of what the file lost;
7
+ - the toolbar (through the sheet's methods) with Excel's Format Cells dialog;
8
+ - the sheet with its formula bar, tabs, the structure menu on row numbers, column letters and tabs, the cell context menu, the fill handle and selection totals;
9
+ - a status bar (reading the file, calculating the workbook, ready, or calculating while a change is in progress);
10
+ - the workbook's settings: its language, in which formulas, numbers and dates are typed and displayed; the workbook reopens in it, on the sheet that was active.
11
+
12
+ The interface is in Russian or English according to the browser's language (`?lang=ru` or `?lang=en` chooses one). A single change can write at most 100,000 cells with a value or a formula (xlcalc's `limits.writes`): a cell copied over an entire column, which is 1,048,576 cells and would take seconds and hundreds of megabytes in the worker thread, is rejected immediately with the library's message. Excel has no such limit.
13
+
14
+ A file opens as Excel opens it for its user. xlsx stores no language, so a file that specifies none (only xlcalc writes one) opens in the locale of the interface language, ru-RU with the Russian interface, as Excel displays formulas in its user's language; the settings change it. The app opens the sheet that was active when the file was saved, as Excel does, otherwise the first visible one (Excel writes no active sheet when the first one was active), and Save as records the sheet on screen as the active one. What the file lost is shown in one line, like Excel's message bar: each kind of issue with its count, those that change values (links to other workbooks, formulas the engine cannot parse) first and names last; Details expands each message with its count, the sheets, example cells and names. Reading and writing a file run in a worker thread of the app (`example/spa-files-worker.ts`), and the workbook and the values stored in the file are transferred between the threads in slices of 20,000 cells, so a large file does not block the page while it opens or saves; the workbook opens with those values, so a circular reference keeps the values Excel displayed.
15
+
16
+ It needs no server of its own: the dev server serves it, and the build goes to `example/dist`, which any static server can serve.
17
+
18
+ ```sh
19
+ npm run dev:example # http://localhost:5181/spa.html
20
+ npm run build:example # example/dist
21
+ npx vite preview --config vite.example.config.ts # or: python3 -m http.server -d example/dist
22
+ ```
23
+
24
+ `npm run build:demo` builds the app alone, for `xlcalc.pridelab.dev/demo/`, into `example/dist-demo`: with its paths under `/demo/` (the worker threads too, which load via `import.meta.url`), with the app itself as `index.html`, so `/demo/` opens it, and without source maps. Any static server that serves the directory at `/demo/` can host it.
25
+
26
+ The checks that drive the app in a browser are in [checks.md](checks.md).
@@ -0,0 +1,26 @@
1
+ # Find and Replace
2
+
3
+ If the source has `find(req, signal)`, Ctrl+F or the magnifier button on the formula bar opens Excel's Find bar below the formula bar. The sheet sends the source a `FindRequest`: the query with Excel's wildcards (`*` for any characters, `?` for one character, `~` before either of them or before itself to match that character literally) and the options of Excel's dialog under the bar's Options button: Match case, Match entire cell contents, Look in Formulas (the text a cell opens with for editing, Excel's default) or Values (the displayed text), and Search By Rows or By Columns. A selection of more than one cell when the bar opens, or one made while it is open, limits the search (`range`) and is shown on the bar until the user removes it; under a display filter only its visible rows are searched. Enter and Shift+Enter, or the arrow buttons, go to the next and the previous match from the active cell in the chosen order, wrapping around at the end, as Find Next does, and a new query or option goes to its first match after the active cell. A source returns a few hundred matches at a time; beyond them, Find Next requests the next ones from the active cell (`after`, Excel's Find After), so it reaches every match, and the bar then shows the count without the current position. Find Previous goes back over the matches already received. Matches in the viewport are highlighted, and the current one pulses.
4
+
5
+ `memorySource` has both: press Ctrl+F for Find and Ctrl+H for Replace, then try `?ar*`, or Replace All of `Paris` with `Lyon`.
6
+
7
+ ```vue
8
+ <script setup lang="ts">
9
+ import { SheetView, memorySource } from '@xlcalc/vue'
10
+ import '@xlcalc/vue/style.css'
11
+
12
+ const cities = ['Paris', 'Berlin', 'Madrid', 'Rome', 'Paris', 'Vienna', 'Warsaw', 'Paris']
13
+ const cells = Object.fromEntries(cities.flatMap((city, i) => [[`A${i + 1}`, city], [`B${i + 1}`, (i + 1) * 100]]))
14
+ const source = memorySource({ sheets: [{ name: 'Offices', cells }] })
15
+ </script>
16
+
17
+ <template>
18
+ <SheetView :source="source" style="height: 320px" />
19
+ </template>
20
+ ```
21
+
22
+ ## Replace
23
+
24
+ If the sheet is editable and the source has `replace(req, by)`, Ctrl+H (on a Mac as well, as in Excel for Mac) opens the bar with its Replace row; the row's toggle is at the left of the bar. Replace replaces the active cell if it is a match and goes to the next one; otherwise it only finds, as Excel's does. Replace All replaces every match as a single undo step. Both search in formulas whatever the bar's Look in setting, as Excel's Replace does, and their outcome is handled like an edit's: the changes are drawn, and a rejection is displayed in the sheet's message line. The bar then reports how many cells changed and counts the matches again; cells the source left unchanged (`refused`) are reported in the sheet's message line with the first one's reason.
25
+
26
+ `memorySource` matches and replaces a cell's text as xlcalc's Find and Replace do (`findMatcher` and `findReplacer` of `xlcalc/text`, measured in Excel): `?` matches one UTF-16 code unit, so a character outside the BMP takes two; without Match case, σ, ς and Σ are the same letter; a trailing `~` is ignored; and in Replace each `*` matches as little as possible, except the last one in the query, which matches the rest. Unlike `localSource`, it searches hidden rows and columns, returns matches in plain row or column order, and Replace All skips cells whose text would not change.
@@ -0,0 +1,86 @@
1
+ # List mode
2
+
3
+ A host's list, such as a view or a report from a database, is displayed by the same sheet component as a workbook. The host's source returns a single sheet of rows, and a few fields of the contract and props of `SheetView` turn the sheet into a list: column titles in place of column letters, sorting by the host, drill-down cells, rows that open, and a totals row. The list keeps the sheet's features: selection, copying as Excel copies, selection totals, keyboard navigation.
4
+
5
+ ## What the source returns
6
+
7
+ - `SheetInfo.columns`: a title for each column in place of its letter, its `align` (`left`, `center`, `right`) for the title and the totals, and `sortable` when clicking the title sorts.
8
+ - `SheetInfo.rowNumbers: false`: no row numbers on the left.
9
+ - `SheetInfo.freeze`: the header rows and the first columns kept in view, as in a workbook. Rows within `freeze.rows` are the header and do not open.
10
+ - `SheetInfo.totals`: the totals row, the displayed text for each of the sheet's columns (`null` for an empty cell). It stays at the bottom of the viewport as frozen rows stay at its top, and the sheet scrolls so the active cell is not hidden under it.
11
+ - `SheetWindow.drill`: the drill-down cells, by address. They are underlined; an empty cell is never a drill-down.
12
+ - No `edit` for a read-only list, and `formulaBar: false` when the list has no formulas to show.
13
+
14
+ ## Sorting
15
+
16
+ The host sorts, as a server-side list is sorted on the server. Clicking a sortable title emits `sort` with the column (1-based) and the direction: ascending first, descending on the column the `sort` prop already shows as ascending. The host changes its query, sets the `sort` prop, which shows the arrow on the title, and sends `reload` from the source, after which the sheet reads its windows again. Clicking a title that is not sortable selects its column, as a column letter does.
17
+
18
+ A complete list over `memorySource`, which stands in for the host's query here: the page sorts its rows and writes them over the sheet with `setCells`, where a server-side list would answer a new query and send `reload`.
19
+
20
+ ```vue
21
+ <script setup lang="ts">
22
+ import { ref } from 'vue'
23
+ import { SheetView, memorySource, type ListSort, type MemoryCell } from '@xlcalc/vue'
24
+ import '@xlcalc/vue/style.css'
25
+
26
+ interface Client { name: string; region: string; sum: number }
27
+ const clients: Client[] = Array.from({ length: 50 }, (_, i) => ({
28
+ name: `Client ${String(i + 1).padStart(2, '0')}`,
29
+ region: ['North', 'South', 'East', 'West'][i % 4]!,
30
+ sum: ((i + 1) * 7919) % 10_000 / 10,
31
+ }))
32
+ const keys = ['name', 'region', 'sum'] as const
33
+ const letters = ['A', 'B', 'C']
34
+
35
+ // The list as cells: row i + 1 is clients[i]; the sums are drill-downs
36
+ function cellsOf(list: Client[]): Record<string, MemoryCell> {
37
+ const cells: Record<string, MemoryCell> = {}
38
+ list.forEach((client, i) => keys.forEach((k, c) => (cells[`${letters[c]}${i + 1}`] = { value: client[k], ...(k === 'sum' ? { drill: true } : {}) })))
39
+ return cells
40
+ }
41
+
42
+ const source = memorySource({
43
+ sheets: [
44
+ {
45
+ name: 'Clients',
46
+ rowNumbers: false,
47
+ freeze: { rows: 0, cols: 1 },
48
+ columns: [{ title: 'Client', sortable: true }, { title: 'Region', sortable: true }, { title: 'Sum', align: 'right', sortable: true }],
49
+ totals: ['Total', null, clients.reduce((n, c) => n + c.sum, 0).toFixed(1)],
50
+ widths: { A: 14, B: 12, C: 12 },
51
+ cells: cellsOf(clients),
52
+ },
53
+ ],
54
+ })
55
+
56
+ const sort = ref<ListSort | null>(null)
57
+ const opened = ref('')
58
+
59
+ // The host's sort: the rows in the requested order, written over the sheet
60
+ function onSort(next: ListSort): void {
61
+ const k = keys[next.col - 1]!
62
+ const sign = next.dir === 'asc' ? 1 : -1
63
+ const sorted = clients.slice().sort((a, b) => (a[k] < b[k] ? -sign : a[k] > b[k] ? sign : 0))
64
+ const entries: Record<string, string> = {}
65
+ for (const [cell, { value }] of Object.entries(cellsOf(sorted))) entries[cell] = String(value)
66
+ source.setCells('Clients', entries)
67
+ sort.value = next
68
+ }
69
+ </script>
70
+
71
+ <template>
72
+ <p>{{ opened }}</p>
73
+ <SheetView
74
+ :source="source" :sort="sort" open-rows read-only :formula-bar="false" style="height: 360px"
75
+ @sort="onSort" @cell-drill="opened = `The deals behind ${$event.cell}`" @row-open="opened = `Client of row ${$event.row}`"
76
+ />
77
+ </template>
78
+ ```
79
+
80
+ ## Drill-down and opening rows
81
+
82
+ Clicking a drill-down cell, or pressing Enter on it, emits `cellDrill` with the sheet and the cell's address; the selection stays. With `openRows`, the cells of the first column are links that emit `rowOpen` with the sheet and the row, and Enter on any cell that is not a drill-down also opens its row. Without `openRows`, Enter moves the active cell as on a regular sheet. A click with Shift, Ctrl or Cmd extends or adds to the selection and opens nothing.
83
+
84
+ ## Appearance
85
+
86
+ Drill-down cells and the first column's links use `--xs-link`, and the totals row uses `--xs-head-bg` with a `--xs-freeze-line` line above it. The tooltips of the titles and links are `SheetMessages`' `sortColumn` and `openRow`.
package/docs/local.md ADDED
@@ -0,0 +1,94 @@
1
+ # In-browser calculation: `@xlcalc/vue/local`
2
+
3
+ `localSource(book)` is a `SheetSource` over an xlcalc `BookInput`. It requires `xlcalc` 0.46.0 or later, an optional peer dependency of the package that the host installs. The workbook is calculated in a Web Worker (`xlcalc/worker`), or on the page's main thread with `calc: 'inline'`. The workbook's locale module is loaded when the source opens (ru-RU from `xlcalc/locales/ru-RU`); for a locale xlcalc has no module for, opening fails with the library's message.
4
+
5
+ ## What the source does
6
+
7
+ The xlcalc session does the work as Excel does, and the source exposes it through every optional method of `SheetSource`. A window is one `getViewport` call: formulas as displayed in the workbook's locale, the text each cell opens with for editing (`editText`), styles, the cells' conditional formatting (`appearance`), widths, heights, merged ranges and tables.
8
+
9
+ ## Editing
10
+
11
+ Typed text is parsed in the workbook's locale, with several cells as a single undo step (`=СУММ(A1:A2)`, `1 234,5`, `15.03.2026` in ru-RU), and a column widens when a typed number does not fit. A number typed over a percentage cell gets Excel's `%` (6 becomes 6%), while the cell's own text edited in place (`inPlace`: F2, a double click, the formula bar) does not (6% stays 0.06). Delete over a selection clears the contents and the hyperlinks, as Excel's Delete does. An unclosed formula is closed or rejected as Excel handles it on Return (xlcalc 0.44): one that starts with a function whose parenthesis is the only one open is closed silently (`=СУММ(1;2`), and any other is `invalid`, with the session's `correction` when Excel suggests one (`=(1+2` becomes `=(1+2)`, `=1+СУММ(2` becomes `=1+СУММ(2)`), in `edit` and `fill` alike. `paste` parses clipboard text by Excel's paste rules (a leading apostrophe stays in the text, and a percentage cell has no automatic percentage entry), `fill` is Ctrl+Enter, and `undo` and `redo` are the session's, undoing whole changes.
12
+
13
+ ## Data validation
14
+
15
+ `validation` returns the session's rule for a cell (`getValidation`): a list's items as the cells display them, by their number format, when the rule shows its dropdown, and the input message unless the rule turns it off. `pick` writes an item with the session's `setCells` as Excel writes it (measured by xlcalc 0.29 in Excel for Mac): an item from a cell range gives that cell's value as is, and an item of a literal list is parsed as typed in en-US in any workbook (`"001,1/2,x"` gives 1 and January 2 in `d-mmm`); the list is read again first, and an item whose text is no longer at its position is rejected (`conflict`). A value that violates a data validation rule of any style is rejected with the rule's alert before anything is written, and the undo history stays unchanged; several cells at once are rejected by the strictest rule they violate, as xlcalc's `refuseInvalid` chooses it, and Ctrl+Enter by the active cell's rule. With `accept` the texts are written without the check, as Excel writes them after Yes or OK; a rule with `showError: false` accepts any value, as Excel's does.
16
+
17
+ ## Fill, merging, structure and the rest
18
+
19
+ The fill handle's `fillRange` is xlcalc's AutoFill (xlcalc 0.39, measured in Excel for Windows): the target range is filled from the source range as `copyRange` copies it, then series continue (numbers by their trend, dates by day, by step or by month, times, text with a number, quarters, day and month names), backwards when dragging up or left; the copies also go into hidden rows, on a filtered sheet as well, as Excel's fill handle fills them (from xlcalc 0.51.0; with an earlier xlcalc the rows hidden by a filter stay empty). Merge Cells and Unmerge Cells are xlcalc's `mergeCells` and `unmergeCells` structure operations (xlcalc 0.37): the merged range keeps its top-left value, its cells take the top-left cell's formatting, and a range that overlaps a table or a legacy array formula is rejected with the library's message. Structure operations, widths and heights, formatting on top of each cell's formatting (`format`, as Excel's Bold over a selection), frozen panes (`freeze`, one `setLayout` call and a single undo step), `explain`, `end` (the session's `getEnd`, Excel's Range.End as measured by xlcalc), totals, Find and Replace, the display filter and sort complete the source. Formula AutoComplete's `functionNames` returns every function xlcalc calculates, by its name in the workbook's locale, and the argument tooltip's `functionSignature` is xlcalc's `functionSignature` (xlcalc 0.40) with its `functionDescription` (xlcalc 0.43) in the workbook's locale. The localized names (`xlcalc/function-text/ru-RU` for ru-RU) and the descriptions (`xlcalc/function-text`) load with the first tooltip; a name is accepted in English or in the locale, in any letter case, and the result is null for a function xlcalc does not have ([autocomplete.md](autocomplete.md)).
20
+
21
+ ## Find and Replace
22
+
23
+ Find is the session's `findCells` with the options of Excel's dialog, which returns the cells Excel's Find returns, in the same order, as measured by xlcalc in Excel for Windows: `?` matches one UTF-16 code unit (`a??b` finds `a😀b`); Look in Formulas searches the text a cell opens with for editing, but without a text's apostrophe (`'007` is found by `007`, not by `'007`); without Match case, `σ`, `ς` and `Σ` are the same letter and `i` does not match `İ`; and hidden rows and columns are always skipped when searching values, and when searching formulas on a sheet with a filter (xlcalc's `SheetInput.filtered`), where the matches come in the blocks between the hidden rows or columns. The session keeps the texts it searches and updates them with edits: on the data sheet of the synthetic workbook of 34,000 rows in Node, the first search takes about 0.48 s, and the same query again, or a longer one as it is typed, 20 to 35 ms. Under the display filter its rows are passed to the library (`rows`), which takes a few milliseconds; `after` is Find After, the next matches after a cell. Replace All is the session's `replaceInCells`, Excel's Replace All as measured by xlcalc, as a single undo step: it takes the cells Find finds in formulas, whatever the Look in setting, replaces the matches in each (each `*` matches as little as possible, the last one as much as possible), and enters each cell again even if its text is unchanged, without automatic percentage entry (5 in a percentage cell becomes 500%); a cell whose new formula would not parse is left unchanged and is returned in `refused` with the reason, and the other cells are written.
24
+
25
+ ## Display filter and totals
26
+
27
+ The display filter, its sort and the filter funnel's value list follow the same rules as in `memorySource` (`src/sheet-query.ts`): a condition compares the displayed text, the header rows stay on top, and a sort and the funnel's list order values as Excel's SORT does (xlcalc's `valueComparator`): numbers, text case-insensitively in the order of the workbook's locale, FALSE, TRUE, errors, with empty cells and formulas returning `""` last in either direction. `./local` passes the workbook's locale to the comparator and `memorySource` passes none, which means en-US; the ru-RU order is the same as en-US (`Item10` before `Item2`, Latin before Cyrillic), as Excel for Windows sorts in every locale, whereas Excel for Mac in ru-RU puts Cyrillic first. The source reads from the session only the columns a condition, a sort or a funnel needs, the first time they are needed, keeps them in arrays by row and updates them with edits: on the data sheet of the synthetic workbook of 34,000 rows by 15 columns in Node, through the pool, the first sort takes about 0.16 s and 7 MB, and the first funnel 0.12 s. A request's signal stops the session's read of its columns (`getCells` with `signal`), and a read stopped for one request is repeated for another request still waiting for it. Selection totals come from the session's `summarizeRanges`, which counts without transferring the cells, and under a display filter covers only its rows. A cell is counted when it displays something or contains a value, so a number hidden by its format is counted and a formula returning `""` is not, as the grid counts the cells of its windows.
28
+
29
+ ## Outline
30
+
31
+ A sheet's `outline` comes from xlcalc's `outlineGroups`: a group of level L is a maximal run of rows or columns at level L or deeper, collapsed when Excel keeps the collapsed flag on its summary row or column. Excel sets the flag as soon as all the rows or columns under it are hidden, manually hidden ones included, and keeps it on a summary row or column hidden inside a hidden group. `outline(change)` is one of xlcalc's structure operations (`editStructure`), a single undo step that recalculates SUBTOTAL and AGGREGATE, following the rules xlcalc measured in Excel for Windows (its `docs/outline.md`):
32
+
33
+ - Group moves each row or column one level deeper, leaving those at the seventh level there, and is rejected when all of them are at the seventh level.
34
+ - Ungroup removes a level from the rows or columns that have one and leaves hidden ones hidden.
35
+ - A group's − and + buttons are Hide Detail and Show Detail at its summary row or column, which act on all of that summary's detail, so the button of an inner group that shares its summary row with the outer group hides and shows both.
36
+ - Show Detail also shows rows or columns hidden manually, except those of inner groups whose summary keeps its collapsed flag.
37
+ - A level button hides the rows or columns of that level and deeper, shows the other grouped ones, manually hidden ones included, and leaves rows or columns outside groups as they are.
38
+
39
+ Rejection messages come from the library, in the workbook's language. A group at the edge of the sheet (rows 1:3 with summaries above) has no summary row and, as in Excel, no button: the grid draws none, and a command for it is rejected. `memorySource` also derives its groups with xlcalc's `outlineGroups` and runs the same commands without the engine, through xlcalc's `outlineEdit` from `xlcalc/a1`, with the same rejections in English; a row or column it hides gets a size of 0, and one it shows again gets the default size.
40
+
41
+ ## Files
42
+
43
+ `openXlsx` reads a file into a workbook and the values stored in it (`cached`), and `saveXlsx` writes the source's workbook with its values. With `locale` (the language the user chose: a tag whose module is loaded on demand, or a locale object), their issue messages are in that language; without it, in English. A tag xlcalc has no module for fails with the library's message.
44
+
45
+ A page that opens an xlsx file, reports what the file lost, and saves the workbook back under the file's name:
46
+
47
+ ```vue
48
+ <script setup lang="ts">
49
+ import { onUnmounted, shallowRef } from 'vue'
50
+ import { SheetView } from '@xlcalc/vue'
51
+ import { groupIssues, localSource, openXlsx, saveXlsx, type LocalSource } from '@xlcalc/vue/local'
52
+ import '@xlcalc/vue/style.css'
53
+
54
+ const source = shallowRef<LocalSource | null>(null)
55
+ const lost = shallowRef<string[]>([])
56
+ let fileName = 'book.xlsx'
57
+
58
+ async function open(event: Event): Promise<void> {
59
+ const file = (event.target as HTMLInputElement).files?.[0]
60
+ if (!file) return
61
+ // cached: the values stored in the file, which a circular reference keeps; issues: what the workbook cannot keep
62
+ const { book, cached, issues } = await openXlsx(file)
63
+ void source.value?.close()
64
+ source.value = localSource(book, { session: { cached } })
65
+ fileName = file.name
66
+ lost.value = groupIssues(issues).map((group) => `${group.message} (${group.count})`)
67
+ }
68
+
69
+ async function save(): Promise<void> {
70
+ if (!source.value) return
71
+ const { data } = await saveXlsx(source.value) // the xlsx bytes: formulas along with their values
72
+ const link = document.createElement('a')
73
+ link.href = URL.createObjectURL(new Blob([data as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }))
74
+ link.download = fileName
75
+ link.click()
76
+ setTimeout(() => URL.revokeObjectURL(link.href))
77
+ }
78
+
79
+ onUnmounted(() => void source.value?.close())
80
+ </script>
81
+
82
+ <template>
83
+ <input type="file" accept=".xlsx" @change="open" />
84
+ <button :disabled="!source" @click="save">Save as</button>
85
+ <p v-for="line in lost" :key="line">{{ line }}</p>
86
+ <SheetView v-if="source" :source="source" :locale="source.locale" style="height: 420px" />
87
+ </template>
88
+ ```
89
+
90
+ A workbook read from a file opens as Excel opens the file, with its stored values passed to the session as `session: { cached }` (xlcalc's `SessionOptions.cached`, 0.27 or later): a circular reference without iterative calculation is not calculated, and its cells and the cells that depend on them keep the values stored in the file, through edits elsewhere, until a value typed into the cycle breaks it (xlcalc's `docs/cycles.md`). Without them those cells contain 0. The session's workbook records the kept values (`held`), so a workbook obtained with `book()` opens with them again, and `saveXlsx` saves them.
91
+
92
+ ## Threads, the workbook and events
93
+
94
+ The pool starts its thread as `new Worker(new URL('./thread.js', import.meta.url))`, which Vite and other bundlers recognize; where a bundler serves the thread script from elsewhere, pass `pool: { spawn }`. Besides the `SheetSource` methods, the source has `ready`, `locale`, `history` (the steps `undo` and `redo` can take, for a toolbar), `book()`, `snapshot()` (the workbook and its values captured at one moment, which is what `writeXlsx` takes; it replaces `result()` of 0.2) and `close()`. It notifies its subscribers of every change it makes: `changed` for edits and widths, `reload` for structure, number formats, rows and columns, frozen panes and their undo, and `repaint` with the formatted ranges for a style set by `format` (a style changes no text or size). The values the source caches for the display filter are kept through a style change, a freeze, and an undo or redo of an edit, since these change no cells or list the cells they change. An edit, a paste, a fill, an undo or a redo in a workbook with conditional formats sends `repaint` after its `changed`, with the ranges of the workbook's rules returned by the session: their cells may look different anywhere the rules apply (a color scale changes colors when a single number changes), and a cell update does not carry formatting. The values the source caches for search and the filter are kept then, unless a rule displays numbers with its own number format, and a display filter keeps its rows.