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