@veltra/sheet 2.5.6 → 2.5.8

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 (59) hide show
  1. package/dist/components/sheet/formula-bar.js +115 -15
  2. package/dist/components/sheet/import-file.js +1 -1
  3. package/dist/components/sheet/popup-helpers.js +1 -1
  4. package/dist/components/sheet/popups/border-popup.js +2 -2
  5. package/dist/components/sheet/popups/functions-popup.js +117 -0
  6. package/dist/components/sheet/popups/import.worker.d.ts +3 -4
  7. package/dist/components/sheet/popups/import.worker.js +1 -1
  8. package/dist/components/sheet/sheet-context-menu.js +1 -1
  9. package/dist/components/sheet/sheet.js +33 -23
  10. package/dist/components/sheet/style2.css +100 -0
  11. package/dist/components/sheet/use-find-replace.js +2 -2
  12. package/dist/components/sheet/use-formula-suggest.js +18 -2
  13. package/dist/components/sheet/use-sheet-grid.js +1 -1
  14. package/dist/components/sheet/use-sheet-state.js +1 -1
  15. package/dist/components/sheet/use-tool-popup.js +4 -2
  16. package/dist/tools/builtin.js +12 -2
  17. package/dist/tools/context.d.ts +15 -16
  18. package/dist/tools/download.js +1 -1
  19. package/dist/tools/export.worker.d.ts +4 -5
  20. package/dist/tools/export.worker.js +1 -1
  21. package/dist/tools/registry.d.ts +8 -9
  22. package/dist/tools/registry.js +1 -1
  23. package/dist/types/sheet.d.ts +7 -8
  24. package/package.json +5 -5
  25. package/src/components/sheet/__test__/formula-bar.test.ts +314 -2
  26. package/src/components/sheet/__test__/functions-popup.test.ts +182 -0
  27. package/src/components/sheet/__test__/import-file.test.ts +3 -3
  28. package/src/components/sheet/__test__/insert-image-popup.test.ts +1 -1
  29. package/src/components/sheet/__test__/insert-image.test.ts +1 -1
  30. package/src/components/sheet/__test__/sheet-component.test.ts +2 -1
  31. package/src/components/sheet/__test__/sheet-context-menu.test.ts +2 -2
  32. package/src/components/sheet/__test__/use-formula-suggest.test.ts +5 -4
  33. package/src/components/sheet/formula-bar.vue +134 -9
  34. package/src/components/sheet/import-file.ts +3 -3
  35. package/src/components/sheet/insert-image.ts +1 -1
  36. package/src/components/sheet/popup-helpers.ts +3 -3
  37. package/src/components/sheet/popups/border-popup.vue +3 -3
  38. package/src/components/sheet/popups/find-popup.vue +1 -1
  39. package/src/components/sheet/popups/functions-popup.vue +141 -0
  40. package/src/components/sheet/popups/import.worker.ts +2 -2
  41. package/src/components/sheet/sheet-context-menu.ts +2 -2
  42. package/src/components/sheet/sheet-tabs.vue +2 -2
  43. package/src/components/sheet/sheet.vue +15 -2
  44. package/src/components/sheet/style.scss +117 -0
  45. package/src/components/sheet/use-find-replace.ts +3 -3
  46. package/src/components/sheet/use-formula-suggest.ts +20 -1
  47. package/src/components/sheet/use-sheet-grid.ts +12 -4
  48. package/src/components/sheet/use-sheet-state.ts +2 -2
  49. package/src/components/sheet/use-tool-popup.ts +4 -2
  50. package/src/tools/__test__/apply-style.test.ts +2 -2
  51. package/src/tools/__test__/builtin.test.ts +12 -4
  52. package/src/tools/__test__/context.test.ts +2 -2
  53. package/src/tools/apply-style.ts +3 -3
  54. package/src/tools/builtin.ts +15 -3
  55. package/src/tools/context.ts +11 -11
  56. package/src/tools/download.ts +2 -2
  57. package/src/tools/export.worker.ts +3 -3
  58. package/src/tools/registry.ts +2 -1
  59. package/src/types/sheet.ts +2 -2
@@ -0,0 +1,182 @@
1
+ import {
2
+ listFormulaFunctions,
3
+ registerFormulaFunction
4
+ } from '@veltra/sheet-core/core/formula/functions.js'
5
+ import { afterEach, describe, expect, it, vi } from 'vitest'
6
+ import { createApp, h, nextTick, type App } from 'vue'
7
+
8
+ import USheetFunctionsPopup from '../popups/functions-popup.vue'
9
+ import { COMMON_FORMULA_NAMES, FUNCTION_POPUP_CATEGORIES } from '../use-formula-suggest'
10
+
11
+ vi.mock('@veltra/desktop', async () => {
12
+ const { defineComponent, h } = await import('vue')
13
+ return {
14
+ UInput: defineComponent({
15
+ name: 'UInputStub',
16
+ props: ['modelValue'],
17
+ emits: ['update:modelValue'],
18
+ setup(props, { emit }) {
19
+ return () =>
20
+ h('input', {
21
+ class: 'search-stub',
22
+ value: props.modelValue as string,
23
+ onInput: (e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value)
24
+ })
25
+ }
26
+ }),
27
+ UScroll: defineComponent({
28
+ name: 'UScrollStub',
29
+ setup(_, { slots }) {
30
+ return () => h('div', { class: 'scroll-stub' }, slots.default?.())
31
+ }
32
+ })
33
+ }
34
+ })
35
+
36
+ const apps: App[] = []
37
+ const containers: HTMLElement[] = []
38
+
39
+ function mountPopup(onSelect?: (name: string) => void) {
40
+ const el = document.createElement('div')
41
+ document.body.appendChild(el)
42
+ containers.push(el)
43
+ const app = createApp({
44
+ setup() {
45
+ return () => h(USheetFunctionsPopup, onSelect ? { onSelect } : undefined)
46
+ }
47
+ })
48
+ app.mount(el)
49
+ apps.push(app)
50
+ return { el }
51
+ }
52
+
53
+ async function typeKeyword(el: HTMLElement, keyword: string): Promise<void> {
54
+ const input = el.querySelector<HTMLInputElement>('.search-stub')!
55
+ input.value = keyword
56
+ input.dispatchEvent(new Event('input'))
57
+ await Promise.resolve()
58
+ }
59
+
60
+ /** 点击左侧分类导航项(等一帧渲染) */
61
+ async function clickCategory(el: HTMLElement, category: string): Promise<void> {
62
+ const item = [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__functions-nav-item')].find(
63
+ (button) => button.textContent?.trim() === category
64
+ )
65
+ item!.click()
66
+ await nextTick()
67
+ }
68
+
69
+ function signatures(el: HTMLElement): (string | null)[] {
70
+ return [...el.querySelectorAll('.u-sheet__functions-signature')].map((node) => node.textContent)
71
+ }
72
+
73
+ function items(el: HTMLElement): HTMLElement[] {
74
+ return [...el.querySelectorAll<HTMLElement>('.u-sheet__functions-item')]
75
+ }
76
+
77
+ afterEach(() => {
78
+ apps.splice(0).forEach((app) => app.unmount())
79
+ containers.splice(0).forEach((el) => el.remove())
80
+ vi.clearAllMocks()
81
+ })
82
+
83
+ describe('USheetFunctionsPopup', () => {
84
+ it('分类导航渲染固定集合(顺序一致);默认「常用」列全部常用函数', () => {
85
+ const { el } = mountPopup()
86
+ const navLabels = [...el.querySelectorAll('.u-sheet__functions-nav-item')].map(
87
+ (node) => node.textContent
88
+ )
89
+ expect(navLabels).toEqual([...FUNCTION_POPUP_CATEGORIES])
90
+
91
+ // 「常用」= 前端固定清单(COMMON_FORMULA_NAMES 顺序),全部内置注册 → 一一列出
92
+ expect(signatures(el).map((sig) => sig?.split('(')[0])).toEqual([...COMMON_FORMULA_NAMES])
93
+ expect(el.querySelector('.u-sheet__functions-nav-item')!.classList.contains('is-active')).toBe(
94
+ true
95
+ )
96
+ })
97
+
98
+ it('「全部」列出全部已注册函数;分类项只列声明该分类的函数', async () => {
99
+ registerFormulaFunction('MYTESTFN', {
100
+ meta: { params: ['value'], description: '测试未分类自定义函数' },
101
+ impl: () => 1
102
+ })
103
+ const { el } = mountPopup()
104
+
105
+ await clickCategory(el, '全部')
106
+ expect(items(el).length).toBe(listFormulaFunctions().length)
107
+ // 未分类函数仅出现在「全部」
108
+ expect(signatures(el)).toContain('MYTESTFN(value)')
109
+
110
+ await clickCategory(el, '数学')
111
+ const expected = listFormulaFunctions().filter((fn) => fn.category === '数学')
112
+ expect(items(el).length).toBe(expected.length)
113
+ expect(signatures(el)).toContain('SUM(number1, number2, ...)')
114
+ expect(signatures(el)).not.toContain('MYTESTFN(value)')
115
+
116
+ // 「常用」也不含未分类自定义函数
117
+ await clickCategory(el, '常用')
118
+ expect(signatures(el)).not.toContain('MYTESTFN(value)')
119
+ })
120
+
121
+ it('搜索跨分类(含未分类函数):名称 / 描述大小写不敏感;无匹配显空态', async () => {
122
+ registerFormulaFunction('MYTESTFN', {
123
+ meta: { params: ['value'], description: '测试未分类自定义函数' },
124
+ impl: () => 1
125
+ })
126
+ const { el } = mountPopup()
127
+ await clickCategory(el, '数学')
128
+
129
+ // 在「数学」分类下搜索未分类函数名 → 仍可命中(跨分类)
130
+ await typeKeyword(el, 'mytest')
131
+ let names = signatures(el).map((sig) => sig?.split('(')[0])
132
+ expect(names).toEqual(['MYTESTFN'])
133
+
134
+ // 中文描述过滤(SUM:求参数之和)
135
+ await typeKeyword(el, '之和')
136
+ names = signatures(el).map((sig) => sig?.split('(')[0])
137
+ expect(names).toEqual(['SUM'])
138
+
139
+ // 无匹配 → 空态
140
+ await typeKeyword(el, '不存在的关键字zzz')
141
+ expect(el.querySelectorAll('.u-sheet__functions-item').length).toBe(0)
142
+ expect(el.querySelector('.u-sheet__functions-empty')?.textContent).toContain('无匹配函数')
143
+
144
+ // 清空关键词 → 回到当前分类(数学)
145
+ await typeKeyword(el, '')
146
+ expect(items(el).length).toBe(
147
+ listFormulaFunctions().filter((fn) => fn.category === '数学').length
148
+ )
149
+ })
150
+
151
+ it('点击函数项 → emit select(name)', () => {
152
+ const selected: string[] = []
153
+ const { el } = mountPopup((name) => selected.push(name))
154
+
155
+ items(el)[0]!.click()
156
+ expect(selected).toEqual(['SUM'])
157
+ })
158
+
159
+ it('键盘路径:↑↓ 移动高亮(循环),Enter 确认选中', async () => {
160
+ const selected: string[] = []
161
+ const { el } = mountPopup((name) => selected.push(name))
162
+ const panel = el.querySelector<HTMLElement>('.u-sheet__functions-panel')!
163
+
164
+ // 初始高亮第一项(SUM)
165
+ expect(items(el)[0]!.classList.contains('is-active')).toBe(true)
166
+
167
+ panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
168
+ await nextTick()
169
+ expect(items(el)[1]!.classList.contains('is-active')).toBe(true)
170
+
171
+ panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
172
+ expect(selected).toEqual(['AVERAGE'])
173
+
174
+ // ↑ 回到第一项,再 ↑ 循环到最后一项
175
+ panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
176
+ await nextTick()
177
+ expect(items(el)[0]!.classList.contains('is-active')).toBe(true)
178
+ panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
179
+ await nextTick()
180
+ expect(items(el).at(-1)!.classList.contains('is-active')).toBe(true)
181
+ })
182
+ })
@@ -1,5 +1,5 @@
1
- import type { Sheet } from '@veltra/sheet-core/core/sheet'
2
- import { Workbook } from '@veltra/sheet-core/core/workbook'
1
+ import type { Sheet } from '@veltra/sheet-core/core/sheet.js'
2
+ import { Workbook } from '@veltra/sheet-core/core/workbook.js'
3
3
  import { afterEach, describe, expect, it, vi } from 'vitest'
4
4
  import { ref } from 'vue'
5
5
 
@@ -36,7 +36,7 @@ const EMPTY_SNAPSHOT = {
36
36
 
37
37
  vi.mock('@veltra/desktop', () => ({ message: mocks.message, messageConfirm: mocks.messageConfirm }))
38
38
 
39
- vi.mock('@veltra/sheet-core/core/io/import', () => ({
39
+ vi.mock('@veltra/sheet-core/core/io/import.js', () => ({
40
40
  importCsv: mocks.importCsv,
41
41
  importXlsx: mocks.importXlsx,
42
42
  replaceWorkbookWithSnapshots: mocks.replaceWorkbookWithSnapshots
@@ -1,4 +1,4 @@
1
- import { Sheet } from '@veltra/sheet-core/core/sheet'
1
+ import { Sheet } from '@veltra/sheet-core/core/sheet.js'
2
2
  import { afterEach, describe, expect, it, vi } from 'vitest'
3
3
  import { createApp, h, type App } from 'vue'
4
4
 
@@ -1,4 +1,4 @@
1
- import { Sheet } from '@veltra/sheet-core/core/sheet'
1
+ import { Sheet } from '@veltra/sheet-core/core/sheet.js'
2
2
  import { afterEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  import { createSheetContext } from '../../../tools/context'
@@ -1,4 +1,4 @@
1
- import { Workbook } from '@veltra/sheet-core/core/workbook'
1
+ import { Workbook } from '@veltra/sheet-core/core/workbook.js'
2
2
  import { afterEach, describe, expect, it, vi } from 'vitest'
3
3
  import { createApp, h, nextTick, ref, type App } from 'vue'
4
4
 
@@ -120,6 +120,7 @@ describe('USheet 组件', () => {
120
120
  'valign-bottom',
121
121
  'wrap-text',
122
122
  'find',
123
+ 'functions',
123
124
  'insert-image',
124
125
  'import',
125
126
  'export'
@@ -1,5 +1,5 @@
1
- import { createRange } from '@veltra/sheet-core/core/address'
2
- import { Sheet } from '@veltra/sheet-core/core/sheet'
1
+ import { createRange } from '@veltra/sheet-core/core/address.js'
2
+ import { Sheet } from '@veltra/sheet-core/core/sheet.js'
3
3
  import { describe, expect, it } from 'vitest'
4
4
 
5
5
  import { createSheetContext } from '../../../tools/context'
@@ -1,4 +1,4 @@
1
- import { registerFormulaFunction } from '@veltra/sheet-core/core/formula/functions'
1
+ import { registerFormulaFunction } from '@veltra/sheet-core/core/formula/functions.js'
2
2
  import { describe, expect, it } from 'vitest'
3
3
 
4
4
  import {
@@ -46,15 +46,16 @@ describe('getSuggestContext', () => {
46
46
  })
47
47
 
48
48
  describe('filterFormulaSuggestions / applySuggest / moveSuggestIndex', () => {
49
- it('空前缀优先常用函数(含 SUM),至多 10 条;SU 过滤到 SUM', () => {
49
+ it('空前缀优先常用函数(含 SUM),至多 10 条;SU 过滤到 SUBSTITUTE / SUM(名称升序)', () => {
50
50
  const all = filterFormulaSuggestions('')
51
51
  expect(all.length).toBeGreaterThan(0)
52
52
  expect(all.length).toBeLessThanOrEqual(FORMULA_SUGGEST_LIMIT)
53
53
  expect(all[0]!.name).toBe('SUM')
54
54
  expect(all.map((s) => s.name)).toContain('SUM')
55
55
  const su = filterFormulaSuggestions('SU')
56
- expect(su.map((s) => s.name)).toEqual(['SUM'])
57
- expect(su[0]!.signature).toBe(formatFunctionSignature('SUM', su[0]!.params))
56
+ expect(su.map((s) => s.name)).toEqual(['SUBSTITUTE', 'SUM'])
57
+ const sum = su.find((s) => s.name === 'SUM')!
58
+ expect(sum.signature).toBe(formatFunctionSignature('SUM', sum.params))
58
59
  })
59
60
 
60
61
  it('无 meta 函数签名仅为名称', () => {
@@ -10,7 +10,33 @@
10
10
  @blur="handleNameBlur"
11
11
  @keydown="handleNameKeydown"
12
12
  />
13
- <span :class="cls.e('fx-label')">fx</span>
13
+ <button
14
+ ref="fxLabelRef"
15
+ type="button"
16
+ :class="cls.e('fx-label')"
17
+ :disabled="fxDisabled"
18
+ title="插入函数"
19
+ @mousedown.prevent
20
+ @click.stop="toggleFunctions"
21
+ >
22
+ fx
23
+ </button>
24
+ <!-- 函数弹框:锚定 fx 按钮下方(UDropdown Teleport 到 #pop-container,
25
+ 同工具栏弹层机制);打开 / 关闭不触碰选区与草稿 -->
26
+ <u-dropdown
27
+ ref="functionsDropdownRef"
28
+ trigger="custom"
29
+ :visible="functionsOpen"
30
+ width="auto"
31
+ @keydown="handleFunctionsKeydown"
32
+ @update:visible="syncFunctionsOpen"
33
+ >
34
+ <template #content>
35
+ <div v-if="functionsOpen" :class="cls.e('functions-popover')" @click.stop>
36
+ <u-sheet-functions-popup @select="handleFunctionSelect" />
37
+ </div>
38
+ </template>
39
+ </u-dropdown>
14
40
  <div
15
41
  :class="cls.e('fx-editor')"
16
42
  @mouseenter="handleFxMouseEnter"
@@ -68,16 +94,16 @@
68
94
  </template>
69
95
 
70
96
  <script lang="ts" setup>
71
- import { message } from '@veltra/desktop'
97
+ import { message, UDropdown } from '@veltra/desktop'
72
98
  import {
73
99
  formatAddress,
74
100
  formatRange,
75
101
  parseRange,
76
102
  type CellAddress,
77
103
  type CellRange
78
- } from '@veltra/sheet-core/core/address'
79
- import type { SelectionState } from '@veltra/sheet-core/core/selection'
80
- import type { Sheet } from '@veltra/sheet-core/core/sheet'
104
+ } from '@veltra/sheet-core/core/address.js'
105
+ import type { SelectionState } from '@veltra/sheet-core/core/selection.js'
106
+ import type { Sheet } from '@veltra/sheet-core/core/sheet.js'
81
107
  import { bem } from '@veltra/utils'
82
108
  import {
83
109
  computed,
@@ -92,6 +118,7 @@ import {
92
118
 
93
119
  import type { SheetContext } from '../../tools/context'
94
120
  import UFormulaSuggestList from './formula-suggest-list.vue'
121
+ import USheetFunctionsPopup from './popups/functions-popup.vue'
95
122
  import { insertRefText, isRefSelectContext } from './use-formula-ref-select'
96
123
  import {
97
124
  applySuggest,
@@ -107,6 +134,7 @@ defineOptions({ name: 'UFormulaBar' })
107
134
  * 公式栏(名称框 + fx 输入栏),USheet 顶部结构之一。
108
135
  *
109
136
  * - 名称框:显示当前选区(单格 A1 / 区域 A1:B2);合法地址回车跳转。
137
+ * - fx 按钮:打开函数弹框(分类导航 + 搜索),选中进入编辑态输出 `=NAME()`。
110
138
  * - fx 输入栏:活动格内容;Enter/✓ 提交、Esc/✗ 取消、失焦提交。
111
139
  * - 函数补全:`=` 开头且光标在函数名 token 上时弹出候选(↑↓ / Tab / Enter / 点击)。
112
140
  * - 引用选择:光标在运算符/`(`/`,` 后时,画布点选/拖选插入引用(blur 抑制防误提交)。
@@ -155,9 +183,9 @@ function beginBlurSuppress(): void {
155
183
  suppressBlurCommit = true
156
184
  }
157
185
 
158
- /** 选区拦截回调:插入 A1 / A1:B2 到光标处 */
186
+ /** 选区拦截回调:插入 A1 / A1:B2 到光标处(仅引用选择模式;弹框打开但非引用上下文时不插入) */
159
187
  function handleRefSelect(range: CellRange): void {
160
- if (!editing.value || mirroring.value) return
188
+ if (!isRefSelecting()) return
161
189
  const rangeText = formatRange(range)
162
190
  const el = fxRef.value
163
191
  const selEnd = el?.selectionEnd ?? fxCursor.value
@@ -432,7 +460,14 @@ function commitEdit(): void {
432
460
  editAddr = null
433
461
  suppressBlurCommit = false
434
462
  closeSuggest()
435
- if (addr) props.context.setCellValue(addr, fxDraft.value)
463
+ if (addr) {
464
+ props.context.setCellValue(addr, fxDraft.value)
465
+ // 选区离开目标格(如编辑期间名称框跳转):回推模型选区,grid 层随之恢复目标格高亮
466
+ const active = selection.value.activeCell
467
+ if (!active || active.row !== addr.row || active.col !== addr.col) {
468
+ props.context.selectCell(addr)
469
+ }
470
+ }
436
471
  refreshFx()
437
472
  }
438
473
 
@@ -490,7 +525,94 @@ function exitMirror(addr: CellAddress): void {
490
525
  }
491
526
  }
492
527
 
493
- defineExpose({ mirrorGridEdit, exitMirror, isRefSelecting, beginBlurSuppress, handleRefSelect })
528
+ // ─── 函数弹框(fx 按钮)────────────────────────────────────
529
+
530
+ const fxLabelRef = useTemplateRef<HTMLButtonElement>('fxLabelRef')
531
+ const functionsDropdownRef = useTemplateRef<InstanceType<typeof UDropdown>>('functionsDropdownRef')
532
+ const functionsOpen = ref(false)
533
+
534
+ function toggleFunctions(): void {
535
+ if (fxDisabled.value) return
536
+ if (functionsOpen.value) {
537
+ functionsOpen.value = false
538
+ return
539
+ }
540
+ // 焦点即将移向弹框搜索框:抑制 fx blur 提交,草稿与编辑态原样保留
541
+ if (editing.value && fxFocused.value) suppressBlurCommit = true
542
+ functionsOpen.value = true
543
+ }
544
+
545
+ // 弹框打开必须同步调 open() 传 fx 按钮作锚点(同 sheet.vue 工具弹层时序)
546
+ watch(functionsOpen, () => {
547
+ const dropdown = functionsDropdownRef.value
548
+ if (!dropdown) return
549
+ if (functionsOpen.value) {
550
+ dropdown.open({ trigger: fxLabelRef.value ?? undefined })
551
+ } else {
552
+ dropdown.close()
553
+ }
554
+ })
555
+
556
+ /** UDropdown 内部关闭(触发元素位移等)→ 同步本地开合状态 */
557
+ function syncFunctionsOpen(visible: boolean): void {
558
+ if (!visible) functionsOpen.value = false
559
+ }
560
+
561
+ /** 弹框内 Esc 关闭(关闭不还原草稿、不退出挂起的编辑态) */
562
+ function handleFunctionsKeydown(event: KeyboardEvent): void {
563
+ if (event.key !== 'Escape') return
564
+ event.preventDefault()
565
+ functionsOpen.value = false
566
+ }
567
+
568
+ /** 点击弹框外关闭(弹框内容与 fx 按钮均 @click.stop,到达这里的必是外部点击) */
569
+ function closeFunctionsOnOuterClick(): void {
570
+ if (!functionsOpen.value) return
571
+ functionsOpen.value = false
572
+ }
573
+
574
+ /** 选中函数:关弹框并进入编辑态输出 =NAME()(fx / 工具栏共用路径) */
575
+ function handleFunctionSelect(name: string): void {
576
+ functionsOpen.value = false
577
+ insertFunction(name)
578
+ }
579
+
580
+ /** 插入函数:编辑目标 = 当前活动格,内容覆盖为 =NAME(),光标落在括号内 */
581
+ function insertFunction(name: string): void {
582
+ const addr = selection.value.activeCell
583
+ if (!addr) return
584
+ const text = `=${name}()`
585
+ const cursor = text.length - 1
586
+ editing.value = true
587
+ editAddr = addr
588
+ mirroring.value = false
589
+ mirrorAddr = null
590
+ fxDraft.value = text
591
+ fxCursor.value = cursor
592
+ suppressBlurCommit = false
593
+ closeSuggest()
594
+ void nextTick(() => {
595
+ const el = fxRef.value
596
+ if (!el) return
597
+ el.focus()
598
+ // focus 事件里 syncFxCursor 会读到置尾光标,这里用局部值落位覆盖
599
+ el.setSelectionRange(cursor, cursor)
600
+ fxCursor.value = cursor
601
+ autosizeFx()
602
+ // `=NAME()` 光标在括号内:处于引用选择上下文,不再弹补全
603
+ refreshSuggest()
604
+ })
605
+ }
606
+
607
+ defineExpose({
608
+ mirrorGridEdit,
609
+ exitMirror,
610
+ isRefSelecting,
611
+ isFunctionPopupOpen: () => functionsOpen.value,
612
+ beginBlurSuppress,
613
+ handleRefSelect,
614
+ insertFunction
615
+ })
494
616
 
495
617
  // ─── 生命周期 ──────────────────────────────────────────────
496
618
 
@@ -512,6 +634,7 @@ watch(
512
634
  mirroring.value = false
513
635
  mirrorAddr = null
514
636
  suppressBlurCommit = false
637
+ functionsOpen.value = false
515
638
  closeSuggest()
516
639
  bindSheet(sheet)
517
640
  selection.value = sheet.getSelection()
@@ -525,10 +648,12 @@ onMounted(() => {
525
648
  selection.value = props.sheet.getSelection()
526
649
  refreshName()
527
650
  refreshFx()
651
+ window.addEventListener('click', closeFunctionsOnOuterClick)
528
652
  })
529
653
 
530
654
  onBeforeUnmount(() => {
531
655
  for (const off of dispose) off()
532
656
  dispose = []
657
+ window.removeEventListener('click', closeFunctionsOnOuterClick)
533
658
  })
534
659
  </script>
@@ -3,9 +3,9 @@ import {
3
3
  importCsv,
4
4
  importXlsx,
5
5
  replaceWorkbookWithSnapshots
6
- } from '@veltra/sheet-core/core/io/import'
7
- import type { Sheet, SheetSnapshot } from '@veltra/sheet-core/core/sheet'
8
- import type { Workbook } from '@veltra/sheet-core/core/workbook'
6
+ } from '@veltra/sheet-core/core/io/import.js'
7
+ import type { Sheet, SheetSnapshot } from '@veltra/sheet-core/core/sheet.js'
8
+ import type { Workbook } from '@veltra/sheet-core/core/workbook.js'
9
9
  import { nextFrame } from '@veltra/utils'
10
10
  import type { Ref } from 'vue'
11
11
 
@@ -1,5 +1,5 @@
1
1
  import { message } from '@veltra/desktop'
2
- import type { SheetImageAnchor, SheetImageType } from '@veltra/sheet-core/core/image'
2
+ import type { SheetImageAnchor, SheetImageType } from '@veltra/sheet-core/core/image.js'
3
3
 
4
4
  import type { SheetContext } from '../../tools/context'
5
5
 
@@ -1,6 +1,6 @@
1
- import type { CellRange } from '@veltra/sheet-core/core/address'
2
- import type { BorderPreset } from '@veltra/sheet-core/core/style/border-presets'
3
- import { BORDER_STYLE_WIDTH, type BorderLineStyle } from '@veltra/sheet-core/core/style/types'
1
+ import type { CellRange } from '@veltra/sheet-core/core/address.js'
2
+ import type { BorderPreset } from '@veltra/sheet-core/core/style/border-presets.js'
3
+ import { BORDER_STYLE_WIDTH, type BorderLineStyle } from '@veltra/sheet-core/core/style/types.js'
4
4
 
5
5
  import type { SheetContext } from '../../tools/context'
6
6
 
@@ -35,14 +35,14 @@
35
35
 
36
36
  <script lang="ts" setup>
37
37
  import { UPalette } from '@veltra/desktop'
38
- import type { SetCellStyleItem } from '@veltra/sheet-core/core/command/set-cell-style'
39
- import { buildBorderPresetItems } from '@veltra/sheet-core/core/style/border-presets'
38
+ import type { SetCellStyleItem } from '@veltra/sheet-core/core/command/set-cell-style.js'
39
+ import { buildBorderPresetItems } from '@veltra/sheet-core/core/style/border-presets.js'
40
40
  import {
41
41
  BORDER_STYLE_WIDTH,
42
42
  type BorderEdge,
43
43
  type BorderLineStyle,
44
44
  type CellStylePatch
45
- } from '@veltra/sheet-core/core/style/types'
45
+ } from '@veltra/sheet-core/core/style/types.js'
46
46
  import { bem } from '@veltra/utils'
47
47
  import { ref } from 'vue'
48
48
 
@@ -69,7 +69,7 @@
69
69
 
70
70
  <script lang="ts" setup>
71
71
  import { UInput } from '@veltra/desktop'
72
- import type { Sheet } from '@veltra/sheet-core/core/sheet'
72
+ import type { Sheet } from '@veltra/sheet-core/core/sheet.js'
73
73
  import { bem } from '@veltra/utils'
74
74
 
75
75
  import type { SheetContext } from '../../../tools/context'