@veltra/sheet 2.0.1 → 2.1.1

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 (211) hide show
  1. package/dist/index.d.ts +4 -26
  2. package/dist/index.js +3 -23
  3. package/dist/tools/builtin.js +279 -8
  4. package/dist/tools/context.d.ts +54 -11
  5. package/dist/tools/context.js +28 -5
  6. package/dist/tools/download.js +68 -0
  7. package/dist/tools/export.worker.d.ts +16 -0
  8. package/dist/tools/export.worker.js +29 -0
  9. package/dist/tools/registry.d.ts +7 -4
  10. package/dist/tools/registry.js +1 -3
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/types/sheet.d.ts +6 -5
  13. package/dist/vue/formula-bar.js +393 -0
  14. package/dist/vue/formula-suggest-list.js +66 -0
  15. package/dist/vue/import-file.js +140 -0
  16. package/dist/vue/insert-count-menu-item.js +68 -0
  17. package/dist/vue/insert-image.js +79 -0
  18. package/dist/vue/popup-helpers.js +73 -0
  19. package/dist/vue/popups/border-popup.js +75 -0
  20. package/dist/vue/popups/border-preset-glyph.js +106 -0
  21. package/dist/vue/popups/export-popup.js +51 -0
  22. package/dist/vue/popups/fill-color-popup.js +37 -0
  23. package/dist/vue/popups/find-popup.js +113 -0
  24. package/dist/vue/popups/font-color-popup.js +37 -0
  25. package/dist/vue/popups/font-size-popup.js +63 -0
  26. package/dist/vue/popups/import.worker.d.ts +21 -0
  27. package/dist/vue/popups/import.worker.js +41 -0
  28. package/dist/vue/popups/insert-image-popup.js +38 -0
  29. package/dist/vue/sheet-context-menu.js +200 -0
  30. package/dist/vue/sheet-tabs.js +162 -0
  31. package/dist/vue/sheet-toolbar.js +106 -0
  32. package/dist/vue/sheet.js +144 -164
  33. package/dist/vue/style.d.ts +11 -1
  34. package/dist/vue/style.js +10 -0
  35. package/dist/vue/style2.css +564 -3
  36. package/dist/vue/use-find-replace.js +161 -0
  37. package/dist/vue/use-formula-ref-select.js +37 -0
  38. package/dist/vue/use-formula-suggest.js +126 -0
  39. package/dist/vue/use-sheet-grid.js +180 -0
  40. package/dist/vue/use-sheet-state.js +122 -0
  41. package/dist/vue/use-sheet-tabs-bar.js +90 -0
  42. package/dist/vue/use-tool-groups.js +42 -0
  43. package/dist/vue/use-tool-popup.js +123 -0
  44. package/dist/vue/use-toolbar-scroll.js +72 -0
  45. package/package.json +12 -7
  46. package/src/env.d.ts +11 -0
  47. package/src/index.ts +203 -14
  48. package/src/tools/__test__/builtin.test.ts +208 -66
  49. package/src/tools/__test__/context.test.ts +62 -2
  50. package/src/tools/builtin.ts +350 -6
  51. package/src/tools/context.ts +89 -11
  52. package/src/tools/download.ts +82 -0
  53. package/src/tools/export.worker.ts +61 -0
  54. package/src/tools/registry.ts +16 -1
  55. package/src/types/index.ts +6 -0
  56. package/src/types/sheet.ts +5 -3
  57. package/src/vue/__test__/formula-bar.test.ts +455 -0
  58. package/src/vue/__test__/import-file.test.ts +183 -0
  59. package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
  60. package/src/vue/__test__/insert-image-popup.test.ts +89 -0
  61. package/src/vue/__test__/insert-image.test.ts +100 -0
  62. package/src/vue/__test__/sheet-component.test.ts +716 -27
  63. package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
  64. package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
  65. package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
  66. package/src/vue/formula-bar.vue +473 -0
  67. package/src/vue/formula-suggest-list.vue +64 -0
  68. package/src/vue/import-file.ts +173 -0
  69. package/src/vue/insert-count-menu-item.vue +69 -0
  70. package/src/vue/insert-image.ts +90 -0
  71. package/src/vue/parsing.ts +11 -0
  72. package/src/vue/popup-helpers.ts +50 -0
  73. package/src/vue/popups/border-popup.vue +93 -0
  74. package/src/vue/popups/border-preset-glyph.vue +111 -0
  75. package/src/vue/popups/export-popup.vue +52 -0
  76. package/src/vue/popups/fill-color-popup.vue +39 -0
  77. package/src/vue/popups/find-popup.vue +111 -0
  78. package/src/vue/popups/font-color-popup.vue +39 -0
  79. package/src/vue/popups/font-size-popup.vue +52 -0
  80. package/src/vue/popups/import.worker.ts +66 -0
  81. package/src/vue/popups/insert-image-popup.vue +39 -0
  82. package/src/vue/sheet-context-menu.ts +229 -0
  83. package/src/vue/sheet-tabs.vue +162 -0
  84. package/src/vue/sheet-toolbar.vue +91 -0
  85. package/src/vue/sheet.vue +161 -186
  86. package/src/vue/style.scss +646 -3
  87. package/src/vue/style.ts +11 -0
  88. package/src/vue/use-find-replace.ts +200 -0
  89. package/src/vue/use-formula-ref-select.ts +34 -0
  90. package/src/vue/use-formula-suggest.ts +150 -0
  91. package/src/vue/use-sheet-grid.ts +243 -0
  92. package/src/vue/use-sheet-state.ts +161 -0
  93. package/src/vue/use-sheet-tabs-bar.ts +115 -0
  94. package/src/vue/use-tool-groups.ts +61 -0
  95. package/src/vue/use-tool-popup.ts +150 -0
  96. package/src/vue/use-toolbar-scroll.ts +95 -0
  97. package/dist/core/address.d.ts +0 -60
  98. package/dist/core/address.js +0 -142
  99. package/dist/core/address.js.map +0 -1
  100. package/dist/core/cell-store.d.ts +0 -61
  101. package/dist/core/cell-store.js +0 -135
  102. package/dist/core/cell-store.js.map +0 -1
  103. package/dist/core/command/default-registry.d.ts +0 -10
  104. package/dist/core/command/default-registry.js +0 -18
  105. package/dist/core/command/default-registry.js.map +0 -1
  106. package/dist/core/command/history.d.ts +0 -64
  107. package/dist/core/command/history.js +0 -127
  108. package/dist/core/command/history.js.map +0 -1
  109. package/dist/core/command/merge-cells.d.ts +0 -21
  110. package/dist/core/command/merge-cells.js +0 -74
  111. package/dist/core/command/merge-cells.js.map +0 -1
  112. package/dist/core/command/registry.d.ts +0 -17
  113. package/dist/core/command/registry.js +0 -26
  114. package/dist/core/command/registry.js.map +0 -1
  115. package/dist/core/command/set-cell-formula.d.ts +0 -17
  116. package/dist/core/command/set-cell-formula.js +0 -29
  117. package/dist/core/command/set-cell-formula.js.map +0 -1
  118. package/dist/core/command/set-cell-value.d.ts +0 -22
  119. package/dist/core/command/set-cell-value.js +0 -35
  120. package/dist/core/command/set-cell-value.js.map +0 -1
  121. package/dist/core/command/types.d.ts +0 -68
  122. package/dist/core/events.js +0 -29
  123. package/dist/core/events.js.map +0 -1
  124. package/dist/core/fill.d.ts +0 -34
  125. package/dist/core/fill.js +0 -320
  126. package/dist/core/fill.js.map +0 -1
  127. package/dist/core/formula/ast.d.ts +0 -58
  128. package/dist/core/formula/ast.js +0 -38
  129. package/dist/core/formula/ast.js.map +0 -1
  130. package/dist/core/formula/dependency-graph.d.ts +0 -70
  131. package/dist/core/formula/dependency-graph.js +0 -269
  132. package/dist/core/formula/dependency-graph.js.map +0 -1
  133. package/dist/core/formula/errors.d.ts +0 -25
  134. package/dist/core/formula/errors.js +0 -36
  135. package/dist/core/formula/errors.js.map +0 -1
  136. package/dist/core/formula/evaluator.d.ts +0 -41
  137. package/dist/core/formula/evaluator.js +0 -154
  138. package/dist/core/formula/evaluator.js.map +0 -1
  139. package/dist/core/formula/functions.d.ts +0 -30
  140. package/dist/core/formula/functions.js +0 -209
  141. package/dist/core/formula/functions.js.map +0 -1
  142. package/dist/core/formula/parser.d.ts +0 -7
  143. package/dist/core/formula/parser.js +0 -208
  144. package/dist/core/formula/parser.js.map +0 -1
  145. package/dist/core/formula/tokenizer.d.ts +0 -37
  146. package/dist/core/formula/tokenizer.js +0 -146
  147. package/dist/core/formula/tokenizer.js.map +0 -1
  148. package/dist/core/merge-manager.d.ts +0 -64
  149. package/dist/core/merge-manager.js +0 -111
  150. package/dist/core/merge-manager.js.map +0 -1
  151. package/dist/core/selection.d.ts +0 -31
  152. package/dist/core/selection.js +0 -62
  153. package/dist/core/selection.js.map +0 -1
  154. package/dist/core/sheet.d.ts +0 -116
  155. package/dist/core/sheet.js +0 -224
  156. package/dist/core/sheet.js.map +0 -1
  157. package/dist/core/workbook.d.ts +0 -43
  158. package/dist/core/workbook.js +0 -77
  159. package/dist/core/workbook.js.map +0 -1
  160. package/dist/grid/sheet-grid.d.ts +0 -81
  161. package/dist/grid/sheet-grid.js +0 -331
  162. package/dist/grid/sheet-grid.js.map +0 -1
  163. package/dist/grid/vtable-theme.js +0 -80
  164. package/dist/grid/vtable-theme.js.map +0 -1
  165. package/dist/tools/builtin.js.map +0 -1
  166. package/dist/tools/context.js.map +0 -1
  167. package/dist/tools/registry.js.map +0 -1
  168. package/dist/vue/index.d.ts +0 -2
  169. package/dist/vue/sheet.js.map +0 -1
  170. package/dist/vue/sheet.vue.d.ts +0 -24
  171. package/src/core/__test__/address.test.ts +0 -113
  172. package/src/core/__test__/cell-store.test.ts +0 -97
  173. package/src/core/__test__/command.test.ts +0 -377
  174. package/src/core/__test__/fill.test.ts +0 -140
  175. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  176. package/src/core/__test__/formula-engine.test.ts +0 -470
  177. package/src/core/__test__/formula-functions.test.ts +0 -178
  178. package/src/core/__test__/formula-parser.test.ts +0 -160
  179. package/src/core/__test__/history.test.ts +0 -225
  180. package/src/core/__test__/merge-manager.test.ts +0 -106
  181. package/src/core/__test__/sheet.test.ts +0 -142
  182. package/src/core/address.ts +0 -166
  183. package/src/core/cell-store.ts +0 -173
  184. package/src/core/command/default-registry.ts +0 -15
  185. package/src/core/command/history.ts +0 -167
  186. package/src/core/command/index.ts +0 -7
  187. package/src/core/command/merge-cells.ts +0 -74
  188. package/src/core/command/registry.ts +0 -30
  189. package/src/core/command/set-cell-formula.ts +0 -29
  190. package/src/core/command/set-cell-value.ts +0 -38
  191. package/src/core/command/types.ts +0 -73
  192. package/src/core/events.ts +0 -37
  193. package/src/core/fill.ts +0 -314
  194. package/src/core/formula/ast.ts +0 -66
  195. package/src/core/formula/dependency-graph.ts +0 -329
  196. package/src/core/formula/errors.ts +0 -41
  197. package/src/core/formula/evaluator.ts +0 -219
  198. package/src/core/formula/functions.ts +0 -266
  199. package/src/core/formula/index.ts +0 -7
  200. package/src/core/formula/parser.ts +0 -198
  201. package/src/core/formula/tokenizer.ts +0 -160
  202. package/src/core/merge-manager.ts +0 -158
  203. package/src/core/selection.ts +0 -66
  204. package/src/core/sheet.ts +0 -282
  205. package/src/core/workbook.ts +0 -99
  206. package/src/grid/__test__/canvas-mock.ts +0 -49
  207. package/src/grid/__test__/setup.ts +0 -3
  208. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  209. package/src/grid/__test__/sheet-grid.test.ts +0 -275
  210. package/src/grid/sheet-grid.ts +0 -407
  211. package/src/grid/vtable-theme.ts +0 -61
@@ -1,158 +0,0 @@
1
- import {
2
- boundingBox,
3
- cellKey,
4
- createRange,
5
- iterateRange,
6
- rangesIntersect,
7
- type CellAddress,
8
- type CellRange
9
- } from './address'
10
-
11
- /**
12
- * 合并单元格管理器(只管几何,不管数据)。
13
- *
14
- * - 锚点恒为区域左上角,数据只存在锚点格(数据搬迁由 Sheet 编排)
15
- * - merges:anchorKey → CellRange
16
- * - coverIndex:coveredCellKey(不含锚点自身)→ anchorKey,O(1) 查询
17
- */
18
-
19
- /** 单元格在合并语义下的种类 */
20
- export type MergedCellKind = 'normal' | 'merged-anchor' | 'merged-covered'
21
-
22
- export interface CellInfo {
23
- kind: MergedCellKind
24
- /** 锚点地址;普通格为自身 */
25
- anchor: CellAddress
26
- /** 所在合并区域;普通格无 */
27
- mergeRange?: CellRange
28
- }
29
-
30
- export interface MergeResult {
31
- /** 最终生效的合并区域(与既有合并相交时取包围盒,可能大于入参) */
32
- range: CellRange
33
- /** 本次被解除的既有合并 */
34
- removed: CellRange[]
35
- }
36
-
37
- export class MergeManager {
38
- private merges = new Map<number, CellRange>()
39
- private coverIndex = new Map<number, number>()
40
-
41
- /**
42
- * 计算合并结果(纯查询,不修改状态):
43
- * 1. 所有与 range 相交的既有合并将被解除
44
- * 2. 新区域 = range 与被解除合并的包围盒
45
- */
46
- computeMerge(range: CellRange): MergeResult {
47
- const removed: CellRange[] = []
48
- for (const existing of this.merges.values()) {
49
- if (rangesIntersect(existing, range)) removed.push(existing)
50
- }
51
- const finalRange =
52
- removed.length > 0 ? boundingBox([range, ...removed]) : createRange(range.start, range.end)
53
- return { range: finalRange, removed }
54
- }
55
-
56
- /** 合并区域(解除相交既有合并 + 包围盒,见 computeMerge) */
57
- merge(range: CellRange): MergeResult {
58
- const result = this.computeMerge(range)
59
- for (const item of result.removed) this.unregister(item)
60
- this.register(result.range)
61
- return result
62
- }
63
-
64
- /**
65
- * 精确登记一个合并区域(不做相交解除与包围盒)。
66
- * 低层接口,供命令系统回放补丁使用;调用方需保证不与既有合并相交。
67
- */
68
- addMerge(range: CellRange): void {
69
- this.register(range)
70
- }
71
-
72
- /** 精确移除一个合并区域(按锚点匹配);不存在时为空操作 */
73
- removeMerge(range: CellRange): void {
74
- this.unregister(range)
75
- }
76
-
77
- /** 解除所有与 range 相交的合并,返回被解除的区域 */
78
- unmerge(range: CellRange): CellRange[] {
79
- const removed: CellRange[] = []
80
- for (const existing of this.merges.values()) {
81
- if (rangesIntersect(existing, range)) removed.push(existing)
82
- }
83
- for (const item of removed) this.unregister(item)
84
- return removed
85
- }
86
-
87
- /** 地址解析到锚点:被覆盖格 → 锚点;锚点/普通格 → 自身 */
88
- resolveAnchor(addr: CellAddress): CellAddress {
89
- const merge = this.getMergeAt(addr)
90
- return merge ? { ...merge.start } : { ...addr }
91
- }
92
-
93
- /** 是否在任意合并区域内(含锚点) */
94
- isMerged(addr: CellAddress): boolean {
95
- return this.getMergeAt(addr) !== undefined
96
- }
97
-
98
- /** 是否为被覆盖的非锚点格 */
99
- isCovered(addr: CellAddress): boolean {
100
- return this.coverIndex.has(cellKey(addr))
101
- }
102
-
103
- /** 所在合并区域(锚点与被覆盖格均可查);不在任何合并内返回 undefined */
104
- getMergeAt(addr: CellAddress): CellRange | undefined {
105
- const key = cellKey(addr)
106
- const asAnchor = this.merges.get(key)
107
- if (asAnchor) return asAnchor
108
- const anchorKey = this.coverIndex.get(key)
109
- return anchorKey === undefined ? undefined : this.merges.get(anchorKey)
110
- }
111
-
112
- /** 合并语义下的单元格信息 */
113
- getCellInfo(addr: CellAddress): CellInfo {
114
- const merge = this.getMergeAt(addr)
115
- if (!merge) return { kind: 'normal', anchor: { ...addr } }
116
- const anchor = { ...merge.start }
117
- if (addr.row === anchor.row && addr.col === anchor.col) {
118
- return { kind: 'merged-anchor', anchor, mergeRange: merge }
119
- }
120
- return { kind: 'merged-covered', anchor, mergeRange: merge }
121
- }
122
-
123
- /** 全部合并区域(迭代 merges 的副本) */
124
- getMerges(): CellRange[] {
125
- return [...this.merges.values()].map((range) => ({
126
- start: { ...range.start },
127
- end: { ...range.end }
128
- }))
129
- }
130
-
131
- get size(): number {
132
- return this.merges.size
133
- }
134
-
135
- clear(): void {
136
- this.merges.clear()
137
- this.coverIndex.clear()
138
- }
139
-
140
- private register(range: CellRange): void {
141
- const anchorKey = cellKey(range.start)
142
- this.merges.set(anchorKey, range)
143
- for (const addr of iterateRange(range)) {
144
- if (addr.row !== range.start.row || addr.col !== range.start.col) {
145
- this.coverIndex.set(cellKey(addr), anchorKey)
146
- }
147
- }
148
- }
149
-
150
- private unregister(range: CellRange): void {
151
- this.merges.delete(cellKey(range.start))
152
- for (const addr of iterateRange(range)) {
153
- if (addr.row !== range.start.row || addr.col !== range.start.col) {
154
- this.coverIndex.delete(cellKey(addr))
155
- }
156
- }
157
- }
158
- }
@@ -1,66 +0,0 @@
1
- import { createRange, type CellAddress, type CellRange } from './address'
2
- import { TypedEventEmitter } from './events'
3
-
4
- /**
5
- * 选区模型:activeCell + ranges。
6
- * activeCell 永远指向锚点(构造时注入 resolveAnchor)。
7
- */
8
-
9
- export interface SelectionState {
10
- /** 活动单元格(锚点语义);未选中时为 null */
11
- activeCell: CellAddress | null
12
- /** 选中区域列表(本阶段单选区,预留多选区) */
13
- ranges: CellRange[]
14
- }
15
-
16
- type SelectionEvents = { change: SelectionState }
17
-
18
- export class SelectionModel {
19
- private state: SelectionState = { activeCell: null, ranges: [] }
20
- private emitter = new TypedEventEmitter<SelectionEvents>()
21
-
22
- constructor(private readonly resolveAnchor: (addr: CellAddress) => CellAddress) {}
23
-
24
- get activeCell(): CellAddress | null {
25
- return this.state.activeCell
26
- }
27
-
28
- get ranges(): CellRange[] {
29
- return this.state.ranges
30
- }
31
-
32
- /** 选中单格;被覆盖格自动解析到锚点 */
33
- selectCell(addr: CellAddress): void {
34
- const anchor = this.resolveAnchor(addr)
35
- this.setState({ activeCell: anchor, ranges: [createRange(anchor, anchor)] })
36
- }
37
-
38
- /** 选中区域;activeCell 取区域起点的锚点 */
39
- selectRange(range: CellRange): void {
40
- const anchor = this.resolveAnchor(range.start)
41
- this.setState({ activeCell: anchor, ranges: [range] })
42
- }
43
-
44
- getState(): SelectionState {
45
- return {
46
- activeCell: this.state.activeCell ? { ...this.state.activeCell } : null,
47
- ranges: this.state.ranges.map((range) => ({
48
- start: { ...range.start },
49
- end: { ...range.end }
50
- }))
51
- }
52
- }
53
-
54
- clear(): void {
55
- this.setState({ activeCell: null, ranges: [] })
56
- }
57
-
58
- on(handler: (state: SelectionState) => void): () => void {
59
- return this.emitter.on('change', handler)
60
- }
61
-
62
- private setState(state: SelectionState): void {
63
- this.state = state
64
- this.emitter.emit('change', this.getState())
65
- }
66
- }
package/src/core/sheet.ts DELETED
@@ -1,282 +0,0 @@
1
- import type { CellAddress, CellRange } from './address'
2
- import { inferCellType, CellStore, type CellData, type CellValue } from './cell-store'
3
- import { defaultCommandRegistry } from './command/default-registry'
4
- import { HistoryManager, type HistoryState } from './command/history'
5
- import { MergeCellsCommand, UnmergeCellsCommand } from './command/merge-cells'
6
- import { SetCellFormulaCommand } from './command/set-cell-formula'
7
- import { SetCellValueCommand, type SetCellValueItem } from './command/set-cell-value'
8
- import type { Mutation, Patch, PatchDirection } from './command/types'
9
- import { TypedEventEmitter } from './events'
10
- import { DependencyGraph } from './formula/dependency-graph'
11
- import { MergeManager, type CellInfo } from './merge-manager'
12
- import { SelectionModel, type SelectionState } from './selection'
13
-
14
- /**
15
- * Sheet = cell-store + merge-manager + selection + history 的组合,统一操作入口。
16
- *
17
- * 语义约定:
18
- * - `getCellData`:原始存储语义,被合并覆盖的非锚点格 → undefined
19
- * - `getDisplayValue`:锚点解析语义,被覆盖格返回锚点的值
20
- * - `setCellValue` / `selectCell`:内部先 resolveAnchor(用户操作永远落锚点)
21
- * - 一切写操作(setCellValue / setCell / setCells / setCellFormula / mergeCells / unmergeCells)
22
- * 都经命令系统执行(applyPatch 是唯一变更通道),天然获得 undo/redo 能力
23
- * - 公式:`setCellValue` 识别 '=' 前缀走 `setCellFormula`;命令执行后自动增量重算,
24
- * 重算派生变更(含跨表)并入同一 undo 单元
25
- */
26
-
27
- export type SheetEvents = {
28
- /** 单元格数据变化(含删除) */
29
- 'cell-change': { addr: CellAddress }
30
- /** 合并结构变化(合并/取消合并) */
31
- 'merge-change': { range: CellRange }
32
- /** 选区变化 */
33
- 'selection-change': SelectionState
34
- /** 历史栈变化(undo/redo 可用状态,供工具栏按钮置灰) */
35
- 'history-change': HistoryState
36
- }
37
-
38
- export class Sheet {
39
- readonly store = new CellStore()
40
- readonly merges = new MergeManager()
41
- readonly selection: SelectionModel
42
- /** undo/redo 历史(命令系统) */
43
- readonly history: HistoryManager
44
- /** 公式依赖图(工作簿内多 sheet 共享;独立 Sheet 自建) */
45
- readonly formulaGraph: DependencyGraph
46
- /**
47
- * 稀疏行高(模型行号 → 像素高度)。
48
- * 供 SheetGrid 在 tab 切换重建时还原;本期不进 undo。
49
- */
50
- private readonly rowHeights = new Map<number, number>()
51
-
52
- name: string
53
-
54
- private emitter = new TypedEventEmitter<SheetEvents>()
55
-
56
- constructor(name = 'Sheet1', formulaGraph?: DependencyGraph) {
57
- this.name = name
58
- this.selection = new SelectionModel((addr) => this.merges.resolveAnchor(addr))
59
- this.selection.on((state) => this.emitter.emit('selection-change', state))
60
- this.history = new HistoryManager(this.boundApplyPatch)
61
- this.history.onChange((state) => this.emitter.emit('history-change', state))
62
- this.formulaGraph = formulaGraph ?? new DependencyGraph()
63
- this.formulaGraph.registerSheet(this)
64
- }
65
-
66
- get rowCount(): number {
67
- return this.store.rowCount
68
- }
69
-
70
- get colCount(): number {
71
- return this.store.colCount
72
- }
73
-
74
- /** 读取自定义行高;未设置返回 undefined(由视图层用默认行高) */
75
- getRowHeight(row: number): number | undefined {
76
- return this.rowHeights.get(row)
77
- }
78
-
79
- /** 设置自定义行高(不进 undo);height ≤ 0 时清除自定义高度 */
80
- setRowHeight(row: number, height: number): void {
81
- if (!Number.isInteger(row) || row < 0) return
82
- if (!Number.isFinite(height) || height <= 0) {
83
- this.rowHeights.delete(row)
84
- return
85
- }
86
- this.rowHeights.set(row, height)
87
- }
88
-
89
- /** 遍历已设置的自定义行高(SheetGrid 重建还原用) */
90
- getRowHeights(): ReadonlyMap<number, number> {
91
- return this.rowHeights
92
- }
93
-
94
- // ─── 单元格数据 ────────────────────────────────────────────
95
-
96
- /** 原始存储语义读取(被覆盖格 → undefined) */
97
- getCellData(addr: CellAddress): CellData | undefined {
98
- return this.store.getCell(addr)
99
- }
100
-
101
- /** 锚点解析语义读取(被覆盖格 → 锚点的值) */
102
- getDisplayValue(addr: CellAddress): CellValue | undefined {
103
- const anchor = this.merges.resolveAnchor(addr)
104
- return this.store.getCell(anchor)?.v ?? undefined
105
- }
106
-
107
- /** 写入原始值(解析到锚点;空值 = 清除);'=' 前缀按公式处理;经命令执行,可撤销 */
108
- setCellValue(addr: CellAddress, value: CellValue): void {
109
- if (typeof value === 'string' && value.startsWith('=')) {
110
- this.setCellFormula(addr, value)
111
- return
112
- }
113
- const data = value == null || value === '' ? undefined : { v: value, t: inferCellType(value) }
114
- this.setCells([{ addr, data }])
115
- }
116
-
117
- /** 写入完整 CellData(解析到锚点;空数据 = 清除);经命令执行,可撤销 */
118
- setCell(addr: CellAddress, data?: CellData): void {
119
- this.setCells([{ addr, data }])
120
- }
121
-
122
- /**
123
- * 写入公式(可带 '=' 前缀;空白公式 = 清除);经命令执行,可撤销。
124
- * 公式原文存 CellData.f,计算缓存(v/t)由增量重算填充;解析失败 → #ERROR!。
125
- */
126
- setCellFormula(addr: CellAddress, formula: string): void {
127
- const text = formula.startsWith('=') ? formula.slice(1) : formula
128
- if (text.trim() === '') {
129
- this.setCells([{ addr, data: undefined }])
130
- return
131
- }
132
- this.executeCommand(SetCellFormulaCommand.id, { addr, formula: text })
133
- }
134
-
135
- /** 批量写入(一次调用 = 一个 undo 单元,供粘贴/填充复用) */
136
- setCells(items: SetCellValueItem[]): void {
137
- this.executeCommand(SetCellValueCommand.id, { items })
138
- }
139
-
140
- // ─── 合并 ────────────────────────────────────────────────
141
-
142
- /** 合并语义下的单元格信息(普通格 / 合并锚点 / 被覆盖格) */
143
- getCellInfo(addr: CellAddress): CellInfo {
144
- return this.merges.getCellInfo(addr)
145
- }
146
-
147
- /**
148
- * 合并区域。值保留规则(同 Excel/univer):
149
- * 仅保留包围盒内按行主序第一个有值格的值,写入新锚点,其余被覆盖格清空。
150
- * @returns 最终生效的合并区域(与既有合并相交时取包围盒)
151
- */
152
- mergeCells(range: CellRange): CellRange {
153
- return this.executeCommand<CellRange>(MergeCellsCommand.id, { range })!
154
- }
155
-
156
- /** 解除与 range 相交的所有合并;仅原锚点保留值(被覆盖格本就无值) */
157
- unmergeCells(range: CellRange): void {
158
- this.executeCommand(UnmergeCellsCommand.id, { range })
159
- }
160
-
161
- // ─── 命令与历史 ──────────────────────────────────────────
162
-
163
- /** 经默认注册表执行命令;产生的 mutation 与公式重算派生 mutation 一并推入历史 */
164
- executeCommand<R = void>(commandId: string, params: unknown): R | undefined {
165
- const result = defaultCommandRegistry.execute<R>(
166
- { sheet: this, applyPatch: this.boundApplyPatch },
167
- commandId,
168
- params
169
- )
170
- if (result && result.mutations.length > 0) {
171
- const mutations = [...result.mutations]
172
- const recalcMutation = this.recalcAfterCommand(result.mutations)
173
- if (recalcMutation) mutations.push(recalcMutation)
174
- this.history.push(mutations)
175
- }
176
- return result?.result
177
- }
178
-
179
- /** 开启事务(事务内所有命令合并为一个 undo 单元;可嵌套,拍平到最外层) */
180
- beginTransaction(): void {
181
- this.history.beginTransaction()
182
- }
183
-
184
- /** 提交事务 */
185
- commit(): void {
186
- this.history.commit()
187
- }
188
-
189
- /** 回滚事务:还原缓冲中已应用的变更并放弃事务 */
190
- rollback(): void {
191
- this.history.rollback()
192
- }
193
-
194
- /** 撤销一步;无可撤销时返回 false */
195
- undo(): boolean {
196
- return this.history.undo()
197
- }
198
-
199
- /** 重做一步;无可重做时返回 false */
200
- redo(): boolean {
201
- return this.history.redo()
202
- }
203
-
204
- get canUndo(): boolean {
205
- return this.history.canUndo
206
- }
207
-
208
- get canRedo(): boolean {
209
- return this.history.canRedo
210
- }
211
-
212
- // ─── 选区 ────────────────────────────────────────────────
213
-
214
- getSelection(): SelectionState {
215
- return this.selection.getState()
216
- }
217
-
218
- /** 选中单格(被覆盖格自动定位锚点) */
219
- selectCell(addr: CellAddress): void {
220
- this.selection.selectCell(addr)
221
- }
222
-
223
- selectRange(range: CellRange): void {
224
- this.selection.selectRange(range)
225
- }
226
-
227
- // ─── 事件 ────────────────────────────────────────────────
228
-
229
- on<K extends keyof SheetEvents>(type: K, handler: (payload: SheetEvents[K]) => void): () => void {
230
- return this.emitter.on(type, handler)
231
- }
232
-
233
- off<K extends keyof SheetEvents>(type: K, handler: (payload: SheetEvents[K]) => void): void {
234
- this.emitter.off(type, handler)
235
- }
236
-
237
- // ─── 内部 ────────────────────────────────────────────────
238
-
239
- private readonly boundApplyPatch = (patch: Patch, direction: PatchDirection): void => {
240
- // 跨表重算的派生补丁路由到目标 sheet(回放经源 sheet 历史时同样按此路由)
241
- const target = patch.kind === 'cell' ? (patch.sheet ?? this) : this
242
- target.applyPatch(patch, direction)
243
- }
244
-
245
- /** 应用补丁:命令执行与 undo/redo 回放共用的唯一变更通道 */
246
- private applyPatch(patch: Patch, direction: PatchDirection): void {
247
- if (patch.kind === 'cell') {
248
- const data = direction === 'redo' ? patch.after : patch.before
249
- const previous = direction === 'redo' ? patch.before : patch.after
250
- this.store.setCell(patch.addr, data)
251
- // 公式依赖图同步(f 增/删/改 → 节点增/删/重建;undo/redo 回放同样覆盖)
252
- this.formulaGraph.syncCell(this, patch.addr, previous, data)
253
- this.emitter.emit('cell-change', { addr: patch.addr })
254
- return
255
- }
256
- const exists = direction === 'redo' ? patch.after : patch.before
257
- if (exists) {
258
- this.merges.addMerge(patch.range)
259
- } else {
260
- this.merges.removeMerge(patch.range)
261
- }
262
- this.emitter.emit('merge-change', { range: patch.range })
263
- }
264
-
265
- /**
266
- * 命令执行后的公式增量重算:从命令 mutation 提取变更格 → 依赖图标脏 +
267
- * 拓扑序重算 → 派生补丁立即应用,作为附加 mutation 并入同一 undo 单元。
268
- * undo/redo 纯补丁回放(不重算),图状态由 applyPatch 内的 syncCell 维持。
269
- */
270
- private recalcAfterCommand(mutations: Mutation[]): Mutation | undefined {
271
- const changed: { sheet: Sheet; addr: CellAddress }[] = []
272
- for (const mutation of mutations) {
273
- for (const patch of mutation.redo) {
274
- if (patch.kind === 'cell') changed.push({ sheet: patch.sheet ?? this, addr: patch.addr })
275
- }
276
- }
277
- const derived = this.formulaGraph.recalc(changed)
278
- if (derived.length === 0) return undefined
279
- for (const patch of derived) this.boundApplyPatch(patch, 'redo')
280
- return { redo: derived, undo: [...derived].reverse() }
281
- }
282
- }
@@ -1,99 +0,0 @@
1
- import { TypedEventEmitter } from './events'
2
- import { DependencyGraph } from './formula/dependency-graph'
3
- import { Sheet } from './sheet'
4
-
5
- /**
6
- * Workbook:多 Sheet 管理。
7
- * 所有 sheet 共享一个公式依赖图(跨表引用与增量重算的中枢)。
8
- */
9
-
10
- export type WorkbookEvents = {
11
- /** 激活 sheet 切换 */
12
- 'active-sheet-change': { sheet: Sheet; index: number }
13
- /** sheet 增删 */
14
- 'sheets-change': { sheets: Sheet[] }
15
- }
16
-
17
- export class Workbook {
18
- private sheets: Sheet[] = []
19
- private activeIndex = 0
20
- private emitter = new TypedEventEmitter<WorkbookEvents>()
21
- /** 工作簿级公式依赖图(全部 sheet 共享) */
22
- readonly formulaGraph = new DependencyGraph()
23
-
24
- constructor() {
25
- this.addSheet()
26
- }
27
-
28
- get sheetCount(): number {
29
- return this.sheets.length
30
- }
31
-
32
- get activeSheet(): Sheet {
33
- return this.sheets[this.activeIndex]!
34
- }
35
-
36
- get activeSheetIndex(): number {
37
- return this.activeIndex
38
- }
39
-
40
- getSheets(): Sheet[] {
41
- return [...this.sheets]
42
- }
43
-
44
- getSheet(name: string): Sheet | undefined {
45
- return this.sheets.find((sheet) => sheet.name === name)
46
- }
47
-
48
- /** 新增 sheet,名称缺省为 Sheet{n}(保证唯一) */
49
- addSheet(name?: string): Sheet {
50
- const sheet = new Sheet(name ?? this.nextDefaultName(), this.formulaGraph)
51
- this.sheets.push(sheet)
52
- this.emitter.emit('sheets-change', { sheets: this.getSheets() })
53
- return sheet
54
- }
55
-
56
- /** 删除 sheet;至少保留一个。删除激活项时激活相邻 sheet */
57
- removeSheet(name: string): boolean {
58
- if (this.sheets.length <= 1) return false
59
- const index = this.sheets.findIndex((sheet) => sheet.name === name)
60
- if (index < 0) return false
61
- const [removed] = this.sheets.splice(index, 1)
62
- this.formulaGraph.unregisterSheet(removed!)
63
- if (this.activeIndex >= this.sheets.length) {
64
- this.activeIndex = this.sheets.length - 1
65
- }
66
- this.emitter.emit('sheets-change', { sheets: this.getSheets() })
67
- this.emitter.emit('active-sheet-change', { sheet: this.activeSheet, index: this.activeIndex })
68
- return true
69
- }
70
-
71
- /** 激活指定 sheet */
72
- activateSheet(name: string): boolean {
73
- const index = this.sheets.findIndex((sheet) => sheet.name === name)
74
- if (index < 0 || index === this.activeIndex) return false
75
- this.activeIndex = index
76
- this.emitter.emit('active-sheet-change', { sheet: this.activeSheet, index })
77
- return true
78
- }
79
-
80
- on<K extends keyof WorkbookEvents>(
81
- type: K,
82
- handler: (payload: WorkbookEvents[K]) => void
83
- ): () => void {
84
- return this.emitter.on(type, handler)
85
- }
86
-
87
- off<K extends keyof WorkbookEvents>(
88
- type: K,
89
- handler: (payload: WorkbookEvents[K]) => void
90
- ): void {
91
- this.emitter.off(type, handler)
92
- }
93
-
94
- private nextDefaultName(): string {
95
- let n = this.sheets.length + 1
96
- while (this.getSheet(`Sheet${n}`)) n++
97
- return `Sheet${n}`
98
- }
99
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * happy-dom 不实现 canvas 2d 上下文(getContext 返回 null),
3
- * 这里用 Proxy 提供最小可用的 mock,让 @visactor/vrender 能在无头环境
4
- * 完成挂载与绘制调用。仅用于测试(vp test setupFiles)。
5
- */
6
-
7
- type Context2dMock = Record<string | symbol, unknown>
8
-
9
- function createContext2dMock(canvas: HTMLCanvasElement): Context2dMock {
10
- const store: Context2dMock = { canvas }
11
-
12
- return new Proxy(store, {
13
- get(target, prop) {
14
- if (prop in target) return target[prop]
15
- if (prop === 'measureText') return (text: unknown) => ({ width: String(text).length * 8 })
16
- if (prop === 'getImageData')
17
- return () => ({ data: new Uint8ClampedArray(0), width: 0, height: 0 })
18
- if (
19
- prop === 'createLinearGradient' ||
20
- prop === 'createRadialGradient' ||
21
- prop === 'createPattern'
22
- ) {
23
- return () => ({ addColorStop: () => {} })
24
- }
25
- if (prop === 'getLineDash') return () => []
26
- // 其余一律视为无操作绘制方法
27
- return () => {}
28
- },
29
- set(target, prop, value) {
30
- target[prop] = value
31
- return true
32
- }
33
- })
34
- }
35
-
36
- /** 补丁 HTMLCanvasElement,使 getContext('2d') 返回可用 mock */
37
- export function setupCanvasMock(): void {
38
- const getContext = function (this: HTMLCanvasElement, contextId: string): unknown {
39
- if (contextId !== '2d') return null
40
- return createContext2dMock(this)
41
- }
42
-
43
- HTMLCanvasElement.prototype.getContext =
44
- getContext as unknown as typeof HTMLCanvasElement.prototype.getContext
45
-
46
- if (typeof HTMLCanvasElement.prototype.toDataURL !== 'function') {
47
- HTMLCanvasElement.prototype.toDataURL = () => 'data:,'
48
- }
49
- }
@@ -1,3 +0,0 @@
1
- import { setupCanvasMock } from './canvas-mock'
2
-
3
- setupCanvasMock()