vue-editify 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>