MDL-44219 Atto: Convert selection logic to use Rangy JS Library
[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
1940 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";\r
1941 var previousNode, nextNode, boundaryPosition, boundaryNode;\r
1942\r
1943 // Move the working range through the container's children, starting at the end and working backwards, until the\r
1944 // working range reaches or goes past the boundary we're interested in\r
1945 do {\r
1946 containerElement.insertBefore(workingNode, workingNode.previousSibling);\r
1947 workingRange.moveToElementText(workingNode);\r
1948 } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&\r
1949 workingNode.previousSibling);\r
1950\r
1951 // We've now reached or gone past the boundary of the text range we're interested in\r
1952 // so have identified the node we want\r
1953 boundaryNode = workingNode.nextSibling;\r
1954\r
1955 if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {\r
1956 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the\r
1957 // node containing the text range's boundary, so we move the end of the working range to the boundary point\r
1958 // and measure the length of its text to get the boundary's offset within the node.\r
1959 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);\r
1960\r
1961\r
1962 var offset;\r
1963\r
1964 if (/[\r\n]/.test(boundaryNode.data)) {\r
1965 /*\r
1966 For the particular case of a boundary within a text node containing line breaks (within a <pre> element,\r
1967 for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:\r
1968\r
1969 - Each line break is represented as \r in the text node's data/nodeValue properties\r
1970 - Each line break is represented as \r\n in the TextRange's 'text' property\r
1971 - The 'text' property of the TextRange does not contain trailing line breaks\r
1972\r
1973 To get round the problem presented by the final fact above, we can use the fact that TextRange's\r
1974 moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily\r
1975 the same as the number of characters it was instructed to move. The simplest approach is to use this to\r
1976 store the characters moved when moving both the start and end of the range to the start of the document\r
1977 body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).\r
1978 However, this is extremely slow when the document is large and the range is near the end of it. Clearly\r
1979 doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same\r
1980 problem.\r
1981\r
1982 Another approach that works is to use moveStart() to move the start boundary of the range up to the end\r
1983 boundary one character at a time and incrementing a counter with the value returned by the moveStart()\r
1984 call. However, the check for whether the start boundary has reached the end boundary is expensive, so\r
1985 this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of\r
1986 the range within the document).\r
1987\r
1988 The method below is a hybrid of the two methods above. It uses the fact that a string containing the\r
1989 TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the\r
1990 text of the TextRange, so the start of the range is moved that length initially and then a character at\r
1991 a time to make up for any trailing line breaks not contained in the 'text' property. This has good\r
1992 performance in most situations compared to the previous two methods.\r
1993 */\r
1994 var tempRange = workingRange.duplicate();\r
1995 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;\r
1996\r
1997 offset = tempRange.moveStart("character", rangeLength);\r
1998 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {\r
1999 offset++;\r
2000 tempRange.moveStart("character", 1);\r
2001 }\r
2002 } else {\r
2003 offset = workingRange.text.length;\r
2004 }\r
2005 boundaryPosition = new DomPosition(boundaryNode, offset);\r
2006 } else {\r
2007\r
2008\r
2009 // If the boundary immediately follows a character data node and this is the end boundary, we should favour\r
2010 // a position within that, and likewise for a start boundary preceding a character data node\r
2011 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;\r
2012 nextNode = (isCollapsed || isStart) && workingNode.nextSibling;\r
2013\r
2014\r
2015\r
2016 if (nextNode && dom.isCharacterDataNode(nextNode)) {\r
2017 boundaryPosition = new DomPosition(nextNode, 0);\r
2018 } else if (previousNode && dom.isCharacterDataNode(previousNode)) {\r
2019 boundaryPosition = new DomPosition(previousNode, previousNode.length);\r
2020 } else {\r
2021 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));\r
2022 }\r
2023 }\r
2024\r
2025 // Clean up\r
2026 workingNode.parentNode.removeChild(workingNode);\r
2027\r
2028 return boundaryPosition;\r
2029 }\r
2030\r
2031 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.\r
2032 // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange\r
2033 // (http://code.google.com/p/ierange/)\r
2034 function createBoundaryTextRange(boundaryPosition, isStart) {\r
2035 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;\r
2036 var doc = dom.getDocument(boundaryPosition.node);\r
2037 var workingNode, childNodes, workingRange = doc.body.createTextRange();\r
2038 var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);\r
2039\r
2040 if (nodeIsDataNode) {\r
2041 boundaryNode = boundaryPosition.node;\r
2042 boundaryParent = boundaryNode.parentNode;\r
2043 } else {\r
2044 childNodes = boundaryPosition.node.childNodes;\r
2045 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;\r
2046 boundaryParent = boundaryPosition.node;\r
2047 }\r
2048\r
2049 // Position the range immediately before the node containing the boundary\r
2050 workingNode = doc.createElement("span");\r
2051\r
2052 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the\r
2053 // element rather than immediately before or after it, which is what we want\r
2054 workingNode.innerHTML = "&#feff;";\r
2055\r
2056 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report\r
2057 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12\r
2058 if (boundaryNode) {\r
2059 boundaryParent.insertBefore(workingNode, boundaryNode);\r
2060 } else {\r
2061 boundaryParent.appendChild(workingNode);\r
2062 }\r
2063\r
2064 workingRange.moveToElementText(workingNode);\r
2065 workingRange.collapse(!isStart);\r
2066\r
2067 // Clean up\r
2068 boundaryParent.removeChild(workingNode);\r
2069\r
2070 // Move the working range to the text offset, if required\r
2071 if (nodeIsDataNode) {\r
2072 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);\r
2073 }\r
2074\r
2075 return workingRange;\r
2076 }\r
2077\r
2078 /*----------------------------------------------------------------------------------------------------------------*/\r
2079\r
2080 if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {\r
2081 // This is a wrapper around the browser's native DOM Range. It has two aims:\r
2082 // - Provide workarounds for specific browser bugs\r
2083 // - provide convenient extensions, which are inherited from Rangy's DomRange\r
2084\r
2085 (function() {\r
2086 var rangeProto;\r
2087 var rangeProperties = DomRange.rangeProperties;\r
2088 var canSetRangeStartAfterEnd;\r
2089\r
2090 function updateRangeProperties(range) {\r
2091 var i = rangeProperties.length, prop;\r
2092 while (i--) {\r
2093 prop = rangeProperties[i];\r
2094 range[prop] = range.nativeRange[prop];\r
2095 }\r
2096 }\r
2097\r
2098 function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {\r
2099 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);\r
2100 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);\r
2101\r
2102 // Always set both boundaries for the benefit of IE9 (see issue 35)\r
2103 if (startMoved || endMoved) {\r
2104 range.setEnd(endContainer, endOffset);\r
2105 range.setStart(startContainer, startOffset);\r
2106 }\r
2107 }\r
2108\r
2109 function detach(range) {\r
2110 range.nativeRange.detach();\r
2111 range.detached = true;\r
2112 var i = rangeProperties.length, prop;\r
2113 while (i--) {\r
2114 prop = rangeProperties[i];\r
2115 range[prop] = null;\r
2116 }\r
2117 }\r
2118\r
2119 var createBeforeAfterNodeSetter;\r
2120\r
2121 WrappedRange = function(range) {\r
2122 if (!range) {\r
2123 throw new Error("Range must be specified");\r
2124 }\r
2125 this.nativeRange = range;\r
2126 updateRangeProperties(this);\r
2127 };\r
2128\r
2129 DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);\r
2130\r
2131 rangeProto = WrappedRange.prototype;\r
2132\r
2133 rangeProto.selectNode = function(node) {\r
2134 this.nativeRange.selectNode(node);\r
2135 updateRangeProperties(this);\r
2136 };\r
2137\r
2138 rangeProto.deleteContents = function() {\r
2139 this.nativeRange.deleteContents();\r
2140 updateRangeProperties(this);\r
2141 };\r
2142\r
2143 rangeProto.extractContents = function() {\r
2144 var frag = this.nativeRange.extractContents();\r
2145 updateRangeProperties(this);\r
2146 return frag;\r
2147 };\r
2148\r
2149 rangeProto.cloneContents = function() {\r
2150 return this.nativeRange.cloneContents();\r
2151 };\r
2152\r
2153 // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still\r
2154 // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for\r
2155 // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of\r
2156 // insertNode, which works but is almost certainly slower than the native implementation.\r
2157/*\r
2158 rangeProto.insertNode = function(node) {\r
2159 this.nativeRange.insertNode(node);\r
2160 updateRangeProperties(this);\r
2161 };\r
2162*/\r
2163\r
2164 rangeProto.surroundContents = function(node) {\r
2165 this.nativeRange.surroundContents(node);\r
2166 updateRangeProperties(this);\r
2167 };\r
2168\r
2169 rangeProto.collapse = function(isStart) {\r
2170 this.nativeRange.collapse(isStart);\r
2171 updateRangeProperties(this);\r
2172 };\r
2173\r
2174 rangeProto.cloneRange = function() {\r
2175 return new WrappedRange(this.nativeRange.cloneRange());\r
2176 };\r
2177\r
2178 rangeProto.refresh = function() {\r
2179 updateRangeProperties(this);\r
2180 };\r
2181\r
2182 rangeProto.toString = function() {\r
2183 return this.nativeRange.toString();\r
2184 };\r
2185\r
2186 // Create test range and node for feature detection\r
2187\r
2188 var testTextNode = document.createTextNode("test");\r
2189 dom.getBody(document).appendChild(testTextNode);\r
2190 var range = document.createRange();\r
2191\r
2192 /*--------------------------------------------------------------------------------------------------------*/\r
2193\r
2194 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and\r
2195 // correct for it\r
2196\r
2197 range.setStart(testTextNode, 0);\r
2198 range.setEnd(testTextNode, 0);\r
2199\r
2200 try {\r
2201 range.setStart(testTextNode, 1);\r
2202 canSetRangeStartAfterEnd = true;\r
2203\r
2204 rangeProto.setStart = function(node, offset) {\r
2205 this.nativeRange.setStart(node, offset);\r
2206 updateRangeProperties(this);\r
2207 };\r
2208\r
2209 rangeProto.setEnd = function(node, offset) {\r
2210 this.nativeRange.setEnd(node, offset);\r
2211 updateRangeProperties(this);\r
2212 };\r
2213\r
2214 createBeforeAfterNodeSetter = function(name) {\r
2215 return function(node) {\r
2216 this.nativeRange[name](node);\r
2217 updateRangeProperties(this);\r
2218 };\r
2219 };\r
2220\r
2221 } catch(ex) {\r
2222\r
2223\r
2224 canSetRangeStartAfterEnd = false;\r
2225\r
2226 rangeProto.setStart = function(node, offset) {\r
2227 try {\r
2228 this.nativeRange.setStart(node, offset);\r
2229 } catch (ex) {\r
2230 this.nativeRange.setEnd(node, offset);\r
2231 this.nativeRange.setStart(node, offset);\r
2232 }\r
2233 updateRangeProperties(this);\r
2234 };\r
2235\r
2236 rangeProto.setEnd = function(node, offset) {\r
2237 try {\r
2238 this.nativeRange.setEnd(node, offset);\r
2239 } catch (ex) {\r
2240 this.nativeRange.setStart(node, offset);\r
2241 this.nativeRange.setEnd(node, offset);\r
2242 }\r
2243 updateRangeProperties(this);\r
2244 };\r
2245\r
2246 createBeforeAfterNodeSetter = function(name, oppositeName) {\r
2247 return function(node) {\r
2248 try {\r
2249 this.nativeRange[name](node);\r
2250 } catch (ex) {\r
2251 this.nativeRange[oppositeName](node);\r
2252 this.nativeRange[name](node);\r
2253 }\r
2254 updateRangeProperties(this);\r
2255 };\r
2256 };\r
2257 }\r
2258\r
2259 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");\r
2260 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");\r
2261 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");\r
2262 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");\r
2263\r
2264 /*--------------------------------------------------------------------------------------------------------*/\r
2265\r
2266 // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to\r
2267 // the 0th character of the text node\r
2268 range.selectNodeContents(testTextNode);\r
2269 if (range.startContainer == testTextNode && range.endContainer == testTextNode &&\r
2270 range.startOffset == 0 && range.endOffset == testTextNode.length) {\r
2271 rangeProto.selectNodeContents = function(node) {\r
2272 this.nativeRange.selectNodeContents(node);\r
2273 updateRangeProperties(this);\r
2274 };\r
2275 } else {\r
2276 rangeProto.selectNodeContents = function(node) {\r
2277 this.setStart(node, 0);\r
2278 this.setEnd(node, DomRange.getEndOffset(node));\r
2279 };\r
2280 }\r
2281\r
2282 /*--------------------------------------------------------------------------------------------------------*/\r
2283\r
2284 // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants\r
2285 // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738\r
2286\r
2287 range.selectNodeContents(testTextNode);\r
2288 range.setEnd(testTextNode, 3);\r
2289\r
2290 var range2 = document.createRange();\r
2291 range2.selectNodeContents(testTextNode);\r
2292 range2.setEnd(testTextNode, 4);\r
2293 range2.setStart(testTextNode, 2);\r
2294\r
2295 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &\r
2296 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {\r
2297 // This is the wrong way round, so correct for it\r
2298\r
2299\r
2300 rangeProto.compareBoundaryPoints = function(type, range) {\r
2301 range = range.nativeRange || range;\r
2302 if (type == range.START_TO_END) {\r
2303 type = range.END_TO_START;\r
2304 } else if (type == range.END_TO_START) {\r
2305 type = range.START_TO_END;\r
2306 }\r
2307 return this.nativeRange.compareBoundaryPoints(type, range);\r
2308 };\r
2309 } else {\r
2310 rangeProto.compareBoundaryPoints = function(type, range) {\r
2311 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);\r
2312 };\r
2313 }\r
2314\r
2315 /*--------------------------------------------------------------------------------------------------------*/\r
2316\r
2317 // Test for existence of createContextualFragment and delegate to it if it exists\r
2318 if (api.util.isHostMethod(range, "createContextualFragment")) {\r
2319 rangeProto.createContextualFragment = function(fragmentStr) {\r
2320 return this.nativeRange.createContextualFragment(fragmentStr);\r
2321 };\r
2322 }\r
2323\r
2324 /*--------------------------------------------------------------------------------------------------------*/\r
2325\r
2326 // Clean up\r
2327 dom.getBody(document).removeChild(testTextNode);\r
2328 range.detach();\r
2329 range2.detach();\r
2330 })();\r
2331\r
2332 api.createNativeRange = function(doc) {\r
2333 doc = doc || document;\r
2334 return doc.createRange();\r
2335 };\r
2336 } else if (api.features.implementsTextRange) {\r
2337 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a\r
2338 // prototype\r
2339\r
2340 WrappedRange = function(textRange) {\r
2341 this.textRange = textRange;\r
2342 this.refresh();\r
2343 };\r
2344\r
2345 WrappedRange.prototype = new DomRange(document);\r
2346\r
2347 WrappedRange.prototype.refresh = function() {\r
2348 var start, end;\r
2349\r
2350 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.\r
2351 var rangeContainerElement = getTextRangeContainerElement(this.textRange);\r
2352\r
2353 if (textRangeIsCollapsed(this.textRange)) {\r
2354 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);\r
2355 } else {\r
2356\r
2357 start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);\r
2358 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);\r
2359 }\r
2360\r
2361 this.setStart(start.node, start.offset);\r
2362 this.setEnd(end.node, end.offset);\r
2363 };\r
2364\r
2365 DomRange.copyComparisonConstants(WrappedRange);\r
2366\r
2367 // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work\r
2368 var globalObj = (function() { return this; })();\r
2369 if (typeof globalObj.Range == "undefined") {\r
2370 globalObj.Range = WrappedRange;\r
2371 }\r
2372\r
2373 api.createNativeRange = function(doc) {\r
2374 doc = doc || document;\r
2375 return doc.body.createTextRange();\r
2376 };\r
2377 }\r
2378\r
2379 if (api.features.implementsTextRange) {\r
2380 WrappedRange.rangeToTextRange = function(range) {\r
2381 if (range.collapsed) {\r
2382 var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);\r
2383\r
2384\r
2385\r
2386 return tr;\r
2387\r
2388 //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);\r
2389 } else {\r
2390 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);\r
2391 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);\r
2392 var textRange = dom.getDocument(range.startContainer).body.createTextRange();\r
2393 textRange.setEndPoint("StartToStart", startRange);\r
2394 textRange.setEndPoint("EndToEnd", endRange);\r
2395 return textRange;\r
2396 }\r
2397 };\r
2398 }\r
2399\r
2400 WrappedRange.prototype.getName = function() {\r
2401 return "WrappedRange";\r
2402 };\r
2403\r
2404 api.WrappedRange = WrappedRange;\r
2405\r
2406 api.createRange = function(doc) {\r
2407 doc = doc || document;\r
2408 return new WrappedRange(api.createNativeRange(doc));\r
2409 };\r
2410\r
2411 api.createRangyRange = function(doc) {\r
2412 doc = doc || document;\r
2413 return new DomRange(doc);\r
2414 };\r
2415\r
2416 api.createIframeRange = function(iframeEl) {\r
2417 return api.createRange(dom.getIframeDocument(iframeEl));\r
2418 };\r
2419\r
2420 api.createIframeRangyRange = function(iframeEl) {\r
2421 return api.createRangyRange(dom.getIframeDocument(iframeEl));\r
2422 };\r
2423\r
2424 api.addCreateMissingNativeApiListener(function(win) {\r
2425 var doc = win.document;\r
2426 if (typeof doc.createRange == "undefined") {\r
2427 doc.createRange = function() {\r
2428 return api.createRange(this);\r
2429 };\r
2430 }\r
2431 doc = win = null;\r
2432 });\r
2433});rangy.createModule("WrappedSelection", function(api, module) {\r
2434 // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range\r
2435 // spec (http://html5.org/specs/dom-range.html)\r
2436\r
2437 api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );\r
2438\r
2439 api.config.checkSelectionRanges = true;\r
2440\r
2441 var BOOLEAN = "boolean",\r
2442 windowPropertyName = "_rangySelection",\r
2443 dom = api.dom,\r
2444 util = api.util,\r
2445 DomRange = api.DomRange,\r
2446 WrappedRange = api.WrappedRange,\r
2447 DOMException = api.DOMException,\r
2448 DomPosition = dom.DomPosition,\r
2449 getSelection,\r
2450 selectionIsCollapsed,\r
2451 CONTROL = "Control";\r
2452\r
2453\r
2454\r
2455 function getWinSelection(winParam) {\r
2456 return (winParam || window).getSelection();\r
2457 }\r
2458\r
2459 function getDocSelection(winParam) {\r
2460 return (winParam || window).document.selection;\r
2461 }\r
2462\r
2463 // Test for the Range/TextRange and Selection features required\r
2464 // Test for ability to retrieve selection\r
2465 var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),\r
2466 implementsDocSelection = api.util.isHostObject(document, "selection");\r
2467\r
2468 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);\r
2469\r
2470 if (useDocumentSelection) {\r
2471 getSelection = getDocSelection;\r
2472 api.isSelectionValid = function(winParam) {\r
2473 var doc = (winParam || window).document, nativeSel = doc.selection;\r
2474\r
2475 // Check whether the selection TextRange is actually contained within the correct document\r
2476 return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);\r
2477 };\r
2478 } else if (implementsWinGetSelection) {\r
2479 getSelection = getWinSelection;\r
2480 api.isSelectionValid = function() {\r
2481 return true;\r
2482 };\r
2483 } else {\r
2484 module.fail("Neither document.selection or window.getSelection() detected.");\r
2485 }\r
2486\r
2487 api.getNativeSelection = getSelection;\r
2488\r
2489 var testSelection = getSelection();\r
2490 var testRange = api.createNativeRange(document);\r
2491 var body = dom.getBody(document);\r
2492\r
2493 // Obtaining a range from a selection\r
2494 var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&\r
2495 util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));\r
2496 api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;\r
2497\r
2498 // Test for existence of native selection extend() method\r
2499 var selectionHasExtend = util.isHostMethod(testSelection, "extend");\r
2500 api.features.selectionHasExtend = selectionHasExtend;\r
2501\r
2502 // Test if rangeCount exists\r
2503 var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");\r
2504 api.features.selectionHasRangeCount = selectionHasRangeCount;\r
2505\r
2506 var selectionSupportsMultipleRanges = false;\r
2507 var collapsedNonEditableSelectionsSupported = true;\r
2508\r
2509 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&\r
2510 typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {\r
2511\r
2512 (function() {\r
2513 var iframe = document.createElement("iframe");\r
2514 iframe.frameBorder = 0;\r
2515 iframe.style.position = "absolute";\r
2516 iframe.style.left = "-10000px";\r
2517 body.appendChild(iframe);\r
2518\r
2519 var iframeDoc = dom.getIframeDocument(iframe);\r
2520 iframeDoc.open();\r
2521 iframeDoc.write("<html><head></head><body>12</body></html>");\r
2522 iframeDoc.close();\r
2523\r
2524 var sel = dom.getIframeWindow(iframe).getSelection();\r
2525 var docEl = iframeDoc.documentElement;\r
2526 var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;\r
2527\r
2528 // Test whether the native selection will allow a collapsed selection within a non-editable element\r
2529 var r1 = iframeDoc.createRange();\r
2530 r1.setStart(textNode, 1);\r
2531 r1.collapse(true);\r
2532 sel.addRange(r1);\r
2533 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);\r
2534 sel.removeAllRanges();\r
2535\r
2536 // Test whether the native selection is capable of supporting multiple ranges\r
2537 var r2 = r1.cloneRange();\r
2538 r1.setStart(textNode, 0);\r
2539 r2.setEnd(textNode, 2);\r
2540 sel.addRange(r1);\r
2541 sel.addRange(r2);\r
2542\r
2543 selectionSupportsMultipleRanges = (sel.rangeCount == 2);\r
2544\r
2545 // Clean up\r
2546 r1.detach();\r
2547 r2.detach();\r
2548\r
2549 body.removeChild(iframe);\r
2550 })();\r
2551 }\r
2552\r
2553 api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;\r
2554 api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;\r
2555\r
2556 // ControlRanges\r
2557 var implementsControlRange = false, testControlRange;\r
2558\r
2559 if (body && util.isHostMethod(body, "createControlRange")) {\r
2560 testControlRange = body.createControlRange();\r
2561 if (util.areHostProperties(testControlRange, ["item", "add"])) {\r
2562 implementsControlRange = true;\r
2563 }\r
2564 }\r
2565 api.features.implementsControlRange = implementsControlRange;\r
2566\r
2567 // Selection collapsedness\r
2568 if (selectionHasAnchorAndFocus) {\r
2569 selectionIsCollapsed = function(sel) {\r
2570 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;\r
2571 };\r
2572 } else {\r
2573 selectionIsCollapsed = function(sel) {\r
2574 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;\r
2575 };\r
2576 }\r
2577\r
2578 function updateAnchorAndFocusFromRange(sel, range, backwards) {\r
2579 var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";\r
2580 sel.anchorNode = range[anchorPrefix + "Container"];\r
2581 sel.anchorOffset = range[anchorPrefix + "Offset"];\r
2582 sel.focusNode = range[focusPrefix + "Container"];\r
2583 sel.focusOffset = range[focusPrefix + "Offset"];\r
2584 }\r
2585\r
2586 function updateAnchorAndFocusFromNativeSelection(sel) {\r
2587 var nativeSel = sel.nativeSelection;\r
2588 sel.anchorNode = nativeSel.anchorNode;\r
2589 sel.anchorOffset = nativeSel.anchorOffset;\r
2590 sel.focusNode = nativeSel.focusNode;\r
2591 sel.focusOffset = nativeSel.focusOffset;\r
2592 }\r
2593\r
2594 function updateEmptySelection(sel) {\r
2595 sel.anchorNode = sel.focusNode = null;\r
2596 sel.anchorOffset = sel.focusOffset = 0;\r
2597 sel.rangeCount = 0;\r
2598 sel.isCollapsed = true;\r
2599 sel._ranges.length = 0;\r
2600 }\r
2601\r
2602 function getNativeRange(range) {\r
2603 var nativeRange;\r
2604 if (range instanceof DomRange) {\r
2605 nativeRange = range._selectionNativeRange;\r
2606 if (!nativeRange) {\r
2607 nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));\r
2608 nativeRange.setEnd(range.endContainer, range.endOffset);\r
2609 nativeRange.setStart(range.startContainer, range.startOffset);\r
2610 range._selectionNativeRange = nativeRange;\r
2611 range.attachListener("detach", function() {\r
2612\r
2613 this._selectionNativeRange = null;\r
2614 });\r
2615 }\r
2616 } else if (range instanceof WrappedRange) {\r
2617 nativeRange = range.nativeRange;\r
2618 } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {\r
2619 nativeRange = range;\r
2620 }\r
2621 return nativeRange;\r
2622 }\r
2623\r
2624 function rangeContainsSingleElement(rangeNodes) {\r
2625 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {\r
2626 return false;\r
2627 }\r
2628 for (var i = 1, len = rangeNodes.length; i < len; ++i) {\r
2629 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {\r
2630 return false;\r
2631 }\r
2632 }\r
2633 return true;\r
2634 }\r
2635\r
2636 function getSingleElementFromRange(range) {\r
2637 var nodes = range.getNodes();\r
2638 if (!rangeContainsSingleElement(nodes)) {\r
2639 throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");\r
2640 }\r
2641 return nodes[0];\r
2642 }\r
2643\r
2644 function isTextRange(range) {\r
2645 return !!range && typeof range.text != "undefined";\r
2646 }\r
2647\r
2648 function updateFromTextRange(sel, range) {\r
2649 // Create a Range from the selected TextRange\r
2650 var wrappedRange = new WrappedRange(range);\r
2651 sel._ranges = [wrappedRange];\r
2652\r
2653 updateAnchorAndFocusFromRange(sel, wrappedRange, false);\r
2654 sel.rangeCount = 1;\r
2655 sel.isCollapsed = wrappedRange.collapsed;\r
2656 }\r
2657\r
2658 function updateControlSelection(sel) {\r
2659 // Update the wrapped selection based on what's now in the native selection\r
2660 sel._ranges.length = 0;\r
2661 if (sel.docSelection.type == "None") {\r
2662 updateEmptySelection(sel);\r
2663 } else {\r
2664 var controlRange = sel.docSelection.createRange();\r
2665 if (isTextRange(controlRange)) {\r
2666 // This case (where the selection type is "Control" and calling createRange() on the selection returns\r
2667 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected\r
2668 // ControlRange have been removed from the ControlRange and removed from the document.\r
2669 updateFromTextRange(sel, controlRange);\r
2670 } else {\r
2671 sel.rangeCount = controlRange.length;\r
2672 var range, doc = dom.getDocument(controlRange.item(0));\r
2673 for (var i = 0; i < sel.rangeCount; ++i) {\r
2674 range = api.createRange(doc);\r
2675 range.selectNode(controlRange.item(i));\r
2676 sel._ranges.push(range);\r
2677 }\r
2678 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;\r
2679 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);\r
2680 }\r
2681 }\r
2682 }\r
2683\r
2684 function addRangeToControlSelection(sel, range) {\r
2685 var controlRange = sel.docSelection.createRange();\r
2686 var rangeElement = getSingleElementFromRange(range);\r
2687\r
2688 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element\r
2689 // contained by the supplied range\r
2690 var doc = dom.getDocument(controlRange.item(0));\r
2691 var newControlRange = dom.getBody(doc).createControlRange();\r
2692 for (var i = 0, len = controlRange.length; i < len; ++i) {\r
2693 newControlRange.add(controlRange.item(i));\r
2694 }\r
2695 try {\r
2696 newControlRange.add(rangeElement);\r
2697 } catch (ex) {\r
2698 throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");\r
2699 }\r
2700 newControlRange.select();\r
2701\r
2702 // Update the wrapped selection based on what's now in the native selection\r
2703 updateControlSelection(sel);\r
2704 }\r
2705\r
2706 var getSelectionRangeAt;\r
2707\r
2708 if (util.isHostMethod(testSelection, "getRangeAt")) {\r
2709 getSelectionRangeAt = function(sel, index) {\r
2710 try {\r
2711 return sel.getRangeAt(index);\r
2712 } catch(ex) {\r
2713 return null;\r
2714 }\r
2715 };\r
2716 } else if (selectionHasAnchorAndFocus) {\r
2717 getSelectionRangeAt = function(sel) {\r
2718 var doc = dom.getDocument(sel.anchorNode);\r
2719 var range = api.createRange(doc);\r
2720 range.setStart(sel.anchorNode, sel.anchorOffset);\r
2721 range.setEnd(sel.focusNode, sel.focusOffset);\r
2722\r
2723 // Handle the case when the selection was selected backwards (from the end to the start in the\r
2724 // document)\r
2725 if (range.collapsed !== this.isCollapsed) {\r
2726 range.setStart(sel.focusNode, sel.focusOffset);\r
2727 range.setEnd(sel.anchorNode, sel.anchorOffset);\r
2728 }\r
2729\r
2730 return range;\r
2731 };\r
2732 }\r
2733\r
2734 /**\r
2735 * @constructor\r
2736 */\r
2737 function WrappedSelection(selection, docSelection, win) {\r
2738 this.nativeSelection = selection;\r
2739 this.docSelection = docSelection;\r
2740 this._ranges = [];\r
2741 this.win = win;\r
2742 this.refresh();\r
2743 }\r
2744\r
2745 api.getSelection = function(win) {\r
2746 win = win || window;\r
2747 var sel = win[windowPropertyName];\r
2748 var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;\r
2749 if (sel) {\r
2750 sel.nativeSelection = nativeSel;\r
2751 sel.docSelection = docSel;\r
2752 sel.refresh(win);\r
2753 } else {\r
2754 sel = new WrappedSelection(nativeSel, docSel, win);\r
2755 win[windowPropertyName] = sel;\r
2756 }\r
2757 return sel;\r
2758 };\r
2759\r
2760 api.getIframeSelection = function(iframeEl) {\r
2761 return api.getSelection(dom.getIframeWindow(iframeEl));\r
2762 };\r
2763\r
2764 var selProto = WrappedSelection.prototype;\r
2765\r
2766 function createControlSelection(sel, ranges) {\r
2767 // Ensure that the selection becomes of type "Control"\r
2768 var doc = dom.getDocument(ranges[0].startContainer);\r
2769 var controlRange = dom.getBody(doc).createControlRange();\r
2770 for (var i = 0, el; i < rangeCount; ++i) {\r
2771 el = getSingleElementFromRange(ranges[i]);\r
2772 try {\r
2773 controlRange.add(el);\r
2774 } catch (ex) {\r
2775 throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");\r
2776 }\r
2777 }\r
2778 controlRange.select();\r
2779\r
2780 // Update the wrapped selection based on what's now in the native selection\r
2781 updateControlSelection(sel);\r
2782 }\r
2783\r
2784 // Selecting a range\r
2785 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {\r
2786 selProto.removeAllRanges = function() {\r
2787 this.nativeSelection.removeAllRanges();\r
2788 updateEmptySelection(this);\r
2789 };\r
2790\r
2791 var addRangeBackwards = function(sel, range) {\r
2792 var doc = DomRange.getRangeDocument(range);\r
2793 var endRange = api.createRange(doc);\r
2794 endRange.collapseToPoint(range.endContainer, range.endOffset);\r
2795 sel.nativeSelection.addRange(getNativeRange(endRange));\r
2796 sel.nativeSelection.extend(range.startContainer, range.startOffset);\r
2797 sel.refresh();\r
2798 };\r
2799\r
2800 if (selectionHasRangeCount) {\r
2801 selProto.addRange = function(range, backwards) {\r
2802 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {\r
2803 addRangeToControlSelection(this, range);\r
2804 } else {\r
2805 if (backwards && selectionHasExtend) {\r
2806 addRangeBackwards(this, range);\r
2807 } else {\r
2808 var previousRangeCount;\r
2809 if (selectionSupportsMultipleRanges) {\r
2810 previousRangeCount = this.rangeCount;\r
2811 } else {\r
2812 this.removeAllRanges();\r
2813 previousRangeCount = 0;\r
2814 }\r
2815 this.nativeSelection.addRange(getNativeRange(range));\r
2816\r
2817 // Check whether adding the range was successful\r
2818 this.rangeCount = this.nativeSelection.rangeCount;\r
2819\r
2820 if (this.rangeCount == previousRangeCount + 1) {\r
2821 // The range was added successfully\r
2822\r
2823 // Check whether the range that we added to the selection is reflected in the last range extracted from\r
2824 // the selection\r
2825 if (api.config.checkSelectionRanges) {\r
2826 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);\r
2827 if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {\r
2828 // Happens in WebKit with, for example, a selection placed at the start of a text node\r
2829 range = new WrappedRange(nativeRange);\r
2830 }\r
2831 }\r
2832 this._ranges[this.rangeCount - 1] = range;\r
2833 updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));\r
2834 this.isCollapsed = selectionIsCollapsed(this);\r
2835 } else {\r
2836 // The range was not added successfully. The simplest thing is to refresh\r
2837 this.refresh();\r
2838 }\r
2839 }\r
2840 }\r
2841 };\r
2842 } else {\r
2843 selProto.addRange = function(range, backwards) {\r
2844 if (backwards && selectionHasExtend) {\r
2845 addRangeBackwards(this, range);\r
2846 } else {\r
2847 this.nativeSelection.addRange(getNativeRange(range));\r
2848 this.refresh();\r
2849 }\r
2850 };\r
2851 }\r
2852\r
2853 selProto.setRanges = function(ranges) {\r
2854 if (implementsControlRange && ranges.length > 1) {\r
2855 createControlSelection(this, ranges);\r
2856 } else {\r
2857 this.removeAllRanges();\r
2858 for (var i = 0, len = ranges.length; i < len; ++i) {\r
2859 this.addRange(ranges[i]);\r
2860 }\r
2861 }\r
2862 };\r
2863 } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&\r
2864 implementsControlRange && useDocumentSelection) {\r
2865\r
2866 selProto.removeAllRanges = function() {\r
2867 // Added try/catch as fix for issue #21\r
2868 try {\r
2869 this.docSelection.empty();\r
2870\r
2871 // Check for empty() not working (issue #24)\r
2872 if (this.docSelection.type != "None") {\r
2873 // Work around failure to empty a control selection by instead selecting a TextRange and then\r
2874 // calling empty()\r
2875 var doc;\r
2876 if (this.anchorNode) {\r
2877 doc = dom.getDocument(this.anchorNode);\r
2878 } else if (this.docSelection.type == CONTROL) {\r
2879 var controlRange = this.docSelection.createRange();\r
2880 if (controlRange.length) {\r
2881 doc = dom.getDocument(controlRange.item(0)).body.createTextRange();\r
2882 }\r
2883 }\r
2884 if (doc) {\r
2885 var textRange = doc.body.createTextRange();\r
2886 textRange.select();\r
2887 this.docSelection.empty();\r
2888 }\r
2889 }\r
2890 } catch(ex) {}\r
2891 updateEmptySelection(this);\r
2892 };\r
2893\r
2894 selProto.addRange = function(range) {\r
2895 if (this.docSelection.type == CONTROL) {\r
2896 addRangeToControlSelection(this, range);\r
2897 } else {\r
2898 WrappedRange.rangeToTextRange(range).select();\r
2899 this._ranges[0] = range;\r
2900 this.rangeCount = 1;\r
2901 this.isCollapsed = this._ranges[0].collapsed;\r
2902 updateAnchorAndFocusFromRange(this, range, false);\r
2903 }\r
2904 };\r
2905\r
2906 selProto.setRanges = function(ranges) {\r
2907 this.removeAllRanges();\r
2908 var rangeCount = ranges.length;\r
2909 if (rangeCount > 1) {\r
2910 createControlSelection(this, ranges);\r
2911 } else if (rangeCount) {\r
2912 this.addRange(ranges[0]);\r
2913 }\r
2914 };\r
2915 } else {\r
2916 module.fail("No means of selecting a Range or TextRange was found");\r
2917 return false;\r
2918 }\r
2919\r
2920 selProto.getRangeAt = function(index) {\r
2921 if (index < 0 || index >= this.rangeCount) {\r
2922 throw new DOMException("INDEX_SIZE_ERR");\r
2923 } else {\r
2924 return this._ranges[index];\r
2925 }\r
2926 };\r
2927\r
2928 var refreshSelection;\r
2929\r
2930 if (useDocumentSelection) {\r
2931 refreshSelection = function(sel) {\r
2932 var range;\r
2933 if (api.isSelectionValid(sel.win)) {\r
2934 range = sel.docSelection.createRange();\r
2935 } else {\r
2936 range = dom.getBody(sel.win.document).createTextRange();\r
2937 range.collapse(true);\r
2938 }\r
2939\r
2940\r
2941 if (sel.docSelection.type == CONTROL) {\r
2942 updateControlSelection(sel);\r
2943 } else if (isTextRange(range)) {\r
2944 updateFromTextRange(sel, range);\r
2945 } else {\r
2946 updateEmptySelection(sel);\r
2947 }\r
2948 };\r
2949 } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {\r
2950 refreshSelection = function(sel) {\r
2951 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {\r
2952 updateControlSelection(sel);\r
2953 } else {\r
2954 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;\r
2955 if (sel.rangeCount) {\r
2956 for (var i = 0, len = sel.rangeCount; i < len; ++i) {\r
2957 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));\r
2958 }\r
2959 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));\r
2960 sel.isCollapsed = selectionIsCollapsed(sel);\r
2961 } else {\r
2962 updateEmptySelection(sel);\r
2963 }\r
2964 }\r
2965 };\r
2966 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {\r
2967 refreshSelection = function(sel) {\r
2968 var range, nativeSel = sel.nativeSelection;\r
2969 if (nativeSel.anchorNode) {\r
2970 range = getSelectionRangeAt(nativeSel, 0);\r
2971 sel._ranges = [range];\r
2972 sel.rangeCount = 1;\r
2973 updateAnchorAndFocusFromNativeSelection(sel);\r
2974 sel.isCollapsed = selectionIsCollapsed(sel);\r
2975 } else {\r
2976 updateEmptySelection(sel);\r
2977 }\r
2978 };\r
2979 } else {\r
2980 module.fail("No means of obtaining a Range or TextRange from the user's selection was found");\r
2981 return false;\r
2982 }\r
2983\r
2984 selProto.refresh = function(checkForChanges) {\r
2985 var oldRanges = checkForChanges ? this._ranges.slice(0) : null;\r
2986 refreshSelection(this);\r
2987 if (checkForChanges) {\r
2988 var i = oldRanges.length;\r
2989 if (i != this._ranges.length) {\r
2990 return false;\r
2991 }\r
2992 while (i--) {\r
2993 if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {\r
2994 return false;\r
2995 }\r
2996 }\r
2997 return true;\r
2998 }\r
2999 };\r
3000\r
3001 // Removal of a single range\r
3002 var removeRangeManually = function(sel, range) {\r
3003 var ranges = sel.getAllRanges(), removed = false;\r
3004 sel.removeAllRanges();\r
3005 for (var i = 0, len = ranges.length; i < len; ++i) {\r
3006 if (removed || range !== ranges[i]) {\r
3007 sel.addRange(ranges[i]);\r
3008 } else {\r
3009 // According to the draft WHATWG Range spec, the same range may be added to the selection multiple\r
3010 // times. removeRange should only remove the first instance, so the following ensures only the first\r
3011 // instance is removed\r
3012 removed = true;\r
3013 }\r
3014 }\r
3015 if (!sel.rangeCount) {\r
3016 updateEmptySelection(sel);\r
3017 }\r
3018 };\r
3019\r
3020 if (implementsControlRange) {\r
3021 selProto.removeRange = function(range) {\r
3022 if (this.docSelection.type == CONTROL) {\r
3023 var controlRange = this.docSelection.createRange();\r
3024 var rangeElement = getSingleElementFromRange(range);\r
3025\r
3026 // Create a new ControlRange containing all the elements in the selected ControlRange minus the\r
3027 // element contained by the supplied range\r
3028 var doc = dom.getDocument(controlRange.item(0));\r
3029 var newControlRange = dom.getBody(doc).createControlRange();\r
3030 var el, removed = false;\r