@@ -7,58 +7,313 @@
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 ( {
// TODO(sjmiles): ad-hoc signal for `ShadowDOM-lite-enhanced` nodes
// TODO(sjmiles): ad-hoc signal for `ShadowDOM-lite-enhanced` nodes
isHost : true ,
register : function ( prototype ) {
var t = prototype . _template ;
// TODO(sorvell): is qsa is wrong here due to distribution?
// TODO(sjmiles): No element should ever actually stamp a <content> node
// TODO(sjmiles): No element should ever actually stamp a <content> node
// into it's composed tree, so I believe this is actually correct.
// However, I wonder if it's more efficient to capture during annotation
// However, I wonder if it's more efficient to capture during annotation
// parsing, since the parse step does a tree walk in any case, and the
// tree is smaller before element expansion.
prototype . _useContent = prototype . _useContent ||
Boolean ( t && t . content . querySelector ( 'content' ) ) ;
prototype . _useContent = Boolean ( t && t . content . querySelector ( 'content' ) ) ;
} ,
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 >