@veltra/desktop 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/action/action-group.js +110 -3
- package/dist/components/action/action-group.js.map +1 -1
- package/dist/components/action/action-group.vue.d.ts +16 -3
- package/dist/components/action/action.js +26 -13
- package/dist/components/action/action.js.map +1 -1
- package/dist/components/action/style.js +2 -1
- package/dist/components/action/style2.css +35 -7
- package/dist/components/batch-edit/batch-edit-form.js +106 -42
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-list.js +84 -47
- package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +42 -10
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/batch-edit/style.js +2 -4
- package/dist/components/batch-edit/style2.css +202 -30
- package/dist/components/batch-edit/use-edit.js +45 -9
- package/dist/components/batch-edit/use-edit.js.map +1 -1
- package/dist/components/cascade/cascade.js +3 -1
- package/dist/components/cascade/cascade.js.map +1 -1
- package/dist/components/code-editor/code-editor.js +145 -49
- package/dist/components/code-editor/code-editor.js.map +1 -1
- package/dist/components/code-editor/code-editor.vue.d.ts +2 -0
- package/dist/components/code-editor/style2.css +87 -19
- package/dist/components/dialog/dialog.js +6 -2
- package/dist/components/dialog/dialog.js.map +1 -1
- package/dist/components/dialog/style2.css +2 -3
- package/dist/components/expression-editor/components/path-preview.js +27 -0
- package/dist/components/expression-editor/components/path-preview.js.map +1 -0
- package/dist/components/expression-editor/components/variable-picker.js +200 -153
- package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
- package/dist/components/expression-editor/core/chip.js +54 -0
- package/dist/components/expression-editor/core/chip.js.map +1 -0
- package/dist/components/expression-editor/core/editor.js +425 -0
- package/dist/components/expression-editor/core/editor.js.map +1 -0
- package/dist/components/expression-editor/core/mention.js +91 -0
- package/dist/components/expression-editor/core/mention.js.map +1 -0
- package/dist/components/expression-editor/core/model.js +64 -0
- package/dist/components/expression-editor/core/model.js.map +1 -0
- package/dist/components/expression-editor/di.js +4 -10
- package/dist/components/expression-editor/di.js.map +1 -1
- package/dist/components/expression-editor/expression-editor.js +168 -114
- package/dist/components/expression-editor/expression-editor.js.map +1 -1
- package/dist/components/expression-editor/expression-editor.vue.d.ts +2 -1
- package/dist/components/expression-editor/style.js +0 -2
- package/dist/components/expression-editor/style2.css +145 -218
- package/dist/components/index.js +54 -54
- package/dist/components/layout/layout.js +12 -11
- package/dist/components/layout/layout.js.map +1 -1
- package/dist/components/multi-select/multi-select.vue.d.ts +1 -1
- package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +1 -1
- package/dist/components/number-input/number-input.js +4 -4
- package/dist/components/number-input/number-input.js.map +1 -1
- package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
- package/dist/components/rich-text-editor/rich-text-editor.js +1 -1
- package/dist/components/rich-text-editor/toolbar.js +2 -2
- package/dist/components/steps/steps.vue.d.ts +1 -1
- package/dist/components/table/use-table.js +16 -0
- package/dist/components/table/use-table.js.map +1 -1
- package/dist/components/tabs/tabs-horizontal.vue.d.ts +3 -3
- package/dist/components/tabs/tabs-vertical.vue.d.ts +2 -2
- package/dist/components/tabs/tabs.vue.d.ts +3 -3
- package/dist/components/tip/tip.js +4 -5
- package/dist/components/tip/tip.js.map +1 -1
- package/dist/components/tip/tip.vue.d.ts +1 -1
- package/dist/components/tree/style2.css +18 -12
- package/dist/components/tree/tree-node.js +7 -4
- package/dist/components/tree/tree-node.js.map +1 -1
- package/dist/components/tree-select/tree-select.vue.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +83 -83
- package/dist/style.js +3 -3
- package/dist/types/action.d.ts +38 -4
- package/dist/types/cascade.d.ts +2 -3
- package/dist/types/code-editor.d.ts +9 -2
- package/dist/types/expression-editor.d.ts +9 -1
- package/dist/types/index.d.ts +2 -2
- package/package.json +11 -12
- package/src/components/action/action-group.vue +126 -56
- package/src/components/action/action.vue +35 -12
- package/src/components/action/style.scss +46 -10
- package/src/components/action/style.ts +2 -1
- package/src/components/batch-edit/batch-edit-form.vue +121 -45
- package/src/components/batch-edit/batch-edit-list.vue +56 -39
- package/src/components/batch-edit/batch-edit.vue +57 -6
- package/src/components/batch-edit/style.scss +251 -37
- package/src/components/batch-edit/style.ts +5 -8
- package/src/components/batch-edit/use-edit.ts +58 -11
- package/src/components/cascade/cascade.vue +3 -1
- package/src/components/code-editor/code-editor.vue +198 -63
- package/src/components/code-editor/style.scss +110 -12
- package/src/components/dialog/dialog.vue +6 -2
- package/src/components/dialog/style.scss +4 -4
- package/src/components/expression-editor/__test__/chip.test.ts +86 -0
- package/src/components/expression-editor/__test__/editor.test.ts +47 -0
- package/src/components/expression-editor/__test__/mention.test.ts +122 -0
- package/src/components/expression-editor/__test__/model.test.ts +127 -0
- package/src/components/expression-editor/components/path-preview.vue +32 -0
- package/src/components/expression-editor/components/variable-picker.vue +225 -219
- package/src/components/expression-editor/core/chip.ts +101 -0
- package/src/components/expression-editor/core/editor.ts +514 -0
- package/src/components/expression-editor/core/mention.ts +121 -0
- package/src/components/expression-editor/core/model.ts +109 -0
- package/src/components/expression-editor/di.ts +4 -32
- package/src/components/expression-editor/expression-editor.vue +184 -151
- package/src/components/expression-editor/style.scss +148 -219
- package/src/components/expression-editor/style.ts +0 -2
- package/src/components/layout/layout.vue +17 -19
- package/src/components/number-input/number-input.vue +4 -4
- package/src/components/table/use-table.ts +16 -0
- package/src/components/tip/tip.vue +4 -3
- package/src/components/tree/style.scss +30 -33
- package/src/components/tree/tree-node.vue +4 -2
- package/src/types/action.ts +42 -4
- package/src/types/cascade.ts +4 -3
- package/src/types/code-editor.ts +9 -2
- package/src/types/expression-editor.ts +9 -0
- package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js +0 -65
- package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js.map +0 -1
- package/dist/components/expression-editor/constants.js +0 -9
- package/dist/components/expression-editor/constants.js.map +0 -1
- package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js +0 -47
- package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js.map +0 -1
- package/dist/components/expression-editor/internal/editor-runtime/model-sync.js +0 -49
- package/dist/components/expression-editor/internal/editor-runtime/model-sync.js.map +0 -1
- package/dist/components/expression-editor/internal/features/commands/register-command-packs.js +0 -215
- package/dist/components/expression-editor/internal/features/commands/register-command-packs.js.map +0 -1
- package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js +0 -33
- package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js.map +0 -1
- package/dist/components/expression-editor/internal/features/insertion/insertion-service.js +0 -33
- package/dist/components/expression-editor/internal/features/insertion/insertion-service.js.map +0 -1
- package/dist/components/expression-editor/nodes/variable-node.js +0 -94
- package/dist/components/expression-editor/nodes/variable-node.js.map +0 -1
- package/dist/components/expression-editor/parser.js +0 -32
- package/dist/components/expression-editor/parser.js.map +0 -1
- package/dist/components/expression-editor/plain-text.js +0 -19
- package/dist/components/expression-editor/plain-text.js.map +0 -1
- package/dist/components/expression-editor/use-context.js +0 -69
- package/dist/components/expression-editor/use-context.js.map +0 -1
- package/dist/components/expression-editor/use-decorators.js +0 -27
- package/dist/components/expression-editor/use-decorators.js.map +0 -1
- package/dist/components/expression-editor/use-expression-drag-drop.js +0 -275
- package/dist/components/expression-editor/use-expression-drag-drop.js.map +0 -1
- package/src/components/batch-edit/batch-edit-new.vue +0 -168
- package/src/components/batch-edit/style-new.scss +0 -35
- package/src/components/batch-edit/use-edit-new.ts +0 -68
- package/src/components/batch-edit/use-tip.ts +0 -47
- package/src/components/expression-editor/__test__/architecture-boundaries.test.ts +0 -103
- package/src/components/expression-editor/__test__/architecture-compatibility.test.ts +0 -52
- package/src/components/expression-editor/__test__/drag-drop.test.ts +0 -171
- package/src/components/expression-editor/__test__/parser.test.ts +0 -62
- package/src/components/expression-editor/constants.ts +0 -6
- package/src/components/expression-editor/internal/contracts/editor-runtime.ts +0 -11
- package/src/components/expression-editor/internal/editor-runtime/create-runtime.ts +0 -64
- package/src/components/expression-editor/internal/editor-runtime/index.ts +0 -1
- package/src/components/expression-editor/internal/editor-runtime/model-sync.ts +0 -68
- package/src/components/expression-editor/internal/features/commands/register-command-packs.ts +0 -517
- package/src/components/expression-editor/internal/features/drag-drop/drag-drop-service.ts +0 -51
- package/src/components/expression-editor/internal/features/insertion/insertion-service.ts +0 -56
- package/src/components/expression-editor/nodes/variable-node.tsx +0 -118
- package/src/components/expression-editor/parser.ts +0 -46
- package/src/components/expression-editor/plain-text.ts +0 -25
- package/src/components/expression-editor/plan.md +0 -22
- package/src/components/expression-editor/use-context.ts +0 -134
- package/src/components/expression-editor/use-decorators.tsx +0 -29
- package/src/components/expression-editor/use-editor.ts +0 -12
- package/src/components/expression-editor/use-expression-drag-drop.ts +0 -396
|
@@ -3,299 +3,305 @@
|
|
|
3
3
|
hide-arrow
|
|
4
4
|
alignment="start"
|
|
5
5
|
direction="bottom"
|
|
6
|
+
trigger="click"
|
|
6
7
|
:trigger-dom="triggerDom"
|
|
7
8
|
:visible="visible"
|
|
8
|
-
@update:visible="
|
|
9
|
+
@update:visible="handleVisibleChange($event)"
|
|
9
10
|
>
|
|
10
11
|
<template #content>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
>
|
|
20
|
-
<template #suffix>
|
|
21
|
-
<u-icon><Search /></u-icon>
|
|
22
|
-
</template>
|
|
23
|
-
</u-input>
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
|
-
<!-- 面包屑导航 -->
|
|
27
|
-
<div v-if="breadcrumbs.length > 1 && !searchQuery" :class="cls.e('breadcrumbs')">
|
|
28
|
-
<span v-for="(crumb, index) in breadcrumbs" :key="index" :class="cls.e('breadcrumb-item')">
|
|
29
|
-
<span
|
|
30
|
-
:class="[
|
|
31
|
-
cls.e('breadcrumb-link'),
|
|
32
|
-
{ [cls.e('breadcrumb-link--active')]: index === breadcrumbs.length - 1 }
|
|
33
|
-
]"
|
|
34
|
-
@click="navigateToBreadcrumb(index)"
|
|
35
|
-
>
|
|
36
|
-
{{ crumb.label }}
|
|
12
|
+
<div :class="cls.e('picker')" @mousedown.prevent>
|
|
13
|
+
<!-- 面包屑:仅逐级模式且非根级显示 -->
|
|
14
|
+
<div v-if="!isFlat && navPath.length > 0" :class="cls.e('breadcrumbs')">
|
|
15
|
+
<span :class="cls.e('breadcrumb-item')">
|
|
16
|
+
<span :class="cls.e('breadcrumb-link')" @click="navigateTo(0)">全部变量</span>
|
|
17
|
+
<u-icon :class="cls.e('breadcrumb-separator')">
|
|
18
|
+
<ArrowRight />
|
|
19
|
+
</u-icon>
|
|
37
20
|
</span>
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
<!-- 单面板 -->
|
|
45
|
-
<div :class="cls.e('panel')" v-if="currentList.length">
|
|
46
|
-
<u-scroll tag="ul" :class="cls.e('panel-list')" :content-class="cls.e('panel-content')">
|
|
47
|
-
<li
|
|
48
|
-
v-for="(item, idx) in currentList"
|
|
49
|
-
:key="item.value ?? idx"
|
|
50
|
-
:class="getItemCls(item, idx)"
|
|
51
|
-
@click="handleItemClick(item)"
|
|
52
|
-
@mouseenter="activeIndex = idx"
|
|
21
|
+
<span
|
|
22
|
+
v-for="(crumb, i) in navPath"
|
|
23
|
+
:key="`${i}-${crumb.value}`"
|
|
24
|
+
:class="cls.e('breadcrumb-item')"
|
|
53
25
|
>
|
|
54
|
-
<span
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
26
|
+
<span
|
|
27
|
+
:class="[
|
|
28
|
+
cls.e('breadcrumb-link'),
|
|
29
|
+
{ [cls.em('breadcrumb-link', 'active')]: i === navPath.length - 1 }
|
|
30
|
+
]"
|
|
31
|
+
@click="navigateTo(i + 1)"
|
|
32
|
+
>{{ crumb.label }}</span
|
|
33
|
+
>
|
|
34
|
+
<u-icon v-if="i < navPath.length - 1" :class="cls.e('breadcrumb-separator')">
|
|
58
35
|
<ArrowRight />
|
|
59
36
|
</u-icon>
|
|
60
|
-
</
|
|
61
|
-
</
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div :class="cls.e('picker-body')">
|
|
41
|
+
<div :class="cls.e('panel')" v-if="currentList.length > 0">
|
|
42
|
+
<u-scroll tag="ul" :class="cls.e('panel-list')" :content-class="cls.e('panel-content')">
|
|
43
|
+
<li
|
|
44
|
+
v-for="(item, idx) in currentList"
|
|
45
|
+
:key="`${idx}-${item.value}`"
|
|
46
|
+
:data-idx="idx"
|
|
47
|
+
:class="getItemCls(item, idx)"
|
|
48
|
+
@click="handleItemClick(item, idx)"
|
|
49
|
+
@mouseenter="activeIndex = idx"
|
|
50
|
+
>
|
|
51
|
+
<span :class="cls.e('item-label')">{{ item.label }}</span>
|
|
52
|
+
<u-icon v-if="!isFlat && hasChildren(item)" :class="cls.e('item-expand')">
|
|
53
|
+
<ArrowRight />
|
|
54
|
+
</u-icon>
|
|
55
|
+
</li>
|
|
56
|
+
</u-scroll>
|
|
57
|
+
</div>
|
|
58
|
+
<div v-else :class="cls.e('empty')">
|
|
59
|
+
<UEmpty description="暂无可用变量" />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<PathPreview v-if="isFlat && activePath.length > 0" :path="activePath" />
|
|
63
|
+
</div>
|
|
67
64
|
</div>
|
|
68
65
|
</template>
|
|
69
66
|
</u-tip>
|
|
70
67
|
</template>
|
|
71
68
|
|
|
72
69
|
<script lang="ts" setup>
|
|
73
|
-
import { ArrowRight
|
|
70
|
+
import { ArrowRight } from '@veltra/icons/normal'
|
|
74
71
|
import { bem } from '@veltra/utils'
|
|
75
|
-
import {
|
|
72
|
+
import { computed, nextTick, ref, watch } from 'vue'
|
|
76
73
|
|
|
77
74
|
import type { VariableItem } from '../../../types'
|
|
78
75
|
import { UEmpty } from '../../empty'
|
|
79
76
|
import { UIcon } from '../../icon'
|
|
80
|
-
import { UInput } from '../../input'
|
|
81
77
|
import { UScroll } from '../../scroll'
|
|
82
78
|
import { UTip } from '../../tip'
|
|
83
|
-
import
|
|
79
|
+
import PathPreview from './path-preview.vue'
|
|
84
80
|
|
|
85
81
|
defineOptions({
|
|
86
82
|
name: 'VariablePicker'
|
|
87
83
|
})
|
|
88
84
|
|
|
85
|
+
interface FlatItem extends VariableItem {
|
|
86
|
+
/** 仅扁平模式使用:从根到当前节点的路径 */
|
|
87
|
+
__path: VariableItem[]
|
|
88
|
+
}
|
|
89
|
+
|
|
89
90
|
const props = defineProps<{
|
|
90
91
|
visible: boolean
|
|
91
92
|
triggerDom?: HTMLElement
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
/** 顶层变量树 */
|
|
94
|
+
variables?: VariableItem[]
|
|
95
|
+
/** 来自编辑器 mention 状态的过滤字符串。空 = 逐级模式;非空 = 扁平模式。 */
|
|
96
|
+
filter: string
|
|
97
|
+
/** 是否允许选中分支节点 */
|
|
98
|
+
selectableLevels: 'leaf' | 'any'
|
|
94
99
|
}>()
|
|
95
100
|
|
|
96
101
|
const emit = defineEmits<{
|
|
97
|
-
(e: 'select',
|
|
102
|
+
(e: 'select', item: VariableItem): void
|
|
98
103
|
(e: 'update:visible', visible: boolean): void
|
|
104
|
+
/** 用户主动取消(按 Esc)。编辑器据此把 mention 状态置为 dismissed。 */
|
|
105
|
+
(e: 'dismiss'): void
|
|
99
106
|
}>()
|
|
100
107
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const activeIndex = ref
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// 扁平化变量树,用于搜索
|
|
108
|
-
function flattenVariables(
|
|
109
|
-
items: VariableItem[] | undefined,
|
|
110
|
-
parentPath: VariableItem[] = []
|
|
111
|
-
): VariableItem[] {
|
|
112
|
-
if (!items) return []
|
|
113
|
-
const result: VariableItem[] = []
|
|
114
|
-
|
|
115
|
-
for (const item of items) {
|
|
116
|
-
const currentPath = [...parentPath, item]
|
|
117
|
-
// 如果是叶子节点,添加到结果中
|
|
118
|
-
if (!item.children || item.children.length === 0) {
|
|
119
|
-
result.push({
|
|
120
|
-
...item,
|
|
121
|
-
label: currentPath.map((p) => p.label).join(' / ')
|
|
122
|
-
})
|
|
123
|
-
} else {
|
|
124
|
-
// 递归处理子节点
|
|
125
|
-
result.push(...flattenVariables(item.children, currentPath))
|
|
126
|
-
}
|
|
127
|
-
}
|
|
108
|
+
const cls = bem('expression-editor')
|
|
109
|
+
|
|
110
|
+
const navPath = ref<VariableItem[]>([])
|
|
111
|
+
const activeIndex = ref(0)
|
|
112
|
+
|
|
113
|
+
const isFlat = computed(() => props.filter !== '')
|
|
128
114
|
|
|
129
|
-
|
|
115
|
+
function hasChildren(item: VariableItem): boolean {
|
|
116
|
+
return !!item.children && item.children.length > 0
|
|
130
117
|
}
|
|
131
118
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!variables || variables.length === 0) return []
|
|
119
|
+
function isLeaf(item: VariableItem): boolean {
|
|
120
|
+
return !hasChildren(item)
|
|
121
|
+
}
|
|
136
122
|
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
/** 当前层级展示的列表(来源依模式而定) */
|
|
124
|
+
const currentList = computed<VariableItem[]>(() => {
|
|
125
|
+
const variables = props.variables ?? []
|
|
126
|
+
if (isFlat.value) return flatList.value
|
|
127
|
+
let level: VariableItem[] = variables
|
|
128
|
+
for (const node of navPath.value) {
|
|
129
|
+
const found = level.find((it) => it.value === node.value)
|
|
130
|
+
if (!found || !found.children) break
|
|
131
|
+
level = found.children
|
|
139
132
|
}
|
|
140
|
-
|
|
141
|
-
// 搜索模式:扁平化并过滤
|
|
142
|
-
const flat = flattenVariables(variables)
|
|
143
|
-
const query = searchQuery.value.toLowerCase()
|
|
144
|
-
return flat.filter((item) => item.label.toLowerCase().includes(query))
|
|
133
|
+
return level
|
|
145
134
|
})
|
|
146
135
|
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (found?.children) {
|
|
158
|
-
current = found.children
|
|
136
|
+
/** 扁平模式下:根据 selectableLevels 拍平 + 过滤 */
|
|
137
|
+
const flatList = computed<FlatItem[]>(() => {
|
|
138
|
+
const variables = props.variables ?? []
|
|
139
|
+
const out: FlatItem[] = []
|
|
140
|
+
const includeBranches = props.selectableLevels === 'any'
|
|
141
|
+
function walk(items: VariableItem[], path: VariableItem[]) {
|
|
142
|
+
for (const it of items) {
|
|
143
|
+
const nextPath = [...path, it]
|
|
144
|
+
if (isLeaf(it)) {
|
|
145
|
+
out.push({ ...it, __path: nextPath })
|
|
159
146
|
} else {
|
|
160
|
-
|
|
147
|
+
if (includeBranches) out.push({ ...it, __path: nextPath })
|
|
148
|
+
walk(it.children!, nextPath)
|
|
161
149
|
}
|
|
162
150
|
}
|
|
163
|
-
return current
|
|
164
151
|
}
|
|
152
|
+
walk(variables, [])
|
|
153
|
+
const q = props.filter.toLowerCase()
|
|
154
|
+
return out.filter((it) => it.label.toLowerCase().includes(q))
|
|
165
155
|
})
|
|
166
156
|
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
157
|
+
/** 当前 active 项;扁平模式下用于驱动路径预览 */
|
|
158
|
+
const activePath = computed<VariableItem[]>(() => {
|
|
159
|
+
if (!isFlat.value) return []
|
|
160
|
+
const item = flatList.value[activeIndex.value] as FlatItem | undefined
|
|
161
|
+
return item ? item.__path : []
|
|
170
162
|
})
|
|
171
163
|
|
|
172
|
-
function getItemCls(
|
|
173
|
-
|
|
174
|
-
|
|
164
|
+
function getItemCls(_item: VariableItem, idx: number) {
|
|
165
|
+
return [cls.e('item'), bem.is('active', idx === activeIndex.value)]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function navigateTo(i: number) {
|
|
169
|
+
// 0 = 全部变量;i 之后表示进入到 navPath[i-1]
|
|
170
|
+
navPath.value = navPath.value.slice(0, i)
|
|
171
|
+
activeIndex.value = 0
|
|
172
|
+
}
|
|
175
173
|
|
|
176
|
-
|
|
174
|
+
function handleItemClick(item: VariableItem, idx: number) {
|
|
175
|
+
activeIndex.value = idx
|
|
176
|
+
selectActive()
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
if (item
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
function selectActive() {
|
|
180
|
+
const item = currentList.value[activeIndex.value]
|
|
181
|
+
if (!item) return
|
|
182
|
+
|
|
183
|
+
if (isFlat.value) {
|
|
184
|
+
// 扁平模式始终是「选中并替换」
|
|
185
|
+
emit('select', flattenSourceFor(item))
|
|
184
186
|
return
|
|
185
187
|
}
|
|
186
188
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
// 逐级模式
|
|
190
|
+
if (hasChildren(item)) {
|
|
191
|
+
if (props.selectableLevels === 'any') {
|
|
192
|
+
// Enter 在分支项上:选中分支本身
|
|
193
|
+
emit('select', item)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
// 默认 'leaf':进入下一级
|
|
197
|
+
navPath.value = [...navPath.value, item]
|
|
198
|
+
activeIndex.value = 0
|
|
199
|
+
return
|
|
193
200
|
}
|
|
194
|
-
|
|
201
|
+
emit('select', item)
|
|
195
202
|
}
|
|
196
203
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
navigationPath.value = navigationPath.value.slice(0, index)
|
|
205
|
-
}
|
|
206
|
-
activeIndex.value = -1
|
|
204
|
+
function flattenSourceFor(it: VariableItem): VariableItem {
|
|
205
|
+
const flat = it as FlatItem
|
|
206
|
+
// 还原为「不带 __path 的纯 VariableItem」,避免泄漏内部字段
|
|
207
|
+
const out: VariableItem = { value: flat.value, label: flat.label }
|
|
208
|
+
if (flat.type) out.type = flat.type
|
|
209
|
+
if (flat.children) out.children = flat.children
|
|
210
|
+
return out
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
activeIndex.value
|
|
213
|
+
function moveActive(delta: -1 | 1) {
|
|
214
|
+
const max = currentList.value.length - 1
|
|
215
|
+
if (max < 0) return
|
|
216
|
+
let next = activeIndex.value + delta
|
|
217
|
+
if (next < 0) next = 0
|
|
218
|
+
if (next > max) next = max
|
|
219
|
+
activeIndex.value = next
|
|
220
|
+
scrollActiveIntoView()
|
|
213
221
|
}
|
|
214
222
|
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
function scrollActiveIntoView() {
|
|
224
|
+
void nextTick(() => {
|
|
225
|
+
const list = document.querySelector(`.${cls.e('panel-list')}`)
|
|
226
|
+
if (!list) return
|
|
227
|
+
const item = list.querySelector(`[data-idx="${activeIndex.value}"]`)
|
|
228
|
+
if (item && 'scrollIntoView' in item) {
|
|
229
|
+
;(item as HTMLElement).scrollIntoView({ block: 'nearest' })
|
|
230
|
+
}
|
|
231
|
+
})
|
|
223
232
|
}
|
|
224
233
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (!
|
|
234
|
+
function handleVisibleChange(v: boolean) {
|
|
235
|
+
emit('update:visible', v)
|
|
236
|
+
if (!v) reset()
|
|
237
|
+
}
|
|
228
238
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
239
|
+
function reset() {
|
|
240
|
+
navPath.value = []
|
|
241
|
+
activeIndex.value = 0
|
|
242
|
+
}
|
|
234
243
|
|
|
235
|
-
|
|
244
|
+
watch(
|
|
245
|
+
() => props.visible,
|
|
246
|
+
(v) => {
|
|
247
|
+
if (!v) reset()
|
|
248
|
+
else activeIndex.value = 0
|
|
249
|
+
}
|
|
250
|
+
)
|
|
236
251
|
|
|
237
|
-
|
|
238
|
-
|
|
252
|
+
watch(
|
|
253
|
+
() => props.filter,
|
|
254
|
+
() => {
|
|
255
|
+
// 过滤词变化:重置 active;逐级 → 扁平时清空 navPath
|
|
239
256
|
activeIndex.value = 0
|
|
257
|
+
if (isFlat.value) navPath.value = []
|
|
240
258
|
}
|
|
259
|
+
)
|
|
241
260
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
activeIndex.value = 0
|
|
268
|
-
}
|
|
269
|
-
} else if (e.key === 'Enter') {
|
|
270
|
-
// 回车键:确定选择(仅对最末级项有效)
|
|
271
|
-
e.preventDefault()
|
|
272
|
-
if (currentItem) {
|
|
273
|
-
if (!currentItem.children || currentItem.children.length === 0) {
|
|
274
|
-
// 只有叶子节点才能选择
|
|
275
|
-
handleItemClick(currentItem)
|
|
261
|
+
/**
|
|
262
|
+
* 由父级 forward 进来的键盘事件。
|
|
263
|
+
* 返回 true 表示已处理(外部应 preventDefault)。
|
|
264
|
+
*/
|
|
265
|
+
function handleKeydown(e: KeyboardEvent): boolean {
|
|
266
|
+
if (!props.visible) return false
|
|
267
|
+
|
|
268
|
+
switch (e.key) {
|
|
269
|
+
case 'Escape':
|
|
270
|
+
emit('dismiss')
|
|
271
|
+
emit('update:visible', false)
|
|
272
|
+
reset()
|
|
273
|
+
return true
|
|
274
|
+
case 'ArrowDown':
|
|
275
|
+
moveActive(1)
|
|
276
|
+
return true
|
|
277
|
+
case 'ArrowUp':
|
|
278
|
+
moveActive(-1)
|
|
279
|
+
return true
|
|
280
|
+
case 'ArrowLeft':
|
|
281
|
+
// 仅逐级模式:返回上一级;扁平模式下不拦截,让光标正常移动并触发 mention dismiss
|
|
282
|
+
if (!isFlat.value && navPath.value.length > 0) {
|
|
283
|
+
navPath.value = navPath.value.slice(0, -1)
|
|
284
|
+
activeIndex.value = 0
|
|
285
|
+
return true
|
|
276
286
|
}
|
|
287
|
+
return false
|
|
288
|
+
case 'ArrowRight': {
|
|
289
|
+
if (isFlat.value) return false
|
|
290
|
+
const item = currentList.value[activeIndex.value]
|
|
291
|
+
if (item && hasChildren(item)) {
|
|
292
|
+
navPath.value = [...navPath.value, item]
|
|
293
|
+
activeIndex.value = 0
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
return false
|
|
277
297
|
}
|
|
298
|
+
case 'Enter':
|
|
299
|
+
selectActive()
|
|
300
|
+
return true
|
|
301
|
+
default:
|
|
302
|
+
return false
|
|
278
303
|
}
|
|
279
304
|
}
|
|
280
305
|
|
|
281
|
-
|
|
282
|
-
watch(
|
|
283
|
-
() => props.visible,
|
|
284
|
-
(v) => {
|
|
285
|
-
if (v) {
|
|
286
|
-
document.addEventListener('keydown', handleKeydown)
|
|
287
|
-
} else {
|
|
288
|
-
document.removeEventListener('keydown', handleKeydown)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
onMounted(() => {
|
|
294
|
-
props.registerPickerKeyHandler?.(handleKeydown)
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
onBeforeUnmount(() => {
|
|
298
|
-
document.removeEventListener('keydown', handleKeydown)
|
|
299
|
-
props.registerPickerKeyHandler?.(null)
|
|
300
|
-
})
|
|
306
|
+
defineExpose({ handleKeydown })
|
|
301
307
|
</script>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { BEM } from '@veltra/utils'
|
|
2
|
+
|
|
3
|
+
import type { Segment } from './model'
|
|
4
|
+
|
|
5
|
+
export interface ChipDeps {
|
|
6
|
+
cls: BEM<'expression-editor'>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ChipFactoryOptions extends ChipDeps {
|
|
10
|
+
/**
|
|
11
|
+
* 当用户点击 chip 主体(不是 ×)时回调,由调用方决定如何打开重选面板。
|
|
12
|
+
* 收到的 chipEl 当前在 DOM 中的位置即为重选锚点。
|
|
13
|
+
*/
|
|
14
|
+
onReselect: (chipEl: HTMLElement) => void
|
|
15
|
+
/** 当用户点击 × 时回调,调用方应将该 chip 从模型 / DOM 中移除。 */
|
|
16
|
+
onRemove: (chipEl: HTMLElement) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CHIP_DATA_SEG = 'var'
|
|
20
|
+
const CHIP_VALUE_ATTR = 'data-value'
|
|
21
|
+
const CHIP_TYPE_ATTR = 'data-var-type'
|
|
22
|
+
const CHIP_CLOSE_ATTR = 'data-chip-close'
|
|
23
|
+
|
|
24
|
+
/** 判断一个 DOM 节点是否为 chip 元素(var 段渲染体) */
|
|
25
|
+
export function isChipElement(node: Node | null): node is HTMLElement {
|
|
26
|
+
return (
|
|
27
|
+
!!node &&
|
|
28
|
+
node.nodeType === 1 &&
|
|
29
|
+
(node as HTMLElement).getAttribute('data-seg') === CHIP_DATA_SEG
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 判断点击目标是否落在 × 删除区。 */
|
|
34
|
+
function isCloseTarget(target: EventTarget | null): boolean {
|
|
35
|
+
if (!(target instanceof Element)) return false
|
|
36
|
+
return !!target.closest(`[${CHIP_CLOSE_ATTR}]`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 从给定 segment 创建一个 chip 元素(var 段渲染体)。 */
|
|
40
|
+
export function createChip(
|
|
41
|
+
segment: Extract<Segment, { kind: 'var' }>,
|
|
42
|
+
opts: ChipFactoryOptions
|
|
43
|
+
): HTMLElement {
|
|
44
|
+
const { cls } = opts
|
|
45
|
+
|
|
46
|
+
const chip = document.createElement('span')
|
|
47
|
+
chip.setAttribute('data-seg', CHIP_DATA_SEG)
|
|
48
|
+
chip.setAttribute(CHIP_VALUE_ATTR, segment.value)
|
|
49
|
+
if (segment.type) chip.setAttribute(CHIP_TYPE_ATTR, segment.type)
|
|
50
|
+
chip.setAttribute('contenteditable', 'false')
|
|
51
|
+
chip.className = cls.e('chip')
|
|
52
|
+
chip.title = segment.label
|
|
53
|
+
|
|
54
|
+
const label = document.createElement('span')
|
|
55
|
+
label.className = cls.e('chip-label')
|
|
56
|
+
label.textContent = segment.type ? `${segment.label} (${segment.type})` : segment.label
|
|
57
|
+
|
|
58
|
+
const close = document.createElement('span')
|
|
59
|
+
close.className = cls.e('chip-close')
|
|
60
|
+
close.setAttribute(CHIP_CLOSE_ATTR, 'true')
|
|
61
|
+
close.setAttribute('aria-label', '删除')
|
|
62
|
+
close.setAttribute('role', 'button')
|
|
63
|
+
close.textContent = '×'
|
|
64
|
+
|
|
65
|
+
chip.appendChild(label)
|
|
66
|
+
chip.appendChild(close)
|
|
67
|
+
|
|
68
|
+
chip.addEventListener('mousedown', (e) => {
|
|
69
|
+
e.preventDefault()
|
|
70
|
+
if (isCloseTarget(e.target)) {
|
|
71
|
+
opts.onRemove(chip)
|
|
72
|
+
} else {
|
|
73
|
+
opts.onReselect(chip)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return chip
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 读取 chip 元素绑定的变量 value。 */
|
|
81
|
+
export function readChipValue(el: HTMLElement): string {
|
|
82
|
+
return el.getAttribute(CHIP_VALUE_ATTR) ?? ''
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 读取 chip 元素绑定的可选 type。 */
|
|
86
|
+
export function readChipType(el: HTMLElement): string | undefined {
|
|
87
|
+
return el.getAttribute(CHIP_TYPE_ATTR) ?? undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 在 chip 上设置 / 取消选中(焦点)样式。 */
|
|
91
|
+
export function setChipFocused(el: HTMLElement, focused: boolean): void {
|
|
92
|
+
el.classList.toggle('is-focused', focused)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 测试支持:暴露用于 DOM 选择器的属性常量。 */
|
|
96
|
+
export const ChipAttrs = {
|
|
97
|
+
seg: 'data-seg',
|
|
98
|
+
value: CHIP_VALUE_ATTR,
|
|
99
|
+
type: CHIP_TYPE_ATTR,
|
|
100
|
+
close: CHIP_CLOSE_ATTR
|
|
101
|
+
} as const
|