@veltra/desktop 1.0.13 → 1.0.14

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.
Files changed (180) hide show
  1. package/dist/components/action/action-group.js +110 -3
  2. package/dist/components/action/action-group.js.map +1 -1
  3. package/dist/components/action/action-group.vue.d.ts +16 -3
  4. package/dist/components/action/action.js +26 -13
  5. package/dist/components/action/action.js.map +1 -1
  6. package/dist/components/action/action.vue.d.ts +2 -2
  7. package/dist/components/action/style.js +2 -1
  8. package/dist/components/action/style2.css +35 -7
  9. package/dist/components/auto-complete/auto-complete.vue.d.ts +2 -2
  10. package/dist/components/batch-edit/batch-edit-form.js +106 -42
  11. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  12. package/dist/components/batch-edit/batch-edit-list.js +84 -47
  13. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  14. package/dist/components/batch-edit/batch-edit.js +42 -10
  15. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  16. package/dist/components/batch-edit/style.js +2 -4
  17. package/dist/components/batch-edit/style2.css +202 -30
  18. package/dist/components/batch-edit/use-edit.js +45 -9
  19. package/dist/components/batch-edit/use-edit.js.map +1 -1
  20. package/dist/components/cascade/cascade.js +3 -1
  21. package/dist/components/cascade/cascade.js.map +1 -1
  22. package/dist/components/cascade/cascade.vue.d.ts +2 -2
  23. package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
  24. package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
  25. package/dist/components/code-editor/code-editor.js +145 -49
  26. package/dist/components/code-editor/code-editor.js.map +1 -1
  27. package/dist/components/code-editor/code-editor.vue.d.ts +2 -0
  28. package/dist/components/code-editor/style2.css +87 -19
  29. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  30. package/dist/components/date-picker/date-picker.vue.d.ts +2 -2
  31. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +2 -2
  32. package/dist/components/dialog/dialog.js +6 -2
  33. package/dist/components/dialog/dialog.js.map +1 -1
  34. package/dist/components/dialog/style2.css +2 -3
  35. package/dist/components/expression-editor/components/path-preview.js +27 -0
  36. package/dist/components/expression-editor/components/path-preview.js.map +1 -0
  37. package/dist/components/expression-editor/components/variable-picker.js +199 -153
  38. package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
  39. package/dist/components/expression-editor/core/chip.js +54 -0
  40. package/dist/components/expression-editor/core/chip.js.map +1 -0
  41. package/dist/components/expression-editor/core/editor.js +424 -0
  42. package/dist/components/expression-editor/core/editor.js.map +1 -0
  43. package/dist/components/expression-editor/core/mention.js +91 -0
  44. package/dist/components/expression-editor/core/mention.js.map +1 -0
  45. package/dist/components/expression-editor/core/model.js +64 -0
  46. package/dist/components/expression-editor/core/model.js.map +1 -0
  47. package/dist/components/expression-editor/di.js +4 -10
  48. package/dist/components/expression-editor/di.js.map +1 -1
  49. package/dist/components/expression-editor/expression-editor.js +168 -114
  50. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  51. package/dist/components/expression-editor/expression-editor.vue.d.ts +2 -1
  52. package/dist/components/expression-editor/style.js +0 -2
  53. package/dist/components/expression-editor/style2.css +145 -219
  54. package/dist/components/index.js +54 -54
  55. package/dist/components/input/input.vue.d.ts +2 -2
  56. package/dist/components/layout/layout.js +12 -11
  57. package/dist/components/layout/layout.js.map +1 -1
  58. package/dist/components/multi-select/multi-select.vue.d.ts +3 -3
  59. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +3 -3
  60. package/dist/components/notification/notification.vue.d.ts +1 -1
  61. package/dist/components/number/number.vue.d.ts +1 -1
  62. package/dist/components/number-input/number-input.js +4 -4
  63. package/dist/components/number-input/number-input.js.map +1 -1
  64. package/dist/components/number-input/number-input.vue.d.ts +2 -2
  65. package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
  66. package/dist/components/password-input/password-input.vue.d.ts +1 -1
  67. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
  68. package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
  69. package/dist/components/rich-text-editor/rich-text-editor.js +1 -1
  70. package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
  71. package/dist/components/rich-text-editor/toolbar.js +2 -2
  72. package/dist/components/select/select.vue.d.ts +2 -2
  73. package/dist/components/steps/steps.vue.d.ts +1 -1
  74. package/dist/components/table/use-table.js +16 -0
  75. package/dist/components/table/use-table.js.map +1 -1
  76. package/dist/components/tabs/tabs-horizontal.vue.d.ts +1 -1
  77. package/dist/components/tabs/tabs-vertical.vue.d.ts +1 -1
  78. package/dist/components/tabs/tabs.vue.d.ts +1 -1
  79. package/dist/components/textarea/textarea.vue.d.ts +2 -2
  80. package/dist/components/tip/tip.vue.d.ts +1 -1
  81. package/dist/components/tree/style2.css +18 -12
  82. package/dist/components/tree/tree-node.js +7 -4
  83. package/dist/components/tree/tree-node.js.map +1 -1
  84. package/dist/components/tree-select/tree-select.vue.d.ts +3 -3
  85. package/dist/index.d.ts +3 -3
  86. package/dist/index.js +83 -83
  87. package/dist/style.js +3 -3
  88. package/dist/types/action.d.ts +38 -4
  89. package/dist/types/cascade.d.ts +2 -3
  90. package/dist/types/code-editor.d.ts +9 -2
  91. package/dist/types/expression-editor.d.ts +9 -1
  92. package/dist/types/index.d.ts +2 -2
  93. package/package.json +11 -12
  94. package/src/components/action/action-group.vue +126 -56
  95. package/src/components/action/action.vue +35 -12
  96. package/src/components/action/style.scss +46 -10
  97. package/src/components/action/style.ts +2 -1
  98. package/src/components/batch-edit/batch-edit-form.vue +121 -45
  99. package/src/components/batch-edit/batch-edit-list.vue +56 -39
  100. package/src/components/batch-edit/batch-edit.vue +57 -6
  101. package/src/components/batch-edit/style.scss +251 -37
  102. package/src/components/batch-edit/style.ts +5 -8
  103. package/src/components/batch-edit/use-edit.ts +58 -11
  104. package/src/components/cascade/cascade.vue +3 -1
  105. package/src/components/code-editor/code-editor.vue +198 -63
  106. package/src/components/code-editor/style.scss +110 -12
  107. package/src/components/dialog/dialog.vue +6 -2
  108. package/src/components/dialog/style.scss +4 -4
  109. package/src/components/expression-editor/__test__/chip.test.ts +86 -0
  110. package/src/components/expression-editor/__test__/mention.test.ts +122 -0
  111. package/src/components/expression-editor/__test__/model.test.ts +127 -0
  112. package/src/components/expression-editor/components/path-preview.vue +32 -0
  113. package/src/components/expression-editor/components/variable-picker.vue +224 -219
  114. package/src/components/expression-editor/core/chip.ts +101 -0
  115. package/src/components/expression-editor/core/editor.ts +513 -0
  116. package/src/components/expression-editor/core/mention.ts +121 -0
  117. package/src/components/expression-editor/core/model.ts +109 -0
  118. package/src/components/expression-editor/di.ts +4 -32
  119. package/src/components/expression-editor/expression-editor.vue +184 -151
  120. package/src/components/expression-editor/style.scss +148 -220
  121. package/src/components/expression-editor/style.ts +0 -2
  122. package/src/components/layout/layout.vue +17 -19
  123. package/src/components/number-input/number-input.vue +4 -4
  124. package/src/components/table/use-table.ts +16 -0
  125. package/src/components/tree/style.scss +30 -33
  126. package/src/components/tree/tree-node.vue +4 -2
  127. package/src/types/action.ts +42 -4
  128. package/src/types/cascade.ts +4 -3
  129. package/src/types/code-editor.ts +9 -2
  130. package/src/types/expression-editor.ts +9 -0
  131. package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js +0 -65
  132. package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js.map +0 -1
  133. package/dist/components/expression-editor/constants.js +0 -9
  134. package/dist/components/expression-editor/constants.js.map +0 -1
  135. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js +0 -47
  136. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js.map +0 -1
  137. package/dist/components/expression-editor/internal/editor-runtime/model-sync.js +0 -49
  138. package/dist/components/expression-editor/internal/editor-runtime/model-sync.js.map +0 -1
  139. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js +0 -215
  140. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js.map +0 -1
  141. package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js +0 -33
  142. package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js.map +0 -1
  143. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js +0 -33
  144. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js.map +0 -1
  145. package/dist/components/expression-editor/nodes/variable-node.js +0 -94
  146. package/dist/components/expression-editor/nodes/variable-node.js.map +0 -1
  147. package/dist/components/expression-editor/parser.js +0 -32
  148. package/dist/components/expression-editor/parser.js.map +0 -1
  149. package/dist/components/expression-editor/plain-text.js +0 -19
  150. package/dist/components/expression-editor/plain-text.js.map +0 -1
  151. package/dist/components/expression-editor/use-context.js +0 -69
  152. package/dist/components/expression-editor/use-context.js.map +0 -1
  153. package/dist/components/expression-editor/use-decorators.js +0 -27
  154. package/dist/components/expression-editor/use-decorators.js.map +0 -1
  155. package/dist/components/expression-editor/use-expression-drag-drop.js +0 -275
  156. package/dist/components/expression-editor/use-expression-drag-drop.js.map +0 -1
  157. package/src/components/batch-edit/batch-edit-new.vue +0 -168
  158. package/src/components/batch-edit/style-new.scss +0 -35
  159. package/src/components/batch-edit/use-edit-new.ts +0 -68
  160. package/src/components/batch-edit/use-tip.ts +0 -47
  161. package/src/components/expression-editor/__test__/architecture-boundaries.test.ts +0 -103
  162. package/src/components/expression-editor/__test__/architecture-compatibility.test.ts +0 -52
  163. package/src/components/expression-editor/__test__/drag-drop.test.ts +0 -171
  164. package/src/components/expression-editor/__test__/parser.test.ts +0 -62
  165. package/src/components/expression-editor/constants.ts +0 -6
  166. package/src/components/expression-editor/internal/contracts/editor-runtime.ts +0 -11
  167. package/src/components/expression-editor/internal/editor-runtime/create-runtime.ts +0 -64
  168. package/src/components/expression-editor/internal/editor-runtime/index.ts +0 -1
  169. package/src/components/expression-editor/internal/editor-runtime/model-sync.ts +0 -68
  170. package/src/components/expression-editor/internal/features/commands/register-command-packs.ts +0 -517
  171. package/src/components/expression-editor/internal/features/drag-drop/drag-drop-service.ts +0 -51
  172. package/src/components/expression-editor/internal/features/insertion/insertion-service.ts +0 -56
  173. package/src/components/expression-editor/nodes/variable-node.tsx +0 -118
  174. package/src/components/expression-editor/parser.ts +0 -46
  175. package/src/components/expression-editor/plain-text.ts +0 -25
  176. package/src/components/expression-editor/plan.md +0 -22
  177. package/src/components/expression-editor/use-context.ts +0 -134
  178. package/src/components/expression-editor/use-decorators.tsx +0 -29
  179. package/src/components/expression-editor/use-editor.ts +0 -12
  180. package/src/components/expression-editor/use-expression-drag-drop.ts +0 -396
@@ -0,0 +1,109 @@
1
+ import type { VariableItem } from '../../../types'
2
+
3
+ /** 单个文档段:纯文本或变量 chip */
4
+ export type Segment =
5
+ | { kind: 'text'; value: string }
6
+ | { kind: 'var'; value: string; label: string; type?: string }
7
+
8
+ /** 归一化的文档形态:相邻 text 段会合并、空 text 段不保留 */
9
+ export type Doc = Segment[]
10
+
11
+ const VARIABLE_PATTERN = /\{([^}]+)\}/g
12
+
13
+ /**
14
+ * 反序列化:将 modelValue 字符串解析为 Segment[]。
15
+ * 沿用旧 parser.ts 的正则切分逻辑,保证 round-trip 与历史一致。
16
+ */
17
+ export function parse(content: string, variableMap?: Map<string, VariableItem>): Doc {
18
+ const segments: Segment[] = []
19
+ if (!content) return segments
20
+
21
+ let cursor = 0
22
+ for (const match of content.matchAll(VARIABLE_PATTERN)) {
23
+ const value = match[1]!
24
+ const start = match.index!
25
+ if (start > cursor) {
26
+ segments.push({ kind: 'text', value: content.slice(cursor, start) })
27
+ }
28
+ const found = variableMap?.get(value)
29
+ segments.push({
30
+ kind: 'var',
31
+ value,
32
+ label: found?.label ?? value,
33
+ ...(found?.type ? { type: found.type } : {})
34
+ })
35
+ cursor = start + match[0].length
36
+ }
37
+ if (cursor < content.length) {
38
+ segments.push({ kind: 'text', value: content.slice(cursor) })
39
+ }
40
+
41
+ return normalize(segments)
42
+ }
43
+
44
+ /** 序列化:var 段输出 `{value}`,text 段直接拼接。 */
45
+ export function serialize(doc: Doc): string {
46
+ let out = ''
47
+ for (const seg of doc) {
48
+ out += seg.kind === 'text' ? seg.value : `{${seg.value}}`
49
+ }
50
+ return out
51
+ }
52
+
53
+ /**
54
+ * 归一化:合并相邻 text 段、丢弃空 text 段。
55
+ * 注意:DOM 层另需保证 var 两侧有 text 占位(即便空),由 editor 渲染时补齐。
56
+ */
57
+ export function normalize(doc: Doc): Doc {
58
+ const out: Segment[] = []
59
+ for (const seg of doc) {
60
+ if (seg.kind === 'text') {
61
+ if (!seg.value) continue
62
+ const last = out[out.length - 1]
63
+ if (last && last.kind === 'text') {
64
+ out[out.length - 1] = { kind: 'text', value: last.value + seg.value }
65
+ continue
66
+ }
67
+ }
68
+ out.push(seg)
69
+ }
70
+ return out
71
+ }
72
+
73
+ interface SplitResult {
74
+ before: Doc
75
+ after: Doc
76
+ }
77
+
78
+ /**
79
+ * 在序列化后的字符串偏移上切分(不会切开 var)。
80
+ * 若偏移落在 var 中间,向前对齐到 var 起点。
81
+ */
82
+ export function splitAt(doc: Doc, globalOffset: number): SplitResult {
83
+ const before: Doc = []
84
+ const after: Doc = []
85
+ let pos = 0
86
+ let consumed = false
87
+
88
+ for (const seg of doc) {
89
+ const segLen = seg.kind === 'text' ? seg.value.length : seg.value.length + 2
90
+ if (consumed) {
91
+ after.push(seg)
92
+ continue
93
+ }
94
+ if (globalOffset >= pos + segLen) {
95
+ before.push(seg)
96
+ pos += segLen
97
+ continue
98
+ }
99
+ if (seg.kind === 'text') {
100
+ const offset = globalOffset - pos
101
+ if (offset > 0) before.push({ kind: 'text', value: seg.value.slice(0, offset) })
102
+ if (offset < seg.value.length) after.push({ kind: 'text', value: seg.value.slice(offset) })
103
+ } else {
104
+ after.push(seg)
105
+ }
106
+ consumed = true
107
+ }
108
+ return { before, after }
109
+ }
@@ -1,43 +1,15 @@
1
- import type { BEM } from '@veltra/utils'
2
- import type { LexicalEditor } from 'lexical'
3
- import type { InjectionKey, ComputedRef } from 'vue'
1
+ import type { VariableItem } from '../../types'
4
2
 
5
- import type { ExpressionEditorProps, VariableItem } from '../../types'
6
-
7
- /**
8
- * 扁平化变量树,创建 value -> VariableItem 的映射
9
- * @param variables 变量列表(支持树形结构)
10
- * @returns value -> VariableItem 的映射表
11
- */
3
+ /** 扁平化变量树,创建 value -> VariableItem 的映射,用于 DOM → 模型反推时补齐 label/type。 */
12
4
  export function createVariableMap(variables?: VariableItem[]): Map<string, VariableItem> {
13
5
  const map = new Map<string, VariableItem>()
14
-
15
- function traverse(items: VariableItem[] | undefined, parentPath: string[] = []) {
6
+ function traverse(items: VariableItem[] | undefined) {
16
7
  if (!items) return
17
-
18
8
  for (const item of items) {
19
- const currentPath = [...parentPath, item.label]
20
9
  map.set(item.value, item)
21
-
22
- if (item.children) {
23
- traverse(item.children, currentPath)
24
- }
10
+ if (item.children) traverse(item.children)
25
11
  }
26
12
  }
27
-
28
13
  traverse(variables)
29
14
  return map
30
15
  }
31
-
32
- export const ExpressionEditorDIKey: InjectionKey<{
33
- /** 组件的 BEM 类名 */
34
- cls: BEM<'expression-editor'>
35
- /** 组件的 props */
36
- editorProps: ExpressionEditorProps
37
- /** 编辑器实例 */
38
- editor: LexicalEditor
39
- /** 更新变量节点 */
40
- updateVariableNode: (oldValue: string, newValue: string, newLabel?: string) => void
41
- /** 变量映射表 (value -> VariableItem) */
42
- variableMap: ComputedRef<Map<string, VariableItem>>
43
- }> = Symbol('ExpressionEditorDIKey')
@@ -1,56 +1,29 @@
1
1
  <template>
2
2
  <div :class="className">
3
- <!-- 编辑容器 -->
4
3
  <div
5
- :class="cls.e('container')"
6
4
  ref="container"
5
+ :class="cls.e('container')"
7
6
  :contenteditable="!readonly && !disabled"
7
+ :data-empty="isEmpty || undefined"
8
+ :data-placeholder="props.placeholder"
9
+ @keydown="onKeydown"
10
+ @blur="onBlur"
8
11
  ></div>
9
12
 
10
- <div v-if="showFallbackControls" :class="cls.e('fallback')">
11
- <div
12
- v-for="(item, index) in fallbackVariables"
13
- :key="item.key"
14
- :class="cls.e('fallback-item')"
15
- >
16
- <span :class="cls.e('fallback-label')">
17
- {{ item.label || item.variable }}
18
- </span>
19
- <div :class="cls.e('fallback-actions')">
20
- <button
21
- type="button"
22
- :class="cls.e('fallback-button')"
23
- :disabled="disabled || readonly || index === 0"
24
- @click="moveVariable(item.key, -1)"
25
- >
26
- 上移
27
- </button>
28
- <button
29
- type="button"
30
- :class="cls.e('fallback-button')"
31
- :disabled="disabled || readonly || index === fallbackVariables.length - 1"
32
- @click="moveVariable(item.key, 1)"
33
- >
34
- 下移
35
- </button>
36
- </div>
37
- </div>
38
- </div>
39
-
40
- <div :class="cls.e('placeholder')" v-if="showPlaceholder">
41
- {{ placeholder }}
13
+ <div v-if="showPlaceholder" :class="cls.e('placeholder')">
14
+ {{ props.placeholder }}
42
15
  </div>
43
16
 
44
- <!-- 渲染装饰器节点,内部通过Vue的Teleport组件渲染至节点中 -->
45
- <Decorators :decorators="decorators" />
46
-
47
- <!-- 变量选择器 -->
48
17
  <VariablePicker
49
- v-model:visible="contextVisible"
50
- :trigger-dom="contextTriggerDom"
51
- :filterable="true"
52
- :register-picker-key-handler="registerPickerKeyHandler"
53
- @select="handleVariableSelect"
18
+ ref="pickerRef"
19
+ :visible="pickerVisible"
20
+ :trigger-dom="pickerTriggerDom"
21
+ :variables="props.variables"
22
+ :filter="pickerFilter"
23
+ :selectable-levels="props.selectableLevels ?? 'leaf'"
24
+ @select="onPickerSelect"
25
+ @dismiss="onPickerDismiss"
26
+ @update:visible="onPickerVisibleChange"
54
27
  />
55
28
  </div>
56
29
  </template>
@@ -58,154 +31,214 @@
58
31
  <script lang="ts" setup>
59
32
  import { useFormComponent, useFormFallbackProps } from '@veltra/compositions'
60
33
  import { bem } from '@veltra/utils'
61
- import { $getRoot } from 'lexical'
62
- import { useTemplateRef, provide, computed, onBeforeUnmount, shallowRef, type VNode } from 'vue'
34
+ import {
35
+ computed,
36
+ nextTick,
37
+ onBeforeUnmount,
38
+ onMounted,
39
+ shallowRef,
40
+ useTemplateRef,
41
+ watch
42
+ } from 'vue'
63
43
 
64
44
  import type { ExpressionEditorProps, VariableItem } from '../../types'
65
45
  import VariablePicker from './components/variable-picker.vue'
66
- import { ExpressionEditorDIKey, createVariableMap } from './di'
67
- import { createExpressionEditorRuntime } from './internal/editor-runtime'
68
- import { moveVariableByDirection } from './internal/features/drag-drop/drag-drop-service'
69
- import { insertVariableAtTrigger } from './internal/features/insertion/insertion-service'
70
- import { $isVariableNode } from './nodes/variable-node'
71
- import { registerPlainText } from './plain-text'
72
- import { useContext } from './use-context'
73
- import { useDecorators } from './use-decorators'
74
- import { collectVariableNodeDescriptors, supportsNativeDnD } from './use-expression-drag-drop'
46
+ import { createEditor, type EditorAPI } from './core/editor'
47
+ import { createMention, type MentionAPI } from './core/mention'
48
+ import { parse, type Doc } from './core/model'
49
+ import { createVariableMap } from './di'
75
50
 
76
51
  defineOptions({
77
52
  name: 'ExpressionEditor'
78
53
  })
79
54
 
80
55
  const props = withDefaults(defineProps<ExpressionEditorProps>(), {
81
- placeholder: '请输入表达式,输入 @ 可插入变量'
56
+ placeholder: '请输入表达式,输入 @ 可插入变量',
57
+ selectableLevels: 'leaf'
82
58
  })
83
59
 
84
60
  const emit = defineEmits<{
85
61
  (e: 'update:modelValue', value: string): void
86
62
  }>()
87
63
 
88
- const showPlaceholder = computed(() => !props.modelValue || props.modelValue.trim() === '')
89
-
90
64
  const cls = bem('expression-editor')
91
-
92
65
  const { formProps } = useFormComponent()
93
-
94
66
  const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])
95
67
 
96
- const className = computed(() => {
97
- return [
98
- cls.b,
99
- cls.m(size.value),
100
- bem.is('disabled', disabled.value),
101
- bem.is('readonly', readonly.value)
102
- ]
103
- })
68
+ const containerRef = useTemplateRef<HTMLDivElement>('container')
69
+ const pickerRef = useTemplateRef<{ handleKeydown: (e: KeyboardEvent) => boolean }>('pickerRef')
104
70
 
105
- const containerRef = useTemplateRef('container')
71
+ const className = computed(() => [
72
+ cls.b,
73
+ cls.m(size.value),
74
+ bem.is('disabled', disabled.value),
75
+ bem.is('readonly', readonly.value)
76
+ ])
106
77
 
107
- const { editor } = createExpressionEditorRuntime({
108
- disabled,
109
- readonly,
110
- cls,
111
- container: containerRef,
112
- props,
113
- emit
114
- })
78
+ const variableMap = computed(() => createVariableMap(props.variables))
115
79
 
116
- const {
117
- contextVisible,
118
- contextTriggerDom,
119
- textNode,
120
- charPosition,
121
- registerPickerKeyHandler,
122
- registerContextCommands
123
- } = useContext(editor)
124
-
125
- registerPlainText(editor, {
126
- getContextCommands: registerContextCommands
127
- })
80
+ const editorRef = shallowRef<EditorAPI | null>(null)
81
+ const mention: MentionAPI = createMention()
82
+
83
+ const currentDoc = shallowRef<Doc>([])
84
+ const isEmpty = computed(() => currentDoc.value.length === 0)
85
+ const showPlaceholder = computed(() => isEmpty.value && !disabled.value)
86
+
87
+ const pickerMode = shallowRef<'mention' | 'reselect' | null>(null)
88
+ const pickerTriggerDom = shallowRef<HTMLElement | undefined>(undefined)
89
+ const reselectingSegIdx = shallowRef<number | null>(null)
90
+ const pickerFilter = shallowRef('')
91
+
92
+ const pickerVisible = computed(() => pickerMode.value !== null)
93
+
94
+ function syncMention() {
95
+ const editor = editorRef.value
96
+ if (!editor) return
97
+ if (pickerMode.value === 'reselect') return
98
+ const caret = editor.getCaretOffset()
99
+ const state = mention.update(currentDoc.value, caret)
100
+ if (state) {
101
+ pickerMode.value = 'mention'
102
+ pickerFilter.value = state.filter
103
+ pickerTriggerDom.value = containerRef.value ?? undefined
104
+ } else if (pickerMode.value === 'mention') {
105
+ closePicker()
106
+ }
107
+ }
128
108
 
129
- const decorators = useDecorators(editor)
130
- const nativeDnDSupported = supportsNativeDnD()
131
- const fallbackVariables = shallowRef(collectVariableNodeDescriptors(editor))
132
- const showFallbackControls = computed(
133
- () => !nativeDnDSupported && fallbackVariables.value.length > 1
134
- )
109
+ function closePicker() {
110
+ pickerMode.value = null
111
+ reselectingSegIdx.value = null
112
+ pickerFilter.value = ''
113
+ }
135
114
 
136
- const removeFallbackSync = editor.registerUpdateListener(() => {
137
- fallbackVariables.value = collectVariableNodeDescriptors(editor)
115
+ onMounted(() => {
116
+ if (!containerRef.value) return
117
+ const initialDoc = parse(props.modelValue ?? '', variableMap.value)
118
+ currentDoc.value = initialDoc
119
+
120
+ const editor = createEditor({
121
+ container: containerRef.value,
122
+ cls,
123
+ initialDoc,
124
+ getVariableMap: () => variableMap.value,
125
+ onChange: (doc) => {
126
+ currentDoc.value = doc
127
+ const value = serializeFromDoc(doc)
128
+ if (value !== (props.modelValue ?? '')) emit('update:modelValue', value)
129
+ syncMention()
130
+ },
131
+ onSelectionChange: () => {
132
+ syncMention()
133
+ },
134
+ onChipReselect: ({ chipEl, segIndex }) => {
135
+ if (disabled.value || readonly.value) return
136
+ mention.commit()
137
+ pickerMode.value = 'reselect'
138
+ pickerFilter.value = ''
139
+ pickerTriggerDom.value = chipEl
140
+ reselectingSegIdx.value = segIndex
141
+ },
142
+ onChipRemove: ({ segIndex }) => {
143
+ if (disabled.value || readonly.value) return
144
+ editor.removeVarAt(segIndex)
145
+ }
146
+ })
147
+ editorRef.value = editor
138
148
  })
139
149
 
140
150
  onBeforeUnmount(() => {
141
- removeFallbackSync()
151
+ editorRef.value?.dispose()
142
152
  })
143
153
 
144
- // 创建变量映射表
145
- const variableMap = computed(() => createVariableMap(props.variables))
146
-
147
- function Decorators(props: { decorators: VNode[] }) {
148
- return props.decorators
154
+ function serializeFromDoc(doc: Doc): string {
155
+ let s = ''
156
+ for (const seg of doc) s += seg.kind === 'text' ? seg.value : `{${seg.value}}`
157
+ return s
149
158
  }
150
159
 
151
- function moveVariable(variableKey: string, direction: -1 | 1) {
152
- if (disabled.value || readonly.value) return
160
+ watch(
161
+ () => props.modelValue,
162
+ (v) => {
163
+ const editor = editorRef.value
164
+ if (!editor) return
165
+ if (v === editor.getValue()) return
166
+ editor.setValue(v ?? '')
167
+ }
168
+ )
153
169
 
154
- moveVariableByDirection(editor, variableKey, direction, true)
155
- }
170
+ watch(variableMap, () => {
171
+ // variables 变化:重渲染以刷新 chip label / type
172
+ const editor = editorRef.value
173
+ if (!editor) return
174
+ editor.setValue(editor.getValue())
175
+ })
156
176
 
157
- // 处理变量选择
158
- function handleVariableSelect(variable: VariableItem) {
177
+ function onKeydown(e: KeyboardEvent) {
159
178
  if (disabled.value || readonly.value) return
179
+ // mention / reselect 激活时,优先把箭头键 / Enter / Esc 转给 picker
180
+ if (pickerMode.value !== null) {
181
+ const handledByPicker = pickerRef.value?.handleKeydown(e)
182
+ if (handledByPicker) {
183
+ e.preventDefault()
184
+ return
185
+ }
186
+ }
160
187
 
161
- const nodeKey = textNode.value?.getKey()
162
- if (!nodeKey) return
163
-
164
- insertVariableAtTrigger(editor, {
165
- nodeKey,
166
- charPosition: charPosition.value,
167
- variable: {
168
- value: variable.value,
169
- label: variable.label,
170
- type: variable.type
188
+ if (pickerMode.value === 'mention') {
189
+ // 这些键导致 mention 退出(保留 @filter 文本,光标继续移动)
190
+ if (e.key === ' ' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
191
+ mention.dismiss()
192
+ closePicker()
171
193
  }
172
- })
194
+ }
195
+ }
196
+
197
+ function onBlur() {
198
+ if (pickerMode.value === 'mention') {
199
+ mention.dismiss()
200
+ // 下一帧再关闭,避免和 picker 内 click 冲突
201
+ void nextTick(() => closePicker())
202
+ }
173
203
  }
174
204
 
175
- // 更新变量节点的函数
176
- function updateVariableNode(oldValue: string, newValue: string, newLabel?: string) {
177
- editor.update(() => {
178
- const root = $getRoot()
179
-
180
- // 如果没有提供 label 或 type,从映射表中查找
181
- const variable = variableMap.value.get(newValue)
182
- const label = newLabel ?? variable?.label ?? newValue
183
- const type = variable?.type
184
-
185
- // 递归遍历所有节点
186
- function traverse(node: any) {
187
- if ($isVariableNode(node)) {
188
- if (node.getVariable() === oldValue) {
189
- node.updateVariable(newValue, label, type)
190
- }
191
- }
192
-
193
- // 如果节点有 getChildren 方法,继续遍历
194
- if (typeof node.getChildren === 'function') {
195
- const children = node.getChildren()
196
- children.forEach((child: any) => traverse(child))
197
- }
205
+ function onPickerSelect(item: VariableItem) {
206
+ const editor = editorRef.value
207
+ if (!editor) return
208
+
209
+ if (pickerMode.value === 'mention') {
210
+ const state = mention.getState()
211
+ if (!state) return
212
+ const anchor = state.anchorOffset
213
+ const caret = editor.getCaretOffset() ?? anchor + state.filter.length + 1
214
+ editor.replaceRangeWithVar(anchor, caret, {
215
+ value: item.value,
216
+ label: item.label,
217
+ ...(item.type ? { type: item.type } : {})
218
+ })
219
+ mention.commit()
220
+ } else if (pickerMode.value === 'reselect') {
221
+ const idx = reselectingSegIdx.value
222
+ if (idx !== null) {
223
+ editor.replaceVarAt(idx, {
224
+ value: item.value,
225
+ label: item.label,
226
+ ...(item.type ? { type: item.type } : {})
227
+ })
198
228
  }
229
+ }
230
+ closePicker()
231
+ }
199
232
 
200
- traverse(root)
201
- })
233
+ function onPickerDismiss() {
234
+ if (pickerMode.value === 'mention') mention.dismiss()
235
+ closePicker()
202
236
  }
203
237
 
204
- provide(ExpressionEditorDIKey, {
205
- cls,
206
- editorProps: props,
207
- editor,
208
- updateVariableNode,
209
- variableMap
210
- })
238
+ function onPickerVisibleChange(v: boolean) {
239
+ if (!v && pickerMode.value !== null) {
240
+ if (pickerMode.value === 'mention') mention.dismiss()
241
+ closePicker()
242
+ }
243
+ }
211
244
  </script>