@veltra/desktop 1.8.0 → 1.8.2

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 (56) hide show
  1. package/dist/components/condition-editor/condition-editor.js +8 -2
  2. package/dist/components/condition-editor/condition-editor.js.map +1 -1
  3. package/dist/components/expression-editor/core/editor.js +22 -0
  4. package/dist/components/expression-editor/core/editor.js.map +1 -1
  5. package/dist/components/expression-editor/expression-editor.js +17 -6
  6. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  7. package/dist/components/expression-editor/style2.css +2 -2
  8. package/dist/components/form-item/style2.css +14 -0
  9. package/dist/components/index.d.ts +2 -1
  10. package/dist/components/index.js +3 -1
  11. package/dist/components/index.js.map +1 -1
  12. package/dist/components/kanban/di.js +7 -0
  13. package/dist/components/kanban/di.js.map +1 -0
  14. package/dist/components/kanban/kanban-column.js +67 -0
  15. package/dist/components/kanban/kanban-column.js.map +1 -0
  16. package/dist/components/kanban/kanban.js +89 -0
  17. package/dist/components/kanban/kanban.js.map +1 -0
  18. package/dist/components/kanban/style.d.ts +1 -0
  19. package/dist/components/kanban/style.js +1 -0
  20. package/dist/components/kanban/style2.css +128 -0
  21. package/dist/components/rich-text-editor/image-node.js +154 -0
  22. package/dist/components/rich-text-editor/image-node.js.map +1 -0
  23. package/dist/components/rich-text-editor/rich-text-editor.js +80 -22
  24. package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
  25. package/dist/components/rich-text-editor/style2.css +6 -0
  26. package/dist/components/rich-text-editor/toolbar.js +22 -2
  27. package/dist/components/rich-text-editor/toolbar.js.map +1 -1
  28. package/dist/index.d.ts +48 -46
  29. package/dist/index.js +46 -45
  30. package/dist/types/index.d.ts +3 -2
  31. package/dist/types/kanban.d.ts +69 -0
  32. package/dist/types/rich-text-editor.d.ts +13 -2
  33. package/package.json +1 -1
  34. package/src/components/condition-editor/__test__/condition-editor.test.ts +61 -0
  35. package/src/components/condition-editor/condition-editor.vue +6 -1
  36. package/src/components/expression-editor/__test__/editor.test.ts +66 -0
  37. package/src/components/expression-editor/__test__/expression-editor.test.ts +124 -0
  38. package/src/components/expression-editor/core/editor.ts +33 -0
  39. package/src/components/expression-editor/expression-editor.vue +13 -5
  40. package/src/components/expression-editor/style.scss +8 -9
  41. package/src/components/form-item/style.scss +16 -1
  42. package/src/components/index.ts +2 -0
  43. package/src/components/kanban/__test__/kanban.test.ts +212 -0
  44. package/src/components/kanban/di.ts +19 -0
  45. package/src/components/kanban/index.ts +1 -0
  46. package/src/components/kanban/kanban-column.vue +77 -0
  47. package/src/components/kanban/kanban.vue +81 -0
  48. package/src/components/kanban/style.scss +124 -0
  49. package/src/components/kanban/style.ts +1 -0
  50. package/src/components/rich-text-editor/image-node.ts +193 -0
  51. package/src/components/rich-text-editor/rich-text-editor.vue +131 -29
  52. package/src/components/rich-text-editor/style.scss +7 -0
  53. package/src/components/rich-text-editor/toolbar.vue +21 -1
  54. package/src/types/index.ts +1 -0
  55. package/src/types/kanban.ts +63 -0
  56. package/src/types/rich-text-editor.ts +14 -1
@@ -0,0 +1,89 @@
1
+ import { KanbanDIKey } from "./di.js";
2
+ import _sfc_main$1 from "./kanban-column.js";
3
+ import { Fragment, createBlock, createElementBlock, defineComponent, mergeProps, openBlock, provide, ref, renderList, renderSlot, unref, watch, withCtx } from "vue";
4
+ import { bem } from "@veltra/utils";
5
+ //#region src/components/kanban/kanban.vue
6
+ const _sfc_main = /*@__PURE__*/ defineComponent({
7
+ name: "UKanban",
8
+ inheritAttrs: false,
9
+ __name: "kanban",
10
+ props: {
11
+ columns: {},
12
+ cardKey: { default: "id" },
13
+ titleKey: { default: "title" },
14
+ disabled: {
15
+ type: Boolean,
16
+ default: false
17
+ },
18
+ countable: {
19
+ type: Boolean,
20
+ default: true
21
+ },
22
+ placeholder: { default: "暂无内容" }
23
+ },
24
+ emits: ["update:columns", "change"],
25
+ setup(__props, { emit: __emit }) {
26
+ const props = __props;
27
+ const emit = __emit;
28
+ const cls = bem("kanban");
29
+ const localColumns = ref([]);
30
+ watch(() => props.columns, (columns) => {
31
+ localColumns.value = (columns ?? []).map((column) => ({
32
+ ...column,
33
+ items: [...column.items]
34
+ }));
35
+ }, { immediate: true });
36
+ function snapshot() {
37
+ return localColumns.value.map((column) => ({
38
+ ...column,
39
+ items: [...column.items]
40
+ }));
41
+ }
42
+ function reorderColumn(columnKey, items) {
43
+ localColumns.value = localColumns.value.map((column) => column.key === columnKey ? {
44
+ ...column,
45
+ items
46
+ } : column);
47
+ emit("update:columns", snapshot());
48
+ }
49
+ let changePending = false;
50
+ function notifyChange() {
51
+ if (changePending) return;
52
+ changePending = true;
53
+ queueMicrotask(() => {
54
+ changePending = false;
55
+ emit("change", snapshot());
56
+ });
57
+ }
58
+ provide(KanbanDIKey, {
59
+ kanbanProps: props,
60
+ cls,
61
+ reorderColumn,
62
+ notifyChange
63
+ });
64
+ return (_ctx, _cache) => {
65
+ return openBlock(), createElementBlock("div", mergeProps(_ctx.$attrs, { class: [unref(cls).b, unref(bem).is("disabled", __props.disabled)] }), [(openBlock(true), createElementBlock(Fragment, null, renderList(localColumns.value, (column) => {
66
+ return openBlock(), createBlock(_sfc_main$1, {
67
+ key: column.key,
68
+ column
69
+ }, {
70
+ header: withCtx(({ column: col, count }) => [renderSlot(_ctx.$slots, "header", {
71
+ column: col,
72
+ count
73
+ })]),
74
+ card: withCtx(({ card, column: col, index }) => [renderSlot(_ctx.$slots, "card", {
75
+ card,
76
+ column: col,
77
+ index
78
+ })]),
79
+ empty: withCtx(({ column: col }) => [renderSlot(_ctx.$slots, "empty", { column: col })]),
80
+ _: 3
81
+ }, 8, ["column"]);
82
+ }), 128))], 16);
83
+ };
84
+ }
85
+ });
86
+ //#endregion
87
+ export { _sfc_main as default };
88
+
89
+ //# sourceMappingURL=kanban.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kanban.js","names":["$attrs"],"sources":["../../../src/components/kanban/kanban.vue"],"sourcesContent":["<template>\n <div v-bind=\"$attrs\" :class=\"[cls.b, bem.is('disabled', disabled)]\">\n <KanbanColumn v-for=\"column of localColumns\" :key=\"column.key\" :column=\"column\">\n <template #header=\"{ column: col, count }\">\n <slot name=\"header\" :column=\"col\" :count=\"count\" />\n </template>\n <template #card=\"{ card, column: col, index }\">\n <slot name=\"card\" :card=\"card\" :column=\"col\" :index=\"index\" />\n </template>\n <template #empty=\"{ column: col }\">\n <slot name=\"empty\" :column=\"col\" />\n </template>\n </KanbanColumn>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { bem } from '@veltra/utils'\nimport { provide, ref, watch } from 'vue'\n\nimport type { KanbanColumnItem, KanbanEmits, KanbanProps, KanbanSlots } from '../../types'\nimport { KanbanDIKey, type KanbanContext } from './di'\nimport KanbanColumn from './kanban-column.vue'\n\ndefineOptions({ name: 'UKanban', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<KanbanProps>(), {\n cardKey: 'id',\n titleKey: 'title',\n disabled: false,\n countable: true,\n placeholder: '暂无内容'\n})\n\nconst emit = defineEmits<KanbanEmits>()\ndefineSlots<KanbanSlots>()\n\nconst cls = bem('kanban')\n\n// 拖拽写回的本地数据源:props 拷贝一份避免原地改父级数据,\n// 拖拽结果经 update:columns 交给父级决定是否受控回写\nconst localColumns = ref<KanbanColumnItem[]>([])\n\nwatch(\n () => props.columns,\n (columns) => {\n localColumns.value = (columns ?? []).map((column) => ({ ...column, items: [...column.items] }))\n },\n { immediate: true }\n)\n\nfunction snapshot(): KanbanColumnItem[] {\n return localColumns.value.map((column) => ({ ...column, items: [...column.items] }))\n}\n\nfunction reorderColumn(columnKey: string, items: Record<string, any>[]) {\n localColumns.value = localColumns.value.map((column) =>\n column.key === columnKey ? { ...column, items } : column\n )\n emit('update:columns', snapshot())\n}\n\n// 一次拖拽可能同步触发多个回调(跨列转移在源列与目标列各触发一次 onTransfer),\n// microtask 内合并保证一次操作只 emit 一次 change\nlet changePending = false\nfunction notifyChange() {\n if (changePending) return\n changePending = true\n queueMicrotask(() => {\n changePending = false\n emit('change', snapshot())\n })\n}\n\nprovide(KanbanDIKey, {\n kanbanProps: props,\n cls,\n reorderColumn,\n notifyChange\n} satisfies KanbanContext)\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;EA0BA,MAAM,QAAQ;EAQd,MAAM,OAAO;EAGb,MAAM,MAAM,IAAI,QAAQ;EAIxB,MAAM,eAAe,IAAwB,CAAC,CAAC;EAE/C,YACQ,MAAM,UACX,YAAY;GACX,aAAa,SAAS,WAAW,CAAC,EAAA,CAAG,KAAK,YAAY;IAAE,GAAG;IAAQ,OAAO,CAAC,GAAG,OAAO,KAAK;GAAE,EAAE;EAChG,GACA,EAAE,WAAW,KAAK,CACpB;EAEA,SAAS,WAA+B;GACtC,OAAO,aAAa,MAAM,KAAK,YAAY;IAAE,GAAG;IAAQ,OAAO,CAAC,GAAG,OAAO,KAAK;GAAE,EAAE;EACrF;EAEA,SAAS,cAAc,WAAmB,OAA8B;GACtE,aAAa,QAAQ,aAAa,MAAM,KAAK,WAC3C,OAAO,QAAQ,YAAY;IAAE,GAAG;IAAQ;GAAM,IAAI,MACpD;GACA,KAAK,kBAAkB,SAAS,CAAC;EACnC;EAIA,IAAI,gBAAgB;EACpB,SAAS,eAAe;GACtB,IAAI,eAAe;GACnB,gBAAgB;GAChB,qBAAqB;IACnB,gBAAgB;IAChB,KAAK,UAAU,SAAS,CAAC;GAC3B,CAAC;EACH;EAEA,QAAQ,aAAa;GACnB,aAAa;GACb;GACA;GACA;EACF,CAAyB;;GA9EvB,OAAA,UAAA,GAAA,mBAYM,OAZN,WAAaA,KAYP,QAZa,EAAG,OAAK,CAAG,MAAA,GAAA,CAAG,CAAC,GAAG,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,QAAA,QAAQ,CAAA,EAAA,CAAA,GAC9D,EAAA,UAAA,IAAA,GAAA,mBAUe,UAAA,MAAA,WAVgB,aAAA,QAAV,WAAM;IAA3B,OAAA,UAAA,GAAA,YAUe,aAAA;KAV+B,KAAK,OAAO;KAAc;;KAC3D,QAAM,SACoC,EAAA,QADxB,KAAK,YAAK,CACrC,WAAmD,KAAA,QAAA,UAAA;MAA9B,QAAQ;MAAa;;KAEjC,MAAI,SACiD,EAD7C,MAAI,QAAU,KAAK,YAAK,CACzC,WAA8D,KAAA,QAAA,QAAA;MAArC;MAAO,QAAQ;MAAa;;KAE5C,OAAK,SACqB,EAAA,QADT,UAAG,CAC7B,WAAmC,KAAA,QAAA,SAAA,EAAf,QAAQ,IAAG,CAAA,CAAA,CAAA"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1 @@
1
+ import "./style2.css";
@@ -0,0 +1,128 @@
1
+ @charset "UTF-8";
2
+ /** flex布局 */
3
+ /**
4
+ * 设置css变量
5
+ * 参数1:变量前缀
6
+ * 参数2: 一个由值组成的列表或者由key, value组成的map
7
+ * css-var(height, (
8
+ * large: 40px
9
+ * default: 32px
10
+ * small: 24px
11
+ * ))
12
+ * 生成的css变量:
13
+ * --u-height-large: 40px;
14
+ * --u-height-default: 32px;
15
+ * --u-height-small: 24px;
16
+ */
17
+ /** 暗色:`UITheme.render()` 按主题系列写入 html[data-theme] */
18
+ .u-kanban {
19
+ display: flex;
20
+ justify-content: flex-start;
21
+ align-items: flex-start;
22
+ flex-wrap: nowrap;
23
+ gap: var(--u-gap-default);
24
+ padding: var(--u-gap-default);
25
+ overflow-x: auto;
26
+ box-sizing: border-box;
27
+ }
28
+ .u-kanban__column {
29
+ flex: 0 0 auto;
30
+ width: 280px;
31
+ max-width: 100%;
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: var(--u-gap-small);
35
+ padding: var(--u-gap-small);
36
+ background-color: var(--u-bg-color-bottom);
37
+ border-radius: var(--u-radius-default);
38
+ box-sizing: border-box;
39
+ }
40
+ .u-kanban__column-header {
41
+ display: flex;
42
+ justify-content: flex-start;
43
+ align-items: center;
44
+ flex-wrap: nowrap;
45
+ gap: var(--u-gap-small);
46
+ padding: 0 2px;
47
+ }
48
+ .u-kanban__column-title {
49
+ overflow: hidden;
50
+ text-overflow: ellipsis;
51
+ white-space: nowrap;
52
+ flex: 1 1 0%;
53
+ min-width: 0;
54
+ font-size: var(--u-font-size-main-default);
55
+ font-weight: 600;
56
+ color: var(--u-text-color-title);
57
+ }
58
+ .u-kanban__column-count {
59
+ flex: none;
60
+ min-width: 18px;
61
+ padding: 0 6px;
62
+ border-radius: var(--u-radius-large);
63
+ background-color: var(--u-color-primary-a-11);
64
+ color: var(--u-color-primary);
65
+ font-size: var(--u-font-size-main-small);
66
+ line-height: 18px;
67
+ text-align: center;
68
+ }
69
+ .u-kanban__cards {
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: var(--u-gap-small);
73
+ min-height: 64px;
74
+ padding: 2px;
75
+ }
76
+ .u-kanban__cards.is-dropzone {
77
+ background-color: var(--u-color-primary-a-4);
78
+ border-radius: var(--u-radius-default);
79
+ }
80
+ .u-kanban__card {
81
+ display: flex;
82
+ justify-content: flex-start;
83
+ align-items: flex-start;
84
+ flex-wrap: nowrap;
85
+ gap: var(--u-gap-small);
86
+ padding: var(--u-gap-small);
87
+ background-color: var(--u-bg-color-top);
88
+ border-radius: var(--u-radius-default);
89
+ box-shadow: var(--u-shadow-sm);
90
+ font-size: var(--u-font-size-main-default);
91
+ color: var(--u-text-color-main);
92
+ transition: box-shadow var(--u-transition-fast) var(--u-transition-ease), opacity var(--u-transition-fast) var(--u-transition-ease);
93
+ }
94
+ .u-kanban__card:hover {
95
+ box-shadow: var(--u-shadow);
96
+ }
97
+ .u-kanban__card.is-dragging {
98
+ opacity: 0.6;
99
+ }
100
+ .u-kanban__card-handle {
101
+ flex: none;
102
+ line-height: 1;
103
+ color: var(--u-text-color-assist);
104
+ cursor: grab;
105
+ user-select: none;
106
+ }
107
+ .u-kanban__card-handle:active {
108
+ cursor: grabbing;
109
+ }
110
+ .u-kanban__card-body {
111
+ flex: 1 1 0%;
112
+ min-width: 0;
113
+ word-break: break-word;
114
+ }
115
+ .u-kanban__empty {
116
+ border: 1px dashed var(--u-border-color);
117
+ border-radius: var(--u-radius-default);
118
+ padding: var(--u-gap-default) var(--u-gap-small);
119
+ font-size: var(--u-font-size-main-default);
120
+ color: var(--u-text-color-assist);
121
+ text-align: center;
122
+ }
123
+ .u-kanban.is-disabled .u-kanban__card-handle {
124
+ cursor: not-allowed;
125
+ }
126
+ .u-kanban.is-disabled .u-kanban__card-handle:active {
127
+ cursor: not-allowed;
128
+ }
@@ -0,0 +1,154 @@
1
+ import { $insertNodes, DecoratorNode } from "lexical";
2
+ //#region src/components/rich-text-editor/image-node.ts
3
+ let imageUid = 0;
4
+ function nextImageId() {
5
+ return `rte-img-${++imageUid}`;
6
+ }
7
+ /**
8
+ * 图片节点:插入时 src 为本地 objectURL、__file 持有原始文件,提交前由宿主调用
9
+ * uploadImages 换成服务器地址。__file 不参与序列化,仅会话内有效。
10
+ * 本仓库为手写 Vue 绑定(无 React 适配层),不走 decorate 渲染管道,
11
+ * createDOM 直接返回 <img> 元素。
12
+ */
13
+ var ImageNode = class ImageNode extends DecoratorNode {
14
+ __src;
15
+ __alt;
16
+ __id;
17
+ __file;
18
+ static getType() {
19
+ return "u-image";
20
+ }
21
+ static clone(node) {
22
+ return new ImageNode(node.__src, node.__alt, node.__id, node.__file, node.__key);
23
+ }
24
+ static importJSON(serializedNode) {
25
+ return $createImageNode({
26
+ src: serializedNode.src,
27
+ alt: serializedNode.alt,
28
+ id: serializedNode.id
29
+ });
30
+ }
31
+ constructor(src, alt = "", id = nextImageId(), file = null, key) {
32
+ super(key);
33
+ this.__src = src;
34
+ this.__alt = alt;
35
+ this.__id = id;
36
+ this.__file = file;
37
+ }
38
+ static importDOM() {
39
+ return { img: () => ({
40
+ conversion: convertImageElement,
41
+ priority: 0
42
+ }) };
43
+ }
44
+ exportDOM() {
45
+ const element = document.createElement("img");
46
+ element.setAttribute("src", this.__src);
47
+ element.setAttribute("class", "u-rte-image");
48
+ element.setAttribute("alt", this.__alt);
49
+ element.setAttribute("data-rte-image-id", this.__id);
50
+ return { element };
51
+ }
52
+ exportJSON() {
53
+ return {
54
+ type: "u-image",
55
+ version: 1,
56
+ src: this.__src,
57
+ alt: this.__alt,
58
+ id: this.__id
59
+ };
60
+ }
61
+ createDOM() {
62
+ const element = document.createElement("img");
63
+ element.className = "u-rte-image";
64
+ element.src = this.__src;
65
+ element.alt = this.__alt;
66
+ element.draggable = false;
67
+ return element;
68
+ }
69
+ updateDOM(prevNode, element) {
70
+ const img = element;
71
+ if (prevNode.__src !== this.__src) img.src = this.__src;
72
+ if (prevNode.__alt !== this.__alt) img.alt = this.__alt;
73
+ return false;
74
+ }
75
+ /** Vue 环境无 React 渲染管道,装饰内容即 createDOM 返回的元素本身 */
76
+ decorate() {
77
+ return null;
78
+ }
79
+ getSrc() {
80
+ return this.__src;
81
+ }
82
+ getFile() {
83
+ return this.__file;
84
+ }
85
+ /** 替换为服务器地址,同时清除待上传标记(已上传的节点不再重复上传) */
86
+ setSrc(src) {
87
+ const self = this.getWritable();
88
+ self.__src = src;
89
+ self.__file = null;
90
+ }
91
+ };
92
+ function convertImageElement(domNode) {
93
+ const src = domNode.getAttribute("src");
94
+ if (!src) return { node: null };
95
+ return { node: $createImageNode({
96
+ src,
97
+ alt: domNode.getAttribute("alt") ?? "",
98
+ id: domNode.getAttribute("data-rte-image-id") ?? void 0
99
+ }) };
100
+ }
101
+ function $createImageNode(options) {
102
+ return new ImageNode(options.src, options.alt ?? "", options.id ?? nextImageId(), options.file ?? null);
103
+ }
104
+ function $isImageNode(node) {
105
+ return node instanceof ImageNode;
106
+ }
107
+ const objectUrls = /* @__PURE__ */ new WeakMap();
108
+ function getImageObjectUrls(editor) {
109
+ let urls = objectUrls.get(editor);
110
+ if (!urls) {
111
+ urls = /* @__PURE__ */ new Set();
112
+ objectUrls.set(editor, urls);
113
+ }
114
+ return urls;
115
+ }
116
+ /** 插入图片文件:objectURL 即时预览,File 挂在节点上等待提交时上传。返回插入的图片数量 */
117
+ function insertImageFiles(editor, files) {
118
+ const images = Array.from(files).filter((file) => file.type.startsWith("image/"));
119
+ if (!images.length) return 0;
120
+ const urls = getImageObjectUrls(editor);
121
+ editor.update(() => {
122
+ const nodes = images.map((file) => {
123
+ const url = URL.createObjectURL(file);
124
+ urls.add(url);
125
+ return $createImageNode({
126
+ src: url,
127
+ alt: file.name,
128
+ file
129
+ });
130
+ });
131
+ $insertNodes(nodes);
132
+ });
133
+ return images.length;
134
+ }
135
+ /** 回收已从内容中移除的图片 objectURL,aliveUrls 为当前内容中仍存活的图片地址 */
136
+ function gcImageObjectUrls(editor, aliveUrls) {
137
+ const urls = objectUrls.get(editor);
138
+ if (!urls?.size) return;
139
+ urls.forEach((url) => {
140
+ if (!aliveUrls.has(url)) {
141
+ URL.revokeObjectURL(url);
142
+ urls.delete(url);
143
+ }
144
+ });
145
+ }
146
+ /** 回收该编辑器创建的全部图片 objectURL(组件卸载时调用) */
147
+ function revokeImageObjectUrls(editor) {
148
+ objectUrls.get(editor)?.forEach((url) => URL.revokeObjectURL(url));
149
+ objectUrls.delete(editor);
150
+ }
151
+ //#endregion
152
+ export { $createImageNode, $isImageNode, ImageNode, gcImageObjectUrls, insertImageFiles, revokeImageObjectUrls };
153
+
154
+ //# sourceMappingURL=image-node.js.map
@@ -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
+ }