node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
1,028 lines (955 loc) • 47 kB
JavaScript
/**
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* This file is part of WebODF.
*
* WebODF is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* WebODF is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: http://www.webodf.org/
* @source: https://github.com/kogmbh/WebODF/
*/
/*global Node, core, ops, runtime, NodeFilter, Range*/
(function () {
"use strict";
var /**@type{!{rangeBCRIgnoresElementBCR: boolean, unscaledRangeClientRects: boolean, elementBCRIgnoresBodyScroll: !boolean}}*/
browserQuirks;
/**
* Detect various browser quirks
* unscaledRangeClientRects - Firefox doesn't apply parent css transforms to any range client rectangles
* rangeBCRIgnoresElementBCR - Internet explorer returns 0 client rects for an empty element that has fixed dimensions
* elementBCRIgnoresBodyScroll - iOS safari returns false client rects for an element that do not correlate with a scrolled body
* @return {!{unscaledRangeClientRects: !boolean, rangeBCRIgnoresElementBCR: !boolean, elementBCRIgnoresBodyScroll: !boolean}}
*/
function getBrowserQuirks() {
var range,
directBoundingRect,
rangeBoundingRect,
testContainer,
testElement,
detectedQuirks,
window,
document,
docElement,
body,
docOverflow,
bodyOverflow,
bodyHeight,
bodyScroll;
if (browserQuirks === undefined) {
window = runtime.getWindow();
document = window && window.document;
docElement = document.documentElement;
body = document.body;
browserQuirks = {
rangeBCRIgnoresElementBCR: false,
unscaledRangeClientRects: false,
elementBCRIgnoresBodyScroll: false
};
if (document) {
testContainer = document.createElement("div");
testContainer.style.position = "absolute";
testContainer.style.left = "-99999px";
testContainer.style.transform = "scale(2)";
testContainer.style["-webkit-transform"] = "scale(2)";
testElement = document.createElement("div");
testContainer.appendChild(testElement);
body.appendChild(testContainer);
range = document.createRange();
range.selectNode(testElement);
// Internet explorer (v10 and others?) will omit the element's own client rect from
// the returned client rects list for the range
browserQuirks.rangeBCRIgnoresElementBCR = range.getClientRects().length === 0;
testElement.appendChild(document.createTextNode("Rect transform test"));
directBoundingRect = testElement.getBoundingClientRect();
rangeBoundingRect = range.getBoundingClientRect();
// Firefox doesn't apply parent css transforms to any range client rectangles
// See https://bugzilla.mozilla.org/show_bug.cgi?id=863618
// Depending on the browser, client rects can sometimes have sub-pixel rounding effects, so
// add some wiggle room for this. The scale is 200%, so there is no issues with false positives here
browserQuirks.unscaledRangeClientRects = Math.abs(directBoundingRect.height - rangeBoundingRect.height) > 2;
testContainer.style.transform = "";
testContainer.style["-webkit-transform"] = "";
// Backup current values for documentElement and body's overflows, body height, and body scroll.
docOverflow = docElement.style.overflow;
bodyOverflow = body.style.overflow;
bodyHeight = body.style.height;
bodyScroll = body.scrollTop;
// Set new values for the backed up properties
docElement.style.overflow = "visible";
body.style.overflow = "visible";
body.style.height = "200%";
body.scrollTop = body.scrollHeight;
// After extending the body's height to twice and scrolling by that amount,
// if the element's new BCR is not the same as the range's BCR, then
// Houston we have a Quirk! This problem has been seen on iOS7, which
// seems to report the correct BCR for a range but ignores body scroll
// effects on an element...
browserQuirks.elementBCRIgnoresBodyScroll = (range.getBoundingClientRect().top !== testElement.getBoundingClientRect().top);
// Restore backed up property values
body.scrollTop = bodyScroll;
body.style.height = bodyHeight;
body.style.overflow = bodyOverflow;
docElement.style.overflow = docOverflow;
range.detach();
body.removeChild(testContainer);
detectedQuirks = Object.keys(browserQuirks).map(
/**
* @param {!string} quirk
* @return {!string}
*/
function (quirk) {
return quirk + ":" + String(browserQuirks[quirk]);
}
).join(", ");
runtime.log("Detected browser quirks - " + detectedQuirks);
}
}
return browserQuirks;
}
/**
* Return the first child element with the given namespace and name.
* If the parent is null, or if there is no child with the given name and
* namespace, null is returned.
* @param {?Element} parent
* @param {!string} ns
* @param {!string} name
* @return {?Element}
*/
function getDirectChild(parent, ns, name) {
var node = parent ? parent.firstElementChild : null;
while (node) {
if (node.localName === name && node.namespaceURI === ns) {
return /**@type{!Element}*/(node);
}
node = node.nextElementSibling;
}
return null;
}
/**
* A collection of Dom utilities
* @constructor
*/
core.DomUtilsImpl = function DomUtilsImpl() {
var /**@type{?Range}*/
sharedRange = null;
/**
* @param {!Document} doc
* @return {!Range}
*/
function getSharedRange(doc) {
var range;
if (sharedRange) {
range = sharedRange;
} else {
sharedRange = range = /**@type{!Range}*/(doc.createRange());
}
return range;
}
/**
* Find the inner-most child point that is equivalent
* to the provided container and offset.
* @param {!Node} container
* @param {!number} offset
* @return {{container: Node, offset: !number}}
*/
function findStablePoint(container, offset) {
var c = container;
if (offset < c.childNodes.length) {
c = c.childNodes.item(offset);
offset = 0;
while (c.firstChild) {
c = c.firstChild;
}
} else {
while (c.lastChild) {
c = c.lastChild;
offset = c.nodeType === Node.TEXT_NODE
? c.textContent.length
: c.childNodes.length;
}
}
return {container: c, offset: offset};
}
/**
* Gets the unfiltered DOM 'offset' of a node within a container that may not be it's direct parent.
* @param {!Node} node
* @param {!Node} container
* @return {!number}
*/
function getPositionInContainingNode(node, container) {
var offset = 0,
n;
while (node.parentNode !== container) {
runtime.assert(node.parentNode !== null, "parent is null");
node = /**@type{!Node}*/(node.parentNode);
}
n = container.firstChild;
while (n !== node) {
offset += 1;
n = n.nextSibling;
}
return offset;
}
/**
* If either the start or end boundaries of a range start within a text
* node, this function will split these text nodes and reset the range
* boundaries to select the new nodes. The end result is that there are
* no partially contained text nodes within the resulting range.
* E.g., the text node with selection:
* "A|BCD|E"
* would be split into 3 text nodes, with the range modified to maintain
* only the completely selected text node:
* "A" "|BCD|" "E"
* @param {!Range} range
* @return {!Array.<!Node>} Return a list of nodes modified as a result
* of this split operation. These are often
* processed through
* DomUtils.normalizeTextNodes after all
* processing has been complete.
*/
function splitBoundaries(range) {
var modifiedNodes = [],
originalEndContainer,
resetToContainerLength,
end,
splitStart,
node,
text,
offset;
if (range.startContainer.nodeType === Node.TEXT_NODE
|| range.endContainer.nodeType === Node.TEXT_NODE) {
originalEndContainer = range.endContainer;
resetToContainerLength = range.endContainer.nodeType !== Node.TEXT_NODE ?
range.endOffset === range.endContainer.childNodes.length : false;
end = findStablePoint(range.endContainer, range.endOffset);
if (end.container === originalEndContainer) {
originalEndContainer = null;
}
// Stable points need to be found to ensure splitting the text
// node doesn't inadvertently modify the other end of the range
range.setEnd(end.container, end.offset);
// Must split end first to stop the start point from being lost
node = range.endContainer;
if (range.endOffset !== 0 && node.nodeType === Node.TEXT_NODE) {
text = /**@type{!Text}*/(node);
if (range.endOffset !== text.length) {
modifiedNodes.push(text.splitText(range.endOffset));
modifiedNodes.push(text);
// The end doesn't need to be reset as endContainer &
// endOffset are still valid after the modification
}
}
node = range.startContainer;
if (range.startOffset !== 0 && node.nodeType === Node.TEXT_NODE) {
text = /**@type{!Text}*/(node);
if (range.startOffset !== text.length) {
splitStart = text.splitText(range.startOffset);
modifiedNodes.push(text);
modifiedNodes.push(splitStart);
range.setStart(splitStart, 0);
}
}
if (originalEndContainer !== null) {
node = range.endContainer;
while (node.parentNode && node.parentNode !== originalEndContainer) {
node = node.parentNode;
}
if (resetToContainerLength) {
offset = originalEndContainer.childNodes.length;
} else {
offset = getPositionInContainingNode(node, originalEndContainer);
}
range.setEnd(originalEndContainer, offset);
}
}
return modifiedNodes;
}
this.splitBoundaries = splitBoundaries;
/**
* Returns true if the container range completely contains the insideRange.
* Aligned boundaries are counted as inclusion
* @param {!Range} container
* @param {!Range} insideRange
* @return {boolean}
*/
function containsRange(container, insideRange) {
return container.compareBoundaryPoints(Range.START_TO_START, insideRange) <= 0
&& container.compareBoundaryPoints(Range.END_TO_END, insideRange) >= 0;
}
this.containsRange = containsRange;
/**
* Returns true if there is any intersection between range1 and range2
* @param {!Range} range1
* @param {!Range} range2
* @return {boolean}
*/
function rangesIntersect(range1, range2) {
return range1.compareBoundaryPoints(Range.END_TO_START, range2) <= 0
&& range1.compareBoundaryPoints(Range.START_TO_END, range2) >= 0;
}
this.rangesIntersect = rangesIntersect;
/**
* Returns the intersection of two ranges. If there is no intersection, this
* will return undefined.
*
* @param {!Range} range1
* @param {!Range} range2
* @return {!Range|undefined}
*/
function rangeIntersection(range1, range2) {
var newRange;
if (rangesIntersect(range1, range2)) {
newRange = /**@type{!Range}*/(range1.cloneRange());
if (range1.compareBoundaryPoints(Range.START_TO_START, range2) === -1) {
// If range1's start is before range2's start, use range2's start
newRange.setStart(range2.startContainer, range2.startOffset);
}
if (range1.compareBoundaryPoints(Range.END_TO_END, range2) === 1) {
// if range1's end is after range2's end, use range2's end
newRange.setEnd(range2.endContainer, range2.endOffset);
}
}
return newRange;
}
this.rangeIntersection = rangeIntersection;
/**
* Returns the maximum available offset for the node. If this is a text
* node, this will be node.length, or for an element node, childNodes.length
* @param {!Node} node
* @return {!number}
*/
function maximumOffset(node) {
return node.nodeType === Node.TEXT_NODE ? /**@type{!Text}*/(node).length : node.childNodes.length;
}
/**
* Checks all nodes between the tree walker's current node and the defined
* root. If any nodes are rejected, the tree walker is moved to the
* highest rejected node below the root. Note, the root is excluded from
* this check.
*
* This logic is similar to PositionIterator.moveToAcceptedNode
* @param {!TreeWalker} walker
* @param {!Node} root
* @param {!function(!Node) : number} nodeFilter
*
* @return {!Node} Returns the current node the walker is on
*/
function moveToNonRejectedNode(walker, root, nodeFilter) {
var node = walker.currentNode;
// Ensure currentNode is not within a rejected subtree by crawling each parent node
// up to the root and verifying it is either accepted or skipped by the nodeFilter.
// NOTE: The root is deliberately not checked as it is the container iteration happens within.
if (node !== root) {
node = node.parentNode;
while (node && node !== root) {
if (nodeFilter(node) === NodeFilter.FILTER_REJECT) {
walker.currentNode = node;
}
node = node.parentNode;
}
}
return walker.currentNode;
}
/**
* Fetches all nodes within a supplied range that pass the required filter
* @param {!Range} range
* @param {!function(!Node) : number} nodeFilter
* @param {!number} whatToShow
* @return {!Array.<!Node>}
*/
/*jslint bitwise:true*/
function getNodesInRange(range, nodeFilter, whatToShow) {
var document = range.startContainer.ownerDocument,
elements = [],
rangeRoot = range.commonAncestorContainer,
root = /**@type{!Node}*/(rangeRoot.nodeType === Node.TEXT_NODE ? rangeRoot.parentNode : rangeRoot),
treeWalker = document.createTreeWalker(root, whatToShow, nodeFilter, false),
currentNode,
lastNodeInRange,
endNodeCompareFlags,
comparePositionResult;
if (range.endContainer.childNodes[range.endOffset - 1]) {
// This is the last node completely contained in the range
lastNodeInRange = /**@type{!Node}*/(range.endContainer.childNodes[range.endOffset - 1]);
// Accept anything preceding or contained by this node.
endNodeCompareFlags = Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINED_BY;
} else {
// Either no child nodes (e.g., TEXT_NODE) or endOffset = 0
lastNodeInRange = /**@type{!Node}*/(range.endContainer);
// Don't accept things contained within this node, as the range ends before this node's children.
// This is the last node touching the range though, so the node is still accepted into the results.
endNodeCompareFlags = Node.DOCUMENT_POSITION_PRECEDING;
}
if (range.startContainer.childNodes[range.startOffset]) {
// The range starts within startContainer, so this child node is the first node in the range
currentNode = /**@type{!Node}*/(range.startContainer.childNodes[range.startOffset]);
treeWalker.currentNode = currentNode;
} else if (range.startOffset === maximumOffset(range.startContainer)) {
// This condition will be true if the range starts beyond the last position of a node
// E.g., (text, text.length) or (div, div.childNodes.length)
currentNode = /**@type{!Node}*/(range.startContainer);
treeWalker.currentNode = currentNode;
// In this case, move to the last child (if the node has children)
treeWalker.lastChild(); // May return null if the current node has no children
// And navigate onto the next node in sequence
currentNode = treeWalker.nextNode();
} else {
// This will only be hit for a text node that is partially overlapped by the range start
currentNode = /**@type{!Node}*/(range.startContainer);
treeWalker.currentNode = currentNode;
}
if (currentNode) {
// If the treeWalker hit the end of the sequence in the treeWalker.nextNode line just above,
// currentNode will be null.
currentNode = moveToNonRejectedNode(treeWalker, root, nodeFilter);
switch (nodeFilter(/**@type{!Node}*/(currentNode))) {
case NodeFilter.FILTER_REJECT:
// If started on a rejected node, calling nextNode will incorrectly
// dive down into the rejected node's children. Instead, advance to
// the next sibling or parent node's sibling and resume walking from
// there.
currentNode = treeWalker.nextSibling();
while (!currentNode && treeWalker.parentNode()) {
currentNode = treeWalker.nextSibling();
}
break;
case NodeFilter.FILTER_SKIP:
// Immediately advance to the next node without giving an opportunity for the current one to
// be stored.
currentNode = treeWalker.nextNode();
break;
default:
// case NodeFilter.FILTER_ACCEPT:
// Proceed into the following loop. The current node will be stored at the end of the loop
// if it is contained within the requested range.
break;
}
while (currentNode) {
comparePositionResult = lastNodeInRange.compareDocumentPosition(currentNode);
if (comparePositionResult !== 0 && (comparePositionResult & endNodeCompareFlags) === 0) {
// comparePositionResult === 0 if currentNode === lastNodeInRange. This is considered within the range
// comparePositionResult & endNodeCompareFlags would be non-zero if n precedes lastNodeInRange
// If either of these statements are false, currentNode is past the end of the range
break;
}
elements.push(currentNode);
currentNode = treeWalker.nextNode();
}
}
return elements;
}
/*jslint bitwise:false*/
this.getNodesInRange = getNodesInRange;
/**
* Merges the content of node with nextNode.
* If node is an empty text node, it will be removed in any case.
* If nextNode is an empty text node, it will be only removed if node is a text node.
* @param {!Node} node
* @param {!Node} nextNode
* @return {?Node} merged text node or null if there is no text node as result
*/
function mergeTextNodes(node, nextNode) {
var mergedNode = null, text, nextText;
if (node.nodeType === Node.TEXT_NODE) {
text = /**@type{!Text}*/(node);
if (text.length === 0) {
text.parentNode.removeChild(text);
if (nextNode.nodeType === Node.TEXT_NODE) {
mergedNode = nextNode;
}
} else {
if (nextNode.nodeType === Node.TEXT_NODE) {
// in chrome it is important to add nextNode to node.
// doing it the other way around causes random
// whitespace to appear
nextText = /**@type{!Text}*/(nextNode);
text.appendData(nextText.data);
nextNode.parentNode.removeChild(nextNode);
}
mergedNode = node;
}
}
return mergedNode;
}
/**
* Attempts to normalize the node with any surrounding text nodes. No
* actions are performed if the node is undefined, has no siblings, or
* is not a text node
* @param {Node} node
* @return {undefined}
*/
function normalizeTextNodes(node) {
if (node && node.nextSibling) {
node = mergeTextNodes(node, node.nextSibling);
}
if (node && node.previousSibling) {
mergeTextNodes(node.previousSibling, node);
}
}
this.normalizeTextNodes = normalizeTextNodes;
/**
* Checks if the provided limits fully encompass the passed in node
* @param {!Range|{startContainer: Node, startOffset: !number, endContainer: Node, endOffset: !number}} limits
* @param {!Node} node
* @return {boolean} Returns true if the node is fully contained within
* the range
*/
function rangeContainsNode(limits, node) {
var range = node.ownerDocument.createRange(),
nodeRange = node.ownerDocument.createRange(),
result;
range.setStart(limits.startContainer, limits.startOffset);
range.setEnd(limits.endContainer, limits.endOffset);
nodeRange.selectNodeContents(node);
result = containsRange(range, nodeRange);
range.detach();
nodeRange.detach();
return result;
}
this.rangeContainsNode = rangeContainsNode;
/**
* Merge all child nodes into the targetNode's parent and remove the targetNode entirely
* @param {!Node} targetNode
* @return {!Node} parent of targetNode
*/
function mergeIntoParent(targetNode) {
var parent = targetNode.parentNode;
while (targetNode.firstChild) {
parent.insertBefore(targetNode.firstChild, targetNode);
}
parent.removeChild(targetNode);
return parent;
}
this.mergeIntoParent = mergeIntoParent;
/**
* Removes all unwanted nodes from targetNode includes itself.
* The nodeFilter defines which nodes should be removed (NodeFilter.FILTER_REJECT),
* should be skipped including the subtree (NodeFilter.FILTER_SKIP) or should be kept
* and their subtree checked further (NodeFilter.FILTER_ACCEPT).
* @param {!Node} targetNode
* @param {!function(!Node) : !number} nodeFilter
* @return {?Node} parent of targetNode
*/
function removeUnwantedNodes(targetNode, nodeFilter) {
var parent = targetNode.parentNode,
node = targetNode.firstChild,
filterResult = nodeFilter(targetNode),
next;
if (filterResult === NodeFilter.FILTER_SKIP) {
return parent;
}
while (node) {
next = node.nextSibling;
removeUnwantedNodes(node, nodeFilter);
node = next;
}
if (parent && (filterResult === NodeFilter.FILTER_REJECT)) {
mergeIntoParent(targetNode);
}
return parent;
}
this.removeUnwantedNodes = removeUnwantedNodes;
/**
* Removes all child nodes from the given node.
* To be used instead of e.g. `node.innerHTML = "";`
* @param {!Node} node
* @return {undefined}
*/
this.removeAllChildNodes = function (node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
};
/**
* Get an array of nodes below the specified node with the specific namespace and tag name.
*
* Use this function instead of node.getElementsByTagNameNS when modifications are going to be made
* to the document content during iteration. For read-only uses, node.getElementsByTagNameNS will perform
* faster and use less memory. See https://github.com/kogmbh/WebODF/issues/736 for further discussion.
*
* @param {!Element|!Document} node
* @param {!string} namespace
* @param {!string} tagName
* @return {!Array.<!Element>}
*/
function getElementsByTagNameNS(node, namespace, tagName) {
var e = [], list, i, l;
list = node.getElementsByTagNameNS(namespace, tagName);
e.length = l = list.length;
for (i = 0; i < l; i += 1) {
e[i] = /**@type{!Element}*/(list.item(i));
}
return e;
}
this.getElementsByTagNameNS = getElementsByTagNameNS;
/**
* Get an array of nodes below the specified node with the specific name tag name.
*
* Use this function instead of node.getElementsByTagName when modifications are going to be made
* to the document content during iteration. For read-only uses, node.getElementsByTagName will perform
* faster and use less memory. See https://github.com/kogmbh/WebODF/issues/736 for further discussion.
*
* @param {!Element|!Document} node
* @param {!string} tagName
* @return {!Array.<!Element>}
*/
function getElementsByTagName(node, tagName) {
var e = [], list, i, l;
list = node.getElementsByTagName(tagName);
e.length = l = list.length;
for (i = 0; i < l; i += 1) {
e[i] = /**@type{!Element}*/(list.item(i));
}
return e;
}
this.getElementsByTagName = getElementsByTagName;
/**
* Whether a node contains another node
* Wrapper around Node.contains
* http://www.w3.org/TR/domcore/#dom-node-contains
* @param {!Node} parent The node that should contain the other node
* @param {?Node} descendant The node to test presence of
* @return {!boolean}
*/
function containsNode(parent, descendant) {
return parent === descendant
// the casts to Element are a workaround due to a different
// contains() definition in the Closure Compiler externs file.
|| /**@type{!Element}*/(parent).contains(/**@type{!Element}*/(descendant));
}
this.containsNode = containsNode;
/**
* Whether a node contains another node
* @param {!Node} parent The node that should contain the other node
* @param {?Node} descendant The node to test presence of
* @return {!boolean}
*/
/*jslint bitwise:true*/
function containsNodeForBrokenWebKit(parent, descendant) {
// the contains function is not reliable on safari/webkit so use
// compareDocumentPosition instead
return parent === descendant ||
Boolean(parent.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
}
/*jslint bitwise:false*/
/**
* Return a number > 0 when point 1 precedes point 2. Return 0 if the points
* are equal. Return < 0 when point 2 precedes point 1.
* @param {!Node} c1 container of point 1
* @param {!number} o1 offset in unfiltered DOM world of point 1
* @param {!Node} c2 container of point 2
* @param {!number} o2 offset in unfiltered DOM world of point 2
* @return {!number}
*/
function comparePoints(c1, o1, c2, o2) {
if (c1 === c2) {
return o2 - o1;
}
var comparison = c1.compareDocumentPosition(c2);
if (comparison === 2) { // DOCUMENT_POSITION_PRECEDING
comparison = -1;
} else if (comparison === 4) { // DOCUMENT_POSITION_FOLLOWING
comparison = 1;
} else if (comparison === 10) { // DOCUMENT_POSITION_CONTAINS
// c0 contains c2
o1 = getPositionInContainingNode(c1, c2);
comparison = (o1 < o2) ? 1 : -1;
} else { // DOCUMENT_POSITION_CONTAINED_BY
o2 = getPositionInContainingNode(c2, c1);
comparison = (o2 < o1) ? -1 : 1;
}
return comparison;
}
this.comparePoints = comparePoints;
/**
* Scale the supplied number by the specified zoom transformation if the
* bowser does not transform range client rectangles correctly.
* In firefox, the span rectangle will be affected by the zoom, but the
* range is not. In most all other browsers, the range number is
* affected zoom.
*
* See http://dev.w3.org/csswg/cssom-view/#extensions-to-the-range-interface
* Section 10, getClientRects,
* "The transforms that apply to the ancestors are applied."
* @param {!number} inputNumber An input number to be scaled. This is
* expected to be the difference between
* a property on two range-sourced client
* rectangles (e.g., rect1.top - rect2.top)
* @param {!number} zoomLevel Current canvas zoom level
* @return {!number}
*/
function adaptRangeDifferenceToZoomLevel(inputNumber, zoomLevel) {
if (getBrowserQuirks().unscaledRangeClientRects) {
return inputNumber;
}
return inputNumber / zoomLevel;
}
this.adaptRangeDifferenceToZoomLevel = adaptRangeDifferenceToZoomLevel;
/**
* Translate a given child client rectangle to be relative to the parent's rectangle.
* Adapt to the provided zoom level as per adaptRangeDifferenceToZoomLevel.
*
* IMPORTANT: due to browser quirks, any element bounding client rect used with this function
* MUST be retrieved using DomUtils.getBoundingClientRect.
*
* @param {!ClientRect|!Object.<!string, !number>} child
* @param {!ClientRect|!Object.<!string, !number>} parent
* @param {!number} zoomLevel
* @return {!ClientRect|{top: !number, left: !number, bottom: !number, right: !number, width: !number, height: !number}}
*/
this.translateRect = function(child, parent, zoomLevel) {
return {
top: adaptRangeDifferenceToZoomLevel(child.top - parent.top, zoomLevel),
left: adaptRangeDifferenceToZoomLevel(child.left - parent.left, zoomLevel),
bottom: adaptRangeDifferenceToZoomLevel(child.bottom - parent.top, zoomLevel),
right: adaptRangeDifferenceToZoomLevel(child.right - parent.left, zoomLevel),
width: adaptRangeDifferenceToZoomLevel(child.width, zoomLevel),
height: adaptRangeDifferenceToZoomLevel(child.height, zoomLevel)
};
};
/**
* Get the bounding client rect for the specified node.
* This function attempts to cope with various browser quirks, ideally
* returning a rectangle that can be used in conjunction with rectangles
* retrieved from ranges.
*
* Range & element client rectangles can only be mixed if both are
* transformed in the same way.
* See https://bugzilla.mozilla.org/show_bug.cgi?id=863618
* @param {!Node} node
* @return {?ClientRect}
*/
function getBoundingClientRect(node) {
var doc = /**@type{!Document}*/(node.ownerDocument),
quirks = getBrowserQuirks(),
range,
element,
rect,
body = doc.body;
if (quirks.unscaledRangeClientRects === false
|| quirks.rangeBCRIgnoresElementBCR) {
if (node.nodeType === Node.ELEMENT_NODE) {
element = /**@type{!Element}*/(node);
rect = element.getBoundingClientRect();
if (quirks.elementBCRIgnoresBodyScroll) {
return /**@type{?ClientRect}*/({
left: rect.left + body.scrollLeft,
right: rect.right + body.scrollLeft,
top: rect.top + body.scrollTop,
bottom: rect.bottom + body.scrollTop,
width: rect.width,
height: rect.height
});
}
return rect;
}
}
range = getSharedRange(doc);
range.selectNode(node);
return range.getBoundingClientRect();
}
this.getBoundingClientRect = getBoundingClientRect;
/**
* Takes a flat object which is a key-value
* map of strings, and populates/modifies
* the node with child elements which have
* the key name as the node name (namespace
* prefix required in the key name)
* and the value as the text content.
* Example: mapKeyValObjOntoNode(node, {"dc:creator": "Bob"}, nsResolver);
* If a namespace prefix is unresolved with the
* nsResolver, that key will be ignored and not written to the node.
* @param {!Element} node
* @param {!Object.<!string, !string>} properties
* @param {!function(!string):?string} nsResolver
*/
function mapKeyValObjOntoNode(node, properties, nsResolver) {
Object.keys(properties).forEach(function (key) {
var parts = key.split(":"),
prefix = parts[0],
localName = parts[1],
ns = nsResolver(prefix),
value = properties[key],
element;
// Ignore if the prefix is unsupported,
// otherwise set the textContent of the
// element to the value.
if (ns) {
element = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(ns, localName)[0]);
if (!element) {
element = node.ownerDocument.createElementNS(ns, key);
node.appendChild(element);
}
element.textContent = value;
} else {
runtime.log("Key ignored: " + key);
}
});
}
this.mapKeyValObjOntoNode = mapKeyValObjOntoNode;
/**
* Takes an array of strings, which is a listing of
* properties to be removed (namespace required),
* and deletes the corresponding top-level child elements
* that represent those properties, from the
* supplied node.
* Example: removeKeyElementsFromNode(node, ["dc:creator"], nsResolver);
* If a namespace is not resolved with the nsResolver,
* that key element will be not removed.
* If a key element does not exist, it will be ignored.
* @param {!Element} node
* @param {!Array.<!string>} propertyNames
* @param {!function(!string):?string} nsResolver
*/
function removeKeyElementsFromNode(node, propertyNames, nsResolver) {
propertyNames.forEach(function (propertyName) {
var parts = propertyName.split(":"),
prefix = parts[0],
localName = parts[1],
ns = nsResolver(prefix),
element;
// Ignore if the prefix is unsupported,
// otherwise delete the element if found
if (ns) {
element = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(ns, localName)[0]);
if (element) {
element.parentNode.removeChild(element);
} else {
runtime.log("Element for " + propertyName + " not found.");
}
} else {
runtime.log("Property Name ignored: " + propertyName);
}
});
}
this.removeKeyElementsFromNode = removeKeyElementsFromNode;
/**
* Looks at an element's direct children, and generates an object which is a
* flat key-value map from the child's ns:localName to it's text content.
* Only those children that have a resolvable prefixed name will be taken into
* account for generating this map.
* @param {!Element} node
* @param {function(!string):?string} prefixResolver
* @return {!Object.<!string,!string>}
*/
function getKeyValRepresentationOfNode(node, prefixResolver) {
var properties = {},
currentSibling = node.firstElementChild,
prefix;
while (currentSibling) {
prefix = prefixResolver(currentSibling.namespaceURI);
if (prefix) {
properties[prefix + ':' + currentSibling.localName] = currentSibling.textContent;
}
currentSibling = currentSibling.nextElementSibling;
}
return properties;
}
this.getKeyValRepresentationOfNode = getKeyValRepresentationOfNode;
/**
* Maps attributes and elements in the properties object over top of the node.
* Supports recursion and deep mapping.
*
* Supported value types are:
* - string (mapped to an attribute string on node)
* - number (mapped to an attribute string on node)
* - object (deep mapped to a new child node on node)
*
* @param {!Element} node
* @param {!Object.<string,*>} properties
* @param {!function(!string):?string} nsResolver
*/
function mapObjOntoNode(node, properties, nsResolver) {
Object.keys(properties).forEach(function(key) {
var parts = key.split(":"),
prefix = parts[0],
localName = parts[1],
ns = nsResolver(prefix),
value = properties[key],
valueType = typeof value,
element;
if (valueType === "object") {
// Only create the destination sub-element if there are values to populate it with
if (Object.keys(/**@type{!Object}*/(value)).length) {
if (ns) {
element = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(ns, localName)[0])
|| node.ownerDocument.createElementNS(ns, key);
} else {
element = /**@type{!Element|undefined}*/(node.getElementsByTagName(localName)[0])
|| node.ownerDocument.createElement(key);
}
node.appendChild(element);
mapObjOntoNode(element, /**@type{!Object}*/(value), nsResolver);
}
} else if (ns) {
runtime.assert(valueType === "number" || valueType === "string",
"attempting to map unsupported type '" + valueType + "' (key: " + key + ")");
node.setAttributeNS(ns, key, String(value));
// If the prefix is unknown or unsupported, simply ignore it for now
}
});
}
this.mapObjOntoNode = mapObjOntoNode;
/**
* Clones an event object.
* IE10 destructs event objects once the event handler is done:
* "The event object is only available during an event; that is, you can use it in event handlers but not in other code"
* (from http://msdn.microsoft.com/en-us/library/ie/aa703876(v=vs.85).aspx)
* This method can be used to create a copy of the event object, to work around that.
* @param {!Event} event
* @return {!Event}
*/
function cloneEvent(event) {
var e = Object.create(null);
// copy over all direct properties
Object.keys(event.constructor.prototype).forEach(function (x) {
e[x] = event[x];
});
// only now set the prototype (might set properties read-only)
e.prototype = event.constructor.prototype;
return /**@type{!Event}*/(e);
}
this.cloneEvent = cloneEvent;
this.getDirectChild = getDirectChild;
/**
* @param {!core.DomUtilsImpl} self
*/
function init(self) {
var appVersion, webKitOrSafari, ie,
/**@type{?Window}*/
window = runtime.getWindow();
if (window === null) {
return;
}
appVersion = window.navigator.appVersion.toLowerCase();
webKitOrSafari = appVersion.indexOf('chrome') === -1
&& (appVersion.indexOf('applewebkit') !== -1
|| appVersion.indexOf('safari') !== -1);
// See http://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect
// Also, IE cleverly removed the MSIE tag without fixing the bug we're attempting to sniff here...
// http://msdn.microsoft.com/en-us/library/ie/bg182625%28v=vs.110%29.aspx
ie = appVersion.indexOf('msie') !== -1 || appVersion.indexOf('trident') !== -1;
if (webKitOrSafari || ie) {
self.containsNode = containsNodeForBrokenWebKit;
}
}
init(this);
};
/**
* @type {!core.DomUtilsImpl}
*/
core.DomUtils = new core.DomUtilsImpl();
}());