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