@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.
- package/dist/components/sheet/formula-bar.js +115 -15
- package/dist/components/sheet/import-file.js +1 -1
- package/dist/components/sheet/popup-helpers.js +1 -1
- package/dist/components/sheet/popups/border-popup.js +2 -2
- package/dist/components/sheet/popups/functions-popup.js +117 -0
- package/dist/components/sheet/popups/import.worker.d.ts +3 -4
- package/dist/components/sheet/popups/import.worker.js +1 -1
- package/dist/components/sheet/sheet-context-menu.js +1 -1
- package/dist/components/sheet/sheet.js +33 -23
- package/dist/components/sheet/style2.css +100 -0
- package/dist/components/sheet/use-find-replace.js +2 -2
- package/dist/components/sheet/use-formula-suggest.js +18 -2
- package/dist/components/sheet/use-sheet-grid.js +1 -1
- package/dist/components/sheet/use-sheet-state.js +1 -1
- package/dist/components/sheet/use-tool-popup.js +4 -2
- package/dist/tools/builtin.js +12 -2
- package/dist/tools/context.d.ts +15 -16
- package/dist/tools/download.js +1 -1
- package/dist/tools/export.worker.d.ts +4 -5
- package/dist/tools/export.worker.js +1 -1
- package/dist/tools/registry.d.ts +8 -9
- package/dist/tools/registry.js +1 -1
- package/dist/types/sheet.d.ts +7 -8
- package/package.json +5 -5
- package/src/components/sheet/__test__/formula-bar.test.ts +314 -2
- package/src/components/sheet/__test__/functions-popup.test.ts +182 -0
- package/src/components/sheet/__test__/import-file.test.ts +3 -3
- package/src/components/sheet/__test__/insert-image-popup.test.ts +1 -1
- package/src/components/sheet/__test__/insert-image.test.ts +1 -1
- package/src/components/sheet/__test__/sheet-component.test.ts +2 -1
- package/src/components/sheet/__test__/sheet-context-menu.test.ts +2 -2
- package/src/components/sheet/__test__/use-formula-suggest.test.ts +5 -4
- package/src/components/sheet/formula-bar.vue +134 -9
- package/src/components/sheet/import-file.ts +3 -3
- package/src/components/sheet/insert-image.ts +1 -1
- package/src/components/sheet/popup-helpers.ts +3 -3
- package/src/components/sheet/popups/border-popup.vue +3 -3
- package/src/components/sheet/popups/find-popup.vue +1 -1
- package/src/components/sheet/popups/functions-popup.vue +141 -0
- package/src/components/sheet/popups/import.worker.ts +2 -2
- package/src/components/sheet/sheet-context-menu.ts +2 -2
- package/src/components/sheet/sheet-tabs.vue +2 -2
- package/src/components/sheet/sheet.vue +15 -2
- package/src/components/sheet/style.scss +117 -0
- package/src/components/sheet/use-find-replace.ts +3 -3
- package/src/components/sheet/use-formula-suggest.ts +20 -1
- package/src/components/sheet/use-sheet-grid.ts +12 -4
- package/src/components/sheet/use-sheet-state.ts +2 -2
- package/src/components/sheet/use-tool-popup.ts +4 -2
- package/src/tools/__test__/apply-style.test.ts +2 -2
- package/src/tools/__test__/builtin.test.ts +12 -4
- package/src/tools/__test__/context.test.ts +2 -2
- package/src/tools/apply-style.ts +3 -3
- package/src/tools/builtin.ts +15 -3
- package/src/tools/context.ts +11 -11
- package/src/tools/download.ts +2 -2
- package/src/tools/export.worker.ts +3 -3
- package/src/tools/registry.ts +2 -1
- 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 { 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
|
-
|
|
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
|
-
<
|
|
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 (!
|
|
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)
|
|
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
|
-
|
|
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'
|