Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/**
2
 * Rangy, a cross-browser JavaScript range and selection library
3
 * https://github.com/timdown/rangy
4
 *
5
 * Copyright 2022, Tim Down
6
 * Licensed under the MIT license.
7
 * Version: 1.3.1
8
 * Build date: 17 August 2022
9
 */
10
 
11
(function(factory, root) {
12
    // No AMD or CommonJS support so we place Rangy in (probably) the global variable
13
    root.rangy = factory();
14
})(function() {
15
 
16
    var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
17
 
18
    // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
19
    // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
20
    var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
21
        "commonAncestorContainer"];
22
 
23
    // Minimal set of methods required for DOM Level 2 Range compliance
24
    var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
25
        "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
26
        "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
27
 
28
    var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
29
 
30
    // Subset of TextRange's full set of methods that we're interested in
31
    var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
32
        "setEndPoint", "getBoundingClientRect"];
33
 
34
    /*----------------------------------------------------------------------------------------------------------------*/
35
 
36
    // Trio of functions taken from Peter Michaux's article:
37
    // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
38
    function isHostMethod(o, p) {
39
        var t = typeof o[p];
40
        return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
41
    }
42
 
43
    function isHostObject(o, p) {
44
        return !!(typeof o[p] == OBJECT && o[p]);
45
    }
46
 
47
    function isHostProperty(o, p) {
48
        return typeof o[p] != UNDEFINED;
49
    }
50
 
51
    // Creates a convenience function to save verbose repeated calls to tests functions
52
    function createMultiplePropertyTest(testFunc) {
53
        return function(o, props) {
54
            var i = props.length;
55
            while (i--) {
56
                if (!testFunc(o, props[i])) {
57
                    return false;
58
                }
59
            }
60
            return true;
61
        };
62
    }
63
 
64
    // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
65
    var areHostMethods = createMultiplePropertyTest(isHostMethod);
66
    var areHostObjects = createMultiplePropertyTest(isHostObject);
67
    var areHostProperties = createMultiplePropertyTest(isHostProperty);
68
 
69
    function isTextRange(range) {
70
        return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
71
    }
72
 
73
    function getBody(doc) {
74
        return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
75
    }
76
 
77
    var forEach = [].forEach ?
78
        function(arr, func) {
79
            arr.forEach(func);
80
        } :
81
        function(arr, func) {
82
            for (var i = 0, len = arr.length; i < len; ++i) {
83
                func(arr[i], i);
84
            }
85
        };
86
 
87
    var modules = {};
88
 
89
    var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
90
 
91
    var util = {
92
        isHostMethod: isHostMethod,
93
        isHostObject: isHostObject,
94
        isHostProperty: isHostProperty,
95
        areHostMethods: areHostMethods,
96
        areHostObjects: areHostObjects,
97
        areHostProperties: areHostProperties,
98
        isTextRange: isTextRange,
99
        getBody: getBody,
100
        forEach: forEach
101
    };
102
 
103
    var api = {
104
        version: "1.3.1",
105
        initialized: false,
106
        isBrowser: isBrowser,
107
        supported: true,
108
        util: util,
109
        features: {},
110
        modules: modules,
111
        config: {
112
            alertOnFail: false,
113
            alertOnWarn: false,
114
            preferTextRange: false,
115
            autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
116
        }
117
    };
118
 
119
    function consoleLog(msg) {
120
        if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
121
            console.log(msg);
122
        }
123
    }
124
 
125
    function alertOrLog(msg, shouldAlert) {
126
        if (isBrowser && shouldAlert) {
127
            alert(msg);
128
        } else  {
129
            consoleLog(msg);
130
        }
131
    }
132
 
133
    function fail(reason) {
134
        api.initialized = true;
135
        api.supported = false;
136
        alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
137
    }
138
 
139
    api.fail = fail;
140
 
141
    function warn(msg) {
142
        alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
143
    }
144
 
145
    api.warn = warn;
146
 
147
    // Add utility extend() method
148
    var extend;
149
    if ({}.hasOwnProperty) {
150
        util.extend = extend = function(obj, props, deep) {
151
            var o, p;
152
            for (var i in props) {
153
                if (props.hasOwnProperty(i)) {
154
                    o = obj[i];
155
                    p = props[i];
156
                    if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
157
                        extend(o, p, true);
158
                    }
159
                    obj[i] = p;
160
                }
161
            }
162
            // Special case for toString, which does not show up in for...in loops in IE <= 8
163
            if (props.hasOwnProperty("toString")) {
164
                obj.toString = props.toString;
165
            }
166
            return obj;
167
        };
168
 
169
        util.createOptions = function(optionsParam, defaults) {
170
            var options = {};
171
            extend(options, defaults);
172
            if (optionsParam) {
173
                extend(options, optionsParam);
174
            }
175
            return options;
176
        };
177
    } else {
178
        fail("hasOwnProperty not supported");
179
    }
180
 
181
    // Test whether we're in a browser and bail out if not
182
    if (!isBrowser) {
183
        fail("Rangy can only run in a browser");
184
    }
185
 
186
    // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
187
    (function() {
188
        var toArray;
189
 
190
        if (isBrowser) {
191
            var el = document.createElement("div");
192
            el.appendChild(document.createElement("span"));
193
            var slice = [].slice;
194
            try {
195
                if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
196
                    toArray = function(arrayLike) {
197
                        return slice.call(arrayLike, 0);
198
                    };
199
                }
200
            } catch (e) {}
201
        }
202
 
203
        if (!toArray) {
204
            toArray = function(arrayLike) {
205
                var arr = [];
206
                for (var i = 0, len = arrayLike.length; i < len; ++i) {
207
                    arr[i] = arrayLike[i];
208
                }
209
                return arr;
210
            };
211
        }
212
 
213
        util.toArray = toArray;
214
    })();
215
 
216
    // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
217
    // normalization of event properties because we don't need this.
218
    var addListener;
219
    if (isBrowser) {
220
        if (isHostMethod(document, "addEventListener")) {
221
            addListener = function(obj, eventType, listener) {
222
                obj.addEventListener(eventType, listener, false);
223
            };
224
        } else if (isHostMethod(document, "attachEvent")) {
225
            addListener = function(obj, eventType, listener) {
226
                obj.attachEvent("on" + eventType, listener);
227
            };
228
        } else {
229
            fail("Document does not have required addEventListener or attachEvent method");
230
        }
231
 
232
        util.addListener = addListener;
233
    }
234
 
235
    var initListeners = [];
236
 
237
    function getErrorDesc(ex) {
238
        return ex.message || ex.description || String(ex);
239
    }
240
 
241
    // Initialization
242
    function init() {
243
        if (!isBrowser || api.initialized) {
244
            return;
245
        }
246
        var testRange;
247
        var implementsDomRange = false, implementsTextRange = false;
248
 
249
        // First, perform basic feature tests
250
 
251
        if (isHostMethod(document, "createRange")) {
252
            testRange = document.createRange();
253
            if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
254
                implementsDomRange = true;
255
            }
256
        }
257
 
258
        var body = getBody(document);
259
        if (!body || body.nodeName.toLowerCase() != "body") {
260
            fail("No body element found");
261
            return;
262
        }
263
 
264
        if (body && isHostMethod(body, "createTextRange")) {
265
            testRange = body.createTextRange();
266
            if (isTextRange(testRange)) {
267
                implementsTextRange = true;
268
            }
269
        }
270
 
271
        if (!implementsDomRange && !implementsTextRange) {
272
            fail("Neither Range nor TextRange are available");
273
            return;
274
        }
275
 
276
        api.initialized = true;
277
        api.features = {
278
            implementsDomRange: implementsDomRange,
279
            implementsTextRange: implementsTextRange
280
        };
281
 
282
        // Initialize modules
283
        var module, errorMessage;
284
        for (var moduleName in modules) {
285
            if ( (module = modules[moduleName]) instanceof Module ) {
286
                module.init(module, api);
287
            }
288
        }
289
 
290
        // Call init listeners
291
        for (var i = 0, len = initListeners.length; i < len; ++i) {
292
            try {
293
                initListeners[i](api);
294
            } catch (ex) {
295
                errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
296
                consoleLog(errorMessage);
297
            }
298
        }
299
    }
300
 
301
    function deprecationNotice(deprecated, replacement, module) {
302
        if (module) {
303
            deprecated += " in module " + module.name;
304
        }
305
        api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
306
        replacement + " instead.");
307
    }
308
 
309
    function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
310
        owner[deprecated] = function() {
311
            deprecationNotice(deprecated, replacement, module);
312
            return owner[replacement].apply(owner, util.toArray(arguments));
313
        };
314
    }
315
 
316
    util.deprecationNotice = deprecationNotice;
317
    util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
318
 
319
    // Allow external scripts to initialize this library in case it's loaded after the document has loaded
320
    api.init = init;
321
 
322
    // Execute listener immediately if already initialized
323
    api.addInitListener = function(listener) {
324
        if (api.initialized) {
325
            listener(api);
326
        } else {
327
            initListeners.push(listener);
328
        }
329
    };
330
 
331
    var shimListeners = [];
332
 
333
    api.addShimListener = function(listener) {
334
        shimListeners.push(listener);
335
    };
336
 
337
    function shim(win) {
338
        win = win || window;
339
        init();
340
 
341
        // Notify listeners
342
        for (var i = 0, len = shimListeners.length; i < len; ++i) {
343
            shimListeners[i](win);
344
        }
345
    }
346
 
347
    if (isBrowser) {
348
        api.shim = api.createMissingNativeApi = shim;
349
        createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
350
    }
351
 
352
    function Module(name, dependencies, initializer) {
353
        this.name = name;
354
        this.dependencies = dependencies;
355
        this.initialized = false;
356
        this.supported = false;
357
        this.initializer = initializer;
358
    }
359
 
360
    Module.prototype = {
361
        init: function() {
362
            var requiredModuleNames = this.dependencies || [];
363
            for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
364
                moduleName = requiredModuleNames[i];
365
 
366
                requiredModule = modules[moduleName];
367
                if (!requiredModule || !(requiredModule instanceof Module)) {
368
                    throw new Error("required module '" + moduleName + "' not found");
369
                }
370
 
371
                requiredModule.init();
372
 
373
                if (!requiredModule.supported) {
374
                    throw new Error("required module '" + moduleName + "' not supported");
375
                }
376
            }
377
 
378
            // Now run initializer
379
            this.initializer(this);
380
        },
381
 
382
        fail: function(reason) {
383
            this.initialized = true;
384
            this.supported = false;
385
            throw new Error(reason);
386
        },
387
 
388
        warn: function(msg) {
389
            api.warn("Module " + this.name + ": " + msg);
390
        },
391
 
392
        deprecationNotice: function(deprecated, replacement) {
393
            api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
394
                replacement + " instead");
395
        },
396
 
397
        createError: function(msg) {
398
            return new Error("Error in Rangy " + this.name + " module: " + msg);
399
        }
400
    };
401
 
402
    function createModule(name, dependencies, initFunc) {
403
        var newModule = new Module(name, dependencies, function(module) {
404
            if (!module.initialized) {
405
                module.initialized = true;
406
                try {
407
                    initFunc(api, module);
408
                    module.supported = true;
409
                } catch (ex) {
410
                    var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
411
                    consoleLog(errorMessage);
412
                    if (ex.stack) {
413
                        consoleLog(ex.stack);
414
                    }
415
                }
416
            }
417
        });
418
        modules[name] = newModule;
419
        return newModule;
420
    }
421
 
422
    api.createModule = function(name) {
423
        // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
424
        var initFunc, dependencies;
425
        if (arguments.length == 2) {
426
            initFunc = arguments[1];
427
            dependencies = [];
428
        } else {
429
            initFunc = arguments[2];
430
            dependencies = arguments[1];
431
        }
432
 
433
        var module = createModule(name, dependencies, initFunc);
434
 
435
        // Initialize the module immediately if the core is already initialized
436
        if (api.initialized && api.supported) {
437
            module.init();
438
        }
439
    };
440
 
441
    api.createCoreModule = function(name, dependencies, initFunc) {
442
        createModule(name, dependencies, initFunc);
443
    };
444
 
445
    /*----------------------------------------------------------------------------------------------------------------*/
446
 
447
    // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
448
 
449
    function RangePrototype() {}
450
    api.RangePrototype = RangePrototype;
451
    api.rangePrototype = new RangePrototype();
452
 
453
    function SelectionPrototype() {}
454
    api.selectionPrototype = new SelectionPrototype();
455
 
456
    /*----------------------------------------------------------------------------------------------------------------*/
457
 
458
    // DOM utility methods used by Rangy
459
    api.createCoreModule("DomUtil", [], function(api, module) {
460
        var UNDEF = "undefined";
461
        var util = api.util;
462
        var getBody = util.getBody;
463
 
464
        // Perform feature tests
465
        if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
466
            module.fail("document missing a Node creation method");
467
        }
468
 
469
        if (!util.isHostMethod(document, "getElementsByTagName")) {
470
            module.fail("document missing getElementsByTagName method");
471
        }
472
 
473
        var el = document.createElement("div");
474
        if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
475
                !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
476
            module.fail("Incomplete Element implementation");
477
        }
478
 
479
        // innerHTML is required for Range's createContextualFragment method
480
        if (!util.isHostProperty(el, "innerHTML")) {
481
            module.fail("Element is missing innerHTML property");
482
        }
483
 
484
        var textNode = document.createTextNode("test");
485
        if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
486
                !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
487
                !util.areHostProperties(textNode, ["data"]))) {
488
            module.fail("Incomplete Text Node implementation");
489
        }
490
 
491
        /*----------------------------------------------------------------------------------------------------------------*/
492
 
493
        // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
494
        // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
495
        // contains just the document as a single element and the value searched for is the document.
496
        var arrayContains = /*Array.prototype.indexOf ?
497
            function(arr, val) {
498
                return arr.indexOf(val) > -1;
499
            }:*/
500
 
501
            function(arr, val) {
502
                var i = arr.length;
503
                while (i--) {
504
                    if (arr[i] === val) {
505
                        return true;
506
                    }
507
                }
508
                return false;
509
            };
510
 
511
        // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
512
        function isHtmlNamespace(node) {
513
            var ns;
514
            return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
515
        }
516
 
517
        function parentElement(node) {
518
            var parent = node.parentNode;
519
            return (parent.nodeType == 1) ? parent : null;
520
        }
521
 
522
        function getNodeIndex(node) {
523
            var i = 0;
524
            while( (node = node.previousSibling) ) {
525
                ++i;
526
            }
527
            return i;
528
        }
529
 
530
        function getNodeLength(node) {
531
            switch (node.nodeType) {
532
                case 7:
533
                case 10:
534
                    return 0;
535
                case 3:
536
                case 8:
537
                    return node.length;
538
                default:
539
                    return node.childNodes.length;
540
            }
541
        }
542
 
543
        function getCommonAncestor(node1, node2) {
544
            var ancestors = [], n;
545
            for (n = node1; n; n = n.parentNode) {
546
                ancestors.push(n);
547
            }
548
 
549
            for (n = node2; n; n = n.parentNode) {
550
                if (arrayContains(ancestors, n)) {
551
                    return n;
552
                }
553
            }
554
 
555
            return null;
556
        }
557
 
558
        function isAncestorOf(ancestor, descendant, selfIsAncestor) {
559
            var n = selfIsAncestor ? descendant : descendant.parentNode;
560
            while (n) {
561
                if (n === ancestor) {
562
                    return true;
563
                } else {
564
                    n = n.parentNode;
565
                }
566
            }
567
            return false;
568
        }
569
 
570
        function isOrIsAncestorOf(ancestor, descendant) {
571
            return isAncestorOf(ancestor, descendant, true);
572
        }
573
 
574
        function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
575
            var p, n = selfIsAncestor ? node : node.parentNode;
576
            while (n) {
577
                p = n.parentNode;
578
                if (p === ancestor) {
579
                    return n;
580
                }
581
                n = p;
582
            }
583
            return null;
584
        }
585
 
586
        function isCharacterDataNode(node) {
587
            var t = node.nodeType;
588
            return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
589
        }
590
 
591
        function isTextOrCommentNode(node) {
592
            if (!node) {
593
                return false;
594
            }
595
            var t = node.nodeType;
596
            return t == 3 || t == 8 ; // Text or Comment
597
        }
598
 
599
        function insertAfter(node, precedingNode) {
600
            var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
601
            if (nextNode) {
602
                parent.insertBefore(node, nextNode);
603
            } else {
604
                parent.appendChild(node);
605
            }
606
            return node;
607
        }
608
 
609
        // Note that we cannot use splitText() because it is bugridden in IE 9.
610
        function splitDataNode(node, index, positionsToPreserve) {
611
            var newNode = node.cloneNode(false);
612
            newNode.deleteData(0, index);
613
            node.deleteData(index, node.length - index);
614
            insertAfter(newNode, node);
615
 
616
            // Preserve positions
617
            if (positionsToPreserve) {
618
                for (var i = 0, position; position = positionsToPreserve[i++]; ) {
619
                    // Handle case where position was inside the portion of node after the split point
620
                    if (position.node == node && position.offset > index) {
621
                        position.node = newNode;
622
                        position.offset -= index;
623
                    }
624
                    // Handle the case where the position is a node offset within node's parent
625
                    else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
626
                        ++position.offset;
627
                    }
628
                }
629
            }
630
            return newNode;
631
        }
632
 
633
        function getDocument(node) {
634
            if (node.nodeType == 9) {
635
                return node;
636
            } else if (typeof node.ownerDocument != UNDEF) {
637
                return node.ownerDocument;
638
            } else if (typeof node.document != UNDEF) {
639
                return node.document;
640
            } else if (node.parentNode) {
641
                return getDocument(node.parentNode);
642
            } else {
643
                throw module.createError("getDocument: no document found for node");
644
            }
645
        }
646
 
647
        function getWindow(node) {
648
            var doc = getDocument(node);
649
            if (typeof doc.defaultView != UNDEF) {
650
                return doc.defaultView;
651
            } else if (typeof doc.parentWindow != UNDEF) {
652
                return doc.parentWindow;
653
            } else {
654
                throw module.createError("Cannot get a window object for node");
655
            }
656
        }
657
 
658
        function getIframeDocument(iframeEl) {
659
            if (typeof iframeEl.contentDocument != UNDEF) {
660
                return iframeEl.contentDocument;
661
            } else if (typeof iframeEl.contentWindow != UNDEF) {
662
                return iframeEl.contentWindow.document;
663
            } else {
664
                throw module.createError("getIframeDocument: No Document object found for iframe element");
665
            }
666
        }
667
 
668
        function getIframeWindow(iframeEl) {
669
            if (typeof iframeEl.contentWindow != UNDEF) {
670
                return iframeEl.contentWindow;
671
            } else if (typeof iframeEl.contentDocument != UNDEF) {
672
                return iframeEl.contentDocument.defaultView;
673
            } else {
674
                throw module.createError("getIframeWindow: No Window object found for iframe element");
675
            }
676
        }
677
 
678
        // This looks bad. Is it worth it?
679
        function isWindow(obj) {
680
            return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
681
        }
682
 
683
        function getContentDocument(obj, module, methodName) {
684
            var doc;
685
 
686
            if (!obj) {
687
                doc = document;
688
            }
689
 
690
            // Test if a DOM node has been passed and obtain a document object for it if so
691
            else if (util.isHostProperty(obj, "nodeType")) {
692
                doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
693
                    getIframeDocument(obj) : getDocument(obj);
694
            }
695
 
696
            // Test if the doc parameter appears to be a Window object
697
            else if (isWindow(obj)) {
698
                doc = obj.document;
699
            }
700
 
701
            if (!doc) {
702
                throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
703
            }
704
 
705
            return doc;
706
        }
707
 
708
        function getRootContainer(node) {
709
            var parent;
710
            while ( (parent = node.parentNode) ) {
711
                node = parent;
712
            }
713
            return node;
714
        }
715
 
716
        function comparePoints(nodeA, offsetA, nodeB, offsetB) {
717
            // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
718
            var nodeC, root, childA, childB, n;
719
            if (nodeA == nodeB) {
720
                // Case 1: nodes are the same
721
                return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
722
            } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
723
                // Case 2: node C (container B or an ancestor) is a child node of A
724
                return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
725
            } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
726
                // Case 3: node C (container A or an ancestor) is a child node of B
727
                return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
728
            } else {
729
                root = getCommonAncestor(nodeA, nodeB);
730
                if (!root) {
731
                    throw new Error("comparePoints error: nodes have no common ancestor");
732
                }
733
 
734
                // Case 4: containers are siblings or descendants of siblings
735
                childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
736
                childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
737
 
738
                if (childA === childB) {
739
                    // This shouldn't be possible
740
                    throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
741
                } else {
742
                    n = root.firstChild;
743
                    while (n) {
744
                        if (n === childA) {
745
                            return -1;
746
                        } else if (n === childB) {
747
                            return 1;
748
                        }
749
                        n = n.nextSibling;
750
                    }
751
                }
752
            }
753
        }
754
 
755
        /*----------------------------------------------------------------------------------------------------------------*/
756
 
757
        // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
758
        var crashyTextNodes = false;
759
 
760
        function isBrokenNode(node) {
761
            var n;
762
            try {
763
                n = node.parentNode;
764
                return false;
765
            } catch (e) {
766
                return true;
767
            }
768
        }
769
 
770
        (function() {
771
            var el = document.createElement("b");
772
            el.innerHTML = "1";
773
            var textNode = el.firstChild;
774
            el.innerHTML = "<br />";
775
            crashyTextNodes = isBrokenNode(textNode);
776
 
777
            api.features.crashyTextNodes = crashyTextNodes;
778
        })();
779
 
780
        /*----------------------------------------------------------------------------------------------------------------*/
781
 
782
        function inspectNode(node) {
783
            if (!node) {
784
                return "[No node]";
785
            }
786
            if (crashyTextNodes && isBrokenNode(node)) {
787
                return "[Broken node]";
788
            }
789
            if (isCharacterDataNode(node)) {
790
                return '"' + node.data + '"';
791
            }
792
            if (node.nodeType == 1) {
793
                var idAttr = node.id ? ' id="' + node.id + '"' : "";
794
                return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
795
            }
796
            return node.nodeName;
797
        }
798
 
799
        function fragmentFromNodeChildren(node) {
800
            var fragment = getDocument(node).createDocumentFragment(), child;
801
            while ( (child = node.firstChild) ) {
802
                fragment.appendChild(child);
803
            }
804
            return fragment;
805
        }
806
 
807
        var getComputedStyleProperty;
808
        if (typeof window.getComputedStyle != UNDEF) {
809
            getComputedStyleProperty = function(el, propName) {
810
                return getWindow(el).getComputedStyle(el, null)[propName];
811
            };
812
        } else if (typeof document.documentElement.currentStyle != UNDEF) {
813
            getComputedStyleProperty = function(el, propName) {
814
                return el.currentStyle ? el.currentStyle[propName] : "";
815
            };
816
        } else {
817
            module.fail("No means of obtaining computed style properties found");
818
        }
819
 
820
        function createTestElement(doc, html, contentEditable) {
821
            var body = getBody(doc);
822
            var el = doc.createElement("div");
823
            el.contentEditable = "" + !!contentEditable;
824
            if (html) {
825
                el.innerHTML = html;
826
            }
827
 
828
            // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
829
            var bodyFirstChild = body.firstChild;
830
            if (bodyFirstChild) {
831
                body.insertBefore(el, bodyFirstChild);
832
            } else {
833
                body.appendChild(el);
834
            }
835
 
836
            return el;
837
        }
838
 
839
        function removeNode(node) {
840
            return node.parentNode.removeChild(node);
841
        }
842
 
843
        function NodeIterator(root) {
844
            this.root = root;
845
            this._next = root;
846
        }
847
 
848
        NodeIterator.prototype = {
849
            _current: null,
850
 
851
            hasNext: function() {
852
                return !!this._next;
853
            },
854
 
855
            next: function() {
856
                var n = this._current = this._next;
857
                var child, next;
858
                if (this._current) {
859
                    child = n.firstChild;
860
                    if (child) {
861
                        this._next = child;
862
                    } else {
863
                        next = null;
864
                        while ((n !== this.root) && !(next = n.nextSibling)) {
865
                            n = n.parentNode;
866
                        }
867
                        this._next = next;
868
                    }
869
                }
870
                return this._current;
871
            },
872
 
873
            detach: function() {
874
                this._current = this._next = this.root = null;
875
            }
876
        };
877
 
878
        function createIterator(root) {
879
            return new NodeIterator(root);
880
        }
881
 
882
        function DomPosition(node, offset) {
883
            this.node = node;
884
            this.offset = offset;
885
        }
886
 
887
        DomPosition.prototype = {
888
            equals: function(pos) {
889
                return !!pos && this.node === pos.node && this.offset == pos.offset;
890
            },
891
 
892
            inspect: function() {
893
                return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
894
            },
895
 
896
            toString: function() {
897
                return this.inspect();
898
            }
899
        };
900
 
901
        function DOMException(codeName) {
902
            this.code = this[codeName];
903
            this.codeName = codeName;
904
            this.message = "DOMException: " + this.codeName;
905
        }
906
 
907
        DOMException.prototype = {
908
            INDEX_SIZE_ERR: 1,
909
            HIERARCHY_REQUEST_ERR: 3,
910
            WRONG_DOCUMENT_ERR: 4,
911
            NO_MODIFICATION_ALLOWED_ERR: 7,
912
            NOT_FOUND_ERR: 8,
913
            NOT_SUPPORTED_ERR: 9,
914
            INVALID_STATE_ERR: 11,
915
            INVALID_NODE_TYPE_ERR: 24
916
        };
917
 
918
        DOMException.prototype.toString = function() {
919
            return this.message;
920
        };
921
 
922
        api.dom = {
923
            arrayContains: arrayContains,
924
            isHtmlNamespace: isHtmlNamespace,
925
            parentElement: parentElement,
926
            getNodeIndex: getNodeIndex,
927
            getNodeLength: getNodeLength,
928
            getCommonAncestor: getCommonAncestor,
929
            isAncestorOf: isAncestorOf,
930
            isOrIsAncestorOf: isOrIsAncestorOf,
931
            getClosestAncestorIn: getClosestAncestorIn,
932
            isCharacterDataNode: isCharacterDataNode,
933
            isTextOrCommentNode: isTextOrCommentNode,
934
            insertAfter: insertAfter,
935
            splitDataNode: splitDataNode,
936
            getDocument: getDocument,
937
            getWindow: getWindow,
938
            getIframeWindow: getIframeWindow,
939
            getIframeDocument: getIframeDocument,
940
            getBody: getBody,
941
            isWindow: isWindow,
942
            getContentDocument: getContentDocument,
943
            getRootContainer: getRootContainer,
944
            comparePoints: comparePoints,
945
            isBrokenNode: isBrokenNode,
946
            inspectNode: inspectNode,
947
            getComputedStyleProperty: getComputedStyleProperty,
948
            createTestElement: createTestElement,
949
            removeNode: removeNode,
950
            fragmentFromNodeChildren: fragmentFromNodeChildren,
951
            createIterator: createIterator,
952
            DomPosition: DomPosition
953
        };
954
 
955
        api.DOMException = DOMException;
956
    });
957
 
958
    /*----------------------------------------------------------------------------------------------------------------*/
959
 
960
    // Pure JavaScript implementation of DOM Range
961
    api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
962
        var dom = api.dom;
963
        var util = api.util;
964
        var DomPosition = dom.DomPosition;
965
        var DOMException = api.DOMException;
966
 
967
        var isCharacterDataNode = dom.isCharacterDataNode;
968
        var getNodeIndex = dom.getNodeIndex;
969
        var isOrIsAncestorOf = dom.isOrIsAncestorOf;
970
        var getDocument = dom.getDocument;
971
        var comparePoints = dom.comparePoints;
972
        var splitDataNode = dom.splitDataNode;
973
        var getClosestAncestorIn = dom.getClosestAncestorIn;
974
        var getNodeLength = dom.getNodeLength;
975
        var arrayContains = dom.arrayContains;
976
        var getRootContainer = dom.getRootContainer;
977
        var crashyTextNodes = api.features.crashyTextNodes;
978
 
979
        var removeNode = dom.removeNode;
980
 
981
        /*----------------------------------------------------------------------------------------------------------------*/
982
 
983
        // Utility functions
984
 
985
        function isNonTextPartiallySelected(node, range) {
986
            return (node.nodeType != 3) &&
987
                   (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
988
        }
989
 
990
        function getRangeDocument(range) {
991
            return range.document || getDocument(range.startContainer);
992
        }
993
 
994
        function getRangeRoot(range) {
995
            return getRootContainer(range.startContainer);
996
        }
997
 
998
        function getBoundaryBeforeNode(node) {
999
            return new DomPosition(node.parentNode, getNodeIndex(node));
1000
        }
1001
 
1002
        function getBoundaryAfterNode(node) {
1003
            return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1004
        }
1005
 
1006
        function insertNodeAtPosition(node, n, o) {
1007
            var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1008
            if (isCharacterDataNode(n)) {
1009
                if (o == n.length) {
1010
                    dom.insertAfter(node, n);
1011
                } else {
1012
                    n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1013
                }
1014
            } else if (o >= n.childNodes.length) {
1015
                n.appendChild(node);
1016
            } else {
1017
                n.insertBefore(node, n.childNodes[o]);
1018
            }
1019
            return firstNodeInserted;
1020
        }
1021
 
1022
        function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1023
            assertRangeValid(rangeA);
1024
            assertRangeValid(rangeB);
1025
 
1026
            if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1027
                throw new DOMException("WRONG_DOCUMENT_ERR");
1028
            }
1029
 
1030
            var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1031
                endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1032
 
1033
            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1034
        }
1035
 
1036
        function cloneSubtree(iterator) {
1037
            var partiallySelected;
1038
            for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1039
                partiallySelected = iterator.isPartiallySelectedSubtree();
1040
                node = node.cloneNode(!partiallySelected);
1041
                if (partiallySelected) {
1042
                    subIterator = iterator.getSubtreeIterator();
1043
                    node.appendChild(cloneSubtree(subIterator));
1044
                    subIterator.detach();
1045
                }
1046
 
1047
                if (node.nodeType == 10) { // DocumentType
1048
                    throw new DOMException("HIERARCHY_REQUEST_ERR");
1049
                }
1050
                frag.appendChild(node);
1051
            }
1052
            return frag;
1053
        }
1054
 
1055
        function iterateSubtree(rangeIterator, func, iteratorState) {
1056
            var it, n;
1057
            iteratorState = iteratorState || { stop: false };
1058
            for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1059
                if (rangeIterator.isPartiallySelectedSubtree()) {
1060
                    if (func(node) === false) {
1061
                        iteratorState.stop = true;
1062
                        return;
1063
                    } else {
1064
                        // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1065
                        // the node selected by the Range.
1066
                        subRangeIterator = rangeIterator.getSubtreeIterator();
1067
                        iterateSubtree(subRangeIterator, func, iteratorState);
1068
                        subRangeIterator.detach();
1069
                        if (iteratorState.stop) {
1070
                            return;
1071
                        }
1072
                    }
1073
                } else {
1074
                    // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1075
                    // descendants
1076
                    it = dom.createIterator(node);
1077
                    while ( (n = it.next()) ) {
1078
                        if (func(n) === false) {
1079
                            iteratorState.stop = true;
1080
                            return;
1081
                        }
1082
                    }
1083
                }
1084
            }
1085
        }
1086
 
1087
        function deleteSubtree(iterator) {
1088
            var subIterator;
1089
            while (iterator.next()) {
1090
                if (iterator.isPartiallySelectedSubtree()) {
1091
                    subIterator = iterator.getSubtreeIterator();
1092
                    deleteSubtree(subIterator);
1093
                    subIterator.detach();
1094
                } else {
1095
                    iterator.remove();
1096
                }
1097
            }
1098
        }
1099
 
1100
        function extractSubtree(iterator) {
1101
            for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1102
 
1103
                if (iterator.isPartiallySelectedSubtree()) {
1104
                    node = node.cloneNode(false);
1105
                    subIterator = iterator.getSubtreeIterator();
1106
                    node.appendChild(extractSubtree(subIterator));
1107
                    subIterator.detach();
1108
                } else {
1109
                    iterator.remove();
1110
                }
1111
                if (node.nodeType == 10) { // DocumentType
1112
                    throw new DOMException("HIERARCHY_REQUEST_ERR");
1113
                }
1114
                frag.appendChild(node);
1115
            }
1116
            return frag;
1117
        }
1118
 
1119
        function getNodesInRange(range, nodeTypes, filter) {
1120
            var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1121
            var filterExists = !!filter;
1122
            if (filterNodeTypes) {
1123
                regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1124
            }
1125
 
1126
            var nodes = [];
1127
            iterateSubtree(new RangeIterator(range, false), function(node) {
1128
                if (filterNodeTypes && !regex.test(node.nodeType)) {
1129
                    return;
1130
                }
1131
                if (filterExists && !filter(node)) {
1132
                    return;
1133
                }
1134
                // Don't include a boundary container if it is a character data node and the range does not contain any
1135
                // of its character data. See issue 190.
1136
                var sc = range.startContainer;
1137
                if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1138
                    return;
1139
                }
1140
 
1141
                var ec = range.endContainer;
1142
                if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1143
                    return;
1144
                }
1145
 
1146
                nodes.push(node);
1147
            });
1148
            return nodes;
1149
        }
1150
 
1151
        function inspect(range) {
1152
            var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1153
            return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1154
                    dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1155
        }
1156
 
1157
        /*----------------------------------------------------------------------------------------------------------------*/
1158
 
1159
        // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1160
 
1161
        function RangeIterator(range, clonePartiallySelectedTextNodes) {
1162
            this.range = range;
1163
            this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1164
 
1165
 
1166
            if (!range.collapsed) {
1167
                this.sc = range.startContainer;
1168
                this.so = range.startOffset;
1169
                this.ec = range.endContainer;
1170
                this.eo = range.endOffset;
1171
                var root = range.commonAncestorContainer;
1172
 
1173
                if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1174
                    this.isSingleCharacterDataNode = true;
1175
                    this._first = this._last = this._next = this.sc;
1176
                } else {
1177
                    this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1178
                        this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1179
                    this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1180
                        this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
1181
                }
1182
            }
1183
        }
1184
 
1185
        RangeIterator.prototype = {
1186
            _current: null,
1187
            _next: null,
1188
            _first: null,
1189
            _last: null,
1190
            isSingleCharacterDataNode: false,
1191
 
1192
            reset: function() {
1193
                this._current = null;
1194
                this._next = this._first;
1195
            },
1196
 
1197
            hasNext: function() {
1198
                return !!this._next;
1199
            },
1200
 
1201
            next: function() {
1202
                // Move to next node
1203
                var current = this._current = this._next;
1204
                if (current) {
1205
                    this._next = (current !== this._last) ? current.nextSibling : null;
1206
 
1207
                    // Check for partially selected text nodes
1208
                    if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1209
                        if (current === this.ec) {
1210
                            (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1211
                        }
1212
                        if (this._current === this.sc) {
1213
                            (current = current.cloneNode(true)).deleteData(0, this.so);
1214
                        }
1215
                    }
1216
                }
1217
 
1218
                return current;
1219
            },
1220
 
1221
            remove: function() {
1222
                var current = this._current, start, end;
1223
 
1224
                if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1225
                    start = (current === this.sc) ? this.so : 0;
1226
                    end = (current === this.ec) ? this.eo : current.length;
1227
                    if (start != end) {
1228
                        current.deleteData(start, end - start);
1229
                    }
1230
                } else {
1231
                    if (current.parentNode) {
1232
                        removeNode(current);
1233
                    } else {
1234
                    }
1235
                }
1236
            },
1237
 
1238
            // Checks if the current node is partially selected
1239
            isPartiallySelectedSubtree: function() {
1240
                var current = this._current;
1241
                return isNonTextPartiallySelected(current, this.range);
1242
            },
1243
 
1244
            getSubtreeIterator: function() {
1245
                var subRange;
1246
                if (this.isSingleCharacterDataNode) {
1247
                    subRange = this.range.cloneRange();
1248
                    subRange.collapse(false);
1249
                } else {
1250
                    subRange = new Range(getRangeDocument(this.range));
1251
                    var current = this._current;
1252
                    var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1253
 
1254
                    if (isOrIsAncestorOf(current, this.sc)) {
1255
                        startContainer = this.sc;
1256
                        startOffset = this.so;
1257
                    }
1258
                    if (isOrIsAncestorOf(current, this.ec)) {
1259
                        endContainer = this.ec;
1260
                        endOffset = this.eo;
1261
                    }
1262
 
1263
                    updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1264
                }
1265
                return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1266
            },
1267
 
1268
            detach: function() {
1269
                this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1270
            }
1271
        };
1272
 
1273
        /*----------------------------------------------------------------------------------------------------------------*/
1274
 
1275
        var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1276
        var rootContainerNodeTypes = [2, 9, 11];
1277
        var readonlyNodeTypes = [5, 6, 10, 12];
1278
        var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1279
        var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1280
 
1281
        function createAncestorFinder(nodeTypes) {
1282
            return function(node, selfIsAncestor) {
1283
                var t, n = selfIsAncestor ? node : node.parentNode;
1284
                while (n) {
1285
                    t = n.nodeType;
1286
                    if (arrayContains(nodeTypes, t)) {
1287
                        return n;
1288
                    }
1289
                    n = n.parentNode;
1290
                }
1291
                return null;
1292
            };
1293
        }
1294
 
1295
        var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1296
        var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1297
        var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1298
        var getElementAncestor = createAncestorFinder( [1] );
1299
 
1300
        function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1301
            if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1302
                throw new DOMException("INVALID_NODE_TYPE_ERR");
1303
            }
1304
        }
1305
 
1306
        function assertValidNodeType(node, invalidTypes) {
1307
            if (!arrayContains(invalidTypes, node.nodeType)) {
1308
                throw new DOMException("INVALID_NODE_TYPE_ERR");
1309
            }
1310
        }
1311
 
1312
        function assertValidOffset(node, offset) {
1313
            if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1314
                throw new DOMException("INDEX_SIZE_ERR");
1315
            }
1316
        }
1317
 
1318
        function assertSameDocumentOrFragment(node1, node2) {
1319
            if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1320
                throw new DOMException("WRONG_DOCUMENT_ERR");
1321
            }
1322
        }
1323
 
1324
        function assertNodeNotReadOnly(node) {
1325
            if (getReadonlyAncestor(node, true)) {
1326
                throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1327
            }
1328
        }
1329
 
1330
        function assertNode(node, codeName) {
1331
            if (!node) {
1332
                throw new DOMException(codeName);
1333
            }
1334
        }
1335
 
1336
        function isValidOffset(node, offset) {
1337
            return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1338
        }
1339
 
1340
        function isRangeValid(range) {
1341
            return (!!range.startContainer && !!range.endContainer &&
1342
                    !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
1343
                    getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
1344
                    isValidOffset(range.startContainer, range.startOffset) &&
1345
                    isValidOffset(range.endContainer, range.endOffset));
1346
        }
1347
 
1348
        function assertRangeValid(range) {
1349
            if (!isRangeValid(range)) {
1350
                throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
1351
            }
1352
        }
1353
 
1354
        /*----------------------------------------------------------------------------------------------------------------*/
1355
 
1356
        // Test the browser's innerHTML support to decide how to implement createContextualFragment
1357
        var styleEl = document.createElement("style");
1358
        var htmlParsingConforms = false;
1359
        try {
1360
            styleEl.innerHTML = "<b>x</b>";
1361
            htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Pre-Blink Opera incorrectly creates an element node
1362
        } catch (e) {
1363
            // IE 6 and 7 throw
1364
        }
1365
 
1366
        api.features.htmlParsingConforms = htmlParsingConforms;
1367
 
1368
        var createContextualFragment = htmlParsingConforms ?
1369
 
1370
            // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1371
            // discussion and base code for this implementation at issue 67.
1372
            // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1373
            // Thanks to Aleks Williams.
1374
            function(fragmentStr) {
1375
                // "Let node the context object's start's node."
1376
                var node = this.startContainer;
1377
                var doc = getDocument(node);
1378
 
1379
                // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1380
                // exception and abort these steps."
1381
                if (!node) {
1382
                    throw new DOMException("INVALID_STATE_ERR");
1383
                }
1384
 
1385
                // "Let element be as follows, depending on node's interface:"
1386
                // Document, Document Fragment: null
1387
                var el = null;
1388
 
1389
                // "Element: node"
1390
                if (node.nodeType == 1) {
1391
                    el = node;
1392
 
1393
                // "Text, Comment: node's parentElement"
1394
                } else if (isCharacterDataNode(node)) {
1395
                    el = dom.parentElement(node);
1396
                }
1397
 
1398
                // "If either element is null or element's ownerDocument is an HTML document
1399
                // and element's local name is "html" and element's namespace is the HTML
1400
                // namespace"
1401
                if (el === null || (
1402
                    el.nodeName == "HTML" &&
1403
                    dom.isHtmlNamespace(getDocument(el).documentElement) &&
1404
                    dom.isHtmlNamespace(el)
1405
                )) {
1406
 
1407
                // "let element be a new Element with "body" as its local name and the HTML
1408
                // namespace as its namespace.""
1409
                    el = doc.createElement("body");
1410
                } else {
1411
                    el = el.cloneNode(false);
1412
                }
1413
 
1414
                // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1415
                // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1416
                // "In either case, the algorithm must be invoked with fragment as the input
1417
                // and element as the context element."
1418
                el.innerHTML = fragmentStr;
1419
 
1420
                // "If this raises an exception, then abort these steps. Otherwise, let new
1421
                // children be the nodes returned."
1422
 
1423
                // "Let fragment be a new DocumentFragment."
1424
                // "Append all new children to fragment."
1425
                // "Return fragment."
1426
                return dom.fragmentFromNodeChildren(el);
1427
            } :
1428
 
1429
            // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1430
            // previous versions of Rangy used (with the exception of using a body element rather than a div)
1431
            function(fragmentStr) {
1432
                var doc = getRangeDocument(this);
1433
                var el = doc.createElement("body");
1434
                el.innerHTML = fragmentStr;
1435
 
1436
                return dom.fragmentFromNodeChildren(el);
1437
            };
1438
 
1439
        function splitRangeBoundaries(range, positionsToPreserve) {
1440
            assertRangeValid(range);
1441
 
1442
            var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1443
            var startEndSame = (sc === ec);
1444
 
1445
            if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1446
                splitDataNode(ec, eo, positionsToPreserve);
1447
            }
1448
 
1449
            if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1450
                sc = splitDataNode(sc, so, positionsToPreserve);
1451
                if (startEndSame) {
1452
                    eo -= so;
1453
                    ec = sc;
1454
                } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1455
                    eo++;
1456
                }
1457
                so = 0;
1458
            }
1459
            range.setStartAndEnd(sc, so, ec, eo);
1460
        }
1461
 
1462
        function rangeToHtml(range) {
1463
            assertRangeValid(range);
1464
            var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1465
            container.appendChild( range.cloneContents() );
1466
            return container.innerHTML;
1467
        }
1468
 
1469
        /*----------------------------------------------------------------------------------------------------------------*/
1470
 
1471
        var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1472
            "commonAncestorContainer"];
1473
 
1474
        var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1475
        var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1476
 
1477
        util.extend(api.rangePrototype, {
1478
            compareBoundaryPoints: function(how, range) {
1479
                assertRangeValid(this);
1480
                assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1481
 
1482
                var nodeA, offsetA, nodeB, offsetB;
1483
                var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1484
                var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1485
                nodeA = this[prefixA + "Container"];
1486
                offsetA = this[prefixA + "Offset"];
1487
                nodeB = range[prefixB + "Container"];
1488
                offsetB = range[prefixB + "Offset"];
1489
                return comparePoints(nodeA, offsetA, nodeB, offsetB);
1490
            },
1491
 
1492
            insertNode: function(node) {
1493
                assertRangeValid(this);
1494
                assertValidNodeType(node, insertableNodeTypes);
1495
                assertNodeNotReadOnly(this.startContainer);
1496
 
1497
                if (isOrIsAncestorOf(node, this.startContainer)) {
1498
                    throw new DOMException("HIERARCHY_REQUEST_ERR");
1499
                }
1500
 
1501
                // No check for whether the container of the start of the Range is of a type that does not allow
1502
                // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1503
                // to add the node
1504
 
1505
                var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1506
                this.setStartBefore(firstNodeInserted);
1507
            },
1508
 
1509
            cloneContents: function() {
1510
                assertRangeValid(this);
1511
 
1512
                var clone, frag;
1513
                if (this.collapsed) {
1514
                    return getRangeDocument(this).createDocumentFragment();
1515
                } else {
1516
                    if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
1517
                        clone = this.startContainer.cloneNode(true);
1518
                        clone.data = clone.data.slice(this.startOffset, this.endOffset);
1519
                        frag = getRangeDocument(this).createDocumentFragment();
1520
                        frag.appendChild(clone);
1521
                        return frag;
1522
                    } else {
1523
                        var iterator = new RangeIterator(this, true);
1524
                        clone = cloneSubtree(iterator);
1525
                        iterator.detach();
1526
                    }
1527
                    return clone;
1528
                }
1529
            },
1530
 
1531
            canSurroundContents: function() {
1532
                assertRangeValid(this);
1533
                assertNodeNotReadOnly(this.startContainer);
1534
                assertNodeNotReadOnly(this.endContainer);
1535
 
1536
                // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1537
                // no non-text nodes.
1538
                var iterator = new RangeIterator(this, true);
1539
                var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1540
                        (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1541
                iterator.detach();
1542
                return !boundariesInvalid;
1543
            },
1544
 
1545
            surroundContents: function(node) {
1546
                assertValidNodeType(node, surroundNodeTypes);
1547
 
1548
                if (!this.canSurroundContents()) {
1549
                    throw new DOMException("INVALID_STATE_ERR");
1550
                }
1551
 
1552
                // Extract the contents
1553
                var content = this.extractContents();
1554
 
1555
                // Clear the children of the node
1556
                if (node.hasChildNodes()) {
1557
                    while (node.lastChild) {
1558
                        node.removeChild(node.lastChild);
1559
                    }
1560
                }
1561
 
1562
                // Insert the new node and add the extracted contents
1563
                insertNodeAtPosition(node, this.startContainer, this.startOffset);
1564
                node.appendChild(content);
1565
 
1566
                this.selectNode(node);
1567
            },
1568
 
1569
            cloneRange: function() {
1570
                assertRangeValid(this);
1571
                var range = new Range(getRangeDocument(this));
1572
                var i = rangeProperties.length, prop;
1573
                while (i--) {
1574
                    prop = rangeProperties[i];
1575
                    range[prop] = this[prop];
1576
                }
1577
                return range;
1578
            },
1579
 
1580
            toString: function() {
1581
                assertRangeValid(this);
1582
                var sc = this.startContainer;
1583
                if (sc === this.endContainer && isCharacterDataNode(sc)) {
1584
                    return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1585
                } else {
1586
                    var textParts = [], iterator = new RangeIterator(this, true);
1587
                    iterateSubtree(iterator, function(node) {
1588
                        // Accept only text or CDATA nodes, not comments
1589
                        if (node.nodeType == 3 || node.nodeType == 4) {
1590
                            textParts.push(node.data);
1591
                        }
1592
                    });
1593
                    iterator.detach();
1594
                    return textParts.join("");
1595
                }
1596
            },
1597
 
1598
            // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1599
            // been removed from Mozilla.
1600
 
1601
            compareNode: function(node) {
1602
                assertRangeValid(this);
1603
 
1604
                var parent = node.parentNode;
1605
                var nodeIndex = getNodeIndex(node);
1606
 
1607
                if (!parent) {
1608
                    throw new DOMException("NOT_FOUND_ERR");
1609
                }
1610
 
1611
                var startComparison = this.comparePoint(parent, nodeIndex),
1612
                    endComparison = this.comparePoint(parent, nodeIndex + 1);
1613
 
1614
                if (startComparison < 0) { // Node starts before
1615
                    return (endComparison > 0) ? n_b_a : n_b;
1616
                } else {
1617
                    return (endComparison > 0) ? n_a : n_i;
1618
                }
1619
            },
1620
 
1621
            comparePoint: function(node, offset) {
1622
                assertRangeValid(this);
1623
                assertNode(node, "HIERARCHY_REQUEST_ERR");
1624
                assertSameDocumentOrFragment(node, this.startContainer);
1625
 
1626
                if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1627
                    return -1;
1628
                } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1629
                    return 1;
1630
                }
1631
                return 0;
1632
            },
1633
 
1634
            createContextualFragment: createContextualFragment,
1635
 
1636
            toHtml: function() {
1637
                return rangeToHtml(this);
1638
            },
1639
 
1640
            // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1641
            // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1642
            intersectsNode: function(node, touchingIsIntersecting) {
1643
                assertRangeValid(this);
1644
                if (getRootContainer(node) != getRangeRoot(this)) {
1645
                    return false;
1646
                }
1647
 
1648
                var parent = node.parentNode, offset = getNodeIndex(node);
1649
                if (!parent) {
1650
                    return true;
1651
                }
1652
 
1653
                var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1654
                    endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1655
 
1656
                return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1657
            },
1658
 
1659
            isPointInRange: function(node, offset) {
1660
                assertRangeValid(this);
1661
                assertNode(node, "HIERARCHY_REQUEST_ERR");
1662
                assertSameDocumentOrFragment(node, this.startContainer);
1663
 
1664
                return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1665
                       (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1666
            },
1667
 
1668
            // The methods below are non-standard and invented by me.
1669
 
1670
            // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1671
            intersectsRange: function(range) {
1672
                return rangesIntersect(this, range, false);
1673
            },
1674
 
1675
            // Sharing a boundary start-to-end or end-to-start does count as intersection.
1676
            intersectsOrTouchesRange: function(range) {
1677
                return rangesIntersect(this, range, true);
1678
            },
1679
 
1680
            intersection: function(range) {
1681
                if (this.intersectsRange(range)) {
1682
                    var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1683
                        endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1684
 
1685
                    var intersectionRange = this.cloneRange();
1686
                    if (startComparison == -1) {
1687
                        intersectionRange.setStart(range.startContainer, range.startOffset);
1688
                    }
1689
                    if (endComparison == 1) {
1690
                        intersectionRange.setEnd(range.endContainer, range.endOffset);
1691
                    }
1692
                    return intersectionRange;
1693
                }
1694
                return null;
1695
            },
1696
 
1697
            union: function(range) {
1698
                if (this.intersectsOrTouchesRange(range)) {
1699
                    var unionRange = this.cloneRange();
1700
                    if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1701
                        unionRange.setStart(range.startContainer, range.startOffset);
1702
                    }
1703
                    if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1704
                        unionRange.setEnd(range.endContainer, range.endOffset);
1705
                    }
1706
                    return unionRange;
1707
                } else {
1708
                    throw new DOMException("Ranges do not intersect");
1709
                }
1710
            },
1711
 
1712
            containsNode: function(node, allowPartial) {
1713
                if (allowPartial) {
1714
                    return this.intersectsNode(node, false);
1715
                } else {
1716
                    return this.compareNode(node) == n_i;
1717
                }
1718
            },
1719
 
1720
            containsNodeContents: function(node) {
1721
                return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1722
            },
1723
 
1724
            containsRange: function(range) {
1725
                var intersection = this.intersection(range);
1726
                return intersection !== null && range.equals(intersection);
1727
            },
1728
 
1729
            containsNodeText: function(node) {
1730
                var nodeRange = this.cloneRange();
1731
                nodeRange.selectNode(node);
1732
                var textNodes = nodeRange.getNodes([3]);
1733
                if (textNodes.length > 0) {
1734
                    nodeRange.setStart(textNodes[0], 0);
1735
                    var lastTextNode = textNodes.pop();
1736
                    nodeRange.setEnd(lastTextNode, lastTextNode.length);
1737
                    return this.containsRange(nodeRange);
1738
                } else {
1739
                    return this.containsNodeContents(node);
1740
                }
1741
            },
1742
 
1743
            getNodes: function(nodeTypes, filter) {
1744
                assertRangeValid(this);
1745
                return getNodesInRange(this, nodeTypes, filter);
1746
            },
1747
 
1748
            getDocument: function() {
1749
                return getRangeDocument(this);
1750
            },
1751
 
1752
            collapseBefore: function(node) {
1753
                this.setEndBefore(node);
1754
                this.collapse(false);
1755
            },
1756
 
1757
            collapseAfter: function(node) {
1758
                this.setStartAfter(node);
1759
                this.collapse(true);
1760
            },
1761
 
1762
            getBookmark: function(containerNode) {
1763
                var doc = getRangeDocument(this);
1764
                var preSelectionRange = api.createRange(doc);
1765
                containerNode = containerNode || dom.getBody(doc);
1766
                preSelectionRange.selectNodeContents(containerNode);
1767
                var range = this.intersection(preSelectionRange);
1768
                var start = 0, end = 0;
1769
                if (range) {
1770
                    preSelectionRange.setEnd(range.startContainer, range.startOffset);
1771
                    start = preSelectionRange.toString().length;
1772
                    end = start + range.toString().length;
1773
                }
1774
 
1775
                return {
1776
                    start: start,
1777
                    end: end,
1778
                    containerNode: containerNode
1779
                };
1780
            },
1781
 
1782
            moveToBookmark: function(bookmark) {
1783
                var containerNode = bookmark.containerNode;
1784
                var charIndex = 0;
1785
                this.setStart(containerNode, 0);
1786
                this.collapse(true);
1787
                var nodeStack = [containerNode], node, foundStart = false, stop = false;
1788
                var nextCharIndex, i, childNodes;
1789
 
1790
                while (!stop && (node = nodeStack.pop())) {
1791
                    if (node.nodeType == 3) {
1792
                        nextCharIndex = charIndex + node.length;
1793
                        if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
1794
                            this.setStart(node, bookmark.start - charIndex);
1795
                            foundStart = true;
1796
                        }
1797
                        if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1798
                            this.setEnd(node, bookmark.end - charIndex);
1799
                            stop = true;
1800
                        }
1801
                        charIndex = nextCharIndex;
1802
                    } else {
1803
                        childNodes = node.childNodes;
1804
                        i = childNodes.length;
1805
                        while (i--) {
1806
                            nodeStack.push(childNodes[i]);
1807
                        }
1808
                    }
1809
                }
1810
            },
1811
 
1812
            getName: function() {
1813
                return "DomRange";
1814
            },
1815
 
1816
            equals: function(range) {
1817
                return Range.rangesEqual(this, range);
1818
            },
1819
 
1820
            isValid: function() {
1821
                return isRangeValid(this);
1822
            },
1823
 
1824
            inspect: function() {
1825
                return inspect(this);
1826
            },
1827
 
1828
            detach: function() {
1829
                // In DOM4, detach() is now a no-op.
1830
            }
1831
        });
1832
 
1833
        function copyComparisonConstantsToObject(obj) {
1834
            obj.START_TO_START = s2s;
1835
            obj.START_TO_END = s2e;
1836
            obj.END_TO_END = e2e;
1837
            obj.END_TO_START = e2s;
1838
 
1839
            obj.NODE_BEFORE = n_b;
1840
            obj.NODE_AFTER = n_a;
1841
            obj.NODE_BEFORE_AND_AFTER = n_b_a;
1842
            obj.NODE_INSIDE = n_i;
1843
        }
1844
 
1845
        function copyComparisonConstants(constructor) {
1846
            copyComparisonConstantsToObject(constructor);
1847
            copyComparisonConstantsToObject(constructor.prototype);
1848
        }
1849
 
1850
        function createRangeContentRemover(remover, boundaryUpdater) {
1851
            return function() {
1852
                assertRangeValid(this);
1853
 
1854
                var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1855
 
1856
                var iterator = new RangeIterator(this, true);
1857
 
1858
                // Work out where to position the range after content removal
1859
                var node, boundary;
1860
                if (sc !== root) {
1861
                    node = getClosestAncestorIn(sc, root, true);
1862
                    boundary = getBoundaryAfterNode(node);
1863
                    sc = boundary.node;
1864
                    so = boundary.offset;
1865
                }
1866
 
1867
                // Check none of the range is read-only
1868
                iterateSubtree(iterator, assertNodeNotReadOnly);
1869
 
1870
                iterator.reset();
1871
 
1872
                // Remove the content
1873
                var returnValue = remover(iterator);
1874
                iterator.detach();
1875
 
1876
                // Move to the new position
1877
                boundaryUpdater(this, sc, so, sc, so);
1878
 
1879
                return returnValue;
1880
            };
1881
        }
1882
 
1883
        function createPrototypeRange(constructor, boundaryUpdater) {
1884
            function createBeforeAfterNodeSetter(isBefore, isStart) {
1885
                return function(node) {
1886
                    assertValidNodeType(node, beforeAfterNodeTypes);
1887
                    assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1888
 
1889
                    var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1890
                    (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1891
                };
1892
            }
1893
 
1894
            function setRangeStart(range, node, offset) {
1895
                var ec = range.endContainer, eo = range.endOffset;
1896
                if (node !== range.startContainer || offset !== range.startOffset) {
1897
                    // Check the root containers of the range and the new boundary, and also check whether the new boundary
1898
                    // is after the current end. In either case, collapse the range to the new position
1899
                    if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
1900
                        ec = node;
1901
                        eo = offset;
1902
                    }
1903
                    boundaryUpdater(range, node, offset, ec, eo);
1904
                }
1905
            }
1906
 
1907
            function setRangeEnd(range, node, offset) {
1908
                var sc = range.startContainer, so = range.startOffset;
1909
                if (node !== range.endContainer || offset !== range.endOffset) {
1910
                    // Check the root containers of the range and the new boundary, and also check whether the new boundary
1911
                    // is after the current end. In either case, collapse the range to the new position
1912
                    if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
1913
                        sc = node;
1914
                        so = offset;
1915
                    }
1916
                    boundaryUpdater(range, sc, so, node, offset);
1917
                }
1918
            }
1919
 
1920
            // Set up inheritance
1921
            var F = function() {};
1922
            F.prototype = api.rangePrototype;
1923
            constructor.prototype = new F();
1924
 
1925
            util.extend(constructor.prototype, {
1926
                setStart: function(node, offset) {
1927
                    assertNoDocTypeNotationEntityAncestor(node, true);
1928
                    assertValidOffset(node, offset);
1929
 
1930
                    setRangeStart(this, node, offset);
1931
                },
1932
 
1933
                setEnd: function(node, offset) {
1934
                    assertNoDocTypeNotationEntityAncestor(node, true);
1935
                    assertValidOffset(node, offset);
1936
 
1937
                    setRangeEnd(this, node, offset);
1938
                },
1939
 
1940
                /**
1941
                 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
1942
                 * - Two parameters (node, offset) creates a collapsed range at that position
1943
                 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
1944
                 *   startOffset and ending at endOffset
1945
                 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
1946
                 *   startNode and ending at endOffset in endNode
1947
                 */
1948
                setStartAndEnd: function() {
1949
                    var args = arguments;
1950
                    var sc = args[0], so = args[1], ec = sc, eo = so;
1951
 
1952
                    switch (args.length) {
1953
                        case 3:
1954
                            eo = args[2];
1955
                            break;
1956
                        case 4:
1957
                            ec = args[2];
1958
                            eo = args[3];
1959
                            break;
1960
                    }
1961
 
1962
                    assertNoDocTypeNotationEntityAncestor(sc, true);
1963
                    assertValidOffset(sc, so);
1964
 
1965
                    assertNoDocTypeNotationEntityAncestor(ec, true);
1966
                    assertValidOffset(ec, eo);
1967
 
1968
                    boundaryUpdater(this, sc, so, ec, eo);
1969
                },
1970
 
1971
                setBoundary: function(node, offset, isStart) {
1972
                    this["set" + (isStart ? "Start" : "End")](node, offset);
1973
                },
1974
 
1975
                setStartBefore: createBeforeAfterNodeSetter(true, true),
1976
                setStartAfter: createBeforeAfterNodeSetter(false, true),
1977
                setEndBefore: createBeforeAfterNodeSetter(true, false),
1978
                setEndAfter: createBeforeAfterNodeSetter(false, false),
1979
 
1980
                collapse: function(isStart) {
1981
                    assertRangeValid(this);
1982
                    if (isStart) {
1983
                        boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1984
                    } else {
1985
                        boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
1986
                    }
1987
                },
1988
 
1989
                selectNodeContents: function(node) {
1990
                    assertNoDocTypeNotationEntityAncestor(node, true);
1991
 
1992
                    boundaryUpdater(this, node, 0, node, getNodeLength(node));
1993
                },
1994
 
1995
                selectNode: function(node) {
1996
                    assertNoDocTypeNotationEntityAncestor(node, false);
1997
                    assertValidNodeType(node, beforeAfterNodeTypes);
1998
 
1999
                    var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
2000
                    boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
2001
                },
2002
 
2003
                extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
2004
 
2005
                deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
2006
 
2007
                canSurroundContents: function() {
2008
                    assertRangeValid(this);
2009
                    assertNodeNotReadOnly(this.startContainer);
2010
                    assertNodeNotReadOnly(this.endContainer);
2011
 
2012
                    // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2013
                    // no non-text nodes.
2014
                    var iterator = new RangeIterator(this, true);
2015
                    var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2016
                            (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2017
                    iterator.detach();
2018
                    return !boundariesInvalid;
2019
                },
2020
 
2021
                splitBoundaries: function() {
2022
                    splitRangeBoundaries(this);
2023
                },
2024
 
2025
                splitBoundariesPreservingPositions: function(positionsToPreserve) {
2026
                    splitRangeBoundaries(this, positionsToPreserve);
2027
                },
2028
 
2029
                normalizeBoundaries: function() {
2030
                    assertRangeValid(this);
2031
 
2032
                    var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2033
 
2034
                    var mergeForward = function(node) {
2035
                        var sibling = node.nextSibling;
2036
                        if (sibling && sibling.nodeType == node.nodeType) {
2037
                            ec = node;
2038
                            eo = node.length;
2039
                            node.appendData(sibling.data);
2040
                            removeNode(sibling);
2041
                        }
2042
                    };
2043
 
2044
                    var mergeBackward = function(node) {
2045
                        var sibling = node.previousSibling;
2046
                        if (sibling && sibling.nodeType == node.nodeType) {
2047
                            sc = node;
2048
                            var nodeLength = node.length;
2049
                            so = sibling.length;
2050
                            node.insertData(0, sibling.data);
2051
                            removeNode(sibling);
2052
                            if (sc == ec) {
2053
                                eo += so;
2054
                                ec = sc;
2055
                            } else if (ec == node.parentNode) {
2056
                                var nodeIndex = getNodeIndex(node);
2057
                                if (eo == nodeIndex) {
2058
                                    ec = node;
2059
                                    eo = nodeLength;
2060
                                } else if (eo > nodeIndex) {
2061
                                    eo--;
2062
                                }
2063
                            }
2064
                        }
2065
                    };
2066
 
2067
                    var normalizeStart = true;
2068
                    var sibling;
2069
 
2070
                    if (isCharacterDataNode(ec)) {
2071
                        if (eo == ec.length) {
2072
                            mergeForward(ec);
2073
                        } else if (eo == 0) {
2074
                            sibling = ec.previousSibling;
2075
                            if (sibling && sibling.nodeType == ec.nodeType) {
2076
                                eo = sibling.length;
2077
                                if (sc == ec) {
2078
                                    normalizeStart = false;
2079
                                }
2080
                                sibling.appendData(ec.data);
2081
                                removeNode(ec);
2082
                                ec = sibling;
2083
                            }
2084
                        }
2085
                    } else {
2086
                        if (eo > 0) {
2087
                            var endNode = ec.childNodes[eo - 1];
2088
                            if (endNode && isCharacterDataNode(endNode)) {
2089
                                mergeForward(endNode);
2090
                            }
2091
                        }
2092
                        normalizeStart = !this.collapsed;
2093
                    }
2094
 
2095
                    if (normalizeStart) {
2096
                        if (isCharacterDataNode(sc)) {
2097
                            if (so == 0) {
2098
                                mergeBackward(sc);
2099
                            } else if (so == sc.length) {
2100
                                sibling = sc.nextSibling;
2101
                                if (sibling && sibling.nodeType == sc.nodeType) {
2102
                                    if (ec == sibling) {
2103
                                        ec = sc;
2104
                                        eo += sc.length;
2105
                                    }
2106
                                    sc.appendData(sibling.data);
2107
                                    removeNode(sibling);
2108
                                }
2109
                            }
2110
                        } else {
2111
                            if (so < sc.childNodes.length) {
2112
                                var startNode = sc.childNodes[so];
2113
                                if (startNode && isCharacterDataNode(startNode)) {
2114
                                    mergeBackward(startNode);
2115
                                }
2116
                            }
2117
                        }
2118
                    } else {
2119
                        sc = ec;
2120
                        so = eo;
2121
                    }
2122
 
2123
                    boundaryUpdater(this, sc, so, ec, eo);
2124
                },
2125
 
2126
                collapseToPoint: function(node, offset) {
2127
                    assertNoDocTypeNotationEntityAncestor(node, true);
2128
                    assertValidOffset(node, offset);
2129
                    this.setStartAndEnd(node, offset);
2130
                },
2131
 
2132
                parentElement: function() {
2133
                    assertRangeValid(this);
2134
                    var parentNode = this.commonAncestorContainer;
2135
                    return parentNode ? getElementAncestor(this.commonAncestorContainer, true) : null;
2136
                }
2137
            });
2138
 
2139
            copyComparisonConstants(constructor);
2140
        }
2141
 
2142
        /*----------------------------------------------------------------------------------------------------------------*/
2143
 
2144
        // Updates commonAncestorContainer and collapsed after boundary change
2145
        function updateCollapsedAndCommonAncestor(range) {
2146
            range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2147
            range.commonAncestorContainer = range.collapsed ?
2148
                range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2149
        }
2150
 
2151
        function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2152
            range.startContainer = startContainer;
2153
            range.startOffset = startOffset;
2154
            range.endContainer = endContainer;
2155
            range.endOffset = endOffset;
2156
            range.document = dom.getDocument(startContainer);
2157
            updateCollapsedAndCommonAncestor(range);
2158
        }
2159
 
2160
        function Range(doc) {
2161
            updateBoundaries(this, doc, 0, doc, 0);
2162
        }
2163
 
2164
        createPrototypeRange(Range, updateBoundaries);
2165
 
2166
        util.extend(Range, {
2167
            rangeProperties: rangeProperties,
2168
            RangeIterator: RangeIterator,
2169
            copyComparisonConstants: copyComparisonConstants,
2170
            createPrototypeRange: createPrototypeRange,
2171
            inspect: inspect,
2172
            toHtml: rangeToHtml,
2173
            getRangeDocument: getRangeDocument,
2174
            rangesEqual: function(r1, r2) {
2175
                return r1.startContainer === r2.startContainer &&
2176
                    r1.startOffset === r2.startOffset &&
2177
                    r1.endContainer === r2.endContainer &&
2178
                    r1.endOffset === r2.endOffset;
2179
            }
2180
        });
2181
 
2182
        api.DomRange = Range;
2183
    });
2184
 
2185
    /*----------------------------------------------------------------------------------------------------------------*/
2186
 
2187
    // Wrappers for the browser's native DOM Range and/or TextRange implementation
2188
    api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2189
        var WrappedRange, WrappedTextRange;
2190
        var dom = api.dom;
2191
        var util = api.util;
2192
        var DomPosition = dom.DomPosition;
2193
        var DomRange = api.DomRange;
2194
        var getBody = dom.getBody;
2195
        var getContentDocument = dom.getContentDocument;
2196
        var isCharacterDataNode = dom.isCharacterDataNode;
2197
 
2198
 
2199
        /*----------------------------------------------------------------------------------------------------------------*/
2200
 
2201
        if (api.features.implementsDomRange) {
2202
            // This is a wrapper around the browser's native DOM Range. It has two aims:
2203
            // - Provide workarounds for specific browser bugs
2204
            // - provide convenient extensions, which are inherited from Rangy's DomRange
2205
 
2206
            (function() {
2207
                var rangeProto;
2208
                var rangeProperties = DomRange.rangeProperties;
2209
 
2210
                function updateRangeProperties(range) {
2211
                    var i = rangeProperties.length, prop;
2212
                    while (i--) {
2213
                        prop = rangeProperties[i];
2214
                        range[prop] = range.nativeRange[prop];
2215
                    }
2216
                    // Fix for broken collapsed property in IE 9.
2217
                    range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2218
                }
2219
 
2220
                function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2221
                    var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2222
                    var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2223
                    var nativeRangeDifferent = !range.equals(range.nativeRange);
2224
 
2225
                    // Always set both boundaries for the benefit of IE9 (see issue 35)
2226
                    if (startMoved || endMoved || nativeRangeDifferent) {
2227
                        range.setEnd(endContainer, endOffset);
2228
                        range.setStart(startContainer, startOffset);
2229
                    }
2230
                }
2231
 
2232
                var createBeforeAfterNodeSetter;
2233
 
2234
                WrappedRange = function(range) {
2235
                    if (!range) {
2236
                        throw module.createError("WrappedRange: Range must be specified");
2237
                    }
2238
                    this.nativeRange = range;
2239
                    updateRangeProperties(this);
2240
                };
2241
 
2242
                DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2243
 
2244
                rangeProto = WrappedRange.prototype;
2245
 
2246
                rangeProto.selectNode = function(node) {
2247
                    this.nativeRange.selectNode(node);
2248
                    updateRangeProperties(this);
2249
                };
2250
 
2251
                rangeProto.cloneContents = function() {
2252
                    return this.nativeRange.cloneContents();
2253
                };
2254
 
2255
                // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2256
                // insertNode() is never delegated to the native range.
2257
 
2258
                rangeProto.surroundContents = function(node) {
2259
                    this.nativeRange.surroundContents(node);
2260
                    updateRangeProperties(this);
2261
                };
2262
 
2263
                rangeProto.collapse = function(isStart) {
2264
                    this.nativeRange.collapse(isStart);
2265
                    updateRangeProperties(this);
2266
                };
2267
 
2268
                rangeProto.cloneRange = function() {
2269
                    return new WrappedRange(this.nativeRange.cloneRange());
2270
                };
2271
 
2272
                rangeProto.refresh = function() {
2273
                    updateRangeProperties(this);
2274
                };
2275
 
2276
                rangeProto.toString = function() {
2277
                    return this.nativeRange.toString();
2278
                };
2279
 
2280
                // Create test range and node for feature detection
2281
 
2282
                var testTextNode = document.createTextNode("test");
2283
                getBody(document).appendChild(testTextNode);
2284
                var range = document.createRange();
2285
 
2286
                /*--------------------------------------------------------------------------------------------------------*/
2287
 
2288
                // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2289
                // correct for it
2290
 
2291
                range.setStart(testTextNode, 0);
2292
                range.setEnd(testTextNode, 0);
2293
 
2294
                try {
2295
                    range.setStart(testTextNode, 1);
2296
 
2297
                    rangeProto.setStart = function(node, offset) {
2298
                        this.nativeRange.setStart(node, offset);
2299
                        updateRangeProperties(this);
2300
                    };
2301
 
2302
                    rangeProto.setEnd = function(node, offset) {
2303
                        this.nativeRange.setEnd(node, offset);
2304
                        updateRangeProperties(this);
2305
                    };
2306
 
2307
                    createBeforeAfterNodeSetter = function(name) {
2308
                        return function(node) {
2309
                            this.nativeRange[name](node);
2310
                            updateRangeProperties(this);
2311
                        };
2312
                    };
2313
 
2314
                } catch(ex) {
2315
 
2316
                    rangeProto.setStart = function(node, offset) {
2317
                        try {
2318
                            this.nativeRange.setStart(node, offset);
2319
                        } catch (ex) {
2320
                            this.nativeRange.setEnd(node, offset);
2321
                            this.nativeRange.setStart(node, offset);
2322
                        }
2323
                        updateRangeProperties(this);
2324
                    };
2325
 
2326
                    rangeProto.setEnd = function(node, offset) {
2327
                        try {
2328
                            this.nativeRange.setEnd(node, offset);
2329
                        } catch (ex) {
2330
                            this.nativeRange.setStart(node, offset);
2331
                            this.nativeRange.setEnd(node, offset);
2332
                        }
2333
                        updateRangeProperties(this);
2334
                    };
2335
 
2336
                    createBeforeAfterNodeSetter = function(name, oppositeName) {
2337
                        return function(node) {
2338
                            try {
2339
                                this.nativeRange[name](node);
2340
                            } catch (ex) {
2341
                                this.nativeRange[oppositeName](node);
2342
                                this.nativeRange[name](node);
2343
                            }
2344
                            updateRangeProperties(this);
2345
                        };
2346
                    };
2347
                }
2348
 
2349
                rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2350
                rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2351
                rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2352
                rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2353
 
2354
                /*--------------------------------------------------------------------------------------------------------*/
2355
 
2356
                // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2357
                // whether the native implementation can be trusted
2358
                rangeProto.selectNodeContents = function(node) {
2359
                    this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2360
                };
2361
 
2362
                /*--------------------------------------------------------------------------------------------------------*/
2363
 
2364
                // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2365
                // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2366
 
2367
                range.selectNodeContents(testTextNode);
2368
                range.setEnd(testTextNode, 3);
2369
 
2370
                var range2 = document.createRange();
2371
                range2.selectNodeContents(testTextNode);
2372
                range2.setEnd(testTextNode, 4);
2373
                range2.setStart(testTextNode, 2);
2374
 
2375
                if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2376
                        range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2377
                    // This is the wrong way round, so correct for it
2378
 
2379
                    rangeProto.compareBoundaryPoints = function(type, range) {
2380
                        range = range.nativeRange || range;
2381
                        if (type == range.START_TO_END) {
2382
                            type = range.END_TO_START;
2383
                        } else if (type == range.END_TO_START) {
2384
                            type = range.START_TO_END;
2385
                        }
2386
                        return this.nativeRange.compareBoundaryPoints(type, range);
2387
                    };
2388
                } else {
2389
                    rangeProto.compareBoundaryPoints = function(type, range) {
2390
                        return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2391
                    };
2392
                }
2393
 
2394
                /*--------------------------------------------------------------------------------------------------------*/
2395
 
2396
                // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
2397
 
2398
                var el = document.createElement("div");
2399
                el.innerHTML = "123";
2400
                var textNode = el.firstChild;
2401
                var body = getBody(document);
2402
                body.appendChild(el);
2403
 
2404
                range.setStart(textNode, 1);
2405
                range.setEnd(textNode, 2);
2406
                range.deleteContents();
2407
 
2408
                if (textNode.data == "13") {
2409
                    // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2410
                    // extractContents()
2411
                    rangeProto.deleteContents = function() {
2412
                        this.nativeRange.deleteContents();
2413
                        updateRangeProperties(this);
2414
                    };
2415
 
2416
                    rangeProto.extractContents = function() {
2417
                        var frag = this.nativeRange.extractContents();
2418
                        updateRangeProperties(this);
2419
                        return frag;
2420
                    };
2421
                } else {
2422
                }
2423
 
2424
                body.removeChild(el);
2425
                body = null;
2426
 
2427
                /*--------------------------------------------------------------------------------------------------------*/
2428
 
2429
                // Test for existence of createContextualFragment and delegate to it if it exists
2430
                if (util.isHostMethod(range, "createContextualFragment")) {
2431
                    rangeProto.createContextualFragment = function(fragmentStr) {
2432
                        return this.nativeRange.createContextualFragment(fragmentStr);
2433
                    };
2434
                }
2435
 
2436
                /*--------------------------------------------------------------------------------------------------------*/
2437
 
2438
                // Clean up
2439
                getBody(document).removeChild(testTextNode);
2440
 
2441
                rangeProto.getName = function() {
2442
                    return "WrappedRange";
2443
                };
2444
 
2445
                api.WrappedRange = WrappedRange;
2446
 
2447
                api.createNativeRange = function(doc) {
2448
                    doc = getContentDocument(doc, module, "createNativeRange");
2449
                    return doc.createRange();
2450
                };
2451
            })();
2452
        }
2453
 
2454
        if (api.features.implementsTextRange) {
2455
            /*
2456
            This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2457
            method. For example, in the following (where pipes denote the selection boundaries):
2458
 
2459
            <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2460
 
2461
            var range = document.selection.createRange();
2462
            alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2463
 
2464
            This method returns the common ancestor node of the following:
2465
            - the parentElement() of the textRange
2466
            - the parentElement() of the textRange after calling collapse(true)
2467
            - the parentElement() of the textRange after calling collapse(false)
2468
            */
2469
            var getTextRangeContainerElement = function(textRange) {
2470
                var parentEl = textRange.parentElement();
2471
                var range = textRange.duplicate();
2472
                range.collapse(true);
2473
                var startEl = range.parentElement();
2474
                range = textRange.duplicate();
2475
                range.collapse(false);
2476
                var endEl = range.parentElement();
2477
                var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2478
 
2479
                return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2480
            };
2481
 
2482
            var textRangeIsCollapsed = function(textRange) {
2483
                return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2484
            };
2485
 
2486
            // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2487
            // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2488
            // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2489
            // bugs, handling for inputs and images, plus optimizations.
2490
            var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2491
                var workingRange = textRange.duplicate();
2492
                workingRange.collapse(isStart);
2493
                var containerElement = workingRange.parentElement();
2494
 
2495
                // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2496
                // check for that
2497
                if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2498
                    containerElement = wholeRangeContainerElement;
2499
                }
2500
 
2501
 
2502
                // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2503
                // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2504
                if (!containerElement.canHaveHTML) {
2505
                    var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2506
                    return {
2507
                        boundaryPosition: pos,
2508
                        nodeInfo: {
2509
                            nodeIndex: pos.offset,
2510
                            containerElement: pos.node
2511
                        }
2512
                    };
2513
                }
2514
 
2515
                var workingNode = dom.getDocument(containerElement).createElement("span");
2516
 
2517
                // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2518
                // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2519
                if (workingNode.parentNode) {
2520
                    dom.removeNode(workingNode);
2521
                }
2522
 
2523
                var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
2524
                var previousNode, nextNode, boundaryPosition, boundaryNode;
2525
                var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
2526
                var childNodeCount = containerElement.childNodes.length;
2527
                var end = childNodeCount;
2528
 
2529
                // Check end first. Code within the loop assumes that the endth child node of the container is definitely
2530
                // after the range boundary.
2531
                var nodeIndex = end;
2532
 
2533
                while (true) {
2534
                    if (nodeIndex == childNodeCount) {
2535
                        containerElement.appendChild(workingNode);
2536
                    } else {
2537
                        containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2538
                    }
2539
                    workingRange.moveToElementText(workingNode);
2540
                    comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2541
                    if (comparison == 0 || start == end) {
2542
                        break;
2543
                    } else if (comparison == -1) {
2544
                        if (end == start + 1) {
2545
                            // We know the endth child node is after the range boundary, so we must be done.
2546
                            break;
2547
                        } else {
2548
                            start = nodeIndex;
2549
                        }
2550
                    } else {
2551
                        end = (end == start + 1) ? start : nodeIndex;
2552
                    }
2553
                    nodeIndex = Math.floor((start + end) / 2);
2554
                    containerElement.removeChild(workingNode);
2555
                }
2556
 
2557
 
2558
                // We've now reached or gone past the boundary of the text range we're interested in
2559
                // so have identified the node we want
2560
                boundaryNode = workingNode.nextSibling;
2561
 
2562
                if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
2563
                    // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
2564
                    // the node containing the text range's boundary, so we move the end of the working range to the
2565
                    // boundary point and measure the length of its text to get the boundary's offset within the node.
2566
                    workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
2567
 
2568
                    var offset;
2569
 
2570
                    if (/[\r\n]/.test(boundaryNode.data)) {
2571
                        /*
2572
                        For the particular case of a boundary within a text node containing rendered line breaks (within a
2573
                        <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2574
                        IE. The facts:
2575
 
2576
                        - Each line break is represented as \r in the text node's data/nodeValue properties
2577
                        - Each line break is represented as \r\n in the TextRange's 'text' property
2578
                        - The 'text' property of the TextRange does not contain trailing line breaks
2579
 
2580
                        To get round the problem presented by the final fact above, we can use the fact that TextRange's
2581
                        moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2582
                        necessarily the same as the number of characters it was instructed to move. The simplest approach is
2583
                        to use this to store the characters moved when moving both the start and end of the range to the
2584
                        start of the document body and subtracting the start offset from the end offset (the
2585
                        "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2586
                        the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2587
                        the end of the document) has the same problem.
2588
 
2589
                        Another approach that works is to use moveStart() to move the start boundary of the range up to the
2590
                        end boundary one character at a time and incrementing a counter with the value returned by the
2591
                        moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2592
                        expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2593
                        by the location of the range within the document).
2594
 
2595
                        The approach used below is a hybrid of the two methods above. It uses the fact that a string
2596
                        containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2597
                        be longer than the text of the TextRange, so the start of the range is moved that length initially
2598
                        and then a character at a time to make up for any trailing line breaks not contained in the 'text'
2599
                        property. This has good performance in most situations compared to the previous two methods.
2600
                        */
2601
                        var tempRange = workingRange.duplicate();
2602
                        var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2603
 
2604
                        offset = tempRange.moveStart("character", rangeLength);
2605
                        while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2606
                            offset++;
2607
                            tempRange.moveStart("character", 1);
2608
                        }
2609
                    } else {
2610
                        offset = workingRange.text.length;
2611
                    }
2612
                    boundaryPosition = new DomPosition(boundaryNode, offset);
2613
                } else {
2614
 
2615
                    // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2616
                    // a position within that, and likewise for a start boundary preceding a character data node
2617
                    previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2618
                    nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2619
                    if (nextNode && isCharacterDataNode(nextNode)) {
2620
                        boundaryPosition = new DomPosition(nextNode, 0);
2621
                    } else if (previousNode && isCharacterDataNode(previousNode)) {
2622
                        boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
2623
                    } else {
2624
                        boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2625
                    }
2626
                }
2627
 
2628
                // Clean up
2629
                dom.removeNode(workingNode);
2630
 
2631
                return {
2632
                    boundaryPosition: boundaryPosition,
2633
                    nodeInfo: {
2634
                        nodeIndex: nodeIndex,
2635
                        containerElement: containerElement
2636
                    }
2637
                };
2638
            };
2639
 
2640
            // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
2641
            // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2642
            // (http://code.google.com/p/ierange/)
2643
            var createBoundaryTextRange = function(boundaryPosition, isStart) {
2644
                var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2645
                var doc = dom.getDocument(boundaryPosition.node);
2646
                var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
2647
                var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
2648
 
2649
                if (nodeIsDataNode) {
2650
                    boundaryNode = boundaryPosition.node;
2651
                    boundaryParent = boundaryNode.parentNode;
2652
                } else {
2653
                    childNodes = boundaryPosition.node.childNodes;
2654
                    boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2655
                    boundaryParent = boundaryPosition.node;
2656
                }
2657
 
2658
                // Position the range immediately before the node containing the boundary
2659
                workingNode = doc.createElement("span");
2660
 
2661
                // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
2662
                // the element rather than immediately before or after it
2663
                workingNode.innerHTML = "&#feff;";
2664
 
2665
                // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2666
                // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2667
                if (boundaryNode) {
2668
                    boundaryParent.insertBefore(workingNode, boundaryNode);
2669
                } else {
2670
                    boundaryParent.appendChild(workingNode);
2671
                }
2672
 
2673
                workingRange.moveToElementText(workingNode);
2674
                workingRange.collapse(!isStart);
2675
 
2676
                // Clean up
2677
                boundaryParent.removeChild(workingNode);
2678
 
2679
                // Move the working range to the text offset, if required
2680
                if (nodeIsDataNode) {
2681
                    workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2682
                }
2683
 
2684
                return workingRange;
2685
            };
2686
 
2687
            /*------------------------------------------------------------------------------------------------------------*/
2688
 
2689
            // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2690
            // prototype
2691
 
2692
            WrappedTextRange = function(textRange) {
2693
                this.textRange = textRange;
2694
                this.refresh();
2695
            };
2696
 
2697
            WrappedTextRange.prototype = new DomRange(document);
2698
 
2699
            WrappedTextRange.prototype.refresh = function() {
2700
                var start, end, startBoundary;
2701
 
2702
                // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2703
                var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2704
 
2705
                if (textRangeIsCollapsed(this.textRange)) {
2706
                    end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
2707
                        true).boundaryPosition;
2708
                } else {
2709
                    startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2710
                    start = startBoundary.boundaryPosition;
2711
 
2712
                    // An optimization used here is that if the start and end boundaries have the same parent element, the
2713
                    // search scope for the end boundary can be limited to exclude the portion of the element that precedes
2714
                    // the start boundary
2715
                    end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
2716
                        startBoundary.nodeInfo).boundaryPosition;
2717
                }
2718
 
2719
                this.setStart(start.node, start.offset);
2720
                this.setEnd(end.node, end.offset);
2721
            };
2722
 
2723
            WrappedTextRange.prototype.getName = function() {
2724
                return "WrappedTextRange";
2725
            };
2726
 
2727
            DomRange.copyComparisonConstants(WrappedTextRange);
2728
 
2729
            var rangeToTextRange = function(range) {
2730
                if (range.collapsed) {
2731
                    return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2732
                } else {
2733
                    var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2734
                    var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
2735
                    var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
2736
                    textRange.setEndPoint("StartToStart", startRange);
2737
                    textRange.setEndPoint("EndToEnd", endRange);
2738
                    return textRange;
2739
                }
2740
            };
2741
 
2742
            WrappedTextRange.rangeToTextRange = rangeToTextRange;
2743
 
2744
            WrappedTextRange.prototype.toTextRange = function() {
2745
                return rangeToTextRange(this);
2746
            };
2747
 
2748
            api.WrappedTextRange = WrappedTextRange;
2749
 
2750
            // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
2751
            // implementation to use by default.
2752
            if (!api.features.implementsDomRange || api.config.preferTextRange) {
2753
                // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
2754
                var globalObj = (function(f) { return f("return this;")(); })(Function);
2755
                if (typeof globalObj.Range == "undefined") {
2756
                    globalObj.Range = WrappedTextRange;
2757
                }
2758
 
2759
                api.createNativeRange = function(doc) {
2760
                    doc = getContentDocument(doc, module, "createNativeRange");
2761
                    return getBody(doc).createTextRange();
2762
                };
2763
 
2764
                api.WrappedRange = WrappedTextRange;
2765
            }
2766
        }
2767
 
2768
        api.createRange = function(doc) {
2769
            doc = getContentDocument(doc, module, "createRange");
2770
            return new api.WrappedRange(api.createNativeRange(doc));
2771
        };
2772
 
2773
        api.createRangyRange = function(doc) {
2774
            doc = getContentDocument(doc, module, "createRangyRange");
2775
            return new DomRange(doc);
2776
        };
2777
 
2778
        util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
2779
        util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
2780
 
2781
        api.addShimListener(function(win) {
2782
            var doc = win.document;
2783
            if (typeof doc.createRange == "undefined") {
2784
                doc.createRange = function() {
2785
                    return api.createRange(doc);
2786
                };
2787
            }
2788
            doc = win = null;
2789
        });
2790
    });
2791
 
2792
    /*----------------------------------------------------------------------------------------------------------------*/
2793
 
2794
    // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
2795
    // in the W3C Selection API spec (https://www.w3.org/TR/selection-api)
2796
    api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
2797
        api.config.checkSelectionRanges = true;
2798
 
2799
        var BOOLEAN = "boolean";
2800
        var NUMBER = "number";
2801
        var dom = api.dom;
2802
        var util = api.util;
2803
        var isHostMethod = util.isHostMethod;
2804
        var DomRange = api.DomRange;
2805
        var WrappedRange = api.WrappedRange;
2806
        var DOMException = api.DOMException;
2807
        var DomPosition = dom.DomPosition;
2808
        var getNativeSelection;
2809
        var selectionIsCollapsed;
2810
        var features = api.features;
2811
        var CONTROL = "Control";
2812
        var getDocument = dom.getDocument;
2813
        var getBody = dom.getBody;
2814
        var rangesEqual = DomRange.rangesEqual;
2815
 
2816
 
2817
        // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
2818
        // "forward" or "forwards") or a Boolean (true for backwards).
2819
        function isDirectionBackward(dir) {
2820
            return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
2821
        }
2822
 
2823
        function getWindow(win, methodName) {
2824
            if (!win) {
2825
                return window;
2826
            } else if (dom.isWindow(win)) {
2827
                return win;
2828
            } else if (win instanceof WrappedSelection) {
2829
                return win.win;
2830
            } else {
2831
                var doc = dom.getContentDocument(win, module, methodName);
2832
                return dom.getWindow(doc);
2833
            }
2834
        }
2835
 
2836
        function getWinSelection(winParam) {
2837
            return getWindow(winParam, "getWinSelection").getSelection();
2838
        }
2839
 
2840
        function getDocSelection(winParam) {
2841
            return getWindow(winParam, "getDocSelection").document.selection;
2842
        }
2843
 
2844
        function winSelectionIsBackward(sel) {
2845
            var backward = false;
2846
            if (sel.anchorNode) {
2847
                backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
2848
            }
2849
            return backward;
2850
        }
2851
 
2852
        // Test for the Range/TextRange and Selection features required
2853
        // Test for ability to retrieve selection
2854
        var implementsWinGetSelection = isHostMethod(window, "getSelection"),
2855
            implementsDocSelection = util.isHostObject(document, "selection");
2856
 
2857
        features.implementsWinGetSelection = implementsWinGetSelection;
2858
        features.implementsDocSelection = implementsDocSelection;
2859
 
2860
        var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2861
 
2862
        if (useDocumentSelection) {
2863
            getNativeSelection = getDocSelection;
2864
            api.isSelectionValid = function(winParam) {
2865
                var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
2866
 
2867
                // Check whether the selection TextRange is actually contained within the correct document
2868
                return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
2869
            };
2870
        } else if (implementsWinGetSelection) {
2871
            getNativeSelection = getWinSelection;
2872
            api.isSelectionValid = function() {
2873
                return true;
2874
            };
2875
        } else {
2876
            module.fail("Neither document.selection or window.getSelection() detected.");
2877
            return false;
2878
        }
2879
 
2880
        api.getNativeSelection = getNativeSelection;
2881
 
2882
        var testSelection = getNativeSelection();
2883
 
2884
        // In Firefox, the selection is null in an iframe with display: none. See issue #138.
2885
        if (!testSelection) {
2886
            module.fail("Native selection was null (possibly issue 138?)");
2887
            return false;
2888
        }
2889
 
2890
        var testRange = api.createNativeRange(document);
2891
        var body = getBody(document);
2892
 
2893
        // Obtaining a range from a selection
2894
        var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
2895
            ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
2896
 
2897
        features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2898
 
2899
        // Test for existence of native selection extend() method
2900
        var selectionHasExtend = isHostMethod(testSelection, "extend");
2901
        features.selectionHasExtend = selectionHasExtend;
2902
 
2903
        // Test for existence of native selection setBaseAndExtent() method
2904
        var selectionHasSetBaseAndExtent = isHostMethod(testSelection, "setBaseAndExtent");
2905
        features.selectionHasSetBaseAndExtent = selectionHasSetBaseAndExtent;
2906
 
2907
        // Test if rangeCount exists
2908
        var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
2909
        features.selectionHasRangeCount = selectionHasRangeCount;
2910
 
2911
        var selectionSupportsMultipleRanges = false;
2912
        var collapsedNonEditableSelectionsSupported = true;
2913
 
2914
        var addRangeBackwardToNative = selectionHasExtend ?
2915
            function(nativeSelection, range) {
2916
                var doc = DomRange.getRangeDocument(range);
2917
                var endRange = api.createRange(doc);
2918
                endRange.collapseToPoint(range.endContainer, range.endOffset);
2919
                nativeSelection.addRange(getNativeRange(endRange));
2920
                nativeSelection.extend(range.startContainer, range.startOffset);
2921
            } : null;
2922
 
2923
        if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2924
                typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
2925
 
2926
            (function() {
2927
                // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
2928
                // performed on the current document's selection. See issue 109.
2929
 
2930
                // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
2931
                // will result in the selection direction being reversed if the original selection was backwards and the
2932
                // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
2933
                var sel = window.getSelection();
2934
                if (sel) {
2935
                    // Store the current selection
2936
                    var originalSelectionRangeCount = sel.rangeCount;
2937
                    var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
2938
                    var originalSelectionRanges = [];
2939
                    var originalSelectionBackward = winSelectionIsBackward(sel);
2940
                    for (var i = 0; i < originalSelectionRangeCount; ++i) {
2941
                        originalSelectionRanges[i] = sel.getRangeAt(i);
2942
                    }
2943
 
2944
                    // Create some test elements
2945
                    var testEl = dom.createTestElement(document, "", false);
2946
                    var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
2947
 
2948
                    // Test whether the native selection will allow a collapsed selection within a non-editable element
2949
                    var r1 = document.createRange();
2950
 
2951
                    r1.setStart(textNode, 1);
2952
                    r1.collapse(true);
2953
                    sel.removeAllRanges();
2954
                    sel.addRange(r1);
2955
                    collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2956
                    sel.removeAllRanges();
2957
 
2958
                    // Test whether the native selection is capable of supporting multiple ranges.
2959
                    if (!selectionHasMultipleRanges) {
2960
                        // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
2961
                        // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
2962
                        // nothing we can do about this while retaining the feature test so we have to resort to a browser
2963
                        // sniff. I'm not happy about it. See
2964
                        // https://code.google.com/p/chromium/issues/detail?id=399791
2965
                        var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
2966
                        if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
2967
                            selectionSupportsMultipleRanges = false;
2968
                        } else {
2969
                            var r2 = r1.cloneRange();
2970
                            r1.setStart(textNode, 0);
2971
                            r2.setEnd(textNode, 3);
2972
                            r2.setStart(textNode, 2);
2973
                            sel.addRange(r1);
2974
                            sel.addRange(r2);
2975
                            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2976
                        }
2977
                    }
2978
 
2979
                    // Clean up
2980
                    dom.removeNode(testEl);
2981
                    sel.removeAllRanges();
2982
 
2983
                    for (i = 0; i < originalSelectionRangeCount; ++i) {
2984
                        if (i == 0 && originalSelectionBackward) {
2985
                            if (addRangeBackwardToNative) {
2986
                                addRangeBackwardToNative(sel, originalSelectionRanges[i]);
2987
                            } else {
2988
                                api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
2989
                                sel.addRange(originalSelectionRanges[i]);
2990
                            }
2991
                        } else {
2992
                            sel.addRange(originalSelectionRanges[i]);
2993
                        }
2994
                    }
2995
                }
2996
            })();
2997
        }
2998
 
2999
        features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
3000
        features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
3001
 
3002
        // ControlRanges
3003
        var implementsControlRange = false, testControlRange;
3004
 
3005
        if (body && isHostMethod(body, "createControlRange")) {
3006
            testControlRange = body.createControlRange();
3007
            if (util.areHostProperties(testControlRange, ["item", "add"])) {
3008
                implementsControlRange = true;
3009
            }
3010
        }
3011
        features.implementsControlRange = implementsControlRange;
3012
 
3013
        // Selection collapsedness
3014
        if (selectionHasAnchorAndFocus) {
3015
            selectionIsCollapsed = function(sel) {
3016
                return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
3017
            };
3018
        } else {
3019
            selectionIsCollapsed = function(sel) {
3020
                return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
3021
            };
3022
        }
3023
 
3024
        function updateAnchorAndFocusFromRange(sel, range, backward) {
3025
            var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
3026
            sel.anchorNode = range[anchorPrefix + "Container"];
3027
            sel.anchorOffset = range[anchorPrefix + "Offset"];
3028
            sel.focusNode = range[focusPrefix + "Container"];
3029
            sel.focusOffset = range[focusPrefix + "Offset"];
3030
        }
3031
 
3032
        function updateAnchorAndFocusFromNativeSelection(sel) {
3033
            var nativeSel = sel.nativeSelection;
3034
            sel.anchorNode = nativeSel.anchorNode;
3035
            sel.anchorOffset = nativeSel.anchorOffset;
3036
            sel.focusNode = nativeSel.focusNode;
3037
            sel.focusOffset = nativeSel.focusOffset;
3038
        }
3039
 
3040
        function updateEmptySelection(sel) {
3041
            sel.anchorNode = sel.focusNode = null;
3042
            sel.anchorOffset = sel.focusOffset = 0;
3043
            sel.rangeCount = 0;
3044
            sel.isCollapsed = true;
3045
            sel._ranges.length = 0;
3046
            updateType(sel);
3047
        }
3048
 
3049
        function updateType(sel) {
3050
            sel.type = (sel.rangeCount == 0) ? "None" : (selectionIsCollapsed(sel) ? "Caret" : "Range");
3051
        }
3052
 
3053
        function getNativeRange(range) {
3054
            var nativeRange;
3055
            if (range instanceof DomRange) {
3056
                nativeRange = api.createNativeRange(range.getDocument());
3057
                nativeRange.setEnd(range.endContainer, range.endOffset);
3058
                nativeRange.setStart(range.startContainer, range.startOffset);
3059
            } else if (range instanceof WrappedRange) {
3060
                nativeRange = range.nativeRange;
3061
            } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
3062
                nativeRange = range;
3063
            }
3064
            return nativeRange;
3065
        }
3066
 
3067
        function rangeContainsSingleElement(rangeNodes) {
3068
            if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
3069
                return false;
3070
            }
3071
            for (var i = 1, len = rangeNodes.length; i < len; ++i) {
3072
                if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
3073
                    return false;
3074
                }
3075
            }
3076
            return true;
3077
        }
3078
 
3079
        function getSingleElementFromRange(range) {
3080
            var nodes = range.getNodes();
3081
            if (!rangeContainsSingleElement(nodes)) {
3082
                throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
3083
            }
3084
            return nodes[0];
3085
        }
3086
 
3087
        // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
3088
        function isTextRange(range) {
3089
            return !!range && typeof range.text != "undefined";
3090
        }
3091
 
3092
        function updateFromTextRange(sel, range) {
3093
            // Create a Range from the selected TextRange
3094
            var wrappedRange = new WrappedRange(range);
3095
            sel._ranges = [wrappedRange];
3096
 
3097
            updateAnchorAndFocusFromRange(sel, wrappedRange, false);
3098
            sel.rangeCount = 1;
3099
            sel.isCollapsed = wrappedRange.collapsed;
3100
            updateType(sel);
3101
        }
3102
 
3103
        function updateControlSelection(sel) {
3104
            // Update the wrapped selection based on what's now in the native selection
3105
            sel._ranges.length = 0;
3106
            if (sel.docSelection.type == "None") {
3107
                updateEmptySelection(sel);
3108
            } else {
3109
                var controlRange = sel.docSelection.createRange();
3110
                if (isTextRange(controlRange)) {
3111
                    // This case (where the selection type is "Control" and calling createRange() on the selection returns
3112
                    // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
3113
                    // ControlRange have been removed from the ControlRange and removed from the document.
3114
                    updateFromTextRange(sel, controlRange);
3115
                } else {
3116
                    sel.rangeCount = controlRange.length;
3117
                    var range, doc = getDocument(controlRange.item(0));
3118
                    for (var i = 0; i < sel.rangeCount; ++i) {
3119
                        range = api.createRange(doc);
3120
                        range.selectNode(controlRange.item(i));
3121
                        sel._ranges.push(range);
3122
                    }
3123
                    sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
3124
                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
3125
                    updateType(sel);
3126
                }
3127
            }
3128
        }
3129
 
3130
        function addRangeToControlSelection(sel, range) {
3131
            var controlRange = sel.docSelection.createRange();
3132
            var rangeElement = getSingleElementFromRange(range);
3133
 
3134
            // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
3135
            // contained by the supplied range
3136
            var doc = getDocument(controlRange.item(0));
3137
            var newControlRange = getBody(doc).createControlRange();
3138
            for (var i = 0, len = controlRange.length; i < len; ++i) {
3139
                newControlRange.add(controlRange.item(i));
3140
            }
3141
            try {
3142
                newControlRange.add(rangeElement);
3143
            } catch (ex) {
3144
                throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
3145
            }
3146
            newControlRange.select();
3147
 
3148
            // Update the wrapped selection based on what's now in the native selection
3149
            updateControlSelection(sel);
3150
        }
3151
 
3152
        var getSelectionRangeAt;
3153
 
3154
        if (isHostMethod(testSelection, "getRangeAt")) {
3155
            // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
3156
            // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
3157
            // lesson to us all, especially me.
3158
            getSelectionRangeAt = function(sel, index) {
3159
                try {
3160
                    return sel.getRangeAt(index);
3161
                } catch (ex) {
3162
                    return null;
3163
                }
3164
            };
3165
        } else if (selectionHasAnchorAndFocus) {
3166
            getSelectionRangeAt = function(sel) {
3167
                var doc = getDocument(sel.anchorNode);
3168
                var range = api.createRange(doc);
3169
                range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
3170
 
3171
                // Handle the case when the selection was selected backwards (from the end to the start in the
3172
                // document)
3173
                if (range.collapsed !== this.isCollapsed) {
3174
                    range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
3175
                }
3176
 
3177
                return range;
3178
            };
3179
        }
3180
 
3181
        function WrappedSelection(selection, docSelection, win) {
3182
            this.nativeSelection = selection;
3183
            this.docSelection = docSelection;
3184
            this._ranges = [];
3185
            this.win = win;
3186
            this.refresh();
3187
        }
3188
 
3189
        WrappedSelection.prototype = api.selectionPrototype;
3190
 
3191
        function deleteProperties(sel) {
3192
            sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
3193
            sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
3194
            sel.detached = true;
3195
            updateType(sel);
3196
        }
3197
 
3198
        var cachedRangySelections = [];
3199
 
3200
        function actOnCachedSelection(win, action) {
3201
            var i = cachedRangySelections.length, cached, sel;
3202
            while (i--) {
3203
                cached = cachedRangySelections[i];
3204
                sel = cached.selection;
3205
                if (action == "deleteAll") {
3206
                    deleteProperties(sel);
3207
                } else if (cached.win == win) {
3208
                    if (action == "delete") {
3209
                        cachedRangySelections.splice(i, 1);
3210
                        return true;
3211
                    } else {
3212
                        return sel;
3213
                    }
3214
                }
3215
            }
3216
            if (action == "deleteAll") {
3217
                cachedRangySelections.length = 0;
3218
            }
3219
            return null;
3220
        }
3221
 
3222
        var getSelection = function(win) {
3223
            // Check if the parameter is a Rangy Selection object
3224
            if (win && win instanceof WrappedSelection) {
3225
                win.refresh();
3226
                return win;
3227
            }
3228
 
3229
            win = getWindow(win, "getNativeSelection");
3230
 
3231
            var sel = actOnCachedSelection(win);
3232
            var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
3233
            if (sel) {
3234
                sel.nativeSelection = nativeSel;
3235
                sel.docSelection = docSel;
3236
                sel.refresh();
3237
            } else {
3238
                sel = new WrappedSelection(nativeSel, docSel, win);
3239
                cachedRangySelections.push( { win: win, selection: sel } );
3240
            }
3241
            return sel;
3242
        };
3243
 
3244
        api.getSelection = getSelection;
3245
 
3246
        util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
3247
 
3248
        var selProto = WrappedSelection.prototype;
3249
 
3250
        function createControlSelection(sel, ranges) {
3251
            // Ensure that the selection becomes of type "Control"
3252
            var doc = getDocument(ranges[0].startContainer);
3253
            var controlRange = getBody(doc).createControlRange();
3254
            for (var i = 0, el, len = ranges.length; i < len; ++i) {
3255
                el = getSingleElementFromRange(ranges[i]);
3256
                try {
3257
                    controlRange.add(el);
3258
                } catch (ex) {
3259
                    throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
3260
                }
3261
            }
3262
            controlRange.select();
3263
 
3264
            // Update the wrapped selection based on what's now in the native selection
3265
            updateControlSelection(sel);
3266
        }
3267
 
3268
        // Selecting a range
3269
        if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
3270
            selProto.removeAllRanges = function() {
3271
                this.nativeSelection.removeAllRanges();
3272
                updateEmptySelection(this);
3273
            };
3274
 
3275
            var addRangeBackward = function(sel, range) {
3276
                addRangeBackwardToNative(sel.nativeSelection, range);
3277
                sel.refresh();
3278
            };
3279
 
3280
            if (selectionHasRangeCount) {
3281
                selProto.addRange = function(range, direction) {
3282
                    if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3283
                        addRangeToControlSelection(this, range);
3284
                    } else {
3285
                        if (isDirectionBackward(direction) && selectionHasExtend) {
3286
                            addRangeBackward(this, range);
3287
                        } else {
3288
                            var previousRangeCount;
3289
                            if (selectionSupportsMultipleRanges) {
3290
                                previousRangeCount = this.rangeCount;
3291
                            } else {
3292
                                this.removeAllRanges();
3293
                                previousRangeCount = 0;
3294
                            }
3295
                            // Clone the native range so that changing the selected range does not affect the selection.
3296
                            // This is contrary to the spec but is the only way to achieve consistency between browsers. See
3297
                            // issue 80.
3298
                            var clonedNativeRange = getNativeRange(range).cloneRange();
3299
                            try {
3300
                                this.nativeSelection.addRange(clonedNativeRange);
3301
                            } catch (ex) {
3302
                            }
3303
 
3304
                            // Check whether adding the range was successful
3305
                            this.rangeCount = this.nativeSelection.rangeCount;
3306
 
3307
                            if (this.rangeCount == previousRangeCount + 1) {
3308
                                // The range was added successfully
3309
 
3310
                                // Check whether the range that we added to the selection is reflected in the last range extracted from
3311
                                // the selection
3312
                                if (api.config.checkSelectionRanges) {
3313
                                    var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
3314
                                    if (nativeRange && !rangesEqual(nativeRange, range)) {
3315
                                        // Happens in WebKit with, for example, a selection placed at the start of a text node
3316
                                        range = new WrappedRange(nativeRange);
3317
                                    }
3318
                                }
3319
                                this._ranges[this.rangeCount - 1] = range;
3320
                                updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
3321
                                this.isCollapsed = selectionIsCollapsed(this);
3322
                                updateType(this);
3323
                            } else {
3324
                                // The range was not added successfully. The simplest thing is to refresh
3325
                                this.refresh();
3326
                            }
3327
                        }
3328
                    }
3329
                };
3330
            } else {
3331
                selProto.addRange = function(range, direction) {
3332
                    if (isDirectionBackward(direction) && selectionHasExtend) {
3333
                        addRangeBackward(this, range);
3334
                    } else {
3335
                        this.nativeSelection.addRange(getNativeRange(range));
3336
                        this.refresh();
3337
                    }
3338
                };
3339
            }
3340
 
3341
            selProto.setRanges = function(ranges) {
3342
                if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
3343
                    createControlSelection(this, ranges);
3344
                } else {
3345
                    this.removeAllRanges();
3346
                    for (var i = 0, len = ranges.length; i < len; ++i) {
3347
                        this.addRange(ranges[i]);
3348
                    }
3349
                }
3350
            };
3351
        } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
3352
                   implementsControlRange && useDocumentSelection) {
3353
 
3354
            selProto.removeAllRanges = function() {
3355
                // Added try/catch as fix for issue #21
3356
                try {
3357
                    this.docSelection.empty();
3358
 
3359
                    // Check for empty() not working (issue #24)
3360
                    if (this.docSelection.type != "None") {
3361
                        // Work around failure to empty a control selection by instead selecting a TextRange and then
3362
                        // calling empty()
3363
                        var doc;
3364
                        if (this.anchorNode) {
3365
                            doc = getDocument(this.anchorNode);
3366
                        } else if (this.docSelection.type == CONTROL) {
3367
                            var controlRange = this.docSelection.createRange();
3368
                            if (controlRange.length) {
3369
                                doc = getDocument( controlRange.item(0) );
3370
                            }
3371
                        }
3372
                        if (doc) {
3373
                            var textRange = getBody(doc).createTextRange();
3374
                            textRange.select();
3375
                            this.docSelection.empty();
3376
                        }
3377
                    }
3378
                } catch(ex) {}
3379
                updateEmptySelection(this);
3380
            };
3381
 
3382
            selProto.addRange = function(range) {
3383
                if (this.docSelection.type == CONTROL) {
3384
                    addRangeToControlSelection(this, range);
3385
                } else {
3386
                    api.WrappedTextRange.rangeToTextRange(range).select();
3387
                    this._ranges[0] = range;
3388
                    this.rangeCount = 1;
3389
                    this.isCollapsed = this._ranges[0].collapsed;
3390
                    updateAnchorAndFocusFromRange(this, range, false);
3391
                    updateType(this);
3392
                }
3393
            };
3394
 
3395
            selProto.setRanges = function(ranges) {
3396
                this.removeAllRanges();
3397
                var rangeCount = ranges.length;
3398
                if (rangeCount > 1) {
3399
                    createControlSelection(this, ranges);
3400
                } else if (rangeCount) {
3401
                    this.addRange(ranges[0]);
3402
                }
3403
            };
3404
        } else {
3405
            module.fail("No means of selecting a Range or TextRange was found");
3406
            return false;
3407
        }
3408
 
3409
        selProto.getRangeAt = function(index) {
3410
            if (index < 0 || index >= this.rangeCount) {
3411
                throw new DOMException("INDEX_SIZE_ERR");
3412
            } else {
3413
                // Clone the range to preserve selection-range independence. See issue 80.
3414
                return this._ranges[index].cloneRange();
3415
            }
3416
        };
3417
 
3418
        var refreshSelection;
3419
 
3420
        if (useDocumentSelection) {
3421
            refreshSelection = function(sel) {
3422
                var range;
3423
                if (api.isSelectionValid(sel.win)) {
3424
                    range = sel.docSelection.createRange();
3425
                } else {
3426
                    range = getBody(sel.win.document).createTextRange();
3427
                    range.collapse(true);
3428
                }
3429
 
3430
                if (sel.docSelection.type == CONTROL) {
3431
                    updateControlSelection(sel);
3432
                } else if (isTextRange(range)) {
3433
                    updateFromTextRange(sel, range);
3434
                } else {
3435
                    updateEmptySelection(sel);
3436
                }
3437
            };
3438
        } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
3439
            refreshSelection = function(sel) {
3440
                if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
3441
                    updateControlSelection(sel);
3442
                } else {
3443
                    sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
3444
                    if (sel.rangeCount) {
3445
                        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3446
                            sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
3447
                        }
3448
                        updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
3449
                        sel.isCollapsed = selectionIsCollapsed(sel);
3450
                        updateType(sel);
3451
                    } else {
3452
                        updateEmptySelection(sel);
3453
                    }
3454
                }
3455
            };
3456
        } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
3457
            refreshSelection = function(sel) {
3458
                var range, nativeSel = sel.nativeSelection;
3459
                if (nativeSel.anchorNode) {
3460
                    range = getSelectionRangeAt(nativeSel, 0);
3461
                    sel._ranges = [range];
3462
                    sel.rangeCount = 1;
3463
                    updateAnchorAndFocusFromNativeSelection(sel);
3464
                    sel.isCollapsed = selectionIsCollapsed(sel);
3465
                    updateType(sel);
3466
                } else {
3467
                    updateEmptySelection(sel);
3468
                }
3469
            };
3470
        } else {
3471
            module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3472
            return false;
3473
        }
3474
 
3475
        selProto.refresh = function(checkForChanges) {
3476
            var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3477
            var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
3478
 
3479
            refreshSelection(this);
3480
            if (checkForChanges) {
3481
                // Check the range count first
3482
                var i = oldRanges.length;
3483
                if (i != this._ranges.length) {
3484
                    return true;
3485
                }
3486
 
3487
                // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
3488
                // ranges after this
3489
                if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
3490
                    return true;
3491
                }
3492
 
3493
                // Finally, compare each range in turn
3494
                while (i--) {
3495
                    if (!rangesEqual(oldRanges[i], this._ranges[i])) {
3496
                        return true;
3497
                    }
3498
                }
3499
                return false;
3500
            }
3501
        };
3502
 
3503
        // Removal of a single range
3504
        var removeRangeManually = function(sel, range) {
3505
            var ranges = sel.getAllRanges();
3506
            sel.removeAllRanges();
3507
            for (var i = 0, len = ranges.length; i < len; ++i) {
3508
                if (!rangesEqual(range, ranges[i])) {
3509
                    sel.addRange(ranges[i]);
3510
                }
3511
            }
3512
            if (!sel.rangeCount) {
3513
                updateEmptySelection(sel);
3514
            }
3515
        };
3516
 
3517
        if (implementsControlRange && implementsDocSelection) {
3518
            selProto.removeRange = function(range) {
3519
                if (this.docSelection.type == CONTROL) {
3520
                    var controlRange = this.docSelection.createRange();
3521
                    var rangeElement = getSingleElementFromRange(range);
3522
 
3523
                    // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3524
                    // element contained by the supplied range
3525
                    var doc = getDocument(controlRange.item(0));
3526
                    var newControlRange = getBody(doc).createControlRange();
3527
                    var el, removed = false;
3528
                    for (var i = 0, len = controlRange.length; i < len; ++i) {
3529
                        el = controlRange.item(i);
3530
                        if (el !== rangeElement || removed) {
3531
                            newControlRange.add(controlRange.item(i));
3532
                        } else {
3533
                            removed = true;
3534
                        }
3535
                    }
3536
                    newControlRange.select();
3537
 
3538
                    // Update the wrapped selection based on what's now in the native selection
3539
                    updateControlSelection(this);
3540
                } else {
3541
                    removeRangeManually(this, range);
3542
                }
3543
            };
3544
        } else {
3545
            selProto.removeRange = function(range) {
3546
                removeRangeManually(this, range);
3547
            };
3548
        }
3549
 
3550
        // Detecting if a selection is backward
3551
        var selectionIsBackward;
3552
        if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
3553
            selectionIsBackward = winSelectionIsBackward;
3554
 
3555
            selProto.isBackward = function() {
3556
                return selectionIsBackward(this);
3557
            };
3558
        } else {
3559
            selectionIsBackward = selProto.isBackward = function() {
3560
                return false;
3561
            };
3562
        }
3563
 
3564
        // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
3565
        selProto.isBackwards = selProto.isBackward;
3566
 
3567
        // Selection stringifier
3568
        // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
3569
        // The current spec does not yet define this method.
3570
        selProto.toString = function() {
3571
            var rangeTexts = [];
3572
            for (var i = 0, len = this.rangeCount; i < len; ++i) {
3573
                rangeTexts[i] = "" + this._ranges[i];
3574
            }
3575
            return rangeTexts.join("");
3576
        };
3577
 
3578
        function assertNodeInSameDocument(sel, node) {
3579
            if (sel.win.document != getDocument(node)) {
3580
                throw new DOMException("WRONG_DOCUMENT_ERR");
3581
            }
3582
        }
3583
 
3584
        function assertValidOffset(node, offset) {
3585
            if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
3586
                throw new DOMException("INDEX_SIZE_ERR");
3587
            }
3588
        }
3589
 
3590
        // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
3591
        selProto.collapse = function(node, offset) {
3592
            assertNodeInSameDocument(this, node);
3593
            var range = api.createRange(node);
3594
            range.collapseToPoint(node, offset);
3595
            this.setSingleRange(range);
3596
            this.isCollapsed = true;
3597
        };
3598
 
3599
        selProto.collapseToStart = function() {
3600
            if (this.rangeCount) {
3601
                var range = this._ranges[0];
3602
                this.collapse(range.startContainer, range.startOffset);
3603
            } else {
3604
                throw new DOMException("INVALID_STATE_ERR");
3605
            }
3606
        };
3607
 
3608
        selProto.collapseToEnd = function() {
3609
            if (this.rangeCount) {
3610
                var range = this._ranges[this.rangeCount - 1];
3611
                this.collapse(range.endContainer, range.endOffset);
3612
            } else {
3613
                throw new DOMException("INVALID_STATE_ERR");
3614
            }
3615
        };
3616
 
3617
        // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
3618
        // specified so the native implementation is never used by Rangy.
3619
        selProto.selectAllChildren = function(node) {
3620
            assertNodeInSameDocument(this, node);
3621
            var range = api.createRange(node);
3622
            range.selectNodeContents(node);
3623
            this.setSingleRange(range);
3624
        };
3625
 
3626
        if (selectionHasSetBaseAndExtent) {
3627
            selProto.setBaseAndExtent = function(anchorNode, anchorOffset, focusNode, focusOffset) {
3628
                this.nativeSelection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
3629
                this.refresh();
3630
            };
3631
        } else if (selectionHasExtend) {
3632
            selProto.setBaseAndExtent = function(anchorNode, anchorOffset, focusNode, focusOffset) {
3633
                assertValidOffset(anchorNode, anchorOffset);
3634
                assertValidOffset(focusNode, focusOffset);
3635
                assertNodeInSameDocument(this, anchorNode);
3636
                assertNodeInSameDocument(this, focusNode);
3637
                var range = api.createRange(node);
3638
                var isBackwards = (dom.comparePoints(anchorNode, anchorOffset, focusNode, focusOffset) == -1);
3639
                if (isBackwards) {
3640
                    range.setStartAndEnd(focusNode, focusOffset, anchorNode, anchorOffset);
3641
                } else {
3642
                    range.setStartAndEnd(anchorNode, anchorOffset, focusNode, focusOffset);
3643
                }
3644
                this.setSingleRange(range, isBackwards);
3645
            };
3646
        }
3647
 
3648
        selProto.deleteFromDocument = function() {
3649
            // Sepcial behaviour required for IE's control selections
3650
            if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3651
                var controlRange = this.docSelection.createRange();
3652
                var element;
3653
                while (controlRange.length) {
3654
                    element = controlRange.item(0);
3655
                    controlRange.remove(element);
3656
                    dom.removeNode(element);
3657
                }
3658
                this.refresh();
3659
            } else if (this.rangeCount) {
3660
                var ranges = this.getAllRanges();
3661
                if (ranges.length) {
3662
                    this.removeAllRanges();
3663
                    for (var i = 0, len = ranges.length; i < len; ++i) {
3664
                        ranges[i].deleteContents();
3665
                    }
3666
                    // The spec says nothing about what the selection should contain after calling deleteContents on each
3667
                    // range. Firefox moves the selection to where the final selected range was, so we emulate that
3668
                    this.addRange(ranges[len - 1]);
3669
                }
3670
            }
3671
        };
3672
 
3673
        // The following are non-standard extensions
3674
        selProto.eachRange = function(func, returnValue) {
3675
            for (var i = 0, len = this._ranges.length; i < len; ++i) {
3676
                if ( func( this.getRangeAt(i) ) ) {
3677
                    return returnValue;
3678
                }
3679
            }
3680
        };
3681
 
3682
        selProto.getAllRanges = function() {
3683
            var ranges = [];
3684
            this.eachRange(function(range) {
3685
                ranges.push(range);
3686
            });
3687
            return ranges;
3688
        };
3689
 
3690
        selProto.setSingleRange = function(range, direction) {
3691
            this.removeAllRanges();
3692
            this.addRange(range, direction);
3693
        };
3694
 
3695
        selProto.callMethodOnEachRange = function(methodName, params) {
3696
            var results = [];
3697
            this.eachRange( function(range) {
3698
                results.push( range[methodName].apply(range, params || []) );
3699
            } );
3700
            return results;
3701
        };
3702
 
3703
        function createStartOrEndSetter(isStart) {
3704
            return function(node, offset) {
3705
                var range;
3706
                if (this.rangeCount) {
3707
                    range = this.getRangeAt(0);
3708
                    range["set" + (isStart ? "Start" : "End")](node, offset);
3709
                } else {
3710
                    range = api.createRange(this.win.document);
3711
                    range.setStartAndEnd(node, offset);
3712
                }
3713
                this.setSingleRange(range, this.isBackward());
3714
            };
3715
        }
3716
 
3717
        selProto.setStart = createStartOrEndSetter(true);
3718
        selProto.setEnd = createStartOrEndSetter(false);
3719
 
3720
        // Add select() method to Range prototype. Any existing selection will be removed.
3721
        api.rangePrototype.select = function(direction) {
3722
            getSelection( this.getDocument() ).setSingleRange(this, direction);
3723
        };
3724
 
3725
        selProto.changeEachRange = function(func) {
3726
            var ranges = [];
3727
            var backward = this.isBackward();
3728
 
3729
            this.eachRange(function(range) {
3730
                func(range);
3731
                ranges.push(range);
3732
            });
3733
 
3734
            this.removeAllRanges();
3735
            if (backward && ranges.length == 1) {
3736
                this.addRange(ranges[0], "backward");
3737
            } else {
3738
                this.setRanges(ranges);
3739
            }
3740
        };
3741
 
3742
        selProto.containsNode = function(node, allowPartial) {
3743
            return this.eachRange( function(range) {
3744
                return range.containsNode(node, allowPartial);
3745
            }, true ) || false;
3746
        };
3747
 
3748
        selProto.getBookmark = function(containerNode) {
3749
            return {
3750
                backward: this.isBackward(),
3751
                rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
3752
            };
3753
        };
3754
 
3755
        selProto.moveToBookmark = function(bookmark) {
3756
            var selRanges = [];
3757
            for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
3758
                range = api.createRange(this.win);
3759
                range.moveToBookmark(rangeBookmark);
3760
                selRanges.push(range);
3761
            }
3762
            if (bookmark.backward) {
3763
                this.setSingleRange(selRanges[0], "backward");
3764
            } else {
3765
                this.setRanges(selRanges);
3766
            }
3767
        };
3768
 
3769
        selProto.saveRanges = function() {
3770
            return {
3771
                backward: this.isBackward(),
3772
                ranges: this.callMethodOnEachRange("cloneRange")
3773
            };
3774
        };
3775
 
3776
        selProto.restoreRanges = function(selRanges) {
3777
            this.removeAllRanges();
3778
            for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
3779
                this.addRange(range, (selRanges.backward && i == 0));
3780
            }
3781
        };
3782
 
3783
        selProto.toHtml = function() {
3784
            var rangeHtmls = [];
3785
            this.eachRange(function(range) {
3786
                rangeHtmls.push( DomRange.toHtml(range) );
3787
            });
3788
            return rangeHtmls.join("");
3789
        };
3790
 
3791
        if (features.implementsTextRange) {
3792
            selProto.getNativeTextRange = function() {
3793
                var sel, textRange;
3794
                if ( (sel = this.docSelection) ) {
3795
                    var range = sel.createRange();
3796
                    if (isTextRange(range)) {
3797
                        return range;
3798
                    } else {
3799
                        throw module.createError("getNativeTextRange: selection is a control selection");
3800
                    }
3801
                } else if (this.rangeCount > 0) {
3802
                    return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
3803
                } else {
3804
                    throw module.createError("getNativeTextRange: selection contains no range");
3805
                }
3806
            };
3807
        }
3808
 
3809
        function inspect(sel) {
3810
            var rangeInspects = [];
3811
            var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
3812
            var focus = new DomPosition(sel.focusNode, sel.focusOffset);
3813
            var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
3814
 
3815
            if (typeof sel.rangeCount != "undefined") {
3816
                for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3817
                    rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
3818
                }
3819
            }
3820
            return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3821
                    ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3822
        }
3823
 
3824
        selProto.getName = function() {
3825
            return "WrappedSelection";
3826
        };
3827
 
3828
        selProto.inspect = function() {
3829
            return inspect(this);
3830
        };
3831
 
3832
        selProto.detach = function() {
3833
            actOnCachedSelection(this.win, "delete");
3834
            deleteProperties(this);
3835
        };
3836
 
3837
        WrappedSelection.detachAll = function() {
3838
            actOnCachedSelection(null, "deleteAll");
3839
        };
3840
 
3841
        WrappedSelection.inspect = inspect;
3842
        WrappedSelection.isDirectionBackward = isDirectionBackward;
3843
 
3844
        api.Selection = WrappedSelection;
3845
 
3846
        api.selectionPrototype = selProto;
3847
 
3848
        api.addShimListener(function(win) {
3849
            if (typeof win.getSelection == "undefined") {
3850
                win.getSelection = function() {
3851
                    return getSelection(win);
3852
                };
3853
            }
3854
            win = null;
3855
        });
3856
    });
3857
 
3858
 
3859
    /*----------------------------------------------------------------------------------------------------------------*/
3860
 
3861
    // Wait for document to load before initializing
3862
    var docReady = false;
3863
 
3864
    var loadHandler = function(e) {
3865
        if (!docReady) {
3866
            docReady = true;
3867
            if (!api.initialized && api.config.autoInitialize) {
3868
                init();
3869
            }
3870
        }
3871
    };
3872
 
3873
    if (isBrowser) {
3874
        // Test whether the document has already been loaded and initialize immediately if so
3875
        if (document.readyState == "complete") {
3876
            loadHandler();
3877
        } else {
3878
            if (isHostMethod(document, "addEventListener")) {
3879
                document.addEventListener("DOMContentLoaded", loadHandler, false);
3880
            }
3881
 
3882
            // Add a fallback in case the DOMContentLoaded event isn't supported
3883
            addListener(window, "load", loadHandler);
3884
        }
3885
    }
3886
 
3887
    return api;
3888
}, this);