Commit | Line | Data |
---|---|---|
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 | |
10 | window['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 | |
295 | rangy.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 | |