rangy-rails 1.3alpha.772.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
});
|