vue-editify 0.1.2 → 0.1.3

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,1178 @@
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 }">
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>