@veltra/sheet 2.0.0 → 2.1.1
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/index.d.ts +4 -25
- package/dist/index.js +3 -22
- package/dist/tools/builtin.js +279 -8
- package/dist/tools/context.d.ts +54 -11
- package/dist/tools/context.js +28 -5
- package/dist/tools/download.js +68 -0
- package/dist/tools/export.worker.d.ts +16 -0
- package/dist/tools/export.worker.js +29 -0
- package/dist/tools/registry.d.ts +7 -4
- package/dist/tools/registry.js +1 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/sheet.d.ts +6 -5
- package/dist/vue/formula-bar.js +393 -0
- package/dist/vue/formula-suggest-list.js +66 -0
- package/dist/vue/import-file.js +140 -0
- package/dist/vue/insert-count-menu-item.js +68 -0
- package/dist/vue/insert-image.js +79 -0
- package/dist/vue/popup-helpers.js +73 -0
- package/dist/vue/popups/border-popup.js +75 -0
- package/dist/vue/popups/border-preset-glyph.js +106 -0
- package/dist/vue/popups/export-popup.js +51 -0
- package/dist/vue/popups/fill-color-popup.js +37 -0
- package/dist/vue/popups/find-popup.js +113 -0
- package/dist/vue/popups/font-color-popup.js +37 -0
- package/dist/vue/popups/font-size-popup.js +63 -0
- package/dist/vue/popups/import.worker.d.ts +21 -0
- package/dist/vue/popups/import.worker.js +41 -0
- package/dist/vue/popups/insert-image-popup.js +38 -0
- package/dist/vue/sheet-context-menu.js +200 -0
- package/dist/vue/sheet-tabs.js +162 -0
- package/dist/vue/sheet-toolbar.js +106 -0
- package/dist/vue/sheet.js +144 -138
- package/dist/vue/style.d.ts +11 -1
- package/dist/vue/style.js +11 -0
- package/dist/vue/style2.css +564 -3
- package/dist/vue/use-find-replace.js +161 -0
- package/dist/vue/use-formula-ref-select.js +37 -0
- package/dist/vue/use-formula-suggest.js +126 -0
- package/dist/vue/use-sheet-grid.js +180 -0
- package/dist/vue/use-sheet-state.js +122 -0
- package/dist/vue/use-sheet-tabs-bar.js +90 -0
- package/dist/vue/use-tool-groups.js +42 -0
- package/dist/vue/use-tool-popup.js +123 -0
- package/dist/vue/use-toolbar-scroll.js +72 -0
- package/package.json +12 -6
- package/src/env.d.ts +14 -0
- package/src/index.ts +203 -13
- package/src/tools/__test__/builtin.test.ts +208 -66
- package/src/tools/__test__/context.test.ts +62 -2
- package/src/tools/builtin.ts +350 -6
- package/src/tools/context.ts +89 -11
- package/src/tools/download.ts +82 -0
- package/src/tools/export.worker.ts +61 -0
- package/src/tools/registry.ts +16 -1
- package/src/types/index.ts +6 -0
- package/src/types/sheet.ts +5 -3
- package/src/vue/__test__/formula-bar.test.ts +455 -0
- package/src/vue/__test__/import-file.test.ts +183 -0
- package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
- package/src/vue/__test__/insert-image-popup.test.ts +89 -0
- package/src/vue/__test__/insert-image.test.ts +100 -0
- package/src/vue/__test__/sheet-component.test.ts +716 -27
- package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
- package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
- package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
- package/src/vue/formula-bar.vue +473 -0
- package/src/vue/formula-suggest-list.vue +64 -0
- package/src/vue/import-file.ts +173 -0
- package/src/vue/insert-count-menu-item.vue +69 -0
- package/src/vue/insert-image.ts +90 -0
- package/src/vue/parsing.ts +11 -0
- package/src/vue/popup-helpers.ts +50 -0
- package/src/vue/popups/border-popup.vue +93 -0
- package/src/vue/popups/border-preset-glyph.vue +111 -0
- package/src/vue/popups/export-popup.vue +52 -0
- package/src/vue/popups/fill-color-popup.vue +39 -0
- package/src/vue/popups/find-popup.vue +111 -0
- package/src/vue/popups/font-color-popup.vue +39 -0
- package/src/vue/popups/font-size-popup.vue +52 -0
- package/src/vue/popups/import.worker.ts +66 -0
- package/src/vue/popups/insert-image-popup.vue +39 -0
- package/src/vue/sheet-context-menu.ts +229 -0
- package/src/vue/sheet-tabs.vue +162 -0
- package/src/vue/sheet-toolbar.vue +91 -0
- package/src/vue/sheet.vue +163 -148
- package/src/vue/style.scss +646 -3
- package/src/vue/style.ts +12 -0
- package/src/vue/use-find-replace.ts +200 -0
- package/src/vue/use-formula-ref-select.ts +34 -0
- package/src/vue/use-formula-suggest.ts +150 -0
- package/src/vue/use-sheet-grid.ts +243 -0
- package/src/vue/use-sheet-state.ts +161 -0
- package/src/vue/use-sheet-tabs-bar.ts +115 -0
- package/src/vue/use-tool-groups.ts +61 -0
- package/src/vue/use-tool-popup.ts +150 -0
- package/src/vue/use-toolbar-scroll.ts +95 -0
- package/dist/core/address.d.ts +0 -60
- package/dist/core/address.js +0 -142
- package/dist/core/address.js.map +0 -1
- package/dist/core/cell-store.d.ts +0 -61
- package/dist/core/cell-store.js +0 -135
- package/dist/core/cell-store.js.map +0 -1
- package/dist/core/command/default-registry.d.ts +0 -10
- package/dist/core/command/default-registry.js +0 -18
- package/dist/core/command/default-registry.js.map +0 -1
- package/dist/core/command/history.d.ts +0 -64
- package/dist/core/command/history.js +0 -127
- package/dist/core/command/history.js.map +0 -1
- package/dist/core/command/merge-cells.d.ts +0 -21
- package/dist/core/command/merge-cells.js +0 -74
- package/dist/core/command/merge-cells.js.map +0 -1
- package/dist/core/command/registry.d.ts +0 -17
- package/dist/core/command/registry.js +0 -26
- package/dist/core/command/registry.js.map +0 -1
- package/dist/core/command/set-cell-formula.d.ts +0 -17
- package/dist/core/command/set-cell-formula.js +0 -29
- package/dist/core/command/set-cell-formula.js.map +0 -1
- package/dist/core/command/set-cell-value.d.ts +0 -22
- package/dist/core/command/set-cell-value.js +0 -35
- package/dist/core/command/set-cell-value.js.map +0 -1
- package/dist/core/command/types.d.ts +0 -68
- package/dist/core/events.js +0 -29
- package/dist/core/events.js.map +0 -1
- package/dist/core/formula/ast.d.ts +0 -58
- package/dist/core/formula/ast.js +0 -38
- package/dist/core/formula/ast.js.map +0 -1
- package/dist/core/formula/dependency-graph.d.ts +0 -70
- package/dist/core/formula/dependency-graph.js +0 -269
- package/dist/core/formula/dependency-graph.js.map +0 -1
- package/dist/core/formula/errors.d.ts +0 -25
- package/dist/core/formula/errors.js +0 -36
- package/dist/core/formula/errors.js.map +0 -1
- package/dist/core/formula/evaluator.d.ts +0 -41
- package/dist/core/formula/evaluator.js +0 -154
- package/dist/core/formula/evaluator.js.map +0 -1
- package/dist/core/formula/functions.d.ts +0 -30
- package/dist/core/formula/functions.js +0 -209
- package/dist/core/formula/functions.js.map +0 -1
- package/dist/core/formula/parser.d.ts +0 -7
- package/dist/core/formula/parser.js +0 -208
- package/dist/core/formula/parser.js.map +0 -1
- package/dist/core/formula/tokenizer.d.ts +0 -37
- package/dist/core/formula/tokenizer.js +0 -146
- package/dist/core/formula/tokenizer.js.map +0 -1
- package/dist/core/merge-manager.d.ts +0 -64
- package/dist/core/merge-manager.js +0 -111
- package/dist/core/merge-manager.js.map +0 -1
- package/dist/core/selection.d.ts +0 -31
- package/dist/core/selection.js +0 -62
- package/dist/core/selection.js.map +0 -1
- package/dist/core/sheet.d.ts +0 -105
- package/dist/core/sheet.js +0 -202
- package/dist/core/sheet.js.map +0 -1
- package/dist/core/workbook.d.ts +0 -43
- package/dist/core/workbook.js +0 -77
- package/dist/core/workbook.js.map +0 -1
- package/dist/grid/sheet-grid.d.ts +0 -59
- package/dist/grid/sheet-grid.js +0 -230
- package/dist/grid/sheet-grid.js.map +0 -1
- package/dist/tools/builtin.js.map +0 -1
- package/dist/tools/context.js.map +0 -1
- package/dist/tools/registry.js.map +0 -1
- package/dist/vue/index.d.ts +0 -2
- package/dist/vue/sheet.js.map +0 -1
- package/dist/vue/sheet.vue.d.ts +0 -24
- package/src/core/__test__/address.test.ts +0 -113
- package/src/core/__test__/cell-store.test.ts +0 -97
- package/src/core/__test__/command.test.ts +0 -377
- package/src/core/__test__/formula-adversarial.test.ts +0 -265
- package/src/core/__test__/formula-engine.test.ts +0 -470
- package/src/core/__test__/formula-functions.test.ts +0 -178
- package/src/core/__test__/formula-parser.test.ts +0 -160
- package/src/core/__test__/history.test.ts +0 -225
- package/src/core/__test__/merge-manager.test.ts +0 -106
- package/src/core/__test__/sheet.test.ts +0 -142
- package/src/core/address.ts +0 -166
- package/src/core/cell-store.ts +0 -173
- package/src/core/command/default-registry.ts +0 -15
- package/src/core/command/history.ts +0 -167
- package/src/core/command/index.ts +0 -7
- package/src/core/command/merge-cells.ts +0 -74
- package/src/core/command/registry.ts +0 -30
- package/src/core/command/set-cell-formula.ts +0 -29
- package/src/core/command/set-cell-value.ts +0 -38
- package/src/core/command/types.ts +0 -73
- package/src/core/events.ts +0 -37
- package/src/core/formula/ast.ts +0 -66
- package/src/core/formula/dependency-graph.ts +0 -329
- package/src/core/formula/errors.ts +0 -41
- package/src/core/formula/evaluator.ts +0 -219
- package/src/core/formula/functions.ts +0 -266
- package/src/core/formula/index.ts +0 -7
- package/src/core/formula/parser.ts +0 -198
- package/src/core/formula/tokenizer.ts +0 -160
- package/src/core/merge-manager.ts +0 -158
- package/src/core/selection.ts +0 -66
- package/src/core/sheet.ts +0 -257
- package/src/core/workbook.ts +0 -99
- package/src/grid/__test__/canvas-mock.ts +0 -49
- package/src/grid/__test__/setup.ts +0 -3
- package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
- package/src/grid/__test__/sheet-grid.test.ts +0 -177
- package/src/grid/sheet-grid.ts +0 -283
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/tools/export.worker.ts
|
|
2
|
+
self.onmessage = (e) => {
|
|
3
|
+
Promise.all([import("@veltra/sheet-core/core/io/export"), import("@veltra/sheet-core/core/workbook")]).then(([{ exportWorkbookXlsx }, { Workbook }]) => {
|
|
4
|
+
const wb = new Workbook();
|
|
5
|
+
const sheets = e.data.sheets;
|
|
6
|
+
for (let i = 0; i < sheets.length; i++) {
|
|
7
|
+
const { name, snapshot } = sheets[i];
|
|
8
|
+
const sheet = i === 0 ? wb.activeSheet : wb.addSheet(name);
|
|
9
|
+
if (i === 0 && sheet.name !== name) wb.renameSheet(sheet.name, name);
|
|
10
|
+
sheet.restore(snapshot);
|
|
11
|
+
}
|
|
12
|
+
wb.activateSheet(wb.getSheets()[Math.min(e.data.activeIndex, wb.sheetCount - 1)].name);
|
|
13
|
+
return exportWorkbookXlsx(wb);
|
|
14
|
+
}).then((buffer) => {
|
|
15
|
+
const exact = buffer.slice();
|
|
16
|
+
const response = {
|
|
17
|
+
ok: true,
|
|
18
|
+
buffer: exact
|
|
19
|
+
};
|
|
20
|
+
self.postMessage(response, [exact.buffer]);
|
|
21
|
+
}).catch((err) => {
|
|
22
|
+
const response = {
|
|
23
|
+
ok: false,
|
|
24
|
+
error: err instanceof Error ? err.message : String(err)
|
|
25
|
+
};
|
|
26
|
+
self.postMessage(response);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { SheetContext } from "./context.js";
|
|
|
10
10
|
* - 默认注册表全局共享:所有 USheet 实例渲染同一组工具
|
|
11
11
|
* (与 defaultCommandRegistry 同语义;每个实例的 SheetContext 各自绑定当前 sheet)
|
|
12
12
|
*/
|
|
13
|
+
/** 弹层型工具类型:vue 层按类型渲染弹层面板(面板交互走 SheetContext 命令入口) */
|
|
14
|
+
type SheetToolPopupType = 'fill-color' | 'border' | 'font-color' | 'font-size' | 'find' | 'insert-image' | 'export';
|
|
13
15
|
/** 工具定义 */
|
|
14
16
|
interface SheetTool {
|
|
15
17
|
/** 唯一 id;同 id 重复注册视为替换(保留原位置) */
|
|
@@ -28,6 +30,10 @@ interface SheetTool {
|
|
|
28
30
|
visible?(ctx: SheetContext): boolean;
|
|
29
31
|
/** 是否禁用(工具栏状态刷新时求值);缺省可用 */
|
|
30
32
|
disabled?(ctx: SheetContext): boolean;
|
|
33
|
+
/** 是否处于激活/高亮状态(工具栏状态刷新时求值,vue 层渲染 is-active);缺省 false */
|
|
34
|
+
active?(ctx: SheetContext): boolean;
|
|
35
|
+
/** 弹层型工具:vue 层渲染弹层面板,onClick 不执行(面板交互走 SheetContext) */
|
|
36
|
+
popup?: SheetToolPopupType;
|
|
31
37
|
/** 点击执行;ctx 是唯一操作入口(写操作全走命令系统,天然可撤销) */
|
|
32
38
|
onClick(ctx: SheetContext): void;
|
|
33
39
|
}
|
|
@@ -36,8 +42,6 @@ interface SheetToolGroup {
|
|
|
36
42
|
name: string;
|
|
37
43
|
tools: SheetTool[];
|
|
38
44
|
}
|
|
39
|
-
/** 缺省分组名 */
|
|
40
|
-
declare const DEFAULT_TOOL_GROUP = "default";
|
|
41
45
|
declare class ToolRegistry {
|
|
42
46
|
private entries;
|
|
43
47
|
private seq;
|
|
@@ -64,5 +68,4 @@ declare function registerTool(tool: SheetTool): void;
|
|
|
64
68
|
/** 从默认注册表注销工具;id 不存在时返回 false */
|
|
65
69
|
declare function unregisterTool(id: string): boolean;
|
|
66
70
|
//#endregion
|
|
67
|
-
export {
|
|
68
|
-
//# sourceMappingURL=registry.d.ts.map
|
|
71
|
+
export { SheetTool, SheetToolGroup, SheetToolPopupType, ToolRegistry, defaultToolRegistry, registerTool, unregisterTool };
|
package/dist/tools/registry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TypedEventEmitter } from "
|
|
1
|
+
import { TypedEventEmitter } from "@veltra/sheet-core/core/events";
|
|
2
2
|
//#region src/tools/registry.ts
|
|
3
3
|
/** 缺省分组名 */
|
|
4
4
|
const DEFAULT_TOOL_GROUP = "default";
|
|
@@ -73,5 +73,3 @@ function unregisterTool(id) {
|
|
|
73
73
|
}
|
|
74
74
|
//#endregion
|
|
75
75
|
export { DEFAULT_TOOL_GROUP, ToolRegistry, defaultToolRegistry, registerTool, unregisterTool };
|
|
76
|
-
|
|
77
|
-
//# sourceMappingURL=registry.js.map
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/sheet.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Sheet } from "../core/sheet.js";
|
|
2
|
-
import { Workbook } from "../core/workbook.js";
|
|
3
|
-
import { SheetGrid } from "../grid/sheet-grid.js";
|
|
4
1
|
import { SheetContext } from "../tools/context.js";
|
|
5
2
|
import { ComputedRef } from "vue";
|
|
6
3
|
import { DeconstructValue } from "@veltra/utils";
|
|
4
|
+
import { SheetGrid } from "@veltra/sheet-core/grid/sheet-grid";
|
|
5
|
+
import { Workbook } from "@veltra/sheet-core/core/workbook";
|
|
6
|
+
import { Sheet } from "@veltra/sheet-core/core/sheet";
|
|
7
7
|
//#region src/types/sheet.d.ts
|
|
8
8
|
/** 电子表格组件属性 */
|
|
9
9
|
interface SheetProps {
|
|
@@ -15,6 +15,8 @@ interface SheetProps {
|
|
|
15
15
|
cols?: number;
|
|
16
16
|
/** 是否显示工具栏,默认 true */
|
|
17
17
|
showToolbar?: boolean;
|
|
18
|
+
/** 是否显示顶部公式栏(名称框 + fx 输入栏),默认 true */
|
|
19
|
+
showFormulaBar?: boolean;
|
|
18
20
|
/** 是否显示底部 sheet 标签栏,默认 true */
|
|
19
21
|
showTabs?: boolean;
|
|
20
22
|
}
|
|
@@ -39,5 +41,4 @@ interface _SheetExposed {
|
|
|
39
41
|
/** 电子表格组件暴露的属性和方法 */
|
|
40
42
|
type SheetExposed = DeconstructValue<_SheetExposed>;
|
|
41
43
|
//#endregion
|
|
42
|
-
export { SheetEmits, SheetExposed, SheetProps, _SheetExposed };
|
|
43
|
-
//# sourceMappingURL=sheet.d.ts.map
|
|
44
|
+
export { SheetEmits, SheetExposed, SheetProps, _SheetExposed };
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import _sfc_main$1 from "./formula-suggest-list.js";
|
|
2
|
+
import { insertRefText, isRefSelectContext } from "./use-formula-ref-select.js";
|
|
3
|
+
import { applySuggest, filterFormulaSuggestions, getSuggestContext, moveSuggestIndex } from "./use-formula-suggest.js";
|
|
4
|
+
import { formatAddress, formatRange, parseRange } from "@veltra/sheet-core/core/address";
|
|
5
|
+
import { computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, shallowRef, unref, useTemplateRef, vModelText, watch, withDirectives, withModifiers } from "vue";
|
|
6
|
+
import { message } from "@veltra/desktop";
|
|
7
|
+
import { bem } from "@veltra/utils";
|
|
8
|
+
//#region src/vue/formula-bar.vue
|
|
9
|
+
const _hoisted_1 = ["disabled", "readonly"];
|
|
10
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
11
|
+
name: "UFormulaBar",
|
|
12
|
+
__name: "formula-bar",
|
|
13
|
+
props: {
|
|
14
|
+
sheet: {},
|
|
15
|
+
context: {}
|
|
16
|
+
},
|
|
17
|
+
setup(__props, { expose: __expose }) {
|
|
18
|
+
/**
|
|
19
|
+
* 公式栏(名称框 + fx 输入栏),USheet 顶部结构之一。
|
|
20
|
+
*
|
|
21
|
+
* - 名称框:显示当前选区(单格 A1 / 区域 A1:B2);合法地址回车跳转。
|
|
22
|
+
* - fx 输入栏:活动格内容;Enter/✓ 提交、Esc/✗ 取消、失焦提交。
|
|
23
|
+
* - 函数补全:`=` 开头且光标在函数名 token 上时弹出候选(↑↓ / Tab / Enter / 点击)。
|
|
24
|
+
* - 引用选择:光标在运算符/`(`/`,` 后时,画布点选/拖选插入引用(blur 抑制防误提交)。
|
|
25
|
+
* - 网格编辑镜像:只读显示网格编辑器文本。
|
|
26
|
+
*/
|
|
27
|
+
const props = __props;
|
|
28
|
+
const cls = bem("sheet");
|
|
29
|
+
const fxRef = useTemplateRef("fxRef");
|
|
30
|
+
const selection = shallowRef({
|
|
31
|
+
activeCell: null,
|
|
32
|
+
ranges: []
|
|
33
|
+
});
|
|
34
|
+
const nameDraft = ref("");
|
|
35
|
+
const nameFocused = ref(false);
|
|
36
|
+
const editing = ref(false);
|
|
37
|
+
let editAddr = null;
|
|
38
|
+
const mirroring = ref(false);
|
|
39
|
+
let mirrorAddr = null;
|
|
40
|
+
const fxDraft = ref("");
|
|
41
|
+
/** 输入面板是否因内容增高而浮出(文档流仍保持单行) */
|
|
42
|
+
const fxExpanded = ref(false);
|
|
43
|
+
/** textarea 光标(selectionStart);输入/点击/方向键后同步 */
|
|
44
|
+
const fxCursor = ref(0);
|
|
45
|
+
/** 画布 pointerdown 置位:下一帧 blur 跳过提交(引用选择模式) */
|
|
46
|
+
let suppressBlurCommit = false;
|
|
47
|
+
const suggestItems = shallowRef([]);
|
|
48
|
+
const suggestIndex = ref(0);
|
|
49
|
+
const suggestOpen = computed(() => suggestItems.value.length > 0);
|
|
50
|
+
const fxDisabled = computed(() => selection.value.activeCell == null);
|
|
51
|
+
/** 编排层查询:fx 编辑中且光标处于可插入引用位置 */
|
|
52
|
+
function isRefSelecting() {
|
|
53
|
+
return editing.value && !mirroring.value && isRefSelectContext(fxDraft.value, fxCursor.value);
|
|
54
|
+
}
|
|
55
|
+
/** 网格 pointerdown(capture)→ 挂起即将到来的 blur 提交 */
|
|
56
|
+
function beginBlurSuppress() {
|
|
57
|
+
if (!isRefSelecting()) return;
|
|
58
|
+
suppressBlurCommit = true;
|
|
59
|
+
}
|
|
60
|
+
/** 选区拦截回调:插入 A1 / A1:B2 到光标处 */
|
|
61
|
+
function handleRefSelect(range) {
|
|
62
|
+
if (!editing.value || mirroring.value) return;
|
|
63
|
+
const rangeText = formatRange(range);
|
|
64
|
+
const selEnd = fxRef.value?.selectionEnd ?? fxCursor.value;
|
|
65
|
+
const result = insertRefText(fxDraft.value, fxCursor.value, rangeText, selEnd);
|
|
66
|
+
fxDraft.value = result.text;
|
|
67
|
+
fxCursor.value = result.cursor;
|
|
68
|
+
closeSuggest();
|
|
69
|
+
nextTick(() => {
|
|
70
|
+
const input = fxRef.value;
|
|
71
|
+
if (!input) return;
|
|
72
|
+
input.focus();
|
|
73
|
+
input.setSelectionRange(result.cursor, result.cursor);
|
|
74
|
+
autosizeFx();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function nameText(state) {
|
|
78
|
+
const range = state.ranges[0];
|
|
79
|
+
if (range) return formatRange(range);
|
|
80
|
+
return state.activeCell ? formatAddress(state.activeCell) : "";
|
|
81
|
+
}
|
|
82
|
+
function cellText(addr) {
|
|
83
|
+
const data = props.context.getCellData(addr);
|
|
84
|
+
if (data?.f != null) return `=${data.f}`;
|
|
85
|
+
if (data?.v != null) return String(data.v);
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
function refreshName() {
|
|
89
|
+
nameDraft.value = nameText(selection.value);
|
|
90
|
+
}
|
|
91
|
+
function refreshFx() {
|
|
92
|
+
const active = selection.value.activeCell;
|
|
93
|
+
fxDraft.value = active ? cellText(active) : "";
|
|
94
|
+
fxCursor.value = fxDraft.value.length;
|
|
95
|
+
closeSuggest();
|
|
96
|
+
scheduleAutosize();
|
|
97
|
+
}
|
|
98
|
+
/** 每帧合并一次的 autosizeFx(批量 cell-change 同帧 N 次 nextTick → 1 次强制布局) */
|
|
99
|
+
let autosizePending = false;
|
|
100
|
+
function scheduleAutosize() {
|
|
101
|
+
if (autosizePending) return;
|
|
102
|
+
autosizePending = true;
|
|
103
|
+
nextTick(() => {
|
|
104
|
+
autosizePending = false;
|
|
105
|
+
autosizeFx();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 按内容增高浮出面板,不超过 CSS max-height。
|
|
110
|
+
* 增高走绝对定位,公式栏文档流高度始终单行。
|
|
111
|
+
*/
|
|
112
|
+
function autosizeFx() {
|
|
113
|
+
const el = fxRef.value;
|
|
114
|
+
if (!el) return;
|
|
115
|
+
el.style.height = "0px";
|
|
116
|
+
const styles = getComputedStyle(el);
|
|
117
|
+
const max = Number.parseFloat(styles.maxHeight);
|
|
118
|
+
const min = Number.parseFloat(styles.minHeight);
|
|
119
|
+
const content = el.scrollHeight;
|
|
120
|
+
const next = Number.isFinite(max) && max > 0 ? Math.min(content, max) : content;
|
|
121
|
+
el.style.height = `${next}px`;
|
|
122
|
+
fxExpanded.value = Number.isFinite(min) ? next > min + 1 : next > 1;
|
|
123
|
+
}
|
|
124
|
+
function syncFxCursor() {
|
|
125
|
+
const el = fxRef.value;
|
|
126
|
+
if (!el) return;
|
|
127
|
+
fxCursor.value = el.selectionStart;
|
|
128
|
+
refreshSuggest();
|
|
129
|
+
}
|
|
130
|
+
function handleFxInput() {
|
|
131
|
+
syncFxCursor();
|
|
132
|
+
autosizeFx();
|
|
133
|
+
}
|
|
134
|
+
function closeSuggest() {
|
|
135
|
+
suggestItems.value = [];
|
|
136
|
+
suggestIndex.value = 0;
|
|
137
|
+
}
|
|
138
|
+
function refreshSuggest() {
|
|
139
|
+
if (!editing.value || mirroring.value) {
|
|
140
|
+
closeSuggest();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const ctx = getSuggestContext(fxDraft.value, fxCursor.value);
|
|
144
|
+
if (!ctx) {
|
|
145
|
+
closeSuggest();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const items = filterFormulaSuggestions(ctx.prefix);
|
|
149
|
+
suggestItems.value = items;
|
|
150
|
+
if (suggestIndex.value >= items.length) suggestIndex.value = 0;
|
|
151
|
+
}
|
|
152
|
+
function confirmSuggest(item) {
|
|
153
|
+
const pick = item ?? suggestItems.value[suggestIndex.value];
|
|
154
|
+
if (!pick) return;
|
|
155
|
+
const ctx = getSuggestContext(fxDraft.value, fxCursor.value);
|
|
156
|
+
if (!ctx) return;
|
|
157
|
+
const result = applySuggest(fxDraft.value, ctx.start, ctx.end, pick.name);
|
|
158
|
+
fxDraft.value = result.text;
|
|
159
|
+
fxCursor.value = result.cursor;
|
|
160
|
+
closeSuggest();
|
|
161
|
+
nextTick(() => {
|
|
162
|
+
const el = fxRef.value;
|
|
163
|
+
if (!el) return;
|
|
164
|
+
el.focus();
|
|
165
|
+
el.setSelectionRange(result.cursor, result.cursor);
|
|
166
|
+
autosizeFx();
|
|
167
|
+
refreshSuggest();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function handleNameKeydown(event) {
|
|
171
|
+
if (event.key === "Enter") {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
commitName();
|
|
174
|
+
} else if (event.key === "Escape") {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
refreshName();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function handleNameBlur() {
|
|
180
|
+
nameFocused.value = false;
|
|
181
|
+
refreshName();
|
|
182
|
+
}
|
|
183
|
+
function commitName() {
|
|
184
|
+
const range = parseRange(nameDraft.value);
|
|
185
|
+
if (!range) {
|
|
186
|
+
message.warn("无效的单元格地址");
|
|
187
|
+
refreshName();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (range.start.row === range.end.row && range.start.col === range.end.col) props.context.selectCell(range.start);
|
|
191
|
+
else props.context.selectRange(range);
|
|
192
|
+
refreshName();
|
|
193
|
+
}
|
|
194
|
+
function handleFxFocus() {
|
|
195
|
+
if (fxDisabled.value) return;
|
|
196
|
+
mirroring.value = false;
|
|
197
|
+
mirrorAddr = null;
|
|
198
|
+
editing.value = true;
|
|
199
|
+
editAddr = selection.value.activeCell;
|
|
200
|
+
syncFxCursor();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 失焦提交;引用选择期间(画布 pointerdown 已挂起)跳过。
|
|
204
|
+
* ✓/✗ / 候选列表用 mousedown.prevent 拦截失焦。
|
|
205
|
+
*/
|
|
206
|
+
function handleFxBlur() {
|
|
207
|
+
if (!editing.value) return;
|
|
208
|
+
if (suppressBlurCommit) {
|
|
209
|
+
suppressBlurCommit = false;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
commitEdit();
|
|
213
|
+
}
|
|
214
|
+
function handleFxKeydown(event) {
|
|
215
|
+
if (suggestOpen.value) {
|
|
216
|
+
if (event.key === "ArrowDown") {
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
suggestIndex.value = moveSuggestIndex(suggestIndex.value, suggestItems.value.length, 1);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (event.key === "ArrowUp") {
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
suggestIndex.value = moveSuggestIndex(suggestIndex.value, suggestItems.value.length, -1);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
confirmSuggest();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (event.key === "Escape") {
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
closeSuggest();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
commitEdit();
|
|
240
|
+
} else if (event.key === "Escape") {
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
cancelEdit();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function commitEdit() {
|
|
246
|
+
if (!editing.value) return;
|
|
247
|
+
const addr = editAddr;
|
|
248
|
+
editing.value = false;
|
|
249
|
+
editAddr = null;
|
|
250
|
+
suppressBlurCommit = false;
|
|
251
|
+
closeSuggest();
|
|
252
|
+
if (addr) props.context.setCellValue(addr, fxDraft.value);
|
|
253
|
+
refreshFx();
|
|
254
|
+
}
|
|
255
|
+
function cancelEdit() {
|
|
256
|
+
if (!editing.value) return;
|
|
257
|
+
editing.value = false;
|
|
258
|
+
editAddr = null;
|
|
259
|
+
suppressBlurCommit = false;
|
|
260
|
+
closeSuggest();
|
|
261
|
+
refreshFx();
|
|
262
|
+
}
|
|
263
|
+
function handleSelectionChange(state) {
|
|
264
|
+
selection.value = state;
|
|
265
|
+
if (!nameFocused.value) refreshName();
|
|
266
|
+
if (editing.value) return;
|
|
267
|
+
mirroring.value = false;
|
|
268
|
+
mirrorAddr = null;
|
|
269
|
+
refreshFx();
|
|
270
|
+
}
|
|
271
|
+
function handleCellChange({ addr }) {
|
|
272
|
+
if (editing.value) return;
|
|
273
|
+
if (mirroring.value) {
|
|
274
|
+
if (mirrorAddr && addr.row === mirrorAddr.row && addr.col === mirrorAddr.col) {
|
|
275
|
+
mirroring.value = false;
|
|
276
|
+
mirrorAddr = null;
|
|
277
|
+
refreshFx();
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const active = selection.value.activeCell;
|
|
282
|
+
if (!active || addr.row !== active.row || addr.col !== active.col) return;
|
|
283
|
+
refreshFx();
|
|
284
|
+
}
|
|
285
|
+
function mirrorGridEdit(addr) {
|
|
286
|
+
if (editing.value || fxDisabled.value) return;
|
|
287
|
+
mirroring.value = true;
|
|
288
|
+
mirrorAddr = addr;
|
|
289
|
+
fxDraft.value = cellText(addr);
|
|
290
|
+
closeSuggest();
|
|
291
|
+
}
|
|
292
|
+
function exitMirror(addr) {
|
|
293
|
+
if (!mirroring.value) return;
|
|
294
|
+
if (mirrorAddr && addr.row === mirrorAddr.row && addr.col === mirrorAddr.col) {
|
|
295
|
+
mirroring.value = false;
|
|
296
|
+
mirrorAddr = null;
|
|
297
|
+
refreshFx();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
__expose({
|
|
301
|
+
mirrorGridEdit,
|
|
302
|
+
exitMirror,
|
|
303
|
+
isRefSelecting,
|
|
304
|
+
beginBlurSuppress,
|
|
305
|
+
handleRefSelect
|
|
306
|
+
});
|
|
307
|
+
let dispose = [];
|
|
308
|
+
function bindSheet(sheet) {
|
|
309
|
+
for (const off of dispose) off();
|
|
310
|
+
dispose = [sheet.on("selection-change", handleSelectionChange), sheet.on("cell-change", handleCellChange)];
|
|
311
|
+
}
|
|
312
|
+
watch(() => props.sheet, (sheet, prev) => {
|
|
313
|
+
if (sheet === prev) return;
|
|
314
|
+
editing.value = false;
|
|
315
|
+
editAddr = null;
|
|
316
|
+
mirroring.value = false;
|
|
317
|
+
mirrorAddr = null;
|
|
318
|
+
suppressBlurCommit = false;
|
|
319
|
+
closeSuggest();
|
|
320
|
+
bindSheet(sheet);
|
|
321
|
+
selection.value = sheet.getSelection();
|
|
322
|
+
refreshName();
|
|
323
|
+
refreshFx();
|
|
324
|
+
});
|
|
325
|
+
onMounted(() => {
|
|
326
|
+
bindSheet(props.sheet);
|
|
327
|
+
selection.value = props.sheet.getSelection();
|
|
328
|
+
refreshName();
|
|
329
|
+
refreshFx();
|
|
330
|
+
});
|
|
331
|
+
onBeforeUnmount(() => {
|
|
332
|
+
for (const off of dispose) off();
|
|
333
|
+
dispose = [];
|
|
334
|
+
});
|
|
335
|
+
return (_ctx, _cache) => {
|
|
336
|
+
return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).e("formula-bar")) }, [
|
|
337
|
+
withDirectives(createElementVNode("input", {
|
|
338
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => nameDraft.value = $event),
|
|
339
|
+
class: normalizeClass(unref(cls).e("name-box")),
|
|
340
|
+
spellcheck: "false",
|
|
341
|
+
autocomplete: "off",
|
|
342
|
+
title: "单元格地址或区域(如 B3 或 B3:D5),回车跳转",
|
|
343
|
+
onFocus: _cache[1] || (_cache[1] = ($event) => nameFocused.value = true),
|
|
344
|
+
onBlur: handleNameBlur,
|
|
345
|
+
onKeydown: handleNameKeydown
|
|
346
|
+
}, null, 34), [[vModelText, nameDraft.value]]),
|
|
347
|
+
createElementVNode("span", { class: normalizeClass(unref(cls).e("fx-label")) }, "fx", 2),
|
|
348
|
+
createElementVNode("div", { class: normalizeClass(unref(cls).e("fx-editor")) }, [createCommentVNode(" 文档流只占单行;面板绝对定位向下浮起,不撑开公式栏 "), createElementVNode("div", { class: normalizeClass([unref(cls).e("fx-input-panel"), unref(bem).is("expanded", fxExpanded.value)]) }, [withDirectives(createElementVNode("textarea", {
|
|
349
|
+
ref_key: "fxRef",
|
|
350
|
+
ref: fxRef,
|
|
351
|
+
"onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => fxDraft.value = $event),
|
|
352
|
+
class: normalizeClass(unref(cls).e("fx-input")),
|
|
353
|
+
disabled: fxDisabled.value,
|
|
354
|
+
readonly: mirroring.value,
|
|
355
|
+
rows: "1",
|
|
356
|
+
spellcheck: "false",
|
|
357
|
+
autocomplete: "off",
|
|
358
|
+
title: "活动单元格内容('=' 开头为公式);Enter 提交,Esc 取消",
|
|
359
|
+
onFocus: handleFxFocus,
|
|
360
|
+
onBlur: handleFxBlur,
|
|
361
|
+
onKeydown: handleFxKeydown,
|
|
362
|
+
onInput: handleFxInput,
|
|
363
|
+
onClick: syncFxCursor,
|
|
364
|
+
onKeyup: syncFxCursor,
|
|
365
|
+
onSelect: syncFxCursor
|
|
366
|
+
}, null, 42, _hoisted_1), [[vModelText, fxDraft.value]]), createVNode(_sfc_main$1, {
|
|
367
|
+
items: suggestItems.value,
|
|
368
|
+
"active-index": suggestIndex.value,
|
|
369
|
+
onSelect: confirmSuggest,
|
|
370
|
+
onHover: _cache[3] || (_cache[3] = (i) => suggestIndex.value = i)
|
|
371
|
+
}, null, 8, ["items", "active-index"])], 2)], 2),
|
|
372
|
+
editing.value ? (openBlock(), createElementBlock("button", {
|
|
373
|
+
key: 0,
|
|
374
|
+
type: "button",
|
|
375
|
+
class: normalizeClass(unref(cls).e("fx-btn")),
|
|
376
|
+
title: "提交(Enter)",
|
|
377
|
+
onMousedown: _cache[4] || (_cache[4] = withModifiers(() => {}, ["prevent"])),
|
|
378
|
+
onClick: commitEdit
|
|
379
|
+
}, " ✓ ", 34)) : createCommentVNode("v-if", true),
|
|
380
|
+
editing.value ? (openBlock(), createElementBlock("button", {
|
|
381
|
+
key: 1,
|
|
382
|
+
type: "button",
|
|
383
|
+
class: normalizeClass(unref(cls).e("fx-btn")),
|
|
384
|
+
title: "取消(Esc)",
|
|
385
|
+
onMousedown: _cache[5] || (_cache[5] = withModifiers(() => {}, ["prevent"])),
|
|
386
|
+
onClick: cancelEdit
|
|
387
|
+
}, " ✗ ", 34)) : createCommentVNode("v-if", true)
|
|
388
|
+
], 2);
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
//#endregion
|
|
393
|
+
export { _sfc_main as default };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, nextTick, normalizeClass, openBlock, renderList, toDisplayString, unref, watch, withCtx, withModifiers } from "vue";
|
|
2
|
+
import { UScroll } from "@veltra/desktop";
|
|
3
|
+
import { bem } from "@veltra/utils";
|
|
4
|
+
//#region src/vue/formula-suggest-list.vue
|
|
5
|
+
const _hoisted_1 = [
|
|
6
|
+
"aria-selected",
|
|
7
|
+
"onMousedown",
|
|
8
|
+
"onMouseenter"
|
|
9
|
+
];
|
|
10
|
+
const SUGGEST_MAX_HEIGHT = 240;
|
|
11
|
+
/** 单项约高(含 padding + 签名/描述两行);用于估算 UScroll height */
|
|
12
|
+
const SUGGEST_ITEM_HEIGHT = 48;
|
|
13
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
14
|
+
name: "UFormulaSuggestList",
|
|
15
|
+
__name: "formula-suggest-list",
|
|
16
|
+
props: {
|
|
17
|
+
items: {},
|
|
18
|
+
activeIndex: {}
|
|
19
|
+
},
|
|
20
|
+
emits: ["select", "hover"],
|
|
21
|
+
setup(__props, { emit: __emit }) {
|
|
22
|
+
const props = __props;
|
|
23
|
+
const emit = __emit;
|
|
24
|
+
const cls = bem("sheet");
|
|
25
|
+
/** 候选列表最大可视高度;内容更短时收缩,避免空前缀少量项时大片留白 */
|
|
26
|
+
const scrollHeight = computed(() => Math.min(Math.max(props.items.length, 1) * SUGGEST_ITEM_HEIGHT, SUGGEST_MAX_HEIGHT));
|
|
27
|
+
const itemEls = [];
|
|
28
|
+
function setItemRef(index, el) {
|
|
29
|
+
itemEls[index] = el instanceof HTMLElement ? el : null;
|
|
30
|
+
}
|
|
31
|
+
watch(() => [props.activeIndex, props.items], async ([index]) => {
|
|
32
|
+
await nextTick();
|
|
33
|
+
itemEls[index]?.scrollIntoView({ block: "nearest" });
|
|
34
|
+
});
|
|
35
|
+
return (_ctx, _cache) => {
|
|
36
|
+
return __props.items.length > 0 ? (openBlock(), createElementBlock("div", {
|
|
37
|
+
key: 0,
|
|
38
|
+
class: normalizeClass(unref(cls).e("fx-suggest")),
|
|
39
|
+
role: "listbox",
|
|
40
|
+
onMousedown: _cache[0] || (_cache[0] = withModifiers(() => {}, ["prevent"]))
|
|
41
|
+
}, [createCommentVNode(" 滚动用 desktop UScroll(自定义滚动条;原生 overflow 滚动条在浮层里常看不见) "), createVNode(unref(UScroll), {
|
|
42
|
+
height: scrollHeight.value,
|
|
43
|
+
class: normalizeClass(unref(cls).e("fx-suggest-scroll"))
|
|
44
|
+
}, {
|
|
45
|
+
default: withCtx(() => [createElementVNode("ul", { class: normalizeClass(unref(cls).e("fx-suggest-list")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item, index) => {
|
|
46
|
+
return openBlock(), createElementBlock("li", {
|
|
47
|
+
key: item.name,
|
|
48
|
+
ref_for: true,
|
|
49
|
+
ref: (el) => setItemRef(index, el),
|
|
50
|
+
class: normalizeClass([unref(cls).e("fx-suggest-item"), unref(bem).is("active", index === __props.activeIndex)]),
|
|
51
|
+
role: "option",
|
|
52
|
+
"aria-selected": index === __props.activeIndex,
|
|
53
|
+
onMousedown: withModifiers(($event) => emit("select", item), ["prevent"]),
|
|
54
|
+
onMouseenter: ($event) => emit("hover", index)
|
|
55
|
+
}, [createElementVNode("span", { class: normalizeClass(unref(cls).e("fx-suggest-sig")) }, toDisplayString(item.signature), 3), item.description ? (openBlock(), createElementBlock("span", {
|
|
56
|
+
key: 0,
|
|
57
|
+
class: normalizeClass(unref(cls).e("fx-suggest-desc"))
|
|
58
|
+
}, toDisplayString(item.description), 3)) : createCommentVNode("v-if", true)], 42, _hoisted_1);
|
|
59
|
+
}), 128))], 2)]),
|
|
60
|
+
_: 1
|
|
61
|
+
}, 8, ["height", "class"])], 34)) : createCommentVNode("v-if", true);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
//#endregion
|
|
66
|
+
export { _sfc_main as default };
|