vue-editify 0.1.19 → 0.1.20
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/README.md +3 -3
- package/examples/App.vue +62 -62
- package/examples/main.ts +4 -4
- package/lib/components/button/button.vue.d.ts +11 -11
- package/lib/components/checkbox/checkbox.vue.d.ts +8 -8
- package/lib/components/colors/colors.vue.d.ts +4 -4
- package/lib/components/icon/icon.vue.d.ts +1 -1
- package/lib/components/insertImage/insertImage.vue.d.ts +9 -9
- package/lib/components/insertLink/insertLink.vue.d.ts +2 -2
- package/lib/components/insertTable/insertTable.vue.d.ts +2 -2
- package/lib/components/insertVideo/insertVideo.vue.d.ts +9 -9
- package/lib/components/layer/layer.vue.d.ts +9 -9
- package/lib/components/menu/menu.vue.d.ts +4 -4
- package/lib/components/toolbar/toolbar.vue.d.ts +9 -9
- package/lib/components/tooltip/tooltip.vue.d.ts +1 -1
- package/lib/components/triangle/triangle.vue.d.ts +4 -4
- package/lib/editify/editify.vue.d.ts +68 -68
- package/lib/editify.es.js +35 -24
- package/lib/editify.umd.js +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/style.css +1 -1
- package/package.json +45 -45
- package/src/components/button/button.less +145 -145
- package/src/components/button/button.vue +197 -197
- package/src/components/button/props.ts +95 -95
- package/src/components/checkbox/checkbox.less +84 -84
- package/src/components/checkbox/checkbox.vue +68 -68
- package/src/components/checkbox/props.ts +49 -49
- package/src/components/colors/colors.less +75 -75
- package/src/components/colors/colors.vue +36 -36
- package/src/components/colors/props.ts +29 -29
- package/src/components/icon/icon.less +14 -14
- package/src/components/icon/icon.vue +12 -12
- package/src/components/icon/props.ts +11 -11
- package/src/components/insertImage/insertImage.less +135 -135
- package/src/components/insertImage/insertImage.vue +146 -146
- package/src/components/insertImage/props.ts +43 -43
- package/src/components/insertLink/insertLink.less +64 -64
- package/src/components/insertLink/insertLink.vue +58 -58
- package/src/components/insertLink/props.ts +16 -16
- package/src/components/insertTable/insertTable.less +54 -54
- package/src/components/insertTable/insertTable.vue +85 -85
- package/src/components/insertTable/props.ts +27 -27
- package/src/components/insertVideo/insertVideo.less +135 -135
- package/src/components/insertVideo/insertVideo.vue +146 -146
- package/src/components/insertVideo/props.ts +43 -43
- package/src/components/layer/layer.less +49 -49
- package/src/components/layer/layer.vue +598 -598
- package/src/components/layer/props.ts +71 -71
- package/src/components/menu/menu.less +63 -63
- package/src/components/menu/menu.vue +1569 -1569
- package/src/components/menu/props.ts +17 -17
- package/src/components/toolbar/props.ts +35 -35
- package/src/components/toolbar/toolbar.less +89 -89
- package/src/components/toolbar/toolbar.vue +1101 -1101
- package/src/components/tooltip/props.ts +21 -21
- package/src/components/tooltip/tooltip.less +23 -23
- package/src/components/tooltip/tooltip.vue +37 -37
- package/src/components/triangle/props.ts +26 -26
- package/src/components/triangle/triangle.less +79 -79
- package/src/components/triangle/triangle.vue +65 -65
- package/src/core/function.ts +1150 -1144
- package/src/core/rule.ts +259 -259
- package/src/core/tool.ts +1137 -1137
- package/src/css/base.less +30 -30
- package/src/css/hljs.less +54 -54
- package/src/editify/editify.less +404 -404
- package/src/editify/editify.vue +810 -803
- package/src/editify/props.ts +156 -156
- package/src/hljs/index.ts +197 -197
- package/src/icon/iconfont.css +219 -219
- package/src/index.ts +32 -32
- package/src/locale/en_US.ts +88 -88
- package/src/locale/index.ts +12 -12
- package/src/locale/zh_CN.ts +88 -88
- package/tsconfig.json +27 -27
- package/tsconfig.node.json +11 -11
- package/vite-env.d.ts +1 -1
- package/vite.config.ts +42 -42
package/src/editify/editify.vue
CHANGED
@@ -1,803 +1,810 @@
|
|
1
|
-
<template>
|
2
|
-
<div class="editify" :class="{ fullscreen: isFullScreen, autoheight: !isFullScreen && autoheight }" ref="elRef">
|
3
|
-
<!-- 菜单区域 -->
|
4
|
-
<Menu v-if="menuConfig.use" :config="menuConfig" :color="color" ref="menuRef"></Menu>
|
5
|
-
<!-- 编辑层,与编辑区域宽高相同必须适配 -->
|
6
|
-
<div ref="bodyRef" class="editify-body" :class="{ border: showBorder, menu_inner: menuConfig.use && menuConfig.mode == 'inner' }" :data-editify-uid="instance.uid">
|
7
|
-
<!-- 编辑器 -->
|
8
|
-
<div ref="contentRef" class="editify-content" :class="{ placeholder: showPlaceholder, disabled: disabled }" @keydown="handleEditorKeydown" @click="handleEditorClick" @compositionstart="isInputChinese = true" @compositionend="isInputChinese = false" :data-editify-placeholder="placeholder"></div>
|
9
|
-
<!-- 代码视图 -->
|
10
|
-
<textarea v-if="isSourceView" :value="value" readonly class="editify-source" />
|
11
|
-
<!-- 工具条 -->
|
12
|
-
<Toolbar ref="toolbarRef" v-model="toolbarOptions.show" :node="toolbarOptions.node!" :type="toolbarOptions.type" :config="toolbarConfig" :color="color"></Toolbar>
|
13
|
-
</div>
|
14
|
-
<!-- 编辑器尾部 -->
|
15
|
-
<div v-if="showWordLength" class="editify-footer" :class="{ fullscreen: isFullScreen && !isSourceView }" ref="footerRef">
|
16
|
-
<!-- 字数统计 -->
|
17
|
-
<div class="editify-footer-words">{{ $editTrans('totalWordCount') }}{{ textValue.length }}</div>
|
18
|
-
</div>
|
19
|
-
</div>
|
20
|
-
</template>
|
21
|
-
<script setup lang="ts">
|
22
|
-
import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
23
|
-
import { AlexEditor, AlexElement, AlexElementRangeType, AlexElementsRangeType } from 'alex-editor'
|
24
|
-
import { element as DapElement, event as DapEvent, data as DapData, number as DapNumber, color as DapColor } from 'dap-util'
|
25
|
-
import { pasteKeepData, mergeObject, getToolbarConfig, getMenuConfig, MenuConfigType, ObjectType, ToolbarConfigType } from '../core/tool'
|
26
|
-
import { parseList, orderdListHandle, mediaHandle, tableHandle, preHandle, specialInblockHandle } from '../core/rule'
|
27
|
-
import { isTask, elementToParagraph, getCurrentParsedomElement, hasTableInRange, hasLinkInRange, hasPreInRange, hasImageInRange, hasVideoInRange } from '../core/function'
|
28
|
-
import Toolbar from '../components/toolbar/toolbar.vue'
|
29
|
-
import Menu from '../components/menu/menu.vue'
|
30
|
-
import Layer from '../components/layer/layer.vue'
|
31
|
-
import { EditifyProps, EditifyTableColumnResizeParamsType, EditifyToolbarOptionsType } from './props'
|
32
|
-
import { trans } from '../locale'
|
33
|
-
import { LanguagesItemType } from '../hljs'
|
34
|
-
|
35
|
-
//定义组件名称
|
36
|
-
defineOptions({
|
37
|
-
name: 'editify'
|
38
|
-
})
|
39
|
-
//获取实例
|
40
|
-
const instance = getCurrentInstance()!
|
41
|
-
//属性
|
42
|
-
const props = defineProps(EditifyProps)
|
43
|
-
//事件
|
44
|
-
const emits = defineEmits(['update:modelValue', 'focus', 'blur', 'change', 'keydown', 'insertparagraph', 'rangeupdate', 'updateview'])
|
45
|
-
|
46
|
-
//设置国际化方法
|
47
|
-
const $editTrans = trans(props.locale || 'zh_CN')
|
48
|
-
//对子孙后代组件提供国际化方法
|
49
|
-
provide('$editTrans', $editTrans)
|
50
|
-
|
51
|
-
//是否编辑器内部修改值
|
52
|
-
const isModelChange = ref<boolean>(false)
|
53
|
-
//是否正在输入中文
|
54
|
-
const isInputChinese = ref<boolean>(false)
|
55
|
-
//工具条和菜单栏判定延时器
|
56
|
-
const rangeUpdateTimer = ref<any>(null)
|
57
|
-
//表格列宽拖拽记录数据
|
58
|
-
const tableColumnResizeParams = ref<EditifyTableColumnResizeParamsType>({
|
59
|
-
element: null, //被拖拽的td
|
60
|
-
start: 0 //水平方向起点位置
|
61
|
-
})
|
62
|
-
//工具条参数配置
|
63
|
-
const toolbarOptions = ref<EditifyToolbarOptionsType>({
|
64
|
-
//是否显示工具条
|
65
|
-
show: false,
|
66
|
-
//关联元素
|
67
|
-
node: null,
|
68
|
-
//类型
|
69
|
-
type: 'text'
|
70
|
-
})
|
71
|
-
|
72
|
-
const menuRef = ref<InstanceType<typeof Menu> | null>(null)
|
73
|
-
const bodyRef = ref<HTMLElement | null>(null)
|
74
|
-
const contentRef = ref<HTMLElement | null>(null)
|
75
|
-
const toolbarRef = ref<InstanceType<typeof Toolbar> | null>(null)
|
76
|
-
const footerRef = ref<HTMLElement | null>(null)
|
77
|
-
const elRef = ref<HTMLElement | null>(null)
|
78
|
-
|
79
|
-
//编辑器对象
|
80
|
-
const editor = ref<AlexEditor | null>(null)
|
81
|
-
//是否代码视图
|
82
|
-
const isSourceView = ref<boolean>(false)
|
83
|
-
//是否全屏
|
84
|
-
const isFullScreen = ref<boolean>(false)
|
85
|
-
//菜单栏是否可以使用标识
|
86
|
-
const canUseMenu = ref<boolean>(false)
|
87
|
-
//光标选取范围内的元素数组
|
88
|
-
const dataRangeCaches = ref<AlexElementsRangeType>({
|
89
|
-
flatList: [],
|
90
|
-
list: []
|
91
|
-
})
|
92
|
-
|
93
|
-
//编辑器的值
|
94
|
-
const value = computed<string>({
|
95
|
-
set(val) {
|
96
|
-
emits('update:modelValue', val)
|
97
|
-
},
|
98
|
-
get() {
|
99
|
-
return props.modelValue || '<p><br></p>'
|
100
|
-
}
|
101
|
-
})
|
102
|
-
//编辑器的纯文本值
|
103
|
-
const textValue = computed<string>(() => {
|
104
|
-
return (<HTMLElement>DapElement.string2dom(`<div>${value.value}</div>`)).innerText
|
105
|
-
})
|
106
|
-
//是否显示占位符
|
107
|
-
const showPlaceholder = computed<boolean>(() => {
|
108
|
-
if (editor.value) {
|
109
|
-
if (value.value && editor.value.stack.length == 1 && editor.value.stack[0].type == 'block' && editor.value.stack[0].parsedom == AlexElement.BLOCK_NODE && editor.value.stack[0].isOnlyHasBreak() && !editor.value.stack[0].hasStyles() && !editor.value.stack[0].hasMarks()) {
|
110
|
-
return !isInputChinese.value
|
111
|
-
}
|
112
|
-
}
|
113
|
-
return false
|
114
|
-
})
|
115
|
-
//是否显示边框
|
116
|
-
const showBorder = computed<boolean>(() => {
|
117
|
-
//全屏模式下不显示边框
|
118
|
-
if (isFullScreen.value) {
|
119
|
-
return false
|
120
|
-
}
|
121
|
-
return props.border
|
122
|
-
})
|
123
|
-
//最终生效的工具栏配置
|
124
|
-
const toolbarConfig = computed<ToolbarConfigType>(() => {
|
125
|
-
return <ToolbarConfigType>mergeObject(getToolbarConfig($editTrans, props.locale), props.toolbar || {})
|
126
|
-
})
|
127
|
-
//最终生效的菜单栏配置
|
128
|
-
const menuConfig = computed<MenuConfigType>(() => {
|
129
|
-
return <MenuConfigType>mergeObject(getMenuConfig($editTrans, props.locale), props.menu || {})
|
130
|
-
})
|
131
|
-
|
132
|
-
//编辑器内部修改值的方法
|
133
|
-
const internalModify = (val: string) => {
|
134
|
-
isModelChange.value = true
|
135
|
-
value.value = val
|
136
|
-
nextTick(() => {
|
137
|
-
isModelChange.value = false
|
138
|
-
})
|
139
|
-
}
|
140
|
-
//隐藏工具条
|
141
|
-
const hideToolbar = () => {
|
142
|
-
toolbarOptions.value.show = false
|
143
|
-
toolbarOptions.value.node = null
|
144
|
-
}
|
145
|
-
//监听滚动隐藏工具条
|
146
|
-
const handleScroll = () => {
|
147
|
-
const setScroll = (el: HTMLElement) => {
|
148
|
-
DapEvent.on(el, `scroll.editify_${instance.uid}`, () => {
|
149
|
-
if (toolbarConfig.value.use && toolbarOptions.value.show) {
|
150
|
-
hideToolbar()
|
151
|
-
}
|
152
|
-
})
|
153
|
-
if (el.parentNode) {
|
154
|
-
setScroll(<HTMLElement>el.parentNode)
|
155
|
-
}
|
156
|
-
}
|
157
|
-
setScroll(contentRef.value!)
|
158
|
-
}
|
159
|
-
//移除上述滚动事件的监听
|
160
|
-
const removeScrollHandle = () => {
|
161
|
-
const removeScroll = (el: HTMLElement) => {
|
162
|
-
DapEvent.off(el, `scroll.editify_${instance.uid}`)
|
163
|
-
if (el.parentNode) {
|
164
|
-
removeScroll(<HTMLElement>el.parentNode)
|
165
|
-
}
|
166
|
-
}
|
167
|
-
removeScroll(contentRef.value!)
|
168
|
-
}
|
169
|
-
//工具条显示判断
|
170
|
-
const handleToolbar = () => {
|
171
|
-
if (props.disabled || isSourceView.value) {
|
172
|
-
return
|
173
|
-
}
|
174
|
-
hideToolbar()
|
175
|
-
nextTick(() => {
|
176
|
-
const table = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'table')
|
177
|
-
const pre = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'pre')
|
178
|
-
const link = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'a')
|
179
|
-
const image = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'img')
|
180
|
-
const video = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'video')
|
181
|
-
if (link) {
|
182
|
-
toolbarOptions.value.type = 'link'
|
183
|
-
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${link.key}"]`
|
184
|
-
if (toolbarOptions.value.show) {
|
185
|
-
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
186
|
-
} else {
|
187
|
-
toolbarOptions.value.show = true
|
188
|
-
}
|
189
|
-
} else if (image) {
|
190
|
-
toolbarOptions.value.type = 'image'
|
191
|
-
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${image.key}"]`
|
192
|
-
if (toolbarOptions.value.show) {
|
193
|
-
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
194
|
-
} else {
|
195
|
-
toolbarOptions.value.show = true
|
196
|
-
}
|
197
|
-
} else if (video) {
|
198
|
-
toolbarOptions.value.type = 'video'
|
199
|
-
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${video.key}"]`
|
200
|
-
if (toolbarOptions.value.show) {
|
201
|
-
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
202
|
-
} else {
|
203
|
-
toolbarOptions.value.show = true
|
204
|
-
}
|
205
|
-
} else if (table) {
|
206
|
-
toolbarOptions.value.type = 'table'
|
207
|
-
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${table.key}"]`
|
208
|
-
if (toolbarOptions.value.show) {
|
209
|
-
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
210
|
-
} else {
|
211
|
-
toolbarOptions.value.show = true
|
212
|
-
}
|
213
|
-
} else if (pre) {
|
214
|
-
toolbarOptions.value.type = 'codeBlock'
|
215
|
-
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${pre.key}"]`
|
216
|
-
if (toolbarOptions.value.show) {
|
217
|
-
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
218
|
-
} else {
|
219
|
-
toolbarOptions.value.show = true
|
220
|
-
}
|
221
|
-
} else {
|
222
|
-
const result = dataRangeCaches.value.flatList.filter((item: AlexElementRangeType) => {
|
223
|
-
return item.element.isText()
|
224
|
-
})
|
225
|
-
if (result.length && !hasTableInRange(editor.value!, dataRangeCaches.value) && !hasPreInRange(editor.value!, dataRangeCaches.value) && !hasLinkInRange(editor.value!, dataRangeCaches.value) && !hasImageInRange(editor.value!, dataRangeCaches.value) && !hasVideoInRange(editor.value!, dataRangeCaches.value)) {
|
226
|
-
toolbarOptions.value.type = 'text'
|
227
|
-
if (toolbarOptions.value.show) {
|
228
|
-
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
229
|
-
} else {
|
230
|
-
toolbarOptions.value.show = true
|
231
|
-
}
|
232
|
-
}
|
233
|
-
}
|
234
|
-
})
|
235
|
-
}
|
236
|
-
//初始创建编辑器
|
237
|
-
const createEditor = () => {
|
238
|
-
//创建编辑器
|
239
|
-
editor.value = new AlexEditor(contentRef.value!, {
|
240
|
-
value: value.value,
|
241
|
-
disabled: props.disabled,
|
242
|
-
renderRules: [
|
243
|
-
el => {
|
244
|
-
parseList(editor.value!, el)
|
245
|
-
},
|
246
|
-
el => {
|
247
|
-
orderdListHandle(editor.value!, el)
|
248
|
-
},
|
249
|
-
el => {
|
250
|
-
mediaHandle(editor.value!, el)
|
251
|
-
},
|
252
|
-
el => {
|
253
|
-
tableHandle(editor.value!, el)
|
254
|
-
},
|
255
|
-
el => {
|
256
|
-
preHandle(editor.value!, el, !!(toolbarConfig.value?.use && toolbarConfig.value?.codeBlock?.languages?.show), <(string | LanguagesItemType)[]>toolbarConfig.value?.codeBlock?.languages?.options)
|
257
|
-
},
|
258
|
-
el => {
|
259
|
-
specialInblockHandle(editor.value!, el)
|
260
|
-
},
|
261
|
-
...props.renderRules
|
262
|
-
],
|
263
|
-
allowCopy: props.allowCopy,
|
264
|
-
allowPaste: props.allowPaste,
|
265
|
-
allowCut: props.allowCut,
|
266
|
-
allowPasteHtml: props.allowPasteHtml,
|
267
|
-
customTextPaste: props.customTextPaste,
|
268
|
-
customImagePaste: props.customImagePaste,
|
269
|
-
customVideoPaste: props.customVideoPaste,
|
270
|
-
customFilePaste: props.customFilePaste,
|
271
|
-
customHtmlPaste: handleCustomHtmlPaste,
|
272
|
-
customMerge: handleCustomMerge,
|
273
|
-
customParseNode: handleCustomParseNode
|
274
|
-
})
|
275
|
-
//编辑器渲染后会有一个渲染过程,会改变内容,因此重新获取内容的值来设置value
|
276
|
-
internalModify(editor.value.value)
|
277
|
-
//设置监听事件
|
278
|
-
editor.value.on('change', handleEditorChange)
|
279
|
-
editor.value.on('focus', handleEditorFocus)
|
280
|
-
editor.value.on('blur', handleEditorBlur)
|
281
|
-
editor.value.on('insertParagraph', handleInsertParagraph)
|
282
|
-
editor.value.on('rangeUpdate', handleRangeUpdate)
|
283
|
-
editor.value.on('deleteInStart', handleDeleteInStart)
|
284
|
-
editor.value.on('deleteComplete', handleDeleteComplete)
|
285
|
-
editor.value.on('afterRender', handleAfterRender)
|
286
|
-
//格式化和dom渲染
|
287
|
-
editor.value.formatElementStack()
|
288
|
-
editor.value.domRender()
|
289
|
-
//自动获取焦点
|
290
|
-
if (props.autofocus && !isSourceView.value && !props.disabled) {
|
291
|
-
collapseToEnd()
|
292
|
-
}
|
293
|
-
}
|
294
|
-
//设定编辑器内的视频高度
|
295
|
-
const setVideoHeight = () => {
|
296
|
-
contentRef.value!.querySelectorAll('video').forEach(video => {
|
297
|
-
video.style.height = video.offsetWidth / props.videoRatio + 'px'
|
298
|
-
})
|
299
|
-
}
|
300
|
-
//鼠标在页面按下:处理表格拖拽改变列宽和菜单栏是否使用判断
|
301
|
-
const documentMouseDown = (e: Event) => {
|
302
|
-
if (props.disabled) {
|
303
|
-
return
|
304
|
-
}
|
305
|
-
//鼠标在编辑器内按下
|
306
|
-
if (DapElement.isContains(contentRef.value!, <HTMLElement>e.target)) {
|
307
|
-
const elm = <HTMLElement>e.target
|
308
|
-
const key = DapData.get(elm, 'data-alex-editor-key')
|
309
|
-
if (key) {
|
310
|
-
const element = editor.value!.getElementByKey(key)
|
311
|
-
if (element && element.parsedom == 'td') {
|
312
|
-
const length = element.parent!.children!.length
|
313
|
-
//最后一个td不设置
|
314
|
-
if (element.parent!.children![length - 1].isEqual(element)) {
|
315
|
-
return
|
316
|
-
}
|
317
|
-
const rect = DapElement.getElementBounding(elm)
|
318
|
-
//在可拖拽范围内
|
319
|
-
if ((<MouseEvent>e).pageX >= Math.abs(rect.left + elm.offsetWidth - 5) && (<MouseEvent>e).pageX <= Math.abs(rect.left + elm.offsetWidth + 5)) {
|
320
|
-
tableColumnResizeParams.value.element = element
|
321
|
-
tableColumnResizeParams.value.start = (<MouseEvent>e).pageX
|
322
|
-
}
|
323
|
-
}
|
324
|
-
}
|
325
|
-
}
|
326
|
-
//如果点击了除编辑器外的地方,菜单栏不可使用
|
327
|
-
if (!DapElement.isContains(elRef.value!, <HTMLElement>e.target) && !isSourceView.value) {
|
328
|
-
canUseMenu.value = false
|
329
|
-
}
|
330
|
-
}
|
331
|
-
//鼠标在页面移动:处理表格拖拽改变列宽
|
332
|
-
const documentMouseMove = (e: Event) => {
|
333
|
-
if (props.disabled) {
|
334
|
-
return
|
335
|
-
}
|
336
|
-
if (!tableColumnResizeParams.value.element) {
|
337
|
-
return
|
338
|
-
}
|
339
|
-
const table = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'table')
|
340
|
-
if (!table) {
|
341
|
-
return
|
342
|
-
}
|
343
|
-
const colgroup = table.children!.find(item => {
|
344
|
-
return item.parsedom == 'colgroup'
|
345
|
-
})!
|
346
|
-
const index = tableColumnResizeParams.value.element.parent!.children!.findIndex(el => {
|
347
|
-
return el.isEqual(tableColumnResizeParams.value.element!)
|
348
|
-
})
|
349
|
-
const width = `${tableColumnResizeParams.value.element.elm!.offsetWidth + (<MouseEvent>e).pageX - tableColumnResizeParams.value.start}`
|
350
|
-
colgroup.children![index].marks!['width'] = width
|
351
|
-
colgroup.children![index].elm!.setAttribute('width', width)
|
352
|
-
tableColumnResizeParams.value.start = (<MouseEvent>e).pageX
|
353
|
-
}
|
354
|
-
//鼠标在页面松开:处理表格拖拽改变列宽
|
355
|
-
const documentMouseUp = () => {
|
356
|
-
if (props.disabled) {
|
357
|
-
return
|
358
|
-
}
|
359
|
-
if (!tableColumnResizeParams.value.element) {
|
360
|
-
return
|
361
|
-
}
|
362
|
-
const table = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'table')
|
363
|
-
if (!table) {
|
364
|
-
return
|
365
|
-
}
|
366
|
-
const colgroup = table.children!.find(item => {
|
367
|
-
return item.parsedom == 'colgroup'
|
368
|
-
})!
|
369
|
-
const index = tableColumnResizeParams.value.element.parent!.children!.findIndex(el => {
|
370
|
-
return el.isEqual(tableColumnResizeParams.value.element!)
|
371
|
-
})
|
372
|
-
const width = Number(colgroup.children![index].marks!['width'])
|
373
|
-
if (!isNaN(width)) {
|
374
|
-
colgroup.children![index].marks!['width'] = `${Number(((width / tableColumnResizeParams.value.element.parent!.elm!.offsetWidth) * 100).toFixed(2))}%`
|
375
|
-
editor.value!.formatElementStack()
|
376
|
-
editor.value!.domRender()
|
377
|
-
editor.value!.rangeRender()
|
378
|
-
}
|
379
|
-
tableColumnResizeParams.value.element = null
|
380
|
-
tableColumnResizeParams.value.start = 0
|
381
|
-
}
|
382
|
-
//鼠标点击页面:处理任务列表复选框勾选
|
383
|
-
const documentClick = (e: Event) => {
|
384
|
-
if (props.disabled) {
|
385
|
-
return
|
386
|
-
}
|
387
|
-
//鼠标在编辑器内点击
|
388
|
-
if (DapElement.isContains(contentRef.value!, <HTMLElement>e.target)) {
|
389
|
-
const elm = <HTMLElement>e.target
|
390
|
-
const key = DapData.get(elm, 'data-alex-editor-key')
|
391
|
-
if (key) {
|
392
|
-
const element = editor.value!.getElementByKey(key)!
|
393
|
-
//如果是任务列表元素
|
394
|
-
if (isTask(element)) {
|
395
|
-
const rect = DapElement.getElementBounding(elm)
|
396
|
-
//在复选框范围内
|
397
|
-
if ((<MouseEvent>e).pageX >= Math.abs(rect.left) && (<MouseEvent>e).pageX <= Math.abs(rect.left + 16) && (<MouseEvent>e).pageY >= Math.abs(rect.top + elm.offsetHeight / 2 - 8) && (<MouseEvent>e).pageY <= Math.abs(rect.top + elm.offsetHeight / 2 + 8)) {
|
398
|
-
//取消勾选
|
399
|
-
if (element.marks!['data-editify-task'] == 'checked') {
|
400
|
-
element.marks!['data-editify-task'] = 'uncheck'
|
401
|
-
}
|
402
|
-
//勾选
|
403
|
-
else {
|
404
|
-
element.marks!['data-editify-task'] = 'checked'
|
405
|
-
}
|
406
|
-
if (!editor.value!.range) {
|
407
|
-
editor.value!.initRange()
|
408
|
-
}
|
409
|
-
editor.value!.range!.anchor.moveToEnd(element)
|
410
|
-
editor.value!.range!.focus.moveToEnd(element)
|
411
|
-
editor.value!.formatElementStack()
|
412
|
-
editor.value!.domRender()
|
413
|
-
editor.value!.rangeRender()
|
414
|
-
}
|
415
|
-
}
|
416
|
-
}
|
417
|
-
}
|
418
|
-
}
|
419
|
-
//重新定义编辑器粘贴html
|
420
|
-
const handleCustomHtmlPaste = async (elements: AlexElement[]) => {
|
421
|
-
const keepStyles = Object.assign(pasteKeepData.styles, props.pasteKeepStyles || {})
|
422
|
-
const keepMarks = Object.assign(pasteKeepData.marks, props.pasteKeepMarks || {})
|
423
|
-
//粘贴html时过滤元素的样式和属性
|
424
|
-
AlexElement.flatElements(elements).forEach(el => {
|
425
|
-
let marks: ObjectType = {}
|
426
|
-
let styles: ObjectType = {}
|
427
|
-
if (el.hasMarks()) {
|
428
|
-
for (let key in keepMarks) {
|
429
|
-
if (el.marks!.hasOwnProperty(key) && ((Array.isArray(keepMarks[key]) && keepMarks[key].includes(el.parsedom)) || keepMarks[key] == '*')) {
|
430
|
-
marks[key] = el.marks![key]
|
431
|
-
}
|
432
|
-
}
|
433
|
-
el.marks = marks
|
434
|
-
}
|
435
|
-
if (el.hasStyles() && !el.isText()) {
|
436
|
-
for (let key in keepStyles) {
|
437
|
-
if (el.styles!.hasOwnProperty(key) && ((Array.isArray(keepStyles[key]) && keepStyles[key].includes(el.parsedom)) || keepStyles[key] == '*')) {
|
438
|
-
styles[key] = el.styles![key]
|
439
|
-
}
|
440
|
-
}
|
441
|
-
el.styles = styles
|
442
|
-
}
|
443
|
-
})
|
444
|
-
//如果使用了自定义粘贴html的功能
|
445
|
-
if (typeof props.customHtmlPaste == 'function') {
|
446
|
-
await props.customHtmlPaste.apply(this, [elements])
|
447
|
-
}
|
448
|
-
//默认粘贴html
|
449
|
-
else {
|
450
|
-
for (let i = 0; i < elements.length; i++) {
|
451
|
-
editor.value!.insertElement(elements[i], false)
|
452
|
-
}
|
453
|
-
}
|
454
|
-
}
|
455
|
-
//重新定义编辑器合并元素的逻辑
|
456
|
-
const handleCustomMerge = (ele: AlexElement, preEle: AlexElement) => {
|
457
|
-
const uneditable = preEle.getUneditableElement()
|
458
|
-
if (uneditable) {
|
459
|
-
uneditable.toEmpty()
|
460
|
-
} else {
|
461
|
-
preEle.children!.push(...ele.children!)
|
462
|
-
preEle.children!.forEach(item => {
|
463
|
-
item.parent = preEle
|
464
|
-
})
|
465
|
-
ele.children = null
|
466
|
-
}
|
467
|
-
}
|
468
|
-
//针对node转为元素进行额外的处理
|
469
|
-
const handleCustomParseNode = (ele: AlexElement) => {
|
470
|
-
if (ele.parsedom == 'code') {
|
471
|
-
ele.parsedom = 'span'
|
472
|
-
const marks = {
|
473
|
-
'data-editify-code': true
|
474
|
-
}
|
475
|
-
if (ele.hasMarks()) {
|
476
|
-
Object.assign(ele.marks!, marks)
|
477
|
-
} else {
|
478
|
-
ele.marks = marks
|
479
|
-
}
|
480
|
-
}
|
481
|
-
if (typeof props.customParseNode == 'function') {
|
482
|
-
ele = props.customParseNode.apply(instance.proxy, [ele])
|
483
|
-
}
|
484
|
-
return ele
|
485
|
-
}
|
486
|
-
//编辑区域键盘按下:设置缩进快捷键
|
487
|
-
const handleEditorKeydown = (e: Event) => {
|
488
|
-
if (props.disabled) {
|
489
|
-
return
|
490
|
-
}
|
491
|
-
//单独按下tab键
|
492
|
-
if ((<KeyboardEvent>e).key.toLocaleLowerCase() == 'tab' && !(<KeyboardEvent>e).metaKey && !(<KeyboardEvent>e).shiftKey && !(<KeyboardEvent>e).ctrlKey && !(<KeyboardEvent>e).altKey && props.tab) {
|
493
|
-
e.preventDefault()
|
494
|
-
editor.value!.insertText(' ')
|
495
|
-
editor.value!.formatElementStack()
|
496
|
-
editor.value!.domRender()
|
497
|
-
editor.value!.rangeRender()
|
498
|
-
}
|
499
|
-
//自定义键盘按下操作
|
500
|
-
emits('keydown', e)
|
501
|
-
}
|
502
|
-
//点击编辑器:处理图片和视频的光标聚集
|
503
|
-
const handleEditorClick = (e: Event) => {
|
504
|
-
if (props.disabled || isSourceView.value) {
|
505
|
-
return
|
506
|
-
}
|
507
|
-
const node = <HTMLElement>e.target
|
508
|
-
//点击的是图片或者视频
|
509
|
-
if (node.nodeName.toLocaleLowerCase() == 'img' || node.nodeName.toLocaleLowerCase() == 'video') {
|
510
|
-
const key = Number(node.getAttribute('data-editify-element'))
|
511
|
-
if (DapNumber.isNumber(key)) {
|
512
|
-
const element = editor.value!.getElementByKey(key)!
|
513
|
-
if (!editor.value!.range) {
|
514
|
-
editor.value!.initRange()
|
515
|
-
}
|
516
|
-
editor.value!.range!.anchor.moveToStart(element)
|
517
|
-
editor.value!.range!.focus.moveToEnd(element)
|
518
|
-
editor.value!.rangeRender()
|
519
|
-
}
|
520
|
-
}
|
521
|
-
}
|
522
|
-
//编辑器的值更新
|
523
|
-
const handleEditorChange = (newVal: string, oldVal: string) => {
|
524
|
-
if (props.disabled) {
|
525
|
-
return
|
526
|
-
}
|
527
|
-
//内部修改
|
528
|
-
internalModify(newVal)
|
529
|
-
//触发change事件
|
530
|
-
emits('change', newVal, oldVal)
|
531
|
-
}
|
532
|
-
//编辑器失去焦点
|
533
|
-
const handleEditorBlur = (val: string) => {
|
534
|
-
if (props.disabled) {
|
535
|
-
return
|
536
|
-
}
|
537
|
-
if (props.border && props.color && !isFullScreen.value) {
|
538
|
-
//恢复编辑区域边框颜色
|
539
|
-
bodyRef.value!.style.borderColor = ''
|
540
|
-
//恢复编辑区域阴影颜色
|
541
|
-
bodyRef.value!.style.boxShadow = ''
|
542
|
-
//使用菜单栏的情况下恢复菜单栏的样式
|
543
|
-
if (menuConfig.value.use) {
|
544
|
-
menuRef.value!.$el.style.borderColor = ''
|
545
|
-
menuRef.value!.$el.style.boxShadow = ''
|
546
|
-
}
|
547
|
-
}
|
548
|
-
emits('blur', val)
|
549
|
-
}
|
550
|
-
//编辑器获取焦点
|
551
|
-
const handleEditorFocus = (val: string) => {
|
552
|
-
if (props.disabled) {
|
553
|
-
return
|
554
|
-
}
|
555
|
-
if (props.border && props.color && !isFullScreen.value) {
|
556
|
-
//编辑区域边框颜色
|
557
|
-
bodyRef.value!.style.borderColor = props.color
|
558
|
-
//转换颜色值
|
559
|
-
const rgb = DapColor.hex2rgb(props.color)
|
560
|
-
//菜单栏模式为inner
|
561
|
-
if (menuConfig.value.use && menuConfig.value.mode == 'inner') {
|
562
|
-
//编辑区域除顶部边框的阴影
|
563
|
-
bodyRef.value!.style.boxShadow = `0 8px 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5),8px 0 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5), -8px 0 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
|
564
|
-
//菜单栏的边框颜色
|
565
|
-
menuRef.value!.$el.style.borderColor = props.color
|
566
|
-
//菜单栏除底部边框的阴影
|
567
|
-
menuRef.value!.$el.style.boxShadow = `0 -8px 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5),8px 0 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5), -8px 0 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
|
568
|
-
}
|
569
|
-
//其他菜单栏模式
|
570
|
-
else if (menuConfig.value.use) {
|
571
|
-
//编辑区域四边阴影
|
572
|
-
bodyRef.value!.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
|
573
|
-
}
|
574
|
-
//不使用菜单栏
|
575
|
-
else {
|
576
|
-
//编辑区域四边阴影
|
577
|
-
bodyRef.value!.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
|
578
|
-
}
|
579
|
-
}
|
580
|
-
//获取焦点时可以使用菜单栏
|
581
|
-
setTimeout(() => {
|
582
|
-
canUseMenu.value = true
|
583
|
-
emits('focus', val)
|
584
|
-
}, 0)
|
585
|
-
}
|
586
|
-
//编辑器换行
|
587
|
-
const handleInsertParagraph = (element: AlexElement, previousElement: AlexElement) => {
|
588
|
-
|
589
|
-
if (
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
}
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
}
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
if (
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
}
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
if (
|
644
|
-
|
645
|
-
}
|
646
|
-
}
|
647
|
-
|
648
|
-
const
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
}
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
}
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
)
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
)
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
})
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
DapEvent.
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
1
|
+
<template>
|
2
|
+
<div class="editify" :class="{ fullscreen: isFullScreen, autoheight: !isFullScreen && autoheight }" ref="elRef">
|
3
|
+
<!-- 菜单区域 -->
|
4
|
+
<Menu v-if="menuConfig.use" :config="menuConfig" :color="color" ref="menuRef"></Menu>
|
5
|
+
<!-- 编辑层,与编辑区域宽高相同必须适配 -->
|
6
|
+
<div ref="bodyRef" class="editify-body" :class="{ border: showBorder, menu_inner: menuConfig.use && menuConfig.mode == 'inner' }" :data-editify-uid="instance.uid">
|
7
|
+
<!-- 编辑器 -->
|
8
|
+
<div ref="contentRef" class="editify-content" :class="{ placeholder: showPlaceholder, disabled: disabled }" @keydown="handleEditorKeydown" @click="handleEditorClick" @compositionstart="isInputChinese = true" @compositionend="isInputChinese = false" :data-editify-placeholder="placeholder"></div>
|
9
|
+
<!-- 代码视图 -->
|
10
|
+
<textarea v-if="isSourceView" :value="value" readonly class="editify-source" />
|
11
|
+
<!-- 工具条 -->
|
12
|
+
<Toolbar ref="toolbarRef" v-model="toolbarOptions.show" :node="toolbarOptions.node!" :type="toolbarOptions.type" :config="toolbarConfig" :color="color"></Toolbar>
|
13
|
+
</div>
|
14
|
+
<!-- 编辑器尾部 -->
|
15
|
+
<div v-if="showWordLength" class="editify-footer" :class="{ fullscreen: isFullScreen && !isSourceView }" ref="footerRef">
|
16
|
+
<!-- 字数统计 -->
|
17
|
+
<div class="editify-footer-words">{{ $editTrans('totalWordCount') }}{{ textValue.length }}</div>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
</template>
|
21
|
+
<script setup lang="ts">
|
22
|
+
import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
23
|
+
import { AlexEditor, AlexElement, AlexElementRangeType, AlexElementsRangeType } from 'alex-editor'
|
24
|
+
import { element as DapElement, event as DapEvent, data as DapData, number as DapNumber, color as DapColor } from 'dap-util'
|
25
|
+
import { pasteKeepData, mergeObject, getToolbarConfig, getMenuConfig, MenuConfigType, ObjectType, ToolbarConfigType } from '../core/tool'
|
26
|
+
import { parseList, orderdListHandle, mediaHandle, tableHandle, preHandle, specialInblockHandle } from '../core/rule'
|
27
|
+
import { isTask, elementToParagraph, getCurrentParsedomElement, hasTableInRange, hasLinkInRange, hasPreInRange, hasImageInRange, hasVideoInRange } from '../core/function'
|
28
|
+
import Toolbar from '../components/toolbar/toolbar.vue'
|
29
|
+
import Menu from '../components/menu/menu.vue'
|
30
|
+
import Layer from '../components/layer/layer.vue'
|
31
|
+
import { EditifyProps, EditifyTableColumnResizeParamsType, EditifyToolbarOptionsType } from './props'
|
32
|
+
import { trans } from '../locale'
|
33
|
+
import { LanguagesItemType } from '../hljs'
|
34
|
+
|
35
|
+
//定义组件名称
|
36
|
+
defineOptions({
|
37
|
+
name: 'editify'
|
38
|
+
})
|
39
|
+
//获取实例
|
40
|
+
const instance = getCurrentInstance()!
|
41
|
+
//属性
|
42
|
+
const props = defineProps(EditifyProps)
|
43
|
+
//事件
|
44
|
+
const emits = defineEmits(['update:modelValue', 'focus', 'blur', 'change', 'keydown', 'insertparagraph', 'rangeupdate', 'updateview'])
|
45
|
+
|
46
|
+
//设置国际化方法
|
47
|
+
const $editTrans = trans(props.locale || 'zh_CN')
|
48
|
+
//对子孙后代组件提供国际化方法
|
49
|
+
provide('$editTrans', $editTrans)
|
50
|
+
|
51
|
+
//是否编辑器内部修改值
|
52
|
+
const isModelChange = ref<boolean>(false)
|
53
|
+
//是否正在输入中文
|
54
|
+
const isInputChinese = ref<boolean>(false)
|
55
|
+
//工具条和菜单栏判定延时器
|
56
|
+
const rangeUpdateTimer = ref<any>(null)
|
57
|
+
//表格列宽拖拽记录数据
|
58
|
+
const tableColumnResizeParams = ref<EditifyTableColumnResizeParamsType>({
|
59
|
+
element: null, //被拖拽的td
|
60
|
+
start: 0 //水平方向起点位置
|
61
|
+
})
|
62
|
+
//工具条参数配置
|
63
|
+
const toolbarOptions = ref<EditifyToolbarOptionsType>({
|
64
|
+
//是否显示工具条
|
65
|
+
show: false,
|
66
|
+
//关联元素
|
67
|
+
node: null,
|
68
|
+
//类型
|
69
|
+
type: 'text'
|
70
|
+
})
|
71
|
+
|
72
|
+
const menuRef = ref<InstanceType<typeof Menu> | null>(null)
|
73
|
+
const bodyRef = ref<HTMLElement | null>(null)
|
74
|
+
const contentRef = ref<HTMLElement | null>(null)
|
75
|
+
const toolbarRef = ref<InstanceType<typeof Toolbar> | null>(null)
|
76
|
+
const footerRef = ref<HTMLElement | null>(null)
|
77
|
+
const elRef = ref<HTMLElement | null>(null)
|
78
|
+
|
79
|
+
//编辑器对象
|
80
|
+
const editor = ref<AlexEditor | null>(null)
|
81
|
+
//是否代码视图
|
82
|
+
const isSourceView = ref<boolean>(false)
|
83
|
+
//是否全屏
|
84
|
+
const isFullScreen = ref<boolean>(false)
|
85
|
+
//菜单栏是否可以使用标识
|
86
|
+
const canUseMenu = ref<boolean>(false)
|
87
|
+
//光标选取范围内的元素数组
|
88
|
+
const dataRangeCaches = ref<AlexElementsRangeType>({
|
89
|
+
flatList: [],
|
90
|
+
list: []
|
91
|
+
})
|
92
|
+
|
93
|
+
//编辑器的值
|
94
|
+
const value = computed<string>({
|
95
|
+
set(val) {
|
96
|
+
emits('update:modelValue', val)
|
97
|
+
},
|
98
|
+
get() {
|
99
|
+
return props.modelValue || '<p><br></p>'
|
100
|
+
}
|
101
|
+
})
|
102
|
+
//编辑器的纯文本值
|
103
|
+
const textValue = computed<string>(() => {
|
104
|
+
return (<HTMLElement>DapElement.string2dom(`<div>${value.value}</div>`)).innerText
|
105
|
+
})
|
106
|
+
//是否显示占位符
|
107
|
+
const showPlaceholder = computed<boolean>(() => {
|
108
|
+
if (editor.value) {
|
109
|
+
if (value.value && editor.value.stack.length == 1 && editor.value.stack[0].type == 'block' && editor.value.stack[0].parsedom == AlexElement.BLOCK_NODE && editor.value.stack[0].isOnlyHasBreak() && !editor.value.stack[0].hasStyles() && !editor.value.stack[0].hasMarks()) {
|
110
|
+
return !isInputChinese.value
|
111
|
+
}
|
112
|
+
}
|
113
|
+
return false
|
114
|
+
})
|
115
|
+
//是否显示边框
|
116
|
+
const showBorder = computed<boolean>(() => {
|
117
|
+
//全屏模式下不显示边框
|
118
|
+
if (isFullScreen.value) {
|
119
|
+
return false
|
120
|
+
}
|
121
|
+
return props.border
|
122
|
+
})
|
123
|
+
//最终生效的工具栏配置
|
124
|
+
const toolbarConfig = computed<ToolbarConfigType>(() => {
|
125
|
+
return <ToolbarConfigType>mergeObject(getToolbarConfig($editTrans, props.locale), props.toolbar || {})
|
126
|
+
})
|
127
|
+
//最终生效的菜单栏配置
|
128
|
+
const menuConfig = computed<MenuConfigType>(() => {
|
129
|
+
return <MenuConfigType>mergeObject(getMenuConfig($editTrans, props.locale), props.menu || {})
|
130
|
+
})
|
131
|
+
|
132
|
+
//编辑器内部修改值的方法
|
133
|
+
const internalModify = (val: string) => {
|
134
|
+
isModelChange.value = true
|
135
|
+
value.value = val
|
136
|
+
nextTick(() => {
|
137
|
+
isModelChange.value = false
|
138
|
+
})
|
139
|
+
}
|
140
|
+
//隐藏工具条
|
141
|
+
const hideToolbar = () => {
|
142
|
+
toolbarOptions.value.show = false
|
143
|
+
toolbarOptions.value.node = null
|
144
|
+
}
|
145
|
+
//监听滚动隐藏工具条
|
146
|
+
const handleScroll = () => {
|
147
|
+
const setScroll = (el: HTMLElement) => {
|
148
|
+
DapEvent.on(el, `scroll.editify_${instance.uid}`, () => {
|
149
|
+
if (toolbarConfig.value.use && toolbarOptions.value.show) {
|
150
|
+
hideToolbar()
|
151
|
+
}
|
152
|
+
})
|
153
|
+
if (el.parentNode) {
|
154
|
+
setScroll(<HTMLElement>el.parentNode)
|
155
|
+
}
|
156
|
+
}
|
157
|
+
setScroll(contentRef.value!)
|
158
|
+
}
|
159
|
+
//移除上述滚动事件的监听
|
160
|
+
const removeScrollHandle = () => {
|
161
|
+
const removeScroll = (el: HTMLElement) => {
|
162
|
+
DapEvent.off(el, `scroll.editify_${instance.uid}`)
|
163
|
+
if (el.parentNode) {
|
164
|
+
removeScroll(<HTMLElement>el.parentNode)
|
165
|
+
}
|
166
|
+
}
|
167
|
+
removeScroll(contentRef.value!)
|
168
|
+
}
|
169
|
+
//工具条显示判断
|
170
|
+
const handleToolbar = () => {
|
171
|
+
if (props.disabled || isSourceView.value) {
|
172
|
+
return
|
173
|
+
}
|
174
|
+
hideToolbar()
|
175
|
+
nextTick(() => {
|
176
|
+
const table = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'table')
|
177
|
+
const pre = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'pre')
|
178
|
+
const link = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'a')
|
179
|
+
const image = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'img')
|
180
|
+
const video = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'video')
|
181
|
+
if (link) {
|
182
|
+
toolbarOptions.value.type = 'link'
|
183
|
+
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${link.key}"]`
|
184
|
+
if (toolbarOptions.value.show) {
|
185
|
+
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
186
|
+
} else {
|
187
|
+
toolbarOptions.value.show = true
|
188
|
+
}
|
189
|
+
} else if (image) {
|
190
|
+
toolbarOptions.value.type = 'image'
|
191
|
+
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${image.key}"]`
|
192
|
+
if (toolbarOptions.value.show) {
|
193
|
+
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
194
|
+
} else {
|
195
|
+
toolbarOptions.value.show = true
|
196
|
+
}
|
197
|
+
} else if (video) {
|
198
|
+
toolbarOptions.value.type = 'video'
|
199
|
+
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${video.key}"]`
|
200
|
+
if (toolbarOptions.value.show) {
|
201
|
+
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
202
|
+
} else {
|
203
|
+
toolbarOptions.value.show = true
|
204
|
+
}
|
205
|
+
} else if (table) {
|
206
|
+
toolbarOptions.value.type = 'table'
|
207
|
+
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${table.key}"]`
|
208
|
+
if (toolbarOptions.value.show) {
|
209
|
+
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
210
|
+
} else {
|
211
|
+
toolbarOptions.value.show = true
|
212
|
+
}
|
213
|
+
} else if (pre) {
|
214
|
+
toolbarOptions.value.type = 'codeBlock'
|
215
|
+
toolbarOptions.value.node = `[data-editify-uid="${instance.uid}"] [data-editify-element="${pre.key}"]`
|
216
|
+
if (toolbarOptions.value.show) {
|
217
|
+
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
218
|
+
} else {
|
219
|
+
toolbarOptions.value.show = true
|
220
|
+
}
|
221
|
+
} else {
|
222
|
+
const result = dataRangeCaches.value.flatList.filter((item: AlexElementRangeType) => {
|
223
|
+
return item.element.isText()
|
224
|
+
})
|
225
|
+
if (result.length && !hasTableInRange(editor.value!, dataRangeCaches.value) && !hasPreInRange(editor.value!, dataRangeCaches.value) && !hasLinkInRange(editor.value!, dataRangeCaches.value) && !hasImageInRange(editor.value!, dataRangeCaches.value) && !hasVideoInRange(editor.value!, dataRangeCaches.value)) {
|
226
|
+
toolbarOptions.value.type = 'text'
|
227
|
+
if (toolbarOptions.value.show) {
|
228
|
+
;(<InstanceType<typeof Layer>>toolbarRef.value!.$refs.layerRef).setPosition()
|
229
|
+
} else {
|
230
|
+
toolbarOptions.value.show = true
|
231
|
+
}
|
232
|
+
}
|
233
|
+
}
|
234
|
+
})
|
235
|
+
}
|
236
|
+
//初始创建编辑器
|
237
|
+
const createEditor = () => {
|
238
|
+
//创建编辑器
|
239
|
+
editor.value = new AlexEditor(contentRef.value!, {
|
240
|
+
value: value.value,
|
241
|
+
disabled: props.disabled,
|
242
|
+
renderRules: [
|
243
|
+
el => {
|
244
|
+
parseList(editor.value!, el)
|
245
|
+
},
|
246
|
+
el => {
|
247
|
+
orderdListHandle(editor.value!, el)
|
248
|
+
},
|
249
|
+
el => {
|
250
|
+
mediaHandle(editor.value!, el)
|
251
|
+
},
|
252
|
+
el => {
|
253
|
+
tableHandle(editor.value!, el)
|
254
|
+
},
|
255
|
+
el => {
|
256
|
+
preHandle(editor.value!, el, !!(toolbarConfig.value?.use && toolbarConfig.value?.codeBlock?.languages?.show), <(string | LanguagesItemType)[]>toolbarConfig.value?.codeBlock?.languages?.options)
|
257
|
+
},
|
258
|
+
el => {
|
259
|
+
specialInblockHandle(editor.value!, el)
|
260
|
+
},
|
261
|
+
...props.renderRules
|
262
|
+
],
|
263
|
+
allowCopy: props.allowCopy,
|
264
|
+
allowPaste: props.allowPaste,
|
265
|
+
allowCut: props.allowCut,
|
266
|
+
allowPasteHtml: props.allowPasteHtml,
|
267
|
+
customTextPaste: props.customTextPaste,
|
268
|
+
customImagePaste: props.customImagePaste,
|
269
|
+
customVideoPaste: props.customVideoPaste,
|
270
|
+
customFilePaste: props.customFilePaste,
|
271
|
+
customHtmlPaste: handleCustomHtmlPaste,
|
272
|
+
customMerge: handleCustomMerge,
|
273
|
+
customParseNode: handleCustomParseNode
|
274
|
+
})
|
275
|
+
//编辑器渲染后会有一个渲染过程,会改变内容,因此重新获取内容的值来设置value
|
276
|
+
internalModify(editor.value.value)
|
277
|
+
//设置监听事件
|
278
|
+
editor.value.on('change', handleEditorChange)
|
279
|
+
editor.value.on('focus', handleEditorFocus)
|
280
|
+
editor.value.on('blur', handleEditorBlur)
|
281
|
+
editor.value.on('insertParagraph', handleInsertParagraph)
|
282
|
+
editor.value.on('rangeUpdate', handleRangeUpdate)
|
283
|
+
editor.value.on('deleteInStart', handleDeleteInStart)
|
284
|
+
editor.value.on('deleteComplete', handleDeleteComplete)
|
285
|
+
editor.value.on('afterRender', handleAfterRender)
|
286
|
+
//格式化和dom渲染
|
287
|
+
editor.value.formatElementStack()
|
288
|
+
editor.value.domRender()
|
289
|
+
//自动获取焦点
|
290
|
+
if (props.autofocus && !isSourceView.value && !props.disabled) {
|
291
|
+
collapseToEnd()
|
292
|
+
}
|
293
|
+
}
|
294
|
+
//设定编辑器内的视频高度
|
295
|
+
const setVideoHeight = () => {
|
296
|
+
contentRef.value!.querySelectorAll('video').forEach(video => {
|
297
|
+
video.style.height = video.offsetWidth / props.videoRatio + 'px'
|
298
|
+
})
|
299
|
+
}
|
300
|
+
//鼠标在页面按下:处理表格拖拽改变列宽和菜单栏是否使用判断
|
301
|
+
const documentMouseDown = (e: Event) => {
|
302
|
+
if (props.disabled) {
|
303
|
+
return
|
304
|
+
}
|
305
|
+
//鼠标在编辑器内按下
|
306
|
+
if (DapElement.isContains(contentRef.value!, <HTMLElement>e.target)) {
|
307
|
+
const elm = <HTMLElement>e.target
|
308
|
+
const key = DapData.get(elm, 'data-alex-editor-key')
|
309
|
+
if (key) {
|
310
|
+
const element = editor.value!.getElementByKey(key)
|
311
|
+
if (element && element.parsedom == 'td') {
|
312
|
+
const length = element.parent!.children!.length
|
313
|
+
//最后一个td不设置
|
314
|
+
if (element.parent!.children![length - 1].isEqual(element)) {
|
315
|
+
return
|
316
|
+
}
|
317
|
+
const rect = DapElement.getElementBounding(elm)
|
318
|
+
//在可拖拽范围内
|
319
|
+
if ((<MouseEvent>e).pageX >= Math.abs(rect.left + elm.offsetWidth - 5) && (<MouseEvent>e).pageX <= Math.abs(rect.left + elm.offsetWidth + 5)) {
|
320
|
+
tableColumnResizeParams.value.element = element
|
321
|
+
tableColumnResizeParams.value.start = (<MouseEvent>e).pageX
|
322
|
+
}
|
323
|
+
}
|
324
|
+
}
|
325
|
+
}
|
326
|
+
//如果点击了除编辑器外的地方,菜单栏不可使用
|
327
|
+
if (!DapElement.isContains(elRef.value!, <HTMLElement>e.target) && !isSourceView.value) {
|
328
|
+
canUseMenu.value = false
|
329
|
+
}
|
330
|
+
}
|
331
|
+
//鼠标在页面移动:处理表格拖拽改变列宽
|
332
|
+
const documentMouseMove = (e: Event) => {
|
333
|
+
if (props.disabled) {
|
334
|
+
return
|
335
|
+
}
|
336
|
+
if (!tableColumnResizeParams.value.element) {
|
337
|
+
return
|
338
|
+
}
|
339
|
+
const table = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'table')
|
340
|
+
if (!table) {
|
341
|
+
return
|
342
|
+
}
|
343
|
+
const colgroup = table.children!.find(item => {
|
344
|
+
return item.parsedom == 'colgroup'
|
345
|
+
})!
|
346
|
+
const index = tableColumnResizeParams.value.element.parent!.children!.findIndex(el => {
|
347
|
+
return el.isEqual(tableColumnResizeParams.value.element!)
|
348
|
+
})
|
349
|
+
const width = `${tableColumnResizeParams.value.element.elm!.offsetWidth + (<MouseEvent>e).pageX - tableColumnResizeParams.value.start}`
|
350
|
+
colgroup.children![index].marks!['width'] = width
|
351
|
+
colgroup.children![index].elm!.setAttribute('width', width)
|
352
|
+
tableColumnResizeParams.value.start = (<MouseEvent>e).pageX
|
353
|
+
}
|
354
|
+
//鼠标在页面松开:处理表格拖拽改变列宽
|
355
|
+
const documentMouseUp = () => {
|
356
|
+
if (props.disabled) {
|
357
|
+
return
|
358
|
+
}
|
359
|
+
if (!tableColumnResizeParams.value.element) {
|
360
|
+
return
|
361
|
+
}
|
362
|
+
const table = getCurrentParsedomElement(editor.value!, dataRangeCaches.value, 'table')
|
363
|
+
if (!table) {
|
364
|
+
return
|
365
|
+
}
|
366
|
+
const colgroup = table.children!.find(item => {
|
367
|
+
return item.parsedom == 'colgroup'
|
368
|
+
})!
|
369
|
+
const index = tableColumnResizeParams.value.element.parent!.children!.findIndex(el => {
|
370
|
+
return el.isEqual(tableColumnResizeParams.value.element!)
|
371
|
+
})
|
372
|
+
const width = Number(colgroup.children![index].marks!['width'])
|
373
|
+
if (!isNaN(width)) {
|
374
|
+
colgroup.children![index].marks!['width'] = `${Number(((width / tableColumnResizeParams.value.element.parent!.elm!.offsetWidth) * 100).toFixed(2))}%`
|
375
|
+
editor.value!.formatElementStack()
|
376
|
+
editor.value!.domRender()
|
377
|
+
editor.value!.rangeRender()
|
378
|
+
}
|
379
|
+
tableColumnResizeParams.value.element = null
|
380
|
+
tableColumnResizeParams.value.start = 0
|
381
|
+
}
|
382
|
+
//鼠标点击页面:处理任务列表复选框勾选
|
383
|
+
const documentClick = (e: Event) => {
|
384
|
+
if (props.disabled) {
|
385
|
+
return
|
386
|
+
}
|
387
|
+
//鼠标在编辑器内点击
|
388
|
+
if (DapElement.isContains(contentRef.value!, <HTMLElement>e.target)) {
|
389
|
+
const elm = <HTMLElement>e.target
|
390
|
+
const key = DapData.get(elm, 'data-alex-editor-key')
|
391
|
+
if (key) {
|
392
|
+
const element = editor.value!.getElementByKey(key)!
|
393
|
+
//如果是任务列表元素
|
394
|
+
if (isTask(element)) {
|
395
|
+
const rect = DapElement.getElementBounding(elm)
|
396
|
+
//在复选框范围内
|
397
|
+
if ((<MouseEvent>e).pageX >= Math.abs(rect.left) && (<MouseEvent>e).pageX <= Math.abs(rect.left + 16) && (<MouseEvent>e).pageY >= Math.abs(rect.top + elm.offsetHeight / 2 - 8) && (<MouseEvent>e).pageY <= Math.abs(rect.top + elm.offsetHeight / 2 + 8)) {
|
398
|
+
//取消勾选
|
399
|
+
if (element.marks!['data-editify-task'] == 'checked') {
|
400
|
+
element.marks!['data-editify-task'] = 'uncheck'
|
401
|
+
}
|
402
|
+
//勾选
|
403
|
+
else {
|
404
|
+
element.marks!['data-editify-task'] = 'checked'
|
405
|
+
}
|
406
|
+
if (!editor.value!.range) {
|
407
|
+
editor.value!.initRange()
|
408
|
+
}
|
409
|
+
editor.value!.range!.anchor.moveToEnd(element)
|
410
|
+
editor.value!.range!.focus.moveToEnd(element)
|
411
|
+
editor.value!.formatElementStack()
|
412
|
+
editor.value!.domRender()
|
413
|
+
editor.value!.rangeRender()
|
414
|
+
}
|
415
|
+
}
|
416
|
+
}
|
417
|
+
}
|
418
|
+
}
|
419
|
+
//重新定义编辑器粘贴html
|
420
|
+
const handleCustomHtmlPaste = async (elements: AlexElement[]) => {
|
421
|
+
const keepStyles = Object.assign(pasteKeepData.styles, props.pasteKeepStyles || {})
|
422
|
+
const keepMarks = Object.assign(pasteKeepData.marks, props.pasteKeepMarks || {})
|
423
|
+
//粘贴html时过滤元素的样式和属性
|
424
|
+
AlexElement.flatElements(elements).forEach(el => {
|
425
|
+
let marks: ObjectType = {}
|
426
|
+
let styles: ObjectType = {}
|
427
|
+
if (el.hasMarks()) {
|
428
|
+
for (let key in keepMarks) {
|
429
|
+
if (el.marks!.hasOwnProperty(key) && ((Array.isArray(keepMarks[key]) && keepMarks[key].includes(el.parsedom)) || keepMarks[key] == '*')) {
|
430
|
+
marks[key] = el.marks![key]
|
431
|
+
}
|
432
|
+
}
|
433
|
+
el.marks = marks
|
434
|
+
}
|
435
|
+
if (el.hasStyles() && !el.isText()) {
|
436
|
+
for (let key in keepStyles) {
|
437
|
+
if (el.styles!.hasOwnProperty(key) && ((Array.isArray(keepStyles[key]) && keepStyles[key].includes(el.parsedom)) || keepStyles[key] == '*')) {
|
438
|
+
styles[key] = el.styles![key]
|
439
|
+
}
|
440
|
+
}
|
441
|
+
el.styles = styles
|
442
|
+
}
|
443
|
+
})
|
444
|
+
//如果使用了自定义粘贴html的功能
|
445
|
+
if (typeof props.customHtmlPaste == 'function') {
|
446
|
+
await props.customHtmlPaste.apply(this, [elements])
|
447
|
+
}
|
448
|
+
//默认粘贴html
|
449
|
+
else {
|
450
|
+
for (let i = 0; i < elements.length; i++) {
|
451
|
+
editor.value!.insertElement(elements[i], false)
|
452
|
+
}
|
453
|
+
}
|
454
|
+
}
|
455
|
+
//重新定义编辑器合并元素的逻辑
|
456
|
+
const handleCustomMerge = (ele: AlexElement, preEle: AlexElement) => {
|
457
|
+
const uneditable = preEle.getUneditableElement()
|
458
|
+
if (uneditable) {
|
459
|
+
uneditable.toEmpty()
|
460
|
+
} else {
|
461
|
+
preEle.children!.push(...ele.children!)
|
462
|
+
preEle.children!.forEach(item => {
|
463
|
+
item.parent = preEle
|
464
|
+
})
|
465
|
+
ele.children = null
|
466
|
+
}
|
467
|
+
}
|
468
|
+
//针对node转为元素进行额外的处理
|
469
|
+
const handleCustomParseNode = (ele: AlexElement) => {
|
470
|
+
if (ele.parsedom == 'code') {
|
471
|
+
ele.parsedom = 'span'
|
472
|
+
const marks = {
|
473
|
+
'data-editify-code': true
|
474
|
+
}
|
475
|
+
if (ele.hasMarks()) {
|
476
|
+
Object.assign(ele.marks!, marks)
|
477
|
+
} else {
|
478
|
+
ele.marks = marks
|
479
|
+
}
|
480
|
+
}
|
481
|
+
if (typeof props.customParseNode == 'function') {
|
482
|
+
ele = props.customParseNode.apply(instance.proxy, [ele])
|
483
|
+
}
|
484
|
+
return ele
|
485
|
+
}
|
486
|
+
//编辑区域键盘按下:设置缩进快捷键
|
487
|
+
const handleEditorKeydown = (e: Event) => {
|
488
|
+
if (props.disabled) {
|
489
|
+
return
|
490
|
+
}
|
491
|
+
//单独按下tab键
|
492
|
+
if ((<KeyboardEvent>e).key.toLocaleLowerCase() == 'tab' && !(<KeyboardEvent>e).metaKey && !(<KeyboardEvent>e).shiftKey && !(<KeyboardEvent>e).ctrlKey && !(<KeyboardEvent>e).altKey && props.tab) {
|
493
|
+
e.preventDefault()
|
494
|
+
editor.value!.insertText(' ')
|
495
|
+
editor.value!.formatElementStack()
|
496
|
+
editor.value!.domRender()
|
497
|
+
editor.value!.rangeRender()
|
498
|
+
}
|
499
|
+
//自定义键盘按下操作
|
500
|
+
emits('keydown', e)
|
501
|
+
}
|
502
|
+
//点击编辑器:处理图片和视频的光标聚集
|
503
|
+
const handleEditorClick = (e: Event) => {
|
504
|
+
if (props.disabled || isSourceView.value) {
|
505
|
+
return
|
506
|
+
}
|
507
|
+
const node = <HTMLElement>e.target
|
508
|
+
//点击的是图片或者视频
|
509
|
+
if (node.nodeName.toLocaleLowerCase() == 'img' || node.nodeName.toLocaleLowerCase() == 'video') {
|
510
|
+
const key = Number(node.getAttribute('data-editify-element'))
|
511
|
+
if (DapNumber.isNumber(key)) {
|
512
|
+
const element = editor.value!.getElementByKey(key)!
|
513
|
+
if (!editor.value!.range) {
|
514
|
+
editor.value!.initRange()
|
515
|
+
}
|
516
|
+
editor.value!.range!.anchor.moveToStart(element)
|
517
|
+
editor.value!.range!.focus.moveToEnd(element)
|
518
|
+
editor.value!.rangeRender()
|
519
|
+
}
|
520
|
+
}
|
521
|
+
}
|
522
|
+
//编辑器的值更新
|
523
|
+
const handleEditorChange = (newVal: string, oldVal: string) => {
|
524
|
+
if (props.disabled) {
|
525
|
+
return
|
526
|
+
}
|
527
|
+
//内部修改
|
528
|
+
internalModify(newVal)
|
529
|
+
//触发change事件
|
530
|
+
emits('change', newVal, oldVal)
|
531
|
+
}
|
532
|
+
//编辑器失去焦点
|
533
|
+
const handleEditorBlur = (val: string) => {
|
534
|
+
if (props.disabled) {
|
535
|
+
return
|
536
|
+
}
|
537
|
+
if (props.border && props.color && !isFullScreen.value) {
|
538
|
+
//恢复编辑区域边框颜色
|
539
|
+
bodyRef.value!.style.borderColor = ''
|
540
|
+
//恢复编辑区域阴影颜色
|
541
|
+
bodyRef.value!.style.boxShadow = ''
|
542
|
+
//使用菜单栏的情况下恢复菜单栏的样式
|
543
|
+
if (menuConfig.value.use) {
|
544
|
+
menuRef.value!.$el.style.borderColor = ''
|
545
|
+
menuRef.value!.$el.style.boxShadow = ''
|
546
|
+
}
|
547
|
+
}
|
548
|
+
emits('blur', val)
|
549
|
+
}
|
550
|
+
//编辑器获取焦点
|
551
|
+
const handleEditorFocus = (val: string) => {
|
552
|
+
if (props.disabled) {
|
553
|
+
return
|
554
|
+
}
|
555
|
+
if (props.border && props.color && !isFullScreen.value) {
|
556
|
+
//编辑区域边框颜色
|
557
|
+
bodyRef.value!.style.borderColor = props.color
|
558
|
+
//转换颜色值
|
559
|
+
const rgb = DapColor.hex2rgb(props.color)
|
560
|
+
//菜单栏模式为inner
|
561
|
+
if (menuConfig.value.use && menuConfig.value.mode == 'inner') {
|
562
|
+
//编辑区域除顶部边框的阴影
|
563
|
+
bodyRef.value!.style.boxShadow = `0 8px 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5),8px 0 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5), -8px 0 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
|
564
|
+
//菜单栏的边框颜色
|
565
|
+
menuRef.value!.$el.style.borderColor = props.color
|
566
|
+
//菜单栏除底部边框的阴影
|
567
|
+
menuRef.value!.$el.style.boxShadow = `0 -8px 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5),8px 0 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5), -8px 0 8px -8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
|
568
|
+
}
|
569
|
+
//其他菜单栏模式
|
570
|
+
else if (menuConfig.value.use) {
|
571
|
+
//编辑区域四边阴影
|
572
|
+
bodyRef.value!.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
|
573
|
+
}
|
574
|
+
//不使用菜单栏
|
575
|
+
else {
|
576
|
+
//编辑区域四边阴影
|
577
|
+
bodyRef.value!.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
|
578
|
+
}
|
579
|
+
}
|
580
|
+
//获取焦点时可以使用菜单栏
|
581
|
+
setTimeout(() => {
|
582
|
+
canUseMenu.value = true
|
583
|
+
emits('focus', val)
|
584
|
+
}, 0)
|
585
|
+
}
|
586
|
+
//编辑器换行
|
587
|
+
const handleInsertParagraph = (element: AlexElement, previousElement: AlexElement) => {
|
588
|
+
//两个元素不一致,则表示不在代码块样式内
|
589
|
+
if (!element.isEqual(previousElement)) {
|
590
|
+
//前一个块元素如果是只包含换行符的元素,并且当前块元素也是包含换行符的元素,则当前块元素转为段落
|
591
|
+
if (previousElement.isOnlyHasBreak() && element.isOnlyHasBreak()) {
|
592
|
+
if (previousElement.parsedom != AlexElement.BLOCK_NODE) {
|
593
|
+
elementToParagraph(previousElement)
|
594
|
+
editor.value!.range!.anchor.moveToStart(previousElement)
|
595
|
+
editor.value!.range!.focus.moveToStart(previousElement)
|
596
|
+
element.toEmpty()
|
597
|
+
}
|
598
|
+
}
|
599
|
+
//如果当前换行元素是任务列表则改为不勾选状态
|
600
|
+
if (isTask(element)) {
|
601
|
+
element.marks!['data-editify-task'] = 'uncheck'
|
602
|
+
}
|
603
|
+
}
|
604
|
+
emits('insertparagraph', value.value)
|
605
|
+
}
|
606
|
+
//编辑器焦点更新
|
607
|
+
const handleRangeUpdate = () => {
|
608
|
+
if (props.disabled) {
|
609
|
+
return
|
610
|
+
}
|
611
|
+
//如果没有range禁用菜单栏
|
612
|
+
canUseMenu.value = !!editor.value!.range
|
613
|
+
//没有range直接返回
|
614
|
+
if (!editor.value!.range) {
|
615
|
+
return
|
616
|
+
}
|
617
|
+
//获取光标选取范围内的元素数据,并且进行缓存
|
618
|
+
dataRangeCaches.value = editor.value!.getElementsByRange()
|
619
|
+
|
620
|
+
//节流写法
|
621
|
+
if (rangeUpdateTimer.value) {
|
622
|
+
clearTimeout(rangeUpdateTimer.value)
|
623
|
+
rangeUpdateTimer.value = null
|
624
|
+
}
|
625
|
+
//延时200ms进行判断
|
626
|
+
rangeUpdateTimer.value = setTimeout(() => {
|
627
|
+
//如果使用工具条或者菜单栏
|
628
|
+
if (toolbarConfig.value.use || menuConfig.value.use) {
|
629
|
+
//如果使用工具条
|
630
|
+
if (toolbarConfig.value.use) {
|
631
|
+
handleToolbar()
|
632
|
+
}
|
633
|
+
//如果使用菜单栏
|
634
|
+
if (menuConfig.value.use) {
|
635
|
+
menuRef.value!.handleRangeUpdate()
|
636
|
+
}
|
637
|
+
}
|
638
|
+
}, 200)
|
639
|
+
emits('rangeupdate')
|
640
|
+
}
|
641
|
+
//编辑器部分删除情景(在编辑器起始处)
|
642
|
+
const handleDeleteInStart = (element: AlexElement) => {
|
643
|
+
if (element.isBlock()) {
|
644
|
+
elementToParagraph(element)
|
645
|
+
}
|
646
|
+
}
|
647
|
+
//编辑器删除完成后事件
|
648
|
+
const handleDeleteComplete = () => {
|
649
|
+
const uneditable = editor.value!.range!.anchor.element.getUneditableElement()
|
650
|
+
if (uneditable) {
|
651
|
+
uneditable.toEmpty()
|
652
|
+
}
|
653
|
+
}
|
654
|
+
//编辑器dom渲染
|
655
|
+
const handleAfterRender = () => {
|
656
|
+
//设定视频高度
|
657
|
+
setVideoHeight()
|
658
|
+
emits('updateview')
|
659
|
+
}
|
660
|
+
//api:光标设置到文档底部
|
661
|
+
const collapseToEnd = () => {
|
662
|
+
if (props.disabled) {
|
663
|
+
return
|
664
|
+
}
|
665
|
+
editor.value!.collapseToEnd()
|
666
|
+
editor.value!.rangeRender()
|
667
|
+
DapElement.setScrollTop({
|
668
|
+
el: contentRef.value!,
|
669
|
+
number: 1000000,
|
670
|
+
time: 0
|
671
|
+
})
|
672
|
+
}
|
673
|
+
//api:光标设置到文档头部
|
674
|
+
const collapseToStart = () => {
|
675
|
+
if (props.disabled) {
|
676
|
+
return
|
677
|
+
}
|
678
|
+
editor.value!.collapseToStart()
|
679
|
+
editor.value!.rangeRender()
|
680
|
+
nextTick(() => {
|
681
|
+
DapElement.setScrollTop({
|
682
|
+
el: contentRef.value!,
|
683
|
+
number: 0,
|
684
|
+
time: 0
|
685
|
+
})
|
686
|
+
})
|
687
|
+
}
|
688
|
+
//api:撤销
|
689
|
+
const undo = () => {
|
690
|
+
if (props.disabled) {
|
691
|
+
return
|
692
|
+
}
|
693
|
+
const historyRecord = editor.value!.history.get(-1)
|
694
|
+
if (historyRecord) {
|
695
|
+
editor.value!.history.current = historyRecord.current
|
696
|
+
editor.value!.stack = historyRecord.stack
|
697
|
+
editor.value!.range = historyRecord.range
|
698
|
+
editor.value!.formatElementStack()
|
699
|
+
editor.value!.domRender(true)
|
700
|
+
editor.value!.rangeRender()
|
701
|
+
}
|
702
|
+
}
|
703
|
+
//api:重做
|
704
|
+
const redo = () => {
|
705
|
+
if (props.disabled) {
|
706
|
+
return
|
707
|
+
}
|
708
|
+
const historyRecord = editor.value!.history.get(1)
|
709
|
+
if (historyRecord) {
|
710
|
+
editor.value!.history.current = historyRecord.current
|
711
|
+
editor.value!.stack = historyRecord.stack
|
712
|
+
editor.value!.range = historyRecord.range
|
713
|
+
editor.value!.formatElementStack()
|
714
|
+
editor.value!.domRender(true)
|
715
|
+
editor.value!.rangeRender()
|
716
|
+
}
|
717
|
+
}
|
718
|
+
|
719
|
+
//监听编辑的值变更
|
720
|
+
watch(
|
721
|
+
() => value.value,
|
722
|
+
newVal => {
|
723
|
+
//内部修改不处理
|
724
|
+
if (isModelChange.value) {
|
725
|
+
return
|
726
|
+
}
|
727
|
+
//如果是外部修改,需要重新渲染编辑器
|
728
|
+
editor.value!.stack = editor.value!.parseHtml(newVal)
|
729
|
+
editor.value!.range = null
|
730
|
+
editor.value!.formatElementStack()
|
731
|
+
editor.value!.domRender()
|
732
|
+
editor.value!.rangeRender()
|
733
|
+
contentRef.value!.blur()
|
734
|
+
}
|
735
|
+
)
|
736
|
+
//代码视图切换
|
737
|
+
watch(
|
738
|
+
() => isSourceView.value,
|
739
|
+
newVal => {
|
740
|
+
if (toolbarConfig.value.use) {
|
741
|
+
if (newVal) {
|
742
|
+
hideToolbar()
|
743
|
+
} else {
|
744
|
+
handleToolbar()
|
745
|
+
}
|
746
|
+
}
|
747
|
+
}
|
748
|
+
)
|
749
|
+
//监听disabled
|
750
|
+
watch(
|
751
|
+
() => props.disabled,
|
752
|
+
newVal => {
|
753
|
+
if (newVal) {
|
754
|
+
editor.value!.setDisabled()
|
755
|
+
} else {
|
756
|
+
editor.value!.setEnabled()
|
757
|
+
}
|
758
|
+
}
|
759
|
+
)
|
760
|
+
|
761
|
+
onMounted(() => {
|
762
|
+
//创建编辑器
|
763
|
+
createEditor()
|
764
|
+
//监听滚动隐藏工具条
|
765
|
+
handleScroll()
|
766
|
+
//鼠标按下监听
|
767
|
+
DapEvent.on(document.documentElement, `mousedown.editify_${instance.uid}`, documentMouseDown)
|
768
|
+
//鼠标移动监听
|
769
|
+
DapEvent.on(document.documentElement, `mousemove.editify_${instance.uid}`, documentMouseMove)
|
770
|
+
//鼠标松开监听
|
771
|
+
DapEvent.on(document.documentElement, `mouseup.editify_${instance.uid}`, documentMouseUp)
|
772
|
+
//鼠标点击箭头
|
773
|
+
DapEvent.on(document.documentElement, `click.editify_${instance.uid}`, documentClick)
|
774
|
+
//监听窗口改变
|
775
|
+
DapEvent.on(window, `resize.editify_${instance.uid}`, setVideoHeight)
|
776
|
+
})
|
777
|
+
|
778
|
+
onBeforeUnmount(() => {
|
779
|
+
//卸载绑定在滚动元素上的事件
|
780
|
+
removeScrollHandle()
|
781
|
+
//卸载绑定在document.documentElement上的事件
|
782
|
+
DapEvent.off(document.documentElement, `mousedown.editify_${instance.uid} mousemove.editify_${instance.uid} mouseup.editify_${instance.uid} click.editify_${instance.uid}`)
|
783
|
+
//卸载绑定在window上的事件
|
784
|
+
DapEvent.off(window, `resize.editify_${instance.uid}`)
|
785
|
+
//销毁编辑器
|
786
|
+
editor.value!.destroy()
|
787
|
+
})
|
788
|
+
|
789
|
+
provide('editify', instance)
|
790
|
+
provide('isSourceView', isSourceView)
|
791
|
+
provide('isFullScreen', isFullScreen)
|
792
|
+
provide('canUseMenu', canUseMenu)
|
793
|
+
provide('editor', editor)
|
794
|
+
provide('dataRangeCaches', dataRangeCaches)
|
795
|
+
provide('showBorder', showBorder)
|
796
|
+
|
797
|
+
defineExpose({
|
798
|
+
editor,
|
799
|
+
isSourceView,
|
800
|
+
isFullScreen,
|
801
|
+
canUseMenu,
|
802
|
+
dataRangeCaches,
|
803
|
+
textValue,
|
804
|
+
collapseToEnd,
|
805
|
+
collapseToStart,
|
806
|
+
undo,
|
807
|
+
redo
|
808
|
+
})
|
809
|
+
</script>
|
810
|
+
<style scoped src="./editify.less"></style>
|