1 /** 2 * Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com> 3 * 4 * @licstart 5 * This file is part of WebODF. 6 * 7 * WebODF is free software: you can redistribute it and/or modify it 8 * under the terms of the GNU Affero General Public License (GNU AGPL) 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * WebODF is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with WebODF. If not, see <http://www.gnu.org/licenses/>. 19 * @licend 20 * 21 * @source: http://www.webodf.org/ 22 * @source: https://github.com/kogmbh/WebODF/ 23 */ 24 25 /*global Node, core, ops, runtime, NodeFilter, Range*/ 26 27 (function () { 28 "use strict"; 29 var /**@type{!{rangeBCRIgnoresElementBCR: boolean, unscaledRangeClientRects: boolean, elementBCRIgnoresBodyScroll: !boolean}}*/ 30 browserQuirks; 31 32 /** 33 * Detect various browser quirks 34 * unscaledRangeClientRects - Firefox doesn't apply parent css transforms to any range client rectangles 35 * rangeBCRIgnoresElementBCR - Internet explorer returns 0 client rects for an empty element that has fixed dimensions 36 * elementBCRIgnoresBodyScroll - iOS safari returns false client rects for an element that do not correlate with a scrolled body 37 * @return {!{unscaledRangeClientRects: !boolean, rangeBCRIgnoresElementBCR: !boolean, elementBCRIgnoresBodyScroll: !boolean}} 38 */ 39 function getBrowserQuirks() { 40 var range, 41 directBoundingRect, 42 rangeBoundingRect, 43 testContainer, 44 testElement, 45 detectedQuirks, 46 window, 47 document, 48 docElement, 49 body, 50 docOverflow, 51 bodyOverflow, 52 bodyHeight, 53 bodyScroll; 54 55 if (browserQuirks === undefined) { 56 window = runtime.getWindow(); 57 document = window && window.document; 58 docElement = document.documentElement; 59 body = document.body; 60 browserQuirks = { 61 rangeBCRIgnoresElementBCR: false, 62 unscaledRangeClientRects: false, 63 elementBCRIgnoresBodyScroll: false 64 }; 65 if (document) { 66 testContainer = document.createElement("div"); 67 testContainer.style.position = "absolute"; 68 testContainer.style.left = "-99999px"; 69 testContainer.style.transform = "scale(2)"; 70 testContainer.style["-webkit-transform"] = "scale(2)"; 71 72 testElement = document.createElement("div"); 73 testContainer.appendChild(testElement); 74 body.appendChild(testContainer); 75 range = document.createRange(); 76 range.selectNode(testElement); 77 // Internet explorer (v10 and others?) will omit the element's own client rect from 78 // the returned client rects list for the range 79 browserQuirks.rangeBCRIgnoresElementBCR = range.getClientRects().length === 0; 80 81 testElement.appendChild(document.createTextNode("Rect transform test")); 82 directBoundingRect = testElement.getBoundingClientRect(); 83 rangeBoundingRect = range.getBoundingClientRect(); 84 // Firefox doesn't apply parent css transforms to any range client rectangles 85 // See https://bugzilla.mozilla.org/show_bug.cgi?id=863618 86 // Depending on the browser, client rects can sometimes have sub-pixel rounding effects, so 87 // add some wiggle room for this. The scale is 200%, so there is no issues with false positives here 88 browserQuirks.unscaledRangeClientRects = Math.abs(directBoundingRect.height - rangeBoundingRect.height) > 2; 89 90 testContainer.style.transform = ""; 91 testContainer.style["-webkit-transform"] = ""; 92 // Backup current values for documentElement and body's overflows, body height, and body scroll. 93 docOverflow = docElement.style.overflow; 94 bodyOverflow = body.style.overflow; 95 bodyHeight = body.style.height; 96 bodyScroll = body.scrollTop; 97 // Set new values for the backed up properties 98 docElement.style.overflow = "visible"; 99 body.style.overflow = "visible"; 100 body.style.height = "200%"; 101 body.scrollTop = body.scrollHeight; 102 // After extending the body's height to twice and scrolling by that amount, 103 // if the element's new BCR is not the same as the range's BCR, then 104 // Houston we have a Quirk! This problem has been seen on iOS7, which 105 // seems to report the correct BCR for a range but ignores body scroll 106 // effects on an element... 107 browserQuirks.elementBCRIgnoresBodyScroll = (range.getBoundingClientRect().top !== testElement.getBoundingClientRect().top); 108 // Restore backed up property values 109 body.scrollTop = bodyScroll; 110 body.style.height = bodyHeight; 111 body.style.overflow = bodyOverflow; 112 docElement.style.overflow = docOverflow; 113 114 range.detach(); 115 body.removeChild(testContainer); 116 detectedQuirks = Object.keys(browserQuirks).map( 117 /** 118 * @param {!string} quirk 119 * @return {!string} 120 */ 121 function (quirk) { 122 return quirk + ":" + String(browserQuirks[quirk]); 123 } 124 ).join(", "); 125 runtime.log("Detected browser quirks - " + detectedQuirks); 126 } 127 } 128 return browserQuirks; 129 } 130 131 /** 132 * Return the first child element with the given namespace and name. 133 * If the parent is null, or if there is no child with the given name and 134 * namespace, null is returned. 135 * @param {?Element} parent 136 * @param {!string} ns 137 * @param {!string} name 138 * @return {?Element} 139 */ 140 function getDirectChild(parent, ns, name) { 141 var node = parent ? parent.firstElementChild : null; 142 while (node) { 143 if (node.localName === name && node.namespaceURI === ns) { 144 return /**@type{!Element}*/(node); 145 } 146 node = node.nextElementSibling; 147 } 148 return null; 149 } 150 151 /** 152 * A collection of Dom utilities 153 * @constructor 154 */ 155 core.DomUtils = function DomUtils() { 156 var /**@type{?Range}*/ 157 sharedRange = null; 158 159 /** 160 * @param {!Document} doc 161 * @return {!Range} 162 */ 163 function getSharedRange(doc) { 164 var range; 165 if (sharedRange) { 166 range = sharedRange; 167 } else { 168 sharedRange = range = /**@type{!Range}*/(doc.createRange()); 169 } 170 return range; 171 } 172 173 /** 174 * Find the inner-most child point that is equivalent 175 * to the provided container and offset. 176 * @param {!Node} container 177 * @param {!number} offset 178 * @return {{container: Node, offset: !number}} 179 */ 180 function findStablePoint(container, offset) { 181 var c = container; 182 if (offset < c.childNodes.length) { 183 c = c.childNodes.item(offset); 184 offset = 0; 185 while (c.firstChild) { 186 c = c.firstChild; 187 } 188 } else { 189 while (c.lastChild) { 190 c = c.lastChild; 191 offset = c.nodeType === Node.TEXT_NODE 192 ? c.textContent.length 193 : c.childNodes.length; 194 } 195 } 196 return {container: c, offset: offset}; 197 } 198 199 /** 200 * Gets the unfiltered DOM 'offset' of a node within a container that may not be it's direct parent. 201 * @param {!Node} node 202 * @param {!Node} container 203 * @return {!number} 204 */ 205 function getPositionInContainingNode(node, container) { 206 var offset = 0, 207 n; 208 while (node.parentNode !== container) { 209 runtime.assert(node.parentNode !== null, "parent is null"); 210 node = /**@type{!Node}*/(node.parentNode); 211 } 212 n = container.firstChild; 213 while (n !== node) { 214 offset += 1; 215 n = n.nextSibling; 216 } 217 return offset; 218 } 219 220 /** 221 * If either the start or end boundaries of a range start within a text 222 * node, this function will split these text nodes and reset the range 223 * boundaries to select the new nodes. The end result is that there are 224 * no partially contained text nodes within the resulting range. 225 * E.g., the text node with selection: 226 * "A|BCD|E" 227 * would be split into 3 text nodes, with the range modified to maintain 228 * only the completely selected text node: 229 * "A" "|BCD|" "E" 230 * @param {!Range} range 231 * @return {!Array.<!Node>} Return a list of nodes modified as a result 232 * of this split operation. These are often 233 * processed through 234 * DomUtils.normalizeTextNodes after all 235 * processing has been complete. 236 */ 237 function splitBoundaries(range) { 238 var modifiedNodes = [], 239 originalEndContainer, 240 resetToContainerLength, 241 end, 242 splitStart, 243 node, 244 text, 245 offset; 246 247 if (range.startContainer.nodeType === Node.TEXT_NODE 248 || range.endContainer.nodeType === Node.TEXT_NODE) { 249 originalEndContainer = range.endContainer; 250 resetToContainerLength = range.endContainer.nodeType !== Node.TEXT_NODE ? 251 range.endOffset === range.endContainer.childNodes.length : false; 252 253 end = findStablePoint(range.endContainer, range.endOffset); 254 if (end.container === originalEndContainer) { 255 originalEndContainer = null; 256 } 257 // Stable points need to be found to ensure splitting the text 258 // node doesn't inadvertently modify the other end of the range 259 range.setEnd(end.container, end.offset); 260 261 // Must split end first to stop the start point from being lost 262 node = range.endContainer; 263 if (range.endOffset !== 0 && node.nodeType === Node.TEXT_NODE) { 264 text = /**@type{!Text}*/(node); 265 if (range.endOffset !== text.length) { 266 modifiedNodes.push(text.splitText(range.endOffset)); 267 modifiedNodes.push(text); 268 // The end doesn't need to be reset as endContainer & 269 // endOffset are still valid after the modification 270 } 271 } 272 273 node = range.startContainer; 274 if (range.startOffset !== 0 && node.nodeType === Node.TEXT_NODE) { 275 text = /**@type{!Text}*/(node); 276 if (range.startOffset !== text.length) { 277 splitStart = text.splitText(range.startOffset); 278 modifiedNodes.push(text); 279 modifiedNodes.push(splitStart); 280 range.setStart(splitStart, 0); 281 } 282 } 283 284 if (originalEndContainer !== null) { 285 node = range.endContainer; 286 while (node.parentNode && node.parentNode !== originalEndContainer) { 287 node = node.parentNode; 288 } 289 if (resetToContainerLength) { 290 offset = originalEndContainer.childNodes.length; 291 } else { 292 offset = getPositionInContainingNode(node, originalEndContainer); 293 } 294 range.setEnd(originalEndContainer, offset); 295 } 296 } 297 return modifiedNodes; 298 } 299 this.splitBoundaries = splitBoundaries; 300 301 /** 302 * Returns true if the container range completely contains the insideRange. 303 * Aligned boundaries are counted as inclusion 304 * @param {!Range} container 305 * @param {!Range} insideRange 306 * @return {boolean} 307 */ 308 function containsRange(container, insideRange) { 309 return container.compareBoundaryPoints(Range.START_TO_START, insideRange) <= 0 310 && container.compareBoundaryPoints(Range.END_TO_END, insideRange) >= 0; 311 } 312 this.containsRange = containsRange; 313 314 /** 315 * Returns true if there is any intersection between range1 and range2 316 * @param {!Range} range1 317 * @param {!Range} range2 318 * @return {boolean} 319 */ 320 function rangesIntersect(range1, range2) { 321 return range1.compareBoundaryPoints(Range.END_TO_START, range2) <= 0 322 && range1.compareBoundaryPoints(Range.START_TO_END, range2) >= 0; 323 } 324 this.rangesIntersect = rangesIntersect; 325 326 /** 327 * Returns the intersection of two ranges. If there is no intersection, this 328 * will return undefined. 329 * 330 * @param {!Range} range1 331 * @param {!Range} range2 332 * @return {!Range|undefined} 333 */ 334 function rangeIntersection(range1, range2) { 335 var newRange; 336 337 if (rangesIntersect(range1, range2)) { 338 newRange = /**@type{!Range}*/(range1.cloneRange()); 339 if (range1.compareBoundaryPoints(Range.START_TO_START, range2) === -1) { 340 // If range1's start is before range2's start, use range2's start 341 newRange.setStart(range2.startContainer, range2.startOffset); 342 } 343 344 if (range1.compareBoundaryPoints(Range.END_TO_END, range2) === 1) { 345 // if range1's end is after range2's end, use range2's end 346 newRange.setEnd(range2.endContainer, range2.endOffset); 347 } 348 } 349 return newRange; 350 } 351 this.rangeIntersection = rangeIntersection; 352 353 /** 354 * Returns the maximum available offset for the node. If this is a text 355 * node, this will be node.length, or for an element node, childNodes.length 356 * @param {!Node} node 357 * @return {!number} 358 */ 359 function maximumOffset(node) { 360 return node.nodeType === Node.TEXT_NODE ? /**@type{!Text}*/(node).length : node.childNodes.length; 361 } 362 363 /** 364 * Checks all nodes between the tree walker's current node and the defined 365 * root. If any nodes are rejected, the tree walker is moved to the 366 * highest rejected node below the root. Note, the root is excluded from 367 * this check. 368 * 369 * This logic is similar to PositionIterator.moveToAcceptedNode 370 * @param {!TreeWalker} walker 371 * @param {!Node} root 372 * @param {!function(!Node) : number} nodeFilter 373 * 374 * @return {!Node} Returns the current node the walker is on 375 */ 376 function moveToNonRejectedNode(walker, root, nodeFilter) { 377 var node = walker.currentNode; 378 379 // Ensure currentNode is not within a rejected subtree by crawling each parent node 380 // up to the root and verifying it is either accepted or skipped by the nodeFilter. 381 // NOTE: The root is deliberately not checked as it is the container iteration happens within. 382 if (node !== root) { 383 node = node.parentNode; 384 while (node && node !== root) { 385 if (nodeFilter(node) === NodeFilter.FILTER_REJECT) { 386 walker.currentNode = node; 387 } 388 node = node.parentNode; 389 } 390 } 391 return walker.currentNode; 392 } 393 394 /** 395 * Fetches all nodes within a supplied range that pass the required filter 396 * @param {!Range} range 397 * @param {!function(!Node) : number} nodeFilter 398 * @param {!number} whatToShow 399 * @return {!Array.<!Node>} 400 */ 401 /*jslint bitwise:true*/ 402 function getNodesInRange(range, nodeFilter, whatToShow) { 403 var document = range.startContainer.ownerDocument, 404 elements = [], 405 rangeRoot = range.commonAncestorContainer, 406 root = /**@type{!Node}*/(rangeRoot.nodeType === Node.TEXT_NODE ? rangeRoot.parentNode : rangeRoot), 407 treeWalker = document.createTreeWalker(root, whatToShow, nodeFilter, false), 408 currentNode, 409 lastNodeInRange, 410 endNodeCompareFlags, 411 comparePositionResult; 412 413 if (range.endContainer.childNodes[range.endOffset - 1]) { 414 // This is the last node completely contained in the range 415 lastNodeInRange = /**@type{!Node}*/(range.endContainer.childNodes[range.endOffset - 1]); 416 // Accept anything preceding or contained by this node. 417 endNodeCompareFlags = Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINED_BY; 418 } else { 419 // Either no child nodes (e.g., TEXT_NODE) or endOffset = 0 420 lastNodeInRange = /**@type{!Node}*/(range.endContainer); 421 // Don't accept things contained within this node, as the range ends before this node's children. 422 // This is the last node touching the range though, so the node is still accepted into the results. 423 endNodeCompareFlags = Node.DOCUMENT_POSITION_PRECEDING; 424 } 425 426 if (range.startContainer.childNodes[range.startOffset]) { 427 // The range starts within startContainer, so this child node is the first node in the range 428 currentNode = /**@type{!Node}*/(range.startContainer.childNodes[range.startOffset]); 429 treeWalker.currentNode = currentNode; 430 } else if (range.startOffset === maximumOffset(range.startContainer)) { 431 // This condition will be true if the range starts beyond the last position of a node 432 // E.g., (text, text.length) or (div, div.childNodes.length) 433 currentNode = /**@type{!Node}*/(range.startContainer); 434 treeWalker.currentNode = currentNode; 435 // In this case, move to the last child (if the node has children) 436 treeWalker.lastChild(); // May return null if the current node has no children 437 // And navigate onto the next node in sequence 438 currentNode = treeWalker.nextNode(); 439 } else { 440 // This will only be hit for a text node that is partially overlapped by the range start 441 currentNode = /**@type{!Node}*/(range.startContainer); 442 treeWalker.currentNode = currentNode; 443 } 444 445 if (currentNode) { 446 // If the treeWalker hit the end of the sequence in the treeWalker.nextNode line just above, 447 // currentNode will be null. 448 currentNode = moveToNonRejectedNode(treeWalker, root, nodeFilter); 449 switch (nodeFilter(/**@type{!Node}*/(currentNode))) { 450 case NodeFilter.FILTER_REJECT: 451 // If started on a rejected node, calling nextNode will incorrectly 452 // dive down into the rejected node's children. Instead, advance to 453 // the next sibling or parent node's sibling and resume walking from 454 // there. 455 currentNode = treeWalker.nextSibling(); 456 while (!currentNode && treeWalker.parentNode()) { 457 currentNode = treeWalker.nextSibling(); 458 } 459 break; 460 case NodeFilter.FILTER_SKIP: 461 // Immediately advance to the next node without giving an opportunity for the current one to 462 // be stored. 463 currentNode = treeWalker.nextNode(); 464 break; 465 default: 466 // case NodeFilter.FILTER_ACCEPT: 467 // Proceed into the following loop. The current node will be stored at the end of the loop 468 // if it is contained within the requested range. 469 break; 470 } 471 472 while (currentNode) { 473 comparePositionResult = lastNodeInRange.compareDocumentPosition(currentNode); 474 if (comparePositionResult !== 0 && (comparePositionResult & endNodeCompareFlags) === 0) { 475 // comparePositionResult === 0 if currentNode === lastNodeInRange. This is considered within the range 476 // comparePositionResult & endNodeCompareFlags would be non-zero if n precedes lastNodeInRange 477 // If either of these statements are false, currentNode is past the end of the range 478 break; 479 } 480 elements.push(currentNode); 481 currentNode = treeWalker.nextNode(); 482 } 483 } 484 485 return elements; 486 } 487 /*jslint bitwise:false*/ 488 this.getNodesInRange = getNodesInRange; 489 490 /** 491 * Merges the content of node with nextNode. 492 * If node is an empty text node, it will be removed in any case. 493 * If nextNode is an empty text node, it will be only removed if node is a text node. 494 * @param {!Node} node 495 * @param {!Node} nextNode 496 * @return {?Node} merged text node or null if there is no text node as result 497 */ 498 function mergeTextNodes(node, nextNode) { 499 var mergedNode = null, text, nextText; 500 501 if (node.nodeType === Node.TEXT_NODE) { 502 text = /**@type{!Text}*/(node); 503 if (text.length === 0) { 504 text.parentNode.removeChild(text); 505 if (nextNode.nodeType === Node.TEXT_NODE) { 506 mergedNode = nextNode; 507 } 508 } else { 509 if (nextNode.nodeType === Node.TEXT_NODE) { 510 // in chrome it is important to add nextNode to node. 511 // doing it the other way around causes random 512 // whitespace to appear 513 nextText = /**@type{!Text}*/(nextNode); 514 text.appendData(nextText.data); 515 nextNode.parentNode.removeChild(nextNode); 516 } 517 mergedNode = node; 518 } 519 } 520 521 return mergedNode; 522 } 523 524 /** 525 * Attempts to normalize the node with any surrounding text nodes. No 526 * actions are performed if the node is undefined, has no siblings, or 527 * is not a text node 528 * @param {Node} node 529 * @return {undefined} 530 */ 531 function normalizeTextNodes(node) { 532 if (node && node.nextSibling) { 533 node = mergeTextNodes(node, node.nextSibling); 534 } 535 if (node && node.previousSibling) { 536 mergeTextNodes(node.previousSibling, node); 537 } 538 } 539 this.normalizeTextNodes = normalizeTextNodes; 540 541 /** 542 * Checks if the provided limits fully encompass the passed in node 543 * @param {!Range|{startContainer: Node, startOffset: !number, endContainer: Node, endOffset: !number}} limits 544 * @param {!Node} node 545 * @return {boolean} Returns true if the node is fully contained within 546 * the range 547 */ 548 function rangeContainsNode(limits, node) { 549 var range = node.ownerDocument.createRange(), 550 nodeRange = node.ownerDocument.createRange(), 551 result; 552 553 range.setStart(limits.startContainer, limits.startOffset); 554 range.setEnd(limits.endContainer, limits.endOffset); 555 nodeRange.selectNodeContents(node); 556 557 result = containsRange(range, nodeRange); 558 559 range.detach(); 560 nodeRange.detach(); 561 return result; 562 } 563 this.rangeContainsNode = rangeContainsNode; 564 565 /** 566 * Merge all child nodes into the targetNode's parent and remove the targetNode entirely 567 * @param {!Node} targetNode 568 * @return {!Node} parent of targetNode 569 */ 570 function mergeIntoParent(targetNode) { 571 var parent = targetNode.parentNode; 572 while (targetNode.firstChild) { 573 parent.insertBefore(targetNode.firstChild, targetNode); 574 } 575 parent.removeChild(targetNode); 576 return parent; 577 } 578 this.mergeIntoParent = mergeIntoParent; 579 580 /** 581 * Removes all unwanted nodes from targetNode includes itself. 582 * @param {!Node} targetNode 583 * @param {function(!Node):!boolean} shouldRemove check whether a node should be removed or not 584 * @return {?Node} parent of targetNode 585 */ 586 function removeUnwantedNodes(targetNode, shouldRemove) { 587 var parent = targetNode.parentNode, 588 node = targetNode.firstChild, 589 next; 590 while (node) { 591 next = node.nextSibling; 592 removeUnwantedNodes(node, shouldRemove); 593 node = next; 594 } 595 if (parent && shouldRemove(targetNode)) { 596 mergeIntoParent(targetNode); 597 } 598 return parent; 599 } 600 this.removeUnwantedNodes = removeUnwantedNodes; 601 602 /** 603 * Get an array of nodes below the specified node with the specific namespace and tag name 604 * @param {!Element|!Document} node 605 * @param {!string} namespace 606 * @param {!string} tagName 607 * @return {!Array.<!Element>} 608 */ 609 function getElementsByTagNameNS(node, namespace, tagName) { 610 var e = [], list, i, l; 611 list = node.getElementsByTagNameNS(namespace, tagName); 612 e.length = l = list.length; 613 for (i = 0; i < l; i += 1) { 614 e[i] = /**@type{!Element}*/(list.item(i)); 615 } 616 return e; 617 } 618 this.getElementsByTagNameNS = getElementsByTagNameNS; 619 620 /** 621 * Get an array of nodes below the specified node with the specific name tag name. 622 * @param {!Element|!Document} node 623 * @param {!string} tagName 624 * @return {!Array.<!Element>} 625 */ 626 function getElementsByTagName(node, tagName) { 627 var e = [], list, i, l; 628 list = node.getElementsByTagName(tagName); 629 e.length = l = list.length; 630 for (i = 0; i < l; i += 1) { 631 e[i] = /**@type{!Element}*/(list.item(i)); 632 } 633 return e; 634 } 635 this.getElementsByTagName = getElementsByTagName; 636 637 /** 638 * Whether a node contains another node 639 * Wrapper around Node.contains 640 * http://www.w3.org/TR/domcore/#dom-node-contains 641 * @param {!Node} parent The node that should contain the other node 642 * @param {?Node} descendant The node to test presence of 643 * @return {!boolean} 644 */ 645 function containsNode(parent, descendant) { 646 return parent === descendant 647 // the casts to Element are a workaround due to a different 648 // contains() definition in the Closure Compiler externs file. 649 || /**@type{!Element}*/(parent).contains(/**@type{!Element}*/(descendant)); 650 } 651 this.containsNode = containsNode; 652 653 /** 654 * Whether a node contains another node 655 * @param {!Node} parent The node that should contain the other node 656 * @param {?Node} descendant The node to test presence of 657 * @return {!boolean} 658 */ 659 /*jslint bitwise:true*/ 660 function containsNodeForBrokenWebKit(parent, descendant) { 661 // the contains function is not reliable on safari/webkit so use 662 // compareDocumentPosition instead 663 return parent === descendant || 664 Boolean(parent.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); 665 } 666 /*jslint bitwise:false*/ 667 668 /** 669 * Return a number > 0 when point 1 precedes point 2. Return 0 if the points 670 * are equal. Return < 0 when point 2 precedes point 1. 671 * @param {!Node} c1 container of point 1 672 * @param {!number} o1 offset in unfiltered DOM world of point 1 673 * @param {!Node} c2 container of point 2 674 * @param {!number} o2 offset in unfiltered DOM world of point 2 675 * @return {!number} 676 */ 677 function comparePoints(c1, o1, c2, o2) { 678 if (c1 === c2) { 679 return o2 - o1; 680 } 681 var comparison = c1.compareDocumentPosition(c2); 682 if (comparison === 2) { // DOCUMENT_POSITION_PRECEDING 683 comparison = -1; 684 } else if (comparison === 4) { // DOCUMENT_POSITION_FOLLOWING 685 comparison = 1; 686 } else if (comparison === 10) { // DOCUMENT_POSITION_CONTAINS 687 // c0 contains c2 688 o1 = getPositionInContainingNode(c1, c2); 689 comparison = (o1 < o2) ? 1 : -1; 690 } else { // DOCUMENT_POSITION_CONTAINED_BY 691 o2 = getPositionInContainingNode(c2, c1); 692 comparison = (o2 < o1) ? -1 : 1; 693 } 694 return comparison; 695 } 696 this.comparePoints = comparePoints; 697 698 /** 699 * Scale the supplied number by the specified zoom transformation if the 700 * bowser does not transform range client rectangles correctly. 701 * In firefox, the span rectangle will be affected by the zoom, but the 702 * range is not. In most all other browsers, the range number is 703 * affected zoom. 704 * 705 * See http://dev.w3.org/csswg/cssom-view/#extensions-to-the-range-interface 706 * Section 10, getClientRects, 707 * "The transforms that apply to the ancestors are applied." 708 * @param {!number} inputNumber An input number to be scaled. This is 709 * expected to be the difference between 710 * a property on two range-sourced client 711 * rectangles (e.g., rect1.top - rect2.top) 712 * @param {!number} zoomLevel Current canvas zoom level 713 * @return {!number} 714 */ 715 function adaptRangeDifferenceToZoomLevel(inputNumber, zoomLevel) { 716 if (getBrowserQuirks().unscaledRangeClientRects) { 717 return inputNumber; 718 } 719 return inputNumber / zoomLevel; 720 } 721 this.adaptRangeDifferenceToZoomLevel = adaptRangeDifferenceToZoomLevel; 722 723 /** 724 * Translate a given child client rectangle to be relative to the parent's rectangle. 725 * Adapt to the provided zoom level as per adaptRangeDifferenceToZoomLevel. 726 * 727 * IMPORTANT: due to browser quirks, any element bounding client rect used with this function 728 * MUST be retrieved using DomUtils.getBoundingClientRect. 729 * 730 * @param {!ClientRect|!Object.<!string, !number>} child 731 * @param {!ClientRect|!Object.<!string, !number>} parent 732 * @param {!number} zoomLevel 733 * @return {!ClientRect|{top: !number, left: !number, bottom: !number, right: !number, width: !number, height: !number}} 734 */ 735 this.translateRect = function(child, parent, zoomLevel) { 736 return { 737 top: adaptRangeDifferenceToZoomLevel(child.top - parent.top, zoomLevel), 738 left: adaptRangeDifferenceToZoomLevel(child.left - parent.left, zoomLevel), 739 bottom: adaptRangeDifferenceToZoomLevel(child.bottom - parent.top, zoomLevel), 740 right: adaptRangeDifferenceToZoomLevel(child.right - parent.left, zoomLevel), 741 width: adaptRangeDifferenceToZoomLevel(child.width, zoomLevel), 742 height: adaptRangeDifferenceToZoomLevel(child.height, zoomLevel) 743 }; 744 }; 745 746 /** 747 * Get the bounding client rect for the specified node. 748 * This function attempts to cope with various browser quirks, ideally 749 * returning a rectangle that can be used in conjunction with rectangles 750 * retrieved from ranges. 751 * 752 * Range & element client rectangles can only be mixed if both are 753 * transformed in the same way. 754 * See https://bugzilla.mozilla.org/show_bug.cgi?id=863618 755 * @param {!Node} node 756 * @return {?ClientRect} 757 */ 758 function getBoundingClientRect(node) { 759 var doc = /**@type{!Document}*/(node.ownerDocument), 760 quirks = getBrowserQuirks(), 761 range, 762 element, 763 rect, 764 body = doc.body; 765 766 if (quirks.unscaledRangeClientRects === false 767 || quirks.rangeBCRIgnoresElementBCR) { 768 if (node.nodeType === Node.ELEMENT_NODE) { 769 element = /**@type{!Element}*/(node); 770 rect = element.getBoundingClientRect(); 771 if (quirks.elementBCRIgnoresBodyScroll) { 772 return /**@type{?ClientRect}*/({ 773 left: rect.left + body.scrollLeft, 774 right: rect.right + body.scrollLeft, 775 top: rect.top + body.scrollTop, 776 bottom: rect.bottom + body.scrollTop, 777 width: rect.width, 778 height: rect.height 779 }); 780 } 781 return rect; 782 } 783 } 784 range = getSharedRange(doc); 785 range.selectNode(node); 786 return range.getBoundingClientRect(); 787 } 788 this.getBoundingClientRect = getBoundingClientRect; 789 790 /** 791 * Takes a flat object which is a key-value 792 * map of strings, and populates/modifies 793 * the node with child elements which have 794 * the key name as the node name (namespace 795 * prefix required in the key name) 796 * and the value as the text content. 797 * Example: mapKeyValObjOntoNode(node, {"dc:creator": "Bob"}, nsResolver); 798 * If a namespace prefix is unresolved with the 799 * nsResolver, that key will be ignored and not written to the node. 800 * @param {!Element} node 801 * @param {!Object.<!string, !string>} properties 802 * @param {!function(!string):?string} nsResolver 803 */ 804 function mapKeyValObjOntoNode(node, properties, nsResolver) { 805 Object.keys(properties).forEach(function (key) { 806 var parts = key.split(":"), 807 prefix = parts[0], 808 localName = parts[1], 809 ns = nsResolver(prefix), 810 value = properties[key], 811 element; 812 813 // Ignore if the prefix is unsupported, 814 // otherwise set the textContent of the 815 // element to the value. 816 if (ns) { 817 element = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(ns, localName)[0]); 818 if (!element) { 819 element = node.ownerDocument.createElementNS(ns, key); 820 node.appendChild(element); 821 } 822 element.textContent = value; 823 } else { 824 runtime.log("Key ignored: " + key); 825 } 826 }); 827 } 828 this.mapKeyValObjOntoNode = mapKeyValObjOntoNode; 829 830 /** 831 * Takes an array of strings, which is a listing of 832 * properties to be removed (namespace required), 833 * and deletes the corresponding top-level child elements 834 * that represent those properties, from the 835 * supplied node. 836 * Example: removeKeyElementsFromNode(node, ["dc:creator"], nsResolver); 837 * If a namespace is not resolved with the nsResolver, 838 * that key element will be not removed. 839 * If a key element does not exist, it will be ignored. 840 * @param {!Element} node 841 * @param {!Array.<!string>} propertyNames 842 * @param {!function(!string):?string} nsResolver 843 */ 844 function removeKeyElementsFromNode(node, propertyNames, nsResolver) { 845 propertyNames.forEach(function (propertyName) { 846 var parts = propertyName.split(":"), 847 prefix = parts[0], 848 localName = parts[1], 849 ns = nsResolver(prefix), 850 element; 851 852 // Ignore if the prefix is unsupported, 853 // otherwise delete the element if found 854 if (ns) { 855 element = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(ns, localName)[0]); 856 if (element) { 857 element.parentNode.removeChild(element); 858 } else { 859 runtime.log("Element for " + propertyName + " not found."); 860 } 861 } else { 862 runtime.log("Property Name ignored: " + propertyName); 863 } 864 }); 865 } 866 this.removeKeyElementsFromNode = removeKeyElementsFromNode; 867 868 /** 869 * Looks at an element's direct children, and generates an object which is a 870 * flat key-value map from the child's ns:localName to it's text content. 871 * Only those children that have a resolvable prefixed name will be taken into 872 * account for generating this map. 873 * @param {!Element} node 874 * @param {function(!string):?string} prefixResolver 875 * @return {!Object.<!string,!string>} 876 */ 877 function getKeyValRepresentationOfNode(node, prefixResolver) { 878 var properties = {}, 879 currentSibling = node.firstElementChild, 880 prefix; 881 882 while (currentSibling) { 883 prefix = prefixResolver(currentSibling.namespaceURI); 884 if (prefix) { 885 properties[prefix + ':' + currentSibling.localName] = currentSibling.textContent; 886 } 887 currentSibling = currentSibling.nextElementSibling; 888 } 889 890 return properties; 891 } 892 this.getKeyValRepresentationOfNode = getKeyValRepresentationOfNode; 893 894 /** 895 * Maps attributes and elements in the properties object over top of the node. 896 * Supports recursion and deep mapping. 897 * 898 * Supported value types are: 899 * - string (mapped to an attribute string on node) 900 * - number (mapped to an attribute string on node) 901 * - object (deep mapped to a new child node on node) 902 * 903 * @param {!Element} node 904 * @param {!Object.<string,*>} properties 905 * @param {!function(!string):?string} nsResolver 906 */ 907 function mapObjOntoNode(node, properties, nsResolver) { 908 Object.keys(properties).forEach(function(key) { 909 var parts = key.split(":"), 910 prefix = parts[0], 911 localName = parts[1], 912 ns = nsResolver(prefix), 913 value = properties[key], 914 valueType = typeof value, 915 element; 916 917 if (valueType === "object") { 918 // Only create the destination sub-element if there are values to populate it with 919 if (Object.keys(/**@type{!Object}*/(value)).length) { 920 if (ns) { 921 element = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(ns, localName)[0]) 922 || node.ownerDocument.createElementNS(ns, key); 923 } else { 924 element = /**@type{!Element|undefined}*/(node.getElementsByTagName(localName)[0]) 925 || node.ownerDocument.createElement(key); 926 } 927 node.appendChild(element); 928 mapObjOntoNode(element, /**@type{!Object}*/(value), nsResolver); 929 } 930 } else if (ns) { 931 runtime.assert(valueType === "number" || valueType === "string", 932 "attempting to map unsupported type '" + valueType + "' (key: " + key + ")"); 933 node.setAttributeNS(ns, key, String(value)); 934 // If the prefix is unknown or unsupported, simply ignore it for now 935 } 936 }); 937 } 938 this.mapObjOntoNode = mapObjOntoNode; 939 940 /** 941 * Clones an event object. 942 * IE10 destructs event objects once the event handler is done: 943 * "The event object is only available during an event; that is, you can use it in event handlers but not in other code" 944 * (from http://msdn.microsoft.com/en-us/library/ie/aa703876(v=vs.85).aspx) 945 * This method can be used to create a copy of the event object, to work around that. 946 * @param {!Event} event 947 * @return {!Event} 948 */ 949 function cloneEvent(event) { 950 var e = Object.create(null); 951 952 // copy over all direct properties 953 Object.keys(/**@type{!Object}*/(event)).forEach(function (x) { 954 e[x] = event[x]; 955 }); 956 // only now set the prototype (might set properties read-only) 957 e.prototype = event.constructor.prototype; 958 959 return /**@type{!Event}*/(e); 960 } 961 this.cloneEvent = cloneEvent; 962 963 this.getDirectChild = getDirectChild; 964 965 /** 966 * @param {!core.DomUtils} self 967 */ 968 function init(self) { 969 var appVersion, webKitOrSafari, ie, 970 /**@type{?Window}*/ 971 window = runtime.getWindow(); 972 973 if (window === null) { 974 return; 975 } 976 977 appVersion = window.navigator.appVersion.toLowerCase(); 978 webKitOrSafari = appVersion.indexOf('chrome') === -1 979 && (appVersion.indexOf('applewebkit') !== -1 980 || appVersion.indexOf('safari') !== -1); 981 ie = appVersion.indexOf('msie'); // See http://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect 982 if (webKitOrSafari || ie) { 983 self.containsNode = containsNodeForBrokenWebKit; 984 } 985 } 986 init(this); 987 }; 988 }()); 989