@veltra/desktop 1.7.19 → 1.8.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.
Files changed (114) hide show
  1. package/dist/components/batch-edit/batch-edit-form.js +40 -26
  2. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  3. package/dist/components/batch-edit/use-handlers.js +3 -3
  4. package/dist/components/batch-edit/use-handlers.js.map +1 -1
  5. package/dist/components/batch-edit/use-shortcut-key.js +4 -2
  6. package/dist/components/batch-edit/use-shortcut-key.js.map +1 -1
  7. package/dist/components/card/style2.css +0 -2
  8. package/dist/components/condition-editor/condition-editor.js +8 -2
  9. package/dist/components/condition-editor/condition-editor.js.map +1 -1
  10. package/dist/components/dropdown/dropdown.js +5 -3
  11. package/dist/components/dropdown/dropdown.js.map +1 -1
  12. package/dist/components/expression-editor/components/path-preview.js +3 -6
  13. package/dist/components/expression-editor/components/path-preview.js.map +1 -1
  14. package/dist/components/expression-editor/components/variable-picker.js +49 -12
  15. package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
  16. package/dist/components/expression-editor/core/editor.js +42 -0
  17. package/dist/components/expression-editor/core/editor.js.map +1 -1
  18. package/dist/components/expression-editor/expression-editor.js +68 -22
  19. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  20. package/dist/components/expression-editor/style2.css +135 -73
  21. package/dist/components/form/form.js.map +1 -1
  22. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +0 -1
  23. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  24. package/dist/components/form/style2.css +8 -3
  25. package/dist/components/form-item/style2.css +14 -0
  26. package/dist/components/index.d.ts +27 -26
  27. package/dist/components/index.js +54 -52
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/kanban/di.js +7 -0
  30. package/dist/components/kanban/di.js.map +1 -0
  31. package/dist/components/kanban/kanban-column.js +67 -0
  32. package/dist/components/kanban/kanban-column.js.map +1 -0
  33. package/dist/components/kanban/kanban.js +89 -0
  34. package/dist/components/kanban/kanban.js.map +1 -0
  35. package/dist/components/kanban/style.d.ts +1 -0
  36. package/dist/components/kanban/style.js +1 -0
  37. package/dist/components/kanban/style2.css +128 -0
  38. package/dist/components/multi-select/multi-select.js +16 -12
  39. package/dist/components/multi-select/multi-select.js.map +1 -1
  40. package/dist/components/multi-select/style2.css +7 -0
  41. package/dist/components/multi-tree-select/multi-tree-select.js +82 -56
  42. package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
  43. package/dist/components/multi-tree-select/style2.css +7 -0
  44. package/dist/components/rich-text-editor/image-node.js +154 -0
  45. package/dist/components/rich-text-editor/image-node.js.map +1 -0
  46. package/dist/components/rich-text-editor/rich-text-editor.js +80 -22
  47. package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
  48. package/dist/components/rich-text-editor/style2.css +6 -0
  49. package/dist/components/rich-text-editor/toolbar.js +22 -2
  50. package/dist/components/rich-text-editor/toolbar.js.map +1 -1
  51. package/dist/components/select/select.js +7 -3
  52. package/dist/components/select/select.js.map +1 -1
  53. package/dist/components/select/style2.css +7 -0
  54. package/dist/components/select/use-options.js +14 -3
  55. package/dist/components/select/use-options.js.map +1 -1
  56. package/dist/components/tree-select/style2.css +7 -0
  57. package/dist/components/tree-select/tree-select.js +49 -17
  58. package/dist/components/tree-select/tree-select.js.map +1 -1
  59. package/dist/index.d.ts +48 -46
  60. package/dist/index.js +47 -46
  61. package/dist/types/dropdown.d.ts +10 -0
  62. package/dist/types/index.d.ts +3 -2
  63. package/dist/types/kanban.d.ts +69 -0
  64. package/dist/types/multi-tree-select.d.ts +7 -1
  65. package/dist/types/rich-text-editor.d.ts +13 -2
  66. package/dist/types/tree-select.d.ts +7 -1
  67. package/package.json +1 -1
  68. package/src/components/batch-edit/__test__/batch-edit.test.ts +112 -1
  69. package/src/components/batch-edit/batch-edit-form.vue +21 -2
  70. package/src/components/batch-edit/use-handlers.ts +6 -5
  71. package/src/components/batch-edit/use-shortcut-key.ts +5 -2
  72. package/src/components/card/style.scss +0 -2
  73. package/src/components/condition-editor/__test__/condition-editor.test.ts +61 -0
  74. package/src/components/condition-editor/condition-editor.vue +6 -1
  75. package/src/components/dropdown/dropdown.vue +9 -4
  76. package/src/components/expression-editor/__test__/editor.test.ts +66 -0
  77. package/src/components/expression-editor/__test__/expression-editor.test.ts +124 -0
  78. package/src/components/expression-editor/components/path-preview.vue +1 -2
  79. package/src/components/expression-editor/components/variable-picker.vue +54 -13
  80. package/src/components/expression-editor/core/editor.ts +61 -0
  81. package/src/components/expression-editor/expression-editor.vue +58 -20
  82. package/src/components/expression-editor/style.scss +160 -76
  83. package/src/components/form/form.vue +0 -1
  84. package/src/components/form/style.scss +11 -4
  85. package/src/components/form-item/style.scss +16 -1
  86. package/src/components/index.ts +2 -0
  87. package/src/components/kanban/__test__/kanban.test.ts +212 -0
  88. package/src/components/kanban/di.ts +19 -0
  89. package/src/components/kanban/index.ts +1 -0
  90. package/src/components/kanban/kanban-column.vue +77 -0
  91. package/src/components/kanban/kanban.vue +81 -0
  92. package/src/components/kanban/style.scss +124 -0
  93. package/src/components/kanban/style.ts +1 -0
  94. package/src/components/multi-select/multi-select.vue +11 -2
  95. package/src/components/multi-select/style.scss +6 -0
  96. package/src/components/multi-tree-select/multi-tree-select.vue +58 -9
  97. package/src/components/multi-tree-select/style.scss +6 -0
  98. package/src/components/rich-text-editor/image-node.ts +193 -0
  99. package/src/components/rich-text-editor/rich-text-editor.vue +131 -29
  100. package/src/components/rich-text-editor/style.scss +7 -0
  101. package/src/components/rich-text-editor/toolbar.vue +21 -1
  102. package/src/components/select/__test__/select.test.ts +78 -0
  103. package/src/components/select/select.vue +13 -3
  104. package/src/components/select/style.scss +6 -0
  105. package/src/components/select/use-options.ts +25 -4
  106. package/src/components/tree-select/__test__/tree-select.test.ts +116 -0
  107. package/src/components/tree-select/style.scss +6 -0
  108. package/src/components/tree-select/tree-select.vue +63 -13
  109. package/src/types/dropdown.ts +10 -0
  110. package/src/types/index.ts +1 -0
  111. package/src/types/kanban.ts +63 -0
  112. package/src/types/multi-tree-select.ts +12 -1
  113. package/src/types/rich-text-editor.ts +14 -1
  114. package/src/types/tree-select.ts +12 -1
@@ -1 +1 @@
1
- {"version":3,"file":"variable-picker.js","names":[],"sources":["../../../../src/components/expression-editor/components/variable-picker.vue"],"sourcesContent":["<template>\n <u-tip\n hide-arrow\n alignment=\"start\"\n direction=\"bottom\"\n trigger=\"click\"\n :trigger-dom=\"triggerDom\"\n :visible=\"visible\"\n @update:visible=\"handleVisibleChange($event)\"\n >\n <template #content>\n <div :class=\"cls.e('picker')\" @mousedown.prevent>\n <!-- 面包屑:仅逐级模式且非根级显示 -->\n <div v-if=\"!isFlat && navPath.length > 0\" :class=\"cls.e('breadcrumbs')\">\n <span :class=\"cls.e('breadcrumb-item')\">\n <span :class=\"cls.e('breadcrumb-link')\" @click=\"navigateTo(0)\">全部变量</span>\n <u-icon :class=\"cls.e('breadcrumb-separator')\">\n <ArrowRight />\n </u-icon>\n </span>\n <span\n v-for=\"(crumb, i) in navPath\"\n :key=\"`${i}-${crumb.value}`\"\n :class=\"cls.e('breadcrumb-item')\"\n >\n <span\n :class=\"[\n cls.e('breadcrumb-link'),\n { [cls.em('breadcrumb-link', 'active')]: i === navPath.length - 1 }\n ]\"\n @click=\"navigateTo(i + 1)\"\n >{{ crumb.label }}</span\n >\n <u-icon v-if=\"i < navPath.length - 1\" :class=\"cls.e('breadcrumb-separator')\">\n <ArrowRight />\n </u-icon>\n </span>\n </div>\n\n <div :class=\"cls.e('picker-body')\">\n <div :class=\"cls.e('panel')\" v-if=\"currentList.length > 0\">\n <u-scroll tag=\"ul\" :class=\"cls.e('panel-list')\" :content-class=\"cls.e('panel-content')\">\n <li\n v-for=\"(item, idx) in currentList\"\n :key=\"`${idx}-${item.value}`\"\n :data-idx=\"idx\"\n :class=\"getItemCls(item, idx)\"\n @click=\"handleItemClick(item, idx)\"\n @mouseenter=\"activeIndex = idx\"\n >\n <span :class=\"cls.e('item-label')\">{{ item.label }}</span>\n <u-icon v-if=\"!isFlat && hasChildren(item)\" :class=\"cls.e('item-expand')\">\n <ArrowRight />\n </u-icon>\n </li>\n </u-scroll>\n </div>\n <div v-else :class=\"cls.e('empty')\">\n <UEmpty description=\"暂无可用变量\" />\n </div>\n\n <PathPreview v-if=\"isFlat && activePath.length > 0\" :path=\"activePath\" />\n </div>\n </div>\n </template>\n </u-tip>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ArrowRight } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, nextTick, ref, watch } from 'vue'\n\nimport type { VariableItem } from '../../../types'\nimport { UEmpty } from '../../empty'\nimport { UIcon } from '../../icon'\nimport { UScroll } from '../../scroll'\nimport { UTip } from '../../tip'\nimport PathPreview from './path-preview.vue'\n\ndefineOptions({ name: 'UVariablePicker' })\n\ninterface FlatItem extends VariableItem {\n /** 仅扁平模式使用:从根到当前节点的路径 */\n __path: VariableItem[]\n}\n\nconst props = defineProps<{\n visible: boolean\n triggerDom?: HTMLElement\n /** 顶层变量树 */\n variables?: VariableItem[]\n /** 来自编辑器 mention 状态的过滤字符串。空 = 逐级模式;非空 = 扁平模式。 */\n filter: string\n /** 是否允许选中分支节点 */\n selectableLevels: 'leaf' | 'any'\n}>()\n\nconst emit = defineEmits<{\n (e: 'select', item: VariableItem): void\n (e: 'update:visible', visible: boolean): void\n /** 用户主动取消(按 Esc)。编辑器据此把 mention 状态置为 dismissed。 */\n (e: 'dismiss'): void\n}>()\n\nconst cls = bem('expression-editor')\n\nconst navPath = ref<VariableItem[]>([])\nconst activeIndex = ref(0)\n\nconst isFlat = computed(() => props.filter !== '')\n\nfunction hasChildren(item: VariableItem): boolean {\n return !!item.children && item.children.length > 0\n}\n\nfunction isLeaf(item: VariableItem): boolean {\n return !hasChildren(item)\n}\n\n/** 当前层级展示的列表(来源依模式而定) */\nconst currentList = computed<VariableItem[]>(() => {\n const variables = props.variables ?? []\n if (isFlat.value) return flatList.value\n let level: VariableItem[] = variables\n for (const node of navPath.value) {\n const found = level.find((it) => it.value === node.value)\n if (!found || !found.children) break\n level = found.children\n }\n return level\n})\n\n/** 扁平模式下:根据 selectableLevels 拍平 + 过滤 */\nconst flatList = computed<FlatItem[]>(() => {\n const variables = props.variables ?? []\n const out: FlatItem[] = []\n const includeBranches = props.selectableLevels === 'any'\n function walk(items: VariableItem[], path: VariableItem[]) {\n for (const it of items) {\n const nextPath = [...path, it]\n if (isLeaf(it)) {\n out.push({ ...it, __path: nextPath })\n } else {\n if (includeBranches) out.push({ ...it, __path: nextPath })\n walk(it.children!, nextPath)\n }\n }\n }\n walk(variables, [])\n const q = props.filter.toLowerCase()\n return out.filter((it) => it.label.toLowerCase().includes(q))\n})\n\n/** 当前 active 项;扁平模式下用于驱动路径预览 */\nconst activePath = computed<VariableItem[]>(() => {\n if (!isFlat.value) return []\n const item = flatList.value[activeIndex.value] as FlatItem | undefined\n return item ? item.__path : []\n})\n\nfunction getItemCls(_item: VariableItem, idx: number) {\n return [cls.e('item'), bem.is('active', idx === activeIndex.value)]\n}\n\nfunction navigateTo(i: number) {\n // 0 = 全部变量;i 之后表示进入到 navPath[i-1]\n navPath.value = navPath.value.slice(0, i)\n activeIndex.value = 0\n}\n\nfunction handleItemClick(item: VariableItem, idx: number) {\n activeIndex.value = idx\n selectActive()\n}\n\nfunction selectActive() {\n const item = currentList.value[activeIndex.value]\n if (!item) return\n\n if (isFlat.value) {\n // 扁平模式始终是「选中并替换」\n emit('select', flattenSourceFor(item))\n return\n }\n\n // 逐级模式\n if (hasChildren(item)) {\n if (props.selectableLevels === 'any') {\n // Enter 在分支项上:选中分支本身\n emit('select', item)\n return\n }\n // 默认 'leaf':进入下一级\n navPath.value = [...navPath.value, item]\n activeIndex.value = 0\n return\n }\n emit('select', item)\n}\n\nfunction flattenSourceFor(it: VariableItem): VariableItem {\n const flat = it as FlatItem\n // 还原为「不带 __path 的纯 VariableItem」,避免泄漏内部字段\n const out: VariableItem = { value: flat.value, label: flat.label }\n if (flat.type) out.type = flat.type\n if (flat.children) out.children = flat.children\n return out\n}\n\nfunction moveActive(delta: -1 | 1) {\n const max = currentList.value.length - 1\n if (max < 0) return\n let next = activeIndex.value + delta\n if (next < 0) next = 0\n if (next > max) next = max\n activeIndex.value = next\n scrollActiveIntoView()\n}\n\nfunction scrollActiveIntoView() {\n void nextTick(() => {\n const list = document.querySelector(`.${cls.e('panel-list')}`)\n if (!list) return\n const item = list.querySelector(`[data-idx=\"${activeIndex.value}\"]`)\n if (item && 'scrollIntoView' in item) {\n ;(item as HTMLElement).scrollIntoView({ block: 'nearest' })\n }\n })\n}\n\nfunction handleVisibleChange(v: boolean) {\n emit('update:visible', v)\n if (!v) reset()\n}\n\nfunction reset() {\n navPath.value = []\n activeIndex.value = 0\n}\n\nwatch(\n () => props.visible,\n (v) => {\n if (!v) reset()\n else activeIndex.value = 0\n }\n)\n\nwatch(\n () => props.filter,\n () => {\n // 过滤词变化:重置 active;逐级 → 扁平时清空 navPath\n activeIndex.value = 0\n if (isFlat.value) navPath.value = []\n }\n)\n\n/**\n * 由父级 forward 进来的键盘事件。\n * 返回 true 表示已处理(外部应 preventDefault)。\n */\nfunction handleKeydown(e: KeyboardEvent): boolean {\n if (!props.visible) return false\n\n switch (e.key) {\n case 'Escape':\n emit('dismiss')\n emit('update:visible', false)\n reset()\n return true\n case 'ArrowDown':\n moveActive(1)\n return true\n case 'ArrowUp':\n moveActive(-1)\n return true\n case 'ArrowLeft':\n // 仅逐级模式:返回上一级;扁平模式下不拦截,让光标正常移动并触发 mention dismiss\n if (!isFlat.value && navPath.value.length > 0) {\n navPath.value = navPath.value.slice(0, -1)\n activeIndex.value = 0\n return true\n }\n return false\n case 'ArrowRight': {\n if (isFlat.value) return false\n const item = currentList.value[activeIndex.value]\n if (item && hasChildren(item)) {\n navPath.value = [...navPath.value, item]\n activeIndex.value = 0\n return true\n }\n return false\n }\n case 'Enter':\n selectActive()\n return true\n default:\n return false\n }\n}\n\ndefineExpose({ handleKeydown })\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"variable-picker.js","names":[],"sources":["../../../../src/components/expression-editor/components/variable-picker.vue"],"sourcesContent":["<template>\n <u-tip\n hide-arrow\n alignment=\"start\"\n direction=\"bottom\"\n trigger=\"click\"\n :trigger-dom=\"triggerDom\"\n :visible=\"visible\"\n @update:visible=\"handleVisibleChange($event)\"\n >\n <template #content>\n <div ref=\"root\" :class=\"cls.e('picker')\" @mousedown.prevent>\n <!-- 面包屑:仅逐级模式且非根级显示 -->\n <div v-if=\"!isFlat && navPath.length > 0\" :class=\"cls.e('breadcrumbs')\">\n <span :class=\"cls.e('breadcrumb-item')\">\n <span :class=\"cls.e('breadcrumb-link')\" @click=\"navigateTo(0)\">全部变量</span>\n <u-icon :class=\"cls.e('breadcrumb-separator')\">\n <ArrowRight />\n </u-icon>\n </span>\n <span\n v-for=\"(crumb, i) in navPath\"\n :key=\"`${i}-${crumb.value}`\"\n :class=\"cls.e('breadcrumb-item')\"\n >\n <span\n :class=\"[\n cls.e('breadcrumb-link'),\n { [cls.em('breadcrumb-link', 'active')]: i === navPath.length - 1 }\n ]\"\n @click=\"navigateTo(i + 1)\"\n >{{ crumb.label }}</span\n >\n <u-icon v-if=\"i < navPath.length - 1\" :class=\"cls.e('breadcrumb-separator')\">\n <ArrowRight />\n </u-icon>\n </span>\n </div>\n\n <div :class=\"cls.e('picker-body')\">\n <div :class=\"cls.e('panel')\" v-if=\"currentList.length > 0\">\n <u-scroll\n ref=\"listScroll\"\n tag=\"ul\"\n :class=\"cls.e('panel-list')\"\n :content-class=\"cls.e('panel-content')\"\n >\n <li\n v-for=\"(item, idx) in currentList\"\n :key=\"`${idx}-${item.value}`\"\n :data-idx=\"idx\"\n :class=\"getItemCls(item, idx)\"\n @click=\"handleItemClick(item, idx)\"\n @mouseenter=\"activeIndex = idx\"\n >\n <span :class=\"cls.e('item-label')\">{{ item.label }}</span>\n <u-icon\n v-if=\"!isFlat && hasChildren(item)\"\n :class=\"cls.e('item-expand')\"\n title=\"展开下一级\"\n @click.stop=\"expandInto(item)\"\n >\n <ArrowRight />\n </u-icon>\n </li>\n </u-scroll>\n </div>\n <div v-else :class=\"cls.e('empty')\">\n <UEmpty description=\"暂无可用变量\" />\n </div>\n\n <PathPreview v-if=\"isFlat && activePath.length > 0\" :path=\"activePath\" />\n </div>\n\n <div v-if=\"currentList.length > 0\" :class=\"cls.e('picker-hint')\">\n <template v-if=\"isFlat\">\n <span><kbd>↑</kbd><kbd>↓</kbd> 选择</span>\n <span><kbd>Enter</kbd> 确认</span>\n <span><kbd>Esc</kbd> 关闭</span>\n </template>\n <template v-else>\n <span><kbd>↑</kbd><kbd>↓</kbd> 选择</span>\n <span><kbd>→</kbd> 展开</span>\n <span><kbd>←</kbd> 返回</span>\n <span><kbd>Enter</kbd> 确认</span>\n <span><kbd>Esc</kbd> 关闭</span>\n </template>\n </div>\n </div>\n </template>\n </u-tip>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ArrowRight } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, nextTick, ref, useTemplateRef, watch } from 'vue'\n\nimport type { VariableItem } from '../../../types'\nimport { UEmpty } from '../../empty'\nimport { UIcon } from '../../icon'\nimport { UScroll } from '../../scroll'\nimport { UTip } from '../../tip'\nimport PathPreview from './path-preview.vue'\n\ndefineOptions({ name: 'UVariablePicker' })\n\ninterface FlatItem extends VariableItem {\n /** 仅扁平模式使用:从根到当前节点的路径 */\n __path: VariableItem[]\n}\n\nconst props = defineProps<{\n visible: boolean\n triggerDom?: HTMLElement\n /** 顶层变量树 */\n variables?: VariableItem[]\n /** 来自编辑器 mention 状态的过滤字符串。空 = 逐级模式;非空 = 扁平模式。 */\n filter: string\n /** 是否允许选中分支节点 */\n selectableLevels: 'leaf' | 'any'\n}>()\n\nconst emit = defineEmits<{\n (e: 'select', item: VariableItem): void\n (e: 'update:visible', visible: boolean): void\n /** 用户主动取消(按 Esc)。编辑器据此把 mention 状态置为 dismissed。 */\n (e: 'dismiss'): void\n}>()\n\nconst cls = bem('expression-editor')\n\nconst navPath = ref<VariableItem[]>([])\nconst activeIndex = ref(0)\nconst root = useTemplateRef('root')\nconst listScroll = useTemplateRef<InstanceType<typeof UScroll>>('listScroll')\n\nconst isFlat = computed(() => props.filter !== '')\n\nfunction hasChildren(item: VariableItem): boolean {\n return !!item.children && item.children.length > 0\n}\n\nfunction isLeaf(item: VariableItem): boolean {\n return !hasChildren(item)\n}\n\n/** 当前层级展示的列表(来源依模式而定) */\nconst currentList = computed<VariableItem[]>(() => {\n const variables = props.variables ?? []\n if (isFlat.value) return flatList.value\n let level: VariableItem[] = variables\n for (const node of navPath.value) {\n const found = level.find((it) => it.value === node.value)\n if (!found || !found.children) break\n level = found.children\n }\n return level\n})\n\n/** 扁平模式下:根据 selectableLevels 拍平 + 过滤 */\nconst flatList = computed<FlatItem[]>(() => {\n const variables = props.variables ?? []\n const out: FlatItem[] = []\n const includeBranches = props.selectableLevels === 'any'\n function walk(items: VariableItem[], path: VariableItem[]) {\n for (const it of items) {\n const nextPath = [...path, it]\n if (isLeaf(it)) {\n out.push({ ...it, __path: nextPath })\n } else {\n if (includeBranches) out.push({ ...it, __path: nextPath })\n walk(it.children!, nextPath)\n }\n }\n }\n walk(variables, [])\n const q = props.filter.toLowerCase()\n return out.filter((it) => it.label.toLowerCase().includes(q))\n})\n\n/** 当前 active 项;扁平模式下用于驱动路径预览 */\nconst activePath = computed<VariableItem[]>(() => {\n if (!isFlat.value) return []\n const item = flatList.value[activeIndex.value] as FlatItem | undefined\n return item ? item.__path : []\n})\n\nfunction getItemCls(_item: VariableItem, idx: number) {\n return [cls.e('item'), bem.is('active', idx === activeIndex.value)]\n}\n\nfunction navigateTo(i: number) {\n // 0 = 全部变量;i 之后表示进入到 navPath[i-1]\n navPath.value = navPath.value.slice(0, i)\n activeIndex.value = 0\n}\n\nfunction handleItemClick(item: VariableItem, idx: number) {\n activeIndex.value = idx\n selectActive()\n}\n\nfunction selectActive() {\n const item = currentList.value[activeIndex.value]\n if (!item) return\n\n if (isFlat.value) {\n // 扁平模式始终是「选中并替换」\n emit('select', flattenSourceFor(item))\n return\n }\n\n // 逐级模式\n if (hasChildren(item)) {\n if (props.selectableLevels === 'any') {\n // Enter 在分支项上:选中分支本身\n emit('select', item)\n return\n }\n // 默认 'leaf':进入下一级\n expandInto(item)\n return\n }\n emit('select', item)\n}\n\n/** 进入 item 的下一级(点击展开图标 / leaf 模式点击分支项共用) */\nfunction expandInto(item: VariableItem) {\n if (!hasChildren(item)) return\n navPath.value = [...navPath.value, item]\n activeIndex.value = 0\n}\n\nfunction flattenSourceFor(it: VariableItem): VariableItem {\n const flat = it as FlatItem\n // 还原为「不带 __path 的纯 VariableItem」,避免泄漏内部字段\n const out: VariableItem = { value: flat.value, label: flat.label }\n if (flat.type) out.type = flat.type\n if (flat.children) out.children = flat.children\n return out\n}\n\nfunction moveActive(delta: -1 | 1) {\n const max = currentList.value.length - 1\n if (max < 0) return\n let next = activeIndex.value + delta\n if (next < 0) next = 0\n if (next > max) next = max\n activeIndex.value = next\n scrollActiveIntoView()\n}\n\nfunction scrollActiveIntoView() {\n void nextTick(() => {\n // 在面板根节点内查找,避免同页多个编辑器时滚错列表\n const item = root.value?.querySelector(`[data-idx=\"${activeIndex.value}\"]`)\n // expose 代理运行时会解包 ref,类型上按 unknown 收窄\n const scroller = listScroll.value?.containerRef as unknown\n if (!(item instanceof HTMLElement) || !(scroller instanceof HTMLElement)) return\n // 只滚面板列表自身:原生 scrollIntoView 会沿所有滚动祖先链滚动,\n // 面板贴近视口边缘时会连带滚动页面,导致面板与触发器错位抖动\n const top =\n item.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop\n const bottom = top + item.offsetHeight\n if (top < scroller.scrollTop) {\n scroller.scrollTop = top\n } else if (bottom > scroller.scrollTop + scroller.clientHeight) {\n scroller.scrollTop = bottom - scroller.clientHeight\n }\n })\n}\n\nfunction handleVisibleChange(v: boolean) {\n emit('update:visible', v)\n if (!v) reset()\n}\n\nfunction reset() {\n navPath.value = []\n activeIndex.value = 0\n}\n\nwatch(\n () => props.visible,\n (v) => {\n if (!v) reset()\n else activeIndex.value = 0\n }\n)\n\nwatch(\n () => props.filter,\n () => {\n // 过滤词变化:重置 active;逐级 → 扁平时清空 navPath\n activeIndex.value = 0\n if (isFlat.value) navPath.value = []\n }\n)\n\n/**\n * 由父级 forward 进来的键盘事件。\n * 返回 true 表示已处理(外部应 preventDefault)。\n */\nfunction handleKeydown(e: KeyboardEvent): boolean {\n if (!props.visible) return false\n\n switch (e.key) {\n case 'Escape':\n emit('dismiss')\n emit('update:visible', false)\n reset()\n return true\n case 'ArrowDown':\n moveActive(1)\n return true\n case 'ArrowUp':\n moveActive(-1)\n return true\n case 'ArrowLeft':\n // 仅逐级模式:返回上一级;扁平模式下不拦截,让光标正常移动并触发 mention dismiss\n if (!isFlat.value && navPath.value.length > 0) {\n navPath.value = navPath.value.slice(0, -1)\n activeIndex.value = 0\n return true\n }\n return false\n case 'ArrowRight': {\n if (isFlat.value) return false\n const item = currentList.value[activeIndex.value]\n if (item && hasChildren(item)) {\n expandInto(item)\n return true\n }\n return false\n }\n case 'Enter':\n selectActive()\n return true\n default:\n return false\n }\n}\n\ndefineExpose({ handleKeydown })\n</script>\n"],"mappings":"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"}
@@ -6,10 +6,23 @@ const TEXT_SEG_VALUE = "text";
6
6
  function createEditor(opts) {
7
7
  let doc = normalize(opts.initialDoc);
8
8
  let composing = false;
9
+ /** null 表示还没算过,首次计算后必定回调一次 */
10
+ let visualEmpty = null;
9
11
  function emitChange(next) {
10
12
  doc = next;
11
13
  opts.onChange?.(doc);
12
14
  }
15
+ /** 框内是否只剩空 text span:任何文本(含 IME 合成中尚未进模型的)或 chip 都算非空 */
16
+ function isVisuallyEmpty() {
17
+ for (const child of Array.from(opts.container.childNodes)) if (isChipElement(child) || (child.textContent ?? "").length > 0) return false;
18
+ return true;
19
+ }
20
+ function syncVisualEmpty() {
21
+ const empty = isVisuallyEmpty();
22
+ if (empty === visualEmpty) return;
23
+ visualEmpty = empty;
24
+ opts.onVisualEmptyChange?.(empty);
25
+ }
13
26
  function makeTextSpan(value) {
14
27
  const span = document.createElement("span");
15
28
  span.setAttribute(TEXT_SEG_ATTR, TEXT_SEG_VALUE);
@@ -81,6 +94,7 @@ function createEditor(opts) {
81
94
  });
82
95
  for (const seg of expanded) if (seg.kind === "text") container.appendChild(makeTextSpan(seg.value));
83
96
  else container.appendChild(makeChipSpan(seg));
97
+ syncVisualEmpty();
84
98
  if (typeof targetCaret === "number") setCaret(targetCaret);
85
99
  }
86
100
  /** 从 DOM 反推模型:宽容裸 text node、空 text span。 */
@@ -230,6 +244,25 @@ function createEditor(opts) {
230
244
  }
231
245
  return null;
232
246
  }
247
+ function getRectAtOffset(offset) {
248
+ let pos = 0;
249
+ for (const child of Array.from(opts.container.childNodes)) {
250
+ const len = childSerialLength(child);
251
+ if (offset >= pos && offset < pos + len) {
252
+ if (isChipElement(child)) return child.getBoundingClientRect();
253
+ const textNode = firstTextDescendant(child);
254
+ if (!textNode) return child.getBoundingClientRect();
255
+ const local = Math.min(offset - pos, textNode.data.length);
256
+ const range = document.createRange();
257
+ range.setStart(textNode, local);
258
+ range.setEnd(textNode, Math.min(local + 1, textNode.data.length));
259
+ const rects = range.getClientRects();
260
+ return rects.length > 0 ? rects[0] : child.getBoundingClientRect();
261
+ }
262
+ pos += len;
263
+ }
264
+ return null;
265
+ }
233
266
  /** input 后:从 DOM 反推 doc,并触发 onChange(如有变化)。 */
234
267
  function syncFromDOM() {
235
268
  if (composing) return;
@@ -250,13 +283,18 @@ function createEditor(opts) {
250
283
  return true;
251
284
  }
252
285
  function onInput() {
286
+ syncVisualEmpty();
253
287
  syncFromDOM();
254
288
  }
255
289
  function onCompositionStart() {
256
290
  composing = true;
257
291
  }
292
+ function onCompositionUpdate() {
293
+ syncVisualEmpty();
294
+ }
258
295
  function onCompositionEnd() {
259
296
  composing = false;
297
+ syncVisualEmpty();
260
298
  syncFromDOM();
261
299
  }
262
300
  function onPaste(e) {
@@ -292,6 +330,7 @@ function createEditor(opts) {
292
330
  range.collapse(true);
293
331
  sel.removeAllRanges();
294
332
  sel.addRange(range);
333
+ syncVisualEmpty();
295
334
  syncFromDOM();
296
335
  }
297
336
  function replaceRangeWithVar(start, end, variable) {
@@ -369,6 +408,7 @@ function createEditor(opts) {
369
408
  const container = opts.container;
370
409
  container.addEventListener("input", onInput);
371
410
  container.addEventListener("compositionstart", onCompositionStart);
411
+ container.addEventListener("compositionupdate", onCompositionUpdate);
372
412
  container.addEventListener("compositionend", onCompositionEnd);
373
413
  container.addEventListener("paste", onPaste);
374
414
  document.addEventListener("selectionchange", onSelectionChange);
@@ -388,12 +428,14 @@ function createEditor(opts) {
388
428
  },
389
429
  getCaretOffset,
390
430
  setCaretOffset: setCaret,
431
+ getRectAtOffset,
391
432
  replaceRangeWithVar,
392
433
  replaceVarAt,
393
434
  rerender: () => render(),
394
435
  dispose: () => {
395
436
  container.removeEventListener("input", onInput);
396
437
  container.removeEventListener("compositionstart", onCompositionStart);
438
+ container.removeEventListener("compositionupdate", onCompositionUpdate);
397
439
  container.removeEventListener("compositionend", onCompositionEnd);
398
440
  container.removeEventListener("paste", onPaste);
399
441
  document.removeEventListener("selectionchange", onSelectionChange);
@@ -1 +1 @@
1
- {"version":3,"file":"editor.js","names":[],"sources":["../../../../src/components/expression-editor/core/editor.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\n\nimport type { VariableItem } from '../../../types'\nimport { createChip, isChipElement, readChipType, readChipValue } from './chip'\nimport { normalize, parse, serialize, type Doc, type Segment } from './model'\n\nconst TEXT_SEG_ATTR = 'data-seg'\nconst TEXT_SEG_VALUE = 'text'\n\nexport interface EditorVariableInfo {\n value: string\n label: string\n type?: string\n}\n\nexport interface EditorOptions {\n container: HTMLElement\n cls: BEM<'expression-editor'>\n initialDoc: Doc\n /**\n * 在 DOM → 模型反推、字符串 → 模型反序列化时为 chip 补齐 label / type。\n * 用 getter 形式以便外层 `variables` props 变化时拿到最新映射。\n */\n getVariableMap?: () => Map<string, VariableItem>\n /** 模型每次更新(包括用户键入文本)后回调 */\n onChange?: (doc: Doc) => void\n /** 选中区域 / 光标偏移变化时回调,offset 为序列化字符串中的位置;无选中或不在编辑器内时为 null */\n onSelectionChange?: (info: { offset: number | null }) => void\n /** 用户点击某个 chip 的主体(重选) */\n onChipReselect?: (info: { chipEl: HTMLElement; segIndex: number; chipOffset: number }) => void\n}\n\nexport interface EditorAPI {\n /** 当前模型 */\n getDoc(): Doc\n /** 完全替换模型(会重渲染 DOM) */\n setDoc(doc: Doc): void\n /** 序列化为字符串(modelValue 形态) */\n getValue(): string\n /** 反序列化字符串为模型并替换(可选传入 variableMap 以补齐 label/type) */\n setValue(value: string): void\n /** 当前光标偏移(无 selection 返回 null) */\n getCaretOffset(): number | null\n /** 把光标设置到指定偏移(不会修改模型) */\n setCaretOffset(offset: number): void\n /**\n * 用一个 var chip 替换 [start, end) 区间。\n * 调用方通常在 mention 选中时用:start = `@` 起点、end = 当前光标。\n */\n replaceRangeWithVar(start: number, end: number, variable: EditorVariableInfo): void\n /** 替换指定段下标对应的 var chip 为新变量(必须是 var 段)。 */\n replaceVarAt(segIndex: number, variable: EditorVariableInfo): void\n /** 卸载所有事件监听(不会销毁 container DOM) */\n dispose(): void\n /** 强制根据当前模型重渲染 DOM(光标位置尽量保留) */\n rerender(): void\n /** 容器引用(只读) */\n readonly container: HTMLElement\n}\n\nexport function createEditor(opts: EditorOptions): EditorAPI {\n let doc: Doc = normalize(opts.initialDoc)\n let composing = false\n\n function emitChange(next: Doc) {\n doc = next\n opts.onChange?.(doc)\n }\n\n function makeTextSpan(value: string): HTMLSpanElement {\n const span = document.createElement('span')\n span.setAttribute(TEXT_SEG_ATTR, TEXT_SEG_VALUE)\n span.textContent = value\n return span\n }\n\n function makeChipSpan(seg: Extract<Segment, { kind: 'var' }>): HTMLElement {\n return createChip(seg, {\n cls: opts.cls,\n onReselect: (chipEl) => {\n const { segIndex, chipOffset } = locateChip(chipEl)\n if (segIndex < 0) return\n opts.onChipReselect?.({ chipEl, segIndex, chipOffset })\n }\n })\n }\n\n function locateChip(chipEl: HTMLElement): { segIndex: number; chipOffset: number } {\n let chipOffset = 0\n let segIdx = -1\n let i = 0\n for (const child of Array.from(opts.container.childNodes)) {\n if (child === chipEl) {\n segIdx = i\n return { segIndex: segIdx, chipOffset }\n }\n chipOffset += childSerialLength(child)\n i++\n }\n return { segIndex: -1, chipOffset: -1 }\n }\n\n function childSerialLength(node: Node): number {\n if (isChipElement(node)) return readChipValue(node).length + 2\n return (node.textContent ?? '').length\n }\n\n /** 渲染 doc 到 container:每个 var 两侧都补一个 text span(即便空),保证光标可停靠。 */\n function render(targetCaret?: number) {\n const container = opts.container\n container.innerHTML = ''\n\n // 在边界与每两个 var 之间确保有 text span 占位\n const expanded: Segment[] = []\n let lastWasVar = false\n let first = true\n for (const seg of doc) {\n if (first) {\n if (seg.kind === 'var') expanded.push({ kind: 'text', value: '' })\n first = false\n } else if (lastWasVar && seg.kind === 'var') {\n expanded.push({ kind: 'text', value: '' })\n }\n expanded.push(seg)\n lastWasVar = seg.kind === 'var'\n }\n if (lastWasVar || expanded.length === 0) expanded.push({ kind: 'text', value: '' })\n\n for (const seg of expanded) {\n if (seg.kind === 'text') container.appendChild(makeTextSpan(seg.value))\n else container.appendChild(makeChipSpan(seg))\n }\n\n if (typeof targetCaret === 'number') setCaret(targetCaret)\n }\n\n /** 从 DOM 反推模型:宽容裸 text node、空 text span。 */\n function readDocFromDOM(): Doc {\n const segs: Segment[] = []\n for (const child of Array.from(opts.container.childNodes)) {\n if (isChipElement(child)) {\n const value = readChipValue(child)\n const domType = readChipType(child)\n const fallbackLabel =\n child.querySelector(`.${opts.cls.e('chip-label')}`)?.textContent ?? value\n // 优先从 variableMap 取最新 label / type,保持与外部数据源一致\n const fromMap = opts.getVariableMap?.().get(value)\n const finalType = fromMap?.type ?? domType\n const seg: Segment = finalType\n ? { kind: 'var', value, label: fromMap?.label ?? fallbackLabel, type: finalType }\n : { kind: 'var', value, label: fromMap?.label ?? fallbackLabel }\n segs.push(seg)\n } else {\n segs.push({ kind: 'text', value: child.textContent ?? '' })\n }\n }\n return normalize(segs)\n }\n\n /**\n * 计算光标在序列化字符串中的全局偏移。\n * 规则:\n * - 在 text span / 裸 text node 中:父级 / 节点之前的 segments 长度累加 + selection 内偏移\n * - 在 chip 边界:根据 selection 在 chip 节点之前 / 之后判断\n */\n function getCaretOffset(): number | null {\n const sel = document.getSelection()\n if (!sel || sel.rangeCount === 0) return null\n const range = sel.getRangeAt(0)\n if (!opts.container.contains(range.startContainer)) return null\n\n let offset = 0\n let found = false\n for (const child of Array.from(opts.container.childNodes)) {\n if (found) break\n if (isChipElement(child)) {\n if (range.startContainer === child) {\n // selection.startOffset 表示子节点索引:0 表示 chip 起始,>=1 表示 chip 之后\n if (range.startOffset === 0) return offset\n return offset + childSerialLength(child)\n }\n if (range.startContainer === opts.container && getChildIndex(child) === range.startOffset) {\n // 容器层 selection:偏移即子节点索引\n return offset\n }\n offset += childSerialLength(child)\n } else {\n // text span 或裸 text 节点\n if (\n range.startContainer === child ||\n (child.contains(range.startContainer) && child !== opts.container)\n ) {\n // 计算 selection 在该 child textContent 中的偏移\n const localOffset = computeTextOffsetWithin(\n child,\n range.startContainer,\n range.startOffset\n )\n return offset + localOffset\n }\n if (range.startContainer === opts.container && getChildIndex(child) === range.startOffset) {\n return offset\n }\n offset += childSerialLength(child)\n }\n }\n if (\n range.startContainer === opts.container &&\n range.startOffset === opts.container.childNodes.length\n ) {\n return offset\n }\n return null\n }\n\n function getChildIndex(node: Node): number {\n let i = 0\n let cur: Node | null = node.previousSibling\n while (cur) {\n i++\n cur = cur.previousSibling\n }\n return i\n }\n\n function computeTextOffsetWithin(root: Node, target: Node, targetOffset: number): number {\n if (root === target) return targetOffset\n let total = 0\n function walk(n: Node): boolean {\n if (n === target) {\n total += targetOffset\n return true\n }\n if (n.nodeType === 3) {\n total += (n as Text).data.length\n return false\n }\n for (const c of Array.from(n.childNodes)) {\n if (walk(c)) return true\n }\n return false\n }\n walk(root)\n return total\n }\n\n /** 把光标放到 offset 处,offset 大于文档长度时落到末尾。 */\n function setCaret(offset: number): void {\n const sel = document.getSelection()\n if (!sel) return\n\n let remain = offset\n const children = Array.from(opts.container.childNodes)\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const len = childSerialLength(child)\n if (isChipElement(child)) {\n if (remain <= 0) {\n // 光标定到 chip 之前 → 落到容器层 child 索引\n placeAt(opts.container, i)\n return\n }\n if (remain < len) {\n // 落在 chip 内部 → 视作 chip 之后\n placeAt(opts.container, i + 1)\n return\n }\n remain -= len\n } else {\n if (remain <= len) {\n // 落在该 text span 内\n placeWithinText(child, remain)\n return\n }\n remain -= len\n }\n }\n // 落到末尾\n if (children.length === 0) {\n placeAt(opts.container, 0)\n return\n }\n const last = children[children.length - 1]!\n if (isChipElement(last)) placeAt(opts.container, children.length)\n else placeWithinText(last, (last.textContent ?? '').length)\n }\n\n function placeAt(parent: Node, index: number) {\n const sel = document.getSelection()\n if (!sel) return\n const range = document.createRange()\n range.setStart(parent, index)\n range.collapse(true)\n sel.removeAllRanges()\n sel.addRange(range)\n }\n\n function placeWithinText(span: Node, offset: number): void {\n const sel = document.getSelection()\n if (!sel) return\n const target = firstTextDescendant(span)\n const range = document.createRange()\n if (target) {\n const max = target.data.length\n range.setStart(target, Math.min(offset, max))\n } else {\n // 空 span:让光标落到 span 内(child index 0)\n range.setStart(span, 0)\n }\n range.collapse(true)\n sel.removeAllRanges()\n sel.addRange(range)\n }\n\n function firstTextDescendant(root: Node): Text | null {\n if (root.nodeType === 3) return root as Text\n for (const c of Array.from(root.childNodes)) {\n const t = firstTextDescendant(c)\n if (t) return t\n }\n return null\n }\n\n /** input 后:从 DOM 反推 doc,并触发 onChange(如有变化)。 */\n function syncFromDOM(): void {\n if (composing) return\n const next = readDocFromDOM()\n if (!docEqual(next, doc)) emitChange(next)\n }\n\n function docEqual(a: Doc, b: Doc): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n const x = a[i]!\n const y = b[i]!\n if (x.kind !== y.kind) return false\n if (x.kind === 'text' && y.kind === 'text' && x.value !== y.value) return false\n if (x.kind === 'var' && y.kind === 'var') {\n if (x.value !== y.value || x.label !== y.label || x.type !== y.type) return false\n }\n }\n return true\n }\n\n function onInput() {\n syncFromDOM()\n }\n function onCompositionStart() {\n composing = true\n }\n function onCompositionEnd() {\n composing = false\n syncFromDOM()\n }\n function onPaste(e: ClipboardEvent) {\n e.preventDefault()\n const text = e.clipboardData?.getData('text/plain') ?? ''\n if (!text) return\n insertPlainTextAtCaret(text)\n }\n function onSelectionChange() {\n if (composing) return\n if (!opts.onSelectionChange) return\n const sel = document.getSelection()\n if (!sel || sel.rangeCount === 0) {\n opts.onSelectionChange({ offset: null })\n return\n }\n const range = sel.getRangeAt(0)\n if (!opts.container.contains(range.startContainer)) {\n opts.onSelectionChange({ offset: null })\n return\n }\n opts.onSelectionChange({ offset: getCaretOffset() })\n }\n\n function insertPlainTextAtCaret(text: string) {\n const sel = document.getSelection()\n if (!sel || sel.rangeCount === 0) return\n const range = sel.getRangeAt(0)\n if (!opts.container.contains(range.startContainer)) return\n range.deleteContents()\n const node = document.createTextNode(text)\n range.insertNode(node)\n range.setStartAfter(node)\n range.collapse(true)\n sel.removeAllRanges()\n sel.addRange(range)\n syncFromDOM()\n }\n\n // 命令:替换区间为 var chip\n function replaceRangeWithVar(start: number, end: number, variable: EditorVariableInfo) {\n const a = Math.min(start, end)\n const b = Math.max(start, end)\n const left = sliceDoc(doc, 0, a)\n const right = sliceDoc(doc, b, totalLength(doc))\n const inserted: Segment = variable.type\n ? { kind: 'var', value: variable.value, label: variable.label, type: variable.type }\n : { kind: 'var', value: variable.value, label: variable.label }\n const next = normalize([...left, inserted, ...right])\n emitChange(next)\n render(a + variable.value.length + 2) // 光标定到 chip 之后\n }\n\n function replaceVarAt(segIndex: number, variable: EditorVariableInfo) {\n const target = doc[segIndex]\n if (!target || target.kind !== 'var') return\n const next = doc.slice()\n const replaced: Segment = variable.type\n ? { kind: 'var', value: variable.value, label: variable.label, type: variable.type }\n : { kind: 'var', value: variable.value, label: variable.label }\n next[segIndex] = replaced\n const caretOffset = sliceLength(doc.slice(0, segIndex)) + variable.value.length + 2\n emitChange(normalize(next))\n render(caretOffset)\n }\n\n // 工具:基于序列化偏移的切片(不会切开 var)\n function sliceDoc(d: Doc, start: number, end: number): Doc {\n const out: Segment[] = []\n let pos = 0\n for (const seg of d) {\n const len = seg.kind === 'text' ? seg.value.length : seg.value.length + 2\n const segStart = pos\n const segEnd = pos + len\n if (segEnd <= start || segStart >= end) {\n pos += len\n continue\n }\n if (seg.kind === 'text') {\n const a = Math.max(0, start - segStart)\n const b = Math.min(len, end - segStart)\n const slice = seg.value.slice(a, b)\n if (slice) out.push({ kind: 'text', value: slice })\n } else {\n // var 是原子:只有当 [start, end) 完整覆盖该 var 时保留;否则抛弃\n if (start <= segStart && end >= segEnd) out.push(seg)\n }\n pos += len\n }\n return out\n }\n\n function sliceLength(d: Doc): number {\n let n = 0\n for (const s of d) n += s.kind === 'text' ? s.value.length : s.value.length + 2\n return n\n }\n\n function totalLength(d: Doc): number {\n return sliceLength(d)\n }\n\n // 绑定监听\n const container = opts.container\n container.addEventListener('input', onInput)\n container.addEventListener('compositionstart', onCompositionStart)\n container.addEventListener('compositionend', onCompositionEnd)\n container.addEventListener('paste', onPaste)\n document.addEventListener('selectionchange', onSelectionChange)\n\n // 初次渲染\n render()\n\n return {\n getDoc: () => doc,\n setDoc: (next) => {\n doc = normalize(next)\n render()\n opts.onChange?.(doc)\n },\n getValue: () => serialize(doc),\n setValue: (value) => {\n doc = parse(value, opts.getVariableMap?.())\n render()\n opts.onChange?.(doc)\n },\n getCaretOffset,\n setCaretOffset: setCaret,\n replaceRangeWithVar,\n replaceVarAt,\n rerender: () => render(),\n dispose: () => {\n container.removeEventListener('input', onInput)\n container.removeEventListener('compositionstart', onCompositionStart)\n container.removeEventListener('compositionend', onCompositionEnd)\n container.removeEventListener('paste', onPaste)\n document.removeEventListener('selectionchange', onSelectionChange)\n },\n container\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"editor.js","names":[],"sources":["../../../../src/components/expression-editor/core/editor.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\n\nimport type { VariableItem } from '../../../types'\nimport { createChip, isChipElement, readChipType, readChipValue } from './chip'\nimport { normalize, parse, serialize, type Doc, type Segment } from './model'\n\nconst TEXT_SEG_ATTR = 'data-seg'\nconst TEXT_SEG_VALUE = 'text'\n\nexport interface EditorVariableInfo {\n value: string\n label: string\n type?: string\n}\n\nexport interface EditorOptions {\n container: HTMLElement\n cls: BEM<'expression-editor'>\n initialDoc: Doc\n /**\n * 在 DOM → 模型反推、字符串 → 模型反序列化时为 chip 补齐 label / type。\n * 用 getter 形式以便外层 `variables` props 变化时拿到最新映射。\n */\n getVariableMap?: () => Map<string, VariableItem>\n /** 模型每次更新(包括用户键入文本)后回调 */\n onChange?: (doc: Doc) => void\n /**\n * 框内「视觉空态」变化回调:只描述眼下框里有没有可见内容,与模型是否已同步无关。\n * IME 合成期间模型刻意不同步,占位文字 / 提示行这类纯视觉层须跟这个信号走。\n */\n onVisualEmptyChange?: (empty: boolean) => void\n /** 选中区域 / 光标偏移变化时回调,offset 为序列化字符串中的位置;无选中或不在编辑器内时为 null */\n onSelectionChange?: (info: { offset: number | null }) => void\n /** 用户点击某个 chip 的主体(重选) */\n onChipReselect?: (info: { chipEl: HTMLElement; segIndex: number; chipOffset: number }) => void\n}\n\nexport interface EditorAPI {\n /** 当前模型 */\n getDoc(): Doc\n /** 完全替换模型(会重渲染 DOM) */\n setDoc(doc: Doc): void\n /** 序列化为字符串(modelValue 形态) */\n getValue(): string\n /** 反序列化字符串为模型并替换(可选传入 variableMap 以补齐 label/type) */\n setValue(value: string): void\n /** 当前光标偏移(无 selection 返回 null) */\n getCaretOffset(): number | null\n /** 把光标设置到指定偏移(不会修改模型) */\n setCaretOffset(offset: number): void\n /**\n * 取序列化偏移处字符的视口矩形(mention 锚点定位用)。\n * 偏移落在 text 段上返回该字符矩形;落在 chip 上返回 chip 元素矩形;越界返回 null。\n */\n getRectAtOffset(offset: number): DOMRect | null\n /**\n * 用一个 var chip 替换 [start, end) 区间。\n * 调用方通常在 mention 选中时用:start = `@` 起点、end = 当前光标。\n */\n replaceRangeWithVar(start: number, end: number, variable: EditorVariableInfo): void\n /** 替换指定段下标对应的 var chip 为新变量(必须是 var 段)。 */\n replaceVarAt(segIndex: number, variable: EditorVariableInfo): void\n /** 卸载所有事件监听(不会销毁 container DOM) */\n dispose(): void\n /** 强制根据当前模型重渲染 DOM(光标位置尽量保留) */\n rerender(): void\n /** 容器引用(只读) */\n readonly container: HTMLElement\n}\n\nexport function createEditor(opts: EditorOptions): EditorAPI {\n let doc: Doc = normalize(opts.initialDoc)\n let composing = false\n /** null 表示还没算过,首次计算后必定回调一次 */\n let visualEmpty: boolean | null = null\n\n function emitChange(next: Doc) {\n doc = next\n opts.onChange?.(doc)\n }\n\n /** 框内是否只剩空 text span:任何文本(含 IME 合成中尚未进模型的)或 chip 都算非空 */\n function isVisuallyEmpty(): boolean {\n for (const child of Array.from(opts.container.childNodes)) {\n if (isChipElement(child) || (child.textContent ?? '').length > 0) return false\n }\n return true\n }\n\n function syncVisualEmpty(): void {\n const empty = isVisuallyEmpty()\n if (empty === visualEmpty) return\n visualEmpty = empty\n opts.onVisualEmptyChange?.(empty)\n }\n\n function makeTextSpan(value: string): HTMLSpanElement {\n const span = document.createElement('span')\n span.setAttribute(TEXT_SEG_ATTR, TEXT_SEG_VALUE)\n span.textContent = value\n return span\n }\n\n function makeChipSpan(seg: Extract<Segment, { kind: 'var' }>): HTMLElement {\n return createChip(seg, {\n cls: opts.cls,\n onReselect: (chipEl) => {\n const { segIndex, chipOffset } = locateChip(chipEl)\n if (segIndex < 0) return\n opts.onChipReselect?.({ chipEl, segIndex, chipOffset })\n }\n })\n }\n\n function locateChip(chipEl: HTMLElement): { segIndex: number; chipOffset: number } {\n let chipOffset = 0\n let segIdx = -1\n let i = 0\n for (const child of Array.from(opts.container.childNodes)) {\n if (child === chipEl) {\n segIdx = i\n return { segIndex: segIdx, chipOffset }\n }\n chipOffset += childSerialLength(child)\n i++\n }\n return { segIndex: -1, chipOffset: -1 }\n }\n\n function childSerialLength(node: Node): number {\n if (isChipElement(node)) return readChipValue(node).length + 2\n return (node.textContent ?? '').length\n }\n\n /** 渲染 doc 到 container:每个 var 两侧都补一个 text span(即便空),保证光标可停靠。 */\n function render(targetCaret?: number) {\n const container = opts.container\n container.innerHTML = ''\n\n // 在边界与每两个 var 之间确保有 text span 占位\n const expanded: Segment[] = []\n let lastWasVar = false\n let first = true\n for (const seg of doc) {\n if (first) {\n if (seg.kind === 'var') expanded.push({ kind: 'text', value: '' })\n first = false\n } else if (lastWasVar && seg.kind === 'var') {\n expanded.push({ kind: 'text', value: '' })\n }\n expanded.push(seg)\n lastWasVar = seg.kind === 'var'\n }\n if (lastWasVar || expanded.length === 0) expanded.push({ kind: 'text', value: '' })\n\n for (const seg of expanded) {\n if (seg.kind === 'text') container.appendChild(makeTextSpan(seg.value))\n else container.appendChild(makeChipSpan(seg))\n }\n\n syncVisualEmpty()\n if (typeof targetCaret === 'number') setCaret(targetCaret)\n }\n\n /** 从 DOM 反推模型:宽容裸 text node、空 text span。 */\n function readDocFromDOM(): Doc {\n const segs: Segment[] = []\n for (const child of Array.from(opts.container.childNodes)) {\n if (isChipElement(child)) {\n const value = readChipValue(child)\n const domType = readChipType(child)\n const fallbackLabel =\n child.querySelector(`.${opts.cls.e('chip-label')}`)?.textContent ?? value\n // 优先从 variableMap 取最新 label / type,保持与外部数据源一致\n const fromMap = opts.getVariableMap?.().get(value)\n const finalType = fromMap?.type ?? domType\n const seg: Segment = finalType\n ? { kind: 'var', value, label: fromMap?.label ?? fallbackLabel, type: finalType }\n : { kind: 'var', value, label: fromMap?.label ?? fallbackLabel }\n segs.push(seg)\n } else {\n segs.push({ kind: 'text', value: child.textContent ?? '' })\n }\n }\n return normalize(segs)\n }\n\n /**\n * 计算光标在序列化字符串中的全局偏移。\n * 规则:\n * - 在 text span / 裸 text node 中:父级 / 节点之前的 segments 长度累加 + selection 内偏移\n * - 在 chip 边界:根据 selection 在 chip 节点之前 / 之后判断\n */\n function getCaretOffset(): number | null {\n const sel = document.getSelection()\n if (!sel || sel.rangeCount === 0) return null\n const range = sel.getRangeAt(0)\n if (!opts.container.contains(range.startContainer)) return null\n\n let offset = 0\n let found = false\n for (const child of Array.from(opts.container.childNodes)) {\n if (found) break\n if (isChipElement(child)) {\n if (range.startContainer === child) {\n // selection.startOffset 表示子节点索引:0 表示 chip 起始,>=1 表示 chip 之后\n if (range.startOffset === 0) return offset\n return offset + childSerialLength(child)\n }\n if (range.startContainer === opts.container && getChildIndex(child) === range.startOffset) {\n // 容器层 selection:偏移即子节点索引\n return offset\n }\n offset += childSerialLength(child)\n } else {\n // text span 或裸 text 节点\n if (\n range.startContainer === child ||\n (child.contains(range.startContainer) && child !== opts.container)\n ) {\n // 计算 selection 在该 child textContent 中的偏移\n const localOffset = computeTextOffsetWithin(\n child,\n range.startContainer,\n range.startOffset\n )\n return offset + localOffset\n }\n if (range.startContainer === opts.container && getChildIndex(child) === range.startOffset) {\n return offset\n }\n offset += childSerialLength(child)\n }\n }\n if (\n range.startContainer === opts.container &&\n range.startOffset === opts.container.childNodes.length\n ) {\n return offset\n }\n return null\n }\n\n function getChildIndex(node: Node): number {\n let i = 0\n let cur: Node | null = node.previousSibling\n while (cur) {\n i++\n cur = cur.previousSibling\n }\n return i\n }\n\n function computeTextOffsetWithin(root: Node, target: Node, targetOffset: number): number {\n if (root === target) return targetOffset\n let total = 0\n function walk(n: Node): boolean {\n if (n === target) {\n total += targetOffset\n return true\n }\n if (n.nodeType === 3) {\n total += (n as Text).data.length\n return false\n }\n for (const c of Array.from(n.childNodes)) {\n if (walk(c)) return true\n }\n return false\n }\n walk(root)\n return total\n }\n\n /** 把光标放到 offset 处,offset 大于文档长度时落到末尾。 */\n function setCaret(offset: number): void {\n const sel = document.getSelection()\n if (!sel) return\n\n let remain = offset\n const children = Array.from(opts.container.childNodes)\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!\n const len = childSerialLength(child)\n if (isChipElement(child)) {\n if (remain <= 0) {\n // 光标定到 chip 之前 → 落到容器层 child 索引\n placeAt(opts.container, i)\n return\n }\n if (remain < len) {\n // 落在 chip 内部 → 视作 chip 之后\n placeAt(opts.container, i + 1)\n return\n }\n remain -= len\n } else {\n if (remain <= len) {\n // 落在该 text span 内\n placeWithinText(child, remain)\n return\n }\n remain -= len\n }\n }\n // 落到末尾\n if (children.length === 0) {\n placeAt(opts.container, 0)\n return\n }\n const last = children[children.length - 1]!\n if (isChipElement(last)) placeAt(opts.container, children.length)\n else placeWithinText(last, (last.textContent ?? '').length)\n }\n\n function placeAt(parent: Node, index: number) {\n const sel = document.getSelection()\n if (!sel) return\n const range = document.createRange()\n range.setStart(parent, index)\n range.collapse(true)\n sel.removeAllRanges()\n sel.addRange(range)\n }\n\n function placeWithinText(span: Node, offset: number): void {\n const sel = document.getSelection()\n if (!sel) return\n const target = firstTextDescendant(span)\n const range = document.createRange()\n if (target) {\n const max = target.data.length\n range.setStart(target, Math.min(offset, max))\n } else {\n // 空 span:让光标落到 span 内(child index 0)\n range.setStart(span, 0)\n }\n range.collapse(true)\n sel.removeAllRanges()\n sel.addRange(range)\n }\n\n function firstTextDescendant(root: Node): Text | null {\n if (root.nodeType === 3) return root as Text\n for (const c of Array.from(root.childNodes)) {\n const t = firstTextDescendant(c)\n if (t) return t\n }\n return null\n }\n\n function getRectAtOffset(offset: number): DOMRect | null {\n let pos = 0\n for (const child of Array.from(opts.container.childNodes)) {\n const len = childSerialLength(child)\n if (offset >= pos && offset < pos + len) {\n if (isChipElement(child)) return (child as HTMLElement).getBoundingClientRect()\n const textNode = firstTextDescendant(child)\n if (!textNode) return (child as HTMLElement).getBoundingClientRect()\n const local = Math.min(offset - pos, textNode.data.length)\n const range = document.createRange()\n range.setStart(textNode, local)\n range.setEnd(textNode, Math.min(local + 1, textNode.data.length))\n const rects = range.getClientRects()\n return rects.length > 0\n ? (rects[0] as DOMRect)\n : (child as HTMLElement).getBoundingClientRect()\n }\n pos += len\n }\n return null\n }\n\n /** input 后:从 DOM 反推 doc,并触发 onChange(如有变化)。 */\n function syncFromDOM(): void {\n if (composing) return\n const next = readDocFromDOM()\n if (!docEqual(next, doc)) emitChange(next)\n }\n\n function docEqual(a: Doc, b: Doc): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n const x = a[i]!\n const y = b[i]!\n if (x.kind !== y.kind) return false\n if (x.kind === 'text' && y.kind === 'text' && x.value !== y.value) return false\n if (x.kind === 'var' && y.kind === 'var') {\n if (x.value !== y.value || x.label !== y.label || x.type !== y.type) return false\n }\n }\n return true\n }\n\n function onInput() {\n // 合成中模型不动,但框里已经有字:占位层必须立刻让位\n syncVisualEmpty()\n syncFromDOM()\n }\n function onCompositionStart() {\n composing = true\n }\n function onCompositionUpdate() {\n // 少数浏览器把合成文字落进 DOM 的时机晚于 input,这里再兜一次\n syncVisualEmpty()\n }\n function onCompositionEnd() {\n composing = false\n syncVisualEmpty()\n syncFromDOM()\n }\n function onPaste(e: ClipboardEvent) {\n e.preventDefault()\n const text = e.clipboardData?.getData('text/plain') ?? ''\n if (!text) return\n insertPlainTextAtCaret(text)\n }\n function onSelectionChange() {\n if (composing) return\n if (!opts.onSelectionChange) return\n const sel = document.getSelection()\n if (!sel || sel.rangeCount === 0) {\n opts.onSelectionChange({ offset: null })\n return\n }\n const range = sel.getRangeAt(0)\n if (!opts.container.contains(range.startContainer)) {\n opts.onSelectionChange({ offset: null })\n return\n }\n opts.onSelectionChange({ offset: getCaretOffset() })\n }\n\n function insertPlainTextAtCaret(text: string) {\n const sel = document.getSelection()\n if (!sel || sel.rangeCount === 0) return\n const range = sel.getRangeAt(0)\n if (!opts.container.contains(range.startContainer)) return\n range.deleteContents()\n const node = document.createTextNode(text)\n range.insertNode(node)\n range.setStartAfter(node)\n range.collapse(true)\n sel.removeAllRanges()\n sel.addRange(range)\n syncVisualEmpty()\n syncFromDOM()\n }\n\n // 命令:替换区间为 var chip\n function replaceRangeWithVar(start: number, end: number, variable: EditorVariableInfo) {\n const a = Math.min(start, end)\n const b = Math.max(start, end)\n const left = sliceDoc(doc, 0, a)\n const right = sliceDoc(doc, b, totalLength(doc))\n const inserted: Segment = variable.type\n ? { kind: 'var', value: variable.value, label: variable.label, type: variable.type }\n : { kind: 'var', value: variable.value, label: variable.label }\n const next = normalize([...left, inserted, ...right])\n emitChange(next)\n render(a + variable.value.length + 2) // 光标定到 chip 之后\n }\n\n function replaceVarAt(segIndex: number, variable: EditorVariableInfo) {\n const target = doc[segIndex]\n if (!target || target.kind !== 'var') return\n const next = doc.slice()\n const replaced: Segment = variable.type\n ? { kind: 'var', value: variable.value, label: variable.label, type: variable.type }\n : { kind: 'var', value: variable.value, label: variable.label }\n next[segIndex] = replaced\n const caretOffset = sliceLength(doc.slice(0, segIndex)) + variable.value.length + 2\n emitChange(normalize(next))\n render(caretOffset)\n }\n\n // 工具:基于序列化偏移的切片(不会切开 var)\n function sliceDoc(d: Doc, start: number, end: number): Doc {\n const out: Segment[] = []\n let pos = 0\n for (const seg of d) {\n const len = seg.kind === 'text' ? seg.value.length : seg.value.length + 2\n const segStart = pos\n const segEnd = pos + len\n if (segEnd <= start || segStart >= end) {\n pos += len\n continue\n }\n if (seg.kind === 'text') {\n const a = Math.max(0, start - segStart)\n const b = Math.min(len, end - segStart)\n const slice = seg.value.slice(a, b)\n if (slice) out.push({ kind: 'text', value: slice })\n } else {\n // var 是原子:只有当 [start, end) 完整覆盖该 var 时保留;否则抛弃\n if (start <= segStart && end >= segEnd) out.push(seg)\n }\n pos += len\n }\n return out\n }\n\n function sliceLength(d: Doc): number {\n let n = 0\n for (const s of d) n += s.kind === 'text' ? s.value.length : s.value.length + 2\n return n\n }\n\n function totalLength(d: Doc): number {\n return sliceLength(d)\n }\n\n // 绑定监听\n const container = opts.container\n container.addEventListener('input', onInput)\n container.addEventListener('compositionstart', onCompositionStart)\n container.addEventListener('compositionupdate', onCompositionUpdate)\n container.addEventListener('compositionend', onCompositionEnd)\n container.addEventListener('paste', onPaste)\n document.addEventListener('selectionchange', onSelectionChange)\n\n // 初次渲染\n render()\n\n return {\n getDoc: () => doc,\n setDoc: (next) => {\n doc = normalize(next)\n render()\n opts.onChange?.(doc)\n },\n getValue: () => serialize(doc),\n setValue: (value) => {\n doc = parse(value, opts.getVariableMap?.())\n render()\n opts.onChange?.(doc)\n },\n getCaretOffset,\n setCaretOffset: setCaret,\n getRectAtOffset,\n replaceRangeWithVar,\n replaceVarAt,\n rerender: () => render(),\n dispose: () => {\n container.removeEventListener('input', onInput)\n container.removeEventListener('compositionstart', onCompositionStart)\n container.removeEventListener('compositionupdate', onCompositionUpdate)\n container.removeEventListener('compositionend', onCompositionEnd)\n container.removeEventListener('paste', onPaste)\n document.removeEventListener('selectionchange', onSelectionChange)\n },\n container\n }\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { parse } from "./core/model.js";
3
3
  import { createEditor } from "./core/editor.js";
4
4
  import { createMention } from "./core/mention.js";
5
5
  import { createVariableMap } from "./di.js";
6
- import { computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, shallowRef, toDisplayString, unref, useTemplateRef, watch } from "vue";
6
+ import { computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, shallowRef, toDisplayString, unref, useTemplateRef, watch } from "vue";
7
7
  import { useFormFallbackProps } from "@veltra/compositions";
8
8
  import { bem, injectFormContext } from "@veltra/utils";
9
9
  //#region src/components/expression-editor/expression-editor.vue
@@ -24,8 +24,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
24
24
  span: {},
25
25
  label: {},
26
26
  field: {},
27
- disabled: { type: Boolean },
28
- readonly: { type: Boolean },
27
+ disabled: {
28
+ type: Boolean,
29
+ default: void 0
30
+ },
31
+ readonly: {
32
+ type: Boolean,
33
+ default: void 0
34
+ },
29
35
  rules: {},
30
36
  size: {}
31
37
  },
@@ -37,10 +43,13 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
37
43
  const { formProps } = injectFormContext();
38
44
  const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props]);
39
45
  const containerRef = useTemplateRef("container");
46
+ const rootElRef = useTemplateRef("rootEl");
47
+ const mentionAnchorRef = useTemplateRef("mentionAnchor");
40
48
  const pickerRef = useTemplateRef("pickerRef");
41
- const className = computed(() => [
42
- cls.b,
43
- cls.m(size.value),
49
+ /** 视觉盒(边框 / 背景 / 聚焦光晕 / 尺寸态)挂在 shell 上,根节点只做布局 */
50
+ const shellClass = computed(() => [
51
+ cls.e("shell"),
52
+ cls.em("shell", size.value),
44
53
  bem.is("disabled", disabled.value),
45
54
  bem.is("readonly", readonly.value)
46
55
  ]);
@@ -48,13 +57,26 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
48
57
  const editorRef = shallowRef(null);
49
58
  const mention = createMention();
50
59
  const currentDoc = shallowRef([]);
51
- const isEmpty = computed(() => currentDoc.value.length === 0);
52
- const showPlaceholder = computed(() => isEmpty.value && !disabled.value);
60
+ /** 框内视觉空态:与 IME 合成状态无关,由 editor 在 DOM 变化时上报 */
61
+ const visualEmpty = shallowRef(true);
62
+ const showPlaceholder = computed(() => visualEmpty.value && !disabled.value);
63
+ /** 上方 `@` 使用提示常驻:只跟可编辑状态走,不随内容增减隐藏 */
64
+ const showHint = computed(() => !disabled.value && !readonly.value);
53
65
  const pickerMode = shallowRef(null);
54
66
  const pickerTriggerDom = shallowRef(void 0);
55
67
  const reselectingSegIdx = shallowRef(null);
56
68
  const pickerFilter = shallowRef("");
57
69
  const pickerVisible = computed(() => pickerMode.value !== null);
70
+ /** 把 mention 锚点移到 `@` 字符下方,面板据此定位 */
71
+ function positionMentionAnchor(anchorOffset) {
72
+ const rootEl = rootElRef.value;
73
+ const anchor = mentionAnchorRef.value;
74
+ const rect = editorRef.value?.getRectAtOffset(anchorOffset);
75
+ if (!rootEl || !anchor || !rect) return;
76
+ const rootRect = rootEl.getBoundingClientRect();
77
+ anchor.style.left = `${rect.left - rootRect.left - rootEl.clientLeft}px`;
78
+ anchor.style.top = `${rect.bottom - rootRect.top - rootEl.clientTop}px`;
79
+ }
58
80
  function syncMention() {
59
81
  const editor = editorRef.value;
60
82
  if (!editor) return;
@@ -64,7 +86,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
64
86
  if (state) {
65
87
  pickerMode.value = "mention";
66
88
  pickerFilter.value = state.filter;
67
- pickerTriggerDom.value = containerRef.value ?? void 0;
89
+ positionMentionAnchor(state.anchorOffset);
90
+ pickerTriggerDom.value = mentionAnchorRef.value ?? containerRef.value ?? void 0;
68
91
  } else if (pickerMode.value === "mention") closePicker();
69
92
  }
70
93
  function closePicker() {
@@ -87,6 +110,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
87
110
  if (value !== (props.modelValue ?? "")) emit("update:modelValue", value);
88
111
  syncMention();
89
112
  },
113
+ onVisualEmptyChange: (empty) => {
114
+ visualEmpty.value = empty;
115
+ },
90
116
  onSelectionChange: () => {
91
117
  syncMention();
92
118
  },
@@ -97,6 +123,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
97
123
  pickerFilter.value = "";
98
124
  pickerTriggerDom.value = chipEl;
99
125
  reselectingSegIdx.value = segIndex;
126
+ containerRef.value?.focus();
100
127
  }
101
128
  });
102
129
  editorRef.value = editor;
@@ -176,20 +203,39 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
176
203
  }
177
204
  }
178
205
  return (_ctx, _cache) => {
179
- return openBlock(), createElementBlock("div", { class: normalizeClass(className.value) }, [
180
- createElementVNode("div", {
181
- ref: "container",
182
- class: normalizeClass(unref(cls).e("container")),
183
- contenteditable: !unref(readonly) && !unref(disabled),
184
- "data-empty": isEmpty.value || void 0,
185
- "data-placeholder": props.placeholder,
186
- onKeydown,
187
- onBlur
188
- }, null, 42, _hoisted_1),
189
- showPlaceholder.value ? (openBlock(), createElementBlock("div", {
206
+ return openBlock(), createElementBlock("div", {
207
+ ref: "rootEl",
208
+ class: normalizeClass(unref(cls).b)
209
+ }, [
210
+ createCommentVNode(" 提示行独立在边框盒(shell)之外 "),
211
+ showHint.value ? (openBlock(), createElementBlock("div", {
190
212
  key: 0,
191
- class: normalizeClass(unref(cls).e("placeholder"))
192
- }, toDisplayString(props.placeholder), 3)) : createCommentVNode("v-if", true),
213
+ class: normalizeClass(unref(cls).e("hint"))
214
+ }, [..._cache[0] || (_cache[0] = [
215
+ createTextVNode("输入 ", -1),
216
+ createElementVNode("kbd", null, "@", -1),
217
+ createTextVNode(" 唤起变量面板", -1)
218
+ ])], 2)) : createCommentVNode("v-if", true),
219
+ createElementVNode("div", { class: normalizeClass(shellClass.value) }, [
220
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("body")) }, [createElementVNode("div", {
221
+ ref: "container",
222
+ class: normalizeClass(unref(cls).e("container")),
223
+ contenteditable: !unref(readonly) && !unref(disabled),
224
+ "data-empty": visualEmpty.value || void 0,
225
+ "data-placeholder": props.placeholder,
226
+ onKeydown,
227
+ onBlur
228
+ }, null, 42, _hoisted_1), showPlaceholder.value ? (openBlock(), createElementBlock("div", {
229
+ key: 0,
230
+ class: normalizeClass(unref(cls).e("placeholder"))
231
+ }, toDisplayString(props.placeholder), 3)) : createCommentVNode("v-if", true)], 2),
232
+ createCommentVNode(" mention 面板的定位锚点:跟随 `@` 字符位置(见 positionMentionAnchor) "),
233
+ createElementVNode("span", {
234
+ ref: "mentionAnchor",
235
+ class: normalizeClass(unref(cls).e("mention-anchor")),
236
+ "aria-hidden": "true"
237
+ }, null, 2)
238
+ ], 2),
193
239
  createVNode(_sfc_main$1, {
194
240
  ref_key: "pickerRef",
195
241
  ref: pickerRef,
@@ -1 +1 @@
1
- {"version":3,"file":"expression-editor.js","names":[],"sources":["../../../src/components/expression-editor/expression-editor.vue"],"sourcesContent":["<template>\n <div :class=\"className\">\n <div\n ref=\"container\"\n :class=\"cls.e('container')\"\n :contenteditable=\"!readonly && !disabled\"\n :data-empty=\"isEmpty || undefined\"\n :data-placeholder=\"props.placeholder\"\n @keydown=\"onKeydown\"\n @blur=\"onBlur\"\n ></div>\n\n <div v-if=\"showPlaceholder\" :class=\"cls.e('placeholder')\">\n {{ props.placeholder }}\n </div>\n\n <VariablePicker\n ref=\"pickerRef\"\n :visible=\"pickerVisible\"\n :trigger-dom=\"pickerTriggerDom\"\n :variables=\"props.variables\"\n :filter=\"pickerFilter\"\n :selectable-levels=\"props.selectableLevels ?? 'leaf'\"\n @select=\"onPickerSelect\"\n @dismiss=\"onPickerDismiss\"\n @update:visible=\"onPickerVisibleChange\"\n />\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { bem } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport {\n computed,\n nextTick,\n onBeforeUnmount,\n onMounted,\n shallowRef,\n useTemplateRef,\n watch\n} from 'vue'\n\nimport type { ExpressionEditorProps, VariableItem } from '../../types'\nimport VariablePicker from './components/variable-picker.vue'\nimport { createEditor, type EditorAPI } from './core/editor'\nimport { createMention, type MentionAPI } from './core/mention'\nimport { parse, type Doc } from './core/model'\nimport { createVariableMap } from './di'\n\ndefineOptions({ name: 'UExpressionEditor' })\n\nconst props = withDefaults(defineProps<ExpressionEditorProps>(), {\n placeholder: '请输入表达式,输入 @ 可插入变量',\n selectableLevels: 'leaf'\n})\n\nconst emit = defineEmits<{ (e: 'update:modelValue', value: string): void }>()\n\nconst cls = bem('expression-editor')\nconst { formProps } = injectFormContext()\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst containerRef = useTemplateRef<HTMLDivElement>('container')\nconst pickerRef = useTemplateRef<{ handleKeydown: (e: KeyboardEvent) => boolean }>('pickerRef')\n\nconst className = computed(() => [\n cls.b,\n cls.m(size.value),\n bem.is('disabled', disabled.value),\n bem.is('readonly', readonly.value)\n])\n\nconst variableMap = computed(() => createVariableMap(props.variables))\n\nconst editorRef = shallowRef<EditorAPI | null>(null)\nconst mention: MentionAPI = createMention()\n\nconst currentDoc = shallowRef<Doc>([])\nconst isEmpty = computed(() => currentDoc.value.length === 0)\nconst showPlaceholder = computed(() => isEmpty.value && !disabled.value)\n\nconst pickerMode = shallowRef<'mention' | 'reselect' | null>(null)\nconst pickerTriggerDom = shallowRef<HTMLElement | undefined>(undefined)\nconst reselectingSegIdx = shallowRef<number | null>(null)\nconst pickerFilter = shallowRef('')\n\nconst pickerVisible = computed(() => pickerMode.value !== null)\n\nfunction syncMention() {\n const editor = editorRef.value\n if (!editor) return\n if (pickerMode.value === 'reselect') return\n const caret = editor.getCaretOffset()\n const state = mention.update(currentDoc.value, caret)\n if (state) {\n pickerMode.value = 'mention'\n pickerFilter.value = state.filter\n pickerTriggerDom.value = containerRef.value ?? undefined\n } else if (pickerMode.value === 'mention') {\n closePicker()\n }\n}\n\nfunction closePicker() {\n pickerMode.value = null\n reselectingSegIdx.value = null\n pickerFilter.value = ''\n}\n\nonMounted(() => {\n if (!containerRef.value) return\n const initialDoc = parse(props.modelValue ?? '', variableMap.value)\n currentDoc.value = initialDoc\n\n const editor = createEditor({\n container: containerRef.value,\n cls,\n initialDoc,\n getVariableMap: () => variableMap.value,\n onChange: (doc) => {\n currentDoc.value = doc\n const value = serializeFromDoc(doc)\n if (value !== (props.modelValue ?? '')) emit('update:modelValue', value)\n syncMention()\n },\n onSelectionChange: () => {\n syncMention()\n },\n onChipReselect: ({ chipEl, segIndex }) => {\n if (disabled.value || readonly.value) return\n mention.commit()\n pickerMode.value = 'reselect'\n pickerFilter.value = ''\n pickerTriggerDom.value = chipEl\n reselectingSegIdx.value = segIndex\n }\n })\n editorRef.value = editor\n})\n\nonBeforeUnmount(() => {\n editorRef.value?.dispose()\n})\n\nfunction serializeFromDoc(doc: Doc): string {\n let s = ''\n for (const seg of doc) s += seg.kind === 'text' ? seg.value : `{${seg.value}}`\n return s\n}\n\nwatch(\n () => props.modelValue,\n (v) => {\n const editor = editorRef.value\n if (!editor) return\n if (v === editor.getValue()) return\n editor.setValue(v ?? '')\n }\n)\n\nwatch(variableMap, () => {\n // variables 变化:重渲染以刷新 chip label / type\n const editor = editorRef.value\n if (!editor) return\n editor.setValue(editor.getValue())\n})\n\nfunction onKeydown(e: KeyboardEvent) {\n if (disabled.value || readonly.value) return\n // mention / reselect 激活时,优先把箭头键 / Enter / Esc 转给 picker\n if (pickerMode.value !== null) {\n const handledByPicker = pickerRef.value?.handleKeydown(e)\n if (handledByPicker) {\n e.preventDefault()\n return\n }\n }\n\n if (pickerMode.value === 'mention') {\n // 这些键导致 mention 退出(保留 @filter 文本,光标继续移动)\n if (e.key === ' ' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {\n mention.dismiss()\n closePicker()\n }\n }\n}\n\nfunction onBlur() {\n if (pickerMode.value === 'mention') {\n mention.dismiss()\n // 下一帧再关闭,避免和 picker 内 click 冲突\n void nextTick(() => closePicker())\n }\n}\n\nfunction onPickerSelect(item: VariableItem) {\n const editor = editorRef.value\n if (!editor) return\n\n if (pickerMode.value === 'mention') {\n const state = mention.getState()\n if (!state) return\n const anchor = state.anchorOffset\n const caret = editor.getCaretOffset() ?? anchor + state.filter.length + 1\n editor.replaceRangeWithVar(anchor, caret, {\n value: item.value,\n label: item.label,\n ...(item.type ? { type: item.type } : {})\n })\n mention.commit()\n } else if (pickerMode.value === 'reselect') {\n const idx = reselectingSegIdx.value\n if (idx !== null) {\n editor.replaceVarAt(idx, {\n value: item.value,\n label: item.label,\n ...(item.type ? { type: item.type } : {})\n })\n }\n }\n closePicker()\n}\n\nfunction onPickerDismiss() {\n if (pickerMode.value === 'mention') mention.dismiss()\n closePicker()\n}\n\nfunction onPickerVisibleChange(v: boolean) {\n if (!v && pickerMode.value !== null) {\n if (pickerMode.value === 'mention') mention.dismiss()\n closePicker()\n }\n}\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"expression-editor.js","names":[],"sources":["../../../src/components/expression-editor/expression-editor.vue"],"sourcesContent":["<template>\n <div ref=\"rootEl\" :class=\"cls.b\">\n <!-- 提示行独立在边框盒(shell)之外 -->\n <div v-if=\"showHint\" :class=\"cls.e('hint')\">输入 <kbd>@</kbd> 唤起变量面板</div>\n\n <div :class=\"shellClass\">\n <div :class=\"cls.e('body')\">\n <div\n ref=\"container\"\n :class=\"cls.e('container')\"\n :contenteditable=\"!readonly && !disabled\"\n :data-empty=\"visualEmpty || undefined\"\n :data-placeholder=\"props.placeholder\"\n @keydown=\"onKeydown\"\n @blur=\"onBlur\"\n ></div>\n\n <div v-if=\"showPlaceholder\" :class=\"cls.e('placeholder')\">\n {{ props.placeholder }}\n </div>\n </div>\n\n <!-- mention 面板的定位锚点:跟随 `@` 字符位置(见 positionMentionAnchor) -->\n <span ref=\"mentionAnchor\" :class=\"cls.e('mention-anchor')\" aria-hidden=\"true\"></span>\n </div>\n\n <VariablePicker\n ref=\"pickerRef\"\n :visible=\"pickerVisible\"\n :trigger-dom=\"pickerTriggerDom\"\n :variables=\"props.variables\"\n :filter=\"pickerFilter\"\n :selectable-levels=\"props.selectableLevels ?? 'leaf'\"\n @select=\"onPickerSelect\"\n @dismiss=\"onPickerDismiss\"\n @update:visible=\"onPickerVisibleChange\"\n />\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { bem } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport {\n computed,\n nextTick,\n onBeforeUnmount,\n onMounted,\n shallowRef,\n useTemplateRef,\n watch\n} from 'vue'\n\nimport type { ExpressionEditorProps, VariableItem } from '../../types'\nimport VariablePicker from './components/variable-picker.vue'\nimport { createEditor, type EditorAPI } from './core/editor'\nimport { createMention, type MentionAPI } from './core/mention'\nimport { parse, type Doc } from './core/model'\nimport { createVariableMap } from './di'\n\ndefineOptions({ name: 'UExpressionEditor' })\n\nconst props = withDefaults(defineProps<ExpressionEditorProps>(), {\n placeholder: '请输入表达式,输入 @ 可插入变量',\n selectableLevels: 'leaf',\n // Boolean prop 缺省会被 Vue 归一成 false,挡住 UForm 下发的 disabled / readonly,须显式留 undefined\n disabled: undefined,\n readonly: undefined\n})\n\nconst emit = defineEmits<{ (e: 'update:modelValue', value: string): void }>()\n\nconst cls = bem('expression-editor')\nconst { formProps } = injectFormContext()\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst containerRef = useTemplateRef<HTMLDivElement>('container')\nconst rootElRef = useTemplateRef<HTMLDivElement>('rootEl')\nconst mentionAnchorRef = useTemplateRef<HTMLSpanElement>('mentionAnchor')\nconst pickerRef = useTemplateRef<{ handleKeydown: (e: KeyboardEvent) => boolean }>('pickerRef')\n\n/** 视觉盒(边框 / 背景 / 聚焦光晕 / 尺寸态)挂在 shell 上,根节点只做布局 */\nconst shellClass = computed(() => [\n cls.e('shell'),\n cls.em('shell', size.value),\n bem.is('disabled', disabled.value),\n bem.is('readonly', readonly.value)\n])\n\nconst variableMap = computed(() => createVariableMap(props.variables))\n\nconst editorRef = shallowRef<EditorAPI | null>(null)\nconst mention: MentionAPI = createMention()\n\nconst currentDoc = shallowRef<Doc>([])\n/** 框内视觉空态:与 IME 合成状态无关,由 editor 在 DOM 变化时上报 */\nconst visualEmpty = shallowRef(true)\nconst showPlaceholder = computed(() => visualEmpty.value && !disabled.value)\n/** 上方 `@` 使用提示常驻:只跟可编辑状态走,不随内容增减隐藏 */\nconst showHint = computed(() => !disabled.value && !readonly.value)\n\nconst pickerMode = shallowRef<'mention' | 'reselect' | null>(null)\nconst pickerTriggerDom = shallowRef<HTMLElement | undefined>(undefined)\nconst reselectingSegIdx = shallowRef<number | null>(null)\nconst pickerFilter = shallowRef('')\n\nconst pickerVisible = computed(() => pickerMode.value !== null)\n\n/** 把 mention 锚点移到 `@` 字符下方,面板据此定位 */\nfunction positionMentionAnchor(anchorOffset: number) {\n const rootEl = rootElRef.value\n const anchor = mentionAnchorRef.value\n const rect = editorRef.value?.getRectAtOffset(anchorOffset)\n if (!rootEl || !anchor || !rect) return\n // absolute 定位以 padding box 为基准,需扣除根节点边框宽度\n const rootRect = rootEl.getBoundingClientRect()\n anchor.style.left = `${rect.left - rootRect.left - rootEl.clientLeft}px`\n anchor.style.top = `${rect.bottom - rootRect.top - rootEl.clientTop}px`\n}\n\nfunction syncMention() {\n const editor = editorRef.value\n if (!editor) return\n if (pickerMode.value === 'reselect') return\n const caret = editor.getCaretOffset()\n const state = mention.update(currentDoc.value, caret)\n if (state) {\n pickerMode.value = 'mention'\n pickerFilter.value = state.filter\n positionMentionAnchor(state.anchorOffset)\n pickerTriggerDom.value = mentionAnchorRef.value ?? containerRef.value ?? undefined\n } else if (pickerMode.value === 'mention') {\n closePicker()\n }\n}\n\nfunction closePicker() {\n pickerMode.value = null\n reselectingSegIdx.value = null\n pickerFilter.value = ''\n}\n\nonMounted(() => {\n if (!containerRef.value) return\n const initialDoc = parse(props.modelValue ?? '', variableMap.value)\n currentDoc.value = initialDoc\n\n const editor = createEditor({\n container: containerRef.value,\n cls,\n initialDoc,\n getVariableMap: () => variableMap.value,\n onChange: (doc) => {\n currentDoc.value = doc\n const value = serializeFromDoc(doc)\n if (value !== (props.modelValue ?? '')) emit('update:modelValue', value)\n syncMention()\n },\n onVisualEmptyChange: (empty) => {\n visualEmpty.value = empty\n },\n onSelectionChange: () => {\n syncMention()\n },\n onChipReselect: ({ chipEl, segIndex }) => {\n if (disabled.value || readonly.value) return\n mention.commit()\n pickerMode.value = 'reselect'\n pickerFilter.value = ''\n pickerTriggerDom.value = chipEl\n reselectingSegIdx.value = segIndex\n // chip 的 mousedown 被 preventDefault,编辑器没有焦点时点击 chip 焦点不会转移,\n // 键盘导航依赖 container 的 keydown,这里主动把焦点拉回编辑器\n containerRef.value?.focus()\n }\n })\n editorRef.value = editor\n})\n\nonBeforeUnmount(() => {\n editorRef.value?.dispose()\n})\n\nfunction serializeFromDoc(doc: Doc): string {\n let s = ''\n for (const seg of doc) s += seg.kind === 'text' ? seg.value : `{${seg.value}}`\n return s\n}\n\nwatch(\n () => props.modelValue,\n (v) => {\n const editor = editorRef.value\n if (!editor) return\n if (v === editor.getValue()) return\n editor.setValue(v ?? '')\n }\n)\n\nwatch(variableMap, () => {\n // variables 变化:重渲染以刷新 chip label / type\n const editor = editorRef.value\n if (!editor) return\n editor.setValue(editor.getValue())\n})\n\nfunction onKeydown(e: KeyboardEvent) {\n if (disabled.value || readonly.value) return\n // mention / reselect 激活时,优先把箭头键 / Enter / Esc 转给 picker\n if (pickerMode.value !== null) {\n const handledByPicker = pickerRef.value?.handleKeydown(e)\n if (handledByPicker) {\n e.preventDefault()\n return\n }\n }\n\n if (pickerMode.value === 'mention') {\n // 这些键导致 mention 退出(保留 @filter 文本,光标继续移动)\n if (e.key === ' ' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {\n mention.dismiss()\n closePicker()\n }\n }\n}\n\nfunction onBlur() {\n if (pickerMode.value === 'mention') {\n mention.dismiss()\n // 下一帧再关闭,避免和 picker 内 click 冲突\n void nextTick(() => closePicker())\n }\n}\n\nfunction onPickerSelect(item: VariableItem) {\n const editor = editorRef.value\n if (!editor) return\n\n if (pickerMode.value === 'mention') {\n const state = mention.getState()\n if (!state) return\n const anchor = state.anchorOffset\n const caret = editor.getCaretOffset() ?? anchor + state.filter.length + 1\n editor.replaceRangeWithVar(anchor, caret, {\n value: item.value,\n label: item.label,\n ...(item.type ? { type: item.type } : {})\n })\n mention.commit()\n } else if (pickerMode.value === 'reselect') {\n const idx = reselectingSegIdx.value\n if (idx !== null) {\n editor.replaceVarAt(idx, {\n value: item.value,\n label: item.label,\n ...(item.type ? { type: item.type } : {})\n })\n }\n }\n closePicker()\n}\n\nfunction onPickerDismiss() {\n if (pickerMode.value === 'mention') mention.dismiss()\n closePicker()\n}\n\nfunction onPickerVisibleChange(v: boolean) {\n if (!v && pickerMode.value !== null) {\n if (pickerMode.value === 'mention') mention.dismiss()\n closePicker()\n }\n}\n</script>\n"],"mappings":"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"}