-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.js
242 lines (196 loc) · 7.98 KB
/
index.js
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/**
* Module dependencies.
*/
var getDocument = require('get-document');
var unwrapNode = require('unwrap-node');
var extractContents = require('range-extract-contents');
var insertNode = require('range-insert-node');
var wrapRange = require('wrap-range');
var closest = require('component-closest');
var query = require('component-query');
var saveRange = require('save-range');
var RangeIterator = require('range-iterator');
var RangePosition = require('range-position');
var RangeAtIndex = require('range-at-index');
var splitAtRange = require('split-at-range');
var normalize = require('range-normalize');
// create a CSS selector string from the "block elements" array
var blockSel = ['li'].concat(require('block-elements')).join(', ');
var debug = require('debug')('unwrap-range');
/**
* Module exports.
*/
module.exports = unwrap;
/**
* Removes any `nodeName` DOM elements from within the given `range` boundaries.
*
* @param {Range} range - DOM range to "unwrap"
* @param {String} nodeName - Selector to use to determine which nodes to "unwrap"
* @param {Element} [root] - Optional `root` DOM element to stop traversing the parents for
* @param {Document} [doc] - Optional `Document` instance to use
* @public
*/
function unwrap (range, nodeName, root, doc) {
if (!doc) doc = getDocument(range) || document;
var info, node, prevBlock, next;
function doRange (workingRange) {
debug('doRange() %o', workingRange.toString());
node = closest(workingRange.commonAncestorContainer, nodeName, true, root);
if (node) {
debug('found %o common ancestor element: %o', nodeName, node);
// unwrap the common ancestor element, saving the Range state
// and restoring it afterwards
info = saveRange.save(range, doc);
var outer = unwrapNode(node, null, doc);
range = saveRange.load(info, range.commonAncestorContainer);
// at this point, we need to save down the Range state *again*.
// This is somewhat a quick-fix, and more optimized logic could
// probably be implemented
info = saveRange.save(range, doc);
// now re-wrap left-hand side, if necessary
var left = outer.cloneRange();
left.setEnd(range.startContainer, range.startOffset);
if (left.toString()) {
debug('re-wrapping left-hand side with new %o node', nodeName);
wrapRange(left, nodeName, doc);
}
// now re-wrap right-hand side, if necessary
var right = outer.cloneRange();
right.setStart(range.endContainer, range.endOffset);
if (right.toString()) {
debug('re-wrapping right-hand side with new %o node', nodeName);
wrapRange(right, nodeName, doc);
}
// restore the Range at this point
range = saveRange.load(info, range.commonAncestorContainer);
}
info = saveRange.save(range, doc);
var fragment = extractContents(workingRange);
var nodes = query.all(nodeName, fragment);
debug('%o %o elements to "unwrap"', nodes.length, nodeName);
for (var i = 0; i < nodes.length; i++) {
unwrapNode(nodes[i], null, doc);
}
insertNode(workingRange, fragment);
range = saveRange.load(info, range.commonAncestorContainer);
}
if (range.collapsed) {
// for a `collapsed` range, we must check if the current Range is within
// a `nodeName` DOM element.
// If no, do nothing.
// If yes, then we need to unwrap and re-wrap the DOM element such that it
// gets moved to the top of the DOM stack, and then the cursor needs to go
// right beside it selecting a 0-width space TextNode.
// So: <i><b>test|</b></i> → unwrap I → <b><i>test</i>|</b>
// <i><b>|test</b></i> → unwrap I → <b>|<i>test</i></b>
// <i><b>te|st</b></i> → unwrap I → <b><i>te</i>|<i>st</i></b>
debug('unwrapping collapsed Range');
node = closest(range.endContainer, nodeName, true, root);
if (node) {
debug('found parent %o node within collapsed Range', nodeName);
// first attempt to find any existing `.zwsp` span, and remove it
// so that it's not considered when checking if the `node` is "empty"
var span = closest(range.endContainer, '.zwsp', true, root);
if (span) span.parentNode.removeChild(span);
var isEmpty = !node.firstChild;
var parentNode = node.parentNode;
var nextSibling = node.nextSibling;
var previousSibling = node.previousSibling;
var pos, offset;
if (!isEmpty) {
pos = RangePosition(range, node);
offset = range.endOffset;
}
var oldRange = unwrapNode(node, null, doc);
if (!span) {
span = doc.createElement('span');
span.className = 'zwsp';
}
var text = span.firstChild;
if (!text) {
text = doc.createTextNode('\u200B');
span.appendChild(text);
}
if (!isEmpty) {
var els = wrapRange(oldRange, nodeName, doc);
var el = els[0];
// a 0-width space text node is required, otherwise the browser will
// simply continue to type into the old parent node.
debug('inserting 0-width space TextNode after new %o element', el.nodeName);
if (pos === RangePosition.START) {
el.parentNode.insertBefore(span, el);
} else if (pos === RangePosition.MIDDLE) {
var r = RangeAtIndex(el, offset, offset);
var split = splitAtRange(el, r);
// grab the first child if it is the same nodeName as `el`
if (split[0].childNodes[0].nodeName === el.nodeName) {
split[0] = split[0].childNodes[0];
}
if (split[1].childNodes[0].nodeName === el.nodeName) {
split[1] = split[1].childNodes[0];
}
// clone the `el` root node, including attributes, for the "right side"
var other = el.cloneNode(false);
// for `el`, remove all child nodes, and transfer the contents
while (el.firstChild) el.removeChild(el.firstChild);
while (split[0].firstChild) el.appendChild(split[0].firstChild);
// for the `other` node, we have to insert the split[1] child nodes
while (split[1].firstChild) other.appendChild(split[1].firstChild);
insertAfter(other, el);
insertAfter(span, el);
} else if (pos === RangePosition.END) {
insertAfter(span, el);
} else {
throw new Error('should not happen!');
}
} else {
// empty
if (previousSibling) {
insertAfter(span, previousSibling);
} else if (nextSibling) {
parent.insertBefore(span, nextSibling);
} else {
parent.appendChild(span);
}
}
var l = text.nodeValue.length;
range.setStart(text, l);
range.setEnd(text, l);
}
} else {
var originalRange = range.cloneRange();
var workingRange = range.cloneRange();
var iterator = RangeIterator(range, function (node) {
// nodes with no child nodes
return node.childNodes.length === 0;
});
var ranges = [];
while (!(next = iterator.next()).done) {
var block = closest(next.value, blockSel, true, root);
if (prevBlock && prevBlock !== block) {
debug('found block boundary point for %o!', prevBlock);
workingRange.setEndAfter(prevBlock);
ranges.push(normalize(workingRange));
// now we clone the original range again, since it has the
// "end boundary" set up the way to need it still. But reset the
// "start boundary" to point to the beginning of this new block
workingRange = originalRange.cloneRange();
workingRange.setStartBefore(block);
}
prevBlock = block;
}
ranges.push(normalize(workingRange));
for (var i = 0; i < ranges.length; i++) {
doRange(ranges[i]);
}
normalize(range);
}
}
function insertAfter(newElement, targetElement) {
var parent = targetElement.parentNode;
if (parent.lastChild === targetElement) {
parent.appendChild(newElement);
} else {
parent.insertBefore(newElement, targetElement.nextSibling);
}
}