MDL-64573 output: ajax form event
[moodle.git] / lib / editor / atto / yui / build / moodle-editor_atto-rangy / moodle-editor_atto-rangy-debug.js
CommitLineData
d321f68b 1/**\r
0fde7efa
DW
2 * Rangy, a cross-browser JavaScript range and selection library\r
3 * https://github.com/timdown/rangy\r
d321f68b 4 *\r
0fde7efa 5 * Copyright 2015, Tim Down\r
d321f68b 6 * Licensed under the MIT license.\r
0fde7efa
DW
7 * Version: 1.3.0\r
8 * Build date: 10 May 2015\r
d321f68b 9 */\r
d321f68b 10\r
0fde7efa 11(function(factory, root) {\r
fc07d950
DW
12 // No AMD or CommonJS support so we place Rangy in (probably) the global variable\r
13 root.rangy = factory();\r
0fde7efa 14})(function() {\r
d321f68b
DW
15\r
16 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";\r
17\r
0fde7efa
DW
18 // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START\r
19 // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.\r
d321f68b 20 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",\r
0fde7efa 21 "commonAncestorContainer"];\r
d321f68b 22\r
0fde7efa 23 // Minimal set of methods required for DOM Level 2 Range compliance\r
d321f68b
DW
24 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",\r
25 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",\r
26 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];\r
27\r
28 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];\r
29\r
30 // Subset of TextRange's full set of methods that we're interested in\r
0fde7efa
DW
31 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",\r
32 "setEndPoint", "getBoundingClientRect"];\r
d321f68b
DW
33\r
34 /*----------------------------------------------------------------------------------------------------------------*/\r
35\r
36 // Trio of functions taken from Peter Michaux's article:\r
37 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting\r
38 function isHostMethod(o, p) {\r
39 var t = typeof o[p];\r
40 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";\r
41 }\r
42\r
43 function isHostObject(o, p) {\r
44 return !!(typeof o[p] == OBJECT && o[p]);\r
45 }\r
46\r
47 function isHostProperty(o, p) {\r
48 return typeof o[p] != UNDEFINED;\r
49 }\r
50\r
51 // Creates a convenience function to save verbose repeated calls to tests functions\r
52 function createMultiplePropertyTest(testFunc) {\r
53 return function(o, props) {\r
54 var i = props.length;\r
55 while (i--) {\r
56 if (!testFunc(o, props[i])) {\r
57 return false;\r
58 }\r
59 }\r
60 return true;\r
61 };\r
62 }\r
63\r
64 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions\r
65 var areHostMethods = createMultiplePropertyTest(isHostMethod);\r
66 var areHostObjects = createMultiplePropertyTest(isHostObject);\r
67 var areHostProperties = createMultiplePropertyTest(isHostProperty);\r
68\r
69 function isTextRange(range) {\r
70 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);\r
71 }\r
72\r
0fde7efa
DW
73 function getBody(doc) {\r
74 return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];\r
75 }\r
76\r
77 var forEach = [].forEach ?\r
78 function(arr, func) {\r
79 arr.forEach(func);\r
80 } :\r
81 function(arr, func) {\r
82 for (var i = 0, len = arr.length; i < len; ++i) {\r
83 func(arr[i], i);\r
84 }\r
85 };\r
86\r
87 var modules = {};\r
88\r
89 var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);\r
90\r
91 var util = {\r
92 isHostMethod: isHostMethod,\r
93 isHostObject: isHostObject,\r
94 isHostProperty: isHostProperty,\r
95 areHostMethods: areHostMethods,\r
96 areHostObjects: areHostObjects,\r
97 areHostProperties: areHostProperties,\r
98 isTextRange: isTextRange,\r
99 getBody: getBody,\r
100 forEach: forEach\r
101 };\r
102\r
d321f68b 103 var api = {\r
0fde7efa 104 version: "1.3.0",\r
d321f68b 105 initialized: false,\r
0fde7efa 106 isBrowser: isBrowser,\r
d321f68b 107 supported: true,\r
0fde7efa 108 util: util,\r
d321f68b 109 features: {},\r
0fde7efa 110 modules: modules,\r
d321f68b 111 config: {\r
0fde7efa 112 alertOnFail: false,\r
d321f68b 113 alertOnWarn: false,\r
0fde7efa
DW
114 preferTextRange: false,\r
115 autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize\r
d321f68b
DW
116 }\r
117 };\r
118\r
0fde7efa
DW
119 function consoleLog(msg) {\r
120 if (typeof console != UNDEFINED && isHostMethod(console, "log")) {\r
121 console.log(msg);\r
122 }\r
123 }\r
124\r
125 function alertOrLog(msg, shouldAlert) {\r
126 if (isBrowser && shouldAlert) {\r
127 alert(msg);\r
128 } else {\r
129 consoleLog(msg);\r
130 }\r
131 }\r
132\r
d321f68b 133 function fail(reason) {\r
d321f68b
DW
134 api.initialized = true;\r
135 api.supported = false;\r
0fde7efa 136 alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);\r
d321f68b
DW
137 }\r
138\r
139 api.fail = fail;\r
140\r
141 function warn(msg) {\r
0fde7efa 142 alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);\r
d321f68b
DW
143 }\r
144\r
145 api.warn = warn;\r
146\r
0fde7efa
DW
147 // Add utility extend() method\r
148 var extend;\r
d321f68b 149 if ({}.hasOwnProperty) {\r
0fde7efa
DW
150 util.extend = extend = function(obj, props, deep) {\r
151 var o, p;\r
d321f68b
DW
152 for (var i in props) {\r
153 if (props.hasOwnProperty(i)) {\r
0fde7efa
DW
154 o = obj[i];\r
155 p = props[i];\r
156 if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {\r
157 extend(o, p, true);\r
158 }\r
159 obj[i] = p;\r
d321f68b
DW
160 }\r
161 }\r
0fde7efa
DW
162 // Special case for toString, which does not show up in for...in loops in IE <= 8\r
163 if (props.hasOwnProperty("toString")) {\r
164 obj.toString = props.toString;\r
165 }\r
166 return obj;\r
167 };\r
168\r
169 util.createOptions = function(optionsParam, defaults) {\r
170 var options = {};\r
171 extend(options, defaults);\r
172 if (optionsParam) {\r
173 extend(options, optionsParam);\r
174 }\r
175 return options;\r
d321f68b
DW
176 };\r
177 } else {\r
178 fail("hasOwnProperty not supported");\r
179 }\r
180\r
0fde7efa
DW
181 // Test whether we're in a browser and bail out if not\r
182 if (!isBrowser) {\r
183 fail("Rangy can only run in a browser");\r
184 }\r
185\r
186 // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not\r
187 (function() {\r
188 var toArray;\r
189\r
190 if (isBrowser) {\r
191 var el = document.createElement("div");\r
192 el.appendChild(document.createElement("span"));\r
193 var slice = [].slice;\r
194 try {\r
195 if (slice.call(el.childNodes, 0)[0].nodeType == 1) {\r
196 toArray = function(arrayLike) {\r
197 return slice.call(arrayLike, 0);\r
198 };\r
199 }\r
200 } catch (e) {}\r
201 }\r
202\r
203 if (!toArray) {\r
204 toArray = function(arrayLike) {\r
205 var arr = [];\r
206 for (var i = 0, len = arrayLike.length; i < len; ++i) {\r
207 arr[i] = arrayLike[i];\r
208 }\r
209 return arr;\r
210 };\r
211 }\r
212\r
213 util.toArray = toArray;\r
214 })();\r
215\r
216 // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or\r
217 // normalization of event properties\r
218 var addListener;\r
219 if (isBrowser) {\r
220 if (isHostMethod(document, "addEventListener")) {\r
221 addListener = function(obj, eventType, listener) {\r
222 obj.addEventListener(eventType, listener, false);\r
223 };\r
224 } else if (isHostMethod(document, "attachEvent")) {\r
225 addListener = function(obj, eventType, listener) {\r
226 obj.attachEvent("on" + eventType, listener);\r
227 };\r
228 } else {\r
229 fail("Document does not have required addEventListener or attachEvent method");\r
230 }\r
231\r
232 util.addListener = addListener;\r
233 }\r
234\r
d321f68b 235 var initListeners = [];\r
0fde7efa
DW
236\r
237 function getErrorDesc(ex) {\r
238 return ex.message || ex.description || String(ex);\r
239 }\r
d321f68b
DW
240\r
241 // Initialization\r
242 function init() {\r
0fde7efa 243 if (!isBrowser || api.initialized) {\r
d321f68b
DW
244 return;\r
245 }\r
246 var testRange;\r
247 var implementsDomRange = false, implementsTextRange = false;\r
248\r
249 // First, perform basic feature tests\r
250\r
251 if (isHostMethod(document, "createRange")) {\r
252 testRange = document.createRange();\r
253 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {\r
254 implementsDomRange = true;\r
255 }\r
d321f68b
DW
256 }\r
257\r
0fde7efa
DW
258 var body = getBody(document);\r
259 if (!body || body.nodeName.toLowerCase() != "body") {\r
260 fail("No body element found");\r
261 return;\r
262 }\r
d321f68b
DW
263\r
264 if (body && isHostMethod(body, "createTextRange")) {\r
265 testRange = body.createTextRange();\r
266 if (isTextRange(testRange)) {\r
267 implementsTextRange = true;\r
268 }\r
269 }\r
270\r
271 if (!implementsDomRange && !implementsTextRange) {\r
0fde7efa
DW
272 fail("Neither Range nor TextRange are available");\r
273 return;\r
d321f68b
DW
274 }\r
275\r
276 api.initialized = true;\r
277 api.features = {\r
278 implementsDomRange: implementsDomRange,\r
279 implementsTextRange: implementsTextRange\r
280 };\r
281\r
0fde7efa
DW
282 // Initialize modules\r
283 var module, errorMessage;\r
284 for (var moduleName in modules) {\r
285 if ( (module = modules[moduleName]) instanceof Module ) {\r
286 module.init(module, api);\r
287 }\r
288 }\r
289\r
290 // Call init listeners\r
291 for (var i = 0, len = initListeners.length; i < len; ++i) {\r
d321f68b 292 try {\r
0fde7efa 293 initListeners[i](api);\r
d321f68b 294 } catch (ex) {\r
0fde7efa
DW
295 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);\r
296 consoleLog(errorMessage);\r
d321f68b
DW
297 }\r
298 }\r
299 }\r
300\r
0fde7efa
DW
301 function deprecationNotice(deprecated, replacement, module) {\r
302 if (module) {\r
303 deprecated += " in module " + module.name;\r
304 }\r
305 api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +\r
306 replacement + " instead.");\r
307 }\r
308\r
309 function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {\r
310 owner[deprecated] = function() {\r
311 deprecationNotice(deprecated, replacement, module);\r
312 return owner[replacement].apply(owner, util.toArray(arguments));\r
313 };\r
314 }\r
315\r
316 util.deprecationNotice = deprecationNotice;\r
317 util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;\r
318\r
d321f68b
DW
319 // Allow external scripts to initialize this library in case it's loaded after the document has loaded\r
320 api.init = init;\r
321\r
322 // Execute listener immediately if already initialized\r
323 api.addInitListener = function(listener) {\r
324 if (api.initialized) {\r
325 listener(api);\r
326 } else {\r
327 initListeners.push(listener);\r
328 }\r
329 };\r
330\r
0fde7efa 331 var shimListeners = [];\r
d321f68b 332\r
0fde7efa
DW
333 api.addShimListener = function(listener) {\r
334 shimListeners.push(listener);\r
d321f68b
DW
335 };\r
336\r
0fde7efa 337 function shim(win) {\r
d321f68b
DW
338 win = win || window;\r
339 init();\r
340\r
341 // Notify listeners\r
0fde7efa
DW
342 for (var i = 0, len = shimListeners.length; i < len; ++i) {\r
343 shimListeners[i](win);\r
d321f68b
DW
344 }\r
345 }\r
346\r
0fde7efa
DW
347 if (isBrowser) {\r
348 api.shim = api.createMissingNativeApi = shim;\r
349 createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");\r
350 }\r
d321f68b 351\r
0fde7efa 352 function Module(name, dependencies, initializer) {\r
d321f68b 353 this.name = name;\r
0fde7efa 354 this.dependencies = dependencies;\r
d321f68b
DW
355 this.initialized = false;\r
356 this.supported = false;\r
0fde7efa 357 this.initializer = initializer;\r
d321f68b
DW
358 }\r
359\r
0fde7efa
DW
360 Module.prototype = {\r
361 init: function() {\r
362 var requiredModuleNames = this.dependencies || [];\r
363 for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {\r
364 moduleName = requiredModuleNames[i];\r
d321f68b 365\r
0fde7efa
DW
366 requiredModule = modules[moduleName];\r
367 if (!requiredModule || !(requiredModule instanceof Module)) {\r
368 throw new Error("required module '" + moduleName + "' not found");\r
d321f68b 369 }\r
d321f68b 370\r
0fde7efa 371 requiredModule.init();\r
d321f68b 372\r
0fde7efa
DW
373 if (!requiredModule.supported) {\r
374 throw new Error("required module '" + moduleName + "' not supported");\r
375 }\r
d321f68b 376 }\r
d321f68b 377\r
0fde7efa
DW
378 // Now run initializer\r
379 this.initializer(this);\r
380 },\r
d321f68b 381\r
0fde7efa
DW
382 fail: function(reason) {\r
383 this.initialized = true;\r
384 this.supported = false;\r
385 throw new Error(reason);\r
386 },\r
d321f68b 387\r
0fde7efa
DW
388 warn: function(msg) {\r
389 api.warn("Module " + this.name + ": " + msg);\r
390 },\r
d321f68b 391\r
0fde7efa
DW
392 deprecationNotice: function(deprecated, replacement) {\r
393 api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +\r
394 replacement + " instead");\r
395 },\r
d321f68b 396\r
0fde7efa
DW
397 createError: function(msg) {\r
398 return new Error("Error in Rangy " + this.name + " module: " + msg);\r
d321f68b 399 }\r
0fde7efa 400 };\r
d321f68b 401\r
0fde7efa
DW
402 function createModule(name, dependencies, initFunc) {\r
403 var newModule = new Module(name, dependencies, function(module) {\r
404 if (!module.initialized) {\r
405 module.initialized = true;\r
406 try {\r
407 initFunc(api, module);\r
408 module.supported = true;\r
409 } catch (ex) {\r
410 var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);\r
411 consoleLog(errorMessage);\r
412 if (ex.stack) {\r
413 consoleLog(ex.stack);\r
d321f68b 414 }\r
d321f68b 415 }\r
d321f68b 416 }\r
0fde7efa
DW
417 });\r
418 modules[name] = newModule;\r
419 return newModule;\r
d321f68b
DW
420 }\r
421\r
0fde7efa
DW
422 api.createModule = function(name) {\r
423 // Allow 2 or 3 arguments (second argument is an optional array of dependencies)\r
424 var initFunc, dependencies;\r
425 if (arguments.length == 2) {\r
426 initFunc = arguments[1];\r
427 dependencies = [];\r
d321f68b 428 } else {\r
0fde7efa
DW
429 initFunc = arguments[2];\r
430 dependencies = arguments[1];\r
d321f68b 431 }\r
d321f68b 432\r
0fde7efa 433 var module = createModule(name, dependencies, initFunc);\r
d321f68b 434\r
0fde7efa
DW
435 // Initialize the module immediately if the core is already initialized\r
436 if (api.initialized && api.supported) {\r
437 module.init();\r
d321f68b
DW
438 }\r
439 };\r
440\r
0fde7efa
DW
441 api.createCoreModule = function(name, dependencies, initFunc) {\r
442 createModule(name, dependencies, initFunc);\r
d321f68b
DW
443 };\r
444\r
0fde7efa 445 /*----------------------------------------------------------------------------------------------------------------*/\r
d321f68b 446\r
0fde7efa 447 // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately\r
d321f68b 448\r
0fde7efa
DW
449 function RangePrototype() {}\r
450 api.RangePrototype = RangePrototype;\r
451 api.rangePrototype = new RangePrototype();\r
d321f68b 452\r
0fde7efa
DW
453 function SelectionPrototype() {}\r
454 api.selectionPrototype = new SelectionPrototype();\r
d321f68b 455\r
0fde7efa
DW
456 /*----------------------------------------------------------------------------------------------------------------*/\r
457\r
458 // DOM utility methods used by Rangy
459 api.createCoreModule("DomUtil", [], function(api, module) {
460 var UNDEF = "undefined";
461 var util = api.util;
462 var getBody = util.getBody;
d321f68b 463
0fde7efa
DW
464 // Perform feature tests
465 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
466 module.fail("document missing a Node creation method");
467 }
d321f68b 468
0fde7efa
DW
469 if (!util.isHostMethod(document, "getElementsByTagName")) {
470 module.fail("document missing getElementsByTagName method");
471 }
d321f68b 472
0fde7efa
DW
473 var el = document.createElement("div");
474 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
475 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
476 module.fail("Incomplete Element implementation");
477 }
d321f68b 478
0fde7efa
DW
479 // innerHTML is required for Range's createContextualFragment method
480 if (!util.isHostProperty(el, "innerHTML")) {
481 module.fail("Element is missing innerHTML property");
482 }
d321f68b 483
0fde7efa
DW
484 var textNode = document.createTextNode("test");
485 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
486 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
487 !util.areHostProperties(textNode, ["data"]))) {
488 module.fail("Incomplete Text Node implementation");
d321f68b 489 }
d321f68b 490
0fde7efa
DW
491 /*----------------------------------------------------------------------------------------------------------------*/
492
493 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
494 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
495 // contains just the document as a single element and the value searched for is the document.
496 var arrayContains = /*Array.prototype.indexOf ?
497 function(arr, val) {
498 return arr.indexOf(val) > -1;
499 }:*/
500
501 function(arr, val) {
502 var i = arr.length;
503 while (i--) {
504 if (arr[i] === val) {
505 return true;
506 }
507 }
508 return false;
509 };
d321f68b 510
0fde7efa
DW
511 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
512 function isHtmlNamespace(node) {
513 var ns;
514 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
515 }
d321f68b 516
0fde7efa
DW
517 function parentElement(node) {
518 var parent = node.parentNode;
519 return (parent.nodeType == 1) ? parent : null;
520 }
521
522 function getNodeIndex(node) {
523 var i = 0;
524 while( (node = node.previousSibling) ) {
525 ++i;
d321f68b 526 }
0fde7efa 527 return i;
d321f68b 528 }
d321f68b 529
0fde7efa
DW
530 function getNodeLength(node) {
531 switch (node.nodeType) {
532 case 7:
533 case 10:
534 return 0;
535 case 3:
536 case 8:
537 return node.length;
538 default:
539 return node.childNodes.length;
540 }
541 }
d321f68b 542
0fde7efa
DW
543 function getCommonAncestor(node1, node2) {
544 var ancestors = [], n;
545 for (n = node1; n; n = n.parentNode) {
546 ancestors.push(n);
d321f68b
DW
547 }
548
0fde7efa
DW
549 for (n = node2; n; n = n.parentNode) {
550 if (arrayContains(ancestors, n)) {
551 return n;
552 }
d321f68b 553 }
0fde7efa
DW
554
555 return null;
d321f68b 556 }
d321f68b 557
0fde7efa
DW
558 function isAncestorOf(ancestor, descendant, selfIsAncestor) {
559 var n = selfIsAncestor ? descendant : descendant.parentNode;
560 while (n) {
561 if (n === ancestor) {
562 return true;
d321f68b 563 } else {
0fde7efa 564 n = n.parentNode;
d321f68b
DW
565 }
566 }
0fde7efa 567 return false;
d321f68b 568 }
d321f68b 569
0fde7efa
DW
570 function isOrIsAncestorOf(ancestor, descendant) {
571 return isAncestorOf(ancestor, descendant, true);
d321f68b 572 }
d321f68b 573
0fde7efa
DW
574 function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
575 var p, n = selfIsAncestor ? node : node.parentNode;
576 while (n) {
577 p = n.parentNode;
578 if (p === ancestor) {
579 return n;
580 }
581 n = p;
582 }
583 return null;
584 }
d321f68b 585
0fde7efa
DW
586 function isCharacterDataNode(node) {
587 var t = node.nodeType;
588 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
589 }
d321f68b 590
0fde7efa
DW
591 function isTextOrCommentNode(node) {
592 if (!node) {
593 return false;
594 }
595 var t = node.nodeType;
596 return t == 3 || t == 8 ; // Text or Comment
597 }
d321f68b 598
0fde7efa
DW
599 function insertAfter(node, precedingNode) {
600 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
601 if (nextNode) {
602 parent.insertBefore(node, nextNode);
d321f68b 603 } else {
0fde7efa 604 parent.appendChild(node);
d321f68b 605 }
0fde7efa
DW
606 return node;
607 }
608
609 // Note that we cannot use splitText() because it is bugridden in IE 9.
610 function splitDataNode(node, index, positionsToPreserve) {
611 var newNode = node.cloneNode(false);
612 newNode.deleteData(0, index);
613 node.deleteData(index, node.length - index);
614 insertAfter(newNode, node);
615
616 // Preserve positions
617 if (positionsToPreserve) {
618 for (var i = 0, position; position = positionsToPreserve[i++]; ) {
619 // Handle case where position was inside the portion of node after the split point
620 if (position.node == node && position.offset > index) {
621 position.node = newNode;
622 position.offset -= index;
623 }
624 // Handle the case where the position is a node offset within node's parent
625 else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
626 ++position.offset;
627 }
628 }
d321f68b 629 }
0fde7efa 630 return newNode;
d321f68b 631 }
d321f68b 632
0fde7efa
DW
633 function getDocument(node) {
634 if (node.nodeType == 9) {
635 return node;
636 } else if (typeof node.ownerDocument != UNDEF) {
637 return node.ownerDocument;
638 } else if (typeof node.document != UNDEF) {
639 return node.document;
640 } else if (node.parentNode) {
641 return getDocument(node.parentNode);
642 } else {
643 throw module.createError("getDocument: no document found for node");
644 }
d321f68b
DW
645 }
646
0fde7efa
DW
647 function getWindow(node) {
648 var doc = getDocument(node);
649 if (typeof doc.defaultView != UNDEF) {
650 return doc.defaultView;
651 } else if (typeof doc.parentWindow != UNDEF) {
652 return doc.parentWindow;
653 } else {
654 throw module.createError("Cannot get a window object for node");
d321f68b 655 }
0fde7efa 656 }
d321f68b 657
0fde7efa
DW
658 function getIframeDocument(iframeEl) {
659 if (typeof iframeEl.contentDocument != UNDEF) {
660 return iframeEl.contentDocument;
661 } else if (typeof iframeEl.contentWindow != UNDEF) {
662 return iframeEl.contentWindow.document;
663 } else {
664 throw module.createError("getIframeDocument: No Document object found for iframe element");
665 }
666 }
d321f68b 667
0fde7efa
DW
668 function getIframeWindow(iframeEl) {
669 if (typeof iframeEl.contentWindow != UNDEF) {
670 return iframeEl.contentWindow;
671 } else if (typeof iframeEl.contentDocument != UNDEF) {
672 return iframeEl.contentDocument.defaultView;
673 } else {
674 throw module.createError("getIframeWindow: No Window object found for iframe element");
675 }
676 }
d321f68b 677
0fde7efa
DW
678 // This looks bad. Is it worth it?
679 function isWindow(obj) {
680 return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
681 }
d321f68b 682
0fde7efa
DW
683 function getContentDocument(obj, module, methodName) {
684 var doc;
d321f68b 685
0fde7efa
DW
686 if (!obj) {
687 doc = document;
688 }
d321f68b 689
0fde7efa
DW
690 // Test if a DOM node has been passed and obtain a document object for it if so
691 else if (util.isHostProperty(obj, "nodeType")) {
692 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
693 getIframeDocument(obj) : getDocument(obj);
694 }
d321f68b 695
0fde7efa
DW
696 // Test if the doc parameter appears to be a Window object
697 else if (isWindow(obj)) {
698 doc = obj.document;
699 }
d321f68b 700
0fde7efa
DW
701 if (!doc) {
702 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
d321f68b
DW
703 }
704
0fde7efa 705 return doc;
d321f68b 706 }
d321f68b 707
0fde7efa
DW
708 function getRootContainer(node) {
709 var parent;
710 while ( (parent = node.parentNode) ) {
711 node = parent;
712 }
713 return node;
714 }
d321f68b 715
0fde7efa
DW
716 function comparePoints(nodeA, offsetA, nodeB, offsetB) {
717 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
718 var nodeC, root, childA, childB, n;
719 if (nodeA == nodeB) {
720 // Case 1: nodes are the same
721 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
722 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
723 // Case 2: node C (container B or an ancestor) is a child node of A
724 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
725 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
726 // Case 3: node C (container A or an ancestor) is a child node of B
727 return getNodeIndex(nodeC) < offsetB ? -1 : 1;
728 } else {
729 root = getCommonAncestor(nodeA, nodeB);
730 if (!root) {
731 throw new Error("comparePoints error: nodes have no common ancestor");
732 }
d321f68b 733
0fde7efa
DW
734 // Case 4: containers are siblings or descendants of siblings
735 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
736 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
d321f68b 737
0fde7efa
DW
738 if (childA === childB) {
739 // This shouldn't be possible
740 throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
741 } else {
742 n = root.firstChild;
743 while (n) {
744 if (n === childA) {
745 return -1;
746 } else if (n === childB) {
747 return 1;
748 }
749 n = n.nextSibling;
d321f68b
DW
750 }
751 }
752 }
0fde7efa 753 }
d321f68b 754
0fde7efa 755 /*----------------------------------------------------------------------------------------------------------------*/
d321f68b 756
0fde7efa
DW
757 // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
758 var crashyTextNodes = false;
d321f68b 759
0fde7efa
DW
760 function isBrokenNode(node) {
761 var n;
762 try {
763 n = node.parentNode;
764 return false;
765 } catch (e) {
766 return true;
d321f68b 767 }
0fde7efa 768 }
d321f68b 769
0fde7efa
DW
770 (function() {
771 var el = document.createElement("b");
772 el.innerHTML = "1";
773 var textNode = el.firstChild;
774 el.innerHTML = "<br />";
775 crashyTextNodes = isBrokenNode(textNode);
d321f68b 776
0fde7efa
DW
777 api.features.crashyTextNodes = crashyTextNodes;
778 })();
d321f68b 779
0fde7efa 780 /*----------------------------------------------------------------------------------------------------------------*/
d321f68b 781
0fde7efa
DW
782 function inspectNode(node) {
783 if (!node) {
784 return "[No node]";
d321f68b 785 }
0fde7efa
DW
786 if (crashyTextNodes && isBrokenNode(node)) {
787 return "[Broken node]";
d321f68b 788 }
0fde7efa
DW
789 if (isCharacterDataNode(node)) {
790 return '"' + node.data + '"';
791 }
792 if (node.nodeType == 1) {
793 var idAttr = node.id ? ' id="' + node.id + '"' : "";
794 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
795 }
796 return node.nodeName;
d321f68b 797 }
d321f68b 798
0fde7efa
DW
799 function fragmentFromNodeChildren(node) {
800 var fragment = getDocument(node).createDocumentFragment(), child;
801 while ( (child = node.firstChild) ) {
802 fragment.appendChild(child);
803 }
804 return fragment;
805 }
d321f68b 806
0fde7efa
DW
807 var getComputedStyleProperty;
808 if (typeof window.getComputedStyle != UNDEF) {
809 getComputedStyleProperty = function(el, propName) {
810 return getWindow(el).getComputedStyle(el, null)[propName];
811 };
812 } else if (typeof document.documentElement.currentStyle != UNDEF) {
813 getComputedStyleProperty = function(el, propName) {
814 return el.currentStyle ? el.currentStyle[propName] : "";
815 };
816 } else {
817 module.fail("No means of obtaining computed style properties found");
818 }
d321f68b 819
0fde7efa
DW
820 function createTestElement(doc, html, contentEditable) {
821 var body = getBody(doc);
822 var el = doc.createElement("div");
823 el.contentEditable = "" + !!contentEditable;
824 if (html) {
825 el.innerHTML = html;
826 }
d321f68b 827
0fde7efa
DW
828 // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
829 var bodyFirstChild = body.firstChild;
830 if (bodyFirstChild) {
831 body.insertBefore(el, bodyFirstChild);
832 } else {
833 body.appendChild(el);
834 }
d321f68b 835
0fde7efa
DW
836 return el;
837 }
d321f68b 838
0fde7efa
DW
839 function removeNode(node) {
840 return node.parentNode.removeChild(node);
841 }
d321f68b 842
0fde7efa
DW
843 function NodeIterator(root) {
844 this.root = root;
845 this._next = root;
d321f68b 846 }
d321f68b 847
0fde7efa
DW
848 NodeIterator.prototype = {
849 _current: null,
d321f68b 850
0fde7efa
DW
851 hasNext: function() {
852 return !!this._next;
853 },
854
855 next: function() {
856 var n = this._current = this._next;
857 var child, next;
858 if (this._current) {
859 child = n.firstChild;
860 if (child) {
861 this._next = child;
862 } else {
863 next = null;
864 while ((n !== this.root) && !(next = n.nextSibling)) {
865 n = n.parentNode;
866 }
867 this._next = next;
868 }
d321f68b 869 }
0fde7efa
DW
870 return this._current;
871 },
872
873 detach: function() {
874 this._current = this._next = this.root = null;
d321f68b 875 }
d321f68b 876 };
d321f68b 877
0fde7efa
DW
878 function createIterator(root) {
879 return new NodeIterator(root);
d321f68b 880 }
d321f68b 881
0fde7efa
DW
882 function DomPosition(node, offset) {
883 this.node = node;
884 this.offset = offset;
d321f68b 885 }
d321f68b 886
0fde7efa
DW
887 DomPosition.prototype = {
888 equals: function(pos) {
889 return !!pos && this.node === pos.node && this.offset == pos.offset;
890 },
d321f68b 891
0fde7efa
DW
892 inspect: function() {
893 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
894 },
d321f68b 895
0fde7efa
DW
896 toString: function() {
897 return this.inspect();
898 }
899 };
d321f68b 900
0fde7efa
DW
901 function DOMException(codeName) {
902 this.code = this[codeName];
903 this.codeName = codeName;
904 this.message = "DOMException: " + this.codeName;
d321f68b 905 }
d321f68b 906
0fde7efa
DW
907 DOMException.prototype = {
908 INDEX_SIZE_ERR: 1,
909 HIERARCHY_REQUEST_ERR: 3,
910 WRONG_DOCUMENT_ERR: 4,
911 NO_MODIFICATION_ALLOWED_ERR: 7,
912 NOT_FOUND_ERR: 8,
913 NOT_SUPPORTED_ERR: 9,
914 INVALID_STATE_ERR: 11,
915 INVALID_NODE_TYPE_ERR: 24
916 };
d321f68b 917
0fde7efa
DW
918 DOMException.prototype.toString = function() {
919 return this.message;
920 };
d321f68b 921
0fde7efa
DW
922 api.dom = {
923 arrayContains: arrayContains,
924 isHtmlNamespace: isHtmlNamespace,
925 parentElement: parentElement,
926 getNodeIndex: getNodeIndex,
927 getNodeLength: getNodeLength,
928 getCommonAncestor: getCommonAncestor,
929 isAncestorOf: isAncestorOf,
930 isOrIsAncestorOf: isOrIsAncestorOf,
931 getClosestAncestorIn: getClosestAncestorIn,
932 isCharacterDataNode: isCharacterDataNode,
933 isTextOrCommentNode: isTextOrCommentNode,
934 insertAfter: insertAfter,
935 splitDataNode: splitDataNode,
936 getDocument: getDocument,
937 getWindow: getWindow,
938 getIframeWindow: getIframeWindow,
939 getIframeDocument: getIframeDocument,
940 getBody: getBody,
941 isWindow: isWindow,
942 getContentDocument: getContentDocument,
943 getRootContainer: getRootContainer,
944 comparePoints: comparePoints,
945 isBrokenNode: isBrokenNode,
946 inspectNode: inspectNode,
947 getComputedStyleProperty: getComputedStyleProperty,
948 createTestElement: createTestElement,
949 removeNode: removeNode,
950 fragmentFromNodeChildren: fragmentFromNodeChildren,
951 createIterator: createIterator,
952 DomPosition: DomPosition
953 };
d321f68b 954
0fde7efa
DW
955 api.DOMException = DOMException;
956 });\r
957\r
958 /*----------------------------------------------------------------------------------------------------------------*/\r
959\r
960 // Pure JavaScript implementation of DOM Range
961 api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
962 var dom = api.dom;
963 var util = api.util;
964 var DomPosition = dom.DomPosition;
965 var DOMException = api.DOMException;
966
967 var isCharacterDataNode = dom.isCharacterDataNode;
968 var getNodeIndex = dom.getNodeIndex;
969 var isOrIsAncestorOf = dom.isOrIsAncestorOf;
970 var getDocument = dom.getDocument;
971 var comparePoints = dom.comparePoints;
972 var splitDataNode = dom.splitDataNode;
973 var getClosestAncestorIn = dom.getClosestAncestorIn;
974 var getNodeLength = dom.getNodeLength;
975 var arrayContains = dom.arrayContains;
976 var getRootContainer = dom.getRootContainer;
977 var crashyTextNodes = api.features.crashyTextNodes;
978
979 var removeNode = dom.removeNode;
980
981 /*----------------------------------------------------------------------------------------------------------------*/
982
983 // Utility functions
984
985 function isNonTextPartiallySelected(node, range) {
986 return (node.nodeType != 3) &&
987 (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
d321f68b 988 }
d321f68b 989
0fde7efa
DW
990 function getRangeDocument(range) {
991 return range.document || getDocument(range.startContainer);
992 }
d321f68b 993
0fde7efa
DW
994 function getRangeRoot(range) {
995 return getRootContainer(range.startContainer);
996 }
d321f68b 997
0fde7efa
DW
998 function getBoundaryBeforeNode(node) {
999 return new DomPosition(node.parentNode, getNodeIndex(node));
1000 }
d321f68b 1001
0fde7efa
DW
1002 function getBoundaryAfterNode(node) {
1003 return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1004 }
d321f68b 1005
0fde7efa
DW
1006 function insertNodeAtPosition(node, n, o) {
1007 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1008 if (isCharacterDataNode(n)) {
1009 if (o == n.length) {
1010 dom.insertAfter(node, n);
1011 } else {
1012 n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1013 }
1014 } else if (o >= n.childNodes.length) {
1015 n.appendChild(node);
1016 } else {
1017 n.insertBefore(node, n.childNodes[o]);
d321f68b 1018 }
0fde7efa
DW
1019 return firstNodeInserted;
1020 }
d321f68b 1021
0fde7efa
DW
1022 function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1023 assertRangeValid(rangeA);
1024 assertRangeValid(rangeB);
d321f68b 1025
0fde7efa
DW
1026 if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1027 throw new DOMException("WRONG_DOCUMENT_ERR");
d321f68b
DW
1028 }
1029
0fde7efa
DW
1030 var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1031 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
d321f68b 1032
0fde7efa
DW
1033 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1034 }
d321f68b 1035
0fde7efa
DW
1036 function cloneSubtree(iterator) {
1037 var partiallySelected;
1038 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1039 partiallySelected = iterator.isPartiallySelectedSubtree();
1040 node = node.cloneNode(!partiallySelected);
1041 if (partiallySelected) {
1042 subIterator = iterator.getSubtreeIterator();
1043 node.appendChild(cloneSubtree(subIterator));
1044 subIterator.detach();
1045 }
d321f68b 1046
0fde7efa
DW
1047 if (node.nodeType == 10) { // DocumentType
1048 throw new DOMException("HIERARCHY_REQUEST_ERR");
1049 }
1050 frag.appendChild(node);
1051 }
1052 return frag;
1053 }
d321f68b 1054
0fde7efa
DW
1055 function iterateSubtree(rangeIterator, func, iteratorState) {
1056 var it, n;
1057 iteratorState = iteratorState || { stop: false };
1058 for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1059 if (rangeIterator.isPartiallySelectedSubtree()) {
1060 if (func(node) === false) {
1061 iteratorState.stop = true;
1062 return;
1063 } else {
1064 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1065 // the node selected by the Range.
1066 subRangeIterator = rangeIterator.getSubtreeIterator();
1067 iterateSubtree(subRangeIterator, func, iteratorState);
1068 subRangeIterator.detach();
1069 if (iteratorState.stop) {
1070 return;
1071 }
1072 }
1073 } else {
1074 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1075 // descendants
1076 it = dom.createIterator(node);
1077 while ( (n = it.next()) ) {
1078 if (func(n) === false) {
1079 iteratorState.stop = true;
1080 return;
1081 }
1082 }
1083 }
1084 }
1085 }
d321f68b 1086
0fde7efa
DW
1087 function deleteSubtree(iterator) {
1088 var subIterator;
1089 while (iterator.next()) {
1090 if (iterator.isPartiallySelectedSubtree()) {
1091 subIterator = iterator.getSubtreeIterator();
1092 deleteSubtree(subIterator);
1093 subIterator.detach();
1094 } else {
1095 iterator.remove();
1096 }
1097 }
1098 }
d321f68b 1099
0fde7efa
DW
1100 function extractSubtree(iterator) {
1101 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
d321f68b 1102
0fde7efa
DW
1103 if (iterator.isPartiallySelectedSubtree()) {
1104 node = node.cloneNode(false);
1105 subIterator = iterator.getSubtreeIterator();
1106 node.appendChild(extractSubtree(subIterator));
1107 subIterator.detach();
1108 } else {
1109 iterator.remove();
1110 }
1111 if (node.nodeType == 10) { // DocumentType
1112 throw new DOMException("HIERARCHY_REQUEST_ERR");
1113 }
1114 frag.appendChild(node);
1115 }
1116 return frag;
1117 }
d321f68b 1118
0fde7efa
DW
1119 function getNodesInRange(range, nodeTypes, filter) {
1120 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1121 var filterExists = !!filter;
1122 if (filterNodeTypes) {
1123 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1124 }
d321f68b 1125
0fde7efa
DW
1126 var nodes = [];
1127 iterateSubtree(new RangeIterator(range, false), function(node) {
1128 if (filterNodeTypes && !regex.test(node.nodeType)) {
1129 return;
1130 }
1131 if (filterExists && !filter(node)) {
1132 return;
1133 }
1134 // Don't include a boundary container if it is a character data node and the range does not contain any
1135 // of its character data. See issue 190.
1136 var sc = range.startContainer;
1137 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1138 return;
1139 }
d321f68b 1140
0fde7efa
DW
1141 var ec = range.endContainer;
1142 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1143 return;
1144 }
d321f68b 1145
0fde7efa
DW
1146 nodes.push(node);
1147 });
1148 return nodes;
1149 }
d321f68b 1150
0fde7efa
DW
1151 function inspect(range) {
1152 var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1153 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1154 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1155 }
d321f68b 1156
0fde7efa 1157 /*----------------------------------------------------------------------------------------------------------------*/
d321f68b 1158
0fde7efa 1159 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
d321f68b 1160
0fde7efa
DW
1161 function RangeIterator(range, clonePartiallySelectedTextNodes) {
1162 this.range = range;
1163 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
d321f68b 1164
d321f68b 1165
0fde7efa
DW
1166 if (!range.collapsed) {
1167 this.sc = range.startContainer;
1168 this.so = range.startOffset;
1169 this.ec = range.endContainer;
1170 this.eo = range.endOffset;
1171 var root = range.commonAncestorContainer;
d321f68b 1172
0fde7efa
DW
1173 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1174 this.isSingleCharacterDataNode = true;
1175 this._first = this._last = this._next = this.sc;
d321f68b 1176 } else {
0fde7efa
DW
1177 this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1178 this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1179 this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1180 this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
d321f68b 1181 }
d321f68b 1182 }
0fde7efa 1183 }
d321f68b 1184
0fde7efa
DW
1185 RangeIterator.prototype = {
1186 _current: null,
1187 _next: null,
1188 _first: null,
1189 _last: null,
1190 isSingleCharacterDataNode: false,
d321f68b 1191
0fde7efa
DW
1192 reset: function() {
1193 this._current = null;
1194 this._next = this._first;
1195 },
d321f68b 1196
0fde7efa
DW
1197 hasNext: function() {
1198 return !!this._next;
1199 },
d321f68b 1200
0fde7efa
DW
1201 next: function() {
1202 // Move to next node
1203 var current = this._current = this._next;
1204 if (current) {
1205 this._next = (current !== this._last) ? current.nextSibling : null;
d321f68b 1206
0fde7efa
DW
1207 // Check for partially selected text nodes
1208 if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1209 if (current === this.ec) {
1210 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1211 }
1212 if (this._current === this.sc) {
1213 (current = current.cloneNode(true)).deleteData(0, this.so);
1214 }
1215 }
d321f68b 1216 }
d321f68b 1217
0fde7efa
DW
1218 return current;
1219 },
d321f68b 1220
0fde7efa
DW
1221 remove: function() {
1222 var current = this._current, start, end;
d321f68b 1223
0fde7efa
DW
1224 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1225 start = (current === this.sc) ? this.so : 0;
1226 end = (current === this.ec) ? this.eo : current.length;
1227 if (start != end) {
1228 current.deleteData(start, end - start);
1229 }
1230 } else {
1231 if (current.parentNode) {
1232 removeNode(current);
1233 } else {
1234 }
1235 }
1236 },
d321f68b 1237
0fde7efa
DW
1238 // Checks if the current node is partially selected
1239 isPartiallySelectedSubtree: function() {
1240 var current = this._current;
1241 return isNonTextPartiallySelected(current, this.range);
1242 },
d321f68b 1243
0fde7efa
DW
1244 getSubtreeIterator: function() {
1245 var subRange;
1246 if (this.isSingleCharacterDataNode) {
1247 subRange = this.range.cloneRange();
1248 subRange.collapse(false);
1249 } else {
1250 subRange = new Range(getRangeDocument(this.range));
1251 var current = this._current;
1252 var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
d321f68b 1253
0fde7efa
DW
1254 if (isOrIsAncestorOf(current, this.sc)) {
1255 startContainer = this.sc;
1256 startOffset = this.so;
d321f68b 1257 }
0fde7efa
DW
1258 if (isOrIsAncestorOf(current, this.ec)) {
1259 endContainer = this.ec;
1260 endOffset = this.eo;
1261 }
1262
1263 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1264 }
1265 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1266 },
1267
1268 detach: function() {
1269 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
d321f68b 1270 }
0fde7efa 1271 };
d321f68b 1272
0fde7efa
DW
1273 /*----------------------------------------------------------------------------------------------------------------*/
1274
1275 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1276 var rootContainerNodeTypes = [2, 9, 11];
1277 var readonlyNodeTypes = [5, 6, 10, 12];
1278 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1279 var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1280
1281 function createAncestorFinder(nodeTypes) {
1282 return function(node, selfIsAncestor) {
1283 var t, n = selfIsAncestor ? node : node.parentNode;
1284 while (n) {
1285 t = n.nodeType;
1286 if (arrayContains(nodeTypes, t)) {
1287 return n;
1288 }
1289 n = n.parentNode;
1290 }
1291 return null;
1292 };
1293 }
d321f68b 1294
0fde7efa
DW
1295 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1296 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1297 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
d321f68b 1298
0fde7efa
DW
1299 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1300 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1301 throw new DOMException("INVALID_NODE_TYPE_ERR");
1302 }
1303 }
d321f68b 1304
0fde7efa
DW
1305 function assertValidNodeType(node, invalidTypes) {
1306 if (!arrayContains(invalidTypes, node.nodeType)) {
1307 throw new DOMException("INVALID_NODE_TYPE_ERR");
d321f68b 1308 }
0fde7efa 1309 }
d321f68b 1310
0fde7efa
DW
1311 function assertValidOffset(node, offset) {
1312 if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1313 throw new DOMException("INDEX_SIZE_ERR");
1314 }
1315 }
d321f68b 1316
0fde7efa
DW
1317 function assertSameDocumentOrFragment(node1, node2) {
1318 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1319 throw new DOMException("WRONG_DOCUMENT_ERR");
d321f68b 1320 }
0fde7efa 1321 }
d321f68b 1322
0fde7efa
DW
1323 function assertNodeNotReadOnly(node) {
1324 if (getReadonlyAncestor(node, true)) {
1325 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1326 }
1327 }
d321f68b 1328
0fde7efa
DW
1329 function assertNode(node, codeName) {
1330 if (!node) {
1331 throw new DOMException(codeName);
d321f68b 1332 }
0fde7efa 1333 }
d321f68b 1334
0fde7efa
DW
1335 function isValidOffset(node, offset) {
1336 return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1337 }
d321f68b 1338
0fde7efa
DW
1339 function isRangeValid(range) {
1340 return (!!range.startContainer && !!range.endContainer &&
1341 !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
1342 getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
1343 isValidOffset(range.startContainer, range.startOffset) &&
1344 isValidOffset(range.endContainer, range.endOffset));
1345 }
1346
1347 function assertRangeValid(range) {
1348 if (!isRangeValid(range)) {
1349 throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
d321f68b 1350 }
0fde7efa 1351 }
d321f68b 1352
0fde7efa 1353 /*----------------------------------------------------------------------------------------------------------------*/
d321f68b 1354
0fde7efa
DW
1355 // Test the browser's innerHTML support to decide how to implement createContextualFragment
1356 var styleEl = document.createElement("style");
1357 var htmlParsingConforms = false;
1358 try {
1359 styleEl.innerHTML = "<b>x</b>";
1360 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1361 } catch (e) {
1362 // IE 6 and 7 throw
1363 }
d321f68b 1364
0fde7efa 1365 api.features.htmlParsingConforms = htmlParsingConforms;
d321f68b 1366
0fde7efa 1367 var createContextualFragment = htmlParsingConforms ?
d321f68b 1368
0fde7efa
DW
1369 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1370 // discussion and base code for this implementation at issue 67.
1371 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1372 // Thanks to Aleks Williams.
1373 function(fragmentStr) {
1374 // "Let node the context object's start's node."
1375 var node = this.startContainer;
1376 var doc = getDocument(node);
d321f68b 1377
0fde7efa
DW
1378 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1379 // exception and abort these steps."
1380 if (!node) {
1381 throw new DOMException("INVALID_STATE_ERR");
1382 }
d321f68b 1383
0fde7efa
DW
1384 // "Let element be as follows, depending on node's interface:"
1385 // Document, Document Fragment: null
1386 var el = null;
d321f68b 1387
0fde7efa
DW
1388 // "Element: node"
1389 if (node.nodeType == 1) {
1390 el = node;
d321f68b 1391
0fde7efa
DW
1392 // "Text, Comment: node's parentElement"
1393 } else if (isCharacterDataNode(node)) {
1394 el = dom.parentElement(node);
1395 }
d321f68b 1396
0fde7efa
DW
1397 // "If either element is null or element's ownerDocument is an HTML document
1398 // and element's local name is "html" and element's namespace is the HTML
1399 // namespace"
1400 if (el === null || (
1401 el.nodeName == "HTML" &&
1402 dom.isHtmlNamespace(getDocument(el).documentElement) &&
1403 dom.isHtmlNamespace(el)
1404 )) {
1405
1406 // "let element be a new Element with "body" as its local name and the HTML
1407 // namespace as its namespace.""
1408 el = doc.createElement("body");
1409 } else {
1410 el = el.cloneNode(false);
1411 }
d321f68b 1412
0fde7efa
DW
1413 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1414 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1415 // "In either case, the algorithm must be invoked with fragment as the input
1416 // and element as the context element."
1417 el.innerHTML = fragmentStr;
1418
1419 // "If this raises an exception, then abort these steps. Otherwise, let new
1420 // children be the nodes returned."
1421
1422 // "Let fragment be a new DocumentFragment."
1423 // "Append all new children to fragment."
1424 // "Return fragment."
1425 return dom.fragmentFromNodeChildren(el);
1426 } :
1427
1428 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1429 // previous versions of Rangy used (with the exception of using a body element rather than a div)
1430 function(fragmentStr) {
1431 var doc = getRangeDocument(this);
1432 var el = doc.createElement("body");
1433 el.innerHTML = fragmentStr;
1434
1435 return dom.fragmentFromNodeChildren(el);
1436 };
d321f68b 1437
0fde7efa
DW
1438 function splitRangeBoundaries(range, positionsToPreserve) {
1439 assertRangeValid(range);
d321f68b 1440
0fde7efa
DW
1441 var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1442 var startEndSame = (sc === ec);
d321f68b 1443
0fde7efa
DW
1444 if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1445 splitDataNode(ec, eo, positionsToPreserve);
d321f68b 1446 }
d321f68b 1447
0fde7efa
DW
1448 if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1449 sc = splitDataNode(sc, so, positionsToPreserve);
1450 if (startEndSame) {
1451 eo -= so;
1452 ec = sc;
1453 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1454 eo++;
d321f68b 1455 }
0fde7efa 1456 so = 0;
d321f68b 1457 }
0fde7efa
DW
1458 range.setStartAndEnd(sc, so, ec, eo);
1459 }
d321f68b 1460
0fde7efa
DW
1461 function rangeToHtml(range) {
1462 assertRangeValid(range);
1463 var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1464 container.appendChild( range.cloneContents() );
1465 return container.innerHTML;
1466 }
d321f68b 1467
0fde7efa 1468 /*----------------------------------------------------------------------------------------------------------------*/
d321f68b 1469
0fde7efa
DW
1470 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1471 "commonAncestorContainer"];
d321f68b 1472
0fde7efa
DW
1473 var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1474 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
d321f68b 1475
0fde7efa
DW
1476 util.extend(api.rangePrototype, {
1477 compareBoundaryPoints: function(how, range) {
1478 assertRangeValid(this);
1479 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1480
1481 var nodeA, offsetA, nodeB, offsetB;
1482 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1483 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1484 nodeA = this[prefixA + "Container"];
1485 offsetA = this[prefixA + "Offset"];
1486 nodeB = range[prefixB + "Container"];
1487 offsetB = range[prefixB + "Offset"];
1488 return comparePoints(nodeA, offsetA, nodeB, offsetB);
1489 },
d321f68b 1490
0fde7efa
DW
1491 insertNode: function(node) {
1492 assertRangeValid(this);
1493 assertValidNodeType(node, insertableNodeTypes);
1494 assertNodeNotReadOnly(this.startContainer);
d321f68b 1495
0fde7efa
DW
1496 if (isOrIsAncestorOf(node, this.startContainer)) {
1497 throw new DOMException("HIERARCHY_REQUEST_ERR");
1498 }
d321f68b 1499
0fde7efa
DW
1500 // No check for whether the container of the start of the Range is of a type that does not allow
1501 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1502 // to add the node
d321f68b 1503
0fde7efa
DW
1504 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1505 this.setStartBefore(firstNodeInserted);
1506 },
d321f68b 1507
0fde7efa
DW
1508 cloneContents: function() {
1509 assertRangeValid(this);
d321f68b 1510
0fde7efa
DW
1511 var clone, frag;
1512 if (this.collapsed) {
1513 return getRangeDocument(this).createDocumentFragment();
1514 } else {
1515 if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
1516 clone = this.startContainer.cloneNode(true);
1517 clone.data = clone.data.slice(this.startOffset, this.endOffset);
1518 frag = getRangeDocument(this).createDocumentFragment();
1519 frag.appendChild(clone);
1520 return frag;
1521 } else {
1522 var iterator = new RangeIterator(this, true);
1523 clone = cloneSubtree(iterator);
1524 iterator.detach();
1525 }
1526 return clone;
1527 }
1528 },
d321f68b 1529
0fde7efa
DW
1530 canSurroundContents: function() {
1531 assertRangeValid(this);
1532 assertNodeNotReadOnly(this.startContainer);
1533 assertNodeNotReadOnly(this.endContainer);
d321f68b 1534
0fde7efa
DW
1535 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1536 // no non-text nodes.
1537 var iterator = new RangeIterator(this, true);
1538 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1539 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1540 iterator.detach();
1541 return !boundariesInvalid;
1542 },
d321f68b 1543
0fde7efa
DW
1544 surroundContents: function(node) {
1545 assertValidNodeType(node, surroundNodeTypes);
d321f68b 1546
0fde7efa
DW
1547 if (!this.canSurroundContents()) {
1548 throw new DOMException("INVALID_STATE_ERR");
1549 }
d321f68b 1550
0fde7efa
DW
1551 // Extract the contents
1552 var content = this.extractContents();
d321f68b 1553
0fde7efa
DW
1554 // Clear the children of the node
1555 if (node.hasChildNodes()) {
1556 while (node.lastChild) {
1557 node.removeChild(node.lastChild);
1558 }
1559 }
d321f68b 1560
0fde7efa
DW
1561 // Insert the new node and add the extracted contents
1562 insertNodeAtPosition(node, this.startContainer, this.startOffset);
1563 node.appendChild(content);
d321f68b 1564
0fde7efa
DW
1565 this.selectNode(node);
1566 },
d321f68b 1567
0fde7efa
DW
1568 cloneRange: function() {
1569 assertRangeValid(this);
1570 var range = new Range(getRangeDocument(this));
1571 var i = rangeProperties.length, prop;
1572 while (i--) {
1573 prop = rangeProperties[i];
1574 range[prop] = this[prop];
1575 }
1576 return range;
1577 },
d321f68b 1578
0fde7efa
DW
1579 toString: function() {
1580 assertRangeValid(this);
1581 var sc = this.startContainer;
1582 if (sc === this.endContainer && isCharacterDataNode(sc)) {
1583 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1584 } else {
1585 var textParts = [], iterator = new RangeIterator(this, true);
1586 iterateSubtree(iterator, function(node) {
1587 // Accept only text or CDATA nodes, not comments
1588 if (node.nodeType == 3 || node.nodeType == 4) {
1589 textParts.push(node.data);
1590 }
1591 });
1592 iterator.detach();
1593 return textParts.join("");
1594 }
1595 },
d321f68b 1596
0fde7efa
DW
1597 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1598 // been removed from Mozilla.
d321f68b 1599
0fde7efa
DW
1600 compareNode: function(node) {
1601 assertRangeValid(this);
d321f68b 1602
0fde7efa
DW
1603 var parent = node.parentNode;
1604 var nodeIndex = getNodeIndex(node);
d321f68b 1605
0fde7efa
DW
1606 if (!parent) {
1607 throw new DOMException("NOT_FOUND_ERR");
1608 }
d321f68b 1609
0fde7efa
DW
1610 var startComparison = this.comparePoint(parent, nodeIndex),
1611 endComparison = this.comparePoint(parent, nodeIndex + 1);
d321f68b 1612
0fde7efa
DW
1613 if (startComparison < 0) { // Node starts before
1614 return (endComparison > 0) ? n_b_a : n_b;
1615 } else {
1616 return (endComparison > 0) ? n_a : n_i;
d321f68b 1617 }
0fde7efa
DW
1618 },
1619
1620 comparePoint: function(node, offset) {
1621 assertRangeValid(this);
1622 assertNode(node, "HIERARCHY_REQUEST_ERR");
1623 assertSameDocumentOrFragment(node, this.startContainer);
d321f68b 1624
0fde7efa
DW
1625 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1626 return -1;
1627 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1628 return 1;
d321f68b 1629 }
0fde7efa
DW
1630 return 0;
1631 },
d321f68b 1632
0fde7efa 1633 createContextualFragment: createContextualFragment,
d321f68b 1634
0fde7efa
DW
1635 toHtml: function() {
1636 return rangeToHtml(this);
1637 },
d321f68b 1638
0fde7efa
DW
1639 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1640 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1641 intersectsNode: function(node, touchingIsIntersecting) {
1642 assertRangeValid(this);
1643 if (getRootContainer(node) != getRangeRoot(this)) {
1644 return false;
1645 }
d321f68b 1646
0fde7efa
DW
1647 var parent = node.parentNode, offset = getNodeIndex(node);
1648 if (!parent) {
1649 return true;
1650 }
d321f68b 1651
0fde7efa
DW
1652 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1653 endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
d321f68b 1654
0fde7efa 1655 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
d321f68b
DW
1656 },
1657
0fde7efa 1658 isPointInRange: function(node, offset) {
d321f68b 1659 assertRangeValid(this);
0fde7efa
DW
1660 assertNode(node, "HIERARCHY_REQUEST_ERR");
1661 assertSameDocumentOrFragment(node, this.startContainer);
1662
1663 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1664 (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
d321f68b
DW
1665 },
1666
0fde7efa
DW
1667 // The methods below are non-standard and invented by me.
1668
1669 // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1670 intersectsRange: function(range) {
1671 return rangesIntersect(this, range, false);
1672 },
d321f68b 1673
0fde7efa
DW
1674 // Sharing a boundary start-to-end or end-to-start does count as intersection.
1675 intersectsOrTouchesRange: function(range) {
1676 return rangesIntersect(this, range, true);
d321f68b
DW
1677 },
1678
0fde7efa
DW
1679 intersection: function(range) {
1680 if (this.intersectsRange(range)) {
1681 var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1682 endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
d321f68b 1683
0fde7efa
DW
1684 var intersectionRange = this.cloneRange();
1685 if (startComparison == -1) {
1686 intersectionRange.setStart(range.startContainer, range.startOffset);
1687 }
1688 if (endComparison == 1) {
1689 intersectionRange.setEnd(range.endContainer, range.endOffset);
1690 }
1691 return intersectionRange;
1692 }
1693 return null;
d321f68b
DW
1694 },
1695
0fde7efa
DW
1696 union: function(range) {
1697 if (this.intersectsOrTouchesRange(range)) {
1698 var unionRange = this.cloneRange();
1699 if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1700 unionRange.setStart(range.startContainer, range.startOffset);
1701 }
1702 if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1703 unionRange.setEnd(range.endContainer, range.endOffset);
1704 }
1705 return unionRange;
1706 } else {
1707 throw new DOMException("Ranges do not intersect");
1708 }
1709 },
d321f68b 1710
0fde7efa
DW
1711 containsNode: function(node, allowPartial) {
1712 if (allowPartial) {
1713 return this.intersectsNode(node, false);
1714 } else {
1715 return this.compareNode(node) == n_i;
1716 }
1717 },
d321f68b 1718
0fde7efa
DW
1719 containsNodeContents: function(node) {
1720 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1721 },
d321f68b 1722
0fde7efa
DW
1723 containsRange: function(range) {
1724 var intersection = this.intersection(range);
1725 return intersection !== null && range.equals(intersection);
d321f68b
DW
1726 },
1727
0fde7efa
DW
1728 containsNodeText: function(node) {
1729 var nodeRange = this.cloneRange();
1730 nodeRange.selectNode(node);
1731 var textNodes = nodeRange.getNodes([3]);
1732 if (textNodes.length > 0) {
1733 nodeRange.setStart(textNodes[0], 0);
1734 var lastTextNode = textNodes.pop();
1735 nodeRange.setEnd(lastTextNode, lastTextNode.length);
1736 return this.containsRange(nodeRange);
1737 } else {
1738 return this.containsNodeContents(node);
1739 }
d321f68b
DW
1740 },
1741
0fde7efa 1742 getNodes: function(nodeTypes, filter) {
d321f68b 1743 assertRangeValid(this);
0fde7efa
DW
1744 return getNodesInRange(this, nodeTypes, filter);
1745 },
d321f68b 1746
0fde7efa
DW
1747 getDocument: function() {
1748 return getRangeDocument(this);
1749 },
d321f68b 1750
0fde7efa
DW
1751 collapseBefore: function(node) {
1752 this.setEndBefore(node);
1753 this.collapse(false);
1754 },
d321f68b 1755
0fde7efa
DW
1756 collapseAfter: function(node) {
1757 this.setStartAfter(node);
1758 this.collapse(true);
1759 },
d321f68b 1760
0fde7efa
DW
1761 getBookmark: function(containerNode) {
1762 var doc = getRangeDocument(this);
1763 var preSelectionRange = api.createRange(doc);
1764 containerNode = containerNode || dom.getBody(doc);
1765 preSelectionRange.selectNodeContents(containerNode);
1766 var range = this.intersection(preSelectionRange);
1767 var start = 0, end = 0;
1768 if (range) {
1769 preSelectionRange.setEnd(range.startContainer, range.startOffset);
1770 start = preSelectionRange.toString().length;
1771 end = start + range.toString().length;
d321f68b
DW
1772 }
1773
0fde7efa
DW
1774 return {
1775 start: start,
1776 end: end,
1777 containerNode: containerNode
1778 };
1779 },
d321f68b 1780
0fde7efa
DW
1781 moveToBookmark: function(bookmark) {
1782 var containerNode = bookmark.containerNode;
1783 var charIndex = 0;
1784 this.setStart(containerNode, 0);
1785 this.collapse(true);
1786 var nodeStack = [containerNode], node, foundStart = false, stop = false;
1787 var nextCharIndex, i, childNodes;
1788
1789 while (!stop && (node = nodeStack.pop())) {
1790 if (node.nodeType == 3) {
1791 nextCharIndex = charIndex + node.length;
1792 if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
1793 this.setStart(node, bookmark.start - charIndex);
1794 foundStart = true;
1795 }
1796 if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1797 this.setEnd(node, bookmark.end - charIndex);
1798 stop = true;
1799 }
1800 charIndex = nextCharIndex;
1801 } else {
1802 childNodes = node.childNodes;
1803 i = childNodes.length;
1804 while (i--) {
1805 nodeStack.push(childNodes[i]);
1806 }
d321f68b 1807 }
d321f68b 1808 }
d321f68b
DW
1809 },
1810
0fde7efa
DW
1811 getName: function() {
1812 return "DomRange";
1813 },
1814
1815 equals: function(range) {
1816 return Range.rangesEqual(this, range);
1817 },
1818
1819 isValid: function() {
1820 return isRangeValid(this);
1821 },
1822
1823 inspect: function() {
1824 return inspect(this);
1825 },
1826
1827 detach: function() {
1828 // In DOM4, detach() is now a no-op.
1829 }
1830 });
1831
1832 function copyComparisonConstantsToObject(obj) {
1833 obj.START_TO_START = s2s;
1834 obj.START_TO_END = s2e;
1835 obj.END_TO_END = e2e;
1836 obj.END_TO_START = e2s;
1837
1838 obj.NODE_BEFORE = n_b;
1839 obj.NODE_AFTER = n_a;
1840 obj.NODE_BEFORE_AND_AFTER = n_b_a;
1841 obj.NODE_INSIDE = n_i;
1842 }
1843
1844 function copyComparisonConstants(constructor) {
1845 copyComparisonConstantsToObject(constructor);
1846 copyComparisonConstantsToObject(constructor.prototype);
1847 }
1848
1849 function createRangeContentRemover(remover, boundaryUpdater) {
1850 return function() {
d321f68b
DW
1851 assertRangeValid(this);
1852
0fde7efa
DW
1853 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1854
1855 var iterator = new RangeIterator(this, true);
1856
1857 // Work out where to position the range after content removal
1858 var node, boundary;
1859 if (sc !== root) {
1860 node = getClosestAncestorIn(sc, root, true);
1861 boundary = getBoundaryAfterNode(node);
1862 sc = boundary.node;
1863 so = boundary.offset;
1864 }
1865
1866 // Check none of the range is read-only
1867 iterateSubtree(iterator, assertNodeNotReadOnly);
1868
1869 iterator.reset();
1870
1871 // Remove the content
1872 var returnValue = remover(iterator);
1873 iterator.detach();
1874
1875 // Move to the new position
1876 boundaryUpdater(this, sc, so, sc, so);
1877
1878 return returnValue;
1879 };
1880 }
1881
1882 function createPrototypeRange(constructor, boundaryUpdater) {
1883 function createBeforeAfterNodeSetter(isBefore, isStart) {
1884 return function(node) {
1885 assertValidNodeType(node, beforeAfterNodeTypes);
1886 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1887
1888 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1889 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1890 };
1891 }
d321f68b 1892
0fde7efa
DW
1893 function setRangeStart(range, node, offset) {
1894 var ec = range.endContainer, eo = range.endOffset;
1895 if (node !== range.startContainer || offset !== range.startOffset) {
1896 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1897 // is after the current end. In either case, collapse the range to the new position
1898 if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
d321f68b 1899 ec = node;
0fde7efa 1900 eo = offset;
d321f68b 1901 }
0fde7efa
DW
1902 boundaryUpdater(range, node, offset, ec, eo);
1903 }
1904 }
d321f68b 1905
0fde7efa
DW
1906 function setRangeEnd(range, node, offset) {
1907 var sc = range.startContainer, so = range.startOffset;
1908 if (node !== range.endContainer || offset !== range.endOffset) {
1909 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1910 // is after the current end. In either case, collapse the range to the new position
1911 if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
d321f68b 1912 sc = node;
0fde7efa 1913 so = offset;
d321f68b 1914 }
0fde7efa
DW
1915 boundaryUpdater(range, sc, so, node, offset);
1916 }
1917 }
d321f68b 1918
0fde7efa
DW
1919 // Set up inheritance
1920 var F = function() {};
1921 F.prototype = api.rangePrototype;
1922 constructor.prototype = new F();
1923
1924 util.extend(constructor.prototype, {
1925 setStart: function(node, offset) {
1926 assertNoDocTypeNotationEntityAncestor(node, true);
1927 assertValidOffset(node, offset);
1928
1929 setRangeStart(this, node, offset);
1930 },
1931
1932 setEnd: function(node, offset) {
1933 assertNoDocTypeNotationEntityAncestor(node, true);
1934 assertValidOffset(node, offset);
1935
1936 setRangeEnd(this, node, offset);
1937 },
1938
1939 /**
1940 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
1941 * - Two parameters (node, offset) creates a collapsed range at that position
1942 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
1943 * startOffset and ending at endOffset
1944 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
1945 * startNode and ending at endOffset in endNode
1946 */
1947 setStartAndEnd: function() {
1948 var args = arguments;
1949 var sc = args[0], so = args[1], ec = sc, eo = so;
1950
1951 switch (args.length) {
1952 case 3:
1953 eo = args[2];
1954 break;
1955 case 4:
1956 ec = args[2];
1957 eo = args[3];
1958 break;
d321f68b 1959 }
0fde7efa
DW
1960
1961 boundaryUpdater(this, sc, so, ec, eo);
1962 },
1963
1964 setBoundary: function(node, offset, isStart) {
1965 this["set" + (isStart ? "Start" : "End")](node, offset);
1966 },
1967
1968 setStartBefore: createBeforeAfterNodeSetter(true, true),
1969 setStartAfter: createBeforeAfterNodeSetter(false, true),
1970 setEndBefore: createBeforeAfterNodeSetter(true, false),
1971 setEndAfter: createBeforeAfterNodeSetter(false, false),
1972
1973 collapse: function(isStart) {
1974 assertRangeValid(this);
1975 if (isStart) {
1976 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1977 } else {
1978 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
d321f68b 1979 }
0fde7efa
DW
1980 },
1981
1982 selectNodeContents: function(node) {
1983 assertNoDocTypeNotationEntityAncestor(node, true);
1984
1985 boundaryUpdater(this, node, 0, node, getNodeLength(node));
1986 },
1987
1988 selectNode: function(node) {
1989 assertNoDocTypeNotationEntityAncestor(node, false);
1990 assertValidNodeType(node, beforeAfterNodeTypes);
1991
1992 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
1993 boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
1994 },
1995
1996 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
1997
1998 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
1999
2000 canSurroundContents: function() {
2001 assertRangeValid(this);
2002 assertNodeNotReadOnly(this.startContainer);
2003 assertNodeNotReadOnly(this.endContainer);
2004
2005 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2006 // no non-text nodes.
2007 var iterator = new RangeIterator(this, true);
2008 var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2009 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2010 iterator.detach();
2011 return !boundariesInvalid;
2012 },
2013
2014 splitBoundaries: function() {
2015 splitRangeBoundaries(this);
2016 },
2017
2018 splitBoundariesPreservingPositions: function(positionsToPreserve) {
2019 splitRangeBoundaries(this, positionsToPreserve);
2020 },
2021
2022 normalizeBoundaries: function() {
2023 assertRangeValid(this);
2024
2025 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
d321f68b 2026
0fde7efa
DW
2027 var mergeForward = function(node) {
2028 var sibling = node.nextSibling;
2029 if (sibling && sibling.nodeType == node.nodeType) {
2030 ec = node;
2031 eo = node.length;
2032 node.appendData(sibling.data);
2033 removeNode(sibling);
2034 }
2035 };
2036
2037 var mergeBackward = function(node) {
2038 var sibling = node.previousSibling;
2039 if (sibling && sibling.nodeType == node.nodeType) {
2040 sc = node;
2041 var nodeLength = node.length;
2042 so = sibling.length;
2043 node.insertData(0, sibling.data);
2044 removeNode(sibling);
2045 if (sc == ec) {
2046 eo += so;
2047 ec = sc;
2048 } else if (ec == node.parentNode) {
2049 var nodeIndex = getNodeIndex(node);
2050 if (eo == nodeIndex) {
2051 ec = node;
2052 eo = nodeLength;
2053 } else if (eo > nodeIndex) {
2054 eo--;
2055 }
2056 }
2057 }
2058 };
2059
2060 var normalizeStart = true;
2061 var sibling;
2062
2063 if (isCharacterDataNode(ec)) {
2064 if (eo == ec.length) {
2065 mergeForward(ec);
2066 } else if (eo == 0) {
2067 sibling = ec.previousSibling;
2068 if (sibling && sibling.nodeType == ec.nodeType) {
2069 eo = sibling.length;
2070 if (sc == ec) {
2071 normalizeStart = false;
2072 }
2073 sibling.appendData(ec.data);
2074 removeNode(ec);
2075 ec = sibling;
2076 }
d321f68b
DW
2077 }
2078 } else {
0fde7efa
DW
2079 if (eo > 0) {
2080 var endNode = ec.childNodes[eo - 1];
2081 if (endNode && isCharacterDataNode(endNode)) {
2082 mergeForward(endNode);
d321f68b
DW
2083 }
2084 }
0fde7efa 2085 normalizeStart = !this.collapsed;
d321f68b 2086 }
0fde7efa
DW
2087
2088 if (normalizeStart) {
2089 if (isCharacterDataNode(sc)) {
2090 if (so == 0) {
2091 mergeBackward(sc);
2092 } else if (so == sc.length) {
2093 sibling = sc.nextSibling;
2094 if (sibling && sibling.nodeType == sc.nodeType) {
2095 if (ec == sibling) {
2096 ec = sc;
2097 eo += sc.length;
2098 }
2099 sc.appendData(sibling.data);
2100 removeNode(sibling);
2101 }
2102 }
2103 } else {
2104 if (so < sc.childNodes.length) {
2105 var startNode = sc.childNodes[so];
2106 if (startNode && isCharacterDataNode(startNode)) {
2107 mergeBackward(startNode);
2108 }
2109 }
2110 }
2111 } else {
2112 sc = ec;
2113 so = eo;
2114 }
2115
2116 boundaryUpdater(this, sc, so, ec, eo);
2117 },
2118
2119 collapseToPoint: function(node, offset) {
2120 assertNoDocTypeNotationEntityAncestor(node, true);
2121 assertValidOffset(node, offset);
2122 this.setStartAndEnd(node, offset);
d321f68b 2123 }
0fde7efa 2124 });
d321f68b 2125
0fde7efa
DW
2126 copyComparisonConstants(constructor);
2127 }
2128
2129 /*----------------------------------------------------------------------------------------------------------------*/
d321f68b 2130
0fde7efa
DW
2131 // Updates commonAncestorContainer and collapsed after boundary change
2132 function updateCollapsedAndCommonAncestor(range) {
2133 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2134 range.commonAncestorContainer = range.collapsed ?
2135 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2136 }
2137
2138 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2139 range.startContainer = startContainer;
2140 range.startOffset = startOffset;
2141 range.endContainer = endContainer;
2142 range.endOffset = endOffset;
2143 range.document = dom.getDocument(startContainer);
2144
2145 updateCollapsedAndCommonAncestor(range);
2146 }
d321f68b 2147
0fde7efa
DW
2148 function Range(doc) {
2149 this.startContainer = doc;
2150 this.startOffset = 0;
2151 this.endContainer = doc;
2152 this.endOffset = 0;
2153 this.document = doc;
2154 updateCollapsedAndCommonAncestor(this);
2155 }
d321f68b 2156
0fde7efa
DW
2157 createPrototypeRange(Range, updateBoundaries);
2158
2159 util.extend(Range, {
2160 rangeProperties: rangeProperties,
2161 RangeIterator: RangeIterator,
2162 copyComparisonConstants: copyComparisonConstants,
2163 createPrototypeRange: createPrototypeRange,
2164 inspect: inspect,
2165 toHtml: rangeToHtml,
2166 getRangeDocument: getRangeDocument,
2167 rangesEqual: function(r1, r2) {
2168 return r1.startContainer === r2.startContainer &&
2169 r1.startOffset === r2.startOffset &&
2170 r1.endContainer === r2.endContainer &&
2171 r1.endOffset === r2.endOffset;
d321f68b
DW
2172 }
2173 });
2174
0fde7efa
DW
2175 api.DomRange = Range;
2176 });\r
2177\r
2178 /*----------------------------------------------------------------------------------------------------------------*/\r
2179\r
2180 // Wrappers for the browser's native DOM Range and/or TextRange implementation
2181 api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2182 var WrappedRange, WrappedTextRange;
2183 var dom = api.dom;
2184 var util = api.util;
2185 var DomPosition = dom.DomPosition;
2186 var DomRange = api.DomRange;
2187 var getBody = dom.getBody;
2188 var getContentDocument = dom.getContentDocument;
2189 var isCharacterDataNode = dom.isCharacterDataNode;
2190
2191
2192 /*----------------------------------------------------------------------------------------------------------------*/
2193
2194 if (api.features.implementsDomRange) {
2195 // This is a wrapper around the browser's native DOM Range. It has two aims:
2196 // - Provide workarounds for specific browser bugs
2197 // - provide convenient extensions, which are inherited from Rangy's DomRange
2198
2199 (function() {
2200 var rangeProto;
2201 var rangeProperties = DomRange.rangeProperties;
2202
2203 function updateRangeProperties(range) {
2204 var i = rangeProperties.length, prop;
2205 while (i--) {
2206 prop = rangeProperties[i];
2207 range[prop] = range.nativeRange[prop];
2208 }
2209 // Fix for broken collapsed property in IE 9.
2210 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2211 }
d321f68b 2212
0fde7efa
DW
2213 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2214 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2215 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2216 var nativeRangeDifferent = !range.equals(range.nativeRange);
d321f68b 2217
0fde7efa
DW
2218 // Always set both boundaries for the benefit of IE9 (see issue 35)
2219 if (startMoved || endMoved || nativeRangeDifferent) {
2220 range.setEnd(endContainer, endOffset);
2221 range.setStart(startContainer, startOffset);
2222 }
2223 }
d321f68b 2224
0fde7efa 2225 var createBeforeAfterNodeSetter;
d321f68b 2226
0fde7efa
DW
2227 WrappedRange = function(range) {
2228 if (!range) {
2229 throw module.createError("WrappedRange: Range must be specified");
2230 }
2231 this.nativeRange = range;
2232 updateRangeProperties(this);
2233 };
d321f68b 2234
0fde7efa 2235 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
d321f68b 2236
0fde7efa 2237 rangeProto = WrappedRange.prototype;
d321f68b 2238
0fde7efa
DW
2239 rangeProto.selectNode = function(node) {
2240 this.nativeRange.selectNode(node);
2241 updateRangeProperties(this);
2242 };
d321f68b 2243
0fde7efa
DW
2244 rangeProto.cloneContents = function() {
2245 return this.nativeRange.cloneContents();
2246 };
2247
2248 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2249 // insertNode() is never delegated to the native range.
2250
2251 rangeProto.surroundContents = function(node) {
2252 this.nativeRange.surroundContents(node);
2253 updateRangeProperties(this);
2254 };
2255
2256 rangeProto.collapse = function(isStart) {
2257 this.nativeRange.collapse(isStart);
2258 updateRangeProperties(this);
2259 };
2260
2261 rangeProto.cloneRange = function() {
2262 return new WrappedRange(this.nativeRange.cloneRange());
2263 };
2264
2265 rangeProto.refresh = function() {
2266 updateRangeProperties(this);
2267 };
2268
2269 rangeProto.toString = function() {
2270 return this.nativeRange.toString();
2271 };
2272
2273 // Create test range and node for feature detection
2274
2275 var testTextNode = document.createTextNode("test");
2276 getBody(document).appendChild(testTextNode);
2277 var range = document.createRange();
2278
2279 /*--------------------------------------------------------------------------------------------------------*/
2280
2281 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2282 // correct for it
2283
2284 range.setStart(testTextNode, 0);
2285 range.setEnd(testTextNode, 0);
2286
2287 try {
2288 range.setStart(testTextNode, 1);
2289
2290 rangeProto.setStart = function(node, offset) {
2291 this.nativeRange.setStart(node, offset);
2292 updateRangeProperties(this);
2293 };
2294
2295 rangeProto.setEnd = function(node, offset) {
2296 this.nativeRange.setEnd(node, offset);
2297 updateRangeProperties(this);
2298 };
2299
2300 createBeforeAfterNodeSetter = function(name) {
2301 return function(node) {
2302 this.nativeRange[name](node);
2303 updateRangeProperties(this);
2304 };
2305 };
2306
2307 } catch(ex) {
2308
2309 rangeProto.setStart = function(node, offset) {
2310 try {
2311 this.nativeRange.setStart(node, offset);
2312 } catch (ex) {
2313 this.nativeRange.setEnd(node, offset);
2314 this.nativeRange.setStart(node, offset);
2315 }
2316 updateRangeProperties(this);
2317 };
2318
2319 rangeProto.setEnd = function(node, offset) {
2320 try {
2321 this.nativeRange.setEnd(node, offset);
2322 } catch (ex) {
2323 this.nativeRange.setStart(node, offset);
2324 this.nativeRange.setEnd(node, offset);
2325 }
2326 updateRangeProperties(this);
2327 };
2328
2329 createBeforeAfterNodeSetter = function(name, oppositeName) {
2330 return function(node) {
2331 try {
2332 this.nativeRange[name](node);
2333 } catch (ex) {
2334 this.nativeRange[oppositeName](node);
2335 this.nativeRange[name](node);
2336 }
2337 updateRangeProperties(this);
2338 };
2339 };
2340 }
2341
2342 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2343 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2344 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2345 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2346
2347 /*--------------------------------------------------------------------------------------------------------*/
2348
2349 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2350 // whether the native implementation can be trusted
2351 rangeProto.selectNodeContents = function(node) {
2352 this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2353 };
2354
2355 /*--------------------------------------------------------------------------------------------------------*/
2356
2357 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2358 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2359
2360 range.selectNodeContents(testTextNode);
2361 range.setEnd(testTextNode, 3);
2362
2363 var range2 = document.createRange();
2364 range2.selectNodeContents(testTextNode);
2365 range2.setEnd(testTextNode, 4);
2366 range2.setStart(testTextNode, 2);
2367
2368 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2369 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2370 // This is the wrong way round, so correct for it
2371
2372 rangeProto.compareBoundaryPoints = function(type, range) {
2373 range = range.nativeRange || range;
2374 if (type == range.START_TO_END) {
2375 type = range.END_TO_START;
2376 } else if (type == range.END_TO_START) {
2377 type = range.START_TO_END;
2378 }
2379 return this.nativeRange.compareBoundaryPoints(type, range);
2380 };
2381 } else {
2382 rangeProto.compareBoundaryPoints = function(type, range) {
2383 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2384 };
2385 }
2386
2387 /*--------------------------------------------------------------------------------------------------------*/
2388
2389 // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
2390
2391 var el = document.createElement("div");
2392 el.innerHTML = "123";
2393 var textNode = el.firstChild;
2394 var body = getBody(document);
2395 body.appendChild(el);
2396
2397 range.setStart(textNode, 1);
2398 range.setEnd(textNode, 2);
2399 range.deleteContents();
2400
2401 if (textNode.data == "13") {
2402 // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2403 // extractContents()
2404 rangeProto.deleteContents = function() {
2405 this.nativeRange.deleteContents();
2406 updateRangeProperties(this);
2407 };
2408
2409 rangeProto.extractContents = function() {
2410 var frag = this.nativeRange.extractContents();
2411 updateRangeProperties(this);
2412 return frag;
2413 };
2414 } else {
2415 }
2416
2417 body.removeChild(el);
2418 body = null;
2419
2420 /*--------------------------------------------------------------------------------------------------------*/
2421
2422 // Test for existence of createContextualFragment and delegate to it if it exists
2423 if (util.isHostMethod(range, "createContextualFragment")) {
2424 rangeProto.createContextualFragment = function(fragmentStr) {
2425 return this.nativeRange.createContextualFragment(fragmentStr);
2426 };
2427 }
2428
2429 /*--------------------------------------------------------------------------------------------------------*/
2430
2431 // Clean up
2432 getBody(document).removeChild(testTextNode);
2433
2434 rangeProto.getName = function() {
2435 return "WrappedRange";
2436 };
2437
2438 api.WrappedRange = WrappedRange;
2439
2440 api.createNativeRange = function(doc) {
2441 doc = getContentDocument(doc, module, "createNativeRange");
2442 return doc.createRange();
2443 };
2444 })();
2445 }
2446
2447 if (api.features.implementsTextRange) {
2448 /*
2449 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2450 method. For example, in the following (where pipes denote the selection boundaries):
2451
2452 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2453
2454 var range = document.selection.createRange();
2455 alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2456
2457 This method returns the common ancestor node of the following:
2458 - the parentElement() of the textRange
2459 - the parentElement() of the textRange after calling collapse(true)
2460 - the parentElement() of the textRange after calling collapse(false)
2461 */
2462 var getTextRangeContainerElement = function(textRange) {
2463 var parentEl = textRange.parentElement();
2464 var range = textRange.duplicate();
2465 range.collapse(true);
2466 var startEl = range.parentElement();
2467 range = textRange.duplicate();
2468 range.collapse(false);
2469 var endEl = range.parentElement();
2470 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2471
2472 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2473 };
2474
2475 var textRangeIsCollapsed = function(textRange) {
2476 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2477 };
2478
2479 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2480 // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2481 // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2482 // bugs, handling for inputs and images, plus optimizations.
2483 var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2484 var workingRange = textRange.duplicate();
2485 workingRange.collapse(isStart);
2486 var containerElement = workingRange.parentElement();
2487
2488 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2489 // check for that
2490 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2491 containerElement = wholeRangeContainerElement;
2492 }
2493
2494
2495 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2496 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2497 if (!containerElement.canHaveHTML) {
2498 var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2499 return {
2500 boundaryPosition: pos,
2501 nodeInfo: {
2502 nodeIndex: pos.offset,
2503 containerElement: pos.node
2504 }
2505 };
2506 }
2507
2508 var workingNode = dom.getDocument(containerElement).createElement("span");
2509
2510 // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2511 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2512 if (workingNode.parentNode) {
2513 dom.removeNode(workingNode);
2514 }
2515
2516 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
2517 var previousNode, nextNode, boundaryPosition, boundaryNode;
2518 var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
2519 var childNodeCount = containerElement.childNodes.length;
2520 var end = childNodeCount;
2521
2522 // Check end first. Code within the loop assumes that the endth child node of the container is definitely
2523 // after the range boundary.
2524 var nodeIndex = end;
2525
2526 while (true) {
2527 if (nodeIndex == childNodeCount) {
2528 containerElement.appendChild(workingNode);
2529 } else {
2530 containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2531 }
2532 workingRange.moveToElementText(workingNode);
2533 comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2534 if (comparison == 0 || start == end) {
2535 break;
2536 } else if (comparison == -1) {
2537 if (end == start + 1) {
2538 // We know the endth child node is after the range boundary, so we must be done.
2539 break;
2540 } else {
2541 start = nodeIndex;
2542 }
2543 } else {
2544 end = (end == start + 1) ? start : nodeIndex;
2545 }
2546 nodeIndex = Math.floor((start + end) / 2);
2547 containerElement.removeChild(workingNode);
2548 }
2549
2550
2551 // We've now reached or gone past the boundary of the text range we're interested in
2552 // so have identified the node we want
2553 boundaryNode = workingNode.nextSibling;
2554
2555 if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
2556 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
2557 // the node containing the text range's boundary, so we move the end of the working range to the
2558 // boundary point and measure the length of its text to get the boundary's offset within the node.
2559 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
2560
2561 var offset;
2562
2563 if (/[\r\n]/.test(boundaryNode.data)) {
2564 /*
2565 For the particular case of a boundary within a text node containing rendered line breaks (within a
2566 <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2567 IE. The facts:
2568
2569 - Each line break is represented as \r in the text node's data/nodeValue properties
2570 - Each line break is represented as \r\n in the TextRange's 'text' property
2571 - The 'text' property of the TextRange does not contain trailing line breaks
2572
2573 To get round the problem presented by the final fact above, we can use the fact that TextRange's
2574 moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2575 necessarily the same as the number of characters it was instructed to move. The simplest approach is
2576 to use this to store the characters moved when moving both the start and end of the range to the
2577 start of the document body and subtracting the start offset from the end offset (the
2578 "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2579 the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2580 the end of the document) has the same problem.
2581
2582 Another approach that works is to use moveStart() to move the start boundary of the range up to the
2583 end boundary one character at a time and incrementing a counter with the value returned by the
2584 moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2585 expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2586 by the location of the range within the document).
2587
2588 The approach used below is a hybrid of the two methods above. It uses the fact that a string
2589 containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2590 be longer than the text of the TextRange, so the start of the range is moved that length initially
2591 and then a character at a time to make up for any trailing line breaks not contained in the 'text'
2592 property. This has good performance in most situations compared to the previous two methods.
2593 */
2594 var tempRange = workingRange.duplicate();
2595 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2596
2597 offset = tempRange.moveStart("character", rangeLength);
2598 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2599 offset++;
2600 tempRange.moveStart("character", 1);
2601 }
2602 } else {
2603 offset = workingRange.text.length;
2604 }
2605 boundaryPosition = new DomPosition(boundaryNode, offset);
2606 } else {
2607
2608 // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2609 // a position within that, and likewise for a start boundary preceding a character data node
2610 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2611 nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2612 if (nextNode && isCharacterDataNode(nextNode)) {
2613 boundaryPosition = new DomPosition(nextNode, 0);
2614 } else if (previousNode && isCharacterDataNode(previousNode)) {
2615 boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
2616 } else {
2617 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2618 }
2619 }
2620
2621 // Clean up
2622 dom.removeNode(workingNode);
2623
2624 return {
2625 boundaryPosition: boundaryPosition,
2626 nodeInfo: {
2627 nodeIndex: nodeIndex,
2628 containerElement: containerElement
2629 }
2630 };
2631 };
2632
2633 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
2634 // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2635 // (http://code.google.com/p/ierange/)
2636 var createBoundaryTextRange = function(boundaryPosition, isStart) {
2637 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2638 var doc = dom.getDocument(boundaryPosition.node);
2639 var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
2640 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
2641
2642 if (nodeIsDataNode) {
2643 boundaryNode = boundaryPosition.node;
2644 boundaryParent = boundaryNode.parentNode;
2645 } else {
2646 childNodes = boundaryPosition.node.childNodes;
2647 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2648 boundaryParent = boundaryPosition.node;
2649 }
2650
2651 // Position the range immediately before the node containing the boundary
2652 workingNode = doc.createElement("span");
2653
2654 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
2655 // the element rather than immediately before or after it
2656 workingNode.innerHTML = "&#feff;";
2657
2658 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2659 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2660 if (boundaryNode) {
2661 boundaryParent.insertBefore(workingNode, boundaryNode);
2662 } else {
2663 boundaryParent.appendChild(workingNode);
2664 }
2665
2666 workingRange.moveToElementText(workingNode);
2667 workingRange.collapse(!isStart);
2668
2669 // Clean up
2670 boundaryParent.removeChild(workingNode);
2671
2672 // Move the working range to the text offset, if required
2673 if (nodeIsDataNode) {
2674 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2675 }
2676
2677 return workingRange;
2678 };
2679
2680 /*------------------------------------------------------------------------------------------------------------*/
2681
2682 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2683 // prototype
2684
2685 WrappedTextRange = function(textRange) {
2686 this.textRange = textRange;
2687 this.refresh();
2688 };
2689
2690 WrappedTextRange.prototype = new DomRange(document);
2691
2692 WrappedTextRange.prototype.refresh = function() {
2693 var start, end, startBoundary;
2694
2695 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2696 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2697
2698 if (textRangeIsCollapsed(this.textRange)) {
2699 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
2700 true).boundaryPosition;
2701 } else {
2702 startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2703 start = startBoundary.boundaryPosition;
2704
2705 // An optimization used here is that if the start and end boundaries have the same parent element, the
2706 // search scope for the end boundary can be limited to exclude the portion of the element that precedes
2707 // the start boundary
2708 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
2709 startBoundary.nodeInfo).boundaryPosition;
2710 }
2711
2712 this.setStart(start.node, start.offset);
2713 this.setEnd(end.node, end.offset);
2714 };
2715
2716 WrappedTextRange.prototype.getName = function() {
2717 return "WrappedTextRange";
2718 };
2719
2720 DomRange.copyComparisonConstants(WrappedTextRange);
2721
2722 var rangeToTextRange = function(range) {
2723 if (range.collapsed) {
2724 return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2725 } else {
2726 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2727 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);