mirror of
https://github.com/Polymer/polymer.git
synced 2025-02-25 18:55:30 -06:00
620 lines
19 KiB
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> |