Files
polymer/src/lib/dom-api-shady.html

620 lines
19 KiB
HTML

<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="settings.html">
<link rel="import" href="dom-innerHTML.html">
<script>
(function() {
'use strict';
var Settings = Polymer.Settings;
var DomApi = Polymer.DomApi;
var dom = DomApi.factory;
var TreeApi = Polymer.TreeApi;
var getInnerHTML = Polymer.domInnerHTML.getInnerHTML;
var CONTENT = DomApi.CONTENT;
// *************** Configure DomApi for Shady DOM!! ***************
if (Settings.useShadow) {
return;
}
var nativeCloneNode = Element.prototype.cloneNode;
var nativeImportNode = Document.prototype.importNode;
Polymer.Base.extend(DomApi.prototype, {
_lazyDistribute: function(host) {
// note: only try to distribute if the root is not clean; this ensures
// we don't distribute before initial distribution
if (host.shadyRoot && host.shadyRoot._distributionClean) {
host.shadyRoot._distributionClean = false;
Polymer.dom.addDebouncer(host.debounce('_distribute',
host._distributeContent));
}
},
appendChild: function(node) {
return this.insertBefore(node);
},
// cases in which we may not be able to just do standard native call
// 1. container has a shadyRoot (needsDistribution IFF the shadyRoot
// has an insertion point)
// 2. container is a shadyRoot (don't distribute, instead set
// container to container.host.
// 3. node is <content> (host of container needs distribution)
insertBefore: function(node, ref_node) {
if (ref_node && TreeApi.Logical.getParentNode(ref_node) !== this.node) {
throw Error('The ref_node to be inserted before is not a child ' +
'of this node');
}
// notify existing parent that this node is being removed.
var parent = TreeApi.Logical.getParentNode(node);
if (parent && DomApi.hasApi(parent)) {
dom(parent).notifyObserver();
}
this._removeNode(node);
if (!this._addNode(node, ref_node)) {
if (ref_node) {
// if ref_node is <content> replace with first distributed node
ref_node = ref_node.localName === CONTENT ?
this._firstComposedNode(ref_node) : ref_node;
}
// if adding to a shadyRoot, add to host instead
var container = this.node._isShadyRoot ? this.node.host : this.node;
if (ref_node) {
TreeApi.Composed.insertBefore(container, node, ref_node);
} else {
TreeApi.Composed.appendChild(container, node);
}
}
this.notifyObserver();
return node;
},
// Try to add node. Record logical info, track insertion points, perform
// distribution iff needed. Return true if the add is handled.
_addNode: function(node, ref_node) {
var root = this.getOwnerRoot();
if (root) {
root._invalidInsertionPoints =
this._maybeAddInsertionPoint(node, this.node);
this._addNodeToHost(root.host, node);
}
if (TreeApi.Logical.hasChildNodes(this.node)) {
TreeApi.Logical.recordInsertBefore(node, this.node, ref_node);
}
// if not distributing and not adding to host, do a fast path addition
return (this._maybeDistribute(node) ||
this._tryRemoveUndistributedNode(node));
},
/**
Removes the given `node` from the element's `lightChildren`.
This method also performs dom composition.
*/
removeChild: function(node) {
if (TreeApi.Logical.getParentNode(node) !== this.node) {
throw Error('The node to be removed is not a child of this node: ' +
node);
}
if (!this._removeNode(node)) {
// if removing from a shadyRoot, remove form host instead
var container = this.node._isShadyRoot ? this.node.host : this.node;
// not guaranteed to physically be in container; e.g.
// undistributed nodes.
if (container === node.parentNode) {
TreeApi.Composed.removeChild(container, node);
}
}
this.notifyObserver();
return node;
},
// Try to remove node: update logical info and perform distribution iff
// needed. Return true if the removal has been handled.
// note that it's possible for both the node's host and its parent
// to require distribution... both cases are handled here.
_removeNode: function(node) {
// important that we want to do this only if the node has a logical parent
var logicalParent = TreeApi.Logical.hasParentNode(node) &&
TreeApi.Logical.getParentNode(node);
var distributed;
var root = this._ownerShadyRootForNode(node);
if (logicalParent) {
// distribute node's parent iff needed
distributed = dom(node)._maybeDistributeParent();
TreeApi.Logical.recordRemoveChild(node, logicalParent);
// remove node from root and distribute it iff needed
if (root && this._removeDistributedChildren(root, node)) {
root._invalidInsertionPoints = true;
this._lazyDistribute(root.host);
}
}
this._removeOwnerShadyRoot(node);
if (root) {
this._removeNodeFromHost(root.host, node);
}
return distributed;
},
replaceChild: function(node, ref_node) {
this.insertBefore(node, ref_node);
this.removeChild(ref_node);
return node;
},
_hasCachedOwnerRoot: function(node) {
return Boolean(node._ownerShadyRoot !== undefined);
},
getOwnerRoot: function() {
return this._ownerShadyRootForNode(this.node);
},
_ownerShadyRootForNode: function(node) {
if (!node) {
return;
}
if (node._ownerShadyRoot === undefined) {
var root;
if (node._isShadyRoot) {
root = node;
} else {
var parent = TreeApi.Logical.getParentNode(node);
if (parent) {
root = parent._isShadyRoot ? parent :
this._ownerShadyRootForNode(parent);
} else {
root = null;
}
}
node._ownerShadyRoot = root;
}
return node._ownerShadyRoot;
},
_maybeDistribute: function(node) {
// TODO(sorvell): technically we should check non-fragment nodes for
// <content> children but since this case is assumed to be exceedingly
// rare, we avoid the cost and will address with some specific api
// when the need arises. For now, the user must call
// distributeContent(true), which updates insertion points manually
// and forces distribution.
var fragContent = (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) &&
!node.__noContent && dom(node).querySelector(CONTENT);
var wrappedContent = fragContent &&
(TreeApi.Logical.getParentNode(fragContent).nodeType !==
Node.DOCUMENT_FRAGMENT_NODE);
var hasContent = fragContent || (node.localName === CONTENT);
// There are 2 possible cases where a distribution may need to occur:
// 1. <content> being inserted (the host of the shady root where
// content is inserted needs distribution)
// 2. children being inserted into parent with a shady root (parent
// needs distribution)
if (hasContent) {
var root = this.getOwnerRoot();
if (root) {
// note, insertion point list update is handled after node
// mutations are complete
this._lazyDistribute(root.host);
}
}
var needsDist = this._nodeNeedsDistribution(this.node);
if (needsDist) {
this._lazyDistribute(this.node);
}
// Return true when distribution will fully handle the composition
// Note that if a content was being inserted that was wrapped by a node,
// and the parent does not need distribution, return false to allow
// the nodes to be added directly, after which children may be
// distributed and composed into the wrapping node(s)
return needsDist || (hasContent && !wrappedContent);
},
/* note: parent argument is required since node may have an out
of date parent at this point; returns true if a <content> is being added */
_maybeAddInsertionPoint: function(node, parent) {
var added;
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE &&
!node.__noContent) {
var c$ = dom(node).querySelectorAll(CONTENT);
for (var i=0, n, np, na; (i<c$.length) && (n=c$[i]); i++) {
np = TreeApi.Logical.getParentNode(n);
// don't allow node's parent to be fragment itself
if (np === node) {
np = parent;
}
na = this._maybeAddInsertionPoint(n, np);
added = added || na;
}
} else if (node.localName === CONTENT) {
TreeApi.Logical.saveChildNodes(parent);
TreeApi.Logical.saveChildNodes(node);
added = true;
}
return added;
},
_tryRemoveUndistributedNode: function(node) {
if (this.node.shadyRoot) {
var parent = TreeApi.Composed.getParentNode(node);
if (parent) {
TreeApi.Composed.removeChild(parent, node);
}
return true;
}
},
_updateInsertionPoints: function(host) {
var i$ = host.shadyRoot._insertionPoints =
dom(host.shadyRoot).querySelectorAll(CONTENT);
// ensure <content>'s and their parents have logical dom info.
for (var i=0, c; i < i$.length; i++) {
c = i$[i];
TreeApi.Logical.saveChildNodes(c);
TreeApi.Logical.saveChildNodes(TreeApi.Logical.getParentNode(c));
}
},
_nodeNeedsDistribution: function(node) {
return node && node.shadyRoot &&
DomApi.hasInsertionPoint(node.shadyRoot);
},
// a node being added is always in this same host as this.node.
_addNodeToHost: function(host, node) {
if (host._elementAdd) {
host._elementAdd(node);
}
},
_removeNodeFromHost: function(host, node) {
if (host._elementRemove) {
host._elementRemove(node);
}
},
_removeDistributedChildren: function(root, container) {
var hostNeedsDist;
var ip$ = root._insertionPoints;
for (var i=0; i<ip$.length; i++) {
var content = ip$[i];
if (this._contains(container, content)) {
var dc$ = dom(content).getDistributedNodes();
for (var j=0; j<dc$.length; j++) {
hostNeedsDist = true;
var node = dc$[j];
var parent = node.parentNode;
if (parent) {
TreeApi.Composed.removeChild(parent, node);
}
}
}
}
return hostNeedsDist;
},
_contains: function(container, node) {
while (node) {
if (node == container) {
return true;
}
node = TreeApi.Logical.getParentNode(node);
}
},
_removeOwnerShadyRoot: function(node) {
// optimization: only reset the tree if node is actually in a root
if (this._hasCachedOwnerRoot(node)) {
var c$ = TreeApi.Logical.getChildNodes(node);
for (var i=0, l=c$.length, n; (i<l) && (n=c$[i]); i++) {
this._removeOwnerShadyRoot(n);
}
}
node._ownerShadyRoot = undefined;
},
// TODO(sorvell): This will fail if distribution that affects this
// question is pending; this is expected to be exceedingly rare, but if
// the issue comes up, we can force a flush in this case.
_firstComposedNode: function(content) {
var n$ = dom(content).getDistributedNodes();
for (var i=0, l=n$.length, n, p$; (i<l) && (n=n$[i]); i++) {
p$ = dom(n).getDestinationInsertionPoints();
// means that we're composed to this spot.
if (p$[p$.length-1] === content) {
return n;
}
}
},
// TODO(sorvell): consider doing native QSA and filtering results.
querySelector: function(selector) {
return this.querySelectorAll(selector)[0];
},
querySelectorAll: function(selector) {
return this._query(function(n) {
return DomApi.matchesSelector.call(n, selector);
}, this.node);
},
_query: function(matcher, node) {
node = node || this.node;
var list = [];
this._queryElements(TreeApi.Logical.getChildNodes(node), matcher, list);
return list;
},
_queryElements: function(elements, matcher, list) {
for (var i=0, l=elements.length, c; (i<l) && (c=elements[i]); i++) {
if (c.nodeType === Node.ELEMENT_NODE) {
this._queryElement(c, matcher, list);
}
}
},
_queryElement: function(node, matcher, list) {
if (matcher(node)) {
list.push(node);
}
this._queryElements(TreeApi.Logical.getChildNodes(node), matcher, list);
},
getDestinationInsertionPoints: function() {
return this.node._destinationInsertionPoints || [];
},
getDistributedNodes: function() {
return this.node._distributedNodes || [];
},
_clear: function() {
while (this.childNodes.length) {
this.removeChild(this.childNodes[0]);
}
},
setAttribute: function(name, value) {
this.node.setAttribute(name, value);
this._maybeDistributeParent();
},
removeAttribute: function(name) {
this.node.removeAttribute(name);
this._maybeDistributeParent();
},
_maybeDistributeParent: function() {
if (this._nodeNeedsDistribution(this.parentNode)) {
this._lazyDistribute(this.parentNode);
return true;
}
},
cloneNode: function(deep) {
var n = nativeCloneNode.call(this.node, false);
if (deep) {
var c$ = this.childNodes;
var d = dom(n);
for (var i=0, nc; i < c$.length; i++) {
nc = dom(c$[i]).cloneNode(true);
d.appendChild(nc);
}
}
return n;
},
importNode: function(externalNode, deep) {
// for convenience use this node's ownerDoc if the node isn't a document
var doc = this.node instanceof Document ? this.node :
this.node.ownerDocument;
var n = nativeImportNode.call(doc, externalNode, false);
if (deep) {
var c$ = TreeApi.Logical.getChildNodes(externalNode);
var d = dom(n);
for (var i=0, nc; i < c$.length; i++) {
nc = dom(doc).importNode(c$[i], true);
d.appendChild(nc);
}
}
return n;
},
_getComposedInnerHTML: function() {
return getInnerHTML(this.node, true);
}
});
Object.defineProperties(DomApi.prototype, {
childNodes: {
get: function() {
var c$ = TreeApi.Logical.getChildNodes(this.node);
return Array.isArray(c$) ? c$ : TreeApi.arrayCopyChildNodes(this.node);
},
configurable: true
},
children: {
get: function() {
if (TreeApi.Logical.hasChildNodes(this.node)) {
return Array.prototype.filter.call(this.childNodes, function(n) {
return (n.nodeType === Node.ELEMENT_NODE);
});
} else {
return TreeApi.arrayCopyChildren(this.node);
}
},
configurable: true
},
parentNode: {
get: function() {
return TreeApi.Logical.getParentNode(this.node);
},
configurable: true
},
firstChild: {
get: function() {
return TreeApi.Logical.getFirstChild(this.node);
},
configurable: true
},
lastChild: {
get: function() {
return TreeApi.Logical.getLastChild(this.node);
},
configurable: true
},
nextSibling: {
get: function() {
return TreeApi.Logical.getNextSibling(this.node);
},
configurable: true
},
previousSibling: {
get: function() {
return TreeApi.Logical.getPreviousSibling(this.node);
},
configurable: true
},
firstElementChild: {
get: function() {
if (this.node.__firstChild) {
var n = this.node.__firstChild;
while (n && n.nodeType !== Node.ELEMENT_NODE) {
n = n.__nextSibling;
}
return n;
} else {
return this.node.firstElementChild;
}
},
configurable: true
},
lastElementChild: {
get: function() {
if (this.node.__lastChild) {
var n = this.node.__lastChild;
while (n && n.nodeType !== Node.ELEMENT_NODE) {
n = n.__previousSibling;
}
return n;
} else {
return this.node.lastElementChild;
}
},
configurable: true
},
nextElementSibling: {
get: function() {
if (this.node.__nextSibling) {
var n = this.node.__nextSibling;
while (n && n.nodeType !== Node.ELEMENT_NODE) {
n = n.__nextSibling;
}
return n;
} else {
return this.node.nextElementSibling;
}
},
configurable: true
},
previousElementSibling: {
get: function() {
if (this.node.__previousSibling) {
var n = this.node.__previousSibling;
while (n && n.nodeType !== Node.ELEMENT_NODE) {
n = n.__previousSibling;
}
return n;
} else {
return this.node.previousElementSibling;
}
},
configurable: true
},
// textContent / innerHTML
textContent: {
get: function() {
var nt = this.node.nodeType;
if (nt === Node.TEXT_NODE || nt === Node.COMMENT_NODE) {
return this.node.textContent;
} else {
var tc = [];
for (var i = 0, cn = this.childNodes, c; c = cn[i]; i++) {
if (c.nodeType !== Node.COMMENT_NODE) {
tc.push(c.textContent);
}
}
return tc.join('');
}
},
set: function(text) {
var nt = this.node.nodeType;
if (nt === Node.TEXT_NODE || nt === Node.COMMENT_NODE) {
this.node.textContent = text;
} else {
this._clear();
if (text) {
this.appendChild(document.createTextNode(text));
}
}
},
configurable: true
},
innerHTML: {
get: function() {
var nt = this.node.nodeType;
if (nt === Node.TEXT_NODE || nt === Node.COMMENT_NODE) {
return null;
} else {
return getInnerHTML(this.node);
}
},
set: function(text) {
var nt = this.node.nodeType;
if (nt !== Node.TEXT_NODE || nt !== Node.COMMENT_NODE) {
this._clear();
var d = document.createElement('div');
d.innerHTML = text;
// here, appendChild may move nodes async so we cannot rely
// on node position when copying
var c$ = TreeApi.arrayCopyChildNodes(d);
for (var i=0; i < c$.length; i++) {
this.appendChild(c$[i]);
}
}
},
configurable: true
}
});
DomApi.hasInsertionPoint = function(root) {
return Boolean(root && root._insertionPoints.length);
};
})();
</script>