@veltra/sheet 2.0.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/core/address.d.ts +60 -0
- package/dist/core/address.js +142 -0
- package/dist/core/address.js.map +1 -0
- package/dist/core/cell-store.d.ts +61 -0
- package/dist/core/cell-store.js +135 -0
- package/dist/core/cell-store.js.map +1 -0
- package/dist/core/command/default-registry.d.ts +10 -0
- package/dist/core/command/default-registry.js +18 -0
- package/dist/core/command/default-registry.js.map +1 -0
- package/dist/core/command/history.d.ts +64 -0
- package/dist/core/command/history.js +127 -0
- package/dist/core/command/history.js.map +1 -0
- package/dist/core/command/merge-cells.d.ts +21 -0
- package/dist/core/command/merge-cells.js +74 -0
- package/dist/core/command/merge-cells.js.map +1 -0
- package/dist/core/command/registry.d.ts +17 -0
- package/dist/core/command/registry.js +26 -0
- package/dist/core/command/registry.js.map +1 -0
- package/dist/core/command/set-cell-formula.d.ts +17 -0
- package/dist/core/command/set-cell-formula.js +29 -0
- package/dist/core/command/set-cell-formula.js.map +1 -0
- package/dist/core/command/set-cell-value.d.ts +22 -0
- package/dist/core/command/set-cell-value.js +35 -0
- package/dist/core/command/set-cell-value.js.map +1 -0
- package/dist/core/command/types.d.ts +68 -0
- package/dist/core/events.js +29 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/formula/ast.d.ts +58 -0
- package/dist/core/formula/ast.js +38 -0
- package/dist/core/formula/ast.js.map +1 -0
- package/dist/core/formula/dependency-graph.d.ts +70 -0
- package/dist/core/formula/dependency-graph.js +269 -0
- package/dist/core/formula/dependency-graph.js.map +1 -0
- package/dist/core/formula/errors.d.ts +25 -0
- package/dist/core/formula/errors.js +36 -0
- package/dist/core/formula/errors.js.map +1 -0
- package/dist/core/formula/evaluator.d.ts +41 -0
- package/dist/core/formula/evaluator.js +154 -0
- package/dist/core/formula/evaluator.js.map +1 -0
- package/dist/core/formula/functions.d.ts +30 -0
- package/dist/core/formula/functions.js +209 -0
- package/dist/core/formula/functions.js.map +1 -0
- package/dist/core/formula/parser.d.ts +7 -0
- package/dist/core/formula/parser.js +208 -0
- package/dist/core/formula/parser.js.map +1 -0
- package/dist/core/formula/tokenizer.d.ts +37 -0
- package/dist/core/formula/tokenizer.js +146 -0
- package/dist/core/formula/tokenizer.js.map +1 -0
- package/dist/core/merge-manager.d.ts +64 -0
- package/dist/core/merge-manager.js +111 -0
- package/dist/core/merge-manager.js.map +1 -0
- package/dist/core/selection.d.ts +31 -0
- package/dist/core/selection.js +62 -0
- package/dist/core/selection.js.map +1 -0
- package/dist/core/sheet.d.ts +105 -0
- package/dist/core/sheet.js +202 -0
- package/dist/core/sheet.js.map +1 -0
- package/dist/core/workbook.d.ts +43 -0
- package/dist/core/workbook.js +77 -0
- package/dist/core/workbook.js.map +1 -0
- package/dist/grid/sheet-grid.d.ts +59 -0
- package/dist/grid/sheet-grid.js +230 -0
- package/dist/grid/sheet-grid.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +25 -0
- package/dist/tools/builtin.js +72 -0
- package/dist/tools/builtin.js.map +1 -0
- package/dist/tools/context.d.ts +57 -0
- package/dist/tools/context.js +43 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/registry.d.ts +68 -0
- package/dist/tools/registry.js +77 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/sheet.d.ts +43 -0
- package/dist/vue/index.d.ts +2 -0
- package/dist/vue/sheet.js +185 -0
- package/dist/vue/sheet.js.map +1 -0
- package/dist/vue/sheet.vue.d.ts +24 -0
- package/dist/vue/style.d.ts +1 -0
- package/dist/vue/style.js +1 -0
- package/dist/vue/style2.css +93 -0
- package/package.json +37 -0
- package/src/core/__test__/address.test.ts +113 -0
- package/src/core/__test__/cell-store.test.ts +97 -0
- package/src/core/__test__/command.test.ts +377 -0
- package/src/core/__test__/formula-adversarial.test.ts +265 -0
- package/src/core/__test__/formula-engine.test.ts +470 -0
- package/src/core/__test__/formula-functions.test.ts +178 -0
- package/src/core/__test__/formula-parser.test.ts +160 -0
- package/src/core/__test__/history.test.ts +225 -0
- package/src/core/__test__/merge-manager.test.ts +106 -0
- package/src/core/__test__/sheet.test.ts +142 -0
- package/src/core/address.ts +166 -0
- package/src/core/cell-store.ts +173 -0
- package/src/core/command/default-registry.ts +15 -0
- package/src/core/command/history.ts +167 -0
- package/src/core/command/index.ts +7 -0
- package/src/core/command/merge-cells.ts +74 -0
- package/src/core/command/registry.ts +30 -0
- package/src/core/command/set-cell-formula.ts +29 -0
- package/src/core/command/set-cell-value.ts +38 -0
- package/src/core/command/types.ts +73 -0
- package/src/core/events.ts +37 -0
- package/src/core/formula/ast.ts +66 -0
- package/src/core/formula/dependency-graph.ts +329 -0
- package/src/core/formula/errors.ts +41 -0
- package/src/core/formula/evaluator.ts +219 -0
- package/src/core/formula/functions.ts +266 -0
- package/src/core/formula/index.ts +7 -0
- package/src/core/formula/parser.ts +198 -0
- package/src/core/formula/tokenizer.ts +160 -0
- package/src/core/merge-manager.ts +158 -0
- package/src/core/selection.ts +66 -0
- package/src/core/sheet.ts +257 -0
- package/src/core/workbook.ts +99 -0
- package/src/env.d.ts +7 -0
- package/src/grid/__test__/canvas-mock.ts +49 -0
- package/src/grid/__test__/setup.ts +3 -0
- package/src/grid/__test__/sheet-grid-formula.test.ts +140 -0
- package/src/grid/__test__/sheet-grid.test.ts +177 -0
- package/src/grid/sheet-grid.ts +283 -0
- package/src/index.ts +16 -0
- package/src/tools/__test__/builtin.test.ts +128 -0
- package/src/tools/__test__/context.test.ts +157 -0
- package/src/tools/__test__/registry.test.ts +102 -0
- package/src/tools/builtin.ts +75 -0
- package/src/tools/context.ts +106 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/registry.ts +129 -0
- package/src/types/index.ts +1 -0
- package/src/types/sheet.ts +41 -0
- package/src/vue/__test__/sheet-component.test.ts +267 -0
- package/src/vue/index.ts +1 -0
- package/src/vue/sheet.vue +195 -0
- package/src/vue/style.scss +94 -0
- package/src/vue/style.ts +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { rangesEqual } from '../core/address'
|
|
2
|
+
import { registerTool } from './registry'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 内置工具(dogfood 扩展机制):undo/redo + 合并/取消合并。
|
|
6
|
+
*
|
|
7
|
+
* 与第三方工具走同一注册通道,可由宿主 unregisterTool 移除或同 id 覆盖。
|
|
8
|
+
* 本模块由 tools/index.ts 引入(与 core/command 的 default-registry 同构):
|
|
9
|
+
* 经包入口导入即完成注册,无头使用 core 深导入不涉及此全局副作用。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
registerTool({
|
|
13
|
+
id: 'undo',
|
|
14
|
+
title: '撤销',
|
|
15
|
+
tooltip: '撤销(Ctrl/Cmd+Z)',
|
|
16
|
+
group: 'history',
|
|
17
|
+
order: 0,
|
|
18
|
+
disabled: (ctx) => !ctx.canUndo,
|
|
19
|
+
onClick: (ctx) => {
|
|
20
|
+
ctx.undo()
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
registerTool({
|
|
25
|
+
id: 'redo',
|
|
26
|
+
title: '重做',
|
|
27
|
+
tooltip: '重做(Ctrl/Cmd+Shift+Z 或 Ctrl+Y)',
|
|
28
|
+
group: 'history',
|
|
29
|
+
order: 1,
|
|
30
|
+
disabled: (ctx) => !ctx.canRedo,
|
|
31
|
+
onClick: (ctx) => {
|
|
32
|
+
ctx.redo()
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
registerTool({
|
|
37
|
+
id: 'merge',
|
|
38
|
+
title: '合并',
|
|
39
|
+
tooltip: '合并选中区域',
|
|
40
|
+
group: 'cell',
|
|
41
|
+
order: 0,
|
|
42
|
+
disabled: (ctx) => {
|
|
43
|
+
const { activeCell, ranges } = ctx.getSelection()
|
|
44
|
+
const range = ranges[0]
|
|
45
|
+
if (!activeCell || !range) return true
|
|
46
|
+
// 单格无可合并
|
|
47
|
+
if (range.start.row === range.end.row && range.start.col === range.end.col) return true
|
|
48
|
+
// 选区恰好等于既有合并区域 → 无操作必要
|
|
49
|
+
const mergeRange = ctx.getCellInfo(activeCell).mergeRange
|
|
50
|
+
return !!mergeRange && rangesEqual(mergeRange, range)
|
|
51
|
+
},
|
|
52
|
+
onClick: (ctx) => {
|
|
53
|
+
const range = ctx.getSelection().ranges[0]
|
|
54
|
+
if (!range) return
|
|
55
|
+
ctx.mergeCells(range)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
registerTool({
|
|
60
|
+
id: 'unmerge',
|
|
61
|
+
title: '取消合并',
|
|
62
|
+
tooltip: '取消活动格所在合并',
|
|
63
|
+
group: 'cell',
|
|
64
|
+
order: 1,
|
|
65
|
+
disabled: (ctx) => {
|
|
66
|
+
const { activeCell } = ctx.getSelection()
|
|
67
|
+
if (!activeCell) return true
|
|
68
|
+
return !ctx.getCellInfo(activeCell).mergeRange
|
|
69
|
+
},
|
|
70
|
+
onClick: (ctx) => {
|
|
71
|
+
const { activeCell, ranges } = ctx.getSelection()
|
|
72
|
+
if (!activeCell || !ranges[0]) return
|
|
73
|
+
ctx.unmergeCells(ranges[0])
|
|
74
|
+
}
|
|
75
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { CellAddress, CellRange } from '../core/address'
|
|
2
|
+
import type { CellData, CellValue } from '../core/cell-store'
|
|
3
|
+
import type { HistoryState } from '../core/command/history'
|
|
4
|
+
import type { SetCellValueItem } from '../core/command/set-cell-value'
|
|
5
|
+
import type { CellInfo } from '../core/merge-manager'
|
|
6
|
+
import type { SelectionState } from '../core/selection'
|
|
7
|
+
import type { Sheet } from '../core/sheet'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SheetContext:工具的唯一操作门面。
|
|
11
|
+
*
|
|
12
|
+
* 工具(内置与第三方)对表格的全部操作都经由此门面:
|
|
13
|
+
* - 读方法直达当前活动 sheet(语义与 Sheet 同名方法一致)
|
|
14
|
+
* - 写方法全部走命令系统,天然进入 undo 历史——扩展无法绕过命令系统
|
|
15
|
+
* - 不暴露 Sheet 实例本身,保证门面就是扩展能力的全部边界
|
|
16
|
+
* - USheet 切换 sheet tab 后,同一上下文自动指向新的活动 sheet
|
|
17
|
+
*/
|
|
18
|
+
export interface SheetContext {
|
|
19
|
+
/** 当前活动 sheet 名 */
|
|
20
|
+
readonly sheetName: string
|
|
21
|
+
|
|
22
|
+
// ─── 选区 ────────────────────────────────────────────────
|
|
23
|
+
getSelection(): SelectionState
|
|
24
|
+
/** 选中单格(被覆盖格自动定位锚点) */
|
|
25
|
+
selectCell(addr: CellAddress): void
|
|
26
|
+
selectRange(range: CellRange): void
|
|
27
|
+
|
|
28
|
+
// ─── 读取 ────────────────────────────────────────────────
|
|
29
|
+
/** 原始存储语义读取(被合并覆盖格 → undefined) */
|
|
30
|
+
getCellData(addr: CellAddress): CellData | undefined
|
|
31
|
+
/** 锚点解析语义读取(被覆盖格 → 锚点的值) */
|
|
32
|
+
getDisplayValue(addr: CellAddress): CellValue | undefined
|
|
33
|
+
/** 合并语义下的单元格信息(普通格 / 合并锚点 / 被覆盖格) */
|
|
34
|
+
getCellInfo(addr: CellAddress): CellInfo
|
|
35
|
+
|
|
36
|
+
// ─── 写入(全部经命令系统,可 undo) ───────────────────────
|
|
37
|
+
setCellValue(addr: CellAddress, value: CellValue): void
|
|
38
|
+
setCellFormula(addr: CellAddress, formula: string): void
|
|
39
|
+
/** 批量写入(一次调用 = 一个 undo 单元) */
|
|
40
|
+
setCells(items: SetCellValueItem[]): void
|
|
41
|
+
mergeCells(range: CellRange): CellRange
|
|
42
|
+
unmergeCells(range: CellRange): void
|
|
43
|
+
/** 执行命令注册表中的自定义命令(高级扩展点) */
|
|
44
|
+
executeCommand<R = void>(commandId: string, params: unknown): R | undefined
|
|
45
|
+
|
|
46
|
+
// ─── 事务(事务内多次写入合并为一个 undo 单元) ─────────────
|
|
47
|
+
beginTransaction(): void
|
|
48
|
+
commit(): void
|
|
49
|
+
rollback(): void
|
|
50
|
+
|
|
51
|
+
// ─── 历史 ────────────────────────────────────────────────
|
|
52
|
+
undo(): boolean
|
|
53
|
+
redo(): boolean
|
|
54
|
+
readonly canUndo: boolean
|
|
55
|
+
readonly canRedo: boolean
|
|
56
|
+
|
|
57
|
+
// ─── 事件订阅(订阅时绑定到当前活动 sheet;tab 切换后需重新订阅) ──
|
|
58
|
+
onSelectionChange(handler: (state: SelectionState) => void): () => void
|
|
59
|
+
onHistoryChange(handler: (state: HistoryState) => void): () => void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 创建工具上下文。
|
|
64
|
+
* @param resolveSheet 活动 sheet 解析器(USheet 传 `() => activeSheet`,
|
|
65
|
+
* tab 切换后上下文自动指向当前 sheet);也可直接传 Sheet 实例(无头 / 测试场景)
|
|
66
|
+
*/
|
|
67
|
+
export function createSheetContext(resolveSheet: Sheet | (() => Sheet)): SheetContext {
|
|
68
|
+
const sheet = typeof resolveSheet === 'function' ? resolveSheet : () => resolveSheet
|
|
69
|
+
return {
|
|
70
|
+
get sheetName() {
|
|
71
|
+
return sheet().name
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
getSelection: () => sheet().getSelection(),
|
|
75
|
+
selectCell: (addr) => sheet().selectCell(addr),
|
|
76
|
+
selectRange: (range) => sheet().selectRange(range),
|
|
77
|
+
|
|
78
|
+
getCellData: (addr) => sheet().getCellData(addr),
|
|
79
|
+
getDisplayValue: (addr) => sheet().getDisplayValue(addr),
|
|
80
|
+
getCellInfo: (addr) => sheet().getCellInfo(addr),
|
|
81
|
+
|
|
82
|
+
setCellValue: (addr, value) => sheet().setCellValue(addr, value),
|
|
83
|
+
setCellFormula: (addr, formula) => sheet().setCellFormula(addr, formula),
|
|
84
|
+
setCells: (items) => sheet().setCells(items),
|
|
85
|
+
mergeCells: (range) => sheet().mergeCells(range),
|
|
86
|
+
unmergeCells: (range) => sheet().unmergeCells(range),
|
|
87
|
+
executeCommand: <R = void>(commandId: string, params: unknown): R | undefined =>
|
|
88
|
+
sheet().executeCommand<R>(commandId, params),
|
|
89
|
+
|
|
90
|
+
beginTransaction: () => sheet().beginTransaction(),
|
|
91
|
+
commit: () => sheet().commit(),
|
|
92
|
+
rollback: () => sheet().rollback(),
|
|
93
|
+
|
|
94
|
+
undo: () => sheet().undo(),
|
|
95
|
+
redo: () => sheet().redo(),
|
|
96
|
+
get canUndo() {
|
|
97
|
+
return sheet().canUndo
|
|
98
|
+
},
|
|
99
|
+
get canRedo() {
|
|
100
|
+
return sheet().canRedo
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
onSelectionChange: (handler) => sheet().on('selection-change', handler),
|
|
104
|
+
onHistoryChange: (handler) => sheet().on('history-change', handler)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { TypedEventEmitter } from '../core/events'
|
|
2
|
+
import type { SheetContext } from './context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 工具注册表:USheet 工具栏的扩展点。
|
|
6
|
+
*
|
|
7
|
+
* - 工具是框架无关的纯定义(icon 由 vue 层渲染,故此处类型为 unknown)
|
|
8
|
+
* - 分组渲染:同组工具连续排列,组间渲染分隔符;
|
|
9
|
+
* 组序 = 各组最早注册位置,组内按 (order, 注册序) 升序
|
|
10
|
+
* - 同 id 重复注册 = 替换定义并保留原注册位置(HMR / 覆盖内置工具友好)
|
|
11
|
+
* - 默认注册表全局共享:所有 USheet 实例渲染同一组工具
|
|
12
|
+
* (与 defaultCommandRegistry 同语义;每个实例的 SheetContext 各自绑定当前 sheet)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** 工具定义 */
|
|
16
|
+
export interface SheetTool {
|
|
17
|
+
/** 唯一 id;同 id 重复注册视为替换(保留原位置) */
|
|
18
|
+
id: string
|
|
19
|
+
/** 按钮文本 */
|
|
20
|
+
title: string
|
|
21
|
+
/** 图标(Vue 组件,由 USheet 渲染;tools 层不依赖 vue 故为 unknown) */
|
|
22
|
+
icon?: unknown
|
|
23
|
+
/** 悬浮提示;缺省用 title */
|
|
24
|
+
tooltip?: string
|
|
25
|
+
/** 分组名(组间渲染分隔符);缺省 'default' */
|
|
26
|
+
group?: string
|
|
27
|
+
/** 组内排序(升序,相同 order 按注册先后);缺省 0 */
|
|
28
|
+
order?: number
|
|
29
|
+
/** 是否可见(工具栏状态刷新时求值);缺省可见 */
|
|
30
|
+
visible?(ctx: SheetContext): boolean
|
|
31
|
+
/** 是否禁用(工具栏状态刷新时求值);缺省可用 */
|
|
32
|
+
disabled?(ctx: SheetContext): boolean
|
|
33
|
+
/** 点击执行;ctx 是唯一操作入口(写操作全走命令系统,天然可撤销) */
|
|
34
|
+
onClick(ctx: SheetContext): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 分组视图(供工具栏渲染) */
|
|
38
|
+
export interface SheetToolGroup {
|
|
39
|
+
name: string
|
|
40
|
+
tools: SheetTool[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 缺省分组名 */
|
|
44
|
+
export const DEFAULT_TOOL_GROUP = 'default'
|
|
45
|
+
|
|
46
|
+
type ToolEntry = { tool: SheetTool; seq: number }
|
|
47
|
+
|
|
48
|
+
type ToolRegistryEvents = { change: null }
|
|
49
|
+
|
|
50
|
+
export class ToolRegistry {
|
|
51
|
+
private entries = new Map<string, ToolEntry>()
|
|
52
|
+
private seq = 0
|
|
53
|
+
private emitter = new TypedEventEmitter<ToolRegistryEvents>()
|
|
54
|
+
|
|
55
|
+
/** 注册工具;同 id 重复注册 = 替换定义并保留原注册位置 */
|
|
56
|
+
register(tool: SheetTool): void {
|
|
57
|
+
if (!tool.id) throw new Error('工具注册失败:id 不能为空')
|
|
58
|
+
if (!tool.title) throw new Error(`工具注册失败:${tool.id} 缺少 title`)
|
|
59
|
+
if (typeof tool.onClick !== 'function') {
|
|
60
|
+
throw new Error(`工具注册失败:${tool.id} 缺少 onClick`)
|
|
61
|
+
}
|
|
62
|
+
const prev = this.entries.get(tool.id)
|
|
63
|
+
this.entries.set(tool.id, { tool, seq: prev?.seq ?? this.seq++ })
|
|
64
|
+
this.emitter.emit('change', null)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 注销工具;id 不存在时返回 false(不触发 change) */
|
|
68
|
+
unregister(id: string): boolean {
|
|
69
|
+
const existed = this.entries.delete(id)
|
|
70
|
+
if (existed) this.emitter.emit('change', null)
|
|
71
|
+
return existed
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get(id: string): SheetTool | undefined {
|
|
75
|
+
return this.entries.get(id)?.tool
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
has(id: string): boolean {
|
|
79
|
+
return this.entries.has(id)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get size(): number {
|
|
83
|
+
return this.entries.size
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 分组视图:组序 = 各组最早注册位置;组内按 (order, 注册序) 升序 */
|
|
87
|
+
getGroups(): SheetToolGroup[] {
|
|
88
|
+
const groups = new Map<string, { entries: ToolEntry[]; firstSeq: number }>()
|
|
89
|
+
for (const entry of this.entries.values()) {
|
|
90
|
+
const name = entry.tool.group ?? DEFAULT_TOOL_GROUP
|
|
91
|
+
let group = groups.get(name)
|
|
92
|
+
if (!group) {
|
|
93
|
+
group = { entries: [], firstSeq: entry.seq }
|
|
94
|
+
groups.set(name, group)
|
|
95
|
+
}
|
|
96
|
+
group.entries.push(entry)
|
|
97
|
+
}
|
|
98
|
+
return [...groups.entries()]
|
|
99
|
+
.sort((a, b) => a[1].firstSeq - b[1].firstSeq)
|
|
100
|
+
.map(([name, group]) => ({
|
|
101
|
+
name,
|
|
102
|
+
tools: group.entries
|
|
103
|
+
.slice()
|
|
104
|
+
.sort((a, b) => (a.tool.order ?? 0) - (b.tool.order ?? 0) || a.seq - b.seq)
|
|
105
|
+
.map((entry) => entry.tool)
|
|
106
|
+
}))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 订阅注册表变化(register / unregister),返回取消订阅函数 */
|
|
110
|
+
onChange(handler: () => void): () => void {
|
|
111
|
+
return this.emitter.on('change', handler)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 全局默认注册表(USheet 默认渲染这里的工具;内置工具也注册于此)。
|
|
117
|
+
* 工具是应用级扩展:注册一次,所有 USheet 实例共享。
|
|
118
|
+
*/
|
|
119
|
+
export const defaultToolRegistry = new ToolRegistry()
|
|
120
|
+
|
|
121
|
+
/** 注册工具到默认注册表 */
|
|
122
|
+
export function registerTool(tool: SheetTool): void {
|
|
123
|
+
defaultToolRegistry.register(tool)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 从默认注册表注销工具;id 不存在时返回 false */
|
|
127
|
+
export function unregisterTool(id: string): boolean {
|
|
128
|
+
return defaultToolRegistry.unregister(id)
|
|
129
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sheet'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DeconstructValue } from '@veltra/utils'
|
|
2
|
+
import type { ComputedRef } from 'vue'
|
|
3
|
+
|
|
4
|
+
import type { Sheet } from '../core/sheet'
|
|
5
|
+
import type { Workbook } from '../core/workbook'
|
|
6
|
+
import type { SheetGrid } from '../grid/sheet-grid'
|
|
7
|
+
import type { SheetContext } from '../tools/context'
|
|
8
|
+
|
|
9
|
+
/** 电子表格组件属性 */
|
|
10
|
+
export interface SheetProps {
|
|
11
|
+
/** 工作簿实例(多 sheet / 跨表公式的载体);缺省内部自建(单 sheet) */
|
|
12
|
+
workbook?: Workbook
|
|
13
|
+
/** 渲染行数,默认 100 */
|
|
14
|
+
rows?: number
|
|
15
|
+
/** 渲染列数,默认 26(A..Z) */
|
|
16
|
+
cols?: number
|
|
17
|
+
/** 是否显示工具栏,默认 true */
|
|
18
|
+
showToolbar?: boolean
|
|
19
|
+
/** 是否显示底部 sheet 标签栏,默认 true */
|
|
20
|
+
showTabs?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SheetEmits {
|
|
24
|
+
/** 激活 sheet 切换(点击 tab 或宿主调用 workbook.activateSheet) */
|
|
25
|
+
(name: 'active-sheet-change', payload: { sheet: Sheet; index: number }): void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 在组件内部引用 */
|
|
29
|
+
export interface _SheetExposed {
|
|
30
|
+
/** 当前工作簿(props.workbook 缺省时为内部自建实例) */
|
|
31
|
+
workbook: ComputedRef<Workbook>
|
|
32
|
+
/** 当前活动 sheet */
|
|
33
|
+
getActiveSheet: () => Sheet
|
|
34
|
+
/** 工具上下文(与工具栏工具同一门面;tab 切换后自动指向当前 sheet) */
|
|
35
|
+
getContext: () => SheetContext
|
|
36
|
+
/** 底层 SheetGrid(调试/测试用) */
|
|
37
|
+
getGrid: () => SheetGrid | undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 电子表格组件暴露的属性和方法 */
|
|
41
|
+
export type SheetExposed = DeconstructValue<_SheetExposed>
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { createApp, h, nextTick, ref, type App } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { Workbook } from '../../core/workbook'
|
|
5
|
+
import { USheet } from '../../index'
|
|
6
|
+
import { registerTool, unregisterTool } from '../../tools/registry'
|
|
7
|
+
import type { SheetExposed } from '../../types'
|
|
8
|
+
|
|
9
|
+
const apps: App[] = []
|
|
10
|
+
const containers: HTMLElement[] = []
|
|
11
|
+
|
|
12
|
+
function mount(
|
|
13
|
+
props: () => Record<string, unknown> = () => ({}),
|
|
14
|
+
exposedRef?: { value: SheetExposed | undefined }
|
|
15
|
+
) {
|
|
16
|
+
const el = document.createElement('div')
|
|
17
|
+
el.style.width = '800px'
|
|
18
|
+
el.style.height = '600px'
|
|
19
|
+
document.body.appendChild(el)
|
|
20
|
+
containers.push(el)
|
|
21
|
+
const app = createApp({
|
|
22
|
+
render: () =>
|
|
23
|
+
h(USheet, {
|
|
24
|
+
...props(),
|
|
25
|
+
ref: (value: unknown) => {
|
|
26
|
+
if (exposedRef) exposedRef.value = value as SheetExposed | undefined
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
app.mount(el)
|
|
31
|
+
apps.push(app)
|
|
32
|
+
return { app, el }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createWorkbook() {
|
|
36
|
+
const workbook = new Workbook()
|
|
37
|
+
workbook.addSheet('Sheet2')
|
|
38
|
+
return workbook
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toolButton(el: HTMLElement, title: string): HTMLButtonElement | undefined {
|
|
42
|
+
return [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tool')].find((button) =>
|
|
43
|
+
button.textContent?.includes(title)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function tabs(el: HTMLElement): HTMLButtonElement[] {
|
|
48
|
+
return [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tab')]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
while (apps.length) apps.pop()!.unmount()
|
|
53
|
+
while (containers.length) containers.pop()!.remove()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('USheet 组件', () => {
|
|
57
|
+
it('挂载:工具栏渲染内置工具(分组 + 分隔符),tabs 显示 sheet 列表,grid 挂载', async () => {
|
|
58
|
+
const { el } = mount(() => ({ workbook: createWorkbook(), rows: 10, cols: 6 }))
|
|
59
|
+
await nextTick()
|
|
60
|
+
|
|
61
|
+
const tools = [...el.querySelectorAll('.u-sheet__tool')].map((button) =>
|
|
62
|
+
button.textContent?.trim()
|
|
63
|
+
)
|
|
64
|
+
expect(tools).toEqual(['撤销', '重做', '合并', '取消合并'])
|
|
65
|
+
// history 与 cell 两组之间一个分隔符
|
|
66
|
+
expect(el.querySelectorAll('.u-sheet__toolbar-divider')).toHaveLength(1)
|
|
67
|
+
|
|
68
|
+
expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', 'Sheet2'])
|
|
69
|
+
expect(tabs(el)[0]!.classList.contains('is-active')).toBe(true)
|
|
70
|
+
|
|
71
|
+
// VTable 已挂载到 grid 容器
|
|
72
|
+
expect(el.querySelector('.u-sheet__grid canvas')).not.toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('内置 undo/redo 按钮随 history-change 置灰,点击等效快捷键', async () => {
|
|
76
|
+
const workbook = createWorkbook()
|
|
77
|
+
const sheet = workbook.activeSheet
|
|
78
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
79
|
+
await nextTick()
|
|
80
|
+
|
|
81
|
+
const undoButton = toolButton(el, '撤销')!
|
|
82
|
+
const redoButton = toolButton(el, '重做')!
|
|
83
|
+
expect(undoButton.disabled).toBe(true)
|
|
84
|
+
expect(redoButton.disabled).toBe(true)
|
|
85
|
+
|
|
86
|
+
sheet.setCellValue({ row: 0, col: 0 }, 'x')
|
|
87
|
+
await nextTick()
|
|
88
|
+
expect(undoButton.disabled).toBe(false)
|
|
89
|
+
expect(redoButton.disabled).toBe(true)
|
|
90
|
+
|
|
91
|
+
undoButton.click()
|
|
92
|
+
await nextTick()
|
|
93
|
+
expect(sheet.getCellData({ row: 0, col: 0 })).toBeUndefined()
|
|
94
|
+
expect(undoButton.disabled).toBe(true)
|
|
95
|
+
expect(redoButton.disabled).toBe(false)
|
|
96
|
+
|
|
97
|
+
redoButton.click()
|
|
98
|
+
await nextTick()
|
|
99
|
+
expect(sheet.getCellData({ row: 0, col: 0 })).toEqual({ v: 'x', t: 's' })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('tab 切换:active 样式移动,工具上下文指向当前 sheet', async () => {
|
|
103
|
+
const workbook = createWorkbook()
|
|
104
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
105
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }), exposed)
|
|
106
|
+
await nextTick()
|
|
107
|
+
|
|
108
|
+
expect(exposed.value?.getActiveSheet().name).toBe('Sheet1')
|
|
109
|
+
expect(exposed.value?.getContext().sheetName).toBe('Sheet1')
|
|
110
|
+
|
|
111
|
+
tabs(el)[1]!.click()
|
|
112
|
+
await nextTick()
|
|
113
|
+
|
|
114
|
+
expect(tabs(el)[1]!.classList.contains('is-active')).toBe(true)
|
|
115
|
+
// grid 重建:旧 VTable 已 release,容器内只有一个 canvas
|
|
116
|
+
expect(el.querySelectorAll('.u-sheet__grid canvas')).toHaveLength(1)
|
|
117
|
+
expect(exposed.value?.getActiveSheet().name).toBe('Sheet2')
|
|
118
|
+
// 工具上下文(与工具按钮同一门面)指向切换后的 sheet
|
|
119
|
+
expect(exposed.value?.getContext().sheetName).toBe('Sheet2')
|
|
120
|
+
|
|
121
|
+
// grid 已重建到 Sheet2:写入 Sheet2 应在表格可见
|
|
122
|
+
workbook.getSheet('Sheet2')!.setCellValue({ row: 0, col: 0 }, 's2')
|
|
123
|
+
await nextTick()
|
|
124
|
+
expect(exposed.value?.getGrid()?.getTable().getCellValue(1, 1)).toBe('s2')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('props 变更响应:showToolbar/showTabs 显隐,rows 变化重建 grid', async () => {
|
|
128
|
+
const workbook = createWorkbook()
|
|
129
|
+
const state = ref({ rows: 10, showToolbar: true, showTabs: true })
|
|
130
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
131
|
+
const { el } = mount(
|
|
132
|
+
() => ({
|
|
133
|
+
workbook,
|
|
134
|
+
rows: state.value.rows,
|
|
135
|
+
cols: 6,
|
|
136
|
+
showToolbar: state.value.showToolbar,
|
|
137
|
+
showTabs: state.value.showTabs
|
|
138
|
+
}),
|
|
139
|
+
exposed
|
|
140
|
+
)
|
|
141
|
+
await nextTick()
|
|
142
|
+
|
|
143
|
+
const gridBefore = exposed.value?.getGrid()
|
|
144
|
+
expect(gridBefore).toBeDefined()
|
|
145
|
+
|
|
146
|
+
state.value = { ...state.value, showToolbar: false, showTabs: false }
|
|
147
|
+
await nextTick()
|
|
148
|
+
expect(el.querySelector('.u-sheet__toolbar')).toBeNull()
|
|
149
|
+
expect(el.querySelector('.u-sheet__tabs')).toBeNull()
|
|
150
|
+
|
|
151
|
+
state.value = { ...state.value, rows: 20 }
|
|
152
|
+
await nextTick()
|
|
153
|
+
const gridAfter = exposed.value?.getGrid()
|
|
154
|
+
expect(gridAfter).toBeDefined()
|
|
155
|
+
expect(gridAfter).not.toBe(gridBefore)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('workbook prop 变更:tabs 与 grid 切到新工作簿', async () => {
|
|
159
|
+
const workbook1 = createWorkbook()
|
|
160
|
+
const workbook2 = new Workbook()
|
|
161
|
+
workbook2.addSheet('数据表')
|
|
162
|
+
const current = ref(workbook1)
|
|
163
|
+
const { el } = mount(() => ({ workbook: current.value, rows: 10, cols: 6 }))
|
|
164
|
+
await nextTick()
|
|
165
|
+
expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', 'Sheet2'])
|
|
166
|
+
|
|
167
|
+
current.value = workbook2
|
|
168
|
+
await nextTick()
|
|
169
|
+
expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', '数据表'])
|
|
170
|
+
expect(el.querySelector('.u-sheet__grid canvas')).not.toBeNull()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('注册表联动:运行时 register/unregister 工具即时增删按钮', async () => {
|
|
174
|
+
const { el } = mount(() => ({ workbook: createWorkbook(), rows: 10, cols: 6 }))
|
|
175
|
+
await nextTick()
|
|
176
|
+
expect(toolButton(el, '临时工具')).toBeUndefined()
|
|
177
|
+
|
|
178
|
+
registerTool({ id: 'temp-tool', title: '临时工具', onClick: () => {} })
|
|
179
|
+
await nextTick()
|
|
180
|
+
expect(toolButton(el, '临时工具')).toBeDefined()
|
|
181
|
+
|
|
182
|
+
expect(unregisterTool('temp-tool')).toBe(true)
|
|
183
|
+
await nextTick()
|
|
184
|
+
expect(toolButton(el, '临时工具')).toBeUndefined()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('无 props 默认挂载:内部自建工作簿(单 sheet),工具栏/tabs 正常', async () => {
|
|
188
|
+
const { el } = mount()
|
|
189
|
+
await nextTick()
|
|
190
|
+
expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1'])
|
|
191
|
+
expect(el.querySelectorAll('.u-sheet__tool').length).toBeGreaterThan(0)
|
|
192
|
+
expect(el.querySelector('.u-sheet__grid canvas')).not.toBeNull()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('多实例并存:共享注册表,工具点击只作用于各自实例的活动 sheet', async () => {
|
|
196
|
+
const workbook1 = new Workbook()
|
|
197
|
+
const workbook2 = new Workbook()
|
|
198
|
+
const first = mount(() => ({ workbook: workbook1, rows: 10, cols: 6 }))
|
|
199
|
+
const second = mount(() => ({ workbook: workbook2, rows: 10, cols: 6 }))
|
|
200
|
+
await nextTick()
|
|
201
|
+
|
|
202
|
+
registerTool({
|
|
203
|
+
id: 'mark-active',
|
|
204
|
+
title: '标记当前格',
|
|
205
|
+
onClick: (ctx) => {
|
|
206
|
+
const active = ctx.getSelection().activeCell ?? { row: 0, col: 0 }
|
|
207
|
+
ctx.setCellValue(active, 'marked')
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
await nextTick()
|
|
211
|
+
|
|
212
|
+
// 两个实例都渲染该工具(注册表全局共享)
|
|
213
|
+
const button1 = toolButton(first.el, '标记当前格')
|
|
214
|
+
const button2 = toolButton(second.el, '标记当前格')
|
|
215
|
+
expect(button1).toBeDefined()
|
|
216
|
+
expect(button2).toBeDefined()
|
|
217
|
+
|
|
218
|
+
// 点击实例 2 的工具 → 只写实例 2 的 sheet(各自 SheetContext 绑定各自工作簿)
|
|
219
|
+
workbook2.activeSheet.selectCell({ row: 2, col: 1 })
|
|
220
|
+
await nextTick()
|
|
221
|
+
button2!.click()
|
|
222
|
+
await nextTick()
|
|
223
|
+
expect(workbook2.activeSheet.getCellData({ row: 2, col: 1 })).toEqual({ v: 'marked', t: 's' })
|
|
224
|
+
expect(workbook1.activeSheet.getCellData({ row: 2, col: 1 })).toBeUndefined()
|
|
225
|
+
|
|
226
|
+
// 该写入经命令系统:实例 2 的 undo 可用且可撤销
|
|
227
|
+
expect(workbook2.activeSheet.canUndo).toBe(true)
|
|
228
|
+
workbook2.activeSheet.undo()
|
|
229
|
+
expect(workbook2.activeSheet.getCellData({ row: 2, col: 1 })).toBeUndefined()
|
|
230
|
+
|
|
231
|
+
unregisterTool('mark-active')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('合并/取消合并按钮随选区联动,点击生效且可 undo', async () => {
|
|
235
|
+
const workbook = new Workbook()
|
|
236
|
+
const sheet = workbook.activeSheet
|
|
237
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
238
|
+
await nextTick()
|
|
239
|
+
|
|
240
|
+
const mergeButton = toolButton(el, '合并')!
|
|
241
|
+
const unmergeButton = toolButton(el, '取消合并')!
|
|
242
|
+
// 无选区:两者禁用
|
|
243
|
+
expect(mergeButton.disabled).toBe(true)
|
|
244
|
+
expect(unmergeButton.disabled).toBe(true)
|
|
245
|
+
|
|
246
|
+
// 拖选区域(经模型 API 模拟 grid 拖选回写)
|
|
247
|
+
sheet.selectRange({ start: { row: 0, col: 0 }, end: { row: 1, col: 1 } })
|
|
248
|
+
await nextTick()
|
|
249
|
+
expect(mergeButton.disabled).toBe(false)
|
|
250
|
+
|
|
251
|
+
mergeButton.click()
|
|
252
|
+
await nextTick()
|
|
253
|
+
expect(sheet.getCellInfo({ row: 1, col: 1 }).kind).toBe('merged-covered')
|
|
254
|
+
|
|
255
|
+
// 选中合并锚点 → 取消合并可用
|
|
256
|
+
sheet.selectCell({ row: 0, col: 0 })
|
|
257
|
+
await nextTick()
|
|
258
|
+
expect(unmergeButton.disabled).toBe(false)
|
|
259
|
+
|
|
260
|
+
unmergeButton.click()
|
|
261
|
+
await nextTick()
|
|
262
|
+
expect(sheet.getCellInfo({ row: 1, col: 1 }).kind).toBe('normal')
|
|
263
|
+
|
|
264
|
+
sheet.undo()
|
|
265
|
+
expect(sheet.getCellInfo({ row: 1, col: 1 }).kind).toBe('merged-covered')
|
|
266
|
+
})
|
|
267
|
+
})
|
package/src/vue/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as USheet } from './sheet.vue'
|