@veltra/sheet 2.5.7 → 2.6.0
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 +7 -2
- 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 +1 -1
- package/dist/components/sheet/popups/import.worker.d.ts +1 -1
- package/dist/components/sheet/popups/import.worker.js +1 -1
- package/dist/components/sheet/sheet-context-menu.js +42 -6
- package/dist/components/sheet/sheet.js +12 -4
- package/dist/components/sheet/style2.css +1 -1
- package/dist/components/sheet/use-find-replace.js +2 -2
- package/dist/components/sheet/use-formula-suggest.js +1 -1
- package/dist/components/sheet/use-sheet-grid.js +17 -7
- package/dist/components/sheet/use-sheet-state.js +5 -2
- package/dist/components/sheet/use-tool-popup.js +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/tools/builtin.js +1 -1
- package/dist/tools/context.d.ts +26 -11
- package/dist/tools/context.js +22 -1
- package/dist/tools/download.js +1 -1
- package/dist/tools/export.worker.d.ts +1 -1
- package/dist/tools/export.worker.js +1 -1
- package/dist/tools/registry.js +1 -1
- package/dist/types/sheet.d.ts +17 -7
- package/package.json +5 -2
- package/src/components/sheet/__test__/formula-bar.test.ts +54 -44
- package/src/components/sheet/__test__/functions-popup.test.ts +2 -2
- package/src/components/sheet/__test__/import-file.test.ts +4 -4
- package/src/components/sheet/__test__/insert-count-menu-item.test.ts +1 -1
- package/src/components/sheet/__test__/insert-image-popup.test.ts +2 -2
- package/src/components/sheet/__test__/insert-image.test.ts +2 -2
- package/src/components/sheet/__test__/sheet-component.test.ts +312 -25
- package/src/components/sheet/__test__/sheet-context-menu.test.ts +34 -5
- package/src/components/sheet/__test__/use-formula-ref-select.test.ts +1 -1
- package/src/components/sheet/__test__/use-formula-suggest.test.ts +2 -2
- package/src/components/sheet/formula-bar.vue +17 -4
- 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 +1 -1
- package/src/components/sheet/popups/import.worker.ts +2 -2
- package/src/components/sheet/sheet-context-menu.ts +63 -6
- package/src/components/sheet/sheet-tabs.vue +2 -2
- package/src/components/sheet/sheet.vue +5 -2
- package/src/components/sheet/style.scss +2 -2
- package/src/components/sheet/use-find-replace.ts +3 -3
- package/src/components/sheet/use-formula-suggest.ts +1 -1
- package/src/components/sheet/use-sheet-grid.ts +33 -11
- package/src/components/sheet/use-sheet-state.ts +7 -3
- package/src/components/sheet/use-tool-popup.ts +5 -4
- package/src/index.ts +1 -1
- package/src/tools/__test__/apply-style.test.ts +3 -3
- package/src/tools/__test__/builtin.test.ts +4 -4
- package/src/tools/__test__/context.test.ts +68 -4
- package/src/tools/__test__/registry.test.ts +1 -1
- package/src/tools/apply-style.ts +3 -3
- package/src/tools/builtin.ts +2 -2
- package/src/tools/context.ts +58 -11
- package/src/tools/download.ts +2 -2
- package/src/tools/export.worker.ts +3 -3
- package/src/tools/registry.ts +1 -1
- package/src/types/sheet.ts +19 -7
- package/src/components/sheet/parsing.ts +0 -11
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createRange } from '@veltra/sheet-core/core/address.js'
|
|
2
|
+
import { Workbook } from '@veltra/sheet-core/core/workbook.js'
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
4
|
+
import { createApp, h, nextTick, ref, type App, type Component } from 'vue'
|
|
4
5
|
|
|
6
|
+
// 引擎指针事件模拟与画布几何(cellX/cellY):setup 已跨包引用同目录 setup.ts
|
|
7
|
+
import { cellX, cellY, fire } from '../../../../../sheet-core/src/grid/__test__/grid-test-utils'
|
|
5
8
|
import { USheet } from '../../../index'
|
|
6
9
|
import { registerTool, unregisterTool } from '../../../tools/registry'
|
|
7
10
|
import type { SheetExposed } from '../../../types'
|
|
11
|
+
import { buildColHeaderMenus, buildRowHeaderMenus } from '../sheet-context-menu'
|
|
8
12
|
|
|
9
13
|
const apps: App[] = []
|
|
10
14
|
const containers: HTMLElement[] = []
|
|
@@ -152,7 +156,7 @@ describe('USheet 组件', () => {
|
|
|
152
156
|
expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', 'Sheet2'])
|
|
153
157
|
expect(tabs(el)[0]!.classList.contains('is-active')).toBe(true)
|
|
154
158
|
|
|
155
|
-
//
|
|
159
|
+
// 引擎表格已挂载到 grid 容器
|
|
156
160
|
expect(el.querySelector('.u-sheet__grid canvas')).not.toBeNull()
|
|
157
161
|
|
|
158
162
|
// 单行滚动结构:工具按钮全部位于滚动视口内的 list 容器
|
|
@@ -212,8 +216,9 @@ describe('USheet 组件', () => {
|
|
|
212
216
|
await nextTick()
|
|
213
217
|
|
|
214
218
|
expect(tabs(el)[1]!.classList.contains('is-active')).toBe(true)
|
|
215
|
-
// grid 实例缓存:切换后旧实例保留(LRU,visibility:hidden),容器内有 2
|
|
216
|
-
|
|
219
|
+
// grid 实例缓存:切换后旧实例保留(LRU,visibility:hidden),容器内有 2 个实例
|
|
220
|
+
// (引擎画布为 [data-layer-kind] 分层栈,实例数以实例容器为准)
|
|
221
|
+
expect(el.querySelectorAll('.u-sheet__grid-instance')).toHaveLength(2)
|
|
217
222
|
// 激活实例可见、旧实例隐藏
|
|
218
223
|
const instances = el.querySelectorAll<HTMLElement>('.u-sheet__grid-instance')
|
|
219
224
|
expect(instances[0]!.style.visibility).toBe('hidden')
|
|
@@ -222,10 +227,10 @@ describe('USheet 组件', () => {
|
|
|
222
227
|
// 工具上下文(与工具按钮同一门面)指向切换后的 sheet
|
|
223
228
|
expect(exposed.value?.getContext().sheetName).toBe('Sheet2')
|
|
224
229
|
|
|
225
|
-
// grid 已重建到 Sheet2:写入 Sheet2
|
|
230
|
+
// grid 已重建到 Sheet2:写入 Sheet2 应在表格可见(引擎数据坐标 (0,0) = 模型 A1)
|
|
226
231
|
workbook.getSheet('Sheet2')!.setCellValue({ row: 0, col: 0 }, 's2')
|
|
227
232
|
await nextTick()
|
|
228
|
-
expect(exposed.value?.getGrid()?.getTable().
|
|
233
|
+
expect(exposed.value?.getGrid()?.getTable().getCellText(0, 0)).toBe('s2')
|
|
229
234
|
})
|
|
230
235
|
|
|
231
236
|
it('props 变更响应:showToolbar/showTabs 显隐,rows 变化重建 grid', async () => {
|
|
@@ -271,11 +276,13 @@ describe('USheet 组件', () => {
|
|
|
271
276
|
workbook.activeSheet.setCellValue({ row: 0, col: 0 }, 'A1')
|
|
272
277
|
await nextTick()
|
|
273
278
|
const table = exposed.value!.getGrid()!.getTable()
|
|
274
|
-
expect(table.options.
|
|
275
|
-
expect(table.options.
|
|
276
|
-
|
|
277
|
-
expect(table.
|
|
278
|
-
expect(table.
|
|
279
|
+
expect(table.options.showRowHeader).toBe(false)
|
|
280
|
+
expect(table.options.showColHeader).toBe(false)
|
|
281
|
+
// 行号列与列头折叠:内容原点回落 (0,0)
|
|
282
|
+
expect(table.getDrawRange().x).toBe(0)
|
|
283
|
+
expect(table.getDrawRange().y).toBe(0)
|
|
284
|
+
expect(table.getBodyVisibleCellRange().rows.end).toBe(8)
|
|
285
|
+
expect(table.getCellText(0, 0)).toBe('A1')
|
|
279
286
|
})
|
|
280
287
|
|
|
281
288
|
it('删除行后渲染行数跟随模型收缩(不被 props.rows 撑回)', async () => {
|
|
@@ -286,14 +293,15 @@ describe('USheet 组件', () => {
|
|
|
286
293
|
|
|
287
294
|
const sheet = exposed.value!.getActiveSheet()
|
|
288
295
|
expect(sheet.rows).toBe(10)
|
|
289
|
-
|
|
296
|
+
// 渲染行数 = 引擎数据行数(无列头 +1;窗口终点被引擎夹取到模型行数)
|
|
297
|
+
expect(exposed.value!.getGrid()!.getTable().getBodyVisibleCellRange().rows.end).toBe(10)
|
|
290
298
|
|
|
291
299
|
sheet.deleteRows(0, 2)
|
|
292
300
|
await nextTick()
|
|
293
301
|
await nextTick()
|
|
294
302
|
|
|
295
303
|
expect(sheet.rows).toBe(8)
|
|
296
|
-
expect(exposed.value!.getGrid()!.getTable().
|
|
304
|
+
expect(exposed.value!.getGrid()!.getTable().getBodyVisibleCellRange().rows.end).toBe(8)
|
|
297
305
|
})
|
|
298
306
|
|
|
299
307
|
it('resolveCellStyle prop 传入 SheetGrid 视口渲染', async () => {
|
|
@@ -325,11 +333,8 @@ describe('USheet 组件', () => {
|
|
|
325
333
|
|
|
326
334
|
const table = exposed.value?.getGrid()?.getTable()
|
|
327
335
|
expect(table).toBeDefined()
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
const rendered = column.style!({ row: 2, col: 2, table })
|
|
332
|
-
expect(rendered.bgColor).toBe('#FFE0E0')
|
|
336
|
+
// 引擎公开样式投影:格 (1,1) 命中 resolveCellStyle 补丁(fill → background 覆盖)
|
|
337
|
+
expect(table!.resolveStyle(1, 1).background).toBe('#FFE0E0')
|
|
333
338
|
expect(sheet.getCellStyle(addr)?.fill?.color).toBe('#CCCCCC')
|
|
334
339
|
})
|
|
335
340
|
|
|
@@ -596,7 +601,7 @@ describe('USheet 组件', () => {
|
|
|
596
601
|
expect(sheet.getCellStyle({ row: 0, col: 0 })).toBeUndefined()
|
|
597
602
|
})
|
|
598
603
|
|
|
599
|
-
it('冻结:模型 setFrozen
|
|
604
|
+
it('冻结:模型 setFrozen 驱动引擎冻结布局联动(工具栏入口已迁至右键菜单)', async () => {
|
|
600
605
|
const workbook = createWorkbook()
|
|
601
606
|
const sheet = workbook.activeSheet
|
|
602
607
|
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
@@ -606,17 +611,18 @@ describe('USheet 组件', () => {
|
|
|
606
611
|
sheet.setFrozen(1, 0)
|
|
607
612
|
await nextTick()
|
|
608
613
|
expect(sheet.frozen).toEqual({ rows: 1, cols: 0 })
|
|
609
|
-
//
|
|
610
|
-
expect(exposed.value?.getGrid()?.getTable().
|
|
611
|
-
expect(exposed.value?.getGrid()?.getTable().
|
|
614
|
+
// 模型冻结数即引擎数据冻结数(行列头不计数,无 ±1)
|
|
615
|
+
expect(exposed.value?.getGrid()?.getTable().getFrozenRowCount()).toBe(1)
|
|
616
|
+
expect(exposed.value?.getGrid()?.getTable().getFrozenColCount()).toBe(0)
|
|
612
617
|
|
|
613
618
|
sheet.setFrozen(1, 1)
|
|
614
619
|
await nextTick()
|
|
615
|
-
expect(exposed.value?.getGrid()?.getTable().
|
|
620
|
+
expect(exposed.value?.getGrid()?.getTable().getFrozenColCount()).toBe(1)
|
|
616
621
|
|
|
617
622
|
sheet.setFrozen(0, 0)
|
|
618
623
|
await nextTick()
|
|
619
624
|
expect(sheet.frozen).toEqual({ rows: 0, cols: 0 })
|
|
625
|
+
expect(exposed.value?.getGrid()?.getTable().getFrozenRowCount()).toBe(0)
|
|
620
626
|
})
|
|
621
627
|
|
|
622
628
|
it('导入:点击直接拉起系统文件选择,不打开弹层、不产生历史条目', async () => {
|
|
@@ -1029,3 +1035,284 @@ describe('tabs 栏', () => {
|
|
|
1029
1035
|
expect(scrollLeft).toBeGreaterThan(0)
|
|
1030
1036
|
})
|
|
1031
1037
|
})
|
|
1038
|
+
|
|
1039
|
+
describe('USheet 行高/列宽右键菜单', () => {
|
|
1040
|
+
/**
|
|
1041
|
+
* 触发行号/列头右键:直接调引擎 contextMenuListeners(happy-dom 无法驱动
|
|
1042
|
+
* 引擎画布的 DOM contextmenu 命中),走 SheetGrid → handleContextMenu →
|
|
1043
|
+
* contextmenu.pop 的真实链路。坐标取行号带内 / 列头带内(行高 28 / 行号列宽 46)。
|
|
1044
|
+
*/
|
|
1045
|
+
function popHeaderContextMenu(
|
|
1046
|
+
exposed: { value: SheetExposed | undefined },
|
|
1047
|
+
kind: 'row-header' | 'col-header'
|
|
1048
|
+
): void {
|
|
1049
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
1050
|
+
const event = {
|
|
1051
|
+
cell: null,
|
|
1052
|
+
region: kind,
|
|
1053
|
+
x: kind === 'row-header' ? 20 : 100,
|
|
1054
|
+
y: kind === 'row-header' ? 40 : 10,
|
|
1055
|
+
originalEvent: new MouseEvent('contextmenu')
|
|
1056
|
+
}
|
|
1057
|
+
for (const listener of table.contextMenuListeners) {
|
|
1058
|
+
listener(event as Parameters<typeof listener>[0])
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* 最新弹出菜单中含指定文案的菜单项。
|
|
1064
|
+
* happy-dom 无 transition:close 的 leave 不结束,旧弹层不卸载而在 body 累积,
|
|
1065
|
+
* 断言只认 body 中最后一份(每次 pop 追加新根节点)。
|
|
1066
|
+
*/
|
|
1067
|
+
function menuItem(text: string): HTMLLIElement | undefined {
|
|
1068
|
+
const roots = document.body.querySelectorAll<HTMLElement>('.u-contextmenu')
|
|
1069
|
+
const latest = roots[roots.length - 1]
|
|
1070
|
+
return latest
|
|
1071
|
+
? [...latest.querySelectorAll<HTMLLIElement>('.u-contextmenu__item')].find((item) =>
|
|
1072
|
+
item.textContent?.includes(text)
|
|
1073
|
+
)
|
|
1074
|
+
: undefined
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** 数值输入项内输入并确认(UInput @input 同步 model,InsertCountMenuItem 点击本体提交) */
|
|
1078
|
+
async function confirmMenuNumber(root: HTMLElement, value: number): Promise<void> {
|
|
1079
|
+
const input = root.querySelector<HTMLInputElement>('input')!
|
|
1080
|
+
input.value = String(value)
|
|
1081
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1082
|
+
await nextTick()
|
|
1083
|
+
root.querySelector<HTMLElement>('.u-sheet__insert-count-menu')!.click()
|
|
1084
|
+
await nextTick()
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* 独立挂载菜单项 render 组件(menuNumberRender → InsertCountMenuItem,与弹出菜单
|
|
1089
|
+
* 同一组合)。弹层 confirm 的 leave 在 happy-dom 不结束,迟到的 destroy 会卸载
|
|
1090
|
+
* 之后 pop 的新菜单,写入路径统一走独立挂载保证确定性。
|
|
1091
|
+
*/
|
|
1092
|
+
function mountMenuRender(render: Component): HTMLElement {
|
|
1093
|
+
const host = document.createElement('div')
|
|
1094
|
+
document.body.appendChild(host)
|
|
1095
|
+
containers.push(host)
|
|
1096
|
+
const app = createApp({ render: () => h(render) })
|
|
1097
|
+
apps.push(app)
|
|
1098
|
+
app.mount(host)
|
|
1099
|
+
return host
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
it('行号/列头菜单出现「行高」「列宽」数值项:输入确认写回选区覆盖行/列,重建 grid 后保留', async () => {
|
|
1103
|
+
const workbook = createWorkbook()
|
|
1104
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
1105
|
+
const state = ref({ rows: 10, cols: 6 })
|
|
1106
|
+
mount(() => ({ workbook, rows: state.value.rows, cols: state.value.cols }), exposed)
|
|
1107
|
+
await nextTick()
|
|
1108
|
+
const ctx = exposed.value!.getContext()
|
|
1109
|
+
|
|
1110
|
+
// 预选 R1C1:R2C2(右键落点在选区内 → 选区保留,不依赖 happy-dom 画布命中)
|
|
1111
|
+
ctx.selectRange(createRange({ row: 0, col: 0 }, { row: 1, col: 1 }))
|
|
1112
|
+
|
|
1113
|
+
// 行号右键(真实链路):菜单出现「行高」内嵌数值输入
|
|
1114
|
+
popHeaderContextMenu(exposed, 'row-header')
|
|
1115
|
+
await nextTick()
|
|
1116
|
+
const rowItem = menuItem('行高')!
|
|
1117
|
+
expect(rowItem.classList.contains('is-disabled')).toBe(false)
|
|
1118
|
+
expect(rowItem.querySelector('.u-contextmenu__custom')).toBeTruthy()
|
|
1119
|
+
expect(rowItem.querySelector('input')).toBeTruthy()
|
|
1120
|
+
|
|
1121
|
+
// 输入 40 确认(与弹层同一 render 组合):应用到选区覆盖行 0..1
|
|
1122
|
+
await confirmMenuNumber(
|
|
1123
|
+
mountMenuRender(buildRowHeaderMenus(ctx).find((m) => m.label === '行高')!.render!),
|
|
1124
|
+
40
|
|
1125
|
+
)
|
|
1126
|
+
expect(workbook.activeSheet.getRowHeight(0)).toBe(40)
|
|
1127
|
+
expect(workbook.activeSheet.getRowHeight(1)).toBe(40)
|
|
1128
|
+
expect(workbook.activeSheet.getRowHeight(2)).toBeUndefined()
|
|
1129
|
+
// 确认后活动引擎即时生效(门面写模型 → 活动 grid 引擎 table 同步,无需重建)
|
|
1130
|
+
const activeTable = exposed.value!.getGrid()!.getTable()
|
|
1131
|
+
expect(activeTable.getRowHeight(0)).toBe(40)
|
|
1132
|
+
expect(activeTable.getRowHeight(1)).toBe(40)
|
|
1133
|
+
expect(activeTable.getRowHeight(2)).toBe(28)
|
|
1134
|
+
// 尺寸写入不进 undo(同 rowHeights/冻结先例)
|
|
1135
|
+
expect(ctx.canUndo).toBe(false)
|
|
1136
|
+
|
|
1137
|
+
// 列头右键(真实链路):菜单出现「列宽」内嵌数值输入。
|
|
1138
|
+
// 预选 cols 0..2 横跨画布命中可能落点——右键列头无论命中 0/1 或 undefined
|
|
1139
|
+
// 都保持选区(happy-dom 画布命中几何不定,不能依赖落点改选)
|
|
1140
|
+
ctx.selectRange(createRange({ row: 0, col: 0 }, { row: 1, col: 2 }))
|
|
1141
|
+
popHeaderContextMenu(exposed, 'col-header')
|
|
1142
|
+
await nextTick()
|
|
1143
|
+
const colItem = menuItem('列宽')!
|
|
1144
|
+
expect(colItem.querySelector('.u-contextmenu__custom')).toBeTruthy()
|
|
1145
|
+
// 输入 120 确认:应用到选区覆盖列 0..2,活动引擎即时生效
|
|
1146
|
+
await confirmMenuNumber(
|
|
1147
|
+
mountMenuRender(buildColHeaderMenus(ctx).find((m) => m.label === '列宽')!.render!),
|
|
1148
|
+
120
|
|
1149
|
+
)
|
|
1150
|
+
expect(workbook.activeSheet.getColWidth(0)).toBe(120)
|
|
1151
|
+
expect(workbook.activeSheet.getColWidth(1)).toBe(120)
|
|
1152
|
+
expect(workbook.activeSheet.getColWidth(2)).toBe(120)
|
|
1153
|
+
expect(workbook.activeSheet.getColWidth(3)).toBeUndefined()
|
|
1154
|
+
expect(activeTable.getColWidth(0)).toBe(120)
|
|
1155
|
+
expect(activeTable.getColWidth(2)).toBe(120)
|
|
1156
|
+
expect(activeTable.getColWidth(3)).toBe(80)
|
|
1157
|
+
|
|
1158
|
+
// 重建 grid(props 变化触发)后从模型还原行列尺寸
|
|
1159
|
+
const gridBefore = exposed.value!.getGrid()
|
|
1160
|
+
state.value = { rows: 12, cols: 6 }
|
|
1161
|
+
await nextTick()
|
|
1162
|
+
expect(exposed.value!.getGrid()).not.toBe(gridBefore)
|
|
1163
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
1164
|
+
expect(table.getRowHeight(0)).toBe(40)
|
|
1165
|
+
expect(table.getColWidth(1)).toBe(120)
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
it('readonly:「行高」「列宽」菜单项禁用(降级为禁用文本项,无内嵌输入)', async () => {
|
|
1169
|
+
const workbook = createWorkbook()
|
|
1170
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
1171
|
+
mount(() => ({ workbook, rows: 10, cols: 6, readonly: true }), exposed)
|
|
1172
|
+
await nextTick()
|
|
1173
|
+
|
|
1174
|
+
popHeaderContextMenu(exposed, 'row-header')
|
|
1175
|
+
await nextTick()
|
|
1176
|
+
const rowItem = menuItem('行高')!
|
|
1177
|
+
expect(rowItem.classList.contains('is-disabled')).toBe(true)
|
|
1178
|
+
expect(rowItem.querySelector('.u-contextmenu__custom')).toBeNull()
|
|
1179
|
+
expect(rowItem.querySelector('input')).toBeNull()
|
|
1180
|
+
|
|
1181
|
+
popHeaderContextMenu(exposed, 'col-header')
|
|
1182
|
+
await nextTick()
|
|
1183
|
+
const colItem = menuItem('列宽')!
|
|
1184
|
+
expect(colItem.classList.contains('is-disabled')).toBe(true)
|
|
1185
|
+
expect(colItem.querySelector('input')).toBeNull()
|
|
1186
|
+
})
|
|
1187
|
+
})
|
|
1188
|
+
|
|
1189
|
+
describe('fx 选区锚点', () => {
|
|
1190
|
+
const A1_ANCHOR = { minCol: 0, minRow: 0, maxCol: 0, maxRow: 0 }
|
|
1191
|
+
|
|
1192
|
+
function mountSheet(exposed: { value: SheetExposed | undefined }): HTMLElement {
|
|
1193
|
+
const workbook = createWorkbook()
|
|
1194
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }), exposed)
|
|
1195
|
+
return el
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function fxInputOf(el: HTMLElement): HTMLTextAreaElement {
|
|
1199
|
+
return el.querySelector<HTMLTextAreaElement>('.u-sheet__fx-input')!
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
it('fx 焦点/插入函数置锚点:画布引用拾取期选区流动,锚点保持', async () => {
|
|
1203
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
1204
|
+
const el = mountSheet(exposed)
|
|
1205
|
+
await nextTick()
|
|
1206
|
+
|
|
1207
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
1208
|
+
const fxInput = fxInputOf(el)
|
|
1209
|
+
expect(table.selectionAnchor).toBeNull()
|
|
1210
|
+
|
|
1211
|
+
// fx 焦点进入编辑会话:被编辑格 A1 置锚点
|
|
1212
|
+
fxInput.dispatchEvent(new Event('focus'))
|
|
1213
|
+
await nextTick()
|
|
1214
|
+
expect(table.selectionAnchor).toEqual(A1_ANCHOR)
|
|
1215
|
+
|
|
1216
|
+
// 输入 =SUM( 进入引用拾取上下文,画布拖选 B2:D4:拾取段选区流动、
|
|
1217
|
+
// 抬手引用文本插入公式,锚点仍钉在被编辑格
|
|
1218
|
+
fxInput.value = '=SUM('
|
|
1219
|
+
fxInput.setSelectionRange(5, 5)
|
|
1220
|
+
fxInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1221
|
+
const instance = el.querySelector<HTMLElement>('.u-sheet__grid-instance')!
|
|
1222
|
+
fire(instance, 'pointerdown', { clientX: cellX(1), clientY: cellY(1) })
|
|
1223
|
+
fire(instance, 'pointermove', { clientX: cellX(3), clientY: cellY(3) })
|
|
1224
|
+
fire(instance, 'pointerup', { clientX: cellX(3), clientY: cellY(3) })
|
|
1225
|
+
await nextTick()
|
|
1226
|
+
expect(fxInput.value).toBe('=SUM(B2:D4')
|
|
1227
|
+
// 拦截命中:拾取段选区流动被消费为引用(C3 已插入),不落模型选区,
|
|
1228
|
+
// 画布回推模型选区;锚点仍钉在被编辑格 A1
|
|
1229
|
+
expect(exposed.value!.getActiveSheet().getSelection().activeCell).toEqual({ row: 0, col: 0 })
|
|
1230
|
+
expect(table.selectionAnchor).toEqual(A1_ANCHOR)
|
|
1231
|
+
|
|
1232
|
+
// fx 按钮选函数(insertFunction 重置会话内容):锚点保持在目标格
|
|
1233
|
+
el.querySelector<HTMLButtonElement>('.u-sheet__fx-label')!.click()
|
|
1234
|
+
await flushPopup()
|
|
1235
|
+
const item = document.querySelector<HTMLElement>('.u-sheet__functions-item')
|
|
1236
|
+
expect(item).not.toBeNull()
|
|
1237
|
+
item!.click()
|
|
1238
|
+
await nextTick()
|
|
1239
|
+
expect(fxInput.value).toMatch(/^=\w+\(\)$/)
|
|
1240
|
+
expect(table.selectionAnchor).toEqual(A1_ANCHOR)
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
it('提交(Enter/✓/失焦)与取消(Esc/✗)清锚点', async () => {
|
|
1244
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
1245
|
+
const el = mountSheet(exposed)
|
|
1246
|
+
await nextTick()
|
|
1247
|
+
|
|
1248
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
1249
|
+
const fxInput = fxInputOf(el)
|
|
1250
|
+
const commitButton = () => el.querySelectorAll<HTMLButtonElement>('.u-sheet__fx-btn')[0]!
|
|
1251
|
+
const cancelButton = () => el.querySelectorAll<HTMLButtonElement>('.u-sheet__fx-btn')[1]!
|
|
1252
|
+
|
|
1253
|
+
// Enter 提交
|
|
1254
|
+
fxInput.dispatchEvent(new Event('focus'))
|
|
1255
|
+
await nextTick()
|
|
1256
|
+
expect(table.selectionAnchor).toEqual(A1_ANCHOR)
|
|
1257
|
+
fxInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
1258
|
+
await nextTick()
|
|
1259
|
+
expect(table.selectionAnchor).toBeNull()
|
|
1260
|
+
|
|
1261
|
+
// ✓ 按钮
|
|
1262
|
+
fxInput.dispatchEvent(new Event('focus'))
|
|
1263
|
+
await nextTick()
|
|
1264
|
+
expect(table.selectionAnchor).toEqual(A1_ANCHOR)
|
|
1265
|
+
commitButton().click()
|
|
1266
|
+
await nextTick()
|
|
1267
|
+
expect(table.selectionAnchor).toBeNull()
|
|
1268
|
+
|
|
1269
|
+
// 失焦提交
|
|
1270
|
+
fxInput.dispatchEvent(new Event('focus'))
|
|
1271
|
+
await nextTick()
|
|
1272
|
+
expect(table.selectionAnchor).toEqual(A1_ANCHOR)
|
|
1273
|
+
fxInput.dispatchEvent(new Event('blur'))
|
|
1274
|
+
await nextTick()
|
|
1275
|
+
expect(table.selectionAnchor).toBeNull()
|
|
1276
|
+
|
|
1277
|
+
// Esc 取消
|
|
1278
|
+
fxInput.dispatchEvent(new Event('focus'))
|
|
1279
|
+
await nextTick()
|
|
1280
|
+
expect(table.selectionAnchor).toEqual(A1_ANCHOR)
|
|
1281
|
+
fxInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
1282
|
+
await nextTick()
|
|
1283
|
+
expect(table.selectionAnchor).toBeNull()
|
|
1284
|
+
|
|
1285
|
+
// ✗ 按钮
|
|
1286
|
+
fxInput.dispatchEvent(new Event('focus'))
|
|
1287
|
+
await nextTick()
|
|
1288
|
+
expect(table.selectionAnchor).toEqual(A1_ANCHOR)
|
|
1289
|
+
cancelButton().click()
|
|
1290
|
+
await nextTick()
|
|
1291
|
+
expect(table.selectionAnchor).toBeNull()
|
|
1292
|
+
})
|
|
1293
|
+
|
|
1294
|
+
it('切 sheet 清锚点:切回原表缓存实例无残留高亮', async () => {
|
|
1295
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
1296
|
+
const el = mountSheet(exposed)
|
|
1297
|
+
await nextTick()
|
|
1298
|
+
|
|
1299
|
+
const fxInput = fxInputOf(el)
|
|
1300
|
+
fxInput.dispatchEvent(new Event('focus'))
|
|
1301
|
+
await nextTick()
|
|
1302
|
+
const sheet1Table = exposed.value!.getGrid()!.getTable()
|
|
1303
|
+
expect(sheet1Table.selectionAnchor).toEqual(A1_ANCHOR)
|
|
1304
|
+
|
|
1305
|
+
// 切到 Sheet2:新实例无锚点,旧实例锚点已清
|
|
1306
|
+
tabs(el)[1]!.click()
|
|
1307
|
+
await nextTick()
|
|
1308
|
+
const sheet2Table = exposed.value!.getGrid()!.getTable()
|
|
1309
|
+
expect(sheet2Table).not.toBe(sheet1Table)
|
|
1310
|
+
expect(sheet2Table.selectionAnchor).toBeNull()
|
|
1311
|
+
|
|
1312
|
+
// 切回 Sheet1:缓存实例复用,无残留锚点
|
|
1313
|
+
tabs(el)[0]!.click()
|
|
1314
|
+
await nextTick()
|
|
1315
|
+
expect(exposed.value!.getGrid()!.getTable()).toBe(sheet1Table)
|
|
1316
|
+
expect(sheet1Table.selectionAnchor).toBeNull()
|
|
1317
|
+
})
|
|
1318
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createRange } from '@veltra/sheet-core/core/address'
|
|
2
|
-
import { Sheet } from '@veltra/sheet-core/core/sheet'
|
|
3
|
-
import { describe, expect, it } from '
|
|
1
|
+
import { createRange } from '@veltra/sheet-core/core/address.js'
|
|
2
|
+
import { Sheet } from '@veltra/sheet-core/core/sheet.js'
|
|
3
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
4
4
|
|
|
5
5
|
import { createSheetContext } from '../../../tools/context'
|
|
6
6
|
import {
|
|
@@ -102,7 +102,7 @@ describe('ensureContextMenuSelection', () => {
|
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
describe('build*Menus', () => {
|
|
105
|
-
it('行号菜单:插入×2 / 删除 / divider / 冻结项;无合并/插入列', () => {
|
|
105
|
+
it('行号菜单:插入×2 / 删除 / 行高 / divider / 冻结项;无合并/插入列', () => {
|
|
106
106
|
const sheet = new Sheet()
|
|
107
107
|
const ctx = createSheetContext(sheet)
|
|
108
108
|
sheet.selectRange(createRange({ row: 1, col: 0 }, { row: 3, col: 5 }))
|
|
@@ -112,11 +112,12 @@ describe('build*Menus', () => {
|
|
|
112
112
|
'在上方插入行',
|
|
113
113
|
'在下方插入行',
|
|
114
114
|
'删除行',
|
|
115
|
+
'行高',
|
|
115
116
|
'---',
|
|
116
117
|
'冻结到当前行',
|
|
117
118
|
'取消冻结'
|
|
118
119
|
])
|
|
119
|
-
expect(menus.filter((m) => m.keepOpen)).toHaveLength(
|
|
120
|
+
expect(menus.filter((m) => m.keepOpen)).toHaveLength(3)
|
|
120
121
|
expect(menus.some((m) => m.divider)).toBe(true)
|
|
121
122
|
expect(menus.at(-1)?.disabled).toBe(true)
|
|
122
123
|
expect(labels.join()).not.toContain('合并')
|
|
@@ -142,12 +143,40 @@ describe('build*Menus', () => {
|
|
|
142
143
|
'在左侧插入列',
|
|
143
144
|
'在右侧插入列',
|
|
144
145
|
'删除列',
|
|
146
|
+
'列宽',
|
|
145
147
|
'---',
|
|
146
148
|
'冻结到当前列',
|
|
147
149
|
'取消冻结'
|
|
148
150
|
])
|
|
149
151
|
})
|
|
150
152
|
|
|
153
|
+
it('行高/列宽项:默认内嵌数值输入,readonly 降级为禁用文本项', () => {
|
|
154
|
+
const sheet = new Sheet()
|
|
155
|
+
const ctx = createSheetContext(sheet)
|
|
156
|
+
sheet.selectCell({ row: 0, col: 0 })
|
|
157
|
+
|
|
158
|
+
const rowItem = buildRowHeaderMenus(ctx).find((m) => m.label === '行高')!
|
|
159
|
+
expect(rowItem.keepOpen).toBe(true)
|
|
160
|
+
expect(rowItem.render).toBeTruthy()
|
|
161
|
+
expect(rowItem.disabled).toBeFalsy()
|
|
162
|
+
|
|
163
|
+
const colItem = buildColHeaderMenus(ctx).find((m) => m.label === '列宽')!
|
|
164
|
+
expect(colItem.keepOpen).toBe(true)
|
|
165
|
+
expect(colItem.render).toBeTruthy()
|
|
166
|
+
|
|
167
|
+
// readonly:render 内嵌组件自带点击路径,禁用态必须去掉 render 拦死输入
|
|
168
|
+
const readonlyRow = buildRowHeaderMenus(ctx, { readonly: true }).find(
|
|
169
|
+
(m) => m.label === '行高'
|
|
170
|
+
)!
|
|
171
|
+
expect(readonlyRow.disabled).toBe(true)
|
|
172
|
+
expect(readonlyRow.render).toBeUndefined()
|
|
173
|
+
const readonlyCol = buildColHeaderMenus(ctx, { readonly: true }).find(
|
|
174
|
+
(m) => m.label === '列宽'
|
|
175
|
+
)!
|
|
176
|
+
expect(readonlyCol.disabled).toBe(true)
|
|
177
|
+
expect(readonlyCol.render).toBeUndefined()
|
|
178
|
+
})
|
|
179
|
+
|
|
151
180
|
it('body 菜单:合并/取消合并 + 设置数据格式子菜单 + 插入图片,不含行列插入删除', () => {
|
|
152
181
|
const sheet = new Sheet()
|
|
153
182
|
const ctx = createSheetContext(sheet)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { registerFormulaFunction } from '@veltra/sheet-core/core/formula/functions'
|
|
2
|
-
import { describe, expect, it } from '
|
|
1
|
+
import { registerFormulaFunction } from '@veltra/sheet-core/core/formula/functions.js'
|
|
2
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
applySuggest,
|
|
@@ -101,9 +101,9 @@ import {
|
|
|
101
101
|
parseRange,
|
|
102
102
|
type CellAddress,
|
|
103
103
|
type CellRange
|
|
104
|
-
} from '@veltra/sheet-core/core/address'
|
|
105
|
-
import type { SelectionState } from '@veltra/sheet-core/core/selection'
|
|
106
|
-
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'
|
|
107
107
|
import { bem } from '@veltra/utils'
|
|
108
108
|
import {
|
|
109
109
|
computed,
|
|
@@ -140,7 +140,12 @@ defineOptions({ name: 'UFormulaBar' })
|
|
|
140
140
|
* - 引用选择:光标在运算符/`(`/`,` 后时,画布点选/拖选插入引用(blur 抑制防误提交)。
|
|
141
141
|
* - 网格编辑镜像:只读显示网格编辑器文本。
|
|
142
142
|
*/
|
|
143
|
-
const props = defineProps<{
|
|
143
|
+
const props = defineProps<{
|
|
144
|
+
sheet: Sheet
|
|
145
|
+
context: SheetContext
|
|
146
|
+
/** fx 编辑会话置/清引擎选区锚点(use-sheet-grid 经活动实例透传;缺省静默) */
|
|
147
|
+
setSelectionAnchor?: (addr: CellAddress | null) => void
|
|
148
|
+
}>()
|
|
144
149
|
|
|
145
150
|
const cls = bem('sheet')
|
|
146
151
|
|
|
@@ -398,6 +403,8 @@ function handleFxFocus(): void {
|
|
|
398
403
|
mirrorAddr = null
|
|
399
404
|
editing.value = true
|
|
400
405
|
editAddr = selection.value.activeCell
|
|
406
|
+
// 编辑会话开始:被编辑格置选区锚点,画布引用拾取期选区流动不丢目标格高亮
|
|
407
|
+
if (editAddr) props.setSelectionAnchor?.(editAddr)
|
|
401
408
|
syncFxCursor()
|
|
402
409
|
scheduleAutosize()
|
|
403
410
|
}
|
|
@@ -459,6 +466,8 @@ function commitEdit(): void {
|
|
|
459
466
|
editing.value = false
|
|
460
467
|
editAddr = null
|
|
461
468
|
suppressBlurCommit = false
|
|
469
|
+
// 会话结束清锚点(Enter / ✓ / 失焦同路径)
|
|
470
|
+
props.setSelectionAnchor?.(null)
|
|
462
471
|
closeSuggest()
|
|
463
472
|
if (addr) {
|
|
464
473
|
props.context.setCellValue(addr, fxDraft.value)
|
|
@@ -476,6 +485,8 @@ function cancelEdit(): void {
|
|
|
476
485
|
editing.value = false
|
|
477
486
|
editAddr = null
|
|
478
487
|
suppressBlurCommit = false
|
|
488
|
+
// 会话结束清锚点(Esc / ✗)
|
|
489
|
+
props.setSelectionAnchor?.(null)
|
|
479
490
|
closeSuggest()
|
|
480
491
|
refreshFx()
|
|
481
492
|
}
|
|
@@ -590,6 +601,8 @@ function insertFunction(name: string): void {
|
|
|
590
601
|
fxDraft.value = text
|
|
591
602
|
fxCursor.value = cursor
|
|
592
603
|
suppressBlurCommit = false
|
|
604
|
+
// 插入函数即进入引用拾取会话:目标格置选区锚点
|
|
605
|
+
props.setSelectionAnchor?.(addr)
|
|
593
606
|
closeSuggest()
|
|
594
607
|
void nextTick(() => {
|
|
595
608
|
const el = fxRef.value
|
|
@@ -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'
|
|
@@ -47,7 +47,7 @@ import { UInput, UScroll } from '@veltra/desktop'
|
|
|
47
47
|
import {
|
|
48
48
|
listFormulaFunctions,
|
|
49
49
|
type FormulaFunctionMeta
|
|
50
|
-
} from '@veltra/sheet-core/core/formula/functions'
|
|
50
|
+
} from '@veltra/sheet-core/core/formula/functions.js'
|
|
51
51
|
import { bem } from '@veltra/utils'
|
|
52
52
|
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
|
|
53
53
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 注意:必须用**运行时动态 import**——worker 顶层静态 import 在 vite dev 的
|
|
16
16
|
* worker 上下文中会因模块图加载顺序导致 `Workbook is not defined`(实测)。
|
|
17
17
|
*/
|
|
18
|
-
import type { SheetSnapshot } from '@veltra/sheet-core/core/sheet'
|
|
18
|
+
import type { SheetSnapshot } from '@veltra/sheet-core/core/sheet.js'
|
|
19
19
|
|
|
20
20
|
export type ImportWorkerResponse =
|
|
21
21
|
| { type: 'progress'; done: number; total: number }
|
|
@@ -31,7 +31,7 @@ self.onmessage = (e: MessageEvent<{ buffer: ArrayBuffer }>): void => {
|
|
|
31
31
|
const { buffer } = e.data
|
|
32
32
|
void (async () => {
|
|
33
33
|
try {
|
|
34
|
-
const { importXlsx } = await import('@veltra/sheet-core/core/io/import')
|
|
34
|
+
const { importXlsx } = await import('@veltra/sheet-core/core/io/import.js')
|
|
35
35
|
// 分片构建:按 10% 粒度回报进度(避免 196 条/秒的消息风暴与文字跳变过快;
|
|
36
36
|
// 模型构建段约 1s,10 次更新肉眼可见数字推进)
|
|
37
37
|
let lastPercent = -1
|