@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
@@ -0,0 +1,161 @@
1
+ import { computed, ref, shallowRef, watch } from "vue";
2
+ import { debounce } from "@cat-kit/core";
3
+ import { inferCellType, normalizeInputValue } from "@veltra/sheet-core/core/cell-store";
4
+ import { findAll, findNextFrom, findPrevFrom } from "@veltra/sheet-core/core/find";
5
+ //#region src/vue/use-find-replace.ts
6
+ /**
7
+ * 查找条逻辑(关键词 / 上一个 / 下一个 / 命中计数 / 替换):
8
+ * - 关键词 / 选项变化 → 重新查找并定位第一个命中
9
+ * - Enter = 下一个、Shift+Enter = 上一个(行主序、到边界循环)
10
+ * - 替换当前 = 一次 setCells(一个 undo 单元);全部替换 = 一次批量 = 单 undo 单元
11
+ * - 公式格不参与替换(写入 {v,t} 会覆盖公式原文 f,导致公式丢失)
12
+ */
13
+ function useFindReplace(options) {
14
+ const { getSheet, context } = options;
15
+ const findQuery = ref("");
16
+ const findReplace = ref("");
17
+ const caseSensitive = ref(false);
18
+ const wholeCell = ref(false);
19
+ const searchIn = ref("value");
20
+ /** 全部命中(行主序;core/find 纯逻辑,无头可测) */
21
+ const findMatches = shallowRef([]);
22
+ /** 当前命中在 findMatches 中的下标;-1 = 无命中 */
23
+ const findCursor = ref(-1);
24
+ const canFind = computed(() => findMatches.value.length > 0);
25
+ const canReplace = computed(() => canFind.value && findCursor.value >= 0);
26
+ const findCountText = computed(() => {
27
+ const total = findMatches.value.length;
28
+ if (total === 0) return "0 / 0";
29
+ return `${Math.min(findCursor.value + 1, total)} / ${total}`;
30
+ });
31
+ function findOptions() {
32
+ return {
33
+ caseSensitive: caseSensitive.value,
34
+ wholeCell: wholeCell.value,
35
+ searchIn: searchIn.value
36
+ };
37
+ }
38
+ /** 上次全扫时的关键词 / 选项快照(识别防抖窗口内的脏缓存,见 ensureFreshMatches) */
39
+ let scannedQuery = "";
40
+ let scannedOptions = "";
41
+ function optionsSnapshot() {
42
+ return `${caseSensitive.value}|${wholeCell.value}|${searchIn.value}`;
43
+ }
44
+ /** 关键词或选项变化:重新查找并定位第一个命中(否则保持当前位置附近) */
45
+ function refreshFind(initial) {
46
+ const matches = findAll(getSheet(), findQuery.value, findOptions());
47
+ scannedQuery = findQuery.value;
48
+ scannedOptions = optionsSnapshot();
49
+ findMatches.value = matches;
50
+ if (matches.length === 0) {
51
+ findCursor.value = -1;
52
+ return;
53
+ }
54
+ if (initial) {
55
+ findCursor.value = 0;
56
+ context.selectCell(matches[0].addr);
57
+ } else findCursor.value = Math.min(findCursor.value, matches.length - 1);
58
+ }
59
+ /**
60
+ * 防抖窗口内(关键词已输入、尚未触发全扫)Enter 导航 / 替换仍持有旧关键词的
61
+ * 命中缓存——先同步补扫一次,保证导航与替换作用于当前输入框里的关键词
62
+ * (否则「全部替换」会把旧关键词的命中集批量改写,与屏幕上看到的查找词不符)
63
+ */
64
+ function ensureFreshMatches() {
65
+ if (findQuery.value !== scannedQuery || optionsSnapshot() !== scannedOptions) refreshFind(false);
66
+ }
67
+ /** 定位到 findMatches 中与目标地址相同的命中(找不到则保持) */
68
+ function locateMatch(match) {
69
+ const index = findMatches.value.findIndex((item) => item.addr.row === match.addr.row && item.addr.col === match.addr.col);
70
+ if (index >= 0) findCursor.value = index;
71
+ context.selectCell(match.addr);
72
+ }
73
+ function findForward() {
74
+ ensureFreshMatches();
75
+ const matches = findMatches.value;
76
+ if (matches.length === 0) return;
77
+ const active = context.getSelection().activeCell;
78
+ const next = active ? findNextFrom(matches, active) : matches[0];
79
+ if (!next) return;
80
+ locateMatch(next);
81
+ }
82
+ function findPrevious() {
83
+ ensureFreshMatches();
84
+ const matches = findMatches.value;
85
+ if (matches.length === 0) return;
86
+ const active = context.getSelection().activeCell;
87
+ const prev = active ? findPrevFrom(matches, active) : matches[matches.length - 1];
88
+ if (!prev) return;
89
+ locateMatch(prev);
90
+ }
91
+ /** Enter = 下一个;Shift+Enter = 上一个(UInput 的 keydown 透传到外层容器,冒泡可达) */
92
+ function handleFindKeydown(event) {
93
+ if (event.key !== "Enter") return;
94
+ event.preventDefault();
95
+ if (event.shiftKey) findPrevious();
96
+ else findForward();
97
+ }
98
+ /** 替换文本 → CellData(空字符串 = 清除单元格;数字/布尔文本规范化后推断类型) */
99
+ function replaceData() {
100
+ if (findReplace.value === "") return void 0;
101
+ const normalized = normalizeInputValue(findReplace.value);
102
+ const t = inferCellType(normalized);
103
+ return t ? {
104
+ v: normalized,
105
+ t
106
+ } : { v: normalized };
107
+ }
108
+ /** 公式格不参与替换(写入 {v,t} 会覆盖公式原文 f,导致公式丢失) */
109
+ function isReplaceable(match) {
110
+ return !context.getCellData(match.addr)?.f;
111
+ }
112
+ /** 替换当前命中格(一次 setCells = 一个 undo 单元);替换后重新查找 */
113
+ function replaceCurrent() {
114
+ ensureFreshMatches();
115
+ const match = findMatches.value[findCursor.value];
116
+ if (!match || !isReplaceable(match)) return;
117
+ context.setCells([{
118
+ addr: match.addr,
119
+ data: replaceData()
120
+ }]);
121
+ refreshFind(false);
122
+ }
123
+ /** 全部替换(一次 setCells 批量 = 单 undo 单元,undo 一次全部还原);公式格自动跳过 */
124
+ function replaceAll() {
125
+ ensureFreshMatches();
126
+ const matches = findMatches.value.filter(isReplaceable);
127
+ if (matches.length === 0) return;
128
+ context.setCells(matches.map((match) => ({
129
+ addr: match.addr,
130
+ data: replaceData()
131
+ })));
132
+ refreshFind(false);
133
+ }
134
+ const refreshFindDebounced = debounce(() => {
135
+ if (findQuery.value === scannedQuery && optionsSnapshot() === scannedOptions) return;
136
+ refreshFind(true);
137
+ }, 300, false);
138
+ watch([
139
+ findQuery,
140
+ caseSensitive,
141
+ wholeCell,
142
+ searchIn
143
+ ], refreshFindDebounced);
144
+ return {
145
+ findQuery,
146
+ findReplace,
147
+ caseSensitive,
148
+ wholeCell,
149
+ searchIn,
150
+ canFind,
151
+ canReplace,
152
+ findCountText,
153
+ findForward,
154
+ findPrevious,
155
+ handleFindKeydown,
156
+ replaceCurrent,
157
+ replaceAll
158
+ };
159
+ }
160
+ //#endregion
161
+ export { useFindReplace };
@@ -0,0 +1,37 @@
1
+ //#region src/vue/use-formula-ref-select.ts
2
+ /** 引用选择上下文:光标前一个非空字符属于此集合时可插入单元格引用 */
3
+ const REF_TRIGGER_CHARS = /* @__PURE__ */ new Set([
4
+ "=",
5
+ "(",
6
+ ",",
7
+ "+",
8
+ "-",
9
+ "*",
10
+ "/",
11
+ "^",
12
+ "&",
13
+ "<",
14
+ ">"
15
+ ]);
16
+ /**
17
+ * 判定是否处于「可插入引用」位置(无状态:每次按文本 + 光标重算)。
18
+ * `-` 的一元/二元歧义基础版不区分(`A1-` 后框选 → `A1-B2`,合法且误判无害)。
19
+ */
20
+ function isRefSelectContext(text, cursor) {
21
+ if (cursor < 0 || cursor > text.length) return false;
22
+ if (!text.startsWith("=")) return false;
23
+ const before = text.slice(0, cursor).replace(/\s+$/, "");
24
+ if (before.length === 0) return false;
25
+ return REF_TRIGGER_CHARS.has(before[before.length - 1]);
26
+ }
27
+ /** 在光标处插入引用文本(替换空选区;有选区时覆盖选区) */
28
+ function insertRefText(text, cursor, rangeText, selectionEnd = cursor) {
29
+ const start = Math.min(cursor, selectionEnd);
30
+ const end = Math.max(cursor, selectionEnd);
31
+ return {
32
+ text: text.slice(0, start) + rangeText + text.slice(end),
33
+ cursor: start + rangeText.length
34
+ };
35
+ }
36
+ //#endregion
37
+ export { insertRefText, isRefSelectContext };
@@ -0,0 +1,126 @@
1
+ import { listFormulaFunctions } from "@veltra/sheet-core/core/formula/functions";
2
+ /**
3
+ * 空前缀时优先展示的常用函数(其余按名称升序补齐至上限)。
4
+ * 纯字典序会把 SUM / OR / ROUND 挤出前 10(ABS…NOT),导致输入 `=` 看不到 SUM。
5
+ */
6
+ const COMMON_FORMULA_NAMES = [
7
+ "SUM",
8
+ "AVERAGE",
9
+ "COUNT",
10
+ "MAX",
11
+ "MIN",
12
+ "IF",
13
+ "ROUND",
14
+ "ABS",
15
+ "AND",
16
+ "OR"
17
+ ];
18
+ /** 函数名 token 前合法的「触发」字符(= / 运算符 / ( / ,) */
19
+ const SUGGEST_TRIGGER_CHARS = /* @__PURE__ */ new Set([
20
+ "=",
21
+ "(",
22
+ ",",
23
+ "+",
24
+ "-",
25
+ "*",
26
+ "/",
27
+ "^",
28
+ "&",
29
+ "<",
30
+ ">"
31
+ ]);
32
+ /** 格式化函数签名;无 params 时仅返回名称 */
33
+ function formatFunctionSignature(name, params) {
34
+ if (params.length === 0) return name;
35
+ return `${name}(${params.join(", ")})`;
36
+ }
37
+ /**
38
+ * 判定补全上下文:光标前向匹配函数名 token(或紧跟 `=` 的空前缀),
39
+ * 且 token 前一个非空字符 ∈ 触发字符集。
40
+ * 镜像只读期由调用方跳过,本函数不做只读判断。
41
+ */
42
+ function getSuggestContext(text, cursor) {
43
+ if (cursor < 0 || cursor > text.length) return null;
44
+ const before = text.slice(0, cursor);
45
+ const tokenMatch = /[A-Za-z][A-Za-z0-9_.]*$/.exec(before);
46
+ const prefix = tokenMatch?.[0] ?? "";
47
+ const start = tokenMatch ? cursor - prefix.length : cursor;
48
+ if (!tokenMatch) {
49
+ const trimmed = before.replace(/\s+$/, "");
50
+ if (!trimmed.endsWith("=")) return null;
51
+ if (trimmed.endsWith("<=") || trimmed.endsWith(">=")) return null;
52
+ return {
53
+ prefix: "",
54
+ start,
55
+ end: cursor
56
+ };
57
+ }
58
+ const beforeToken = before.slice(0, start).replace(/\s+$/, "");
59
+ if (beforeToken.length === 0) return null;
60
+ const last = beforeToken[beforeToken.length - 1];
61
+ if (!SUGGEST_TRIGGER_CHARS.has(last)) return null;
62
+ return {
63
+ prefix,
64
+ start,
65
+ end: cursor
66
+ };
67
+ }
68
+ /**
69
+ * 前缀过滤函数表(大小写不敏感),上限 FORMULA_SUGGEST_LIMIT。
70
+ * 空前缀:常用函数优先,其余名称升序补齐;有前缀:名称升序。
71
+ */
72
+ function filterFormulaSuggestions(prefix) {
73
+ const upper = prefix.toUpperCase();
74
+ const all = listFormulaFunctions();
75
+ if (!upper) {
76
+ const byName = new Map(all.map((fn) => [fn.name, fn]));
77
+ const items = [];
78
+ const used = /* @__PURE__ */ new Set();
79
+ for (const name of COMMON_FORMULA_NAMES) {
80
+ const fn = byName.get(name);
81
+ if (!fn) continue;
82
+ items.push(toSuggestItem(fn));
83
+ used.add(name);
84
+ if (items.length >= 10) return items;
85
+ }
86
+ for (const fn of all) {
87
+ if (used.has(fn.name)) continue;
88
+ items.push(toSuggestItem(fn));
89
+ if (items.length >= 10) break;
90
+ }
91
+ return items;
92
+ }
93
+ const items = [];
94
+ for (const fn of all) {
95
+ if (!fn.name.startsWith(upper)) continue;
96
+ items.push(toSuggestItem(fn));
97
+ if (items.length >= 10) break;
98
+ }
99
+ return items;
100
+ }
101
+ function toSuggestItem(fn) {
102
+ return {
103
+ name: fn.name,
104
+ params: fn.params,
105
+ description: fn.description,
106
+ signature: formatFunctionSignature(fn.name, fn.params)
107
+ };
108
+ }
109
+ /** 将 [start, end) 的 token 替换为 `NAME()`,光标入括号内(免手动补右括号) */
110
+ function applySuggest(text, start, end, name) {
111
+ const insert = `${name}()`;
112
+ return {
113
+ text: text.slice(0, start) + insert + text.slice(end),
114
+ cursor: start + name.length + 1
115
+ };
116
+ }
117
+ /**
118
+ * 键盘导航:↑↓ 循环;返回新的高亮下标。
119
+ * activeIndex 越界时按 0 处理。
120
+ */
121
+ function moveSuggestIndex(activeIndex, length, delta) {
122
+ if (length <= 0) return 0;
123
+ return ((activeIndex % length + length) % length + delta + length) % length;
124
+ }
125
+ //#endregion
126
+ export { COMMON_FORMULA_NAMES, applySuggest, filterFormulaSuggestions, formatFunctionSignature, getSuggestContext, moveSuggestIndex };
@@ -0,0 +1,180 @@
1
+ import { buildBodyMenus, buildColHeaderMenus, buildRowHeaderMenus, ensureContextMenuSelection, resolveRenderSize } from "./sheet-context-menu.js";
2
+ import { onBeforeUnmount, onMounted, watch } from "vue";
3
+ import { contextmenu } from "@veltra/desktop";
4
+ import { SheetGrid } from "@veltra/sheet-core/grid/sheet-grid";
5
+ //#region src/vue/use-sheet-grid.ts
6
+ /**
7
+ * 缓存容量:最近 N 个 sheet 的 SheetGrid 实例常驻(tab 来回切换零重建)。
8
+ * 大文件多 sheet 场景下切换卡顿的主因是每次重建 VTable(实例创建 + scenegraph
9
+ * 首屏构建 ~300ms);缓存命中时仅切换容器可见性 + 选区回驱(≈0)。
10
+ * 每实例持有一个 canvas + scenegraph,容量即内存上限(含隐藏实例的渲染开销)。
11
+ */
12
+ const GRID_CACHE_CAPACITY = 3;
13
+ /**
14
+ * SheetGrid 生命周期与网格右键菜单(LRU 实例缓存):
15
+ * - activateGrid:tab 切换入口——命中缓存 → 显示 + 模型同步(零重建);
16
+ * 未命中 → 新建实例入缓存,超容量淘汰最久未用(release + 移除容器)
17
+ * - rebuildGrid:强制重建激活实例并清空缓存(structure-change / props 尺寸
18
+ * 变化 / 工作簿切换 / 导入替换——这些场景下缓存实例的 columns / 数据源失效)
19
+ * - 右键菜单:body / 行号 / 列头
20
+ * - 公式栏引用选择:interceptSelection 拦截选区回写 + pointerdown 挂起 blur
21
+ */
22
+ function useSheetGrid(options) {
23
+ const { props, gridRef, getActiveSheet, context, formulaBarRef } = options;
24
+ const cache = /* @__PURE__ */ new Map();
25
+ let active;
26
+ let seq = 0;
27
+ let detachPointerDown;
28
+ function handleContextMenu(info) {
29
+ const sheet = getActiveSheet();
30
+ const renderSize = resolveRenderSize(props.rows, props.cols, sheet.rows, sheet.cols);
31
+ ensureContextMenuSelection(context, info, renderSize);
32
+ const menus = info.kind === "row-header" ? buildRowHeaderMenus(context) : info.kind === "col-header" ? buildColHeaderMenus(context) : buildBodyMenus(context);
33
+ contextmenu.pop({
34
+ mousePosition: {
35
+ x: info.x,
36
+ y: info.y
37
+ },
38
+ width: "max-content",
39
+ menus
40
+ });
41
+ }
42
+ function bindGridPointerDown(container) {
43
+ detachPointerDown?.();
44
+ const onPointerDown = () => {
45
+ formulaBarRef.value?.beginBlurSuppress();
46
+ };
47
+ container.addEventListener("pointerdown", onPointerDown, true);
48
+ detachPointerDown = () => {
49
+ container.removeEventListener("pointerdown", onPointerDown, true);
50
+ detachPointerDown = void 0;
51
+ };
52
+ }
53
+ /** 新建实例:创建独立容器 div(absolute 堆叠于 grid 区内)并构造 SheetGrid */
54
+ function createGrid(sheet) {
55
+ const host = gridRef.value;
56
+ if (!host) throw new Error("useSheetGrid: grid 容器未挂载");
57
+ const el = document.createElement("div");
58
+ el.className = "u-sheet__grid-instance";
59
+ host.appendChild(el);
60
+ const grid = new SheetGrid({
61
+ container: el,
62
+ sheet,
63
+ rows: props.rows,
64
+ cols: props.cols,
65
+ onContextMenu: handleContextMenu,
66
+ onEditStart: (addr) => formulaBarRef.value?.mirrorGridEdit(addr),
67
+ onEditEnd: (addr) => formulaBarRef.value?.exitMirror(addr),
68
+ interceptSelection: () => formulaBarRef.value?.isRefSelecting() ?? false,
69
+ onSelectionIntercept: (range) => formulaBarRef.value?.handleRefSelect(range)
70
+ });
71
+ let dirty = false;
72
+ const offStructureChange = sheet.on("structure-change", () => {
73
+ dirty = true;
74
+ });
75
+ const release = () => {
76
+ offStructureChange();
77
+ grid.release();
78
+ el.remove();
79
+ };
80
+ return {
81
+ sheet,
82
+ grid,
83
+ el,
84
+ lastUsed: ++seq,
85
+ release,
86
+ get dirty() {
87
+ return dirty;
88
+ }
89
+ };
90
+ }
91
+ /** 切换激活实例:显示目标、隐藏其余(visibility:hidden 保持尺寸,canvas 不重建) */
92
+ function showOnly(target) {
93
+ for (const item of cache.values()) {
94
+ const on = item === target;
95
+ item.el.style.visibility = on ? "visible" : "hidden";
96
+ item.grid.setVisible(on);
97
+ }
98
+ }
99
+ /** 淘汰最久未用的缓存实例(保留激活实例) */
100
+ function evict() {
101
+ while (cache.size > GRID_CACHE_CAPACITY) {
102
+ let oldest;
103
+ for (const item of cache.values()) {
104
+ if (item === active) continue;
105
+ if (!oldest || item.lastUsed < oldest.lastUsed) oldest = item;
106
+ }
107
+ if (!oldest) break;
108
+ cache.delete(oldest.sheet);
109
+ oldest.release();
110
+ }
111
+ }
112
+ /** 清空全部缓存实例(workbook 切换 / 导入替换 / 渲染尺寸变化 / 结构变更) */
113
+ function invalidateAll() {
114
+ for (const item of cache.values()) item.release();
115
+ cache.clear();
116
+ active = void 0;
117
+ detachPointerDown?.();
118
+ }
119
+ /**
120
+ * tab 切换:优先复用缓存实例(零重建)。
121
+ * 命中时校验实例是否过期(隐藏期间程序化 structure-change——插入/删除/undo
122
+ * 行列——vue 层只绑定激活 sheet 的 structure-change,隐藏实例靠自持订阅标记
123
+ * dirty);过期则释放重建,否则同步模型状态(选区回驱 + 冻结校正;数据/样式/
124
+ * 行高由实例常驻的 sheet 事件订阅持续同步,隐藏期间跨表重算等模型变更不丢)。
125
+ */
126
+ function activateGrid() {
127
+ const sheet = getActiveSheet();
128
+ if (!gridRef.value) return;
129
+ const cached = cache.get(sheet);
130
+ if (cached && !cached.dirty) {
131
+ cached.lastUsed = ++seq;
132
+ cached.grid.syncFromModel();
133
+ showOnly(cached);
134
+ active = cached;
135
+ bindGridPointerDown(cached.el);
136
+ return;
137
+ }
138
+ if (cached) {
139
+ cache.delete(sheet);
140
+ cached.release();
141
+ }
142
+ const created = createGrid(sheet);
143
+ cache.set(sheet, created);
144
+ created.lastUsed = ++seq;
145
+ showOnly(created);
146
+ active = created;
147
+ bindGridPointerDown(created.el);
148
+ evict();
149
+ }
150
+ /**
151
+ * 清理已不存在的 sheet 的缓存实例(workbook 删除 sheet 后及时释放,
152
+ * 避免条目与事件订阅残留到容量淘汰)。
153
+ */
154
+ function pruneCache(sheets) {
155
+ const alive = new Set(sheets);
156
+ for (const [sheet, item] of cache) {
157
+ if (alive.has(sheet) || item === active) continue;
158
+ cache.delete(sheet);
159
+ item.release();
160
+ }
161
+ }
162
+ /** 强制重建激活实例并清空缓存(structure-change / props / 工作簿替换) */
163
+ function rebuildGrid() {
164
+ invalidateAll();
165
+ activateGrid();
166
+ }
167
+ onMounted(activateGrid);
168
+ watch(() => [props.rows, props.cols], rebuildGrid);
169
+ onBeforeUnmount(() => {
170
+ invalidateAll();
171
+ });
172
+ return {
173
+ rebuildGrid,
174
+ activateGrid,
175
+ pruneCache,
176
+ getGrid: () => active?.grid
177
+ };
178
+ }
179
+ //#endregion
180
+ export { useSheetGrid };
@@ -0,0 +1,122 @@
1
+ import { defaultToolRegistry } from "../tools/registry.js";
2
+ import { createSheetContext } from "../tools/context.js";
3
+ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
4
+ import { Workbook } from "@veltra/sheet-core/core/workbook";
5
+ //#region src/vue/use-sheet-state.ts
6
+ /**
7
+ * USheet 的状态源(单一数据源,其余状态由此推导):
8
+ * - workbook(外部传入或内部自建单 sheet 工作簿)、sheetList、activeIndex、activeSheet
9
+ * - SheetContext 工具门面(动态解析活动 sheet,tab 切换后自动指向当前 sheet)
10
+ * - stateTick:工具 visible/disabled/active 等纯函数的状态源版本号(bump 触发重算)
11
+ * - 工作簿 / 活动 sheet 的事件绑定(tab 切换、工作簿 prop 变更时重绑)
12
+ */
13
+ function useSheetState(props, emit, hooks) {
14
+ /** 工作簿:外部传入或内部自建(单 sheet) */
15
+ const internalWorkbook = new Workbook();
16
+ const workbook = computed(() => props.workbook ?? internalWorkbook);
17
+ /** 激活 sheet 索引(镜像 workbook.activeSheetIndex,经事件同步) */
18
+ const activeIndex = ref(workbook.value.activeSheetIndex);
19
+ /** sheet 列表(tabs 渲染;由 sheets-change 同步) */
20
+ const sheetList = shallowRef(workbook.value.getSheets());
21
+ const activeSheet = computed(() => sheetList.value[activeIndex.value] ?? workbook.value.activeSheet);
22
+ /** 工具上下文:动态解析活动 sheet 与当前工作簿,tab 切换 / workbook prop 切换后自动指向最新值 */
23
+ const context = createSheetContext(() => activeSheet.value, () => workbook.value);
24
+ /** 工具栏状态版本号:visible/disabled/active 是 (ctx) => boolean 纯函数,状态源变化时 bump 触发重算 */
25
+ const stateTick = ref(0);
26
+ const bump = () => {
27
+ stateTick.value++;
28
+ };
29
+ let disposeSheetEvents = [];
30
+ /** 订阅活动 sheet 的状态源(tab 切换 / 工作簿变更时重绑) */
31
+ function bindSheetEvents(sheet) {
32
+ for (const dispose of disposeSheetEvents) dispose();
33
+ let tickScheduled = false;
34
+ const scheduleBump = () => {
35
+ if (tickScheduled) return;
36
+ tickScheduled = true;
37
+ queueMicrotask(() => {
38
+ tickScheduled = false;
39
+ bump();
40
+ });
41
+ };
42
+ disposeSheetEvents = [
43
+ sheet.on("selection-change", bump),
44
+ sheet.on("history-change", bump),
45
+ sheet.on("cell-change", bump),
46
+ sheet.on("merge-change", bump),
47
+ sheet.on("frozen-change", bump),
48
+ sheet.on("content-reset", scheduleBump),
49
+ sheet.on("structure-change", hooks.rebuildGrid)
50
+ ];
51
+ }
52
+ let disposeWorkbookEvents = [];
53
+ function bindWorkbookEvents(wb) {
54
+ for (const dispose of disposeWorkbookEvents) dispose();
55
+ disposeWorkbookEvents = [
56
+ wb.on("sheets-change", ({ sheets }) => {
57
+ sheetList.value = sheets;
58
+ hooks.pruneCache?.(sheets);
59
+ bump();
60
+ }),
61
+ wb.on("sheet-rename", () => {
62
+ sheetList.value = workbook.value.getSheets();
63
+ bump();
64
+ }),
65
+ wb.on("active-sheet-change", ({ sheet, index }) => {
66
+ hooks.onBeforeSheetChange();
67
+ activeIndex.value = index;
68
+ bindSheetEvents(sheet);
69
+ hooks.activateGrid();
70
+ hooks.pruneCache?.(workbook.value.getSheets());
71
+ bump();
72
+ emit("active-sheet-change", {
73
+ sheet,
74
+ index
75
+ });
76
+ })
77
+ ];
78
+ }
79
+ /**
80
+ * 工作簿内容被结构性替换后同步(如导入 xlsx 的 replaceWorkbook):
81
+ * replaceWorkbook 未必触发 active-sheet-change(同 index),需显式同步 tabs、
82
+ * 重绑 sheet 事件并重建网格。
83
+ */
84
+ function syncFromWorkbook() {
85
+ sheetList.value = workbook.value.getSheets();
86
+ activeIndex.value = workbook.value.activeSheetIndex;
87
+ bindSheetEvents(activeSheet.value);
88
+ hooks.rebuildGrid();
89
+ bump();
90
+ }
91
+ const offRegistryChange = defaultToolRegistry.onChange(bump);
92
+ onMounted(() => {
93
+ bindWorkbookEvents(workbook.value);
94
+ bindSheetEvents(activeSheet.value);
95
+ });
96
+ watch(() => props.workbook, (wb, prev) => {
97
+ if (wb === prev) return;
98
+ hooks.onBeforeSheetChange();
99
+ activeIndex.value = workbook.value.activeSheetIndex;
100
+ sheetList.value = workbook.value.getSheets();
101
+ bindWorkbookEvents(workbook.value);
102
+ bindSheetEvents(activeSheet.value);
103
+ hooks.rebuildGrid();
104
+ bump();
105
+ });
106
+ onBeforeUnmount(() => {
107
+ offRegistryChange();
108
+ for (const dispose of disposeWorkbookEvents) dispose();
109
+ for (const dispose of disposeSheetEvents) dispose();
110
+ });
111
+ return {
112
+ workbook,
113
+ activeIndex,
114
+ sheetList,
115
+ activeSheet,
116
+ context,
117
+ stateTick,
118
+ syncFromWorkbook
119
+ };
120
+ }
121
+ //#endregion
122
+ export { useSheetState };