@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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/examples/App.vue +342 -0
  4. package/examples/content.js +1 -0
  5. package/examples/main.ts +4 -0
  6. package/examples/test.html +23 -0
  7. package/lib/extensions/Extension.d.ts +172 -0
  8. package/lib/extensions/align/index.d.ts +10 -0
  9. package/lib/extensions/attachment/index.d.ts +29 -0
  10. package/lib/extensions/back-color/index.d.ts +9 -0
  11. package/lib/extensions/blockquote/index.d.ts +12 -0
  12. package/lib/extensions/bold/index.d.ts +9 -0
  13. package/lib/extensions/code/index.d.ts +12 -0
  14. package/lib/extensions/code-block/hljs.d.ts +12 -0
  15. package/lib/extensions/code-block/index.d.ts +15 -0
  16. package/lib/extensions/color/index.d.ts +9 -0
  17. package/lib/extensions/font-family/index.d.ts +9 -0
  18. package/lib/extensions/font-size/index.d.ts +9 -0
  19. package/lib/extensions/heading/index.d.ts +13 -0
  20. package/lib/extensions/history/index.d.ts +10 -0
  21. package/lib/extensions/horizontal/index.d.ts +7 -0
  22. package/lib/extensions/image/index.d.ts +26 -0
  23. package/lib/extensions/indent/index.d.ts +8 -0
  24. package/lib/extensions/index.d.ts +29 -0
  25. package/lib/extensions/italic/index.d.ts +9 -0
  26. package/lib/extensions/line-height/index.d.ts +9 -0
  27. package/lib/extensions/link/index.d.ts +27 -0
  28. package/lib/extensions/list/index.d.ts +18 -0
  29. package/lib/extensions/math/index.d.ts +11 -0
  30. package/lib/extensions/strikethrough/index.d.ts +9 -0
  31. package/lib/extensions/subscript/index.d.ts +9 -0
  32. package/lib/extensions/superscript/index.d.ts +9 -0
  33. package/lib/extensions/table/index.d.ts +21 -0
  34. package/lib/extensions/task/index.d.ts +12 -0
  35. package/lib/extensions/text/index.d.ts +14 -0
  36. package/lib/extensions/underline/index.d.ts +9 -0
  37. package/lib/extensions/video/index.d.ts +27 -0
  38. package/lib/index.d.ts +3 -0
  39. package/lib/kaitify-core.es.js +38337 -0
  40. package/lib/kaitify-core.umd.js +2 -0
  41. package/lib/model/Editor.d.ts +504 -0
  42. package/lib/model/History.d.ts +42 -0
  43. package/lib/model/KNode.d.ts +258 -0
  44. package/lib/model/Selection.d.ts +29 -0
  45. package/lib/model/config/dom-observe.d.ts +10 -0
  46. package/lib/model/config/event-handler.d.ts +33 -0
  47. package/lib/model/config/format-patch.d.ts +25 -0
  48. package/lib/model/config/format-rules.d.ts +37 -0
  49. package/lib/model/config/function.d.ts +84 -0
  50. package/lib/model/index.d.ts +6 -0
  51. package/lib/tools/index.d.ts +49 -0
  52. package/lib/view/index.d.ts +21 -0
  53. package/lib/view/js-render/dom-patch.d.ts +65 -0
  54. package/lib/view/js-render/index.d.ts +5 -0
  55. package/package.json +52 -0
  56. package/src/css/style.less +56 -0
  57. package/src/css/var.less +45 -0
  58. package/src/extensions/Extension.ts +200 -0
  59. package/src/extensions/align/index.ts +115 -0
  60. package/src/extensions/attachment/icon.svg +1 -0
  61. package/src/extensions/attachment/index.ts +293 -0
  62. package/src/extensions/attachment/style.less +25 -0
  63. package/src/extensions/back-color/index.ts +56 -0
  64. package/src/extensions/blockquote/index.ts +144 -0
  65. package/src/extensions/blockquote/style.less +16 -0
  66. package/src/extensions/bold/index.ts +77 -0
  67. package/src/extensions/code/index.ts +295 -0
  68. package/src/extensions/code/style.less +14 -0
  69. package/src/extensions/code-block/hljs.less +183 -0
  70. package/src/extensions/code-block/hljs.ts +95 -0
  71. package/src/extensions/code-block/index.ts +308 -0
  72. package/src/extensions/code-block/style.less +20 -0
  73. package/src/extensions/color/index.ts +56 -0
  74. package/src/extensions/font-family/index.ts +80 -0
  75. package/src/extensions/font-size/index.ts +56 -0
  76. package/src/extensions/heading/index.ts +164 -0
  77. package/src/extensions/heading/style.less +42 -0
  78. package/src/extensions/history/index.ts +96 -0
  79. package/src/extensions/horizontal/index.ts +45 -0
  80. package/src/extensions/horizontal/style.less +13 -0
  81. package/src/extensions/image/index.ts +242 -0
  82. package/src/extensions/image/style.less +8 -0
  83. package/src/extensions/indent/index.ts +98 -0
  84. package/src/extensions/index.ts +29 -0
  85. package/src/extensions/italic/index.ts +77 -0
  86. package/src/extensions/line-height/index.ts +113 -0
  87. package/src/extensions/link/index.ts +184 -0
  88. package/src/extensions/link/style.less +19 -0
  89. package/src/extensions/list/index.ts +410 -0
  90. package/src/extensions/list/style.less +19 -0
  91. package/src/extensions/math/index.ts +233 -0
  92. package/src/extensions/math/style.less +21 -0
  93. package/src/extensions/strikethrough/index.ts +78 -0
  94. package/src/extensions/subscript/index.ts +77 -0
  95. package/src/extensions/superscript/index.ts +77 -0
  96. package/src/extensions/table/index.ts +1148 -0
  97. package/src/extensions/table/style.less +71 -0
  98. package/src/extensions/task/index.ts +243 -0
  99. package/src/extensions/task/style.less +59 -0
  100. package/src/extensions/text/index.ts +359 -0
  101. package/src/extensions/underline/index.ts +78 -0
  102. package/src/extensions/video/index.ts +273 -0
  103. package/src/extensions/video/style.less +8 -0
  104. package/src/index.ts +9 -0
  105. package/src/model/Editor.ts +1963 -0
  106. package/src/model/History.ts +115 -0
  107. package/src/model/KNode.ts +677 -0
  108. package/src/model/Selection.ts +39 -0
  109. package/src/model/config/dom-observe.ts +184 -0
  110. package/src/model/config/event-handler.ts +237 -0
  111. package/src/model/config/format-patch.ts +215 -0
  112. package/src/model/config/format-rules.ts +218 -0
  113. package/src/model/config/function.ts +1018 -0
  114. package/src/model/index.ts +6 -0
  115. package/src/tools/index.ts +156 -0
  116. package/src/view/index.ts +46 -0
  117. package/src/view/js-render/dom-patch.ts +324 -0
  118. package/src/view/js-render/index.ts +210 -0
  119. package/vite-env.d.ts +2 -0
@@ -0,0 +1,359 @@
1
+ import { common as DapCommon, string as DapString, color as DapColor } from 'dap-util'
2
+ import { KNode, KNodeMarksType, KNodeStylesType } from '@/model'
3
+ import { Extension } from '../Extension'
4
+
5
+ declare module '../../model' {
6
+ interface EditorCommandsType {
7
+ isTextStyle?: (styleName: string, styleValue?: string | number) => boolean
8
+ isTextMark?: (markName: string, markValue?: string | number) => boolean
9
+ setTextStyle?: (styles: KNodeStylesType, updateView?: boolean) => Promise<void>
10
+ setTextMark?: (marks: KNodeMarksType, updateView?: boolean) => Promise<void>
11
+ removeTextStyle?: (styleNames?: string[], updateView?: boolean) => Promise<void>
12
+ removeTextMark?: (markNames?: string[], updateView?: boolean) => Promise<void>
13
+ clearFormat?: () => Promise<void>
14
+ }
15
+ }
16
+
17
+ /**
18
+ * 移除单个文本节点的标记
19
+ */
20
+ const removeTextNodeMarks = (node: KNode, markNames?: string[]) => {
21
+ //删除指定标记
22
+ if (markNames && node.hasMarks()) {
23
+ const marks: KNodeMarksType = {}
24
+ Object.keys(node.marks!).forEach(key => {
25
+ if (!markNames.includes(key)) {
26
+ marks[key] = node.marks![key]
27
+ }
28
+ })
29
+ node.marks = marks
30
+ }
31
+ //删除所有的标记
32
+ else {
33
+ node.marks = {}
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 移除单个文本节点的样式
39
+ */
40
+ const removeTextNodeStyles = (node: KNode, styleNames?: string[]) => {
41
+ //删除指定样式
42
+ if (styleNames && node.hasStyles()) {
43
+ const styles: KNodeStylesType = {}
44
+ Object.keys(node.styles!).forEach(key => {
45
+ if (!styleNames.includes(key)) {
46
+ styles[key] = node.styles![key]
47
+ }
48
+ })
49
+ node.styles = styles
50
+ }
51
+ //删除所有的样式
52
+ else {
53
+ node.styles = {}
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 判断单个文本节点是否拥有某个标记
59
+ */
60
+ const isTextNodeMark = (node: KNode, markName: string, markValue?: string | number) => {
61
+ if (node.hasMarks()) {
62
+ if (markValue === undefined) {
63
+ return node.marks!.hasOwnProperty(markName)
64
+ }
65
+ return node.marks![markName] == markValue
66
+ }
67
+ return false
68
+ }
69
+
70
+ /**
71
+ * 判断单个文本节点是否拥有某个样式
72
+ */
73
+ const isTextNodeStyle = (node: KNode, styleName: string, styleValue?: string | number) => {
74
+ if (node.hasStyles()) {
75
+ if (styleValue === undefined) {
76
+ return node.styles!.hasOwnProperty(styleName)
77
+ }
78
+ let ownValue = node.styles![styleName]
79
+ //是字符串先将值转为小写
80
+ if (typeof styleValue == 'string') {
81
+ styleValue = styleValue.toLocaleLowerCase()
82
+ }
83
+ if (typeof ownValue == 'string') {
84
+ ownValue = ownValue.toLocaleLowerCase()
85
+ }
86
+ //是rgb或者rgba格式,则去除空格
87
+ if (typeof styleValue == 'string' && styleValue && (DapCommon.matchingText(styleValue, 'rgb') || DapCommon.matchingText(styleValue, 'rgba'))) {
88
+ styleValue = DapString.trim(styleValue, true)
89
+ }
90
+ if (typeof ownValue == 'string' && ownValue && (DapCommon.matchingText(ownValue, 'rgb') || DapCommon.matchingText(ownValue, 'rgba'))) {
91
+ ownValue = DapString.trim(ownValue, true)
92
+ }
93
+ //是十六进制值,转为rgb值
94
+ if (typeof styleValue == 'string' && styleValue && DapCommon.matchingText(styleValue, 'hex')) {
95
+ const arr = DapColor.hex2rgb(styleValue)
96
+ styleValue = `rgb(${arr[0]},${arr[1]},${arr[2]})`
97
+ }
98
+ if (typeof ownValue == 'string' && ownValue && DapCommon.matchingText(ownValue, 'hex')) {
99
+ const arr = DapColor.hex2rgb(ownValue)
100
+ ownValue = `rgb(${arr[0]},${arr[1]},${arr[2]})`
101
+ }
102
+ return ownValue == styleValue
103
+ }
104
+ return false
105
+ }
106
+
107
+ export const TextExtension = () =>
108
+ Extension.create({
109
+ name: 'text',
110
+ addCommands() {
111
+ /**
112
+ * 判断光标所在文本是否具有某个样式
113
+ */
114
+ const isTextStyle = (styleName: string, styleValue?: string | number) => {
115
+ if (!this.selection.focused()) {
116
+ return false
117
+ }
118
+ //起点和终点在一起
119
+ if (this.selection.collapsed()) {
120
+ const node = this.selection.start!.node
121
+ //文本节点
122
+ if (node.isText()) {
123
+ return isTextNodeStyle(node, styleName, styleValue)
124
+ }
125
+ return false
126
+ }
127
+ //存在选区
128
+ const textArray = this.getFocusNodesBySelection('text')
129
+ return !!textArray.length && textArray.every(item => isTextNodeStyle(item, styleName, styleValue))
130
+ }
131
+
132
+ /**
133
+ * 判断光标所在文本是否具有某个标记
134
+ */
135
+ const isTextMark = (markName: string, markValue?: string | number) => {
136
+ if (!this.selection.focused()) {
137
+ return false
138
+ }
139
+ //起点和终点在一起
140
+ if (this.selection.collapsed()) {
141
+ const node = this.selection.start!.node
142
+ //文本节点
143
+ if (node.isText()) {
144
+ return isTextNodeMark(node, markName, markValue)
145
+ }
146
+ return false
147
+ }
148
+ //存在选区
149
+ const textArray = this.getFocusNodesBySelection('text')
150
+ return !!textArray.length && textArray.every(item => isTextNodeMark(item, markName, markValue))
151
+ }
152
+
153
+ /**
154
+ * 设置光标所在文本样式
155
+ */
156
+ const setTextStyle = async (styles: KNodeStylesType, updateView: boolean | undefined = true) => {
157
+ if (!this.selection.focused()) {
158
+ return
159
+ }
160
+ //起点和终点在一起
161
+ if (this.selection.collapsed()) {
162
+ const node = this.selection.start!.node
163
+ //空白文本节点直接设置样式
164
+ if (node.isZeroWidthText()) {
165
+ if (node.hasStyles()) {
166
+ Object.assign(node.styles!, DapCommon.clone(styles))
167
+ } else {
168
+ node.styles = DapCommon.clone(styles)
169
+ }
170
+ }
171
+ //文本节点
172
+ else if (node.isText()) {
173
+ //新建一个空白文本节点
174
+ const newTextNode = KNode.createZeroWidthText()
175
+ //继承文本节点的样式和标记
176
+ newTextNode.styles = DapCommon.clone(node.styles)
177
+ newTextNode.marks = DapCommon.clone(node.marks)
178
+ //设置样式
179
+ if (newTextNode.hasStyles()) {
180
+ Object.assign(newTextNode.styles!, DapCommon.clone(styles))
181
+ } else {
182
+ newTextNode.styles = DapCommon.clone(styles)
183
+ }
184
+ //插入空白文本节点
185
+ this.insertNode(newTextNode)
186
+ }
187
+ //闭合节点
188
+ else {
189
+ //新建一个空白文本节点
190
+ const newTextNode = KNode.createZeroWidthText()
191
+ //设置样式
192
+ newTextNode.styles = DapCommon.clone(styles)
193
+ //插入空白文本节点
194
+ this.insertNode(newTextNode)
195
+ }
196
+ }
197
+ //存在选区
198
+ else {
199
+ this.getFocusSplitNodesBySelection('text').forEach(item => {
200
+ if (item.hasStyles()) {
201
+ item.styles = { ...item.styles, ...styles }
202
+ } else {
203
+ item.styles = { ...styles }
204
+ }
205
+ })
206
+ }
207
+ //更新视图
208
+ if (updateView) await this.updateView()
209
+ }
210
+
211
+ /**
212
+ * 设置光标所在文本标记
213
+ */
214
+ const setTextMark = async (marks: KNodeMarksType, updateView: boolean | undefined = true) => {
215
+ if (!this.selection.focused()) {
216
+ return
217
+ }
218
+ //起点和终点在一起
219
+ if (this.selection.collapsed()) {
220
+ const node = this.selection.start!.node
221
+ //空白文本节点直接设置标记
222
+ if (node.isZeroWidthText()) {
223
+ if (node.hasMarks()) {
224
+ Object.assign(node.marks!, DapCommon.clone(marks))
225
+ } else {
226
+ node.marks = DapCommon.clone(marks)
227
+ }
228
+ }
229
+ //文本节点
230
+ else if (node.isText()) {
231
+ //新建一个空白文本节点
232
+ const newTextNode = KNode.createZeroWidthText()
233
+ //继承文本节点的样式和标记
234
+ newTextNode.styles = DapCommon.clone(node.styles)
235
+ newTextNode.marks = DapCommon.clone(node.marks)
236
+ //设置标记
237
+ if (newTextNode.hasMarks()) {
238
+ Object.assign(newTextNode.marks!, DapCommon.clone(marks))
239
+ } else {
240
+ newTextNode.marks = DapCommon.clone(marks)
241
+ }
242
+ //插入空白文本节点
243
+ this.insertNode(newTextNode)
244
+ }
245
+ //闭合节点
246
+ else {
247
+ //新建一个空白文本节点
248
+ const newTextNode = KNode.createZeroWidthText()
249
+ //设置样式
250
+ newTextNode.marks = DapCommon.clone(marks)
251
+ //插入空白文本节点
252
+ this.insertNode(newTextNode)
253
+ }
254
+ }
255
+ //存在选区
256
+ else {
257
+ this.getFocusSplitNodesBySelection('text').forEach(item => {
258
+ if (item.hasMarks()) {
259
+ item.marks = { ...item.marks, ...marks }
260
+ } else {
261
+ item.marks = { ...marks }
262
+ }
263
+ })
264
+ }
265
+ //更新视图
266
+ if (updateView) await this.updateView()
267
+ }
268
+
269
+ /**
270
+ * 移除光标所在文本样式
271
+ */
272
+ const removeTextStyle = async (styleNames?: string[], updateView: boolean | undefined = true) => {
273
+ if (!this.selection.focused()) {
274
+ return
275
+ }
276
+ //起点和终点在一起
277
+ if (this.selection.collapsed()) {
278
+ const node = this.selection.start!.node
279
+ //空白文本节点直接移除样式
280
+ if (node.isZeroWidthText()) {
281
+ removeTextNodeStyles(node, styleNames)
282
+ }
283
+ //文本节点则新建一个空白文本节点
284
+ else if (node.isText()) {
285
+ const newTextNode = KNode.createZeroWidthText()
286
+ //继承文本节点的样式和标记
287
+ newTextNode.styles = DapCommon.clone(node.styles)
288
+ newTextNode.marks = DapCommon.clone(node.marks)
289
+ //移除样式
290
+ removeTextNodeStyles(newTextNode, styleNames)
291
+ //插入
292
+ this.insertNode(newTextNode)
293
+ }
294
+ }
295
+ //存在选区
296
+ else {
297
+ this.getFocusSplitNodesBySelection('text').forEach(item => {
298
+ removeTextNodeStyles(item, styleNames)
299
+ })
300
+ }
301
+ //更新视图
302
+ if (updateView) await this.updateView()
303
+ }
304
+
305
+ /**
306
+ * 移除光标所在文本标记
307
+ */
308
+ const removeTextMark = async (markNames?: string[], updateView: boolean | undefined = true) => {
309
+ if (!this.selection.focused()) {
310
+ return
311
+ }
312
+ //起点和终点在一起
313
+ if (this.selection.collapsed()) {
314
+ const node = this.selection.start!.node
315
+ //空白文本节点直接移除标记
316
+ if (node.isZeroWidthText()) {
317
+ removeTextNodeMarks(node, markNames)
318
+ }
319
+ //文本节点则新建一个空白文本节点
320
+ else if (node.isText()) {
321
+ const newTextNode = KNode.createZeroWidthText()
322
+ //继承文本节点的样式和标记
323
+ newTextNode.styles = DapCommon.clone(node.styles)
324
+ newTextNode.marks = DapCommon.clone(node.marks)
325
+ //移除标记
326
+ removeTextNodeMarks(newTextNode, markNames)
327
+ //插入
328
+ this.insertNode(newTextNode)
329
+ }
330
+ }
331
+ //存在选区
332
+ else {
333
+ this.getFocusSplitNodesBySelection('text').forEach(item => {
334
+ removeTextNodeMarks(item, markNames)
335
+ })
336
+ }
337
+ //更新视图
338
+ if (updateView) await this.updateView()
339
+ }
340
+
341
+ /**
342
+ * 清除格式
343
+ */
344
+ const clearFormat = async () => {
345
+ await removeTextMark(undefined, false)
346
+ await removeTextStyle()
347
+ }
348
+
349
+ return {
350
+ isTextStyle,
351
+ isTextMark,
352
+ setTextStyle,
353
+ setTextMark,
354
+ removeTextStyle,
355
+ removeTextMark,
356
+ clearFormat
357
+ }
358
+ }
359
+ })
@@ -0,0 +1,78 @@
1
+ import { KNodeStylesType } from '@/model'
2
+ import { splitNodeToNodes } from '@/model/config/function'
3
+ import { Extension } from '../Extension'
4
+
5
+ declare module '../../model' {
6
+ interface EditorCommandsType {
7
+ isUnderline?: () => boolean
8
+ setUnderline?: () => Promise<void>
9
+ unsetUnderline?: () => Promise<void>
10
+ }
11
+ }
12
+
13
+ export const UnderlineExtension = () =>
14
+ Extension.create({
15
+ name: 'underline',
16
+ pasteKeepStyles(node) {
17
+ const styles: KNodeStylesType = {}
18
+ if (node.isText() && node.hasStyles()) {
19
+ if (node.styles!.hasOwnProperty('textDecoration')) styles.textDecoration = node.styles!.textDecoration
20
+ if (node.styles!.hasOwnProperty('textDecorationLine')) styles.textDecorationLine = node.styles!.textDecorationLine
21
+ }
22
+ return styles
23
+ },
24
+ extraKeepTags: ['u'],
25
+ domParseNodeCallback(node) {
26
+ if (node.isMatch({ tag: 'u' })) {
27
+ node.type = 'inline'
28
+ }
29
+ return node
30
+ },
31
+ formatRules: [
32
+ ({ editor, node }) => {
33
+ if (!node.isEmpty() && node.isMatch({ tag: 'u' })) {
34
+ const styles: KNodeStylesType = node.styles || {}
35
+ node.styles = {
36
+ ...styles,
37
+ textDecorationLine: 'underline'
38
+ }
39
+ node.tag = editor.textRenderTag
40
+ splitNodeToNodes.apply(editor, [node])
41
+ }
42
+ }
43
+ ],
44
+ addCommands() {
45
+ /**
46
+ * 光标所在文本是否下划线
47
+ */
48
+ const isUnderline = () => {
49
+ return this.commands.isTextStyle!('textDecoration', 'underline') || this.commands.isTextStyle!('textDecorationLine', 'underline')
50
+ }
51
+ /**
52
+ * 设置下划线
53
+ */
54
+ const setUnderline = async () => {
55
+ if (isUnderline()) {
56
+ return
57
+ }
58
+ await this.commands.setTextStyle!({
59
+ textDecorationLine: 'underline'
60
+ })
61
+ }
62
+ /**
63
+ * 取消下划线
64
+ */
65
+ const unsetUnderline = async () => {
66
+ if (!isUnderline()) {
67
+ return
68
+ }
69
+ await this.commands.removeTextStyle!(['textDecoration', 'textDecorationLine'])
70
+ }
71
+
72
+ return {
73
+ isUnderline,
74
+ setUnderline,
75
+ unsetUnderline
76
+ }
77
+ }
78
+ })
@@ -0,0 +1,273 @@
1
+ import interact from 'interactjs'
2
+ import { event as DapEvent, data as DapData } from 'dap-util'
3
+ import { Editor, KNode, KNodeMarksType, KNodeStylesType } from '@/model'
4
+ import { Extension } from '../Extension'
5
+ import './style.less'
6
+ import { deleteProperty } from '@/tools'
7
+
8
+ /**
9
+ * 插入视频方法入参类型
10
+ */
11
+ export type SetVideoOptionType = {
12
+ src: string
13
+ width?: string
14
+ autoplay?: boolean
15
+ }
16
+
17
+ /**
18
+ * 更新视频方法入参类型
19
+ */
20
+ export type UpdateVideoOptionType = {
21
+ controls?: boolean
22
+ muted?: boolean
23
+ loop?: boolean
24
+ }
25
+
26
+ declare module '../../model' {
27
+ interface EditorCommandsType {
28
+ getVideo?: () => KNode | null
29
+ hasVideo?: () => boolean
30
+ setVideo?: (options: SetVideoOptionType) => Promise<void>
31
+ updateVideo?: (options: UpdateVideoOptionType) => Promise<void>
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 设置视频选中
37
+ */
38
+ const videoFocus = (editor: Editor) => {
39
+ DapEvent.off(editor.$el!, 'click.video_focus')
40
+ DapEvent.on(editor.$el!, 'click.video_focus', e => {
41
+ //编辑器不可编辑状态下不设置
42
+ if (!editor.isEditable()) {
43
+ return
44
+ }
45
+ const event = e as MouseEvent
46
+ const elm = event.target as HTMLElement
47
+ if (elm === editor.$el) {
48
+ return
49
+ }
50
+ const node = editor.findNode(elm)
51
+ const matchNode = node.getMatchNode({
52
+ tag: 'video'
53
+ })
54
+ if (matchNode) {
55
+ editor.setSelectionBefore(matchNode, 'start')
56
+ editor.setSelectionAfter(matchNode, 'end')
57
+ editor.updateRealSelection()
58
+ }
59
+ })
60
+ }
61
+ /**
62
+ * 设置视频拖拽
63
+ */
64
+ const videoResizable = (editor: Editor) => {
65
+ //设置拖拽改变大小的功能
66
+ interact('.Kaitify video').unset()
67
+ interact('.Kaitify video').resizable({
68
+ //是否启用
69
+ enabled: true,
70
+ //指定可以调整大小的边缘
71
+ edges: { left: false, right: true, bottom: false, top: false },
72
+ //设置鼠标样式
73
+ cursorChecker() {
74
+ return editor.isEditable() ? 'ew-resize' : 'default'
75
+ },
76
+ //启用惯性效果
77
+ inertia: false,
78
+ //调整大小时的自动滚动功能
79
+ autoScroll: true,
80
+ //保持图片的宽高比
81
+ preserveAspectRatio: true,
82
+ //水平调整
83
+ axis: 'x',
84
+ //事件
85
+ listeners: {
86
+ //开始拖拽
87
+ start(event) {
88
+ //不可编辑状态下不能拖拽
89
+ if (!editor.isEditable()) {
90
+ event.interaction.stop()
91
+ return
92
+ }
93
+ //禁用dragstart
94
+ DapEvent.on(event.target, 'dragstart', e => e.preventDefault())
95
+ //获取视频节点
96
+ const node = editor.findNode(event.target)
97
+ //暂存
98
+ DapData.set(event.target, 'node', node)
99
+ },
100
+ //拖拽
101
+ move(event) {
102
+ //获取宽度
103
+ const { width } = event.rect
104
+ //设置dom的宽度
105
+ event.target.style.width = `${width}px`
106
+ },
107
+ //结束拖拽
108
+ end(event) {
109
+ //恢复dragstart
110
+ DapEvent.off(event.target, 'dragstart')
111
+ //获取宽度
112
+ const { width } = event.rect
113
+ //设置百分比宽度
114
+ const percentWidth = Number(((width / event.target.parentElement.offsetWidth) * 100).toFixed(2))
115
+ //获取视频节点
116
+ const node = DapData.get(event.target, 'node')
117
+ //设置节点的styles
118
+ if (node.hasStyles()) {
119
+ node.styles!.width = `${percentWidth}%`
120
+ } else {
121
+ node.styles = {
122
+ width: `${percentWidth}%`
123
+ }
124
+ }
125
+ //更新视图
126
+ editor.updateView()
127
+ }
128
+ }
129
+ })
130
+ }
131
+
132
+ export const VideoExtension = () =>
133
+ Extension.create({
134
+ name: 'video',
135
+ extraKeepTags: ['video'],
136
+ domParseNodeCallback(node) {
137
+ if (node.isMatch({ tag: 'video' })) {
138
+ node.type = 'closed'
139
+ }
140
+ return node
141
+ },
142
+ pasteKeepMarks(node) {
143
+ const marks: KNodeMarksType = {}
144
+ if (node.isMatch({ tag: 'video' }) && node.hasMarks()) {
145
+ if (node.marks!.hasOwnProperty('src')) marks['src'] = node.marks!['src']
146
+ if (node.marks!.hasOwnProperty('autoplay')) marks['autoplay'] = node.marks!['autoplay']
147
+ if (node.marks!.hasOwnProperty('loop')) marks['loop'] = node.marks!['loop']
148
+ if (node.marks!.hasOwnProperty('muted')) marks['muted'] = node.marks!['muted']
149
+ if (node.marks!.hasOwnProperty('controls')) marks['controls'] = node.marks!['controls']
150
+ }
151
+ return marks
152
+ },
153
+ pasteKeepStyles(node) {
154
+ const styles: KNodeStylesType = {}
155
+ if (node.isMatch({ tag: 'video' }) && node.hasStyles()) {
156
+ styles['width'] = node.styles!['width'] || 'auto'
157
+ }
158
+ return styles
159
+ },
160
+ formatRules: [
161
+ ({ editor, node }) => {
162
+ if (node.isMatch({ tag: 'video' })) {
163
+ //视频必须是闭合节点
164
+ node.type = 'closed'
165
+ const previousNode = node.getPrevious(node.parent ? node.parent!.children! : editor.stackNodes)
166
+ const nextNode = node.getNext(node.parent ? node.parent!.children! : editor.stackNodes)
167
+ //前一个节点不存在或者不是零宽度空白文本节点
168
+ if (!previousNode || !previousNode.isZeroWidthText()) {
169
+ const zeroWidthText = KNode.createZeroWidthText()
170
+ editor.addNodeBefore(zeroWidthText, node)
171
+ }
172
+ //后一个节点不存在或者不是零宽度空白文本节点
173
+ if (!nextNode || !nextNode.isZeroWidthText()) {
174
+ const zeroWidthText = KNode.createZeroWidthText()
175
+ editor.addNodeAfter(zeroWidthText, node)
176
+ }
177
+ }
178
+ }
179
+ ],
180
+ afterUpdateView() {
181
+ //视频选中
182
+ videoFocus(this)
183
+ //视频拖拽改变大小
184
+ videoResizable(this)
185
+ },
186
+ addCommands() {
187
+ /**
188
+ * 获取光标所在的视频,如果光标不在一个视频内,返回null
189
+ */
190
+ const getVideo = () => {
191
+ return this.getMatchNodeBySelection({
192
+ tag: 'video'
193
+ })
194
+ }
195
+ /**
196
+ * 判断光标范围内是否有视频
197
+ */
198
+ const hasVideo = () => {
199
+ return this.isSelectionNodesSomeMatch({
200
+ tag: 'video'
201
+ })
202
+ }
203
+ /**
204
+ * 插入视频
205
+ */
206
+ const setVideo = async (options: SetVideoOptionType) => {
207
+ if (!this.selection.focused()) {
208
+ return
209
+ }
210
+ if (!options.src) {
211
+ return
212
+ }
213
+ const marks: KNodeMarksType = {
214
+ src: options.src
215
+ }
216
+ if (options.autoplay) {
217
+ marks['autoplay'] = 'autoplay'
218
+ marks['muted'] = 'muted'
219
+ }
220
+ const videoNode = KNode.create({
221
+ type: 'closed',
222
+ tag: 'video',
223
+ marks,
224
+ styles: {
225
+ width: options.width || 'auto'
226
+ }
227
+ })
228
+ this.insertNode(videoNode)
229
+ this.setSelectionAfter(videoNode)
230
+ await this.updateView()
231
+ }
232
+
233
+ /**
234
+ * 更新视频
235
+ */
236
+ const updateVideo = async (options: UpdateVideoOptionType) => {
237
+ if (!this.selection.focused()) {
238
+ return
239
+ }
240
+ if (typeof options.controls != 'boolean' && typeof options.muted != 'boolean' && typeof options.loop != 'boolean') {
241
+ return
242
+ }
243
+ const videoNode = getVideo()
244
+ if (!videoNode) {
245
+ return
246
+ }
247
+ if (typeof options.controls == 'boolean') {
248
+ if (options.controls) {
249
+ videoNode.marks!['controls'] = 'controls'
250
+ } else {
251
+ videoNode.marks = deleteProperty(videoNode.marks!, 'controls')
252
+ }
253
+ }
254
+ if (typeof options.loop == 'boolean') {
255
+ if (options.loop) {
256
+ videoNode.marks!['loop'] = 'loop'
257
+ } else {
258
+ videoNode.marks = deleteProperty(videoNode.marks!, 'loop')
259
+ }
260
+ }
261
+ if (typeof options.muted == 'boolean') {
262
+ if (options.muted) {
263
+ videoNode.marks!['muted'] = 'muted'
264
+ } else {
265
+ videoNode.marks = deleteProperty(videoNode.marks!, 'muted')
266
+ }
267
+ }
268
+ await this.updateView()
269
+ }
270
+
271
+ return { getVideo, hasVideo, setVideo, updateVideo }
272
+ }
273
+ })