vue-editify 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>