@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
@@ -0,0 +1,221 @@
1
+ # Quick start
2
+
3
+ Common uses of the component, each with a single-file Vue component that works as written. Install the package first ([README](../README.md#install)); every example imports the stylesheet once, which an app does in its entry file instead. Each section links to the full documentation.
4
+
5
+ ## A table the page already has
6
+
7
+ The host's data shown as a sheet: `memorySource` turns it into a workbook in memory, without the calculation engine. A cell displays its `text` when it has one, so the host formats numbers itself; `readOnly` turns editing off, and the frozen first row keeps the titles in view. The sheet's `state` event reports the selection and the active cell's text, as Excel's Name Box and formula bar show them.
8
+
9
+ ```vue
10
+ <script setup lang="ts">
11
+ import { shallowRef } from 'vue'
12
+ import { SheetView, memorySource, type MemoryCell, type SheetViewState } from '@xlcalc/vue'
13
+ import '@xlcalc/vue/style.css'
14
+
15
+ const orders = [
16
+ { client: 'Acme', amount: 1250.5, paid: true },
17
+ { client: 'Globex', amount: 980, paid: false },
18
+ { client: 'Initech', amount: 15300.75, paid: true },
19
+ ]
20
+ const money = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
21
+
22
+ // The titles in row 1 in bold, each order in a row below them
23
+ const cells: Record<string, MemoryCell> = { A1: { value: 'Client', style: 0 }, B1: { value: 'Amount', style: 0 }, C1: { value: 'Paid', style: 0 } }
24
+ orders.forEach((order, i) => {
25
+ cells[`A${i + 2}`] = { value: order.client }
26
+ cells[`B${i + 2}`] = { value: order.amount, text: money.format(order.amount) }
27
+ cells[`C${i + 2}`] = { value: order.paid }
28
+ })
29
+
30
+ const source = memorySource({
31
+ styles: [{ font: { bold: true } }],
32
+ sheets: [{ name: 'Orders', freeze: { rows: 1, cols: 0 }, widths: { A: 16, B: 12 }, cells }],
33
+ })
34
+ const state = shallowRef<SheetViewState | null>(null)
35
+ </script>
36
+
37
+ <template>
38
+ <p>{{ state ? `${state.ref}: ${state.value ?? ''}` : '' }}</p>
39
+ <SheetView :source="source" read-only hide-lone-tab style="height: 300px" @state="state = $event" />
40
+ </template>
41
+ ```
42
+
43
+ Without `readOnly` the cells can be edited, and `memorySource` keeps what is typed; it does not calculate formulas, so a formula cell displays the `value` given with it. [The `memorySource` reference](api.md#memorysource) lists what a sheet can carry: styles, merged ranges, notes, hyperlinks, data validation, outline.
44
+
45
+ ## A model the user edits
46
+
47
+ `localSource` from `@xlcalc/vue/local` calculates an xlcalc workbook in the browser, in a Web Worker: the user types into the input cells, and the formulas recalculate as in Excel, with undo, fill, Find and the context menus. The workbook is the library's `BookInput`, formulas without `=`, so it can come from the host's code, its server or an xlsx file. This entry point needs `xlcalc` installed.
48
+
49
+ ```vue
50
+ <script setup lang="ts">
51
+ import { onUnmounted } from 'vue'
52
+ import { SheetView } from '@xlcalc/vue'
53
+ import { localSource } from '@xlcalc/vue/local'
54
+ import '@xlcalc/vue/style.css'
55
+
56
+ // A loan: the inputs in B1:B3, the payment and the interest calculated from them
57
+ const source = localSource({
58
+ sheets: [
59
+ {
60
+ name: 'Loan',
61
+ columns: { A: { width: 12 }, B: { width: 14 } },
62
+ cells: {
63
+ A1: { value: 'Amount' }, B1: { value: 200000, numberFormat: '#,##0' },
64
+ A2: { value: 'Rate' }, B2: { value: 0.06, numberFormat: '0.00%' },
65
+ A3: { value: 'Years' }, B3: { value: 20 },
66
+ A5: { value: 'Payment' }, B5: { formula: '-PMT(B2/12,B3*12,B1)', numberFormat: '#,##0.00' },
67
+ A6: { value: 'Interest' }, B6: { formula: 'B5*B3*12-B1', numberFormat: '#,##0.00' },
68
+ },
69
+ },
70
+ ],
71
+ })
72
+ // The worker and the session end with the page's component
73
+ onUnmounted(() => void source.close())
74
+ </script>
75
+
76
+ <template>
77
+ <SheetView :source="source" :locale="source.locale" hide-lone-tab style="height: 300px" />
78
+ </template>
79
+ ```
80
+
81
+ `source.book()` returns the workbook with the user's changes, for the host to store. [In-browser calculation](local.md) describes what the local source does and its options, such as `calc: 'inline'` to calculate on the page's main thread.
82
+
83
+ ## Excel files: open, edit, save
84
+
85
+ `openXlsx` reads a file into a workbook and the values Excel stored in it, and `saveXlsx` writes the workbook back, formulas along with their values. A new source replaces the previous workbook; the previous source is closed.
86
+
87
+ ```vue
88
+ <script setup lang="ts">
89
+ import { onUnmounted, shallowRef } from 'vue'
90
+ import { SheetView } from '@xlcalc/vue'
91
+ import { localSource, openXlsx, saveXlsx, type LocalSource } from '@xlcalc/vue/local'
92
+ import '@xlcalc/vue/style.css'
93
+
94
+ const source = shallowRef<LocalSource | null>(null)
95
+
96
+ async function open(event: Event): Promise<void> {
97
+ const file = (event.target as HTMLInputElement).files?.[0]
98
+ if (!file) return
99
+ const { book, cached } = await openXlsx(file)
100
+ void source.value?.close()
101
+ source.value = localSource(book, { session: { cached } })
102
+ }
103
+
104
+ async function save(): Promise<void> {
105
+ if (!source.value) return
106
+ const { data } = await saveXlsx(source.value)
107
+ const link = document.createElement('a')
108
+ link.href = URL.createObjectURL(new Blob([data as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }))
109
+ link.download = 'workbook.xlsx'
110
+ link.click()
111
+ setTimeout(() => URL.revokeObjectURL(link.href))
112
+ }
113
+
114
+ onUnmounted(() => void source.value?.close())
115
+ </script>
116
+
117
+ <template>
118
+ <input type="file" accept=".xlsx" @change="open" />
119
+ <button :disabled="!source" @click="save">Save as</button>
120
+ <SheetView v-if="source" :source="source" :locale="source.locale" style="height: 420px" />
121
+ </template>
122
+ ```
123
+
124
+ `openXlsx` also returns `issues`, what the workbook could not keep from the file (pictures, charts); [Files](local.md#files) shows how to report them, and the [example app](example-app.md) is a complete Excel-like page built this way.
125
+
126
+ ## Excel's formatting toolbar
127
+
128
+ `SheetToolbar` is Excel's formatting toolbar for the sheet's selection: Bold, colors, borders, alignment, Wrap Text, the Number Format box, Merge & Center, Undo and Redo. It reads the selection from the sheet's `state` event and formats through the sheet's template ref. It is shown for a source with `format`, such as the local one; Ctrl+1 opens Excel's Format Cells dialog on the sheet itself.
129
+
130
+ ```vue
131
+ <script setup lang="ts">
132
+ import { onUnmounted, ref, shallowRef } from 'vue'
133
+ import { SheetToolbar, SheetView, type SheetViewApi, type SheetViewState } from '@xlcalc/vue'
134
+ import { localSource } from '@xlcalc/vue/local'
135
+ import '@xlcalc/vue/style.css'
136
+
137
+ const source = localSource({
138
+ sheets: [{ name: 'Sheet1', cells: { A1: { value: 'Region' }, B1: { value: 'Sales' }, A2: { value: 'North' }, B2: { value: 1200 }, A3: { value: 'South' }, B3: { value: 950 } } }],
139
+ })
140
+ onUnmounted(() => void source.close())
141
+
142
+ const sheet = ref<SheetViewApi | null>(null)
143
+ const state = shallowRef<SheetViewState | null>(null)
144
+ </script>
145
+
146
+ <template>
147
+ <SheetToolbar :source="source" :sheet="sheet" :state="state" :locale="source.locale" />
148
+ <SheetView ref="sheet" :source="source" :locale="source.locale" style="height: 360px" @state="state = $event" />
149
+ </template>
150
+ ```
151
+
152
+ [The formatting toolbar and Format Cells](toolbar.md) describes each button and how a host places its own Format Cells dialog.
153
+
154
+ ## A workbook on your server
155
+
156
+ The component reads a workbook only through a `SheetSource`, so a workbook that lives on the host's server needs two methods: the list of sheets and a window of cells. Each further method turns on a feature: `edit` makes the sheet editable, `subscribe` brings in changes made elsewhere, `undo`, `find`, `format` and the rest add their commands. The `fallback` slot shows a failure to read the workbook, with a retry.
157
+
158
+ ```vue
159
+ <script setup lang="ts">
160
+ import { SheetView, type SheetInfo, type SheetPending, type SheetSource, type SheetWindow } from '@xlcalc/vue'
161
+ import '@xlcalc/vue/style.css'
162
+
163
+ const props = defineProps<{ base: string }>()
164
+
165
+ async function get<T>(path: string, signal?: AbortSignal): Promise<T> {
166
+ const res = await fetch(props.base + path, { signal })
167
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
168
+ return (await res.json()) as T
169
+ }
170
+
171
+ // The server answers with the contract's types as JSON; without `edit` the sheet is read-only
172
+ const source: SheetSource = {
173
+ sheets: (signal) => get<SheetInfo[]>('/sheets', signal),
174
+ window: ({ sheet, row, rows, col, cols }, signal) =>
175
+ get<SheetWindow | SheetPending>(`/window?${new URLSearchParams({ sheet, row: `${row}`, rows: `${rows}`, col: `${col}`, cols: `${cols}` })}`, signal),
176
+ }
177
+ </script>
178
+
179
+ <template>
180
+ <SheetView :source="source" style="height: 420px">
181
+ <template #fallback="{ failure, retry }">
182
+ <p>{{ failure.text }} <button @click="retry">Try again</button></p>
183
+ </template>
184
+ </SheetView>
185
+ </template>
186
+ ```
187
+
188
+ The server can calculate with xlcalc itself: a session's `getViewport` returns what a window holds. [The source](api.md#the-source-sheetsource) lists every method with the feature it turns on, with a fuller source over GET, POST and server-sent events; a new `source` prop means a new workbook.
189
+
190
+ ## The Russian interface and a Russian workbook
191
+
192
+ The interface texts are English by default; `ruMessages` is the complete Russian set, and a host overrides single texts by spreading it. A workbook's locale is separate: a ru-RU workbook displays and accepts formulas, numbers and dates as Russian Excel does (`=СУММ(B2:B3)`, `1 234,50`), and the sheet takes it as `locale`.
193
+
194
+ ```vue
195
+ <script setup lang="ts">
196
+ import { onUnmounted } from 'vue'
197
+ import { SheetView, ruMessages } from '@xlcalc/vue'
198
+ import { localSource } from '@xlcalc/vue/local'
199
+ import '@xlcalc/vue/style.css'
200
+
201
+ const messages = { ...ruMessages, loading: 'Загружаю книгу…' }
202
+ const source = localSource({
203
+ locale: 'ru-RU',
204
+ sheets: [{ name: 'Лист1', cells: { A1: { value: 'Выручка' }, B2: { value: 1234.5 }, B3: { value: 800 }, B4: { formula: 'SUM(B2:B3)', numberFormat: '#,##0.00' } } }],
205
+ })
206
+ onUnmounted(() => void source.close())
207
+ </script>
208
+
209
+ <template>
210
+ <SheetView :source="source" :locale="source.locale" :messages="messages" style="height: 300px" />
211
+ </template>
212
+ ```
213
+
214
+ [Interface texts](api.md#interface-texts-sheetmessages) lists the keys; the colors and sizes are CSS variables ([CSS variables](api.md#css-variables)).
215
+
216
+ ## Where to go next
217
+
218
+ - [Cell editing and keyboard shortcuts](editing.md): what each key does and which source method each feature needs.
219
+ - [Rows, columns and sheets](sheets.md), [Clipboard and fill handle](clipboard.md), [Find and Replace](find-replace.md), [Data validation](validation.md).
220
+ - [List mode](list-mode.md): the host's own list shown on the sheet, sorted by the host, with drill-down cells and a totals row.
221
+ - [API reference](api.md): every export, prop, event and method.
package/docs/sheets.md ADDED
@@ -0,0 +1,61 @@
1
+ # Rows, columns and sheets
2
+
3
+ ## Context menus
4
+
5
+ Right-clicking a row number or a column letter, or a long press of half a second on it on a touch screen, opens Excel's context menu for the selected rows or columns (a row or column outside a selection of entire rows or columns is selected first):
6
+
7
+ - Insert and Delete, as many rows or columns as are selected, through `structure(op)`;
8
+ - Row Height or Column Width in Excel's units (points, up to 409.5; characters of the default font, as Excel for Mac's dialog shows them) with Excel's limits, and Hide and Unhide (always present, as in Excel), through `layout(change)`; a size of 0 hides the rows or columns.
9
+
10
+ The same on a sheet tab makes that sheet active and offers Insert (before it, with Excel's next default name: `Sheet4`, or `Лист4` with `ruMessages`), Delete (with a confirmation when the sheet has data), Rename (in place on the tab, also by double-clicking it), Move Left and Move Right among the visible tabs, Hide, and Unhide with the list of hidden sheets. If the source rejects a command, its message is displayed (`./local` returns the library's message in the workbook's locale), and after a change the sheet reads its sheets and windows again unless the source has sent `reload`. The sheet displayed afterwards is the one Excel displays: an inserted or unhidden sheet, and after a sheet is deleted or hidden, the next visible sheet, or the previous one if it was the last. Without `structure` or `layout` the corresponding items are absent; without both, the browser's own context menu remains, and a read-only sheet has none. The dialogs for sizes, the delete confirmation, the hidden sheets, and Group and Ungroup are modal: Esc or a click outside closes them, and Tab and Shift+Tab stay inside.
11
+
12
+ A workbook over `localSource`, which has `structure`, `layout` and `outline`: right-click a row number, a column letter or a sheet tab to open its menu (Unhide on a tab offers the hidden sheet), and click the outline buttons to the left of the rows, where South is collapsed.
13
+
14
+ ```vue
15
+ <script setup lang="ts">
16
+ import { onUnmounted } from 'vue'
17
+ import { SheetView } from '@xlcalc/vue'
18
+ import { localSource } from '@xlcalc/vue/local'
19
+ import '@xlcalc/vue/style.css'
20
+
21
+ const source = localSource({
22
+ sheets: [
23
+ {
24
+ name: 'Sales',
25
+ cells: {
26
+ A1: { value: 'Region' }, B1: { value: 'Sales' },
27
+ A2: { value: 'North A' }, B2: { value: 10 },
28
+ A3: { value: 'North B' }, B3: { value: 20 },
29
+ A4: { value: 'North' }, B4: { formula: 'SUBTOTAL(9,B2:B3)' },
30
+ A5: { value: 'South A' }, B5: { value: 5 },
31
+ A6: { value: 'South B' }, B6: { value: 15 },
32
+ A7: { value: 'South' }, B7: { formula: 'SUBTOTAL(9,B5:B6)' },
33
+ A8: { value: 'Total' }, B8: { formula: 'SUBTOTAL(9,B2:B7)' },
34
+ },
35
+ // Two level-2 groups inside a level-1 group; South's rows are hidden and its summary row keeps the collapsed flag
36
+ rows: {
37
+ '2:3': { outlineLevel: 2 },
38
+ '4': { outlineLevel: 1 },
39
+ '5:6': { outlineLevel: 2, hidden: true },
40
+ '7': { outlineLevel: 1, collapsed: true },
41
+ },
42
+ columns: { A: { width: 14 } },
43
+ },
44
+ { name: 'Archive', hidden: true, cells: { A1: { value: 'Last year' } } },
45
+ ],
46
+ })
47
+ onUnmounted(() => void source.close())
48
+ </script>
49
+
50
+ <template>
51
+ <SheetView :source="source" :locale="source.locale" style="height: 360px" />
52
+ </template>
53
+ ```
54
+
55
+ ## Outline
56
+
57
+ A sheet whose `SheetInfo.outline` has groups gets Excel's outline bars: the row bar to the left of the row numbers and the column bar above the column letters, with a slot for each level plus one. Each expanded group has a thin bracket along its rows or columns with a tick at the end away from its summary row or column, and a − button on the summary row or column (below the group, or above it without `summaryBelow`; to the right of the columns, or to the left without `summaryRight`); a collapsed group has only its + button. The level buttons 1 to N+1 are in the corner: the rows' buttons run across their bar at the height of the column letters, the columns' buttons run down their bar above the row numbers. A group's button calls `outline({ op: 'collapse' | 'expand', sheet, rows: '3:6', level })` (`columns: 'B:D'` for columns), a level button calls `outline({ op: 'showLevel', sheet, axis, level })`, and the sheet reloads afterwards unless the source has sent `reload`. Without `outline`, or on a read-only sheet, the bars are drawn but their buttons do nothing. Under a display filter the row bar is not shown, because the displayed rows are not the sheet's rows in order. On a touch screen the bars' slots are wider, and a tap on a bar presses the nearest button within 22 px, a 44 px target around a button drawn at Excel's size. The context menu of row numbers and column letters has Group and Ungroup for the selected rows or columns, as do Excel's shortcuts Shift+Alt+Right and Shift+Alt+Left (Cmd+Shift+K and Cmd+Shift+J on a Mac); with a range of cells selected, a small dialog asks whether to group rows or columns, rows first, as Excel's does. Commands that Excel does not perform are reported and not sent: a Group beyond the seventh level, an Ungroup of rows or columns that are in no group. The bars' colors and the buttons' size are set by the `--xs-outline-*` variables.
58
+
59
+ ## Tabs and sizes
60
+
61
+ The sheet tabs are shown even for a single sheet and end with a "+" button, which inserts a sheet at the end and displays it (with `structure`); hidden sheets have no tabs. Double-clicking a column header's edge fits the column to its contents through `layout({ columns, fit: true })`, or every selected entire column when the column is one of them, as Excel does; without `layout` it restores the workbook's width. With `layout`, the bottom edge of a row number drags the row's height with a mouse (on a touch screen, Row Height is in the row menu, since a finger on a row number would snap to the edge); the row is redrawn as it moves, and the release sets the height in points through `layout({ rows, height })`, for every selected entire row when the dragged row is among them, as Excel does. Double-clicking that edge fits the row (and the selected entire rows it belongs to) to its contents through `layout({ rows, fit: true })`, which `./local` passes to xlcalc's row AutoFit. With `./local`, text typed with a line break (Alt+Enter) wraps its cell and fits its row, unless the row height was set manually, as in Excel (xlcalc 0.33). Two `SheetView` props reproduce an older grid's behavior: `hide-lone-tab` (no tabs for a workbook with one sheet) and `edge-restores-width` (double-clicking a column edge restores the workbook's width).
@@ -0,0 +1,72 @@
1
+ # The formatting toolbar and Format Cells
2
+
3
+ ## `SheetToolbar`
4
+
5
+ `SheetToolbar` is Excel's formatting toolbar over the same source: Undo and Redo, Bold, Italic, Underline, font and fill colors from Excel's palette, borders (All Borders, Outside Borders, Bottom Border, No Border), horizontal alignment, Wrap Text, Excel's Number Format box and, for a host that displays the dialog ([below](#formatdialog)), Format Cells. It formats the selection reported by the sheet's `state` event through `format(sheet, patches)`, as Excel formats a selection: Bold applies on top of each cell's formatting, and Outside Borders is four patches in one call and therefore a single undo step. Its buttons show as pressed according to the active cell's formatting, and clicking a pressed button removes that formatting, as in Excel. The toolbar is shown only when the source has `format`; Undo and Redo appear when the source has `undo` and `redo`, and are disabled while its `history` (the local source's) has no steps.
6
+
7
+ With `sheet` and the source's `structure`, the toolbar has Excel's Merge & Center: the button merges the selection and centers its contents, and its dropdown has Merge & Center, Merge Across, Merge Cells and Unmerge Cells, each a single undo step through the sheet's `merge(command)`. The button shows as pressed on a merged cell, and clicking it then unmerges, as Excel's does; on a single cell outside a merged range it does nothing, and it is disabled on a read-only sheet and under a display filter. Before a merge that would keep only one of several values in a range (in a row, for Merge Across), the sheet shows Excel's warning "Merging cells only keeps the upper-left value and discards other values.", with OK on Enter and Cancel on Esc; it counts the cells that contain a value or a formula in its loaded windows, or in the source's windows over the range when the loaded ones do not cover it. If the source rejects the merge, its message is displayed.
8
+
9
+ ```vue
10
+ <script setup lang="ts">
11
+ import { onUnmounted, ref } from 'vue'
12
+ import { SheetToolbar, SheetView, type SheetViewApi, type SheetViewState } from '@xlcalc/vue'
13
+ import { localSource } from '@xlcalc/vue/local'
14
+ import '@xlcalc/vue/style.css'
15
+
16
+ // A source with format, undo and structure: the toolbar is shown only for a source with format
17
+ const source = localSource({
18
+ sheets: [{ name: 'Sheet1', cells: { A1: { value: 'Region' }, B1: { value: 'Sales' }, A2: { value: 'North' }, B2: { value: 0.125 }, A3: { value: 'South' }, B3: { value: 0.3 } } }],
19
+ })
20
+ onUnmounted(() => void source.close())
21
+
22
+ const state = ref<SheetViewState | null>(null)
23
+ const sheet = ref<SheetViewApi | null>(null)
24
+ </script>
25
+
26
+ <template>
27
+ <SheetToolbar :source="source" :sheet="sheet" :state="state" :locale="source.locale" />
28
+ <SheetView :source="source" ref="sheet" :locale="source.locale" style="height: 360px" @state="state = $event" />
29
+ </template>
30
+ ```
31
+
32
+ The Number Format box works as Excel's does, as measured by xlcalc (`numberFormatCategory` and `numberFormatChoices` of `xlcalc/text`, without the engine): it shows the category of the active cell's format (General if there is none, Custom where no Excel category matches the code), and its list applies Excel's eleven items (General, Number, Currency, Accounting, Short Date, Long Date, Time, Percentage, Fraction, Scientific, Text), each with the format Excel sets (Percentage is `0.00%`), after which the box shows the new category. `locale` is the workbook's locale, as for `SheetView`: the category names and the Currency item's code follow it, with ru-RU's data loaded as a separate chunk; without it, en-US. With `sheet` (the sheet's template ref), the toolbar formats, undoes and redoes through the sheet's methods ([api.md](api.md#sheetviewapi)), and the sheet sets the selection and displays a rejection itself. Without it, the toolbar calls the source and emits `select` with where the selection goes after an undo or a redo (for the sheet's `focus`) and `error` with the source's rejection. It emits `done` after each call, so the host can return focus to the grid. Its buttons do not take focus from the grid. Below 600 px it keeps Undo, Redo, Bold, Italic and Underline in a row and moves the rest into a menu; in a narrow viewport or on a touch screen every target is 44 × 44 px.
33
+
34
+ ## `FormatDialog`
35
+
36
+ `SheetView` has its own Excel Format Cells dialog when the source has `format`: Ctrl+1 opens it (on a Mac as well, since the browser reserves Cmd+1), as do the sheet's `formatCells()` and the toolbar's Format Cells… with `sheet`, and OK formats the selection through the sheet's `format`. The dialog is also exported as `FormatDialog`, for a host that places its own. It is Excel's Format Cells for the selection reported by the sheet's `state` event, with the tabs whose settings xlcalc's cell style supports:
37
+
38
+ - Number is Excel's Number Format box: the category of the code in the input, the list's items (an item puts its format's code in the input) and the code itself, which may be any code.
39
+ - Alignment has the horizontal and vertical alignment, the indent (with Left, Right and Distributed alignment, as in Excel) and text wrapping.
40
+ - Font has the font name, Regular, Italic, Bold or Bold Italic, the size, the underline, strikethrough and the color.
41
+ - Border has the line style and color, the presets None, Outline and Inside, and a button for each edge around a preview, the inside ones for a selection of more than one row or column.
42
+ - Fill has the background color.
43
+
44
+ Colors come from Excel's palette, as on the toolbar, or can be any `#RRGGBB`. The dialog opens with the active cell's formatting; for a single cell the edges show its own borders, and for a range they show as unknown, grayed out as in Excel. OK emits `apply` with patches for `SheetView.format` covering only what changed: the font, the alignment, the fill and the number format over the selection, and each border edge on its row or column, or on the cells on both sides of an inside line (a single patch over every cell when all edges use the same line), all in one call and therefore a single undo step; then it emits `close`, as Cancel, Esc and a click outside the dialog do. Tab and Shift+Tab stay inside it. An input beyond Excel's limits (a size of 1 to 409 points, an indent of 0 to 250) or an invalid color code blocks OK. On a phone the dialog takes the full screen with 44 px targets.
45
+
46
+ A host with its own dialog listens to the toolbar's `formatCells` event, which the button then emits instead (a host whose listener the toolbar cannot see, such as a custom element, also sets `hostDialog`):
47
+
48
+ ```vue
49
+ <script setup lang="ts">
50
+ import { onUnmounted, ref } from 'vue'
51
+ import { FormatDialog, SheetToolbar, SheetView, ruMessages, type SheetViewApi, type SheetViewState } from '@xlcalc/vue'
52
+ import { localSource } from '@xlcalc/vue/local'
53
+ import '@xlcalc/vue/style.css'
54
+
55
+ // A workbook in ru-RU: its locale module loads when the source opens
56
+ const source = localSource({ locale: 'ru-RU', sheets: [{ name: 'Лист1', cells: { A1: { value: 1234.5 }, A2: { value: 0.25 } } }] })
57
+ onUnmounted(() => void source.close())
58
+
59
+ const state = ref<SheetViewState | null>(null)
60
+ const sheet = ref<SheetViewApi | null>(null)
61
+ const formatOpen = ref(false)
62
+ </script>
63
+
64
+ <template>
65
+ <SheetToolbar :source="source" :sheet="sheet" :state="state" :messages="ruMessages" :locale="source.locale" @format-cells="formatOpen = true" />
66
+ <SheetView :source="source" ref="sheet" :messages="ruMessages" :locale="source.locale" style="height: 360px" @state="state = $event" />
67
+ <!-- The host's own dialog, placed by the host: OK formats through the sheet -->
68
+ <FormatDialog :open="formatOpen" :state="state" :messages="ruMessages" :locale="source.locale" @apply="sheet?.format($event)" @close="formatOpen = false" />
69
+ </template>
70
+ ```
71
+
72
+ Fill patterns, diagonal borders, rotated text, shrink to fit, merging, text direction, superscript, subscript and the Protection tab are not available, since xlcalc's cell style does not support them.
@@ -0,0 +1,41 @@
1
+ # Data validation
2
+
3
+ A source with `validation(sheet, cell, signal)` returns what Excel displays for a cell under a data validation rule (`SheetValidation`): the items of a list rule's dropdown as text, in order (empty when the list's formula returns none), and the input message with its title and text; null when no rule applies to the cell. An edit or a Ctrl+Enter whose value violates a rule is rejected with `alert` (`SheetValidationAlert`): the rule's style (`stop`, `warning` or `information`), its title (none for Excel's default "Microsoft Excel"), its message and the cell. After Yes on a warning or OK on an information alert, the component sends the same call again with `{ accept: true }`, and the source writes the value without checking the rule again. A paste is not checked, as Excel does not check one. `memorySource` accepts a sheet's `validations` (`MemoryValidation`: a list or whole numbers within bounds, an input message, and the alert's style, title and message), which is enough for an example or a test; `./local` uses the workbook's rules ([local.md](local.md#data-validation)).
4
+
5
+ A list rule with an input message and a whole-number rule with a warning, over `memorySource`: select a cell in B2:B6 to see its dropdown arrow (Alt+Down opens the list), and type 150 into C2.
6
+
7
+ ```vue
8
+ <script setup lang="ts">
9
+ import { SheetView, memorySource } from '@xlcalc/vue'
10
+ import '@xlcalc/vue/style.css'
11
+
12
+ const source = memorySource({
13
+ sheets: [
14
+ {
15
+ name: 'Tasks',
16
+ cells: { A1: 'Task', B1: 'Status', C1: 'Done, %', A2: 'Design', B2: 'Open', C2: 40, A3: 'Build', B3: 'On hold', C3: 0 },
17
+ validations: [
18
+ { range: 'B2:B6', list: ['Open', 'On hold', 'Closed'], prompt: { title: 'Status', text: 'Pick one from the list.' } },
19
+ { range: 'C2:C6', whole: { min: 0, max: 100 }, alert: { severity: 'warning', title: 'Done, %', message: 'A whole number from 0 to 100.' } },
20
+ ],
21
+ },
22
+ ],
23
+ })
24
+ </script>
25
+
26
+ <template>
27
+ <SheetView :source="source" style="height: 300px" />
28
+ </template>
29
+ ```
30
+
31
+ ## Dropdown, input message and alert
32
+
33
+ These parts look and behave like Excel's. `useValidation` (`src/core/validation.ts`) requests the rule once the active cell has stayed in place for 60 ms (and after the same pause once a burst of source events ends), aborts the request when the active cell moves away and discards its response, and keeps the list's state:
34
+
35
+ - Alt+Down opens the list at the item the cell displays and requests the items again, since Excel recalculates them when the list opens;
36
+ - the arrow keys, Page Up and Page Down (eight items, as many as the list displays), Home and End move the highlight;
37
+ - Enter or a click enters the item into the cell, Esc and Alt+Up close the list, and any other key closes it and goes on to the grid.
38
+
39
+ `ValidationDropdown` is the arrow at the bottom right of the active cell, and the list below the cell, as wide as the cell and its arrow, or above the cell where the viewport has no room below. `ValidationPrompt` is the input message: a pale yellow box below the cell starting from its middle, shifted left or placed over the cell where the viewport ends. `ValidationAlert` is the alert with its style's icon and buttons: Stop has Retry and Cancel; Warning has the rule's message, "Continue?" and Yes, No and Cancel; Information has OK and Cancel. It opens with Excel's default button focused (Retry, No, OK), and Esc means Cancel. Yes and OK keep the value, Retry and No reopen the cell editor with the typed text, and Cancel leaves the cell unchanged. On a touch screen the arrow and the list items are 44 px, and on a phone so are the alert's buttons. Their texts come from `SheetMessages` (`validationList`, `validationTitle`, `retry`, `validationYes`, `validationNo`, `validationContinue`, `validationStyle`, with `ok` and `cancel`), and their colors from the `--xs-prompt-*` and `--xs-alert-*` variables.
40
+
41
+ `SheetView` mounts these parts: the arrow and the input message next to the active cell when the source has `validation`, and the alert when an edit or a Ctrl+Enter is rejected with `alert`. A chosen item is written through the source's `pick(sheet, cell, item, text)`, by its position in the list and its text, and the source writes it as Excel writes a picked item: the item's value, not parsed as typed text (a list cell containing the text `001` gives `001`, one displaying `50%` gives the number 0.5), keeping the cell's number format and without checking the rule. A source without `pick` (`memorySource`) receives the item's text through `edit` with `accept`. While the alert is open, the edited cell stays active and displays the typed text as pending.
package/docs/view.md ADDED
@@ -0,0 +1,50 @@
1
+ # Freeze Panes, Reapply and Trace Precedents
2
+
3
+ ## Reapply
4
+
5
+ When a display filter is on, the cell context menu has Excel's Reapply command, also available as Ctrl+Alt+L (on a Mac as well, since the browser reserves Cmd+Option+L) and as a button on the filter's status line at the bottom. Reapply applies the filter conditions and the sort again to the current data through `visibleRows`: rows that no longer match after an edit are hidden, and rows that now match appear. Until then such rows stay as they are, as in Excel. The scroll position is kept, and so is the active cell, unless its row was hidden; the active cell then moves to the row that takes its place.
6
+
7
+ ## Freeze Panes
8
+
9
+ If the source has `freeze(sheet, panes)` and the sheet is editable, the context menu has Excel's Freeze Panes, Freeze Top Row and Freeze First Column; on a sheet whose workbook has frozen panes, Unfreeze Panes replaces Freeze Panes. Freeze Panes keeps the rows above and the columns to the left of the active cell in view, so with entire columns selected it freezes the columns to the left of the selection, and with entire rows selected the rows above it. With A1 active it freezes the rows and columns above and to the left of the middle of the viewport, where Excel splits the window. Each command is a single undo step in the source, and the sheet reloads afterwards unless the source has sent `reload`.
10
+
11
+ The lock button on the formula bar only controls how the workbook's frozen panes are displayed: it hides or shows them in this browser and does not change the workbook. By default the sheet does not display frozen panes that take more than half of the viewport. Panes frozen from the menu are displayed regardless of the lock's earlier state and of how much of the viewport they take, and Unfreeze Panes clears the lock's saved choice. Without `freeze` these menu items are absent and the lock works as before. They are also absent under a display filter, because the rows on screen are then not the sheet's rows in order.
12
+
13
+ The same commands can be run from the host's buttons through the sheet's methods ([api.md](api.md#sheetviewapi)). The example uses `memorySource`, which has `freeze` and `explain`: select C4 and click Trace precedents to see its precedents.
14
+
15
+ ```vue
16
+ <script setup lang="ts">
17
+ import { ref } from 'vue'
18
+ import { SheetView, memorySource, type SheetViewApi } from '@xlcalc/vue'
19
+ import '@xlcalc/vue/style.css'
20
+
21
+ const source = memorySource({
22
+ sheets: [
23
+ {
24
+ name: 'Budget',
25
+ cells: {
26
+ A1: 'Item', B1: 'Price', C1: 'Sum',
27
+ A2: 'Desks', B2: 250, C2: { value: 1000, formula: 'B2*Rates!B1' },
28
+ A3: 'Chairs', B3: 90, C3: { value: 360, formula: 'B3*Rates!B1' },
29
+ A4: 'Total', C4: { value: 1360, formula: 'SUM(C2:C3)' },
30
+ },
31
+ },
32
+ { name: 'Rates', cells: { A1: 'Quantity', B1: 4 } },
33
+ ],
34
+ })
35
+ const sheet = ref<SheetViewApi | null>(null)
36
+ </script>
37
+
38
+ <template>
39
+ <button @click="sheet?.freezePanes('topRow')">Freeze the top row</button>
40
+ <button @click="sheet?.freezePanes('unfreeze')">Unfreeze</button>
41
+ <button @click="sheet?.explain()">Trace precedents</button>
42
+ <SheetView ref="sheet" :source="source" style="height: 360px" />
43
+ </template>
44
+ ```
45
+
46
+ ## Trace Precedents
47
+
48
+ If the source has `explain(sheet, cell)`, the context menu has Trace Precedents. Instead of drawing arrows as Excel does, the sheet opens a panel to the right of the grid (below it on a phone) with the source's tree of the active cell's precedents. While open, the panel follows the active cell: it requests the tree again 150 ms after the active cell stops moving or its value changes, and when the source reports changes. Since `explain` takes no abort signal, a response that arrives after a newer request is discarded.
49
+
50
+ Each line is a node, and each two-space indent is one level deeper, as in the text of xlcalc's `explain` (`'Q 4'!A2 = 60 ← Data!A4*2`). References in it to cells, columns or rows on a single sheet are links; a reference without a sheet name refers to the sheet of its line's node. Clicking a link selects its range, like Ctrl+click on a reference in the formula bar, and the panel then shows the precedents of that cell. The close button (✕) and Esc close the panel. `./local` returns the session's `explain` in the workbook's locale, and `memorySource` builds a tree from the formulas as given, with xlcalc's English wording. The panel's width, and its height on a phone, are `--xs-explain-w` (340 px) and `--xs-explain-h` (45 %).
package/package.json CHANGED
@@ -1,6 +1,94 @@
1
1
  {
2
2
  "name": "@xlcalc/vue",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.21.2",
4
+ "description": "Vue 3 sheet component for xlcalc workbooks: a windowed grid with styles, formulas and editing over a pluggable sheet source",
5
+ "keywords": [
6
+ "vue",
7
+ "vue3",
8
+ "spreadsheet",
9
+ "excel",
10
+ "xlsx",
11
+ "workbook",
12
+ "sheet",
13
+ "grid",
14
+ "formula",
15
+ "xlcalc"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "Mikhail Kostryukov",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/drakosha/xlcalc-vue.git"
22
+ },
23
+ "homepage": "https://xlcalc.pridelab.dev/vue",
24
+ "bugs": {
25
+ "url": "https://github.com/drakosha/xlcalc-vue/issues"
26
+ },
27
+ "type": "module",
28
+ "engines": {
29
+ "node": ">=20.19"
30
+ },
31
+ "sideEffects": [
32
+ "**/*.css"
33
+ ],
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ },
39
+ "./local": {
40
+ "types": "./dist/local/index.d.ts",
41
+ "default": "./dist/local.js"
42
+ },
43
+ "./style.css": "./dist/style.css"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "LICENSE",
48
+ "README.md",
49
+ "docs",
50
+ "CHANGELOG.md"
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "scripts": {
56
+ "build": "vite build && vite build --config vite.local.config.ts && vue-tsc -p tsconfig.build.json && node scripts/dts-paths.ts",
57
+ "test": "vitest run",
58
+ "typecheck": "vue-tsc --noEmit",
59
+ "pack:release": "npm run build && node -e \"require('fs').mkdirSync('release',{recursive:true})\" && npm pack --pack-destination release",
60
+ "check:package": "npm run build && publint --strict && attw --pack . --profile esm-only --exclude-entrypoints ./style.css",
61
+ "dev:example": "vite --config vite.example.config.ts",
62
+ "build:example": "vite build --config vite.example.config.ts",
63
+ "build:demo": "vite build --config vite.example.config.ts --mode demo",
64
+ "screens": "node scripts/screens.ts",
65
+ "check:local": "node scripts/local-check.ts",
66
+ "check:spa": "node scripts/spa-check.ts",
67
+ "check:big": "node scripts/big-check.ts",
68
+ "check:excel-values": "node scripts/excel-values.ts",
69
+ "check:compare-values": "node scripts/compare-values.ts"
70
+ },
71
+ "peerDependencies": {
72
+ "vue": "^3.5.0",
73
+ "xlcalc": ">=0.46.0"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "xlcalc": {
77
+ "optional": true
78
+ }
79
+ },
80
+ "devDependencies": {
81
+ "@arethetypeswrong/cli": "^0.18.5",
82
+ "@vitejs/plugin-vue": "^6.0.7",
83
+ "@vue/test-utils": "^2.4.6",
84
+ "happy-dom": "^20.0.0",
85
+ "playwright": "1.63.0",
86
+ "publint": "^0.3.25",
87
+ "typescript": "^6.0.3",
88
+ "vite": "^8.1.0",
89
+ "vitest": "^4.1.10",
90
+ "vue": "^3.5.39",
91
+ "vue-tsc": "^3.3.5",
92
+ "xlcalc": "^0.54.2"
93
+ }
94
+ }