61 | ```
62 |
63 | - 详细信息
64 |
65 | 提供一个入参,类型为 `AlignValueType`,可取值为 `left` `right` `center` `justify`,该方法会取消光标所在的块节点的指定对齐方式,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
66 |
67 | 如果通过 `isAlign` 判断所在块节点都已经不是该对齐方式了,则不会继续执行
68 |
69 | - 示例
70 |
71 | ```ts
72 | await editor.commands.unsetAlign('center')
73 | ```
74 |
75 | ## 代码示例
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
113 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/math.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: math 数学公式
3 | ---
4 |
5 | # math 数学公式
6 |
7 | 支持 `Latex` 数学公式的渲染,提供插入数学公式的能力
8 |
9 | ## Commands 命令
10 |
11 | ##### getMath()
12 |
13 | 获取光标所在的数学公式节点
14 |
15 | - 类型
16 |
17 | ```ts
18 | getMath(): KNode | null
19 | ```
20 |
21 | - 详细信息
22 |
23 | 该方法可以获取光标所在的唯一数学公式节点,如果光标不在一个数学公式节点内,则返回 `null`
24 |
25 | - 示例
26 |
27 | ```ts
28 | const mathNode = editor.commands.getMath()
29 | ```
30 |
31 | ##### hasMath()
32 |
33 | 判断光标范围内是否有数学公式节点
34 |
35 | - 类型
36 |
37 | ```ts
38 | hasMath(): boolean
39 | ```
40 |
41 | - 详细信息
42 |
43 | 该方法用来判断光标范围内是否有数学公式节点,返回 `boolean` 值
44 |
45 | - 示例
46 |
47 | ```ts
48 | const has = editor.commands.hasMath()
49 | ```
50 |
51 | ##### setMath()
52 |
53 | 插入数学公式
54 |
55 | - 类型
56 |
57 | ```ts
58 | setMath(value: string): Promise
59 | ```
60 |
61 | - 详细信息
62 |
63 | 提供一个入参,类型为 `string`,表示插入的数学公式 `Latex` 语法字符串,该方法会向编辑器内插入数学公式节点,在插入完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
64 |
65 | 需要注意:当光标范围内包含其他的数学公式节点时,无法插入新的数学公式节点
66 |
67 | - 示例
68 |
69 | ```ts
70 | await editor.commands.setMath('\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}')
71 | ```
72 |
73 | ##### updateMath()
74 |
75 | 更新数学公式
76 |
77 | - 类型
78 |
79 | ```ts
80 | updateMath(value: string): Promise
81 | ```
82 |
83 | - 详细信息
84 |
85 | 提供一个入参,类型为 `string`,表示更新的数学公式 `Latex` 语法字符串,该方法更新当前光标所指向的唯一数学公式节点的内容,在更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
86 |
87 | 需要注意:当光标不在同一个数学公式节点内时,此方法无效,因为获取不到唯一的数学公式节点
88 |
89 | - 示例
90 |
91 | ```ts
92 | await editor.commands.updateMath('\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}')
93 | ```
94 |
95 | ## 代码示例
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
131 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/task.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: task 待办
3 | ---
4 |
5 | # task 待办
6 |
7 | 支持待办的渲染,提供插入待办的能力
8 |
9 | ## Commands 命令
10 |
11 | ##### getTask()
12 |
13 | 获取光标所在的待办节点
14 |
15 | - 类型
16 |
17 | ```ts
18 | getTask(): KNode | null
19 | ```
20 |
21 | - 详细信息
22 |
23 | 该方法可以获取光标所在的唯一待办节点,如果光标不在一个待办节点内,则返回 `null`
24 |
25 | - 示例
26 |
27 | ```ts
28 | const taskNode = editor.commands.getTask()
29 | ```
30 |
31 | ##### hasTask()
32 |
33 | 判断光标范围内是否有待办节点
34 |
35 | - 类型
36 |
37 | ```ts
38 | hasTask(): boolean
39 | ```
40 |
41 | - 详细信息
42 |
43 | 该方法用来判断光标范围内是否有待办节点,返回 `boolean` 值
44 |
45 | - 示例
46 |
47 | ```ts
48 | const hasTask = editor.commands.hasTask()
49 | ```
50 |
51 | ##### allTask()
52 |
53 | 光标范围内是否都是待办节点
54 |
55 | - 类型
56 |
57 | ```ts
58 | allTask(): boolean
59 | ```
60 |
61 | - 详细信息
62 |
63 | 该方法用来判断光标范围内都是待办节点,返回 `boolean` 值
64 |
65 | - 示例
66 |
67 | ```ts
68 | const allTask = editor.commands.allTask()
69 | ```
70 |
71 | ##### setTask()
72 |
73 | 设置待办
74 |
75 | - 类型
76 |
77 | ```ts
78 | setTask(): Promise
79 | ```
80 |
81 | - 详细信息
82 |
83 | 该方法会将光标所在的块节点都转为待办节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
84 |
85 | 如果通过 `allTask` 判断光标范围内都是待办节点,则不会继续执行
86 |
87 | - 示例
88 |
89 | ```ts
90 | await editor.commands.setTask()
91 | ```
92 |
93 | ##### unsetTask()
94 |
95 | 取消待办
96 |
97 | - 类型
98 |
99 | ```ts
100 | unsetTask(): Promise
101 | ```
102 |
103 | - 详细信息
104 |
105 | 该方法会将光标所在的待办节点都转为段落节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
106 |
107 | 如果通过 `allTask` 判断光标范围内不都是待办节点,则不会继续执行
108 |
109 | - 示例
110 |
111 | ```ts
112 | await editor.commands.unsetTask()
113 | ```
114 |
115 | ## 代码示例
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
151 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/code.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: code 行内代码
3 | ---
4 |
5 | # code 行内代码
6 |
7 | 支持行内代码的渲染,提供插入行内代码的能力
8 |
9 | ## Commands 命令
10 |
11 | ##### getCode()
12 |
13 | 获取光标所在的行内代码
14 |
15 | - 类型
16 |
17 | ```ts
18 | getCode(): KNode | null
19 | ```
20 |
21 | - 详细信息
22 |
23 | 该方法可以获取光标所在的唯一行内代码节点,如果光标不在一个行内代码节点内,则返回 `null`
24 |
25 | - 示例
26 |
27 | ```ts
28 | const codeNode = editor.commands.getCode()
29 | ```
30 |
31 | ##### hasCode()
32 |
33 | 判断光标范围内是否有行内代码节点
34 |
35 | - 类型
36 |
37 | ```ts
38 | hasCode(): boolean
39 | ```
40 |
41 | - 详细信息
42 |
43 | 该方法用来判断光标范围内是否有行内代码节点,返回 `boolean` 值
44 |
45 | - 示例
46 |
47 | ```ts
48 | const hasCode = editor.commands.hasCode()
49 | ```
50 |
51 | ##### allCode()
52 |
53 | 光标范围内是否都是行内代码节点
54 |
55 | - 类型
56 |
57 | ```ts
58 | allCode(): boolean
59 | ```
60 |
61 | - 详细信息
62 |
63 | 该方法用来判断光标范围内都是行内代码节点,返回 `boolean` 值
64 |
65 | - 示例
66 |
67 | ```ts
68 | const allCode = editor.commands.allCode()
69 | ```
70 |
71 | ##### setCode()
72 |
73 | 设置行内代码
74 |
75 | - 类型
76 |
77 | ```ts
78 | setCode(): Promise
79 | ```
80 |
81 | - 详细信息
82 |
83 | 该方法会在光标所在范围内设置行内代码,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
84 |
85 | 如果通过 `allCode` 判断光标范围内都是行内代码节点,则不会继续执行
86 |
87 | - 示例
88 |
89 | ```ts
90 | await editor.commands.setCode()
91 | ```
92 |
93 | ##### unsetCode()
94 |
95 | 取消行内代码
96 |
97 | - 类型
98 |
99 | ```ts
100 | unsetCode(): Promise
101 | ```
102 |
103 | - 详细信息
104 |
105 | 该方法会将光标所在范围内的行内代码全部取消,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
106 |
107 | 如果通过 `allCode` 判断光标范围内不都是行内代码节点,则不会继续执行
108 |
109 | - 示例
110 |
111 | ```ts
112 | await editor.commands.unsetCode()
113 | ```
114 |
115 | ## 代码示例
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
151 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/blockquote.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: blockquote 引用
3 | ---
4 |
5 | # blockquote 引用
6 |
7 | 支持引用的渲染,提供插入引用的能力
8 |
9 | ## Commands 命令
10 |
11 | ##### getBlockquote()
12 |
13 | 获取光标所在的引用节点
14 |
15 | - 类型
16 |
17 | ```ts
18 | getBlockquote(): KNode | null
19 | ```
20 |
21 | - 详细信息
22 |
23 | 该方法可以获取光标所在的唯一引用节点,如果光标不在一个引用节点内,则返回 `null`
24 |
25 | - 示例
26 |
27 | ```ts
28 | const blockquoteNode = editor.commands.getBlockquote()
29 | ```
30 |
31 | ##### hasBlockquote()
32 |
33 | 判断光标范围内是否有引用节点
34 |
35 | - 类型
36 |
37 | ```ts
38 | hasBlockquote(): boolean
39 | ```
40 |
41 | - 详细信息
42 |
43 | 该方法用来判断光标范围内是否有引用节点,返回 `boolean` 值
44 |
45 | - 示例
46 |
47 | ```ts
48 | const hasBlockquote = editor.commands.hasBlockquote()
49 | ```
50 |
51 | ##### allBlockquote()
52 |
53 | 光标范围内是否都是引用节点
54 |
55 | - 类型
56 |
57 | ```ts
58 | allBlockquote(): boolean
59 | ```
60 |
61 | - 详细信息
62 |
63 | 该方法用来判断光标范围内都是引用节点,返回 `boolean` 值
64 |
65 | - 示例
66 |
67 | ```ts
68 | const allBlockquote = editor.commands.allBlockquote()
69 | ```
70 |
71 | ##### setBlockquote()
72 |
73 | 设置引用
74 |
75 | - 类型
76 |
77 | ```ts
78 | setBlockquote(): Promise
79 | ```
80 |
81 | - 详细信息
82 |
83 | 该方法会将光标所在的块节点都转为引用节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
84 |
85 | 如果通过 `allBlockquote` 判断光标范围内都是引用节点,则不会继续执行
86 |
87 | - 示例
88 |
89 | ```ts
90 | await editor.commands.setBlockquote()
91 | ```
92 |
93 | ##### unsetBlockquote()
94 |
95 | 取消引用
96 |
97 | - 类型
98 |
99 | ```ts
100 | unsetBlockquote(): Promise
101 | ```
102 |
103 | - 详细信息
104 |
105 | 该方法会将光标所在的引用节点都转为段落节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
106 |
107 | 如果通过 `allBlockquote` 判断光标范围内不都是引用节点,则不会继续执行
108 |
109 | - 示例
110 |
111 | ```ts
112 | await editor.commands.unsetBlockquote()
113 | ```
114 |
115 | ## 代码示例
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
151 |
--------------------------------------------------------------------------------
/lib/model/config/function.d.ts:
--------------------------------------------------------------------------------
1 | import { Extension } from '../../extensions';
2 | import { Editor } from '../Editor';
3 | import { KNode } from '../KNode';
4 | import { RuleFunctionType } from './format-rules';
5 | /**
6 | * 获取选区内的可聚焦节点所在的块节点数组
7 | */
8 | export declare const getSelectionBlockNodes: (this: Editor) => KNode[];
9 | /**
10 | * 打散指定的节点,将其分裂成多个节点,如果子孙节点还有子节点则继续打散
11 | */
12 | export declare const splitNodeToNodes: (this: Editor, node: KNode) => void;
13 | /**
14 | * 清空固定块节点的内容
15 | */
16 | export declare const emptyFixedBlock: (this: Editor, node: KNode) => void;
17 | /**
18 | * 该方法目前只为delete方法内部使用:将后一个块节点与前一个块节点合并
19 | */
20 | export declare const mergeBlock: (this: Editor, node: KNode, target: KNode) => void;
21 | /**
22 | * 判断编辑器内的指定节点是否可以进行合并操作,parent表示和父节点进行合并,prevSibling表示和前一个兄弟节点进行合并,nextSibling表示和下一个兄弟节点合并,如果可以返回合并的对象节点
23 | */
24 | export declare const getAllowMergeNode: (this: Editor, node: KNode, type: "parent" | "prevSibling" | "nextSibling") => KNode | null;
25 | /**
26 | * 对编辑器内的某个节点执行合并操作,parent表示和父节点进行合并,prevSibling表示和前一个兄弟节点进行合并,nextSibling表示和下一个兄弟节点合并(可能会更新光标)
27 | */
28 | export declare const applyMergeNode: (this: Editor, node: KNode, type: "parent" | "prevSibling" | "nextSibling") => void;
29 | /**
30 | * 将编辑器内的某个非块级节点转为默认块级节点
31 | */
32 | export declare const convertToBlock: (this: Editor, node: KNode) => void;
33 | /**
34 | * 对节点数组使用指定规则进行格式化,nodes是需要格式化的节点数组,sourceNodes是格式化的节点所在的源数组
35 | */
36 | export declare const formatNodes: (this: Editor, rule: RuleFunctionType, nodes: KNode[], sourceNodes: KNode[]) => void;
37 | /**
38 | * 注册扩展
39 | */
40 | export declare const registerExtension: (this: Editor, extension: Extension) => void;
41 | /**
42 | * 根据真实光标更新selection,返回布尔值表示是否更新成功
43 | */
44 | export declare const updateSelection: (this: Editor) => boolean;
45 | /**
46 | * 纠正光标位置,返回布尔值表示是否存在纠正行为
47 | */
48 | export declare const redressSelection: (this: Editor) => boolean;
49 | /**
50 | * 初始化校验编辑器的节点数组,如果编辑器的节点数组为空或者都是空节点,则初始化创建一个只有占位符的段落
51 | */
52 | export declare const checkNodes: (this: Editor) => void;
53 | /**
54 | * 粘贴时对节点的标记和样式的保留处理
55 | */
56 | export declare const handlerForPasteKeepMarksAndStyles: (this: Editor, nodes: KNode[]) => void;
57 | /**
58 | * 粘贴时对文件的处理
59 | */
60 | export declare const handlerForPasteFiles: (this: Editor, files: FileList) => Promise;
61 | /**
62 | * 处理某个节点数组,针对为空的块级节点补充占位符,目前仅用于粘贴处理
63 | */
64 | export declare const fillPlaceholderToEmptyBlock: (this: Editor, nodes: KNode[]) => void;
65 | /**
66 | * 粘贴处理
67 | */
68 | export declare const handlerForPasteDrop: (this: Editor, dataTransfer: DataTransfer) => Promise;
69 | /**
70 | * 将指定的非固定块节点从父节点(非固定块节点)中抽离,插入到和父节点同级的位置
71 | */
72 | export declare const removeBlockFromParentToSameLevel: (this: Editor, node: KNode) => void;
73 | /**
74 | * 光标所在的块节点不是只有占位符,且非固定块节点,非代码块样式的块节点,在该块节点内正常换行方法
75 | */
76 | export declare const handlerForNormalInsertParagraph: (this: Editor) => void;
77 | /**
78 | * 设置placeholder,在每次视图更新时调用此方法
79 | */
80 | export declare const setPlaceholder: (this: Editor) => void;
81 | /**
82 | * 合并扩展数组(只能用在扩展注册之前)
83 | */
84 | export declare const mergeExtensions: (this: Editor, from: Extension[]) => Extension[];
85 |
--------------------------------------------------------------------------------
/src/model/History.ts:
--------------------------------------------------------------------------------
1 | import { KNode } from './KNode'
2 | import { Selection } from './Selection'
3 | /**
4 | * 历史记录的record类型
5 | */
6 | export type HistoryRecordType = {
7 | nodes: KNode[]
8 | selection: Selection
9 | }
10 | /**
11 | * 历史记录
12 | */
13 | export class History {
14 | /**
15 | * 存放历史记录的堆栈
16 | */
17 | records: HistoryRecordType[] = []
18 | /**
19 | * 存放撤销记录的堆栈
20 | */
21 | redoRecords: HistoryRecordType[] = []
22 |
23 | /**
24 | * 复制selection
25 | */
26 | cloneSelection(newNodes: KNode[], selection: Selection) {
27 | const newSelection = new Selection()
28 | //如果存在选区
29 | if (selection.focused()) {
30 | //查找新的节点数组中start对应的节点
31 | const startNode = KNode.searchByKey(selection.start!.node.key, newNodes)
32 | //查找新的节点数组中end对应的节点
33 | const endNode = KNode.searchByKey(selection.end!.node.key, newNodes)
34 | //如果都存在
35 | if (startNode && endNode) {
36 | newSelection.start = {
37 | node: startNode,
38 | offset: selection.start!.offset
39 | }
40 | newSelection.end = {
41 | node: endNode,
42 | offset: selection.end!.offset
43 | }
44 | }
45 | }
46 | return newSelection
47 | }
48 |
49 | /**
50 | * 保存新的记录
51 | */
52 | setState(nodes: KNode[], selection: Selection) {
53 | const newNodes = nodes.map(item => item.fullClone())
54 | const newSelection = this.cloneSelection(newNodes, selection)
55 | this.records.push({
56 | nodes: newNodes,
57 | selection: newSelection
58 | })
59 | //每次保存新状态时清空撤销记录的堆栈
60 | this.redoRecords = []
61 | }
62 |
63 | /**
64 | * 撤销操作:返回上一个历史记录
65 | */
66 | setUndo(): HistoryRecordType | null {
67 | //存在的历史记录大于1则表示可以进行撤销操作
68 | if (this.records.length > 1) {
69 | //取出最近的历史记录
70 | const record = this.records.pop()!
71 | //将这个历史记录加入到撤销记录数组中
72 | this.redoRecords.push(record)
73 | //再次获取历史记录数组中的最近的一个
74 | const lastRecord = this.records[this.records.length - 1]
75 | const newNodes = lastRecord.nodes.map(item => item.fullClone())
76 | const newSelection = this.cloneSelection(newNodes, lastRecord.selection)
77 | return {
78 | nodes: newNodes,
79 | selection: newSelection
80 | }
81 | }
82 | //没有历史记录则返回null
83 | return null
84 | }
85 |
86 | /**
87 | * 重做操作:返回下一个历史记录
88 | */
89 | setRedo(): HistoryRecordType | null {
90 | //如果存在撤销记录
91 | if (this.redoRecords.length > 0) {
92 | //取出最近的一个撤销记录
93 | const record = this.redoRecords.pop()!
94 | //将撤销记录加入历史记录中
95 | this.records.push(record)
96 | //返回取出的这个撤销记录,即最近的一个历史记录
97 | const newNodes = record.nodes.map(item => item.fullClone())
98 | const newSelection = this.cloneSelection(newNodes, record.selection)
99 | return {
100 | nodes: newNodes,
101 | selection: newSelection
102 | }
103 | }
104 | return null
105 | }
106 |
107 | /**
108 | * 更新当前记录的编辑器的光标
109 | */
110 | updateSelection(selection: Selection) {
111 | if (this.records.length === 0) return
112 | const record = this.records[this.records.length - 1]
113 | const newSelection = this.cloneSelection(record.nodes, selection)
114 | this.records[this.records.length - 1].selection = newSelection
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/image.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: image 图片
3 | ---
4 |
5 | # image 图片
6 |
7 | 支持图片的渲染,提供插入图片的能力,并且支持拖拽图片的右侧边缘可修改大小
8 |
9 | ## Commands 命令
10 |
11 | ##### getImage()
12 |
13 | 获取光标所在的图片节点
14 |
15 | - 类型
16 |
17 | ```ts
18 | getImage(): KNode | null
19 | ```
20 |
21 | - 详细信息
22 |
23 | 该方法可以获取光标所在的唯一图片节点,如果光标不在一个图片节点内,则返回 `null`
24 |
25 | - 示例
26 |
27 | ```ts
28 | const imageNode = editor.commands.getImage()
29 | ```
30 |
31 | ##### hasImage()
32 |
33 | 判断光标范围内是否有图片节点
34 |
35 | - 类型
36 |
37 | ```ts
38 | hasImage(): boolean
39 | ```
40 |
41 | - 详细信息
42 |
43 | 该方法用来判断光标范围内是否有图片节点,返回 `boolean` 值
44 |
45 | - 示例
46 |
47 | ```ts
48 | const has = editor.commands.hasImage()
49 | ```
50 |
51 | ##### setImage()
52 |
53 | 插入图片
54 |
55 | - 类型
56 |
57 | ```ts
58 | setImage(options: SetImageOptionType): Promise
59 | ```
60 |
61 | - 详细信息
62 |
63 | 提供一个入参,类型为 `SetImageOptionType`,包含 3 个属性:
64 |
65 | - src :图片的链接地址
66 | - alt :图片加载失败显示的值
67 | - width :图片的初始宽度
68 |
69 | 该方法会向编辑器内插入图片节点,在插入完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
70 |
71 | - 示例
72 |
73 | ```ts
74 | await editor.commands.setImage({
75 | src: 'https://xxxxx.png',
76 | alt: '图片加载失败'
77 | })
78 | ```
79 |
80 | ##### updateImage()
81 |
82 | 更新图片信息
83 |
84 | - 类型
85 |
86 | ```ts
87 | updateImage(options: UpdateImageOptionType): Promise
88 | ```
89 |
90 | - 详细信息
91 |
92 | 提供一个入参,类型为 `UpdateImageOptionType`,包含以下 2 个属性:
93 |
94 | - src :图片的链接地址,可选,不设置则不更新此属性
95 | - alt :图片加载失败显示的值,不设置或者设置为空值则移除此属性
96 |
97 | 该方法可以更新图片信息,并且在更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
98 |
99 | - 示例
100 |
101 | ```ts
102 | await editor.commands.updateImage({
103 | src: 'www.baidu.com'
104 | })
105 | ```
106 |
107 | ## 代码示例
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
151 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/video.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: video 视频
3 | ---
4 |
5 | # video 视频
6 |
7 | 支持视频的渲染,提供插入视频的能力,并且支持拖拽视频的右侧边缘可修改大小
8 |
9 | ## Commands 命令
10 |
11 | ##### getVideo()
12 |
13 | 获取光标所在的视频节点
14 |
15 | - 类型
16 |
17 | ```ts
18 | getVideo(): KNode | null
19 | ```
20 |
21 | - 详细信息
22 |
23 | 该方法可以获取光标所在的唯一视频节点,如果光标不在一个视频节点内,则返回 `null`
24 |
25 | - 示例
26 |
27 | ```ts
28 | const videoNode = editor.commands.getVideo()
29 | ```
30 |
31 | ##### hasVideo()
32 |
33 | 判断光标范围内是否有视频节点
34 |
35 | - 类型
36 |
37 | ```ts
38 | hasVideo(): boolean
39 | ```
40 |
41 | - 详细信息
42 |
43 | 该方法用来判断光标范围内是否有视频节点,返回 `boolean` 值
44 |
45 | - 示例
46 |
47 | ```ts
48 | const hasVideo = editor.commands.hasVideo()
49 | ```
50 |
51 | ##### setVideo()
52 |
53 | 插入视频
54 |
55 | - 类型
56 |
57 | ```ts
58 | setVideo(options: SetVideoOptionType): Promise
59 | ```
60 |
61 | - 详细信息
62 |
63 | 提供一个入参,类型为 `SetVideoOptionType`,包含 3 个属性:
64 |
65 | - src :视频的链接地址
66 | - autoplay :视频加载完成是否自动播放
67 | - width :视频的初始宽度
68 |
69 | 该方法会向编辑器内插入视频节点,在插入完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
70 |
71 | - 示例
72 |
73 | ```ts
74 | await editor.commands.setVideo({
75 | src: 'https://xxxxx.mp4',
76 | autoplay: true
77 | })
78 | ```
79 |
80 | ##### updateVideo()
81 |
82 | 更新视频信息
83 |
84 | - 类型
85 |
86 | ```ts
87 | updateVideo(options: UpdateVideoOptionType): Promise
88 | ```
89 |
90 | - 详细信息
91 |
92 | 提供一个入参,类型为 `UpdateVideoOptionType`,包含以下 3 个属性:
93 |
94 | - controls :视频是否显示控制器,不设置则不更新此属性
95 | - muted :视频是否静音,不设置则不更新此属性
96 | - loop :视频是否循环,不设置则不更新此属性
97 |
98 | 该方法可以更新视频相关的设定,并且在更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
99 |
100 | - 示例
101 |
102 | ```ts
103 | await editor.commands.updateVideo({
104 | loop: true
105 | })
106 | ```
107 |
108 | ## 代码示例
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
152 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/heading.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: hading 标题
3 | ---
4 |
5 | # hading 标题
6 |
7 | 支持标题的渲染,提供插入标题的能力
8 |
9 | ## Commands 命令
10 |
11 | ##### getHeading()
12 |
13 | 获取光标所在的指定等级的标题节点
14 |
15 | - 类型
16 |
17 | ```ts
18 | getHeading(level: HeadingLevelType): KNode | null
19 | ```
20 |
21 | - 详细信息
22 |
23 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法可以获取光标所在的唯一指定的标题节点,如果光标不在一个该指定节点内,则返回 `null`
24 |
25 | - 示例
26 |
27 | ```ts
28 | //获取h1,即一级标题
29 | const headingNode = editor.commands.getHeading(1)
30 | ```
31 |
32 | ##### hasHeading()
33 |
34 | 判断光标范围内是否有指定等级的标题节点
35 |
36 | - 类型
37 |
38 | ```ts
39 | hasHeading(level: HeadingLevelType): boolean
40 | ```
41 |
42 | - 详细信息
43 |
44 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法用来判断光标范围内是否有指定等级的标题节点,返回 `boolean` 值
45 |
46 | - 示例
47 |
48 | ```ts
49 | //判断光标范围内是否有h1,即一级标题
50 | const hasHeading = editor.commands.hasHeading(1)
51 | ```
52 |
53 | ##### allHeading()
54 |
55 | 光标范围内是否都是指定等级的标题节点
56 |
57 | - 类型
58 |
59 | ```ts
60 | allHeading(level: HeadingLevelType): boolean
61 | ```
62 |
63 | - 详细信息
64 |
65 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法用来判断光标范围内都是指定等级的标题节点,返回 `boolean` 值
66 |
67 | - 示例
68 |
69 | ```ts
70 | //判断光标范围内是否都是h1,即一级标题
71 | const allHeading = editor.commands.allHeading(1)
72 | ```
73 |
74 | ##### setHeading()
75 |
76 | 设置标题
77 |
78 | - 类型
79 |
80 | ```ts
81 | setHeading(level: HeadingLevelType): Promise
82 | ```
83 |
84 | - 详细信息
85 |
86 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法会将光标所在的块节点都转为指定等级的标题节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
87 |
88 | 如果通过 `allHeading` 判断光标范围内都是该等级的标题节点,则不会继续执行
89 |
90 | - 示例
91 |
92 | ```ts
93 | await editor.commands.setHeading(1)
94 | ```
95 |
96 | ##### unsetHeading()
97 |
98 | 取消标题
99 |
100 | - 类型
101 |
102 | ```ts
103 | unsetHeading(): Promise
104 | ```
105 |
106 | - 详细信息
107 |
108 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法会将光标所在的指定标题节点都转为段落节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
109 |
110 | 如果通过 `allHeading` 判断光标范围内不都是该等级的标题节点,则不会继续执行
111 |
112 | - 示例
113 |
114 | ```ts
115 | await editor.commands.unsetHeading()
116 | ```
117 |
118 | ## 代码示例
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
154 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/link.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: link 链接
3 | ---
4 |
5 | # link 链接
6 |
7 | 支持链接的渲染,提供插入链接的能力
8 |
9 | ## Commands 命令
10 |
11 | ##### getLink()
12 |
13 | 获取光标所在的链接节点
14 |
15 | - 类型
16 |
17 | ```ts
18 | getLink(): KNode | null
19 | ```
20 |
21 | - 详细信息
22 |
23 | 该方法可以获取光标所在的唯一链接节点,如果光标不在一个链接节点内,则返回 `null`
24 |
25 | - 示例
26 |
27 | ```ts
28 | const linkNode = editor.commands.getLink()
29 | ```
30 |
31 | ##### hasLink()
32 |
33 | 判断光标范围内是否有链接节点
34 |
35 | - 类型
36 |
37 | ```ts
38 | hasLink(): boolean
39 | ```
40 |
41 | - 详细信息
42 |
43 | 该方法用来判断光标范围内是否有链接节点,返回 `boolean` 值
44 |
45 | - 示例
46 |
47 | ```ts
48 | const has = editor.commands.hasLink()
49 | ```
50 |
51 | ##### setLink()
52 |
53 | 插入链接
54 |
55 | - 类型
56 |
57 | ```ts
58 | setLink(options: SetLinkOptionType): Promise
59 | ```
60 |
61 | - 详细信息
62 |
63 | 提供一个入参,类型为 `SetLinkOptionType`,包含 3 个属性:
64 |
65 | - href :链接的地址
66 | - text :链接的文本,如果光标选择了一段内容,则使用光标选择的内容
67 | - newOpen :链接是否新窗口打开
68 |
69 | 该方法会向编辑器内插入链接节点,在插入完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
70 |
71 | 需要注意:当光标范围内包含其他的链接节点时,无法插入新的链接节点
72 |
73 | - 示例
74 |
75 | ```ts
76 | await editor.commands.setLink({
77 | href: 'https://www.baidu.com',
78 | text: '百度一下,你就知道'
79 | })
80 | ```
81 |
82 | ##### unsetLink()
83 |
84 | 取消链接
85 |
86 | - 类型
87 |
88 | ```ts
89 | unsetLink(): Promise
90 | ```
91 |
92 | - 详细信息
93 |
94 | 该方法会取消当前光标所在的唯一链接,在取消完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
95 |
96 | - 示例
97 |
98 | ```ts
99 | await editor.commands.setLink({
100 | href: 'https://www.baidu.com',
101 | text: '百度一下,你就知道'
102 | })
103 | ```
104 |
105 | ##### updateLink()
106 |
107 | 更新链接信息
108 |
109 | - 类型
110 |
111 | ```ts
112 | updateLink(options: UpdateLinkOptionType): Promise
113 | ```
114 |
115 | - 详细信息
116 |
117 | 提供一个入参,类型为 `UpdateLinkOptionType`,包含以下 2 个属性:
118 |
119 | - href :链接的地址,可选,不设置则不更新此属性
120 | - newOpen :链接是否新窗口打开,可选,不设置则不更新此属性
121 |
122 | 该方法可以自由地更新链接的地址、文本等,并且在更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
123 |
124 | 需要注意:当光标不在同一个链接节点内时,此方法无效,因为获取不到唯一的链接节点
125 |
126 | - 示例
127 |
128 | ```ts
129 | await editor.commands.updateLink({
130 | href: 'www.baidu.com'
131 | })
132 | ```
133 |
134 | ## 代码示例
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
170 |
--------------------------------------------------------------------------------
/src/extensions/align/index.ts:
--------------------------------------------------------------------------------
1 | import { KNode, KNodeStylesType } from '@/model'
2 | import { deleteProperty } from '@/tools'
3 | import { Extension } from '../Extension'
4 | import { getSelectionBlockNodes } from '@/model/config/function'
5 |
6 | export type AlignValueType = 'left' | 'right' | 'center' | 'justify'
7 |
8 | declare module '../../model' {
9 | interface EditorCommandsType {
10 | /**
11 | * 光标所在的块节点是否都是符合的对齐方式
12 | */
13 | isAlign?: (value: AlignValueType) => boolean
14 | /**
15 | * 设置对齐方式
16 | */
17 | setAlign?: (value: AlignValueType) => Promise
18 | /**
19 | * 取消对齐方式
20 | */
21 | unsetAlign?: (value: AlignValueType) => Promise
22 | }
23 | }
24 |
25 | /**
26 | * 删除指定块节点及以上块节点的对齐方式
27 | */
28 | const clearAlign = (blockNode: KNode, value: AlignValueType) => {
29 | const matchNode = blockNode.getMatchNode({ styles: { textAlign: value } })
30 | if (matchNode) {
31 | matchNode.styles = deleteProperty(matchNode.styles!, 'textAlign')
32 | clearAlign(matchNode, value)
33 | }
34 | }
35 |
36 | export const AlignExtension = () =>
37 | Extension.create({
38 | name: 'align',
39 | onPasteKeepStyles(node) {
40 | const styles: KNodeStylesType = {}
41 | if (node.isBlock() && node.hasStyles()) {
42 | if (node.styles!.hasOwnProperty('textAlign')) styles.textAlign = node.styles!.textAlign
43 | }
44 | return styles
45 | },
46 | addCommands() {
47 | const isAlign = (value: AlignValueType) => {
48 | if (!this.selection.focused()) {
49 | return false
50 | }
51 | //起点和终点在一起
52 | if (this.selection.collapsed()) {
53 | const block = this.selection.start!.node.getBlock()
54 | return !!block.getMatchNode({ styles: { textAlign: value } })
55 | }
56 | //起点和终点不在一起
57 | const blockNodes = getSelectionBlockNodes.apply(this)
58 | return blockNodes.every(item => {
59 | return !!item.getMatchNode({ styles: { textAlign: value } })
60 | })
61 | }
62 |
63 | const setAlign = async (value: AlignValueType) => {
64 | if (isAlign(value)) {
65 | return
66 | }
67 | //起点和终点在一起
68 | if (this.selection.collapsed()) {
69 | const blockNode = this.selection.start!.node.getBlock()
70 | const styles: KNodeStylesType = blockNode.hasStyles() ? blockNode.styles! : {}
71 | blockNode.styles = {
72 | ...styles,
73 | textAlign: value
74 | }
75 | }
76 | //起点和终点不在一起
77 | else {
78 | const blockNodes = getSelectionBlockNodes.apply(this)
79 | blockNodes.forEach(item => {
80 | const styles: KNodeStylesType = item.hasStyles() ? item.styles! : {}
81 | item.styles = {
82 | ...styles,
83 | textAlign: value
84 | }
85 | })
86 | }
87 | await this.updateView()
88 | }
89 |
90 | const unsetAlign = async (value: AlignValueType) => {
91 | if (!isAlign(value)) {
92 | return
93 | }
94 | //起点和终点在一起
95 | if (this.selection.collapsed()) {
96 | const blockNode = this.selection.start!.node.getBlock()
97 | clearAlign(blockNode, value)
98 | }
99 | //起点和终点不在一起
100 | else {
101 | const blockNodes = getSelectionBlockNodes.apply(this)
102 | blockNodes.forEach(item => {
103 | clearAlign(item, value)
104 | })
105 | }
106 | await this.updateView()
107 | }
108 |
109 | return {
110 | isAlign,
111 | setAlign,
112 | unsetAlign
113 | }
114 | }
115 | })
116 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/code-block.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: code-block 代码块
3 | ---
4 |
5 | # code-block 代码块
6 |
7 | 支持代码块的渲染,提供插入代码块的能力,注:在代码块内按下 `Tab` 键会插入 2 个空格
8 |
9 | ## Commands 命令
10 |
11 | ##### getCodeBlock()
12 |
13 | 获取光标所在的代码块节点
14 |
15 | - 类型
16 |
17 | ```ts
18 | getCodeBlock(): KNode | null
19 | ```
20 |
21 | - 详细信息
22 |
23 | 该方法可以获取光标所在的唯一代码块节点,如果光标不在一个代码块节点内,则返回 `null`
24 |
25 | - 示例
26 |
27 | ```ts
28 | const codeBlockNode = editor.commands.getCodeBlock()
29 | ```
30 |
31 | ##### hasCodeBlock()
32 |
33 | 判断光标范围内是否有代码块节点
34 |
35 | - 类型
36 |
37 | ```ts
38 | hasCodeBlock(): boolean
39 | ```
40 |
41 | - 详细信息
42 |
43 | 该方法用来判断光标范围内是否有代码块节点,返回 `boolean` 值
44 |
45 | - 示例
46 |
47 | ```ts
48 | const hasCodeBlock = editor.commands.hasCodeBlock()
49 | ```
50 |
51 | ##### allCodeBlock()
52 |
53 | 光标范围内是否都是代码块节点
54 |
55 | - 类型
56 |
57 | ```ts
58 | allCodeBlock(): boolean
59 | ```
60 |
61 | - 详细信息
62 |
63 | 该方法用来判断光标范围内都是代码块节点,返回 `boolean` 值
64 |
65 | - 示例
66 |
67 | ```ts
68 | const allCodeBlock = editor.commands.allCodeBlock()
69 | ```
70 |
71 | ##### setCodeBlock()
72 |
73 | 设置代码块
74 |
75 | - 类型
76 |
77 | ```ts
78 | setCodeBlock(): Promise
79 | ```
80 |
81 | - 详细信息
82 |
83 | 该方法会将光标所在的块节点都转为代码块节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
84 |
85 | 如果通过 `allCodeBlock` 判断光标范围内都是代码块节点,则不会继续执行
86 |
87 | - 示例
88 |
89 | ```ts
90 | await editor.commands.setCodeBlock()
91 | ```
92 |
93 | ##### unsetCodeBlock()
94 |
95 | 取消代码块
96 |
97 | - 类型
98 |
99 | ```ts
100 | unsetCodeBlock(): Promise
101 | ```
102 |
103 | - 详细信息
104 |
105 | 该方法会将光标所在的代码块节点都转为段落节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
106 |
107 | 如果通过 `allCodeBlock` 判断光标范围内不都是代码块节点,则不会继续执行
108 |
109 | - 示例
110 |
111 | ```ts
112 | await editor.commands.unsetCodeBlock()
113 | ```
114 |
115 | ##### updateCodeBlockLanguage()
116 |
117 | 更新光标所在代码块的语言类型
118 |
119 | - 类型
120 |
121 | ```ts
122 | updateCodeBlockLanguage(language?: HljsLanguageType): Promise
123 | ```
124 |
125 | - 详细信息
126 |
127 | 提供一个入参,类型为 `HljsLanguageType`,取值范围是 `plaintext` `json` `javascript` `java` `typescript` `python` `php` `css` `less` `scss` `html` `markdown` `objectivec` `swift` `dart` `nginx` `http` `go` `ruby` `c` `cpp` `csharp` `sql` `shell` `r` `kotlin` `rust` ,表示更新的语言值,该方法会修改所在代码块的语言类型,以达到不同的高亮效果,更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
128 |
129 | 如果光标不是在唯一的代码块节点内,则不会执行
130 |
131 | - 示例
132 |
133 | ```ts
134 | await editor.commands.updateCodeBlockLanguage('java')
135 | ```
136 |
137 | ## 代码示例
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
173 |
--------------------------------------------------------------------------------
/src/extensions/line-height/index.ts:
--------------------------------------------------------------------------------
1 | import { KNode, KNodeStylesType } from '@/model'
2 | import { getSelectionBlockNodes } from '@/model/config/function'
3 | import { deleteProperty } from '@/tools'
4 | import { Extension } from '../Extension'
5 |
6 | declare module '../../model' {
7 | interface EditorCommandsType {
8 | /**
9 | * 光标所在的块节点是否都是符合的行高
10 | */
11 | isLineHeight?: (value: string | number) => boolean
12 | /**
13 | * 设置行高
14 | */
15 | setLineHeight?: (value: string | number) => Promise
16 | /**
17 | * 取消行高
18 | */
19 | unsetLineHeight?: (value: string | number) => Promise
20 | }
21 | }
22 |
23 | /**
24 | * 删除指定块节点及以上块节点的行高样式
25 | */
26 | const clearLineHeight = (blockNode: KNode, value: string | number) => {
27 | const matchNode = blockNode.getMatchNode({ styles: { lineHeight: value } })
28 | if (matchNode) {
29 | matchNode.styles = deleteProperty(matchNode.styles!, 'lineHeight')
30 | clearLineHeight(matchNode, value)
31 | }
32 | }
33 |
34 | export const LineHeightExtension = () =>
35 | Extension.create({
36 | name: 'lineHeight',
37 | onPasteKeepStyles(node) {
38 | const styles: KNodeStylesType = {}
39 | if (node.isBlock() && node.hasStyles()) {
40 | if (node.styles!.hasOwnProperty('lineHeight')) styles.lineHeight = node.styles!.lineHeight
41 | }
42 | return styles
43 | },
44 | addCommands() {
45 | const isLineHeight = (value: string | number) => {
46 | if (!this.selection.focused()) {
47 | return false
48 | }
49 | //起点和终点在一起
50 | if (this.selection.collapsed()) {
51 | const block = this.selection.start!.node.getBlock()
52 | return !!block.getMatchNode({ styles: { lineHeight: value } })
53 | }
54 | //起点和终点不在一起
55 | const blockNodes = getSelectionBlockNodes.apply(this)
56 | return blockNodes.every(item => {
57 | return !!item.getMatchNode({ styles: { lineHeight: value } })
58 | })
59 | }
60 |
61 | const setLineHeight = async (value: string | number) => {
62 | if (isLineHeight(value)) {
63 | return
64 | }
65 | //起点和终点在一起
66 | if (this.selection.collapsed()) {
67 | const blockNode = this.selection.start!.node.getBlock()
68 | const styles: KNodeStylesType = blockNode.hasStyles() ? blockNode.styles! : {}
69 | blockNode.styles = {
70 | ...styles,
71 | lineHeight: value
72 | }
73 | }
74 | //起点和终点不在一起
75 | else {
76 | const blockNodes = getSelectionBlockNodes.apply(this)
77 | blockNodes.forEach(item => {
78 | const styles: KNodeStylesType = item.hasStyles() ? item.styles! : {}
79 | item.styles = {
80 | ...styles,
81 | lineHeight: value
82 | }
83 | })
84 | }
85 | await this.updateView()
86 | }
87 |
88 | const unsetLineHeight = async (value: string | number) => {
89 | if (!isLineHeight(value)) {
90 | return
91 | }
92 | //起点和终点在一起
93 | if (this.selection.collapsed()) {
94 | const blockNode = this.selection.start!.node.getBlock()
95 | clearLineHeight(blockNode, value)
96 | }
97 | //起点和终点不在一起
98 | else {
99 | const blockNodes = getSelectionBlockNodes.apply(this)
100 | blockNodes.forEach(item => {
101 | clearLineHeight(item, value)
102 | })
103 | }
104 | await this.updateView()
105 | }
106 |
107 | return {
108 | isLineHeight,
109 | setLineHeight,
110 | unsetLineHeight
111 | }
112 | }
113 | })
114 |
--------------------------------------------------------------------------------
/docs/guide/knode.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: KNode
3 | ---
4 |
5 | # KNode
6 |
7 | > KNode 是编辑器内容的唯一组成元素,可以说编辑器的任何操作都是与 KNode 有关的,也就是我们后面通常所说的节点
8 |
9 | 富文本编辑器的内容原本都是 html 元素,但在 kaitify 的内部实现中,都被转换成了一个个节点,从而构成一个节点数组,挂载在编辑器实例属性 `stackNodes` 下
10 |
11 | ## 如何创建一个节点?
12 |
13 | 通过 `KNode.create` 方法来创建一个节点
14 |
15 | ```ts
16 | const node = KNode.create({
17 | type: 'block',
18 | tag: 'p',
19 | children: [
20 | {
21 | type: 'text',
22 | textContent: '这是一个段落'
23 | }
24 | ]
25 | })
26 | ```
27 |
28 | ## 节点构建参数 KNodeCreateOptionType
29 |
30 | ##### type
31 |
32 | 节点类型,可取值 `block` `inline` `closed` `text`
33 |
34 | - `text`:文本节点,表示一段文本内容,没有 `tag` 属性,没有子节点,在视图渲染时会根据编辑器实例属性 `textRenderTag` 来渲染成对应的 dom
35 | - `closed`:闭合节点,即没有子节点的节点,如图片、视频等
36 | - `inline`:行内节点,必须有子节点,子节点可以是 `text`、`closed` 和 `inline` 类型的
37 | - `block`:块节点,编辑器的 `stackNodes` 数组里的都是块节点,块节点的子节点可以是其他节点,但是其他节点不能作为块节点的父节点
38 |
39 | ##### tag
40 |
41 | 节点渲染成 dom 的真实元素标签,如`p`、`span`等,文本节点不需要设置此参数
42 |
43 | ##### textContent
44 |
45 | 文本节点的独有属性,表示节点的文本内容,非文本节点不需要设置此参数
46 |
47 | ##### marks
48 |
49 | 节点的标记集合,渲染成 `dom` 后表示元素的属性集合,但是不包括 `style` 属性
50 |
51 | ##### styles
52 |
53 | 节点的样式集合,样式名称请使用驼峰写法,如 `backgroundColor`,`textAlign` 等
54 |
55 | ##### locked
56 |
57 | 是否锁定节点,锁定的节点不会被编辑器格式化校验时与其他节点进行合并,默认值为 `false`
58 |
59 | - 针对块节点:在符合合并条件的情况下是否允许编辑器将其与父节点或者子节点进行合并;
60 | - 针对行内节点:在符合合并条件的情况下是否允许编辑器将其与相邻节点或者父节点或者子节点进行合并;
61 | - 针对文本节点:在符合合并的条件下是否允许编辑器将其与相邻节点或者父节点进行合并。
62 |
63 | ##### fixed
64 |
65 | 是否为固定块节点,值为 `true` 时:当光标在节点起始处或者光标在节点内只有占位符时,执行删除操作不会删除此节点,会再次创建一个占位符进行处理;当光标在节点内且节点不是代码块样式,不会进行换行,默认值为 `false`
66 |
67 | ##### nested
68 |
69 | 是否为固定格式的内嵌块节点,如 `li`、`tr`、`td` 等,默认值为 `false`
70 |
71 | ##### void
72 |
73 | 是否为不可见节点,意味着此类节点在编辑器内视图内无法看到,如`colgroup`、`col`等,默认值为 `false`
74 |
75 | ##### namespace
76 |
77 | 渲染 `dom` 所用到的命名空间。如果此值不存在,在默认的渲染方法中使用 `document.createElement` 方法来创建 `dom` 元素;如果此值存在,在默认的渲染方法中则会使用 `document.createElementNS` 方法来创建 `dom` 元素
78 |
79 | ##### children
80 |
81 | 子节点构建参数数组,文本节点和闭合节点无需设置此属性
82 |
83 | ## 零宽度空白文本节点
84 |
85 | 编辑器设定了一个非常特殊的文本节点,该节点内容没有长度,在页面表现上仅仅是一个光标的占位大小,我们称之为“零宽度空白文本节点”。这类节点在很多场景下有非常特殊的用途。
86 |
87 | 那么如何创建一个零宽度空白文本节点呢?
88 |
89 | 可以通过`KNode.createZeroWidthText`方法来创建:
90 |
91 | ```ts
92 | const zeroTextNode = KNode.createZeroWidthText(options)
93 | ```
94 |
95 | 该方法的入参是一个对象,包含以下属性:
96 |
97 | ##### marks
98 |
99 | 同创建节点入参的 `marks` 属性,表示零宽度空白文本节点的标记
100 |
101 | ##### styles
102 |
103 | 同创建节点入参的 `styles` 属性,表示零宽度空白文本节点的样式
104 |
105 | ##### namespace
106 |
107 | 同创建节点入参的 `namespace` 属性,表示零宽度空白文本节点的命名空间
108 |
109 | ##### locked
110 |
111 | 同创建节点入参的 `locked` 属性,表示零宽度空白文本节点是否锁定
112 |
113 | ## 占位符节点
114 |
115 | 编辑器中标签 `
` 被视为占位符节点,该节点只能存在于块节点的子节点中,并且与其他节点无法共存,其通常仅在块节点无内容时作占位使用
116 | 编辑器提供了一个方法来快速创建一个占位符节点:
117 |
118 | ```ts
119 | const placeholderNode = KNode.createPlaceholder()
120 | ```
121 |
122 | 当然你也可以使用 `create` 方法来创建,只是相对繁琐:
123 |
124 | ```ts
125 | const placeholderNode = KNode.create({
126 | type: 'closed',
127 | tag: 'br'
128 | })
129 | ```
130 |
131 | ## 空节点
132 |
133 | - 对于文本节点,没有文本内容则视为空节点
134 | - 对于行内节点和块节点,没有子节点则视为空节点
135 | - 对于有子节点的节点,所有的子节点都是空节点的情况下,也视为空节点
136 |
137 | 编辑器内部会自动过滤所有的空节点,因为在 `kaitify` 中,空节点被视为无意义的节点,所以我们在创建节点时,需要避免创建空节点,以防止导致开发的功能未能按照预期执行
138 |
--------------------------------------------------------------------------------
/src/extensions/code-block/hljs.ts:
--------------------------------------------------------------------------------
1 | //引入核心库
2 | import hljs from 'highlight.js/lib/core'
3 | //引入语言支持
4 | import plaintext from 'highlight.js/lib/languages/plaintext'
5 | import json from 'highlight.js/lib/languages/json'
6 | import javascript from 'highlight.js/lib/languages/javascript'
7 | import java from 'highlight.js/lib/languages/java'
8 | import typescript from 'highlight.js/lib/languages/typescript'
9 | import python from 'highlight.js/lib/languages/python'
10 | import php from 'highlight.js/lib/languages/php'
11 | import css from 'highlight.js/lib/languages/css'
12 | import less from 'highlight.js/lib/languages/less'
13 | import scss from 'highlight.js/lib/languages/scss'
14 | import html from 'highlight.js/lib/languages/xml'
15 | import markdown from 'highlight.js/lib/languages/markdown'
16 | import objectivec from 'highlight.js/lib/languages/objectivec'
17 | import swift from 'highlight.js/lib/languages/swift'
18 | import dart from 'highlight.js/lib/languages/dart'
19 | import nginx from 'highlight.js/lib/languages/nginx'
20 | import go from 'highlight.js/lib/languages/go'
21 | import http from 'highlight.js/lib/languages/http'
22 | import ruby from 'highlight.js/lib/languages/ruby'
23 | import c from 'highlight.js/lib/languages/c'
24 | import cpp from 'highlight.js/lib/languages/cpp'
25 | import csharp from 'highlight.js/lib/languages/csharp'
26 | import sql from 'highlight.js/lib/languages/sql'
27 | import shell from 'highlight.js/lib/languages/shell'
28 | import r from 'highlight.js/lib/languages/r'
29 | import kotlin from 'highlight.js/lib/languages/kotlin'
30 | import rust from 'highlight.js/lib/languages/rust'
31 | //引入css样式主题
32 | import './hljs.less'
33 | //import 'highlight.js/styles/github.css'
34 | //import 'highlight.js/styles/atom-one-light.css'
35 | //import 'highlight.js/styles/lightfair.css'
36 | //import 'highlight.js/styles/color-brewer.css'
37 |
38 | //注册语言
39 | hljs.registerLanguage('plaintext', plaintext)
40 | hljs.registerLanguage('json', json)
41 | hljs.registerLanguage('javascript', javascript)
42 | hljs.registerLanguage('java', java)
43 | hljs.registerLanguage('typescript', typescript)
44 | hljs.registerLanguage('python', python)
45 | hljs.registerLanguage('php', php)
46 | hljs.registerLanguage('css', css)
47 | hljs.registerLanguage('less', less)
48 | hljs.registerLanguage('scss', scss)
49 | hljs.registerLanguage('html', html)
50 | hljs.registerLanguage('markdown', markdown)
51 | hljs.registerLanguage('objectivec', objectivec)
52 | hljs.registerLanguage('swift', swift)
53 | hljs.registerLanguage('dart', dart)
54 | hljs.registerLanguage('nginx', nginx)
55 | hljs.registerLanguage('go', go)
56 | hljs.registerLanguage('http', http)
57 | hljs.registerLanguage('ruby', ruby)
58 | hljs.registerLanguage('c', c)
59 | hljs.registerLanguage('cpp', cpp)
60 | hljs.registerLanguage('csharp', csharp)
61 | hljs.registerLanguage('sql', sql)
62 | hljs.registerLanguage('shell', shell)
63 | hljs.registerLanguage('r', r)
64 | hljs.registerLanguage('kotlin', kotlin)
65 | hljs.registerLanguage('rust', rust)
66 |
67 | /**
68 | * 支持的语言列表
69 | */
70 | export const HljsLanguages = ['plaintext', 'json', 'javascript', 'java', 'typescript', 'python', 'php', 'css', 'less', 'scss', 'html', 'markdown', 'objectivec', 'swift', 'dart', 'nginx', 'http', 'go', 'ruby', 'c', 'cpp', 'csharp', 'sql', 'shell', 'r', 'kotlin', 'rust'] as const
71 |
72 | //全局设置
73 | hljs.configure({
74 | cssSelector: 'pre',
75 | classPrefix: 'kaitify-hljs-',
76 | languages: [...HljsLanguages],
77 | ignoreUnescapedHTML: true
78 | })
79 |
80 | /**
81 | * 语言类型
82 | */
83 | export type HljsLanguageType = (typeof HljsLanguages)[number]
84 | /**
85 | * 获取经过hljs处理的html元素
86 | */
87 | export const getHljsHtml = function (code: string, language: string) {
88 | if (language) {
89 | return hljs.highlight(code, {
90 | language: language,
91 | ignoreIllegals: true
92 | }).value
93 | }
94 | return hljs.highlightAuto(code).value
95 | }
96 |
--------------------------------------------------------------------------------
/docs/extensions/custom-extension.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 如何自己创建一个扩展?
3 | ---
4 |
5 | # 如何自己创建一个扩展?
6 |
7 | ## 创建扩展
8 |
9 | 通过 `Extension.create` 方法可以创建一个扩展实例
10 |
11 | ```ts
12 | import { Editor, Extension } from '@kaitify/core'
13 | const redExtension = Extension.create({
14 | name: 'red',
15 | formatRules: [
16 | ({ editor, node }) => {
17 | if (node.isBlock() && node.tag === editor.blockRenderTag) {
18 | if (node.hasStyles()) {
19 | node.styles.color = 'red'
20 | } else {
21 | node.styles = {
22 | color: 'red'
23 | }
24 | }
25 | }
26 | }
27 | ]
28 | })
29 | const editor = await Editor.configure({
30 | value: '
',
31 | extension: [redExtension]
32 | })
33 | ```
34 |
35 | ## 扩展构建参数
36 |
37 | `Extension.create`方法接收一个 `ExtensionCreateOptionType` 类型的入参,该参数包含以下属性:
38 |
39 | ##### name
40 |
41 | 扩展的名称,不同的扩展的 `name` 必须唯一
42 |
43 | ##### emptyRenderTags
44 |
45 | 自定义编辑器内定义需要置空的标签数组,编辑器针对需要置空的标签,会转为空节点,不会渲染到视图中,同编辑器构建参数 `emptyRenderTags`
46 |
47 | ##### extraKeepTags
48 |
49 | 自定义编辑器内额外保留的标签,如果某个标签的元素被编辑器转为了默认的行内节点,不符合预期行为,可以通过此参数配置保留该标签,同编辑器构建参数 `extraKeepTags`
50 |
51 | ##### formatRules
52 |
53 | 自定义节点数组格式化规则,同编辑器构建参数 `formatRules`
54 |
55 | ##### onDomParseNode
56 |
57 | 自定义 `dom` 转为非文本节点的后续处理,同编辑器构建参数 `onDomParseNode`
58 |
59 | ##### onSelectionUpdate
60 |
61 | 编辑器光标发生变化时触发,同编辑器构建参数 `onSelectionUpdate`
62 |
63 | ##### onInsertParagraph
64 |
65 | 换行时触发,参数为换行操作后光标所在的块节点,同编辑器构建参数 `onInsertParagraph`
66 |
67 | ##### onDeleteComplete
68 |
69 | 完成删除时触发,同编辑器构建参数 `onDeleteComplete`
70 |
71 | ##### onKeydown
72 |
73 | 光标在编辑器内时键盘按下触发,同编辑器构建参数 `onKeydown`
74 |
75 | ##### onKeyup
76 |
77 | 光标在编辑器内时键盘松开触发,同编辑器构建参数 `onKeyup`
78 |
79 | ##### onFocus
80 |
81 | 编辑器聚焦时触发,同编辑器构建参数 `onFocus`
82 |
83 | ##### onBlur
84 |
85 | 编辑器失焦时触发,同编辑器构建参数 `onBlur`
86 |
87 | ##### onPasteKeepMarks
88 |
89 | 粘贴 `html` 时,对于节点标记保留的自定义方法,同编辑器构建参数 `onPasteKeepMarks`
90 |
91 | ##### onPasteKeepStyles
92 |
93 | 粘贴 `html` 时,对于节点样式保留的自定义方法,同编辑器构建参数 `onPasteKeepStyles`
94 |
95 | ##### onBeforeUpdateView
96 |
97 | 视图更新前回调方法,同编辑器构建参数 `onBeforeUpdateView`
98 |
99 | ##### onAfterUpdateView
100 |
101 | 视图更新后回调方法,同编辑器构建参数 `onAfterUpdateView`
102 |
103 | ##### onDetachMentBlockFromParent
104 |
105 | 在删除和换行操作中块节点从其父节点中抽离出去成为与父节点同级的节点后触发,同编辑器构建参数 `onDetachMentBlockFromParent`
106 |
107 | ##### onBeforePatchNodeToFormat
108 |
109 | 编辑器 `updateView` 执行时,通过比对新旧节点数组获取需要格式化的节点,在这些节点被格式化前,触发此方法,同编辑器构建参数 `onBeforePatchNodeToFormat`
110 |
111 | ##### addCommands
112 |
113 | 自定义扩展命令,添加的命令可以通过 `editor.commands` 来调用
114 |
115 | ## 添加自定义命令
116 |
117 | 创建扩展时通过配置 `addCommands` 来添加自定义命令
118 |
119 | ```ts
120 | import { Editor, Extension } from '@kaitify/core'
121 | const setTextExtension = Extension.create({
122 | name: 'setText',
123 | addCommands() {
124 | return {
125 | insertText: async (val: string) => {
126 | this.insertText(val)
127 | await this.updateView()
128 | }
129 | }
130 | }
131 | })
132 | const editor = await Editor.configure({
133 | value: '
',
134 | extension: [setTextExtension]
135 | })
136 | ```
137 |
138 | ```ts
139 | //通过命令去调用该扩展提供的insertText方法,会向编辑器插入文本并且更新视图
140 | editor.commands.insertText('hello')
141 | ```
142 |
--------------------------------------------------------------------------------
/src/extensions/indent/index.ts:
--------------------------------------------------------------------------------
1 | import { KNodeStylesType } from '@/model'
2 | import { getSelectionBlockNodes } from '@/model/config/function'
3 | import { isOnlyTab, isTabWithShift } from '@/tools'
4 | import { Extension } from '../Extension'
5 |
6 | declare module '../../model' {
7 | interface EditorCommandsType {
8 | /**
9 | * 是否可以使用缩进
10 | */
11 | canUseIndent?: () => boolean
12 | /**
13 | * 增加缩进
14 | */
15 | setIncreaseIndent?: () => Promise
16 | /**
17 | * 减少缩进
18 | */
19 | setDecreaseIndent?: () => Promise
20 | }
21 | }
22 |
23 | export const IndentExtension = () =>
24 | Extension.create({
25 | name: 'indent',
26 | onPasteKeepStyles(node) {
27 | const styles: KNodeStylesType = {}
28 | if (node.isBlock() && node.hasStyles()) {
29 | if (node.styles!.hasOwnProperty('textIndent')) styles.textIndent = node.styles!.textIndent
30 | }
31 | return styles
32 | },
33 | onKeydown(event) {
34 | if (isOnlyTab(event) && this.commands.canUseIndent?.()) {
35 | event.preventDefault()
36 | this.commands.setIncreaseIndent?.()
37 | }
38 | if (isTabWithShift(event) && this.commands.canUseIndent?.()) {
39 | event.preventDefault()
40 | this.commands.setDecreaseIndent?.()
41 | }
42 | },
43 | addCommands() {
44 | const canUseIndent = () => {
45 | //光标范围内有代码块则不能使用缩进
46 | if (this.commands.hasCodeBlock?.()) {
47 | return false
48 | }
49 | //光标范围内可以生成内嵌列表则不能使用缩进
50 | if (!!this.commands.canCreateInnerList?.()) {
51 | return false
52 | }
53 | return true
54 | }
55 |
56 | const setIncreaseIndent = async () => {
57 | //起点和终点在一起
58 | if (this.selection.collapsed()) {
59 | const blockNode = this.selection.start!.node.getBlock()
60 | const styles: KNodeStylesType = blockNode.hasStyles() ? blockNode.styles! : {}
61 | let oldVal = 0
62 | if (styles.textIndent && typeof styles.textIndent == 'string' && styles.textIndent.endsWith('em')) {
63 | oldVal = parseFloat(styles.textIndent)
64 | }
65 | blockNode.styles = {
66 | ...styles,
67 | textIndent: `${oldVal + 2}em`
68 | }
69 | }
70 | //起点和终点不在一起
71 | else {
72 | const blockNodes = getSelectionBlockNodes.apply(this)
73 | blockNodes.forEach(item => {
74 | const styles: KNodeStylesType = item.hasStyles() ? item.styles! : {}
75 | let oldVal = 0
76 | if (styles.textIndent && typeof styles.textIndent == 'string' && styles.textIndent.endsWith('em')) {
77 | oldVal = parseFloat(styles.textIndent)
78 | }
79 | item.styles = {
80 | ...styles,
81 | textIndent: `${oldVal + 2}em`
82 | }
83 | })
84 | }
85 | await this.updateView()
86 | }
87 |
88 | const setDecreaseIndent = async () => {
89 | //起点和终点在一起
90 | if (this.selection.collapsed()) {
91 | const blockNode = this.selection.start!.node.getBlock()
92 | const styles: KNodeStylesType = blockNode.hasStyles() ? blockNode.styles! : {}
93 | let oldVal = 0
94 | if (styles.textIndent && typeof styles.textIndent == 'string' && styles.textIndent.endsWith('em')) {
95 | oldVal = parseFloat(styles.textIndent)
96 | }
97 | blockNode.styles = {
98 | ...styles,
99 | textIndent: `${oldVal - 2 > 0 ? oldVal - 2 : 0}em`
100 | }
101 | }
102 | //起点和终点不在一起
103 | else {
104 | const blockNodes = getSelectionBlockNodes.apply(this)
105 | blockNodes.forEach(item => {
106 | const styles: KNodeStylesType = item.hasStyles() ? item.styles! : {}
107 | let oldVal = 0
108 | if (styles.textIndent && typeof styles.textIndent == 'string' && styles.textIndent.endsWith('em')) {
109 | oldVal = parseFloat(styles.textIndent)
110 | }
111 | item.styles = {
112 | ...styles,
113 | textIndent: `${oldVal - 2 > 0 ? oldVal - 2 : 0}em`
114 | }
115 | })
116 | }
117 | await this.updateView()
118 | }
119 |
120 | return {
121 | canUseIndent,
122 | setDecreaseIndent,
123 | setIncreaseIndent
124 | }
125 | }
126 | })
127 |
--------------------------------------------------------------------------------
/docs/extensions/built-in/text.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: text 文本
3 | ---
4 |
5 | # text 文本
6 |
7 | 支持文本的渲染,包括设置样式和设置标记
8 |
9 | ## Commands 命令
10 |
11 | ##### isTextStyle()
12 |
13 | 判断光标所在文本是否具有某个样式
14 |
15 | - 类型
16 |
17 | ```ts
18 | isTextStyle(styleName: string, styleValue?: string | number): boolean
19 | ```
20 |
21 | - 详细信息
22 |
23 | 第一个入参表示样式名称,第二个入参表示样式的值,该方法用来判断光标所在文本是否具有某个样式,且样式值是否符合,返回 `boolean` 值
24 |
25 | 如果第二个参数不设置,则会判断光标所在的文本是否具有指定的样式,而不关心这个样式的值是什么
26 |
27 | - 示例
28 |
29 | ```ts
30 | const isTextStyle = editor.commands.isTextStyle('fontSize', '16px')
31 | ```
32 |
33 | ##### isTextMark()
34 |
35 | 判断光标所在文本是否具有某个标记
36 |
37 | - 类型
38 |
39 | ```ts
40 | isTextMark(markName: string, markValue?: string | number): boolean
41 | ```
42 |
43 | - 详细信息
44 |
45 | 第一个入参表示标记名称,第二个入参表示标记的值,该方法用来判断光标所在文本是否具有某个标记,且标记值是否符合,返回 `boolean` 值
46 |
47 | 如果第二个参数不设置,则会判断光标所在的文本是否具有指定的标记,而不关心这个标记的值是什么
48 |
49 | - 示例
50 |
51 | ```ts
52 | const isTextMark = editor.commands.isTextMark('data-span', '1')
53 | ```
54 |
55 | ##### setTextStyle()
56 |
57 | 设置光标范围内的文本的样式
58 |
59 | - 类型
60 |
61 | ```ts
62 | setTextStyle(styles: KNodeStylesType, updateView?: boolean): Promise
63 | ```
64 |
65 | - 详细信息
66 |
67 | 第一个入参表示设置的样式集合,第二个参数表示是否更新视图,该方法会对光标范围内的文本设置样式
68 |
69 | 如果第二个参数为 `false`,则在设置完毕后不会更新视图和光标的渲染,需要你主动 `updateView`,如果为 `true` 则会不需要主动 `updateView`,该参数默认为 `true`
70 |
71 | - 示例
72 |
73 | ```ts
74 | await editor.commands.setTextStyle({
75 | fontSize: '20px'
76 | })
77 | ```
78 |
79 | ##### setTextMark()
80 |
81 | 设置光标范围内的文本的标记
82 |
83 | - 类型
84 |
85 | ```ts
86 | setTextMark(marks: KNodeMarksType, updateView?: boolean): Promise
87 | ```
88 |
89 | - 详细信息
90 |
91 | 第一个入参表示设置的标记集合,第二个参数表示是否更新视图,该方法会对光标范围内的文本设置标记
92 |
93 | 如果第二个参数为 `false`,则在设置完毕后不会更新视图和光标的渲染,需要你主动 `updateView`,如果为 `true` 则会不需要主动 `updateView`,该参数默认为 `true`
94 |
95 | - 示例
96 |
97 | ```ts
98 | await editor.commands.setTextMark({
99 | 'data-span': '1'
100 | })
101 | ```
102 |
103 | ##### removeTextStyle()
104 |
105 | 取消光标范围内的文本的样式
106 |
107 | - 类型
108 |
109 | ```ts
110 | removeTextStyle(styleNames?: string[], updateView?: boolean): Promise
111 | ```
112 |
113 | - 详细信息
114 |
115 | 第一个入参表示要取消的样式名称数组,第二个参数表示是否更新视图,该方法会对光标范围内的文本取消指定的样式
116 |
117 | 如果第二个参数为 `false`,则在设置完毕后不会更新视图和光标的渲染,需要你主动 `updateView`,如果为 `true` 则会不需要主动 `updateView`,该参数默认为 `true`
118 |
119 | - 示例
120 |
121 | ```ts
122 | await editor.commands.removeTextStyle(['fontSize'])
123 | ```
124 |
125 | ##### removeTextMark()
126 |
127 | 取消光标范围内的文本的标记
128 |
129 | - 类型
130 |
131 | ```ts
132 | removeTextMark(markNames?: string[], updateView?: boolean): Promise
133 | ```
134 |
135 | - 详细信息
136 |
137 | 第一个入参表示要取消的标记名称数组,第二个参数表示是否更新视图,该方法会对光标范围内的文本取消指定的标记
138 |
139 | 如果第二个参数为 `false`,则在设置完毕后不会更新视图和光标的渲染,需要你主动 `updateView`,如果为 `true` 则会不需要主动 `updateView`,该参数默认为 `true`
140 |
141 | - 示例
142 |
143 | ```ts
144 | await editor.commands.removeTextMark(['data-span'])
145 | ```
146 |
147 | ##### clearFormat()
148 |
149 | 清除格式
150 |
151 | - 类型
152 |
153 | ```ts
154 | clearFormat(): Promise
155 | ```
156 |
157 | - 详细信息
158 |
159 | 该方法会将光标范围内的文本的所有样式和标记都清除,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView`
160 |
161 | - 示例
162 |
163 | ```ts
164 | await editor.commands.clearFormat()
165 | ```
166 |
167 | ## 代码示例
168 |
169 |
170 |
176 |
177 |
178 |
179 |
180 |
208 |
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
1 | import { data as DapData, element as DapElement } from 'dap-util'
2 | import { KNodeMarksType, KNodeStylesType } from '@/model'
3 | import { NODE_MARK } from '@/view'
4 |
5 | /**
6 | * 用于KNode生成唯一的key
7 | */
8 | export const createUniqueKey = (): number => {
9 | let key = DapData.get(window, 'kaitify-node-key') || 0
10 | key++
11 | DapData.set(window, 'kaitify-node-key', key)
12 | return key
13 | }
14 |
15 | /**
16 | * 用于编辑器生成唯一的guid
17 | */
18 | export const createGuid = function (): number {
19 | //获取唯一id
20 | let key = DapData.get(window, 'kaitify-guid') || 0
21 | key++
22 | DapData.set(window, 'kaitify-guid', key)
23 | return key
24 | }
25 |
26 | /**
27 | * 判断字符串是否零宽度空白字符
28 | */
29 | export const isZeroWidthText = (val: string) => {
30 | return /^[\u200B]+$/g.test(val)
31 | }
32 |
33 | /**
34 | * 获取一个零宽度空白字符
35 | */
36 | export const getZeroWidthText = () => {
37 | return '\u200B'
38 | }
39 |
40 | /**
41 | * 驼峰转中划线
42 | */
43 | export const camelToKebab = (val: string) => {
44 | return val.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
45 | }
46 |
47 | /**
48 | * 中划线转驼峰
49 | */
50 | export const kebabToCamel = (val: string) => {
51 | return val.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
52 | }
53 |
54 | /**
55 | * 获取dom元素的属性集合
56 | */
57 | export const getDomAttributes = (dom: HTMLElement) => {
58 | const attributes = Array.from(dom.attributes)
59 | const length = attributes.length
60 | const regExp = new RegExp(`(^on)|(^style$)|(^${NODE_MARK}$)`, 'g')
61 | const result: KNodeMarksType = {}
62 | for (let i = 0; i < length; i++) {
63 | const { nodeName, nodeValue } = attributes[i]
64 | //匹配事件、样式和face外的属性
65 | if (!regExp.test(nodeName)) {
66 | result[nodeName] = nodeValue ?? ''
67 | }
68 | }
69 | return result
70 | }
71 |
72 | /**
73 | * 获取dom元素的样式集合
74 | */
75 | export const getDomStyles = (dom: HTMLElement) => {
76 | let o: KNodeStylesType = {}
77 | const styles = dom.getAttribute('style')
78 | if (styles) {
79 | let i = 0
80 | let start = 0
81 | let splitStyles = []
82 | while (i < styles.length) {
83 | if (styles[i] == ';' && styles.substring(i + 1, i + 8) != 'base64,') {
84 | splitStyles.push(styles.substring(start, i))
85 | start = i + 1
86 | }
87 | //到最后了,并且最后没有分号
88 | if (i == styles.length - 1 && start < i + 1) {
89 | splitStyles.push(styles.substring(start, i + 1))
90 | }
91 | i++
92 | }
93 | splitStyles.forEach(style => {
94 | const index = style.indexOf(':')
95 | const property = style.substring(0, index).trim()
96 | const value = style.substring(index + 1).trim()
97 | o[kebabToCamel(property)] = value
98 | })
99 | }
100 | return o
101 | }
102 |
103 | /**
104 | * 初始化编辑器dom
105 | */
106 | export const initEditorDom = (dom: HTMLElement | string) => {
107 | //判断是否字符串,如果是字符串按照选择器来寻找元素
108 | if (typeof dom == 'string' && dom) {
109 | dom = document.body.querySelector(dom) as HTMLElement
110 | }
111 | dom = dom as HTMLElement
112 | //如何node不是元素则抛出异常
113 | if (!DapElement.isElement(dom)) {
114 | throw new Error('You must specify a dom container to initialize the editor')
115 | }
116 | //如果已经初始化过了则抛出异常
117 | if (DapData.get(dom, 'kaitify-init')) {
118 | throw new Error('The element node has been initialized to the editor')
119 | }
120 | //添加初始化的标记
121 | DapData.set(dom, 'kaitify-init', true)
122 | return dom
123 | }
124 |
125 | /**
126 | * 判断某个dom是否包含另一个dom
127 | */
128 | export const isContains = (parent: Node, child: Node) => {
129 | if (child.nodeType == 3) {
130 | return DapElement.isContains(parent as HTMLElement, child.parentNode as HTMLElement)
131 | }
132 | return DapElement.isContains(parent as HTMLElement, child as HTMLElement)
133 | }
134 |
135 | /**
136 | * 延迟指定时间
137 | */
138 | export const delay = (num: number | undefined = 0) => {
139 | return new Promise(resolve => {
140 | setTimeout(() => {
141 | resolve()
142 | }, num)
143 | })
144 | }
145 |
146 | /**
147 | * 删除对象的某个属性
148 | */
149 | export const deleteProperty = (val: any, propertyName: string) => {
150 | const newObj: any = {}
151 | Object.keys(val).forEach(key => {
152 | if (key != propertyName) {
153 | newObj[key] = val[key]
154 | }
155 | })
156 | return newObj as T
157 | }
158 |
159 | /**
160 | * 键盘Tab是否按下
161 | */
162 | export const isOnlyTab = (e: KeyboardEvent) => {
163 | return e.key.toLocaleLowerCase() == 'tab' && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
164 | }
165 |
166 | /**
167 | * 键盘Tab和shift是否一起按下
168 | */
169 | export const isTabWithShift = (e: KeyboardEvent) => {
170 | return e.key.toLocaleLowerCase() == 'tab' && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey
171 | }
172 |
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | ---
2 | lastUpdated: false
3 | title: 更新日志
4 | ---
5 |
6 | # 更新日志
7 |
8 | ## v0.0.2-beta.8
9 |
10 | - 优化编辑器的 `destroy` 方法,修复了原来销毁后再创建出现问题的 bug
11 | - 编辑器的主题颜色全部挂载在容器上,而不是 `document` 上,另外编辑器的主题颜色不再受全局影响
12 | - 编辑器的深色模式样式设置从属性 `[kaitify-dark]` 改为样式类 `.kaitify-dark`
13 | - 编辑器构建入参新增 `onCreate` 和 `onCreated` 参数
14 |
15 | ## v0.0.2-beta.2
16 |
17 | - 优化编辑器对样式的判断,如果是空字符串则默认无样式
18 | - 优化 `List` 内置扩展的样式
19 |
20 | ## v0.0.1
21 |
22 | - `Editor` 所有的构建参数中的函数事件都以 `on` 开头
23 | - 第一个正式版本发布
24 |
25 | ## v0.0.1-beta.37
26 |
27 | - 修复了当段落进行缩进时,行内代码、图片、视频、数学公式等样式为 `inline-block` 的元素也进行了缩进的问题
28 |
29 | ## v0.0.1-beta.36
30 |
31 | - 优化了 `Attachment` 的样式
32 | - 优化了 `Blockquote` 的样式
33 | - 优化了 `Code` 的样式
34 | - 优化了 `CodeBlock` 的样式
35 | - 优化了 `Link` 的样式
36 | - 优化了 `Math` 的样式
37 | - 优化了 `Table` 的样式
38 | - 引用节点优化:在创建时如果子节点无块节点,会在引用内创建一个段落节点
39 |
40 | ## v0.0.1-beta.35
41 |
42 | - `Attachment` `Math` 和 `Horizontal`扩展的表现优化
43 |
44 | ## v0.0.1-beta.34
45 |
46 | - `Video` `Image` `Table`等扩展设定可拖拽的边缘大小
47 | - 代码优化
48 |
49 | ## v0.0.1-beta.33
50 |
51 | - `getContent` 方法新增两个入参,分别表示是否排除 `\n` 换行符、是否排除零宽度空白字符
52 | - 编辑器格式化处理 `\n` 后面加上零宽度空白字符时,优化了光标的位置更新逻辑
53 | - 代码块换行逻辑优化:代码块内最后一行如果没有内容,此时再进行换行会在整个代码块后进行换行(之前有这个功能,但是在空格逻辑优化后功能需要优化和修改)
54 | - 修复了在代码块内执行撤销偶然导致光标丢失的 bug
55 |
56 | ## v0.0.1-beta.32
57 |
58 | - 优化 `getHTML` 方法:对于返回的 `style` 标签内的样式进行了过滤,现在只会返回与编辑器相关的样式;同时新增了 `filterCssText` 参数,用于自定义需要保留的样式
59 | - 优化行内代码的样式
60 | - 修复中文输入没有加入历史记录的 bug
61 | - 重新定义了空格的渲染逻辑,并且针对 `\n` 换行符进行了格式化处理
62 |
63 | ## v0.0.1-beta.30
64 |
65 | - 代码细节优化
66 |
67 | ## v0.0.1-beta.28
68 |
69 | - `Editor` 新增实例方法 `setDomObserve`,用于监听编辑器内的非法 `dom` 操作
70 | - `Editor` 新增实例方法 `removeDomObserve`,用于取消监听编辑器内的非法 `dom` 操作
71 | - 修复了 `Image` 和 `Video` 扩展内的图片和视频无法拖拽改变大小的 bug
72 |
73 | ## v0.0.1-beta.27
74 |
75 | - 优化编辑器对非法 `dom` 插入/删除/更新的处理,以适配 `Grammarly` 等第三方插件对 `dom` 的修改
76 | - 其他代码优化
77 |
78 | ## v0.0.1-beta.26
79 |
80 | - 优化 `unicode` 字符删除时的操作逻辑,例如 `emoji` 表情包删除的优化
81 | - 新增 `isSelectionInView` 函数:用以判断光标是否完全在可视范围内
82 |
83 | ## v0.0.1-beta.24
84 |
85 | - 待办扩展优化:勾选的动画效果优化和样式优化(现在复选框始终只会显示在待办的顶部,而不是和之前一样居中)
86 | - 优化不可编辑节点的整体逻辑,在删除、换行、插入等操作时都被视为整体进行处理
87 | - 编辑可编辑下单击附件和数学公式会选中附件和数学公式
88 | - 代码块换行逻辑优化:代码块内最后一行如果没有内容,此时再进行换行会在整个代码块后进行换行
89 | - 表格换行逻辑优化:表格最后一行的最后一列内,如果光标所在的块节点是段落且前一个块节点也是段落,则再次换行时会在整个表格后换行
90 | - 表格格式化规则新增对 `td` 内容的处理,如果 `td` 内没有块节点,会默认使用段落进行包裹
91 | - 列表样式优化:解决了在字体较大时列标显示不全的问题
92 |
93 | ## v0.0.1-beta.22
94 |
95 | - 修复了一个中文输入的 bug
96 | - `Editor` 新增实例方法 `isEmpty`,用以判断编辑器内容是否为空
97 | - 优化了 `placeholder` 占位内容的显示机制,在输入中文时隐藏 `placeholder`
98 |
99 | ## v0.0.1-beta.20
100 |
101 | - 代码优化,解决了 `Grammarly` 插入的问题
102 | - `indent` 扩展新增 `canUseIndent` 指令,用于判断是否可以使用缩进功能
103 | - `indent` 扩展新增键盘事件:`tab` 键按下增加缩进;`shift+tab` 键按下减少缩进
104 |
105 | ## v0.0.1-beta.19
106 |
107 | - 对 `KNode` 的实例方法 `clone` `fullClone` `getFocusNodes` 进行了优化
108 | - 对 `KNode` 的类方法 `flat` `searchByKey` 进行了优化
109 | - `KNode` 的实例方法 `firstTextClosedInNode` 更名为 `firstInTargetNode`
110 | - `KNode` 的实例方法 `lastTextClosedInNode` 更名为 `lastInTargetNode`
111 | - `Editor` 的实例方法 `getLastSelectionNodeInChildren` 更名为 `getLastSelectionNode`,并进行了性能的优化
112 | - `Editor` 的实例方法 `getFirstSelectionNodeInChildren` 更名为 `getFirstSelectionNode`,并进行了性能的优化
113 | - 对 `Editor` 的实例方法 `getFocusNodesBySelection` 进行了优化
114 | - `Editor` 的实例方法 `isSelectionInNode` 更名为 `isSelectionInTargetNode`
115 | - 内部的一些方法和逻辑进行了优化
116 | - 修复了格式化过程中将非块级节点转为块节点时的逻辑处理出现的一些问题,这个问题曾导致内存溢出
117 |
118 | ## v0.0.1-beta.18
119 |
120 | - 新增 `getHTML` 函数获取编辑器 `html` 内容
121 |
122 | ## v0.0.1-beta.17
123 |
124 | - 代码块扩展新增键盘事件:在代码块内按下 `Tab` 键会插入 2 个空格
125 | - 部分代码优化
126 |
127 | ## v0.0.1-beta.16
128 |
129 | - 列表优化:现在会给默认的列表节点设置 `listStyleType` 样式
130 | - 列表扩展 `unsetList` 方法问题修复
131 | - 列表扩展新增 `canCreateInnerList` 和 `createInnertList` 命令
132 | - 列表扩展新增键盘事件:在可以生成内嵌列表时,按下 `Tab` 键会执行 `createInnertList` 命令
133 |
134 | ## v0.0.1-beta.13
135 |
136 | - 优化列表渲染,序标改为外侧,设置左侧内边距
137 |
138 | ## v0.0.1-beta.12
139 |
140 | - kaitify 的第一个发布版本
141 |
--------------------------------------------------------------------------------