@kaitify/core 0.0.1-beta.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/examples/App.vue +342 -0
- package/examples/content.js +1 -0
- package/examples/main.ts +4 -0
- package/examples/test.html +23 -0
- package/lib/extensions/Extension.d.ts +172 -0
- package/lib/extensions/align/index.d.ts +10 -0
- package/lib/extensions/attachment/index.d.ts +29 -0
- package/lib/extensions/back-color/index.d.ts +9 -0
- package/lib/extensions/blockquote/index.d.ts +12 -0
- package/lib/extensions/bold/index.d.ts +9 -0
- package/lib/extensions/code/index.d.ts +12 -0
- package/lib/extensions/code-block/hljs.d.ts +12 -0
- package/lib/extensions/code-block/index.d.ts +15 -0
- package/lib/extensions/color/index.d.ts +9 -0
- package/lib/extensions/font-family/index.d.ts +9 -0
- package/lib/extensions/font-size/index.d.ts +9 -0
- package/lib/extensions/heading/index.d.ts +13 -0
- package/lib/extensions/history/index.d.ts +10 -0
- package/lib/extensions/horizontal/index.d.ts +7 -0
- package/lib/extensions/image/index.d.ts +26 -0
- package/lib/extensions/indent/index.d.ts +8 -0
- package/lib/extensions/index.d.ts +29 -0
- package/lib/extensions/italic/index.d.ts +9 -0
- package/lib/extensions/line-height/index.d.ts +9 -0
- package/lib/extensions/link/index.d.ts +27 -0
- package/lib/extensions/list/index.d.ts +18 -0
- package/lib/extensions/math/index.d.ts +11 -0
- package/lib/extensions/strikethrough/index.d.ts +9 -0
- package/lib/extensions/subscript/index.d.ts +9 -0
- package/lib/extensions/superscript/index.d.ts +9 -0
- package/lib/extensions/table/index.d.ts +21 -0
- package/lib/extensions/task/index.d.ts +12 -0
- package/lib/extensions/text/index.d.ts +14 -0
- package/lib/extensions/underline/index.d.ts +9 -0
- package/lib/extensions/video/index.d.ts +27 -0
- package/lib/index.d.ts +3 -0
- package/lib/kaitify-core.es.js +38337 -0
- package/lib/kaitify-core.umd.js +2 -0
- package/lib/model/Editor.d.ts +504 -0
- package/lib/model/History.d.ts +42 -0
- package/lib/model/KNode.d.ts +258 -0
- package/lib/model/Selection.d.ts +29 -0
- package/lib/model/config/dom-observe.d.ts +10 -0
- package/lib/model/config/event-handler.d.ts +33 -0
- package/lib/model/config/format-patch.d.ts +25 -0
- package/lib/model/config/format-rules.d.ts +37 -0
- package/lib/model/config/function.d.ts +84 -0
- package/lib/model/index.d.ts +6 -0
- package/lib/tools/index.d.ts +49 -0
- package/lib/view/index.d.ts +21 -0
- package/lib/view/js-render/dom-patch.d.ts +65 -0
- package/lib/view/js-render/index.d.ts +5 -0
- package/package.json +52 -0
- package/src/css/style.less +56 -0
- package/src/css/var.less +45 -0
- package/src/extensions/Extension.ts +200 -0
- package/src/extensions/align/index.ts +115 -0
- package/src/extensions/attachment/icon.svg +1 -0
- package/src/extensions/attachment/index.ts +293 -0
- package/src/extensions/attachment/style.less +25 -0
- package/src/extensions/back-color/index.ts +56 -0
- package/src/extensions/blockquote/index.ts +144 -0
- package/src/extensions/blockquote/style.less +16 -0
- package/src/extensions/bold/index.ts +77 -0
- package/src/extensions/code/index.ts +295 -0
- package/src/extensions/code/style.less +14 -0
- package/src/extensions/code-block/hljs.less +183 -0
- package/src/extensions/code-block/hljs.ts +95 -0
- package/src/extensions/code-block/index.ts +308 -0
- package/src/extensions/code-block/style.less +20 -0
- package/src/extensions/color/index.ts +56 -0
- package/src/extensions/font-family/index.ts +80 -0
- package/src/extensions/font-size/index.ts +56 -0
- package/src/extensions/heading/index.ts +164 -0
- package/src/extensions/heading/style.less +42 -0
- package/src/extensions/history/index.ts +96 -0
- package/src/extensions/horizontal/index.ts +45 -0
- package/src/extensions/horizontal/style.less +13 -0
- package/src/extensions/image/index.ts +242 -0
- package/src/extensions/image/style.less +8 -0
- package/src/extensions/indent/index.ts +98 -0
- package/src/extensions/index.ts +29 -0
- package/src/extensions/italic/index.ts +77 -0
- package/src/extensions/line-height/index.ts +113 -0
- package/src/extensions/link/index.ts +184 -0
- package/src/extensions/link/style.less +19 -0
- package/src/extensions/list/index.ts +410 -0
- package/src/extensions/list/style.less +19 -0
- package/src/extensions/math/index.ts +233 -0
- package/src/extensions/math/style.less +21 -0
- package/src/extensions/strikethrough/index.ts +78 -0
- package/src/extensions/subscript/index.ts +77 -0
- package/src/extensions/superscript/index.ts +77 -0
- package/src/extensions/table/index.ts +1148 -0
- package/src/extensions/table/style.less +71 -0
- package/src/extensions/task/index.ts +243 -0
- package/src/extensions/task/style.less +59 -0
- package/src/extensions/text/index.ts +359 -0
- package/src/extensions/underline/index.ts +78 -0
- package/src/extensions/video/index.ts +273 -0
- package/src/extensions/video/style.less +8 -0
- package/src/index.ts +9 -0
- package/src/model/Editor.ts +1963 -0
- package/src/model/History.ts +115 -0
- package/src/model/KNode.ts +677 -0
- package/src/model/Selection.ts +39 -0
- package/src/model/config/dom-observe.ts +184 -0
- package/src/model/config/event-handler.ts +237 -0
- package/src/model/config/format-patch.ts +215 -0
- package/src/model/config/format-rules.ts +218 -0
- package/src/model/config/function.ts +1018 -0
- package/src/model/index.ts +6 -0
- package/src/tools/index.ts +156 -0
- package/src/view/index.ts +46 -0
- package/src/view/js-render/dom-patch.ts +324 -0
- package/src/view/js-render/index.ts +210 -0
- package/vite-env.d.ts +2 -0
|
@@ -0,0 +1,1963 @@
|
|
|
1
|
+
import { event as DapEvent, element as DapElement } from 'dap-util'
|
|
2
|
+
import { KNode, KNodeCreateOptionType, KNodeMarksType, KNodeMatchOptionType, KNodeStylesType } from './KNode'
|
|
3
|
+
import { createGuid, delay, getDomAttributes, getDomStyles, initEditorDom, isContains, isZeroWidthText } from '../tools'
|
|
4
|
+
import { Selection } from './Selection'
|
|
5
|
+
import { History } from './History'
|
|
6
|
+
import { formatSiblingNodesMerge, formatPlaceholderMerge, formatZeroWidthTextMerge, RuleFunctionType, formatParentNodeMerge, formatUneditableNoodes, formatBlockInChildren, fomratBlockTagParse } from './config/format-rules'
|
|
7
|
+
import { patchNodes } from './config/format-patch'
|
|
8
|
+
import { onBeforeInput, onBlur, onComposition, onCopy, onCut, onFocus, onKeyboard, onSelectionChange } from './config/event-handler'
|
|
9
|
+
import { removeDomObserve, setDomObserve } from './config/dom-observe'
|
|
10
|
+
import { Extension, HistoryExtension, ImageExtension, TextExtension, BoldExtension, ItalicExtension, StrikethroughExtension, UnderlineExtension, SuperscriptExtension, SubscriptExtension, CodeExtension, FontSizeExtension, VideoExtension, FontFamilyExtension, ColorExtension, BackColorExtension, LinkExtension, AlignExtension, LineHeightExtension, IndentExtension, HorizontalExtension, BlockquoteExtension, HeadingExtension, ListExtension, TaskExtension, MathExtension, CodeBlockExtension, AttachmentExtension, TableExtension } from '@/extensions'
|
|
11
|
+
import { NODE_MARK } from '@/view'
|
|
12
|
+
import { defaultUpdateView } from '@/view/js-render'
|
|
13
|
+
import { checkNodes, emptyFixedBlock, formatNodes, handlerForNormalInsertParagraph, mergeBlock, mergeExtensions, redressSelection, registerExtension, removeBlockFromParentToSameLevel, setPlaceholder } from './config/function'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 编辑器获取光标范围内节点数据的类型
|
|
17
|
+
*/
|
|
18
|
+
export type EditorSelectedType = {
|
|
19
|
+
node: KNode
|
|
20
|
+
offset: number[] | false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 编辑器命令集合类型
|
|
25
|
+
*/
|
|
26
|
+
export interface EditorCommandsType {
|
|
27
|
+
[name: string]: ((...args: any[]) => any | void) | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 编辑器配置入参类型
|
|
32
|
+
*/
|
|
33
|
+
export type EditorConfigureOptionType = {
|
|
34
|
+
/**
|
|
35
|
+
* 编辑器渲染的dom或者选择器
|
|
36
|
+
*/
|
|
37
|
+
el: HTMLElement | string
|
|
38
|
+
/**
|
|
39
|
+
* 是否允许复制
|
|
40
|
+
*/
|
|
41
|
+
allowCopy?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* 是否允许粘贴
|
|
44
|
+
*/
|
|
45
|
+
allowPaste?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* 是否允许剪切
|
|
48
|
+
*/
|
|
49
|
+
allowCut?: boolean
|
|
50
|
+
/**
|
|
51
|
+
* 是否允许粘贴html
|
|
52
|
+
*/
|
|
53
|
+
allowPasteHtml?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* 剪切板同时存在文件和html/text时,是否优先粘贴文件
|
|
56
|
+
*/
|
|
57
|
+
priorityPasteFiles?: boolean
|
|
58
|
+
/**
|
|
59
|
+
* 自定义编辑器内渲染文本节点的真实标签
|
|
60
|
+
*/
|
|
61
|
+
textRenderTag?: string
|
|
62
|
+
/**
|
|
63
|
+
* 自定义编辑内渲染默认块级节点的真实标签,即段落标签
|
|
64
|
+
*/
|
|
65
|
+
blockRenderTag?: string
|
|
66
|
+
/**
|
|
67
|
+
* 自定义编辑器内定义需要置空的标签
|
|
68
|
+
*/
|
|
69
|
+
emptyRenderTags?: string[]
|
|
70
|
+
/**
|
|
71
|
+
* 自定义编辑器内额外保留的标签
|
|
72
|
+
*/
|
|
73
|
+
extraKeepTags?: string[]
|
|
74
|
+
/**
|
|
75
|
+
* 自定义扩展数组
|
|
76
|
+
*/
|
|
77
|
+
extensions?: Extension[]
|
|
78
|
+
/**
|
|
79
|
+
* 自定义节点数组格式化规则
|
|
80
|
+
*/
|
|
81
|
+
formatRules?: RuleFunctionType[]
|
|
82
|
+
/**
|
|
83
|
+
* 自定义dom转为非文本节点的后续处理
|
|
84
|
+
*/
|
|
85
|
+
domParseNodeCallback?: (this: Editor, node: KNode) => KNode
|
|
86
|
+
/**
|
|
87
|
+
* 视图渲染时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要自定义渲染视图
|
|
88
|
+
*/
|
|
89
|
+
onUpdateView?: (this: Editor, init: boolean) => boolean | Promise<boolean>
|
|
90
|
+
/**
|
|
91
|
+
* 编辑器粘贴纯文本时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要进行自定义处理
|
|
92
|
+
*/
|
|
93
|
+
onPasteText?: (this: Editor, text: string) => boolean | Promise<boolean>
|
|
94
|
+
/**
|
|
95
|
+
* 编辑器粘贴html内容时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要进行自定义处理
|
|
96
|
+
*/
|
|
97
|
+
onPasteHtml?: (this: Editor, nodes: KNode[], html: string) => boolean | Promise<boolean>
|
|
98
|
+
/**
|
|
99
|
+
* 编辑器粘贴图片时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要进行自定义处理
|
|
100
|
+
*/
|
|
101
|
+
onPasteImage?: (this: Editor, file: File) => boolean | Promise<boolean>
|
|
102
|
+
/**
|
|
103
|
+
* 编辑器粘贴视频时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要进行自定义处理
|
|
104
|
+
*/
|
|
105
|
+
onPasteVideo?: (this: Editor, file: File) => boolean | Promise<boolean>
|
|
106
|
+
/**
|
|
107
|
+
* 编辑器粘贴除了图片和视频以外的文件时触发,需要自定义处理
|
|
108
|
+
*/
|
|
109
|
+
onPasteFile?: (this: Editor, file: File) => void | Promise<void>
|
|
110
|
+
/**
|
|
111
|
+
* 编辑器内容改变时触发
|
|
112
|
+
*/
|
|
113
|
+
onChange?: (this: Editor, newVal: string, oldVal: string) => void
|
|
114
|
+
/**
|
|
115
|
+
* 编辑器光标发生变化时触发
|
|
116
|
+
*/
|
|
117
|
+
onSelectionUpdate?: (this: Editor, selection: Selection) => void
|
|
118
|
+
/**
|
|
119
|
+
* 换行时触发,参数为换行操作后光标所在的块节点
|
|
120
|
+
*/
|
|
121
|
+
onInsertParagraph?: (this: Editor, node: KNode) => void
|
|
122
|
+
/**
|
|
123
|
+
* 完成删除时触发
|
|
124
|
+
*/
|
|
125
|
+
onDeleteComplete?: (this: Editor) => void
|
|
126
|
+
/**
|
|
127
|
+
* 光标在编辑器内时键盘按下触发
|
|
128
|
+
*/
|
|
129
|
+
onKeydown?: (this: Editor, event: KeyboardEvent) => void
|
|
130
|
+
/**
|
|
131
|
+
* 光标在编辑器内时键盘松开触发
|
|
132
|
+
*/
|
|
133
|
+
onKeyup?: (this: Editor, event: KeyboardEvent) => void
|
|
134
|
+
/**
|
|
135
|
+
* 编辑器聚焦时触发
|
|
136
|
+
*/
|
|
137
|
+
onFocus?: (this: Editor, event: FocusEvent) => void
|
|
138
|
+
/**
|
|
139
|
+
* 编辑器失焦时触发
|
|
140
|
+
*/
|
|
141
|
+
onBlur?: (this: Editor, event: FocusEvent) => void
|
|
142
|
+
/**
|
|
143
|
+
* 节点粘贴保留标记的自定义方法
|
|
144
|
+
*/
|
|
145
|
+
pasteKeepMarks?: (this: Editor, node: KNode) => KNodeMarksType
|
|
146
|
+
/**
|
|
147
|
+
* 节点粘贴保留样式的自定义方法
|
|
148
|
+
*/
|
|
149
|
+
pasteKeepStyles?: (this: Editor, node: KNode) => KNodeStylesType
|
|
150
|
+
/**
|
|
151
|
+
* 视图更新前回调方法
|
|
152
|
+
*/
|
|
153
|
+
beforeUpdateView?: (this: Editor) => void
|
|
154
|
+
/**
|
|
155
|
+
* 视图更新后回调方法
|
|
156
|
+
*/
|
|
157
|
+
afterUpdateView?: (this: Editor) => void
|
|
158
|
+
/**
|
|
159
|
+
* 在删除和换行操作中块节点从其父节点中抽离出去成为与父节点同级的节点后触发,如果返回true则表示继续使用默认逻辑,会将该节点转为段落,返回false则不走默认逻辑,需要自定义处理
|
|
160
|
+
*/
|
|
161
|
+
onDetachMentBlockFromParentCallback?: (this: Editor, node: KNode) => boolean
|
|
162
|
+
/**
|
|
163
|
+
* 编辑器updateView执行时,通过比对新旧节点数组获取需要格式化的节点,在这些节点被格式化前,触发此方法,回调参数即当前需要被格式化的节点,该方法返回一个节点,返回的节点将会被格式化,如果你不需要任何特殊处理,返回入参提供的节点即可
|
|
164
|
+
*/
|
|
165
|
+
beforePatchNodeToFormat?: (this: Editor, node: KNode) => KNode
|
|
166
|
+
|
|
167
|
+
/*--------------------------以下不作为编辑器内部属性-------------------------------*/
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 编辑器的初始默认值
|
|
171
|
+
*/
|
|
172
|
+
value: string
|
|
173
|
+
/**
|
|
174
|
+
* 编辑器初始是否可编辑,默认true
|
|
175
|
+
*/
|
|
176
|
+
editable?: boolean
|
|
177
|
+
/**
|
|
178
|
+
* 是否自动聚焦
|
|
179
|
+
*/
|
|
180
|
+
autofocus?: boolean
|
|
181
|
+
/**
|
|
182
|
+
* 编辑器内容只有一个段落时的占位符内容
|
|
183
|
+
*/
|
|
184
|
+
placeholder?: string
|
|
185
|
+
/**
|
|
186
|
+
* 是否深色模式
|
|
187
|
+
*/
|
|
188
|
+
dark?: boolean
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 编辑器核心类
|
|
193
|
+
*/
|
|
194
|
+
export class Editor {
|
|
195
|
+
/**
|
|
196
|
+
* 编辑器的真实dom【初始化后不可修改】【open】
|
|
197
|
+
*/
|
|
198
|
+
$el?: HTMLElement
|
|
199
|
+
/**
|
|
200
|
+
* 是否允许复制【初始化后可以修改】【open】
|
|
201
|
+
*/
|
|
202
|
+
allowCopy: boolean = true
|
|
203
|
+
/**
|
|
204
|
+
* 是否允许粘贴【初始化后可以修改】【open】
|
|
205
|
+
*/
|
|
206
|
+
allowPaste: boolean = true
|
|
207
|
+
/**
|
|
208
|
+
* 是否允许剪切【初始化后可以修改】【open】
|
|
209
|
+
*/
|
|
210
|
+
allowCut: boolean = true
|
|
211
|
+
/**
|
|
212
|
+
* 是否允许粘贴html【初始化后可以修改】【open】
|
|
213
|
+
*/
|
|
214
|
+
allowPasteHtml: boolean = false
|
|
215
|
+
/**
|
|
216
|
+
* 剪切板同时存在文件和html/text时,是否优先粘贴文件【初始化后可以修改】【open】
|
|
217
|
+
*/
|
|
218
|
+
priorityPasteFiles: boolean = false
|
|
219
|
+
/**
|
|
220
|
+
* 编辑器内渲染文本节点的真实标签【初始化后不建议修改】【open】
|
|
221
|
+
*/
|
|
222
|
+
textRenderTag: string = 'span'
|
|
223
|
+
/**
|
|
224
|
+
* 编辑内渲染默认块级节点的真实标签,即段落标签【初始化后不建议修改】【open】
|
|
225
|
+
*/
|
|
226
|
+
blockRenderTag: string = 'p'
|
|
227
|
+
/**
|
|
228
|
+
* 编辑器内定义需要置空的标签【初始化后不建议修改】【open】
|
|
229
|
+
*/
|
|
230
|
+
emptyRenderTags: string[] = ['meta', 'link', 'style', 'script', 'title', 'base', 'noscript', 'template', 'annotation', 'input', 'form', 'button']
|
|
231
|
+
/**
|
|
232
|
+
* 编辑器内额外保留的标签【初始化后不建议修改】【open】
|
|
233
|
+
*/
|
|
234
|
+
extraKeepTags: string[] = []
|
|
235
|
+
/**
|
|
236
|
+
* 扩展数组【初始化后不可修改】【open】
|
|
237
|
+
*/
|
|
238
|
+
extensions: Extension[] = [TextExtension(), ImageExtension(), VideoExtension(), HistoryExtension(), BoldExtension(), ItalicExtension(), StrikethroughExtension(), UnderlineExtension(), SuperscriptExtension(), SubscriptExtension(), CodeExtension(), FontSizeExtension(), FontFamilyExtension(), ColorExtension(), BackColorExtension(), LinkExtension(), AlignExtension(), LineHeightExtension(), IndentExtension(), HorizontalExtension(), BlockquoteExtension(), HeadingExtension(), ListExtension(), TaskExtension(), MathExtension(), CodeBlockExtension(), AttachmentExtension(), TableExtension()]
|
|
239
|
+
/**
|
|
240
|
+
* 编辑器的节点数组格式化规则【初始化后不可修改】【open】
|
|
241
|
+
*/
|
|
242
|
+
formatRules: RuleFunctionType[] = [fomratBlockTagParse, formatBlockInChildren, formatUneditableNoodes, formatPlaceholderMerge, formatZeroWidthTextMerge, formatSiblingNodesMerge, formatParentNodeMerge]
|
|
243
|
+
/**
|
|
244
|
+
* 自定义dom转为非文本节点的后续处理【初始化后不可修改】
|
|
245
|
+
*/
|
|
246
|
+
domParseNodeCallback?: (this: Editor, node: KNode) => KNode
|
|
247
|
+
/**
|
|
248
|
+
* 视图渲染时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要自定义渲染视图【初始化后不可修改】
|
|
249
|
+
*/
|
|
250
|
+
onUpdateView?: (this: Editor, init: boolean) => boolean | Promise<boolean>
|
|
251
|
+
/**
|
|
252
|
+
* 编辑器粘贴纯文本时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要进行自定义处理【初始化后不可修改】
|
|
253
|
+
*/
|
|
254
|
+
onPasteText?: (this: Editor, text: string) => boolean | Promise<boolean>
|
|
255
|
+
/**
|
|
256
|
+
* 编辑器粘贴html内容时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要进行自定义处理【初始化后不可修改】
|
|
257
|
+
*/
|
|
258
|
+
onPasteHtml?: (this: Editor, nodes: KNode[], html: string) => boolean | Promise<boolean>
|
|
259
|
+
/**
|
|
260
|
+
* 编辑器粘贴图片时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要进行自定义处理【初始化后不可修改】
|
|
261
|
+
*/
|
|
262
|
+
onPasteImage?: (this: Editor, file: File) => boolean | Promise<boolean>
|
|
263
|
+
/**
|
|
264
|
+
* 编辑器粘贴视频时触发,如果返回true则表示继续使用默认逻辑,返回false则不走默认逻辑,需要进行自定义处理【初始化后不可修改】
|
|
265
|
+
*/
|
|
266
|
+
onPasteVideo?: (this: Editor, file: File) => boolean | Promise<boolean>
|
|
267
|
+
/**
|
|
268
|
+
* 编辑器粘贴除了图片和视频以外的文件时触发,需要自定义处理【初始化后不可修改】
|
|
269
|
+
*/
|
|
270
|
+
onPasteFile?: (this: Editor, file: File) => void | Promise<void>
|
|
271
|
+
/**
|
|
272
|
+
* 编辑器内容改变触发【初始化后不可修改】
|
|
273
|
+
*/
|
|
274
|
+
onChange?: (this: Editor, newVal: string, oldVal: string) => void
|
|
275
|
+
/**
|
|
276
|
+
* 编辑器光标发生变化【初始化后不可修改】
|
|
277
|
+
*/
|
|
278
|
+
onSelectionUpdate?: (this: Editor, selection: Selection) => void
|
|
279
|
+
/**
|
|
280
|
+
* 换行时触发,参数为换行操作后光标所在的块节点
|
|
281
|
+
*/
|
|
282
|
+
onInsertParagraph?: (this: Editor, node: KNode) => void
|
|
283
|
+
/**
|
|
284
|
+
* 完成删除时触发【初始化后不可修改】
|
|
285
|
+
*/
|
|
286
|
+
onDeleteComplete?: (this: Editor) => void
|
|
287
|
+
/**
|
|
288
|
+
* 光标在编辑器内时键盘按下触发【初始化后不可修改】
|
|
289
|
+
*/
|
|
290
|
+
onKeydown?: (this: Editor, event: KeyboardEvent) => void
|
|
291
|
+
/**
|
|
292
|
+
* 光标在编辑器内时键盘松开触发【初始化后不可修改】
|
|
293
|
+
*/
|
|
294
|
+
onKeyup?: (this: Editor, event: KeyboardEvent) => void
|
|
295
|
+
/**
|
|
296
|
+
* 编辑器聚焦时触发【初始化后不可修改】
|
|
297
|
+
*/
|
|
298
|
+
onFocus?: (this: Editor, event: FocusEvent) => void
|
|
299
|
+
/**
|
|
300
|
+
* 编辑器失焦时触发【初始化后不可修改】
|
|
301
|
+
*/
|
|
302
|
+
onBlur?: (this: Editor, event: FocusEvent) => void
|
|
303
|
+
/**
|
|
304
|
+
* 节点粘贴保留标记的自定义方法【初始化后不可修改】
|
|
305
|
+
*/
|
|
306
|
+
pasteKeepMarks?: (this: Editor, node: KNode) => KNodeMarksType
|
|
307
|
+
/**
|
|
308
|
+
* 节点粘贴保留样式的自定义方法【初始化后不可修改】
|
|
309
|
+
*/
|
|
310
|
+
pasteKeepStyles?: (this: Editor, node: KNode) => KNodeStylesType
|
|
311
|
+
/**
|
|
312
|
+
* 视图更新前回调方法【初始化后不可修改】
|
|
313
|
+
*/
|
|
314
|
+
beforeUpdateView?: (this: Editor) => void
|
|
315
|
+
/**
|
|
316
|
+
* 视图更新后回调方法【初始化后不可修改】
|
|
317
|
+
*/
|
|
318
|
+
afterUpdateView?: (this: Editor) => void
|
|
319
|
+
/**
|
|
320
|
+
* 在删除和换行操作中块节点节点从其父节点中抽离出去成为与父节点同级的节点后触发,如果返回true则表示继续使用默认逻辑,会将该节点转为段落,返回false则不走默认逻辑,需要自定义处理【初始化后不可修改】
|
|
321
|
+
*/
|
|
322
|
+
onDetachMentBlockFromParentCallback?: (this: Editor, node: KNode) => boolean
|
|
323
|
+
/**
|
|
324
|
+
* 编辑器updateView执行时,通过比对新旧节点数组获取需要格式化的节点,在这些节点被格式化前,触发此方法,回调参数即当前需要被格式化的节点,该方法返回一个节点,返回的节点将会被格式化,如果你不需要任何特殊处理,返回入参提供的节点即可【初始化后不可修改】
|
|
325
|
+
*/
|
|
326
|
+
beforePatchNodeToFormat?: (this: Editor, node: KNode) => KNode
|
|
327
|
+
|
|
328
|
+
/*---------------------下面的属性都是不属于创建编辑器的参数---------------------------*/
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 唯一id【不可修改】【open】
|
|
332
|
+
*/
|
|
333
|
+
guid: number = createGuid()
|
|
334
|
+
/**
|
|
335
|
+
* 虚拟光标【不建议修改】【open】
|
|
336
|
+
*/
|
|
337
|
+
selection: Selection = new Selection()
|
|
338
|
+
/**
|
|
339
|
+
* 历史记录【不建议修改】【open】
|
|
340
|
+
*/
|
|
341
|
+
history: History = new History()
|
|
342
|
+
/**
|
|
343
|
+
* 命令集合【不可修改】【open】
|
|
344
|
+
*/
|
|
345
|
+
commands: EditorCommandsType = {}
|
|
346
|
+
/**
|
|
347
|
+
* 节点数组【不建议修改】【open】
|
|
348
|
+
*/
|
|
349
|
+
stackNodes: KNode[] = []
|
|
350
|
+
/**
|
|
351
|
+
* 旧节点数组【不可修改】
|
|
352
|
+
*/
|
|
353
|
+
oldStackNodes: KNode[] = []
|
|
354
|
+
/**
|
|
355
|
+
* 是否在输入中文【不可修改】
|
|
356
|
+
*/
|
|
357
|
+
isComposition: boolean = false
|
|
358
|
+
/**
|
|
359
|
+
* 是否编辑器内部渲染真实光标引起selctionChange事件【不可修改】
|
|
360
|
+
*/
|
|
361
|
+
internalCauseSelectionChange: boolean = false
|
|
362
|
+
/**
|
|
363
|
+
* 是否用户操作的删除行为,如果是用户操作的删除行为,则在处理不可编辑的节点是会删除该节点,如果是API调用的删除方法则走正常的删除逻辑【不可修改】
|
|
364
|
+
*/
|
|
365
|
+
isUserDelection: boolean = false
|
|
366
|
+
/**
|
|
367
|
+
* dom监听【不可修改】
|
|
368
|
+
*/
|
|
369
|
+
domObserver: MutationObserver | null = null
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 如果编辑器内有滚动条,滚动编辑器到光标可视范围
|
|
373
|
+
*/
|
|
374
|
+
scrollViewToSelection() {
|
|
375
|
+
if (this.selection.focused()) {
|
|
376
|
+
const focusDom = this.findDom(this.selection.end!.node)
|
|
377
|
+
const scrollFunction = async (scrollEl: HTMLElement) => {
|
|
378
|
+
const scrollHeight = DapElement.getScrollHeight(scrollEl)
|
|
379
|
+
const scrollWidth = DapElement.getScrollWidth(scrollEl)
|
|
380
|
+
//存在横向或者垂直滚动条
|
|
381
|
+
if (scrollEl.clientHeight < scrollHeight || scrollEl.clientWidth < scrollWidth) {
|
|
382
|
+
const selection = window.getSelection()!
|
|
383
|
+
const range = selection.getRangeAt(0)
|
|
384
|
+
const rects = range.getClientRects()
|
|
385
|
+
let target: Range | HTMLElement = range
|
|
386
|
+
if (rects.length == 0) {
|
|
387
|
+
target = focusDom
|
|
388
|
+
}
|
|
389
|
+
const childRect = target.getBoundingClientRect()
|
|
390
|
+
const parentRect = scrollEl.getBoundingClientRect()
|
|
391
|
+
//存在垂直滚动条
|
|
392
|
+
if (scrollEl.clientHeight < scrollHeight) {
|
|
393
|
+
//如果光标所在节点不在视图内则滚动到视图内
|
|
394
|
+
if (childRect.top < parentRect.top) {
|
|
395
|
+
await DapElement.setScrollTop({
|
|
396
|
+
el: scrollEl,
|
|
397
|
+
number: 0
|
|
398
|
+
})
|
|
399
|
+
const tempChildRect = target.getBoundingClientRect()
|
|
400
|
+
const tempParentRect = scrollEl.getBoundingClientRect()
|
|
401
|
+
DapElement.setScrollTop({
|
|
402
|
+
el: scrollEl,
|
|
403
|
+
number: tempChildRect.top - tempParentRect.top
|
|
404
|
+
})
|
|
405
|
+
} else if (childRect.bottom > parentRect.bottom) {
|
|
406
|
+
await DapElement.setScrollTop({
|
|
407
|
+
el: scrollEl,
|
|
408
|
+
number: 0
|
|
409
|
+
})
|
|
410
|
+
const tempChildRect = target.getBoundingClientRect()
|
|
411
|
+
const tempParentRect = scrollEl.getBoundingClientRect()
|
|
412
|
+
DapElement.setScrollTop({
|
|
413
|
+
el: scrollEl,
|
|
414
|
+
number: tempChildRect.bottom - tempParentRect.bottom
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
//存在横向滚动条
|
|
419
|
+
if (scrollEl.clientWidth < scrollWidth) {
|
|
420
|
+
//如果光标所在节点不在视图内则滚动到视图内
|
|
421
|
+
if (childRect.left < parentRect.left) {
|
|
422
|
+
await DapElement.setScrollLeft({
|
|
423
|
+
el: scrollEl,
|
|
424
|
+
number: 0
|
|
425
|
+
})
|
|
426
|
+
const tempChildRect = target.getBoundingClientRect()
|
|
427
|
+
const tempParentRect = scrollEl.getBoundingClientRect()
|
|
428
|
+
DapElement.setScrollLeft({
|
|
429
|
+
el: scrollEl,
|
|
430
|
+
number: tempChildRect.left - tempParentRect.left + 20
|
|
431
|
+
})
|
|
432
|
+
} else if (childRect.right > parentRect.right) {
|
|
433
|
+
await DapElement.setScrollLeft({
|
|
434
|
+
el: scrollEl,
|
|
435
|
+
number: 0
|
|
436
|
+
})
|
|
437
|
+
const tempChildRect = target.getBoundingClientRect()
|
|
438
|
+
const tempParentRect = scrollEl.getBoundingClientRect()
|
|
439
|
+
DapElement.setScrollLeft({
|
|
440
|
+
el: scrollEl,
|
|
441
|
+
number: tempChildRect.right - tempParentRect.right + 20
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
let dom = focusDom
|
|
448
|
+
while (DapElement.isElement(dom) && dom != document.documentElement) {
|
|
449
|
+
scrollFunction(dom)
|
|
450
|
+
dom = dom.parentNode as HTMLElement
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 根据dom查找到编辑内的对应节点
|
|
457
|
+
*/
|
|
458
|
+
findNode(dom: HTMLElement) {
|
|
459
|
+
if (!isContains(this.$el!, dom)) {
|
|
460
|
+
throw new Error(`The dom should be in the editor area, but what you provide is not`)
|
|
461
|
+
}
|
|
462
|
+
const key = dom.getAttribute(NODE_MARK)
|
|
463
|
+
if (!key) {
|
|
464
|
+
throw new Error(`The dom generated by editor should all have a ${NODE_MARK} attribute, but your dom does not. Check for "updateView" related issues`)
|
|
465
|
+
}
|
|
466
|
+
const node = KNode.searchByKey(key, this.stackNodes)
|
|
467
|
+
if (!node) {
|
|
468
|
+
throw new Error(`Unexpected error occurred: the knode was not found in the editor`)
|
|
469
|
+
}
|
|
470
|
+
return node
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 根据编辑器内的node查找真实dom
|
|
475
|
+
*/
|
|
476
|
+
findDom(node: KNode) {
|
|
477
|
+
//获取所有的符合选择器的元素
|
|
478
|
+
const doms = this.$el!.querySelectorAll(`[${NODE_MARK}="${node.key}"]`)
|
|
479
|
+
//如果没有则抛出异常
|
|
480
|
+
if (doms.length == 0) {
|
|
481
|
+
throw new Error(`Unexpected error occurred: the dom was not found in the editor`)
|
|
482
|
+
}
|
|
483
|
+
//查找父元素匹配的元素
|
|
484
|
+
const dom = Array.from(doms).find(item => (node.parent ? item.parentElement?.getAttribute(`${NODE_MARK}`) == `${node.parent.key}` : item.parentElement === this.$el))
|
|
485
|
+
//不存在则抛出异常
|
|
486
|
+
if (!dom) {
|
|
487
|
+
throw new Error(`Unexpected error occurred: the dom was not found in the editor`)
|
|
488
|
+
}
|
|
489
|
+
return dom as HTMLElement
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 设置编辑器是否可编辑
|
|
494
|
+
*/
|
|
495
|
+
setEditable(editable: boolean) {
|
|
496
|
+
if (editable) {
|
|
497
|
+
this.$el?.setAttribute('contenteditable', 'true')
|
|
498
|
+
} else {
|
|
499
|
+
this.$el?.removeAttribute('contenteditable')
|
|
500
|
+
}
|
|
501
|
+
this.$el?.setAttribute('spellcheck', 'false')
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 判断编辑器是否可编辑
|
|
506
|
+
*/
|
|
507
|
+
isEditable() {
|
|
508
|
+
return this.$el?.getAttribute('contenteditable') == 'true'
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 设置编辑器是否深色模式
|
|
513
|
+
*/
|
|
514
|
+
setDark(dark: boolean) {
|
|
515
|
+
if (dark) {
|
|
516
|
+
document.documentElement.setAttribute('kaitify-dark', '')
|
|
517
|
+
} else {
|
|
518
|
+
document.documentElement.removeAttribute('kaitify-dark')
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* 是否深色模式
|
|
524
|
+
*/
|
|
525
|
+
isDark() {
|
|
526
|
+
return document.documentElement.hasAttribute('kaitify-dark')
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* dom转KNode
|
|
531
|
+
*/
|
|
532
|
+
domParseNode(dom: Node) {
|
|
533
|
+
if (dom.nodeType != 1 && dom.nodeType != 3) {
|
|
534
|
+
throw new Error('The argument must be an element node or text node')
|
|
535
|
+
}
|
|
536
|
+
//文本节点
|
|
537
|
+
if (dom.nodeType == 3) {
|
|
538
|
+
return KNode.create({
|
|
539
|
+
type: 'text',
|
|
540
|
+
textContent: dom.textContent || ''
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
//元素节点
|
|
544
|
+
const marks = getDomAttributes(dom as HTMLElement) //标记
|
|
545
|
+
const styles = getDomStyles(dom as HTMLElement) //样式
|
|
546
|
+
const tag = dom.nodeName.toLocaleLowerCase() //标签名称
|
|
547
|
+
const namespace = (dom as HTMLElement).namespaceURI //命名空间
|
|
548
|
+
//如果是需要置为空的标签返回空文本节点
|
|
549
|
+
if (this.emptyRenderTags.includes(tag)) {
|
|
550
|
+
return KNode.create({
|
|
551
|
+
type: 'text'
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
//如果是默认的文本节点标签并且内部只有文本,则返回文本节点
|
|
555
|
+
if (tag == this.textRenderTag && dom.childNodes.length && Array.from(dom.childNodes).every(childNode => childNode.nodeType == 3)) {
|
|
556
|
+
return KNode.create({
|
|
557
|
+
type: 'text',
|
|
558
|
+
marks,
|
|
559
|
+
styles,
|
|
560
|
+
textContent: dom.textContent || ''
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
//构造参数
|
|
564
|
+
const config: KNodeCreateOptionType = {
|
|
565
|
+
type: 'inline',
|
|
566
|
+
tag,
|
|
567
|
+
marks,
|
|
568
|
+
styles,
|
|
569
|
+
namespace: namespace || ''
|
|
570
|
+
}
|
|
571
|
+
//默认的块节点
|
|
572
|
+
if (['p', 'div', 'address', 'article', 'aside', 'nav', 'section'].includes(tag)) {
|
|
573
|
+
config.type = 'block'
|
|
574
|
+
config.children = []
|
|
575
|
+
}
|
|
576
|
+
//默认的行内节点
|
|
577
|
+
else if (['span', 'label'].includes(tag)) {
|
|
578
|
+
config.type = 'inline'
|
|
579
|
+
config.children = []
|
|
580
|
+
}
|
|
581
|
+
//默认的自闭合节点
|
|
582
|
+
else if (['br'].includes(tag)) {
|
|
583
|
+
config.type = 'closed'
|
|
584
|
+
}
|
|
585
|
+
//其余元素如果不在extraKeepTags范围内则默认转为行内的默认文本节点标签
|
|
586
|
+
else if (!this.extraKeepTags.includes(tag)) {
|
|
587
|
+
config.type = 'inline'
|
|
588
|
+
config.tag = this.textRenderTag
|
|
589
|
+
config.namespace = ''
|
|
590
|
+
config.children = []
|
|
591
|
+
}
|
|
592
|
+
let node = KNode.create(config)
|
|
593
|
+
//如果不是闭合节点则设置子节点
|
|
594
|
+
if (!closed) {
|
|
595
|
+
Array.from(dom.childNodes).forEach(child => {
|
|
596
|
+
if (child.nodeType == 1 || child.nodeType == 3) {
|
|
597
|
+
const childNode = this.domParseNode(child)
|
|
598
|
+
childNode.parent = node
|
|
599
|
+
if (node.hasChildren()) {
|
|
600
|
+
node.children!.push(childNode)
|
|
601
|
+
} else {
|
|
602
|
+
node.children = [childNode]
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
//转换后的回调处理,在这里可以自定义处理节点
|
|
608
|
+
if (typeof this.domParseNodeCallback == 'function') {
|
|
609
|
+
node = this.domParseNodeCallback.apply(this, [node])
|
|
610
|
+
}
|
|
611
|
+
return node
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* html转KNode
|
|
616
|
+
*/
|
|
617
|
+
htmlParseNode(html: string) {
|
|
618
|
+
const template = document.createElement('template')
|
|
619
|
+
template.innerHTML = html
|
|
620
|
+
const nodes: KNode[] = []
|
|
621
|
+
template.content.childNodes.forEach(item => {
|
|
622
|
+
if (item.nodeType == 1 || item.nodeType == 3) {
|
|
623
|
+
const node = this.domParseNode(item)
|
|
624
|
+
nodes.push(node)
|
|
625
|
+
}
|
|
626
|
+
})
|
|
627
|
+
return nodes
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 将指定节点所在的块节点转为段落
|
|
632
|
+
*/
|
|
633
|
+
toParagraph(node: KNode) {
|
|
634
|
+
if (!node.isBlock()) {
|
|
635
|
+
node = node.getBlock()
|
|
636
|
+
}
|
|
637
|
+
node.tag = this.blockRenderTag
|
|
638
|
+
node.marks = {}
|
|
639
|
+
node.styles = {}
|
|
640
|
+
node.fixed = false
|
|
641
|
+
node.nested = false
|
|
642
|
+
node.locked = false
|
|
643
|
+
node.namespace = ''
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* 指定的块节点是否是一个段落
|
|
648
|
+
*/
|
|
649
|
+
isParagraph(node: KNode) {
|
|
650
|
+
if (!node.isBlock()) {
|
|
651
|
+
return false
|
|
652
|
+
}
|
|
653
|
+
return node.isMatch({ tag: this.blockRenderTag }) && !node.hasMarks() && !node.hasStyles()
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 将指定节点添加到某个节点的子节点数组里
|
|
658
|
+
*/
|
|
659
|
+
addNode(node: KNode, parentNode: KNode, index: number | undefined = 0) {
|
|
660
|
+
//排除空节点
|
|
661
|
+
if (node.isEmpty()) {
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
//父节点不能是文本节点或者闭合节点
|
|
665
|
+
if (parentNode.isText() || parentNode.isClosed()) {
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
//不存在子节点,初始为空数组
|
|
669
|
+
if (!parentNode.hasChildren()) {
|
|
670
|
+
parentNode.children = []
|
|
671
|
+
}
|
|
672
|
+
if (index >= parentNode.children!.length) {
|
|
673
|
+
parentNode.children!.push(node)
|
|
674
|
+
} else {
|
|
675
|
+
parentNode.children!.splice(index, 0, node)
|
|
676
|
+
}
|
|
677
|
+
node.parent = parentNode
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* 将指定节点添加到某个节点前面
|
|
682
|
+
*/
|
|
683
|
+
addNodeBefore(node: KNode, target: KNode) {
|
|
684
|
+
if (target.parent) {
|
|
685
|
+
const index = target.parent!.children!.findIndex(item => {
|
|
686
|
+
return target.isEqual(item)
|
|
687
|
+
})
|
|
688
|
+
this.addNode(node, target.parent!, index)
|
|
689
|
+
} else {
|
|
690
|
+
const index = this.stackNodes.findIndex(item => {
|
|
691
|
+
return target.isEqual(item)
|
|
692
|
+
})
|
|
693
|
+
this.stackNodes.splice(index, 0, node)
|
|
694
|
+
node.parent = undefined
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* 将指定节点添加到某个节点后面
|
|
700
|
+
*/
|
|
701
|
+
addNodeAfter(node: KNode, target: KNode) {
|
|
702
|
+
if (target.parent) {
|
|
703
|
+
const index = target.parent!.children!.findIndex(item => {
|
|
704
|
+
return target.isEqual(item)
|
|
705
|
+
})
|
|
706
|
+
this.addNode(node, target.parent!, index + 1)
|
|
707
|
+
} else {
|
|
708
|
+
const index = this.stackNodes.findIndex(item => {
|
|
709
|
+
return target.isEqual(item)
|
|
710
|
+
})
|
|
711
|
+
this.stackNodes.splice(index + 1, 0, node)
|
|
712
|
+
node.parent = undefined
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* 获取某个节点内的最后一个可以设置光标点的节点,包括自身
|
|
718
|
+
*/
|
|
719
|
+
getLastSelectionNodeInChildren(node: KNode): KNode | null {
|
|
720
|
+
//空节点
|
|
721
|
+
if (node.isEmpty()) {
|
|
722
|
+
return null
|
|
723
|
+
}
|
|
724
|
+
//子节点是不可见节点
|
|
725
|
+
if (node.void) {
|
|
726
|
+
return null
|
|
727
|
+
}
|
|
728
|
+
//文本节点和闭合节点返回自身
|
|
729
|
+
if (node.isText() || node.isClosed()) {
|
|
730
|
+
return node
|
|
731
|
+
}
|
|
732
|
+
let selectionNode = null
|
|
733
|
+
const length = node.children!.length
|
|
734
|
+
//遍历子节点
|
|
735
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
736
|
+
const child = node.children![i]
|
|
737
|
+
selectionNode = this.getLastSelectionNodeInChildren(child)
|
|
738
|
+
//这里如果在子节点中找到了可以设置光标点的节点,一定要break直接终止for循环的执行
|
|
739
|
+
if (selectionNode) {
|
|
740
|
+
break
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return selectionNode
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* 获取某个节点内的第一个可以设置光标点的节点,包括自身
|
|
748
|
+
*/
|
|
749
|
+
getFirstSelectionNodeInChildren(node: KNode): KNode | null {
|
|
750
|
+
//空节点
|
|
751
|
+
if (node.isEmpty()) {
|
|
752
|
+
return null
|
|
753
|
+
}
|
|
754
|
+
//子节点是不可见节点
|
|
755
|
+
if (node.void) {
|
|
756
|
+
return null
|
|
757
|
+
}
|
|
758
|
+
//文本节点和闭合节点返回自身
|
|
759
|
+
if (node.isText() || node.isClosed()) {
|
|
760
|
+
return node
|
|
761
|
+
}
|
|
762
|
+
let selectionNode = null
|
|
763
|
+
const length = node.children!.length
|
|
764
|
+
//遍历子节点
|
|
765
|
+
for (let i = 0; i < length; i++) {
|
|
766
|
+
const child = node.children![i]
|
|
767
|
+
selectionNode = this.getFirstSelectionNodeInChildren(child)
|
|
768
|
+
//这里如果在子节点中找到了可以设置光标点的节点,一定要break直接终止for循环的执行
|
|
769
|
+
if (selectionNode) {
|
|
770
|
+
break
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return selectionNode
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* 查找指定节点之前可以设置为光标点的非空节点,不包括自身
|
|
778
|
+
*/
|
|
779
|
+
getPreviousSelectionNode(node: KNode): KNode | null {
|
|
780
|
+
const nodes = node.parent ? node.parent.children! : this.stackNodes
|
|
781
|
+
//获取前一个节点
|
|
782
|
+
const previousNode = node.getPrevious(nodes)
|
|
783
|
+
//前一个节点存在
|
|
784
|
+
if (previousNode) {
|
|
785
|
+
//是空节点,则跳过继续向前
|
|
786
|
+
if (previousNode.isEmpty()) {
|
|
787
|
+
return this.getPreviousSelectionNode(previousNode)
|
|
788
|
+
}
|
|
789
|
+
//是不可见节点,则跳过继续向前
|
|
790
|
+
if (previousNode.void) {
|
|
791
|
+
return this.getPreviousSelectionNode(previousNode)
|
|
792
|
+
}
|
|
793
|
+
//是文本节点或者闭合节点
|
|
794
|
+
if (previousNode.isText() || previousNode.isClosed()) {
|
|
795
|
+
return previousNode
|
|
796
|
+
}
|
|
797
|
+
//其他节点:查找子节点中的最后一个可以设置光标点的节点
|
|
798
|
+
return this.getLastSelectionNodeInChildren(previousNode)
|
|
799
|
+
}
|
|
800
|
+
//前一个节点不存在的情况,说明该节点是节点数组中的第一个节点
|
|
801
|
+
return node.parent ? this.getPreviousSelectionNode(node.parent) : null
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* 查找指定节点之后可以设置为光标点的非空节点,不包括自身
|
|
806
|
+
*/
|
|
807
|
+
getNextSelectionNode(node: KNode): KNode | null {
|
|
808
|
+
const nodes = node.parent ? node.parent.children! : this.stackNodes
|
|
809
|
+
//获取后一个节点
|
|
810
|
+
const nextNode = node.getNext(nodes)
|
|
811
|
+
//后一个节点存在
|
|
812
|
+
if (nextNode) {
|
|
813
|
+
//是空节点,则跳过继续向后
|
|
814
|
+
if (nextNode.isEmpty()) {
|
|
815
|
+
return this.getNextSelectionNode(nextNode)
|
|
816
|
+
}
|
|
817
|
+
//是不可见节点,则跳过继续向后
|
|
818
|
+
if (nextNode.void) {
|
|
819
|
+
return this.getNextSelectionNode(nextNode)
|
|
820
|
+
}
|
|
821
|
+
//是文本节点或者闭合节点
|
|
822
|
+
if (nextNode.isText() || nextNode.isClosed()) {
|
|
823
|
+
return nextNode
|
|
824
|
+
}
|
|
825
|
+
//其他节点:查找子节点中的第一个可以设置光标点的节点
|
|
826
|
+
return this.getFirstSelectionNodeInChildren(nextNode)
|
|
827
|
+
}
|
|
828
|
+
//后一个节点不存在的情况,说明该节点是节点数组中的最后一个节点
|
|
829
|
+
return node.parent ? this.getNextSelectionNode(node.parent) : null
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* 设置光标到指定节点内部的起始处,如果没有指定节点则设置光标到编辑器起始处,start表示只设置起点,end表示只设置终点,all表示起点和终点都设置
|
|
834
|
+
*/
|
|
835
|
+
setSelectionBefore(node?: KNode, type: 'all' | 'start' | 'end' | undefined = 'all') {
|
|
836
|
+
//指定到某个节点
|
|
837
|
+
if (node) {
|
|
838
|
+
const selectionNode = this.getFirstSelectionNodeInChildren(node)
|
|
839
|
+
if (selectionNode) {
|
|
840
|
+
if (type == 'start' || type == 'all') {
|
|
841
|
+
this.selection.start = {
|
|
842
|
+
node: selectionNode,
|
|
843
|
+
offset: 0
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (type == 'end' || type == 'all') {
|
|
847
|
+
this.selection.end = {
|
|
848
|
+
node: selectionNode,
|
|
849
|
+
offset: 0
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
//指定到文档前面
|
|
855
|
+
else {
|
|
856
|
+
//获取第一个节点
|
|
857
|
+
const firstNode = this.stackNodes[0]
|
|
858
|
+
//获取firstNode中的第一个可以设置光标点的节点
|
|
859
|
+
let selectionNode = this.getFirstSelectionNodeInChildren(firstNode)
|
|
860
|
+
//如果firstNode不能设置光标点,则向后查询
|
|
861
|
+
if (!selectionNode) selectionNode = this.getNextSelectionNode(firstNode)
|
|
862
|
+
//如果firstNode可以设置光标点
|
|
863
|
+
if (selectionNode) {
|
|
864
|
+
if (type == 'start' || type == 'all') {
|
|
865
|
+
this.selection.start = {
|
|
866
|
+
node: selectionNode,
|
|
867
|
+
offset: 0
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (type == 'end' || type == 'all') {
|
|
871
|
+
this.selection.end = {
|
|
872
|
+
node: selectionNode,
|
|
873
|
+
offset: 0
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* 设置光标到指定节点内部的末尾处,如果没有指定节点则设置光标到编辑器末尾处,start表示只设置起点,end表示只设置终点,all表示起点和终点都设置
|
|
882
|
+
*/
|
|
883
|
+
setSelectionAfter(node?: KNode, type: 'all' | 'start' | 'end' | undefined = 'all') {
|
|
884
|
+
//指定到某个节点
|
|
885
|
+
if (node) {
|
|
886
|
+
const selectionNode = this.getLastSelectionNodeInChildren(node)
|
|
887
|
+
if (selectionNode) {
|
|
888
|
+
if (type == 'start' || type == 'all') {
|
|
889
|
+
this.selection.start = {
|
|
890
|
+
node: selectionNode,
|
|
891
|
+
offset: selectionNode.isText() ? selectionNode.textContent!.length : 1
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (type == 'end' || type == 'all') {
|
|
895
|
+
this.selection.end = {
|
|
896
|
+
node: selectionNode,
|
|
897
|
+
offset: selectionNode.isText() ? selectionNode.textContent!.length : 1
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
//指定到文档前面
|
|
903
|
+
else {
|
|
904
|
+
//获取最后一个节点
|
|
905
|
+
const lastNode = this.stackNodes[this.stackNodes.length - 1]
|
|
906
|
+
//获取lastNode中的最后一个可以设置光标点的节点
|
|
907
|
+
let selectionNode = this.getLastSelectionNodeInChildren(lastNode)
|
|
908
|
+
//如果lastNode不能设置光标点,则向前查询
|
|
909
|
+
if (!selectionNode) selectionNode = this.getPreviousSelectionNode(lastNode)
|
|
910
|
+
//如果lastNode可以设置光标点
|
|
911
|
+
if (selectionNode) {
|
|
912
|
+
if (type == 'start' || type == 'all') {
|
|
913
|
+
this.selection.start = {
|
|
914
|
+
node: selectionNode,
|
|
915
|
+
offset: selectionNode.isText() ? selectionNode.textContent!.length : 1
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (type == 'end' || type == 'all') {
|
|
919
|
+
this.selection.end = {
|
|
920
|
+
node: selectionNode,
|
|
921
|
+
offset: selectionNode.isText() ? selectionNode.textContent!.length : 1
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* 更新指定光标到离当前光标点最近的节点上,start表示只更新起点,end表示只更新终点,all表示起点和终点都更新,不包括当前光标所在节点
|
|
930
|
+
*/
|
|
931
|
+
updateSelectionRecently(type: 'all' | 'start' | 'end' | undefined = 'all') {
|
|
932
|
+
if (!this.selection.focused()) {
|
|
933
|
+
return
|
|
934
|
+
}
|
|
935
|
+
if (type == 'start' || type == 'all') {
|
|
936
|
+
const previousNode = this.getPreviousSelectionNode(this.selection.start!.node)
|
|
937
|
+
const nextNode = this.getNextSelectionNode(this.selection.start!.node)
|
|
938
|
+
const blockNode = this.selection.start!.node.getBlock()
|
|
939
|
+
if (previousNode && blockNode.isContains(previousNode)) {
|
|
940
|
+
this.selection.start!.node = previousNode
|
|
941
|
+
this.selection.start!.offset = previousNode.isText() ? previousNode.textContent!.length : 1
|
|
942
|
+
} else if (nextNode && blockNode.isContains(nextNode)) {
|
|
943
|
+
this.selection.start!.node = nextNode
|
|
944
|
+
this.selection.start!.offset = 0
|
|
945
|
+
} else if (previousNode) {
|
|
946
|
+
this.selection.start!.node = previousNode
|
|
947
|
+
this.selection.start!.offset = previousNode.isText() ? previousNode.textContent!.length : 1
|
|
948
|
+
} else if (nextNode) {
|
|
949
|
+
this.selection.start!.node = nextNode
|
|
950
|
+
this.selection.start!.offset = 0
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (type == 'end' || type == 'all') {
|
|
954
|
+
const previousNode = this.getPreviousSelectionNode(this.selection.end!.node)
|
|
955
|
+
const nextNode = this.getNextSelectionNode(this.selection.end!.node)
|
|
956
|
+
const blockNode = this.selection.end!.node.getBlock()
|
|
957
|
+
if (previousNode && blockNode.isContains(previousNode)) {
|
|
958
|
+
this.selection.end!.node = previousNode
|
|
959
|
+
this.selection.end!.offset = previousNode.isText() ? previousNode.textContent!.length : 1
|
|
960
|
+
} else if (nextNode && blockNode.isContains(nextNode)) {
|
|
961
|
+
this.selection.end!.node = nextNode
|
|
962
|
+
this.selection.end!.offset = 0
|
|
963
|
+
} else if (previousNode) {
|
|
964
|
+
this.selection.end!.node = previousNode
|
|
965
|
+
this.selection.end!.offset = previousNode.isText() ? previousNode.textContent!.length : 1
|
|
966
|
+
} else if (nextNode) {
|
|
967
|
+
this.selection.end!.node = nextNode
|
|
968
|
+
this.selection.end!.offset = 0
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* 判断光标是否在某个节点内,start表示只判断起点,end表示只判断终点,all表示起点和终点都判断
|
|
975
|
+
*/
|
|
976
|
+
isSelectionInNode(node: KNode, type: 'all' | 'start' | 'end' | undefined = 'all') {
|
|
977
|
+
//没有初始化设置光标
|
|
978
|
+
if (!this.selection.focused()) {
|
|
979
|
+
return false
|
|
980
|
+
}
|
|
981
|
+
if (type == 'start') {
|
|
982
|
+
return node.isContains(this.selection.start!.node)
|
|
983
|
+
}
|
|
984
|
+
if (type == 'end') {
|
|
985
|
+
return node.isContains(this.selection.end!.node)
|
|
986
|
+
}
|
|
987
|
+
if (type == 'all') {
|
|
988
|
+
return node.isContains(this.selection.start!.node) && node.isContains(this.selection.end!.node)
|
|
989
|
+
}
|
|
990
|
+
return false
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* 获取光标选区内的节点数据
|
|
995
|
+
*/
|
|
996
|
+
getSelectedNodes(): EditorSelectedType[] {
|
|
997
|
+
//没有聚焦或者没有选区
|
|
998
|
+
if (!this.selection.focused() || this.selection.collapsed()) {
|
|
999
|
+
return []
|
|
1000
|
+
}
|
|
1001
|
+
const startNode = this.selection.start!.node
|
|
1002
|
+
const endNode = this.selection.end!.node
|
|
1003
|
+
const startOffset = this.selection.start!.offset
|
|
1004
|
+
const endOffset = this.selection.end!.offset
|
|
1005
|
+
|
|
1006
|
+
//起点和终点在一个节点内
|
|
1007
|
+
if (startNode.isEqual(endNode)) {
|
|
1008
|
+
//闭合节点
|
|
1009
|
+
if (startNode.isClosed()) {
|
|
1010
|
+
return [
|
|
1011
|
+
{
|
|
1012
|
+
node: startNode,
|
|
1013
|
+
offset: false
|
|
1014
|
+
}
|
|
1015
|
+
]
|
|
1016
|
+
}
|
|
1017
|
+
//文本节点
|
|
1018
|
+
return [
|
|
1019
|
+
{
|
|
1020
|
+
node: startNode,
|
|
1021
|
+
offset: startOffset == 0 && endOffset == startNode.textContent!.length ? false : [startOffset, endOffset]
|
|
1022
|
+
}
|
|
1023
|
+
]
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
//起点和终点不在一个节点内时
|
|
1027
|
+
const result: EditorSelectedType[] = []
|
|
1028
|
+
let node = startNode
|
|
1029
|
+
//遍历到终点所在节点时,循环结束
|
|
1030
|
+
while (true) {
|
|
1031
|
+
//是起点所在节点
|
|
1032
|
+
if (node.isEqual(startNode)) {
|
|
1033
|
+
if (startOffset == 0) {
|
|
1034
|
+
result.push({
|
|
1035
|
+
node: node,
|
|
1036
|
+
offset: false
|
|
1037
|
+
})
|
|
1038
|
+
} else if (node.isText() && startOffset < node.textContent!.length) {
|
|
1039
|
+
result.push({
|
|
1040
|
+
node: node,
|
|
1041
|
+
offset: [startOffset, node.textContent!.length]
|
|
1042
|
+
})
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
//是终点所在节点
|
|
1046
|
+
else if (node.isEqual(endNode)) {
|
|
1047
|
+
if (endOffset == (node.isText() ? node.textContent!.length : 1)) {
|
|
1048
|
+
result.push({
|
|
1049
|
+
node: node,
|
|
1050
|
+
offset: false
|
|
1051
|
+
})
|
|
1052
|
+
} else if (node.isText() && endOffset > 0) {
|
|
1053
|
+
result.push({
|
|
1054
|
+
node: node,
|
|
1055
|
+
offset: [0, endOffset]
|
|
1056
|
+
})
|
|
1057
|
+
}
|
|
1058
|
+
break
|
|
1059
|
+
}
|
|
1060
|
+
//不是起点和终点所在节点
|
|
1061
|
+
else {
|
|
1062
|
+
//包含终点
|
|
1063
|
+
if (node.isContains(endNode)) {
|
|
1064
|
+
//获取该节点最后一个可以设置光标的节点
|
|
1065
|
+
const lastSelectionNode = this.getLastSelectionNodeInChildren(node)!
|
|
1066
|
+
//如果终点所在节点是它最后一个可以设置光标的节点,并且光标在末尾处
|
|
1067
|
+
if (endNode.isEqual(lastSelectionNode) && endOffset == (endNode.isText() ? endNode.textContent!.length : 1)) {
|
|
1068
|
+
result.push({
|
|
1069
|
+
node,
|
|
1070
|
+
offset: false
|
|
1071
|
+
})
|
|
1072
|
+
break
|
|
1073
|
+
}
|
|
1074
|
+
//只有部分在选区内,处理子节点
|
|
1075
|
+
node = node.children![0]
|
|
1076
|
+
continue
|
|
1077
|
+
}
|
|
1078
|
+
//不包含终点,则说明节点完全在选区内
|
|
1079
|
+
result.push({
|
|
1080
|
+
node: node,
|
|
1081
|
+
offset: false
|
|
1082
|
+
})
|
|
1083
|
+
}
|
|
1084
|
+
//下一个节点
|
|
1085
|
+
let tempNode: KNode = node
|
|
1086
|
+
let nextNode: KNode | null = null
|
|
1087
|
+
while (true) {
|
|
1088
|
+
//获取下一个节点
|
|
1089
|
+
nextNode = tempNode.getNext(tempNode.parent ? tempNode.parent!.children! : this.stackNodes)
|
|
1090
|
+
if (nextNode || !tempNode.parent) {
|
|
1091
|
+
break
|
|
1092
|
+
}
|
|
1093
|
+
tempNode = tempNode.parent
|
|
1094
|
+
}
|
|
1095
|
+
if (nextNode) {
|
|
1096
|
+
node = nextNode
|
|
1097
|
+
continue
|
|
1098
|
+
}
|
|
1099
|
+
break
|
|
1100
|
+
}
|
|
1101
|
+
return result
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* 判断光标范围内的可聚焦节点是否全都在同一个符合条件节点内,如果是返回那个符合条件的节点,否则返回null
|
|
1106
|
+
*/
|
|
1107
|
+
getMatchNodeBySelection(options: KNodeMatchOptionType) {
|
|
1108
|
+
//没有聚焦
|
|
1109
|
+
if (!this.selection.focused()) {
|
|
1110
|
+
return null
|
|
1111
|
+
}
|
|
1112
|
+
//起点和终点在一起
|
|
1113
|
+
if (this.selection.collapsed()) {
|
|
1114
|
+
return this.selection.start!.node.getMatchNode(options)
|
|
1115
|
+
}
|
|
1116
|
+
//起点和终点不在一起的情况,获取所有可聚焦的节点
|
|
1117
|
+
const nodes = this.getFocusNodesBySelection('all')
|
|
1118
|
+
if (nodes.length == 0) {
|
|
1119
|
+
return null
|
|
1120
|
+
}
|
|
1121
|
+
//获取第一个可聚焦节点所在的符合条件的节点
|
|
1122
|
+
const matchNode = nodes[0].getMatchNode(options)
|
|
1123
|
+
//如果后续每个可聚焦节点都在该节点内,返回该节点
|
|
1124
|
+
if (matchNode && nodes.every(item => matchNode.isContains(item))) {
|
|
1125
|
+
return matchNode
|
|
1126
|
+
}
|
|
1127
|
+
return null
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* 判断光标范围内的可聚焦节点是否全都在符合条件的(不一定是同一个)节点内
|
|
1132
|
+
*/
|
|
1133
|
+
isSelectionNodesAllMatch(options: KNodeMatchOptionType) {
|
|
1134
|
+
//没有聚焦
|
|
1135
|
+
if (!this.selection.focused()) {
|
|
1136
|
+
return false
|
|
1137
|
+
}
|
|
1138
|
+
//起点和终点在一起
|
|
1139
|
+
if (this.selection.collapsed()) {
|
|
1140
|
+
return !!this.selection.start!.node.getMatchNode(options)
|
|
1141
|
+
}
|
|
1142
|
+
//起点和终点不在一起的情况,获取所有可聚焦的节点进行判断
|
|
1143
|
+
const focusNodes = this.getFocusNodesBySelection('all')
|
|
1144
|
+
if (focusNodes.length == 0) {
|
|
1145
|
+
return false
|
|
1146
|
+
}
|
|
1147
|
+
return focusNodes.every(item => !!item.getMatchNode(options))
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* 判断光标范围内是否有可聚焦节点在符合条件的节点内
|
|
1152
|
+
*/
|
|
1153
|
+
isSelectionNodesSomeMatch(options: KNodeMatchOptionType) {
|
|
1154
|
+
//没有聚焦
|
|
1155
|
+
if (!this.selection.focused()) {
|
|
1156
|
+
return false
|
|
1157
|
+
}
|
|
1158
|
+
//起点和终点在一起
|
|
1159
|
+
if (this.selection.collapsed()) {
|
|
1160
|
+
return !!this.selection.start!.node.getMatchNode(options)
|
|
1161
|
+
}
|
|
1162
|
+
//起点和终点不在一起的情况,获取所有可聚焦的节点进行判断
|
|
1163
|
+
const focusNodes = this.getFocusNodesBySelection('all')
|
|
1164
|
+
if (focusNodes.length == 0) {
|
|
1165
|
+
return false
|
|
1166
|
+
}
|
|
1167
|
+
return focusNodes.some(item => !!item.getMatchNode(options))
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* 获取所有在光标范围内的可聚焦节点,该方法拿到的可聚焦节点(文本)可能部分区域不在光标范围内
|
|
1172
|
+
*/
|
|
1173
|
+
getFocusNodesBySelection(type: 'all' | 'closed' | 'text' | undefined = 'all') {
|
|
1174
|
+
if (!this.selection.focused() || this.selection.collapsed()) {
|
|
1175
|
+
return []
|
|
1176
|
+
}
|
|
1177
|
+
const nodes: KNode[] = []
|
|
1178
|
+
this.getSelectedNodes().forEach(item => {
|
|
1179
|
+
nodes.push(...item.node.getFocusNodes(type))
|
|
1180
|
+
})
|
|
1181
|
+
return nodes
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* 获取所有在光标范围内的可聚焦节点,该方法可能会切割部分文本节点,摒弃其不在光标范围内的部分,所以也可能会更新光标的位置
|
|
1186
|
+
*/
|
|
1187
|
+
getFocusSplitNodesBySelection(type: 'all' | 'closed' | 'text' | undefined = 'all') {
|
|
1188
|
+
if (!this.selection.focused() || this.selection.collapsed()) {
|
|
1189
|
+
return []
|
|
1190
|
+
}
|
|
1191
|
+
const nodes: KNode[] = []
|
|
1192
|
+
this.getSelectedNodes().forEach(item => {
|
|
1193
|
+
//文本节点
|
|
1194
|
+
if (item.node.isText() && (type == 'all' || type == 'text')) {
|
|
1195
|
+
//选择部分文本
|
|
1196
|
+
if (item.offset) {
|
|
1197
|
+
const textContent = item.node.textContent!
|
|
1198
|
+
//选中了文本的前半段
|
|
1199
|
+
if (item.offset[0] == 0) {
|
|
1200
|
+
const newTextNode = item.node.clone(true)
|
|
1201
|
+
this.addNodeAfter(newTextNode, item.node)
|
|
1202
|
+
item.node.textContent = textContent.substring(0, item.offset[1])
|
|
1203
|
+
newTextNode.textContent = textContent.substring(item.offset[1])
|
|
1204
|
+
nodes.push(item.node)
|
|
1205
|
+
}
|
|
1206
|
+
//选中了文本的后半段
|
|
1207
|
+
else if (item.offset[1] == textContent.length) {
|
|
1208
|
+
const newTextNode = item.node.clone(true)
|
|
1209
|
+
this.addNodeBefore(newTextNode, item.node)
|
|
1210
|
+
newTextNode.textContent = textContent.substring(0, item.offset[0])
|
|
1211
|
+
item.node.textContent = textContent.substring(item.offset[0])
|
|
1212
|
+
nodes.push(item.node)
|
|
1213
|
+
}
|
|
1214
|
+
//选中文本中间部分
|
|
1215
|
+
else {
|
|
1216
|
+
const newBeforeTextNode = item.node.clone(true)
|
|
1217
|
+
const newAfterTextNode = item.node.clone(true)
|
|
1218
|
+
this.addNodeBefore(newBeforeTextNode, item.node)
|
|
1219
|
+
this.addNodeAfter(newAfterTextNode, item.node)
|
|
1220
|
+
newBeforeTextNode.textContent = textContent.substring(0, item.offset[0])
|
|
1221
|
+
item.node.textContent = textContent.substring(item.offset[0], item.offset[1])
|
|
1222
|
+
newAfterTextNode.textContent = textContent.substring(item.offset[1])
|
|
1223
|
+
nodes.push(item.node)
|
|
1224
|
+
}
|
|
1225
|
+
//重置光标位置
|
|
1226
|
+
if (this.isSelectionInNode(item.node, 'start')) {
|
|
1227
|
+
this.setSelectionBefore(item.node, 'start')
|
|
1228
|
+
}
|
|
1229
|
+
if (this.isSelectionInNode(item.node, 'end')) {
|
|
1230
|
+
this.setSelectionAfter(item.node, 'end')
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
//选择整个文本
|
|
1234
|
+
else {
|
|
1235
|
+
nodes.push(item.node)
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
//闭合节点
|
|
1239
|
+
else if (item.node.isClosed() && (type == 'all' || type == 'closed')) {
|
|
1240
|
+
nodes.push(item.node)
|
|
1241
|
+
}
|
|
1242
|
+
//非文本节点存在子节点数组
|
|
1243
|
+
else if (item.node.hasChildren()) {
|
|
1244
|
+
nodes.push(...item.node.getFocusNodes(type))
|
|
1245
|
+
}
|
|
1246
|
+
})
|
|
1247
|
+
return nodes
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* 向选区插入文本
|
|
1252
|
+
*/
|
|
1253
|
+
insertText(text: string) {
|
|
1254
|
+
if (!text) {
|
|
1255
|
+
return
|
|
1256
|
+
}
|
|
1257
|
+
if (!this.selection.focused()) {
|
|
1258
|
+
return
|
|
1259
|
+
}
|
|
1260
|
+
//统一将\r\n换成\n,解决Windows兼容问题
|
|
1261
|
+
text = text.replace(/\r\n/g, '\n')
|
|
1262
|
+
//起点和终点在一个位置
|
|
1263
|
+
if (this.selection.collapsed()) {
|
|
1264
|
+
const node = this.selection.start!.node
|
|
1265
|
+
const offset = this.selection.start!.offset
|
|
1266
|
+
//不是在拥有代码块样式的块级节点内,则将空格转换成
|
|
1267
|
+
if (!node.isInCodeBlockStyle()) {
|
|
1268
|
+
text = text.replace(/\s/g, () => {
|
|
1269
|
+
const span = document.createElement('span')
|
|
1270
|
+
span.innerHTML = ' '
|
|
1271
|
+
return span.innerText
|
|
1272
|
+
})
|
|
1273
|
+
}
|
|
1274
|
+
//光标所在节点是文本节点
|
|
1275
|
+
if (node.isText()) {
|
|
1276
|
+
node.textContent = node.textContent!.substring(0, offset) + text + node.textContent!.substring(offset)
|
|
1277
|
+
this.selection.start!.offset = this.selection.end!.offset = this.selection.start!.offset + text.length
|
|
1278
|
+
}
|
|
1279
|
+
//光标所在节点是闭合节点
|
|
1280
|
+
else {
|
|
1281
|
+
const textNode = KNode.create({
|
|
1282
|
+
type: 'text',
|
|
1283
|
+
textContent: text
|
|
1284
|
+
})
|
|
1285
|
+
offset == 0 ? this.addNodeBefore(textNode, node) : this.addNodeAfter(textNode, node)
|
|
1286
|
+
this.setSelectionAfter(textNode)
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
//起点和终点不在一个位置,即存在选区
|
|
1290
|
+
else {
|
|
1291
|
+
this.delete()
|
|
1292
|
+
this.insertText(text)
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* 向选区进行换行,如果所在块节点只有占位符并且块节点不是段落则会转为段落
|
|
1298
|
+
*/
|
|
1299
|
+
insertParagraph() {
|
|
1300
|
+
if (!this.selection.focused()) {
|
|
1301
|
+
return
|
|
1302
|
+
}
|
|
1303
|
+
//起点和终点在一起
|
|
1304
|
+
if (this.selection.collapsed()) {
|
|
1305
|
+
//光标所在节点
|
|
1306
|
+
const node = this.selection.start!.node
|
|
1307
|
+
//光标所在块节点
|
|
1308
|
+
const blockNode = node.getBlock()
|
|
1309
|
+
//如果在代码块样式内
|
|
1310
|
+
if (!!node.isInCodeBlockStyle()) {
|
|
1311
|
+
this.insertText('\n')
|
|
1312
|
+
const zeroWidthText = KNode.createZeroWidthText()
|
|
1313
|
+
this.insertNode(zeroWidthText)
|
|
1314
|
+
this.setSelectionAfter(zeroWidthText, 'all')
|
|
1315
|
+
if (typeof this.onInsertParagraph == 'function') {
|
|
1316
|
+
this.onInsertParagraph.apply(this, [blockNode])
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
//在非代码块样式内,且不是固定的块节点
|
|
1320
|
+
else if (!blockNode.fixed) {
|
|
1321
|
+
//块节点只有占位符,并且存在父节点,且父节点不是固定块节点
|
|
1322
|
+
if (blockNode.allIsPlaceholder() && blockNode.parent && !blockNode.parent.fixed) {
|
|
1323
|
+
//将块节点从父节点中抽离到父节点同级
|
|
1324
|
+
removeBlockFromParentToSameLevel.apply(this, [blockNode])
|
|
1325
|
+
//是否走默认逻辑
|
|
1326
|
+
const useDefault = typeof this.onDetachMentBlockFromParentCallback == 'function' ? this.onDetachMentBlockFromParentCallback.apply(this, [blockNode]) : true
|
|
1327
|
+
//走默认逻辑,将非段落的块节点转为段落
|
|
1328
|
+
if (useDefault && !this.isParagraph(blockNode)) {
|
|
1329
|
+
this.toParagraph(blockNode)
|
|
1330
|
+
}
|
|
1331
|
+
//触发换行事件
|
|
1332
|
+
if (typeof this.onInsertParagraph == 'function') {
|
|
1333
|
+
this.onInsertParagraph.apply(this, [blockNode])
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
//块节点只有占位符,并且不存在父节点,且不是段落
|
|
1337
|
+
else if (blockNode.allIsPlaceholder() && !blockNode.parent && !this.isParagraph(blockNode)) {
|
|
1338
|
+
//转为段落
|
|
1339
|
+
this.toParagraph(blockNode)
|
|
1340
|
+
//触发换行事件
|
|
1341
|
+
if (typeof this.onInsertParagraph == 'function') {
|
|
1342
|
+
this.onInsertParagraph.apply(this, [blockNode])
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
//其他情况正常换行
|
|
1346
|
+
else {
|
|
1347
|
+
handlerForNormalInsertParagraph.apply(this)
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
//起点和终点不在一起
|
|
1352
|
+
else {
|
|
1353
|
+
this.delete()
|
|
1354
|
+
this.insertParagraph()
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* 向选区插入节点,cover为true表示当向某个只有占位符的非固定块节点插入另一个非固定块节点时是否覆盖此节点,而不是直接插入进去
|
|
1360
|
+
*/
|
|
1361
|
+
insertNode(node: KNode, cover: boolean | undefined = false) {
|
|
1362
|
+
//未聚焦不处理
|
|
1363
|
+
if (!this.selection.focused()) {
|
|
1364
|
+
return
|
|
1365
|
+
}
|
|
1366
|
+
//空节点不处理
|
|
1367
|
+
if (node.isEmpty()) {
|
|
1368
|
+
return
|
|
1369
|
+
}
|
|
1370
|
+
//起点和终点在一起
|
|
1371
|
+
if (this.selection.collapsed()) {
|
|
1372
|
+
//光标所在节点
|
|
1373
|
+
const selectionNode = this.selection.start!.node
|
|
1374
|
+
//光标偏移值
|
|
1375
|
+
const offset = this.selection.start!.offset
|
|
1376
|
+
//光标所在节点的块节点
|
|
1377
|
+
const blockNode = selectionNode.getBlock()
|
|
1378
|
+
//块节点的第一个可设光标的节点
|
|
1379
|
+
const firstSelectionNode = this.getFirstSelectionNodeInChildren(blockNode)!
|
|
1380
|
+
//块节点的最后一个可设光标的节点
|
|
1381
|
+
const lastSelectionNode = this.getLastSelectionNodeInChildren(blockNode)!
|
|
1382
|
+
//光标所在块节点是非固定块节点,插入的也是非固定块节点
|
|
1383
|
+
if (!blockNode.fixed && node.isBlock() && !node.fixed) {
|
|
1384
|
+
//光标在代码块样式内则将该节点转为行内节点后重新执行插入
|
|
1385
|
+
if (!!selectionNode.isInCodeBlockStyle()) {
|
|
1386
|
+
node.type = 'inline'
|
|
1387
|
+
this.insertNode(node, cover)
|
|
1388
|
+
return
|
|
1389
|
+
}
|
|
1390
|
+
//光标所在块节点只有换行符且cover为true,则替换当前块节点
|
|
1391
|
+
if (blockNode.allIsPlaceholder() && cover) {
|
|
1392
|
+
this.addNodeBefore(node, blockNode)
|
|
1393
|
+
blockNode.toEmpty()
|
|
1394
|
+
}
|
|
1395
|
+
//光标在块节点的起始处,则在块节点之前插入
|
|
1396
|
+
else if (firstSelectionNode.isEqual(selectionNode) && offset == 0) {
|
|
1397
|
+
this.addNodeBefore(node, blockNode)
|
|
1398
|
+
}
|
|
1399
|
+
//光标在块节点的末尾处,则在块节点之后插入
|
|
1400
|
+
else if (lastSelectionNode.isEqual(selectionNode) && offset == (selectionNode.isText() ? selectionNode.textContent!.length : 1)) {
|
|
1401
|
+
this.addNodeAfter(node, blockNode)
|
|
1402
|
+
}
|
|
1403
|
+
//光标在块节点的中间部分则需要分割
|
|
1404
|
+
else {
|
|
1405
|
+
//执行换行
|
|
1406
|
+
this.insertParagraph()
|
|
1407
|
+
//获取换行后光标位置所在的块节点
|
|
1408
|
+
const newBlockNode = this.selection.start!.node.getBlock()
|
|
1409
|
+
//在新的块节点之前插入
|
|
1410
|
+
this.addNodeBefore(node, newBlockNode)
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
//其他情况
|
|
1414
|
+
else {
|
|
1415
|
+
//光标在文本节点或者闭合节点的起始处
|
|
1416
|
+
if (offset == 0) {
|
|
1417
|
+
this.addNodeBefore(node, selectionNode)
|
|
1418
|
+
}
|
|
1419
|
+
//光标在文本节点或者闭合节点的末尾处
|
|
1420
|
+
else if (offset == (selectionNode.isText() ? selectionNode.textContent!.length : 1)) {
|
|
1421
|
+
this.addNodeAfter(node, selectionNode)
|
|
1422
|
+
}
|
|
1423
|
+
//光标在文本节点内
|
|
1424
|
+
else {
|
|
1425
|
+
const val = selectionNode.textContent!
|
|
1426
|
+
const newTextNode = selectionNode.clone()
|
|
1427
|
+
selectionNode.textContent = val.substring(0, offset)
|
|
1428
|
+
newTextNode.textContent = val.substring(offset)
|
|
1429
|
+
this.addNodeAfter(newTextNode, selectionNode)
|
|
1430
|
+
this.addNodeBefore(node, newTextNode)
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
//重置光标
|
|
1434
|
+
this.setSelectionAfter(node, 'all')
|
|
1435
|
+
}
|
|
1436
|
+
//起点和终点不在一起
|
|
1437
|
+
else {
|
|
1438
|
+
this.delete()
|
|
1439
|
+
this.insertNode(node, cover)
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* 对选区进行删除
|
|
1445
|
+
*/
|
|
1446
|
+
delete() {
|
|
1447
|
+
if (!this.selection.focused()) {
|
|
1448
|
+
return
|
|
1449
|
+
}
|
|
1450
|
+
//起点和终点在一起
|
|
1451
|
+
if (this.selection.collapsed()) {
|
|
1452
|
+
const node = this.selection.start!.node
|
|
1453
|
+
const uneditableNode = node.getUneditable()
|
|
1454
|
+
//是用户操作的删除行为并且在不可编辑的节点里,则直接删除该不可编辑的节点
|
|
1455
|
+
if (this.isUserDelection && uneditableNode) {
|
|
1456
|
+
uneditableNode.toEmpty()
|
|
1457
|
+
}
|
|
1458
|
+
//否则走正常删除逻辑
|
|
1459
|
+
else {
|
|
1460
|
+
const offset = this.selection.start!.offset
|
|
1461
|
+
//前一个可设置光标的节点
|
|
1462
|
+
const previousSelectionNode = this.getPreviousSelectionNode(node)
|
|
1463
|
+
//光标所在的块节点
|
|
1464
|
+
const blockNode = node.getBlock()
|
|
1465
|
+
//光标在节点的起始处
|
|
1466
|
+
if (offset == 0) {
|
|
1467
|
+
//前一个可设置光标的节点存在
|
|
1468
|
+
if (previousSelectionNode) {
|
|
1469
|
+
//获取前一个可设置光标的节点所在的块节点
|
|
1470
|
+
const previousBlock = previousSelectionNode.getBlock()
|
|
1471
|
+
//前一个可设置光标的节点和光标所在节点都属于一个块节点,则表示当前光标所在节点不是块节点的起始处,则将光标移动到前一个可设置光标的节点的末尾处再执行一次删除
|
|
1472
|
+
if (previousBlock.isEqual(blockNode)) {
|
|
1473
|
+
this.setSelectionAfter(previousSelectionNode, 'all')
|
|
1474
|
+
this.delete()
|
|
1475
|
+
return
|
|
1476
|
+
}
|
|
1477
|
+
//光标在块节点的开始处并且块节点不是固定的
|
|
1478
|
+
else if (!blockNode.fixed) {
|
|
1479
|
+
//块节点的父节点存在,且父节点不包含前一个可设置光标的节点所在的块节点(块节点在父节点的第一个),且父节点不是固定的
|
|
1480
|
+
if (blockNode.parent && !blockNode.parent.isContains(previousBlock) && !blockNode.parent.fixed) {
|
|
1481
|
+
//将块节点从父节点中抽离到父节点同级
|
|
1482
|
+
removeBlockFromParentToSameLevel.apply(this, [blockNode])
|
|
1483
|
+
//是否走默认逻辑
|
|
1484
|
+
const useDefault = typeof this.onDetachMentBlockFromParentCallback == 'function' ? this.onDetachMentBlockFromParentCallback.apply(this, [blockNode]) : true
|
|
1485
|
+
//走默认逻辑,将非段落的块节点转为段落
|
|
1486
|
+
if (useDefault && !this.isParagraph(blockNode)) {
|
|
1487
|
+
this.toParagraph(blockNode)
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
//块节点不存在父节点,或者父节点包含前一个可设置光标的节点所在的块节点(块节点不是父节点的第一个)
|
|
1491
|
+
else if (!blockNode.parent || blockNode.parent.isContains(previousBlock)) {
|
|
1492
|
+
mergeBlock.apply(this, [previousBlock, blockNode])
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
//是固定的块节点就不用处理
|
|
1496
|
+
}
|
|
1497
|
+
//光标在编辑器开始处
|
|
1498
|
+
else {
|
|
1499
|
+
//光标所在的块节点存在非固定的父节点
|
|
1500
|
+
if (blockNode.parent && !blockNode.parent.fixed) {
|
|
1501
|
+
//将块节点从父节点中抽离到父节点同级
|
|
1502
|
+
removeBlockFromParentToSameLevel.apply(this, [blockNode])
|
|
1503
|
+
//是否走默认逻辑
|
|
1504
|
+
const useDefault = typeof this.onDetachMentBlockFromParentCallback == 'function' ? this.onDetachMentBlockFromParentCallback.apply(this, [blockNode]) : true
|
|
1505
|
+
//走默认逻辑,将非段落的块节点转为段落
|
|
1506
|
+
if (useDefault && !this.isParagraph(blockNode)) {
|
|
1507
|
+
this.toParagraph(blockNode)
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
//光标所在的块节点非段落节点,且非固定块节点,且不存在父节点
|
|
1511
|
+
else if (!this.isParagraph(blockNode) && !blockNode.fixed && !blockNode.parent) {
|
|
1512
|
+
//转为段落
|
|
1513
|
+
this.toParagraph(blockNode)
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
//光标在所在节点的内部
|
|
1518
|
+
else {
|
|
1519
|
+
//在空白文本节点内
|
|
1520
|
+
if (node.isZeroWidthText()) {
|
|
1521
|
+
//置空节点
|
|
1522
|
+
node.toEmpty()
|
|
1523
|
+
//在清除空白文本节点后判断块节点是否为空,是则建立占位符
|
|
1524
|
+
if (blockNode.isEmpty()) {
|
|
1525
|
+
const placeholderNode = KNode.createPlaceholder()
|
|
1526
|
+
this.addNode(placeholderNode, blockNode)
|
|
1527
|
+
this.setSelectionBefore(placeholderNode)
|
|
1528
|
+
}
|
|
1529
|
+
//块节点不是空,将光标移动到该节点前面
|
|
1530
|
+
else {
|
|
1531
|
+
this.selection.start!.node = this.selection.end!.node = node
|
|
1532
|
+
this.selection.start!.offset = this.selection.end!.offset = 0
|
|
1533
|
+
}
|
|
1534
|
+
//再执行一次删除
|
|
1535
|
+
this.delete()
|
|
1536
|
+
return
|
|
1537
|
+
}
|
|
1538
|
+
//在文本节点内
|
|
1539
|
+
else if (node.isText()) {
|
|
1540
|
+
//获取删除的字符
|
|
1541
|
+
const deleteChart = node.textContent!.substring(offset - 1, offset)
|
|
1542
|
+
//进行删除
|
|
1543
|
+
node.textContent = node.textContent!.substring(0, offset - 1) + node.textContent!.substring(offset)
|
|
1544
|
+
//更新光标到删除后的位置
|
|
1545
|
+
this.selection.start!.offset = offset - 1
|
|
1546
|
+
this.selection.end!.offset = offset - 1
|
|
1547
|
+
//删除的是空白字符,再次删除
|
|
1548
|
+
if (isZeroWidthText(deleteChart)) {
|
|
1549
|
+
this.delete()
|
|
1550
|
+
return
|
|
1551
|
+
}
|
|
1552
|
+
//块节点为空,创建占位符
|
|
1553
|
+
if (blockNode.isEmpty()) {
|
|
1554
|
+
const placeholderNode = KNode.createPlaceholder()
|
|
1555
|
+
this.addNode(placeholderNode, blockNode)
|
|
1556
|
+
this.setSelectionBefore(placeholderNode)
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
//在闭合节点内
|
|
1560
|
+
else if (node.isClosed()) {
|
|
1561
|
+
//是否占位符
|
|
1562
|
+
const isPlaceholder = node.isPlaceholder()
|
|
1563
|
+
//删除闭合节点
|
|
1564
|
+
node.toEmpty()
|
|
1565
|
+
//块节点为空
|
|
1566
|
+
if (blockNode.isEmpty()) {
|
|
1567
|
+
//删除的是占位符
|
|
1568
|
+
if (isPlaceholder) {
|
|
1569
|
+
//块节点是固定状态的,则创建占位符;
|
|
1570
|
+
if (blockNode.fixed) {
|
|
1571
|
+
const placeholderNode = KNode.createPlaceholder()
|
|
1572
|
+
this.addNode(placeholderNode, blockNode)
|
|
1573
|
+
this.setSelectionBefore(placeholderNode)
|
|
1574
|
+
}
|
|
1575
|
+
//块节点不是固定状态的,且前一个可获取光标的节点不存在则说明光标在编辑器起始处,创建占位符
|
|
1576
|
+
else if (!blockNode.fixed && !previousSelectionNode) {
|
|
1577
|
+
const placeholderNode = KNode.createPlaceholder()
|
|
1578
|
+
this.addNode(placeholderNode, blockNode)
|
|
1579
|
+
this.setSelectionBefore(placeholderNode)
|
|
1580
|
+
//转为段落
|
|
1581
|
+
blockNode.tag = this.blockRenderTag
|
|
1582
|
+
blockNode.marks = {}
|
|
1583
|
+
blockNode.styles = {}
|
|
1584
|
+
}
|
|
1585
|
+
//其余情况就是块节点被删除,光标自动更新到附近位置
|
|
1586
|
+
}
|
|
1587
|
+
//删除的不是占位符
|
|
1588
|
+
else {
|
|
1589
|
+
const placeholderNode = KNode.createPlaceholder()
|
|
1590
|
+
this.addNode(placeholderNode, blockNode)
|
|
1591
|
+
this.setSelectionBefore(placeholderNode)
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
//起点和终点不在一起
|
|
1599
|
+
else {
|
|
1600
|
+
//获取选区内的节点信息
|
|
1601
|
+
const result = this.getSelectedNodes().filter(item => {
|
|
1602
|
+
//批量删除时需要过滤掉那些不显示的节点
|
|
1603
|
+
return !item.node.void
|
|
1604
|
+
})
|
|
1605
|
+
//起点所在块节点
|
|
1606
|
+
const startBlockNode = this.selection.start!.node.getBlock()
|
|
1607
|
+
//终点所在块节点
|
|
1608
|
+
const endBlockNode = this.selection.end!.node.getBlock()
|
|
1609
|
+
result.forEach(item => {
|
|
1610
|
+
const { node, offset } = item
|
|
1611
|
+
//是数组的情况,说明node是文本节点,需要进行裁剪
|
|
1612
|
+
if (offset) {
|
|
1613
|
+
node.textContent = node.textContent!.substring(0, offset[0]) + node.textContent!.substring(offset[1])
|
|
1614
|
+
}
|
|
1615
|
+
//说明节点都在选区内
|
|
1616
|
+
else {
|
|
1617
|
+
//固定状态的块节点,进行清空处理
|
|
1618
|
+
if (node.isBlock() && node.fixed) {
|
|
1619
|
+
emptyFixedBlock.apply(this, [node])
|
|
1620
|
+
}
|
|
1621
|
+
//其他节点置空进行删除
|
|
1622
|
+
else {
|
|
1623
|
+
node.toEmpty()
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
})
|
|
1627
|
+
//起点和终点在同一个块节点下
|
|
1628
|
+
if (startBlockNode.isEqual(endBlockNode)) {
|
|
1629
|
+
//块节点为空创建占位符
|
|
1630
|
+
if (startBlockNode.isEmpty()) {
|
|
1631
|
+
const placeholder = KNode.createPlaceholder()
|
|
1632
|
+
this.addNode(placeholder, startBlockNode)
|
|
1633
|
+
this.setSelectionBefore(placeholder)
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
//起点和终点不在一个块节点的情况下,需要考虑块节点合并
|
|
1637
|
+
else {
|
|
1638
|
+
//起点所在块节点为空,创建占位符
|
|
1639
|
+
if (startBlockNode.isEmpty()) {
|
|
1640
|
+
const placeholder = KNode.createPlaceholder()
|
|
1641
|
+
this.addNode(placeholder, startBlockNode)
|
|
1642
|
+
this.setSelectionBefore(placeholder, 'start')
|
|
1643
|
+
}
|
|
1644
|
+
//终点所在块节点为空,创建占位符
|
|
1645
|
+
if (endBlockNode.isEmpty()) {
|
|
1646
|
+
const placeholder = KNode.createPlaceholder()
|
|
1647
|
+
this.addNode(placeholder, endBlockNode)
|
|
1648
|
+
this.setSelectionBefore(placeholder, 'end')
|
|
1649
|
+
}
|
|
1650
|
+
//不是固定的块节点
|
|
1651
|
+
if (!endBlockNode.fixed) {
|
|
1652
|
+
mergeBlock.apply(this, [startBlockNode, endBlockNode])
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
//如果起点所在节点为空则更新起点
|
|
1657
|
+
if (this.selection.start!.node.isEmpty()) {
|
|
1658
|
+
this.updateSelectionRecently('start')
|
|
1659
|
+
}
|
|
1660
|
+
//合并起点和终点
|
|
1661
|
+
this.selection.end!.node = this.selection.start!.node
|
|
1662
|
+
this.selection.end!.offset = this.selection.start!.offset
|
|
1663
|
+
//触发事件
|
|
1664
|
+
if (typeof this.onDeleteComplete == 'function') {
|
|
1665
|
+
this.onDeleteComplete.apply(this)
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* 更新编辑器视图
|
|
1671
|
+
*/
|
|
1672
|
+
async updateView(updateRealSelection: boolean | undefined = true, unPushHistory: boolean | undefined = false) {
|
|
1673
|
+
if (!this.$el) {
|
|
1674
|
+
return
|
|
1675
|
+
}
|
|
1676
|
+
//视图更新前回调
|
|
1677
|
+
if (typeof this.beforeUpdateView == 'function') this.beforeUpdateView.apply(this)
|
|
1678
|
+
//克隆旧节点数组,防止在patch过程中旧节点数组中存在null,影响后续的视图更新
|
|
1679
|
+
const oldStackNodes = this.oldStackNodes.map(item => item.fullClone())
|
|
1680
|
+
//这里存放格式化过的节点数组,后续进行判断避免重复格式化造成资源浪费和性能问题
|
|
1681
|
+
const hasUpdateNodes: KNode[] = []
|
|
1682
|
+
//对编辑器的新旧节点数组进行比对,遍历比对的结果进行动态格式化
|
|
1683
|
+
patchNodes(this.stackNodes, oldStackNodes).forEach(item => {
|
|
1684
|
+
//需要进行格式化的节点
|
|
1685
|
+
let node: KNode | null = null
|
|
1686
|
+
//有新节点表示insert、update、replace、move和empty
|
|
1687
|
+
if (item.newNode) {
|
|
1688
|
+
//优先从父节点开始格式化
|
|
1689
|
+
node = item.newNode.parent ? item.newNode.parent : item.newNode
|
|
1690
|
+
}
|
|
1691
|
+
//没有新节点但是有旧节点,表示remove,如果是根级块节点remove就不用处理,非根级块节点需要对新数组中的父节点进行格式化
|
|
1692
|
+
else if (item.oldNode && item.oldNode.parent) {
|
|
1693
|
+
//获取新节点数组中的父节点
|
|
1694
|
+
const parentNode = KNode.searchByKey(item.oldNode.parent.key, this.stackNodes)
|
|
1695
|
+
//如果存在对父节点进行格式化
|
|
1696
|
+
node = parentNode ? parentNode : null
|
|
1697
|
+
}
|
|
1698
|
+
//是否有需要格式化的节点
|
|
1699
|
+
if (node) {
|
|
1700
|
+
//针对需要格式化的节点存在一些特殊处理
|
|
1701
|
+
if (typeof this.beforePatchNodeToFormat == 'function') {
|
|
1702
|
+
node = this.beforePatchNodeToFormat.apply(this, [node])
|
|
1703
|
+
}
|
|
1704
|
+
//判断该节点是否格式化过
|
|
1705
|
+
const hasUpdate = hasUpdateNodes.some(n => {
|
|
1706
|
+
//已经在hasUpdateNodes数组里了,说明格式化过了
|
|
1707
|
+
if (n.isContains(node!)) {
|
|
1708
|
+
return true
|
|
1709
|
+
}
|
|
1710
|
+
//需要格式化的节点有父节点,并且hasUpdateNodes也存在同父节点的节点
|
|
1711
|
+
if (node!.parent && n.parent && n.parent.isContains(node!.parent)) {
|
|
1712
|
+
return true
|
|
1713
|
+
}
|
|
1714
|
+
//其他情况说明没有格式化过
|
|
1715
|
+
return false
|
|
1716
|
+
})
|
|
1717
|
+
//没有格式化过则进行格式化
|
|
1718
|
+
if (!hasUpdate) {
|
|
1719
|
+
//加入到已经格式化的节点数组里
|
|
1720
|
+
hasUpdateNodes.push(node)
|
|
1721
|
+
//格式化
|
|
1722
|
+
this.formatRules.forEach(rule => {
|
|
1723
|
+
let nodes: KNode[] = []
|
|
1724
|
+
if (node!.parent) {
|
|
1725
|
+
nodes = node!.parent.children!
|
|
1726
|
+
} else {
|
|
1727
|
+
//没有父节点的根级节点默认格式化当前节点的前后加起来三个节点
|
|
1728
|
+
const previousNode = node!.getPrevious(this.stackNodes)
|
|
1729
|
+
const nextNode = node!.getNext(this.stackNodes)
|
|
1730
|
+
if (previousNode) {
|
|
1731
|
+
nodes.push(previousNode)
|
|
1732
|
+
}
|
|
1733
|
+
nodes.push(node!)
|
|
1734
|
+
if (nextNode) {
|
|
1735
|
+
nodes.push(nextNode)
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
const sourceNodes = node!.parent ? node!.parent.children! : this.stackNodes
|
|
1739
|
+
//需要格式化的节点存在于源数组内才需要进行格式化
|
|
1740
|
+
if (sourceNodes.some(n => n.isEqual(node!))) {
|
|
1741
|
+
formatNodes.apply(this, [rule, nodes, sourceNodes])
|
|
1742
|
+
}
|
|
1743
|
+
})
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
})
|
|
1747
|
+
//判断节点数组是否为空进行初始化
|
|
1748
|
+
checkNodes.apply(this)
|
|
1749
|
+
//设置placeholder
|
|
1750
|
+
setPlaceholder.apply(this)
|
|
1751
|
+
//旧的html值
|
|
1752
|
+
const oldHtml = this.$el.innerHTML
|
|
1753
|
+
//视图更新之前取消dom监听,以免干扰更新dom
|
|
1754
|
+
removeDomObserve(this)
|
|
1755
|
+
//此处进行视图的更新
|
|
1756
|
+
const useDefault = typeof this.onUpdateView == 'function' ? await this.onUpdateView.apply(this, [false]) : true
|
|
1757
|
+
//使用默认逻辑
|
|
1758
|
+
useDefault && defaultUpdateView.apply(this, [false])
|
|
1759
|
+
//视图更新完毕后重新设置dom监听
|
|
1760
|
+
setDomObserve(this)
|
|
1761
|
+
//新的html值
|
|
1762
|
+
const newHtml = this.$el.innerHTML
|
|
1763
|
+
//html值发生变化
|
|
1764
|
+
if (oldHtml != newHtml) {
|
|
1765
|
+
if (typeof this.onChange == 'function') {
|
|
1766
|
+
this.onChange.apply(this, [newHtml, oldHtml])
|
|
1767
|
+
}
|
|
1768
|
+
//如果unPushHistory为false,则加入历史记录
|
|
1769
|
+
if (!unPushHistory) {
|
|
1770
|
+
this.history.setState(this.stackNodes, this.selection)
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
//更新旧节点数组
|
|
1774
|
+
this.oldStackNodes = this.stackNodes.map(item => item.fullClone())
|
|
1775
|
+
//此处进行光标的渲染
|
|
1776
|
+
if (updateRealSelection) await this.updateRealSelection()
|
|
1777
|
+
//视图更新后回调
|
|
1778
|
+
if (typeof this.afterUpdateView == 'function') this.afterUpdateView.apply(this)
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* 根据selection更新编辑器真实光标
|
|
1783
|
+
*/
|
|
1784
|
+
async updateRealSelection() {
|
|
1785
|
+
const realSelection = window.getSelection()
|
|
1786
|
+
if (!realSelection) {
|
|
1787
|
+
return
|
|
1788
|
+
}
|
|
1789
|
+
this.internalCauseSelectionChange = true
|
|
1790
|
+
//聚焦情况下
|
|
1791
|
+
if (this.selection.focused()) {
|
|
1792
|
+
//先进行纠正
|
|
1793
|
+
redressSelection.apply(this)
|
|
1794
|
+
//更新真实光标
|
|
1795
|
+
const range = document.createRange()
|
|
1796
|
+
const startDom = this.findDom(this.selection.start!.node)
|
|
1797
|
+
const endDom = this.findDom(this.selection.end!.node)
|
|
1798
|
+
//起点所在节点是文本节点
|
|
1799
|
+
if (this.selection.start!.node.isText()) {
|
|
1800
|
+
range.setStart(startDom.childNodes[0], this.selection.start!.offset)
|
|
1801
|
+
}
|
|
1802
|
+
//起点所在节点是闭合节点
|
|
1803
|
+
else if (this.selection.start!.node.isClosed()) {
|
|
1804
|
+
const index = this.selection.start!.node.parent!.children!.findIndex(item => this.selection.start!.node.isEqual(item))
|
|
1805
|
+
range.setStart(startDom.parentNode!, this.selection.start!.offset + index)
|
|
1806
|
+
}
|
|
1807
|
+
//终点所在节点是文本节点
|
|
1808
|
+
if (this.selection.end!.node.isText()) {
|
|
1809
|
+
range.setEnd(endDom.childNodes[0], this.selection.end!.offset)
|
|
1810
|
+
}
|
|
1811
|
+
//终点所在节点是闭合节点
|
|
1812
|
+
else if (this.selection.end!.node.isClosed()) {
|
|
1813
|
+
const index = this.selection.end!.node.parent!.children!.findIndex(item => this.selection.end!.node.isEqual(item))
|
|
1814
|
+
range.setEnd(endDom.parentNode!, this.selection.end!.offset + index)
|
|
1815
|
+
}
|
|
1816
|
+
//设置真实光标
|
|
1817
|
+
realSelection.removeAllRanges()
|
|
1818
|
+
realSelection.addRange(range)
|
|
1819
|
+
}
|
|
1820
|
+
//没有聚焦
|
|
1821
|
+
else {
|
|
1822
|
+
realSelection.removeAllRanges()
|
|
1823
|
+
}
|
|
1824
|
+
await delay()
|
|
1825
|
+
this.internalCauseSelectionChange = false
|
|
1826
|
+
this.scrollViewToSelection()
|
|
1827
|
+
this.history.updateSelection(this.selection)
|
|
1828
|
+
if (typeof this.onSelectionUpdate == 'function') {
|
|
1829
|
+
this.onSelectionUpdate.apply(this, [this.selection])
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* 重新渲染编辑器视图,不会触发onChange
|
|
1835
|
+
*/
|
|
1836
|
+
async review(value: string) {
|
|
1837
|
+
//视图更新前回调
|
|
1838
|
+
if (typeof this.beforeUpdateView == 'function') this.beforeUpdateView.apply(this)
|
|
1839
|
+
//根据value设置节点数组
|
|
1840
|
+
this.stackNodes = this.htmlParseNode(value || '')
|
|
1841
|
+
//将节点数组进行格式化
|
|
1842
|
+
this.formatRules.forEach(rule => {
|
|
1843
|
+
formatNodes.apply(this, [rule, this.stackNodes, this.stackNodes])
|
|
1844
|
+
})
|
|
1845
|
+
//初始化检查节点数组
|
|
1846
|
+
checkNodes.apply(this)
|
|
1847
|
+
//设置placeholder
|
|
1848
|
+
setPlaceholder.apply(this)
|
|
1849
|
+
//视图更新之前取消dom监听,以免干扰更新dom
|
|
1850
|
+
removeDomObserve(this)
|
|
1851
|
+
//进行视图的渲染
|
|
1852
|
+
const useDefault = typeof this.onUpdateView == 'function' ? await this.onUpdateView.apply(this, [true]) : true
|
|
1853
|
+
//使用默认逻辑
|
|
1854
|
+
if (useDefault) defaultUpdateView.apply(this, [true])
|
|
1855
|
+
//视图更新完毕后重新设置dom监听
|
|
1856
|
+
setDomObserve(this)
|
|
1857
|
+
//设置历史记录
|
|
1858
|
+
this.history.setState(this.stackNodes, this.selection)
|
|
1859
|
+
//更新旧节点数组
|
|
1860
|
+
this.oldStackNodes = this.stackNodes.map(item => item.fullClone())
|
|
1861
|
+
//视图更新后回调
|
|
1862
|
+
if (typeof this.afterUpdateView == 'function') this.afterUpdateView.apply(this)
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/**
|
|
1866
|
+
* 销毁编辑器的方法
|
|
1867
|
+
*/
|
|
1868
|
+
destroy() {
|
|
1869
|
+
//去除可编辑效果
|
|
1870
|
+
this.setEditable(false)
|
|
1871
|
+
//移除相关监听事件
|
|
1872
|
+
DapEvent.off(document, `selectionchange.kaitify_${this.guid}`)
|
|
1873
|
+
DapEvent.off(this.$el!, 'beforeinput.kaitify compositionstart.kaitify compositionupdate.kaitify compositionend.kaitify keydown.kaitify keyup.kaitify copy.kaitify focus.kaitify blur.kaitify')
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* 获取编辑器的纯文本内容
|
|
1878
|
+
*/
|
|
1879
|
+
getText() {
|
|
1880
|
+
if (!this.$el) {
|
|
1881
|
+
return ''
|
|
1882
|
+
}
|
|
1883
|
+
return this.$el.innerText
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* 配置编辑器,返回创建的编辑器
|
|
1888
|
+
*/
|
|
1889
|
+
static async configure(options: EditorConfigureOptionType) {
|
|
1890
|
+
//创建编辑器
|
|
1891
|
+
const editor = new Editor()
|
|
1892
|
+
//初始化编辑器dom
|
|
1893
|
+
editor.$el = initEditorDom(options.el)
|
|
1894
|
+
//初始化设置编辑器样式
|
|
1895
|
+
editor.$el.classList.add('Kaitify')
|
|
1896
|
+
//设置是否深色模式
|
|
1897
|
+
if (options.dark) editor.setDark(options.dark)
|
|
1898
|
+
//初始化设置编辑器默认提示文本
|
|
1899
|
+
if (options.placeholder) editor.$el.setAttribute('kaitify-placeholder', options.placeholder)
|
|
1900
|
+
//初始化内部属性
|
|
1901
|
+
if (typeof options.allowCopy == 'boolean') editor.allowCopy = options.allowCopy
|
|
1902
|
+
if (typeof options.allowCut == 'boolean') editor.allowCut = options.allowCut
|
|
1903
|
+
if (typeof options.allowPaste == 'boolean') editor.allowPaste = options.allowPaste
|
|
1904
|
+
if (typeof options.allowPasteHtml == 'boolean') editor.allowPasteHtml = options.allowPasteHtml
|
|
1905
|
+
if (typeof options.priorityPasteFiles == 'boolean') editor.priorityPasteFiles = options.priorityPasteFiles
|
|
1906
|
+
if (options.textRenderTag) editor.textRenderTag = options.textRenderTag
|
|
1907
|
+
if (options.blockRenderTag) editor.blockRenderTag = options.blockRenderTag
|
|
1908
|
+
if (options.emptyRenderTags) editor.emptyRenderTags = [...editor.emptyRenderTags, ...options.emptyRenderTags]
|
|
1909
|
+
if (options.extraKeepTags) editor.extraKeepTags = [...editor.extraKeepTags, ...options.extraKeepTags]
|
|
1910
|
+
if (options.extensions) editor.extensions = mergeExtensions.apply(editor, [options.extensions])
|
|
1911
|
+
if (options.formatRules) editor.formatRules = [...options.formatRules, ...editor.formatRules]
|
|
1912
|
+
if (options.domParseNodeCallback) editor.domParseNodeCallback = options.domParseNodeCallback
|
|
1913
|
+
if (options.onUpdateView) editor.onUpdateView = options.onUpdateView
|
|
1914
|
+
if (options.onPasteText) editor.onPasteText = options.onPasteText
|
|
1915
|
+
if (options.onPasteHtml) editor.onPasteHtml = options.onPasteHtml
|
|
1916
|
+
if (options.onPasteImage) editor.onPasteImage = options.onPasteImage
|
|
1917
|
+
if (options.onPasteVideo) editor.onPasteVideo = options.onPasteVideo
|
|
1918
|
+
if (options.onPasteFile) editor.onPasteFile = options.onPasteFile
|
|
1919
|
+
if (options.onChange) editor.onChange = options.onChange
|
|
1920
|
+
if (options.onSelectionUpdate) editor.onSelectionUpdate = options.onSelectionUpdate
|
|
1921
|
+
if (options.onInsertParagraph) editor.onInsertParagraph = options.onInsertParagraph
|
|
1922
|
+
if (options.onDeleteComplete) editor.onDeleteComplete = options.onDeleteComplete
|
|
1923
|
+
if (options.onKeydown) editor.onKeydown = options.onKeydown
|
|
1924
|
+
if (options.onKeyup) editor.onKeyup = options.onKeyup
|
|
1925
|
+
if (options.onFocus) editor.onFocus = options.onFocus
|
|
1926
|
+
if (options.onBlur) editor.onBlur = options.onBlur
|
|
1927
|
+
if (options.pasteKeepMarks) editor.pasteKeepMarks = options.pasteKeepMarks
|
|
1928
|
+
if (options.pasteKeepStyles) editor.pasteKeepStyles = options.pasteKeepStyles
|
|
1929
|
+
if (options.beforeUpdateView) editor.beforeUpdateView = options.beforeUpdateView
|
|
1930
|
+
if (options.afterUpdateView) editor.afterUpdateView = options.afterUpdateView
|
|
1931
|
+
if (options.onDetachMentBlockFromParentCallback) editor.onDetachMentBlockFromParentCallback = options.onDetachMentBlockFromParentCallback
|
|
1932
|
+
if (options.beforePatchNodeToFormat) editor.beforePatchNodeToFormat = options.beforePatchNodeToFormat
|
|
1933
|
+
//注册扩展
|
|
1934
|
+
editor.extensions.forEach(item => registerExtension.apply(editor, [item]))
|
|
1935
|
+
//设置编辑器是否可编辑
|
|
1936
|
+
editor.setEditable(typeof options.editable == 'boolean' ? options.editable : true)
|
|
1937
|
+
//视图渲染
|
|
1938
|
+
await editor.review(options.value || '')
|
|
1939
|
+
//自动聚焦
|
|
1940
|
+
if (options.autofocus) {
|
|
1941
|
+
editor.setSelectionAfter()
|
|
1942
|
+
await editor.updateRealSelection()
|
|
1943
|
+
}
|
|
1944
|
+
//监听js selection更新Selection
|
|
1945
|
+
DapEvent.on(document, `selectionchange.kaitify_${editor.guid}`, onSelectionChange.bind(editor))
|
|
1946
|
+
//监听内容输入
|
|
1947
|
+
DapEvent.on(editor.$el, 'beforeinput.kaitify', onBeforeInput.bind(editor))
|
|
1948
|
+
//监听中文输入
|
|
1949
|
+
DapEvent.on(editor.$el, 'compositionstart.kaitify compositionupdate.kaitify compositionend.kaitify', onComposition.bind(editor))
|
|
1950
|
+
//监听键盘事件
|
|
1951
|
+
DapEvent.on(editor.$el, 'keydown.kaitify keyup.kaitify', onKeyboard.bind(editor))
|
|
1952
|
+
//监听编辑器获取焦点
|
|
1953
|
+
DapEvent.on(editor.$el, 'focus.kaitify', onFocus.bind(editor))
|
|
1954
|
+
//监听编辑器失去焦点
|
|
1955
|
+
DapEvent.on(editor.$el, 'blur.kaitify', onBlur.bind(editor))
|
|
1956
|
+
//监听编辑器复制
|
|
1957
|
+
DapEvent.on(editor.$el, 'copy.kaitify', onCopy.bind(editor))
|
|
1958
|
+
//监听编辑器剪切
|
|
1959
|
+
DapEvent.on(editor.$el, 'cut.kaitify', onCut.bind(editor))
|
|
1960
|
+
//返回编辑器实例
|
|
1961
|
+
return editor
|
|
1962
|
+
}
|
|
1963
|
+
}
|