@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.
- package/dist/components/batch-edit/batch-edit-form.js +40 -26
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/use-handlers.js +3 -3
- package/dist/components/batch-edit/use-handlers.js.map +1 -1
- package/dist/components/batch-edit/use-shortcut-key.js +4 -2
- package/dist/components/batch-edit/use-shortcut-key.js.map +1 -1
- package/dist/components/card/style2.css +0 -2
- package/dist/components/condition-editor/condition-editor.js +8 -2
- package/dist/components/condition-editor/condition-editor.js.map +1 -1
- package/dist/components/dropdown/dropdown.js +5 -3
- package/dist/components/dropdown/dropdown.js.map +1 -1
- package/dist/components/expression-editor/components/path-preview.js +3 -6
- package/dist/components/expression-editor/components/path-preview.js.map +1 -1
- package/dist/components/expression-editor/components/variable-picker.js +49 -12
- package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
- package/dist/components/expression-editor/core/editor.js +42 -0
- package/dist/components/expression-editor/core/editor.js.map +1 -1
- package/dist/components/expression-editor/expression-editor.js +68 -22
- package/dist/components/expression-editor/expression-editor.js.map +1 -1
- package/dist/components/expression-editor/style2.css +135 -73
- package/dist/components/form/form.js.map +1 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +0 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/style2.css +8 -3
- package/dist/components/form-item/style2.css +14 -0
- package/dist/components/index.d.ts +27 -26
- package/dist/components/index.js +54 -52
- package/dist/components/index.js.map +1 -1
- package/dist/components/kanban/di.js +7 -0
- package/dist/components/kanban/di.js.map +1 -0
- package/dist/components/kanban/kanban-column.js +67 -0
- package/dist/components/kanban/kanban-column.js.map +1 -0
- package/dist/components/kanban/kanban.js +89 -0
- package/dist/components/kanban/kanban.js.map +1 -0
- package/dist/components/kanban/style.d.ts +1 -0
- package/dist/components/kanban/style.js +1 -0
- package/dist/components/kanban/style2.css +128 -0
- package/dist/components/multi-select/multi-select.js +16 -12
- package/dist/components/multi-select/multi-select.js.map +1 -1
- package/dist/components/multi-select/style2.css +7 -0
- package/dist/components/multi-tree-select/multi-tree-select.js +82 -56
- package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
- package/dist/components/multi-tree-select/style2.css +7 -0
- package/dist/components/rich-text-editor/image-node.js +154 -0
- package/dist/components/rich-text-editor/image-node.js.map +1 -0
- package/dist/components/rich-text-editor/rich-text-editor.js +80 -22
- package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
- package/dist/components/rich-text-editor/style2.css +6 -0
- package/dist/components/rich-text-editor/toolbar.js +22 -2
- package/dist/components/rich-text-editor/toolbar.js.map +1 -1
- package/dist/components/select/select.js +7 -3
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/select/style2.css +7 -0
- package/dist/components/select/use-options.js +14 -3
- package/dist/components/select/use-options.js.map +1 -1
- package/dist/components/tree-select/style2.css +7 -0
- package/dist/components/tree-select/tree-select.js +49 -17
- package/dist/components/tree-select/tree-select.js.map +1 -1
- package/dist/index.d.ts +48 -46
- package/dist/index.js +47 -46
- package/dist/types/dropdown.d.ts +10 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/kanban.d.ts +69 -0
- package/dist/types/multi-tree-select.d.ts +7 -1
- package/dist/types/rich-text-editor.d.ts +13 -2
- package/dist/types/tree-select.d.ts +7 -1
- package/package.json +1 -1
- package/src/components/batch-edit/__test__/batch-edit.test.ts +112 -1
- package/src/components/batch-edit/batch-edit-form.vue +21 -2
- package/src/components/batch-edit/use-handlers.ts +6 -5
- package/src/components/batch-edit/use-shortcut-key.ts +5 -2
- package/src/components/card/style.scss +0 -2
- package/src/components/condition-editor/__test__/condition-editor.test.ts +61 -0
- package/src/components/condition-editor/condition-editor.vue +6 -1
- package/src/components/dropdown/dropdown.vue +9 -4
- package/src/components/expression-editor/__test__/editor.test.ts +66 -0
- package/src/components/expression-editor/__test__/expression-editor.test.ts +124 -0
- package/src/components/expression-editor/components/path-preview.vue +1 -2
- package/src/components/expression-editor/components/variable-picker.vue +54 -13
- package/src/components/expression-editor/core/editor.ts +61 -0
- package/src/components/expression-editor/expression-editor.vue +58 -20
- package/src/components/expression-editor/style.scss +160 -76
- package/src/components/form/form.vue +0 -1
- package/src/components/form/style.scss +11 -4
- package/src/components/form-item/style.scss +16 -1
- package/src/components/index.ts +2 -0
- package/src/components/kanban/__test__/kanban.test.ts +212 -0
- package/src/components/kanban/di.ts +19 -0
- package/src/components/kanban/index.ts +1 -0
- package/src/components/kanban/kanban-column.vue +77 -0
- package/src/components/kanban/kanban.vue +81 -0
- package/src/components/kanban/style.scss +124 -0
- package/src/components/kanban/style.ts +1 -0
- package/src/components/multi-select/multi-select.vue +11 -2
- package/src/components/multi-select/style.scss +6 -0
- package/src/components/multi-tree-select/multi-tree-select.vue +58 -9
- package/src/components/multi-tree-select/style.scss +6 -0
- package/src/components/rich-text-editor/image-node.ts +193 -0
- package/src/components/rich-text-editor/rich-text-editor.vue +131 -29
- package/src/components/rich-text-editor/style.scss +7 -0
- package/src/components/rich-text-editor/toolbar.vue +21 -1
- package/src/components/select/__test__/select.test.ts +78 -0
- package/src/components/select/select.vue +13 -3
- package/src/components/select/style.scss +6 -0
- package/src/components/select/use-options.ts +25 -4
- package/src/components/tree-select/__test__/tree-select.test.ts +116 -0
- package/src/components/tree-select/style.scss +6 -0
- package/src/components/tree-select/tree-select.vue +63 -13
- package/src/types/dropdown.ts +10 -0
- package/src/types/index.ts +1 -0
- package/src/types/kanban.ts +63 -0
- package/src/types/multi-tree-select.ts +12 -1
- package/src/types/rich-text-editor.ts +14 -1
- 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
|
-
|
|
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
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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))
|
|
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(
|
|
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:
|
|
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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"}
|