vue-editify 0.1.3 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
package/src/Editify.vue CHANGED
@@ -1,1178 +1,1197 @@
1
- <template>
2
- <div class="editify" :class="{ fullscreen: isFullScreen }">
3
- <!-- 菜单区域 -->
4
- <Menu v-if="menuConfig.use" :config="menuConfig" :color="color" ref="menu"></Menu>
5
- <!-- 编辑层,与编辑区域宽高相同必须适配 -->
6
- <div ref="body" class="editify-body" :class="{ border: showBorder, menu_inner: menuConfig.use && menuConfig.mode == 'inner' }" :data-editify-uid="uid">
7
- <!-- 编辑器 -->
8
- <div ref="content" class="editify-content" :class="{ placeholder: showPlaceholder, disabled: disabled }" @keydown="handleEditorKeydown" @click="handleEditorClick" @compositionstart="isInputChinese = true" @compositionend="isInputChinese = false" :data-editify-placeholder="placeholder"></div>
9
- <!-- 代码视图 -->
10
- <textarea v-if="isSourceView" :value="value" readonly class="editify-source" />
11
- <!-- 工具条 -->
12
- <Toolbar ref="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" :class="{ fullscreen: isFullScreen && !isSourceView }" ref="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 { element as DapElement, event as DapEvent, data as DapData, number as DapNumber, color as DapColor } from 'dap-util'
25
- import { pasteKeepData, editorProps, mergeObject, getToolbarConfig, getMenuConfig } from './core/tool'
26
- import { parseList, orderdListHandle, mediaHandle, tableHandle, preHandle, specialInblockHandle } from './core/rule'
27
- import { isTask, elementToParagraph, getCurrentParsedomElement, hasTableInRange, hasLinkInRange, hasPreInRange, hasImageInRange, hasVideoInRange, setIndentIncrease, setIndentDecrease, insertImage, insertVideo } from './core/function'
28
- import Tooltip from './components/base/Tooltip'
29
- import Toolbar from './components/Toolbar'
30
- import Menu from './components/Menu'
31
-
32
- export default {
33
- name: 'editify',
34
- props: { ...editorProps },
35
- emits: ['update:modelValue', 'focus', 'blur', 'change', 'keydown', 'insertparagraph', 'rangeupdate', 'updateview'],
36
- setup() {
37
- const instance = getCurrentInstance()
38
- return {
39
- uid: instance.uid
40
- }
41
- },
42
- data() {
43
- return {
44
- //是否编辑器内部修改值
45
- isModelChange: false,
46
- //是否正在输入中文
47
- isInputChinese: false,
48
- //表格列宽拖拽记录数据
49
- tableColumnResizeParams: {
50
- element: null, //被拖拽的td
51
- start: 0 //水平方向起点位置
52
- },
53
- //工具条参数配置
54
- toolbarOptions: {
55
- //是否显示工具条
56
- show: false,
57
- //关联元素
58
- node: null,
59
- //类型
60
- type: 'text'
61
- },
62
-
63
- /** 以下是可对外使用的属性 */
64
-
65
- //编辑器对象
66
- editor: null,
67
- //是否代码视图
68
- isSourceView: false,
69
- //是否全屏
70
- isFullScreen: false,
71
- //菜单栏是否可以使用标识
72
- canUseMenu: false,
73
- //光标选取范围内的元素数组
74
- dataRangeCaches: {
75
- flatList: [],
76
- list: []
77
- }
78
- }
79
- },
80
- computed: {
81
- //编辑器的值
82
- value: {
83
- set(val) {
84
- this.$emit('update:modelValue', val)
85
- },
86
- get() {
87
- return this.modelValue || '<p><br></p>'
88
- }
89
- },
90
- //编辑器的纯文本值
91
- textValue() {
92
- return DapElement.string2dom(`<div>${this.value}</div>`).innerText
93
- },
94
- //是否显示占位符
95
- showPlaceholder() {
96
- if (this.editor) {
97
- const elements = this.editor.parseHtml(this.value)
98
- if (elements.length == 1 && elements[0].type == 'block' && elements[0].parsedom == AlexElement.BLOCK_NODE && elements[0].isOnlyHasBreak()) {
99
- return !this.isInputChinese
100
- }
101
- }
102
- return false
103
- },
104
- //是否显示边框
105
- showBorder() {
106
- //全屏模式下不显示边框
107
- if (this.isFullScreen) {
108
- return false
109
- }
110
- return this.border
111
- },
112
- //最终生效的工具栏配置
113
- toolbarConfig() {
114
- return mergeObject(getToolbarConfig(this.$editTrans, this.$editLocale), this.toolbar || {})
115
- },
116
- //最终生效的菜单栏配置
117
- menuConfig() {
118
- return mergeObject(getMenuConfig(this.$editTrans, this.$editLocale), this.menu || {})
119
- }
120
- },
121
- components: {
122
- Toolbar,
123
- Tooltip,
124
- Menu
125
- },
126
- inject: ['$editTrans', '$editLocale'],
127
- watch: {
128
- //监听编辑的值变更
129
- value(newVal) {
130
- //内部修改不处理
131
- if (this.isModelChange) {
132
- return
133
- }
134
- //如果是外部修改,需要重新渲染编辑器
135
- this.editor.stack = this.editor.parseHtml(newVal)
136
- this.editor.range = null
137
- this.editor.formatElementStack()
138
- this.editor.domRender()
139
- this.editor.rangeRender()
140
- this.$refs.content.blur()
141
- },
142
- //代码视图切换
143
- isSourceView(newValue) {
144
- if (this.toolbarConfig.use) {
145
- if (newValue) {
146
- this.hideToolbar()
147
- } else {
148
- this.handleToolbar()
149
- }
150
- }
151
- },
152
- //监听disabled
153
- disabled(newValue) {
154
- if (newValue) {
155
- this.editor.setDisabled()
156
- } else {
157
- this.editor.setEnabled()
158
- }
159
- }
160
- },
161
- mounted() {
162
- //创建编辑器
163
- this.createEditor()
164
- //监听滚动隐藏工具条
165
- this.handleScroll()
166
- //鼠标按下监听
167
- DapEvent.on(document.documentElement, `mousedown.editify_${this.uid}`, this.documentMouseDown)
168
- //鼠标移动监听
169
- DapEvent.on(document.documentElement, `mousemove.editify_${this.uid}`, this.documentMouseMove)
170
- //鼠标松开监听
171
- DapEvent.on(document.documentElement, `mouseup.editify_${this.uid}`, this.documentMouseUp)
172
- //鼠标点击箭头
173
- DapEvent.on(document.documentElement, `click.editify_${this.uid}`, this.documentClick)
174
- //监听窗口改变
175
- DapEvent.on(window, `resize.editify_${this.uid}`, this.setVideoHeight)
176
- },
177
- methods: {
178
- //编辑器内部修改值的方法
179
- internalModify(val) {
180
- this.isModelChange = true
181
- this.value = val
182
- this.$nextTick(() => {
183
- this.isModelChange = false
184
- })
185
- },
186
- //隐藏工具条
187
- hideToolbar() {
188
- this.toolbarOptions.show = false
189
- this.toolbarOptions.node = null
190
- },
191
- //监听滚动隐藏工具条
192
- handleScroll() {
193
- const setScroll = el => {
194
- DapEvent.on(el, `scroll.editify_${this.uid}`, () => {
195
- if (this.toolbarConfig.use && this.toolbarOptions.show) {
196
- this.hideToolbar()
197
- }
198
- })
199
- if (el.parentNode) {
200
- setScroll(el.parentNode)
201
- }
202
- }
203
- setScroll(this.$refs.content)
204
- },
205
- //移除上述滚动事件的监听
206
- removeScrollHandle() {
207
- const removeScroll = el => {
208
- DapEvent.off(el, `scroll.editify_${this.uid}`)
209
- if (el.parentNode) {
210
- removeScroll(el.parentNode)
211
- }
212
- }
213
- removeScroll(this.$refs.content)
214
- },
215
- //工具条显示判断
216
- handleToolbar() {
217
- if (this.disabled || this.isSourceView) {
218
- return
219
- }
220
- this.hideToolbar()
221
- this.$nextTick(() => {
222
- const table = getCurrentParsedomElement(this, 'table')
223
- const pre = getCurrentParsedomElement(this, 'pre')
224
- const link = getCurrentParsedomElement(this, 'a')
225
- const image = getCurrentParsedomElement(this, 'img')
226
- const video = getCurrentParsedomElement(this, 'video')
227
- if (link) {
228
- this.toolbarOptions.type = 'link'
229
- this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${link.key}"]`
230
- if (this.toolbarOptions.show) {
231
- this.$refs.toolbar.$refs.layer.setPosition()
232
- } else {
233
- this.toolbarOptions.show = true
234
- }
235
- } else if (image) {
236
- this.toolbarOptions.type = 'image'
237
- this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${image.key}"]`
238
- if (this.toolbarOptions.show) {
239
- this.$refs.toolbar.$refs.layer.setPosition()
240
- } else {
241
- this.toolbarOptions.show = true
242
- }
243
- } else if (video) {
244
- this.toolbarOptions.type = 'video'
245
- this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${video.key}"]`
246
- if (this.toolbarOptions.show) {
247
- this.$refs.toolbar.$refs.layer.setPosition()
248
- } else {
249
- this.toolbarOptions.show = true
250
- }
251
- } else if (table) {
252
- this.toolbarOptions.type = 'table'
253
- this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${table.key}"]`
254
- if (this.toolbarOptions.show) {
255
- this.$refs.toolbar.$refs.layer.setPosition()
256
- } else {
257
- this.toolbarOptions.show = true
258
- }
259
- } else if (pre) {
260
- this.toolbarOptions.type = 'codeBlock'
261
- this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${pre.key}"]`
262
- if (this.toolbarOptions.show) {
263
- this.$refs.toolbar.$refs.layer.setPosition()
264
- } else {
265
- this.toolbarOptions.show = true
266
- }
267
- } else {
268
- const result = this.dataRangeCaches.flatList.filter(item => {
269
- return item.element.isText()
270
- })
271
- if (result.length && !hasTableInRange(this) && !hasPreInRange(this) && !hasLinkInRange(this) && !hasImageInRange(this) && !hasVideoInRange(this)) {
272
- this.toolbarOptions.type = 'text'
273
- if (this.toolbarOptions.show) {
274
- this.$refs.toolbar.$refs.layer.setPosition()
275
- } else {
276
- this.toolbarOptions.show = true
277
- }
278
- }
279
- }
280
- })
281
- },
282
- //设定编辑器内的视频高度
283
- setVideoHeight() {
284
- this.$refs.content.querySelectorAll('video').forEach(video => {
285
- video.style.height = video.offsetWidth / this.videoRatio + 'px'
286
- })
287
- },
288
- //初始创建编辑器
289
- createEditor() {
290
- //创建编辑器
291
- this.editor = new AlexEditor(this.$refs.content, {
292
- value: this.value,
293
- disabled: this.disabled,
294
- renderRules: [
295
- el => {
296
- parseList(this.editor, el)
297
- },
298
- el => {
299
- orderdListHandle(this.editor, el)
300
- },
301
- el => {
302
- mediaHandle(this.editor, el)
303
- },
304
- el => {
305
- tableHandle(this.editor, el)
306
- },
307
- el => {
308
- preHandle(this.editor, el, this.toolbarConfig?.use && this.toolbarConfig?.codeBlock?.languages?.show, this.toolbarConfig?.codeBlock?.languages.options)
309
- },
310
- el => {
311
- specialInblockHandle(this.editor, el)
312
- },
313
- ...this.renderRules
314
- ],
315
- allowCopy: this.allowCopy,
316
- allowPaste: this.allowPaste,
317
- allowCut: this.allowCut,
318
- allowPasteHtml: this.allowPasteHtml,
319
- allowPasteHtml: this.allowPasteHtml,
320
- customImagePaste: this.handleCustomImagePaste,
321
- customVideoPaste: this.handleCustomVideoPaste,
322
- customMerge: this.handleCustomMerge,
323
- customParseNode: this.handleCustomParseNode
324
- })
325
- //编辑器渲染后会有一个渲染过程,会改变内容,因此重新获取内容的值来设置value
326
- this.internalModify(this.editor.value)
327
- //设置监听事件
328
- this.editor.on('change', this.handleEditorChange)
329
- this.editor.on('focus', this.handleEditorFocus)
330
- this.editor.on('blur', this.handleEditorBlur)
331
- this.editor.on('insertParagraph', this.handleInsertParagraph)
332
- this.editor.on('rangeUpdate', this.handleRangeUpdate)
333
- this.editor.on('pasteHtml', this.handlePasteHtml)
334
- this.editor.on('deleteInStart', this.handleDeleteInStart)
335
- this.editor.on('deleteComplete', this.handleDeleteComplete)
336
- this.editor.on('afterRender', this.handleAfterRender)
337
- //格式化和dom渲染
338
- this.editor.formatElementStack()
339
- this.editor.domRender()
340
- //自动获取焦点
341
- if (this.autofocus && !this.isSourceView && !this.disabled) {
342
- this.collapseToEnd()
343
- }
344
- },
345
- //鼠标在页面按下:处理表格拖拽改变列宽和菜单栏是否使用判断
346
- documentMouseDown(e) {
347
- if (this.disabled) {
348
- return
349
- }
350
- //鼠标在编辑器内按下
351
- if (DapElement.isContains(this.$refs.content, e.target)) {
352
- const elm = e.target
353
- const key = DapData.get(elm, 'data-alex-editor-key')
354
- if (key) {
355
- const element = this.editor.getElementByKey(key)
356
- if (element && element.parsedom == 'td') {
357
- const length = element.parent.children.length
358
- //最后一个td不设置
359
- if (element.parent.children[length - 1].isEqual(element)) {
360
- return
361
- }
362
- const rect = DapElement.getElementBounding(elm)
363
- //在可拖拽范围内
364
- if (e.pageX >= Math.abs(rect.left + elm.offsetWidth - 5) && e.pageX <= Math.abs(rect.left + elm.offsetWidth + 5)) {
365
- this.tableColumnResizeParams.element = element
366
- this.tableColumnResizeParams.start = e.pageX
367
- }
368
- }
369
- }
370
- }
371
- //如果点击了除编辑器外的地方,菜单栏不可使用
372
- if (!DapElement.isContains(this.$el, e.target) && !this.isSourceView) {
373
- this.canUseMenu = false
374
- }
375
- },
376
- //鼠标在页面移动:处理表格拖拽改变列宽
377
- documentMouseMove(e) {
378
- if (this.disabled) {
379
- return
380
- }
381
- if (!this.tableColumnResizeParams.element) {
382
- return
383
- }
384
- const table = getCurrentParsedomElement(this, 'table')
385
- if (!table) {
386
- return
387
- }
388
- const colgroup = table.children.find(item => {
389
- return item.parsedom == 'colgroup'
390
- })
391
- const index = this.tableColumnResizeParams.element.parent.children.findIndex(el => {
392
- return el.isEqual(this.tableColumnResizeParams.element)
393
- })
394
- const width = `${this.tableColumnResizeParams.element.elm.offsetWidth + e.pageX - this.tableColumnResizeParams.start}`
395
- colgroup.children[index].marks['width'] = width
396
- colgroup.children[index].elm.setAttribute('width', width)
397
- this.tableColumnResizeParams.start = e.pageX
398
- },
399
- //鼠标在页面松开:处理表格拖拽改变列宽
400
- documentMouseUp() {
401
- if (this.disabled) {
402
- return
403
- }
404
- if (!this.tableColumnResizeParams.element) {
405
- return
406
- }
407
- const table = getCurrentParsedomElement(this, 'table')
408
- if (!table) {
409
- return
410
- }
411
- const colgroup = table.children.find(item => {
412
- return item.parsedom == 'colgroup'
413
- })
414
- const index = this.tableColumnResizeParams.element.parent.children.findIndex(el => {
415
- return el.isEqual(this.tableColumnResizeParams.element)
416
- })
417
- const width = Number(colgroup.children[index].marks['width'])
418
- if (!isNaN(width)) {
419
- colgroup.children[index].marks['width'] = `${Number(((width / this.tableColumnResizeParams.element.parent.elm.offsetWidth) * 100).toFixed(2))}%`
420
- this.editor.formatElementStack()
421
- this.editor.domRender()
422
- this.editor.rangeRender()
423
- }
424
- this.tableColumnResizeParams.element = null
425
- this.tableColumnResizeParams.start = 0
426
- },
427
- //鼠标点击页面:处理任务列表复选框勾选
428
- documentClick(e) {
429
- if (this.disabled) {
430
- return
431
- }
432
- //鼠标在编辑器内点击
433
- if (DapElement.isContains(this.$refs.content, e.target)) {
434
- const elm = e.target
435
- const key = DapData.get(elm, 'data-alex-editor-key')
436
- if (key) {
437
- const element = this.editor.getElementByKey(key)
438
- //如果是任务列表元素
439
- if (isTask(element)) {
440
- const rect = DapElement.getElementBounding(elm)
441
- //在复选框范围内
442
- if (e.pageX >= Math.abs(rect.left) && e.pageX <= Math.abs(rect.left + 16) && e.pageY >= Math.abs(rect.top + 2) && e.pageY <= Math.abs(rect.top + 18)) {
443
- //取消勾选
444
- if (element.marks['data-editify-task'] == 'checked') {
445
- element.marks['data-editify-task'] = 'uncheck'
446
- }
447
- //勾选
448
- else {
449
- element.marks['data-editify-task'] = 'checked'
450
- }
451
- if (!this.editor.range) {
452
- this.editor.initRange()
453
- }
454
- this.editor.range.anchor.moveToEnd(element)
455
- this.editor.range.focus.moveToEnd(element)
456
- this.editor.formatElementStack()
457
- this.editor.domRender()
458
- this.editor.rangeRender()
459
- }
460
- }
461
- }
462
- }
463
- },
464
- //自定义图片粘贴
465
- async handleCustomImagePaste(url) {
466
- const newUrl = await this.customImagePaste.apply(this, [url])
467
- if (newUrl) {
468
- insertImage(this, newUrl)
469
- }
470
- },
471
- //自定义视频粘贴
472
- async handleCustomVideoPaste(url) {
473
- const newUrl = await this.customVideoPaste.apply(this, [url])
474
- if (newUrl) {
475
- insertVideo(this, newUrl)
476
- }
477
- },
478
- //重新定义编辑器合并元素的逻辑
479
- handleCustomMerge(ele, preEle) {
480
- const uneditable = preEle.getUneditableElement()
481
- if (uneditable) {
482
- uneditable.toEmpty()
483
- } else {
484
- preEle.children.push(...ele.children)
485
- preEle.children.forEach(item => {
486
- item.parent = preEle
487
- })
488
- ele.children = null
489
- }
490
- },
491
- //针对node转为元素进行额外的处理
492
- handleCustomParseNode(ele) {
493
- if (ele.parsedom == 'code') {
494
- ele.parsedom = 'span'
495
- const marks = {
496
- 'data-editify-code': true
497
- }
498
- if (ele.hasMarks()) {
499
- Object.assign(ele.marks, marks)
500
- } else {
501
- ele.marks = marks
502
- }
503
- }
504
- if (typeof this.customParseNode == 'function') {
505
- ele = this.customParseNode.apply(this, [ele])
506
- }
507
- return ele
508
- },
509
- //编辑区域键盘按下:设置缩进快捷键
510
- handleEditorKeydown(e) {
511
- if (this.disabled) {
512
- return
513
- }
514
- //增加缩进
515
- if (e.keyCode == 9 && !e.metaKey && !e.shiftKey && !e.ctrlKey && !e.altKey) {
516
- e.preventDefault()
517
- if (!hasTableInRange(this)) {
518
- setIndentIncrease(this)
519
- this.editor.formatElementStack()
520
- this.editor.domRender()
521
- this.editor.rangeRender()
522
- }
523
- }
524
- //减少缩进
525
- else if (e.keyCode == 9 && !e.metaKey && e.shiftKey && !e.ctrlKey && !e.altKey) {
526
- e.preventDefault()
527
- if (!hasTableInRange(this)) {
528
- setIndentDecrease(this)
529
- this.editor.formatElementStack()
530
- this.editor.domRender()
531
- this.editor.rangeRender()
532
- }
533
- }
534
- //自定义键盘按下操作
535
- this.$emit('keydown', e)
536
- },
537
- //点击编辑器:处理图片和视频的光标聚集
538
- handleEditorClick(e) {
539
- if (this.disabled || this.isSourceView) {
540
- return
541
- }
542
- const node = e.target
543
- //点击的是图片或者视频
544
- if (node.nodeName.toLocaleLowerCase() == 'img' || node.nodeName.toLocaleLowerCase() == 'video') {
545
- const key = Number(node.getAttribute('data-editify-element'))
546
- if (DapNumber.isNumber(key)) {
547
- const element = this.editor.getElementByKey(key)
548
- if (!this.editor.range) {
549
- this.editor.initRange()
550
- }
551
- this.editor.range.anchor.moveToStart(element)
552
- this.editor.range.focus.moveToEnd(element)
553
- this.editor.rangeRender()
554
- }
555
- }
556
- },
557
- //编辑器的值更新
558
- handleEditorChange(newVal, oldVal) {
559
- if (this.disabled) {
560
- return
561
- }
562
- //内部修改
563
- this.internalModify(newVal)
564
- //触发change事件
565
- this.$emit('change', newVal, oldVal)
566
- },
567
- //编辑器失去焦点
568
- handleEditorBlur(val) {
569
- if (this.disabled) {
570
- return
571
- }
572
- if (this.border && this.color && !this.isFullScreen) {
573
- //恢复编辑区域边框颜色
574
- this.$refs.body.style.borderColor = ''
575
- //恢复编辑区域阴影颜色
576
- this.$refs.body.style.boxShadow = ''
577
- //使用菜单栏的情况下恢复菜单栏的样式
578
- if (this.menuConfig.use) {
579
- this.$refs.menu.$el.style.borderColor = ''
580
- this.$refs.menu.$el.style.boxShadow = ''
581
- }
582
- }
583
- this.$emit('blur', val)
584
- },
585
- //编辑器获取焦点
586
- handleEditorFocus(val) {
587
- if (this.disabled) {
588
- return
589
- }
590
- if (this.border && this.color && !this.isFullScreen) {
591
- //编辑区域边框颜色
592
- this.$refs.body.style.borderColor = this.color
593
- //转换颜色值
594
- const rgb = DapColor.hex2rgb(this.color)
595
- //菜单栏模式为inner
596
- if (this.menuConfig.use && this.menuConfig.mode == 'inner') {
597
- //编辑区域除顶部边框的阴影
598
- 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)`
599
- //菜单栏的边框颜色
600
- this.$refs.menu.$el.style.borderColor = this.color
601
- //菜单栏除底部边框的阴影
602
- 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)`
603
- }
604
- //其他菜单栏模式
605
- else if (this.menuConfig.use) {
606
- //编辑区域四边阴影
607
- this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
608
- }
609
- //不使用菜单栏
610
- else {
611
- //编辑区域四边阴影
612
- this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
613
- }
614
- }
615
- //获取焦点时可以使用菜单栏
616
- setTimeout(() => {
617
- this.canUseMenu = true
618
- this.$emit('focus', val)
619
- }, 0)
620
- },
621
- //编辑器换行
622
- handleInsertParagraph(element, previousElement) {
623
- //前一个块元素如果是只包含换行符的元素,并且当前块元素也是包含换行符的元素,则当前块元素转为段落
624
- if (previousElement.isOnlyHasBreak() && element.isOnlyHasBreak()) {
625
- if (previousElement.parsedom != AlexElement.BLOCK_NODE) {
626
- elementToParagraph(previousElement)
627
- this.editor.range.anchor.moveToStart(previousElement)
628
- this.editor.range.focus.moveToStart(previousElement)
629
- element.toEmpty()
630
- }
631
- }
632
- this.$emit('insertparagraph', this.value)
633
- },
634
- //编辑器焦点更新
635
- handleRangeUpdate() {
636
- if (this.disabled) {
637
- return
638
- }
639
-
640
- //如果没有range禁用菜单栏
641
- this.canUseMenu = !!this.editor.range
642
-
643
- //没有range直接返回
644
- if (!this.editor.range) {
645
- return
646
- }
647
-
648
- //获取光标选取范围内的元素数据,并且进行缓存
649
- this.dataRangeCaches = this.editor.getElementsByRange()
650
-
651
- //如果使用工具条或者菜单栏
652
- if (this.toolbarConfig.use || this.menuConfig.use) {
653
- //如果使用工具条
654
- if (this.toolbarConfig.use) {
655
- this.handleToolbar()
656
- }
657
- //如果使用菜单栏
658
- if (this.menuConfig.use) {
659
- this.$refs.menu.handleRangeUpdate()
660
- }
661
- }
662
-
663
- this.$emit('rangeupdate')
664
- },
665
- //编辑器粘贴html
666
- handlePasteHtml(elements) {
667
- const keepStyles = Object.assign(pasteKeepData.styles, this.pasteKeepStyles || {})
668
- const keepMarks = Object.assign(pasteKeepData.marks, this.pasteKeepMarks || {})
669
- //粘贴html时过滤元素的样式和属性
670
- AlexElement.flatElements(elements).forEach(el => {
671
- let marks = {}
672
- let styles = {}
673
- if (el.hasMarks()) {
674
- for (let key in keepMarks) {
675
- if (el.marks.hasOwnProperty(key) && ((Array.isArray(keepMarks[key]) && keepMarks[key].includes(el.parsedom)) || keepMarks[key] == '*')) {
676
- marks[key] = el.marks[key]
677
- }
678
- }
679
- el.marks = marks
680
- }
681
- if (el.hasStyles() && !el.isText()) {
682
- for (let key in keepStyles) {
683
- if (el.styles.hasOwnProperty(key) && ((Array.isArray(keepStyles[key]) && keepStyles[key].includes(el.parsedom)) || keepStyles[key] == '*')) {
684
- styles[key] = el.styles[key]
685
- }
686
- }
687
- el.styles = styles
688
- }
689
- })
690
- },
691
- //编辑器部分删除情景(在编辑器起始处)
692
- handleDeleteInStart(element) {
693
- if (element.isBlock()) {
694
- elementToParagraph(element)
695
- }
696
- },
697
- //编辑器删除完成后事件
698
- handleDeleteComplete() {
699
- const uneditable = this.editor.range.anchor.element.getUneditableElement()
700
- if (uneditable) {
701
- uneditable.toEmpty()
702
- }
703
- },
704
- //编辑器dom渲染
705
- handleAfterRender() {
706
- //设定视频高度
707
- this.setVideoHeight()
708
-
709
- this.$emit('updateview')
710
- },
711
-
712
- //api:光标设置到文档底部
713
- collapseToEnd() {
714
- if (this.disabled) {
715
- return
716
- }
717
- this.editor.collapseToEnd()
718
- this.editor.rangeRender()
719
- DapElement.setScrollTop({
720
- el: this.$refs.content,
721
- number: 1000000,
722
- time: 0
723
- })
724
- },
725
- //api:光标设置到文档头部
726
- collapseToStart() {
727
- if (this.disabled) {
728
- return
729
- }
730
- this.editor.collapseToStart()
731
- this.editor.rangeRender()
732
- this.$nextTick(() => {
733
- DapElement.setScrollTop({
734
- el: this.$refs.content,
735
- number: 0,
736
- time: 0
737
- })
738
- })
739
- },
740
- //api:撤销
741
- undo() {
742
- if (this.disabled) {
743
- return
744
- }
745
- const historyRecord = this.editor.history.get(-1)
746
- if (historyRecord) {
747
- this.editor.history.current = historyRecord.current
748
- this.editor.stack = historyRecord.stack
749
- this.editor.range = historyRecord.range
750
- this.editor.formatElementStack()
751
- this.editor.domRender(true)
752
- this.editor.rangeRender()
753
- }
754
- },
755
- //api:重做
756
- redo() {
757
- if (this.disabled) {
758
- return
759
- }
760
- const historyRecord = this.editor.history.get(1)
761
- if (historyRecord) {
762
- this.editor.history.current = historyRecord.current
763
- this.editor.stack = historyRecord.stack
764
- this.editor.range = historyRecord.range
765
- this.editor.formatElementStack()
766
- this.editor.domRender(true)
767
- this.editor.rangeRender()
768
- }
769
- }
770
- },
771
- beforeUnmount() {
772
- //卸载绑定在滚动元素上的事件
773
- this.removeScrollHandle()
774
- //卸载绑定在document.documentElement上的事件
775
- DapEvent.off(document.documentElement, `mousedown.editify_${this.uid} mousemove.editify_${this.uid} mouseup.editify_${this.uid} click.editify_${this.uid}`)
776
- //卸载绑定在window上的事件
777
- DapEvent.off(window, `resize.editify_${this.uid}`)
778
- //销毁编辑器
779
- this.editor.destroy()
780
- }
781
- }
782
- </script>
783
- <style lang="less" scoped>
784
- .editify {
785
- display: flex;
786
- justify-content: flex-start;
787
- flex-direction: column;
788
- width: 100%;
789
- height: 100%;
790
- position: relative;
791
- box-sizing: border-box;
792
- -webkit-tap-highlight-color: transparent;
793
- outline: none;
794
- font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, Roboto, 'Segoe UI', 'Microsoft YaHei', Arial, sans-serif;
795
- line-height: 1.5;
796
-
797
- *,
798
- *::before,
799
- *::after {
800
- box-sizing: border-box;
801
- -webkit-tap-highlight-color: transparent;
802
- outline: none;
803
- }
804
- &.fullscreen {
805
- position: fixed;
806
- z-index: 1000;
807
- left: 0;
808
- top: 0;
809
- width: 100vw !important;
810
- height: 100vh !important;
811
- background: @background;
812
-
813
- .editify-body {
814
- border-radius: 0;
815
- }
816
- }
817
- }
818
-
819
- .editify-body {
820
- display: block;
821
- width: 100%;
822
- height: 0;
823
- flex: 1;
824
- position: relative;
825
- background-color: @background;
826
- padding: 1px;
827
- border-radius: 4px;
828
-
829
- &.border {
830
- border: 1px solid @border-color;
831
- transition: all 500ms;
832
-
833
- &.menu_inner {
834
- border-top: none;
835
- border-radius: 0 0 4px 4px;
836
- }
837
- }
838
-
839
- &.menu_inner {
840
- padding-top: 21px;
841
-
842
- .editify-source {
843
- top: 21px;
844
- height: calc(100% - 21px);
845
- }
846
- }
847
-
848
- //编辑器样式
849
- .editify-content {
850
- display: block;
851
- position: relative;
852
- overflow-x: hidden;
853
- overflow-y: auto;
854
- width: 100%;
855
- height: 100%;
856
- border-radius: inherit;
857
- padding: 6px 10px;
858
- line-height: 1.5;
859
- color: @font-color-dark;
860
- font-size: @font-size;
861
- position: relative;
862
- line-height: 1.5;
863
-
864
- //显示占位符
865
- &.placeholder::before {
866
- position: absolute;
867
- top: 0;
868
- left: 0;
869
- display: block;
870
- width: 100%;
871
- content: attr(data-editify-placeholder);
872
- font-size: inherit;
873
- font-family: inherit;
874
- color: @font-color-disabled;
875
- line-height: inherit;
876
- padding: 6px 10px;
877
- cursor: text;
878
- touch-action: none;
879
- user-select: none;
880
- }
881
-
882
- //段落样式和标题
883
- :deep(p),
884
- :deep(h1),
885
- :deep(h2),
886
- :deep(h3),
887
- :deep(h4),
888
- :deep(h5),
889
- :deep(h6) {
890
- display: block;
891
- width: 100%;
892
- margin: 0 0 15px 0;
893
- padding: 0;
894
- }
895
- :deep(h1) {
896
- font-size: 32px;
897
- }
898
- :deep(h2) {
899
- font-size: 28px;
900
- }
901
- :deep(h3) {
902
- font-size: 24px;
903
- }
904
- :deep(h4) {
905
- font-size: 20px;
906
- }
907
- :deep(h5) {
908
- font-size: 18px;
909
- }
910
- :deep(h6) {
911
- font-size: 16px;
912
- }
913
- //有序列表样式
914
- :deep(div[data-editify-list='ol']) {
915
- margin-bottom: 15px;
916
-
917
- &::before {
918
- content: attr(data-editify-value) '.';
919
- margin-right: 10px;
920
- }
921
- }
922
- //无序列表样式
923
- :deep(div[data-editify-list='ul']) {
924
- margin-bottom: 15px;
925
-
926
- &::before {
927
- content: '\2022';
928
- margin-right: 10px;
929
- }
930
- }
931
- //代码样式
932
- :deep(span[data-editify-code]) {
933
- display: inline-block;
934
- padding: 3px 6px;
935
- margin: 0 4px;
936
- border-radius: 4px;
937
- line-height: 1;
938
- font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
939
- background-color: @pre-background;
940
- color: @font-color;
941
- border: 1px solid @border-color;
942
- text-indent: initial;
943
- font-size: @font-size;
944
- font-weight: normal;
945
- }
946
- //链接样式
947
- :deep(a) {
948
- color: @font-color-link;
949
- transition: all 200ms;
950
- text-decoration: none;
951
- cursor: text;
952
-
953
- &:hover {
954
- color: @font-color-link-dark;
955
- text-decoration: underline;
956
- }
957
- }
958
- //表格样式
959
- :deep(table) {
960
- width: 100%;
961
- border: 1px solid @border-color;
962
- margin: 0;
963
- padding: 0;
964
- border-collapse: collapse;
965
- margin-bottom: 15px;
966
- background-color: @background;
967
- color: @font-color-dark;
968
- font-size: @font-size;
969
-
970
- * {
971
- margin: 0 !important;
972
- }
973
-
974
- tbody {
975
- margin: 0;
976
- padding: 0;
977
-
978
- tr {
979
- margin: 0;
980
- padding: 0;
981
-
982
- &:first-child {
983
- background-color: @background-darker;
984
-
985
- td {
986
- font-weight: bold;
987
- position: relative;
988
- }
989
- }
990
-
991
- td {
992
- margin: 0;
993
- border: 1px solid @border-color;
994
- padding: 6px 10px;
995
- position: relative;
996
- word-break: break-word;
997
-
998
- &:not(:last-child)::after {
999
- position: absolute;
1000
- right: -5px;
1001
- top: 0;
1002
- width: 10px;
1003
- height: 100%;
1004
- content: '';
1005
- z-index: 1;
1006
- cursor: col-resize;
1007
- user-select: none;
1008
- }
1009
- }
1010
- }
1011
- }
1012
- }
1013
- //代码块样式
1014
- :deep(pre) {
1015
- display: block;
1016
- padding: 6px 10px;
1017
- margin: 0 0 15px;
1018
- font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
1019
- line-height: 1.5;
1020
- font-size: @font-size;
1021
- color: @font-color-dark;
1022
- background-color: @pre-background;
1023
- border: 1px solid @border-color;
1024
- border-radius: 4px;
1025
- overflow: auto;
1026
- position: relative;
1027
- }
1028
- //图片样式
1029
- :deep(img) {
1030
- position: relative;
1031
- display: inline-block;
1032
- width: 30%;
1033
- height: auto;
1034
- border-radius: 2px;
1035
- vertical-align: text-bottom;
1036
- margin: 0 2px;
1037
- max-width: 100%;
1038
- }
1039
- //视频样式
1040
- :deep(video) {
1041
- position: relative;
1042
- display: inline-block;
1043
- width: 30%;
1044
- border-radius: 2px;
1045
- vertical-align: text-bottom;
1046
- background-color: #000;
1047
- object-fit: contain;
1048
- margin: 0 2px;
1049
- max-width: 100%;
1050
- }
1051
- //引用样式
1052
- :deep(blockquote) {
1053
- display: block;
1054
- border-left: 8px solid @background-darker;
1055
- padding: 6px 10px 6px 20px;
1056
- margin: 0 0 15px;
1057
- line-height: 1.5;
1058
- font-size: @font-size;
1059
- color: @font-color-light;
1060
- border-radius: 0;
1061
- }
1062
- //任务列表样式
1063
- :deep(div[data-editify-task]) {
1064
- margin-bottom: 15px;
1065
- position: relative;
1066
- padding-left: 26px;
1067
- font-size: @font-size;
1068
- color: @font-color-dark;
1069
- transition: all 200ms;
1070
-
1071
- &::before {
1072
- display: block;
1073
- width: 16px;
1074
- height: 16px;
1075
- border-radius: 2px;
1076
- border: 2px solid @font-color-light;
1077
- transition: all 200ms;
1078
- box-sizing: border-box;
1079
- user-select: none;
1080
- content: '';
1081
- position: absolute;
1082
- left: 0;
1083
- top: 2px;
1084
- z-index: 1;
1085
- cursor: pointer;
1086
- }
1087
-
1088
- &::after {
1089
- position: absolute;
1090
- content: '';
1091
- left: 3px;
1092
- top: 6px;
1093
- display: inline-block;
1094
- width: 10px;
1095
- height: 6px;
1096
- border: 2px solid @font-color-light;
1097
- border-top: none;
1098
- border-right: none;
1099
- transform: rotate(-45deg);
1100
- transform-origin: center;
1101
- margin-bottom: 2px;
1102
- box-sizing: border-box;
1103
- z-index: 2;
1104
- cursor: pointer;
1105
- opacity: 0;
1106
- transition: all 200ms;
1107
- }
1108
-
1109
- &[data-editify-task='checked'] {
1110
- text-decoration: line-through;
1111
- color: @font-color-light;
1112
- &::after {
1113
- opacity: 1;
1114
- }
1115
- }
1116
- }
1117
-
1118
- //禁用样式
1119
- &.disabled {
1120
- cursor: auto !important;
1121
- &.placeholder::before {
1122
- cursor: auto;
1123
- }
1124
- :deep(a) {
1125
- cursor: pointer;
1126
- }
1127
-
1128
- :deep(table) {
1129
- td:not(:last-child)::after {
1130
- cursor: auto;
1131
- }
1132
- }
1133
- }
1134
- }
1135
-
1136
- //代码视图
1137
- .editify-source {
1138
- display: block;
1139
- width: 100%;
1140
- height: 100%;
1141
- position: absolute;
1142
- left: 0;
1143
- top: 0;
1144
- background-color: @reverse-background;
1145
- margin: 0;
1146
- padding: 6px 10px;
1147
- overflow-x: hidden;
1148
- overflow-y: auto;
1149
- font-size: @font-size;
1150
- color: @reverse-color;
1151
- font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
1152
- resize: none;
1153
- border: none;
1154
- border-radius: inherit;
1155
- z-index: 1;
1156
- }
1157
- }
1158
-
1159
- .editify-footer {
1160
- display: flex;
1161
- justify-content: end;
1162
- align-items: center;
1163
- width: 100%;
1164
- padding: 10px;
1165
- position: relative;
1166
-
1167
- .editify-footer-words {
1168
- font-size: @font-size;
1169
- color: @font-color-light;
1170
- line-height: 1;
1171
- }
1172
-
1173
- //全屏模式下并且不是代码视图下,显示一个上边框
1174
- &.fullscreen {
1175
- border-top: 1px solid @border-color;
1176
- }
1177
- }
1178
- </style>
1
+ <template>
2
+ <div class="editify" :class="{ fullscreen: isFullScreen, autoheight: autoheight }">
3
+ <!-- 菜单区域 -->
4
+ <Menu v-if="menuConfig.use" :config="menuConfig" :color="color" ref="menu"></Menu>
5
+ <!-- 编辑层,与编辑区域宽高相同必须适配 -->
6
+ <div ref="body" class="editify-body" :class="{ border: showBorder, menu_inner: menuConfig.use && menuConfig.mode == 'inner' }" :data-editify-uid="uid">
7
+ <!-- 编辑器 -->
8
+ <div ref="content" class="editify-content" :class="{ placeholder: showPlaceholder, disabled: disabled }" @keydown="handleEditorKeydown" @click="handleEditorClick" @compositionstart="isInputChinese = true" @compositionend="isInputChinese = false" :data-editify-placeholder="placeholder"></div>
9
+ <!-- 代码视图 -->
10
+ <textarea v-if="isSourceView" :value="value" readonly class="editify-source" />
11
+ <!-- 工具条 -->
12
+ <Toolbar ref="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" :class="{ fullscreen: isFullScreen && !isSourceView }" ref="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 { element as DapElement, event as DapEvent, data as DapData, number as DapNumber, color as DapColor } from 'dap-util'
25
+ import { pasteKeepData, editorProps, mergeObject, getToolbarConfig, getMenuConfig } from './core/tool'
26
+ import { parseList, orderdListHandle, mediaHandle, tableHandle, preHandle, specialInblockHandle } from './core/rule'
27
+ import { isTask, elementToParagraph, getCurrentParsedomElement, hasTableInRange, hasLinkInRange, hasPreInRange, hasImageInRange, hasVideoInRange, setIndentIncrease, setIndentDecrease, insertImage, insertVideo } from './core/function'
28
+ import Tooltip from './components/base/Tooltip'
29
+ import Toolbar from './components/Toolbar'
30
+ import Menu from './components/Menu'
31
+
32
+ export default {
33
+ name: 'editify',
34
+ props: { ...editorProps },
35
+ emits: ['update:modelValue', 'focus', 'blur', 'change', 'keydown', 'insertparagraph', 'rangeupdate', 'updateview'],
36
+ setup() {
37
+ const instance = getCurrentInstance()
38
+ return {
39
+ uid: instance.uid
40
+ }
41
+ },
42
+ data() {
43
+ return {
44
+ //是否编辑器内部修改值
45
+ isModelChange: false,
46
+ //是否正在输入中文
47
+ isInputChinese: false,
48
+ //工具条和菜单栏判定延时器
49
+ rangeUpdateTimer: null,
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
+
65
+ /** 以下是可对外使用的属性 */
66
+
67
+ //编辑器对象
68
+ editor: null,
69
+ //是否代码视图
70
+ isSourceView: false,
71
+ //是否全屏
72
+ isFullScreen: false,
73
+ //菜单栏是否可以使用标识
74
+ canUseMenu: false,
75
+ //光标选取范围内的元素数组
76
+ dataRangeCaches: {
77
+ flatList: [],
78
+ list: []
79
+ }
80
+ }
81
+ },
82
+ computed: {
83
+ //编辑器的值
84
+ value: {
85
+ set(val) {
86
+ this.$emit('update:modelValue', val)
87
+ },
88
+ get() {
89
+ return this.modelValue || '<p><br></p>'
90
+ }
91
+ },
92
+ //编辑器的纯文本值
93
+ textValue() {
94
+ return DapElement.string2dom(`<div>${this.value}</div>`).innerText
95
+ },
96
+ //是否显示占位符
97
+ showPlaceholder() {
98
+ if (this.editor) {
99
+ const elements = this.editor.parseHtml(this.value)
100
+ if (elements.length == 1 && elements[0].type == 'block' && elements[0].parsedom == AlexElement.BLOCK_NODE && elements[0].isOnlyHasBreak()) {
101
+ return !this.isInputChinese
102
+ }
103
+ }
104
+ return false
105
+ },
106
+ //是否显示边框
107
+ showBorder() {
108
+ //全屏模式下不显示边框
109
+ if (this.isFullScreen) {
110
+ return false
111
+ }
112
+ return this.border
113
+ },
114
+ //最终生效的工具栏配置
115
+ toolbarConfig() {
116
+ return mergeObject(getToolbarConfig(this.$editTrans, this.$editLocale), this.toolbar || {})
117
+ },
118
+ //最终生效的菜单栏配置
119
+ menuConfig() {
120
+ return mergeObject(getMenuConfig(this.$editTrans, this.$editLocale), this.menu || {})
121
+ }
122
+ },
123
+ components: {
124
+ Toolbar,
125
+ Tooltip,
126
+ Menu
127
+ },
128
+ inject: ['$editTrans', '$editLocale'],
129
+ watch: {
130
+ //监听编辑的值变更
131
+ value(newVal) {
132
+ //内部修改不处理
133
+ if (this.isModelChange) {
134
+ return
135
+ }
136
+ //如果是外部修改,需要重新渲染编辑器
137
+ this.editor.stack = this.editor.parseHtml(newVal)
138
+ this.editor.range = null
139
+ this.editor.formatElementStack()
140
+ this.editor.domRender()
141
+ this.editor.rangeRender()
142
+ this.$refs.content.blur()
143
+ },
144
+ //代码视图切换
145
+ isSourceView(newValue) {
146
+ if (this.toolbarConfig.use) {
147
+ if (newValue) {
148
+ this.hideToolbar()
149
+ } else {
150
+ this.handleToolbar()
151
+ }
152
+ }
153
+ },
154
+ //监听disabled
155
+ disabled(newValue) {
156
+ if (newValue) {
157
+ this.editor.setDisabled()
158
+ } else {
159
+ this.editor.setEnabled()
160
+ }
161
+ }
162
+ },
163
+ mounted() {
164
+ //创建编辑器
165
+ this.createEditor()
166
+ //监听滚动隐藏工具条
167
+ this.handleScroll()
168
+ //鼠标按下监听
169
+ DapEvent.on(document.documentElement, `mousedown.editify_${this.uid}`, this.documentMouseDown)
170
+ //鼠标移动监听
171
+ DapEvent.on(document.documentElement, `mousemove.editify_${this.uid}`, this.documentMouseMove)
172
+ //鼠标松开监听
173
+ DapEvent.on(document.documentElement, `mouseup.editify_${this.uid}`, this.documentMouseUp)
174
+ //鼠标点击箭头
175
+ DapEvent.on(document.documentElement, `click.editify_${this.uid}`, this.documentClick)
176
+ //监听窗口改变
177
+ DapEvent.on(window, `resize.editify_${this.uid}`, this.setVideoHeight)
178
+ },
179
+ methods: {
180
+ //编辑器内部修改值的方法
181
+ internalModify(val) {
182
+ this.isModelChange = true
183
+ this.value = val
184
+ this.$nextTick(() => {
185
+ this.isModelChange = false
186
+ })
187
+ },
188
+ //隐藏工具条
189
+ hideToolbar() {
190
+ this.toolbarOptions.show = false
191
+ this.toolbarOptions.node = null
192
+ },
193
+ //监听滚动隐藏工具条
194
+ handleScroll() {
195
+ const setScroll = el => {
196
+ DapEvent.on(el, `scroll.editify_${this.uid}`, () => {
197
+ if (this.toolbarConfig.use && this.toolbarOptions.show) {
198
+ this.hideToolbar()
199
+ }
200
+ })
201
+ if (el.parentNode) {
202
+ setScroll(el.parentNode)
203
+ }
204
+ }
205
+ setScroll(this.$refs.content)
206
+ },
207
+ //移除上述滚动事件的监听
208
+ removeScrollHandle() {
209
+ const removeScroll = el => {
210
+ DapEvent.off(el, `scroll.editify_${this.uid}`)
211
+ if (el.parentNode) {
212
+ removeScroll(el.parentNode)
213
+ }
214
+ }
215
+ removeScroll(this.$refs.content)
216
+ },
217
+ //工具条显示判断
218
+ handleToolbar() {
219
+ if (this.disabled || this.isSourceView) {
220
+ return
221
+ }
222
+ this.hideToolbar()
223
+ this.$nextTick(() => {
224
+ const table = getCurrentParsedomElement(this, 'table')
225
+ const pre = getCurrentParsedomElement(this, 'pre')
226
+ const link = getCurrentParsedomElement(this, 'a')
227
+ const image = getCurrentParsedomElement(this, 'img')
228
+ const video = getCurrentParsedomElement(this, 'video')
229
+ if (link) {
230
+ this.toolbarOptions.type = 'link'
231
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${link.key}"]`
232
+ if (this.toolbarOptions.show) {
233
+ this.$refs.toolbar.$refs.layer.setPosition()
234
+ } else {
235
+ this.toolbarOptions.show = true
236
+ }
237
+ } else if (image) {
238
+ this.toolbarOptions.type = 'image'
239
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${image.key}"]`
240
+ if (this.toolbarOptions.show) {
241
+ this.$refs.toolbar.$refs.layer.setPosition()
242
+ } else {
243
+ this.toolbarOptions.show = true
244
+ }
245
+ } else if (video) {
246
+ this.toolbarOptions.type = 'video'
247
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${video.key}"]`
248
+ if (this.toolbarOptions.show) {
249
+ this.$refs.toolbar.$refs.layer.setPosition()
250
+ } else {
251
+ this.toolbarOptions.show = true
252
+ }
253
+ } else if (table) {
254
+ this.toolbarOptions.type = 'table'
255
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${table.key}"]`
256
+ if (this.toolbarOptions.show) {
257
+ this.$refs.toolbar.$refs.layer.setPosition()
258
+ } else {
259
+ this.toolbarOptions.show = true
260
+ }
261
+ } else if (pre) {
262
+ this.toolbarOptions.type = 'codeBlock'
263
+ this.toolbarOptions.node = `[data-editify-uid="${this.uid}"] [data-editify-element="${pre.key}"]`
264
+ if (this.toolbarOptions.show) {
265
+ this.$refs.toolbar.$refs.layer.setPosition()
266
+ } else {
267
+ this.toolbarOptions.show = true
268
+ }
269
+ } else {
270
+ const result = this.dataRangeCaches.flatList.filter(item => {
271
+ return item.element.isText()
272
+ })
273
+ if (result.length && !hasTableInRange(this) && !hasPreInRange(this) && !hasLinkInRange(this) && !hasImageInRange(this) && !hasVideoInRange(this)) {
274
+ this.toolbarOptions.type = 'text'
275
+ if (this.toolbarOptions.show) {
276
+ this.$refs.toolbar.$refs.layer.setPosition()
277
+ } else {
278
+ this.toolbarOptions.show = true
279
+ }
280
+ }
281
+ }
282
+ })
283
+ },
284
+ //设定编辑器内的视频高度
285
+ setVideoHeight() {
286
+ this.$refs.content.querySelectorAll('video').forEach(video => {
287
+ video.style.height = video.offsetWidth / this.videoRatio + 'px'
288
+ })
289
+ },
290
+ //初始创建编辑器
291
+ createEditor() {
292
+ //创建编辑器
293
+ this.editor = new AlexEditor(this.$refs.content, {
294
+ value: this.value,
295
+ disabled: this.disabled,
296
+ renderRules: [
297
+ el => {
298
+ parseList(this.editor, el)
299
+ },
300
+ el => {
301
+ orderdListHandle(this.editor, el)
302
+ },
303
+ el => {
304
+ mediaHandle(this.editor, el)
305
+ },
306
+ el => {
307
+ tableHandle(this.editor, el)
308
+ },
309
+ el => {
310
+ preHandle(this.editor, el, this.toolbarConfig?.use && this.toolbarConfig?.codeBlock?.languages?.show, this.toolbarConfig?.codeBlock?.languages.options)
311
+ },
312
+ el => {
313
+ specialInblockHandle(this.editor, el)
314
+ },
315
+ ...this.renderRules
316
+ ],
317
+ allowCopy: this.allowCopy,
318
+ allowPaste: this.allowPaste,
319
+ allowCut: this.allowCut,
320
+ allowPasteHtml: this.allowPasteHtml,
321
+ allowPasteHtml: this.allowPasteHtml,
322
+ customImagePaste: this.handleCustomImagePaste,
323
+ customVideoPaste: this.handleCustomVideoPaste,
324
+ customMerge: this.handleCustomMerge,
325
+ customParseNode: this.handleCustomParseNode
326
+ })
327
+ //编辑器渲染后会有一个渲染过程,会改变内容,因此重新获取内容的值来设置value
328
+ this.internalModify(this.editor.value)
329
+ //设置监听事件
330
+ this.editor.on('change', this.handleEditorChange)
331
+ this.editor.on('focus', this.handleEditorFocus)
332
+ this.editor.on('blur', this.handleEditorBlur)
333
+ this.editor.on('insertParagraph', this.handleInsertParagraph)
334
+ this.editor.on('rangeUpdate', this.handleRangeUpdate)
335
+ this.editor.on('pasteHtml', this.handlePasteHtml)
336
+ this.editor.on('deleteInStart', this.handleDeleteInStart)
337
+ this.editor.on('deleteComplete', this.handleDeleteComplete)
338
+ this.editor.on('afterRender', this.handleAfterRender)
339
+ //格式化和dom渲染
340
+ this.editor.formatElementStack()
341
+ this.editor.domRender()
342
+ //自动获取焦点
343
+ if (this.autofocus && !this.isSourceView && !this.disabled) {
344
+ this.collapseToEnd()
345
+ }
346
+ },
347
+ //鼠标在页面按下:处理表格拖拽改变列宽和菜单栏是否使用判断
348
+ documentMouseDown(e) {
349
+ if (this.disabled) {
350
+ return
351
+ }
352
+ //鼠标在编辑器内按下
353
+ if (DapElement.isContains(this.$refs.content, e.target)) {
354
+ const elm = e.target
355
+ const key = DapData.get(elm, 'data-alex-editor-key')
356
+ if (key) {
357
+ const element = this.editor.getElementByKey(key)
358
+ if (element && element.parsedom == 'td') {
359
+ const length = element.parent.children.length
360
+ //最后一个td不设置
361
+ if (element.parent.children[length - 1].isEqual(element)) {
362
+ return
363
+ }
364
+ const rect = DapElement.getElementBounding(elm)
365
+ //在可拖拽范围内
366
+ if (e.pageX >= Math.abs(rect.left + elm.offsetWidth - 5) && e.pageX <= Math.abs(rect.left + elm.offsetWidth + 5)) {
367
+ this.tableColumnResizeParams.element = element
368
+ this.tableColumnResizeParams.start = e.pageX
369
+ }
370
+ }
371
+ }
372
+ }
373
+ //如果点击了除编辑器外的地方,菜单栏不可使用
374
+ if (!DapElement.isContains(this.$el, e.target) && !this.isSourceView) {
375
+ this.canUseMenu = false
376
+ }
377
+ },
378
+ //鼠标在页面移动:处理表格拖拽改变列宽
379
+ documentMouseMove(e) {
380
+ if (this.disabled) {
381
+ return
382
+ }
383
+ if (!this.tableColumnResizeParams.element) {
384
+ return
385
+ }
386
+ const table = getCurrentParsedomElement(this, 'table')
387
+ if (!table) {
388
+ return
389
+ }
390
+ const colgroup = table.children.find(item => {
391
+ return item.parsedom == 'colgroup'
392
+ })
393
+ const index = this.tableColumnResizeParams.element.parent.children.findIndex(el => {
394
+ return el.isEqual(this.tableColumnResizeParams.element)
395
+ })
396
+ const width = `${this.tableColumnResizeParams.element.elm.offsetWidth + e.pageX - this.tableColumnResizeParams.start}`
397
+ colgroup.children[index].marks['width'] = width
398
+ colgroup.children[index].elm.setAttribute('width', width)
399
+ this.tableColumnResizeParams.start = e.pageX
400
+ },
401
+ //鼠标在页面松开:处理表格拖拽改变列宽
402
+ documentMouseUp() {
403
+ if (this.disabled) {
404
+ return
405
+ }
406
+ if (!this.tableColumnResizeParams.element) {
407
+ return
408
+ }
409
+ const table = getCurrentParsedomElement(this, 'table')
410
+ if (!table) {
411
+ return
412
+ }
413
+ const colgroup = table.children.find(item => {
414
+ return item.parsedom == 'colgroup'
415
+ })
416
+ const index = this.tableColumnResizeParams.element.parent.children.findIndex(el => {
417
+ return el.isEqual(this.tableColumnResizeParams.element)
418
+ })
419
+ const width = Number(colgroup.children[index].marks['width'])
420
+ if (!isNaN(width)) {
421
+ colgroup.children[index].marks['width'] = `${Number(((width / this.tableColumnResizeParams.element.parent.elm.offsetWidth) * 100).toFixed(2))}%`
422
+ this.editor.formatElementStack()
423
+ this.editor.domRender()
424
+ this.editor.rangeRender()
425
+ }
426
+ this.tableColumnResizeParams.element = null
427
+ this.tableColumnResizeParams.start = 0
428
+ },
429
+ //鼠标点击页面:处理任务列表复选框勾选
430
+ documentClick(e) {
431
+ if (this.disabled) {
432
+ return
433
+ }
434
+ //鼠标在编辑器内点击
435
+ if (DapElement.isContains(this.$refs.content, e.target)) {
436
+ const elm = e.target
437
+ const key = DapData.get(elm, 'data-alex-editor-key')
438
+ if (key) {
439
+ const element = this.editor.getElementByKey(key)
440
+ //如果是任务列表元素
441
+ if (isTask(element)) {
442
+ const rect = DapElement.getElementBounding(elm)
443
+ //在复选框范围内
444
+ if (e.pageX >= Math.abs(rect.left) && e.pageX <= Math.abs(rect.left + 16) && e.pageY >= Math.abs(rect.top + 2) && e.pageY <= Math.abs(rect.top + 18)) {
445
+ //取消勾选
446
+ if (element.marks['data-editify-task'] == 'checked') {
447
+ element.marks['data-editify-task'] = 'uncheck'
448
+ }
449
+ //勾选
450
+ else {
451
+ element.marks['data-editify-task'] = 'checked'
452
+ }
453
+ if (!this.editor.range) {
454
+ this.editor.initRange()
455
+ }
456
+ this.editor.range.anchor.moveToEnd(element)
457
+ this.editor.range.focus.moveToEnd(element)
458
+ this.editor.formatElementStack()
459
+ this.editor.domRender()
460
+ this.editor.rangeRender()
461
+ }
462
+ }
463
+ }
464
+ }
465
+ },
466
+ //自定义图片粘贴
467
+ async handleCustomImagePaste(url) {
468
+ const newUrl = await this.customImagePaste.apply(this, [url])
469
+ if (newUrl) {
470
+ insertImage(this, newUrl)
471
+ }
472
+ },
473
+ //自定义视频粘贴
474
+ async handleCustomVideoPaste(url) {
475
+ const newUrl = await this.customVideoPaste.apply(this, [url])
476
+ if (newUrl) {
477
+ insertVideo(this, newUrl)
478
+ }
479
+ },
480
+ //重新定义编辑器合并元素的逻辑
481
+ handleCustomMerge(ele, preEle) {
482
+ const uneditable = preEle.getUneditableElement()
483
+ if (uneditable) {
484
+ uneditable.toEmpty()
485
+ } else {
486
+ preEle.children.push(...ele.children)
487
+ preEle.children.forEach(item => {
488
+ item.parent = preEle
489
+ })
490
+ ele.children = null
491
+ }
492
+ },
493
+ //针对node转为元素进行额外的处理
494
+ handleCustomParseNode(ele) {
495
+ if (ele.parsedom == 'code') {
496
+ ele.parsedom = 'span'
497
+ const marks = {
498
+ 'data-editify-code': true
499
+ }
500
+ if (ele.hasMarks()) {
501
+ Object.assign(ele.marks, marks)
502
+ } else {
503
+ ele.marks = marks
504
+ }
505
+ }
506
+ if (typeof this.customParseNode == 'function') {
507
+ ele = this.customParseNode.apply(this, [ele])
508
+ }
509
+ return ele
510
+ },
511
+ //编辑区域键盘按下:设置缩进快捷键
512
+ handleEditorKeydown(e) {
513
+ if (this.disabled) {
514
+ return
515
+ }
516
+ //增加缩进
517
+ if (e.keyCode == 9 && !e.metaKey && !e.shiftKey && !e.ctrlKey && !e.altKey) {
518
+ e.preventDefault()
519
+ if (!hasTableInRange(this)) {
520
+ setIndentIncrease(this)
521
+ this.editor.formatElementStack()
522
+ this.editor.domRender()
523
+ this.editor.rangeRender()
524
+ }
525
+ }
526
+ //减少缩进
527
+ else if (e.keyCode == 9 && !e.metaKey && e.shiftKey && !e.ctrlKey && !e.altKey) {
528
+ e.preventDefault()
529
+ if (!hasTableInRange(this)) {
530
+ setIndentDecrease(this)
531
+ this.editor.formatElementStack()
532
+ this.editor.domRender()
533
+ this.editor.rangeRender()
534
+ }
535
+ }
536
+ //自定义键盘按下操作
537
+ this.$emit('keydown', e)
538
+ },
539
+ //点击编辑器:处理图片和视频的光标聚集
540
+ handleEditorClick(e) {
541
+ if (this.disabled || this.isSourceView) {
542
+ return
543
+ }
544
+ const node = e.target
545
+ //点击的是图片或者视频
546
+ if (node.nodeName.toLocaleLowerCase() == 'img' || node.nodeName.toLocaleLowerCase() == 'video') {
547
+ const key = Number(node.getAttribute('data-editify-element'))
548
+ if (DapNumber.isNumber(key)) {
549
+ const element = this.editor.getElementByKey(key)
550
+ if (!this.editor.range) {
551
+ this.editor.initRange()
552
+ }
553
+ this.editor.range.anchor.moveToStart(element)
554
+ this.editor.range.focus.moveToEnd(element)
555
+ this.editor.rangeRender()
556
+ }
557
+ }
558
+ },
559
+ //编辑器的值更新
560
+ handleEditorChange(newVal, oldVal) {
561
+ if (this.disabled) {
562
+ return
563
+ }
564
+ //内部修改
565
+ this.internalModify(newVal)
566
+ //触发change事件
567
+ this.$emit('change', newVal, oldVal)
568
+ },
569
+ //编辑器失去焦点
570
+ handleEditorBlur(val) {
571
+ if (this.disabled) {
572
+ return
573
+ }
574
+ if (this.border && this.color && !this.isFullScreen) {
575
+ //恢复编辑区域边框颜色
576
+ this.$refs.body.style.borderColor = ''
577
+ //恢复编辑区域阴影颜色
578
+ this.$refs.body.style.boxShadow = ''
579
+ //使用菜单栏的情况下恢复菜单栏的样式
580
+ if (this.menuConfig.use) {
581
+ this.$refs.menu.$el.style.borderColor = ''
582
+ this.$refs.menu.$el.style.boxShadow = ''
583
+ }
584
+ }
585
+ this.$emit('blur', val)
586
+ },
587
+ //编辑器获取焦点
588
+ handleEditorFocus(val) {
589
+ if (this.disabled) {
590
+ return
591
+ }
592
+ if (this.border && this.color && !this.isFullScreen) {
593
+ //编辑区域边框颜色
594
+ this.$refs.body.style.borderColor = this.color
595
+ //转换颜色值
596
+ const rgb = DapColor.hex2rgb(this.color)
597
+ //菜单栏模式为inner
598
+ if (this.menuConfig.use && this.menuConfig.mode == 'inner') {
599
+ //编辑区域除顶部边框的阴影
600
+ 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)`
601
+ //菜单栏的边框颜色
602
+ this.$refs.menu.$el.style.borderColor = this.color
603
+ //菜单栏除底部边框的阴影
604
+ 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)`
605
+ }
606
+ //其他菜单栏模式
607
+ else if (this.menuConfig.use) {
608
+ //编辑区域四边阴影
609
+ this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
610
+ }
611
+ //不使用菜单栏
612
+ else {
613
+ //编辑区域四边阴影
614
+ this.$refs.body.style.boxShadow = `0 0 8px rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`
615
+ }
616
+ }
617
+ //获取焦点时可以使用菜单栏
618
+ setTimeout(() => {
619
+ this.canUseMenu = true
620
+ this.$emit('focus', val)
621
+ }, 0)
622
+ },
623
+ //编辑器换行
624
+ handleInsertParagraph(element, previousElement) {
625
+ //前一个块元素如果是只包含换行符的元素,并且当前块元素也是包含换行符的元素,则当前块元素转为段落
626
+ if (previousElement.isOnlyHasBreak() && element.isOnlyHasBreak()) {
627
+ if (previousElement.parsedom != AlexElement.BLOCK_NODE) {
628
+ elementToParagraph(previousElement)
629
+ this.editor.range.anchor.moveToStart(previousElement)
630
+ this.editor.range.focus.moveToStart(previousElement)
631
+ element.toEmpty()
632
+ }
633
+ }
634
+ this.$emit('insertparagraph', this.value)
635
+ },
636
+ //编辑器焦点更新
637
+ handleRangeUpdate() {
638
+ if (this.disabled) {
639
+ return
640
+ }
641
+
642
+ //如果没有range禁用菜单栏
643
+ this.canUseMenu = !!this.editor.range
644
+
645
+ //没有range直接返回
646
+ if (!this.editor.range) {
647
+ return
648
+ }
649
+
650
+ //获取光标选取范围内的元素数据,并且进行缓存
651
+ this.dataRangeCaches = this.editor.getElementsByRange()
652
+
653
+ //节流写法
654
+ if (this.rangeUpdateTimer) {
655
+ clearTimeout(this.rangeUpdateTimer)
656
+ this.rangeUpdateTimer = null
657
+ }
658
+ //延时200ms进行判断
659
+ this.rangeUpdateTimer = setTimeout(() => {
660
+ //如果使用工具条或者菜单栏
661
+ if (this.toolbarConfig.use || this.menuConfig.use) {
662
+ //如果使用工具条
663
+ if (this.toolbarConfig.use) {
664
+ this.handleToolbar()
665
+ }
666
+ //如果使用菜单栏
667
+ if (this.menuConfig.use) {
668
+ this.$refs.menu.handleRangeUpdate()
669
+ }
670
+ }
671
+ }, 200)
672
+ this.$emit('rangeupdate')
673
+ },
674
+ //编辑器粘贴html
675
+ handlePasteHtml(elements) {
676
+ const keepStyles = Object.assign(pasteKeepData.styles, this.pasteKeepStyles || {})
677
+ const keepMarks = Object.assign(pasteKeepData.marks, this.pasteKeepMarks || {})
678
+ //粘贴html时过滤元素的样式和属性
679
+ AlexElement.flatElements(elements).forEach(el => {
680
+ let marks = {}
681
+ let styles = {}
682
+ if (el.hasMarks()) {
683
+ for (let key in keepMarks) {
684
+ if (el.marks.hasOwnProperty(key) && ((Array.isArray(keepMarks[key]) && keepMarks[key].includes(el.parsedom)) || keepMarks[key] == '*')) {
685
+ marks[key] = el.marks[key]
686
+ }
687
+ }
688
+ el.marks = marks
689
+ }
690
+ if (el.hasStyles() && !el.isText()) {
691
+ for (let key in keepStyles) {
692
+ if (el.styles.hasOwnProperty(key) && ((Array.isArray(keepStyles[key]) && keepStyles[key].includes(el.parsedom)) || keepStyles[key] == '*')) {
693
+ styles[key] = el.styles[key]
694
+ }
695
+ }
696
+ el.styles = styles
697
+ }
698
+ })
699
+ },
700
+ //编辑器部分删除情景(在编辑器起始处)
701
+ handleDeleteInStart(element) {
702
+ if (element.isBlock()) {
703
+ elementToParagraph(element)
704
+ }
705
+ },
706
+ //编辑器删除完成后事件
707
+ handleDeleteComplete() {
708
+ const uneditable = this.editor.range.anchor.element.getUneditableElement()
709
+ if (uneditable) {
710
+ uneditable.toEmpty()
711
+ }
712
+ },
713
+ //编辑器dom渲染
714
+ handleAfterRender() {
715
+ //设定视频高度
716
+ this.setVideoHeight()
717
+
718
+ this.$emit('updateview')
719
+ },
720
+
721
+ //api:光标设置到文档底部
722
+ collapseToEnd() {
723
+ if (this.disabled) {
724
+ return
725
+ }
726
+ this.editor.collapseToEnd()
727
+ this.editor.rangeRender()
728
+ DapElement.setScrollTop({
729
+ el: this.$refs.content,
730
+ number: 1000000,
731
+ time: 0
732
+ })
733
+ },
734
+ //api:光标设置到文档头部
735
+ collapseToStart() {
736
+ if (this.disabled) {
737
+ return
738
+ }
739
+ this.editor.collapseToStart()
740
+ this.editor.rangeRender()
741
+ this.$nextTick(() => {
742
+ DapElement.setScrollTop({
743
+ el: this.$refs.content,
744
+ number: 0,
745
+ time: 0
746
+ })
747
+ })
748
+ },
749
+ //api:撤销
750
+ undo() {
751
+ if (this.disabled) {
752
+ return
753
+ }
754
+ const historyRecord = this.editor.history.get(-1)
755
+ if (historyRecord) {
756
+ this.editor.history.current = historyRecord.current
757
+ this.editor.stack = historyRecord.stack
758
+ this.editor.range = historyRecord.range
759
+ this.editor.formatElementStack()
760
+ this.editor.domRender(true)
761
+ this.editor.rangeRender()
762
+ }
763
+ },
764
+ //api:重做
765
+ redo() {
766
+ if (this.disabled) {
767
+ return
768
+ }
769
+ const historyRecord = this.editor.history.get(1)
770
+ if (historyRecord) {
771
+ this.editor.history.current = historyRecord.current
772
+ this.editor.stack = historyRecord.stack
773
+ this.editor.range = historyRecord.range
774
+ this.editor.formatElementStack()
775
+ this.editor.domRender(true)
776
+ this.editor.rangeRender()
777
+ }
778
+ }
779
+ },
780
+ beforeUnmount() {
781
+ //卸载绑定在滚动元素上的事件
782
+ this.removeScrollHandle()
783
+ //卸载绑定在document.documentElement上的事件
784
+ DapEvent.off(document.documentElement, `mousedown.editify_${this.uid} mousemove.editify_${this.uid} mouseup.editify_${this.uid} click.editify_${this.uid}`)
785
+ //卸载绑定在window上的事件
786
+ DapEvent.off(window, `resize.editify_${this.uid}`)
787
+ //销毁编辑器
788
+ this.editor.destroy()
789
+ }
790
+ }
791
+ </script>
792
+ <style lang="less" scoped>
793
+ .editify {
794
+ display: flex;
795
+ justify-content: flex-start;
796
+ flex-direction: column;
797
+ width: 100%;
798
+ height: 100%;
799
+ position: relative;
800
+ box-sizing: border-box;
801
+ -webkit-tap-highlight-color: transparent;
802
+ outline: none;
803
+ font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, Roboto, 'Segoe UI', 'Microsoft YaHei', Arial, sans-serif;
804
+ line-height: 1.5;
805
+
806
+ *,
807
+ *::before,
808
+ *::after {
809
+ box-sizing: border-box;
810
+ -webkit-tap-highlight-color: transparent;
811
+ outline: none;
812
+ }
813
+
814
+ &.fullscreen {
815
+ position: fixed;
816
+ z-index: 1000;
817
+ left: 0;
818
+ top: 0;
819
+ width: 100vw !important;
820
+ height: 100vh !important;
821
+ background: @background;
822
+
823
+ .editify-body {
824
+ border-radius: 0;
825
+ }
826
+ }
827
+
828
+ &.autoheight {
829
+ height: auto;
830
+
831
+ .editify-body {
832
+ height: auto;
833
+ flex: none;
834
+ }
835
+ }
836
+ }
837
+
838
+ .editify-body {
839
+ display: block;
840
+ width: 100%;
841
+ height: 0;
842
+ flex: 1;
843
+ position: relative;
844
+ background-color: @background;
845
+ padding: 1px;
846
+ border-radius: 4px;
847
+
848
+ &.border {
849
+ border: 1px solid @border-color;
850
+ transition: all 500ms;
851
+
852
+ &.menu_inner {
853
+ border-top: none;
854
+ border-radius: 0 0 4px 4px;
855
+ }
856
+ }
857
+
858
+ &.menu_inner {
859
+ padding-top: 21px;
860
+
861
+ .editify-source {
862
+ top: 21px;
863
+ height: calc(100% - 21px);
864
+ }
865
+ }
866
+
867
+ //编辑器样式
868
+ .editify-content {
869
+ display: block;
870
+ position: relative;
871
+ overflow-x: hidden;
872
+ overflow-y: auto;
873
+ width: 100%;
874
+ height: 100%;
875
+ border-radius: inherit;
876
+ padding: 6px 10px;
877
+ line-height: 1.5;
878
+ color: @font-color-dark;
879
+ font-size: @font-size;
880
+ position: relative;
881
+ line-height: 1.5;
882
+
883
+ //显示占位符
884
+ &.placeholder::before {
885
+ position: absolute;
886
+ top: 0;
887
+ left: 0;
888
+ display: block;
889
+ width: 100%;
890
+ content: attr(data-editify-placeholder);
891
+ font-size: inherit;
892
+ font-family: inherit;
893
+ color: @font-color-disabled;
894
+ line-height: inherit;
895
+ padding: 6px 10px;
896
+ cursor: text;
897
+ touch-action: none;
898
+ user-select: none;
899
+ }
900
+
901
+ //段落样式和标题
902
+ :deep(p),
903
+ :deep(h1),
904
+ :deep(h2),
905
+ :deep(h3),
906
+ :deep(h4),
907
+ :deep(h5),
908
+ :deep(h6) {
909
+ display: block;
910
+ width: 100%;
911
+ margin: 0 0 15px 0;
912
+ padding: 0;
913
+ }
914
+ :deep(h1) {
915
+ font-size: 32px;
916
+ }
917
+ :deep(h2) {
918
+ font-size: 28px;
919
+ }
920
+ :deep(h3) {
921
+ font-size: 24px;
922
+ }
923
+ :deep(h4) {
924
+ font-size: 20px;
925
+ }
926
+ :deep(h5) {
927
+ font-size: 18px;
928
+ }
929
+ :deep(h6) {
930
+ font-size: 16px;
931
+ }
932
+ //有序列表样式
933
+ :deep(div[data-editify-list='ol']) {
934
+ margin-bottom: 15px;
935
+
936
+ &::before {
937
+ content: attr(data-editify-value) '.';
938
+ margin-right: 10px;
939
+ }
940
+ }
941
+ //无序列表样式
942
+ :deep(div[data-editify-list='ul']) {
943
+ margin-bottom: 15px;
944
+
945
+ &::before {
946
+ content: '\2022';
947
+ margin-right: 10px;
948
+ }
949
+ }
950
+ //代码样式
951
+ :deep(span[data-editify-code]) {
952
+ display: inline-block;
953
+ padding: 3px 6px;
954
+ margin: 0 4px;
955
+ border-radius: 4px;
956
+ line-height: 1;
957
+ font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
958
+ background-color: @pre-background;
959
+ color: @font-color;
960
+ border: 1px solid @border-color;
961
+ text-indent: initial;
962
+ font-size: @font-size;
963
+ font-weight: normal;
964
+ }
965
+ //链接样式
966
+ :deep(a) {
967
+ color: @font-color-link;
968
+ transition: all 200ms;
969
+ text-decoration: none;
970
+ cursor: text;
971
+
972
+ &:hover {
973
+ color: @font-color-link-dark;
974
+ text-decoration: underline;
975
+ }
976
+ }
977
+ //表格样式
978
+ :deep(table) {
979
+ width: 100%;
980
+ border: 1px solid @border-color;
981
+ margin: 0;
982
+ padding: 0;
983
+ border-collapse: collapse;
984
+ margin-bottom: 15px;
985
+ background-color: @background;
986
+ color: @font-color-dark;
987
+ font-size: @font-size;
988
+
989
+ * {
990
+ margin: 0 !important;
991
+ }
992
+
993
+ tbody {
994
+ margin: 0;
995
+ padding: 0;
996
+
997
+ tr {
998
+ margin: 0;
999
+ padding: 0;
1000
+
1001
+ &:first-child {
1002
+ background-color: @background-darker;
1003
+
1004
+ td {
1005
+ font-weight: bold;
1006
+ position: relative;
1007
+ }
1008
+ }
1009
+
1010
+ td {
1011
+ margin: 0;
1012
+ border: 1px solid @border-color;
1013
+ padding: 6px 10px;
1014
+ position: relative;
1015
+ word-break: break-word;
1016
+
1017
+ &:not(:last-child)::after {
1018
+ position: absolute;
1019
+ right: -5px;
1020
+ top: 0;
1021
+ width: 10px;
1022
+ height: 100%;
1023
+ content: '';
1024
+ z-index: 1;
1025
+ cursor: col-resize;
1026
+ user-select: none;
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+ //代码块样式
1033
+ :deep(pre) {
1034
+ display: block;
1035
+ padding: 6px 10px;
1036
+ margin: 0 0 15px;
1037
+ font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
1038
+ line-height: 1.5;
1039
+ font-size: @font-size;
1040
+ color: @font-color-dark;
1041
+ background-color: @pre-background;
1042
+ border: 1px solid @border-color;
1043
+ border-radius: 4px;
1044
+ overflow: auto;
1045
+ position: relative;
1046
+ }
1047
+ //图片样式
1048
+ :deep(img) {
1049
+ position: relative;
1050
+ display: inline-block;
1051
+ width: 30%;
1052
+ height: auto;
1053
+ border-radius: 2px;
1054
+ vertical-align: text-bottom;
1055
+ margin: 0 2px;
1056
+ max-width: 100%;
1057
+ }
1058
+ //视频样式
1059
+ :deep(video) {
1060
+ position: relative;
1061
+ display: inline-block;
1062
+ width: 30%;
1063
+ border-radius: 2px;
1064
+ vertical-align: text-bottom;
1065
+ background-color: #000;
1066
+ object-fit: contain;
1067
+ margin: 0 2px;
1068
+ max-width: 100%;
1069
+ }
1070
+ //引用样式
1071
+ :deep(blockquote) {
1072
+ display: block;
1073
+ border-left: 8px solid @background-darker;
1074
+ padding: 6px 10px 6px 20px;
1075
+ margin: 0 0 15px;
1076
+ line-height: 1.5;
1077
+ font-size: @font-size;
1078
+ color: @font-color-light;
1079
+ border-radius: 0;
1080
+ }
1081
+ //任务列表样式
1082
+ :deep(div[data-editify-task]) {
1083
+ margin-bottom: 15px;
1084
+ position: relative;
1085
+ padding-left: 26px;
1086
+ font-size: @font-size;
1087
+ color: @font-color-dark;
1088
+ transition: all 200ms;
1089
+
1090
+ &::before {
1091
+ display: block;
1092
+ width: 16px;
1093
+ height: 16px;
1094
+ border-radius: 2px;
1095
+ border: 2px solid @font-color-light;
1096
+ transition: all 200ms;
1097
+ box-sizing: border-box;
1098
+ user-select: none;
1099
+ content: '';
1100
+ position: absolute;
1101
+ left: 0;
1102
+ top: 2px;
1103
+ z-index: 1;
1104
+ cursor: pointer;
1105
+ }
1106
+
1107
+ &::after {
1108
+ position: absolute;
1109
+ content: '';
1110
+ left: 3px;
1111
+ top: 6px;
1112
+ display: inline-block;
1113
+ width: 10px;
1114
+ height: 6px;
1115
+ border: 2px solid @font-color-light;
1116
+ border-top: none;
1117
+ border-right: none;
1118
+ transform: rotate(-45deg);
1119
+ transform-origin: center;
1120
+ margin-bottom: 2px;
1121
+ box-sizing: border-box;
1122
+ z-index: 2;
1123
+ cursor: pointer;
1124
+ opacity: 0;
1125
+ transition: all 200ms;
1126
+ }
1127
+
1128
+ &[data-editify-task='checked'] {
1129
+ text-decoration: line-through;
1130
+ color: @font-color-light;
1131
+ &::after {
1132
+ opacity: 1;
1133
+ }
1134
+ }
1135
+ }
1136
+
1137
+ //禁用样式
1138
+ &.disabled {
1139
+ cursor: auto !important;
1140
+ &.placeholder::before {
1141
+ cursor: auto;
1142
+ }
1143
+ :deep(a) {
1144
+ cursor: pointer;
1145
+ }
1146
+
1147
+ :deep(table) {
1148
+ td:not(:last-child)::after {
1149
+ cursor: auto;
1150
+ }
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ //代码视图
1156
+ .editify-source {
1157
+ display: block;
1158
+ width: 100%;
1159
+ height: 100%;
1160
+ position: absolute;
1161
+ left: 0;
1162
+ top: 0;
1163
+ background-color: @reverse-background;
1164
+ margin: 0;
1165
+ padding: 6px 10px;
1166
+ overflow-x: hidden;
1167
+ overflow-y: auto;
1168
+ font-size: @font-size;
1169
+ color: @reverse-color;
1170
+ font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
1171
+ resize: none;
1172
+ border: none;
1173
+ border-radius: inherit;
1174
+ z-index: 1;
1175
+ }
1176
+ }
1177
+
1178
+ .editify-footer {
1179
+ display: flex;
1180
+ justify-content: end;
1181
+ align-items: center;
1182
+ width: 100%;
1183
+ padding: 10px;
1184
+ position: relative;
1185
+
1186
+ .editify-footer-words {
1187
+ font-size: @font-size;
1188
+ color: @font-color-light;
1189
+ line-height: 1;
1190
+ }
1191
+
1192
+ //全屏模式下并且不是代码视图下,显示一个上边框
1193
+ &.fullscreen {
1194
+ border-top: 1px solid @border-color;
1195
+ }
1196
+ }
1197
+ </style>