@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-node.js","names":[],"sources":["../../../src/components/rich-text-editor/image-node.ts"],"sourcesContent":["import {\n $insertNodes,\n DecoratorNode,\n type DOMConversionMap,\n type DOMConversionOutput,\n type DOMExportOutput,\n type LexicalEditor,\n type LexicalNode,\n type NodeKey,\n type SerializedLexicalNode,\n type Spread\n} from 'lexical'\n\nlet imageUid = 0\n\nfunction nextImageId(): string {\n return `rte-img-${++imageUid}`\n}\n\nexport interface SerializedImageNode extends Spread<\n { src: string; alt: string; id: string },\n SerializedLexicalNode\n> {}\n\n/**\n * 图片节点:插入时 src 为本地 objectURL、__file 持有原始文件,提交前由宿主调用\n * uploadImages 换成服务器地址。__file 不参与序列化,仅会话内有效。\n * 本仓库为手写 Vue 绑定(无 React 适配层),不走 decorate 渲染管道,\n * createDOM 直接返回 <img> 元素。\n */\nexport class ImageNode extends DecoratorNode<void> {\n __src: string\n __alt: string\n __id: string\n __file: File | null\n\n static override getType(): string {\n return 'u-image'\n }\n\n static override clone(node: ImageNode): ImageNode {\n return new ImageNode(node.__src, node.__alt, node.__id, node.__file, node.__key)\n }\n\n static override importJSON(serializedNode: SerializedImageNode): ImageNode {\n return $createImageNode({\n src: serializedNode.src,\n alt: serializedNode.alt,\n id: serializedNode.id\n })\n }\n\n constructor(src: string, alt = '', id = nextImageId(), file: File | null = null, key?: NodeKey) {\n super(key)\n this.__src = src\n this.__alt = alt\n this.__id = id\n this.__file = file\n }\n\n static override importDOM(): DOMConversionMap | null {\n return { img: () => ({ conversion: convertImageElement, priority: 0 }) }\n }\n\n override exportDOM(): DOMExportOutput {\n const element = document.createElement('img')\n element.setAttribute('src', this.__src)\n element.setAttribute('class', 'u-rte-image')\n element.setAttribute('alt', this.__alt)\n element.setAttribute('data-rte-image-id', this.__id)\n return { element }\n }\n\n override exportJSON(): SerializedImageNode {\n return { type: 'u-image', version: 1, src: this.__src, alt: this.__alt, id: this.__id }\n }\n\n override createDOM(): HTMLElement {\n const element = document.createElement('img')\n element.className = 'u-rte-image'\n element.src = this.__src\n element.alt = this.__alt\n element.draggable = false\n return element\n }\n\n override updateDOM(prevNode: ImageNode, element: HTMLElement): boolean {\n const img = element as HTMLImageElement\n if (prevNode.__src !== this.__src) img.src = this.__src\n if (prevNode.__alt !== this.__alt) img.alt = this.__alt\n return false\n }\n\n /** Vue 环境无 React 渲染管道,装饰内容即 createDOM 返回的元素本身 */\n override decorate(): null {\n return null\n }\n\n getSrc(): string {\n return this.__src\n }\n\n getFile(): File | null {\n return this.__file\n }\n\n /** 替换为服务器地址,同时清除待上传标记(已上传的节点不再重复上传) */\n setSrc(src: string): void {\n const self = this.getWritable()\n self.__src = src\n self.__file = null\n }\n}\n\nfunction convertImageElement(domNode: HTMLElement): DOMConversionOutput {\n const src = domNode.getAttribute('src')\n if (!src) return { node: null }\n return {\n node: $createImageNode({\n src,\n alt: domNode.getAttribute('alt') ?? '',\n id: domNode.getAttribute('data-rte-image-id') ?? undefined\n })\n }\n}\n\nexport function $createImageNode(options: {\n src: string\n alt?: string\n id?: string\n file?: File | null\n}): ImageNode {\n return new ImageNode(\n options.src,\n options.alt ?? '',\n options.id ?? nextImageId(),\n options.file ?? null\n )\n}\n\nexport function $isImageNode(node: LexicalNode | null | undefined): node is ImageNode {\n return node instanceof ImageNode\n}\n\n// 各编辑器实例创建的图片 objectURL,用于节点删除 / 组件卸载时回收\nconst objectUrls = new WeakMap<LexicalEditor, Set<string>>()\n\nfunction getImageObjectUrls(editor: LexicalEditor): Set<string> {\n let urls = objectUrls.get(editor)\n if (!urls) {\n urls = new Set()\n objectUrls.set(editor, urls)\n }\n return urls\n}\n\n/** 插入图片文件:objectURL 即时预览,File 挂在节点上等待提交时上传。返回插入的图片数量 */\nexport function insertImageFiles(editor: LexicalEditor, files: File[] | FileList): number {\n const images = Array.from(files).filter((file) => file.type.startsWith('image/'))\n if (!images.length) return 0\n\n const urls = getImageObjectUrls(editor)\n\n editor.update(() => {\n const nodes = images.map((file) => {\n const url = URL.createObjectURL(file)\n urls.add(url)\n return $createImageNode({ src: url, alt: file.name, file })\n })\n $insertNodes(nodes)\n })\n\n return images.length\n}\n\n/** 回收已从内容中移除的图片 objectURL,aliveUrls 为当前内容中仍存活的图片地址 */\nexport function gcImageObjectUrls(editor: LexicalEditor, aliveUrls: Set<string>): void {\n const urls = objectUrls.get(editor)\n if (!urls?.size) return\n urls.forEach((url) => {\n if (!aliveUrls.has(url)) {\n URL.revokeObjectURL(url)\n urls.delete(url)\n }\n })\n}\n\n/** 回收该编辑器创建的全部图片 objectURL(组件卸载时调用) */\nexport function revokeImageObjectUrls(editor: LexicalEditor): void {\n const urls = objectUrls.get(editor)\n urls?.forEach((url) => URL.revokeObjectURL(url))\n objectUrls.delete(editor)\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { $isImageNode, ImageNode, gcImageObjectUrls, insertImageFiles, revokeImageObjectUrls } from "./image-node.js";
1
2
  import _sfc_main$1 from "./toolbar.js";
2
3
  import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, mergeModels, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, shallowRef, toDisplayString, unref, useModel, useTemplateRef, watch } from "vue";
3
4
  import { useFormFallbackProps } from "@veltra/compositions";
@@ -7,7 +8,7 @@ import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html";
7
8
  import { AutoLinkNode, LinkNode } from "@lexical/link";
8
9
  import { ListItemNode, ListNode, registerList } from "@lexical/list";
9
10
  import { HeadingNode, QuoteNode, registerRichText } from "@lexical/rich-text";
10
- import { $createParagraphNode, $getRoot, createEditor } from "lexical";
11
+ import { $createParagraphNode, $createRangeSelection, $getNodeByKey, $getRoot, $nodesOfType, $setSelection, COMMAND_PRIORITY_HIGH, DROP_COMMAND, PASTE_COMMAND, createEditor } from "lexical";
11
12
  //#region src/components/rich-text-editor/rich-text-editor.vue
12
13
  const _hoisted_1 = ["contenteditable"];
13
14
  const LEXICAL_THEME = {
@@ -52,7 +53,9 @@ const DEFAULT_TOOLBAR = [
52
53
  "|",
53
54
  "blockquote",
54
55
  "|",
55
- "link"
56
+ "link",
57
+ "|",
58
+ "image"
56
59
  ];
57
60
  const _sfc_main = /*@__PURE__*/ defineComponent({
58
61
  __name: "rich-text-editor",
@@ -61,6 +64,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
61
64
  format: { default: "html" },
62
65
  toolbar: { default: () => DEFAULT_TOOLBAR },
63
66
  placeholder: { default: "" },
67
+ image: {
68
+ type: Boolean,
69
+ default: true
70
+ },
64
71
  tips: {},
65
72
  span: {},
66
73
  label: {},
@@ -80,7 +87,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
80
87
  "modelModifiers": {}
81
88
  }),
82
89
  emits: ["update:modelValue"],
83
- setup(__props) {
90
+ setup(__props, { expose: __expose }) {
84
91
  const props = __props;
85
92
  const model = useModel(__props, "modelValue");
86
93
  const cls = bem("rich-text-editor");
@@ -97,7 +104,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
97
104
  ];
98
105
  });
99
106
  const toolbarItems = computed(() => {
100
- return props.toolbar ?? DEFAULT_TOOLBAR;
107
+ const items = props.toolbar ?? DEFAULT_TOOLBAR;
108
+ return props.image ? items : items.filter((item) => item !== "image");
101
109
  });
102
110
  const editorContainer = useTemplateRef("editorContainer");
103
111
  const editor = shallowRef(null);
@@ -115,7 +123,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
115
123
  ListNode,
116
124
  ListItemNode,
117
125
  LinkNode,
118
- AutoLinkNode
126
+ AutoLinkNode,
127
+ ImageNode
119
128
  ],
120
129
  editable: !isDisabled.value && !isReadonly.value,
121
130
  onError: (error) => {
@@ -126,15 +135,39 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
126
135
  cleanupFns.push(registerRichText(editorInstance));
127
136
  cleanupFns.push(registerList(editorInstance));
128
137
  cleanupFns.push(registerHistory(editorInstance, createEmptyHistoryState(), 300));
138
+ const acceptsImage = () => props.image && !isDisabled.value && !isReadonly.value;
139
+ cleanupFns.push(editorInstance.registerCommand(PASTE_COMMAND, (event) => {
140
+ if (!acceptsImage()) return false;
141
+ if (insertImageFiles(editorInstance, event.clipboardData?.files ?? []) > 0) {
142
+ event.preventDefault();
143
+ return true;
144
+ }
145
+ return false;
146
+ }, COMMAND_PRIORITY_HIGH), editorInstance.registerCommand(DROP_COMMAND, (event) => {
147
+ if (!acceptsImage()) return false;
148
+ const files = event.dataTransfer?.files;
149
+ if (!files?.length) return false;
150
+ const range = document.caretRangeFromPoint(event.clientX, event.clientY);
151
+ if (range) editorInstance.update(() => {
152
+ const selection = $createRangeSelection();
153
+ selection.applyDOMRange(range);
154
+ $setSelection(selection);
155
+ });
156
+ if (insertImageFiles(editorInstance, files) > 0) {
157
+ event.preventDefault();
158
+ return true;
159
+ }
160
+ return false;
161
+ }, COMMAND_PRIORITY_HIGH));
129
162
  if (model.value) setEditorContent(editorInstance, model.value);
130
163
  cleanupFns.push(editorInstance.registerUpdateListener(({ editorState }) => {
131
164
  editorState.read(() => {
132
- const textContent = $getRoot().getTextContent();
133
- showPlaceholder.value = textContent.trim().length === 0;
165
+ const root = $getRoot();
166
+ const imageNodes = $nodesOfType(ImageNode);
167
+ showPlaceholder.value = root.getTextContent().trim().length === 0 && imageNodes.length === 0;
168
+ gcImageObjectUrls(editorInstance, new Set(imageNodes.map((node) => node.getSrc())));
134
169
  if (isComposing) return;
135
- let value;
136
- if (props.format === "json") value = JSON.stringify(editorState.toJSON());
137
- else value = $generateHtmlFromNodes(editorInstance);
170
+ const value = serializeContent(editorInstance);
138
171
  if (value !== model.value) {
139
172
  isComposing = true;
140
173
  model.value = value;
@@ -178,21 +211,45 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
178
211
  }
179
212
  });
180
213
  }
181
- watch(model, (newVal) => {
182
- if (!editor.value || isComposing) return;
214
+ function serializeContent(editorInstance) {
215
+ return editorInstance.read(() => props.format === "json" ? JSON.stringify(editorInstance.getEditorState().toJSON()) : $generateHtmlFromNodes(editorInstance));
216
+ }
217
+ let isUploadingImages = false;
218
+ async function uploadImages(upload) {
183
219
  const editorInstance = editor.value;
184
- editorInstance.read(() => {
185
- let currentValue;
186
- if (props.format === "json") currentValue = JSON.stringify(editorInstance.getEditorState().toJSON());
187
- else currentValue = $generateHtmlFromNodes(editorInstance);
188
- if (currentValue !== newVal) {
189
- isComposing = true;
190
- setEditorContent(editorInstance, newVal ?? "");
191
- queueMicrotask(() => {
192
- isComposing = false;
220
+ if (!editorInstance) return model.value ?? "";
221
+ if (isUploadingImages) throw new Error("图片正在上传中");
222
+ const pending = editorInstance.getEditorState().read(() => $nodesOfType(ImageNode).filter((node) => node.getFile()).map((node) => ({
223
+ key: node.getKey(),
224
+ file: node.getFile()
225
+ })));
226
+ if (pending.length) {
227
+ isUploadingImages = true;
228
+ try {
229
+ const urls = await Promise.all(pending.map(({ file }) => upload(file)));
230
+ editorInstance.update(() => {
231
+ pending.forEach(({ key }, index) => {
232
+ const node = $getNodeByKey(key);
233
+ if ($isImageNode(node)) node.setSrc(urls[index]);
234
+ });
193
235
  });
236
+ } finally {
237
+ isUploadingImages = false;
194
238
  }
195
- });
239
+ }
240
+ return serializeContent(editorInstance);
241
+ }
242
+ __expose({ uploadImages });
243
+ watch(model, (newVal) => {
244
+ if (!editor.value || isComposing) return;
245
+ const editorInstance = editor.value;
246
+ if (serializeContent(editorInstance) !== newVal) {
247
+ isComposing = true;
248
+ setEditorContent(editorInstance, newVal ?? "");
249
+ queueMicrotask(() => {
250
+ isComposing = false;
251
+ });
252
+ }
196
253
  });
197
254
  watch([isDisabled, isReadonly], () => {
198
255
  if (editor.value) editor.value.setEditable(!isDisabled.value && !isReadonly.value);
@@ -203,6 +260,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
203
260
  onBeforeUnmount(() => {
204
261
  cleanupFns.forEach((fn) => fn());
205
262
  cleanupFns = [];
263
+ if (editor.value) revokeImageObjectUrls(editor.value);
206
264
  editor.value = null;
207
265
  });
208
266
  return (_ctx, _cache) => {
@@ -1 +1 @@
1
- {"version":3,"file":"rich-text-editor.js","names":[],"sources":["../../../src/components/rich-text-editor/rich-text-editor.vue"],"sourcesContent":["<template>\n <div :class=\"className\">\n <toolbar\n v-if=\"!isReadonly && toolbarItems.length > 0\"\n :editor=\"editor\"\n :toolbar=\"toolbarItems\"\n :disabled=\"isDisabled\"\n />\n <div :class=\"cls.e('content')\">\n <div\n ref=\"editorContainer\"\n :class=\"cls.e('editable')\"\n :contenteditable=\"!isDisabled && !isReadonly\"\n ></div>\n <div v-if=\"showPlaceholder\" :class=\"cls.e('placeholder')\">\n {{ placeholder }}\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport type { ToolbarItem } from '../../types'\n\nconst LEXICAL_THEME = {\n paragraph: 'u-rte-paragraph',\n heading: {\n h1: 'u-rte-h1',\n h2: 'u-rte-h2',\n h3: 'u-rte-h3',\n h4: 'u-rte-h4',\n h5: 'u-rte-h5',\n h6: 'u-rte-h6'\n },\n text: {\n bold: 'u-rte-bold',\n italic: 'u-rte-italic',\n underline: 'u-rte-underline',\n strikethrough: 'u-rte-strikethrough'\n },\n list: {\n ul: 'u-rte-ul',\n ol: 'u-rte-ol',\n listitem: 'u-rte-li',\n nested: { listitem: 'u-rte-li--nested' }\n },\n quote: 'u-rte-blockquote',\n\n link: 'u-rte-link',\n image: 'u-rte-image'\n}\n\nconst DEFAULT_TOOLBAR: ToolbarItem[] = [\n 'undo',\n 'redo',\n '|',\n 'heading',\n '|',\n 'bold',\n 'italic',\n 'underline',\n 'strikethrough',\n '|',\n 'bullet-list',\n 'ordered-list',\n '|',\n 'blockquote',\n '|',\n 'link'\n]\n</script>\n\n<script lang=\"ts\" setup>\nimport { registerHistory, createEmptyHistoryState } from '@lexical/history'\nimport { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html'\nimport { LinkNode, AutoLinkNode } from '@lexical/link'\nimport { ListNode, ListItemNode, registerList } from '@lexical/list'\nimport { registerRichText } from '@lexical/rich-text'\nimport { HeadingNode, QuoteNode } from '@lexical/rich-text'\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { bem } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { createEditor, $getRoot, $createParagraphNode, type LexicalEditor } from 'lexical'\nimport { computed, onBeforeUnmount, onMounted, ref, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type { RichTextEditorProps } from '../../types'\nimport Toolbar from './toolbar.vue'\n\nconst props = withDefaults(defineProps<RichTextEditorProps>(), {\n disabled: undefined,\n readonly: undefined,\n format: 'html',\n toolbar: () => DEFAULT_TOOLBAR,\n placeholder: ''\n})\n\nconst model = defineModel<string>()\n\nconst cls = bem('rich-text-editor')\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst isDisabled = computed(() => !!disabled.value)\nconst isReadonly = computed(() => !!readonly.value)\n\nconst className = computed<string[]>(() => {\n return [\n cls.b,\n cls.m(size.value),\n bem.is('disabled', isDisabled.value),\n bem.is('readonly', isReadonly.value)\n ]\n})\n\nconst toolbarItems = computed<ToolbarItem[]>(() => {\n return props.toolbar ?? DEFAULT_TOOLBAR\n})\n\nconst editorContainer = useTemplateRef('editorContainer')\nconst editor = shallowRef<LexicalEditor | null>(null)\nconst showPlaceholder = ref(true)\nlet isComposing = false\nlet cleanupFns: (() => void)[] = []\n\nfunction initEditor() {\n if (!editorContainer.value) return\n\n const editorInstance = createEditor({\n namespace: 'URichTextEditor',\n theme: LEXICAL_THEME,\n nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, AutoLinkNode],\n editable: !isDisabled.value && !isReadonly.value,\n onError: (error: Error) => {\n console.error('Lexical error:', error)\n }\n })\n\n editorInstance.setRootElement(editorContainer.value)\n\n // Register plugins\n cleanupFns.push(registerRichText(editorInstance))\n cleanupFns.push(registerList(editorInstance))\n cleanupFns.push(registerHistory(editorInstance, createEmptyHistoryState(), 300))\n\n // Set initial value\n if (model.value) {\n setEditorContent(editorInstance, model.value)\n }\n\n // Listen for content changes\n cleanupFns.push(\n editorInstance.registerUpdateListener(({ editorState }) => {\n editorState.read(\n () => {\n const root = $getRoot()\n const textContent = root.getTextContent()\n showPlaceholder.value = textContent.trim().length === 0\n\n if (isComposing) return\n\n let value: string\n if (props.format === 'json') {\n value = JSON.stringify(editorState.toJSON())\n } else {\n value = $generateHtmlFromNodes(editorInstance)\n }\n\n // Avoid circular updates: only emit if content really changed\n if (value !== model.value) {\n isComposing = true\n model.value = value\n // Use microtask to reset after Vue processes the update\n queueMicrotask(() => {\n isComposing = false\n })\n }\n },\n { editor: editorInstance }\n )\n })\n )\n\n // Composing event listeners for IME\n const rootElement = editorInstance.getRootElement()\n if (rootElement) {\n const onCompositionStart = () => {\n isComposing = true\n }\n const onCompositionEnd = () => {\n isComposing = false\n }\n rootElement.addEventListener('compositionstart', onCompositionStart)\n rootElement.addEventListener('compositionend', onCompositionEnd)\n cleanupFns.push(() => {\n rootElement.removeEventListener('compositionstart', onCompositionStart)\n rootElement.removeEventListener('compositionend', onCompositionEnd)\n })\n }\n\n editor.value = editorInstance\n}\n\nfunction setEditorContent(editorInstance: LexicalEditor, value: string) {\n editorInstance.update(() => {\n const root = $getRoot()\n root.clear()\n\n if (props.format === 'json') {\n try {\n const parsedState = editorInstance.parseEditorState(value)\n editorInstance.setEditorState(parsedState)\n } catch {\n // If JSON parse fails, treat as empty\n root.append($createParagraphNode())\n }\n } else {\n // HTML format\n const parser = new DOMParser()\n const dom = parser.parseFromString(value, 'text/html')\n const nodes = $generateNodesFromDOM(editorInstance, dom)\n root.append(...nodes)\n }\n })\n}\n\n// Watch external model changes\nwatch(model, (newVal) => {\n if (!editor.value || isComposing) return\n\n const editorInstance = editor.value\n\n // Compare current content to avoid circular updates\n editorInstance.read(() => {\n let currentValue: string\n if (props.format === 'json') {\n currentValue = JSON.stringify(editorInstance.getEditorState().toJSON())\n } else {\n currentValue = $generateHtmlFromNodes(editorInstance)\n }\n\n if (currentValue !== newVal) {\n isComposing = true\n setEditorContent(editorInstance, newVal ?? '')\n queueMicrotask(() => {\n isComposing = false\n })\n }\n })\n})\n\n// Watch disabled/readonly changes\nwatch([isDisabled, isReadonly], () => {\n if (editor.value) {\n editor.value.setEditable(!isDisabled.value && !isReadonly.value)\n }\n})\n\nonMounted(() => {\n initEditor()\n})\n\nonBeforeUnmount(() => {\n cleanupFns.forEach((fn) => fn())\n cleanupFns = []\n editor.value = null\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"rich-text-editor.js","names":[],"sources":["../../../src/components/rich-text-editor/rich-text-editor.vue"],"sourcesContent":["<template>\n <div :class=\"className\">\n <toolbar\n v-if=\"!isReadonly && toolbarItems.length > 0\"\n :editor=\"editor\"\n :toolbar=\"toolbarItems\"\n :disabled=\"isDisabled\"\n />\n <div :class=\"cls.e('content')\">\n <div\n ref=\"editorContainer\"\n :class=\"cls.e('editable')\"\n :contenteditable=\"!isDisabled && !isReadonly\"\n ></div>\n <div v-if=\"showPlaceholder\" :class=\"cls.e('placeholder')\">\n {{ placeholder }}\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport type { ToolbarItem } from '../../types'\n\nconst LEXICAL_THEME = {\n paragraph: 'u-rte-paragraph',\n heading: {\n h1: 'u-rte-h1',\n h2: 'u-rte-h2',\n h3: 'u-rte-h3',\n h4: 'u-rte-h4',\n h5: 'u-rte-h5',\n h6: 'u-rte-h6'\n },\n text: {\n bold: 'u-rte-bold',\n italic: 'u-rte-italic',\n underline: 'u-rte-underline',\n strikethrough: 'u-rte-strikethrough'\n },\n list: {\n ul: 'u-rte-ul',\n ol: 'u-rte-ol',\n listitem: 'u-rte-li',\n nested: { listitem: 'u-rte-li--nested' }\n },\n quote: 'u-rte-blockquote',\n\n link: 'u-rte-link',\n image: 'u-rte-image'\n}\n\nconst DEFAULT_TOOLBAR: ToolbarItem[] = [\n 'undo',\n 'redo',\n '|',\n 'heading',\n '|',\n 'bold',\n 'italic',\n 'underline',\n 'strikethrough',\n '|',\n 'bullet-list',\n 'ordered-list',\n '|',\n 'blockquote',\n '|',\n 'link',\n '|',\n 'image'\n]\n</script>\n\n<script lang=\"ts\" setup>\nimport { registerHistory, createEmptyHistoryState } from '@lexical/history'\nimport { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html'\nimport { LinkNode, AutoLinkNode } from '@lexical/link'\nimport { ListNode, ListItemNode, registerList } from '@lexical/list'\nimport { registerRichText } from '@lexical/rich-text'\nimport { HeadingNode, QuoteNode } from '@lexical/rich-text'\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { bem } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport {\n $createRangeSelection,\n $getNodeByKey,\n $getRoot,\n $createParagraphNode,\n $nodesOfType,\n $setSelection,\n COMMAND_PRIORITY_HIGH,\n DROP_COMMAND,\n PASTE_COMMAND,\n createEditor,\n type LexicalEditor\n} from 'lexical'\nimport { computed, onBeforeUnmount, onMounted, ref, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type {\n RichTextEditorProps,\n RichTextImageUploader,\n _RichTextEditorExposed\n} from '../../types'\nimport {\n ImageNode,\n $isImageNode,\n gcImageObjectUrls,\n insertImageFiles,\n revokeImageObjectUrls\n} from './image-node'\nimport Toolbar from './toolbar.vue'\n\nconst props = withDefaults(defineProps<RichTextEditorProps>(), {\n disabled: undefined,\n readonly: undefined,\n format: 'html',\n toolbar: () => DEFAULT_TOOLBAR,\n placeholder: '',\n image: true\n})\n\nconst model = defineModel<string>()\n\nconst cls = bem('rich-text-editor')\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst isDisabled = computed(() => !!disabled.value)\nconst isReadonly = computed(() => !!readonly.value)\n\nconst className = computed<string[]>(() => {\n return [\n cls.b,\n cls.m(size.value),\n bem.is('disabled', isDisabled.value),\n bem.is('readonly', isReadonly.value)\n ]\n})\n\nconst toolbarItems = computed<ToolbarItem[]>(() => {\n const items = props.toolbar ?? DEFAULT_TOOLBAR\n return props.image ? items : items.filter((item) => item !== 'image')\n})\n\nconst editorContainer = useTemplateRef('editorContainer')\nconst editor = shallowRef<LexicalEditor | null>(null)\nconst showPlaceholder = ref(true)\nlet isComposing = false\nlet cleanupFns: (() => void)[] = []\n\nfunction initEditor() {\n if (!editorContainer.value) return\n\n const editorInstance = createEditor({\n namespace: 'URichTextEditor',\n theme: LEXICAL_THEME,\n nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, AutoLinkNode, ImageNode],\n editable: !isDisabled.value && !isReadonly.value,\n onError: (error: Error) => {\n console.error('Lexical error:', error)\n }\n })\n\n editorInstance.setRootElement(editorContainer.value)\n\n // Register plugins\n cleanupFns.push(registerRichText(editorInstance))\n cleanupFns.push(registerList(editorInstance))\n cleanupFns.push(registerHistory(editorInstance, createEmptyHistoryState(), 300))\n\n // Image input: paste & drop\n const acceptsImage = () => props.image && !isDisabled.value && !isReadonly.value\n\n cleanupFns.push(\n editorInstance.registerCommand(\n PASTE_COMMAND,\n (event) => {\n if (!acceptsImage()) return false\n if (insertImageFiles(editorInstance, event.clipboardData?.files ?? []) > 0) {\n event.preventDefault()\n return true\n }\n return false\n },\n COMMAND_PRIORITY_HIGH\n ),\n editorInstance.registerCommand(\n DROP_COMMAND,\n (event) => {\n if (!acceptsImage()) return false\n const files = event.dataTransfer?.files\n if (!files?.length) return false\n // 先把光标落到落点,再插入\n const range = document.caretRangeFromPoint(event.clientX, event.clientY)\n if (range) {\n editorInstance.update(() => {\n const selection = $createRangeSelection()\n selection.applyDOMRange(range)\n $setSelection(selection)\n })\n }\n if (insertImageFiles(editorInstance, files) > 0) {\n event.preventDefault()\n return true\n }\n return false\n },\n COMMAND_PRIORITY_HIGH\n )\n )\n\n // Set initial value\n if (model.value) {\n setEditorContent(editorInstance, model.value)\n }\n\n // Listen for content changes\n cleanupFns.push(\n editorInstance.registerUpdateListener(({ editorState }) => {\n editorState.read(\n () => {\n const root = $getRoot()\n const imageNodes = $nodesOfType(ImageNode)\n showPlaceholder.value =\n root.getTextContent().trim().length === 0 && imageNodes.length === 0\n\n // 回收已从内容中移除的图片 objectURL\n gcImageObjectUrls(editorInstance, new Set(imageNodes.map((node) => node.getSrc())))\n\n if (isComposing) return\n\n const value = serializeContent(editorInstance)\n\n // Avoid circular updates: only emit if content really changed\n if (value !== model.value) {\n isComposing = true\n model.value = value\n // Use microtask to reset after Vue processes the update\n queueMicrotask(() => {\n isComposing = false\n })\n }\n },\n { editor: editorInstance }\n )\n })\n )\n\n // Composing event listeners for IME\n const rootElement = editorInstance.getRootElement()\n if (rootElement) {\n const onCompositionStart = () => {\n isComposing = true\n }\n const onCompositionEnd = () => {\n isComposing = false\n }\n rootElement.addEventListener('compositionstart', onCompositionStart)\n rootElement.addEventListener('compositionend', onCompositionEnd)\n cleanupFns.push(() => {\n rootElement.removeEventListener('compositionstart', onCompositionStart)\n rootElement.removeEventListener('compositionend', onCompositionEnd)\n })\n }\n\n editor.value = editorInstance\n}\n\nfunction setEditorContent(editorInstance: LexicalEditor, value: string) {\n editorInstance.update(() => {\n const root = $getRoot()\n root.clear()\n\n if (props.format === 'json') {\n try {\n const parsedState = editorInstance.parseEditorState(value)\n editorInstance.setEditorState(parsedState)\n } catch {\n // If JSON parse fails, treat as empty\n root.append($createParagraphNode())\n }\n } else {\n // HTML format\n const parser = new DOMParser()\n const dom = parser.parseFromString(value, 'text/html')\n const nodes = $generateNodesFromDOM(editorInstance, dom)\n root.append(...nodes)\n }\n })\n}\n\nfunction serializeContent(editorInstance: LexicalEditor): string {\n return editorInstance.read(() =>\n props.format === 'json'\n ? JSON.stringify(editorInstance.getEditorState().toJSON())\n : $generateHtmlFromNodes(editorInstance)\n )\n}\n\nlet isUploadingImages = false\n\nasync function uploadImages(upload: RichTextImageUploader): Promise<string> {\n const editorInstance = editor.value\n if (!editorInstance) return model.value ?? ''\n if (isUploadingImages) throw new Error('图片正在上传中')\n\n const pending = editorInstance.getEditorState().read(() =>\n $nodesOfType(ImageNode)\n .filter((node) => node.getFile())\n .map((node) => ({ key: node.getKey(), file: node.getFile() as File }))\n )\n\n if (pending.length) {\n isUploadingImages = true\n try {\n const urls = await Promise.all(pending.map(({ file }) => upload(file)))\n editorInstance.update(() => {\n pending.forEach(({ key }, index) => {\n const node = $getNodeByKey(key)\n if ($isImageNode(node)) node.setSrc(urls[index])\n })\n })\n } finally {\n isUploadingImages = false\n }\n }\n\n return serializeContent(editorInstance)\n}\n\ndefineExpose<_RichTextEditorExposed>({ uploadImages })\n\n// Watch external model changes\nwatch(model, (newVal) => {\n if (!editor.value || isComposing) return\n\n const editorInstance = editor.value\n\n // Compare current content to avoid circular updates\n const currentValue = serializeContent(editorInstance)\n\n if (currentValue !== newVal) {\n isComposing = true\n setEditorContent(editorInstance, newVal ?? '')\n queueMicrotask(() => {\n isComposing = false\n })\n }\n})\n\n// Watch disabled/readonly changes\nwatch([isDisabled, isReadonly], () => {\n if (editor.value) {\n editor.value.setEditable(!isDisabled.value && !isReadonly.value)\n }\n})\n\nonMounted(() => {\n initEditor()\n})\n\nonBeforeUnmount(() => {\n cleanupFns.forEach((fn) => fn())\n cleanupFns = []\n if (editor.value) revokeImageObjectUrls(editor.value)\n editor.value = null\n})\n</script>\n"],"mappings":"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"}
@@ -336,3 +336,9 @@
336
336
  .u-rte-link:hover {
337
337
  text-decoration: none;
338
338
  }
339
+
340
+ .u-rte-image {
341
+ max-width: 100%;
342
+ height: auto;
343
+ vertical-align: bottom;
344
+ }
@@ -1,4 +1,5 @@
1
- import { Fragment, createElementBlock, createElementVNode, createStaticVNode, defineComponent, normalizeClass, onBeforeUnmount, openBlock, ref, renderList, unref, watch } from "vue";
1
+ import { insertImageFiles } from "./image-node.js";
2
+ import { Fragment, createElementBlock, createElementVNode, createStaticVNode, defineComponent, normalizeClass, onBeforeUnmount, openBlock, ref, renderList, unref, useTemplateRef, watch } from "vue";
2
3
  import { bem } from "@veltra/utils";
3
4
  import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
4
5
  import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode } from "@lexical/list";
@@ -24,6 +25,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
24
25
  setup(__props) {
25
26
  const props = __props;
26
27
  const cls = bem("rich-text-editor");
28
+ const imageInput = useTemplateRef("imageInput");
27
29
  const isBold = ref(false);
28
30
  const isItalic = ref(false);
29
31
  const isUnderline = ref(false);
@@ -119,6 +121,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
119
121
  case "link":
120
122
  toggleLink(editorInstance);
121
123
  break;
124
+ case "image":
125
+ imageInput.value?.click();
126
+ break;
122
127
  case "undo":
123
128
  editorInstance.dispatchCommand(UNDO_COMMAND, void 0);
124
129
  break;
@@ -126,6 +131,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
126
131
  }
127
132
  editorInstance.focus();
128
133
  }
134
+ function onImageChange(event) {
135
+ const input = event.target;
136
+ if (input.files?.length && props.editor && !props.disabled) insertImageFiles(props.editor, input.files);
137
+ input.value = "";
138
+ }
129
139
  function onHeadingChange(event) {
130
140
  const editorInstance = props.editor;
131
141
  if (!editorInstance || props.disabled) return;
@@ -166,6 +176,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
166
176
  blockquote: "引用",
167
177
  "code-block": "代码块",
168
178
  link: "链接",
179
+ image: "图片",
169
180
  undo: "撤销",
170
181
  redo: "重做"
171
182
  }[item] ?? item;
@@ -182,6 +193,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
182
193
  blockquote: "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z\"/><path d=\"M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3z\"/></svg>",
183
194
  "code-block": "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\"/><path d=\"m8 10-3 2 3 2\"/><path d=\"m16 10 3 2-3 2\"/><path d=\"m13 7-2 10\"/></svg>",
184
195
  link: "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>",
196
+ image: "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"/><circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\"/><polyline points=\"21 15 16 10 5 21\"/></svg>",
185
197
  undo: "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 7v6h6\"/><path d=\"M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13\"/></svg>",
186
198
  redo: "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 7v6h-6\"/><path d=\"M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13\"/></svg>"
187
199
  }[item] ?? "";
@@ -218,7 +230,15 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
218
230
  class: normalizeClass(unref(cls).e("toolbar-icon")),
219
231
  innerHTML: getIcon(item)
220
232
  }, null, 10, _hoisted_3)], 10, _hoisted_2))], 64);
221
- }), 128))], 2);
233
+ }), 128)), createElementVNode("input", {
234
+ ref_key: "imageInput",
235
+ ref: imageInput,
236
+ type: "file",
237
+ accept: "image/*",
238
+ multiple: "",
239
+ hidden: "",
240
+ onChange: onImageChange
241
+ }, null, 544)], 2);
222
242
  };
223
243
  }
224
244
  });
@@ -1 +1 @@
1
- {"version":3,"file":"toolbar.js","names":[],"sources":["../../../src/components/rich-text-editor/toolbar.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('toolbar')\">\n <template v-for=\"(item, index) in toolbar\" :key=\"index\">\n <div v-if=\"item === '|'\" :class=\"cls.e('separator')\"></div>\n\n <div v-else-if=\"item === 'heading'\" :class=\"[cls.e('toolbar-dropdown')]\">\n <select\n :class=\"[cls.e('heading-select'), { 'is-disabled': disabled }]\"\n :value=\"activeHeading\"\n :disabled=\"disabled\"\n @change=\"onHeadingChange\"\n >\n <option value=\"paragraph\">正文</option>\n <option value=\"h1\">Heading 1</option>\n <option value=\"h2\">Heading 2</option>\n <option value=\"h3\">Heading 3</option>\n <option value=\"h4\">Heading 4</option>\n <option value=\"h5\">Heading 5</option>\n <option value=\"h6\">Heading 6</option>\n </select>\n </div>\n\n <button\n v-else\n type=\"button\"\n :class=\"[\n cls.e('toolbar-btn'),\n { 'is-active': isActive(item) },\n { 'is-disabled': disabled }\n ]\"\n :disabled=\"disabled\"\n :title=\"getTitle(item)\"\n @click=\"onToolbarAction(item)\"\n >\n <span :class=\"cls.e('toolbar-icon')\" v-html=\"getIcon(item)\"></span>\n </button>\n </template>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'\nimport { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode } from '@lexical/list'\nimport { $createHeadingNode, $isHeadingNode, type HeadingTagType } from '@lexical/rich-text'\nimport { $isQuoteNode, $createQuoteNode } from '@lexical/rich-text'\nimport { $setBlocksType } from '@lexical/selection'\nimport { $getNearestNodeOfType } from '@lexical/utils'\nimport { bem } from '@veltra/utils'\nimport {\n $getSelection,\n $isRangeSelection,\n FORMAT_TEXT_COMMAND,\n UNDO_COMMAND,\n REDO_COMMAND,\n type LexicalEditor,\n $createParagraphNode,\n type RangeSelection\n} from 'lexical'\nimport { ref, onBeforeUnmount, watch } from 'vue'\n\nimport type { ToolbarItem } from '../../types'\n\nconst props = defineProps<{\n editor: LexicalEditor | null\n toolbar: ToolbarItem[]\n disabled: boolean\n}>()\n\nconst cls = bem('rich-text-editor')\n\n// Active states\nconst isBold = ref(false)\nconst isItalic = ref(false)\nconst isUnderline = ref(false)\nconst isStrikethrough = ref(false)\nconst isLink = ref(false)\nconst isBulletList = ref(false)\nconst isOrderedList = ref(false)\nconst isBlockquote = ref(false)\nconst activeHeading = ref('paragraph')\n\nlet cleanupListener: (() => void) | null = null\n\nfunction setupListener(editorInstance: LexicalEditor) {\n cleanupListener?.()\n cleanupListener = editorInstance.registerUpdateListener(({ editorState }) => {\n editorState.read(\n () => {\n const selection = $getSelection()\n if (!$isRangeSelection(selection)) return\n\n updateToolbarState(selection)\n },\n { editor: editorInstance }\n )\n })\n}\n\nfunction updateToolbarState(selection: RangeSelection) {\n isBold.value = selection.hasFormat('bold')\n isItalic.value = selection.hasFormat('italic')\n isUnderline.value = selection.hasFormat('underline')\n isStrikethrough.value = selection.hasFormat('strikethrough')\n\n // Check block type\n const anchorNode = selection.anchor.getNode()\n let element = anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow()\n\n if ($isHeadingNode(element)) {\n activeHeading.value = (element as any).getTag()\n } else {\n activeHeading.value = 'paragraph'\n }\n\n // Check list\n const parentList = $getNearestNodeOfType(anchorNode, ListNode)\n if (parentList) {\n const listType = (parentList as any).getListType()\n isBulletList.value = listType === 'bullet'\n isOrderedList.value = listType === 'number'\n } else {\n isBulletList.value = false\n isOrderedList.value = false\n }\n\n // Check blockquote\n isBlockquote.value = $isQuoteNode(element)\n\n // Check link\n const parent = anchorNode.getParent()\n isLink.value = $isLinkNode(parent)\n}\n\nwatch(\n () => props.editor,\n (editorInstance) => {\n if (editorInstance) {\n setupListener(editorInstance)\n }\n },\n { immediate: true }\n)\n\nfunction isActive(item: ToolbarItem): boolean {\n switch (item) {\n case 'bold':\n return isBold.value\n case 'italic':\n return isItalic.value\n case 'underline':\n return isUnderline.value\n case 'strikethrough':\n return isStrikethrough.value\n case 'bullet-list':\n return isBulletList.value\n case 'ordered-list':\n return isOrderedList.value\n case 'blockquote':\n return isBlockquote.value\n case 'link':\n return isLink.value\n default:\n return false\n }\n}\n\nfunction onToolbarAction(item: ToolbarItem) {\n const editorInstance = props.editor\n if (!editorInstance || props.disabled) return\n\n switch (item) {\n case 'bold':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')\n break\n case 'italic':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')\n break\n case 'underline':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')\n break\n case 'strikethrough':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')\n break\n case 'code':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'code')\n break\n case 'bullet-list':\n editorInstance.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)\n break\n case 'ordered-list':\n editorInstance.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined)\n break\n case 'blockquote':\n editorInstance.update(() => {\n const selection = $getSelection()\n if ($isRangeSelection(selection)) {\n if (isBlockquote.value) {\n $setBlocksType(selection, () => $createParagraphNode())\n } else {\n $setBlocksType(selection, () => $createQuoteNode())\n }\n }\n })\n break\n case 'link':\n toggleLink(editorInstance)\n break\n case 'undo':\n editorInstance.dispatchCommand(UNDO_COMMAND, undefined)\n break\n case 'redo':\n editorInstance.dispatchCommand(REDO_COMMAND, undefined)\n break\n }\n\n // Keep focus on editor\n editorInstance.focus()\n}\n\nfunction onHeadingChange(event: Event) {\n const editorInstance = props.editor\n if (!editorInstance || props.disabled) return\n\n const value = (event.target as HTMLSelectElement).value\n\n editorInstance.update(() => {\n const selection = $getSelection()\n if ($isRangeSelection(selection)) {\n if (value === 'paragraph') {\n $setBlocksType(selection, () => $createParagraphNode())\n } else {\n $setBlocksType(selection, () => $createHeadingNode(value as HeadingTagType))\n }\n }\n })\n\n editorInstance.focus()\n}\n\nfunction toggleLink(editorInstance: LexicalEditor) {\n let shouldRemoveLink = false\n\n editorInstance.read(() => {\n const selection = $getSelection()\n if (!$isRangeSelection(selection)) return\n\n const parent = selection.anchor.getNode().getParent()\n shouldRemoveLink = $isLinkNode(parent)\n })\n\n if (shouldRemoveLink) {\n editorInstance.dispatchCommand(TOGGLE_LINK_COMMAND, null)\n return\n }\n\n const url = prompt('请输入链接地址:', 'https://')\n if (url && url !== 'https://') {\n editorInstance.dispatchCommand(TOGGLE_LINK_COMMAND, url)\n }\n}\n\nfunction getTitle(item: ToolbarItem): string {\n const titles: Record<string, string> = {\n bold: '加粗',\n italic: '斜体',\n underline: '下划线',\n strikethrough: '删除线',\n code: '行内代码',\n 'bullet-list': '无序列表',\n 'ordered-list': '有序列表',\n blockquote: '引用',\n 'code-block': '代码块',\n link: '链接',\n undo: '撤销',\n redo: '重做'\n }\n return titles[item] ?? item\n}\n\nfunction getIcon(item: ToolbarItem): string {\n const icons: Record<string, string> = {\n bold: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/><path d=\"M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/></svg>',\n italic:\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"19\" y1=\"4\" x2=\"10\" y2=\"4\"/><line x1=\"14\" y1=\"20\" x2=\"5\" y2=\"20\"/><line x1=\"15\" y1=\"4\" x2=\"9\" y2=\"20\"/></svg>',\n underline:\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3\"/><line x1=\"4\" y1=\"21\" x2=\"20\" y2=\"21\"/></svg>',\n strikethrough:\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16 4H9a3 3 0 0 0-2.83 4\"/><path d=\"M14 12a4 4 0 0 1 0 8H6\"/><line x1=\"4\" y1=\"12\" x2=\"20\" y2=\"12\"/></svg>',\n code: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"16 18 22 12 16 6\"/><polyline points=\"8 6 2 12 8 18\"/></svg>',\n 'bullet-list':\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"8\" y1=\"6\" x2=\"21\" y2=\"6\"/><line x1=\"8\" y1=\"12\" x2=\"21\" y2=\"12\"/><line x1=\"8\" y1=\"18\" x2=\"21\" y2=\"18\"/><line x1=\"3\" y1=\"6\" x2=\"3.01\" y2=\"6\"/><line x1=\"3\" y1=\"12\" x2=\"3.01\" y2=\"12\"/><line x1=\"3\" y1=\"18\" x2=\"3.01\" y2=\"18\"/></svg>',\n 'ordered-list':\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"10\" y1=\"6\" x2=\"21\" y2=\"6\"/><line x1=\"10\" y1=\"12\" x2=\"21\" y2=\"12\"/><line x1=\"10\" y1=\"18\" x2=\"21\" y2=\"18\"/><path d=\"M4 6h1v4\"/><path d=\"M4 10h2\"/><path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"/></svg>',\n blockquote:\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z\"/><path d=\"M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3z\"/></svg>',\n 'code-block':\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\"/><path d=\"m8 10-3 2 3 2\"/><path d=\"m16 10 3 2-3 2\"/><path d=\"m13 7-2 10\"/></svg>',\n link: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n undo: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 7v6h6\"/><path d=\"M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13\"/></svg>',\n redo: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 7v6h-6\"/><path d=\"M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13\"/></svg>'\n }\n return icons[item] ?? ''\n}\n\nonBeforeUnmount(() => {\n cleanupListener?.()\n cleanupListener = null\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"toolbar.js","names":[],"sources":["../../../src/components/rich-text-editor/toolbar.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('toolbar')\">\n <template v-for=\"(item, index) in toolbar\" :key=\"index\">\n <div v-if=\"item === '|'\" :class=\"cls.e('separator')\"></div>\n\n <div v-else-if=\"item === 'heading'\" :class=\"[cls.e('toolbar-dropdown')]\">\n <select\n :class=\"[cls.e('heading-select'), { 'is-disabled': disabled }]\"\n :value=\"activeHeading\"\n :disabled=\"disabled\"\n @change=\"onHeadingChange\"\n >\n <option value=\"paragraph\">正文</option>\n <option value=\"h1\">Heading 1</option>\n <option value=\"h2\">Heading 2</option>\n <option value=\"h3\">Heading 3</option>\n <option value=\"h4\">Heading 4</option>\n <option value=\"h5\">Heading 5</option>\n <option value=\"h6\">Heading 6</option>\n </select>\n </div>\n\n <button\n v-else\n type=\"button\"\n :class=\"[\n cls.e('toolbar-btn'),\n { 'is-active': isActive(item) },\n { 'is-disabled': disabled }\n ]\"\n :disabled=\"disabled\"\n :title=\"getTitle(item)\"\n @click=\"onToolbarAction(item)\"\n >\n <span :class=\"cls.e('toolbar-icon')\" v-html=\"getIcon(item)\"></span>\n </button>\n </template>\n\n <input ref=\"imageInput\" type=\"file\" accept=\"image/*\" multiple hidden @change=\"onImageChange\" />\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'\nimport { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode } from '@lexical/list'\nimport { $createHeadingNode, $isHeadingNode, type HeadingTagType } from '@lexical/rich-text'\nimport { $isQuoteNode, $createQuoteNode } from '@lexical/rich-text'\nimport { $setBlocksType } from '@lexical/selection'\nimport { $getNearestNodeOfType } from '@lexical/utils'\nimport { bem } from '@veltra/utils'\nimport {\n $getSelection,\n $isRangeSelection,\n FORMAT_TEXT_COMMAND,\n UNDO_COMMAND,\n REDO_COMMAND,\n type LexicalEditor,\n $createParagraphNode,\n type RangeSelection\n} from 'lexical'\nimport { ref, onBeforeUnmount, useTemplateRef, watch } from 'vue'\n\nimport type { ToolbarItem } from '../../types'\nimport { insertImageFiles } from './image-node'\n\nconst props = defineProps<{\n editor: LexicalEditor | null\n toolbar: ToolbarItem[]\n disabled: boolean\n}>()\n\nconst cls = bem('rich-text-editor')\n\nconst imageInput = useTemplateRef('imageInput')\n\n// Active states\nconst isBold = ref(false)\nconst isItalic = ref(false)\nconst isUnderline = ref(false)\nconst isStrikethrough = ref(false)\nconst isLink = ref(false)\nconst isBulletList = ref(false)\nconst isOrderedList = ref(false)\nconst isBlockquote = ref(false)\nconst activeHeading = ref('paragraph')\n\nlet cleanupListener: (() => void) | null = null\n\nfunction setupListener(editorInstance: LexicalEditor) {\n cleanupListener?.()\n cleanupListener = editorInstance.registerUpdateListener(({ editorState }) => {\n editorState.read(\n () => {\n const selection = $getSelection()\n if (!$isRangeSelection(selection)) return\n\n updateToolbarState(selection)\n },\n { editor: editorInstance }\n )\n })\n}\n\nfunction updateToolbarState(selection: RangeSelection) {\n isBold.value = selection.hasFormat('bold')\n isItalic.value = selection.hasFormat('italic')\n isUnderline.value = selection.hasFormat('underline')\n isStrikethrough.value = selection.hasFormat('strikethrough')\n\n // Check block type\n const anchorNode = selection.anchor.getNode()\n let element = anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow()\n\n if ($isHeadingNode(element)) {\n activeHeading.value = (element as any).getTag()\n } else {\n activeHeading.value = 'paragraph'\n }\n\n // Check list\n const parentList = $getNearestNodeOfType(anchorNode, ListNode)\n if (parentList) {\n const listType = (parentList as any).getListType()\n isBulletList.value = listType === 'bullet'\n isOrderedList.value = listType === 'number'\n } else {\n isBulletList.value = false\n isOrderedList.value = false\n }\n\n // Check blockquote\n isBlockquote.value = $isQuoteNode(element)\n\n // Check link\n const parent = anchorNode.getParent()\n isLink.value = $isLinkNode(parent)\n}\n\nwatch(\n () => props.editor,\n (editorInstance) => {\n if (editorInstance) {\n setupListener(editorInstance)\n }\n },\n { immediate: true }\n)\n\nfunction isActive(item: ToolbarItem): boolean {\n switch (item) {\n case 'bold':\n return isBold.value\n case 'italic':\n return isItalic.value\n case 'underline':\n return isUnderline.value\n case 'strikethrough':\n return isStrikethrough.value\n case 'bullet-list':\n return isBulletList.value\n case 'ordered-list':\n return isOrderedList.value\n case 'blockquote':\n return isBlockquote.value\n case 'link':\n return isLink.value\n default:\n return false\n }\n}\n\nfunction onToolbarAction(item: ToolbarItem) {\n const editorInstance = props.editor\n if (!editorInstance || props.disabled) return\n\n switch (item) {\n case 'bold':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')\n break\n case 'italic':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')\n break\n case 'underline':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')\n break\n case 'strikethrough':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')\n break\n case 'code':\n editorInstance.dispatchCommand(FORMAT_TEXT_COMMAND, 'code')\n break\n case 'bullet-list':\n editorInstance.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)\n break\n case 'ordered-list':\n editorInstance.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined)\n break\n case 'blockquote':\n editorInstance.update(() => {\n const selection = $getSelection()\n if ($isRangeSelection(selection)) {\n if (isBlockquote.value) {\n $setBlocksType(selection, () => $createParagraphNode())\n } else {\n $setBlocksType(selection, () => $createQuoteNode())\n }\n }\n })\n break\n case 'link':\n toggleLink(editorInstance)\n break\n case 'image':\n imageInput.value?.click()\n break\n case 'undo':\n editorInstance.dispatchCommand(UNDO_COMMAND, undefined)\n break\n case 'redo':\n editorInstance.dispatchCommand(REDO_COMMAND, undefined)\n break\n }\n\n // Keep focus on editor\n editorInstance.focus()\n}\n\nfunction onImageChange(event: Event) {\n const input = event.target as HTMLInputElement\n if (input.files?.length && props.editor && !props.disabled) {\n insertImageFiles(props.editor, input.files)\n }\n // 允许重复选择同一文件\n input.value = ''\n}\n\nfunction onHeadingChange(event: Event) {\n const editorInstance = props.editor\n if (!editorInstance || props.disabled) return\n\n const value = (event.target as HTMLSelectElement).value\n\n editorInstance.update(() => {\n const selection = $getSelection()\n if ($isRangeSelection(selection)) {\n if (value === 'paragraph') {\n $setBlocksType(selection, () => $createParagraphNode())\n } else {\n $setBlocksType(selection, () => $createHeadingNode(value as HeadingTagType))\n }\n }\n })\n\n editorInstance.focus()\n}\n\nfunction toggleLink(editorInstance: LexicalEditor) {\n let shouldRemoveLink = false\n\n editorInstance.read(() => {\n const selection = $getSelection()\n if (!$isRangeSelection(selection)) return\n\n const parent = selection.anchor.getNode().getParent()\n shouldRemoveLink = $isLinkNode(parent)\n })\n\n if (shouldRemoveLink) {\n editorInstance.dispatchCommand(TOGGLE_LINK_COMMAND, null)\n return\n }\n\n const url = prompt('请输入链接地址:', 'https://')\n if (url && url !== 'https://') {\n editorInstance.dispatchCommand(TOGGLE_LINK_COMMAND, url)\n }\n}\n\nfunction getTitle(item: ToolbarItem): string {\n const titles: Record<string, string> = {\n bold: '加粗',\n italic: '斜体',\n underline: '下划线',\n strikethrough: '删除线',\n code: '行内代码',\n 'bullet-list': '无序列表',\n 'ordered-list': '有序列表',\n blockquote: '引用',\n 'code-block': '代码块',\n link: '链接',\n image: '图片',\n undo: '撤销',\n redo: '重做'\n }\n return titles[item] ?? item\n}\n\nfunction getIcon(item: ToolbarItem): string {\n const icons: Record<string, string> = {\n bold: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/><path d=\"M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/></svg>',\n italic:\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"19\" y1=\"4\" x2=\"10\" y2=\"4\"/><line x1=\"14\" y1=\"20\" x2=\"5\" y2=\"20\"/><line x1=\"15\" y1=\"4\" x2=\"9\" y2=\"20\"/></svg>',\n underline:\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3\"/><line x1=\"4\" y1=\"21\" x2=\"20\" y2=\"21\"/></svg>',\n strikethrough:\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16 4H9a3 3 0 0 0-2.83 4\"/><path d=\"M14 12a4 4 0 0 1 0 8H6\"/><line x1=\"4\" y1=\"12\" x2=\"20\" y2=\"12\"/></svg>',\n code: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"16 18 22 12 16 6\"/><polyline points=\"8 6 2 12 8 18\"/></svg>',\n 'bullet-list':\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"8\" y1=\"6\" x2=\"21\" y2=\"6\"/><line x1=\"8\" y1=\"12\" x2=\"21\" y2=\"12\"/><line x1=\"8\" y1=\"18\" x2=\"21\" y2=\"18\"/><line x1=\"3\" y1=\"6\" x2=\"3.01\" y2=\"6\"/><line x1=\"3\" y1=\"12\" x2=\"3.01\" y2=\"12\"/><line x1=\"3\" y1=\"18\" x2=\"3.01\" y2=\"18\"/></svg>',\n 'ordered-list':\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"10\" y1=\"6\" x2=\"21\" y2=\"6\"/><line x1=\"10\" y1=\"12\" x2=\"21\" y2=\"12\"/><line x1=\"10\" y1=\"18\" x2=\"21\" y2=\"18\"/><path d=\"M4 6h1v4\"/><path d=\"M4 10h2\"/><path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"/></svg>',\n blockquote:\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z\"/><path d=\"M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3z\"/></svg>',\n 'code-block':\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\"/><path d=\"m8 10-3 2 3 2\"/><path d=\"m16 10 3 2-3 2\"/><path d=\"m13 7-2 10\"/></svg>',\n link: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n image:\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"/><circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\"/><polyline points=\"21 15 16 10 5 21\"/></svg>',\n undo: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 7v6h6\"/><path d=\"M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13\"/></svg>',\n redo: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 7v6h-6\"/><path d=\"M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13\"/></svg>'\n }\n return icons[item] ?? ''\n}\n\nonBeforeUnmount(() => {\n cleanupListener?.()\n cleanupListener = null\n})\n</script>\n"],"mappings":"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"}
@@ -3,6 +3,7 @@ import _sfc_main$2 from "../empty/empty.js";
3
3
  import _sfc_main$3 from "../scroll/scroll.js";
4
4
  import _sfc_main$4 from "../dropdown/dropdown.js";
5
5
  import input_default from "../input/input.js";
6
+ import _sfc_main$5 from "../loading/loading.js";
6
7
  import { useKeyboard } from "./use-keyboard.js";
7
8
  import { useOptions } from "./use-options.js";
8
9
  import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createSlots, createTextVNode, createVNode, defineComponent, mergeProps, nextTick, normalizeClass, normalizeStyle, openBlock, renderList, renderSlot, shallowRef, toDisplayString, unref, useTemplateRef, watch, withCtx, withModifiers } from "vue";
@@ -85,7 +86,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
85
86
  const filterable = computed(() => {
86
87
  return props.filterable || typeof props.options === "function";
87
88
  });
88
- const { queryString, options, allOptions, temOptionsToCreatedOptions, clearCreatedOptions } = useOptions({ props });
89
+ const { queryString, loading, options, allOptions, temOptionsToCreatedOptions, clearCreatedOptions } = useOptions({ props });
89
90
  const dropdownVisible = shallowRef(false);
90
91
  /** 选中临时选项后待转正标记:面板关闭后再落定,避免关闭动画期间列表恢复全量导致面板变长 */
91
92
  let pendingTempPromote = false;
@@ -313,8 +314,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
313
314
  "onKeydown",
314
315
  "native-readonly"
315
316
  ])]),
316
- content: withCtx(() => [createCommentVNode("\n 单选列表。虚拟化启用时,内容容器 height 由 useVirtualizer 命令式写入;\n 此处 content-style 仅承担 grid 相关样式,避免 height 变化触发模板重渲染。\n "), unref(options).length ? (openBlock(), createBlock(unref(_sfc_main$3), {
317
+ content: withCtx(() => [createCommentVNode("\n 单选列表。虚拟化启用时,内容容器 height 由 useVirtualizer 命令式写入;\n 此处 content-style 仅承担 grid 相关样式,避免 height 变化触发模板重渲染。\n "), unref(loading) ? (openBlock(), createElementBlock("div", {
317
318
  key: 0,
319
+ class: normalizeClass(unref(cls).e("loading"))
320
+ }, [createVNode(unref(_sfc_main$5), { type: "dual-ring" })], 2)) : unref(options).length ? (openBlock(), createBlock(unref(_sfc_main$3), {
321
+ key: 1,
318
322
  tag: "ul",
319
323
  class: normalizeClass(unref(cls).e("options")),
320
324
  ref_key: "scrollRef",
@@ -360,7 +364,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
360
364
  "content-class",
361
365
  "content-style"
362
366
  ])) : (openBlock(), createElementBlock("div", {
363
- key: 1,
367
+ key: 2,
364
368
  class: normalizeClass(unref(cls).e("empty"))
365
369
  }, [createVNode(unref(_sfc_main$2))], 2))]),
366
370
  _: 3
@@ -1 +1 @@
1
- {"version":3,"file":"select.js","names":["$attrs","$slots"],"sources":["../../../src/components/select/select.vue"],"sourcesContent":["<template>\n <u-dropdown\n v-if=\"!readonly\"\n v-bind=\"$attrs\"\n trigger=\"click\"\n :class=\"[cls.b, bem.is('disabled', disabled)]\"\n ref=\"dropdownRef\"\n v-model:visible=\"dropdownVisible\"\n :content-class=\"[cls.e('panel'), cls.em('panel', size), contentClass]\"\n :content-style=\"contentStyle\"\n :disabled=\"disabled\"\n :min-width=\"minWidth\"\n :width=\"width\"\n @keydown=\"handleKeydown\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n >\n <!-- 触发 -->\n <template #trigger>\n <u-input\n ref=\"inputRef\"\n :size=\"size\"\n :disabled=\"disabled\"\n :placeholder=\"inputPlaceholder\"\n :clearable=\"false\"\n :model-value=\"inputValue\"\n @update:model-value=\"handleQueryInput\"\n @keydown=\"handleKeydown\"\n :native-readonly=\"!filterable || !querying\"\n @click.capture=\"handleTriggerClickCapture\"\n >\n <template #prefix v-if=\"$slots.prefix\">\n <slot name=\"prefix\" />\n </template>\n\n <template #suffix>\n <transition name=\"zoom-in\" mode=\"out-in\">\n <u-icon\n v-if=\"showClear\"\n :class=\"cls.e('clear')\"\n title=\"清除\"\n key=\"clear\"\n @click.stop=\"handleClear\"\n >\n <Close />\n </u-icon>\n <u-icon v-else :class=\"cls.e('arrow')\" key=\"arrow\">\n <ArrowDown />\n </u-icon>\n </transition>\n </template>\n </u-input>\n </template>\n\n <!-- 下拉内容 -->\n <template #content>\n <!--\n 单选列表。虚拟化启用时,内容容器 height 由 useVirtualizer 命令式写入;\n 此处 content-style 仅承担 grid 相关样式,避免 height 变化触发模板重渲染。\n -->\n <u-scroll\n v-if=\"options.length\"\n tag=\"ul\"\n :class=\"cls.e('options')\"\n ref=\"scrollRef\"\n :content-class=\"[\n cls.e('options-wrap'),\n bem.is('virtual', virtualEnabled),\n bem.is('grid', !!grid)\n ]\"\n :content-style=\"{\n gridTemplateColumns: grid ? `repeat(${grid.cols}, minmax(0px, 1fr))` : undefined,\n gridGap: grid ? withUnit(grid.gap, 'px') : undefined\n }\"\n >\n <template v-if=\"virtualEnabled\">\n <!-- @vue-ignore -->\n <li\n v-for=\"{ option, index, val, label, key, offset } of virtualOptions\"\n :class=\"[optionClass, bem.is('selected', index === currentIndex)]\"\n @click=\"handleSelect(option, index)\"\n :key=\"key\"\n :style=\"{ transform: `translateY(${offset}px)` }\"\n :data-index=\"index\"\n :ref=\"(el) => measureElement(index, el as Element | null)\"\n >\n <slot v-bind=\"{ option, index }\">\n {{ label }}\n </slot>\n </li>\n </template>\n\n <template v-else>\n <li\n v-for=\"(option, index) of options\"\n :class=\"[optionClass, bem.is('selected', index === currentIndex)]\"\n @click=\"handleSelect(option, index)\"\n :data-index=\"index\"\n :title=\"o(option).get(labelKey)\"\n :key=\"o(option).get(valueKey)\"\n >\n <slot v-bind=\"{ option, index }\">\n {{ o(option).get(labelKey) }}\n </slot>\n </li>\n </template>\n </u-scroll>\n\n <div v-else :class=\"cls.e('empty')\">\n <UEmpty />\n </div>\n </template>\n </u-dropdown>\n\n <template v-else>\n {{ displayedValue || FORM_EMPTY_CONTENT }}\n </template>\n</template>\n\n<script lang=\"ts\" setup>\nimport { o } from '@cat-kit/core'\nimport { useFormFallbackProps, useUserAction, useVirtualizer } from '@veltra/compositions'\nimport { ArrowDown, Close } from '@veltra/icons/normal'\nimport { bem, fieldKey, FORM_EMPTY_CONTENT, scrollIntoContainerView, withUnit } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type {\n SelectEmits,\n SelectProps,\n _SelectExposed,\n DropdownExposed,\n InputExposed,\n ScrollExposed\n} from '../../types'\nimport { UDropdown } from '../dropdown'\nimport { UEmpty } from '../empty'\nimport { UIcon } from '../icon'\nimport { UInput } from '../input'\nimport { UScroll } from '../scroll'\nimport { useKeyboard } from './use-keyboard'\nimport { useOptions } from './use-options'\n\ndefineOptions({ name: 'USelect', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<SelectProps>(), {\n labelKey: 'label',\n valueKey: 'value',\n placeholder: '请选择',\n clearable: true,\n disabled: undefined,\n readonly: undefined\n})\n\nconst emit = defineEmits<SelectEmits>()\n\ndefineSlots<{\n /** 前缀插槽 */\n prefix?: () => any\n /** 默认插槽 */\n default?: (scope: { option: Record<string, any>; index: number }) => any\n}>()\n\nconst cls = bem('select')\n\nconst labelKey = computed(() => fieldKey(props.labelKey, 'label'))\nconst valueKey = computed(() => fieldKey(props.valueKey, 'value'))\n\nconst optionClass = cls.e('option')\n\nconst { formProps } = injectFormContext()\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {\n size: 'default',\n disabled: false,\n readonly: false\n})\n\nconst currentIndex = shallowRef(-1)\n/** 内部展示文案,仅由选项推导;通过 update:text 单向通知父级 */\nconst label = shallowRef<string>()\nconst selected = shallowRef<Record<string, any>>()\n\nconst displayedValue = computed(() => {\n if (label.value) return label.value\n\n return selected.value ? o(selected.value).get(labelKey.value) : String(props.modelValue ?? '')\n})\n\nconst dropdownRef = shallowRef<DropdownExposed>()\nconst scrollRef = shallowRef<ScrollExposed>()\nconst inputRef = useTemplateRef<InputExposed>('inputRef')\n\nconst filterable = computed(() => {\n return props.filterable || typeof props.options === 'function'\n})\n\nconst { queryString, options, allOptions, temOptionsToCreatedOptions, clearCreatedOptions } =\n useOptions({ props })\n\nconst dropdownVisible = shallowRef(false)\n\n/** 选中临时选项后待转正标记:面板关闭后再落定,避免关闭动画期间列表恢复全量导致面板变长 */\nlet pendingTempPromote = false\n\n/**\n * 是否处于查询态:决定触发输入框显示查询串还是选中标签。\n * 与面板可见状态解耦 —— 选择后立即退出查询态,\n * 不等面板关闭动画结束(否则显示值会延迟一个动画时长才恢复)。\n */\nconst querying = shallowRef(false)\n\n/** 触发输入框的值:查询态下承载查询串,否则展示选中标签 */\nconst inputValue = computed(() => {\n if (filterable.value && querying.value) return queryString.value\n return displayedValue.value\n})\n\n/** 查询态下已选标签降级为占位提示 */\nconst inputPlaceholder = computed(() => {\n if (filterable.value && querying.value && displayedValue.value) {\n return displayedValue.value\n }\n return props.placeholder\n})\n\nfunction handleQueryInput(value: string) {\n if (!filterable.value) return\n queryString.value = value\n}\n\nconst hovered = shallowRef(false)\n\n/** 悬停且存在选中值时展示清除按钮(替代下拉箭头) */\nconst showClear = computed(() => {\n return props.clearable && !disabled.value && hovered.value && !!displayedValue.value\n})\n\n/**\n * 过滤模式下拦截输入区域的点击:\n * 阻止 dropdown 的 trigger 开合切换,保持面板展开以不中断输入。\n * 非输入区域(箭头、留白)放行,维持原开合行为。\n */\nfunction handleTriggerClickCapture(e: MouseEvent) {\n if (!filterable.value || disabled.value) return\n if (!(e.target instanceof HTMLInputElement)) return\n\n e.stopPropagation()\n if (!dropdownVisible.value) dropdownRef.value?.open()\n}\n\nconst { userAction, isUserActive } = useUserAction()\n\n/** 更新内部文案;值变化时单向 emit update:text */\nfunction setLabel(next?: string) {\n if (label.value === next) return\n label.value = next\n emit('update:text', next)\n}\n\n/** 按 modelValue 与完整选项列表同步高亮索引、选中项与显示标签(外部回显用,O(n)) */\nfunction syncSelected(modelValue: any, sourceOptions: Record<string, any>[] | undefined) {\n // 查询输入期间高亮第一项(创建模式下通常是临时项,便于回车创建)\n if (queryString.value) {\n currentIndex.value = options.value.length ? 0 : -1\n return\n }\n\n if (!sourceOptions?.length) return\n\n if (modelValue !== undefined && modelValue !== null && modelValue !== '') {\n currentIndex.value = sourceOptions.findIndex(\n (option) => o(option).get(valueKey.value) === modelValue\n )\n selected.value = currentIndex.value >= 0 ? sourceOptions[currentIndex.value] : undefined\n setLabel(selected.value ? o(selected.value).get(labelKey.value) : undefined)\n } else {\n currentIndex.value = -1\n selected.value = undefined\n setLabel(undefined)\n }\n}\n\n// 回显:以 allOptions 匹配,避免过滤列表缺项时清掉已选文案;用户动作期内跳过\nwatch(\n [() => props.modelValue, allOptions],\n ([modelValue]) => {\n if (isUserActive()) return\n syncSelected(modelValue, allOptions.value)\n },\n { immediate: true }\n)\n\n// 过滤时单独同步高亮(allOptions 不变,不会触发上方回显 watch)\nwatch([queryString, options], () => {\n if (!queryString.value) return\n currentIndex.value = options.value.length ? 0 : -1\n})\n\nconst baseVirtualEnabled = computed(() => options.value.length > 80)\nconst virtualEnabled = computed(() => baseVirtualEnabled.value && !props.grid)\n\nconst { virtualizer, items } = useVirtualizer({\n count: computed(() => options.value.length),\n scrollEl: () => scrollRef.value?.containerRef ?? null,\n contentEl: () => (virtualEnabled.value ? (scrollRef.value?.contentRef ?? null) : null),\n gap: 4,\n estimateSize: () => 40\n})\n\nconst virtualOptions = computed(() => {\n const _options = options.value\n return items.value.map((item) => {\n const option = _options[item.index]!\n return {\n option,\n index: item.index,\n label: o(option).get(labelKey.value),\n val: o(option).get(valueKey.value),\n key: item.index,\n offset: item.start\n }\n })\n})\n\nconst measureElement: (index: number, el: Element | null) => void = (index, el) =>\n virtualizer.measureElement(index, el)\n\nfunction scrollTo(index: number): void {\n virtualizer.scrollToIndex(index, { align: 'center' })\n}\n\nwatch([scrollRef, virtualEnabled], ([scroll, virtualEnabled]) => {\n if (!scroll || !props.modelValue) return\n\n if (virtualEnabled) {\n const index = options.value.findIndex((option) => option === selected.value)\n index !== -1 && dropdownVisible.value && nextTick(() => scrollTo(index))\n } else {\n const selectedEl = scroll?.contentRef?.getElementsByClassName('is-selected')[0]\n if (selectedEl) {\n scrollIntoContainerView(selectedEl as HTMLElement, scroll.containerRef ?? null)\n }\n }\n})\n\n/** 落定待转正的临时选项(面板 DOM 已卸载,列表恢复全量用户不可见) */\nfunction promotePendingTemp() {\n if (!pendingTempPromote) return\n pendingTempPromote = false\n temOptionsToCreatedOptions()\n}\n\nwatch(dropdownVisible, (visible) => {\n if (visible) {\n // 关闭动画期间被重新打开时补一次转正,保证已选临时项落到完整列表\n promotePendingTemp()\n // 面板展开后进入查询态并聚焦输入框,可以立即输入查询\n if (filterable.value) {\n queryString.value = ''\n querying.value = true\n nextTick(() => inputRef.value?.el?.focus())\n }\n } else {\n // 面板完全关闭(动画结束、DOM 卸载)后才清空查询串并转正,关闭动画期间列表保持过滤态\n querying.value = false\n queryString.value = ''\n promotePendingTemp()\n }\n})\n\n/** 单选:用户动作内 O(1) 写入值与文案,跳过 modelValue 回显的 O(n) 查找 */\nconst handleSelect = userAction((option: Record<string, any>, _index: number) => {\n const value = option?.[valueKey.value]\n selected.value = option\n\n emit('update:modelValue', value)\n setLabel(option?.[labelKey.value])\n emit('change', option)\n if (option.__isTemp) {\n // 转正延迟到面板关闭后:动画期间保持过滤列表,避免列表瞬间恢复全量导致面板变长闪烁\n pendingTempPromote = true\n }\n // 立即退出查询态,输入框同步恢复显示选中标签;查询串保留至面板关闭,避免关闭动画期间列表恢复全量\n querying.value = false\n // 按当前列表定位高亮(临时项尚未转正,仍在过滤列表中,通常为 0)\n currentIndex.value = options.value.findIndex((item) => o(item).get(valueKey.value) === value)\n if (currentIndex.value >= 0) {\n selected.value = options.value[currentIndex.value]\n }\n dropdownRef.value?.close()\n})\n\n/** 清除选项 */\nconst handleClear = userAction(() => {\n selected.value = undefined\n currentIndex.value = -1\n setLabel(undefined)\n clearCreatedOptions()\n emit('update:modelValue', undefined)\n emit('change', undefined)\n})\n\nfunction getCurrentEl() {\n return scrollRef.value?.contentRef?.querySelector('li.is-selected') as HTMLElement | undefined\n}\n\nconst { handleKeydown } = useKeyboard({\n options,\n currentIndex,\n selectOption: handleSelect,\n getCurrentEl\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"select.js","names":["$attrs","$slots"],"sources":["../../../src/components/select/select.vue"],"sourcesContent":["<template>\n <u-dropdown\n v-if=\"!readonly\"\n v-bind=\"$attrs\"\n trigger=\"click\"\n :class=\"[cls.b, bem.is('disabled', disabled)]\"\n ref=\"dropdownRef\"\n v-model:visible=\"dropdownVisible\"\n :content-class=\"[cls.e('panel'), cls.em('panel', size), contentClass]\"\n :content-style=\"contentStyle\"\n :disabled=\"disabled\"\n :min-width=\"minWidth\"\n :width=\"width\"\n @keydown=\"handleKeydown\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n >\n <!-- 触发 -->\n <template #trigger>\n <u-input\n ref=\"inputRef\"\n :size=\"size\"\n :disabled=\"disabled\"\n :placeholder=\"inputPlaceholder\"\n :clearable=\"false\"\n :model-value=\"inputValue\"\n @update:model-value=\"handleQueryInput\"\n @keydown=\"handleKeydown\"\n :native-readonly=\"!filterable || !querying\"\n @click.capture=\"handleTriggerClickCapture\"\n >\n <template #prefix v-if=\"$slots.prefix\">\n <slot name=\"prefix\" />\n </template>\n\n <template #suffix>\n <transition name=\"zoom-in\" mode=\"out-in\">\n <u-icon\n v-if=\"showClear\"\n :class=\"cls.e('clear')\"\n title=\"清除\"\n key=\"clear\"\n @click.stop=\"handleClear\"\n >\n <Close />\n </u-icon>\n <u-icon v-else :class=\"cls.e('arrow')\" key=\"arrow\">\n <ArrowDown />\n </u-icon>\n </transition>\n </template>\n </u-input>\n </template>\n\n <!-- 下拉内容 -->\n <template #content>\n <!--\n 单选列表。虚拟化启用时,内容容器 height 由 useVirtualizer 命令式写入;\n 此处 content-style 仅承担 grid 相关样式,避免 height 变化触发模板重渲染。\n -->\n <div v-if=\"loading\" :class=\"cls.e('loading')\">\n <ULoading type=\"dual-ring\" />\n </div>\n <u-scroll\n v-else-if=\"options.length\"\n tag=\"ul\"\n :class=\"cls.e('options')\"\n ref=\"scrollRef\"\n :content-class=\"[\n cls.e('options-wrap'),\n bem.is('virtual', virtualEnabled),\n bem.is('grid', !!grid)\n ]\"\n :content-style=\"{\n gridTemplateColumns: grid ? `repeat(${grid.cols}, minmax(0px, 1fr))` : undefined,\n gridGap: grid ? withUnit(grid.gap, 'px') : undefined\n }\"\n >\n <template v-if=\"virtualEnabled\">\n <!-- @vue-ignore -->\n <li\n v-for=\"{ option, index, val, label, key, offset } of virtualOptions\"\n :class=\"[optionClass, bem.is('selected', index === currentIndex)]\"\n @click=\"handleSelect(option, index)\"\n :key=\"key\"\n :style=\"{ transform: `translateY(${offset}px)` }\"\n :data-index=\"index\"\n :ref=\"(el) => measureElement(index, el as Element | null)\"\n >\n <slot v-bind=\"{ option, index }\">\n {{ label }}\n </slot>\n </li>\n </template>\n\n <template v-else>\n <li\n v-for=\"(option, index) of options\"\n :class=\"[optionClass, bem.is('selected', index === currentIndex)]\"\n @click=\"handleSelect(option, index)\"\n :data-index=\"index\"\n :title=\"o(option).get(labelKey)\"\n :key=\"o(option).get(valueKey)\"\n >\n <slot v-bind=\"{ option, index }\">\n {{ o(option).get(labelKey) }}\n </slot>\n </li>\n </template>\n </u-scroll>\n\n <div v-else :class=\"cls.e('empty')\">\n <UEmpty />\n </div>\n </template>\n </u-dropdown>\n\n <template v-else>\n {{ displayedValue || FORM_EMPTY_CONTENT }}\n </template>\n</template>\n\n<script lang=\"ts\" setup>\nimport { o } from '@cat-kit/core'\nimport { useFormFallbackProps, useUserAction, useVirtualizer } from '@veltra/compositions'\nimport { ArrowDown, Close } from '@veltra/icons/normal'\nimport { bem, fieldKey, FORM_EMPTY_CONTENT, scrollIntoContainerView, withUnit } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type {\n SelectEmits,\n SelectProps,\n _SelectExposed,\n DropdownExposed,\n InputExposed,\n ScrollExposed\n} from '../../types'\nimport { UDropdown } from '../dropdown'\nimport { UEmpty } from '../empty'\nimport { UIcon } from '../icon'\nimport { UInput } from '../input'\nimport { ULoading } from '../loading'\nimport { UScroll } from '../scroll'\nimport { useKeyboard } from './use-keyboard'\nimport { useOptions } from './use-options'\n\ndefineOptions({ name: 'USelect', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<SelectProps>(), {\n labelKey: 'label',\n valueKey: 'value',\n placeholder: '请选择',\n clearable: true,\n disabled: undefined,\n readonly: undefined\n})\n\nconst emit = defineEmits<SelectEmits>()\n\ndefineSlots<{\n /** 前缀插槽 */\n prefix?: () => any\n /** 默认插槽 */\n default?: (scope: { option: Record<string, any>; index: number }) => any\n}>()\n\nconst cls = bem('select')\n\nconst labelKey = computed(() => fieldKey(props.labelKey, 'label'))\nconst valueKey = computed(() => fieldKey(props.valueKey, 'value'))\n\nconst optionClass = cls.e('option')\n\nconst { formProps } = injectFormContext()\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {\n size: 'default',\n disabled: false,\n readonly: false\n})\n\nconst currentIndex = shallowRef(-1)\n/** 内部展示文案,仅由选项推导;通过 update:text 单向通知父级 */\nconst label = shallowRef<string>()\nconst selected = shallowRef<Record<string, any>>()\n\nconst displayedValue = computed(() => {\n if (label.value) return label.value\n\n return selected.value ? o(selected.value).get(labelKey.value) : String(props.modelValue ?? '')\n})\n\nconst dropdownRef = shallowRef<DropdownExposed>()\nconst scrollRef = shallowRef<ScrollExposed>()\nconst inputRef = useTemplateRef<InputExposed>('inputRef')\n\nconst filterable = computed(() => {\n return props.filterable || typeof props.options === 'function'\n})\n\nconst {\n queryString,\n loading,\n options,\n allOptions,\n temOptionsToCreatedOptions,\n clearCreatedOptions\n} = useOptions({ props })\n\nconst dropdownVisible = shallowRef(false)\n\n/** 选中临时选项后待转正标记:面板关闭后再落定,避免关闭动画期间列表恢复全量导致面板变长 */\nlet pendingTempPromote = false\n\n/**\n * 是否处于查询态:决定触发输入框显示查询串还是选中标签。\n * 与面板可见状态解耦 —— 选择后立即退出查询态,\n * 不等面板关闭动画结束(否则显示值会延迟一个动画时长才恢复)。\n */\nconst querying = shallowRef(false)\n\n/** 触发输入框的值:查询态下承载查询串,否则展示选中标签 */\nconst inputValue = computed(() => {\n if (filterable.value && querying.value) return queryString.value\n return displayedValue.value\n})\n\n/** 查询态下已选标签降级为占位提示 */\nconst inputPlaceholder = computed(() => {\n if (filterable.value && querying.value && displayedValue.value) {\n return displayedValue.value\n }\n return props.placeholder\n})\n\nfunction handleQueryInput(value: string) {\n if (!filterable.value) return\n queryString.value = value\n}\n\nconst hovered = shallowRef(false)\n\n/** 悬停且存在选中值时展示清除按钮(替代下拉箭头) */\nconst showClear = computed(() => {\n return props.clearable && !disabled.value && hovered.value && !!displayedValue.value\n})\n\n/**\n * 过滤模式下拦截输入区域的点击:\n * 阻止 dropdown 的 trigger 开合切换,保持面板展开以不中断输入。\n * 非输入区域(箭头、留白)放行,维持原开合行为。\n */\nfunction handleTriggerClickCapture(e: MouseEvent) {\n if (!filterable.value || disabled.value) return\n if (!(e.target instanceof HTMLInputElement)) return\n\n e.stopPropagation()\n if (!dropdownVisible.value) dropdownRef.value?.open()\n}\n\nconst { userAction, isUserActive } = useUserAction()\n\n/** 更新内部文案;值变化时单向 emit update:text */\nfunction setLabel(next?: string) {\n if (label.value === next) return\n label.value = next\n emit('update:text', next)\n}\n\n/** 按 modelValue 与完整选项列表同步高亮索引、选中项与显示标签(外部回显用,O(n)) */\nfunction syncSelected(modelValue: any, sourceOptions: Record<string, any>[] | undefined) {\n // 查询输入期间高亮第一项(创建模式下通常是临时项,便于回车创建)\n if (queryString.value) {\n currentIndex.value = options.value.length ? 0 : -1\n return\n }\n\n if (!sourceOptions?.length) return\n\n if (modelValue !== undefined && modelValue !== null && modelValue !== '') {\n currentIndex.value = sourceOptions.findIndex(\n (option) => o(option).get(valueKey.value) === modelValue\n )\n selected.value = currentIndex.value >= 0 ? sourceOptions[currentIndex.value] : undefined\n setLabel(selected.value ? o(selected.value).get(labelKey.value) : undefined)\n } else {\n currentIndex.value = -1\n selected.value = undefined\n setLabel(undefined)\n }\n}\n\n// 回显:以 allOptions 匹配,避免过滤列表缺项时清掉已选文案;用户动作期内跳过\nwatch(\n [() => props.modelValue, allOptions],\n ([modelValue]) => {\n if (isUserActive()) return\n syncSelected(modelValue, allOptions.value)\n },\n { immediate: true }\n)\n\n// 过滤时单独同步高亮(allOptions 不变,不会触发上方回显 watch)\nwatch([queryString, options], () => {\n if (!queryString.value) return\n currentIndex.value = options.value.length ? 0 : -1\n})\n\nconst baseVirtualEnabled = computed(() => options.value.length > 80)\nconst virtualEnabled = computed(() => baseVirtualEnabled.value && !props.grid)\n\nconst { virtualizer, items } = useVirtualizer({\n count: computed(() => options.value.length),\n scrollEl: () => scrollRef.value?.containerRef ?? null,\n contentEl: () => (virtualEnabled.value ? (scrollRef.value?.contentRef ?? null) : null),\n gap: 4,\n estimateSize: () => 40\n})\n\nconst virtualOptions = computed(() => {\n const _options = options.value\n return items.value.map((item) => {\n const option = _options[item.index]!\n return {\n option,\n index: item.index,\n label: o(option).get(labelKey.value),\n val: o(option).get(valueKey.value),\n key: item.index,\n offset: item.start\n }\n })\n})\n\nconst measureElement: (index: number, el: Element | null) => void = (index, el) =>\n virtualizer.measureElement(index, el)\n\nfunction scrollTo(index: number): void {\n virtualizer.scrollToIndex(index, { align: 'center' })\n}\n\nwatch([scrollRef, virtualEnabled], ([scroll, virtualEnabled]) => {\n if (!scroll || !props.modelValue) return\n\n if (virtualEnabled) {\n const index = options.value.findIndex((option) => option === selected.value)\n index !== -1 && dropdownVisible.value && nextTick(() => scrollTo(index))\n } else {\n const selectedEl = scroll?.contentRef?.getElementsByClassName('is-selected')[0]\n if (selectedEl) {\n scrollIntoContainerView(selectedEl as HTMLElement, scroll.containerRef ?? null)\n }\n }\n})\n\n/** 落定待转正的临时选项(面板 DOM 已卸载,列表恢复全量用户不可见) */\nfunction promotePendingTemp() {\n if (!pendingTempPromote) return\n pendingTempPromote = false\n temOptionsToCreatedOptions()\n}\n\nwatch(dropdownVisible, (visible) => {\n if (visible) {\n // 关闭动画期间被重新打开时补一次转正,保证已选临时项落到完整列表\n promotePendingTemp()\n // 面板展开后进入查询态并聚焦输入框,可以立即输入查询\n if (filterable.value) {\n queryString.value = ''\n querying.value = true\n nextTick(() => inputRef.value?.el?.focus())\n }\n } else {\n // 面板完全关闭(动画结束、DOM 卸载)后才清空查询串并转正,关闭动画期间列表保持过滤态\n querying.value = false\n queryString.value = ''\n promotePendingTemp()\n }\n})\n\n/** 单选:用户动作内 O(1) 写入值与文案,跳过 modelValue 回显的 O(n) 查找 */\nconst handleSelect = userAction((option: Record<string, any>, _index: number) => {\n const value = option?.[valueKey.value]\n selected.value = option\n\n emit('update:modelValue', value)\n setLabel(option?.[labelKey.value])\n emit('change', option)\n if (option.__isTemp) {\n // 转正延迟到面板关闭后:动画期间保持过滤列表,避免列表瞬间恢复全量导致面板变长闪烁\n pendingTempPromote = true\n }\n // 立即退出查询态,输入框同步恢复显示选中标签;查询串保留至面板关闭,避免关闭动画期间列表恢复全量\n querying.value = false\n // 按当前列表定位高亮(临时项尚未转正,仍在过滤列表中,通常为 0)\n currentIndex.value = options.value.findIndex((item) => o(item).get(valueKey.value) === value)\n if (currentIndex.value >= 0) {\n selected.value = options.value[currentIndex.value]\n }\n dropdownRef.value?.close()\n})\n\n/** 清除选项 */\nconst handleClear = userAction(() => {\n selected.value = undefined\n currentIndex.value = -1\n setLabel(undefined)\n clearCreatedOptions()\n emit('update:modelValue', undefined)\n emit('change', undefined)\n})\n\nfunction getCurrentEl() {\n return scrollRef.value?.contentRef?.querySelector('li.is-selected') as HTMLElement | undefined\n}\n\nconst { handleKeydown } = useKeyboard({\n options,\n currentIndex,\n selectOption: handleSelect,\n getCurrentEl\n})\n</script>\n"],"mappings":"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"}
@@ -119,3 +119,10 @@
119
119
  text-align: center;
120
120
  padding: var(--u-gap-small) 0;
121
121
  }
122
+ .u-select__loading {
123
+ display: flex;
124
+ justify-content: center;
125
+ align-items: center;
126
+ flex-wrap: nowrap;
127
+ min-height: 240px;
128
+ }