rangy-rails 1.3alpha.772.0

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