dom/imptests/editing/selecttest/common.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 "use strict";
michael@0 2 // TODO: iframes, contenteditable/designMode
michael@0 3
michael@0 4 // Everything is done in functions in this test harness, so we have to declare
michael@0 5 // all the variables before use to make sure they can be reused.
michael@0 6 var selection;
michael@0 7 var testDiv, paras, detachedDiv, detachedPara1, detachedPara2,
michael@0 8 foreignDoc, foreignPara1, foreignPara2, xmlDoc, xmlElement,
michael@0 9 detachedXmlElement, detachedTextNode, foreignTextNode,
michael@0 10 detachedForeignTextNode, xmlTextNode, detachedXmlTextNode,
michael@0 11 processingInstruction, detachedProcessingInstruction, comment,
michael@0 12 detachedComment, foreignComment, detachedForeignComment, xmlComment,
michael@0 13 detachedXmlComment, docfrag, foreignDocfrag, xmlDocfrag, doctype,
michael@0 14 foreignDoctype, xmlDoctype;
michael@0 15 var testRanges, testPoints, testNodes;
michael@0 16
michael@0 17 function setupRangeTests() {
michael@0 18 selection = getSelection();
michael@0 19 testDiv = document.querySelector("#test");
michael@0 20 if (testDiv) {
michael@0 21 testDiv.parentNode.removeChild(testDiv);
michael@0 22 }
michael@0 23 testDiv = document.createElement("div");
michael@0 24 testDiv.id = "test";
michael@0 25 document.body.insertBefore(testDiv, document.body.firstChild);
michael@0 26 // Test some diacritics, to make sure browsers are using code units here
michael@0 27 // and not something like grapheme clusters.
michael@0 28 testDiv.innerHTML = "<p id=a>A&#x308;b&#x308;c&#x308;d&#x308;e&#x308;f&#x308;g&#x308;h&#x308;\n"
michael@0 29 + "<p id=b style=display:none>Ijklmnop\n"
michael@0 30 + "<p id=c>Qrstuvwx"
michael@0 31 + "<p id=d style=display:none>Yzabcdef"
michael@0 32 + "<p id=e style=display:none>Ghijklmn";
michael@0 33 paras = testDiv.querySelectorAll("p");
michael@0 34
michael@0 35 detachedDiv = document.createElement("div");
michael@0 36 detachedPara1 = document.createElement("p");
michael@0 37 detachedPara1.appendChild(document.createTextNode("Opqrstuv"));
michael@0 38 detachedPara2 = document.createElement("p");
michael@0 39 detachedPara2.appendChild(document.createTextNode("Wxyzabcd"));
michael@0 40 detachedDiv.appendChild(detachedPara1);
michael@0 41 detachedDiv.appendChild(detachedPara2);
michael@0 42
michael@0 43 // Opera doesn't automatically create a doctype for a new HTML document,
michael@0 44 // contrary to spec. It also doesn't let you add doctypes to documents
michael@0 45 // after the fact through any means I've tried. So foreignDoc in Opera
michael@0 46 // will have no doctype, foreignDoctype will be null, and Opera will fail
michael@0 47 // some tests somewhat mysteriously as a result.
michael@0 48 foreignDoc = document.implementation.createHTMLDocument("");
michael@0 49 foreignPara1 = foreignDoc.createElement("p");
michael@0 50 foreignPara1.appendChild(foreignDoc.createTextNode("Efghijkl"));
michael@0 51 foreignPara2 = foreignDoc.createElement("p");
michael@0 52 foreignPara2.appendChild(foreignDoc.createTextNode("Mnopqrst"));
michael@0 53 foreignDoc.body.appendChild(foreignPara1);
michael@0 54 foreignDoc.body.appendChild(foreignPara2);
michael@0 55
michael@0 56 // Now we get to do really silly stuff, which nobody in the universe is
michael@0 57 // ever going to actually do, but the spec defines behavior, so too bad.
michael@0 58 // Testing is fun!
michael@0 59 xmlDoctype = document.implementation.createDocumentType("qorflesnorf", "abcde", "x\"'y");
michael@0 60 xmlDoc = document.implementation.createDocument(null, null, xmlDoctype);
michael@0 61 detachedXmlElement = xmlDoc.createElement("everyone-hates-hyphenated-element-names");
michael@0 62 detachedTextNode = document.createTextNode("Uvwxyzab");
michael@0 63 detachedForeignTextNode = foreignDoc.createTextNode("Cdefghij");
michael@0 64 detachedXmlTextNode = xmlDoc.createTextNode("Klmnopqr");
michael@0 65 // PIs only exist in XML documents, so don't bother with document or
michael@0 66 // foreignDoc.
michael@0 67 detachedProcessingInstruction = xmlDoc.createProcessingInstruction("whippoorwill", "chirp chirp chirp");
michael@0 68 detachedComment = document.createComment("Stuvwxyz");
michael@0 69 // Hurrah, we finally got to "z" at the end!
michael@0 70 detachedForeignComment = foreignDoc.createComment("אריה יהודה");
michael@0 71 detachedXmlComment = xmlDoc.createComment("בן חיים אליעזר");
michael@0 72
michael@0 73 // We should also test with document fragments that actually contain stuff
michael@0 74 // . . . but, maybe later.
michael@0 75 docfrag = document.createDocumentFragment();
michael@0 76 foreignDocfrag = foreignDoc.createDocumentFragment();
michael@0 77 xmlDocfrag = xmlDoc.createDocumentFragment();
michael@0 78
michael@0 79 xmlElement = xmlDoc.createElement("igiveuponcreativenames");
michael@0 80 xmlTextNode = xmlDoc.createTextNode("do re mi fa so la ti");
michael@0 81 xmlElement.appendChild(xmlTextNode);
michael@0 82 processingInstruction = xmlDoc.createProcessingInstruction("somePI", 'Did you know that ":syn sync fromstart" is very useful when using vim to edit large amounts of JavaScript embedded in HTML?');
michael@0 83 xmlDoc.appendChild(xmlElement);
michael@0 84 xmlDoc.appendChild(processingInstruction);
michael@0 85 xmlComment = xmlDoc.createComment("I maliciously created a comment that will break incautious XML serializers, but Firefox threw an exception, so all I got was this lousy T-shirt");
michael@0 86 xmlDoc.appendChild(xmlComment);
michael@0 87
michael@0 88 comment = document.createComment("Alphabet soup?");
michael@0 89 testDiv.appendChild(comment);
michael@0 90
michael@0 91 foreignComment = foreignDoc.createComment('"Commenter" and "commentator" mean different things. I\'ve seen non-native speakers trip up on this.');
michael@0 92 foreignDoc.appendChild(foreignComment);
michael@0 93 foreignTextNode = foreignDoc.createTextNode("I admit that I harbor doubts about whether we really need so many things to test, but it's too late to stop now.");
michael@0 94 foreignDoc.body.appendChild(foreignTextNode);
michael@0 95
michael@0 96 doctype = document.doctype;
michael@0 97 foreignDoctype = foreignDoc.doctype;
michael@0 98
michael@0 99 testRanges = [
michael@0 100 // Various ranges within the text node children of different
michael@0 101 // paragraphs. All should be valid.
michael@0 102 "[paras[0].firstChild, 0, paras[0].firstChild, 0]",
michael@0 103 "[paras[0].firstChild, 0, paras[0].firstChild, 1]",
michael@0 104 "[paras[0].firstChild, 2, paras[0].firstChild, 8]",
michael@0 105 "[paras[0].firstChild, 2, paras[0].firstChild, 9]",
michael@0 106 "[paras[1].firstChild, 0, paras[1].firstChild, 0]",
michael@0 107 "[paras[1].firstChild, 0, paras[1].firstChild, 1]",
michael@0 108 "[paras[1].firstChild, 2, paras[1].firstChild, 8]",
michael@0 109 "[paras[1].firstChild, 2, paras[1].firstChild, 9]",
michael@0 110 "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]",
michael@0 111 "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]",
michael@0 112 "[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]",
michael@0 113 "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]",
michael@0 114 "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]",
michael@0 115 "[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]",
michael@0 116 // Now try testing some elements, not just text nodes.
michael@0 117 "[document.documentElement, 0, document.documentElement, 1]",
michael@0 118 "[document.documentElement, 0, document.documentElement, 2]",
michael@0 119 "[document.documentElement, 1, document.documentElement, 2]",
michael@0 120 "[document.head, 1, document.head, 1]",
michael@0 121 "[document.body, 0, document.body, 1]",
michael@0 122 "[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]",
michael@0 123 "[foreignDoc.head, 1, foreignDoc.head, 1]",
michael@0 124 "[foreignDoc.body, 0, foreignDoc.body, 0]",
michael@0 125 "[paras[0], 0, paras[0], 0]",
michael@0 126 "[paras[0], 0, paras[0], 1]",
michael@0 127 "[detachedPara1, 0, detachedPara1, 0]",
michael@0 128 "[detachedPara1, 0, detachedPara1, 1]",
michael@0 129 // Now try some ranges that span elements.
michael@0 130 "[paras[0].firstChild, 0, paras[1].firstChild, 0]",
michael@0 131 "[paras[0].firstChild, 0, paras[1].firstChild, 8]",
michael@0 132 "[paras[0].firstChild, 3, paras[3], 1]",
michael@0 133 // How about something that spans a node and its descendant?
michael@0 134 "[paras[0], 0, paras[0].firstChild, 7]",
michael@0 135 "[testDiv, 2, paras[4], 1]",
michael@0 136 "[testDiv, 1, paras[2].firstChild, 5]",
michael@0 137 "[document.documentElement, 1, document.body, 0]",
michael@0 138 "[foreignDoc.documentElement, 1, foreignDoc.body, 0]",
michael@0 139 // Then a few more interesting things just for good measure.
michael@0 140 "[document, 0, document, 1]",
michael@0 141 "[document, 0, document, 2]",
michael@0 142 "[document, 1, document, 2]",
michael@0 143 "[testDiv, 0, comment, 5]",
michael@0 144 "[paras[2].firstChild, 4, comment, 2]",
michael@0 145 "[paras[3], 1, comment, 8]",
michael@0 146 "[foreignDoc, 0, foreignDoc, 0]",
michael@0 147 "[foreignDoc, 1, foreignComment, 2]",
michael@0 148 "[foreignDoc.body, 0, foreignTextNode, 36]",
michael@0 149 "[xmlDoc, 0, xmlDoc, 0]",
michael@0 150 // Opera 11 crashes if you extractContents() a range that ends at offset
michael@0 151 // zero in a comment. Comment out this line to run the tests successfully.
michael@0 152 "[xmlDoc, 1, xmlComment, 0]",
michael@0 153 "[detachedTextNode, 0, detachedTextNode, 8]",
michael@0 154 "[detachedForeignTextNode, 7, detachedForeignTextNode, 7]",
michael@0 155 "[detachedForeignTextNode, 0, detachedForeignTextNode, 8]",
michael@0 156 "[detachedXmlTextNode, 7, detachedXmlTextNode, 7]",
michael@0 157 "[detachedXmlTextNode, 0, detachedXmlTextNode, 8]",
michael@0 158 "[detachedComment, 3, detachedComment, 4]",
michael@0 159 "[detachedComment, 5, detachedComment, 5]",
michael@0 160 "[detachedForeignComment, 0, detachedForeignComment, 1]",
michael@0 161 "[detachedForeignComment, 4, detachedForeignComment, 4]",
michael@0 162 "[detachedXmlComment, 2, detachedXmlComment, 6]",
michael@0 163 "[docfrag, 0, docfrag, 0]",
michael@0 164 "[foreignDocfrag, 0, foreignDocfrag, 0]",
michael@0 165 "[xmlDocfrag, 0, xmlDocfrag, 0]",
michael@0 166 ];
michael@0 167
michael@0 168 testPoints = [
michael@0 169 // Various positions within the page, some invalid. Remember that
michael@0 170 // paras[0] is visible, and paras[1] is display: none.
michael@0 171 "[paras[0].firstChild, -1]",
michael@0 172 "[paras[0].firstChild, 0]",
michael@0 173 "[paras[0].firstChild, 1]",
michael@0 174 "[paras[0].firstChild, 2]",
michael@0 175 "[paras[0].firstChild, 8]",
michael@0 176 "[paras[0].firstChild, 9]",
michael@0 177 "[paras[0].firstChild, 10]",
michael@0 178 "[paras[0].firstChild, 65535]",
michael@0 179 "[paras[1].firstChild, -1]",
michael@0 180 "[paras[1].firstChild, 0]",
michael@0 181 "[paras[1].firstChild, 1]",
michael@0 182 "[paras[1].firstChild, 2]",
michael@0 183 "[paras[1].firstChild, 8]",
michael@0 184 "[paras[1].firstChild, 9]",
michael@0 185 "[paras[1].firstChild, 10]",
michael@0 186 "[paras[1].firstChild, 65535]",
michael@0 187 "[detachedPara1.firstChild, 0]",
michael@0 188 "[detachedPara1.firstChild, 1]",
michael@0 189 "[detachedPara1.firstChild, 8]",
michael@0 190 "[detachedPara1.firstChild, 9]",
michael@0 191 "[foreignPara1.firstChild, 0]",
michael@0 192 "[foreignPara1.firstChild, 1]",
michael@0 193 "[foreignPara1.firstChild, 8]",
michael@0 194 "[foreignPara1.firstChild, 9]",
michael@0 195 // Now try testing some elements, not just text nodes.
michael@0 196 "[document.documentElement, -1]",
michael@0 197 "[document.documentElement, 0]",
michael@0 198 "[document.documentElement, 1]",
michael@0 199 "[document.documentElement, 2]",
michael@0 200 "[document.documentElement, 7]",
michael@0 201 "[document.head, 1]",
michael@0 202 "[document.body, 3]",
michael@0 203 "[foreignDoc.documentElement, 0]",
michael@0 204 "[foreignDoc.documentElement, 1]",
michael@0 205 "[foreignDoc.head, 0]",
michael@0 206 "[foreignDoc.body, 1]",
michael@0 207 "[paras[0], 0]",
michael@0 208 "[paras[0], 1]",
michael@0 209 "[paras[0], 2]",
michael@0 210 "[paras[1], 0]",
michael@0 211 "[paras[1], 1]",
michael@0 212 "[paras[1], 2]",
michael@0 213 "[detachedPara1, 0]",
michael@0 214 "[detachedPara1, 1]",
michael@0 215 "[testDiv, 0]",
michael@0 216 "[testDiv, 3]",
michael@0 217 // Then a few more interesting things just for good measure.
michael@0 218 "[document, -1]",
michael@0 219 "[document, 0]",
michael@0 220 "[document, 1]",
michael@0 221 "[document, 2]",
michael@0 222 "[document, 3]",
michael@0 223 "[comment, -1]",
michael@0 224 "[comment, 0]",
michael@0 225 "[comment, 4]",
michael@0 226 "[comment, 96]",
michael@0 227 "[foreignDoc, 0]",
michael@0 228 "[foreignDoc, 1]",
michael@0 229 "[foreignComment, 2]",
michael@0 230 "[foreignTextNode, 0]",
michael@0 231 "[foreignTextNode, 36]",
michael@0 232 "[xmlDoc, -1]",
michael@0 233 "[xmlDoc, 0]",
michael@0 234 "[xmlDoc, 1]",
michael@0 235 "[xmlDoc, 5]",
michael@0 236 "[xmlComment, 0]",
michael@0 237 "[xmlComment, 4]",
michael@0 238 "[processingInstruction, 0]",
michael@0 239 "[processingInstruction, 5]",
michael@0 240 "[processingInstruction, 9]",
michael@0 241 "[detachedTextNode, 0]",
michael@0 242 "[detachedTextNode, 8]",
michael@0 243 "[detachedForeignTextNode, 0]",
michael@0 244 "[detachedForeignTextNode, 8]",
michael@0 245 "[detachedXmlTextNode, 0]",
michael@0 246 "[detachedXmlTextNode, 8]",
michael@0 247 "[detachedProcessingInstruction, 12]",
michael@0 248 "[detachedComment, 3]",
michael@0 249 "[detachedComment, 5]",
michael@0 250 "[detachedForeignComment, 0]",
michael@0 251 "[detachedForeignComment, 4]",
michael@0 252 "[detachedXmlComment, 2]",
michael@0 253 "[docfrag, 0]",
michael@0 254 "[foreignDocfrag, 0]",
michael@0 255 "[xmlDocfrag, 0]",
michael@0 256 "[doctype, 0]",
michael@0 257 "[doctype, -17]",
michael@0 258 "[doctype, 1]",
michael@0 259 "[foreignDoctype, 0]",
michael@0 260 "[xmlDoctype, 0]",
michael@0 261 ];
michael@0 262
michael@0 263 testNodes = [
michael@0 264 "paras[0]",
michael@0 265 "paras[0].firstChild",
michael@0 266 "paras[1]",
michael@0 267 "paras[1].firstChild",
michael@0 268 "foreignPara1",
michael@0 269 "foreignPara1.firstChild",
michael@0 270 "detachedPara1",
michael@0 271 "detachedPara1.firstChild",
michael@0 272 "detachedPara1",
michael@0 273 "detachedPara1.firstChild",
michael@0 274 "testDiv",
michael@0 275 "document",
michael@0 276 "detachedDiv",
michael@0 277 "detachedPara2",
michael@0 278 "foreignDoc",
michael@0 279 "foreignPara2",
michael@0 280 "xmlDoc",
michael@0 281 "xmlElement",
michael@0 282 "detachedXmlElement",
michael@0 283 "detachedTextNode",
michael@0 284 "foreignTextNode",
michael@0 285 "detachedForeignTextNode",
michael@0 286 "xmlTextNode",
michael@0 287 "detachedXmlTextNode",
michael@0 288 "processingInstruction",
michael@0 289 "detachedProcessingInstruction",
michael@0 290 "comment",
michael@0 291 "detachedComment",
michael@0 292 "foreignComment",
michael@0 293 "detachedForeignComment",
michael@0 294 "xmlComment",
michael@0 295 "detachedXmlComment",
michael@0 296 "docfrag",
michael@0 297 "foreignDocfrag",
michael@0 298 "xmlDocfrag",
michael@0 299 "doctype",
michael@0 300 "foreignDoctype",
michael@0 301 "xmlDoctype",
michael@0 302 ];
michael@0 303 }
michael@0 304 if ("setup" in window) {
michael@0 305 setup(setupRangeTests);
michael@0 306 } else {
michael@0 307 // Presumably we're running from within an iframe or something
michael@0 308 setupRangeTests();
michael@0 309 }
michael@0 310
michael@0 311 /**
michael@0 312 * Return the length of a node as specified in DOM Range.
michael@0 313 */
michael@0 314 function getNodeLength(node) {
michael@0 315 if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
michael@0 316 return 0;
michael@0 317 }
michael@0 318 if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.PROCESSING_INSTRUCTION_NODE || node.nodeType == Node.COMMENT_NODE) {
michael@0 319 return node.length;
michael@0 320 }
michael@0 321 return node.childNodes.length;
michael@0 322 }
michael@0 323
michael@0 324 /**
michael@0 325 * Returns the furthest ancestor of a Node as defined by the spec.
michael@0 326 */
michael@0 327 function furthestAncestor(node) {
michael@0 328 var root = node;
michael@0 329 while (root.parentNode != null) {
michael@0 330 root = root.parentNode;
michael@0 331 }
michael@0 332 return root;
michael@0 333 }
michael@0 334
michael@0 335 /**
michael@0 336 * "The ancestor containers of a Node are the Node itself and all its
michael@0 337 * ancestors."
michael@0 338 *
michael@0 339 * Is node1 an ancestor container of node2?
michael@0 340 */
michael@0 341 function isAncestorContainer(node1, node2) {
michael@0 342 return node1 == node2 ||
michael@0 343 (node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS);
michael@0 344 }
michael@0 345
michael@0 346 /**
michael@0 347 * Returns the first Node that's after node in tree order, or null if node is
michael@0 348 * the last Node.
michael@0 349 */
michael@0 350 function nextNode(node) {
michael@0 351 if (node.hasChildNodes()) {
michael@0 352 return node.firstChild;
michael@0 353 }
michael@0 354 return nextNodeDescendants(node);
michael@0 355 }
michael@0 356
michael@0 357 /**
michael@0 358 * Returns the last Node that's before node in tree order, or null if node is
michael@0 359 * the first Node.
michael@0 360 */
michael@0 361 function previousNode(node) {
michael@0 362 if (node.previousSibling) {
michael@0 363 node = node.previousSibling;
michael@0 364 while (node.hasChildNodes()) {
michael@0 365 node = node.lastChild;
michael@0 366 }
michael@0 367 return node;
michael@0 368 }
michael@0 369 return node.parentNode;
michael@0 370 }
michael@0 371
michael@0 372 /**
michael@0 373 * Returns the next Node that's after node and all its descendants in tree
michael@0 374 * order, or null if node is the last Node or an ancestor of it.
michael@0 375 */
michael@0 376 function nextNodeDescendants(node) {
michael@0 377 while (node && !node.nextSibling) {
michael@0 378 node = node.parentNode;
michael@0 379 }
michael@0 380 if (!node) {
michael@0 381 return null;
michael@0 382 }
michael@0 383 return node.nextSibling;
michael@0 384 }
michael@0 385
michael@0 386 /**
michael@0 387 * Returns the ownerDocument of the Node, or the Node itself if it's a
michael@0 388 * Document.
michael@0 389 */
michael@0 390 function ownerDocument(node) {
michael@0 391 return node.nodeType == Node.DOCUMENT_NODE
michael@0 392 ? node
michael@0 393 : node.ownerDocument;
michael@0 394 }
michael@0 395
michael@0 396 /**
michael@0 397 * Returns true if ancestor is an ancestor of descendant, false otherwise.
michael@0 398 */
michael@0 399 function isAncestor(ancestor, descendant) {
michael@0 400 if (!ancestor || !descendant) {
michael@0 401 return false;
michael@0 402 }
michael@0 403 while (descendant && descendant != ancestor) {
michael@0 404 descendant = descendant.parentNode;
michael@0 405 }
michael@0 406 return descendant == ancestor;
michael@0 407 }
michael@0 408
michael@0 409 /**
michael@0 410 * Returns true if descendant is a descendant of ancestor, false otherwise.
michael@0 411 */
michael@0 412 function isDescendant(descendant, ancestor) {
michael@0 413 return isAncestor(ancestor, descendant);
michael@0 414 }
michael@0 415
michael@0 416 /**
michael@0 417 * The position of two boundary points relative to one another, as defined by
michael@0 418 * the spec.
michael@0 419 */
michael@0 420 function getPosition(nodeA, offsetA, nodeB, offsetB) {
michael@0 421 // "If node A is the same as node B, return equal if offset A equals offset
michael@0 422 // B, before if offset A is less than offset B, and after if offset A is
michael@0 423 // greater than offset B."
michael@0 424 if (nodeA == nodeB) {
michael@0 425 if (offsetA == offsetB) {
michael@0 426 return "equal";
michael@0 427 }
michael@0 428 if (offsetA < offsetB) {
michael@0 429 return "before";
michael@0 430 }
michael@0 431 if (offsetA > offsetB) {
michael@0 432 return "after";
michael@0 433 }
michael@0 434 }
michael@0 435
michael@0 436 // "If node A is after node B in tree order, compute the position of (node
michael@0 437 // B, offset B) relative to (node A, offset A). If it is before, return
michael@0 438 // after. If it is after, return before."
michael@0 439 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
michael@0 440 var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
michael@0 441 if (pos == "before") {
michael@0 442 return "after";
michael@0 443 }
michael@0 444 if (pos == "after") {
michael@0 445 return "before";
michael@0 446 }
michael@0 447 }
michael@0 448
michael@0 449 // "If node A is an ancestor of node B:"
michael@0 450 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
michael@0 451 // "Let child equal node B."
michael@0 452 var child = nodeB;
michael@0 453
michael@0 454 // "While child is not a child of node A, set child to its parent."
michael@0 455 while (child.parentNode != nodeA) {
michael@0 456 child = child.parentNode;
michael@0 457 }
michael@0 458
michael@0 459 // "If the index of child is less than offset A, return after."
michael@0 460 if (indexOf(child) < offsetA) {
michael@0 461 return "after";
michael@0 462 }
michael@0 463 }
michael@0 464
michael@0 465 // "Return before."
michael@0 466 return "before";
michael@0 467 }
michael@0 468
michael@0 469 /**
michael@0 470 * "contained" as defined by DOM Range: "A Node node is contained in a range
michael@0 471 * range if node's furthest ancestor is the same as range's root, and (node, 0)
michael@0 472 * is after range's start, and (node, length of node) is before range's end."
michael@0 473 */
michael@0 474 function isContained(node, range) {
michael@0 475 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
michael@0 476 var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
michael@0 477
michael@0 478 return furthestAncestor(node) == furthestAncestor(range.startContainer)
michael@0 479 && pos1 == "after"
michael@0 480 && pos2 == "before";
michael@0 481 }
michael@0 482
michael@0 483 /**
michael@0 484 * "partially contained" as defined by DOM Range: "A Node is partially
michael@0 485 * contained in a range if it is an ancestor container of the range's start but
michael@0 486 * not its end, or vice versa."
michael@0 487 */
michael@0 488 function isPartiallyContained(node, range) {
michael@0 489 var cond1 = isAncestorContainer(node, range.startContainer);
michael@0 490 var cond2 = isAncestorContainer(node, range.endContainer);
michael@0 491 return (cond1 && !cond2) || (cond2 && !cond1);
michael@0 492 }
michael@0 493
michael@0 494 /**
michael@0 495 * Index of a node as defined by the spec.
michael@0 496 */
michael@0 497 function indexOf(node) {
michael@0 498 if (!node.parentNode) {
michael@0 499 // No preceding sibling nodes, right?
michael@0 500 return 0;
michael@0 501 }
michael@0 502 var i = 0;
michael@0 503 while (node != node.parentNode.childNodes[i]) {
michael@0 504 i++;
michael@0 505 }
michael@0 506 return i;
michael@0 507 }
michael@0 508
michael@0 509 /**
michael@0 510 * extractContents() implementation, following the spec. If an exception is
michael@0 511 * supposed to be thrown, will return a string with the name (e.g.,
michael@0 512 * "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also
michael@0 513 * return an arbitrary human-readable string if a condition is hit that implies
michael@0 514 * a spec bug.
michael@0 515 */
michael@0 516 function myExtractContents(range) {
michael@0 517 // "If the context object's detached flag is set, raise an
michael@0 518 // INVALID_STATE_ERR exception and abort these steps."
michael@0 519 try {
michael@0 520 range.collapsed;
michael@0 521 } catch (e) {
michael@0 522 return "INVALID_STATE_ERR";
michael@0 523 }
michael@0 524
michael@0 525 // "Let frag be a new DocumentFragment whose ownerDocument is the same as
michael@0 526 // the ownerDocument of the context object's start node."
michael@0 527 var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE
michael@0 528 ? range.startContainer
michael@0 529 : range.startContainer.ownerDocument;
michael@0 530 var frag = ownerDoc.createDocumentFragment();
michael@0 531
michael@0 532 // "If the context object's start and end are the same, abort this method,
michael@0 533 // returning frag."
michael@0 534 if (range.startContainer == range.endContainer
michael@0 535 && range.startOffset == range.endOffset) {
michael@0 536 return frag;
michael@0 537 }
michael@0 538
michael@0 539 // "Let original start node, original start offset, original end node, and
michael@0 540 // original end offset be the context object's start and end nodes and
michael@0 541 // offsets, respectively."
michael@0 542 var originalStartNode = range.startContainer;
michael@0 543 var originalStartOffset = range.startOffset;
michael@0 544 var originalEndNode = range.endContainer;
michael@0 545 var originalEndOffset = range.endOffset;
michael@0 546
michael@0 547 // "If original start node and original end node are the same, and they are
michael@0 548 // a Text or Comment node:"
michael@0 549 if (range.startContainer == range.endContainer
michael@0 550 && (range.startContainer.nodeType == Node.TEXT_NODE
michael@0 551 || range.startContainer.nodeType == Node.COMMENT_NODE)) {
michael@0 552 // "Let clone be the result of calling cloneNode(false) on original
michael@0 553 // start node."
michael@0 554 var clone = originalStartNode.cloneNode(false);
michael@0 555
michael@0 556 // "Set the data of clone to the result of calling
michael@0 557 // substringData(original start offset, original end offset − original
michael@0 558 // start offset) on original start node."
michael@0 559 clone.data = originalStartNode.substringData(originalStartOffset,
michael@0 560 originalEndOffset - originalStartOffset);
michael@0 561
michael@0 562 // "Append clone as the last child of frag."
michael@0 563 frag.appendChild(clone);
michael@0 564
michael@0 565 // "Call deleteData(original start offset, original end offset −
michael@0 566 // original start offset) on original start node."
michael@0 567 originalStartNode.deleteData(originalStartOffset,
michael@0 568 originalEndOffset - originalStartOffset);
michael@0 569
michael@0 570 // "Abort this method, returning frag."
michael@0 571 return frag;
michael@0 572 }
michael@0 573
michael@0 574 // "Let common ancestor equal original start node."
michael@0 575 var commonAncestor = originalStartNode;
michael@0 576
michael@0 577 // "While common ancestor is not an ancestor container of original end
michael@0 578 // node, set common ancestor to its own parent."
michael@0 579 while (!isAncestorContainer(commonAncestor, originalEndNode)) {
michael@0 580 commonAncestor = commonAncestor.parentNode;
michael@0 581 }
michael@0 582
michael@0 583 // "If original start node is an ancestor container of original end node,
michael@0 584 // let first partially contained child be null."
michael@0 585 var firstPartiallyContainedChild;
michael@0 586 if (isAncestorContainer(originalStartNode, originalEndNode)) {
michael@0 587 firstPartiallyContainedChild = null;
michael@0 588 // "Otherwise, let first partially contained child be the first child of
michael@0 589 // common ancestor that is partially contained in the context object."
michael@0 590 } else {
michael@0 591 for (var i = 0; i < commonAncestor.childNodes.length; i++) {
michael@0 592 if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
michael@0 593 firstPartiallyContainedChild = commonAncestor.childNodes[i];
michael@0 594 break;
michael@0 595 }
michael@0 596 }
michael@0 597 if (!firstPartiallyContainedChild) {
michael@0 598 throw "Spec bug: no first partially contained child!";
michael@0 599 }
michael@0 600 }
michael@0 601
michael@0 602 // "If original end node is an ancestor container of original start node,
michael@0 603 // let last partially contained child be null."
michael@0 604 var lastPartiallyContainedChild;
michael@0 605 if (isAncestorContainer(originalEndNode, originalStartNode)) {
michael@0 606 lastPartiallyContainedChild = null;
michael@0 607 // "Otherwise, let last partially contained child be the last child of
michael@0 608 // common ancestor that is partially contained in the context object."
michael@0 609 } else {
michael@0 610 for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) {
michael@0 611 if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
michael@0 612 lastPartiallyContainedChild = commonAncestor.childNodes[i];
michael@0 613 break;
michael@0 614 }
michael@0 615 }
michael@0 616 if (!lastPartiallyContainedChild) {
michael@0 617 throw "Spec bug: no last partially contained child!";
michael@0 618 }
michael@0 619 }
michael@0 620
michael@0 621 // "Let contained children be a list of all children of common ancestor
michael@0 622 // that are contained in the context object, in tree order."
michael@0 623 //
michael@0 624 // "If any member of contained children is a DocumentType, raise a
michael@0 625 // HIERARCHY_REQUEST_ERR exception and abort these steps."
michael@0 626 var containedChildren = [];
michael@0 627 for (var i = 0; i < commonAncestor.childNodes.length; i++) {
michael@0 628 if (isContained(commonAncestor.childNodes[i], range)) {
michael@0 629 if (commonAncestor.childNodes[i].nodeType
michael@0 630 == Node.DOCUMENT_TYPE_NODE) {
michael@0 631 return "HIERARCHY_REQUEST_ERR";
michael@0 632 }
michael@0 633 containedChildren.push(commonAncestor.childNodes[i]);
michael@0 634 }
michael@0 635 }
michael@0 636
michael@0 637 // "If original start node is an ancestor container of original end node,
michael@0 638 // set new node to original start node and new offset to original start
michael@0 639 // offset."
michael@0 640 var newNode, newOffset;
michael@0 641 if (isAncestorContainer(originalStartNode, originalEndNode)) {
michael@0 642 newNode = originalStartNode;
michael@0 643 newOffset = originalStartOffset;
michael@0 644 // "Otherwise:"
michael@0 645 } else {
michael@0 646 // "Let reference node equal original start node."
michael@0 647 var referenceNode = originalStartNode;
michael@0 648
michael@0 649 // "While reference node's parent is not null and is not an ancestor
michael@0 650 // container of original end node, set reference node to its parent."
michael@0 651 while (referenceNode.parentNode
michael@0 652 && !isAncestorContainer(referenceNode.parentNode, originalEndNode)) {
michael@0 653 referenceNode = referenceNode.parentNode;
michael@0 654 }
michael@0 655
michael@0 656 // "Set new node to the parent of reference node, and new offset to one
michael@0 657 // plus the index of reference node."
michael@0 658 newNode = referenceNode.parentNode;
michael@0 659 newOffset = 1 + indexOf(referenceNode);
michael@0 660 }
michael@0 661
michael@0 662 // "If first partially contained child is a Text or Comment node:"
michael@0 663 if (firstPartiallyContainedChild
michael@0 664 && (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE
michael@0 665 || firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
michael@0 666 // "Let clone be the result of calling cloneNode(false) on original
michael@0 667 // start node."
michael@0 668 var clone = originalStartNode.cloneNode(false);
michael@0 669
michael@0 670 // "Set the data of clone to the result of calling substringData() on
michael@0 671 // original start node, with original start offset as the first
michael@0 672 // argument and (length of original start node − original start offset)
michael@0 673 // as the second."
michael@0 674 clone.data = originalStartNode.substringData(originalStartOffset,
michael@0 675 getNodeLength(originalStartNode) - originalStartOffset);
michael@0 676
michael@0 677 // "Append clone as the last child of frag."
michael@0 678 frag.appendChild(clone);
michael@0 679
michael@0 680 // "Call deleteData() on original start node, with original start
michael@0 681 // offset as the first argument and (length of original start node −
michael@0 682 // original start offset) as the second."
michael@0 683 originalStartNode.deleteData(originalStartOffset,
michael@0 684 getNodeLength(originalStartNode) - originalStartOffset);
michael@0 685 // "Otherwise, if first partially contained child is not null:"
michael@0 686 } else if (firstPartiallyContainedChild) {
michael@0 687 // "Let clone be the result of calling cloneNode(false) on first
michael@0 688 // partially contained child."
michael@0 689 var clone = firstPartiallyContainedChild.cloneNode(false);
michael@0 690
michael@0 691 // "Append clone as the last child of frag."
michael@0 692 frag.appendChild(clone);
michael@0 693
michael@0 694 // "Let subrange be a new Range whose start is (original start node,
michael@0 695 // original start offset) and whose end is (first partially contained
michael@0 696 // child, length of first partially contained child)."
michael@0 697 var subrange = ownerDoc.createRange();
michael@0 698 subrange.setStart(originalStartNode, originalStartOffset);
michael@0 699 subrange.setEnd(firstPartiallyContainedChild,
michael@0 700 getNodeLength(firstPartiallyContainedChild));
michael@0 701
michael@0 702 // "Let subfrag be the result of calling extractContents() on
michael@0 703 // subrange."
michael@0 704 var subfrag = myExtractContents(subrange);
michael@0 705
michael@0 706 // "For each child of subfrag, in order, append that child to clone as
michael@0 707 // its last child."
michael@0 708 for (var i = 0; i < subfrag.childNodes.length; i++) {
michael@0 709 clone.appendChild(subfrag.childNodes[i]);
michael@0 710 }
michael@0 711 }
michael@0 712
michael@0 713 // "For each contained child in contained children, append contained child
michael@0 714 // as the last child of frag."
michael@0 715 for (var i = 0; i < containedChildren.length; i++) {
michael@0 716 frag.appendChild(containedChildren[i]);
michael@0 717 }
michael@0 718
michael@0 719 // "If last partially contained child is a Text or Comment node:"
michael@0 720 if (lastPartiallyContainedChild
michael@0 721 && (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE
michael@0 722 || lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
michael@0 723 // "Let clone be the result of calling cloneNode(false) on original
michael@0 724 // end node."
michael@0 725 var clone = originalEndNode.cloneNode(false);
michael@0 726
michael@0 727 // "Set the data of clone to the result of calling substringData(0,
michael@0 728 // original end offset) on original end node."
michael@0 729 clone.data = originalEndNode.substringData(0, originalEndOffset);
michael@0 730
michael@0 731 // "Append clone as the last child of frag."
michael@0 732 frag.appendChild(clone);
michael@0 733
michael@0 734 // "Call deleteData(0, original end offset) on original end node."
michael@0 735 originalEndNode.deleteData(0, originalEndOffset);
michael@0 736 // "Otherwise, if last partially contained child is not null:"
michael@0 737 } else if (lastPartiallyContainedChild) {
michael@0 738 // "Let clone be the result of calling cloneNode(false) on last
michael@0 739 // partially contained child."
michael@0 740 var clone = lastPartiallyContainedChild.cloneNode(false);
michael@0 741
michael@0 742 // "Append clone as the last child of frag."
michael@0 743 frag.appendChild(clone);
michael@0 744
michael@0 745 // "Let subrange be a new Range whose start is (last partially
michael@0 746 // contained child, 0) and whose end is (original end node, original
michael@0 747 // end offset)."
michael@0 748 var subrange = ownerDoc.createRange();
michael@0 749 subrange.setStart(lastPartiallyContainedChild, 0);
michael@0 750 subrange.setEnd(originalEndNode, originalEndOffset);
michael@0 751
michael@0 752 // "Let subfrag be the result of calling extractContents() on
michael@0 753 // subrange."
michael@0 754 var subfrag = myExtractContents(subrange);
michael@0 755
michael@0 756 // "For each child of subfrag, in order, append that child to clone as
michael@0 757 // its last child."
michael@0 758 for (var i = 0; i < subfrag.childNodes.length; i++) {
michael@0 759 clone.appendChild(subfrag.childNodes[i]);
michael@0 760 }
michael@0 761 }
michael@0 762
michael@0 763 // "Set the context object's start and end to (new node, new offset)."
michael@0 764 range.setStart(newNode, newOffset);
michael@0 765 range.setEnd(newNode, newOffset);
michael@0 766
michael@0 767 // "Return frag."
michael@0 768 return frag;
michael@0 769 }
michael@0 770
michael@0 771 /**
michael@0 772 * insertNode() implementation, following the spec. If an exception is
michael@0 773 * supposed to be thrown, will return a string with the name (e.g.,
michael@0 774 * "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also
michael@0 775 * return an arbitrary human-readable string if a condition is hit that implies
michael@0 776 * a spec bug.
michael@0 777 */
michael@0 778 function myInsertNode(range, newNode) {
michael@0 779 // "If the context object's detached flag is set, raise an
michael@0 780 // INVALID_STATE_ERR exception and abort these steps."
michael@0 781 //
michael@0 782 // Assume that if accessing collapsed throws, it's detached.
michael@0 783 try {
michael@0 784 range.collapsed;
michael@0 785 } catch (e) {
michael@0 786 return "INVALID_STATE_ERR";
michael@0 787 }
michael@0 788
michael@0 789 // "If the context object's start node is a Text or Comment node and its
michael@0 790 // parent is null, raise an HIERARCHY_REQUEST_ERR exception and abort these
michael@0 791 // steps."
michael@0 792 if ((range.startContainer.nodeType == Node.TEXT_NODE
michael@0 793 || range.startContainer.nodeType == Node.COMMENT_NODE)
michael@0 794 && !range.startContainer.parentNode) {
michael@0 795 return "HIERARCHY_REQUEST_ERR";
michael@0 796 }
michael@0 797
michael@0 798 // "If the context object's start node is a Text node, run splitText() on
michael@0 799 // it with the context object's start offset as its argument, and let
michael@0 800 // reference node be the result."
michael@0 801 var referenceNode;
michael@0 802 if (range.startContainer.nodeType == Node.TEXT_NODE) {
michael@0 803 // We aren't testing how ranges vary under mutations, and browsers vary
michael@0 804 // in how they mutate for splitText, so let's just force the correct
michael@0 805 // way.
michael@0 806 var start = [range.startContainer, range.startOffset];
michael@0 807 var end = [range.endContainer, range.endOffset];
michael@0 808
michael@0 809 referenceNode = range.startContainer.splitText(range.startOffset);
michael@0 810
michael@0 811 if (start[0] == end[0]
michael@0 812 && end[1] > start[1]) {
michael@0 813 end[0] = referenceNode;
michael@0 814 end[1] -= start[1];
michael@0 815 } else if (end[0] == start[0].parentNode
michael@0 816 && end[1] > indexOf(referenceNode)) {
michael@0 817 end[1]++;
michael@0 818 }
michael@0 819 range.setStart(start[0], start[1]);
michael@0 820 range.setEnd(end[0], end[1]);
michael@0 821 // "Otherwise, if the context object's start node is a Comment, let
michael@0 822 // reference node be the context object's start node."
michael@0 823 } else if (range.startContainer.nodeType == Node.COMMENT_NODE) {
michael@0 824 referenceNode = range.startContainer;
michael@0 825 // "Otherwise, let reference node be the child of the context object's
michael@0 826 // start node with index equal to the context object's start offset, or
michael@0 827 // null if there is no such child."
michael@0 828 } else {
michael@0 829 referenceNode = range.startContainer.childNodes[range.startOffset];
michael@0 830 if (typeof referenceNode == "undefined") {
michael@0 831 referenceNode = null;
michael@0 832 }
michael@0 833 }
michael@0 834
michael@0 835 // "If reference node is null, let parent node be the context object's
michael@0 836 // start node."
michael@0 837 var parentNode;
michael@0 838 if (!referenceNode) {
michael@0 839 parentNode = range.startContainer;
michael@0 840 // "Otherwise, let parent node be the parent of reference node."
michael@0 841 } else {
michael@0 842 parentNode = referenceNode.parentNode;
michael@0 843 }
michael@0 844
michael@0 845 // "Call insertBefore(newNode, reference node) on parent node, re-raising
michael@0 846 // any exceptions that call raised."
michael@0 847 try {
michael@0 848 parentNode.insertBefore(newNode, referenceNode);
michael@0 849 } catch (e) {
michael@0 850 return getDomExceptionName(e);
michael@0 851 }
michael@0 852 }
michael@0 853
michael@0 854 /**
michael@0 855 * Asserts that two nodes are equal, in the sense of isEqualNode(). If they
michael@0 856 * aren't, tries to print a relatively informative reason why not. TODO: Move
michael@0 857 * this to testharness.js?
michael@0 858 */
michael@0 859 function assertNodesEqual(actual, expected, msg) {
michael@0 860 if (!actual.isEqualNode(expected)) {
michael@0 861 msg = "Actual and expected mismatch for " + msg + ". ";
michael@0 862
michael@0 863 while (actual && expected) {
michael@0 864 assert_true(actual.nodeType === expected.nodeType
michael@0 865 && actual.nodeName === expected.nodeName
michael@0 866 && actual.nodeValue === expected.nodeValue
michael@0 867 && actual.childNodes.length === expected.childNodes.length,
michael@0 868 "First differing node: expected " + format_value(expected)
michael@0 869 + ", got " + format_value(actual));
michael@0 870 actual = nextNode(actual);
michael@0 871 expected = nextNode(expected);
michael@0 872 }
michael@0 873
michael@0 874 assert_unreached("DOMs were not equal but we couldn't figure out why");
michael@0 875 }
michael@0 876 }
michael@0 877
michael@0 878 /**
michael@0 879 * Given a DOMException, return the name (e.g., "HIERARCHY_REQUEST_ERR"). In
michael@0 880 * theory this should be just e.name, but in practice it's not. So I could
michael@0 881 * legitimately just return e.name, but then every engine but WebKit would fail
michael@0 882 * every test, since no one seems to care much for standardizing DOMExceptions.
michael@0 883 * Instead I mangle it to account for browser bugs, so as not to fail
michael@0 884 * insertNode() tests (for instance) for insertBefore() bugs. Of course, a
michael@0 885 * standards-compliant browser will work right in any event.
michael@0 886 *
michael@0 887 * If the exception has no string property called "name" or "message", we just
michael@0 888 * re-throw it.
michael@0 889 */
michael@0 890 function getDomExceptionName(e) {
michael@0 891 if (typeof e.name == "string"
michael@0 892 && /^[A-Z_]+_ERR$/.test(e.name)) {
michael@0 893 // Either following the standard, or prefixing NS_ERROR_DOM (I'm
michael@0 894 // looking at you, Gecko).
michael@0 895 return e.name.replace(/^NS_ERROR_DOM_/, "");
michael@0 896 }
michael@0 897
michael@0 898 if (typeof e.message == "string"
michael@0 899 && /^[A-Z_]+_ERR$/.test(e.message)) {
michael@0 900 // Opera
michael@0 901 return e.message;
michael@0 902 }
michael@0 903
michael@0 904 if (typeof e.message == "string"
michael@0 905 && /^DOM Exception:/.test(e.message)) {
michael@0 906 // IE
michael@0 907 return /[A-Z_]+_ERR/.exec(e.message)[0];
michael@0 908 }
michael@0 909
michael@0 910 throw e;
michael@0 911 }
michael@0 912
michael@0 913 /**
michael@0 914 * Given an array of endpoint data [start container, start offset, end
michael@0 915 * container, end offset], returns a Range with those endpoints.
michael@0 916 */
michael@0 917 function rangeFromEndpoints(endpoints) {
michael@0 918 // If we just use document instead of the ownerDocument of endpoints[0],
michael@0 919 // WebKit will throw on setStart/setEnd. This is a WebKit bug, but it's in
michael@0 920 // range, not selection, so we don't want to fail anything for it.
michael@0 921 var range = ownerDocument(endpoints[0]).createRange();
michael@0 922 range.setStart(endpoints[0], endpoints[1]);
michael@0 923 range.setEnd(endpoints[2], endpoints[3]);
michael@0 924 return range;
michael@0 925 }
michael@0 926
michael@0 927 /**
michael@0 928 * Given an array of endpoint data [start container, start offset, end
michael@0 929 * container, end offset], sets the selection to have those endpoints. Uses
michael@0 930 * addRange, so the range will be forwards. Accepts an empty array for
michael@0 931 * endpoints, in which case the selection will just be emptied.
michael@0 932 */
michael@0 933 function setSelectionForwards(endpoints) {
michael@0 934 selection.removeAllRanges();
michael@0 935 if (endpoints.length) {
michael@0 936 selection.addRange(rangeFromEndpoints(endpoints));
michael@0 937 }
michael@0 938 }
michael@0 939
michael@0 940 /**
michael@0 941 * Given an array of endpoint data [start container, start offset, end
michael@0 942 * container, end offset], sets the selection to have those endpoints, with the
michael@0 943 * direction backwards. Uses extend, so it will throw in IE. Accepts an empty
michael@0 944 * array for endpoints, in which case the selection will just be emptied.
michael@0 945 */
michael@0 946 function setSelectionBackwards(endpoints) {
michael@0 947 selection.removeAllRanges();
michael@0 948 if (endpoints.length) {
michael@0 949 selection.collapse(endpoints[2], endpoints[3]);
michael@0 950 selection.extend(endpoints[0], endpoints[1]);
michael@0 951 }
michael@0 952 }

mercurial