@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,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
|
+
})
|