@veltra/desktop 1.7.18 → 1.7.19

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.
@@ -12,7 +12,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
12
12
  __name: "batch-edit-list",
13
13
  props: { slots: {} },
14
14
  setup(__props) {
15
- const { cls, state, props, emit, staticFeatures, dynamicFeatures, startEdit, handleCreate, handleDelete, handleInsertToNext, handleInsertToPrev, handleInsertChild } = inject(BatchEditDIKey);
15
+ const { cls, state, props, emit, staticFeatures, dynamicFeatures, startEdit, handleClose, handleCreate, handleDelete, handleInsertToNext, handleInsertToPrev, handleInsertChild } = inject(BatchEditDIKey);
16
16
  const tableProps = computed(() => {
17
17
  return o(props).omit([
18
18
  "model",
@@ -53,6 +53,15 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
53
53
  function canDelete(row) {
54
54
  return !props.readonly && allowed("delete", row);
55
55
  }
56
+ /**
57
+ * 点击行打开编辑/查看表单;再次点击当前行取消选中并关闭表单。
58
+ * update 特性关闭时点击行不响应
59
+ */
60
+ function handleUpdateCurrentRow(row) {
61
+ if (!allowed("update", row)) return;
62
+ if (row) startEdit(row);
63
+ else handleClose();
64
+ }
56
65
  const showCreateBtn = computed(() => {
57
66
  if (props.readonly) return false;
58
67
  const hasFeature = staticFeatures.value.has("create") || dynamicFeatures.value.create?.();
@@ -64,13 +73,16 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
64
73
  slots: __props.slots,
65
74
  class: [unref(cls).e("table")],
66
75
  columns: columns.value,
76
+ "highlight-current": "",
67
77
  current: unref(state).row,
68
- "onUpdate:checked": _cache[0] || (_cache[0] = ($event) => unref(emit)("update:checked", $event)),
69
- "onUpdate:selected": _cache[1] || (_cache[1] = ($event) => unref(emit)("update:selected", $event))
78
+ "onUpdate:current": handleUpdateCurrentRow,
79
+ "onUpdate:checked": _cache[1] || (_cache[1] = ($event) => unref(emit)("update:checked", $event)),
80
+ "onUpdate:selected": _cache[2] || (_cache[2] = ($event) => unref(emit)("update:selected", $event))
70
81
  }), createSlots({
71
82
  "column:__action__": withCtx(({ row }) => [createVNode(unref(action_group_default), {
72
83
  max: 5,
73
- circle: ""
84
+ circle: "",
85
+ onClick: _cache[0] || (_cache[0] = withModifiers(() => {}, ["stop"]))
74
86
  }, {
75
87
  default: withCtx(() => [
76
88
  allowed("update", row) ? (openBlock(), createBlock(unref(_sfc_main$3), mergeProps({
@@ -122,7 +134,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
122
134
  plain: "",
123
135
  onClick: withModifiers(unref(handleCreate), ["stop"])
124
136
  }, {
125
- default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode(" 新增一行 ", -1)])]),
137
+ default: withCtx(() => [..._cache[3] || (_cache[3] = [createTextVNode(" 新增一行 ", -1)])]),
126
138
  _: 1
127
139
  }, 8, [
128
140
  "class",
@@ -1 +1 @@
1
- {"version":3,"file":"batch-edit-list.js","names":[],"sources":["../../../src/components/batch-edit/batch-edit-list.vue"],"sourcesContent":["<template>\n <u-table\n v-bind=\"tableProps\"\n :slots=\"slots\"\n :class=\"[cls.e('table')]\"\n :columns=\"columns\"\n :current=\"state.row\"\n @update:checked=\"emit('update:checked', $event)\"\n @update:selected=\"emit('update:selected', $event)\"\n >\n <template #column:__action__=\"{ row }\">\n <u-action-group :max=\"5\" circle>\n <u-action\n v-if=\"allowed('update', row)\"\n :icon=\"props.readonly ? View : EditPen\"\n :title=\"props.readonly ? '查看' : '编辑'\"\n @run=\"startEdit(row)\"\n v-bind=\"props.actionsProps?.update\"\n />\n <u-action\n v-if=\"!props.readonly && !!props.tree && allowed('createChild', row)\"\n :icon=\"AddChild\"\n title=\"添加子级\"\n @run=\"handleInsertChild(row)\"\n />\n <u-action\n v-if=\"canDelete(row)\"\n :icon=\"Delete\"\n type=\"danger\"\n title=\"删除\"\n @run=\"handleDelete(row)\"\n v-bind=\"props.actionsProps?.delete\"\n />\n <u-action\n v-if=\"!props.readonly && allowed('create', row)\"\n :icon=\"InsertToPrev\"\n title=\"在上方插入\"\n @run=\"handleInsertToPrev(row)\"\n />\n <u-action\n v-if=\"!props.readonly && allowed('create', row)\"\n :icon=\"InsertToNext\"\n title=\"在下方插入\"\n @run=\"handleInsertToNext(row)\"\n />\n </u-action-group>\n </template>\n\n <!-- 新增按钮 -->\n <template #append v-if=\"showCreateBtn\">\n <div :class=\"cls.e('add')\">\n <u-button\n :class=\"cls.e('add-btn')\"\n :icon=\"Plus\"\n :loading=\"state.loading\"\n type=\"primary\"\n plain\n @click.stop=\"handleCreate\"\n >\n 新增一行\n </u-button>\n </div>\n </template>\n </u-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { o } from '@cat-kit/core'\nimport {\n AddChild,\n Delete,\n EditPen,\n InsertToNext,\n InsertToPrev,\n Plus,\n View\n} from '@veltra/icons/normal'\nimport { computed, inject, type Slots } from 'vue'\n\nimport type { BatchEditFeature, TableRow } from '../../types'\nimport { UAction, UActionGroup } from '../action'\nimport { UButton } from '../button'\nimport { UTable } from '../table'\nimport { BatchEditDIKey } from './di'\n\ndefineOptions({ name: 'UBatchEditList' })\n\ndefineProps<{ slots: Slots }>()\n\nconst {\n cls,\n state,\n props,\n emit,\n staticFeatures,\n dynamicFeatures,\n startEdit,\n handleCreate,\n handleDelete,\n handleInsertToNext,\n handleInsertToPrev,\n handleInsertChild\n} = inject(BatchEditDIKey)!\n\nconst tableProps = computed(() => {\n return o(props as Record<string, any>).omit([\n 'model',\n 'columns',\n 'cols',\n 'readonly',\n 'deleteMethod',\n 'saveMethod',\n 'features',\n 'beforeCreate',\n 'formMode',\n 'quickEdit',\n 'labelWidth',\n 'actionsProps'\n ])\n})\n\nconst hasNot = (value: BatchEditFeature[]) => value.every((v) => !staticFeatures.value.has(v))\n\nconst allowed = (feature: BatchEditFeature, row?: TableRow) => {\n return staticFeatures.value.has(feature) || !!dynamicFeatures.value[feature]?.(row)\n}\n\nconst columns = computed(() => {\n // 只读模式的操作列只保留「查看」入口\n const related: BatchEditFeature[] = props.readonly\n ? ['update']\n : ['create', 'delete', 'createChild', 'update']\n if (hasNot(related)) return props.columns\n\n return (props.columns ?? []).concat({\n name: '操作',\n key: '__action__',\n align: 'center',\n width: 180,\n fixed: 'right',\n resizable: false\n })\n})\n\nfunction canDelete(row: TableRow) {\n return !props.readonly && allowed('delete', row)\n}\n\nconst showCreateBtn = computed(() => {\n if (props.readonly) return false\n\n const hasFeature = staticFeatures.value.has('create') || dynamicFeatures.value.create?.()\n const createActionVisible = state.formActionType === 'create' && state.formVisible\n return hasFeature && !createActionVisible\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"batch-edit-list.js","names":[],"sources":["../../../src/components/batch-edit/batch-edit-list.vue"],"sourcesContent":["<template>\n <u-table\n v-bind=\"tableProps\"\n :slots=\"slots\"\n :class=\"[cls.e('table')]\"\n :columns=\"columns\"\n highlight-current\n :current=\"state.row\"\n @update:current=\"handleUpdateCurrentRow\"\n @update:checked=\"emit('update:checked', $event)\"\n @update:selected=\"emit('update:selected', $event)\"\n >\n <template #column:__action__=\"{ row }\">\n <u-action-group :max=\"5\" circle @click.stop>\n <u-action\n v-if=\"allowed('update', row)\"\n :icon=\"props.readonly ? View : EditPen\"\n :title=\"props.readonly ? '查看' : '编辑'\"\n @run=\"startEdit(row)\"\n v-bind=\"props.actionsProps?.update\"\n />\n <u-action\n v-if=\"!props.readonly && !!props.tree && allowed('createChild', row)\"\n :icon=\"AddChild\"\n title=\"添加子级\"\n @run=\"handleInsertChild(row)\"\n />\n <u-action\n v-if=\"canDelete(row)\"\n :icon=\"Delete\"\n type=\"danger\"\n title=\"删除\"\n @run=\"handleDelete(row)\"\n v-bind=\"props.actionsProps?.delete\"\n />\n <u-action\n v-if=\"!props.readonly && allowed('create', row)\"\n :icon=\"InsertToPrev\"\n title=\"在上方插入\"\n @run=\"handleInsertToPrev(row)\"\n />\n <u-action\n v-if=\"!props.readonly && allowed('create', row)\"\n :icon=\"InsertToNext\"\n title=\"在下方插入\"\n @run=\"handleInsertToNext(row)\"\n />\n </u-action-group>\n </template>\n\n <!-- 新增按钮 -->\n <template #append v-if=\"showCreateBtn\">\n <div :class=\"cls.e('add')\">\n <u-button\n :class=\"cls.e('add-btn')\"\n :icon=\"Plus\"\n :loading=\"state.loading\"\n type=\"primary\"\n plain\n @click.stop=\"handleCreate\"\n >\n 新增一行\n </u-button>\n </div>\n </template>\n </u-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { o } from '@cat-kit/core'\nimport {\n AddChild,\n Delete,\n EditPen,\n InsertToNext,\n InsertToPrev,\n Plus,\n View\n} from '@veltra/icons/normal'\nimport { computed, inject, type Slots } from 'vue'\n\nimport type { BatchEditFeature, TableRow } from '../../types'\nimport { UAction, UActionGroup } from '../action'\nimport { UButton } from '../button'\nimport { UTable } from '../table'\nimport { BatchEditDIKey } from './di'\n\ndefineOptions({ name: 'UBatchEditList' })\n\ndefineProps<{ slots: Slots }>()\n\nconst {\n cls,\n state,\n props,\n emit,\n staticFeatures,\n dynamicFeatures,\n startEdit,\n handleClose,\n handleCreate,\n handleDelete,\n handleInsertToNext,\n handleInsertToPrev,\n handleInsertChild\n} = inject(BatchEditDIKey)!\n\nconst tableProps = computed(() => {\n return o(props as Record<string, any>).omit([\n 'model',\n 'columns',\n 'cols',\n 'readonly',\n 'deleteMethod',\n 'saveMethod',\n 'features',\n 'beforeCreate',\n 'formMode',\n 'quickEdit',\n 'labelWidth',\n 'actionsProps'\n ])\n})\n\nconst hasNot = (value: BatchEditFeature[]) => value.every((v) => !staticFeatures.value.has(v))\n\nconst allowed = (feature: BatchEditFeature, row?: TableRow) => {\n return staticFeatures.value.has(feature) || !!dynamicFeatures.value[feature]?.(row)\n}\n\nconst columns = computed(() => {\n // 只读模式的操作列只保留「查看」入口\n const related: BatchEditFeature[] = props.readonly\n ? ['update']\n : ['create', 'delete', 'createChild', 'update']\n if (hasNot(related)) return props.columns\n\n return (props.columns ?? []).concat({\n name: '操作',\n key: '__action__',\n align: 'center',\n width: 180,\n fixed: 'right',\n resizable: false\n })\n})\n\nfunction canDelete(row: TableRow) {\n return !props.readonly && allowed('delete', row)\n}\n\n/**\n * 点击行打开编辑/查看表单;再次点击当前行取消选中并关闭表单。\n * update 特性关闭时点击行不响应\n */\nfunction handleUpdateCurrentRow(row?: TableRow) {\n if (!allowed('update', row)) return\n\n if (row) {\n startEdit(row)\n } else {\n handleClose()\n }\n}\n\nconst showCreateBtn = computed(() => {\n if (props.readonly) return false\n\n const hasFeature = staticFeatures.value.has('create') || dynamicFeatures.value.create?.()\n const createActionVisible = state.formActionType === 'create' && state.formVisible\n return hasFeature && !createActionVisible\n})\n</script>\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"di.js","names":[],"sources":["../../../src/components/batch-edit/di.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\nimport type { ComputedRef, InjectionKey, ShallowRef } from 'vue'\n\nimport type {\n BatchEditEmits,\n BatchEditFeature,\n BatchEditProps,\n BatchEditStates,\n TableRow\n} from '../../types'\nimport type { EditReturned } from './use-handlers'\n\nexport const BatchEditDIKey: InjectionKey<\n {\n cls: BEM<'batch-edit', 'u-'>\n props: BatchEditProps\n emit: BatchEditEmits\n state: BatchEditStates\n staticFeatures: ComputedRef<Set<BatchEditFeature>>\n dynamicFeatures: ComputedRef<\n Record<BatchEditFeature, ((row?: TableRow) => boolean) | undefined>\n >\n /** 组件或其子节点是否处于聚焦状态 */\n focused: ShallowRef<boolean>\n /** 是否正在以编程方式重置/回显表单,此期间禁止 quick-edit 回写行数据 */\n syncing: ShallowRef<boolean>\n /** 开始编辑/查看一行(操作列「编辑」按钮触发) */\n startEdit: (row: TableRow) => void\n } & EditReturned\n> = Symbol('BatchEditDIKey')\n"],"mappings":";AAYA,MAAa,iBAiBT,OAAO,gBAAgB"}
1
+ {"version":3,"file":"di.js","names":[],"sources":["../../../src/components/batch-edit/di.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\nimport type { ComputedRef, InjectionKey, ShallowRef } from 'vue'\n\nimport type {\n BatchEditEmits,\n BatchEditFeature,\n BatchEditProps,\n BatchEditStates,\n TableRow\n} from '../../types'\nimport type { EditReturned } from './use-handlers'\n\nexport const BatchEditDIKey: InjectionKey<\n {\n cls: BEM<'batch-edit', 'u-'>\n props: BatchEditProps\n emit: BatchEditEmits\n state: BatchEditStates\n staticFeatures: ComputedRef<Set<BatchEditFeature>>\n dynamicFeatures: ComputedRef<\n Record<BatchEditFeature, ((row?: TableRow) => boolean) | undefined>\n >\n /** 组件或其子节点是否处于聚焦状态 */\n focused: ShallowRef<boolean>\n /** 是否正在以编程方式重置/回显表单,此期间禁止 quick-edit 回写行数据 */\n syncing: ShallowRef<boolean>\n /** 开始编辑/查看一行(点击行或操作列「编辑」按钮触发) */\n startEdit: (row: TableRow) => void\n } & EditReturned\n> = Symbol('BatchEditDIKey')\n"],"mappings":";AAYA,MAAa,iBAiBT,OAAO,gBAAgB"}
@@ -92,7 +92,7 @@
92
92
  backdrop-filter: var(--u-bg-filter);
93
93
  flex-grow: 1;
94
94
  }
95
- .u-dialog__body .u-scroll__content {
95
+ .u-dialog__body > .u-scroll__container > .u-scroll__content {
96
96
  padding: 8px;
97
97
  }
98
98
  .u-dialog__footer {
@@ -2,16 +2,10 @@
2
2
  const CHIP_DATA_SEG = "var";
3
3
  const CHIP_VALUE_ATTR = "data-value";
4
4
  const CHIP_TYPE_ATTR = "data-var-type";
5
- const CHIP_CLOSE_ATTR = "data-chip-close";
6
5
  /** 判断一个 DOM 节点是否为 chip 元素(var 段渲染体) */
7
6
  function isChipElement(node) {
8
7
  return !!node && node.nodeType === 1 && node.getAttribute("data-seg") === CHIP_DATA_SEG;
9
8
  }
10
- /** 判断点击目标是否落在 × 删除区。 */
11
- function isCloseTarget(target) {
12
- if (!(target instanceof Element)) return false;
13
- return !!target.closest(`[${CHIP_CLOSE_ATTR}]`);
14
- }
15
9
  /** 从给定 segment 创建一个 chip 元素(var 段渲染体)。 */
16
10
  function createChip(segment, opts) {
17
11
  const { cls } = opts;
@@ -25,19 +19,11 @@ function createChip(segment, opts) {
25
19
  const label = document.createElement("span");
26
20
  label.className = cls.e("chip-label");
27
21
  label.textContent = segment.type ? `${segment.label} (${segment.type})` : segment.label;
28
- const close = document.createElement("span");
29
- close.className = cls.e("chip-close");
30
- close.setAttribute(CHIP_CLOSE_ATTR, "true");
31
- close.setAttribute("aria-label", "删除");
32
- close.setAttribute("role", "button");
33
- close.textContent = "×";
34
22
  chip.appendChild(label);
35
- chip.appendChild(close);
36
23
  chip.addEventListener("mousedown", (e) => {
37
24
  e.preventDefault();
38
25
  e.stopPropagation();
39
- if (isCloseTarget(e.target)) opts.onRemove(chip);
40
- else opts.onReselect(chip);
26
+ opts.onReselect(chip);
41
27
  });
42
28
  return chip;
43
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"chip.js","names":[],"sources":["../../../../src/components/expression-editor/core/chip.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\n\nimport type { Segment } from './model'\n\nexport interface ChipDeps {\n cls: BEM<'expression-editor'>\n}\n\nexport interface ChipFactoryOptions extends ChipDeps {\n /**\n * 当用户点击 chip 主体(不是 ×)时回调,由调用方决定如何打开重选面板。\n * 收到的 chipEl 当前在 DOM 中的位置即为重选锚点。\n */\n onReselect: (chipEl: HTMLElement) => void\n /** 当用户点击 × 时回调,调用方应将该 chip 从模型 / DOM 中移除。 */\n onRemove: (chipEl: HTMLElement) => void\n}\n\nconst CHIP_DATA_SEG = 'var'\nconst CHIP_VALUE_ATTR = 'data-value'\nconst CHIP_TYPE_ATTR = 'data-var-type'\nconst CHIP_CLOSE_ATTR = 'data-chip-close'\n\n/** 判断一个 DOM 节点是否为 chip 元素(var 段渲染体) */\nexport function isChipElement(node: Node | null): node is HTMLElement {\n return (\n !!node &&\n node.nodeType === 1 &&\n (node as HTMLElement).getAttribute('data-seg') === CHIP_DATA_SEG\n )\n}\n\n/** 判断点击目标是否落在 × 删除区。 */\nfunction isCloseTarget(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false\n return !!target.closest(`[${CHIP_CLOSE_ATTR}]`)\n}\n\n/** 从给定 segment 创建一个 chip 元素(var 段渲染体)。 */\nexport function createChip(\n segment: Extract<Segment, { kind: 'var' }>,\n opts: ChipFactoryOptions\n): HTMLElement {\n const { cls } = opts\n\n const chip = document.createElement('span')\n chip.setAttribute('data-seg', CHIP_DATA_SEG)\n chip.setAttribute(CHIP_VALUE_ATTR, segment.value)\n if (segment.type) chip.setAttribute(CHIP_TYPE_ATTR, segment.type)\n chip.setAttribute('contenteditable', 'false')\n chip.className = cls.e('chip')\n chip.title = segment.label\n\n const label = document.createElement('span')\n label.className = cls.e('chip-label')\n label.textContent = segment.type ? `${segment.label} (${segment.type})` : segment.label\n\n const close = document.createElement('span')\n close.className = cls.e('chip-close')\n close.setAttribute(CHIP_CLOSE_ATTR, 'true')\n close.setAttribute('aria-label', '删除')\n close.setAttribute('role', 'button')\n close.textContent = '×'\n\n chip.appendChild(label)\n chip.appendChild(close)\n\n chip.addEventListener('mousedown', (e) => {\n e.preventDefault()\n // 阻止冒泡:避免宿主弹框(如 dialog)的 mousedown 层级提升逻辑把 chip 打开的重选面板盖住\n e.stopPropagation()\n if (isCloseTarget(e.target)) {\n opts.onRemove(chip)\n } else {\n opts.onReselect(chip)\n }\n })\n\n return chip\n}\n\n/** 读取 chip 元素绑定的变量 value。 */\nexport function readChipValue(el: HTMLElement): string {\n return el.getAttribute(CHIP_VALUE_ATTR) ?? ''\n}\n\n/** 读取 chip 元素绑定的可选 type。 */\nexport function readChipType(el: HTMLElement): string | undefined {\n return el.getAttribute(CHIP_TYPE_ATTR) ?? undefined\n}\n\n/** 在 chip 上设置 / 取消选中(焦点)样式。 */\nexport function setChipFocused(el: HTMLElement, focused: boolean): void {\n el.classList.toggle('is-focused', focused)\n}\n\n/** 测试支持:暴露用于 DOM 选择器的属性常量。 */\nexport const ChipAttrs = {\n seg: 'data-seg',\n value: CHIP_VALUE_ATTR,\n type: CHIP_TYPE_ATTR,\n close: CHIP_CLOSE_ATTR\n} as const\n"],"mappings":";AAkBA,MAAM,gBAAgB;AACtB,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;AACvB,MAAM,kBAAkB;;AAGxB,SAAgB,cAAc,MAAwC;CACpE,OACE,CAAC,CAAC,QACF,KAAK,aAAa,KACjB,KAAqB,aAAa,UAAU,MAAM;AAEvD;;AAGA,SAAS,cAAc,QAAqC;CAC1D,IAAI,EAAE,kBAAkB,UAAU,OAAO;CACzC,OAAO,CAAC,CAAC,OAAO,QAAQ,IAAI,gBAAgB,EAAE;AAChD;;AAGA,SAAgB,WACd,SACA,MACa;CACb,MAAM,EAAE,QAAQ;CAEhB,MAAM,OAAO,SAAS,cAAc,MAAM;CAC1C,KAAK,aAAa,YAAY,aAAa;CAC3C,KAAK,aAAa,iBAAiB,QAAQ,KAAK;CAChD,IAAI,QAAQ,MAAM,KAAK,aAAa,gBAAgB,QAAQ,IAAI;CAChE,KAAK,aAAa,mBAAmB,OAAO;CAC5C,KAAK,YAAY,IAAI,EAAE,MAAM;CAC7B,KAAK,QAAQ,QAAQ;CAErB,MAAM,QAAQ,SAAS,cAAc,MAAM;CAC3C,MAAM,YAAY,IAAI,EAAE,YAAY;CACpC,MAAM,cAAc,QAAQ,OAAO,GAAG,QAAQ,MAAM,IAAI,QAAQ,KAAK,KAAK,QAAQ;CAElF,MAAM,QAAQ,SAAS,cAAc,MAAM;CAC3C,MAAM,YAAY,IAAI,EAAE,YAAY;CACpC,MAAM,aAAa,iBAAiB,MAAM;CAC1C,MAAM,aAAa,cAAc,IAAI;CACrC,MAAM,aAAa,QAAQ,QAAQ;CACnC,MAAM,cAAc;CAEpB,KAAK,YAAY,KAAK;CACtB,KAAK,YAAY,KAAK;CAEtB,KAAK,iBAAiB,cAAc,MAAM;EACxC,EAAE,eAAe;EAEjB,EAAE,gBAAgB;EAClB,IAAI,cAAc,EAAE,MAAM,GACxB,KAAK,SAAS,IAAI;OAElB,KAAK,WAAW,IAAI;CAExB,CAAC;CAED,OAAO;AACT;;AAGA,SAAgB,cAAc,IAAyB;CACrD,OAAO,GAAG,aAAa,eAAe,KAAK;AAC7C;;AAGA,SAAgB,aAAa,IAAqC;CAChE,OAAO,GAAG,aAAa,cAAc,KAAK,KAAA;AAC5C"}
1
+ {"version":3,"file":"chip.js","names":[],"sources":["../../../../src/components/expression-editor/core/chip.ts"],"sourcesContent":["import type { BEM } from '@veltra/utils'\n\nimport type { Segment } from './model'\n\nexport interface ChipDeps {\n cls: BEM<'expression-editor'>\n}\n\nexport interface ChipFactoryOptions extends ChipDeps {\n /**\n * 当用户点击 chip 主体时回调,由调用方决定如何打开重选面板。\n * 收到的 chipEl 当前在 DOM 中的位置即为重选锚点。\n */\n onReselect: (chipEl: HTMLElement) => void\n}\n\nconst CHIP_DATA_SEG = 'var'\nconst CHIP_VALUE_ATTR = 'data-value'\nconst CHIP_TYPE_ATTR = 'data-var-type'\n\n/** 判断一个 DOM 节点是否为 chip 元素(var 段渲染体) */\nexport function isChipElement(node: Node | null): node is HTMLElement {\n return (\n !!node &&\n node.nodeType === 1 &&\n (node as HTMLElement).getAttribute('data-seg') === CHIP_DATA_SEG\n )\n}\n\n/** 从给定 segment 创建一个 chip 元素(var 段渲染体)。 */\nexport function createChip(\n segment: Extract<Segment, { kind: 'var' }>,\n opts: ChipFactoryOptions\n): HTMLElement {\n const { cls } = opts\n\n const chip = document.createElement('span')\n chip.setAttribute('data-seg', CHIP_DATA_SEG)\n chip.setAttribute(CHIP_VALUE_ATTR, segment.value)\n if (segment.type) chip.setAttribute(CHIP_TYPE_ATTR, segment.type)\n chip.setAttribute('contenteditable', 'false')\n chip.className = cls.e('chip')\n chip.title = segment.label\n\n const label = document.createElement('span')\n label.className = cls.e('chip-label')\n label.textContent = segment.type ? `${segment.label} (${segment.type})` : segment.label\n\n chip.appendChild(label)\n\n chip.addEventListener('mousedown', (e) => {\n e.preventDefault()\n // 阻止冒泡:避免宿主弹框(如 dialog)的 mousedown 层级提升逻辑把 chip 打开的重选面板盖住\n e.stopPropagation()\n opts.onReselect(chip)\n })\n\n return chip\n}\n\n/** 读取 chip 元素绑定的变量 value。 */\nexport function readChipValue(el: HTMLElement): string {\n return el.getAttribute(CHIP_VALUE_ATTR) ?? ''\n}\n\n/** 读取 chip 元素绑定的可选 type。 */\nexport function readChipType(el: HTMLElement): string | undefined {\n return el.getAttribute(CHIP_TYPE_ATTR) ?? undefined\n}\n\n/** 在 chip 上设置 / 取消选中(焦点)样式。 */\nexport function setChipFocused(el: HTMLElement, focused: boolean): void {\n el.classList.toggle('is-focused', focused)\n}\n\n/** 测试支持:暴露用于 DOM 选择器的属性常量。 */\nexport const ChipAttrs = { seg: 'data-seg', value: CHIP_VALUE_ATTR, type: CHIP_TYPE_ATTR } as const\n"],"mappings":";AAgBA,MAAM,gBAAgB;AACtB,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;;AAGvB,SAAgB,cAAc,MAAwC;CACpE,OACE,CAAC,CAAC,QACF,KAAK,aAAa,KACjB,KAAqB,aAAa,UAAU,MAAM;AAEvD;;AAGA,SAAgB,WACd,SACA,MACa;CACb,MAAM,EAAE,QAAQ;CAEhB,MAAM,OAAO,SAAS,cAAc,MAAM;CAC1C,KAAK,aAAa,YAAY,aAAa;CAC3C,KAAK,aAAa,iBAAiB,QAAQ,KAAK;CAChD,IAAI,QAAQ,MAAM,KAAK,aAAa,gBAAgB,QAAQ,IAAI;CAChE,KAAK,aAAa,mBAAmB,OAAO;CAC5C,KAAK,YAAY,IAAI,EAAE,MAAM;CAC7B,KAAK,QAAQ,QAAQ;CAErB,MAAM,QAAQ,SAAS,cAAc,MAAM;CAC3C,MAAM,YAAY,IAAI,EAAE,YAAY;CACpC,MAAM,cAAc,QAAQ,OAAO,GAAG,QAAQ,MAAM,IAAI,QAAQ,KAAK,KAAK,QAAQ;CAElF,KAAK,YAAY,KAAK;CAEtB,KAAK,iBAAiB,cAAc,MAAM;EACxC,EAAE,eAAe;EAEjB,EAAE,gBAAgB;EAClB,KAAK,WAAW,IAAI;CACtB,CAAC;CAED,OAAO;AACT;;AAGA,SAAgB,cAAc,IAAyB;CACrD,OAAO,GAAG,aAAa,eAAe,KAAK;AAC7C;;AAGA,SAAgB,aAAa,IAAqC;CAChE,OAAO,GAAG,aAAa,cAAc,KAAK,KAAA;AAC5C"}
@@ -27,14 +27,6 @@ function createEditor(opts) {
27
27
  segIndex,
28
28
  chipOffset
29
29
  });
30
- },
31
- onRemove: (chipEl) => {
32
- const { segIndex } = locateChip(chipEl);
33
- if (segIndex < 0) return;
34
- opts.onChipRemove?.({
35
- chipEl,
36
- segIndex
37
- });
38
30
  }
39
31
  });
40
32
  }
@@ -324,15 +316,6 @@ function createEditor(opts) {
324
316
  ]));
325
317
  render(a + variable.value.length + 2);
326
318
  }
327
- function removeVarAt(segIndex) {
328
- const target = doc[segIndex];
329
- if (!target || target.kind !== "var") return;
330
- const before = doc.slice(0, segIndex);
331
- const after = doc.slice(segIndex + 1);
332
- const caretOffset = sliceLength(before);
333
- emitChange(normalize([...before, ...after]));
334
- render(caretOffset);
335
- }
336
319
  function replaceVarAt(segIndex, variable) {
337
320
  const target = doc[segIndex];
338
321
  if (!target || target.kind !== "var") return;
@@ -406,7 +389,6 @@ function createEditor(opts) {
406
389
  getCaretOffset,
407
390
  setCaretOffset: setCaret,
408
391
  replaceRangeWithVar,
409
- removeVarAt,
410
392
  replaceVarAt,
411
393
  rerender: () => render(),
412
394
  dispose: () => {
@@ -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 /** 用户点击某个 chip 的 ×(删除) */\n onChipRemove?: (info: { chipEl: HTMLElement; segIndex: 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 removeVarAt(segIndex: number): 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 onRemove: (chipEl) => {\n const { segIndex } = locateChip(chipEl)\n if (segIndex < 0) return\n opts.onChipRemove?.({ chipEl, segIndex })\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 removeVarAt(segIndex: number) {\n const target = doc[segIndex]\n if (!target || target.kind !== 'var') return\n const before = doc.slice(0, segIndex)\n const after = doc.slice(segIndex + 1)\n const caretOffset = sliceLength(before)\n const next = normalize([...before, ...after])\n emitChange(next)\n render(caretOffset)\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 removeVarAt,\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 /** 选中区域 / 光标偏移变化时回调,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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"}
@@ -97,10 +97,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
97
97
  pickerFilter.value = "";
98
98
  pickerTriggerDom.value = chipEl;
99
99
  reselectingSegIdx.value = segIndex;
100
- },
101
- onChipRemove: ({ segIndex }) => {
102
- if (disabled.value || readonly.value) return;
103
- editor.removeVarAt(segIndex);
104
100
  }
105
101
  });
106
102
  editorRef.value = editor;
@@ -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 onChipRemove: ({ segIndex }) => {\n if (disabled.value || readonly.value) return\n editor.removeVarAt(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 :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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"}
@@ -93,35 +93,12 @@
93
93
  background-color: var(--u-expression-editor-chip-bg, var(--u-color-primary-light-7));
94
94
  box-shadow: 0 1px 4px var(--u-shadow-color);
95
95
  }
96
- .u-expression-editor__chip:hover .u-expression-editor__chip-close {
97
- opacity: 1;
98
- max-width: 14px;
99
- margin-left: 2px;
100
- }
101
96
  .u-expression-editor__chip.is-focused {
102
97
  box-shadow: 0 0 0 2px var(--u-color-primary-light-7);
103
98
  }
104
99
  .u-expression-editor__chip-label {
105
100
  pointer-events: none;
106
101
  }
107
- .u-expression-editor__chip-close {
108
- display: inline-flex;
109
- align-items: center;
110
- justify-content: center;
111
- width: 14px;
112
- max-width: 0;
113
- margin-left: 0;
114
- overflow: hidden;
115
- font-size: 14px;
116
- line-height: 1;
117
- color: var(--u-color-primary);
118
- opacity: 0;
119
- transition: opacity 0.12s ease, max-width 0.12s ease, margin-left 0.12s ease;
120
- cursor: pointer;
121
- }
122
- .u-expression-editor__chip-close:hover {
123
- transform: scale(1.1);
124
- }
125
102
  .u-expression-editor__picker {
126
103
  min-width: 220px;
127
104
  background-color: var(--u-bg-color-top);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/desktop",
3
- "version": "1.7.18",
3
+ "version": "1.7.19",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -154,19 +154,20 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
154
154
  unmount()
155
155
  })
156
156
 
157
- it('点击行不触发编辑,仅编辑按钮打开表单', async () => {
157
+ it('点击行打开表单,再次点击当前行关闭表单', async () => {
158
158
  const { host, model, unmount } = mountBatchEdit()
159
159
 
160
- await clickRow(host, 0)
161
-
162
160
  const aside = host.querySelector<HTMLElement>('.u-batch-edit__form')!
163
- expect(aside.style.display).toBe('none')
164
- expect(model.label).toBe('')
165
161
 
166
- await clickEditRow(host, 0)
162
+ await clickRow(host, 0)
167
163
  expect(aside.style.display).not.toBe('none')
168
164
  expect(model.label).toBe('存草稿')
169
165
 
166
+ // 再次点击当前行:取消选中并关闭表单,model 恢复初始值
167
+ await clickRow(host, 0)
168
+ expect(aside.style.display).toBe('none')
169
+ expect(model.label).toBe('')
170
+
170
171
  unmount()
171
172
  })
172
173
 
@@ -4,12 +4,14 @@
4
4
  :slots="slots"
5
5
  :class="[cls.e('table')]"
6
6
  :columns="columns"
7
+ highlight-current
7
8
  :current="state.row"
9
+ @update:current="handleUpdateCurrentRow"
8
10
  @update:checked="emit('update:checked', $event)"
9
11
  @update:selected="emit('update:selected', $event)"
10
12
  >
11
13
  <template #column:__action__="{ row }">
12
- <u-action-group :max="5" circle>
14
+ <u-action-group :max="5" circle @click.stop>
13
15
  <u-action
14
16
  v-if="allowed('update', row)"
15
17
  :icon="props.readonly ? View : EditPen"
@@ -95,6 +97,7 @@ const {
95
97
  staticFeatures,
96
98
  dynamicFeatures,
97
99
  startEdit,
100
+ handleClose,
98
101
  handleCreate,
99
102
  handleDelete,
100
103
  handleInsertToNext,
@@ -146,6 +149,20 @@ function canDelete(row: TableRow) {
146
149
  return !props.readonly && allowed('delete', row)
147
150
  }
148
151
 
152
+ /**
153
+ * 点击行打开编辑/查看表单;再次点击当前行取消选中并关闭表单。
154
+ * update 特性关闭时点击行不响应
155
+ */
156
+ function handleUpdateCurrentRow(row?: TableRow) {
157
+ if (!allowed('update', row)) return
158
+
159
+ if (row) {
160
+ startEdit(row)
161
+ } else {
162
+ handleClose()
163
+ }
164
+ }
165
+
149
166
  const showCreateBtn = computed(() => {
150
167
  if (props.readonly) return false
151
168
 
@@ -24,7 +24,7 @@ export const BatchEditDIKey: InjectionKey<
24
24
  focused: ShallowRef<boolean>
25
25
  /** 是否正在以编程方式重置/回显表单,此期间禁止 quick-edit 回写行数据 */
26
26
  syncing: ShallowRef<boolean>
27
- /** 开始编辑/查看一行(操作列「编辑」按钮触发) */
27
+ /** 开始编辑/查看一行(点击行或操作列「编辑」按钮触发) */
28
28
  startEdit: (row: TableRow) => void
29
29
  } & EditReturned
30
30
  > = Symbol('BatchEditDIKey')
@@ -81,7 +81,8 @@ $root-name: dialog;
81
81
  flex-grow: 1;
82
82
 
83
83
  // padding 加在滚动内容而非 body 本身:u-scroll 的 overflow 会裁切子元素 box-shadow
84
- @include m.bem(scroll, content) {
84
+ // 用子代链限定 body 自己的滚动内容,避免命中弹框内嵌套的 u-scroll
85
+ > #{fn.bem(scroll, container)} > #{fn.bem(scroll, content)} {
85
86
  padding: 8px;
86
87
  }
87
88
  }
@@ -14,7 +14,6 @@ const cls = {
14
14
  } as any
15
15
 
16
16
  function makeChip(overrides: Partial<{ value: string; label: string; type?: string }> = {}) {
17
- const onRemove = vi.fn<(el: HTMLElement) => void>()
18
17
  const onReselect = vi.fn<(el: HTMLElement) => void>()
19
18
  const segment = {
20
19
  kind: 'var' as const,
@@ -22,8 +21,8 @@ function makeChip(overrides: Partial<{ value: string; label: string; type?: stri
22
21
  label: overrides.label ?? '用户姓名',
23
22
  ...(overrides.type ? { type: overrides.type } : {})
24
23
  }
25
- const el = createChip(segment, { cls, onRemove, onReselect })
26
- return { el, onRemove, onReselect, segment }
24
+ const el = createChip(segment, { cls, onReselect })
25
+ return { el, onReselect, segment }
27
26
  }
28
27
 
29
28
  describe('createChip', () => {
@@ -54,21 +53,11 @@ describe('createChip', () => {
54
53
  })
55
54
 
56
55
  it('mousedown on chip body triggers onReselect', () => {
57
- const { el, onReselect, onRemove } = makeChip()
56
+ const { el, onReselect } = makeChip()
58
57
  const label = el.querySelector(`.${cls.e('chip-label')}`) as HTMLElement
59
58
  label.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
60
59
  expect(onReselect).toHaveBeenCalledTimes(1)
61
60
  expect(onReselect).toHaveBeenCalledWith(el)
62
- expect(onRemove).not.toHaveBeenCalled()
63
- })
64
-
65
- it('mousedown on close (×) triggers onRemove and not onReselect', () => {
66
- const { el, onReselect, onRemove } = makeChip()
67
- const close = el.querySelector(`[${ChipAttrs.close}]`) as HTMLElement
68
- close.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
69
- expect(onRemove).toHaveBeenCalledTimes(1)
70
- expect(onRemove).toHaveBeenCalledWith(el)
71
- expect(onReselect).not.toHaveBeenCalled()
72
61
  })
73
62
 
74
63
  it('readChipValue returns the bound value', () => {
@@ -8,18 +8,15 @@ export interface ChipDeps {
8
8
 
9
9
  export interface ChipFactoryOptions extends ChipDeps {
10
10
  /**
11
- * 当用户点击 chip 主体(不是 ×)时回调,由调用方决定如何打开重选面板。
11
+ * 当用户点击 chip 主体时回调,由调用方决定如何打开重选面板。
12
12
  * 收到的 chipEl 当前在 DOM 中的位置即为重选锚点。
13
13
  */
14
14
  onReselect: (chipEl: HTMLElement) => void
15
- /** 当用户点击 × 时回调,调用方应将该 chip 从模型 / DOM 中移除。 */
16
- onRemove: (chipEl: HTMLElement) => void
17
15
  }
18
16
 
19
17
  const CHIP_DATA_SEG = 'var'
20
18
  const CHIP_VALUE_ATTR = 'data-value'
21
19
  const CHIP_TYPE_ATTR = 'data-var-type'
22
- const CHIP_CLOSE_ATTR = 'data-chip-close'
23
20
 
24
21
  /** 判断一个 DOM 节点是否为 chip 元素(var 段渲染体) */
25
22
  export function isChipElement(node: Node | null): node is HTMLElement {
@@ -30,12 +27,6 @@ export function isChipElement(node: Node | null): node is HTMLElement {
30
27
  )
31
28
  }
32
29
 
33
- /** 判断点击目标是否落在 × 删除区。 */
34
- function isCloseTarget(target: EventTarget | null): boolean {
35
- if (!(target instanceof Element)) return false
36
- return !!target.closest(`[${CHIP_CLOSE_ATTR}]`)
37
- }
38
-
39
30
  /** 从给定 segment 创建一个 chip 元素(var 段渲染体)。 */
40
31
  export function createChip(
41
32
  segment: Extract<Segment, { kind: 'var' }>,
@@ -55,25 +46,13 @@ export function createChip(
55
46
  label.className = cls.e('chip-label')
56
47
  label.textContent = segment.type ? `${segment.label} (${segment.type})` : segment.label
57
48
 
58
- const close = document.createElement('span')
59
- close.className = cls.e('chip-close')
60
- close.setAttribute(CHIP_CLOSE_ATTR, 'true')
61
- close.setAttribute('aria-label', '删除')
62
- close.setAttribute('role', 'button')
63
- close.textContent = '×'
64
-
65
49
  chip.appendChild(label)
66
- chip.appendChild(close)
67
50
 
68
51
  chip.addEventListener('mousedown', (e) => {
69
52
  e.preventDefault()
70
53
  // 阻止冒泡:避免宿主弹框(如 dialog)的 mousedown 层级提升逻辑把 chip 打开的重选面板盖住
71
54
  e.stopPropagation()
72
- if (isCloseTarget(e.target)) {
73
- opts.onRemove(chip)
74
- } else {
75
- opts.onReselect(chip)
76
- }
55
+ opts.onReselect(chip)
77
56
  })
78
57
 
79
58
  return chip
@@ -95,9 +74,4 @@ export function setChipFocused(el: HTMLElement, focused: boolean): void {
95
74
  }
96
75
 
97
76
  /** 测试支持:暴露用于 DOM 选择器的属性常量。 */
98
- export const ChipAttrs = {
99
- seg: 'data-seg',
100
- value: CHIP_VALUE_ATTR,
101
- type: CHIP_TYPE_ATTR,
102
- close: CHIP_CLOSE_ATTR
103
- } as const
77
+ export const ChipAttrs = { seg: 'data-seg', value: CHIP_VALUE_ATTR, type: CHIP_TYPE_ATTR } as const
@@ -28,8 +28,6 @@ export interface EditorOptions {
28
28
  onSelectionChange?: (info: { offset: number | null }) => void
29
29
  /** 用户点击某个 chip 的主体(重选) */
30
30
  onChipReselect?: (info: { chipEl: HTMLElement; segIndex: number; chipOffset: number }) => void
31
- /** 用户点击某个 chip 的 ×(删除) */
32
- onChipRemove?: (info: { chipEl: HTMLElement; segIndex: number }) => void
33
31
  }
34
32
 
35
33
  export interface EditorAPI {
@@ -50,8 +48,6 @@ export interface EditorAPI {
50
48
  * 调用方通常在 mention 选中时用:start = `@` 起点、end = 当前光标。
51
49
  */
52
50
  replaceRangeWithVar(start: number, end: number, variable: EditorVariableInfo): void
53
- /** 移除指定段下标对应的 var chip(必须是 var 段)。 */
54
- removeVarAt(segIndex: number): void
55
51
  /** 替换指定段下标对应的 var chip 为新变量(必须是 var 段)。 */
56
52
  replaceVarAt(segIndex: number, variable: EditorVariableInfo): void
57
53
  /** 卸载所有事件监听(不会销毁 container DOM) */
@@ -85,11 +81,6 @@ export function createEditor(opts: EditorOptions): EditorAPI {
85
81
  const { segIndex, chipOffset } = locateChip(chipEl)
86
82
  if (segIndex < 0) return
87
83
  opts.onChipReselect?.({ chipEl, segIndex, chipOffset })
88
- },
89
- onRemove: (chipEl) => {
90
- const { segIndex } = locateChip(chipEl)
91
- if (segIndex < 0) return
92
- opts.onChipRemove?.({ chipEl, segIndex })
93
84
  }
94
85
  })
95
86
  }
@@ -412,17 +403,6 @@ export function createEditor(opts: EditorOptions): EditorAPI {
412
403
  render(a + variable.value.length + 2) // 光标定到 chip 之后
413
404
  }
414
405
 
415
- function removeVarAt(segIndex: number) {
416
- const target = doc[segIndex]
417
- if (!target || target.kind !== 'var') return
418
- const before = doc.slice(0, segIndex)
419
- const after = doc.slice(segIndex + 1)
420
- const caretOffset = sliceLength(before)
421
- const next = normalize([...before, ...after])
422
- emitChange(next)
423
- render(caretOffset)
424
- }
425
-
426
406
  function replaceVarAt(segIndex: number, variable: EditorVariableInfo) {
427
407
  const target = doc[segIndex]
428
408
  if (!target || target.kind !== 'var') return
@@ -499,7 +479,6 @@ export function createEditor(opts: EditorOptions): EditorAPI {
499
479
  getCaretOffset,
500
480
  setCaretOffset: setCaret,
501
481
  replaceRangeWithVar,
502
- removeVarAt,
503
482
  replaceVarAt,
504
483
  rerender: () => render(),
505
484
  dispose: () => {
@@ -135,10 +135,6 @@ onMounted(() => {
135
135
  pickerFilter.value = ''
136
136
  pickerTriggerDom.value = chipEl
137
137
  reselectingSegIdx.value = segIndex
138
- },
139
- onChipRemove: ({ segIndex }) => {
140
- if (disabled.value || readonly.value) return
141
- editor.removeVarAt(segIndex)
142
138
  }
143
139
  })
144
140
  editorRef.value = editor
@@ -105,12 +105,6 @@ $root-name: expression-editor;
105
105
  fn.use-var(color, primary, light-7)
106
106
  );
107
107
  box-shadow: 0 1px 4px fn.use-var(shadow, color);
108
-
109
- #{fn.bem($root-name, chip-close)} {
110
- opacity: 1;
111
- max-width: 14px;
112
- margin-left: 2px;
113
- }
114
108
  }
115
109
 
116
110
  &.is-focused {
@@ -122,29 +116,6 @@ $root-name: expression-editor;
122
116
  pointer-events: none;
123
117
  }
124
118
 
125
- @include m.e(chip-close) {
126
- display: inline-flex;
127
- align-items: center;
128
- justify-content: center;
129
- width: 14px;
130
- max-width: 0;
131
- margin-left: 0;
132
- overflow: hidden;
133
- font-size: 14px;
134
- line-height: 1;
135
- color: fn.use-var(color, primary);
136
- opacity: 0;
137
- transition:
138
- opacity 0.12s ease,
139
- max-width 0.12s ease,
140
- margin-left 0.12s ease;
141
- cursor: pointer;
142
-
143
- &:hover {
144
- transform: scale(1.1);
145
- }
146
- }
147
-
148
119
  // ─── 变量选择面板 ──────────────────────────────────────────
149
120
  @include m.e(picker) {
150
121
  min-width: 220px;