@veltra/sheet 2.0.0 → 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 (203) hide show
  1. package/dist/index.d.ts +4 -25
  2. package/dist/index.js +3 -22
  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 -138
  33. package/dist/vue/style.d.ts +11 -1
  34. package/dist/vue/style.js +11 -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 -6
  46. package/src/env.d.ts +14 -0
  47. package/src/index.ts +203 -13
  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 +163 -148
  86. package/src/vue/style.scss +646 -3
  87. package/src/vue/style.ts +12 -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/formula/ast.d.ts +0 -58
  125. package/dist/core/formula/ast.js +0 -38
  126. package/dist/core/formula/ast.js.map +0 -1
  127. package/dist/core/formula/dependency-graph.d.ts +0 -70
  128. package/dist/core/formula/dependency-graph.js +0 -269
  129. package/dist/core/formula/dependency-graph.js.map +0 -1
  130. package/dist/core/formula/errors.d.ts +0 -25
  131. package/dist/core/formula/errors.js +0 -36
  132. package/dist/core/formula/errors.js.map +0 -1
  133. package/dist/core/formula/evaluator.d.ts +0 -41
  134. package/dist/core/formula/evaluator.js +0 -154
  135. package/dist/core/formula/evaluator.js.map +0 -1
  136. package/dist/core/formula/functions.d.ts +0 -30
  137. package/dist/core/formula/functions.js +0 -209
  138. package/dist/core/formula/functions.js.map +0 -1
  139. package/dist/core/formula/parser.d.ts +0 -7
  140. package/dist/core/formula/parser.js +0 -208
  141. package/dist/core/formula/parser.js.map +0 -1
  142. package/dist/core/formula/tokenizer.d.ts +0 -37
  143. package/dist/core/formula/tokenizer.js +0 -146
  144. package/dist/core/formula/tokenizer.js.map +0 -1
  145. package/dist/core/merge-manager.d.ts +0 -64
  146. package/dist/core/merge-manager.js +0 -111
  147. package/dist/core/merge-manager.js.map +0 -1
  148. package/dist/core/selection.d.ts +0 -31
  149. package/dist/core/selection.js +0 -62
  150. package/dist/core/selection.js.map +0 -1
  151. package/dist/core/sheet.d.ts +0 -105
  152. package/dist/core/sheet.js +0 -202
  153. package/dist/core/sheet.js.map +0 -1
  154. package/dist/core/workbook.d.ts +0 -43
  155. package/dist/core/workbook.js +0 -77
  156. package/dist/core/workbook.js.map +0 -1
  157. package/dist/grid/sheet-grid.d.ts +0 -59
  158. package/dist/grid/sheet-grid.js +0 -230
  159. package/dist/grid/sheet-grid.js.map +0 -1
  160. package/dist/tools/builtin.js.map +0 -1
  161. package/dist/tools/context.js.map +0 -1
  162. package/dist/tools/registry.js.map +0 -1
  163. package/dist/vue/index.d.ts +0 -2
  164. package/dist/vue/sheet.js.map +0 -1
  165. package/dist/vue/sheet.vue.d.ts +0 -24
  166. package/src/core/__test__/address.test.ts +0 -113
  167. package/src/core/__test__/cell-store.test.ts +0 -97
  168. package/src/core/__test__/command.test.ts +0 -377
  169. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  170. package/src/core/__test__/formula-engine.test.ts +0 -470
  171. package/src/core/__test__/formula-functions.test.ts +0 -178
  172. package/src/core/__test__/formula-parser.test.ts +0 -160
  173. package/src/core/__test__/history.test.ts +0 -225
  174. package/src/core/__test__/merge-manager.test.ts +0 -106
  175. package/src/core/__test__/sheet.test.ts +0 -142
  176. package/src/core/address.ts +0 -166
  177. package/src/core/cell-store.ts +0 -173
  178. package/src/core/command/default-registry.ts +0 -15
  179. package/src/core/command/history.ts +0 -167
  180. package/src/core/command/index.ts +0 -7
  181. package/src/core/command/merge-cells.ts +0 -74
  182. package/src/core/command/registry.ts +0 -30
  183. package/src/core/command/set-cell-formula.ts +0 -29
  184. package/src/core/command/set-cell-value.ts +0 -38
  185. package/src/core/command/types.ts +0 -73
  186. package/src/core/events.ts +0 -37
  187. package/src/core/formula/ast.ts +0 -66
  188. package/src/core/formula/dependency-graph.ts +0 -329
  189. package/src/core/formula/errors.ts +0 -41
  190. package/src/core/formula/evaluator.ts +0 -219
  191. package/src/core/formula/functions.ts +0 -266
  192. package/src/core/formula/index.ts +0 -7
  193. package/src/core/formula/parser.ts +0 -198
  194. package/src/core/formula/tokenizer.ts +0 -160
  195. package/src/core/merge-manager.ts +0 -158
  196. package/src/core/selection.ts +0 -66
  197. package/src/core/sheet.ts +0 -257
  198. package/src/core/workbook.ts +0 -99
  199. package/src/grid/__test__/canvas-mock.ts +0 -49
  200. package/src/grid/__test__/setup.ts +0 -3
  201. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  202. package/src/grid/__test__/sheet-grid.test.ts +0 -177
  203. package/src/grid/sheet-grid.ts +0 -283
@@ -1,160 +0,0 @@
1
- /**
2
- * 公式分词器:数字 / 字符串 / 布尔 / 标识符(函数名、引用、表名)/ 带引号表名 / 运算符。
3
- *
4
- * 不做语义消歧:`A1`(引用)、`SUM`(函数名)、`Sheet2`(表名)统一产出 ident,
5
- * 由 parser 依据后随 token(`(` / `!` / `:` / 运算位)判定。
6
- */
7
-
8
- /** 解析失败异常(Sheet 层捕获 → 单元格记为 #ERROR!) */
9
- export class FormulaParseError extends Error {
10
- constructor(message: string) {
11
- super(message)
12
- this.name = 'FormulaParseError'
13
- }
14
- }
15
-
16
- export type FormulaOperator =
17
- | '+'
18
- | '-'
19
- | '*'
20
- | '/'
21
- | '^'
22
- | '&'
23
- | '%'
24
- | '('
25
- | ')'
26
- | ','
27
- | '!'
28
- | ':'
29
- | '='
30
- | '<>'
31
- | '<'
32
- | '<='
33
- | '>'
34
- | '>='
35
-
36
- export type FormulaToken =
37
- | { type: 'number'; value: number }
38
- | { type: 'string'; value: string }
39
- /** 标识符:函数名 / 单元格引用形态 / 裸表名(含 $ 绝对引用写法);TRUE/FALSE 由 parser 归约为布尔 */
40
- | { type: 'ident'; name: string }
41
- /** 带单引号的表名('' 转义为字面单引号) */
42
- | { type: 'quoted-name'; name: string }
43
- | { type: 'op'; op: FormulaOperator }
44
-
45
- const NUMBER_RE = /^(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/
46
-
47
- /** 标识符首字符:字母 / _ / $ / 非 ASCII(中文表名等) */
48
- function isIdentStart(ch: string): boolean {
49
- return /[A-Za-z_$]/.test(ch) || ch.charCodeAt(0) >= 0x80
50
- }
51
-
52
- /** 标识符后续字符:首字符集 + 数字 + . */
53
- function isIdentPart(ch: string): boolean {
54
- return /[A-Za-z0-9_.$]/.test(ch) || ch.charCodeAt(0) >= 0x80
55
- }
56
-
57
- const TWO_CHAR_OPS = ['<>', '<=', '>='] as const
58
- const ONE_CHAR_OPS = [
59
- '+',
60
- '-',
61
- '*',
62
- '/',
63
- '^',
64
- '&',
65
- '%',
66
- '(',
67
- ')',
68
- ',',
69
- '!',
70
- ':',
71
- '=',
72
- '<',
73
- '>'
74
- ] as const
75
-
76
- /** 公式文本(不含 '=')→ token 序列;非法输入抛 FormulaParseError */
77
- export function tokenizeFormula(text: string): FormulaToken[] {
78
- const tokens: FormulaToken[] = []
79
- let i = 0
80
- while (i < text.length) {
81
- const ch = text[i]!
82
- // 空白忽略(不支持 Excel 的空格交集运算符)
83
- if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
84
- i++
85
- continue
86
- }
87
- // 数字
88
- if ((ch >= '0' && ch <= '9') || (ch === '.' && text[i + 1]! >= '0' && text[i + 1]! <= '9')) {
89
- const match = NUMBER_RE.exec(text.slice(i))
90
- if (!match) throw new FormulaParseError(`非法数字(位置 ${i + 1})`)
91
- tokens.push({ type: 'number', value: Number.parseFloat(match[0]) })
92
- i += match[0].length
93
- continue
94
- }
95
- // 字符串字面量("...","" 转义)
96
- if (ch === '"') {
97
- let value = ''
98
- let j = i + 1
99
- for (;;) {
100
- if (j >= text.length) throw new FormulaParseError('字符串缺少结束引号')
101
- if (text[j] === '"') {
102
- if (text[j + 1] === '"') {
103
- value += '"'
104
- j += 2
105
- continue
106
- }
107
- break
108
- }
109
- value += text[j]
110
- j++
111
- }
112
- tokens.push({ type: 'string', value })
113
- i = j + 1
114
- continue
115
- }
116
- // 带引号表名('...','' 转义)
117
- if (ch === "'") {
118
- let name = ''
119
- let j = i + 1
120
- for (;;) {
121
- if (j >= text.length) throw new FormulaParseError('表名缺少结束引号')
122
- if (text[j] === "'") {
123
- if (text[j + 1] === "'") {
124
- name += "'"
125
- j += 2
126
- continue
127
- }
128
- break
129
- }
130
- name += text[j]
131
- j++
132
- }
133
- tokens.push({ type: 'quoted-name', name })
134
- i = j + 1
135
- continue
136
- }
137
- // 标识符(函数名 / 引用 / 裸表名)
138
- if (isIdentStart(ch)) {
139
- let j = i + 1
140
- while (j < text.length && isIdentPart(text[j]!)) j++
141
- tokens.push({ type: 'ident', name: text.slice(i, j) })
142
- i = j
143
- continue
144
- }
145
- // 双字符运算符优先
146
- const two = text.slice(i, i + 2)
147
- if ((TWO_CHAR_OPS as readonly string[]).includes(two)) {
148
- tokens.push({ type: 'op', op: two as FormulaOperator })
149
- i += 2
150
- continue
151
- }
152
- if ((ONE_CHAR_OPS as readonly string[]).includes(ch)) {
153
- tokens.push({ type: 'op', op: ch as FormulaOperator })
154
- i++
155
- continue
156
- }
157
- throw new FormulaParseError(`无法识别的字符 "${ch}"(位置 ${i + 1})`)
158
- }
159
- return tokens
160
- }
@@ -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,257 +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
- name: string
48
-
49
- private emitter = new TypedEventEmitter<SheetEvents>()
50
-
51
- constructor(name = 'Sheet1', formulaGraph?: DependencyGraph) {
52
- this.name = name
53
- this.selection = new SelectionModel((addr) => this.merges.resolveAnchor(addr))
54
- this.selection.on((state) => this.emitter.emit('selection-change', state))
55
- this.history = new HistoryManager(this.boundApplyPatch)
56
- this.history.onChange((state) => this.emitter.emit('history-change', state))
57
- this.formulaGraph = formulaGraph ?? new DependencyGraph()
58
- this.formulaGraph.registerSheet(this)
59
- }
60
-
61
- get rowCount(): number {
62
- return this.store.rowCount
63
- }
64
-
65
- get colCount(): number {
66
- return this.store.colCount
67
- }
68
-
69
- // ─── 单元格数据 ────────────────────────────────────────────
70
-
71
- /** 原始存储语义读取(被覆盖格 → undefined) */
72
- getCellData(addr: CellAddress): CellData | undefined {
73
- return this.store.getCell(addr)
74
- }
75
-
76
- /** 锚点解析语义读取(被覆盖格 → 锚点的值) */
77
- getDisplayValue(addr: CellAddress): CellValue | undefined {
78
- const anchor = this.merges.resolveAnchor(addr)
79
- return this.store.getCell(anchor)?.v ?? undefined
80
- }
81
-
82
- /** 写入原始值(解析到锚点;空值 = 清除);'=' 前缀按公式处理;经命令执行,可撤销 */
83
- setCellValue(addr: CellAddress, value: CellValue): void {
84
- if (typeof value === 'string' && value.startsWith('=')) {
85
- this.setCellFormula(addr, value)
86
- return
87
- }
88
- const data = value == null || value === '' ? undefined : { v: value, t: inferCellType(value) }
89
- this.setCells([{ addr, data }])
90
- }
91
-
92
- /** 写入完整 CellData(解析到锚点;空数据 = 清除);经命令执行,可撤销 */
93
- setCell(addr: CellAddress, data?: CellData): void {
94
- this.setCells([{ addr, data }])
95
- }
96
-
97
- /**
98
- * 写入公式(可带 '=' 前缀;空白公式 = 清除);经命令执行,可撤销。
99
- * 公式原文存 CellData.f,计算缓存(v/t)由增量重算填充;解析失败 → #ERROR!。
100
- */
101
- setCellFormula(addr: CellAddress, formula: string): void {
102
- const text = formula.startsWith('=') ? formula.slice(1) : formula
103
- if (text.trim() === '') {
104
- this.setCells([{ addr, data: undefined }])
105
- return
106
- }
107
- this.executeCommand(SetCellFormulaCommand.id, { addr, formula: text })
108
- }
109
-
110
- /** 批量写入(一次调用 = 一个 undo 单元,供粘贴/填充复用) */
111
- setCells(items: SetCellValueItem[]): void {
112
- this.executeCommand(SetCellValueCommand.id, { items })
113
- }
114
-
115
- // ─── 合并 ────────────────────────────────────────────────
116
-
117
- /** 合并语义下的单元格信息(普通格 / 合并锚点 / 被覆盖格) */
118
- getCellInfo(addr: CellAddress): CellInfo {
119
- return this.merges.getCellInfo(addr)
120
- }
121
-
122
- /**
123
- * 合并区域。值保留规则(同 Excel/univer):
124
- * 仅保留包围盒内按行主序第一个有值格的值,写入新锚点,其余被覆盖格清空。
125
- * @returns 最终生效的合并区域(与既有合并相交时取包围盒)
126
- */
127
- mergeCells(range: CellRange): CellRange {
128
- return this.executeCommand<CellRange>(MergeCellsCommand.id, { range })!
129
- }
130
-
131
- /** 解除与 range 相交的所有合并;仅原锚点保留值(被覆盖格本就无值) */
132
- unmergeCells(range: CellRange): void {
133
- this.executeCommand(UnmergeCellsCommand.id, { range })
134
- }
135
-
136
- // ─── 命令与历史 ──────────────────────────────────────────
137
-
138
- /** 经默认注册表执行命令;产生的 mutation 与公式重算派生 mutation 一并推入历史 */
139
- executeCommand<R = void>(commandId: string, params: unknown): R | undefined {
140
- const result = defaultCommandRegistry.execute<R>(
141
- { sheet: this, applyPatch: this.boundApplyPatch },
142
- commandId,
143
- params
144
- )
145
- if (result && result.mutations.length > 0) {
146
- const mutations = [...result.mutations]
147
- const recalcMutation = this.recalcAfterCommand(result.mutations)
148
- if (recalcMutation) mutations.push(recalcMutation)
149
- this.history.push(mutations)
150
- }
151
- return result?.result
152
- }
153
-
154
- /** 开启事务(事务内所有命令合并为一个 undo 单元;可嵌套,拍平到最外层) */
155
- beginTransaction(): void {
156
- this.history.beginTransaction()
157
- }
158
-
159
- /** 提交事务 */
160
- commit(): void {
161
- this.history.commit()
162
- }
163
-
164
- /** 回滚事务:还原缓冲中已应用的变更并放弃事务 */
165
- rollback(): void {
166
- this.history.rollback()
167
- }
168
-
169
- /** 撤销一步;无可撤销时返回 false */
170
- undo(): boolean {
171
- return this.history.undo()
172
- }
173
-
174
- /** 重做一步;无可重做时返回 false */
175
- redo(): boolean {
176
- return this.history.redo()
177
- }
178
-
179
- get canUndo(): boolean {
180
- return this.history.canUndo
181
- }
182
-
183
- get canRedo(): boolean {
184
- return this.history.canRedo
185
- }
186
-
187
- // ─── 选区 ────────────────────────────────────────────────
188
-
189
- getSelection(): SelectionState {
190
- return this.selection.getState()
191
- }
192
-
193
- /** 选中单格(被覆盖格自动定位锚点) */
194
- selectCell(addr: CellAddress): void {
195
- this.selection.selectCell(addr)
196
- }
197
-
198
- selectRange(range: CellRange): void {
199
- this.selection.selectRange(range)
200
- }
201
-
202
- // ─── 事件 ────────────────────────────────────────────────
203
-
204
- on<K extends keyof SheetEvents>(type: K, handler: (payload: SheetEvents[K]) => void): () => void {
205
- return this.emitter.on(type, handler)
206
- }
207
-
208
- off<K extends keyof SheetEvents>(type: K, handler: (payload: SheetEvents[K]) => void): void {
209
- this.emitter.off(type, handler)
210
- }
211
-
212
- // ─── 内部 ────────────────────────────────────────────────
213
-
214
- private readonly boundApplyPatch = (patch: Patch, direction: PatchDirection): void => {
215
- // 跨表重算的派生补丁路由到目标 sheet(回放经源 sheet 历史时同样按此路由)
216
- const target = patch.kind === 'cell' ? (patch.sheet ?? this) : this
217
- target.applyPatch(patch, direction)
218
- }
219
-
220
- /** 应用补丁:命令执行与 undo/redo 回放共用的唯一变更通道 */
221
- private applyPatch(patch: Patch, direction: PatchDirection): void {
222
- if (patch.kind === 'cell') {
223
- const data = direction === 'redo' ? patch.after : patch.before
224
- const previous = direction === 'redo' ? patch.before : patch.after
225
- this.store.setCell(patch.addr, data)
226
- // 公式依赖图同步(f 增/删/改 → 节点增/删/重建;undo/redo 回放同样覆盖)
227
- this.formulaGraph.syncCell(this, patch.addr, previous, data)
228
- this.emitter.emit('cell-change', { addr: patch.addr })
229
- return
230
- }
231
- const exists = direction === 'redo' ? patch.after : patch.before
232
- if (exists) {
233
- this.merges.addMerge(patch.range)
234
- } else {
235
- this.merges.removeMerge(patch.range)
236
- }
237
- this.emitter.emit('merge-change', { range: patch.range })
238
- }
239
-
240
- /**
241
- * 命令执行后的公式增量重算:从命令 mutation 提取变更格 → 依赖图标脏 +
242
- * 拓扑序重算 → 派生补丁立即应用,作为附加 mutation 并入同一 undo 单元。
243
- * undo/redo 纯补丁回放(不重算),图状态由 applyPatch 内的 syncCell 维持。
244
- */
245
- private recalcAfterCommand(mutations: Mutation[]): Mutation | undefined {
246
- const changed: { sheet: Sheet; addr: CellAddress }[] = []
247
- for (const mutation of mutations) {
248
- for (const patch of mutation.redo) {
249
- if (patch.kind === 'cell') changed.push({ sheet: patch.sheet ?? this, addr: patch.addr })
250
- }
251
- }
252
- const derived = this.formulaGraph.recalc(changed)
253
- if (derived.length === 0) return undefined
254
- for (const patch of derived) this.boundApplyPatch(patch, 'redo')
255
- return { redo: derived, undo: [...derived].reverse() }
256
- }
257
- }