rangy-rails 1.3alpha.772.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +44 -0
- data/Rakefile +1 -0
- data/lib/rangy-rails.rb +12 -0
- data/lib/rangy-rails/version.rb +5 -0
- data/rangy-rails.gemspec +21 -0
- data/vendor/assets/javascripts/rangy-core.js +3644 -0
- data/vendor/assets/javascripts/rangy-cssclassapplier.js +935 -0
- data/vendor/assets/javascripts/rangy-highlighter.js +496 -0
- data/vendor/assets/javascripts/rangy-selectionsaverestore.js +237 -0
- data/vendor/assets/javascripts/rangy-serializer.js +294 -0
- data/vendor/assets/javascripts/rangy-textrange.js +1896 -0
- metadata +61 -0
@@ -0,0 +1,935 @@
|
|
1
|
+
/**
|
2
|
+
* Class Applier module for Rangy.
|
3
|
+
* Adds, removes and toggles classes on Ranges and Selections
|
4
|
+
*
|
5
|
+
* Part of Rangy, a cross-browser JavaScript range and selection library
|
6
|
+
* http://code.google.com/p/rangy/
|
7
|
+
*
|
8
|
+
* Depends on Rangy core.
|
9
|
+
*
|
10
|
+
* Copyright 2013, Tim Down
|
11
|
+
* Licensed under the MIT license.
|
12
|
+
* Version: 1.3alpha.772
|
13
|
+
* Build date: 26 February 2013
|
14
|
+
*/
|
15
|
+
rangy.createModule("CssClassApplier", function(api, module) {
|
16
|
+
api.requireModules( ["WrappedSelection", "WrappedRange"] );
|
17
|
+
|
18
|
+
var dom = api.dom;
|
19
|
+
var DomPosition = dom.DomPosition;
|
20
|
+
var contains = dom.arrayContains;
|
21
|
+
|
22
|
+
|
23
|
+
var defaultTagName = "span";
|
24
|
+
|
25
|
+
function trim(str) {
|
26
|
+
return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
|
27
|
+
}
|
28
|
+
|
29
|
+
function hasClass(el, cssClass) {
|
30
|
+
return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className);
|
31
|
+
}
|
32
|
+
|
33
|
+
function addClass(el, cssClass) {
|
34
|
+
if (el.className) {
|
35
|
+
if (!hasClass(el, cssClass)) {
|
36
|
+
el.className += " " + cssClass;
|
37
|
+
}
|
38
|
+
} else {
|
39
|
+
el.className = cssClass;
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
var removeClass = (function() {
|
44
|
+
function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
|
45
|
+
return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
|
46
|
+
}
|
47
|
+
|
48
|
+
return function(el, cssClass) {
|
49
|
+
if (el.className) {
|
50
|
+
el.className = el.className.replace(new RegExp("(^|\\s)" + cssClass + "(\\s|$)"), replacer);
|
51
|
+
}
|
52
|
+
};
|
53
|
+
})();
|
54
|
+
|
55
|
+
function sortClassName(className) {
|
56
|
+
return className.split(/\s+/).sort().join(" ");
|
57
|
+
}
|
58
|
+
|
59
|
+
function getSortedClassName(el) {
|
60
|
+
return sortClassName(el.className);
|
61
|
+
}
|
62
|
+
|
63
|
+
function haveSameClasses(el1, el2) {
|
64
|
+
return getSortedClassName(el1) == getSortedClassName(el2);
|
65
|
+
}
|
66
|
+
|
67
|
+
function compareRanges(r1, r2) {
|
68
|
+
return r1.compareBoundaryPoints(r2.START_TO_START, r2);
|
69
|
+
}
|
70
|
+
|
71
|
+
function movePosition(position, oldParent, oldIndex, newParent, newIndex) {
|
72
|
+
var node = position.node, offset = position.offset;
|
73
|
+
|
74
|
+
var newNode = node, newOffset = offset;
|
75
|
+
|
76
|
+
if (node == newParent && offset > newIndex) {
|
77
|
+
newOffset++;
|
78
|
+
}
|
79
|
+
|
80
|
+
if (node == oldParent && (offset == oldIndex || offset == oldIndex + 1)) {
|
81
|
+
newNode = newParent;
|
82
|
+
newOffset += newIndex - oldIndex;
|
83
|
+
}
|
84
|
+
|
85
|
+
if (node == oldParent && offset > oldIndex + 1) {
|
86
|
+
newOffset--;
|
87
|
+
}
|
88
|
+
|
89
|
+
position.node = newNode;
|
90
|
+
position.offset = newOffset;
|
91
|
+
}
|
92
|
+
|
93
|
+
function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) {
|
94
|
+
// For convenience, allow newIndex to be -1 to mean "insert at the end".
|
95
|
+
if (newIndex == -1) {
|
96
|
+
newIndex = newParent.childNodes.length;
|
97
|
+
}
|
98
|
+
|
99
|
+
var oldParent = node.parentNode;
|
100
|
+
var oldIndex = dom.getNodeIndex(node);
|
101
|
+
|
102
|
+
for (var i = 0, position; position = positionsToPreserve[i++]; ) {
|
103
|
+
movePosition(position, oldParent, oldIndex, newParent, newIndex);
|
104
|
+
}
|
105
|
+
|
106
|
+
// Now actually move the node.
|
107
|
+
if (newParent.childNodes.length == newIndex) {
|
108
|
+
newParent.appendChild(node);
|
109
|
+
} else {
|
110
|
+
newParent.insertBefore(node, newParent.childNodes[newIndex]);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
|
114
|
+
function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) {
|
115
|
+
var child, children = [];
|
116
|
+
while ( (child = node.firstChild) ) {
|
117
|
+
movePreservingPositions(child, newParent, newIndex++, positionsToPreserve);
|
118
|
+
children.push(child);
|
119
|
+
}
|
120
|
+
if (removeNode) {
|
121
|
+
node.parentNode.removeChild(node);
|
122
|
+
}
|
123
|
+
return children;
|
124
|
+
}
|
125
|
+
|
126
|
+
function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) {
|
127
|
+
return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve);
|
128
|
+
}
|
129
|
+
|
130
|
+
function rangeSelectsAnyText(range, textNode) {
|
131
|
+
var textNodeRange = range.cloneRange();
|
132
|
+
textNodeRange.selectNodeContents(textNode);
|
133
|
+
|
134
|
+
var intersectionRange = textNodeRange.intersection(range);
|
135
|
+
var text = intersectionRange ? intersectionRange.toString() : "";
|
136
|
+
textNodeRange.detach();
|
137
|
+
|
138
|
+
return text != "";
|
139
|
+
}
|
140
|
+
|
141
|
+
function getEffectiveTextNodes(range) {
|
142
|
+
var nodes = range.getNodes([3]);
|
143
|
+
|
144
|
+
// Optimization as per issue 145
|
145
|
+
|
146
|
+
// Remove non-intersecting text nodes from the start of the range
|
147
|
+
var start = 0, node;
|
148
|
+
while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) {
|
149
|
+
++start;
|
150
|
+
}
|
151
|
+
|
152
|
+
// Remove non-intersecting text nodes from the start of the range
|
153
|
+
var end = nodes.length - 1;
|
154
|
+
while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) {
|
155
|
+
--end;
|
156
|
+
}
|
157
|
+
|
158
|
+
return nodes.slice(start, end + 1);
|
159
|
+
}
|
160
|
+
|
161
|
+
function elementsHaveSameNonClassAttributes(el1, el2) {
|
162
|
+
if (el1.attributes.length != el2.attributes.length) return false;
|
163
|
+
for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
|
164
|
+
attr1 = el1.attributes[i];
|
165
|
+
name = attr1.name;
|
166
|
+
if (name != "class") {
|
167
|
+
attr2 = el2.attributes.getNamedItem(name);
|
168
|
+
if (attr1.specified != attr2.specified) return false;
|
169
|
+
if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
|
170
|
+
}
|
171
|
+
}
|
172
|
+
return true;
|
173
|
+
}
|
174
|
+
|
175
|
+
function elementHasNonClassAttributes(el, exceptions) {
|
176
|
+
for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
|
177
|
+
attrName = el.attributes[i].name;
|
178
|
+
if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
|
179
|
+
return true;
|
180
|
+
}
|
181
|
+
}
|
182
|
+
return false;
|
183
|
+
}
|
184
|
+
|
185
|
+
function elementHasProps(el, props) {
|
186
|
+
var propValue;
|
187
|
+
for (var p in props) {
|
188
|
+
if (props.hasOwnProperty(p)) {
|
189
|
+
propValue = props[p];
|
190
|
+
if (typeof propValue == "object") {
|
191
|
+
if (!elementHasProps(el[p], propValue)) {
|
192
|
+
return false;
|
193
|
+
}
|
194
|
+
} else if (el[p] !== propValue) {
|
195
|
+
return false;
|
196
|
+
}
|
197
|
+
}
|
198
|
+
}
|
199
|
+
return true;
|
200
|
+
}
|
201
|
+
|
202
|
+
var getComputedStyleProperty = dom.getComputedStyleProperty;
|
203
|
+
var isEditableElement;
|
204
|
+
|
205
|
+
(function() {
|
206
|
+
var testEl = document.createElement("div");
|
207
|
+
if (typeof testEl.isContentEditable == "boolean") {
|
208
|
+
isEditableElement = function(node) {
|
209
|
+
return node && node.nodeType == 1 && node.isContentEditable;
|
210
|
+
};
|
211
|
+
} else {
|
212
|
+
isEditableElement = function(node) {
|
213
|
+
if (!node || node.nodeType != 1 || node.contentEditable == "false") {
|
214
|
+
return false;
|
215
|
+
}
|
216
|
+
return node.contentEditable == "true" || isEditableElement(node.parentNode);
|
217
|
+
};
|
218
|
+
}
|
219
|
+
})();
|
220
|
+
|
221
|
+
function isEditingHost(node) {
|
222
|
+
var parent;
|
223
|
+
return node && node.nodeType == 1
|
224
|
+
&& (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on")
|
225
|
+
|| (isEditableElement(node) && !isEditableElement(node.parentNode)));
|
226
|
+
}
|
227
|
+
|
228
|
+
function isEditable(node) {
|
229
|
+
return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
|
230
|
+
}
|
231
|
+
|
232
|
+
var inlineDisplayRegex = /^inline(-block|-table)?$/i;
|
233
|
+
|
234
|
+
function isNonInlineElement(node) {
|
235
|
+
return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
|
236
|
+
}
|
237
|
+
|
238
|
+
// White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
|
239
|
+
var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
|
240
|
+
|
241
|
+
function isUnrenderedWhiteSpaceNode(node) {
|
242
|
+
if (node.data.length == 0) {
|
243
|
+
return true;
|
244
|
+
}
|
245
|
+
if (htmlNonWhiteSpaceRegex.test(node.data)) {
|
246
|
+
return false;
|
247
|
+
}
|
248
|
+
var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
|
249
|
+
switch (cssWhiteSpace) {
|
250
|
+
case "pre":
|
251
|
+
case "pre-wrap":
|
252
|
+
case "-moz-pre-wrap":
|
253
|
+
return false;
|
254
|
+
case "pre-line":
|
255
|
+
if (/[\r\n]/.test(node.data)) {
|
256
|
+
return false;
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
// We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
|
261
|
+
// non-inline element, it will not be rendered. This seems to be a good enough definition.
|
262
|
+
return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
|
263
|
+
}
|
264
|
+
|
265
|
+
function getRangeBoundaries(ranges) {
|
266
|
+
var positions = [], i, range;
|
267
|
+
for (i = 0; range = ranges[i++]; ) {
|
268
|
+
positions.push(
|
269
|
+
new DomPosition(range.startContainer, range.startOffset),
|
270
|
+
new DomPosition(range.endContainer, range.endOffset)
|
271
|
+
);
|
272
|
+
}
|
273
|
+
return positions;
|
274
|
+
}
|
275
|
+
|
276
|
+
function updateRangesFromBoundaries(ranges, positions) {
|
277
|
+
for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) {
|
278
|
+
range = ranges[i];
|
279
|
+
start = positions[i * 2];
|
280
|
+
end = positions[i * 2 + 1];
|
281
|
+
range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
function isSplitPoint(node, offset) {
|
286
|
+
if (dom.isCharacterDataNode(node)) {
|
287
|
+
if (offset == 0) {
|
288
|
+
return !!node.previousSibling;
|
289
|
+
} else if (offset == node.length) {
|
290
|
+
return !!node.nextSibling;
|
291
|
+
} else {
|
292
|
+
return true;
|
293
|
+
}
|
294
|
+
}
|
295
|
+
|
296
|
+
return offset > 0 && offset < node.childNodes.length;
|
297
|
+
}
|
298
|
+
|
299
|
+
function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) {
|
300
|
+
var newNode, parentNode;
|
301
|
+
var splitAtStart = (descendantOffset == 0);
|
302
|
+
|
303
|
+
if (dom.isAncestorOf(descendantNode, node)) {
|
304
|
+
return node;
|
305
|
+
}
|
306
|
+
|
307
|
+
if (dom.isCharacterDataNode(descendantNode)) {
|
308
|
+
var descendantIndex = dom.getNodeIndex(descendantNode);
|
309
|
+
if (descendantOffset == 0) {
|
310
|
+
descendantOffset = descendantIndex;
|
311
|
+
} else if (descendantOffset == descendantNode.length) {
|
312
|
+
descendantOffset = descendantIndex + 1;
|
313
|
+
} else {
|
314
|
+
throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node ("
|
315
|
+
+ descendantOffset + " in " + descendantNode.data);
|
316
|
+
}
|
317
|
+
descendantNode = descendantNode.parentNode;
|
318
|
+
}
|
319
|
+
|
320
|
+
if (isSplitPoint(descendantNode, descendantOffset)) {
|
321
|
+
// descendantNode is now guaranteed not to be a text or other character node
|
322
|
+
newNode = descendantNode.cloneNode(false);
|
323
|
+
parentNode = descendantNode.parentNode;
|
324
|
+
if (newNode.id) {
|
325
|
+
newNode.removeAttribute("id");
|
326
|
+
}
|
327
|
+
var child, newChildIndex = 0;
|
328
|
+
|
329
|
+
while ( (child = descendantNode.childNodes[descendantOffset]) ) {
|
330
|
+
movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve);
|
331
|
+
//newNode.appendChild(child);
|
332
|
+
}
|
333
|
+
movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve);
|
334
|
+
//dom.insertAfter(newNode, descendantNode);
|
335
|
+
return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve);
|
336
|
+
} else if (node != descendantNode) {
|
337
|
+
newNode = descendantNode.parentNode;
|
338
|
+
|
339
|
+
// Work out a new split point in the parent node
|
340
|
+
var newNodeIndex = dom.getNodeIndex(descendantNode);
|
341
|
+
|
342
|
+
if (!splitAtStart) {
|
343
|
+
newNodeIndex++;
|
344
|
+
}
|
345
|
+
return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve);
|
346
|
+
}
|
347
|
+
return node;
|
348
|
+
}
|
349
|
+
|
350
|
+
function areElementsMergeable(el1, el2) {
|
351
|
+
return el1.tagName == el2.tagName
|
352
|
+
&& haveSameClasses(el1, el2)
|
353
|
+
&& elementsHaveSameNonClassAttributes(el1, el2)
|
354
|
+
&& getComputedStyleProperty(el1, "display") == "inline"
|
355
|
+
&& getComputedStyleProperty(el2, "display") == "inline";
|
356
|
+
}
|
357
|
+
|
358
|
+
function createAdjacentMergeableTextNodeGetter(forward) {
|
359
|
+
var propName = forward ? "nextSibling" : "previousSibling";
|
360
|
+
|
361
|
+
return function(textNode, checkParentElement) {
|
362
|
+
var el = textNode.parentNode;
|
363
|
+
var adjacentNode = textNode[propName];
|
364
|
+
if (adjacentNode) {
|
365
|
+
// Can merge if the node's previous/next sibling is a text node
|
366
|
+
if (adjacentNode && adjacentNode.nodeType == 3) {
|
367
|
+
return adjacentNode;
|
368
|
+
}
|
369
|
+
} else if (checkParentElement) {
|
370
|
+
// Compare text node parent element with its sibling
|
371
|
+
adjacentNode = el[propName];
|
372
|
+
if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)/* && adjacentNode.hasChildNodes()*/) {
|
373
|
+
return adjacentNode[forward ? "firstChild" : "lastChild"];
|
374
|
+
}
|
375
|
+
}
|
376
|
+
return null;
|
377
|
+
};
|
378
|
+
}
|
379
|
+
|
380
|
+
var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
|
381
|
+
getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
|
382
|
+
|
383
|
+
|
384
|
+
function Merge(firstNode) {
|
385
|
+
this.isElementMerge = (firstNode.nodeType == 1);
|
386
|
+
this.textNodes = [];
|
387
|
+
var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
|
388
|
+
if (firstTextNode) {
|
389
|
+
this.textNodes[0] = firstTextNode;
|
390
|
+
}
|
391
|
+
}
|
392
|
+
|
393
|
+
Merge.prototype = {
|
394
|
+
doMerge: function(positionsToPreserve) {
|
395
|
+
var textNodes = this.textNodes;
|
396
|
+
var firstTextNode = textNodes[0];
|
397
|
+
if (textNodes.length > 1) {
|
398
|
+
var textParts = [], combinedTextLength = 0, textNode, parent;
|
399
|
+
for (var i = 0, len = textNodes.length, j, position; i < len; ++i) {
|
400
|
+
textNode = textNodes[i];
|
401
|
+
parent = textNode.parentNode;
|
402
|
+
if (i > 0) {
|
403
|
+
parent.removeChild(textNode);
|
404
|
+
if (!parent.hasChildNodes()) {
|
405
|
+
parent.parentNode.removeChild(parent);
|
406
|
+
}
|
407
|
+
if (positionsToPreserve) {
|
408
|
+
for (j = 0; position = positionsToPreserve[j++]; ) {
|
409
|
+
// Handle case where position is inside the text node being merged into a preceding node
|
410
|
+
if (position.node == textNode) {
|
411
|
+
position.node = firstTextNode;
|
412
|
+
position.offset += combinedTextLength;
|
413
|
+
}
|
414
|
+
}
|
415
|
+
}
|
416
|
+
}
|
417
|
+
textParts[i] = textNode.data;
|
418
|
+
combinedTextLength += textNode.data.length;
|
419
|
+
}
|
420
|
+
firstTextNode.data = textParts.join("");
|
421
|
+
}
|
422
|
+
return firstTextNode.data;
|
423
|
+
},
|
424
|
+
|
425
|
+
getLength: function() {
|
426
|
+
var i = this.textNodes.length, len = 0;
|
427
|
+
while (i--) {
|
428
|
+
len += this.textNodes[i].length;
|
429
|
+
}
|
430
|
+
return len;
|
431
|
+
},
|
432
|
+
|
433
|
+
toString: function() {
|
434
|
+
var textBits = [];
|
435
|
+
for (var i = 0, len = this.textNodes.length; i < len; ++i) {
|
436
|
+
textBits[i] = "'" + this.textNodes[i].data + "'";
|
437
|
+
}
|
438
|
+
return "[Merge(" + textBits.join(",") + ")]";
|
439
|
+
}
|
440
|
+
};
|
441
|
+
|
442
|
+
var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements",
|
443
|
+
"removeEmptyElements"];
|
444
|
+
|
445
|
+
// TODO: Populate this with every attribute name that corresponds to a property with a different name
|
446
|
+
var attrNamesForProperties = {};
|
447
|
+
|
448
|
+
function ClassApplier(cssClass, options, tagNames) {
|
449
|
+
this.cssClass = cssClass;
|
450
|
+
var normalize, i, len, propName;
|
451
|
+
|
452
|
+
var elementPropertiesFromOptions = null;
|
453
|
+
|
454
|
+
// Initialize from options object
|
455
|
+
if (typeof options == "object" && options !== null) {
|
456
|
+
tagNames = options.tagNames;
|
457
|
+
elementPropertiesFromOptions = options.elementProperties;
|
458
|
+
|
459
|
+
for (i = 0; propName = optionProperties[i++]; ) {
|
460
|
+
if (options.hasOwnProperty(propName)) {
|
461
|
+
this[propName] = options[propName];
|
462
|
+
}
|
463
|
+
}
|
464
|
+
normalize = options.normalize;
|
465
|
+
} else {
|
466
|
+
normalize = options;
|
467
|
+
}
|
468
|
+
|
469
|
+
// Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying
|
470
|
+
this.normalize = (typeof normalize == "undefined") ? true : normalize;
|
471
|
+
|
472
|
+
// Initialize element properties and attribute exceptions
|
473
|
+
this.attrExceptions = [];
|
474
|
+
var el = document.createElement(this.elementTagName);
|
475
|
+
this.elementProperties = this.copyPropertiesToElement(elementPropertiesFromOptions, el, true);
|
476
|
+
|
477
|
+
this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ?
|
478
|
+
this.elementProperties.className : cssClass;
|
479
|
+
|
480
|
+
// Initialize tag names
|
481
|
+
this.applyToAnyTagName = false;
|
482
|
+
var type = typeof tagNames;
|
483
|
+
if (type == "string") {
|
484
|
+
if (tagNames == "*") {
|
485
|
+
this.applyToAnyTagName = true;
|
486
|
+
} else {
|
487
|
+
this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
|
488
|
+
}
|
489
|
+
} else if (type == "object" && typeof tagNames.length == "number") {
|
490
|
+
this.tagNames = [];
|
491
|
+
for (i = 0, len = tagNames.length; i < len; ++i) {
|
492
|
+
if (tagNames[i] == "*") {
|
493
|
+
this.applyToAnyTagName = true;
|
494
|
+
} else {
|
495
|
+
this.tagNames.push(tagNames[i].toLowerCase());
|
496
|
+
}
|
497
|
+
}
|
498
|
+
} else {
|
499
|
+
this.tagNames = [this.elementTagName];
|
500
|
+
}
|
501
|
+
}
|
502
|
+
|
503
|
+
ClassApplier.prototype = {
|
504
|
+
elementTagName: defaultTagName,
|
505
|
+
elementProperties: {},
|
506
|
+
ignoreWhiteSpace: true,
|
507
|
+
applyToEditableOnly: false,
|
508
|
+
useExistingElements: true,
|
509
|
+
removeEmptyElements: true,
|
510
|
+
|
511
|
+
copyPropertiesToElement: function(props, el, createCopy) {
|
512
|
+
var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName;
|
513
|
+
|
514
|
+
for (var p in props) {
|
515
|
+
if (props.hasOwnProperty(p)) {
|
516
|
+
propValue = props[p];
|
517
|
+
elPropValue = el[p];
|
518
|
+
|
519
|
+
// Special case for class. The copied properties object has the applier's CSS class as well as its
|
520
|
+
// own to simplify checks when removing styling elements
|
521
|
+
if (p == "className") {
|
522
|
+
addClass(el, propValue);
|
523
|
+
addClass(el, this.cssClass);
|
524
|
+
el[p] = sortClassName(el[p]);
|
525
|
+
if (createCopy) {
|
526
|
+
elProps[p] = el[p];
|
527
|
+
}
|
528
|
+
}
|
529
|
+
|
530
|
+
// Special case for style
|
531
|
+
else if (p == "style") {
|
532
|
+
elStyle = elPropValue;
|
533
|
+
if (createCopy) {
|
534
|
+
elProps[p] = elPropsStyle = {};
|
535
|
+
}
|
536
|
+
for (s in props[p]) {
|
537
|
+
elStyle[s] = propValue[s];
|
538
|
+
if (createCopy) {
|
539
|
+
elPropsStyle[s] = elStyle[s];
|
540
|
+
}
|
541
|
+
}
|
542
|
+
this.attrExceptions.push(p);
|
543
|
+
} else {
|
544
|
+
el[p] = propValue;
|
545
|
+
// Copy the property back from the dummy element so that later comparisons to check whether
|
546
|
+
// elements may be removed are checking against the right value. For example, the href property
|
547
|
+
// of an element returns a fully qualified URL even if it was previously assigned a relative
|
548
|
+
// URL.
|
549
|
+
if (createCopy) {
|
550
|
+
elProps[p] = el[p];
|
551
|
+
|
552
|
+
// Not all properties map to identically named attributes
|
553
|
+
attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p;
|
554
|
+
this.attrExceptions.push(attrName);
|
555
|
+
}
|
556
|
+
}
|
557
|
+
}
|
558
|
+
}
|
559
|
+
|
560
|
+
return createCopy ? elProps : "";
|
561
|
+
},
|
562
|
+
|
563
|
+
hasClass: function(node) {
|
564
|
+
return node.nodeType == 1 &&
|
565
|
+
contains(this.tagNames, node.tagName.toLowerCase()) &&
|
566
|
+
hasClass(node, this.cssClass);
|
567
|
+
},
|
568
|
+
|
569
|
+
getSelfOrAncestorWithClass: function(node) {
|
570
|
+
while (node) {
|
571
|
+
if (this.hasClass(node)) {
|
572
|
+
return node;
|
573
|
+
}
|
574
|
+
node = node.parentNode;
|
575
|
+
}
|
576
|
+
return null;
|
577
|
+
},
|
578
|
+
|
579
|
+
isModifiable: function(node) {
|
580
|
+
return !this.applyToEditableOnly || isEditable(node);
|
581
|
+
},
|
582
|
+
|
583
|
+
// White space adjacent to an unwrappable node can be ignored for wrapping
|
584
|
+
isIgnorableWhiteSpaceNode: function(node) {
|
585
|
+
return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
|
586
|
+
},
|
587
|
+
|
588
|
+
// Normalizes nodes after applying a CSS class to a Range.
|
589
|
+
postApply: function(textNodes, range, positionsToPreserve, isUndo) {
|
590
|
+
var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
|
591
|
+
|
592
|
+
var merges = [], currentMerge;
|
593
|
+
|
594
|
+
var rangeStartNode = firstNode, rangeEndNode = lastNode;
|
595
|
+
var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
|
596
|
+
|
597
|
+
var textNode, precedingTextNode;
|
598
|
+
|
599
|
+
// Check for every required merge and create a Merge object for each
|
600
|
+
for (var i = 0, len = textNodes.length; i < len; ++i) {
|
601
|
+
textNode = textNodes[i];
|
602
|
+
precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
|
603
|
+
if (precedingTextNode) {
|
604
|
+
if (!currentMerge) {
|
605
|
+
currentMerge = new Merge(precedingTextNode);
|
606
|
+
merges.push(currentMerge);
|
607
|
+
}
|
608
|
+
currentMerge.textNodes.push(textNode);
|
609
|
+
if (textNode === firstNode) {
|
610
|
+
rangeStartNode = currentMerge.textNodes[0];
|
611
|
+
rangeStartOffset = rangeStartNode.length;
|
612
|
+
}
|
613
|
+
if (textNode === lastNode) {
|
614
|
+
rangeEndNode = currentMerge.textNodes[0];
|
615
|
+
rangeEndOffset = currentMerge.getLength();
|
616
|
+
}
|
617
|
+
} else {
|
618
|
+
currentMerge = null;
|
619
|
+
}
|
620
|
+
}
|
621
|
+
|
622
|
+
// Test whether the first node after the range needs merging
|
623
|
+
var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
|
624
|
+
|
625
|
+
if (nextTextNode) {
|
626
|
+
if (!currentMerge) {
|
627
|
+
currentMerge = new Merge(lastNode);
|
628
|
+
merges.push(currentMerge);
|
629
|
+
}
|
630
|
+
currentMerge.textNodes.push(nextTextNode);
|
631
|
+
}
|
632
|
+
|
633
|
+
// Apply the merges
|
634
|
+
if (merges.length) {
|
635
|
+
for (i = 0, len = merges.length; i < len; ++i) {
|
636
|
+
merges[i].doMerge(positionsToPreserve);
|
637
|
+
}
|
638
|
+
|
639
|
+
// Set the range boundaries
|
640
|
+
range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
|
641
|
+
}
|
642
|
+
},
|
643
|
+
|
644
|
+
createContainer: function(doc) {
|
645
|
+
var el = doc.createElement(this.elementTagName);
|
646
|
+
this.copyPropertiesToElement(this.elementProperties, el, false);
|
647
|
+
addClass(el, this.cssClass);
|
648
|
+
return el;
|
649
|
+
},
|
650
|
+
|
651
|
+
applyToTextNode: function(textNode, positionsToPreserve) {
|
652
|
+
var parent = textNode.parentNode;
|
653
|
+
if (parent.childNodes.length == 1 &&
|
654
|
+
this.useExistingElements &&
|
655
|
+
contains(this.tagNames, parent.tagName.toLowerCase()) &&
|
656
|
+
elementHasProps(parent, this.elementProperties)) {
|
657
|
+
|
658
|
+
addClass(parent, this.cssClass);
|
659
|
+
} else {
|
660
|
+
var el = this.createContainer(dom.getDocument(textNode));
|
661
|
+
textNode.parentNode.insertBefore(el, textNode);
|
662
|
+
el.appendChild(textNode);
|
663
|
+
}
|
664
|
+
},
|
665
|
+
|
666
|
+
isRemovable: function(el) {
|
667
|
+
return el.tagName.toLowerCase() == this.elementTagName
|
668
|
+
&& getSortedClassName(el) == this.elementSortedClassName
|
669
|
+
&& elementHasProps(el, this.elementProperties)
|
670
|
+
&& !elementHasNonClassAttributes(el, this.attrExceptions)
|
671
|
+
&& this.isModifiable(el);
|
672
|
+
},
|
673
|
+
|
674
|
+
isEmptyContainer: function(el) {
|
675
|
+
var childNodeCount = el.childNodes.length;
|
676
|
+
return el.nodeType == 1
|
677
|
+
&& this.isRemovable(el)
|
678
|
+
&& (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild)));
|
679
|
+
},
|
680
|
+
|
681
|
+
removeEmptyContainers: function(range) {
|
682
|
+
var applier = this;
|
683
|
+
var nodesToRemove = range.getNodes([1], function(el) {
|
684
|
+
return applier.isEmptyContainer(el);
|
685
|
+
});
|
686
|
+
|
687
|
+
for (var i = 0, node; node = nodesToRemove[i++]; ) {
|
688
|
+
node.parentNode.removeChild(node);
|
689
|
+
}
|
690
|
+
},
|
691
|
+
|
692
|
+
undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) {
|
693
|
+
if (!range.containsNode(ancestorWithClass)) {
|
694
|
+
// Split out the portion of the ancestor from which we can remove the CSS class
|
695
|
+
//var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
|
696
|
+
var ancestorRange = range.cloneRange();
|
697
|
+
ancestorRange.selectNode(ancestorWithClass);
|
698
|
+
if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) {
|
699
|
+
splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve);
|
700
|
+
range.setEndAfter(ancestorWithClass);
|
701
|
+
}
|
702
|
+
if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) {
|
703
|
+
ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve);
|
704
|
+
}
|
705
|
+
}
|
706
|
+
if (this.isRemovable(ancestorWithClass)) {
|
707
|
+
replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
|
708
|
+
} else {
|
709
|
+
removeClass(ancestorWithClass, this.cssClass);
|
710
|
+
}
|
711
|
+
},
|
712
|
+
|
713
|
+
applyToRange: function(range, rangesToPreserve) {
|
714
|
+
rangesToPreserve = rangesToPreserve || [];
|
715
|
+
|
716
|
+
// Create an array of range boundaries to preserve
|
717
|
+
var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []);
|
718
|
+
|
719
|
+
range.splitBoundariesPreservingPositions(positionsToPreserve);
|
720
|
+
|
721
|
+
// Tidy up the DOM by removing empty containers
|
722
|
+
if (this.removeEmptyElements) {
|
723
|
+
this.removeEmptyContainers(range);
|
724
|
+
}
|
725
|
+
|
726
|
+
var textNodes = getEffectiveTextNodes(range);
|
727
|
+
|
728
|
+
if (textNodes.length) {
|
729
|
+
for (var i = 0, textNode; textNode = textNodes[i++]; ) {
|
730
|
+
if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode)
|
731
|
+
&& this.isModifiable(textNode)) {
|
732
|
+
this.applyToTextNode(textNode, positionsToPreserve);
|
733
|
+
}
|
734
|
+
}
|
735
|
+
textNode = textNodes[textNodes.length - 1];
|
736
|
+
range.setStartAndEnd(textNodes[0], 0, textNode, textNode.length);
|
737
|
+
if (this.normalize) {
|
738
|
+
this.postApply(textNodes, range, positionsToPreserve, false);
|
739
|
+
}
|
740
|
+
|
741
|
+
// Update the ranges from the preserved boundary positions
|
742
|
+
updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
|
743
|
+
}
|
744
|
+
},
|
745
|
+
|
746
|
+
applyToRanges: function(ranges) {
|
747
|
+
|
748
|
+
var i = ranges.length;
|
749
|
+
while (i--) {
|
750
|
+
this.applyToRange(ranges[i], ranges);
|
751
|
+
}
|
752
|
+
|
753
|
+
|
754
|
+
return ranges;
|
755
|
+
},
|
756
|
+
|
757
|
+
applyToSelection: function(win) {
|
758
|
+
var sel = api.getSelection(win);
|
759
|
+
sel.setRanges( this.applyToRanges(sel.getAllRanges()) );
|
760
|
+
},
|
761
|
+
|
762
|
+
undoToRange: function(range, rangesToPreserve) {
|
763
|
+
// Create an array of range boundaries to preserve
|
764
|
+
rangesToPreserve = rangesToPreserve || [];
|
765
|
+
var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
|
766
|
+
|
767
|
+
|
768
|
+
range.splitBoundariesPreservingPositions(positionsToPreserve);
|
769
|
+
|
770
|
+
// Tidy up the DOM by removing empty containers
|
771
|
+
if (this.removeEmptyElements) {
|
772
|
+
this.removeEmptyContainers(range, positionsToPreserve);
|
773
|
+
}
|
774
|
+
|
775
|
+
var textNodes = getEffectiveTextNodes(range);
|
776
|
+
var textNode, ancestorWithClass;
|
777
|
+
var lastTextNode = textNodes[textNodes.length - 1];
|
778
|
+
|
779
|
+
if (textNodes.length) {
|
780
|
+
for (var i = 0, len = textNodes.length; i < len; ++i) {
|
781
|
+
textNode = textNodes[i];
|
782
|
+
ancestorWithClass = this.getSelfOrAncestorWithClass(textNode);
|
783
|
+
if (ancestorWithClass && this.isModifiable(textNode)) {
|
784
|
+
this.undoToTextNode(textNode, range, ancestorWithClass, positionsToPreserve);
|
785
|
+
}
|
786
|
+
|
787
|
+
// Ensure the range is still valid
|
788
|
+
range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
|
789
|
+
}
|
790
|
+
|
791
|
+
|
792
|
+
if (this.normalize) {
|
793
|
+
this.postApply(textNodes, range, positionsToPreserve, true);
|
794
|
+
}
|
795
|
+
|
796
|
+
// Update the ranges from the preserved boundary positions
|
797
|
+
updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
|
798
|
+
}
|
799
|
+
},
|
800
|
+
|
801
|
+
undoToRanges: function(ranges) {
|
802
|
+
// Get ranges returned in document order
|
803
|
+
var i = ranges.length;
|
804
|
+
|
805
|
+
while (i--) {
|
806
|
+
this.undoToRange(ranges[i], ranges);
|
807
|
+
}
|
808
|
+
|
809
|
+
return ranges;
|
810
|
+
},
|
811
|
+
|
812
|
+
undoToSelection: function(win) {
|
813
|
+
var sel = api.getSelection(win);
|
814
|
+
var ranges = api.getSelection(win).getAllRanges();
|
815
|
+
this.undoToRanges(ranges);
|
816
|
+
sel.setRanges(ranges);
|
817
|
+
},
|
818
|
+
|
819
|
+
getTextSelectedByRange: function(textNode, range) {
|
820
|
+
var textRange = range.cloneRange();
|
821
|
+
textRange.selectNodeContents(textNode);
|
822
|
+
|
823
|
+
var intersectionRange = textRange.intersection(range);
|
824
|
+
var text = intersectionRange ? intersectionRange.toString() : "";
|
825
|
+
textRange.detach();
|
826
|
+
|
827
|
+
return text;
|
828
|
+
},
|
829
|
+
|
830
|
+
isAppliedToRange: function(range) {
|
831
|
+
if (range.collapsed || range.toString() == "") {
|
832
|
+
return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
|
833
|
+
} else {
|
834
|
+
var textNodes = range.getNodes( [3] );
|
835
|
+
if (textNodes.length)
|
836
|
+
for (var i = 0, textNode; textNode = textNodes[i++]; ) {
|
837
|
+
if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode)
|
838
|
+
&& this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
|
839
|
+
return false;
|
840
|
+
}
|
841
|
+
}
|
842
|
+
return true;
|
843
|
+
}
|
844
|
+
},
|
845
|
+
|
846
|
+
isAppliedToRanges: function(ranges) {
|
847
|
+
var i = ranges.length;
|
848
|
+
while (i--) {
|
849
|
+
if (!this.isAppliedToRange(ranges[i])) {
|
850
|
+
return false;
|
851
|
+
}
|
852
|
+
}
|
853
|
+
return true;
|
854
|
+
},
|
855
|
+
|
856
|
+
isAppliedToSelection: function(win) {
|
857
|
+
var sel = api.getSelection(win);
|
858
|
+
return this.isAppliedToRanges(sel.getAllRanges());
|
859
|
+
},
|
860
|
+
|
861
|
+
toggleRange: function(range) {
|
862
|
+
if (this.isAppliedToRange(range)) {
|
863
|
+
this.undoToRange(range);
|
864
|
+
} else {
|
865
|
+
this.applyToRange(range);
|
866
|
+
}
|
867
|
+
},
|
868
|
+
|
869
|
+
toggleRanges: function(ranges) {
|
870
|
+
if (this.isAppliedToRanges(ranges)) {
|
871
|
+
this.undoToRanges(ranges);
|
872
|
+
} else {
|
873
|
+
this.applyToRanges(ranges);
|
874
|
+
}
|
875
|
+
},
|
876
|
+
|
877
|
+
toggleSelection: function(win) {
|
878
|
+
if (this.isAppliedToSelection(win)) {
|
879
|
+
this.undoToSelection(win);
|
880
|
+
} else {
|
881
|
+
this.applyToSelection(win);
|
882
|
+
}
|
883
|
+
},
|
884
|
+
|
885
|
+
getElementsWithClassIntersectingRange: function(range) {
|
886
|
+
var elements = [];
|
887
|
+
var applier = this;
|
888
|
+
range.getNodes([3], function(textNode) {
|
889
|
+
var el = applier.getSelfOrAncestorWithClass(textNode);
|
890
|
+
if (el && !contains(elements, el)) {
|
891
|
+
elements.push(el);
|
892
|
+
}
|
893
|
+
});
|
894
|
+
return elements;
|
895
|
+
},
|
896
|
+
|
897
|
+
getElementsWithClassIntersectingSelection: function(win) {
|
898
|
+
var sel = api.getSelection(win);
|
899
|
+
var elements = [];
|
900
|
+
var applier = this;
|
901
|
+
sel.eachRange(function(range) {
|
902
|
+
var rangeElements = applier.getElementsWithClassIntersectingRange(range);
|
903
|
+
for (var i = 0, el; el = rangeElements[i++]; ) {
|
904
|
+
if (!contains(elements, el)) {
|
905
|
+
elements.push(el);
|
906
|
+
}
|
907
|
+
}
|
908
|
+
});
|
909
|
+
return elements;
|
910
|
+
},
|
911
|
+
|
912
|
+
detach: function() {}
|
913
|
+
};
|
914
|
+
|
915
|
+
function createClassApplier(cssClass, options, tagNames) {
|
916
|
+
return new ClassApplier(cssClass, options, tagNames);
|
917
|
+
}
|
918
|
+
|
919
|
+
ClassApplier.util = {
|
920
|
+
hasClass: hasClass,
|
921
|
+
addClass: addClass,
|
922
|
+
removeClass: removeClass,
|
923
|
+
hasSameClasses: haveSameClasses,
|
924
|
+
replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions,
|
925
|
+
elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
|
926
|
+
elementHasNonClassAttributes: elementHasNonClassAttributes,
|
927
|
+
splitNodeAt: splitNodeAt,
|
928
|
+
isEditableElement: isEditableElement,
|
929
|
+
isEditingHost: isEditingHost,
|
930
|
+
isEditable: isEditable
|
931
|
+
};
|
932
|
+
|
933
|
+
api.CssClassApplier = api.ClassApplier = ClassApplier;
|
934
|
+
api.createCssClassApplier = api.createClassApplier = createClassApplier;
|
935
|
+
});
|