Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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