rangy-rails 1.3alpha.772.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ });