@veltra/desktop 1.8.2 → 1.8.4
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/action/action-group.js.map +1 -1
- package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/action/action.js.map +1 -1
- package/dist/components/auto-complete/auto-complete.js.map +1 -1
- package/dist/components/badge/badge.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/button/button-group.js.map +1 -1
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/calendar/calendar.js.map +1 -1
- package/dist/components/card/card-action.js.map +1 -1
- package/dist/components/card/card-content.js.map +1 -1
- package/dist/components/card/card-cover.js.map +1 -1
- package/dist/components/card/card-header.js.map +1 -1
- package/dist/components/card/card.js.map +1 -1
- package/dist/components/cascade/cascade-panel-item.js.map +1 -1
- package/dist/components/cascade/cascade.js +1 -0
- package/dist/components/cascade/cascade.js.map +1 -1
- package/dist/components/cascade/use-select.js +6 -2
- package/dist/components/cascade/use-select.js.map +1 -1
- package/dist/components/check-tag/check-tag.js.map +1 -1
- package/dist/components/checkbox/checkbox-button.js.map +1 -1
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
- package/dist/components/code-editor/code-editor.js.map +1 -1
- package/dist/components/collapse/collapse-item.js.map +1 -1
- package/dist/components/collapse/collapse.js.map +1 -1
- package/dist/components/condition-editor/components/condition-group.js.map +1 -1
- package/dist/components/condition-editor/components/condition-row.js.map +1 -1
- package/dist/components/condition-editor/condition-editor.js.map +1 -1
- package/dist/components/contextmenu/contextmenu-item.js.map +1 -1
- package/dist/components/contextmenu/contextmenu-panel.js.map +1 -1
- package/dist/components/contextmenu/contextmenu-sub.js.map +1 -1
- package/dist/components/contextmenu/contextmenu.js.map +1 -1
- package/dist/components/date-panel/date-panel.js.map +1 -1
- package/dist/components/date-panel/headers/day.js.map +1 -1
- package/dist/components/date-panel/headers/month.js.map +1 -1
- package/dist/components/date-panel/headers/year.js.map +1 -1
- package/dist/components/date-panel/panels/day.js.map +1 -1
- package/dist/components/date-panel/panels/month.js.map +1 -1
- package/dist/components/date-panel/panels/year.js.map +1 -1
- package/dist/components/date-picker/date-picker.js.map +1 -1
- package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
- package/dist/components/dialog/dialog.js.map +1 -1
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/dropdown/dropdown.js.map +1 -1
- package/dist/components/dual-nav/dual-nav-app.js.map +1 -1
- package/dist/components/dual-nav/dual-nav.js.map +1 -1
- package/dist/components/empty/empty.js.map +1 -1
- package/dist/components/expression-editor/components/path-preview.js.map +1 -1
- package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
- package/dist/components/expression-editor/expression-editor.js.map +1 -1
- package/dist/components/file-picker/file-picker.js.map +1 -1
- package/dist/components/file-viewer/file-viewer.js.map +1 -1
- package/dist/components/file-viewer/previewers/docx-previewer.js.map +1 -1
- package/dist/components/file-viewer/previewers/image-previewer.js.map +1 -1
- package/dist/components/file-viewer/previewers/ofd-previewer.js.map +1 -1
- package/dist/components/file-viewer/previewers/pdf-previewer.js.map +1 -1
- package/dist/components/file-viewer/previewers/pdf-viewport-scroll.js.map +1 -1
- package/dist/components/file-viewer/previewers/pdf-zoom-bridge.js.map +1 -1
- package/dist/components/file-viewer/previewers/sheet-previewer.js.map +1 -1
- package/dist/components/file-viewer/previewers/text-previewer.js.map +1 -1
- package/dist/components/file-viewer/previewers/video-previewer.js.map +1 -1
- package/dist/components/float-button/float-button.js.map +1 -1
- package/dist/components/form/form.js.map +1 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form-item/form-item.js.map +1 -1
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/gantt-chart/gantt-chart.js.map +1 -1
- package/dist/components/grid/grid-item.js.map +1 -1
- package/dist/components/grid/grid.js.map +1 -1
- package/dist/components/grid-input/grid-input.js.map +1 -1
- package/dist/components/group-input/group-input.js.map +1 -1
- package/dist/components/group-nav/group-nav-item.js.map +1 -1
- package/dist/components/group-nav/group-nav.js.map +1 -1
- package/dist/components/icon/icon.js.map +1 -1
- package/dist/components/image-cropper/image-cropper.js.map +1 -1
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/input/input.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/kanban/kanban-column.js.map +1 -1
- package/dist/components/kanban/kanban.js.map +1 -1
- package/dist/components/kbd/kbd.js.map +1 -1
- package/dist/components/layout/layout-resizer.js.map +1 -1
- package/dist/components/layout/layout.js.map +1 -1
- package/dist/components/list/list-item.js.map +1 -1
- package/dist/components/list/list.js.map +1 -1
- package/dist/components/loading/loading.js.map +1 -1
- package/dist/components/message/message-box.js.map +1 -1
- package/dist/components/message/message.js.map +1 -1
- package/dist/components/message-confirm/message-confirm-box.js.map +1 -1
- package/dist/components/message-confirm/message-confirm.js.map +1 -1
- package/dist/components/multi-select/multi-select-option.js.map +1 -1
- package/dist/components/multi-select/multi-select.js.map +1 -1
- package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
- package/dist/components/nav/nav-icon.js.map +1 -1
- package/dist/components/nav/nav-item-collapsed.js.map +1 -1
- package/dist/components/nav/nav-item.js.map +1 -1
- package/dist/components/nav/nav-sub-collapsed.js.map +1 -1
- package/dist/components/nav/nav-sub.js.map +1 -1
- package/dist/components/nav/nav.js.map +1 -1
- package/dist/components/notification/notification-box.js.map +1 -1
- package/dist/components/notification/notification.js.map +1 -1
- package/dist/components/number/number.js.map +1 -1
- package/dist/components/number-input/number-input.js.map +1 -1
- package/dist/components/number-range-input/number-range-input.js.map +1 -1
- package/dist/components/paginator/paginator.js.map +1 -1
- package/dist/components/palette/palette-alpha.js.map +1 -1
- package/dist/components/palette/palette-color-switch.js.map +1 -1
- package/dist/components/palette/palette-hue.js.map +1 -1
- package/dist/components/palette/palette-sv.js.map +1 -1
- package/dist/components/palette/palette.js.map +1 -1
- package/dist/components/password-input/password-input.js.map +1 -1
- package/dist/components/pop-confirm/pop-confirm.js.map +1 -1
- package/dist/components/progress/progress.js.map +1 -1
- package/dist/components/progress-nodes/progress-nodes.js.map +1 -1
- package/dist/components/radio/radio.js.map +1 -1
- package/dist/components/radio-group/radio-group.js.map +1 -1
- package/dist/components/rich-text-editor/image-node.js +53 -14
- package/dist/components/rich-text-editor/image-node.js.map +1 -1
- package/dist/components/rich-text-editor/rich-text-editor.js +9 -3
- package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
- package/dist/components/rich-text-editor/style2.css +31 -0
- package/dist/components/rich-text-editor/toolbar.js.map +1 -1
- package/dist/components/rich-text-editor/use-image-resize.js +118 -0
- package/dist/components/rich-text-editor/use-image-resize.js.map +1 -0
- package/dist/components/scroll/scroll-bar.js.map +1 -1
- package/dist/components/scroll/scroll.js.map +1 -1
- package/dist/components/segment/segment.js.map +1 -1
- package/dist/components/select/select.js +9 -5
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/slider/slider-thumb.js.map +1 -1
- package/dist/components/slider/slider.js.map +1 -1
- package/dist/components/steps/steps.js.map +1 -1
- package/dist/components/switch/switch.js.map +1 -1
- package/dist/components/table/table-body.js.map +1 -1
- package/dist/components/table/table-cell.js.map +1 -1
- package/dist/components/table/table-foot.js.map +1 -1
- package/dist/components/table/table-head.js.map +1 -1
- package/dist/components/table/table-row.js.map +1 -1
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/table-editor/table-editor.js.map +1 -1
- package/dist/components/tabs/tabs-horizontal.js.map +1 -1
- package/dist/components/tabs/tabs-vertical.js.map +1 -1
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/components/tag/tag.js.map +1 -1
- package/dist/components/text/text.js.map +1 -1
- package/dist/components/textarea/textarea.js.map +1 -1
- package/dist/components/theme/theme.js.map +1 -1
- package/dist/components/tip/tip.js.map +1 -1
- package/dist/components/tree/tree-node.js.map +1 -1
- package/dist/components/tree/tree.js.map +1 -1
- package/dist/components/tree-select/tree-select.js +19 -10
- package/dist/components/tree-select/tree-select.js.map +1 -1
- package/dist/components/watermark/watermark.js.map +1 -1
- package/dist/types/cascade.d.ts +5 -0
- package/dist/types/select.d.ts +8 -2
- package/dist/types/tree-select.d.ts +8 -2
- package/package.json +18 -18
- package/src/components/batch-edit/__test__/batch-edit.test.ts +1 -1
- package/src/components/button/__test__/button.test.ts +1 -1
- package/src/components/cascade/__test__/cascade.test.ts +26 -1
- package/src/components/cascade/use-select.ts +5 -5
- package/src/components/collapse/__test__/collapse.test.ts +1 -1
- package/src/components/condition-editor/__test__/condition-editor.test.ts +1 -1
- package/src/components/contextmenu/__test__/contextmenu.test.ts +1 -1
- package/src/components/date-picker/__test__/date-picker.test.ts +1 -1
- package/src/components/date-range-picker/__test__/date-range-picker.test.ts +1 -1
- package/src/components/drawer/__test__/drawer.test.ts +1 -1
- package/src/components/expression-editor/__test__/chip.test.ts +1 -1
- package/src/components/expression-editor/__test__/editor.test.ts +1 -1
- package/src/components/expression-editor/__test__/expression-editor.test.ts +1 -1
- package/src/components/expression-editor/__test__/mention.test.ts +1 -1
- package/src/components/expression-editor/__test__/model.test.ts +1 -1
- package/src/components/file-viewer/__test__/helper.test.ts +1 -1
- package/src/components/form/__test__/form.test.ts +1 -1
- package/src/components/form/__test__/is-field-modified.test.ts +1 -1
- package/src/components/form-item/__test__/form-item.test.ts +1 -1
- package/src/components/image-cropper/__test__/draw-crop.test.ts +1 -1
- package/src/components/image-cropper/__test__/image-cropper.test.ts +1 -1
- package/src/components/image-cropper/__test__/use-transform.test.ts +1 -1
- package/src/components/kanban/__test__/kanban.test.ts +1 -1
- package/src/components/message-confirm/__test__/message-confirm.test.ts +1 -1
- package/src/components/multi-tree-select/__test__/multi-tree-select.test.ts +1 -1
- package/src/components/number-input/__test__/number-input.test.ts +1 -1
- package/src/components/number-range-input/__test__/number-range-input.test.ts +1 -1
- package/src/components/palette/__test__/color-transform.test.ts +1 -1
- package/src/components/palette/__test__/palette.test.ts +1 -1
- package/src/components/password-input/__test__/password-input.test.ts +1 -1
- package/src/components/radio-group/__test__/radio-group.test.ts +1 -1
- package/src/components/rich-text-editor/__test__/image-node.test.ts +140 -0
- package/src/components/rich-text-editor/image-node.ts +84 -15
- package/src/components/rich-text-editor/rich-text-editor.vue +15 -4
- package/src/components/rich-text-editor/style.scss +35 -0
- package/src/components/rich-text-editor/use-image-resize.ts +168 -0
- package/src/components/segment/__test__/segment.test.ts +1 -1
- package/src/components/select/__test__/select.test.ts +68 -1
- package/src/components/select/select.vue +15 -5
- package/src/components/table/__test__/col-width.test.ts +1 -1
- package/src/components/table/__test__/column-style.test.ts +1 -1
- package/src/components/table-editor/__test__/table-editor.test.ts +1 -1
- package/src/components/tree-select/__test__/tree-select.test.ts +50 -1
- package/src/components/tree-select/tree-select.vue +28 -11
- package/src/types/cascade.ts +5 -0
- package/src/types/select.ts +8 -2
- package/src/types/tree-select.ts +9 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from '
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
2
2
|
import { createApp, h, nextTick, ref, shallowRef } from 'vue'
|
|
3
3
|
|
|
4
4
|
import type { ImageCropperChangePayload, ImageCropperExposed } from '../../../types'
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createEditor, type LexicalEditor } from 'lexical'
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vite-plus/test'
|
|
3
|
+
|
|
4
|
+
import { $createImageNode, $isImageNode, ImageNode } from '../image-node'
|
|
5
|
+
|
|
6
|
+
let editor: LexicalEditor
|
|
7
|
+
let container: HTMLDivElement
|
|
8
|
+
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
container = document.createElement('div')
|
|
11
|
+
document.body.appendChild(container)
|
|
12
|
+
editor = createEditor({
|
|
13
|
+
namespace: 'test',
|
|
14
|
+
nodes: [ImageNode],
|
|
15
|
+
onError: (error) => {
|
|
16
|
+
throw error
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
editor.setRootElement(container)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
editor.setRootElement(null)
|
|
24
|
+
container.remove()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
function createImage(options: { src?: string; width?: number; height?: number } = {}) {
|
|
28
|
+
return $createImageNode({
|
|
29
|
+
src: options.src ?? 'https://cdn.example.com/a.png',
|
|
30
|
+
alt: '示例图',
|
|
31
|
+
width: options.width,
|
|
32
|
+
height: options.height
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 节点构造与写方法要求可写上下文(Lexical 0.50 在 read 回调内禁止 new 节点) */
|
|
37
|
+
function inUpdate<T>(fn: () => T): T {
|
|
38
|
+
let result!: T
|
|
39
|
+
editor.update(() => {
|
|
40
|
+
result = fn()
|
|
41
|
+
})
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('ImageNode', () => {
|
|
46
|
+
it('createDOM 返回 wrapper + img + 缩放手柄,未设置尺寸时无内联宽高', () => {
|
|
47
|
+
const { wrapper, key } = inUpdate(() => {
|
|
48
|
+
const node = createImage()
|
|
49
|
+
return { wrapper: node.createDOM(), key: node.getKey() }
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(wrapper.className).toBe('u-rte-image-wrap')
|
|
53
|
+
expect(wrapper.getAttribute('data-rte-image-key')).toBe(key)
|
|
54
|
+
|
|
55
|
+
const img = wrapper.querySelector('img')
|
|
56
|
+
expect(img?.className).toBe('u-rte-image')
|
|
57
|
+
expect(img?.getAttribute('src')).toBe('https://cdn.example.com/a.png')
|
|
58
|
+
expect(img?.getAttribute('alt')).toBe('示例图')
|
|
59
|
+
expect(img?.style.width).toBe('')
|
|
60
|
+
expect(img?.style.height).toBe('')
|
|
61
|
+
|
|
62
|
+
expect(wrapper.querySelector('.u-rte-image-resizer')).not.toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('createDOM 应用节点上的显示尺寸', () => {
|
|
66
|
+
const img = inUpdate(() =>
|
|
67
|
+
createImage({ width: 320, height: 180 }).createDOM().querySelector('img')!
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
expect(img.style.width).toBe('320px')
|
|
71
|
+
expect(img.style.height).toBe('180px')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('updateDOM 在尺寸变化时同步 img 内联样式', () => {
|
|
75
|
+
const { img } = inUpdate(() => {
|
|
76
|
+
const node = createImage({ width: 320, height: 180 })
|
|
77
|
+
const wrapper = node.createDOM()
|
|
78
|
+
const resized = createImage({ width: 480, height: 270 })
|
|
79
|
+
resized.updateDOM(node, wrapper)
|
|
80
|
+
return { img: wrapper.querySelector('img')! }
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(img.style.width).toBe('480px')
|
|
84
|
+
expect(img.style.height).toBe('270px')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('exportDOM 仅在设置尺寸时输出 width / height 属性', () => {
|
|
88
|
+
const { sizedEl, unsizedEl } = inUpdate(() => ({
|
|
89
|
+
sizedEl: createImage({ width: 480, height: 270 }).exportDOM().element as HTMLImageElement,
|
|
90
|
+
unsizedEl: createImage().exportDOM().element as HTMLImageElement
|
|
91
|
+
}))
|
|
92
|
+
|
|
93
|
+
expect(sizedEl.getAttribute('width')).toBe('480')
|
|
94
|
+
expect(sizedEl.getAttribute('height')).toBe('270')
|
|
95
|
+
expect(sizedEl.className).toBe('u-rte-image')
|
|
96
|
+
|
|
97
|
+
expect(unsizedEl.hasAttribute('width')).toBe(false)
|
|
98
|
+
expect(unsizedEl.hasAttribute('height')).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('importDOM 读取 img 的 width / height 属性,缺失时为 0', () => {
|
|
102
|
+
const { sized, unsized } = inUpdate(() => {
|
|
103
|
+
const conversion = ImageNode.importDOM()!.img().conversion
|
|
104
|
+
const dom = document.createElement('img')
|
|
105
|
+
dom.setAttribute('src', 'https://cdn.example.com/a.png')
|
|
106
|
+
dom.setAttribute('width', '640')
|
|
107
|
+
dom.setAttribute('height', '360')
|
|
108
|
+
|
|
109
|
+
const noSizeDom = document.createElement('img')
|
|
110
|
+
noSizeDom.setAttribute('src', 'https://cdn.example.com/b.png')
|
|
111
|
+
|
|
112
|
+
return { sized: conversion(dom).node, unsized: conversion(noSizeDom).node }
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect($isImageNode(sized)).toBe(true)
|
|
116
|
+
expect(sized!.exportJSON()).toMatchObject({
|
|
117
|
+
src: 'https://cdn.example.com/a.png',
|
|
118
|
+
width: 640,
|
|
119
|
+
height: 360
|
|
120
|
+
})
|
|
121
|
+
expect(unsized!.exportJSON()).toMatchObject({ width: 0, height: 0 })
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('exportJSON / importJSON roundtrip 保留尺寸', () => {
|
|
125
|
+
const json = inUpdate(() => createImage({ width: 480, height: 270 }).exportJSON())
|
|
126
|
+
|
|
127
|
+
const restored = inUpdate(() => ImageNode.importJSON(json).exportJSON())
|
|
128
|
+
expect(restored).toEqual(json)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('editor update 中 setWidthAndHeight 写入尺寸', () => {
|
|
132
|
+
const json = inUpdate(() => {
|
|
133
|
+
const node = createImage()
|
|
134
|
+
node.setWidthAndHeight(200, 100)
|
|
135
|
+
return node.exportJSON()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
expect(json).toMatchObject({ width: 200, height: 100 })
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -18,44 +18,68 @@ function nextImageId(): string {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface SerializedImageNode extends Spread<
|
|
21
|
-
{ src: string; alt: string; id: string },
|
|
21
|
+
{ src: string; alt: string; id: string; width: number; height: number },
|
|
22
22
|
SerializedLexicalNode
|
|
23
23
|
> {}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* 图片节点:插入时 src 为本地 objectURL、__file 持有原始文件,提交前由宿主调用
|
|
27
27
|
* uploadImages 换成服务器地址。__file 不参与序列化,仅会话内有效。
|
|
28
|
+
* width / height 为显示尺寸(px),0 表示未设置、按图片自然尺寸(受 max-width 约束)。
|
|
28
29
|
* 本仓库为手写 Vue 绑定(无 React 适配层),不走 decorate 渲染管道,
|
|
29
|
-
* createDOM
|
|
30
|
+
* createDOM 返回 wrapper:`<span><img><span 手柄/></span>`,选中与拖拽缩放
|
|
31
|
+
* 由组件层(use-image-resize)基于 data-rte-image-key 驱动。
|
|
30
32
|
*/
|
|
31
33
|
export class ImageNode extends DecoratorNode<void> {
|
|
32
34
|
__src: string
|
|
33
35
|
__alt: string
|
|
34
36
|
__id: string
|
|
35
37
|
__file: File | null
|
|
38
|
+
__width: number
|
|
39
|
+
__height: number
|
|
36
40
|
|
|
37
41
|
static override getType(): string {
|
|
38
42
|
return 'u-image'
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
static override clone(node: ImageNode): ImageNode {
|
|
42
|
-
return new ImageNode(
|
|
46
|
+
return new ImageNode(
|
|
47
|
+
node.__src,
|
|
48
|
+
node.__alt,
|
|
49
|
+
node.__id,
|
|
50
|
+
node.__file,
|
|
51
|
+
node.__width,
|
|
52
|
+
node.__height,
|
|
53
|
+
node.__key
|
|
54
|
+
)
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
static override importJSON(serializedNode: SerializedImageNode): ImageNode {
|
|
46
58
|
return $createImageNode({
|
|
47
59
|
src: serializedNode.src,
|
|
48
60
|
alt: serializedNode.alt,
|
|
49
|
-
id: serializedNode.id
|
|
61
|
+
id: serializedNode.id,
|
|
62
|
+
width: serializedNode.width,
|
|
63
|
+
height: serializedNode.height
|
|
50
64
|
})
|
|
51
65
|
}
|
|
52
66
|
|
|
53
|
-
constructor(
|
|
67
|
+
constructor(
|
|
68
|
+
src: string,
|
|
69
|
+
alt = '',
|
|
70
|
+
id = nextImageId(),
|
|
71
|
+
file: File | null = null,
|
|
72
|
+
width = 0,
|
|
73
|
+
height = 0,
|
|
74
|
+
key?: NodeKey
|
|
75
|
+
) {
|
|
54
76
|
super(key)
|
|
55
77
|
this.__src = src
|
|
56
78
|
this.__alt = alt
|
|
57
79
|
this.__id = id
|
|
58
80
|
this.__file = file
|
|
81
|
+
this.__width = width
|
|
82
|
+
this.__height = height
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
static override importDOM(): DOMConversionMap | null {
|
|
@@ -68,26 +92,58 @@ export class ImageNode extends DecoratorNode<void> {
|
|
|
68
92
|
element.setAttribute('class', 'u-rte-image')
|
|
69
93
|
element.setAttribute('alt', this.__alt)
|
|
70
94
|
element.setAttribute('data-rte-image-id', this.__id)
|
|
95
|
+
if (this.__width) element.setAttribute('width', String(this.__width))
|
|
96
|
+
if (this.__height) element.setAttribute('height', String(this.__height))
|
|
71
97
|
return { element }
|
|
72
98
|
}
|
|
73
99
|
|
|
74
100
|
override exportJSON(): SerializedImageNode {
|
|
75
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
type: 'u-image',
|
|
103
|
+
version: 1,
|
|
104
|
+
src: this.__src,
|
|
105
|
+
alt: this.__alt,
|
|
106
|
+
id: this.__id,
|
|
107
|
+
width: this.__width,
|
|
108
|
+
height: this.__height
|
|
109
|
+
}
|
|
76
110
|
}
|
|
77
111
|
|
|
78
112
|
override createDOM(): HTMLElement {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
const wrapper = document.createElement('span')
|
|
114
|
+
wrapper.className = 'u-rte-image-wrap'
|
|
115
|
+
wrapper.contentEditable = 'false'
|
|
116
|
+
wrapper.setAttribute('data-rte-image-key', this.__key)
|
|
117
|
+
|
|
118
|
+
const img = document.createElement('img')
|
|
119
|
+
img.className = 'u-rte-image'
|
|
120
|
+
img.src = this.__src
|
|
121
|
+
img.alt = this.__alt
|
|
122
|
+
img.draggable = false
|
|
123
|
+
if (this.__width) img.style.width = `${this.__width}px`
|
|
124
|
+
if (this.__height) img.style.height = `${this.__height}px`
|
|
125
|
+
|
|
126
|
+
const handle = document.createElement('span')
|
|
127
|
+
handle.className = 'u-rte-image-resizer'
|
|
128
|
+
handle.setAttribute('aria-hidden', 'true')
|
|
129
|
+
|
|
130
|
+
wrapper.append(img, handle)
|
|
131
|
+
return wrapper
|
|
85
132
|
}
|
|
86
133
|
|
|
87
134
|
override updateDOM(prevNode: ImageNode, element: HTMLElement): boolean {
|
|
88
|
-
const img = element
|
|
135
|
+
const img = element.querySelector('img')
|
|
136
|
+
if (!img) return false
|
|
89
137
|
if (prevNode.__src !== this.__src) img.src = this.__src
|
|
90
138
|
if (prevNode.__alt !== this.__alt) img.alt = this.__alt
|
|
139
|
+
if (prevNode.__width !== this.__width) {
|
|
140
|
+
if (this.__width) img.style.width = `${this.__width}px`
|
|
141
|
+
else img.style.removeProperty('width')
|
|
142
|
+
}
|
|
143
|
+
if (prevNode.__height !== this.__height) {
|
|
144
|
+
if (this.__height) img.style.height = `${this.__height}px`
|
|
145
|
+
else img.style.removeProperty('height')
|
|
146
|
+
}
|
|
91
147
|
return false
|
|
92
148
|
}
|
|
93
149
|
|
|
@@ -110,6 +166,13 @@ export class ImageNode extends DecoratorNode<void> {
|
|
|
110
166
|
self.__src = src
|
|
111
167
|
self.__file = null
|
|
112
168
|
}
|
|
169
|
+
|
|
170
|
+
/** 写入显示尺寸(拖拽缩放结束时调用),px,0 表示恢复自然尺寸 */
|
|
171
|
+
setWidthAndHeight(width: number, height: number): void {
|
|
172
|
+
const self = this.getWritable()
|
|
173
|
+
self.__width = width
|
|
174
|
+
self.__height = height
|
|
175
|
+
}
|
|
113
176
|
}
|
|
114
177
|
|
|
115
178
|
function convertImageElement(domNode: HTMLElement): DOMConversionOutput {
|
|
@@ -119,7 +182,9 @@ function convertImageElement(domNode: HTMLElement): DOMConversionOutput {
|
|
|
119
182
|
node: $createImageNode({
|
|
120
183
|
src,
|
|
121
184
|
alt: domNode.getAttribute('alt') ?? '',
|
|
122
|
-
id: domNode.getAttribute('data-rte-image-id') ?? undefined
|
|
185
|
+
id: domNode.getAttribute('data-rte-image-id') ?? undefined,
|
|
186
|
+
width: parseInt(domNode.getAttribute('width') ?? '') || 0,
|
|
187
|
+
height: parseInt(domNode.getAttribute('height') ?? '') || 0
|
|
123
188
|
})
|
|
124
189
|
}
|
|
125
190
|
}
|
|
@@ -129,12 +194,16 @@ export function $createImageNode(options: {
|
|
|
129
194
|
alt?: string
|
|
130
195
|
id?: string
|
|
131
196
|
file?: File | null
|
|
197
|
+
width?: number
|
|
198
|
+
height?: number
|
|
132
199
|
}): ImageNode {
|
|
133
200
|
return new ImageNode(
|
|
134
201
|
options.src,
|
|
135
202
|
options.alt ?? '',
|
|
136
203
|
options.id ?? nextImageId(),
|
|
137
|
-
options.file ?? null
|
|
204
|
+
options.file ?? null,
|
|
205
|
+
options.width ?? 0,
|
|
206
|
+
options.height ?? 0
|
|
138
207
|
)
|
|
139
208
|
}
|
|
140
209
|
|
|
@@ -88,6 +88,8 @@ import {
|
|
|
88
88
|
$getRoot,
|
|
89
89
|
$createParagraphNode,
|
|
90
90
|
$nodesOfType,
|
|
91
|
+
$getSelection,
|
|
92
|
+
$isNodeSelection,
|
|
91
93
|
$setSelection,
|
|
92
94
|
COMMAND_PRIORITY_HIGH,
|
|
93
95
|
DROP_COMMAND,
|
|
@@ -110,6 +112,7 @@ import {
|
|
|
110
112
|
revokeImageObjectUrls
|
|
111
113
|
} from './image-node'
|
|
112
114
|
import Toolbar from './toolbar.vue'
|
|
115
|
+
import { useImageResize } from './use-image-resize'
|
|
113
116
|
|
|
114
117
|
const props = withDefaults(defineProps<RichTextEditorProps>(), {
|
|
115
118
|
disabled: undefined,
|
|
@@ -151,6 +154,11 @@ const showPlaceholder = ref(true)
|
|
|
151
154
|
let isComposing = false
|
|
152
155
|
let cleanupFns: (() => void)[] = []
|
|
153
156
|
|
|
157
|
+
const acceptsImage = () => props.image && !isDisabled.value && !isReadonly.value
|
|
158
|
+
|
|
159
|
+
// 图片选中 / 拖拽缩放交互
|
|
160
|
+
useImageResize(editor, acceptsImage)
|
|
161
|
+
|
|
154
162
|
function initEditor() {
|
|
155
163
|
if (!editorContainer.value) return
|
|
156
164
|
|
|
@@ -172,8 +180,6 @@ function initEditor() {
|
|
|
172
180
|
cleanupFns.push(registerHistory(editorInstance, createEmptyHistoryState(), 300))
|
|
173
181
|
|
|
174
182
|
// Image input: paste & drop
|
|
175
|
-
const acceptsImage = () => props.image && !isDisabled.value && !isReadonly.value
|
|
176
|
-
|
|
177
183
|
cleanupFns.push(
|
|
178
184
|
editorInstance.registerCommand(
|
|
179
185
|
PASTE_COMMAND,
|
|
@@ -353,8 +359,13 @@ watch(model, (newVal) => {
|
|
|
353
359
|
|
|
354
360
|
// Watch disabled/readonly changes
|
|
355
361
|
watch([isDisabled, isReadonly], () => {
|
|
356
|
-
if (editor.value)
|
|
357
|
-
|
|
362
|
+
if (!editor.value) return
|
|
363
|
+
editor.value.setEditable(!isDisabled.value && !isReadonly.value)
|
|
364
|
+
if (isDisabled.value || isReadonly.value) {
|
|
365
|
+
// 切出可编辑态时取消可能残留的图片选中
|
|
366
|
+
editor.value.update(() => {
|
|
367
|
+
if ($isNodeSelection($getSelection())) $setSelection(null)
|
|
368
|
+
})
|
|
358
369
|
}
|
|
359
370
|
})
|
|
360
371
|
|
|
@@ -388,3 +388,38 @@ $root-name: rich-text-editor;
|
|
|
388
388
|
height: auto;
|
|
389
389
|
vertical-align: bottom;
|
|
390
390
|
}
|
|
391
|
+
|
|
392
|
+
// 编辑器内图片 wrapper:承载选中态描边与缩放手柄(宿主直接渲染导出 HTML 时无此结构)
|
|
393
|
+
.u-rte-image-wrap {
|
|
394
|
+
position: relative;
|
|
395
|
+
display: inline-block;
|
|
396
|
+
max-width: 100%;
|
|
397
|
+
vertical-align: bottom;
|
|
398
|
+
line-height: 0;
|
|
399
|
+
|
|
400
|
+
.u-rte-image {
|
|
401
|
+
display: block;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
&.is-selected .u-rte-image {
|
|
405
|
+
outline: 2px solid fn.use-var(color, primary);
|
|
406
|
+
border-radius: 2px;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.u-rte-image-resizer {
|
|
411
|
+
position: absolute;
|
|
412
|
+
right: -5px;
|
|
413
|
+
bottom: -5px;
|
|
414
|
+
display: none;
|
|
415
|
+
width: 10px;
|
|
416
|
+
height: 10px;
|
|
417
|
+
border-radius: 50%;
|
|
418
|
+
background-color: fn.use-var(bg-color, top);
|
|
419
|
+
border: 2px solid fn.use-var(color, primary);
|
|
420
|
+
cursor: nwse-resize;
|
|
421
|
+
|
|
422
|
+
.u-rte-image-wrap.is-selected & {
|
|
423
|
+
display: block;
|
|
424
|
+
}
|
|
425
|
+
}
|