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