vue-editify 0.0.24 → 0.0.26

Sign up to get free protection for your applications and to get access to all the features.
package/src/Editify.vue CHANGED
@@ -1,2004 +1,2004 @@
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.range = null
123
- this.editor.formatElementStack()
124
- this.editor.domRender()
125
- this.editor.rangeRender()
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
- if (!this.editor.range) {
434
- this.editor.initRange()
435
- }
436
- this.editor.range.anchor.moveToEnd(element)
437
- this.editor.range.focus.moveToEnd(element)
438
- this.editor.formatElementStack()
439
- this.editor.domRender()
440
- this.editor.rangeRender()
441
- }
442
- }
443
- }
444
- }
445
- },
446
- //编辑器的值更新
447
- handleEditorChange(newVal, oldVal) {
448
- if (this.disabled) {
449
- return
450
- }
451
- //内部修改
452
- this.internalModify(newVal)
453
- //触发change事件
454
- this.$emit('change', newVal, oldVal)
455
- },
456
- //编辑器失去焦点
457
- handleEditorBlur(val) {
458
- if (this.disabled) {
459
- return
460
- }
461
- if (this.border && this.color) {
462
- //恢复编辑区域边框颜色
463
- this.$refs.body.style.borderColor = ''
464
- //恢复编辑区域阴影颜色
465
- this.$refs.body.style.boxShadow = ''
466
- //使用菜单栏的情况下恢复菜单栏的样式
467
- if (this.menuConfig.use) {
468
- this.$refs.menu.$el.style.borderColor = ''
469
- this.$refs.menu.$el.style.boxShadow = ''
470
- }
471
- }
472
- this.$emit('blur', val)
473
- },
474
- //编辑器获取焦点
475
- handleEditorFocus(val) {
476
- if (this.disabled) {
477
- return
478
- }
479
- if (this.border && this.color) {
480
- //编辑区域边框颜色
481
- this.$refs.body.style.borderColor = this.color
482
- //转换颜色值
483
- const rgb = Dap.color.hex2rgb(this.color)
484
- //菜单栏模式为inner
485
- if (this.menuConfig.use && this.menuConfig.mode == 'inner') {
486
- //编辑区域除顶部边框的阴影
487
- 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)`
488
- //菜单栏的边框颜色
489
- this.$refs.menu.$el.style.borderColor = this.color
490
- //菜单栏除底部边框的阴影
491
- 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)`
492
- }
493
- //其他菜单栏模式
494
- else if (this.menuConfig.use) {
495
- //编辑区域四边阴影
496
- this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
497
- }
498
- //不使用菜单栏
499
- else {
500
- //编辑区域四边阴影
501
- this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
502
- }
503
- }
504
- //获取焦点时可以使用菜单栏
505
- setTimeout(() => {
506
- this.canUseMenu = true
507
- }, 0)
508
- this.$emit('focus', val)
509
- },
510
- //编辑区域键盘按下
511
- handleEditorKeydown(e) {
512
- if (this.disabled) {
513
- return
514
- }
515
- //增加缩进
516
- if (e.keyCode == 9 && !e.metaKey && !e.shiftKey && !e.ctrlKey && !e.altKey) {
517
- e.preventDefault()
518
- this.setIndentIncrease()
519
- }
520
- //减少缩进
521
- else if (e.keyCode == 9 && !e.metaKey && e.shiftKey && !e.ctrlKey && !e.altKey) {
522
- e.preventDefault()
523
- this.setIndentDecrease()
524
- }
525
- //自定义键盘按下操作
526
- this.$emit('keydown', e)
527
- },
528
- //点击编辑器
529
- handleEditorClick(e) {
530
- if (this.disabled || this.isSourceView) {
531
- return
532
- }
533
- const node = e.target
534
- //点击的是图片或者视频
535
- if (node.nodeName.toLocaleLowerCase() == 'img' || node.nodeName.toLocaleLowerCase() == 'video') {
536
- const key = node.getAttribute('data-editify-element')
537
- if (key) {
538
- const element = this.editor.getElementByKey(key)
539
- if (!this.editor.range) {
540
- this.editor.initRange()
541
- }
542
- this.editor.range.anchor.moveToStart(element)
543
- this.editor.range.focus.moveToEnd(element)
544
- this.editor.rangeRender()
545
- }
546
- }
547
- },
548
- //编辑器换行
549
- handleInsertParagraph(element, previousElement) {
550
- //前一个块元素如果是只包含换行符的元素,并且当前块元素也是包含换行符的元素,则当前块元素转为段落
551
- if (previousElement.isOnlyHasBreak() && element.isOnlyHasBreak()) {
552
- if (!previousElement.isBlock()) {
553
- previousElement.convertToBlock()
554
- }
555
- if (previousElement.parsedom != AlexElement.BLOCK_NODE) {
556
- blockToParagraph(previousElement)
557
- this.editor.range.anchor.moveToStart(previousElement)
558
- this.editor.range.focus.moveToStart(previousElement)
559
- element.toEmpty()
560
- }
561
- }
562
- this.$emit('insertparagraph', this.value)
563
- },
564
- //编辑器焦点更新
565
- handleRangeUpdate(range) {
566
- if (this.disabled) {
567
- return
568
- }
569
- if (this.toolbarConfig.use) {
570
- this.handleToolbar()
571
- }
572
- if (this.menuConfig.use) {
573
- this.$refs.menu.handleRangeUpdate()
574
- }
575
- this.$emit('rangeupdate', this.value, range)
576
- },
577
- //编辑器复制
578
- handleCopy(text, html) {
579
- this.$emit('copy', text, html)
580
- },
581
- //编辑器剪切
582
- handleCut(text, html) {
583
- this.$emit('cut', text, html)
584
- },
585
- //编辑器粘贴纯文本
586
- handlePasteText(data) {
587
- this.$emit('paste-text', data)
588
- },
589
- //编辑器粘贴html
590
- handlePasteHtml(elements, data) {
591
- //粘贴html时过滤元素的样式和属性
592
- AlexElement.flatElements(elements).forEach(el => {
593
- let marks = {}
594
- let styles = {}
595
- if (el.hasMarks()) {
596
- for (let key in pasteKeepData.marks) {
597
- if (el.marks.hasOwnProperty(key) && ((Array.isArray(pasteKeepData.marks[key]) && pasteKeepData.marks[key].includes(el.parsedom)) || pasteKeepData.marks[key] == '*')) {
598
- marks[key] = el.marks[key]
599
- }
600
- }
601
- el.marks = marks
602
- }
603
- if (el.hasStyles() && !el.isText()) {
604
- for (let key in pasteKeepData.styles) {
605
- if (el.styles.hasOwnProperty(key) && ((Array.isArray(pasteKeepData.styles[key]) && pasteKeepData.styles[key].includes(el.parsedom)) || pasteKeepData.styles[key] == '*')) {
606
- styles[key] = el.styles[key]
607
- }
608
- }
609
- el.styles = styles
610
- }
611
- })
612
- this.$emit('paste-html', elements)
613
- },
614
- //编辑器粘贴图片
615
- handlePasteImage(url) {
616
- this.$emit('paste-image', url)
617
- },
618
- //编辑器粘贴视频
619
- handlePasteVideo(url) {
620
- this.$emit('paste-video', url)
621
- },
622
- //编辑器部分删除情景
623
- handleDeleteInStart(element) {
624
- if (element.isBlock()) {
625
- blockToParagraph(element)
626
- }
627
- },
628
- //编辑器删除完成后事件
629
- handleDeleteComplete() {
630
- const uneditable = this.editor.range.anchor.element.getUneditableElement()
631
- if (uneditable) {
632
- uneditable.toEmpty()
633
- }
634
- },
635
- //编辑器dom渲染之前
636
- handleBeforeRender() {
637
- this.$emit('before-render')
638
- },
639
- //编辑器dom渲染
640
- handleAfterRender() {
641
- //设定视频高度
642
- this.setVideoHeight()
643
- //触发事件
644
- this.$emit('after-render')
645
- },
646
- //设定视频高度
647
- setVideoHeight() {
648
- this.$refs.content.querySelectorAll('video').forEach(video => {
649
- video.style.height = video.offsetWidth / this.videoRatio + 'px'
650
- })
651
- },
652
-
653
- //api:光标设置到文档底部
654
- collapseToEnd() {
655
- if (this.disabled) {
656
- return
657
- }
658
- this.editor.collapseToEnd()
659
- this.editor.rangeRender()
660
- Dap.element.setScrollTop({
661
- el: this.$refs.content,
662
- number: 1000000,
663
- time: 0
664
- })
665
- },
666
- //api:光标设置到文档头部
667
- collapseToStart() {
668
- if (this.disabled) {
669
- return
670
- }
671
- this.editor.collapseToStart()
672
- this.editor.rangeRender()
673
- this.$nextTick(() => {
674
- Dap.element.setScrollTop({
675
- el: this.$refs.content,
676
- number: 0,
677
- time: 0
678
- })
679
- })
680
- },
681
- //api:获取某个元素是否在某个标签元素下,如果是返回该标签元素,否则返回null
682
- getParsedomElementByElement(element, parsedom) {
683
- if (element.isBlock()) {
684
- return element.parsedom == parsedom ? element : null
685
- }
686
- if (!element.isText() && element.parsedom == parsedom) {
687
- return element
688
- }
689
- return this.getParsedomElementByElement(element.parent, parsedom)
690
- },
691
- //api:获取光标是否在指定标签元素下,如果是返回该标签元素,否则返回null
692
- getCurrentParsedomElement(parsedom) {
693
- if (this.disabled) {
694
- return null
695
- }
696
- if (!this.editor.range) {
697
- return null
698
- }
699
- if (this.editor.range.anchor.element.isEqual(this.editor.range.focus.element)) {
700
- return this.getParsedomElementByElement(this.editor.range.anchor.element, parsedom)
701
- }
702
- const arr = this.editor.getElementsByRange(true, false).map(item => {
703
- return this.getParsedomElementByElement(item.element, parsedom)
704
- })
705
- let hasNull = arr.some(el => {
706
- return el == null
707
- })
708
- //如果存在null,则表示有的选区元素不在指定标签下,返回null
709
- if (hasNull) {
710
- return null
711
- }
712
- //如果只有一个元素,则返回该元素
713
- if (arr.length == 1) {
714
- return arr[0]
715
- }
716
- //默认数组中的元素都相等
717
- let flag = true
718
- for (let i = 1; i < arr.length; i++) {
719
- if (!arr[i].isEqual(arr[0])) {
720
- flag = false
721
- break
722
- }
723
- }
724
- //如果相等,则返回该元素
725
- if (flag) {
726
- return arr[0]
727
- }
728
- return null
729
- },
730
- //api:删除光标所在的指定标签元素
731
- deleteByParsedom(parsedom) {
732
- if (this.disabled) {
733
- return
734
- }
735
- if (!this.editor.range) {
736
- return
737
- }
738
- const element = this.getCurrentParsedomElement(parsedom)
739
- if (element) {
740
- element.toEmpty()
741
- this.editor.formatElementStack()
742
- this.editor.domRender()
743
- this.editor.rangeRender()
744
- }
745
- },
746
- //api:当光标在链接上时可以移除链接
747
- removeLink() {
748
- if (this.disabled) {
749
- return
750
- }
751
- if (!this.editor.range) {
752
- return
753
- }
754
- const link = this.getCurrentParsedomElement('a')
755
- if (link) {
756
- link.parsedom = AlexElement.TEXT_NODE
757
- delete link.marks.target
758
- delete link.marks.href
759
- this.editor.formatElementStack()
760
- this.editor.domRender()
761
- this.editor.rangeRender()
762
- }
763
- },
764
- //api:设置标题
765
- setHeading(parsedom) {
766
- if (this.disabled) {
767
- return
768
- }
769
- if (!this.editor.range) {
770
- return
771
- }
772
- const values = getButtonOptionsConfig(this.$editTrans, this.$editLocale).heading.map(item => {
773
- return item.value
774
- })
775
- if (!values.includes(parsedom)) {
776
- throw new Error('The parameter supports only h1-h6 and p')
777
- }
778
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
779
- const block = this.editor.range.anchor.element.getBlock()
780
- //先转为段落
781
- blockToParagraph(block)
782
- //设置标题
783
- block.parsedom = parsedom
784
- } else {
785
- const result = this.editor.getElementsByRange(true, false)
786
- result.forEach(el => {
787
- if (el.element.isBlock()) {
788
- blockToParagraph(el.element)
789
- el.element.parsedom = parsedom
790
- } else {
791
- const block = el.element.getBlock()
792
- blockToParagraph(block)
793
- block.parsedom = parsedom
794
- }
795
- })
796
- }
797
- this.editor.formatElementStack()
798
- this.editor.domRender()
799
- this.editor.rangeRender()
800
- },
801
- //api:插入有序列表 ordered为true表示有序列表
802
- setList(ordered) {
803
- if (this.disabled) {
804
- return
805
- }
806
- if (!this.editor.range) {
807
- return
808
- }
809
- //起点和终点在一起
810
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
811
- const block = this.editor.range.anchor.element.getBlock()
812
- const isList = blockIsList(block, ordered)
813
- if (isList) {
814
- blockToParagraph(block)
815
- } else {
816
- blockToList(block, ordered)
817
- }
818
- }
819
- //起点和终点不在一起
820
- else {
821
- let blocks = []
822
- const result = this.editor.getElementsByRange(true, false)
823
- result.forEach(item => {
824
- const block = item.element.getBlock()
825
- const exist = blocks.some(el => block.isEqual(el))
826
- if (!exist) {
827
- blocks.push(block)
828
- }
829
- })
830
- blocks.forEach(block => {
831
- const isList = blockIsList(block, ordered)
832
- if (isList) {
833
- blockToParagraph(block)
834
- } else {
835
- blockToList(block, ordered)
836
- }
837
- })
838
- }
839
- this.editor.formatElementStack()
840
- this.editor.domRender()
841
- this.editor.rangeRender()
842
- },
843
- //api:插入任务列表
844
- setTask() {
845
- if (this.disabled) {
846
- return
847
- }
848
- if (!this.editor.range) {
849
- return
850
- }
851
- //起点和终点在一起
852
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
853
- const block = this.editor.range.anchor.element.getBlock()
854
- const isTask = blockIsTask(block)
855
- if (isTask) {
856
- blockToParagraph(block)
857
- } else {
858
- blockToTask(block)
859
- }
860
- }
861
- //起点和终点不在一起
862
- else {
863
- let blocks = []
864
- const result = this.editor.getElementsByRange(true, false)
865
- result.forEach(item => {
866
- const block = item.element.getBlock()
867
- const exist = blocks.some(el => block.isEqual(el))
868
- if (!exist) {
869
- blocks.push(block)
870
- }
871
- })
872
- blocks.forEach(block => {
873
- const isTask = blockIsTask(block)
874
- if (isTask) {
875
- blockToParagraph(block)
876
- } else {
877
- blockToTask(block)
878
- }
879
- })
880
- }
881
- this.editor.formatElementStack()
882
- this.editor.domRender()
883
- this.editor.rangeRender()
884
- },
885
- //api:设置样式
886
- setTextStyle(name, value) {
887
- if (this.disabled) {
888
- return
889
- }
890
- if (!this.editor.range) {
891
- return
892
- }
893
- const active = this.queryTextStyle(name, value)
894
- if (active) {
895
- this.editor.removeTextStyle([name])
896
- } else {
897
- let styles = {}
898
- styles[name] = value
899
- this.editor.setTextStyle(styles)
900
- }
901
- this.editor.formatElementStack()
902
- this.editor.domRender()
903
- this.editor.rangeRender()
904
- },
905
- //api:查询是否具有某个样式
906
- queryTextStyle(name, value, useCache) {
907
- return this.editor.queryTextStyle(name, value, useCache)
908
- },
909
- //api:设置标记
910
- setTextMark(name, value) {
911
- if (this.disabled) {
912
- return
913
- }
914
- if (!this.editor.range) {
915
- return
916
- }
917
- const active = this.queryTextMark(name, value)
918
- if (active) {
919
- this.editor.removeTextMark([name])
920
- } else {
921
- let marks = {}
922
- marks[name] = value
923
- this.editor.setTextMark(marks)
924
- }
925
- this.editor.formatElementStack()
926
- this.editor.domRender()
927
- this.editor.rangeRender()
928
- },
929
- //api:查询是否具有某个标记
930
- queryTextMark(name, value, useCache) {
931
- return this.editor.queryTextMark(name, value, useCache)
932
- },
933
- //api:清除文本样式和标记
934
- formatText() {
935
- if (this.disabled) {
936
- return
937
- }
938
- if (!this.editor.range) {
939
- return
940
- }
941
- this.editor.removeTextStyle()
942
- this.editor.removeTextMark()
943
- this.editor.formatElementStack()
944
- this.editor.domRender()
945
- this.editor.rangeRender()
946
- },
947
- //api:设置对齐方式,参数取值justify/left/right/center
948
- setAlign(value) {
949
- if (this.disabled) {
950
- return
951
- }
952
- if (!this.editor.range) {
953
- return
954
- }
955
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
956
- const block = this.editor.range.anchor.element.getBlock()
957
- const inblock = this.editor.range.anchor.element.getInblock()
958
- if (inblock) {
959
- if (inblock.hasStyles()) {
960
- inblock.styles['text-align'] = value
961
- } else {
962
- inblock.styles = {
963
- 'text-align': value
964
- }
965
- }
966
- } else {
967
- if (block.hasStyles()) {
968
- block.styles['text-align'] = value
969
- } else {
970
- block.styles = {
971
- 'text-align': value
972
- }
973
- }
974
- }
975
- } else {
976
- const result = this.editor.getElementsByRange(true, false)
977
- result.forEach(el => {
978
- if (el.element.isBlock() || el.element.isInblock()) {
979
- if (el.element.hasStyles()) {
980
- el.element.styles['text-align'] = value
981
- } else {
982
- el.element.styles = {
983
- 'text-align': value
984
- }
985
- }
986
- } else {
987
- const block = el.element.getBlock()
988
- const inblock = el.element.getInblock()
989
- if (inblock) {
990
- if (inblock.hasStyles()) {
991
- inblock.styles['text-align'] = value
992
- } else {
993
- inblock.styles = {
994
- 'text-align': value
995
- }
996
- }
997
- } else {
998
- if (block.hasStyles()) {
999
- block.styles['text-align'] = value
1000
- } else {
1001
- block.styles = {
1002
- 'text-align': value
1003
- }
1004
- }
1005
- }
1006
- }
1007
- })
1008
- }
1009
- this.editor.formatElementStack()
1010
- this.editor.domRender()
1011
- this.editor.rangeRender()
1012
- },
1013
- //api:撤销
1014
- undo() {
1015
- if (this.disabled) {
1016
- return
1017
- }
1018
- const historyRecord = this.editor.history.get(-1)
1019
- if (historyRecord) {
1020
- this.editor.history.current = historyRecord.current
1021
- this.editor.stack = historyRecord.stack
1022
- this.editor.range = historyRecord.range
1023
- this.editor.formatElementStack()
1024
- this.editor.domRender(true)
1025
- this.editor.rangeRender()
1026
- }
1027
- },
1028
- //api:重做
1029
- redo() {
1030
- if (this.disabled) {
1031
- return
1032
- }
1033
- const historyRecord = this.editor.history.get(1)
1034
- if (historyRecord) {
1035
- this.editor.history.current = historyRecord.current
1036
- this.editor.stack = historyRecord.stack
1037
- this.editor.range = historyRecord.range
1038
- this.editor.formatElementStack()
1039
- this.editor.domRender(true)
1040
- this.editor.rangeRender()
1041
- }
1042
- },
1043
- //api:插入引用
1044
- setQuote() {
1045
- if (this.disabled) {
1046
- return
1047
- }
1048
- if (!this.editor.range) {
1049
- return
1050
- }
1051
- //起点和终点在一起
1052
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1053
- const block = this.editor.range.anchor.element.getBlock()
1054
- const oldParsedom = block.parsedom
1055
- blockToParagraph(block)
1056
- if (oldParsedom != 'blockquote') {
1057
- block.parsedom = 'blockquote'
1058
- }
1059
- }
1060
- //起点和终点不在一起
1061
- else {
1062
- let blocks = []
1063
- const result = this.editor.getElementsByRange(true, false)
1064
- result.forEach(item => {
1065
- const block = item.element.getBlock()
1066
- const exist = blocks.some(el => block.isEqual(el))
1067
- if (!exist) {
1068
- blocks.push(block)
1069
- }
1070
- })
1071
- blocks.forEach(block => {
1072
- const oldParsedom = block.parsedom
1073
- blockToParagraph(block)
1074
- if (oldParsedom != 'blockquote') {
1075
- block.parsedom = 'blockquote'
1076
- }
1077
- })
1078
- }
1079
- this.editor.formatElementStack()
1080
- this.editor.domRender()
1081
- this.editor.rangeRender()
1082
- },
1083
- //api:设置行高
1084
- setLineHeight(value) {
1085
- if (this.disabled) {
1086
- return
1087
- }
1088
- if (!this.editor.range) {
1089
- return
1090
- }
1091
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1092
- const block = this.editor.range.anchor.element.getBlock()
1093
- const inblock = this.editor.range.anchor.element.getInblock()
1094
- if (inblock) {
1095
- if (inblock.hasStyles()) {
1096
- inblock.styles['line-height'] = value
1097
- } else {
1098
- inblock.styles = {
1099
- 'line-height': value
1100
- }
1101
- }
1102
- } else {
1103
- if (block.hasStyles()) {
1104
- block.styles['line-height'] = value
1105
- } else {
1106
- block.styles = {
1107
- 'line-height': value
1108
- }
1109
- }
1110
- }
1111
- } else {
1112
- const result = this.editor.getElementsByRange(true, false)
1113
- result.forEach(el => {
1114
- if (el.element.isBlock() || el.element.isInblock()) {
1115
- if (el.element.hasStyles()) {
1116
- el.element.styles['line-height'] = value
1117
- } else {
1118
- el.element.styles = {
1119
- 'line-height': value
1120
- }
1121
- }
1122
- } else {
1123
- const block = el.element.getBlock()
1124
- const inblock = el.element.getInblock()
1125
- if (inblock) {
1126
- if (inblock.hasStyles()) {
1127
- inblock.styles['line-height'] = value
1128
- } else {
1129
- inblock.styles = {
1130
- 'line-height': value
1131
- }
1132
- }
1133
- } else {
1134
- if (block.hasStyles()) {
1135
- block.styles['line-height'] = value
1136
- } else {
1137
- block.styles = {
1138
- 'line-height': value
1139
- }
1140
- }
1141
- }
1142
- }
1143
- })
1144
- }
1145
- this.editor.formatElementStack()
1146
- this.editor.domRender()
1147
- this.editor.rangeRender()
1148
- },
1149
- //api:增加缩进
1150
- setIndentIncrease() {
1151
- if (this.disabled) {
1152
- return
1153
- }
1154
- if (!this.editor.range) {
1155
- return
1156
- }
1157
- const fn = element => {
1158
- if (element.hasStyles()) {
1159
- if (element.styles.hasOwnProperty('text-indent')) {
1160
- let val = element.styles['text-indent']
1161
- if (val.endsWith('em')) {
1162
- val = parseFloat(val)
1163
- } else {
1164
- val = 0
1165
- }
1166
- element.styles['text-indent'] = `${val + 2}em`
1167
- } else {
1168
- element.styles['text-indent'] = '2em'
1169
- }
1170
- } else {
1171
- element.styles = {
1172
- 'text-indent': '2em'
1173
- }
1174
- }
1175
- }
1176
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1177
- const block = this.editor.range.anchor.element.getBlock()
1178
- const inblock = this.editor.range.anchor.element.getInblock()
1179
- if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1180
- fn(inblock)
1181
- } else if (!block.isPreStyle()) {
1182
- fn(block)
1183
- }
1184
- } else {
1185
- const result = this.editor.getElementsByRange(true, false)
1186
- result.forEach(item => {
1187
- const block = item.element.getBlock()
1188
- const inblock = item.element.getInblock()
1189
- if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1190
- fn(inblock)
1191
- } else if (!block.isPreStyle()) {
1192
- fn(block)
1193
- }
1194
- })
1195
- }
1196
- this.editor.formatElementStack()
1197
- this.editor.domRender()
1198
- this.editor.rangeRender()
1199
- },
1200
- //api:减少缩进
1201
- setIndentDecrease() {
1202
- if (this.disabled) {
1203
- return
1204
- }
1205
- if (!this.editor.range) {
1206
- return
1207
- }
1208
- const fn = element => {
1209
- if (element.hasStyles() && element.styles.hasOwnProperty('text-indent')) {
1210
- let val = element.styles['text-indent']
1211
- if (val.endsWith('em')) {
1212
- val = parseFloat(val)
1213
- } else {
1214
- val = 0
1215
- }
1216
- element.styles['text-indent'] = `${val - 2 >= 0 ? val - 2 : 0}em`
1217
- }
1218
- }
1219
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1220
- const block = this.editor.range.anchor.element.getBlock()
1221
- const inblock = this.editor.range.anchor.element.getInblock()
1222
- if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1223
- fn(inblock)
1224
- } else if (!block.isPreStyle()) {
1225
- fn(block)
1226
- }
1227
- } else {
1228
- const result = this.editor.getElementsByRange(true, false)
1229
- result.forEach(item => {
1230
- const block = item.element.getBlock()
1231
- const inblock = item.element.getInblock()
1232
- if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1233
- fn(inblock)
1234
- } else if (!block.isPreStyle()) {
1235
- fn(block)
1236
- }
1237
- })
1238
- }
1239
- this.editor.formatElementStack()
1240
- this.editor.domRender()
1241
- this.editor.rangeRender()
1242
- },
1243
- //api:插入图片
1244
- insertImage(url, isRender = true) {
1245
- if (this.disabled) {
1246
- return
1247
- }
1248
- if (!this.editor.range) {
1249
- return
1250
- }
1251
- if (!url || typeof url != 'string') {
1252
- throw new Error('An image address must be given')
1253
- }
1254
- const image = new AlexElement(
1255
- 'closed',
1256
- 'img',
1257
- {
1258
- src: url
1259
- },
1260
- null,
1261
- null
1262
- )
1263
- this.editor.insertElement(image)
1264
- if (isRender) {
1265
- this.editor.formatElementStack()
1266
- this.editor.domRender()
1267
- this.editor.rangeRender()
1268
- }
1269
- },
1270
- //api:插入视频
1271
- insertVideo(url, isRender = true) {
1272
- if (this.disabled) {
1273
- return
1274
- }
1275
- if (!this.editor.range) {
1276
- return
1277
- }
1278
- if (!url || typeof url != 'string') {
1279
- throw new Error('A video address must be given')
1280
- }
1281
- const video = new AlexElement(
1282
- 'closed',
1283
- 'video',
1284
- {
1285
- src: url
1286
- },
1287
- null,
1288
- null
1289
- )
1290
- this.editor.insertElement(video)
1291
- const leftSpace = AlexElement.getSpaceElement()
1292
- const rightSpace = AlexElement.getSpaceElement()
1293
- this.editor.addElementAfter(rightSpace, video)
1294
- this.editor.addElementBefore(leftSpace, video)
1295
- this.editor.range.anchor.moveToEnd(rightSpace)
1296
- this.editor.range.focus.moveToEnd(rightSpace)
1297
- if (isRender) {
1298
- this.editor.formatElementStack()
1299
- this.editor.domRender()
1300
- this.editor.rangeRender()
1301
- }
1302
- },
1303
- //api:选区是否含有代码块样式
1304
- hasPreStyle() {
1305
- if (!this.editor.range) {
1306
- return false
1307
- }
1308
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1309
- return this.editor.range.anchor.element.isPreStyle()
1310
- }
1311
- const result = this.editor.getElementsByRange(true, false)
1312
- return result.some(item => {
1313
- return item.element.isPreStyle()
1314
- })
1315
- },
1316
- //api:选区是否含有引用
1317
- hasQuote() {
1318
- if (!this.editor.range) {
1319
- return false
1320
- }
1321
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1322
- const block = this.editor.range.anchor.element.getBlock()
1323
- return block.parsedom == 'blockquote'
1324
- }
1325
- const result = this.editor.getElementsByRange(true, false)
1326
- return result.some(item => {
1327
- if (item.element.isBlock()) {
1328
- return item.element.parsedom == 'blockquote'
1329
- } else {
1330
- const block = item.element.getBlock()
1331
- return block.parsedom == 'blockquote'
1332
- }
1333
- })
1334
- },
1335
- //api:选区是否含有列表
1336
- hasList(ordered = false) {
1337
- if (!this.editor.range) {
1338
- return false
1339
- }
1340
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1341
- const block = this.editor.range.anchor.element.getBlock()
1342
- return blockIsList(block, ordered)
1343
- }
1344
- const result = this.editor.getElementsByRange(true, false)
1345
- return result.some(item => {
1346
- if (item.element.isBlock()) {
1347
- return blockIsList(item.element, ordered)
1348
- } else {
1349
- const block = item.element.getBlock()
1350
- return blockIsList(block, ordered)
1351
- }
1352
- })
1353
- },
1354
- //api:选区是否含有链接
1355
- hasLink() {
1356
- if (!this.editor.range) {
1357
- return false
1358
- }
1359
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1360
- return !!this.getParsedomElementByElement(this.editor.range.anchor.element, 'a')
1361
- }
1362
- const result = this.editor.getElementsByRange(true, true).filter(item => {
1363
- return item.element.isText()
1364
- })
1365
- return result.some(item => {
1366
- return !!this.getParsedomElementByElement(item.element, 'a')
1367
- })
1368
- },
1369
- //api:选区是否含有表格
1370
- hasTable() {
1371
- if (!this.editor.range) {
1372
- return false
1373
- }
1374
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1375
- const block = this.editor.range.anchor.element.getBlock()
1376
- return block.parsedom == 'table'
1377
- }
1378
- const result = this.editor.getElementsByRange(true, false)
1379
- return result.some(item => {
1380
- if (item.element.isBlock()) {
1381
- return item.element.parsedom == 'table'
1382
- } else {
1383
- const block = item.element.getBlock()
1384
- return block.parsedom == 'table'
1385
- }
1386
- })
1387
- },
1388
- //api:选区是否含有任务列表
1389
- hasTask() {
1390
- if (!this.editor.range) {
1391
- return false
1392
- }
1393
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1394
- const block = this.editor.range.anchor.element.getBlock()
1395
- return blockIsTask(block)
1396
- }
1397
- const result = this.editor.getElementsByRange(true, false)
1398
- return result.some(item => {
1399
- if (item.element.isBlock()) {
1400
- return blockIsTask(item.element)
1401
- } else {
1402
- const block = item.element.getBlock()
1403
- return blockIsTask(block)
1404
- }
1405
- })
1406
- },
1407
- //选区是否含有图片
1408
- hasImage() {
1409
- if (!this.editor.range) {
1410
- return false
1411
- }
1412
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1413
- return this.editor.range.anchor.element.isClosed() && this.editor.range.anchor.element.parsedom == 'img'
1414
- }
1415
- const result = this.editor.getElementsByRange(true, true)
1416
- return result.some(item => {
1417
- return item.element.isClosed() && item.element.parsedom == 'img'
1418
- })
1419
- },
1420
- //选区是否含有视频
1421
- hasVideo() {
1422
- if (!this.editor.range) {
1423
- return false
1424
- }
1425
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1426
- return this.editor.range.anchor.element.isClosed() && this.editor.range.anchor.element.parsedom == 'video'
1427
- }
1428
- const result = this.editor.getElementsByRange(true, true)
1429
- return result.some(item => {
1430
- return item.element.isClosed() && item.element.parsedom == 'video'
1431
- })
1432
- },
1433
- //api:选区是否全部在引用内
1434
- inQuote() {
1435
- if (!this.editor.range) {
1436
- return false
1437
- }
1438
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1439
- const block = this.editor.range.anchor.element.getBlock()
1440
- return block.parsedom == 'blockquote'
1441
- }
1442
- const result = this.editor.getElementsByRange(true, false)
1443
- return result.every(item => {
1444
- if (item.element.isBlock()) {
1445
- return item.element.parsedom == 'blockquote'
1446
- } else {
1447
- const block = item.element.getBlock()
1448
- return block.parsedom == 'blockquote'
1449
- }
1450
- })
1451
- },
1452
- //api:选区是否全部在列表内
1453
- inList(ordered = false) {
1454
- if (!this.editor.range) {
1455
- return false
1456
- }
1457
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1458
- const block = this.editor.range.anchor.element.getBlock()
1459
- return blockIsList(block, ordered)
1460
- }
1461
- const result = this.editor.getElementsByRange(true, false)
1462
- return result.every(item => {
1463
- if (item.element.isBlock()) {
1464
- return blockIsList(item.element, ordered)
1465
- } else {
1466
- const block = item.element.getBlock()
1467
- return blockIsList(block, ordered)
1468
- }
1469
- })
1470
- },
1471
- //api:选区是否全部在任务列表里
1472
- inTask() {
1473
- if (!this.editor.range) {
1474
- return false
1475
- }
1476
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1477
- const block = this.editor.range.anchor.element.getBlock()
1478
- return blockIsTask(block)
1479
- }
1480
- const result = this.editor.getElementsByRange(true, false)
1481
- return result.every(item => {
1482
- if (item.element.isBlock()) {
1483
- return blockIsTask(item.element)
1484
- } else {
1485
- const block = item.element.getBlock()
1486
- return blockIsTask(block)
1487
- }
1488
- })
1489
- },
1490
- //api:创建一个空的表格
1491
- insertTable(rowLength, colLength) {
1492
- if (this.disabled) {
1493
- return
1494
- }
1495
- if (!this.editor.range) {
1496
- return
1497
- }
1498
- const table = new AlexElement('block', 'table', null, null, null)
1499
- const tbody = new AlexElement('inblock', 'tbody', null, null, null)
1500
- this.editor.addElementTo(tbody, table)
1501
- for (let i = 0; i < rowLength; i++) {
1502
- const row = new AlexElement('inblock', 'tr', null, null, null)
1503
- for (let j = 0; j < colLength; j++) {
1504
- const column = new AlexElement('inblock', 'td', null, null, null)
1505
- const breakEl = new AlexElement('closed', 'br', null, null, null)
1506
- this.editor.addElementTo(breakEl, column)
1507
- this.editor.addElementTo(column, row)
1508
- }
1509
- this.editor.addElementTo(row, tbody)
1510
- }
1511
- this.editor.insertElement(table)
1512
- //在表格后创建一个段落
1513
- const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1514
- const breakEl = new AlexElement('closed', 'br', null, null, null)
1515
- this.editor.addElementTo(breakEl, paragraph)
1516
- this.editor.addElementAfter(paragraph, table)
1517
- this.editor.formatElementStack()
1518
- this.editor.range.anchor.moveToStart(tbody)
1519
- this.editor.range.focus.moveToStart(tbody)
1520
- this.editor.domRender()
1521
- this.editor.rangeRender()
1522
- },
1523
- //api:插入代码块
1524
- insertCodeBlock() {
1525
- if (this.disabled) {
1526
- return
1527
- }
1528
- if (!this.editor.range) {
1529
- return
1530
- }
1531
- const pre = this.getCurrentParsedomElement('pre')
1532
- if (pre) {
1533
- let content = ''
1534
- AlexElement.flatElements(pre.children)
1535
- .filter(item => {
1536
- return item.isText()
1537
- })
1538
- .forEach(item => {
1539
- content += item.textContent
1540
- })
1541
- const splits = content.split('\n')
1542
- splits.forEach(item => {
1543
- const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1544
- const text = new AlexElement('text', null, null, null, item)
1545
- this.editor.addElementTo(text, paragraph)
1546
- this.editor.addElementBefore(paragraph, pre)
1547
- })
1548
- pre.toEmpty()
1549
- } else {
1550
- //起点和终点在一起
1551
- if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1552
- const block = this.editor.range.anchor.element.getBlock()
1553
- blockToParagraph(block)
1554
- block.parsedom = 'pre'
1555
- const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1556
- const breakEl = new AlexElement('closed', 'br', null, null, null)
1557
- this.editor.addElementTo(breakEl, paragraph)
1558
- this.editor.addElementAfter(paragraph, block)
1559
- }
1560
- //起点和终点不在一起
1561
- else {
1562
- let result = this.editor.getElementsByRange(true, false)
1563
- this.editor.range.anchor.moveToStart(result[0].element.getBlock())
1564
- this.editor.range.focus.moveToEnd(result[result.length - 1].element.getBlock())
1565
- const res = this.editor.getElementsByRange(true, true).filter(el => el.element.isText())
1566
- const obj = {}
1567
- res.forEach(el => {
1568
- if (obj[el.element.getBlock().key]) {
1569
- obj[el.element.getBlock().key].push(el.element.clone())
1570
- } else {
1571
- obj[el.element.getBlock().key] = [el.element.clone()]
1572
- }
1573
- })
1574
- const pre = new AlexElement('block', 'pre', null, null, null)
1575
- Object.keys(obj).forEach((key, index) => {
1576
- if (index > 0) {
1577
- const text = new AlexElement('text', null, null, null, '\n')
1578
- if (pre.hasChildren()) {
1579
- this.editor.addElementTo(text, pre, pre.children.length)
1580
- } else {
1581
- this.editor.addElementTo(text, pre)
1582
- }
1583
- }
1584
- obj[key].forEach(el => {
1585
- if (pre.hasChildren()) {
1586
- this.editor.addElementTo(el, pre, pre.children.length)
1587
- } else {
1588
- this.editor.addElementTo(el, pre)
1589
- }
1590
- })
1591
- })
1592
- this.editor.delete()
1593
- this.editor.insertElement(pre)
1594
- const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1595
- const breakEl = new AlexElement('closed', 'br', null, null, null)
1596
- this.editor.addElementTo(breakEl, paragraph)
1597
- this.editor.addElementAfter(paragraph, pre)
1598
- }
1599
- }
1600
- this.editor.formatElementStack()
1601
- this.editor.domRender()
1602
- this.editor.rangeRender()
1603
- },
1604
- //api:插入文本
1605
- insertText(text) {
1606
- if (this.disabled) {
1607
- return
1608
- }
1609
- if (!this.editor.range) {
1610
- return
1611
- }
1612
- this.editor.insertText(text)
1613
- this.editor.formatElementStack()
1614
- this.editor.domRender()
1615
- this.editor.rangeRender()
1616
- },
1617
- //api:插入html
1618
- insertHtml(html) {
1619
- if (this.disabled) {
1620
- return
1621
- }
1622
- if (!this.editor.range) {
1623
- return
1624
- }
1625
- const elements = this.editor.parseHtml(html)
1626
- for (let i = 0; i < elements.length; i++) {
1627
- this.editor.insertElement(elements[i], false)
1628
- }
1629
- this.editor.formatElementStack()
1630
- this.editor.domRender()
1631
- this.editor.rangeRender()
1632
- }
1633
- },
1634
- beforeUnmount() {
1635
- //卸载绑定在滚动元素上的事件
1636
- this.removeScrollHandle()
1637
- //卸载绑定在document.documentElement上的事件
1638
- Dap.event.off(document.documentElement, `mousedown.editify_${this.uid} mousemove.editify_${this.uid} mouseup.editify_${this.uid} click.editify_${this.uid}`)
1639
- //卸载绑定在window上的事件
1640
- Dap.event.off(window, `resize.editify_${this.uid}`)
1641
- //销毁编辑器
1642
- this.editor.destroy()
1643
- }
1644
- }
1645
- </script>
1646
- <style lang="less" scoped>
1647
- .editify {
1648
- display: block;
1649
- width: 100%;
1650
- position: relative;
1651
- box-sizing: border-box;
1652
- -webkit-tap-highlight-color: transparent;
1653
- outline: none;
1654
- font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, Roboto, 'Segoe UI', 'Microsoft YaHei', Arial, sans-serif;
1655
- line-height: 1.5;
1656
-
1657
- *,
1658
- *::before,
1659
- *::after {
1660
- box-sizing: border-box;
1661
- -webkit-tap-highlight-color: transparent;
1662
- outline: none;
1663
- }
1664
- }
1665
-
1666
- .editify-body {
1667
- display: block;
1668
- width: 100%;
1669
- position: relative;
1670
- background-color: @background;
1671
- padding: 1px;
1672
-
1673
- &.border {
1674
- border: 1px solid @border-color;
1675
- border-radius: 4px;
1676
- transition: all 500ms;
1677
-
1678
- &.menu_inner {
1679
- border-radius: 0 0 4px 4px;
1680
- border-top: none;
1681
- }
1682
- }
1683
-
1684
- //编辑器样式
1685
- .editify-content {
1686
- display: block;
1687
- position: relative;
1688
- overflow-x: hidden;
1689
- overflow-y: auto;
1690
- width: 100%;
1691
- border-radius: inherit;
1692
- padding: 6px 10px;
1693
- line-height: 1.5;
1694
- color: @font-color-dark;
1695
- font-size: @font-size;
1696
- position: relative;
1697
- line-height: 1.5;
1698
-
1699
- //显示占位符
1700
- &.placeholder::before {
1701
- position: absolute;
1702
- top: 0;
1703
- left: 0;
1704
- display: block;
1705
- width: 100%;
1706
- content: attr(data-editify-placeholder);
1707
- font-size: inherit;
1708
- font-family: inherit;
1709
- color: @font-color-disabled;
1710
- line-height: inherit;
1711
- padding: 6px 10px;
1712
- cursor: text;
1713
- }
1714
-
1715
- //段落样式和标题
1716
- :deep(p),
1717
- :deep(h1),
1718
- :deep(h2),
1719
- :deep(h3),
1720
- :deep(h4),
1721
- :deep(h5),
1722
- :deep(h6) {
1723
- display: block;
1724
- width: 100%;
1725
- margin: 0 0 15px 0;
1726
- padding: 0;
1727
- }
1728
- :deep(h1) {
1729
- font-size: 32px;
1730
- }
1731
- :deep(h2) {
1732
- font-size: 28px;
1733
- }
1734
- :deep(h3) {
1735
- font-size: 24px;
1736
- }
1737
- :deep(h4) {
1738
- font-size: 20px;
1739
- }
1740
- :deep(h5) {
1741
- font-size: 18px;
1742
- }
1743
- :deep(h6) {
1744
- font-size: 16px;
1745
- }
1746
- //有序列表样式
1747
- :deep(div[data-editify-list='ol']) {
1748
- margin-bottom: 15px;
1749
-
1750
- &::before {
1751
- content: attr(data-editify-value) '.';
1752
- margin-right: 10px;
1753
- }
1754
- }
1755
- //无序列表样式
1756
- :deep(div[data-editify-list='ul']) {
1757
- margin-bottom: 15px;
1758
-
1759
- &::before {
1760
- content: '\2022';
1761
- margin-right: 10px;
1762
- }
1763
- }
1764
- //代码样式
1765
- :deep(span[data-editify-code]) {
1766
- display: inline-block;
1767
- padding: 3px 6px;
1768
- margin: 0 4px;
1769
- border-radius: 4px;
1770
- line-height: 1;
1771
- font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
1772
- background-color: @pre-background;
1773
- color: @font-color;
1774
- border: 1px solid @border-color;
1775
- text-indent: initial;
1776
- font-size: @font-size;
1777
- font-weight: normal;
1778
- }
1779
- //链接样式
1780
- :deep(a) {
1781
- color: @font-color-link;
1782
- transition: all 200ms;
1783
- text-decoration: none;
1784
- cursor: text;
1785
-
1786
- &:hover {
1787
- color: @font-color-link-dark;
1788
- text-decoration: underline;
1789
- }
1790
- }
1791
- //表格样式
1792
- :deep(table) {
1793
- width: 100%;
1794
- border: 1px solid @border-color;
1795
- margin: 0;
1796
- padding: 0;
1797
- border-collapse: collapse;
1798
- margin-bottom: 15px;
1799
- background-color: @background;
1800
- color: @font-color-dark;
1801
- font-size: @font-size;
1802
-
1803
- * {
1804
- margin: 0 !important;
1805
- }
1806
-
1807
- tbody {
1808
- margin: 0;
1809
- padding: 0;
1810
-
1811
- tr {
1812
- margin: 0;
1813
- padding: 0;
1814
-
1815
- &:first-child {
1816
- background-color: @background-darker;
1817
-
1818
- td {
1819
- font-weight: bold;
1820
- position: relative;
1821
- }
1822
- }
1823
-
1824
- td {
1825
- margin: 0;
1826
- border: 1px solid @border-color;
1827
- padding: 6px 10px;
1828
- position: relative;
1829
- word-break: break-word;
1830
-
1831
- &:not(:last-child)::after {
1832
- position: absolute;
1833
- right: -5px;
1834
- top: 0;
1835
- width: 10px;
1836
- height: 100%;
1837
- content: '';
1838
- z-index: 1;
1839
- cursor: col-resize;
1840
- user-select: none;
1841
- }
1842
- }
1843
- }
1844
- }
1845
- }
1846
- //代码块样式
1847
- :deep(pre) {
1848
- display: block;
1849
- padding: 6px 10px;
1850
- margin: 0 0 15px;
1851
- font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
1852
- line-height: 1.5;
1853
- font-size: @font-size;
1854
- color: @font-color-dark;
1855
- background-color: @pre-background;
1856
- border: 1px solid @border-color;
1857
- border-radius: 4px;
1858
- overflow: auto;
1859
- position: relative;
1860
- }
1861
- //图片样式
1862
- :deep(img) {
1863
- position: relative;
1864
- display: inline-block;
1865
- width: 30%;
1866
- height: auto;
1867
- border-radius: 2px;
1868
- vertical-align: text-bottom;
1869
- margin: 0 2px;
1870
- }
1871
- //视频样式
1872
- :deep(video) {
1873
- position: relative;
1874
- display: inline-block;
1875
- width: 30%;
1876
- border-radius: 2px;
1877
- vertical-align: text-bottom;
1878
- background-color: #000;
1879
- object-fit: contain;
1880
- margin: 0 2px;
1881
- }
1882
- //引用样式
1883
- :deep(blockquote) {
1884
- display: block;
1885
- border-left: 8px solid @background-darker;
1886
- padding: 6px 10px 6px 20px;
1887
- margin: 0 0 15px;
1888
- line-height: 1.5;
1889
- font-size: @font-size;
1890
- color: @font-color-light;
1891
- border-radius: 0;
1892
- }
1893
- //任务列表样式
1894
- :deep(div[data-editify-task]) {
1895
- margin-bottom: 15px;
1896
- position: relative;
1897
- padding-left: 26px;
1898
- font-size: @font-size;
1899
- color: @font-color-dark;
1900
- transition: all 200ms;
1901
-
1902
- &::before {
1903
- display: block;
1904
- width: 16px;
1905
- height: 16px;
1906
- border-radius: 2px;
1907
- border: 2px solid @font-color-light;
1908
- transition: all 200ms;
1909
- box-sizing: border-box;
1910
- user-select: none;
1911
- content: '';
1912
- position: absolute;
1913
- left: 0;
1914
- top: 50%;
1915
- transform: translateY(-50%);
1916
- z-index: 1;
1917
- cursor: pointer;
1918
- }
1919
-
1920
- &::after {
1921
- position: absolute;
1922
- content: '';
1923
- left: 3px;
1924
- top: 6px;
1925
- display: inline-block;
1926
- width: 10px;
1927
- height: 6px;
1928
- border: 2px solid @font-color-light;
1929
- border-top: none;
1930
- border-right: none;
1931
- transform: rotate(-45deg);
1932
- transform-origin: center;
1933
- margin-bottom: 2px;
1934
- box-sizing: border-box;
1935
- z-index: 2;
1936
- cursor: pointer;
1937
- opacity: 0;
1938
- transition: all 200ms;
1939
- }
1940
-
1941
- &[data-editify-task='checked'] {
1942
- text-decoration: line-through;
1943
- color: @font-color-light;
1944
- &::after {
1945
- opacity: 1;
1946
- }
1947
- }
1948
- }
1949
-
1950
- //禁用样式
1951
- &.disabled {
1952
- cursor: auto !important;
1953
- &.placeholder::before {
1954
- cursor: auto;
1955
- }
1956
- :deep(a) {
1957
- cursor: pointer;
1958
- }
1959
-
1960
- :deep(table) {
1961
- td:not(:last-child)::after {
1962
- cursor: auto;
1963
- }
1964
- }
1965
- }
1966
- }
1967
-
1968
- //代码视图
1969
- .editify-source {
1970
- display: block;
1971
- width: 100%;
1972
- height: 100%;
1973
- position: absolute;
1974
- left: 0;
1975
- top: 0;
1976
- z-index: 10;
1977
- background-color: @reverse-background;
1978
- border-radius: inherit;
1979
- margin: 0;
1980
- padding: 6px 10px;
1981
- overflow-x: hidden;
1982
- overflow-y: auto;
1983
- font-size: @font-size;
1984
- color: @reverse-color;
1985
- font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
1986
- resize: none;
1987
- border: none;
1988
- }
1989
- }
1990
-
1991
- .editify-footer {
1992
- display: flex;
1993
- justify-content: end;
1994
- align-items: center;
1995
- width: 100%;
1996
- padding: 10px;
1997
-
1998
- .editify-footer-words {
1999
- font-size: @font-size;
2000
- color: @font-color-light;
2001
- line-height: 1;
2002
- }
2003
- }
2004
- </style>
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.range = null
123
+ this.editor.formatElementStack()
124
+ this.editor.domRender()
125
+ this.editor.rangeRender()
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
+ if (!this.editor.range) {
434
+ this.editor.initRange()
435
+ }
436
+ this.editor.range.anchor.moveToEnd(element)
437
+ this.editor.range.focus.moveToEnd(element)
438
+ this.editor.formatElementStack()
439
+ this.editor.domRender()
440
+ this.editor.rangeRender()
441
+ }
442
+ }
443
+ }
444
+ }
445
+ },
446
+ //编辑器的值更新
447
+ handleEditorChange(newVal, oldVal) {
448
+ if (this.disabled) {
449
+ return
450
+ }
451
+ //内部修改
452
+ this.internalModify(newVal)
453
+ //触发change事件
454
+ this.$emit('change', newVal, oldVal)
455
+ },
456
+ //编辑器失去焦点
457
+ handleEditorBlur(val) {
458
+ if (this.disabled) {
459
+ return
460
+ }
461
+ if (this.border && this.color) {
462
+ //恢复编辑区域边框颜色
463
+ this.$refs.body.style.borderColor = ''
464
+ //恢复编辑区域阴影颜色
465
+ this.$refs.body.style.boxShadow = ''
466
+ //使用菜单栏的情况下恢复菜单栏的样式
467
+ if (this.menuConfig.use) {
468
+ this.$refs.menu.$el.style.borderColor = ''
469
+ this.$refs.menu.$el.style.boxShadow = ''
470
+ }
471
+ }
472
+ this.$emit('blur', val)
473
+ },
474
+ //编辑器获取焦点
475
+ handleEditorFocus(val) {
476
+ if (this.disabled) {
477
+ return
478
+ }
479
+ if (this.border && this.color) {
480
+ //编辑区域边框颜色
481
+ this.$refs.body.style.borderColor = this.color
482
+ //转换颜色值
483
+ const rgb = Dap.color.hex2rgb(this.color)
484
+ //菜单栏模式为inner
485
+ if (this.menuConfig.use && this.menuConfig.mode == 'inner') {
486
+ //编辑区域除顶部边框的阴影
487
+ 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)`
488
+ //菜单栏的边框颜色
489
+ this.$refs.menu.$el.style.borderColor = this.color
490
+ //菜单栏除底部边框的阴影
491
+ 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)`
492
+ }
493
+ //其他菜单栏模式
494
+ else if (this.menuConfig.use) {
495
+ //编辑区域四边阴影
496
+ this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
497
+ }
498
+ //不使用菜单栏
499
+ else {
500
+ //编辑区域四边阴影
501
+ this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
502
+ }
503
+ }
504
+ //获取焦点时可以使用菜单栏
505
+ setTimeout(() => {
506
+ this.canUseMenu = true
507
+ }, 0)
508
+ this.$emit('focus', val)
509
+ },
510
+ //编辑区域键盘按下
511
+ handleEditorKeydown(e) {
512
+ if (this.disabled) {
513
+ return
514
+ }
515
+ //增加缩进
516
+ if (e.keyCode == 9 && !e.metaKey && !e.shiftKey && !e.ctrlKey && !e.altKey) {
517
+ e.preventDefault()
518
+ this.setIndentIncrease()
519
+ }
520
+ //减少缩进
521
+ else if (e.keyCode == 9 && !e.metaKey && e.shiftKey && !e.ctrlKey && !e.altKey) {
522
+ e.preventDefault()
523
+ this.setIndentDecrease()
524
+ }
525
+ //自定义键盘按下操作
526
+ this.$emit('keydown', e)
527
+ },
528
+ //点击编辑器
529
+ handleEditorClick(e) {
530
+ if (this.disabled || this.isSourceView) {
531
+ return
532
+ }
533
+ const node = e.target
534
+ //点击的是图片或者视频
535
+ if (node.nodeName.toLocaleLowerCase() == 'img' || node.nodeName.toLocaleLowerCase() == 'video') {
536
+ const key = node.getAttribute('data-editify-element')
537
+ if (key) {
538
+ const element = this.editor.getElementByKey(key)
539
+ if (!this.editor.range) {
540
+ this.editor.initRange()
541
+ }
542
+ this.editor.range.anchor.moveToStart(element)
543
+ this.editor.range.focus.moveToEnd(element)
544
+ this.editor.rangeRender()
545
+ }
546
+ }
547
+ },
548
+ //编辑器换行
549
+ handleInsertParagraph(element, previousElement) {
550
+ //前一个块元素如果是只包含换行符的元素,并且当前块元素也是包含换行符的元素,则当前块元素转为段落
551
+ if (previousElement.isOnlyHasBreak() && element.isOnlyHasBreak()) {
552
+ if (!previousElement.isBlock()) {
553
+ previousElement.convertToBlock()
554
+ }
555
+ if (previousElement.parsedom != AlexElement.BLOCK_NODE) {
556
+ blockToParagraph(previousElement)
557
+ this.editor.range.anchor.moveToStart(previousElement)
558
+ this.editor.range.focus.moveToStart(previousElement)
559
+ element.toEmpty()
560
+ }
561
+ }
562
+ this.$emit('insertparagraph', this.value)
563
+ },
564
+ //编辑器焦点更新
565
+ handleRangeUpdate(range) {
566
+ if (this.disabled) {
567
+ return
568
+ }
569
+ if (this.toolbarConfig.use) {
570
+ this.handleToolbar()
571
+ }
572
+ if (this.menuConfig.use) {
573
+ this.$refs.menu.handleRangeUpdate()
574
+ }
575
+ this.$emit('rangeupdate', this.value, range)
576
+ },
577
+ //编辑器复制
578
+ handleCopy(text, html) {
579
+ this.$emit('copy', text, html)
580
+ },
581
+ //编辑器剪切
582
+ handleCut(text, html) {
583
+ this.$emit('cut', text, html)
584
+ },
585
+ //编辑器粘贴纯文本
586
+ handlePasteText(data) {
587
+ this.$emit('paste-text', data)
588
+ },
589
+ //编辑器粘贴html
590
+ handlePasteHtml(elements, data) {
591
+ //粘贴html时过滤元素的样式和属性
592
+ AlexElement.flatElements(elements).forEach(el => {
593
+ let marks = {}
594
+ let styles = {}
595
+ if (el.hasMarks()) {
596
+ for (let key in pasteKeepData.marks) {
597
+ if (el.marks.hasOwnProperty(key) && ((Array.isArray(pasteKeepData.marks[key]) && pasteKeepData.marks[key].includes(el.parsedom)) || pasteKeepData.marks[key] == '*')) {
598
+ marks[key] = el.marks[key]
599
+ }
600
+ }
601
+ el.marks = marks
602
+ }
603
+ if (el.hasStyles() && !el.isText()) {
604
+ for (let key in pasteKeepData.styles) {
605
+ if (el.styles.hasOwnProperty(key) && ((Array.isArray(pasteKeepData.styles[key]) && pasteKeepData.styles[key].includes(el.parsedom)) || pasteKeepData.styles[key] == '*')) {
606
+ styles[key] = el.styles[key]
607
+ }
608
+ }
609
+ el.styles = styles
610
+ }
611
+ })
612
+ this.$emit('paste-html', elements)
613
+ },
614
+ //编辑器粘贴图片
615
+ handlePasteImage(url) {
616
+ this.$emit('paste-image', url)
617
+ },
618
+ //编辑器粘贴视频
619
+ handlePasteVideo(url) {
620
+ this.$emit('paste-video', url)
621
+ },
622
+ //编辑器部分删除情景
623
+ handleDeleteInStart(element) {
624
+ if (element.isBlock()) {
625
+ blockToParagraph(element)
626
+ }
627
+ },
628
+ //编辑器删除完成后事件
629
+ handleDeleteComplete() {
630
+ const uneditable = this.editor.range.anchor.element.getUneditableElement()
631
+ if (uneditable) {
632
+ uneditable.toEmpty()
633
+ }
634
+ },
635
+ //编辑器dom渲染之前
636
+ handleBeforeRender() {
637
+ this.$emit('before-render')
638
+ },
639
+ //编辑器dom渲染
640
+ handleAfterRender() {
641
+ //设定视频高度
642
+ this.setVideoHeight()
643
+ //触发事件
644
+ this.$emit('after-render')
645
+ },
646
+ //设定视频高度
647
+ setVideoHeight() {
648
+ this.$refs.content.querySelectorAll('video').forEach(video => {
649
+ video.style.height = video.offsetWidth / this.videoRatio + 'px'
650
+ })
651
+ },
652
+
653
+ //api:光标设置到文档底部
654
+ collapseToEnd() {
655
+ if (this.disabled) {
656
+ return
657
+ }
658
+ this.editor.collapseToEnd()
659
+ this.editor.rangeRender()
660
+ Dap.element.setScrollTop({
661
+ el: this.$refs.content,
662
+ number: 1000000,
663
+ time: 0
664
+ })
665
+ },
666
+ //api:光标设置到文档头部
667
+ collapseToStart() {
668
+ if (this.disabled) {
669
+ return
670
+ }
671
+ this.editor.collapseToStart()
672
+ this.editor.rangeRender()
673
+ this.$nextTick(() => {
674
+ Dap.element.setScrollTop({
675
+ el: this.$refs.content,
676
+ number: 0,
677
+ time: 0
678
+ })
679
+ })
680
+ },
681
+ //api:获取某个元素是否在某个标签元素下,如果是返回该标签元素,否则返回null
682
+ getParsedomElementByElement(element, parsedom) {
683
+ if (element.isBlock()) {
684
+ return element.parsedom == parsedom ? element : null
685
+ }
686
+ if (!element.isText() && element.parsedom == parsedom) {
687
+ return element
688
+ }
689
+ return this.getParsedomElementByElement(element.parent, parsedom)
690
+ },
691
+ //api:获取光标是否在指定标签元素下,如果是返回该标签元素,否则返回null
692
+ getCurrentParsedomElement(parsedom) {
693
+ if (this.disabled) {
694
+ return null
695
+ }
696
+ if (!this.editor.range) {
697
+ return null
698
+ }
699
+ if (this.editor.range.anchor.element.isEqual(this.editor.range.focus.element)) {
700
+ return this.getParsedomElementByElement(this.editor.range.anchor.element, parsedom)
701
+ }
702
+ const arr = this.editor.getElementsByRange(true, false).map(item => {
703
+ return this.getParsedomElementByElement(item.element, parsedom)
704
+ })
705
+ let hasNull = arr.some(el => {
706
+ return el == null
707
+ })
708
+ //如果存在null,则表示有的选区元素不在指定标签下,返回null
709
+ if (hasNull) {
710
+ return null
711
+ }
712
+ //如果只有一个元素,则返回该元素
713
+ if (arr.length == 1) {
714
+ return arr[0]
715
+ }
716
+ //默认数组中的元素都相等
717
+ let flag = true
718
+ for (let i = 1; i < arr.length; i++) {
719
+ if (!arr[i].isEqual(arr[0])) {
720
+ flag = false
721
+ break
722
+ }
723
+ }
724
+ //如果相等,则返回该元素
725
+ if (flag) {
726
+ return arr[0]
727
+ }
728
+ return null
729
+ },
730
+ //api:删除光标所在的指定标签元素
731
+ deleteByParsedom(parsedom) {
732
+ if (this.disabled) {
733
+ return
734
+ }
735
+ if (!this.editor.range) {
736
+ return
737
+ }
738
+ const element = this.getCurrentParsedomElement(parsedom)
739
+ if (element) {
740
+ element.toEmpty()
741
+ this.editor.formatElementStack()
742
+ this.editor.domRender()
743
+ this.editor.rangeRender()
744
+ }
745
+ },
746
+ //api:当光标在链接上时可以移除链接
747
+ removeLink() {
748
+ if (this.disabled) {
749
+ return
750
+ }
751
+ if (!this.editor.range) {
752
+ return
753
+ }
754
+ const link = this.getCurrentParsedomElement('a')
755
+ if (link) {
756
+ link.parsedom = AlexElement.TEXT_NODE
757
+ delete link.marks.target
758
+ delete link.marks.href
759
+ this.editor.formatElementStack()
760
+ this.editor.domRender()
761
+ this.editor.rangeRender()
762
+ }
763
+ },
764
+ //api:设置标题
765
+ setHeading(parsedom) {
766
+ if (this.disabled) {
767
+ return
768
+ }
769
+ if (!this.editor.range) {
770
+ return
771
+ }
772
+ const values = getButtonOptionsConfig(this.$editTrans, this.$editLocale).heading.map(item => {
773
+ return item.value
774
+ })
775
+ if (!values.includes(parsedom)) {
776
+ throw new Error('The parameter supports only h1-h6 and p')
777
+ }
778
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
779
+ const block = this.editor.range.anchor.element.getBlock()
780
+ //先转为段落
781
+ blockToParagraph(block)
782
+ //设置标题
783
+ block.parsedom = parsedom
784
+ } else {
785
+ const result = this.editor.getElementsByRange(true, false)
786
+ result.forEach(el => {
787
+ if (el.element.isBlock()) {
788
+ blockToParagraph(el.element)
789
+ el.element.parsedom = parsedom
790
+ } else {
791
+ const block = el.element.getBlock()
792
+ blockToParagraph(block)
793
+ block.parsedom = parsedom
794
+ }
795
+ })
796
+ }
797
+ this.editor.formatElementStack()
798
+ this.editor.domRender()
799
+ this.editor.rangeRender()
800
+ },
801
+ //api:插入有序列表 ordered为true表示有序列表
802
+ setList(ordered) {
803
+ if (this.disabled) {
804
+ return
805
+ }
806
+ if (!this.editor.range) {
807
+ return
808
+ }
809
+ //起点和终点在一起
810
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
811
+ const block = this.editor.range.anchor.element.getBlock()
812
+ const isList = blockIsList(block, ordered)
813
+ if (isList) {
814
+ blockToParagraph(block)
815
+ } else {
816
+ blockToList(block, ordered)
817
+ }
818
+ }
819
+ //起点和终点不在一起
820
+ else {
821
+ let blocks = []
822
+ const result = this.editor.getElementsByRange(true, false)
823
+ result.forEach(item => {
824
+ const block = item.element.getBlock()
825
+ const exist = blocks.some(el => block.isEqual(el))
826
+ if (!exist) {
827
+ blocks.push(block)
828
+ }
829
+ })
830
+ blocks.forEach(block => {
831
+ const isList = blockIsList(block, ordered)
832
+ if (isList) {
833
+ blockToParagraph(block)
834
+ } else {
835
+ blockToList(block, ordered)
836
+ }
837
+ })
838
+ }
839
+ this.editor.formatElementStack()
840
+ this.editor.domRender()
841
+ this.editor.rangeRender()
842
+ },
843
+ //api:插入任务列表
844
+ setTask() {
845
+ if (this.disabled) {
846
+ return
847
+ }
848
+ if (!this.editor.range) {
849
+ return
850
+ }
851
+ //起点和终点在一起
852
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
853
+ const block = this.editor.range.anchor.element.getBlock()
854
+ const isTask = blockIsTask(block)
855
+ if (isTask) {
856
+ blockToParagraph(block)
857
+ } else {
858
+ blockToTask(block)
859
+ }
860
+ }
861
+ //起点和终点不在一起
862
+ else {
863
+ let blocks = []
864
+ const result = this.editor.getElementsByRange(true, false)
865
+ result.forEach(item => {
866
+ const block = item.element.getBlock()
867
+ const exist = blocks.some(el => block.isEqual(el))
868
+ if (!exist) {
869
+ blocks.push(block)
870
+ }
871
+ })
872
+ blocks.forEach(block => {
873
+ const isTask = blockIsTask(block)
874
+ if (isTask) {
875
+ blockToParagraph(block)
876
+ } else {
877
+ blockToTask(block)
878
+ }
879
+ })
880
+ }
881
+ this.editor.formatElementStack()
882
+ this.editor.domRender()
883
+ this.editor.rangeRender()
884
+ },
885
+ //api:设置样式
886
+ setTextStyle(name, value) {
887
+ if (this.disabled) {
888
+ return
889
+ }
890
+ if (!this.editor.range) {
891
+ return
892
+ }
893
+ const active = this.queryTextStyle(name, value)
894
+ if (active) {
895
+ this.editor.removeTextStyle([name])
896
+ } else {
897
+ let styles = {}
898
+ styles[name] = value
899
+ this.editor.setTextStyle(styles)
900
+ }
901
+ this.editor.formatElementStack()
902
+ this.editor.domRender()
903
+ this.editor.rangeRender()
904
+ },
905
+ //api:查询是否具有某个样式
906
+ queryTextStyle(name, value, useCache) {
907
+ return this.editor.queryTextStyle(name, value, useCache)
908
+ },
909
+ //api:设置标记
910
+ setTextMark(name, value) {
911
+ if (this.disabled) {
912
+ return
913
+ }
914
+ if (!this.editor.range) {
915
+ return
916
+ }
917
+ const active = this.queryTextMark(name, value)
918
+ if (active) {
919
+ this.editor.removeTextMark([name])
920
+ } else {
921
+ let marks = {}
922
+ marks[name] = value
923
+ this.editor.setTextMark(marks)
924
+ }
925
+ this.editor.formatElementStack()
926
+ this.editor.domRender()
927
+ this.editor.rangeRender()
928
+ },
929
+ //api:查询是否具有某个标记
930
+ queryTextMark(name, value, useCache) {
931
+ return this.editor.queryTextMark(name, value, useCache)
932
+ },
933
+ //api:清除文本样式和标记
934
+ formatText() {
935
+ if (this.disabled) {
936
+ return
937
+ }
938
+ if (!this.editor.range) {
939
+ return
940
+ }
941
+ this.editor.removeTextStyle()
942
+ this.editor.removeTextMark()
943
+ this.editor.formatElementStack()
944
+ this.editor.domRender()
945
+ this.editor.rangeRender()
946
+ },
947
+ //api:设置对齐方式,参数取值justify/left/right/center
948
+ setAlign(value) {
949
+ if (this.disabled) {
950
+ return
951
+ }
952
+ if (!this.editor.range) {
953
+ return
954
+ }
955
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
956
+ const block = this.editor.range.anchor.element.getBlock()
957
+ const inblock = this.editor.range.anchor.element.getInblock()
958
+ if (inblock) {
959
+ if (inblock.hasStyles()) {
960
+ inblock.styles['text-align'] = value
961
+ } else {
962
+ inblock.styles = {
963
+ 'text-align': value
964
+ }
965
+ }
966
+ } else {
967
+ if (block.hasStyles()) {
968
+ block.styles['text-align'] = value
969
+ } else {
970
+ block.styles = {
971
+ 'text-align': value
972
+ }
973
+ }
974
+ }
975
+ } else {
976
+ const result = this.editor.getElementsByRange(true, false)
977
+ result.forEach(el => {
978
+ if (el.element.isBlock() || el.element.isInblock()) {
979
+ if (el.element.hasStyles()) {
980
+ el.element.styles['text-align'] = value
981
+ } else {
982
+ el.element.styles = {
983
+ 'text-align': value
984
+ }
985
+ }
986
+ } else {
987
+ const block = el.element.getBlock()
988
+ const inblock = el.element.getInblock()
989
+ if (inblock) {
990
+ if (inblock.hasStyles()) {
991
+ inblock.styles['text-align'] = value
992
+ } else {
993
+ inblock.styles = {
994
+ 'text-align': value
995
+ }
996
+ }
997
+ } else {
998
+ if (block.hasStyles()) {
999
+ block.styles['text-align'] = value
1000
+ } else {
1001
+ block.styles = {
1002
+ 'text-align': value
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+ })
1008
+ }
1009
+ this.editor.formatElementStack()
1010
+ this.editor.domRender()
1011
+ this.editor.rangeRender()
1012
+ },
1013
+ //api:撤销
1014
+ undo() {
1015
+ if (this.disabled) {
1016
+ return
1017
+ }
1018
+ const historyRecord = this.editor.history.get(-1)
1019
+ if (historyRecord) {
1020
+ this.editor.history.current = historyRecord.current
1021
+ this.editor.stack = historyRecord.stack
1022
+ this.editor.range = historyRecord.range
1023
+ this.editor.formatElementStack()
1024
+ this.editor.domRender(true)
1025
+ this.editor.rangeRender()
1026
+ }
1027
+ },
1028
+ //api:重做
1029
+ redo() {
1030
+ if (this.disabled) {
1031
+ return
1032
+ }
1033
+ const historyRecord = this.editor.history.get(1)
1034
+ if (historyRecord) {
1035
+ this.editor.history.current = historyRecord.current
1036
+ this.editor.stack = historyRecord.stack
1037
+ this.editor.range = historyRecord.range
1038
+ this.editor.formatElementStack()
1039
+ this.editor.domRender(true)
1040
+ this.editor.rangeRender()
1041
+ }
1042
+ },
1043
+ //api:插入引用
1044
+ setQuote() {
1045
+ if (this.disabled) {
1046
+ return
1047
+ }
1048
+ if (!this.editor.range) {
1049
+ return
1050
+ }
1051
+ //起点和终点在一起
1052
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1053
+ const block = this.editor.range.anchor.element.getBlock()
1054
+ const oldParsedom = block.parsedom
1055
+ blockToParagraph(block)
1056
+ if (oldParsedom != 'blockquote') {
1057
+ block.parsedom = 'blockquote'
1058
+ }
1059
+ }
1060
+ //起点和终点不在一起
1061
+ else {
1062
+ let blocks = []
1063
+ const result = this.editor.getElementsByRange(true, false)
1064
+ result.forEach(item => {
1065
+ const block = item.element.getBlock()
1066
+ const exist = blocks.some(el => block.isEqual(el))
1067
+ if (!exist) {
1068
+ blocks.push(block)
1069
+ }
1070
+ })
1071
+ blocks.forEach(block => {
1072
+ const oldParsedom = block.parsedom
1073
+ blockToParagraph(block)
1074
+ if (oldParsedom != 'blockquote') {
1075
+ block.parsedom = 'blockquote'
1076
+ }
1077
+ })
1078
+ }
1079
+ this.editor.formatElementStack()
1080
+ this.editor.domRender()
1081
+ this.editor.rangeRender()
1082
+ },
1083
+ //api:设置行高
1084
+ setLineHeight(value) {
1085
+ if (this.disabled) {
1086
+ return
1087
+ }
1088
+ if (!this.editor.range) {
1089
+ return
1090
+ }
1091
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1092
+ const block = this.editor.range.anchor.element.getBlock()
1093
+ const inblock = this.editor.range.anchor.element.getInblock()
1094
+ if (inblock) {
1095
+ if (inblock.hasStyles()) {
1096
+ inblock.styles['line-height'] = value
1097
+ } else {
1098
+ inblock.styles = {
1099
+ 'line-height': value
1100
+ }
1101
+ }
1102
+ } else {
1103
+ if (block.hasStyles()) {
1104
+ block.styles['line-height'] = value
1105
+ } else {
1106
+ block.styles = {
1107
+ 'line-height': value
1108
+ }
1109
+ }
1110
+ }
1111
+ } else {
1112
+ const result = this.editor.getElementsByRange(true, false)
1113
+ result.forEach(el => {
1114
+ if (el.element.isBlock() || el.element.isInblock()) {
1115
+ if (el.element.hasStyles()) {
1116
+ el.element.styles['line-height'] = value
1117
+ } else {
1118
+ el.element.styles = {
1119
+ 'line-height': value
1120
+ }
1121
+ }
1122
+ } else {
1123
+ const block = el.element.getBlock()
1124
+ const inblock = el.element.getInblock()
1125
+ if (inblock) {
1126
+ if (inblock.hasStyles()) {
1127
+ inblock.styles['line-height'] = value
1128
+ } else {
1129
+ inblock.styles = {
1130
+ 'line-height': value
1131
+ }
1132
+ }
1133
+ } else {
1134
+ if (block.hasStyles()) {
1135
+ block.styles['line-height'] = value
1136
+ } else {
1137
+ block.styles = {
1138
+ 'line-height': value
1139
+ }
1140
+ }
1141
+ }
1142
+ }
1143
+ })
1144
+ }
1145
+ this.editor.formatElementStack()
1146
+ this.editor.domRender()
1147
+ this.editor.rangeRender()
1148
+ },
1149
+ //api:增加缩进
1150
+ setIndentIncrease() {
1151
+ if (this.disabled) {
1152
+ return
1153
+ }
1154
+ if (!this.editor.range) {
1155
+ return
1156
+ }
1157
+ const fn = element => {
1158
+ if (element.hasStyles()) {
1159
+ if (element.styles.hasOwnProperty('text-indent')) {
1160
+ let val = element.styles['text-indent']
1161
+ if (val.endsWith('em')) {
1162
+ val = parseFloat(val)
1163
+ } else {
1164
+ val = 0
1165
+ }
1166
+ element.styles['text-indent'] = `${val + 2}em`
1167
+ } else {
1168
+ element.styles['text-indent'] = '2em'
1169
+ }
1170
+ } else {
1171
+ element.styles = {
1172
+ 'text-indent': '2em'
1173
+ }
1174
+ }
1175
+ }
1176
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1177
+ const block = this.editor.range.anchor.element.getBlock()
1178
+ const inblock = this.editor.range.anchor.element.getInblock()
1179
+ if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1180
+ fn(inblock)
1181
+ } else if (!block.isPreStyle()) {
1182
+ fn(block)
1183
+ }
1184
+ } else {
1185
+ const result = this.editor.getElementsByRange(true, false)
1186
+ result.forEach(item => {
1187
+ const block = item.element.getBlock()
1188
+ const inblock = item.element.getInblock()
1189
+ if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1190
+ fn(inblock)
1191
+ } else if (!block.isPreStyle()) {
1192
+ fn(block)
1193
+ }
1194
+ })
1195
+ }
1196
+ this.editor.formatElementStack()
1197
+ this.editor.domRender()
1198
+ this.editor.rangeRender()
1199
+ },
1200
+ //api:减少缩进
1201
+ setIndentDecrease() {
1202
+ if (this.disabled) {
1203
+ return
1204
+ }
1205
+ if (!this.editor.range) {
1206
+ return
1207
+ }
1208
+ const fn = element => {
1209
+ if (element.hasStyles() && element.styles.hasOwnProperty('text-indent')) {
1210
+ let val = element.styles['text-indent']
1211
+ if (val.endsWith('em')) {
1212
+ val = parseFloat(val)
1213
+ } else {
1214
+ val = 0
1215
+ }
1216
+ element.styles['text-indent'] = `${val - 2 >= 0 ? val - 2 : 0}em`
1217
+ }
1218
+ }
1219
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1220
+ const block = this.editor.range.anchor.element.getBlock()
1221
+ const inblock = this.editor.range.anchor.element.getInblock()
1222
+ if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1223
+ fn(inblock)
1224
+ } else if (!block.isPreStyle()) {
1225
+ fn(block)
1226
+ }
1227
+ } else {
1228
+ const result = this.editor.getElementsByRange(true, false)
1229
+ result.forEach(item => {
1230
+ const block = item.element.getBlock()
1231
+ const inblock = item.element.getInblock()
1232
+ if (inblock && inblock.behavior == 'block' && !inblock.isPreStyle()) {
1233
+ fn(inblock)
1234
+ } else if (!block.isPreStyle()) {
1235
+ fn(block)
1236
+ }
1237
+ })
1238
+ }
1239
+ this.editor.formatElementStack()
1240
+ this.editor.domRender()
1241
+ this.editor.rangeRender()
1242
+ },
1243
+ //api:插入图片
1244
+ insertImage(url, isRender = true) {
1245
+ if (this.disabled) {
1246
+ return
1247
+ }
1248
+ if (!this.editor.range) {
1249
+ return
1250
+ }
1251
+ if (!url || typeof url != 'string') {
1252
+ throw new Error('An image address must be given')
1253
+ }
1254
+ const image = new AlexElement(
1255
+ 'closed',
1256
+ 'img',
1257
+ {
1258
+ src: url
1259
+ },
1260
+ null,
1261
+ null
1262
+ )
1263
+ this.editor.insertElement(image)
1264
+ if (isRender) {
1265
+ this.editor.formatElementStack()
1266
+ this.editor.domRender()
1267
+ this.editor.rangeRender()
1268
+ }
1269
+ },
1270
+ //api:插入视频
1271
+ insertVideo(url, isRender = true) {
1272
+ if (this.disabled) {
1273
+ return
1274
+ }
1275
+ if (!this.editor.range) {
1276
+ return
1277
+ }
1278
+ if (!url || typeof url != 'string') {
1279
+ throw new Error('A video address must be given')
1280
+ }
1281
+ const video = new AlexElement(
1282
+ 'closed',
1283
+ 'video',
1284
+ {
1285
+ src: url
1286
+ },
1287
+ null,
1288
+ null
1289
+ )
1290
+ this.editor.insertElement(video)
1291
+ const leftSpace = AlexElement.getSpaceElement()
1292
+ const rightSpace = AlexElement.getSpaceElement()
1293
+ this.editor.addElementAfter(rightSpace, video)
1294
+ this.editor.addElementBefore(leftSpace, video)
1295
+ this.editor.range.anchor.moveToEnd(rightSpace)
1296
+ this.editor.range.focus.moveToEnd(rightSpace)
1297
+ if (isRender) {
1298
+ this.editor.formatElementStack()
1299
+ this.editor.domRender()
1300
+ this.editor.rangeRender()
1301
+ }
1302
+ },
1303
+ //api:选区是否含有代码块样式
1304
+ hasPreStyle() {
1305
+ if (!this.editor.range) {
1306
+ return false
1307
+ }
1308
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1309
+ return this.editor.range.anchor.element.isPreStyle()
1310
+ }
1311
+ const result = this.editor.getElementsByRange(true, false)
1312
+ return result.some(item => {
1313
+ return item.element.isPreStyle()
1314
+ })
1315
+ },
1316
+ //api:选区是否含有引用
1317
+ hasQuote() {
1318
+ if (!this.editor.range) {
1319
+ return false
1320
+ }
1321
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1322
+ const block = this.editor.range.anchor.element.getBlock()
1323
+ return block.parsedom == 'blockquote'
1324
+ }
1325
+ const result = this.editor.getElementsByRange(true, false)
1326
+ return result.some(item => {
1327
+ if (item.element.isBlock()) {
1328
+ return item.element.parsedom == 'blockquote'
1329
+ } else {
1330
+ const block = item.element.getBlock()
1331
+ return block.parsedom == 'blockquote'
1332
+ }
1333
+ })
1334
+ },
1335
+ //api:选区是否含有列表
1336
+ hasList(ordered = false) {
1337
+ if (!this.editor.range) {
1338
+ return false
1339
+ }
1340
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1341
+ const block = this.editor.range.anchor.element.getBlock()
1342
+ return blockIsList(block, ordered)
1343
+ }
1344
+ const result = this.editor.getElementsByRange(true, false)
1345
+ return result.some(item => {
1346
+ if (item.element.isBlock()) {
1347
+ return blockIsList(item.element, ordered)
1348
+ } else {
1349
+ const block = item.element.getBlock()
1350
+ return blockIsList(block, ordered)
1351
+ }
1352
+ })
1353
+ },
1354
+ //api:选区是否含有链接
1355
+ hasLink() {
1356
+ if (!this.editor.range) {
1357
+ return false
1358
+ }
1359
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1360
+ return !!this.getParsedomElementByElement(this.editor.range.anchor.element, 'a')
1361
+ }
1362
+ const result = this.editor.getElementsByRange(true, true).filter(item => {
1363
+ return item.element.isText()
1364
+ })
1365
+ return result.some(item => {
1366
+ return !!this.getParsedomElementByElement(item.element, 'a')
1367
+ })
1368
+ },
1369
+ //api:选区是否含有表格
1370
+ hasTable() {
1371
+ if (!this.editor.range) {
1372
+ return false
1373
+ }
1374
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1375
+ const block = this.editor.range.anchor.element.getBlock()
1376
+ return block.parsedom == 'table'
1377
+ }
1378
+ const result = this.editor.getElementsByRange(true, false)
1379
+ return result.some(item => {
1380
+ if (item.element.isBlock()) {
1381
+ return item.element.parsedom == 'table'
1382
+ } else {
1383
+ const block = item.element.getBlock()
1384
+ return block.parsedom == 'table'
1385
+ }
1386
+ })
1387
+ },
1388
+ //api:选区是否含有任务列表
1389
+ hasTask() {
1390
+ if (!this.editor.range) {
1391
+ return false
1392
+ }
1393
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1394
+ const block = this.editor.range.anchor.element.getBlock()
1395
+ return blockIsTask(block)
1396
+ }
1397
+ const result = this.editor.getElementsByRange(true, false)
1398
+ return result.some(item => {
1399
+ if (item.element.isBlock()) {
1400
+ return blockIsTask(item.element)
1401
+ } else {
1402
+ const block = item.element.getBlock()
1403
+ return blockIsTask(block)
1404
+ }
1405
+ })
1406
+ },
1407
+ //选区是否含有图片
1408
+ hasImage() {
1409
+ if (!this.editor.range) {
1410
+ return false
1411
+ }
1412
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1413
+ return this.editor.range.anchor.element.isClosed() && this.editor.range.anchor.element.parsedom == 'img'
1414
+ }
1415
+ const result = this.editor.getElementsByRange(true, true)
1416
+ return result.some(item => {
1417
+ return item.element.isClosed() && item.element.parsedom == 'img'
1418
+ })
1419
+ },
1420
+ //选区是否含有视频
1421
+ hasVideo() {
1422
+ if (!this.editor.range) {
1423
+ return false
1424
+ }
1425
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1426
+ return this.editor.range.anchor.element.isClosed() && this.editor.range.anchor.element.parsedom == 'video'
1427
+ }
1428
+ const result = this.editor.getElementsByRange(true, true)
1429
+ return result.some(item => {
1430
+ return item.element.isClosed() && item.element.parsedom == 'video'
1431
+ })
1432
+ },
1433
+ //api:选区是否全部在引用内
1434
+ inQuote() {
1435
+ if (!this.editor.range) {
1436
+ return false
1437
+ }
1438
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1439
+ const block = this.editor.range.anchor.element.getBlock()
1440
+ return block.parsedom == 'blockquote'
1441
+ }
1442
+ const result = this.editor.getElementsByRange(true, false)
1443
+ return result.every(item => {
1444
+ if (item.element.isBlock()) {
1445
+ return item.element.parsedom == 'blockquote'
1446
+ } else {
1447
+ const block = item.element.getBlock()
1448
+ return block.parsedom == 'blockquote'
1449
+ }
1450
+ })
1451
+ },
1452
+ //api:选区是否全部在列表内
1453
+ inList(ordered = false) {
1454
+ if (!this.editor.range) {
1455
+ return false
1456
+ }
1457
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1458
+ const block = this.editor.range.anchor.element.getBlock()
1459
+ return blockIsList(block, ordered)
1460
+ }
1461
+ const result = this.editor.getElementsByRange(true, false)
1462
+ return result.every(item => {
1463
+ if (item.element.isBlock()) {
1464
+ return blockIsList(item.element, ordered)
1465
+ } else {
1466
+ const block = item.element.getBlock()
1467
+ return blockIsList(block, ordered)
1468
+ }
1469
+ })
1470
+ },
1471
+ //api:选区是否全部在任务列表里
1472
+ inTask() {
1473
+ if (!this.editor.range) {
1474
+ return false
1475
+ }
1476
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1477
+ const block = this.editor.range.anchor.element.getBlock()
1478
+ return blockIsTask(block)
1479
+ }
1480
+ const result = this.editor.getElementsByRange(true, false)
1481
+ return result.every(item => {
1482
+ if (item.element.isBlock()) {
1483
+ return blockIsTask(item.element)
1484
+ } else {
1485
+ const block = item.element.getBlock()
1486
+ return blockIsTask(block)
1487
+ }
1488
+ })
1489
+ },
1490
+ //api:创建一个空的表格
1491
+ insertTable(rowLength, colLength) {
1492
+ if (this.disabled) {
1493
+ return
1494
+ }
1495
+ if (!this.editor.range) {
1496
+ return
1497
+ }
1498
+ const table = new AlexElement('block', 'table', null, null, null)
1499
+ const tbody = new AlexElement('inblock', 'tbody', null, null, null)
1500
+ this.editor.addElementTo(tbody, table)
1501
+ for (let i = 0; i < rowLength; i++) {
1502
+ const row = new AlexElement('inblock', 'tr', null, null, null)
1503
+ for (let j = 0; j < colLength; j++) {
1504
+ const column = new AlexElement('inblock', 'td', null, null, null)
1505
+ const breakEl = new AlexElement('closed', 'br', null, null, null)
1506
+ this.editor.addElementTo(breakEl, column)
1507
+ this.editor.addElementTo(column, row)
1508
+ }
1509
+ this.editor.addElementTo(row, tbody)
1510
+ }
1511
+ this.editor.insertElement(table)
1512
+ //在表格后创建一个段落
1513
+ const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1514
+ const breakEl = new AlexElement('closed', 'br', null, null, null)
1515
+ this.editor.addElementTo(breakEl, paragraph)
1516
+ this.editor.addElementAfter(paragraph, table)
1517
+ this.editor.formatElementStack()
1518
+ this.editor.range.anchor.moveToStart(tbody)
1519
+ this.editor.range.focus.moveToStart(tbody)
1520
+ this.editor.domRender()
1521
+ this.editor.rangeRender()
1522
+ },
1523
+ //api:插入代码块
1524
+ insertCodeBlock() {
1525
+ if (this.disabled) {
1526
+ return
1527
+ }
1528
+ if (!this.editor.range) {
1529
+ return
1530
+ }
1531
+ const pre = this.getCurrentParsedomElement('pre')
1532
+ if (pre) {
1533
+ let content = ''
1534
+ AlexElement.flatElements(pre.children)
1535
+ .filter(item => {
1536
+ return item.isText()
1537
+ })
1538
+ .forEach(item => {
1539
+ content += item.textContent
1540
+ })
1541
+ const splits = content.split('\n')
1542
+ splits.forEach(item => {
1543
+ const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1544
+ const text = new AlexElement('text', null, null, null, item)
1545
+ this.editor.addElementTo(text, paragraph)
1546
+ this.editor.addElementBefore(paragraph, pre)
1547
+ })
1548
+ pre.toEmpty()
1549
+ } else {
1550
+ //起点和终点在一起
1551
+ if (this.editor.range.anchor.isEqual(this.editor.range.focus)) {
1552
+ const block = this.editor.range.anchor.element.getBlock()
1553
+ blockToParagraph(block)
1554
+ block.parsedom = 'pre'
1555
+ const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1556
+ const breakEl = new AlexElement('closed', 'br', null, null, null)
1557
+ this.editor.addElementTo(breakEl, paragraph)
1558
+ this.editor.addElementAfter(paragraph, block)
1559
+ }
1560
+ //起点和终点不在一起
1561
+ else {
1562
+ let result = this.editor.getElementsByRange(true, false)
1563
+ this.editor.range.anchor.moveToStart(result[0].element.getBlock())
1564
+ this.editor.range.focus.moveToEnd(result[result.length - 1].element.getBlock())
1565
+ const res = this.editor.getElementsByRange(true, true).filter(el => el.element.isText())
1566
+ const obj = {}
1567
+ res.forEach(el => {
1568
+ if (obj[el.element.getBlock().key]) {
1569
+ obj[el.element.getBlock().key].push(el.element.clone())
1570
+ } else {
1571
+ obj[el.element.getBlock().key] = [el.element.clone()]
1572
+ }
1573
+ })
1574
+ const pre = new AlexElement('block', 'pre', null, null, null)
1575
+ Object.keys(obj).forEach((key, index) => {
1576
+ if (index > 0) {
1577
+ const text = new AlexElement('text', null, null, null, '\n')
1578
+ if (pre.hasChildren()) {
1579
+ this.editor.addElementTo(text, pre, pre.children.length)
1580
+ } else {
1581
+ this.editor.addElementTo(text, pre)
1582
+ }
1583
+ }
1584
+ obj[key].forEach(el => {
1585
+ if (pre.hasChildren()) {
1586
+ this.editor.addElementTo(el, pre, pre.children.length)
1587
+ } else {
1588
+ this.editor.addElementTo(el, pre)
1589
+ }
1590
+ })
1591
+ })
1592
+ this.editor.delete()
1593
+ this.editor.insertElement(pre)
1594
+ const paragraph = new AlexElement('block', AlexElement.BLOCK_NODE, null, null, null)
1595
+ const breakEl = new AlexElement('closed', 'br', null, null, null)
1596
+ this.editor.addElementTo(breakEl, paragraph)
1597
+ this.editor.addElementAfter(paragraph, pre)
1598
+ }
1599
+ }
1600
+ this.editor.formatElementStack()
1601
+ this.editor.domRender()
1602
+ this.editor.rangeRender()
1603
+ },
1604
+ //api:插入文本
1605
+ insertText(text) {
1606
+ if (this.disabled) {
1607
+ return
1608
+ }
1609
+ if (!this.editor.range) {
1610
+ return
1611
+ }
1612
+ this.editor.insertText(text)
1613
+ this.editor.formatElementStack()
1614
+ this.editor.domRender()
1615
+ this.editor.rangeRender()
1616
+ },
1617
+ //api:插入html
1618
+ insertHtml(html) {
1619
+ if (this.disabled) {
1620
+ return
1621
+ }
1622
+ if (!this.editor.range) {
1623
+ return
1624
+ }
1625
+ const elements = this.editor.parseHtml(html)
1626
+ for (let i = 0; i < elements.length; i++) {
1627
+ this.editor.insertElement(elements[i], false)
1628
+ }
1629
+ this.editor.formatElementStack()
1630
+ this.editor.domRender()
1631
+ this.editor.rangeRender()
1632
+ }
1633
+ },
1634
+ beforeUnmount() {
1635
+ //卸载绑定在滚动元素上的事件
1636
+ this.removeScrollHandle()
1637
+ //卸载绑定在document.documentElement上的事件
1638
+ Dap.event.off(document.documentElement, `mousedown.editify_${this.uid} mousemove.editify_${this.uid} mouseup.editify_${this.uid} click.editify_${this.uid}`)
1639
+ //卸载绑定在window上的事件
1640
+ Dap.event.off(window, `resize.editify_${this.uid}`)
1641
+ //销毁编辑器
1642
+ this.editor.destroy()
1643
+ }
1644
+ }
1645
+ </script>
1646
+ <style lang="less" scoped>
1647
+ .editify {
1648
+ display: block;
1649
+ width: 100%;
1650
+ position: relative;
1651
+ box-sizing: border-box;
1652
+ -webkit-tap-highlight-color: transparent;
1653
+ outline: none;
1654
+ font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, Roboto, 'Segoe UI', 'Microsoft YaHei', Arial, sans-serif;
1655
+ line-height: 1.5;
1656
+
1657
+ *,
1658
+ *::before,
1659
+ *::after {
1660
+ box-sizing: border-box;
1661
+ -webkit-tap-highlight-color: transparent;
1662
+ outline: none;
1663
+ }
1664
+ }
1665
+
1666
+ .editify-body {
1667
+ display: block;
1668
+ width: 100%;
1669
+ position: relative;
1670
+ background-color: @background;
1671
+ padding: 1px;
1672
+
1673
+ &.border {
1674
+ border: 1px solid @border-color;
1675
+ border-radius: 4px;
1676
+ transition: all 500ms;
1677
+
1678
+ &.menu_inner {
1679
+ border-radius: 0 0 4px 4px;
1680
+ border-top: none;
1681
+ }
1682
+ }
1683
+
1684
+ //编辑器样式
1685
+ .editify-content {
1686
+ display: block;
1687
+ position: relative;
1688
+ overflow-x: hidden;
1689
+ overflow-y: auto;
1690
+ width: 100%;
1691
+ border-radius: inherit;
1692
+ padding: 6px 10px;
1693
+ line-height: 1.5;
1694
+ color: @font-color-dark;
1695
+ font-size: @font-size;
1696
+ position: relative;
1697
+ line-height: 1.5;
1698
+
1699
+ //显示占位符
1700
+ &.placeholder::before {
1701
+ position: absolute;
1702
+ top: 0;
1703
+ left: 0;
1704
+ display: block;
1705
+ width: 100%;
1706
+ content: attr(data-editify-placeholder);
1707
+ font-size: inherit;
1708
+ font-family: inherit;
1709
+ color: @font-color-disabled;
1710
+ line-height: inherit;
1711
+ padding: 6px 10px;
1712
+ cursor: text;
1713
+ }
1714
+
1715
+ //段落样式和标题
1716
+ :deep(p),
1717
+ :deep(h1),
1718
+ :deep(h2),
1719
+ :deep(h3),
1720
+ :deep(h4),
1721
+ :deep(h5),
1722
+ :deep(h6) {
1723
+ display: block;
1724
+ width: 100%;
1725
+ margin: 0 0 15px 0;
1726
+ padding: 0;
1727
+ }
1728
+ :deep(h1) {
1729
+ font-size: 32px;
1730
+ }
1731
+ :deep(h2) {
1732
+ font-size: 28px;
1733
+ }
1734
+ :deep(h3) {
1735
+ font-size: 24px;
1736
+ }
1737
+ :deep(h4) {
1738
+ font-size: 20px;
1739
+ }
1740
+ :deep(h5) {
1741
+ font-size: 18px;
1742
+ }
1743
+ :deep(h6) {
1744
+ font-size: 16px;
1745
+ }
1746
+ //有序列表样式
1747
+ :deep(div[data-editify-list='ol']) {
1748
+ margin-bottom: 15px;
1749
+
1750
+ &::before {
1751
+ content: attr(data-editify-value) '.';
1752
+ margin-right: 10px;
1753
+ }
1754
+ }
1755
+ //无序列表样式
1756
+ :deep(div[data-editify-list='ul']) {
1757
+ margin-bottom: 15px;
1758
+
1759
+ &::before {
1760
+ content: '\2022';
1761
+ margin-right: 10px;
1762
+ }
1763
+ }
1764
+ //代码样式
1765
+ :deep(span[data-editify-code]) {
1766
+ display: inline-block;
1767
+ padding: 3px 6px;
1768
+ margin: 0 4px;
1769
+ border-radius: 4px;
1770
+ line-height: 1;
1771
+ font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
1772
+ background-color: @pre-background;
1773
+ color: @font-color;
1774
+ border: 1px solid @border-color;
1775
+ text-indent: initial;
1776
+ font-size: @font-size;
1777
+ font-weight: normal;
1778
+ }
1779
+ //链接样式
1780
+ :deep(a) {
1781
+ color: @font-color-link;
1782
+ transition: all 200ms;
1783
+ text-decoration: none;
1784
+ cursor: text;
1785
+
1786
+ &:hover {
1787
+ color: @font-color-link-dark;
1788
+ text-decoration: underline;
1789
+ }
1790
+ }
1791
+ //表格样式
1792
+ :deep(table) {
1793
+ width: 100%;
1794
+ border: 1px solid @border-color;
1795
+ margin: 0;
1796
+ padding: 0;
1797
+ border-collapse: collapse;
1798
+ margin-bottom: 15px;
1799
+ background-color: @background;
1800
+ color: @font-color-dark;
1801
+ font-size: @font-size;
1802
+
1803
+ * {
1804
+ margin: 0 !important;
1805
+ }
1806
+
1807
+ tbody {
1808
+ margin: 0;
1809
+ padding: 0;
1810
+
1811
+ tr {
1812
+ margin: 0;
1813
+ padding: 0;
1814
+
1815
+ &:first-child {
1816
+ background-color: @background-darker;
1817
+
1818
+ td {
1819
+ font-weight: bold;
1820
+ position: relative;
1821
+ }
1822
+ }
1823
+
1824
+ td {
1825
+ margin: 0;
1826
+ border: 1px solid @border-color;
1827
+ padding: 6px 10px;
1828
+ position: relative;
1829
+ word-break: break-word;
1830
+
1831
+ &:not(:last-child)::after {
1832
+ position: absolute;
1833
+ right: -5px;
1834
+ top: 0;
1835
+ width: 10px;
1836
+ height: 100%;
1837
+ content: '';
1838
+ z-index: 1;
1839
+ cursor: col-resize;
1840
+ user-select: none;
1841
+ }
1842
+ }
1843
+ }
1844
+ }
1845
+ }
1846
+ //代码块样式
1847
+ :deep(pre) {
1848
+ display: block;
1849
+ padding: 6px 10px;
1850
+ margin: 0 0 15px;
1851
+ font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
1852
+ line-height: 1.5;
1853
+ font-size: @font-size;
1854
+ color: @font-color-dark;
1855
+ background-color: @pre-background;
1856
+ border: 1px solid @border-color;
1857
+ border-radius: 4px;
1858
+ overflow: auto;
1859
+ position: relative;
1860
+ }
1861
+ //图片样式
1862
+ :deep(img) {
1863
+ position: relative;
1864
+ display: inline-block;
1865
+ width: 30%;
1866
+ height: auto;
1867
+ border-radius: 2px;
1868
+ vertical-align: text-bottom;
1869
+ margin: 0 2px;
1870
+ }
1871
+ //视频样式
1872
+ :deep(video) {
1873
+ position: relative;
1874
+ display: inline-block;
1875
+ width: 30%;
1876
+ border-radius: 2px;
1877
+ vertical-align: text-bottom;
1878
+ background-color: #000;
1879
+ object-fit: contain;
1880
+ margin: 0 2px;
1881
+ }
1882
+ //引用样式
1883
+ :deep(blockquote) {
1884
+ display: block;
1885
+ border-left: 8px solid @background-darker;
1886
+ padding: 6px 10px 6px 20px;
1887
+ margin: 0 0 15px;
1888
+ line-height: 1.5;
1889
+ font-size: @font-size;
1890
+ color: @font-color-light;
1891
+ border-radius: 0;
1892
+ }
1893
+ //任务列表样式
1894
+ :deep(div[data-editify-task]) {
1895
+ margin-bottom: 15px;
1896
+ position: relative;
1897
+ padding-left: 26px;
1898
+ font-size: @font-size;
1899
+ color: @font-color-dark;
1900
+ transition: all 200ms;
1901
+
1902
+ &::before {
1903
+ display: block;
1904
+ width: 16px;
1905
+ height: 16px;
1906
+ border-radius: 2px;
1907
+ border: 2px solid @font-color-light;
1908
+ transition: all 200ms;
1909
+ box-sizing: border-box;
1910
+ user-select: none;
1911
+ content: '';
1912
+ position: absolute;
1913
+ left: 0;
1914
+ top: 50%;
1915
+ transform: translateY(-50%);
1916
+ z-index: 1;
1917
+ cursor: pointer;
1918
+ }
1919
+
1920
+ &::after {
1921
+ position: absolute;
1922
+ content: '';
1923
+ left: 3px;
1924
+ top: 6px;
1925
+ display: inline-block;
1926
+ width: 10px;
1927
+ height: 6px;
1928
+ border: 2px solid @font-color-light;
1929
+ border-top: none;
1930
+ border-right: none;
1931
+ transform: rotate(-45deg);
1932
+ transform-origin: center;
1933
+ margin-bottom: 2px;
1934
+ box-sizing: border-box;
1935
+ z-index: 2;
1936
+ cursor: pointer;
1937
+ opacity: 0;
1938
+ transition: all 200ms;
1939
+ }
1940
+
1941
+ &[data-editify-task='checked'] {
1942
+ text-decoration: line-through;
1943
+ color: @font-color-light;
1944
+ &::after {
1945
+ opacity: 1;
1946
+ }
1947
+ }
1948
+ }
1949
+
1950
+ //禁用样式
1951
+ &.disabled {
1952
+ cursor: auto !important;
1953
+ &.placeholder::before {
1954
+ cursor: auto;
1955
+ }
1956
+ :deep(a) {
1957
+ cursor: pointer;
1958
+ }
1959
+
1960
+ :deep(table) {
1961
+ td:not(:last-child)::after {
1962
+ cursor: auto;
1963
+ }
1964
+ }
1965
+ }
1966
+ }
1967
+
1968
+ //代码视图
1969
+ .editify-source {
1970
+ display: block;
1971
+ width: 100%;
1972
+ height: 100%;
1973
+ position: absolute;
1974
+ left: 0;
1975
+ top: 0;
1976
+ z-index: 10;
1977
+ background-color: @reverse-background;
1978
+ border-radius: inherit;
1979
+ margin: 0;
1980
+ padding: 6px 10px;
1981
+ overflow-x: hidden;
1982
+ overflow-y: auto;
1983
+ font-size: @font-size;
1984
+ color: @reverse-color;
1985
+ font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
1986
+ resize: none;
1987
+ border: none;
1988
+ }
1989
+ }
1990
+
1991
+ .editify-footer {
1992
+ display: flex;
1993
+ justify-content: end;
1994
+ align-items: center;
1995
+ width: 100%;
1996
+ padding: 10px;
1997
+
1998
+ .editify-footer-words {
1999
+ font-size: @font-size;
2000
+ color: @font-color-light;
2001
+ line-height: 1;
2002
+ }
2003
+ }
2004
+ </style>