@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,308 @@
1
+ import { Editor, KNode, KNodeMarksType } from '@/model'
2
+ import { getSelectionBlockNodes } from '@/model/config/function'
3
+ import { Extension } from '../Extension'
4
+ import { getHljsHtml, HljsLanguages, HljsLanguageType } from './hljs'
5
+ import './style.less'
6
+
7
+ declare module '../../model' {
8
+ interface EditorCommandsType {
9
+ getCodeBlock?: () => KNode | null
10
+ hasCodeBlock?: () => boolean
11
+ allCodeBlock?: () => boolean
12
+ setCodeBlock?: () => Promise<void>
13
+ unsetCodeBlock?: () => Promise<void>
14
+ updateCodeBlockLanguage?: (language: HljsLanguageType) => Promise<void>
15
+ }
16
+ }
17
+
18
+ /**
19
+ * 块节点转为代码块
20
+ */
21
+ const toCodeBlock = (editor: Editor, node: KNode) => {
22
+ if (!node.isBlock()) {
23
+ return
24
+ }
25
+ //是固定的块节点或者内嵌套的块节点
26
+ if (node.fixed || node.nested) {
27
+ //创建代码块节点
28
+ const codeBlockNode = KNode.create({
29
+ type: 'block',
30
+ tag: 'pre',
31
+ children: []
32
+ })
33
+ //将块节点的子节点给代码块节点
34
+ node.children!.forEach((item, index) => {
35
+ editor.addNode(item, codeBlockNode, index)
36
+ })
37
+ //将代码块节点添加到块节点下
38
+ codeBlockNode.parent = node
39
+ node.children = [codeBlockNode]
40
+ }
41
+ //非固定块节点
42
+ else {
43
+ editor.toParagraph(node)
44
+ node.tag = 'pre'
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 更新代码块内的光标位置
50
+ */
51
+ const updateSelection = (editor: Editor, node: KNode, textNodes: KNode[], newNodes: KNode[]) => {
52
+ if (!editor.selection.focused()) {
53
+ return
54
+ }
55
+ //如果光标的起点在代码块内对光标的起点进行重新定位
56
+ if (editor.isSelectionInNode(node, 'start')) {
57
+ //获取起点所在文本节点的在所有文本节点中的序列
58
+ const startIndex = textNodes.findIndex(n => editor.selection.start!.node.isEqual(n))
59
+ //起点在整个代码内容中的位置
60
+ const offset = textNodes.filter((_n, i) => i < startIndex).reduce((total, item) => total + item.textContent!.length, 0) + editor.selection.start!.offset
61
+ //获取代码块下新的子孙节点中全部的文本节点
62
+ const newTextNodes = KNode.flat(newNodes).filter(n => n.isText() && !n.isEmpty())
63
+ let i = 0
64
+ let index = 0
65
+ //遍历
66
+ while (i < newTextNodes.length) {
67
+ let newIndex = index + newTextNodes[i].textContent!.length
68
+ if (offset >= index && offset <= newIndex) {
69
+ editor.selection.start!.node = newTextNodes[i]
70
+ editor.selection.start!.offset = offset - index
71
+ break
72
+ }
73
+ i++
74
+ index = newIndex
75
+ }
76
+ }
77
+ //如果光标的终点在代码块内对光标的终点进行重新定位
78
+ if (editor.isSelectionInNode(node, 'end')) {
79
+ //获取终点所在文本节点的在所有文本节点中的序列
80
+ const endIndex = textNodes.findIndex(n => editor.selection.end!.node.isEqual(n))
81
+ //终点在整个代码内容中的位置
82
+ const offset = textNodes.filter((_n, i) => i < endIndex).reduce((total, item) => total + item.textContent!.length, 0) + editor.selection.end!.offset
83
+ //获取全部的新文本节点
84
+ const newTextNodes = KNode.flat(newNodes).filter(n => n.isText() && !n.isEmpty())
85
+ let i = 0
86
+ let index = 0
87
+ //遍历
88
+ while (i < newTextNodes.length) {
89
+ let newIndex = index + newTextNodes[i].textContent!.length
90
+ if (offset >= index && offset <= newIndex) {
91
+ editor.selection.end!.node = newTextNodes[i]
92
+ editor.selection.end!.offset = offset - index
93
+ break
94
+ }
95
+ i++
96
+ index = newIndex
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * 判断代码块是否需要更新
103
+ */
104
+ const isNeedUpdate = (editor: Editor, node: KNode, language: string, textContent: string) => {
105
+ try {
106
+ const domPre = editor.findDom(node)
107
+ if (domPre && domPre.nodeName.toLocaleLowerCase() == 'pre') {
108
+ //语言不一致
109
+ const oldLanguage = domPre.getAttribute('kaitify-hljs') || ''
110
+ if (oldLanguage != language) {
111
+ return true
112
+ }
113
+ //文本内容不一致
114
+ const oldTextContent = domPre.innerText
115
+ if (oldTextContent != textContent) {
116
+ return true
117
+ }
118
+ //子孙节点数量不一致(防止在代码块里插入非文本节点,比如图片等)
119
+ if (KNode.flat(node.children!).length != domPre.querySelectorAll('*').length) {
120
+ return true
121
+ }
122
+ return false
123
+ }
124
+ return true
125
+ } catch (error) {
126
+ return true
127
+ }
128
+ }
129
+
130
+ export const CodeBlockExtension = () =>
131
+ Extension.create({
132
+ name: 'codeBlock',
133
+ extraKeepTags: ['pre'],
134
+ domParseNodeCallback(node) {
135
+ if (node.isMatch({ tag: 'pre' })) {
136
+ node.type = 'block'
137
+ }
138
+ return node
139
+ },
140
+ pasteKeepMarks(node) {
141
+ const marks: KNodeMarksType = {}
142
+ if (node.isMatch({ tag: 'pre' }) && node.hasMarks()) {
143
+ if (node.marks!.hasOwnProperty('kaitify-hljs')) marks['kaitify-hljs'] = node.marks!['kaitify-hljs']
144
+ }
145
+ return marks
146
+ },
147
+ beforePatchNodeToFormat(node) {
148
+ const codeBlockNode = node.getMatchNode({ tag: 'pre' })
149
+ if (codeBlockNode) {
150
+ return codeBlockNode
151
+ }
152
+ return node
153
+ },
154
+ formatRules: [
155
+ //代码块高亮处理
156
+ ({ editor, node }) => {
157
+ if (node.isMatch({ tag: 'pre' }) && node.hasChildren()) {
158
+ //代码块必须是块节点
159
+ if (!node.isBlock()) node.type = 'block'
160
+ //获取语言类型
161
+ let language = (node.marks?.['kaitify-hljs'] || '') as string
162
+ //语言存在但不是列表内的
163
+ if (language && !HljsLanguages.some(item => item == language)) {
164
+ language = ''
165
+ }
166
+ //获取代码块内的所有文本节点
167
+ const textNodes = KNode.flat(node.children!).filter(item => item.isText() && !item.isEmpty())
168
+ //获取代码块内的代码文本值
169
+ const textContent = textNodes.reduce((val, item) => {
170
+ return val + item.textContent
171
+ }, '')
172
+ //只有代码块语言改变和内容改变才需要重新进行高亮处理
173
+ if (isNeedUpdate(editor, node, language, textContent)) {
174
+ //将文本节点的内容转为经过hljs处理的内容
175
+ const html = getHljsHtml(textContent, language)
176
+ if (html) {
177
+ //将经过hljs处理的内容转为节点数组
178
+ const nodes = editor.htmlParseNode(html)
179
+ //将新的文本节点全部加入到代码块的子节点数组中
180
+ node.children = nodes.map(item => {
181
+ item.parent = node
182
+ return item
183
+ })
184
+ //更新光标位置
185
+ updateSelection(editor, node, textNodes, nodes)
186
+ } else {
187
+ const selectionStartInNode = editor.isSelectionInNode(node, 'start')
188
+ const selectionEndInNode = editor.isSelectionInNode(node, 'end')
189
+ const placeholderNode = KNode.createPlaceholder()
190
+ node.children = [placeholderNode]
191
+ placeholderNode.parent = node
192
+ if (selectionStartInNode) {
193
+ editor.setSelectionBefore(placeholderNode, 'start')
194
+ }
195
+ if (selectionEndInNode) {
196
+ editor.setSelectionBefore(placeholderNode, 'end')
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ ],
203
+ addCommands() {
204
+ /**
205
+ * 获取光标所在的代码块节点,如果光标不在一个代码块节点内,返回null
206
+ */
207
+ const getCodeBlock = () => {
208
+ return this.getMatchNodeBySelection({
209
+ tag: 'pre'
210
+ })
211
+ }
212
+
213
+ /**
214
+ * 判断光标范围内是否有代码块节点
215
+ */
216
+ const hasCodeBlock = () => {
217
+ return this.isSelectionNodesSomeMatch({
218
+ tag: 'pre'
219
+ })
220
+ }
221
+
222
+ /**
223
+ * 光标范围内是否都是代码块节点
224
+ */
225
+ const allCodeBlock = () => {
226
+ return this.isSelectionNodesAllMatch({
227
+ tag: 'pre'
228
+ })
229
+ }
230
+
231
+ /**
232
+ * 设置代码块
233
+ */
234
+ const setCodeBlock = async () => {
235
+ if (allCodeBlock()) {
236
+ return
237
+ }
238
+ //起点和终点在一起
239
+ if (this.selection.collapsed()) {
240
+ const blockNode = this.selection.start!.node.getBlock()
241
+ toCodeBlock(this, blockNode)
242
+ }
243
+ //起点和终点不在一起
244
+ else {
245
+ const blockNodes = getSelectionBlockNodes.apply(this)
246
+ blockNodes.forEach(item => {
247
+ toCodeBlock(this, item)
248
+ })
249
+ }
250
+ await this.updateView()
251
+ }
252
+
253
+ /**
254
+ * 取消代码块
255
+ */
256
+ const unsetCodeBlock = async () => {
257
+ if (!allCodeBlock()) {
258
+ return
259
+ }
260
+ //起点和终点在一起
261
+ if (this.selection.collapsed()) {
262
+ const matchNode = this.selection.start!.node.getMatchNode({ tag: 'pre' })
263
+ if (matchNode) this.toParagraph(matchNode)
264
+ }
265
+ //起点和终点不在一起
266
+ else {
267
+ const blockNodes = getSelectionBlockNodes.apply(this)
268
+ blockNodes.forEach(item => {
269
+ const matchNode = item.getMatchNode({ tag: 'pre' })
270
+ if (matchNode) this.toParagraph(matchNode)
271
+ })
272
+ }
273
+ await this.updateView()
274
+ }
275
+
276
+ /**
277
+ * 更新光标所在代码块的语言类型
278
+ */
279
+ const updateCodeBlockLanguage = async (language?: HljsLanguageType) => {
280
+ if (!this.selection.focused()) {
281
+ return
282
+ }
283
+ const codeBlockNode = getCodeBlock()
284
+ if (!codeBlockNode) {
285
+ return
286
+ }
287
+ if (codeBlockNode.hasMarks()) {
288
+ codeBlockNode.marks!['kaitify-hljs'] = language || ''
289
+ } else {
290
+ codeBlockNode.marks = {
291
+ 'kaitify-hljs': language || ''
292
+ }
293
+ }
294
+ await this.updateView()
295
+ }
296
+
297
+ return {
298
+ getCodeBlock,
299
+ hasCodeBlock,
300
+ allCodeBlock,
301
+ setCodeBlock,
302
+ unsetCodeBlock,
303
+ updateCodeBlockLanguage
304
+ }
305
+ }
306
+ })
307
+
308
+ export * from './hljs'
@@ -0,0 +1,20 @@
1
+ .Kaitify pre {
2
+ background: #f6f9ff;
3
+ color: var(--kaitify-font-color);
4
+ border: 1px solid #f6f9ff;
5
+ border-radius: var(--kaitify-border-radius);
6
+ padding: var(--kaitify-padding);
7
+ line-height: var(--kaitify-line-height);
8
+ font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono;
9
+ margin: 0 0 var(--kaitify-large-margin) 0 !important;
10
+ overflow-x: auto;
11
+
12
+ &:last-child {
13
+ margin-bottom: 0 !important;
14
+ }
15
+ }
16
+
17
+ :root[kaitify-dark] .Kaitify pre {
18
+ background: #29292d;
19
+ border-color: #29292d;
20
+ }
@@ -0,0 +1,56 @@
1
+ import { KNodeStylesType } from '@/model'
2
+ import { Extension } from '../Extension'
3
+
4
+ declare module '../../model' {
5
+ interface EditorCommandsType {
6
+ isColor?: (value: string) => boolean
7
+ setColor?: (value: string) => Promise<void>
8
+ unsetColor?: (value: string) => Promise<void>
9
+ }
10
+ }
11
+
12
+ export const ColorExtension = () =>
13
+ Extension.create({
14
+ name: 'color',
15
+ pasteKeepStyles(node) {
16
+ const styles: KNodeStylesType = {}
17
+ if (node.isText() && node.hasStyles()) {
18
+ if (node.styles!.hasOwnProperty('color')) styles.color = node.styles!.color
19
+ }
20
+ return styles
21
+ },
22
+ addCommands() {
23
+ /**
24
+ * 光标所在文本的颜色是否与入参一致
25
+ */
26
+ const isColor = (value: string) => {
27
+ return this.commands.isTextStyle!('color', value)
28
+ }
29
+ /**
30
+ * 设置颜色
31
+ */
32
+ const setColor = async (value: string) => {
33
+ if (isColor(value)) {
34
+ return
35
+ }
36
+ await this.commands.setTextStyle!({
37
+ color: value
38
+ })
39
+ }
40
+ /**
41
+ * 取消颜色
42
+ */
43
+ const unsetColor = async (value: string) => {
44
+ if (!isColor(value)) {
45
+ return
46
+ }
47
+ await this.commands.removeTextStyle!(['color'])
48
+ }
49
+
50
+ return {
51
+ isColor,
52
+ setColor,
53
+ unsetColor
54
+ }
55
+ }
56
+ })
@@ -0,0 +1,80 @@
1
+ import { KNodeMarksType, KNodeStylesType } from '@/model'
2
+ import { splitNodeToNodes } from '@/model/config/function'
3
+ import { deleteProperty } from '@/tools'
4
+ import { Extension } from '../Extension'
5
+
6
+ declare module '../../model' {
7
+ interface EditorCommandsType {
8
+ isFontFamily?: (value: string) => boolean
9
+ setFontFamily?: (value: string) => Promise<void>
10
+ unsetFontFamily?: (value: string) => Promise<void>
11
+ }
12
+ }
13
+
14
+ export const FontFamilyExtension = () =>
15
+ Extension.create({
16
+ name: 'fontFamily',
17
+ pasteKeepStyles(node) {
18
+ const styles: KNodeStylesType = {}
19
+ if (node.isText() && node.hasStyles()) {
20
+ if (node.styles!.hasOwnProperty('fontFamily')) styles.fontFamily = node.styles!.fontFamily
21
+ }
22
+ return styles
23
+ },
24
+ extraKeepTags: ['font'],
25
+ domParseNodeCallback(node) {
26
+ if (node.isMatch({ tag: 'font' })) {
27
+ node.type = 'inline'
28
+ }
29
+ return node
30
+ },
31
+ formatRules: [
32
+ ({ editor, node }) => {
33
+ if (!node.isEmpty() && node.isMatch({ tag: 'font' })) {
34
+ const marks: KNodeMarksType = node.marks || {}
35
+ const styles: KNodeStylesType = node.styles || {}
36
+ node.styles = {
37
+ ...styles,
38
+ fontFamily: (marks.face as string) || ''
39
+ }
40
+ node.marks = deleteProperty(marks, 'face')
41
+ node.tag = editor.textRenderTag
42
+ splitNodeToNodes.apply(editor, [node])
43
+ }
44
+ }
45
+ ],
46
+ addCommands() {
47
+ /**
48
+ * 光标所在文本的字体是否与入参一致
49
+ */
50
+ const isFontFamily = (value: string) => {
51
+ return this.commands.isTextStyle!('fontFamily', value)
52
+ }
53
+ /**
54
+ * 设置字体
55
+ */
56
+ const setFontFamily = async (value: string) => {
57
+ if (isFontFamily(value)) {
58
+ return
59
+ }
60
+ await this.commands.setTextStyle!({
61
+ fontFamily: value
62
+ })
63
+ }
64
+ /**
65
+ * 取消字体
66
+ */
67
+ const unsetFontFamily = async (value: string) => {
68
+ if (!isFontFamily(value)) {
69
+ return
70
+ }
71
+ await this.commands.removeTextStyle!(['fontFamily'])
72
+ }
73
+
74
+ return {
75
+ isFontFamily,
76
+ setFontFamily,
77
+ unsetFontFamily
78
+ }
79
+ }
80
+ })
@@ -0,0 +1,56 @@
1
+ import { KNodeStylesType } from '@/model'
2
+ import { Extension } from '../Extension'
3
+
4
+ declare module '../../model' {
5
+ interface EditorCommandsType {
6
+ isFontSize?: (value: string) => boolean
7
+ setFontSize?: (value: string) => Promise<void>
8
+ unsetFontSize?: (value: string) => Promise<void>
9
+ }
10
+ }
11
+
12
+ export const FontSizeExtension = () =>
13
+ Extension.create({
14
+ name: 'fontSize',
15
+ pasteKeepStyles(node) {
16
+ const styles: KNodeStylesType = {}
17
+ if (node.isText() && node.hasStyles()) {
18
+ if (node.styles!.hasOwnProperty('fontSize')) styles.fontSize = node.styles!.fontSize
19
+ }
20
+ return styles
21
+ },
22
+ addCommands() {
23
+ /**
24
+ * 光标所在文本的字号大小是否与入参一致
25
+ */
26
+ const isFontSize = (value: string) => {
27
+ return this.commands.isTextStyle!('fontSize', value)
28
+ }
29
+ /**
30
+ * 设置字号
31
+ */
32
+ const setFontSize = async (value: string) => {
33
+ if (isFontSize(value)) {
34
+ return
35
+ }
36
+ await this.commands.setTextStyle!({
37
+ fontSize: value
38
+ })
39
+ }
40
+ /**
41
+ * 取消字号
42
+ */
43
+ const unsetFontSize = async (value: string) => {
44
+ if (!isFontSize(value)) {
45
+ return
46
+ }
47
+ await this.commands.removeTextStyle!(['fontSize'])
48
+ }
49
+
50
+ return {
51
+ isFontSize,
52
+ setFontSize,
53
+ unsetFontSize
54
+ }
55
+ }
56
+ })
@@ -0,0 +1,164 @@
1
+ import { Editor, KNode } from '@/model'
2
+ import { getSelectionBlockNodes } from '@/model/config/function'
3
+ import { Extension } from '../Extension'
4
+ import './style.less'
5
+
6
+ export type HeadingLevelType = 0 | 1 | 2 | 3 | 4 | 5 | 6
7
+
8
+ declare module '../../model' {
9
+ interface EditorCommandsType {
10
+ getHeading?: (level: HeadingLevelType) => KNode | null
11
+ hasHeading?: (level: HeadingLevelType) => boolean
12
+ allHeading?: (level: HeadingLevelType) => boolean
13
+ setHeading?: (level: HeadingLevelType) => Promise<void>
14
+ unsetHeading?: (level: HeadingLevelType) => Promise<void>
15
+ }
16
+ }
17
+
18
+ /**
19
+ * 块节点转为标题
20
+ */
21
+ const toHeading = (editor: Editor, node: KNode, level: HeadingLevelType) => {
22
+ if (!node.isBlock()) {
23
+ return
24
+ }
25
+ const headingLevelMap = {
26
+ 0: editor.blockRenderTag,
27
+ 1: 'h1',
28
+ 2: 'h2',
29
+ 3: 'h3',
30
+ 4: 'h4',
31
+ 5: 'h5',
32
+ 6: 'h6'
33
+ }
34
+ //是固定的块节点或者内嵌套的块节点
35
+ if (node.fixed || node.nested) {
36
+ //创建标题节点
37
+ const headingNode = KNode.create({
38
+ type: 'block',
39
+ tag: headingLevelMap[level],
40
+ children: []
41
+ })
42
+ //将块节点的子节点给标题节点
43
+ node.children!.forEach((item, index) => {
44
+ editor.addNode(item, headingNode, index)
45
+ })
46
+ //将标题节点添加到块节点下
47
+ headingNode.parent = node
48
+ node.children = [headingNode]
49
+ }
50
+ //非固定块节点
51
+ else {
52
+ editor.toParagraph(node)
53
+ node.tag = headingLevelMap[level]
54
+ }
55
+ }
56
+
57
+ export const HeadingExtension = () =>
58
+ Extension.create({
59
+ name: 'heading',
60
+ extraKeepTags: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
61
+ domParseNodeCallback(node) {
62
+ if (node.isMatch({ tag: 'h1' }) || node.isMatch({ tag: 'h2' }) || node.isMatch({ tag: 'h3' }) || node.isMatch({ tag: 'h4' }) || node.isMatch({ tag: 'h5' }) || node.isMatch({ tag: 'h6' })) {
63
+ node.type = 'block'
64
+ }
65
+ return node
66
+ },
67
+ formatRules: [
68
+ ({ node }) => {
69
+ if (node.isMatch({ tag: 'h1' }) || node.isMatch({ tag: 'h2' }) || node.isMatch({ tag: 'h3' }) || node.isMatch({ tag: 'h4' }) || node.isMatch({ tag: 'h5' }) || node.isMatch({ tag: 'h6' })) {
70
+ node.type = 'block'
71
+ }
72
+ }
73
+ ],
74
+ addCommands() {
75
+ const headingLevelMap = {
76
+ 0: this.blockRenderTag,
77
+ 1: 'h1',
78
+ 2: 'h2',
79
+ 3: 'h3',
80
+ 4: 'h4',
81
+ 5: 'h5',
82
+ 6: 'h6'
83
+ }
84
+ /**
85
+ * 获取光标所在的标题,如果光标不在一个标题内,返回null
86
+ */
87
+ const getHeading = (level: HeadingLevelType) => {
88
+ return this.getMatchNodeBySelection({
89
+ tag: headingLevelMap[level]
90
+ })
91
+ }
92
+
93
+ /**
94
+ * 判断光标范围内是否有标题
95
+ */
96
+ const hasHeading = (level: HeadingLevelType) => {
97
+ return this.isSelectionNodesSomeMatch({
98
+ tag: headingLevelMap[level]
99
+ })
100
+ }
101
+
102
+ /**
103
+ * 光标范围内是否都是标题
104
+ */
105
+ const allHeading = (level: HeadingLevelType) => {
106
+ return this.isSelectionNodesAllMatch({
107
+ tag: headingLevelMap[level]
108
+ })
109
+ }
110
+
111
+ /**
112
+ * 设置标题
113
+ */
114
+ const setHeading = async (level: HeadingLevelType) => {
115
+ if (allHeading(level)) {
116
+ return
117
+ }
118
+ //起点和终点在一起
119
+ if (this.selection.collapsed()) {
120
+ const blockNode = this.selection.start!.node.getBlock()
121
+ toHeading(this, blockNode, level)
122
+ }
123
+ //起点和终点不在一起
124
+ else {
125
+ const blockNodes = getSelectionBlockNodes.apply(this)
126
+ blockNodes.forEach(item => {
127
+ toHeading(this, item, level)
128
+ })
129
+ }
130
+ await this.updateView()
131
+ }
132
+
133
+ /**
134
+ * 取消标题
135
+ */
136
+ const unsetHeading = async (level: HeadingLevelType) => {
137
+ if (!allHeading(level)) {
138
+ return
139
+ }
140
+ //起点和终点在一起
141
+ if (this.selection.collapsed()) {
142
+ const matchNode = this.selection.start!.node.getMatchNode({ tag: headingLevelMap[level] })
143
+ if (matchNode) this.toParagraph(matchNode)
144
+ }
145
+ //起点和终点不在一起
146
+ else {
147
+ const blockNodes = getSelectionBlockNodes.apply(this)
148
+ blockNodes.forEach(item => {
149
+ const matchNode = item.getMatchNode({ tag: headingLevelMap[level] })
150
+ if (matchNode) this.toParagraph(matchNode)
151
+ })
152
+ }
153
+ await this.updateView()
154
+ }
155
+
156
+ return {
157
+ getHeading,
158
+ hasHeading,
159
+ allHeading,
160
+ setHeading,
161
+ unsetHeading
162
+ }
163
+ }
164
+ })