@veltra/sheet 2.0.1 → 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 -26
- package/dist/index.js +3 -23
- 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 -164
- package/dist/vue/style.d.ts +11 -1
- package/dist/vue/style.js +10 -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 -7
- package/src/env.d.ts +11 -0
- package/src/index.ts +203 -14
- 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 +161 -186
- package/src/vue/style.scss +646 -3
- package/src/vue/style.ts +11 -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/fill.d.ts +0 -34
- package/dist/core/fill.js +0 -320
- package/dist/core/fill.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 -116
- package/dist/core/sheet.js +0 -224
- 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 -81
- package/dist/grid/sheet-grid.js +0 -331
- package/dist/grid/sheet-grid.js.map +0 -1
- package/dist/grid/vtable-theme.js +0 -80
- package/dist/grid/vtable-theme.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__/fill.test.ts +0 -140
- 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/fill.ts +0 -314
- 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 -282
- 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 -275
- package/src/grid/sheet-grid.ts +0 -407
- package/src/grid/vtable-theme.ts +0 -61
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="cls.e('formula-bar')">
|
|
3
|
+
<input
|
|
4
|
+
v-model="nameDraft"
|
|
5
|
+
:class="cls.e('name-box')"
|
|
6
|
+
spellcheck="false"
|
|
7
|
+
autocomplete="off"
|
|
8
|
+
title="单元格地址或区域(如 B3 或 B3:D5),回车跳转"
|
|
9
|
+
@focus="nameFocused = true"
|
|
10
|
+
@blur="handleNameBlur"
|
|
11
|
+
@keydown="handleNameKeydown"
|
|
12
|
+
/>
|
|
13
|
+
<span :class="cls.e('fx-label')">fx</span>
|
|
14
|
+
<div :class="cls.e('fx-editor')">
|
|
15
|
+
<!-- 文档流只占单行;面板绝对定位向下浮起,不撑开公式栏 -->
|
|
16
|
+
<div :class="[cls.e('fx-input-panel'), bem.is('expanded', fxExpanded)]">
|
|
17
|
+
<textarea
|
|
18
|
+
ref="fxRef"
|
|
19
|
+
v-model="fxDraft"
|
|
20
|
+
:class="cls.e('fx-input')"
|
|
21
|
+
:disabled="fxDisabled"
|
|
22
|
+
:readonly="mirroring"
|
|
23
|
+
rows="1"
|
|
24
|
+
spellcheck="false"
|
|
25
|
+
autocomplete="off"
|
|
26
|
+
title="活动单元格内容('=' 开头为公式);Enter 提交,Esc 取消"
|
|
27
|
+
@focus="handleFxFocus"
|
|
28
|
+
@blur="handleFxBlur"
|
|
29
|
+
@keydown="handleFxKeydown"
|
|
30
|
+
@input="handleFxInput"
|
|
31
|
+
@click="syncFxCursor"
|
|
32
|
+
@keyup="syncFxCursor"
|
|
33
|
+
@select="syncFxCursor"
|
|
34
|
+
/>
|
|
35
|
+
<u-formula-suggest-list
|
|
36
|
+
:items="suggestItems"
|
|
37
|
+
:active-index="suggestIndex"
|
|
38
|
+
@select="confirmSuggest"
|
|
39
|
+
@hover="(i) => (suggestIndex = i)"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<button
|
|
44
|
+
v-if="editing"
|
|
45
|
+
type="button"
|
|
46
|
+
:class="cls.e('fx-btn')"
|
|
47
|
+
title="提交(Enter)"
|
|
48
|
+
@mousedown.prevent
|
|
49
|
+
@click="commitEdit"
|
|
50
|
+
>
|
|
51
|
+
✓
|
|
52
|
+
</button>
|
|
53
|
+
<button
|
|
54
|
+
v-if="editing"
|
|
55
|
+
type="button"
|
|
56
|
+
:class="cls.e('fx-btn')"
|
|
57
|
+
title="取消(Esc)"
|
|
58
|
+
@mousedown.prevent
|
|
59
|
+
@click="cancelEdit"
|
|
60
|
+
>
|
|
61
|
+
✗
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<script lang="ts" setup>
|
|
67
|
+
import { message } from '@veltra/desktop'
|
|
68
|
+
import {
|
|
69
|
+
formatAddress,
|
|
70
|
+
formatRange,
|
|
71
|
+
parseRange,
|
|
72
|
+
type CellAddress,
|
|
73
|
+
type CellRange
|
|
74
|
+
} from '@veltra/sheet-core/core/address'
|
|
75
|
+
import type { SelectionState } from '@veltra/sheet-core/core/selection'
|
|
76
|
+
import type { Sheet } from '@veltra/sheet-core/core/sheet'
|
|
77
|
+
import { bem } from '@veltra/utils'
|
|
78
|
+
import {
|
|
79
|
+
computed,
|
|
80
|
+
nextTick,
|
|
81
|
+
onBeforeUnmount,
|
|
82
|
+
onMounted,
|
|
83
|
+
ref,
|
|
84
|
+
shallowRef,
|
|
85
|
+
useTemplateRef,
|
|
86
|
+
watch
|
|
87
|
+
} from 'vue'
|
|
88
|
+
|
|
89
|
+
import type { SheetContext } from '../tools/context'
|
|
90
|
+
import UFormulaSuggestList from './formula-suggest-list.vue'
|
|
91
|
+
import { insertRefText, isRefSelectContext } from './use-formula-ref-select'
|
|
92
|
+
import {
|
|
93
|
+
applySuggest,
|
|
94
|
+
filterFormulaSuggestions,
|
|
95
|
+
getSuggestContext,
|
|
96
|
+
moveSuggestIndex,
|
|
97
|
+
type FormulaSuggestItem
|
|
98
|
+
} from './use-formula-suggest'
|
|
99
|
+
|
|
100
|
+
defineOptions({ name: 'UFormulaBar' })
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 公式栏(名称框 + fx 输入栏),USheet 顶部结构之一。
|
|
104
|
+
*
|
|
105
|
+
* - 名称框:显示当前选区(单格 A1 / 区域 A1:B2);合法地址回车跳转。
|
|
106
|
+
* - fx 输入栏:活动格内容;Enter/✓ 提交、Esc/✗ 取消、失焦提交。
|
|
107
|
+
* - 函数补全:`=` 开头且光标在函数名 token 上时弹出候选(↑↓ / Tab / Enter / 点击)。
|
|
108
|
+
* - 引用选择:光标在运算符/`(`/`,` 后时,画布点选/拖选插入引用(blur 抑制防误提交)。
|
|
109
|
+
* - 网格编辑镜像:只读显示网格编辑器文本。
|
|
110
|
+
*/
|
|
111
|
+
const props = defineProps<{ sheet: Sheet; context: SheetContext }>()
|
|
112
|
+
|
|
113
|
+
const cls = bem('sheet')
|
|
114
|
+
|
|
115
|
+
const fxRef = useTemplateRef<HTMLTextAreaElement>('fxRef')
|
|
116
|
+
|
|
117
|
+
const selection = shallowRef<SelectionState>({ activeCell: null, ranges: [] })
|
|
118
|
+
const nameDraft = ref('')
|
|
119
|
+
const nameFocused = ref(false)
|
|
120
|
+
const editing = ref(false)
|
|
121
|
+
let editAddr: CellAddress | null = null
|
|
122
|
+
const mirroring = ref(false)
|
|
123
|
+
let mirrorAddr: CellAddress | null = null
|
|
124
|
+
const fxDraft = ref('')
|
|
125
|
+
/** 输入面板是否因内容增高而浮出(文档流仍保持单行) */
|
|
126
|
+
const fxExpanded = ref(false)
|
|
127
|
+
/** textarea 光标(selectionStart);输入/点击/方向键后同步 */
|
|
128
|
+
const fxCursor = ref(0)
|
|
129
|
+
|
|
130
|
+
/** 画布 pointerdown 置位:下一帧 blur 跳过提交(引用选择模式) */
|
|
131
|
+
let suppressBlurCommit = false
|
|
132
|
+
|
|
133
|
+
const suggestItems = shallowRef<FormulaSuggestItem[]>([])
|
|
134
|
+
const suggestIndex = ref(0)
|
|
135
|
+
const suggestOpen = computed(() => suggestItems.value.length > 0)
|
|
136
|
+
|
|
137
|
+
const fxDisabled = computed(() => selection.value.activeCell == null)
|
|
138
|
+
|
|
139
|
+
/** 编排层查询:fx 编辑中且光标处于可插入引用位置 */
|
|
140
|
+
function isRefSelecting(): boolean {
|
|
141
|
+
return editing.value && !mirroring.value && isRefSelectContext(fxDraft.value, fxCursor.value)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 网格 pointerdown(capture)→ 挂起即将到来的 blur 提交 */
|
|
145
|
+
function beginBlurSuppress(): void {
|
|
146
|
+
if (!isRefSelecting()) return
|
|
147
|
+
suppressBlurCommit = true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 选区拦截回调:插入 A1 / A1:B2 到光标处 */
|
|
151
|
+
function handleRefSelect(range: CellRange): void {
|
|
152
|
+
if (!editing.value || mirroring.value) return
|
|
153
|
+
const rangeText = formatRange(range)
|
|
154
|
+
const el = fxRef.value
|
|
155
|
+
const selEnd = el?.selectionEnd ?? fxCursor.value
|
|
156
|
+
const result = insertRefText(fxDraft.value, fxCursor.value, rangeText, selEnd)
|
|
157
|
+
fxDraft.value = result.text
|
|
158
|
+
fxCursor.value = result.cursor
|
|
159
|
+
closeSuggest()
|
|
160
|
+
void nextTick(() => {
|
|
161
|
+
const input = fxRef.value
|
|
162
|
+
if (!input) return
|
|
163
|
+
input.focus()
|
|
164
|
+
input.setSelectionRange(result.cursor, result.cursor)
|
|
165
|
+
autosizeFx()
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── 显示文本 ──────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
function nameText(state: SelectionState): string {
|
|
172
|
+
const range = state.ranges[0]
|
|
173
|
+
if (range) return formatRange(range)
|
|
174
|
+
return state.activeCell ? formatAddress(state.activeCell) : ''
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cellText(addr: CellAddress): string {
|
|
178
|
+
const data = props.context.getCellData(addr)
|
|
179
|
+
if (data?.f != null) return `=${data.f}`
|
|
180
|
+
if (data?.v != null) return String(data.v)
|
|
181
|
+
return ''
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function refreshName(): void {
|
|
185
|
+
nameDraft.value = nameText(selection.value)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function refreshFx(): void {
|
|
189
|
+
const active = selection.value.activeCell
|
|
190
|
+
fxDraft.value = active ? cellText(active) : ''
|
|
191
|
+
fxCursor.value = fxDraft.value.length
|
|
192
|
+
closeSuggest()
|
|
193
|
+
scheduleAutosize()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 每帧合并一次的 autosizeFx(批量 cell-change 同帧 N 次 nextTick → 1 次强制布局) */
|
|
197
|
+
let autosizePending = false
|
|
198
|
+
function scheduleAutosize(): void {
|
|
199
|
+
if (autosizePending) return
|
|
200
|
+
autosizePending = true
|
|
201
|
+
nextTick(() => {
|
|
202
|
+
autosizePending = false
|
|
203
|
+
autosizeFx()
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 按内容增高浮出面板,不超过 CSS max-height。
|
|
209
|
+
* 增高走绝对定位,公式栏文档流高度始终单行。
|
|
210
|
+
*/
|
|
211
|
+
function autosizeFx(): void {
|
|
212
|
+
const el = fxRef.value
|
|
213
|
+
if (!el) return
|
|
214
|
+
el.style.height = '0px'
|
|
215
|
+
const styles = getComputedStyle(el)
|
|
216
|
+
const max = Number.parseFloat(styles.maxHeight)
|
|
217
|
+
const min = Number.parseFloat(styles.minHeight)
|
|
218
|
+
const content = el.scrollHeight
|
|
219
|
+
const next = Number.isFinite(max) && max > 0 ? Math.min(content, max) : content
|
|
220
|
+
el.style.height = `${next}px`
|
|
221
|
+
fxExpanded.value = Number.isFinite(min) ? next > min + 1 : next > 1
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function syncFxCursor(): void {
|
|
225
|
+
const el = fxRef.value
|
|
226
|
+
if (!el) return
|
|
227
|
+
fxCursor.value = el.selectionStart
|
|
228
|
+
refreshSuggest()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handleFxInput(): void {
|
|
232
|
+
syncFxCursor()
|
|
233
|
+
autosizeFx()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── 函数补全 ──────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
function closeSuggest(): void {
|
|
239
|
+
suggestItems.value = []
|
|
240
|
+
suggestIndex.value = 0
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function refreshSuggest(): void {
|
|
244
|
+
if (!editing.value || mirroring.value) {
|
|
245
|
+
closeSuggest()
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
const ctx = getSuggestContext(fxDraft.value, fxCursor.value)
|
|
249
|
+
if (!ctx) {
|
|
250
|
+
closeSuggest()
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
const items = filterFormulaSuggestions(ctx.prefix)
|
|
254
|
+
suggestItems.value = items
|
|
255
|
+
if (suggestIndex.value >= items.length) suggestIndex.value = 0
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function confirmSuggest(item?: FormulaSuggestItem): void {
|
|
259
|
+
const pick = item ?? suggestItems.value[suggestIndex.value]
|
|
260
|
+
if (!pick) return
|
|
261
|
+
const ctx = getSuggestContext(fxDraft.value, fxCursor.value)
|
|
262
|
+
if (!ctx) return
|
|
263
|
+
const result = applySuggest(fxDraft.value, ctx.start, ctx.end, pick.name)
|
|
264
|
+
fxDraft.value = result.text
|
|
265
|
+
fxCursor.value = result.cursor
|
|
266
|
+
closeSuggest()
|
|
267
|
+
void nextTick(() => {
|
|
268
|
+
const el = fxRef.value
|
|
269
|
+
if (!el) return
|
|
270
|
+
el.focus()
|
|
271
|
+
el.setSelectionRange(result.cursor, result.cursor)
|
|
272
|
+
autosizeFx()
|
|
273
|
+
// 替换为 NAME() 后通常进入引用选择上下文,不再弹补全
|
|
274
|
+
refreshSuggest()
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── 名称框 ────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
function handleNameKeydown(event: KeyboardEvent): void {
|
|
281
|
+
if (event.key === 'Enter') {
|
|
282
|
+
event.preventDefault()
|
|
283
|
+
commitName()
|
|
284
|
+
} else if (event.key === 'Escape') {
|
|
285
|
+
event.preventDefault()
|
|
286
|
+
refreshName()
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function handleNameBlur(): void {
|
|
291
|
+
nameFocused.value = false
|
|
292
|
+
refreshName()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function commitName(): void {
|
|
296
|
+
const range = parseRange(nameDraft.value)
|
|
297
|
+
if (!range) {
|
|
298
|
+
message.warn('无效的单元格地址')
|
|
299
|
+
refreshName()
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
if (range.start.row === range.end.row && range.start.col === range.end.col) {
|
|
303
|
+
props.context.selectCell(range.start)
|
|
304
|
+
} else {
|
|
305
|
+
props.context.selectRange(range)
|
|
306
|
+
}
|
|
307
|
+
refreshName()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── fx 输入栏 ─────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
function handleFxFocus(): void {
|
|
313
|
+
if (fxDisabled.value) return
|
|
314
|
+
mirroring.value = false
|
|
315
|
+
mirrorAddr = null
|
|
316
|
+
editing.value = true
|
|
317
|
+
editAddr = selection.value.activeCell
|
|
318
|
+
syncFxCursor()
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 失焦提交;引用选择期间(画布 pointerdown 已挂起)跳过。
|
|
323
|
+
* ✓/✗ / 候选列表用 mousedown.prevent 拦截失焦。
|
|
324
|
+
*/
|
|
325
|
+
function handleFxBlur(): void {
|
|
326
|
+
if (!editing.value) return
|
|
327
|
+
if (suppressBlurCommit) {
|
|
328
|
+
suppressBlurCommit = false
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
commitEdit()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function handleFxKeydown(event: KeyboardEvent): void {
|
|
335
|
+
if (suggestOpen.value) {
|
|
336
|
+
if (event.key === 'ArrowDown') {
|
|
337
|
+
event.preventDefault()
|
|
338
|
+
suggestIndex.value = moveSuggestIndex(suggestIndex.value, suggestItems.value.length, 1)
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
if (event.key === 'ArrowUp') {
|
|
342
|
+
event.preventDefault()
|
|
343
|
+
suggestIndex.value = moveSuggestIndex(suggestIndex.value, suggestItems.value.length, -1)
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
347
|
+
event.preventDefault()
|
|
348
|
+
confirmSuggest()
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
if (event.key === 'Escape') {
|
|
352
|
+
event.preventDefault()
|
|
353
|
+
closeSuggest()
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
359
|
+
event.preventDefault()
|
|
360
|
+
commitEdit()
|
|
361
|
+
} else if (event.key === 'Escape') {
|
|
362
|
+
event.preventDefault()
|
|
363
|
+
cancelEdit()
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function commitEdit(): void {
|
|
368
|
+
if (!editing.value) return
|
|
369
|
+
const addr = editAddr
|
|
370
|
+
editing.value = false
|
|
371
|
+
editAddr = null
|
|
372
|
+
suppressBlurCommit = false
|
|
373
|
+
closeSuggest()
|
|
374
|
+
if (addr) props.context.setCellValue(addr, fxDraft.value)
|
|
375
|
+
refreshFx()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function cancelEdit(): void {
|
|
379
|
+
if (!editing.value) return
|
|
380
|
+
editing.value = false
|
|
381
|
+
editAddr = null
|
|
382
|
+
suppressBlurCommit = false
|
|
383
|
+
closeSuggest()
|
|
384
|
+
refreshFx()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ─── 双向同步 ──────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
function handleSelectionChange(state: SelectionState): void {
|
|
390
|
+
selection.value = state
|
|
391
|
+
if (!nameFocused.value) refreshName()
|
|
392
|
+
if (editing.value) return
|
|
393
|
+
mirroring.value = false
|
|
394
|
+
mirrorAddr = null
|
|
395
|
+
refreshFx()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function handleCellChange({ addr }: { addr: CellAddress }): void {
|
|
399
|
+
if (editing.value) return
|
|
400
|
+
if (mirroring.value) {
|
|
401
|
+
if (mirrorAddr && addr.row === mirrorAddr.row && addr.col === mirrorAddr.col) {
|
|
402
|
+
mirroring.value = false
|
|
403
|
+
mirrorAddr = null
|
|
404
|
+
refreshFx()
|
|
405
|
+
}
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
// 公式栏只显示活动格:非活动格变更(批量写入的绝大多数补丁)直接忽略,
|
|
409
|
+
// 避免每个 cell-change 都 refreshFx + nextTick(autosizeFx) 强制同步布局(#5)
|
|
410
|
+
const active = selection.value.activeCell
|
|
411
|
+
if (!active || addr.row !== active.row || addr.col !== active.col) return
|
|
412
|
+
refreshFx()
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function mirrorGridEdit(addr: CellAddress): void {
|
|
416
|
+
if (editing.value || fxDisabled.value) return
|
|
417
|
+
mirroring.value = true
|
|
418
|
+
mirrorAddr = addr
|
|
419
|
+
fxDraft.value = cellText(addr)
|
|
420
|
+
closeSuggest()
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function exitMirror(addr: CellAddress): void {
|
|
424
|
+
if (!mirroring.value) return
|
|
425
|
+
if (mirrorAddr && addr.row === mirrorAddr.row && addr.col === mirrorAddr.col) {
|
|
426
|
+
mirroring.value = false
|
|
427
|
+
mirrorAddr = null
|
|
428
|
+
refreshFx()
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
defineExpose({ mirrorGridEdit, exitMirror, isRefSelecting, beginBlurSuppress, handleRefSelect })
|
|
433
|
+
|
|
434
|
+
// ─── 生命周期 ──────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
let dispose: (() => void)[] = []
|
|
437
|
+
function bindSheet(sheet: Sheet): void {
|
|
438
|
+
for (const off of dispose) off()
|
|
439
|
+
dispose = [
|
|
440
|
+
sheet.on('selection-change', handleSelectionChange),
|
|
441
|
+
sheet.on('cell-change', handleCellChange)
|
|
442
|
+
]
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
watch(
|
|
446
|
+
() => props.sheet,
|
|
447
|
+
(sheet, prev) => {
|
|
448
|
+
if (sheet === prev) return
|
|
449
|
+
editing.value = false
|
|
450
|
+
editAddr = null
|
|
451
|
+
mirroring.value = false
|
|
452
|
+
mirrorAddr = null
|
|
453
|
+
suppressBlurCommit = false
|
|
454
|
+
closeSuggest()
|
|
455
|
+
bindSheet(sheet)
|
|
456
|
+
selection.value = sheet.getSelection()
|
|
457
|
+
refreshName()
|
|
458
|
+
refreshFx()
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
onMounted(() => {
|
|
463
|
+
bindSheet(props.sheet)
|
|
464
|
+
selection.value = props.sheet.getSelection()
|
|
465
|
+
refreshName()
|
|
466
|
+
refreshFx()
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
onBeforeUnmount(() => {
|
|
470
|
+
for (const off of dispose) off()
|
|
471
|
+
dispose = []
|
|
472
|
+
})
|
|
473
|
+
</script>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="items.length > 0" :class="cls.e('fx-suggest')" role="listbox" @mousedown.prevent>
|
|
3
|
+
<!-- 滚动用 desktop UScroll(自定义滚动条;原生 overflow 滚动条在浮层里常看不见) -->
|
|
4
|
+
<u-scroll :height="scrollHeight" :class="cls.e('fx-suggest-scroll')">
|
|
5
|
+
<ul :class="cls.e('fx-suggest-list')">
|
|
6
|
+
<li
|
|
7
|
+
v-for="(item, index) in items"
|
|
8
|
+
:key="item.name"
|
|
9
|
+
:ref="(el) => setItemRef(index, el)"
|
|
10
|
+
:class="[cls.e('fx-suggest-item'), bem.is('active', index === activeIndex)]"
|
|
11
|
+
role="option"
|
|
12
|
+
:aria-selected="index === activeIndex"
|
|
13
|
+
@mousedown.prevent="emit('select', item)"
|
|
14
|
+
@mouseenter="emit('hover', index)"
|
|
15
|
+
>
|
|
16
|
+
<span :class="cls.e('fx-suggest-sig')">{{ item.signature }}</span>
|
|
17
|
+
<span v-if="item.description" :class="cls.e('fx-suggest-desc')">{{
|
|
18
|
+
item.description
|
|
19
|
+
}}</span>
|
|
20
|
+
</li>
|
|
21
|
+
</ul>
|
|
22
|
+
</u-scroll>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script lang="ts" setup>
|
|
27
|
+
import { UScroll } from '@veltra/desktop'
|
|
28
|
+
import { bem } from '@veltra/utils'
|
|
29
|
+
import { computed, nextTick, watch } from 'vue'
|
|
30
|
+
|
|
31
|
+
import type { FormulaSuggestItem } from './use-formula-suggest'
|
|
32
|
+
|
|
33
|
+
defineOptions({ name: 'UFormulaSuggestList' })
|
|
34
|
+
|
|
35
|
+
const props = defineProps<{ items: FormulaSuggestItem[]; activeIndex: number }>()
|
|
36
|
+
|
|
37
|
+
const emit = defineEmits<{ select: [item: FormulaSuggestItem]; hover: [index: number] }>()
|
|
38
|
+
|
|
39
|
+
const cls = bem('sheet')
|
|
40
|
+
// bem.is 挂在工厂函数上,与 cls.e 分开用
|
|
41
|
+
|
|
42
|
+
/** 候选列表最大可视高度;内容更短时收缩,避免空前缀少量项时大片留白 */
|
|
43
|
+
const SUGGEST_MAX_HEIGHT = 240
|
|
44
|
+
/** 单项约高(含 padding + 签名/描述两行);用于估算 UScroll height */
|
|
45
|
+
const SUGGEST_ITEM_HEIGHT = 48
|
|
46
|
+
|
|
47
|
+
const scrollHeight = computed(() =>
|
|
48
|
+
Math.min(Math.max(props.items.length, 1) * SUGGEST_ITEM_HEIGHT, SUGGEST_MAX_HEIGHT)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const itemEls: (HTMLElement | null)[] = []
|
|
52
|
+
|
|
53
|
+
function setItemRef(index: number, el: unknown): void {
|
|
54
|
+
itemEls[index] = el instanceof HTMLElement ? el : null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
watch(
|
|
58
|
+
() => [props.activeIndex, props.items] as const,
|
|
59
|
+
async ([index]) => {
|
|
60
|
+
await nextTick()
|
|
61
|
+
itemEls[index]?.scrollIntoView({ block: 'nearest' })
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
</script>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { message, messageConfirm } from '@veltra/desktop'
|
|
2
|
+
import {
|
|
3
|
+
importCsv,
|
|
4
|
+
importXlsx,
|
|
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'
|
|
9
|
+
import { nextFrame } from '@veltra/utils'
|
|
10
|
+
import type { Ref } from 'vue'
|
|
11
|
+
|
|
12
|
+
import type { ImportWorkerResponse } from './popups/import.worker'
|
|
13
|
+
|
|
14
|
+
/** 导入文件 accept(工具栏直接选文件) */
|
|
15
|
+
export const IMPORT_FILE_ACCEPT = '.xlsx,.csv'
|
|
16
|
+
|
|
17
|
+
/** 导入所需上下文(vue 层编排;tools 层不 import vue) */
|
|
18
|
+
export interface ImportFileOptions {
|
|
19
|
+
workbook: Workbook
|
|
20
|
+
activeSheet: Sheet
|
|
21
|
+
/** xlsx worker 解析期 → grid 遮罩 */
|
|
22
|
+
parsing?: Ref<boolean>
|
|
23
|
+
/** worker 分片构建进度 */
|
|
24
|
+
parseProgress?: Ref<{ done: number; total: number }>
|
|
25
|
+
/** csv 导入完成(可能扩张尺寸,宿主需重建网格) */
|
|
26
|
+
onCsvImported?: () => void
|
|
27
|
+
/** xlsx 已替换工作簿(宿主需同步 tabs / 重绑 / 重建) */
|
|
28
|
+
onWorkbookReplaced?: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ParsedXlsx = { sheets: { name: string; snapshot: SheetSnapshot }[]; activeIndex: number }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* xlsx 解析(worker 优先):importXlsx 是同步重活,主线程直接跑会冻结 UI。
|
|
35
|
+
* worker 返回纯数据快照;不可用时回退主线程解析。
|
|
36
|
+
*/
|
|
37
|
+
async function parseXlsxAsync(
|
|
38
|
+
buffer: ArrayBuffer,
|
|
39
|
+
parseProgress?: Ref<{ done: number; total: number }>
|
|
40
|
+
): Promise<ParsedXlsx> {
|
|
41
|
+
let worker: Worker | undefined
|
|
42
|
+
try {
|
|
43
|
+
// new URL 模式:dev(vite)与 build(rolldown)都支持把 worker 提为独立 chunk
|
|
44
|
+
const url = import.meta.env?.DEV
|
|
45
|
+
? new URL('./popups/import.worker.ts', import.meta.url)
|
|
46
|
+
: new URL('./popups/import.worker.js', import.meta.url)
|
|
47
|
+
worker = new Worker(url, { type: 'module' })
|
|
48
|
+
} catch {
|
|
49
|
+
return toSnapshotArray(await importXlsx(new Uint8Array(buffer)))
|
|
50
|
+
}
|
|
51
|
+
return await new Promise<ParsedXlsx>((resolve, reject) => {
|
|
52
|
+
let settled = false
|
|
53
|
+
const fallback = (): void => {
|
|
54
|
+
if (settled) return
|
|
55
|
+
settled = true
|
|
56
|
+
worker!.terminate()
|
|
57
|
+
void importXlsx(new Uint8Array(buffer))
|
|
58
|
+
.then((wb) => resolve(toSnapshotArray(wb)))
|
|
59
|
+
.catch((err: unknown) => reject(err))
|
|
60
|
+
}
|
|
61
|
+
worker!.onmessage = (e: MessageEvent<ImportWorkerResponse>) => {
|
|
62
|
+
const data = e.data
|
|
63
|
+
if (data.type === 'progress') {
|
|
64
|
+
if (parseProgress) parseProgress.value = { done: data.done, total: data.total }
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (settled) return
|
|
68
|
+
settled = true
|
|
69
|
+
if (!data.ok) {
|
|
70
|
+
worker!.terminate()
|
|
71
|
+
reject(new Error(data.error ?? 'worker 解析失败'))
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
worker!.terminate()
|
|
75
|
+
resolve({ sheets: data.sheets ?? [], activeIndex: data.activeIndex ?? 0 })
|
|
76
|
+
}
|
|
77
|
+
worker!.onerror = () => fallback()
|
|
78
|
+
// transfer 一份拷贝给 worker;主线程保留原 buffer 供降级解析
|
|
79
|
+
const transfer = buffer.slice(0)
|
|
80
|
+
worker!.postMessage({ buffer: transfer }, [transfer])
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toSnapshotArray(wb: Workbook): ParsedXlsx {
|
|
85
|
+
return {
|
|
86
|
+
sheets: wb.getSheets().map((s) => ({ name: s.name, snapshot: s.snapshot() })),
|
|
87
|
+
activeIndex: wb.activeSheetIndex
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 从本地 File 导入:
|
|
93
|
+
* - .csv → importCsv 写入当前活动表
|
|
94
|
+
* - .xlsx → 解析后经 messageConfirm 确认再 replaceWorkbookWithSnapshots
|
|
95
|
+
*/
|
|
96
|
+
export function importFromFile(file: File, options: ImportFileOptions): void {
|
|
97
|
+
const { workbook, activeSheet, parsing, parseProgress, onCsvImported, onWorkbookReplaced } =
|
|
98
|
+
options
|
|
99
|
+
|
|
100
|
+
if (file.name.toLowerCase().endsWith('.csv')) {
|
|
101
|
+
void file.text().then((text) => {
|
|
102
|
+
importCsv(text, activeSheet)
|
|
103
|
+
onCsvImported?.()
|
|
104
|
+
message.success(`已从 ${file.name} 导入到工作表「${activeSheet.name}」`)
|
|
105
|
+
})
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
void file.arrayBuffer().then((buffer) => {
|
|
110
|
+
if (parsing) parsing.value = true
|
|
111
|
+
if (parseProgress) parseProgress.value = { done: 0, total: 0 }
|
|
112
|
+
void parseXlsxAsync(buffer, parseProgress)
|
|
113
|
+
.then((imported) => {
|
|
114
|
+
if (parsing) parsing.value = false
|
|
115
|
+
messageConfirm.danger(
|
|
116
|
+
`导入将替换当前工作簿(共 ${imported.sheets.length} 个工作表),确定吗?`,
|
|
117
|
+
{
|
|
118
|
+
confirmButtonText: '导入',
|
|
119
|
+
onClosed: (action) => {
|
|
120
|
+
if (action !== 'confirm') return
|
|
121
|
+
const loading = message({ message: '正在导入…', duration: 0 })
|
|
122
|
+
try {
|
|
123
|
+
replaceWorkbookWithSnapshots(workbook, imported.sheets, imported.activeIndex)
|
|
124
|
+
onWorkbookReplaced?.()
|
|
125
|
+
nextFrame(() => {
|
|
126
|
+
message.success('导入完成')
|
|
127
|
+
loading.close()
|
|
128
|
+
})
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('[sheet] 导入失败:', err)
|
|
131
|
+
message.error(`导入失败:${err instanceof Error ? err.message : String(err)}`)
|
|
132
|
+
loading.close()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
.catch((err: unknown) => {
|
|
139
|
+
if (parsing) parsing.value = false
|
|
140
|
+
message.error(`文件解析失败:${err instanceof Error ? err.message : String(err)}`)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 拉起系统文件选择框(工具栏导入用;不用 UFilePicker——无编程式打开 API)。
|
|
147
|
+
* 选中文件后走 importFromFile。
|
|
148
|
+
*/
|
|
149
|
+
export function pickAndImportFile(options: ImportFileOptions): void {
|
|
150
|
+
const input = document.createElement('input')
|
|
151
|
+
input.type = 'file'
|
|
152
|
+
input.accept = IMPORT_FILE_ACCEPT
|
|
153
|
+
input.hidden = true
|
|
154
|
+
const cleanup = (): void => {
|
|
155
|
+
input.remove()
|
|
156
|
+
}
|
|
157
|
+
input.addEventListener('change', () => {
|
|
158
|
+
const file = input.files?.[0]
|
|
159
|
+
cleanup()
|
|
160
|
+
if (file) importFromFile(file, options)
|
|
161
|
+
})
|
|
162
|
+
window.addEventListener(
|
|
163
|
+
'focus',
|
|
164
|
+
() => {
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
if (document.body.contains(input) && !input.files?.length) cleanup()
|
|
167
|
+
}, 300)
|
|
168
|
+
},
|
|
169
|
+
{ once: true }
|
|
170
|
+
)
|
|
171
|
+
document.body.appendChild(input)
|
|
172
|
+
input.click()
|
|
173
|
+
}
|