-
Notifications
You must be signed in to change notification settings - Fork 144
/
insertNode.ts
147 lines (133 loc) · 5.7 KB
/
insertNode.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import EditorCore, { InsertNode } from '../interfaces/EditorCore';
import {
ContentPosition,
InsertOption,
NodeType,
PositionType,
BlockElement,
} from 'roosterjs-editor-types';
import {
Position,
getBlockElementAtNode,
getFirstLastBlockElement,
isBlockElement,
isVoidHtmlElement,
wrap,
adjustNodeInsertPosition,
createRange,
} from 'roosterjs-editor-dom';
function getInitialRange(
core: EditorCore,
option: InsertOption
): { range: Range; rangeToRestore: Range } {
// Selection start replaces based on the current selection.
// Range inserts based on a provided range.
// Both have the potential to use the current selection to restore cursor position
// So in both cases we need to store the selection state.
let range = core.api.getSelectionRange(core, true /*tryGetFromCache*/);
let rangeToRestore = null;
if (option.position == ContentPosition.Range) {
rangeToRestore = range;
range = option.range;
} else if (range) {
rangeToRestore = range.cloneRange();
}
return { range, rangeToRestore };
}
/**
* Insert a DOM node into editor content
* @param core The EditorCore object. No op if null.
* @param option An insert option object to specify how to insert the node
*/
export const insertNode: InsertNode = (core: EditorCore, node: Node, option: InsertOption) => {
option = option || {
position: ContentPosition.SelectionStart,
insertOnNewLine: false,
updateCursor: true,
replaceSelection: true,
};
let contentDiv = core.contentDiv;
if (option.updateCursor) {
core.api.focus(core);
}
switch (option.position) {
case ContentPosition.Begin:
case ContentPosition.End: {
let isBegin = option.position == ContentPosition.Begin;
let block = getFirstLastBlockElement(contentDiv, isBegin);
let insertedNode: Node;
if (block) {
let refNode = isBegin ? block.getStartNode() : block.getEndNode();
if (
option.insertOnNewLine ||
refNode.nodeType == NodeType.Text ||
isVoidHtmlElement(refNode)
) {
// For insert on new line, or refNode is text or void html element (HR, BR etc.)
// which cannot have children, i.e. <div>hello<br>world</div>. 'hello', 'world' are the
// first and last node. Insert before 'hello' or after 'world', but still inside DIV
insertedNode = refNode.parentNode.insertBefore(
node,
isBegin ? refNode : refNode.nextSibling
);
} else {
// if the refNode can have child, use appendChild (which is like to insert as first/last child)
// i.e. <div>hello</div>, the content will be inserted before/after hello
insertedNode = refNode.insertBefore(node, isBegin ? refNode.firstChild : null);
}
} else {
// No first block, this can happen when editor is empty. Use appendChild to insert the content in contentDiv
insertedNode = contentDiv.appendChild(node);
}
// Final check to see if the inserted node is a block. If not block and the ask is to insert on new line,
// add a DIV wrapping
if (insertedNode && option.insertOnNewLine && !isBlockElement(insertedNode)) {
wrap(insertedNode);
}
break;
}
case ContentPosition.DomEnd:
// Use appendChild to insert the node at the end of the content div.
let insertedNode = contentDiv.appendChild(node);
// Final check to see if the inserted node is a block. If not block and the ask is to insert on new line,
// add a DIV wrapping
if (insertedNode && option.insertOnNewLine && !isBlockElement(insertedNode)) {
wrap(insertedNode);
}
break;
case ContentPosition.Range:
case ContentPosition.SelectionStart:
let { range, rangeToRestore } = getInitialRange(core, option);
if (!range) {
return;
}
// if to replace the selection and the selection is not collapsed, remove the the content at selection first
if (option.replaceSelection && !range.collapsed) {
range.deleteContents();
}
let pos = Position.getStart(range);
let blockElement: BlockElement;
if (
option.insertOnNewLine &&
(blockElement = getBlockElementAtNode(contentDiv, pos.normalize().node))
) {
pos = new Position(blockElement.getEndNode(), PositionType.After);
} else {
pos = adjustNodeInsertPosition(contentDiv, node, pos);
}
let nodeForCursor = node.nodeType == NodeType.DocumentFragment ? node.lastChild : node;
range = createRange(pos);
range.insertNode(node);
if (option.updateCursor && nodeForCursor) {
rangeToRestore = createRange(
new Position(nodeForCursor, PositionType.After).normalize()
);
}
core.api.selectRange(core, rangeToRestore);
break;
case ContentPosition.Outside:
core.contentDiv.parentNode.insertBefore(node, contentDiv.nextSibling);
break;
}
return true;
};