vue-editify 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1880 @@
1
+ <template>
2
+ <div class="editify">
3
+ <!-- 菜单区域 -->
4
+ <Menu v-if="menuConfig.use" :config="menuConfig" :disabled="disabled || !canUseMenu" :color="color" ref="menu"></Menu>
5
+ <!-- 编辑层,与编辑区域宽高相同必须适配 -->
6
+ <div ref="body" class="editify-body" :class="{ border: border, menu_inner: menuConfig.use && menuConfig.mode == 'inner' }" :data-editify-uid="uid">
7
+ <!-- 编辑器 -->
8
+ <div ref="content" class="editify-content" :class="{ placeholder: showPlaceholder, disabled: disabled }" :style="contentStyle" @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="toolbar" v-model="toolbarOptions.show" :node="toolbarOptions.node" :type="toolbarOptions.type" :config="toolbarConfig"></Toolbar>
13
+ </div>
14
+ <!-- 编辑器尾部 -->
15
+ <div v-if="showWordLength" class="editify-footer">
16
+ <!-- 字数统计 -->
17
+ <div class="editify-footer-words">{{ $editTrans('totalWordCount') }}{{ textValue.length }}</div>
18
+ </div>
19
+ </div>
20
+ </template>
21
+ <script>
22
+ import { getCurrentInstance } from 'vue'
23
+ import { AlexEditor, AlexElement } from 'alex-editor'
24
+ import Dap from 'dap-util'
25
+ import { pasteKeepData, editorProps, parseList, parseCode, mediaHandle, tableHandle, preHandle, blockToParagraph, blockToList, blockIsList, getButtonOptionsConfig, getToolbarConfig, getMenuConfig, mergeObject, blockIsTask, blockToTask } from './core'
26
+ import Toolbar from './components/bussiness/Toolbar'
27
+ import Tooltip from './components/base/Tooltip'
28
+ import Menu from './components/bussiness/Menu'
29
+
30
+ export default {
31
+ name: 'editify',
32
+ props: { ...editorProps },
33
+ emits: ['update:modelValue', 'focus', 'blur', 'change', 'keydown', 'insertparagraph', 'rangeupdate', 'copy', 'cut', 'paste-text', 'paste-html', 'paste-image', 'paste-video', 'before-render', 'after-render'],
34
+ setup() {
35
+ const instance = getCurrentInstance()
36
+ return {
37
+ uid: instance.uid
38
+ }
39
+ },
40
+ data() {
41
+ return {
42
+ //编辑器对象
43
+ editor: null,
44
+ //是否编辑器内部修改值
45
+ isModelChange: false,
46
+ //是否代码视图
47
+ isSourceView: false,
48
+ //是否正在输入中文
49
+ isInputChinese: false,
50
+ //表格列宽拖拽记录数据
51
+ tableColumnResizeParams: {
52
+ element: null, //被拖拽的td
53
+ start: 0 //水平方向起点位置
54
+ },
55
+ //工具条参数配置
56
+ toolbarOptions: {
57
+ //是否显示工具条
58
+ show: false,
59
+ //关联元素
60
+ node: null,
61
+ //类型
62
+ type: 'text'
63
+ },
64
+ //toolbar显示延时器
65
+ toolbarTime: null,
66
+ //菜单栏是否可以使用标识
67
+ canUseMenu: false
68
+ }
69
+ },
70
+ computed: {
71
+ //编辑器的值
72
+ value: {
73
+ set(val) {
74
+ this.$emit('update:modelValue', val)
75
+ },
76
+ get() {
77
+ return this.modelValue || '<p><br></p>'
78
+ }
79
+ },
80
+ //编辑器的纯文本值
81
+ textValue() {
82
+ return Dap.element.string2dom(`<div>${this.value}</div>`).innerText
83
+ },
84
+ //是否显示占位符
85
+ showPlaceholder() {
86
+ if (this.editor) {
87
+ const elements = this.editor.parseHtml(this.value)
88
+ if (elements.length == 1 && elements[0].type == 'block' && elements[0].parsedom == AlexElement.BLOCK_NODE && elements[0].isOnlyHasBreak()) {
89
+ return !this.isInputChinese
90
+ }
91
+ }
92
+ return false
93
+ },
94
+ //编辑器样式设置
95
+ contentStyle() {
96
+ return this.autoheight ? { minHeight: this.height } : { height: this.height }
97
+ },
98
+ //最终生效的工具栏配置
99
+ toolbarConfig() {
100
+ return mergeObject(getToolbarConfig(this.$editTrans, this.$editLocale), this.toolbar || {})
101
+ },
102
+ //最终生效的菜单栏配置
103
+ menuConfig() {
104
+ return mergeObject(getMenuConfig(this.$editTrans, this.$editLocale), this.menu || {})
105
+ }
106
+ },
107
+ components: {
108
+ Toolbar,
109
+ Tooltip,
110
+ Menu
111
+ },
112
+ inject: ['$editTrans', '$editLocale'],
113
+ watch: {
114
+ //监听编辑的值变更
115
+ value(newVal) {
116
+ //内部修改不处理
117
+ if (this.isModelChange) {
118
+ return
119
+ }
120
+ //如果是外部修改,需要重新渲染编辑器
121
+ this.editor.stack = this.editor.parseHtml(newVal)
122
+ this.editor.formatElementStack()
123
+ this.editor.range.anchor.moveToStart(this.editor.stack[0])
124
+ this.editor.range.focus.moveToStart(this.editor.stack[0])
125
+ this.editor.domRender()
126
+ },
127
+ //代码视图切换
128
+ isSourceView(newValue) {
129
+ if (this.toolbarConfig.use) {
130
+ if (newValue) {
131
+ this.hideToolbar()
132
+ } else {
133
+ this.handleToolbar()
134
+ }
135
+ }
136
+ }
137
+ },
138
+ mounted() {
139
+ //创建编辑器
140
+ this.createEditor()
141
+ //监听滚动隐藏工具条
142
+ this.handleScroll()
143
+ //鼠标按下监听
144
+ Dap.event.on(document.documentElement, `mousedown.editify_${this.uid}`, this.documentMouseDown)
145
+ //鼠标移动监听
146
+ Dap.event.on(document.documentElement, `mousemove.editify_${this.uid}`, this.documentMouseMove)
147
+ //鼠标松开监听
148
+ Dap.event.on(document.documentElement, `mouseup.editify_${this.uid}`, this.documentMouseUp)
149
+ //鼠标点击箭头
150
+ Dap.event.on(document.documentElement, `click.editify_${this.uid}`, this.documentClick)
151
+ //监听窗口改变
152
+ Dap.event.on(window, `resize.editify_${this.uid}`, this.setVideoHeight)
153
+ },
154
+ methods: {
155
+ //初始创建编辑器
156
+ createEditor() {
157
+ //创建编辑器
158
+ this.editor = new AlexEditor(this.$refs.content, {
159
+ value: this.value,
160
+ disabled: this.disabled,
161
+ renderRules: [
162
+ parseList,
163
+ parseCode,
164
+ mediaHandle,
165
+ tableHandle,
166
+ el => {
167
+ preHandle.apply(this.editor, [el, this.toolbarConfig?.use && this.toolbarConfig?.codeBlock?.languages?.show, this.toolbarConfig?.codeBlock?.languages.options])
168
+ }
169
+ ],
170
+ allowCopy: this.allowCopy,
171
+ allowPaste: this.allowPaste,
172
+ allowCut: this.allowCut,
173
+ allowPasteHtml: this.allowPasteHtml,
174
+ allowPasteHtml: this.allowPasteHtml,
175
+ customImagePaste: this.customImagePaste,
176
+ customVideoPaste: this.customVideoPaste,
177
+ customMerge: this.handleCustomMerge
178
+ })
179
+ //编辑器渲染后会有一个渲染过程,会改变内容,因此重新获取内容的值来设置value
180
+ this.internalModify(this.editor.value)
181
+ //设置监听事件
182
+ this.editor.on('change', this.handleEditorChange)
183
+ this.editor.on('focus', this.handleEditorFocus)
184
+ this.editor.on('blur', this.handleEditorBlur)
185
+ this.editor.on('insertParagraph', this.handleInsertParagraph)
186
+ this.editor.on('rangeUpdate', this.handleRangeUpdate)
187
+ this.editor.on('copy', this.handleCopy)
188
+ this.editor.on('cut', this.handleCut)
189
+ this.editor.on('pasteText', this.handlePasteText)
190
+ this.editor.on('pasteHtml', this.handlePasteHtml)
191
+ this.editor.on('pasteImage', this.handlePasteImage)
192
+ this.editor.on('pasteVideo', this.handlePasteVideo)
193
+ this.editor.on('deleteInStart', this.handleDeleteInStart)
194
+ this.editor.on('deleteComplete', this.handleDeleteComplete)
195
+ this.editor.on('beforeRender', this.handleBeforeRender)
196
+ this.editor.on('afterRender', this.handleAfterRender)
197
+ //格式化和dom渲染
198
+ this.editor.formatElementStack()
199
+ this.editor.domRender()
200
+ //自动获取焦点
201
+ if (this.autofocus && !this.isSourceView && !this.disabled) {
202
+ this.collapseToEnd()
203
+ }
204
+ },
205
+ //编辑器内部修改值的方法
206
+ internalModify(val) {
207
+ this.isModelChange = true
208
+ this.value = val
209
+ this.$nextTick(() => {
210
+ this.isModelChange = false
211
+ })
212
+ },
213
+ //监听滚动隐藏工具条
214
+ handleScroll() {
215
+ const setScroll = el => {
216
+ Dap.event.on(el, `scroll.editify_${this.uid}`, () => {
217
+ if (this.toolbarConfig.use && this.toolbarOptions.show) {
218
+ this.hideToolbar()
219
+ }
220
+ })
221
+ if (el.parentNode) {
222
+ setScroll(el.parentNode)
223
+ }
224
+ }
225
+ setScroll(this.$refs.content)
226
+ },
227
+ //移除上述滚动事件的监听
228
+ removeScrollHandle() {
229
+ const removeScroll = el => {
230
+ Dap.event.off(el, `scroll.editify_${this.uid}`)
231
+ if (el.parentNode) {
232
+ removeScroll(el.parentNode)
233
+ }
234
+ }
235
+ removeScroll(this.$refs.content)
236
+ },
237
+ //工具条显示判断
238
+ handleToolbar() {
239
+ if (this.disabled || this.isSourceView) {
240
+ return
241
+ }
242
+ if (this.toolbarTime) {
243
+ clearTimeout(this.toolbarTime)
244
+ }
245
+ this.toolbarTime = setTimeout(() => {
246
+ this.hideToolbar()
247
+ this.$nextTick(() => {
248
+ const table = this.getCurrentParsedomElement('table')
249
+ const pre = this.getCurrentParsedomElement('pre')
250
+ const link = this.getCurrentParsedomElement('a')
251
+ const image = this.getCurrentParsedomElement('img')
252
+ const video = this.getCurrentParsedomElement('video')
253
+ if (link) {
254
+ this.toolbarOptions.type = 'link'
255
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${link.key}"]`
256
+ if (this.toolbarOptions.show) {
257
+ this.$refs.toolbar.$refs.layer.setPosition()
258
+ } else {
259
+ this.toolbarOptions.show = true
260
+ }
261
+ } else if (image) {
262
+ this.toolbarOptions.type = 'image'
263
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${image.key}"]`
264
+ if (this.toolbarOptions.show) {
265
+ this.$refs.toolbar.$refs.layer.setPosition()
266
+ } else {
267
+ this.toolbarOptions.show = true
268
+ }
269
+ } else if (video) {
270
+ this.toolbarOptions.type = 'video'
271
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${video.key}"]`
272
+ if (this.toolbarOptions.show) {
273
+ this.$refs.toolbar.$refs.layer.setPosition()
274
+ } else {
275
+ this.toolbarOptions.show = true
276
+ }
277
+ } else if (table) {
278
+ this.toolbarOptions.type = 'table'
279
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${table.key}"]`
280
+ if (this.toolbarOptions.show) {
281
+ this.$refs.toolbar.$refs.layer.setPosition()
282
+ } else {
283
+ this.toolbarOptions.show = true
284
+ }
285
+ } else if (pre) {
286
+ this.toolbarOptions.type = 'codeBlock'
287
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${pre.key}"]`
288
+ if (this.toolbarOptions.show) {
289
+ this.$refs.toolbar.$refs.layer.setPosition()
290
+ } else {
291
+ this.toolbarOptions.show = true
292
+ }
293
+ } else {
294
+ const result = this.editor.getElementsByRange(true, true).filter(item => {
295
+ return item.element.isText()
296
+ })
297
+ if (result.length && !this.hasTable() && !this.hasPreStyle() && !this.hasLink() && !this.hasImage() && !this.hasVideo()) {
298
+ this.toolbarOptions.type = 'text'
299
+ if (this.toolbarOptions.show) {
300
+ this.$refs.toolbar.$refs.layer.setPosition()
301
+ } else {
302
+ this.toolbarOptions.show = true
303
+ }
304
+ }
305
+ }
306
+ })
307
+ }, 200)
308
+ },
309
+ //重新定义编辑器合并元素的逻辑
310
+ handleCustomMerge(ele, preEle) {
311
+ const uneditable = preEle.getUneditableElement()
312
+ if (uneditable) {
313
+ uneditable.toEmpty()
314
+ } else {
315
+ preEle.children.push(...ele.children)
316
+ preEle.children.forEach(item => {
317
+ item.parent = preEle
318
+ })
319
+ ele.children = null
320
+ }
321
+ },
322
+ //隐藏工具条
323
+ hideToolbar() {
324
+ this.toolbarOptions.show = false
325
+ this.toolbarOptions.node = null
326
+ },
327
+ //鼠标在页面按下:处理表格拖拽改变列宽和菜单栏是否使用判断
328
+ documentMouseDown(e) {
329
+ if (this.disabled) {
330
+ return
331
+ }
332
+ //鼠标在编辑器内按下
333
+ if (Dap.element.isContains(this.$refs.content, e.target)) {
334
+ const elm = e.target
335
+ const key = Dap.data.get(elm, 'data-alex-editor-key')
336
+ if (key) {
337
+ const element = this.editor.getElementByKey(key)
338
+ if (element && element.parsedom == 'td') {
339
+ const length = element.parent.children.length
340
+ //最后一个td不设置
341
+ if (element.parent.children[length - 1].isEqual(element)) {
342
+ return
343
+ }
344
+ const rect = Dap.element.getElementBounding(elm)
345
+ //在可拖拽范围内
346
+ if (e.pageX >= Math.abs(rect.left + elm.offsetWidth - 5) && e.pageX <= Math.abs(rect.left + elm.offsetWidth + 5)) {
347
+ this.tableColumnResizeParams.element = element
348
+ this.tableColumnResizeParams.start = e.pageX
349
+ }
350
+ }
351
+ }
352
+ }
353
+ //如果点击了除编辑器外的地方,菜单栏不可使用
354
+ if (!Dap.element.isContains(this.$el, e.target)) {
355
+ this.canUseMenu = false
356
+ }
357
+ },
358
+ //鼠标在页面移动:处理表格拖拽改变列宽
359
+ documentMouseMove(e) {
360
+ if (this.disabled) {
361
+ return
362
+ }
363
+ if (!this.tableColumnResizeParams.element) {
364
+ return
365
+ }
366
+ const table = this.getCurrentParsedomElement('table')
367
+ if (!table) {
368
+ return
369
+ }
370
+ const colgroup = table.children.find(item => {
371
+ return item.parsedom == 'colgroup'
372
+ })
373
+ const index = this.tableColumnResizeParams.element.parent.children.findIndex(el => {
374
+ return el.isEqual(this.tableColumnResizeParams.element)
375
+ })
376
+ const width = `${this.tableColumnResizeParams.element.elm.offsetWidth + e.pageX - this.tableColumnResizeParams.start}`
377
+ colgroup.children[index].marks['width'] = width
378
+ colgroup.children[index].elm.setAttribute('width', width)
379
+ this.tableColumnResizeParams.start = e.pageX
380
+ },
381
+ //鼠标在页面松开:处理表格拖拽改变列宽
382
+ documentMouseUp(e) {
383
+ if (this.disabled) {
384
+ return
385
+ }
386
+ if (!this.tableColumnResizeParams.element) {
387
+ return
388
+ }
389
+ const table = this.getCurrentParsedomElement('table')
390
+ if (!table) {
391
+ return
392
+ }
393
+ const colgroup = table.children.find(item => {
394
+ return item.parsedom == 'colgroup'
395
+ })
396
+ const index = this.tableColumnResizeParams.element.parent.children.findIndex(el => {
397
+ return el.isEqual(this.tableColumnResizeParams.element)
398
+ })
399
+ const width = Number(colgroup.children[index].marks['width'])
400
+ if (!isNaN(width)) {
401
+ colgroup.children[index].marks['width'] = `${Number(((width / this.tableColumnResizeParams.element.parent.elm.offsetWidth) * 100).toFixed(2))}%`
402
+ this.editor.formatElementStack()
403
+ this.editor.domRender()
404
+ this.editor.rangeRender()
405
+ }
406
+ this.tableColumnResizeParams.element = null
407
+ this.tableColumnResizeParams.start = 0
408
+ },
409
+ //鼠标点击页面:处理任务列表复选框勾选
410
+ documentClick(e) {
411
+ if (this.disabled) {
412
+ return
413
+ }
414
+ //鼠标在编辑器内按下
415
+ if (Dap.element.isContains(this.$refs.content, e.target)) {
416
+ const elm = e.target
417
+ const key = Dap.data.get(elm, 'data-alex-editor-key')
418
+ if (key) {
419
+ const element = this.editor.getElementByKey(key)
420
+ //如果是任务列表元素
421
+ if (blockIsTask(element)) {
422
+ const rect = Dap.element.getElementBounding(elm)
423
+ //在复选框范围内
424
+ if (e.pageX >= Math.abs(rect.left) && e.pageX <= Math.abs(rect.left + 16) && e.pageY >= Math.abs(rect.top + elm.offsetHeight / 2 - 8) && e.pageY <= Math.abs(rect.top + elm.offsetHeight / 2 + 8)) {
425
+ //取消勾选
426
+ if (element.marks['data-editify-task'] == 'checked') {
427
+ element.marks['data-editify-task'] = 'uncheck'
428
+ }
429
+ //勾选
430
+ else {
431
+ element.marks['data-editify-task'] = 'checked'
432
+ }
433
+ this.editor.range.anchor.moveToEnd(element)
434
+ this.editor.range.focus.moveToEnd(element)
435
+ this.editor.formatElementStack()
436
+ this.editor.domRender()
437
+ this.editor.rangeRender()
438
+ }
439
+ }
440
+ }
441
+ }
442
+ },
443
+ //编辑器的值更新
444
+ handleEditorChange(newVal, oldVal) {
445
+ if (this.disabled) {
446
+ return
447
+ }
448
+ //内部修改
449
+ this.internalModify(newVal)
450
+ //触发change事件
451
+ this.$emit('change', newVal, oldVal)
452
+ },
453
+ //编辑器失去焦点
454
+ handleEditorBlur(val) {
455
+ if (this.disabled) {
456
+ return
457
+ }
458
+ if (this.border && this.color) {
459
+ //恢复编辑区域边框颜色
460
+ this.$refs.body.style.borderColor = ''
461
+ //恢复编辑区域阴影颜色
462
+ this.$refs.body.style.boxShadow = ''
463
+ //使用菜单栏的情况下恢复菜单栏的样式
464
+ if (this.menuConfig.use) {
465
+ this.$refs.menu.$el.style.borderColor = ''
466
+ this.$refs.menu.$el.style.boxShadow = ''
467
+ }
468
+ }
469
+ this.$emit('blur', val)
470
+ },
471
+ //编辑器获取焦点
472
+ handleEditorFocus(val) {
473
+ if (this.disabled) {
474
+ return
475
+ }
476
+ if (this.border && this.color) {
477
+ //编辑区域边框颜色
478
+ this.$refs.body.style.borderColor = this.color
479
+ //转换颜色值
480
+ const rgb = Dap.color.hex2rgb(this.color)
481
+ //菜单栏模式为inner
482
+ if (this.menuConfig.use && this.menuConfig.mode == 'inner') {
483
+ //编辑区域除顶部边框的阴影
484
+ this.$refs.body.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)`
485
+ //菜单栏的边框颜色
486
+ this.$refs.menu.$el.style.borderColor = this.color
487
+ //菜单栏除底部边框的阴影
488
+ this.$refs.menu.$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)`
489
+ }
490
+ //其他菜单栏模式
491
+ else if (this.menuConfig.use) {
492
+ //编辑区域四边阴影
493
+ this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
494
+ }
495
+ //不使用菜单栏
496
+ else {
497
+ //编辑区域四边阴影
498
+ this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
499
+ }
500
+ }
501
+ //获取焦点时可以使用菜单栏
502
+ setTimeout(() => {
503
+ this.canUseMenu = true
504
+ }, 0)
505
+ this.$emit('focus', val)
506
+ },
507
+ //编辑区域键盘按下
508
+ handleEditorKeydown(e) {
509
+ if (this.disabled) {
510
+ return
511
+ }
512
+ //增加缩进
513
+ if (e.keyCode == 9 && !e.metaKey && !e.shiftKey && !e.ctrlKey && !e.altKey) {
514
+ e.preventDefault()
515
+ this.setIndentIncrease()
516
+ }
517
+ //减少缩进
518
+ else if (e.keyCode == 9 && !e.metaKey && e.shiftKey && !e.ctrlKey && !e.altKey) {
519
+ e.preventDefault()
520
+ this.setIndentDecrease()
521
+ }
522
+ //自定义键盘按下操作
523
+ this.$emit('keydown', e)
524
+ },
525
+ //点击编辑器
526
+ handleEditorClick(e) {
527
+ if (this.disabled || this.isSourceView) {
528
+ return
529
+ }
530
+ const node = e.target
531
+ //点击的是图片或者视频
532
+ if (node.nodeName.toLocaleLowerCase() == 'img' || node.nodeName.toLocaleLowerCase() == 'video') {
533
+ const key = node.getAttribute('data-editify-element')
534
+ if (key) {
535
+ const element = this.editor.getElementByKey(key)
536
+ this.editor.range.anchor.moveToStart(element)
537
+ this.editor.range.focus.moveToEnd(element)
538
+ }
539
+ }
540
+ this.editor.rangeRender()
541
+ },
542
+ //编辑器换行
543
+ handleInsertParagraph(element, previousElement) {
544
+ //前一个块元素如果是只包含换行符的元素,并且当前块元素也是包含换行符的元素,则当前块元素转为段落
545
+ if (previousElement.isOnlyHasBreak() && element.isOnlyHasBreak()) {
546
+ if (!previousElement.isBlock()) {
547
+ previousElement.convertToBlock()
548
+ }
549
+ if (previousElement.parsedom != AlexElement.BLOCK_NODE) {
550
+ blockToParagraph(previousElement)
551
+ this.editor.range.anchor.moveToStart(previousElement)
552
+ this.editor.range.focus.moveToStart(previousElement)
553
+ element.toEmpty()
554
+ }
555
+ }
556
+ this.$emit('insertparagraph', this.value)
557
+ },
558
+ //编辑器焦点更新
559
+ handleRangeUpdate() {
560
+ if (this.disabled) {
561
+ return
562
+ }
563
+ if (this.toolbarConfig.use) {
564
+ this.handleToolbar()
565
+ }
566
+ if (this.menuConfig.use) {
567
+ this.$refs.menu.handleRangeUpdate()
568
+ }
569
+ this.$emit('rangeupdate', this.value)
570
+ },
571
+ //编辑器复制
572
+ handleCopy(text, html) {
573
+ this.$emit('copy', text, html)
574
+ },
575
+ //编辑器剪切
576
+ handleCut(text, html) {
577
+ this.$emit('cut', text, html)
578
+ },
579
+ //编辑器粘贴纯文本
580
+ handlePasteText(data) {
581
+ this.$emit('paste-text', data)
582
+ },
583
+ //编辑器粘贴html
584
+ handlePasteHtml(elements, data) {
585
+ //粘贴html时过滤元素的样式和属性
586
+ AlexElement.flatElements(elements).forEach(el => {
587
+ let marks = {}
588
+ let styles = {}
589
+ if (el.hasMarks()) {
590
+ for (let key in pasteKeepData.marks) {
591
+ if (el.marks.hasOwnProperty(key) && ((Array.isArray(pasteKeepData.marks[key]) && pasteKeepData.marks[key].includes(el.parsedom)) || pasteKeepData.marks[key] == '*')) {
592
+ marks[key] = el.marks[key]
593
+ }
594
+ }
595
+ el.marks = marks
596
+ }
597
+ if (el.hasStyles() && !el.isText()) {
598
+ for (let key in pasteKeepData.styles) {
599
+ if (el.styles.hasOwnProperty(key) && ((Array.isArray(pasteKeepData.styles[key]) && pasteKeepData.styles[key].includes(el.parsedom)) || pasteKeepData.styles[key] == '*')) {
600
+ styles[key] = el.styles[key]
601
+ }
602
+ }
603
+ el.styles = styles
604
+ }
605
+ })
606
+ this.$emit('paste-html', elements)
607
+ },
608
+ //编辑器粘贴图片
609
+ handlePasteImage(url) {
610
+ this.$emit('paste-image', url)
611
+ },
612
+ //编辑器粘贴视频
613
+ handlePasteVideo(url) {
614
+ this.$emit('paste-video', url)
615
+ },
616
+ //编辑器部分删除情景
617
+ handleDeleteInStart(element) {
618
+ if (element.isBlock()) {
619
+ blockToParagraph(element)
620
+ }
621
+ },
622
+ //编辑器删除完成后事件
623
+ handleDeleteComplete() {
624
+ const uneditable = this.editor.range.anchor.element.getUneditableElement()
625
+ if (uneditable) {
626
+ uneditable.toEmpty()
627
+ }
628
+ },
629
+ //编辑器dom渲染之前
630
+ handleBeforeRender() {
631
+ this.$emit('before-render')
632
+ },
633
+ //编辑器dom渲染
634
+ handleAfterRender() {
635
+ //设定视频高度
636
+ this.setVideoHeight()
637
+ //触发事件
638
+ this.$emit('after-render')
639
+ },
640
+ //设定视频高度
641
+ setVideoHeight() {
642
+ this.$refs.content.querySelectorAll('video').forEach(video => {
643
+ video.style.height = video.offsetWidth / this.videoRatio + 'px'
644
+ })
645
+ },
646
+
647
+ //api:光标设置到文档底部
648
+ collapseToEnd() {
649
+ if (this.disabled) {
650
+ return
651
+ }
652
+ this.editor.collapseToEnd()
653
+ this.editor.rangeRender()
654
+ Dap.element.setScrollTop({
655
+ el: this.$refs.content,
656
+ number: 1000000,
657
+ time: 0
658
+ })
659
+ },
660
+ //api:光标设置到文档头部
661
+ collapseToStart() {
662
+ if (this.disabled) {
663
+ return
664
+ }
665
+ this.editor.collapseToStart()
666
+ this.editor.rangeRender()
667
+ this.$nextTick(() => {
668
+ Dap.element.setScrollTop({
669
+ el: this.$refs.content,
670
+ number: 0,
671
+ time: 0
672
+ })
673
+ })
674
+ },
675
+ //api:获取某个元素是否在某个标签元素下,如果是返回该标签元素,否则返回null
676
+ getParsedomElementByElement(element, parsedom) {
677
+ if (element.isBlock()) {
678
+ return element.parsedom == parsedom ? element : null
679
+ }
680
+ if (!element.isText() && element.parsedom == parsedom) {
681
+ return element
682
+ }
683
+ return this.getParsedomElementByElement(element.parent, parsedom)
684
+ },
685
+ //api:获取光标是否在指定标签元素下,如果是返回该标签元素,否则返回null
686
+ getCurrentParsedomElement(parsedom) {
687
+ if (this.disabled) {
688
+ return null
689
+ }
690
+ if (this.editor.range.anchor.element.isEqual(this.editor.range.focus.element)) {
691
+ return this.getParsedomElementByElement(this.editor.range.anchor.element, parsedom)
692
+ }
693
+ const arr = this.editor.getElementsByRange(true, false).map(item => {
694
+ return this.getParsedomElementByElement(item.element, parsedom)
695
+ })
696
+ let hasNull = arr.some(el => {
697
+ return el == null
698
+ })
699
+ //如果存在null,则表示有的选区元素不在指定标签下,返回null
700
+ if (hasNull) {
701
+ return null
702
+ }
703
+ //如果只有一个元素,则返回该元素
704
+ if (arr.length == 1) {
705
+ return arr[0]
706
+ }
707
+ //默认数组中的元素都相等
708
+ let flag = true
709
+ for (let i = 1; i < arr.length; i++) {
710
+ if (!arr[i].isEqual(arr[0])) {
711
+ flag = false
712
+ break
713
+ }
714
+ }
715
+ //如果相等,则返回该元素
716
+ if (flag) {
717
+ return arr[0]
718
+ }
719
+ return null
720
+ },
721
+ //api:删除光标所在的指定标签元素
722
+ deleteByParsedom(parsedom) {
723
+ if (this.disabled) {
724
+ return
725
+ }
726
+ const element = this.getCurrentParsedomElement(parsedom)
727
+ if (element) {
728
+ element.toEmpty()
729
+ this.editor.formatElementStack()
730
+ this.editor.domRender()
731
+ this.editor.rangeRender()
732
+ }
733
+ },
734
+ //api:当光标在链接上时可以移除链接
735
+ removeLink() {
736
+ if (this.disabled) {
737
+ return
738
+ }
739
+ const link = this.getCurrentParsedomElement('a')
740
+ if (link) {
741
+ link.parsedom = AlexElement.TEXT_NODE
742
+ delete link.marks.target
743
+ delete link.marks.href
744
+ this.editor.formatElementStack()
745
+ this.editor.domRender()
746
+ this.editor.rangeRender()
747
+ }
748
+ },
749
+ //api:设置标题
750
+ setHeading(parsedom) {
751
+ if (this.disabled) {
752
+ return
753
+ }
754
+ const values = getButtonOptionsConfig(this.$editTrans, this.$editLocale).heading.map(item => {
755
+ return item.value
756
+ })
757
+ if (!values.includes(parsedom)) {
758
+ throw new Error('The parameter supports only h1-h6 and p')
759
+ }
760
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
761
+ const block = this.editor.range.anchor.element.getBlock()
762
+ //先转为段落
763
+ blockToParagraph(block)
764
+ //设置标题
765
+ block.parsedom = parsedom
766
+ } else {
767
+ const result = this.editor.getElementsByRange(true, false)
768
+ result.forEach(el => {
769
+ if (el.element.isBlock()) {
770
+ blockToParagraph(el.element)
771
+ el.element.parsedom = parsedom
772
+ } else {
773
+ const block = el.element.getBlock()
774
+ blockToParagraph(block)
775
+ block.parsedom = parsedom
776
+ }
777
+ })
778
+ }
779
+ this.editor.formatElementStack()
780
+ this.editor.domRender()
781
+ this.editor.rangeRender()
782
+ },
783
+ //api:插入有序列表 ordered为true表示有序列表
784
+ setList(ordered) {
785
+ if (this.disabled) {
786
+ return
787
+ }
788
+ //起点和终点在一起
789
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
790
+ const block = this.editor.range.anchor.element.getBlock()
791
+ const isList = blockIsList(block, ordered)
792
+ if (isList) {
793
+ blockToParagraph(block)
794
+ } else {
795
+ blockToList(block, ordered)
796
+ }
797
+ }
798
+ //起点和终点不在一起
799
+ else {
800
+ let blocks = []
801
+ const result = this.editor.getElementsByRange(true, false)
802
+ result.forEach(item => {
803
+ const block = item.element.getBlock()
804
+ const exist = blocks.some(el => block.isEqual(el))
805
+ if (!exist) {
806
+ blocks.push(block)
807
+ }
808
+ })
809
+ blocks.forEach(block => {
810
+ const isList = blockIsList(block, ordered)
811
+ if (isList) {
812
+ blockToParagraph(block)
813
+ } else {
814
+ blockToList(block, ordered)
815
+ }
816
+ })
817
+ }
818
+ this.editor.formatElementStack()
819
+ this.editor.domRender()
820
+ this.editor.rangeRender()
821
+ },
822
+ //api:插入任务列表
823
+ setTask() {
824
+ if (this.disabled) {
825
+ return
826
+ }
827
+ //起点和终点在一起
828
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
829
+ const block = this.editor.range.anchor.element.getBlock()
830
+ const isTask = blockIsTask(block)
831
+ if (isTask) {
832
+ blockToParagraph(block)
833
+ } else {
834
+ blockToTask(block)
835
+ }
836
+ }
837
+ //起点和终点不在一起
838
+ else {
839
+ let blocks = []
840
+ const result = this.editor.getElementsByRange(true, false)
841
+ result.forEach(item => {
842
+ const block = item.element.getBlock()
843
+ const exist = blocks.some(el => block.isEqual(el))
844
+ if (!exist) {
845
+ blocks.push(block)
846
+ }
847
+ })
848
+ blocks.forEach(block => {
849
+ const isTask = blockIsTask(block)
850
+ if (isTask) {
851
+ blockToParagraph(block)
852
+ } else {
853
+ blockToTask(block)
854
+ }
855
+ })
856
+ }
857
+ this.editor.formatElementStack()
858
+ this.editor.domRender()
859
+ this.editor.rangeRender()
860
+ },
861
+ //api:设置样式
862
+ setTextStyle(name, value) {
863
+ if (this.disabled) {
864
+ return
865
+ }
866
+ const active = this.queryTextStyle(name, value)
867
+ if (active) {
868
+ this.editor.removeTextStyle([name])
869
+ } else {
870
+ let styles = {}
871
+ styles[name] = value
872
+ this.editor.setTextStyle(styles)
873
+ }
874
+ this.editor.formatElementStack()
875
+ this.editor.domRender()
876
+ this.editor.rangeRender()
877
+ },
878
+ //api:查询是否具有某个样式
879
+ queryTextStyle(name, value, useCache) {
880
+ return this.editor.queryTextStyle(name, value, useCache)
881
+ },
882
+ //api:设置标记
883
+ setTextMark(name, value) {
884
+ if (this.disabled) {
885
+ return
886
+ }
887
+ const active = this.queryTextMark(name, value)
888
+ if (active) {
889
+ this.editor.removeTextMark([name])
890
+ } else {
891
+ let marks = {}
892
+ marks[name] = value
893
+ this.editor.setTextMark(marks)
894
+ }
895
+ this.editor.formatElementStack()
896
+ this.editor.domRender()
897
+ this.editor.rangeRender()
898
+ },
899
+ //api:查询是否具有某个标记
900
+ queryTextMark(name, value, useCache) {
901
+ return this.editor.queryTextMark(name, value, useCache)
902
+ },
903
+ //api:清除文本样式和标记
904
+ formatText() {
905
+ if (this.disabled) {
906
+ return
907
+ }
908
+ this.editor.removeTextStyle()
909
+ this.editor.removeTextMark()
910
+ this.editor.formatElementStack()
911
+ this.editor.domRender()
912
+ this.editor.rangeRender()
913
+ },
914
+ //api:设置对齐方式,参数取值justify/left/right/center
915
+ setAlign(value) {
916
+ if (this.disabled) {
917
+ return
918
+ }
919
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
920
+ const block = this.editor.range.anchor.element.getBlock()
921
+ const inblock = this.editor.range.anchor.element.getInblock()
922
+ if (inblock) {
923
+ if (inblock.hasStyles()) {
924
+ inblock.styles['text-align'] = value
925
+ } else {
926
+ inblock.styles = {
927
+ 'text-align': value
928
+ }
929
+ }
930
+ } else {
931
+ if (block.hasStyles()) {
932
+ block.styles['text-align'] = value
933
+ } else {
934
+ block.styles = {
935
+ 'text-align': value
936
+ }
937
+ }
938
+ }
939
+ } else {
940
+ const result = this.editor.getElementsByRange(true, false)
941
+ result.forEach(el => {
942
+ if (el.element.isBlock() || el.element.isInblock()) {
943
+ if (el.element.hasStyles()) {
944
+ el.element.styles['text-align'] = value
945
+ } else {
946
+ el.element.styles = {
947
+ 'text-align': value
948
+ }
949
+ }
950
+ } else {
951
+ const block = el.element.getBlock()
952
+ const inblock = el.element.getInblock()
953
+ if (inblock) {
954
+ if (inblock.hasStyles()) {
955
+ inblock.styles['text-align'] = value
956
+ } else {
957
+ inblock.styles = {
958
+ 'text-align': value
959
+ }
960
+ }
961
+ } else {
962
+ if (block.hasStyles()) {
963
+ block.styles['text-align'] = value
964
+ } else {
965
+ block.styles = {
966
+ 'text-align': value
967
+ }
968
+ }
969
+ }
970
+ }
971
+ })
972
+ }
973
+ this.editor.formatElementStack()
974
+ this.editor.domRender()
975
+ this.editor.rangeRender()
976
+ },
977
+ //api:撤销
978
+ undo() {
979
+ if (this.disabled) {
980
+ return
981
+ }
982
+ const historyRecord = this.editor.history.get(-1)
983
+ if (historyRecord) {
984
+ this.editor.history.current = historyRecord.current
985
+ this.editor.stack = historyRecord.stack
986
+ this.editor.range = historyRecord.range
987
+ this.editor.formatElementStack()
988
+ this.editor.domRender(true)
989
+ this.editor.rangeRender()
990
+ }
991
+ },
992
+ //api:重做
993
+ redo() {
994
+ if (this.disabled) {
995
+ return
996
+ }
997
+ const historyRecord = this.editor.history.get(1)
998
+ if (historyRecord) {
999
+ this.editor.history.current = historyRecord.current
1000
+ this.editor.stack = historyRecord.stack
1001
+ this.editor.range = historyRecord.range
1002
+ this.editor.formatElementStack()
1003
+ this.editor.domRender(true)
1004
+ this.editor.rangeRender()
1005
+ }
1006
+ },
1007
+ //api:插入引用
1008
+ setQuote() {
1009
+ if (this.disabled) {
1010
+ return
1011
+ }
1012
+ //起点和终点在一起
1013
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1014
+ const block = this.editor.range.anchor.element.getBlock()
1015
+ const oldParsedom = block.parsedom
1016
+ blockToParagraph(block)
1017
+ if (oldParsedom != 'blockquote') {
1018
+ block.parsedom = 'blockquote'
1019
+ }
1020
+ }
1021
+ //起点和终点不在一起
1022
+ else {
1023
+ let blocks = []
1024
+ const result = this.editor.getElementsByRange(true, false)
1025
+ result.forEach(item => {
1026
+ const block = item.element.getBlock()
1027
+ const exist = blocks.some(el => block.isEqual(el))
1028
+ if (!exist) {
1029
+ blocks.push(block)
1030
+ }
1031
+ })
1032
+ blocks.forEach(block => {
1033
+ const oldParsedom = block.parsedom
1034
+ blockToParagraph(block)
1035
+ if (oldParsedom != 'blockquote') {
1036
+ block.parsedom = 'blockquote'
1037
+ }
1038
+ })
1039
+ }
1040
+ this.editor.formatElementStack()
1041
+ this.editor.domRender()
1042
+ this.editor.rangeRender()
1043
+ },
1044
+ //api:设置行高
1045
+ setLineHeight(value) {
1046
+ if (this.disabled) {
1047
+ return
1048
+ }
1049
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1050
+ const block = this.editor.range.anchor.element.getBlock()
1051
+ const inblock = this.editor.range.anchor.element.getInblock()
1052
+ if (inblock) {
1053
+ if (inblock.hasStyles()) {
1054
+ inblock.styles['line-height'] = value
1055
+ } else {
1056
+ inblock.styles = {
1057
+ 'line-height': value
1058
+ }
1059
+ }
1060
+ } else {
1061
+ if (block.hasStyles()) {
1062
+ block.styles['line-height'] = value
1063
+ } else {
1064
+ block.styles = {
1065
+ 'line-height': value
1066
+ }
1067
+ }
1068
+ }
1069
+ } else {
1070
+ const result = this.editor.getElementsByRange(true, false)
1071
+ result.forEach(el => {
1072
+ if (el.element.isBlock() || el.element.isInblock()) {
1073
+ if (el.element.hasStyles()) {
1074
+ el.element.styles['line-height'] = value
1075
+ } else {
1076
+ el.element.styles = {
1077
+ 'line-height': value
1078
+ }
1079
+ }
1080
+ } else {
1081
+ const block = el.element.getBlock()
1082
+ const inblock = el.element.getInblock()
1083
+ if (inblock) {
1084
+ if (inblock.hasStyles()) {
1085
+ inblock.styles['line-height'] = value
1086
+ } else {
1087
+ inblock.styles = {
1088
+ 'line-height': value
1089
+ }
1090
+ }
1091
+ } else {
1092
+ if (block.hasStyles()) {
1093
+ block.styles['line-height'] = value
1094
+ } else {
1095
+ block.styles = {
1096
+ 'line-height': value
1097
+ }
1098
+ }
1099
+ }
1100
+ }
1101
+ })
1102
+ }
1103
+ this.editor.formatElementStack()
1104
+ this.editor.domRender()
1105
+ this.editor.rangeRender()
1106
+ },
1107
+ //api:增加缩进
1108
+ setIndentIncrease() {
1109
+ if (this.disabled) {
1110
+ return
1111
+ }
1112
+ const fn = element => {
1113
+ if (element.hasStyles()) {
1114
+ if (element.styles.hasOwnProperty('text-indent')) {
1115
+ let val = element.styles['text-indent']
1116
+ if (val.endsWith('em')) {
1117
+ val = parseFloat(val)
1118
+ } else {
1119
+ val = 0
1120
+ }
1121
+ element.styles['text-indent'] = `${val + 2}em`
1122
+ } else {
1123
+ element.styles['text-indent'] = '2em'
1124
+ }
1125
+ } else {
1126
+ element.styles = {
1127
+ 'text-indent': '2em'
1128
+ }
1129
+ }
1130
+ }
1131
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1132
+ const block = this.editor.range.anchor.element.getBlock()
1133
+ const inblock = this.editor.range.anchor.element.getInblock()
1134
+ if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1135
+ fn(inblock)
1136
+ } else if (!block.isPreStyle()) {
1137
+ fn(block)
1138
+ }
1139
+ } else {
1140
+ const result = this.editor.getElementsByRange(true, false)
1141
+ result.forEach(item => {
1142
+ const block = item.element.getBlock()
1143
+ const inblock = item.element.getInblock()
1144
+ if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1145
+ fn(inblock)
1146
+ } else if (!block.isPreStyle()) {
1147
+ fn(block)
1148
+ }
1149
+ })
1150
+ }
1151
+ this.editor.formatElementStack()
1152
+ this.editor.domRender()
1153
+ this.editor.rangeRender()
1154
+ },
1155
+ //api:减少缩进
1156
+ setIndentDecrease() {
1157
+ if (this.disabled) {
1158
+ return
1159
+ }
1160
+ const fn = element => {
1161
+ if (element.hasStyles() && element.styles.hasOwnProperty('text-indent')) {
1162
+ let val = element.styles['text-indent']
1163
+ if (val.endsWith('em')) {
1164
+ val = parseFloat(val)
1165
+ } else {
1166
+ val = 0
1167
+ }
1168
+ element.styles['text-indent'] = `${val - 2 >= 0 ? val - 2 : 0}em`
1169
+ }
1170
+ }
1171
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1172
+ const block = this.editor.range.anchor.element.getBlock()
1173
+ const inblock = this.editor.range.anchor.element.getInblock()
1174
+ if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1175
+ fn(inblock)
1176
+ } else if (!block.isPreStyle()) {
1177
+ fn(block)
1178
+ }
1179
+ } else {
1180
+ const result = this.getElementsByRange(true, false)
1181
+ result.forEach(item => {
1182
+ const block = item.element.getBlock()
1183
+ const inblock = item.element.getInblock()
1184
+ if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1185
+ fn(inblock)
1186
+ } else if (!block.isPreStyle()) {
1187
+ fn(block)
1188
+ }
1189
+ })
1190
+ }
1191
+ this.editor.formatElementStack()
1192
+ this.editor.domRender()
1193
+ this.editor.rangeRender()
1194
+ },
1195
+ //api:插入图片
1196
+ insertImage(url) {
1197
+ if (this.disabled) {
1198
+ return
1199
+ }
1200
+ if (!url || typeof url != 'string') {
1201
+ throw new Error('An image address must be given')
1202
+ }
1203
+ const image = new AlexElement(
1204
+ 'closed',
1205
+ 'img',
1206
+ {
1207
+ src: url
1208
+ },
1209
+ null,
1210
+ null
1211
+ )
1212
+ this.editor.insertElement(image)
1213
+ this.editor.formatElementStack()
1214
+ this.editor.domRender()
1215
+ this.editor.rangeRender()
1216
+ },
1217
+ //api:插入视频
1218
+ insertVideo(url) {
1219
+ if (this.disabled) {
1220
+ return
1221
+ }
1222
+ if (!url || typeof url != 'string') {
1223
+ throw new Error('A video address must be given')
1224
+ }
1225
+ const video = new AlexElement(
1226
+ 'closed',
1227
+ 'video',
1228
+ {
1229
+ src: url
1230
+ },
1231
+ null,
1232
+ null
1233
+ )
1234
+ this.editor.insertElement(video)
1235
+ const leftSpace = AlexElement.getSpaceElement()
1236
+ const rightSpace = AlexElement.getSpaceElement()
1237
+ this.editor.addElementAfter(rightSpace, video)
1238
+ this.editor.addElementBefore(leftSpace, video)
1239
+ this.editor.range.anchor.moveToEnd(rightSpace)
1240
+ this.editor.range.focus.moveToEnd(rightSpace)
1241
+ this.editor.formatElementStack()
1242
+ this.editor.domRender()
1243
+ this.editor.rangeRender()
1244
+ },
1245
+ //api:选区是否含有代码块样式
1246
+ hasPreStyle() {
1247
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1248
+ return this.editor.range.anchor.element.isPreStyle()
1249
+ } else {
1250
+ const result = this.editor.getElementsByRange(true, false)
1251
+ return result.some(item => {
1252
+ return item.element.isPreStyle()
1253
+ })
1254
+ }
1255
+ },
1256
+ //api:选区是否含有引用
1257
+ hasQuote() {
1258
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1259
+ const block = this.editor.range.anchor.element.getBlock()
1260
+ return block.parsedom == 'blockquote'
1261
+ } else {
1262
+ const result = this.editor.getElementsByRange(true, false)
1263
+ return result.some(item => {
1264
+ if (item.element.isBlock()) {
1265
+ return item.element.parsedom == 'blockquote'
1266
+ } else {
1267
+ const block = item.element.getBlock()
1268
+ return block.parsedom == 'blockquote'
1269
+ }
1270
+ })
1271
+ }
1272
+ },
1273
+ //api:选区是否含有列表
1274
+ hasList(ordered = false) {
1275
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1276
+ const block = this.editor.range.anchor.element.getBlock()
1277
+ return blockIsList(block, ordered)
1278
+ } else {
1279
+ const result = this.editor.getElementsByRange(true, false)
1280
+ return result.some(item => {
1281
+ if (item.element.isBlock()) {
1282
+ return blockIsList(item.element, ordered)
1283
+ } else {
1284
+ const block = item.element.getBlock()
1285
+ return blockIsList(block, ordered)
1286
+ }
1287
+ })
1288
+ }
1289
+ },
1290
+ //api:选区是否含有链接
1291
+ hasLink() {
1292
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1293
+ return !!this.getParsedomElementByElement(this.editor.range.anchor.element, 'a')
1294
+ } else {
1295
+ const result = this.editor.getElementsByRange(true, true).filter(item => {
1296
+ return item.element.isText()
1297
+ })
1298
+ return result.some(item => {
1299
+ return !!this.getParsedomElementByElement(item.element, 'a')
1300
+ })
1301
+ }
1302
+ },
1303
+ //api:选区是否含有表格
1304
+ hasTable() {
1305
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1306
+ const block = this.editor.range.anchor.element.getBlock()
1307
+ return block.parsedom == 'table'
1308
+ } else {
1309
+ const result = this.editor.getElementsByRange(true, false)
1310
+ return result.some(item => {
1311
+ if (item.element.isBlock()) {
1312
+ return item.element.parsedom == 'table'
1313
+ } else {
1314
+ const block = item.element.getBlock()
1315
+ return block.parsedom == 'table'
1316
+ }
1317
+ })
1318
+ }
1319
+ },
1320
+ //api:选区是否含有任务列表
1321
+ hasTask() {
1322
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1323
+ const block = this.editor.range.anchor.element.getBlock()
1324
+ return blockIsTask(block)
1325
+ } else {
1326
+ const result = this.editor.getElementsByRange(true, false)
1327
+ return result.some(item => {
1328
+ if (item.element.isBlock()) {
1329
+ return blockIsTask(item.element)
1330
+ } else {
1331
+ const block = item.element.getBlock()
1332
+ return blockIsTask(block)
1333
+ }
1334
+ })
1335
+ }
1336
+ },
1337
+ //选区是否含有图片
1338
+ hasImage() {
1339
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1340
+ return this.editor.range.anchor.element.isClosed() && this.editor.range.anchor.element.parsedom == 'img'
1341
+ } else {
1342
+ const result = this.editor.getElementsByRange(true, true)
1343
+ return result.some(item => {
1344
+ return item.element.isClosed() && item.element.parsedom == 'img'
1345
+ })
1346
+ }
1347
+ },
1348
+ //选区是否含有视频
1349
+ hasVideo() {
1350
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1351
+ return this.editor.range.anchor.element.isClosed() && this.editor.range.anchor.element.parsedom == 'video'
1352
+ } else {
1353
+ const result = this.editor.getElementsByRange(true, true)
1354
+ return result.some(item => {
1355
+ return item.element.isClosed() && item.element.parsedom == 'video'
1356
+ })
1357
+ }
1358
+ },
1359
+ //api:选区是否全部在引用内
1360
+ inQuote() {
1361
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1362
+ const block = this.editor.range.anchor.element.getBlock()
1363
+ return block.parsedom == 'blockquote'
1364
+ } else {
1365
+ const result = this.editor.getElementsByRange(true, false)
1366
+ return result.every(item => {
1367
+ if (item.element.isBlock()) {
1368
+ return item.element.parsedom == 'blockquote'
1369
+ } else {
1370
+ const block = item.element.getBlock()
1371
+ return block.parsedom == 'blockquote'
1372
+ }
1373
+ })
1374
+ }
1375
+ },
1376
+ //api:选区是否全部在列表内
1377
+ inList(ordered = false) {
1378
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1379
+ const block = this.editor.range.anchor.element.getBlock()
1380
+ return blockIsList(block, ordered)
1381
+ } else {
1382
+ const result = this.editor.getElementsByRange(true, false)
1383
+ return result.every(item => {
1384
+ if (item.element.isBlock()) {
1385
+ return blockIsList(item.element, ordered)
1386
+ } else {
1387
+ const block = item.element.getBlock()
1388
+ return blockIsList(block, ordered)
1389
+ }
1390
+ })
1391
+ }
1392
+ },
1393
+ //api:选区是否全部在任务列表里
1394
+ inTask() {
1395
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1396
+ const block = this.editor.range.anchor.element.getBlock()
1397
+ return blockIsTask(block)
1398
+ } else {
1399
+ const result = this.editor.getElementsByRange(true, false)
1400
+ return result.every(item => {
1401
+ if (item.element.isBlock()) {
1402
+ return blockIsTask(item.element)
1403
+ } else {
1404
+ const block = item.element.getBlock()
1405
+ return blockIsTask(block)
1406
+ }
1407
+ })
1408
+ }
1409
+ },
1410
+ //api:创建一个空的表格
1411
+ insertTable(rowLength, colLength) {
1412
+ const table = new AlexElement('block', 'table', null, null, null)
1413
+ const tbody = new AlexElement('inblock', 'tbody', null, null, null)
1414
+ this.editor.addElementTo(tbody, table)
1415
+ for (let i = 0; i < rowLength; i++) {
1416
+ const row = new AlexElement('inblock', 'tr', null, null, null)
1417
+ for (let j = 0; j < colLength; j++) {
1418
+ const column = new AlexElement('inblock', 'td', null, null, null)
1419
+ const breakEl = new AlexElement('closed', 'br', null, null, null)
1420
+ this.editor.addElementTo(breakEl, column)
1421
+ this.editor.addElementTo(column, row)
1422
+ }
1423
+ this.editor.addElementTo(row, tbody)
1424
+ }
1425
+ this.editor.insertElement(table)
1426
+ //在表格后创建一个段落
1427
+ const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1428
+ const breakEl = new AlexElement('closed', 'br', null, null, null)
1429
+ this.editor.addElementTo(breakEl, paragraph)
1430
+ this.editor.addElementAfter(paragraph, table)
1431
+ this.editor.formatElementStack()
1432
+ this.editor.range.anchor.moveToStart(tbody)
1433
+ this.editor.range.focus.moveToStart(tbody)
1434
+ this.editor.domRender()
1435
+ this.editor.rangeRender()
1436
+ },
1437
+ //api:插入代码块
1438
+ insertCodeBlock() {
1439
+ if (this.disabled) {
1440
+ return
1441
+ }
1442
+ const pre = this.getCurrentParsedomElement('pre')
1443
+ if (pre) {
1444
+ let content = ''
1445
+ AlexElement.flatElements(pre.children)
1446
+ .filter(item => {
1447
+ return item.isText()
1448
+ })
1449
+ .forEach(item => {
1450
+ content += item.textContent
1451
+ })
1452
+ const splits = content.split('\n')
1453
+ splits.forEach(item => {
1454
+ const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1455
+ const text = new AlexElement('text', null, null, null, item)
1456
+ this.editor.addElementTo(text, paragraph)
1457
+ this.editor.addElementBefore(paragraph, pre)
1458
+ })
1459
+ pre.toEmpty()
1460
+ } else {
1461
+ //起点和终点在一起
1462
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1463
+ const block = this.editor.range.anchor.element.getBlock()
1464
+ blockToParagraph(block)
1465
+ block.parsedom = 'pre'
1466
+ const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1467
+ const breakEl = new AlexElement('closed', 'br', null, null, null)
1468
+ this.editor.addElementTo(breakEl, paragraph)
1469
+ this.editor.addElementAfter(paragraph, block)
1470
+ }
1471
+ //起点和终点不在一起
1472
+ else {
1473
+ let result = this.editor.getElementsByRange(true, false)
1474
+ this.editor.range.anchor.moveToStart(result[0].element.getBlock())
1475
+ this.editor.range.focus.moveToEnd(result[result.length - 1].element.getBlock())
1476
+ const res = this.editor.getElementsByRange(true, true).filter(el => el.element.isText())
1477
+ const obj = {}
1478
+ res.forEach(el => {
1479
+ if (obj[el.element.getBlock().key]) {
1480
+ obj[el.element.getBlock().key].push(el.element.clone())
1481
+ } else {
1482
+ obj[el.element.getBlock().key] = [el.element.clone()]
1483
+ }
1484
+ })
1485
+ const pre = new AlexElement('block', 'pre', null, null, null)
1486
+ Object.keys(obj).forEach((key, index) => {
1487
+ if (index > 0) {
1488
+ const text = new AlexElement('text', null, null, null, '\n')
1489
+ if (pre.hasChildren()) {
1490
+ this.editor.addElementTo(text, pre, pre.children.length)
1491
+ } else {
1492
+ this.editor.addElementTo(text, pre)
1493
+ }
1494
+ }
1495
+ obj[key].forEach(el => {
1496
+ if (pre.hasChildren()) {
1497
+ this.editor.addElementTo(el, pre, pre.children.length)
1498
+ } else {
1499
+ this.editor.addElementTo(el, pre)
1500
+ }
1501
+ })
1502
+ })
1503
+ this.editor.delete()
1504
+ this.editor.insertElement(pre)
1505
+ const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1506
+ const breakEl = new AlexElement('closed', 'br', null, null, null)
1507
+ this.editor.addElementTo(breakEl, paragraph)
1508
+ this.editor.addElementAfter(paragraph, pre)
1509
+ }
1510
+ }
1511
+ this.editor.formatElementStack()
1512
+ this.editor.domRender()
1513
+ this.editor.rangeRender()
1514
+ }
1515
+ },
1516
+ beforeUnmount() {
1517
+ //卸载绑定在滚动元素上的事件
1518
+ this.removeScrollHandle()
1519
+ //卸载绑定在document.documentElement上的事件
1520
+ Dap.event.off(document.documentElement, `mousedown.editify_${this.uid} mousemove.editify_${this.uid} mouseup.editify_${this.uid} click.editify_${this.uid}`)
1521
+ //卸载绑定在window上的事件
1522
+ Dap.event.off(window, `resize.editify_${this.uid}`)
1523
+ //销毁编辑器
1524
+ this.editor.destroy()
1525
+ }
1526
+ }
1527
+ </script>
1528
+ <style lang="less" scoped>
1529
+ .editify {
1530
+ display: block;
1531
+ width: 100%;
1532
+ position: relative;
1533
+ box-sizing: border-box;
1534
+ -webkit-tap-highlight-color: transparent;
1535
+ outline: none;
1536
+ font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, Roboto, 'Segoe UI', 'Microsoft YaHei', Arial, sans-serif;
1537
+ line-height: 1.5;
1538
+
1539
+ *,
1540
+ *::before,
1541
+ *::after {
1542
+ box-sizing: border-box;
1543
+ -webkit-tap-highlight-color: transparent;
1544
+ outline: none;
1545
+ }
1546
+ }
1547
+
1548
+ .editify-body {
1549
+ display: block;
1550
+ width: 100%;
1551
+ position: relative;
1552
+ background-color: @background;
1553
+ padding: 1px;
1554
+
1555
+ &.border {
1556
+ border: 1px solid @border-color;
1557
+ border-radius: 4px;
1558
+ transition: all 500ms;
1559
+
1560
+ &.menu_inner {
1561
+ border-radius: 0 0 4px 4px;
1562
+ border-top: none;
1563
+ }
1564
+ }
1565
+
1566
+ //编辑器样式
1567
+ .editify-content {
1568
+ display: block;
1569
+ position: relative;
1570
+ overflow-x: hidden;
1571
+ overflow-y: auto;
1572
+ width: 100%;
1573
+ border-radius: inherit;
1574
+ padding: 6px 10px;
1575
+ line-height: 1.5;
1576
+ color: @font-color-dark;
1577
+ font-size: @font-size;
1578
+ position: relative;
1579
+ line-height: 1.5;
1580
+
1581
+ //显示占位符
1582
+ &.placeholder::before {
1583
+ position: absolute;
1584
+ top: 0;
1585
+ left: 0;
1586
+ display: block;
1587
+ width: 100%;
1588
+ content: attr(data-editify-placeholder);
1589
+ font-size: inherit;
1590
+ font-family: inherit;
1591
+ color: @font-color-disabled;
1592
+ line-height: inherit;
1593
+ padding: 6px 10px;
1594
+ cursor: text;
1595
+ }
1596
+
1597
+ //段落样式和标题
1598
+ :deep(p),
1599
+ :deep(h1),
1600
+ :deep(h2),
1601
+ :deep(h3),
1602
+ :deep(h4),
1603
+ :deep(h5),
1604
+ :deep(h6) {
1605
+ display: block;
1606
+ width: 100%;
1607
+ margin: 0 0 15px 0;
1608
+ padding: 0;
1609
+ }
1610
+ :deep(h1) {
1611
+ font-size: 32px;
1612
+ }
1613
+ :deep(h2) {
1614
+ font-size: 28px;
1615
+ }
1616
+ :deep(h3) {
1617
+ font-size: 24px;
1618
+ }
1619
+ :deep(h4) {
1620
+ font-size: 20px;
1621
+ }
1622
+ :deep(h5) {
1623
+ font-size: 18px;
1624
+ }
1625
+ :deep(h6) {
1626
+ font-size: 16px;
1627
+ }
1628
+ //有序列表样式
1629
+ :deep(div[data-editify-list='ol']) {
1630
+ margin-bottom: 15px;
1631
+
1632
+ &::before {
1633
+ content: attr(data-editify-value) '.';
1634
+ margin-right: 10px;
1635
+ }
1636
+ }
1637
+ //无序列表样式
1638
+ :deep(div[data-editify-list='ul']) {
1639
+ margin-bottom: 15px;
1640
+
1641
+ &::before {
1642
+ content: '\2022';
1643
+ margin-right: 10px;
1644
+ }
1645
+ }
1646
+ //代码样式
1647
+ :deep(span[data-editify-code]) {
1648
+ display: inline-block;
1649
+ padding: 3px 6px;
1650
+ margin: 0 4px;
1651
+ border-radius: 4px;
1652
+ line-height: 1;
1653
+ font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
1654
+ background-color: @pre-background;
1655
+ color: @font-color;
1656
+ border: 1px solid @border-color;
1657
+ text-indent: initial;
1658
+ font-size: @font-size;
1659
+ font-weight: normal;
1660
+ }
1661
+ //链接样式
1662
+ :deep(a) {
1663
+ color: @font-color-link;
1664
+ transition: all 200ms;
1665
+ text-decoration: none;
1666
+ cursor: text;
1667
+
1668
+ &:hover {
1669
+ color: @font-color-link-dark;
1670
+ text-decoration: underline;
1671
+ }
1672
+ }
1673
+ //表格样式
1674
+ :deep(table) {
1675
+ width: 100%;
1676
+ border: 1px solid @border-color;
1677
+ margin: 0;
1678
+ padding: 0;
1679
+ border-collapse: collapse;
1680
+ margin-bottom: 15px;
1681
+ background-color: @background;
1682
+ color: @font-color-dark;
1683
+ font-size: @font-size;
1684
+
1685
+ * {
1686
+ margin: 0 !important;
1687
+ }
1688
+
1689
+ tbody {
1690
+ margin: 0;
1691
+ padding: 0;
1692
+
1693
+ tr {
1694
+ margin: 0;
1695
+ padding: 0;
1696
+
1697
+ &:first-child {
1698
+ background-color: @background-darker;
1699
+
1700
+ td {
1701
+ font-weight: bold;
1702
+ position: relative;
1703
+ }
1704
+ }
1705
+
1706
+ td {
1707
+ margin: 0;
1708
+ border: 1px solid @border-color;
1709
+ padding: 6px 10px;
1710
+ position: relative;
1711
+ word-break: break-word;
1712
+
1713
+ &:not(:last-child)::after {
1714
+ position: absolute;
1715
+ right: -5px;
1716
+ top: 0;
1717
+ width: 10px;
1718
+ height: 100%;
1719
+ content: '';
1720
+ z-index: 1;
1721
+ cursor: col-resize;
1722
+ user-select: none;
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+ }
1728
+ //代码块样式
1729
+ :deep(pre) {
1730
+ display: block;
1731
+ padding: 6px 10px;
1732
+ margin: 0 0 15px;
1733
+ font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
1734
+ line-height: 1.5;
1735
+ font-size: @font-size;
1736
+ color: @font-color-dark;
1737
+ background-color: @pre-background;
1738
+ border: 1px solid @border-color;
1739
+ border-radius: 4px;
1740
+ overflow: auto;
1741
+ position: relative;
1742
+ }
1743
+ //图片样式
1744
+ :deep(img) {
1745
+ position: relative;
1746
+ display: inline-block;
1747
+ width: 30%;
1748
+ height: auto;
1749
+ border-radius: 2px;
1750
+ vertical-align: text-bottom;
1751
+ margin: 0 2px;
1752
+ }
1753
+ //视频样式
1754
+ :deep(video) {
1755
+ position: relative;
1756
+ display: inline-block;
1757
+ width: 30%;
1758
+ border-radius: 2px;
1759
+ vertical-align: text-bottom;
1760
+ background-color: #000;
1761
+ object-fit: contain;
1762
+ margin: 0 2px;
1763
+ }
1764
+ //引用样式
1765
+ :deep(blockquote) {
1766
+ display: block;
1767
+ border-left: 8px solid @background-darker;
1768
+ padding: 6px 10px 6px 20px;
1769
+ margin: 0 0 15px;
1770
+ line-height: 1.5;
1771
+ font-size: @font-size;
1772
+ color: @font-color-light;
1773
+ border-radius: 0;
1774
+ }
1775
+ //任务列表样式
1776
+ :deep(div[data-editify-task]) {
1777
+ margin-bottom: 15px;
1778
+ position: relative;
1779
+ padding-left: 26px;
1780
+ font-size: @font-size;
1781
+ color: @font-color-dark;
1782
+ transition: all 200ms;
1783
+
1784
+ &::before {
1785
+ display: block;
1786
+ width: 16px;
1787
+ height: 16px;
1788
+ border-radius: 2px;
1789
+ border: 2px solid @font-color-light;
1790
+ transition: all 200ms;
1791
+ box-sizing: border-box;
1792
+ user-select: none;
1793
+ content: '';
1794
+ position: absolute;
1795
+ left: 0;
1796
+ top: 50%;
1797
+ transform: translateY(-50%);
1798
+ z-index: 1;
1799
+ cursor: pointer;
1800
+ }
1801
+
1802
+ &::after {
1803
+ position: absolute;
1804
+ content: '';
1805
+ left: 3px;
1806
+ top: 6px;
1807
+ display: inline-block;
1808
+ width: 10px;
1809
+ height: 6px;
1810
+ border: 2px solid @font-color-light;
1811
+ border-top: none;
1812
+ border-right: none;
1813
+ transform: rotate(-45deg);
1814
+ transform-origin: center;
1815
+ margin-bottom: 2px;
1816
+ box-sizing: border-box;
1817
+ z-index: 2;
1818
+ cursor: pointer;
1819
+ opacity: 0;
1820
+ transition: all 200ms;
1821
+ }
1822
+
1823
+ &[data-editify-task='checked'] {
1824
+ text-decoration: line-through;
1825
+ color: @font-color-light;
1826
+ &::after {
1827
+ opacity: 1;
1828
+ }
1829
+ }
1830
+ }
1831
+
1832
+ //禁用样式
1833
+ &.disabled {
1834
+ cursor: auto !important;
1835
+ &.placeholder::before {
1836
+ cursor: auto;
1837
+ }
1838
+ :deep(a) {
1839
+ cursor: pointer;
1840
+ }
1841
+ }
1842
+ }
1843
+
1844
+ //代码视图
1845
+ .editify-source {
1846
+ display: block;
1847
+ width: 100%;
1848
+ height: 100%;
1849
+ position: absolute;
1850
+ left: 0;
1851
+ top: 0;
1852
+ z-index: 10;
1853
+ background-color: @reverse-background;
1854
+ border-radius: inherit;
1855
+ margin: 0;
1856
+ padding: 6px 10px;
1857
+ overflow-x: hidden;
1858
+ overflow-y: auto;
1859
+ font-size: @font-size;
1860
+ color: @reverse-color;
1861
+ font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
1862
+ resize: none;
1863
+ border: none;
1864
+ }
1865
+ }
1866
+
1867
+ .editify-footer {
1868
+ display: flex;
1869
+ justify-content: end;
1870
+ align-items: center;
1871
+ width: 100%;
1872
+ padding: 10px;
1873
+
1874
+ .editify-footer-words {
1875
+ font-size: @font-size;
1876
+ color: @font-color-light;
1877
+ line-height: 1;
1878
+ }
1879
+ }
1880
+ </style>