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.DomUtilsImpl = function DomUtilsImpl() {
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          * The nodeFilter defines which nodes should be removed (NodeFilter.FILTER_REJECT),
583          * should be skipped including the subtree (NodeFilter.FILTER_SKIP) or should be kept
584          * and their subtree checked further (NodeFilter.FILTER_ACCEPT).
585          * @param {!Node} targetNode
586          * @param {!function(!Node) : !number} nodeFilter
587          * @return {?Node} parent of targetNode
588          */
589         function removeUnwantedNodes(targetNode, nodeFilter) {
590             var parent = targetNode.parentNode,
591                 node = targetNode.firstChild,
592                 filterResult = nodeFilter(targetNode),
593                 next;
594 
595             if (filterResult === NodeFilter.FILTER_SKIP) {
596                 return parent;
597             }
598 
599             while (node) {
600                 next = node.nextSibling;
601                 removeUnwantedNodes(node, nodeFilter);
602                 node = next;
603             }
604             if (parent && (filterResult === NodeFilter.FILTER_REJECT)) {
605                 mergeIntoParent(targetNode);
606             }
607             return parent;
608         }
609         this.removeUnwantedNodes = removeUnwantedNodes;
610 
611         /**
612          * Removes all child nodes from the given node.
613          * To be used instead of e.g. `node.innerHTML = "";`
614          * @param {!Node} node
615          * @return {undefined}
616          */
617         this.removeAllChildNodes = function (node) {
618             while (node.firstChild) {
619                 node.removeChild(node.firstChild);
620             }
621         };
622 
623         /**
624          * Get an array of nodes below the specified node with the specific namespace and tag name.
625          *
626          * Use this function instead of node.getElementsByTagNameNS when modifications are going to be made
627          * to the document content during iteration. For read-only uses, node.getElementsByTagNameNS will perform
628          * faster and use less memory. See https://github.com/kogmbh/WebODF/issues/736 for further discussion.
629          *
630          * @param {!Element|!Document} node
631          * @param {!string} namespace
632          * @param {!string} tagName
633          * @return {!Array.<!Element>}
634          */
635         function getElementsByTagNameNS(node, namespace, tagName) {
636             var e = [], list, i, l;
637             list = node.getElementsByTagNameNS(namespace, tagName);
638             e.length = l = list.length;
639             for (i = 0; i < l; i += 1) {
640                 e[i] = /**@type{!Element}*/(list.item(i));
641             }
642             return e;
643         }
644         this.getElementsByTagNameNS = getElementsByTagNameNS;
645 
646         /**
647          * Get an array of nodes below the specified node with the specific name tag name.
648          *
649          * Use this function instead of node.getElementsByTagName when modifications are going to be made
650          * to the document content during iteration. For read-only uses, node.getElementsByTagName will perform
651          * faster and use less memory. See https://github.com/kogmbh/WebODF/issues/736 for further discussion.
652          *
653          * @param {!Element|!Document} node
654          * @param {!string} tagName
655          * @return {!Array.<!Element>}
656          */
657         function getElementsByTagName(node, tagName) {
658             var e = [], list, i, l;
659             list = node.getElementsByTagName(tagName);
660             e.length = l = list.length;
661             for (i = 0; i < l; i += 1) {
662                 e[i] = /**@type{!Element}*/(list.item(i));
663             }
664             return e;
665         }
666         this.getElementsByTagName = getElementsByTagName;
667 
668         /**
669          * Whether a node contains another node
670          * Wrapper around Node.contains
671          * http://www.w3.org/TR/domcore/#dom-node-contains
672          * @param {!Node} parent The node that should contain the other node
673          * @param {?Node} descendant The node to test presence of
674          * @return {!boolean}
675          */
676         function containsNode(parent, descendant) {
677             return parent === descendant
678                 // the casts to Element are a workaround due to a different
679                 // contains() definition in the Closure Compiler externs file.
680                 || /**@type{!Element}*/(parent).contains(/**@type{!Element}*/(descendant));
681         }
682         this.containsNode = containsNode;
683 
684         /**
685          * Whether a node contains another node
686          * @param {!Node} parent The node that should contain the other node
687          * @param {?Node} descendant The node to test presence of
688          * @return {!boolean}
689          */
690         /*jslint bitwise:true*/
691         function containsNodeForBrokenWebKit(parent, descendant) {
692             // the contains function is not reliable on safari/webkit so use
693             // compareDocumentPosition instead
694             return parent === descendant ||
695                 Boolean(parent.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
696         }
697         /*jslint bitwise:false*/
698 
699         /**
700          * Return a number > 0 when point 1 precedes point 2. Return 0 if the points
701          * are equal. Return < 0 when point 2 precedes point 1.
702          * @param {!Node} c1 container of point 1
703          * @param {!number} o1  offset in unfiltered DOM world of point 1
704          * @param {!Node} c2 container of point 2
705          * @param {!number} o2  offset in unfiltered DOM world of point 2
706          * @return {!number}
707          */
708         function comparePoints(c1, o1, c2, o2) {
709             if (c1 === c2) {
710                 return o2 - o1;
711             }
712             var comparison = c1.compareDocumentPosition(c2);
713             if (comparison === 2) { // DOCUMENT_POSITION_PRECEDING
714                 comparison = -1;
715             } else if (comparison === 4) { // DOCUMENT_POSITION_FOLLOWING
716                 comparison = 1;
717             } else if (comparison === 10) { // DOCUMENT_POSITION_CONTAINS
718                 // c0 contains c2
719                 o1 = getPositionInContainingNode(c1, c2);
720                 comparison = (o1 < o2) ? 1 : -1;
721             } else { // DOCUMENT_POSITION_CONTAINED_BY
722                 o2 = getPositionInContainingNode(c2, c1);
723                 comparison = (o2 < o1) ? -1 : 1;
724             }
725             return comparison;
726         }
727         this.comparePoints = comparePoints;
728 
729         /**
730          * Scale the supplied number by the specified zoom transformation if the
731          * bowser does not transform range client rectangles correctly.
732          * In firefox, the span rectangle will be affected by the zoom, but the
733          * range is not. In most all other browsers, the range number is
734          * affected zoom.
735          *
736          * See http://dev.w3.org/csswg/cssom-view/#extensions-to-the-range-interface
737          * Section 10, getClientRects,
738          * "The transforms that apply to the ancestors are applied."
739          * @param {!number} inputNumber An input number to be scaled. This is
740          *                              expected to be the difference between
741          *                              a property on two range-sourced client
742          *                              rectangles (e.g., rect1.top - rect2.top)
743          * @param {!number} zoomLevel   Current canvas zoom level
744          * @return {!number}
745          */
746         function adaptRangeDifferenceToZoomLevel(inputNumber, zoomLevel) {
747             if (getBrowserQuirks().unscaledRangeClientRects) {
748                 return inputNumber;
749             }
750             return inputNumber / zoomLevel;
751         }
752         this.adaptRangeDifferenceToZoomLevel = adaptRangeDifferenceToZoomLevel;
753 
754         /**
755          * Translate a given child client rectangle to be relative to the parent's rectangle.
756          * Adapt to the provided zoom level as per adaptRangeDifferenceToZoomLevel.
757          *
758          * IMPORTANT: due to browser quirks, any element bounding client rect used with this function
759          * MUST be retrieved using DomUtils.getBoundingClientRect.
760          *
761          * @param {!ClientRect|!Object.<!string, !number>} child
762          * @param {!ClientRect|!Object.<!string, !number>} parent
763          * @param {!number} zoomLevel
764          * @return {!ClientRect|{top: !number, left: !number,  bottom: !number, right: !number, width: !number, height: !number}}
765          */
766         this.translateRect = function(child, parent, zoomLevel) {
767             return {
768                 top: adaptRangeDifferenceToZoomLevel(child.top - parent.top, zoomLevel),
769                 left: adaptRangeDifferenceToZoomLevel(child.left - parent.left, zoomLevel),
770                 bottom: adaptRangeDifferenceToZoomLevel(child.bottom - parent.top, zoomLevel),
771                 right: adaptRangeDifferenceToZoomLevel(child.right - parent.left, zoomLevel),
772                 width: adaptRangeDifferenceToZoomLevel(child.width, zoomLevel),
773                 height: adaptRangeDifferenceToZoomLevel(child.height, zoomLevel)
774             };
775         };
776 
777         /**
778          * Get the bounding client rect for the specified node.
779          * This function attempts to cope with various browser quirks, ideally
780          * returning a rectangle that can be used in conjunction with rectangles
781          * retrieved from ranges.
782          *
783          * Range & element client rectangles can only be mixed if both are
784          * transformed in the same way.
785          * See https://bugzilla.mozilla.org/show_bug.cgi?id=863618
786          * @param {!Node} node
787          * @return {?ClientRect}
788          */
789         function getBoundingClientRect(node) {
790             var doc = /**@type{!Document}*/(node.ownerDocument),
791                 quirks = getBrowserQuirks(),
792                 range,
793                 element,
794                 rect,
795                 body = doc.body;
796 
797             if (quirks.unscaledRangeClientRects === false
798                     || quirks.rangeBCRIgnoresElementBCR) {
799                 if (node.nodeType === Node.ELEMENT_NODE) {
800                     element = /**@type{!Element}*/(node);
801                     rect = element.getBoundingClientRect();
802                     if (quirks.elementBCRIgnoresBodyScroll) {
803                         return /**@type{?ClientRect}*/({
804                             left: rect.left + body.scrollLeft,
805                             right: rect.right + body.scrollLeft,
806                             top: rect.top + body.scrollTop,
807                             bottom: rect.bottom + body.scrollTop,
808                             width: rect.width,
809                             height: rect.height
810                         });
811                     }
812                     return rect;
813                 }
814             }
815             range = getSharedRange(doc);
816             range.selectNode(node);
817             return range.getBoundingClientRect();
818         }
819         this.getBoundingClientRect = getBoundingClientRect;
820 
821         /**
822          * Takes a flat object which is a key-value
823          * map of strings, and populates/modifies
824          * the node with child elements which have
825          * the key name as the node name (namespace
826          * prefix required in the key name)
827          * and the value as the text content. 
828          * Example: mapKeyValObjOntoNode(node, {"dc:creator": "Bob"}, nsResolver);
829          * If a namespace prefix is unresolved with the
830          * nsResolver, that key will be ignored and not written to the node.
831          * @param {!Element} node
832          * @param {!Object.<!string, !string>} properties
833          * @param {!function(!string):?string} nsResolver
834          */
835         function mapKeyValObjOntoNode(node, properties, nsResolver) {
836             Object.keys(properties).forEach(function (key) {
837                 var parts = key.split(":"),
838                     prefix = parts[0],
839                     localName = parts[1],
840                     ns = nsResolver(prefix),
841                     value = properties[key],
842                     element;
843 
844                 // Ignore if the prefix is unsupported,
845                 // otherwise set the textContent of the
846                 // element to the value.
847                 if (ns) {
848                     element = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(ns, localName)[0]);
849                     if (!element) {
850                         element = node.ownerDocument.createElementNS(ns, key);
851                         node.appendChild(element);
852                     }
853                     element.textContent = value;
854                 } else {
855                     runtime.log("Key ignored: " + key);
856                 }
857             });
858         }
859         this.mapKeyValObjOntoNode = mapKeyValObjOntoNode;
860 
861         /**
862          * Takes an array of strings, which is a listing of
863          * properties to be removed (namespace required),
864          * and deletes the corresponding top-level child elements
865          * that represent those properties, from the
866          * supplied node.
867          * Example: removeKeyElementsFromNode(node, ["dc:creator"], nsResolver);
868          * If a namespace is not resolved with the nsResolver,
869          * that key element will be not removed.
870          * If a key element does not exist, it will be ignored.
871          * @param {!Element} node
872          * @param {!Array.<!string>} propertyNames
873          * @param {!function(!string):?string} nsResolver
874          */
875         function removeKeyElementsFromNode(node, propertyNames, nsResolver) {
876             propertyNames.forEach(function (propertyName) {
877                 var parts = propertyName.split(":"),
878                     prefix = parts[0],
879                     localName = parts[1],
880                     ns = nsResolver(prefix),
881                     element;
882 
883                 // Ignore if the prefix is unsupported,
884                 // otherwise delete the element if found
885                 if (ns) {
886                     element = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(ns, localName)[0]);
887                     if (element) {
888                         element.parentNode.removeChild(element);
889                     } else {
890                          runtime.log("Element for " + propertyName + " not found.");
891                     }
892                 } else {
893                     runtime.log("Property Name ignored: " + propertyName);
894                 }
895             });
896         }
897         this.removeKeyElementsFromNode = removeKeyElementsFromNode;
898  
899         /**
900          * Looks at an element's direct children, and generates an object which is a
901          * flat key-value map from the child's ns:localName to it's text content.
902          * Only those children that have a resolvable prefixed name will be taken into
903          * account for generating this map.
904          * @param {!Element} node
905          * @param {function(!string):?string} prefixResolver 
906          * @return {!Object.<!string,!string>}
907          */
908         function getKeyValRepresentationOfNode(node, prefixResolver) {
909             var properties = {},
910                 currentSibling = node.firstElementChild,
911                 prefix;
912 
913             while (currentSibling) {
914                 prefix = prefixResolver(currentSibling.namespaceURI);
915                 if (prefix) {
916                     properties[prefix + ':' + currentSibling.localName] = currentSibling.textContent;
917                 }
918                 currentSibling = currentSibling.nextElementSibling;
919             }
920 
921             return properties;
922         }
923         this.getKeyValRepresentationOfNode = getKeyValRepresentationOfNode;
924 
925         /**
926          * Maps attributes and elements in the properties object over top of the node.
927          * Supports recursion and deep mapping.
928          *
929          * Supported value types are:
930          * - string (mapped to an attribute string on node)
931          * - number (mapped to an attribute string on node)
932          * - object (deep mapped to a new child node on node)
933          *
934          * @param {!Element} node
935          * @param {!Object.<string,*>} properties
936          * @param {!function(!string):?string} nsResolver
937          */
938         function mapObjOntoNode(node, properties, nsResolver) {
939             Object.keys(properties).forEach(function(key) {
940                 var parts = key.split(":"),
941                     prefix = parts[0],
942                     localName = parts[1],
943                     ns = nsResolver(prefix),
944                     value = properties[key],
945                     valueType = typeof value,
946                     element;
947 
948                 if (valueType === "object") {
949                     // Only create the destination sub-element if there are values to populate it with
950                     if (Object.keys(/**@type{!Object}*/(value)).length) {
951                         if (ns) {
952                             element = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(ns, localName)[0])
953                                 || node.ownerDocument.createElementNS(ns, key);
954                         } else {
955                             element = /**@type{!Element|undefined}*/(node.getElementsByTagName(localName)[0])
956                                 || node.ownerDocument.createElement(key);
957                         }
958                         node.appendChild(element);
959                         mapObjOntoNode(element, /**@type{!Object}*/(value), nsResolver);
960                     }
961                 } else if (ns) {
962                     runtime.assert(valueType === "number" || valueType === "string",
963                         "attempting to map unsupported type '" + valueType + "' (key: " + key + ")");
964                     node.setAttributeNS(ns, key, String(value));
965                     // If the prefix is unknown or unsupported, simply ignore it for now
966                 }
967             });
968         }
969         this.mapObjOntoNode = mapObjOntoNode;
970 
971         /**
972          * Clones an event object.
973          * IE10 destructs event objects once the event handler is done:
974          * "The event object is only available during an event; that is, you can use it in event handlers but not in other code"
975          * (from http://msdn.microsoft.com/en-us/library/ie/aa703876(v=vs.85).aspx)
976          * This method can be used to create a copy of the event object, to work around that.
977          * @param {!Event} event
978          * @return {!Event}
979          */
980         function cloneEvent(event) {
981             var e = Object.create(null);
982 
983             // copy over all direct properties
984             Object.keys(event.constructor.prototype).forEach(function (x) {
985                 e[x] = event[x];
986             });
987             // only now set the prototype (might set properties read-only)
988             e.prototype = event.constructor.prototype;
989 
990             return /**@type{!Event}*/(e);
991         }
992         this.cloneEvent = cloneEvent;
993 
994         this.getDirectChild = getDirectChild;
995 
996         /**
997          * @param {!core.DomUtilsImpl} self
998          */
999         function init(self) {
1000             var appVersion, webKitOrSafari, ie,
1001                 /**@type{?Window}*/
1002                 window = runtime.getWindow();
1003 
1004             if (window === null) {
1005                 return;
1006             }
1007 
1008             appVersion = window.navigator.appVersion.toLowerCase();
1009             webKitOrSafari = appVersion.indexOf('chrome') === -1
1010                 && (appVersion.indexOf('applewebkit') !== -1
1011                     || appVersion.indexOf('safari') !== -1);
1012             // See http://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect
1013             // Also, IE cleverly removed the MSIE tag without fixing the bug we're attempting to sniff here...
1014             // http://msdn.microsoft.com/en-us/library/ie/bg182625%28v=vs.110%29.aspx
1015             ie = appVersion.indexOf('msie') !== -1 || appVersion.indexOf('trident') !== -1;
1016             if (webKitOrSafari || ie) {
1017                 self.containsNode = containsNodeForBrokenWebKit;
1018             }
1019         }
1020         init(this);
1021     };
1022 
1023     /**
1024      * @type {!core.DomUtilsImpl}
1025      */
1026     core.DomUtils = new core.DomUtilsImpl();
1027 }());
1028