-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
145 lines (117 loc) · 4.6 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
/**
* Module dependencies.
*/
var closest = require('component-closest');
var normalize = require('range-normalize');
var getDocument = require('get-document');
var insertNode = require('range-insert-node');
var extractContents = require('range-extract-contents');
var RangeIterator = require('range-iterator');
// create a CSS selector string from the "block elements" array
var blockSel = ['li'].concat(require('block-elements')).join(', ');
var debug = require('debug')('wrap-range');
/**
* Module exports.
*/
module.exports = wrap;
/**
* Wraps the given `range` object with a new `nodeName` DOM element.
*
* Based off of: http://stackoverflow.com/a/10785093/376773
*
* @param {Range} range - DOM Range instance to "wrap"
* @param {String} nodeName - Name of node to create. i.e. "a" to create an <a> node
* @param {Document} [doc] - Optional `document` object to use when creating the new DOM element
* @return {Array<HTMLElement>} returns an array of the newly created DOM elements
* @public
*/
function wrap (range, nodeName, doc) {
if (!doc) doc = getDocument(range) || document;
var createElement = typeof nodeName === 'function' ? nodeName : function () {
return doc.createElement(nodeName);
};
var nodes = [];
var ranges = [];
if (range.collapsed) {
// for a collapsed Range, we must create a new TextNode with a 0-width space
// character inside of it, so that we can manually select it as the contents
// of the Range afterwards. The 0-width space char is required otherwise the
// browser will simply skip over the newly created `node` when the user is
// typing. Selecting the empty space char forces the browser type inside of
// `node`.
var node = createElement();
nodes.push(node);
// attempt to find an existing `.zwsp` SPAN parent of the cursor.
// if we find one, then re-use that, otherwise create one from scratch.
debug('appending 0-width space TextNode to new %o element', node.nodeName);
var span = closest(range.endContainer, '.zwsp', true, doc);
var text;
if (span) {
text = span.firstChild;
} else {
span = doc.createElement('span');
span.className = 'zwsp';
text = doc.createTextNode('\u200B');
span.appendChild(text);
}
node.appendChild(span);
insertNode(range, node);
var l = text.nodeValue.length;
range.setStart(text, l);
range.setEnd(text, l);
} else {
// For a Range with any selection within it, we must iterate over the
// TextNode instances and "void elements" within the Range, and figure
// out the parent "block element" boundaries.
// Each time a new "block" is encountered within the Range, we create a new
// "sub-range" and wrap it with a new `nodeName` element.
var next;
var prevBlock;
var first = true;
var originalRange = range.cloneRange();
var workingRange = range.cloneRange();
var iterator = RangeIterator(range, function (node) {
// nodes with no child nodes
return node.childNodes.length === 0;
});
function doRange (workingRange) {
var node = createElement();
nodes.push(node);
debug('wrapping Range %o with new %o node', workingRange.toString(), node.nodeName);
node.appendChild(extractContents(workingRange));
insertNode(workingRange, node);
if (first) {
// the first Range that we process, we must re-set the
// "start boundary" on the passed in Range instance
range.setStartBefore(node);
first = false;
}
range.setEndAfter(node);
}
// first order of business is to collect an Array of Ranges that
// need to be processed
while (!(next = iterator.next()).done) {
var block = closest(next.value, blockSel, true);
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));
// process each Range instance
for (var i = 0; i < ranges.length; i++) {
doRange(ranges[i]);
}
// finally, normalize the passed in Range, since we've been setting
// it on block-level boundaries so far most likely, rather then text ones
normalize(range);
}
return nodes;
}