@veltra/sheet 2.5.6 → 2.5.7
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 +114 -14
- package/dist/components/sheet/popups/functions-popup.js +117 -0
- package/dist/components/sheet/popups/import.worker.d.ts +2 -3
- package/dist/components/sheet/sheet.js +33 -23
- package/dist/components/sheet/style2.css +100 -0
- package/dist/components/sheet/use-formula-suggest.js +17 -1
- package/dist/components/sheet/use-sheet-grid.js +1 -1
- package/dist/components/sheet/use-tool-popup.js +4 -2
- package/dist/tools/builtin.js +11 -1
- package/dist/tools/context.d.ts +4 -5
- package/dist/tools/export.worker.d.ts +3 -4
- package/dist/tools/registry.d.ts +8 -9
- package/dist/types/sheet.d.ts +5 -6
- package/package.json +5 -5
- package/src/components/sheet/__test__/formula-bar.test.ts +312 -0
- package/src/components/sheet/__test__/functions-popup.test.ts +182 -0
- package/src/components/sheet/__test__/sheet-component.test.ts +1 -0
- package/src/components/sheet/__test__/use-formula-suggest.test.ts +4 -3
- package/src/components/sheet/formula-bar.vue +131 -6
- package/src/components/sheet/popups/functions-popup.vue +141 -0
- package/src/components/sheet/sheet.vue +15 -2
- package/src/components/sheet/style.scss +117 -0
- package/src/components/sheet/use-formula-suggest.ts +19 -0
- package/src/components/sheet/use-sheet-grid.ts +10 -2
- package/src/components/sheet/use-tool-popup.ts +4 -2
- package/src/tools/__test__/builtin.test.ts +9 -1
- package/src/tools/builtin.ts +13 -1
- package/src/tools/registry.ts +1 -0
package/dist/tools/builtin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registerTool } from "./registry.js";
|
|
2
2
|
import { FontColor } from "@veltra/icons/colorful";
|
|
3
|
-
import { AlignBottom, AlignCenter, AlignTop, Bold, Border, Download, Fill, FontSize, Italic, MergeCells, PictureRounded, Rollback, Rollfront, Search, Strikethrough, Underline, UnmergeCells, Upload, VerticalAlignCenter, VerticalAlignLeft, VerticalAlignRight, Wrap } from "@veltra/icons/normal";
|
|
3
|
+
import { AlignBottom, AlignCenter, AlignTop, Bold, Border, Download, Fill, FontSize, Italic, MergeCells, PictureRounded, Rollback, Rollfront, Search, Strikethrough, Underline, UnmergeCells, Upload, Variable, VerticalAlignCenter, VerticalAlignLeft, VerticalAlignRight, Wrap } from "@veltra/icons/normal";
|
|
4
4
|
import { rangesEqual } from "@veltra/sheet-core/core/address";
|
|
5
5
|
//#region src/tools/builtin.ts
|
|
6
6
|
/** 取活动格所在首选区;无选区返回 null */
|
|
@@ -310,6 +310,16 @@ registerTool({
|
|
|
310
310
|
popup: "find",
|
|
311
311
|
onClick: () => {}
|
|
312
312
|
});
|
|
313
|
+
registerTool({
|
|
314
|
+
id: "functions",
|
|
315
|
+
title: "函数",
|
|
316
|
+
icon: Variable,
|
|
317
|
+
tooltip: "按分类浏览并插入函数",
|
|
318
|
+
group: "edit",
|
|
319
|
+
order: 1,
|
|
320
|
+
popup: "functions",
|
|
321
|
+
onClick: () => {}
|
|
322
|
+
});
|
|
313
323
|
registerTool({
|
|
314
324
|
id: "insert-image",
|
|
315
325
|
title: "插入图片",
|
package/dist/tools/context.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { SelectionState } from "@veltra/sheet-core/core/selection";
|
|
|
19
19
|
* - 不暴露 Sheet 实例本身,保证门面就是扩展能力的全部边界
|
|
20
20
|
* - USheet 切换 sheet tab 后,同一上下文自动指向新的活动 sheet
|
|
21
21
|
*/
|
|
22
|
-
interface SheetContext {
|
|
22
|
+
export interface SheetContext {
|
|
23
23
|
/** 当前活动 sheet 名 */
|
|
24
24
|
readonly sheetName: string;
|
|
25
25
|
/**
|
|
@@ -108,7 +108,7 @@ interface SheetContext {
|
|
|
108
108
|
}) => void): () => void;
|
|
109
109
|
}
|
|
110
110
|
/** createSheetContext 可选配置 */
|
|
111
|
-
interface SheetContextOptions {
|
|
111
|
+
export interface SheetContextOptions {
|
|
112
112
|
/**
|
|
113
113
|
* 当前渲染网格尺寸(与 SheetGrid / resolveRenderSize 一致)。
|
|
114
114
|
* applyStyle / clearStyle 用其判定整行/整列;缺省时不走行列默认样式。
|
|
@@ -128,6 +128,5 @@ interface SheetContextOptions {
|
|
|
128
128
|
* workbook prop 后导出工具仍导出旧工作簿)
|
|
129
129
|
* @param options.resolveGridSize 渲染尺寸解析器(整行/整列样式路由)
|
|
130
130
|
*/
|
|
131
|
-
declare function createSheetContext(resolveSheet: Sheet | (() => Sheet), workbook?: Workbook | (() => Workbook), options?: SheetContextOptions): SheetContext;
|
|
132
|
-
//#endregion
|
|
133
|
-
export { SheetContext, SheetContextOptions, createSheetContext };
|
|
131
|
+
export declare function createSheetContext(resolveSheet: Sheet | (() => Sheet), workbook?: Workbook | (() => Workbook), options?: SheetContextOptions): SheetContext;
|
|
132
|
+
//#endregion
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { SheetSnapshot } from "@veltra/sheet-core/core/sheet";
|
|
2
2
|
//#region src/tools/export.worker.d.ts
|
|
3
|
-
interface ExportWorkerPayload {
|
|
3
|
+
export interface ExportWorkerPayload {
|
|
4
4
|
sheets: {
|
|
5
5
|
name: string;
|
|
6
6
|
snapshot: SheetSnapshot;
|
|
7
7
|
}[];
|
|
8
8
|
activeIndex: number;
|
|
9
9
|
}
|
|
10
|
-
interface ExportWorkerResponse {
|
|
10
|
+
export interface ExportWorkerResponse {
|
|
11
11
|
ok: boolean;
|
|
12
12
|
buffer?: Uint8Array;
|
|
13
13
|
error?: string;
|
|
14
14
|
}
|
|
15
|
-
//#endregion
|
|
16
|
-
export { ExportWorkerPayload, ExportWorkerResponse };
|
|
15
|
+
//#endregion
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -11,9 +11,9 @@ import { SheetContext } from "./context.js";
|
|
|
11
11
|
* (与 defaultCommandRegistry 同语义;每个实例的 SheetContext 各自绑定当前 sheet)
|
|
12
12
|
*/
|
|
13
13
|
/** 弹层型工具类型:vue 层按类型渲染弹层面板(面板交互走 SheetContext 命令入口) */
|
|
14
|
-
type SheetToolPopupType = 'fill-color' | 'border' | 'font-color' | 'font-size' | 'find' | 'insert-image' | 'export';
|
|
14
|
+
export type SheetToolPopupType = 'fill-color' | 'border' | 'font-color' | 'font-size' | 'find' | 'functions' | 'insert-image' | 'export';
|
|
15
15
|
/** 工具定义 */
|
|
16
|
-
interface SheetTool {
|
|
16
|
+
export interface SheetTool {
|
|
17
17
|
/** 唯一 id;同 id 重复注册视为替换(保留原位置) */
|
|
18
18
|
id: string;
|
|
19
19
|
/** 按钮文本 */
|
|
@@ -38,11 +38,11 @@ interface SheetTool {
|
|
|
38
38
|
onClick(ctx: SheetContext): void;
|
|
39
39
|
}
|
|
40
40
|
/** 分组视图(供工具栏渲染) */
|
|
41
|
-
interface SheetToolGroup {
|
|
41
|
+
export interface SheetToolGroup {
|
|
42
42
|
name: string;
|
|
43
43
|
tools: SheetTool[];
|
|
44
44
|
}
|
|
45
|
-
declare class ToolRegistry {
|
|
45
|
+
export declare class ToolRegistry {
|
|
46
46
|
private entries;
|
|
47
47
|
private seq;
|
|
48
48
|
private emitter;
|
|
@@ -62,10 +62,9 @@ declare class ToolRegistry {
|
|
|
62
62
|
* 全局默认注册表(USheet 默认渲染这里的工具;内置工具也注册于此)。
|
|
63
63
|
* 工具是应用级扩展:注册一次,所有 USheet 实例共享。
|
|
64
64
|
*/
|
|
65
|
-
declare const defaultToolRegistry: ToolRegistry;
|
|
65
|
+
export declare const defaultToolRegistry: ToolRegistry;
|
|
66
66
|
/** 注册工具到默认注册表 */
|
|
67
|
-
declare function registerTool(tool: SheetTool): void;
|
|
67
|
+
export declare function registerTool(tool: SheetTool): void;
|
|
68
68
|
/** 从默认注册表注销工具;id 不存在时返回 false */
|
|
69
|
-
declare function unregisterTool(id: string): boolean;
|
|
70
|
-
//#endregion
|
|
71
|
-
export { SheetTool, SheetToolGroup, SheetToolPopupType, ToolRegistry, defaultToolRegistry, registerTool, unregisterTool };
|
|
69
|
+
export declare function unregisterTool(id: string): boolean;
|
|
70
|
+
//#endregion
|
package/dist/types/sheet.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Workbook } from "@veltra/sheet-core/core/workbook";
|
|
|
6
6
|
import { Sheet } from "@veltra/sheet-core/core/sheet";
|
|
7
7
|
//#region src/types/sheet.d.ts
|
|
8
8
|
/** 电子表格组件属性 */
|
|
9
|
-
interface SheetProps {
|
|
9
|
+
export interface SheetProps {
|
|
10
10
|
/** 工作簿实例(多 sheet / 跨表公式的载体);缺省内部自建(单 sheet) */
|
|
11
11
|
workbook?: Workbook;
|
|
12
12
|
/** 渲染行数,默认 100 */
|
|
@@ -40,7 +40,7 @@ interface SheetProps {
|
|
|
40
40
|
/** 只读预览(关闭编辑回写、填充柄等写入口) */
|
|
41
41
|
readonly?: boolean;
|
|
42
42
|
}
|
|
43
|
-
interface SheetEmits {
|
|
43
|
+
export interface SheetEmits {
|
|
44
44
|
/** 激活 sheet 切换(点击 tab 或宿主调用 workbook.activateSheet) */
|
|
45
45
|
(name: 'active-sheet-change', payload: {
|
|
46
46
|
sheet: Sheet;
|
|
@@ -48,7 +48,7 @@ interface SheetEmits {
|
|
|
48
48
|
}): void;
|
|
49
49
|
}
|
|
50
50
|
/** 在组件内部引用 */
|
|
51
|
-
interface _SheetExposed {
|
|
51
|
+
export interface _SheetExposed {
|
|
52
52
|
/** 当前工作簿(props.workbook 缺省时为内部自建实例) */
|
|
53
53
|
workbook: ComputedRef<Workbook>;
|
|
54
54
|
/** 当前活动 sheet */
|
|
@@ -59,6 +59,5 @@ interface _SheetExposed {
|
|
|
59
59
|
getGrid: () => SheetGrid | undefined;
|
|
60
60
|
}
|
|
61
61
|
/** 电子表格组件暴露的属性和方法 */
|
|
62
|
-
type SheetExposed = DeconstructValue<_SheetExposed>;
|
|
63
|
-
//#endregion
|
|
64
|
-
export { SheetEmits, SheetExposed, SheetProps, _SheetExposed };
|
|
62
|
+
export type SheetExposed = DeconstructValue<_SheetExposed>;
|
|
63
|
+
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veltra/sheet",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.7",
|
|
4
4
|
"homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/cabinet-fe/ultra-ui/issues"
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@cat-kit/core": ">=1.2.1",
|
|
38
38
|
"@cat-kit/fe": ">=1.2.1",
|
|
39
|
-
"@veltra/desktop": "^1.7.
|
|
39
|
+
"@veltra/desktop": "^1.7.14",
|
|
40
40
|
"@veltra/icons": "^1.5.0",
|
|
41
|
-
"@veltra/sheet-core": "^2.5.
|
|
42
|
-
"@veltra/styles": "^1.7.
|
|
43
|
-
"@veltra/utils": "^1.7.
|
|
41
|
+
"@veltra/sheet-core": "^2.5.7",
|
|
42
|
+
"@veltra/styles": "^1.7.14",
|
|
43
|
+
"@veltra/utils": "^1.7.14",
|
|
44
44
|
"vue": ">=3.5.42"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseRange } from '@veltra/sheet-core/core/address'
|
|
2
2
|
import { Workbook } from '@veltra/sheet-core/core/workbook'
|
|
3
|
+
import type { SheetGrid } from '@veltra/sheet-core/grid'
|
|
3
4
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
4
5
|
import { createApp, h, nextTick, type App } from 'vue'
|
|
5
6
|
|
|
@@ -342,6 +343,145 @@ function setFxText(el: HTMLElement, text: string): HTMLTextAreaElement {
|
|
|
342
343
|
return input
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
function fxButton(el: HTMLElement): HTMLButtonElement {
|
|
347
|
+
return el.querySelector<HTMLButtonElement>('button.u-sheet__fx-label')!
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** 函数弹框 Teleport 到 body 级 #pop-container(fx 按钮与工具栏共用同一弹框组件) */
|
|
351
|
+
function functionsPanel(): HTMLElement | null {
|
|
352
|
+
return document.querySelector<HTMLElement>('#pop-container .u-sheet__functions-panel')
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** 等待弹层渲染:watch flush + Teleport 挂载 */
|
|
356
|
+
async function flushPopup(): Promise<void> {
|
|
357
|
+
await nextTick()
|
|
358
|
+
await nextTick()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
describe('USheet 公式栏:fx 函数弹框', () => {
|
|
362
|
+
it('fx 渲染为可点击 button;无活动单元格时禁用且不打开弹框', async () => {
|
|
363
|
+
const sheet = new Workbook().activeSheet
|
|
364
|
+
const { el } = mountFormulaBar(sheet)
|
|
365
|
+
await nextTick()
|
|
366
|
+
|
|
367
|
+
// 清空选区(Sheet 构造默认选 A1):与 fx 输入框同一禁用条件
|
|
368
|
+
sheet.selection.clear()
|
|
369
|
+
await nextTick()
|
|
370
|
+
const button = fxButton(el)
|
|
371
|
+
expect(button.tagName).toBe('BUTTON')
|
|
372
|
+
expect(button.disabled).toBe(true)
|
|
373
|
+
expect(fxInput(el).disabled).toBe(true)
|
|
374
|
+
|
|
375
|
+
button.click()
|
|
376
|
+
await flushPopup()
|
|
377
|
+
expect(functionsPanel()).toBeNull()
|
|
378
|
+
|
|
379
|
+
// 有活动格 → 可点击
|
|
380
|
+
sheet.selectCell({ row: 0, col: 0 })
|
|
381
|
+
await nextTick()
|
|
382
|
+
expect(button.disabled).toBe(false)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('点击 fx 开弹框;再点 fx / 点击弹框外 / Esc 关闭;开关不改选区、单元格内容与草稿', async () => {
|
|
386
|
+
const sheet = new Workbook().activeSheet
|
|
387
|
+
sheet.setCellValue({ row: 0, col: 0 }, 'keep')
|
|
388
|
+
const { el } = mountFormulaBar(sheet)
|
|
389
|
+
await nextTick()
|
|
390
|
+
sheet.selectCell({ row: 2, col: 0 })
|
|
391
|
+
await nextTick()
|
|
392
|
+
|
|
393
|
+
// 挂起草稿编辑(focus + 输入,不提交)
|
|
394
|
+
const input = setFxText(el, 'draft')
|
|
395
|
+
const button = fxButton(el)
|
|
396
|
+
const selectionBefore = sheet.getSelection()
|
|
397
|
+
|
|
398
|
+
// 点击 fx → 弹框打开
|
|
399
|
+
button.click()
|
|
400
|
+
await flushPopup()
|
|
401
|
+
expect(functionsPanel()).not.toBeNull()
|
|
402
|
+
// 开弹框不改选区 / 不写单元格 / 草稿保留
|
|
403
|
+
expect(sheet.getSelection()).toEqual(selectionBefore)
|
|
404
|
+
expect(sheet.getCellData({ row: 2, col: 0 })).toBeUndefined()
|
|
405
|
+
expect(input.value).toBe('draft')
|
|
406
|
+
|
|
407
|
+
// 再次点击 fx → 关闭
|
|
408
|
+
button.click()
|
|
409
|
+
await flushPopup()
|
|
410
|
+
expect(functionsPanel()).toBeNull()
|
|
411
|
+
expect(input.value).toBe('draft')
|
|
412
|
+
|
|
413
|
+
// 点击弹框外 → 关闭
|
|
414
|
+
button.click()
|
|
415
|
+
await flushPopup()
|
|
416
|
+
window.dispatchEvent(new MouseEvent('click'))
|
|
417
|
+
await flushPopup()
|
|
418
|
+
expect(functionsPanel()).toBeNull()
|
|
419
|
+
expect(input.value).toBe('draft')
|
|
420
|
+
|
|
421
|
+
// Esc → 关闭;全程单元格未被写入
|
|
422
|
+
button.click()
|
|
423
|
+
await flushPopup()
|
|
424
|
+
functionsPanel()!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
425
|
+
await flushPopup()
|
|
426
|
+
expect(functionsPanel()).toBeNull()
|
|
427
|
+
expect(input.value).toBe('draft')
|
|
428
|
+
expect(sheet.getCellData({ row: 2, col: 0 })).toBeUndefined()
|
|
429
|
+
expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'keep' })
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('选中函数:进入编辑态输出 =SUM(),光标在括号内、焦点在 fx 输入框;Enter 提交到活动格', async () => {
|
|
433
|
+
const sheet = new Workbook().activeSheet
|
|
434
|
+
const { el } = mountFormulaBar(sheet)
|
|
435
|
+
await nextTick()
|
|
436
|
+
sheet.selectCell({ row: 2, col: 0 })
|
|
437
|
+
await nextTick()
|
|
438
|
+
|
|
439
|
+
fxButton(el).click()
|
|
440
|
+
await flushPopup()
|
|
441
|
+
// 默认「常用」首项 SUM
|
|
442
|
+
document.querySelector<HTMLElement>('#pop-container .u-sheet__functions-item')!.click()
|
|
443
|
+
await nextTick()
|
|
444
|
+
await nextTick()
|
|
445
|
+
|
|
446
|
+
const input = fxInput(el)
|
|
447
|
+
expect(input.value).toBe('=SUM()')
|
|
448
|
+
expect(input.selectionStart).toBe('=SUM()'.length - 1)
|
|
449
|
+
expect(input.selectionEnd).toBe('=SUM()'.length - 1)
|
|
450
|
+
expect(document.activeElement).toBe(input)
|
|
451
|
+
expect(el.querySelector('.u-sheet__fx-suggest')).toBeNull()
|
|
452
|
+
|
|
453
|
+
// 提交 → 写入进入编辑时的目标格(当前活动格 A3)
|
|
454
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
455
|
+
await nextTick()
|
|
456
|
+
expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ f: 'SUM()' })
|
|
457
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('工具栏「函数」按钮打开同一弹框,选中行为与 fx 按钮一致', async () => {
|
|
461
|
+
const workbook = new Workbook()
|
|
462
|
+
const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
|
|
463
|
+
await nextTick()
|
|
464
|
+
|
|
465
|
+
const toolbarButton = el.querySelector<HTMLButtonElement>('[data-tool-id="functions"]')!
|
|
466
|
+
toolbarButton.click()
|
|
467
|
+
await nextTick()
|
|
468
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
469
|
+
await flushPopup()
|
|
470
|
+
expect(functionsPanel()).not.toBeNull()
|
|
471
|
+
|
|
472
|
+
document.querySelector<HTMLElement>('#pop-container .u-sheet__functions-item')!.click()
|
|
473
|
+
await nextTick()
|
|
474
|
+
await nextTick()
|
|
475
|
+
// 公式栏进入编辑态(默认选区 A1)
|
|
476
|
+
const input = fxInput(el)
|
|
477
|
+
expect(input.value).toBe('=SUM()')
|
|
478
|
+
expect(input.selectionStart).toBe('=SUM()'.length - 1)
|
|
479
|
+
expect(document.activeElement).toBe(input)
|
|
480
|
+
// 弹框随选中关闭
|
|
481
|
+
expect(functionsPanel()).toBeNull()
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
345
485
|
function suggestList(el: HTMLElement): HTMLElement | null {
|
|
346
486
|
return el.querySelector('.u-sheet__fx-suggest')
|
|
347
487
|
}
|
|
@@ -501,3 +641,175 @@ describe('USheet 公式栏:函数补全与引用选择', () => {
|
|
|
501
641
|
expect(input.style.height).toBe('')
|
|
502
642
|
})
|
|
503
643
|
})
|
|
644
|
+
|
|
645
|
+
// ─── 目标格选区高亮保持(弹框打开与参数点选期间)──────────────
|
|
646
|
+
|
|
647
|
+
/** VTable 事件名(ListTable.EVENT_TYPE 常量值;@veltra/sheet 不直接依赖 @visactor/vtable,不做运行时引入) */
|
|
648
|
+
const EVT_SELECTED_CELL = 'selected_cell'
|
|
649
|
+
const EVT_DRAG_SELECT_END = 'drag_select_end'
|
|
650
|
+
|
|
651
|
+
type GridTable = ReturnType<SheetGrid['getTable']>
|
|
652
|
+
|
|
653
|
+
/** 模拟画布点选格(VTable 时序:selectCells → SELECTED_CELL) */
|
|
654
|
+
function canvasClickCell(table: GridTable, col: number, row: number): void {
|
|
655
|
+
table.selectCells([{ start: { col, row }, end: { col, row } }])
|
|
656
|
+
table.fireListeners(EVT_SELECTED_CELL, { col, row })
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** 画布当前选区末尾项(表格坐标;VTable 范围对象含额外内部字段,按坐标匹配) */
|
|
660
|
+
function lastCanvasRange(table: GridTable) {
|
|
661
|
+
return table.getSelectedCellRanges().at(-1)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
describe('USheet 公式栏:目标格选区高亮保持', () => {
|
|
665
|
+
it('fx 弹框打开期间点选画布:目标格高亮与名称框保持、模型选区不变,不插入引用文本', async () => {
|
|
666
|
+
const workbook = new Workbook()
|
|
667
|
+
const sheet = workbook.activeSheet
|
|
668
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
669
|
+
const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
|
|
670
|
+
await nextTick()
|
|
671
|
+
sheet.selectCell({ row: 2, col: 0 })
|
|
672
|
+
await nextTick()
|
|
673
|
+
|
|
674
|
+
fxButton(el).click()
|
|
675
|
+
await flushPopup()
|
|
676
|
+
expect(functionsPanel()).not.toBeNull()
|
|
677
|
+
|
|
678
|
+
// 弹框打开期间点选画布 B2(表格坐标 (2,2))
|
|
679
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
680
|
+
canvasClickCell(table, 2, 2)
|
|
681
|
+
|
|
682
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
|
|
683
|
+
expect(sheet.getSelection().ranges[0]).toEqual({
|
|
684
|
+
start: { row: 2, col: 0 },
|
|
685
|
+
end: { row: 2, col: 0 }
|
|
686
|
+
})
|
|
687
|
+
// 画布选区回推为目标格 A3(表格坐标 (1,3)),名称框仍显示 A3
|
|
688
|
+
expect(lastCanvasRange(table)).toMatchObject({
|
|
689
|
+
start: { col: 1, row: 3 },
|
|
690
|
+
end: { col: 1, row: 3 }
|
|
691
|
+
})
|
|
692
|
+
expect(nameBox(el).value).toBe('A3')
|
|
693
|
+
// 未处于引用选择上下文:不插入引用文本
|
|
694
|
+
expect(fxInput(el).value).toBe('')
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
it('选中函数后点选画布参数格:只插入引用文本、目标格选区不变;提交后选区与名称框仍指向目标格', async () => {
|
|
698
|
+
const workbook = new Workbook()
|
|
699
|
+
const sheet = workbook.activeSheet
|
|
700
|
+
sheet.setCellValue({ row: 0, col: 0 }, 1)
|
|
701
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
702
|
+
const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
|
|
703
|
+
await nextTick()
|
|
704
|
+
sheet.selectCell({ row: 2, col: 0 })
|
|
705
|
+
await nextTick()
|
|
706
|
+
|
|
707
|
+
// fx 弹框选中 SUM → 编辑态 =SUM()(目标 = 活动格 A3)
|
|
708
|
+
fxButton(el).click()
|
|
709
|
+
await flushPopup()
|
|
710
|
+
document.querySelector<HTMLElement>('#pop-container .u-sheet__functions-item')!.click()
|
|
711
|
+
await nextTick()
|
|
712
|
+
await nextTick()
|
|
713
|
+
const input = fxInput(el)
|
|
714
|
+
expect(input.value).toBe('=SUM()')
|
|
715
|
+
|
|
716
|
+
// 点选参数格 A1(表格坐标 (1,1))→ 只插入引用文本(括号随 =SUM() 模板闭合),目标格选区不变
|
|
717
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
718
|
+
canvasClickCell(table, 1, 1)
|
|
719
|
+
await nextTick()
|
|
720
|
+
expect(input.value).toBe('=SUM(A1)')
|
|
721
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
|
|
722
|
+
expect(lastCanvasRange(table)).toMatchObject({
|
|
723
|
+
start: { col: 1, row: 3 },
|
|
724
|
+
end: { col: 1, row: 3 }
|
|
725
|
+
})
|
|
726
|
+
expect(nameBox(el).value).toBe('A3')
|
|
727
|
+
|
|
728
|
+
// 提交 → 写入目标格 A3,选区与名称框仍指向 A3
|
|
729
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
730
|
+
await nextTick()
|
|
731
|
+
expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ f: 'SUM(A1)', v: 1 })
|
|
732
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
|
|
733
|
+
expect(nameBox(el).value).toBe('A3')
|
|
734
|
+
expect(lastCanvasRange(table)).toMatchObject({
|
|
735
|
+
start: { col: 1, row: 3 },
|
|
736
|
+
end: { col: 1, row: 3 }
|
|
737
|
+
})
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
it('工具栏「函数」入口与 fx 一致:弹框期间点选画布保持目标格;拖选参数后提交仍指向目标格', async () => {
|
|
741
|
+
const workbook = new Workbook()
|
|
742
|
+
const sheet = workbook.activeSheet
|
|
743
|
+
sheet.setCellValue({ row: 0, col: 0 }, 1)
|
|
744
|
+
sheet.setCellValue({ row: 1, col: 0 }, 2)
|
|
745
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
746
|
+
const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
|
|
747
|
+
await nextTick()
|
|
748
|
+
sheet.selectCell({ row: 2, col: 0 })
|
|
749
|
+
await nextTick()
|
|
750
|
+
|
|
751
|
+
el.querySelector<HTMLButtonElement>('[data-tool-id="functions"]')!.click()
|
|
752
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
753
|
+
await flushPopup()
|
|
754
|
+
expect(functionsPanel()).not.toBeNull()
|
|
755
|
+
|
|
756
|
+
// 弹框打开期间点选画布 B2:目标格 A3 高亮保持、模型选区不变、不插入文本
|
|
757
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
758
|
+
canvasClickCell(table, 2, 2)
|
|
759
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
|
|
760
|
+
expect(lastCanvasRange(table)).toMatchObject({
|
|
761
|
+
start: { col: 1, row: 3 },
|
|
762
|
+
end: { col: 1, row: 3 }
|
|
763
|
+
})
|
|
764
|
+
expect(fxInput(el).value).toBe('')
|
|
765
|
+
|
|
766
|
+
// 选中 SUM → 拖选参数区域 A1:A2(SELECTED_CELL → DRAG_SELECT_END 完整时序)
|
|
767
|
+
document.querySelector<HTMLElement>('#pop-container .u-sheet__functions-item')!.click()
|
|
768
|
+
await nextTick()
|
|
769
|
+
await nextTick()
|
|
770
|
+
const input = fxInput(el)
|
|
771
|
+
expect(input.value).toBe('=SUM()')
|
|
772
|
+
table.selectCells([{ start: { col: 1, row: 1 }, end: { col: 1, row: 2 } }])
|
|
773
|
+
table.fireListeners(EVT_SELECTED_CELL, { col: 1, row: 2 })
|
|
774
|
+
table.fireListeners(EVT_DRAG_SELECT_END, {})
|
|
775
|
+
await nextTick()
|
|
776
|
+
expect(input.value).toBe('=SUM(A1:A2)')
|
|
777
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
|
|
778
|
+
expect(nameBox(el).value).toBe('A3')
|
|
779
|
+
|
|
780
|
+
// 提交 → 选区与名称框仍指向目标格 A3
|
|
781
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
782
|
+
await nextTick()
|
|
783
|
+
expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ f: 'SUM(A1:A2)', v: 3 })
|
|
784
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
|
|
785
|
+
expect(nameBox(el).value).toBe('A3')
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
it('公式提交后选区与名称框仍指向目标格(编辑期间名称框跳转离开时回推)', async () => {
|
|
789
|
+
const workbook = new Workbook()
|
|
790
|
+
const sheet = workbook.activeSheet
|
|
791
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
792
|
+
const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
|
|
793
|
+
await nextTick()
|
|
794
|
+
sheet.selectCell({ row: 2, col: 0 })
|
|
795
|
+
await nextTick()
|
|
796
|
+
|
|
797
|
+
// 编辑目标 A3;期间经名称框跳转到 B5(模型选区离开目标格)
|
|
798
|
+
const input = setFxText(el, '=1+1')
|
|
799
|
+
typeName(el, 'B5')
|
|
800
|
+
await nextTick()
|
|
801
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 4, col: 1 })
|
|
802
|
+
expect(nameBox(el).value).toBe('B5')
|
|
803
|
+
|
|
804
|
+
// 提交 → 写入 A3,模型选区 / 名称框 / 画布高亮回推目标格
|
|
805
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
806
|
+
await nextTick()
|
|
807
|
+
expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ f: '1+1', v: 2 })
|
|
808
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
|
|
809
|
+
expect(nameBox(el).value).toBe('A3')
|
|
810
|
+
expect(lastCanvasRange(exposed.value!.getGrid()!.getTable())).toMatchObject({
|
|
811
|
+
start: { col: 1, row: 3 },
|
|
812
|
+
end: { col: 1, row: 3 }
|
|
813
|
+
})
|
|
814
|
+
})
|
|
815
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listFormulaFunctions,
|
|
3
|
+
registerFormulaFunction
|
|
4
|
+
} from '@veltra/sheet-core/core/formula/functions'
|
|
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
|
+
})
|