Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expands the <content> element to remember logical DOM #1017

Merged
merged 1 commit into from
Dec 11, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions polymer-core.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@
}
this.listenListeners();
this.listenKeyPresses();
if (this._useContent) {
this.distributeContent();
}
this.takeAttributes();
}

Expand Down
304 changes: 280 additions & 24 deletions src/features/content.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@
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="ready.html">
<script>
(function() {

/**

Implements a pared down version of ShadowDOM's scoping, which is easy to
polyfill across browsers.

*/

Base.addFeature({

Expand All @@ -27,38 +36,285 @@
},

poolContent: function() {
// pool the light dom
var pool = document.createDocumentFragment();
while (this.firstChild) {
pool.appendChild(this.firstChild);
}
this.contentPool = pool;
// capture lightChildren to help reify dom scoping
this.lightChildren =
Array.prototype.slice.call(this.contentPool.childNodes, 0);
saveLightChildrenIfNeeded(this);
// create our lite ShadowRoot document fragment
// this is where the <template> contents will be stamped
var root = document.createDocumentFragment();
// add a pointer back from the lite ShadowRoot to this node.
root.host = this;
// initialize the `root` pointers: `root` is guarenteed to always be
// available, and be either `this` or `this.contentRoot`. By contrast,
// `contentRoot` is only set if _useContent is true.
this.contentRoot = root;
this.root = root;
// TODO(jmesserly): ad-hoc signal for `ShadowDOM-lite-enhanced` root
root.isShadowRoot = true;
},

distributeContent: function() {
var content, pool = this.contentPool;
// replace <content> with nodes teleported from pool
while (content = this.querySelector('content')) {
var select = content.getAttribute('select');
var frag = pool;
if (select) {
frag = document.createDocumentFragment();
// TODO(sjmiles): diverges from ShadowDOM spec behavior: ShadowDOM
// only selects top level nodes from pool. Iterate children and match
// manually instead.
var nodes = pool.querySelectorAll(select);
for (var i=0, l=nodes.length; i<l; i++) {
frag.appendChild(nodes[i]);
// sanity check to guard against uninitialized state
if (!this.contentRoot) {
throw Error('poolContent() must be called before distributeContent()');
}
// reset distributions
this._resetLightTree(this.contentRoot);
// compute which nodes should be distributed where
// TODO(jmesserly): this is simplified because we assume a single
// ShadowRoot per host and no `<shadow>`.
this._poolDistribution(this.contentRoot, this._poolPopulation());
// update the real DOM to be the composed tree
this._composeTree(this);
},

// TODO(jmesserly): these methods will perform in O(N^2) where N is the
// number of times they are called. That is because each call does
// `distibuteContent` and the work it needs to do increases with each
// subsequent call. An alternative approach would be to schedule the work,
// and do it asynchronously, which would give us O(N) performance because
// we'd do it once per frame in the worst case.
addLightChild: function(node, opt_index) {
saveLightChildrenIfNeeded(this);
if (opt_index === undefined) {
this.lightChildren.push(node);
} else {
this.lightChildren.splice(opt_index, 0, node);
}
this.distributeContent();
},

removeLightChild: function(node) {
saveLightChildrenIfNeeded(this);
var index = this.lightChildren.indexOf(node);
if (index < 0) {
throw Error('The node to be removed is not a light child of this node');
}
this.lightChildren.splice(index, 1);
this.distributeContent();
},

// This is a polyfill for Element.prototype.matches, which is sometimes
// still prefixed. Alternatively we could just polyfill it somewhere.
// Note that the arguments are reversed from what you might expect.
elementMatches: function(selector, node) {
if (node === undefined) node = this;
return matchesSelector.call(node, selector);
},

_poolPopulation: function() {
// Gather the pool of nodes that should be distributed. We will combine
// these with the "content root" to arrive at the composed tree.
var pool = [];
var children = getLightChildren(this);
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (isInsertionPoint(child)) {
pool.push.apply(pool, child._distributedNodes);
} else {
pool.push(child);
}
}
return pool;
},

// Many of the following methods are all conceptually static, but they are
// included here as "protected" methods to allow overriding.

_resetLightTree: function(node) {
var children = getLightChildren(node);
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (isInsertionPoint(child)) {
child._distributedNodes = [];
} else if (child._destinationInsertionPoints) {
child._destinationInsertionPoints = undefined;
}
this._resetLightTree(child);
}
},

_poolDistribution: function(node, pool) {
if (node.localName == 'content') {
// distribute nodes from the pool that this selector matches
var content = node;
var anyDistributed = false;
for (var i = 0; i < pool.length; i++) {
var node = pool[i];
// skip nodes that were already used
if (!node) continue;
// distribute this node if it matches
if (this._matchesContentSelect(node, content)) {
distributeNodeInto(node, content);
// remove this node from the pool
pool[i] = undefined;
// since at least one node matched, we won't need fallback content
anyDistributed = true;
}
}
// content self-destructs
content.parentNode.replaceChild(frag, content);
// Fallback content if nothing was distributed here
if (!anyDistributed) {
var children = getLightChildren(content);
for (var i = 0; i < children.length; i++) {
distributeNodeInto(children[i], content);
}
}
return;
}
}
// recursively distribute.
var children = getLightChildren(node);
for (var i = 0; i < children.length; i++) {
this._poolDistribution(children[i], pool);
}
},

_composeTree: function(node) {
var children = this._composeNode(node);
for (var i = 0; i < children.length; i++) {
var child = children[i];
// If the child has a content root, let it compose itself.
if (!child.contentRoot) {
this._composeTree(child);
}
}
this._updateChildNodes(node, children);
},

_composeNode: function(node) {
var children = [];
var lightChildren = getLightChildren(node.contentRoot || node);
for (var i = 0; i < lightChildren.length; i++) {
var child = lightChildren[i];
if (isInsertionPoint(child)) {
var distributedNodes = child._distributedNodes;
for (var j = 0; j < distributedNodes.length; j++) {
var distributedNode = distributedNodes[j];
if (isFinalDestination(child, distributedNode)) {
children.push(distributedNode);
}
}
} else {
children.push(child);
}
}
return children;
},

_updateChildNodes: function(node, children) {
// Add the children that need to be added. Walk the list backwards so we can
// use insertBefore easily.
for (var i = children.length - 1, nextNode = null; i >= 0; i--) {
var child = children[i];
// if the node is in the wrong place, move it.
if (child.parentNode != node || child.nextSibling != nextNode) {
insertBefore(node, child, nextNode);
}
nextNode = child;
}
// We just added nodes in order, starting from the end, so anything before
// the first node is gone and should be removed.
var first = children[0];
var child = node.firstChild;
while (child && child != first) {
var nextNode = child.nextSibling;
node.removeChild(child);
child = nextNode;
}
},

_matchesContentSelect: function(node, contentElement) {
var select = contentElement.getAttribute('select');
// no selector matches all nodes (including text)
if (!select) return true;
select = select.trim();
// same thing if it had only whitespace
if (!select) return true;
// selectors can only match Elements
if (!(node instanceof Element)) return false;
// only valid selectors can match:
// TypeSelector
// *
// ClassSelector
// IDSelector
// AttributeSelector
// negation
var validSelectors = /^(:not\()?[*.#[a-zA-Z_|]/;
if (!validSelectors.test(select)) return false;
try {
return this.elementMatches(select, node);
} catch (ex) {
// Invalid selector.
return false;
}
},
});

function distributeNodeInto(child, insertionPoint) {
insertionPoint._distributedNodes.push(child);
var points = child._destinationInsertionPoints;
if (!points) {
child._destinationInsertionPoints = [insertionPoint];
} else {
points.push(insertionPoint);
}
}

function isFinalDestination(insertionPoint, node) {
var points = node._destinationInsertionPoints;
return points && points[points.length - 1] === insertionPoint;
}

function isInsertionPoint(node) {
// TODO(jmesserly): we could add back 'shadow' support here.
return node.localName == 'content';
}

function getLightChildren(node) {
var children = node.lightChildren;
return children ? children : node.childNodes;
}

function insertBefore(parentNode, newChild, refChild) {
// remove child from its old parent first
remove(newChild);
// make sure we never lose logical DOM information:
// if the parentNode doesn't have lightChildren, save that information now.
saveLightChildrenIfNeeded(parentNode);
// insert it into the real DOM
parentNode.insertBefore(newChild, refChild);
}

function remove(node) {
var parentNode = node.parentNode;
if (!parentNode) return;
// make sure we never lose logical DOM information:
// if the parentNode doesn't have lightChildren, save that information now.
saveLightChildrenIfNeeded(parentNode);
// remove it from the real DOM
parentNode.removeChild(node);
}

function saveLightChildrenIfNeeded(node) {
// Capture the list of light children. It's important to do this before we
// start transforming the DOM into "rendered" state.
//
// Children may be added to this list dynamically. It will be treated as the
// source of truth for the light children of the element. This element's
// actual children will be treated as the rendered state once lightChildren
// is populated.
if (!node.lightChildren) {
var children = [];
for (var child = node.firstChild; child; child = child.nextSibling) {
children.push(child);
child.lightParent = node;
}
node.lightChildren = children;
}
}

var proto = Element.prototype;
var matchesSelector = proto.matches || proto.matchesSelector ||
proto.mozMatchesSelector || proto.msMatchesSelector ||
proto.oMatchesSelector || proto.webkitMatchesSelector;

})();
</script>
5 changes: 5 additions & 0 deletions src/features/ready.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@

_ready: function() {
this._readied = true;
// TODO(jmesserly): this is a hook to allow content.html to be called
// before "ready". This needs to be factored better.
if (this._useContent) {
this.distributeContent();
}
this.ready();
},

Expand Down
1 change: 1 addition & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
WCT.loadSuites([
'unit/base.html',
'unit/ready.html',
'unit/content.html'
]);
</script>
</body>
Expand Down
Loading