@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 type { Sheet } from '@veltra/sheet-core/core/sheet'
2
+ import { Workbook } from '@veltra/sheet-core/core/workbook'
3
+ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
4
+
5
+ import { createSheetContext } from '../tools/context'
6
+ import { defaultToolRegistry } from '../tools/registry'
7
+ import type { SheetProps } from '../types'
8
+
9
+ /**
10
+ * useSheetState 与宿主组件的协作钩子。
11
+ * 弹层关闭 / 网格重建是宿主编排动作(由 use-tool-popup / use-sheet-grid 提供),
12
+ * 以钩子注入避免组合函数之间互相引用。
13
+ */
14
+ export interface SheetStateHooks {
15
+ /** 活动 sheet 或工作簿切换前调用(关闭弹层并提交面板事务) */
16
+ onBeforeSheetChange: () => void
17
+ /** 需要重建网格(structure-change / 工作簿切换 / 导入替换):清缓存 + 重建激活实例 */
18
+ rebuildGrid: () => void
19
+ /** tab 切换:优先复用 LRU 缓存实例(零重建),未命中才新建 */
20
+ activateGrid: () => void
21
+ /** sheet 列表变化(增删)后清理已删除 sheet 的缓存实例 */
22
+ pruneCache?: (sheets: ReadonlyArray<Sheet>) => void
23
+ }
24
+
25
+ type Emit = (event: 'active-sheet-change', payload: { sheet: Sheet; index: number }) => void
26
+
27
+ /**
28
+ * USheet 的状态源(单一数据源,其余状态由此推导):
29
+ * - workbook(外部传入或内部自建单 sheet 工作簿)、sheetList、activeIndex、activeSheet
30
+ * - SheetContext 工具门面(动态解析活动 sheet,tab 切换后自动指向当前 sheet)
31
+ * - stateTick:工具 visible/disabled/active 等纯函数的状态源版本号(bump 触发重算)
32
+ * - 工作簿 / 活动 sheet 的事件绑定(tab 切换、工作簿 prop 变更时重绑)
33
+ */
34
+ export function useSheetState(props: SheetProps, emit: Emit, hooks: SheetStateHooks) {
35
+ /** 工作簿:外部传入或内部自建(单 sheet) */
36
+ const internalWorkbook = new Workbook()
37
+ const workbook = computed(() => props.workbook ?? internalWorkbook)
38
+
39
+ /** 激活 sheet 索引(镜像 workbook.activeSheetIndex,经事件同步) */
40
+ const activeIndex = ref(workbook.value.activeSheetIndex)
41
+ /** sheet 列表(tabs 渲染;由 sheets-change 同步) */
42
+ const sheetList = shallowRef<Sheet[]>(workbook.value.getSheets())
43
+ const activeSheet = computed(
44
+ () => sheetList.value[activeIndex.value] ?? workbook.value.activeSheet
45
+ )
46
+
47
+ /** 工具上下文:动态解析活动 sheet 与当前工作簿,tab 切换 / workbook prop 切换后自动指向最新值 */
48
+ const context = createSheetContext(
49
+ () => activeSheet.value,
50
+ () => workbook.value
51
+ )
52
+
53
+ /** 工具栏状态版本号:visible/disabled/active 是 (ctx) => boolean 纯函数,状态源变化时 bump 触发重算 */
54
+ const stateTick = ref(0)
55
+ const bump = (): void => {
56
+ stateTick.value++
57
+ }
58
+
59
+ let disposeSheetEvents: (() => void)[] = []
60
+ /** 订阅活动 sheet 的状态源(tab 切换 / 工作簿变更时重绑) */
61
+ function bindSheetEvents(sheet: Sheet): void {
62
+ for (const dispose of disposeSheetEvents) dispose()
63
+ // 高频变更(选区移动 / 批量写入逐补丁 cell-change)的 bump 合并到微任务:
64
+ // N 补丁 → 1 次 bump,工具栏 visible/disabled/active 整树重算每帧最多一次(#25)
65
+ let tickScheduled = false
66
+ const scheduleBump = (): void => {
67
+ if (tickScheduled) return
68
+ tickScheduled = true
69
+ queueMicrotask(() => {
70
+ tickScheduled = false
71
+ bump()
72
+ })
73
+ }
74
+ disposeSheetEvents = [
75
+ sheet.on('selection-change', bump),
76
+ sheet.on('history-change', bump),
77
+ sheet.on('cell-change', bump),
78
+ sheet.on('merge-change', bump),
79
+ sheet.on('frozen-change', bump),
80
+ // 整表内容替换(导入 / undo/redo 回放):restore 静默不发 cell-change,
81
+ // 状态源需 bump(grid 层自行订阅 content-reset 全量刷新,此处不重建)
82
+ sheet.on('content-reset', scheduleBump),
83
+ // 行列插入/删除 → 重建网格(渲染行列数 = max(props, sheet.rows/cols),
84
+ // 数据/选区/冻结/行高随重建恢复;低频操作直接重建)
85
+ sheet.on('structure-change', hooks.rebuildGrid)
86
+ ]
87
+ }
88
+
89
+ let disposeWorkbookEvents: (() => void)[] = []
90
+ function bindWorkbookEvents(wb: Workbook): void {
91
+ for (const dispose of disposeWorkbookEvents) dispose()
92
+ disposeWorkbookEvents = [
93
+ wb.on('sheets-change', ({ sheets }) => {
94
+ sheetList.value = sheets
95
+ // 删除的 sheet 不在列表中 → 及时释放其缓存实例(含事件订阅)
96
+ hooks.pruneCache?.(sheets)
97
+ bump()
98
+ }),
99
+ wb.on('sheet-rename', () => {
100
+ // sheet 对象未变(浅引用),换新数组引用触发 tab 文本重渲染
101
+ sheetList.value = workbook.value.getSheets()
102
+ bump()
103
+ }),
104
+ wb.on('active-sheet-change', ({ sheet, index }) => {
105
+ hooks.onBeforeSheetChange() // tab 切换:关闭弹层并提交面板事务
106
+ activeIndex.value = index
107
+ bindSheetEvents(sheet)
108
+ // tab 切换走实例缓存(命中零重建);structure-change / 工作簿替换走 rebuildGrid
109
+ hooks.activateGrid()
110
+ // 删除激活 sheet 的联动:activateGrid 已切到新实例,此刻 prune 可释放
111
+ // 刚被替换的旧激活实例(sheets-change 先于 active-sheet-change 派发,
112
+ // 彼时旧实例仍是 active 被保留)
113
+ hooks.pruneCache?.(workbook.value.getSheets())
114
+ bump()
115
+ emit('active-sheet-change', { sheet, index })
116
+ })
117
+ ]
118
+ }
119
+
120
+ /**
121
+ * 工作簿内容被结构性替换后同步(如导入 xlsx 的 replaceWorkbook):
122
+ * replaceWorkbook 未必触发 active-sheet-change(同 index),需显式同步 tabs、
123
+ * 重绑 sheet 事件并重建网格。
124
+ */
125
+ function syncFromWorkbook(): void {
126
+ sheetList.value = workbook.value.getSheets()
127
+ activeIndex.value = workbook.value.activeSheetIndex
128
+ bindSheetEvents(activeSheet.value)
129
+ hooks.rebuildGrid()
130
+ bump()
131
+ }
132
+
133
+ const offRegistryChange = defaultToolRegistry.onChange(bump)
134
+
135
+ onMounted(() => {
136
+ bindWorkbookEvents(workbook.value)
137
+ bindSheetEvents(activeSheet.value)
138
+ })
139
+
140
+ watch(
141
+ () => props.workbook,
142
+ (wb, prev) => {
143
+ if (wb === prev) return
144
+ hooks.onBeforeSheetChange() // 工作簿切换:关闭弹层并提交面板事务
145
+ activeIndex.value = workbook.value.activeSheetIndex
146
+ sheetList.value = workbook.value.getSheets()
147
+ bindWorkbookEvents(workbook.value)
148
+ bindSheetEvents(activeSheet.value)
149
+ hooks.rebuildGrid()
150
+ bump()
151
+ }
152
+ )
153
+
154
+ onBeforeUnmount(() => {
155
+ offRegistryChange()
156
+ for (const dispose of disposeWorkbookEvents) dispose()
157
+ for (const dispose of disposeSheetEvents) dispose()
158
+ })
159
+
160
+ return { workbook, activeIndex, sheetList, activeSheet, context, stateTick, syncFromWorkbook }
161
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ applyWheelHorizontalScroll,
3
+ computeOverflowNavState,
4
+ scrollElementIntoView,
5
+ scrollViewportByStep
6
+ } from '@veltra/utils'
7
+ import { type Ref, nextTick, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'
8
+
9
+ /** 模板 ref(useTemplateRef / shallowRef 均可) */
10
+ type ElRef = { readonly value: HTMLElement | null | undefined }
11
+
12
+ interface UseSheetTabsBarOptions {
13
+ viewportRef: ElRef
14
+ listRef: ElRef
15
+ /** sheet 列表(长度变化触发导航状态刷新) */
16
+ sheetList: Ref<readonly unknown[]>
17
+ /** 活动 tab 下标(number,非 string key) */
18
+ activeIndex: Ref<number>
19
+ }
20
+
21
+ /**
22
+ * Sheet 底部标签栏溢出滚动(滚动几何/滚轮转横滚/元素滚入视野共享
23
+ * @veltra/utils overflow-nav,见 #17;此处保留事件绑定与 watch 差异):
24
+ * - showNav / canPrev / canNext
25
+ * - 箭头按视口宽度 ~80% 步进
26
+ * - 活动 tab 滚入视野
27
+ * - 纵向滚轮转横滚;触控板横滑不拦截
28
+ * - 原生 ResizeObserver(不引入 @veltra/compositions peer)
29
+ */
30
+ export function useSheetTabsBar(options: UseSheetTabsBarOptions) {
31
+ const { viewportRef, listRef, sheetList, activeIndex } = options
32
+
33
+ const showNav = shallowRef(false)
34
+ const canPrev = shallowRef(false)
35
+ const canNext = shallowRef(false)
36
+
37
+ let resizeObserver: ResizeObserver | undefined
38
+
39
+ const updateNavState = () => {
40
+ const vp = viewportRef.value
41
+ if (!vp) return
42
+ const state = computeOverflowNavState(vp)
43
+ showNav.value = state.overflowing
44
+ canPrev.value = state.canPrev
45
+ canNext.value = state.canNext
46
+ }
47
+
48
+ const scrollByStep = (dir: 1 | -1) => {
49
+ const vp = viewportRef.value
50
+ if (!vp) return
51
+ scrollViewportByStep(vp, dir)
52
+ }
53
+
54
+ const ensureActiveVisible = () => {
55
+ const vp = viewportRef.value
56
+ const list = listRef.value
57
+ if (!vp || !list) return
58
+ const index = activeIndex.value
59
+ if (index < 0) return
60
+ const el = list.children[index] as HTMLElement | undefined
61
+ if (!el) return
62
+ scrollElementIntoView(vp, el)
63
+ }
64
+
65
+ /** 鼠标滚轮(纵向)驱动水平滚动;触控板横滑(已有 deltaX)不拦截 */
66
+ const handleWheel = (e: WheelEvent) => {
67
+ const vp = viewportRef.value
68
+ if (!vp) return
69
+ applyWheelHorizontalScroll(e, vp, showNav.value)
70
+ }
71
+
72
+ watch(activeIndex, async () => {
73
+ await nextTick()
74
+ ensureActiveVisible()
75
+ updateNavState()
76
+ })
77
+
78
+ watch(
79
+ sheetList,
80
+ async () => {
81
+ await nextTick()
82
+ updateNavState()
83
+ ensureActiveVisible()
84
+ },
85
+ { deep: false }
86
+ )
87
+
88
+ onMounted(() => {
89
+ const vp = viewportRef.value
90
+ const list = listRef.value
91
+ if (vp) {
92
+ vp.addEventListener('wheel', handleWheel, { passive: false })
93
+ vp.addEventListener('scroll', updateNavState, { passive: true })
94
+ }
95
+ if (typeof ResizeObserver !== 'undefined') {
96
+ resizeObserver = new ResizeObserver(() => updateNavState())
97
+ if (vp) resizeObserver.observe(vp)
98
+ if (list) resizeObserver.observe(list)
99
+ }
100
+ updateNavState()
101
+ nextTick(ensureActiveVisible)
102
+ })
103
+
104
+ onBeforeUnmount(() => {
105
+ const vp = viewportRef.value
106
+ if (vp) {
107
+ vp.removeEventListener('wheel', handleWheel)
108
+ vp.removeEventListener('scroll', updateNavState)
109
+ }
110
+ resizeObserver?.disconnect()
111
+ resizeObserver = undefined
112
+ })
113
+
114
+ return { showNav, canPrev, canNext, scrollByStep }
115
+ }
@@ -0,0 +1,61 @@
1
+ import { computed, type ComputedRef, type Ref } from 'vue'
2
+
3
+ import type { SheetContext } from '../tools/context'
4
+ import { defaultToolRegistry, type SheetTool } from '../tools/registry'
5
+
6
+ /** 工具栏按钮视图模型 */
7
+ export interface ToolGroupItem {
8
+ tool: SheetTool
9
+ disabled: boolean
10
+ active: boolean
11
+ }
12
+
13
+ /** 工具栏分组视图模型(组间渲染分隔符) */
14
+ export interface ToolGroupView {
15
+ name: string
16
+ tools: ToolGroupItem[]
17
+ }
18
+
19
+ /**
20
+ * 内置组稳定顺序:history | cell | text | edit | insert | file。
21
+ * 未列出的自定义组按注册先后排在已知组之后。
22
+ */
23
+ const BUILTIN_GROUP_ORDER = ['history', 'cell', 'text', 'edit', 'insert', 'file'] as const
24
+
25
+ /**
26
+ * 工具栏分组视图模型:工具的 visible/disabled/active 是 (ctx) => boolean 纯函数,
27
+ * 依赖 stateTick 版本号(选区 / 历史 / 单元格 / 合并 / 冻结 / 注册表变化时 bump)触发重算。
28
+ */
29
+ export function useToolGroups(
30
+ context: SheetContext,
31
+ stateTick: Ref<number>
32
+ ): ComputedRef<ToolGroupView[]> {
33
+ return computed(() => {
34
+ void stateTick.value // 依赖状态源:选区 / 历史 / 单元格 / 合并 / 冻结 / 注册表
35
+ const groups = defaultToolRegistry
36
+ .getGroups()
37
+ .map((group) => ({
38
+ name: group.name,
39
+ tools: group.tools
40
+ .map((tool) => ({
41
+ tool,
42
+ visible: tool.visible?.(context) ?? true,
43
+ disabled: tool.disabled?.(context) ?? false,
44
+ active: tool.active?.(context) ?? false
45
+ }))
46
+ .filter((item) => item.visible)
47
+ }))
48
+ .filter((group) => group.tools.length > 0)
49
+
50
+ const orderIndex = new Map<string, number>(
51
+ BUILTIN_GROUP_ORDER.map((name, index) => [name, index])
52
+ )
53
+ return groups.sort((a, b) => {
54
+ const ai = orderIndex.get(a.name) ?? BUILTIN_GROUP_ORDER.length
55
+ const bi = orderIndex.get(b.name) ?? BUILTIN_GROUP_ORDER.length
56
+ if (ai !== bi) return ai - bi
57
+ // 未知组之间保持 getGroups() 注册序(已稳定)
58
+ return 0
59
+ })
60
+ })
61
+ }
@@ -0,0 +1,150 @@
1
+ import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
2
+
3
+ import type { SheetContext } from '../tools/context'
4
+ import { defaultToolRegistry, type SheetTool } from '../tools/registry'
5
+
6
+ /** 模板 ref 只读形态 */
7
+ type ElRef = { readonly value: HTMLElement | null | undefined }
8
+
9
+ /**
10
+ * 面板打开期间的写入是否合并为一个 undo 单元(关闭时提交):
11
+ * 填充颜色 / 边框 / 字体颜色 / 字号参与事务;查找(每次替换独立 undo)、
12
+ * 导出(下载侧效应,无模型写入)不参与。导入无弹层,不经此路径。
13
+ */
14
+ function joinsTransaction(tool: SheetTool): boolean {
15
+ return (
16
+ tool.popup === 'fill-color' ||
17
+ tool.popup === 'border' ||
18
+ tool.popup === 'font-color' ||
19
+ tool.popup === 'font-size'
20
+ )
21
+ }
22
+
23
+ /**
24
+ * 弹层型工具编排(填充颜色 / 边框 / 查找 / 插入图片 / 导出):
25
+ * - popupTool:当前打开的弹层工具(null = 未打开)
26
+ * - popupAnchor:触发按钮元素(面板定位参照,见 sheet.vue 的 left 计算)
27
+ * - 打开 / 关闭时的事务包裹(面板期间写入合并为一个 undo 单元,关闭时提交)
28
+ * - 点击面板外关闭(面板内 @click.stop 不冒泡到 window)
29
+ * - Ctrl/Cmd+F 开合查找条(与工具按钮同一 toggle 逻辑)
30
+ * 导入在 sheet.vue 特殊处理(直接系统文件选择),不经弹层。
31
+ */
32
+ export function useToolPopup(context: SheetContext, rootEl: ElRef) {
33
+ /** 当前打开的弹层工具(null = 未打开) */
34
+ const popupTool = shallowRef<SheetTool | null>(null)
35
+ /** 触发按钮元素(打开时的 currentTarget;用于面板 left 对齐按钮) */
36
+ const popupAnchor = shallowRef<HTMLElement | null>(null)
37
+ /** 延迟打开定时器(#23:组件卸载后不再执行 openPopup) */
38
+ let openTimer: ReturnType<typeof setTimeout> | null = null
39
+
40
+ function openPopup(tool: SheetTool, anchor?: HTMLElement | null): void {
41
+ closePopup()
42
+ popupTool.value = tool
43
+ popupAnchor.value = anchor ?? null
44
+ // 面板打开期间的所有写入合并为一个 undo 单元(关闭时提交)
45
+ if (joinsTransaction(tool)) context.beginTransaction()
46
+ }
47
+
48
+ /**
49
+ * 延迟到本次点击事件流结束后打开(setTimeout 宏任务,避开冒泡到 window 时
50
+ * onWindowClick 误关——queueMicrotask 会在事件传播中途的 microtask checkpoint
51
+ * 执行,面板会被同一 click 关闭)。保存 timer id,卸载/提前关闭时取消。
52
+ */
53
+ function scheduleOpen(tool: SheetTool, anchor?: HTMLElement | null): void {
54
+ if (openTimer !== null) clearTimeout(openTimer)
55
+ openTimer = setTimeout(() => {
56
+ openTimer = null
57
+ openPopup(tool, anchor)
58
+ }, 0)
59
+ }
60
+
61
+ /** 关闭弹层并提交面板期间的事务(无写入则空事务,不入历史) */
62
+ function closePopup(): void {
63
+ if (openTimer !== null) {
64
+ clearTimeout(openTimer)
65
+ openTimer = null
66
+ }
67
+ const tool = popupTool.value
68
+ if (!tool) return
69
+ popupTool.value = null
70
+ popupAnchor.value = null
71
+ if (!joinsTransaction(tool)) return
72
+ try {
73
+ context.commit()
74
+ } catch {
75
+ context.rollback()
76
+ }
77
+ }
78
+
79
+ function handleToolClick(tool: SheetTool, event?: MouseEvent): void {
80
+ if (tool.disabled?.(context)) return
81
+ if (tool.popup) {
82
+ // 弹层工具:同 id 再点 = 关闭;否则延迟打开(见 scheduleOpen)
83
+ if (popupTool.value?.id === tool.id) {
84
+ closePopup()
85
+ return
86
+ }
87
+ const anchor =
88
+ event?.currentTarget instanceof HTMLElement ? (event.currentTarget as HTMLElement) : null
89
+ scheduleOpen(tool, anchor)
90
+ return
91
+ }
92
+ tool.onClick(context)
93
+ }
94
+
95
+ /**
96
+ * 点击面板外任意处关闭。面板内部 @click.stop 不冒泡到 window;
97
+ * 触发按钮本身的点击由 handleToolClick 同步处理(打开/关闭 toggle),
98
+ * 冒泡到达这里时 popupTool 已更新,不会误关。
99
+ */
100
+ function onWindowClick(): void {
101
+ if (!popupTool.value) return
102
+ closePopup()
103
+ }
104
+
105
+ /** Ctrl/Cmd+F 打开 / 关闭查找条(与工具按钮同一 toggle 逻辑) */
106
+ function onGlobalKeydown(event: KeyboardEvent): void {
107
+ if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'f') return
108
+ // 仅当焦点/事件源落在本实例容器内才响应(#6):容器外不得 preventDefault,
109
+ // 否则劫持浏览器原生查找(页面任意位置按 Ctrl+F 都被屏蔽);同页多实例
110
+ // 也各只响应自己容器内的按键,不再每个实例都弹出查找条
111
+ const root = rootEl.value
112
+ if (root) {
113
+ const target = event.target instanceof Node ? event.target : null
114
+ const active = document.activeElement instanceof Node ? document.activeElement : null
115
+ const inside =
116
+ (target !== null && root.contains(target)) || (active !== null && root.contains(active))
117
+ // 例外:弹层 Teleport 到 body 级 #pop-container(不在 root 内),本实例弹层
118
+ // 打开期间焦点在弹层输入框里(如查找条内再按 Ctrl+F)——同样视为本实例的
119
+ // 按键,否则查找条无法 toggle 关闭,且放行浏览器原生查找盖在上面。
120
+ // (弹层打开期间点击容器外任意处会先经 onWindowClick 关闭弹层,故
121
+ // popupTool 非空时焦点必然属于本实例,多实例不会互相抢占)
122
+ if (!inside && popupTool.value === null) return
123
+ }
124
+ event.preventDefault()
125
+ const findTool = defaultToolRegistry.get('find')
126
+ if (!findTool) return
127
+ if (popupTool.value?.id === findTool.id) {
128
+ closePopup()
129
+ return
130
+ }
131
+ scheduleOpen(findTool)
132
+ }
133
+
134
+ onMounted(() => {
135
+ window.addEventListener('click', onWindowClick)
136
+ window.addEventListener('keydown', onGlobalKeydown)
137
+ })
138
+
139
+ onBeforeUnmount(() => {
140
+ if (openTimer !== null) {
141
+ clearTimeout(openTimer)
142
+ openTimer = null
143
+ }
144
+ closePopup()
145
+ window.removeEventListener('click', onWindowClick)
146
+ window.removeEventListener('keydown', onGlobalKeydown)
147
+ })
148
+
149
+ return { popupTool, popupAnchor, closePopup, handleToolClick }
150
+ }
@@ -0,0 +1,95 @@
1
+ import {
2
+ applyWheelHorizontalScroll,
3
+ computeOverflowNavState,
4
+ scrollViewportByStep
5
+ } from '@veltra/utils'
6
+ import { onBeforeUnmount, onMounted, shallowRef, watch, type Ref } from 'vue'
7
+
8
+ /** 模板 ref(useTemplateRef / shallowRef 均可) */
9
+ type ElRef = { readonly value: HTMLElement | null | undefined }
10
+
11
+ interface UseToolbarScrollOptions {
12
+ /** 单行滚动视口(overflow-x: auto) */
13
+ viewportRef: ElRef
14
+ /** 工具列表容器(宽度随内容增长) */
15
+ listRef: ElRef
16
+ /** 内容源(工具组变化触发导航状态刷新) */
17
+ content: Ref<readonly unknown[]>
18
+ }
19
+
20
+ /**
21
+ * 工具栏溢出滚动(滚动几何/滚轮转横滚共享 @veltra/utils overflow-nav,见 #17;
22
+ * 无活动项跟随):
23
+ * - 内容超出视口宽度时显示左右箭头(showNav / canPrev / canNext)
24
+ * - 箭头按视口宽度 ~80% 步进
25
+ * - 纵向滚轮转横滚;触控板横滑不拦截
26
+ * - 原生 ResizeObserver(不引入 @veltra/compositions peer)
27
+ */
28
+ export function useToolbarScroll(options: UseToolbarScrollOptions) {
29
+ const { viewportRef, listRef, content } = options
30
+
31
+ const showNav = shallowRef(false)
32
+ const canPrev = shallowRef(false)
33
+ const canNext = shallowRef(false)
34
+
35
+ let resizeObserver: ResizeObserver | undefined
36
+
37
+ const updateNavState = () => {
38
+ const vp = viewportRef.value
39
+ if (!vp) return
40
+ const state = computeOverflowNavState(vp)
41
+ showNav.value = state.overflowing
42
+ canPrev.value = state.canPrev
43
+ canNext.value = state.canNext
44
+ }
45
+
46
+ const scrollByStep = (dir: 1 | -1) => {
47
+ const vp = viewportRef.value
48
+ if (!vp) return
49
+ scrollViewportByStep(vp, dir)
50
+ }
51
+
52
+ /** 鼠标滚轮(纵向)驱动水平滚动;触控板横滑(已有 deltaX)不拦截 */
53
+ const handleWheel = (e: WheelEvent) => {
54
+ const vp = viewportRef.value
55
+ if (!vp) return
56
+ applyWheelHorizontalScroll(e, vp, showNav.value)
57
+ }
58
+
59
+ // 工具显隐 / 分组变化后宽度改变 → 刷新导航状态(等 DOM 更新完成再测)
60
+ watch(
61
+ content,
62
+ async () => {
63
+ await Promise.resolve()
64
+ updateNavState()
65
+ },
66
+ { deep: false }
67
+ )
68
+
69
+ onMounted(() => {
70
+ const vp = viewportRef.value
71
+ if (vp) {
72
+ vp.addEventListener('wheel', handleWheel, { passive: false })
73
+ vp.addEventListener('scroll', updateNavState, { passive: true })
74
+ }
75
+ if (typeof ResizeObserver !== 'undefined') {
76
+ resizeObserver = new ResizeObserver(() => updateNavState())
77
+ if (vp) resizeObserver.observe(vp)
78
+ const list = listRef.value
79
+ if (list) resizeObserver.observe(list)
80
+ }
81
+ updateNavState()
82
+ })
83
+
84
+ onBeforeUnmount(() => {
85
+ const vp = viewportRef.value
86
+ if (vp) {
87
+ vp.removeEventListener('wheel', handleWheel)
88
+ vp.removeEventListener('scroll', updateNavState)
89
+ }
90
+ resizeObserver?.disconnect()
91
+ resizeObserver = undefined
92
+ })
93
+
94
+ return { showNav, canPrev, canNext, scrollByStep }
95
+ }
@@ -1,60 +0,0 @@
1
- //#region src/core/address.d.ts
2
- /**
3
- * A1 地址系统(纯函数)。
4
- *
5
- * 坐标统一 0-based:`{ row: 0, col: 0 }` 即 A1。
6
- * 区域为闭区间,且经过规范化:start 恒为左上角,end 恒为右下角。
7
- */
8
- /** 单元格地址(0-based) */
9
- interface CellAddress {
10
- row: number;
11
- col: number;
12
- }
13
- /** 单元格区域(闭区间,start 左上角,end 右下角) */
14
- interface CellRange {
15
- start: CellAddress;
16
- end: CellAddress;
17
- }
18
- /**
19
- * cellKey 的列基数(2^20)。
20
- * key = row * COL_KEY_BASE + col,要求 col < COL_KEY_BASE;
21
- * JS 安全整数范围内可容纳约 8.5e9 行,远超实际使用。
22
- */
23
- declare const COL_KEY_BASE: number;
24
- /** 地址 → 数值 key(Map 索引用) */
25
- declare function cellKey(addr: CellAddress): number;
26
- /** 数值 key → 地址 */
27
- declare function addressFromKey(key: number): CellAddress;
28
- /** 列号(0-based)→ 列名:0 → 'A',25 → 'Z',26 → 'AA' */
29
- declare function colIndexToName(col: number): string;
30
- /** 列名 → 列号(0-based):'A' → 0,'AA' → 26;非法列名返回 -1 */
31
- declare function colNameToIndex(name: string): number;
32
- /** 解析 A1 记法(兼容 `$A$1` 绝对引用写法)→ 地址;非法返回 null */
33
- declare function parseAddress(text: string): CellAddress | null;
34
- /** 地址 → A1 记法:{ row: 1, col: 1 } → 'B2' */
35
- declare function formatAddress(addr: CellAddress): string;
36
- /** 由两个角点构造规范化区域(start ≤ end) */
37
- declare function createRange(a: CellAddress, b: CellAddress): CellRange;
38
- /** 解析区域记法:'B2' 或 'B2:D5'(兼容绝对引用);非法返回 null */
39
- declare function parseRange(text: string): CellRange | null;
40
- /** 区域 → 记法:单格 → 'B2',多格 → 'B2:D5' */
41
- declare function formatRange(range: CellRange): string;
42
- /** 地址相等 */
43
- declare function addressesEqual(a: CellAddress, b: CellAddress): boolean;
44
- /** 区域相等 */
45
- declare function rangesEqual(a: CellRange, b: CellRange): boolean;
46
- /** 两区域是否相交 */
47
- declare function rangesIntersect(a: CellRange, b: CellRange): boolean;
48
- /** 区域是否包含地址 */
49
- declare function rangeContainsAddress(range: CellRange, addr: CellAddress): boolean;
50
- /** 区域是否完整包含另一区域 */
51
- declare function rangeContainsRange(outer: CellRange, inner: CellRange): boolean;
52
- /** 一组区域的最小包围盒(至少传入一个区域) */
53
- declare function boundingBox(ranges: readonly [CellRange, ...CellRange[]]): CellRange;
54
- /** 按行主序遍历区域内的所有地址 */
55
- declare function iterateRange(range: CellRange): Generator<CellAddress, void, undefined>;
56
- /** 区域内的单元格数量 */
57
- declare function rangeSize(range: CellRange): number;
58
- //#endregion
59
- export { COL_KEY_BASE, CellAddress, CellRange, addressFromKey, addressesEqual, boundingBox, cellKey, colIndexToName, colNameToIndex, createRange, formatAddress, formatRange, iterateRange, parseAddress, parseRange, rangeContainsAddress, rangeContainsRange, rangeSize, rangesEqual, rangesIntersect };
60
- //# sourceMappingURL=address.d.ts.map