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.
@@ -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
+ });