Skip to content

Instantly share code, notes, and snippets.

@jcmoore
Last active August 29, 2015 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcmoore/3432b06621e3c3ccd39c to your computer and use it in GitHub Desktop.
Save jcmoore/3432b06621e3c3ccd39c to your computer and use it in GitHub Desktop.
polymer shadow DOM missed events
/*
* Copyright 2013 The Polymer Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/**
* Implements `document.register`
* @module CustomElements
*/
/**
* Polyfilled extensions to the `document` object.
* @class Document
*/
(function(scope) {
// imports
if (!scope) {
scope = window.CustomElements = {flags:{}};
}
var flags = scope.flags;
// native document.registerElement?
var hasNative = Boolean(document.registerElement);
// TODO(sorvell): See https://github.com/Polymer/polymer/issues/399
// we'll address this by defaulting to CE polyfill in the presence of the SD
// polyfill. This will avoid spamming excess attached/detached callbacks.
// If there is a compelling need to run CE native with SD polyfill,
// we'll need to fix this issue.
var useNative = !flags.register && hasNative && !window.ShadowDOMPolyfill;
if (useNative) {
// stub
var nop = function() {};
// exports
scope.registry = {};
scope.upgradeElement = nop;
scope.watchShadow = nop;
scope.upgrade = nop;
scope.upgradeAll = nop;
scope.upgradeSubtree = nop;
scope.observeDocument = nop;
scope.upgradeDocument = nop;
scope.upgradeDocumentTree = nop;
scope.takeRecords = nop;
scope.reservedTagList = [];
} else {
/**
* Registers a custom tag name with the document.
*
* When a registered element is created, a `readyCallback` method is called
* in the scope of the element. The `readyCallback` method can be specified on
* either `options.prototype` or `options.lifecycle` with the latter taking
* precedence.
*
* @method register
* @param {String} name The tag name to register. Must include a dash ('-'),
* for example 'x-component'.
* @param {Object} options
* @param {String} [options.extends]
* (_off spec_) Tag name of an element to extend (or blank for a new
* element). This parameter is not part of the specification, but instead
* is a hint for the polyfill because the extendee is difficult to infer.
* Remember that the input prototype must chain to the extended element's
* prototype (or HTMLElement.prototype) regardless of the value of
* `extends`.
* @param {Object} options.prototype The prototype to use for the new
* element. The prototype must inherit from HTMLElement.
* @param {Object} [options.lifecycle]
* Callbacks that fire at important phases in the life of the custom
* element.
*
* @example
* FancyButton = document.registerElement("fancy-button", {
* extends: 'button',
* prototype: Object.create(HTMLButtonElement.prototype, {
* readyCallback: {
* value: function() {
* console.log("a fancy-button was created",
* }
* }
* })
* });
* @return {Function} Constructor for the newly registered type.
*/
function register(name, options) {
//console.warn('document.registerElement("' + name + '", ', options, ')');
// construct a defintion out of options
// TODO(sjmiles): probably should clone options instead of mutating it
var definition = options || {};
if (!name) {
// TODO(sjmiles): replace with more appropriate error (EricB can probably
// offer guidance)
throw new Error('document.registerElement: first argument `name` must not be empty');
}
if (name.indexOf('-') < 0) {
// TODO(sjmiles): replace with more appropriate error (EricB can probably
// offer guidance)
throw new Error('document.registerElement: first argument (\'name\') must contain a dash (\'-\'). Argument provided was \'' + String(name) + '\'.');
}
// prevent registering reserved names
if (isReservedTag(name)) {
throw new Error('Failed to execute \'registerElement\' on \'Document\': Registration failed for type \'' + String(name) + '\'. The type name is invalid.');
}
// elements may only be registered once
if (getRegisteredDefinition(name)) {
throw new Error('DuplicateDefinitionError: a type with name \'' + String(name) + '\' is already registered');
}
// must have a prototype, default to an extension of HTMLElement
// TODO(sjmiles): probably should throw if no prototype, check spec
if (!definition.prototype) {
// TODO(sjmiles): replace with more appropriate error (EricB can probably
// offer guidance)
throw new Error('Options missing required prototype property');
}
// record name
definition.__name = name.toLowerCase();
// ensure a lifecycle object so we don't have to null test it
definition.lifecycle = definition.lifecycle || {};
// build a list of ancestral custom elements (for native base detection)
// TODO(sjmiles): we used to need to store this, but current code only
// uses it in 'resolveTagName': it should probably be inlined
definition.ancestry = ancestry(definition.extends);
// extensions of native specializations of HTMLElement require localName
// to remain native, and use secondary 'is' specifier for extension type
resolveTagName(definition);
// some platforms require modifications to the user-supplied prototype
// chain
resolvePrototypeChain(definition);
// overrides to implement attributeChanged callback
overrideAttributeApi(definition.prototype);
// 7.1.5: Register the DEFINITION with DOCUMENT
registerDefinition(definition.__name, definition);
// 7.1.7. Run custom element constructor generation algorithm with PROTOTYPE
// 7.1.8. Return the output of the previous step.
definition.ctor = generateConstructor(definition);
definition.ctor.prototype = definition.prototype;
// force our .constructor to be our actual constructor
definition.prototype.constructor = definition.ctor;
// if initial parsing is complete
if (scope.ready) {
// upgrade any pre-existing nodes of this type
scope.upgradeDocumentTree(document);
}
return definition.ctor;
}
function isReservedTag(name) {
for (var i = 0; i < reservedTagList.length; i++) {
if (name === reservedTagList[i]) {
return true;
}
}
}
var reservedTagList = [
'annotation-xml', 'color-profile', 'font-face', 'font-face-src',
'font-face-uri', 'font-face-format', 'font-face-name', 'missing-glyph'
];
function ancestry(extnds) {
var extendee = getRegisteredDefinition(extnds);
if (extendee) {
return ancestry(extendee.extends).concat([extendee]);
}
return [];
}
function resolveTagName(definition) {
// if we are explicitly extending something, that thing is our
// baseTag, unless it represents a custom component
var baseTag = definition.extends;
// if our ancestry includes custom components, we only have a
// baseTag if one of them does
for (var i=0, a; (a=definition.ancestry[i]); i++) {
baseTag = a.is && a.tag;
}
// our tag is our baseTag, if it exists, and otherwise just our name
definition.tag = baseTag || definition.__name;
if (baseTag) {
// if there is a base tag, use secondary 'is' specifier
definition.is = definition.__name;
}
}
function resolvePrototypeChain(definition) {
// if we don't support __proto__ we need to locate the native level
// prototype for precise mixing in
if (!Object.__proto__) {
// default prototype
var nativePrototype = HTMLElement.prototype;
// work out prototype when using type-extension
if (definition.is) {
var inst = document.createElement(definition.tag);
nativePrototype = Object.getPrototypeOf(inst);
}
// ensure __proto__ reference is installed at each point on the prototype
// chain.
// NOTE: On platforms without __proto__, a mixin strategy is used instead
// of prototype swizzling. In this case, this generated __proto__ provides
// limited support for prototype traversal.
var proto = definition.prototype, ancestor;
while (proto && (proto !== nativePrototype)) {
var ancestor = Object.getPrototypeOf(proto);
proto.__proto__ = ancestor;
proto = ancestor;
}
}
// cache this in case of mixin
definition.native = nativePrototype;
}
// SECTION 4
function instantiate(definition) {
// 4.a.1. Create a new object that implements PROTOTYPE
// 4.a.2. Let ELEMENT by this new object
//
// the custom element instantiation algorithm must also ensure that the
// output is a valid DOM element with the proper wrapper in place.
//
return upgrade(domCreateElement(definition.tag), definition);
}
function upgrade(element, definition) {
return upgradeWrapped.apply(this, arguments);
}
function upgradeWrapped(element, definition) {
var polyfill = window.ShadowDOMPolyfill;
if (!polyfill || !polyfill.forbidAllPending || !polyfill.permitAllPending) {
return upgradeUnwrapped.apply(this, arguments);
} else {
polyfill.forbidAllPending();
try {
return upgradeUnwrapped.apply(this, arguments);
} catch (error) {
throw error;
} finally {
polyfill.permitAllPending();
}
}
}
function upgradeUnwrapped(element, definition) {
// some definitions specify an 'is' attribute
if (definition.is) {
element.setAttribute('is', definition.is);
}
// remove 'unresolved' attr, which is a standin for :unresolved.
element.removeAttribute('unresolved');
// make 'element' implement definition.prototype
implement(element, definition);
// flag as upgraded
element.__upgraded__ = true;
// lifecycle management
created(element);
// attachedCallback fires in tree order, call before recursing
scope.insertedNode(element);
// there should never be a shadow root on element at this point
scope.upgradeSubtree(element);
// OUTPUT
return element;
}
function implement(element, definition) {
// prototype swizzling is best
if (Object.__proto__) {
element.__proto__ = definition.prototype;
} else {
// where above we can re-acquire inPrototype via
// getPrototypeOf(Element), we cannot do so when
// we use mixin, so we install a magic reference
customMixin(element, definition.prototype, definition.native);
element.__proto__ = definition.prototype;
}
}
function customMixin(inTarget, inSrc, inNative) {
// TODO(sjmiles): 'used' allows us to only copy the 'youngest' version of
// any property. This set should be precalculated. We also need to
// consider this for supporting 'super'.
var used = {};
// start with inSrc
var p = inSrc;
// The default is HTMLElement.prototype, so we add a test to avoid mixing in
// native prototypes
while (p !== inNative && p !== HTMLElement.prototype) {
var keys = Object.getOwnPropertyNames(p);
for (var i=0, k; k=keys[i]; i++) {
if (!used[k]) {
Object.defineProperty(inTarget, k,
Object.getOwnPropertyDescriptor(p, k));
used[k] = 1;
}
}
p = Object.getPrototypeOf(p);
}
}
function created(element) {
// invoke createdCallback
if (element.createdCallback) {
element.createdCallback();
}
}
// attribute watching
function overrideAttributeApi(prototype) {
// overrides to implement callbacks
// TODO(sjmiles): should support access via .attributes NamedNodeMap
// TODO(sjmiles): preserves user defined overrides, if any
if (prototype.setAttribute._polyfilled) {
return;
}
var setAttribute = prototype.setAttribute;
prototype.setAttribute = function(name, value) {
changeAttribute.call(this, name, value, setAttribute);
}
var removeAttribute = prototype.removeAttribute;
prototype.removeAttribute = function(name) {
changeAttribute.call(this, name, null, removeAttribute);
}
prototype.setAttribute._polyfilled = true;
}
// https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/
// index.html#dfn-attribute-changed-callback
function changeAttribute(name, value, operation) {
var oldValue = this.getAttribute(name);
operation.apply(this, arguments);
var newValue = this.getAttribute(name);
if (this.attributeChangedCallback
&& (newValue !== oldValue)) {
this.attributeChangedCallback(name, oldValue, newValue);
}
}
// element registry (maps tag names to definitions)
var registry = {};
function getRegisteredDefinition(name) {
if (name) {
return registry[name.toLowerCase()];
}
}
function registerDefinition(name, definition) {
registry[name] = definition;
}
function generateConstructor(definition) {
return function() {
return instantiate(definition);
};
}
var HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
function createElementNS(namespace, tag, typeExtension) {
// NOTE: we do not support non-HTML elements,
// just call createElementNS for non HTML Elements
if (namespace === HTML_NAMESPACE) {
return createElement(tag, typeExtension);
} else {
return domCreateElementNS(namespace, tag);
}
}
function createElement(tag, typeExtension) {
// TODO(sjmiles): ignore 'tag' when using 'typeExtension', we could
// error check it, or perhaps there should only ever be one argument
var definition = getRegisteredDefinition(typeExtension || tag);
if (definition) {
if (tag == definition.tag && typeExtension == definition.is) {
return new definition.ctor();
}
// Handle empty string for type extension.
if (!typeExtension && !definition.is) {
return new definition.ctor();
}
}
if (typeExtension) {
var element = createElement(tag);
element.setAttribute('is', typeExtension);
return element;
}
var element = domCreateElement(tag);
// Custom tags should be HTMLElements even if not upgraded.
if (tag.indexOf('-') >= 0) {
implement(element, HTMLElement);
}
return element;
}
function upgradeElement(element) {
if (!element.__upgraded__ && (element.nodeType === Node.ELEMENT_NODE)) {
var is = element.getAttribute('is');
var definition = getRegisteredDefinition(is || element.localName);
if (definition) {
if (is && definition.tag == element.localName) {
return upgrade(element, definition);
} else if (!is && !definition.extends) {
return upgrade(element, definition);
}
}
}
}
function cloneNode(deep) {
// call original clone
var n = domCloneNode.call(this, deep);
// upgrade the element and subtree
scope.upgradeAll(n);
// return the clone
return n;
}
// capture native createElement before we override it
var domCreateElement = document.createElement.bind(document);
var domCreateElementNS = document.createElementNS.bind(document);
// capture native cloneNode before we override it
var domCloneNode = Node.prototype.cloneNode;
// exports
document.registerElement = register;
document.createElement = createElement; // override
document.createElementNS = createElementNS; // override
Node.prototype.cloneNode = cloneNode; // override
scope.registry = registry;
/**
* Upgrade an element to a custom element. Upgrading an element
* causes the custom prototype to be applied, an `is` attribute
* to be attached (as needed), and invocation of the `readyCallback`.
* `upgrade` does nothing if the element is already upgraded, or
* if it matches no registered custom tag name.
*
* @method ugprade
* @param {Element} element The element to upgrade.
* @return {Element} The upgraded element.
*/
scope.upgrade = upgradeElement;
}
// Create a custom 'instanceof'. This is necessary when CustomElements
// are implemented via a mixin strategy, as for example on IE10.
var isInstance;
if (!Object.__proto__ && !useNative) {
isInstance = function(obj, ctor) {
var p = obj;
while (p) {
// NOTE: this is not technically correct since we're not checking if
// an object is an instance of a constructor; however, this should
// be good enough for the mixin strategy.
if (p === ctor.prototype) {
return true;
}
p = p.__proto__;
}
return false;
}
} else {
isInstance = function(obj, base) {
return obj instanceof base;
}
}
// exports
scope.instanceof = isInstance;
scope.reservedTagList = reservedTagList;
// bc
document.register = document.registerElement;
scope.hasNative = hasNative;
scope.useNative = useNative;
})(window.CustomElements);
<!DOCTYPE html>
<html>
<head>
<!-- problem exists in v0.2.4 but not v0.2.3 (change the versions below to see the difference) -->
<script language="javascript" src="//cdnjs.cloudflare.com/ajax/libs/polymer/0.2.4/platform.js"></script>
<script language="javascript" src="//cdnjs.cloudflare.com/ajax/libs/polymer/0.2.4/polymer.js"></script>
<polymer-element name="mm-list-item"> <!-- <A/> -->
<template>
<content></content>
</template>
<script>
Polymer('mm-list-item', {
somethingChanged: function() {
},
get something() {
// preventing the problematic behavior can be accomplished by
// uncommenting the following if statement
// (i.e. forbidding read access of this.offsetWidth until after shadowRoot creation)
//if (this.shadowRoot)
{
var unused = this.offsetWidth;
getComputedStyle(this);
}
return 0;
}
});
</script>
</polymer-element>
<polymer-element name="mm-list"> <!-- <B/> -->
<template>
<content></content>
</template>
<script>
Polymer('mm-list', {
ready: function() {
this.addEventListener("down", this.selectItem.bind(this) );
},
selectItem: function(ev) {
// clicking on the "click here" text should pop up the following alert
alert("CLICKED!? " + ev.target.innerHTML);
},
});
</script>
</polymer-element>
<polymer-element name="mm-dropdown" noscript> <!-- <C/> -->
<template>
<!-- Inclusion of the following empty <link rel="stylesheet"/> prevents the issue from happening -->
<!-- (whether or not mm-list-item reads its this.offsetWidth value) -->
<!-- <link rel="stylesheet"/> -->
<mm-list id="list">
<content></content>
</mm-list>
</template>
</polymer-element>
</head>
<body>
<mm-dropdown>
<mm-list-item>click here</mm-list-item>
<mm-list-item>or here</mm-list-item>
</mm-dropdown>
</body>
</html>
// Copyright 2013 The Polymer Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
(function(scope) {
'use strict';
var Element = scope.wrappers.Element;
var HTMLContentElement = scope.wrappers.HTMLContentElement;
var HTMLShadowElement = scope.wrappers.HTMLShadowElement;
var Node = scope.wrappers.Node;
var ShadowRoot = scope.wrappers.ShadowRoot;
var assert = scope.assert;
var getTreeScope = scope.getTreeScope;
var mixin = scope.mixin;
var oneOf = scope.oneOf;
var unwrap = scope.unwrap;
var wrap = scope.wrap;
/**
* Updates the fields of a wrapper to a snapshot of the logical DOM as needed.
* Up means parentNode
* Sideways means previous and next sibling.
* @param {!Node} wrapper
*/
function updateWrapperUpAndSideways(wrapper) {
wrapper.previousSibling_ = wrapper.previousSibling;
wrapper.nextSibling_ = wrapper.nextSibling;
wrapper.parentNode_ = wrapper.parentNode;
}
/**
* Updates the fields of a wrapper to a snapshot of the logical DOM as needed.
* Down means first and last child
* @param {!Node} wrapper
*/
function updateWrapperDown(wrapper) {
wrapper.firstChild_ = wrapper.firstChild;
wrapper.lastChild_ = wrapper.lastChild;
}
function updateAllChildNodes(parentNodeWrapper) {
assert(parentNodeWrapper instanceof Node);
for (var childWrapper = parentNodeWrapper.firstChild;
childWrapper;
childWrapper = childWrapper.nextSibling) {
updateWrapperUpAndSideways(childWrapper);
}
updateWrapperDown(parentNodeWrapper);
}
function insertBefore(parentNodeWrapper, newChildWrapper, refChildWrapper) {
var parentNode = unwrap(parentNodeWrapper);
var newChild = unwrap(newChildWrapper);
var refChild = refChildWrapper ? unwrap(refChildWrapper) : null;
remove(newChildWrapper);
updateWrapperUpAndSideways(newChildWrapper);
if (!refChildWrapper) {
parentNodeWrapper.lastChild_ = parentNodeWrapper.lastChild;
if (parentNodeWrapper.lastChild === parentNodeWrapper.firstChild)
parentNodeWrapper.firstChild_ = parentNodeWrapper.firstChild;
var lastChildWrapper = wrap(parentNode.lastChild);
if (lastChildWrapper)
lastChildWrapper.nextSibling_ = lastChildWrapper.nextSibling;
} else {
if (parentNodeWrapper.firstChild === refChildWrapper)
parentNodeWrapper.firstChild_ = refChildWrapper;
refChildWrapper.previousSibling_ = refChildWrapper.previousSibling;
}
parentNode.insertBefore(newChild, refChild);
}
function remove(nodeWrapper) {
var node = unwrap(nodeWrapper)
var parentNode = node.parentNode;
if (!parentNode)
return;
var parentNodeWrapper = wrap(parentNode);
updateWrapperUpAndSideways(nodeWrapper);
if (nodeWrapper.previousSibling)
nodeWrapper.previousSibling.nextSibling_ = nodeWrapper;
if (nodeWrapper.nextSibling)
nodeWrapper.nextSibling.previousSibling_ = nodeWrapper;
if (parentNodeWrapper.lastChild === nodeWrapper)
parentNodeWrapper.lastChild_ = nodeWrapper;
if (parentNodeWrapper.firstChild === nodeWrapper)
parentNodeWrapper.firstChild_ = nodeWrapper;
parentNode.removeChild(node);
}
var distributedNodesTable = new WeakMap();
var destinationInsertionPointsTable = new WeakMap();
var rendererForHostTable = new WeakMap();
function resetDistributedNodes(insertionPoint) {
distributedNodesTable.set(insertionPoint, []);
}
function getDistributedNodes(insertionPoint) {
var rv = distributedNodesTable.get(insertionPoint);
if (!rv)
distributedNodesTable.set(insertionPoint, rv = []);
return rv;
}
function getChildNodesSnapshot(node) {
var result = [], i = 0;
for (var child = node.firstChild; child; child = child.nextSibling) {
result[i++] = child;
}
return result;
}
var request = oneOf(window, [
'requestAnimationFrame',
'mozRequestAnimationFrame',
'webkitRequestAnimationFrame',
'setTimeout'
]);
var pendingDirtyRenderers = [];
var renderTimer;
var pendingRenderersLocks = 0;
var requestedAllPending = false;
function forbidAllPending () {
pendingRenderersLocks++;
}
function permitAllPending () {
if (pendingRenderersLocks > 0) {
pendingRenderersLocks--;
if (pendingRenderersLocks === 0 && requestedAllPending === true) {
requestedAllPending = false;
renderAllPending();
}
}
}
scope.forbidAllPending = forbidAllPending;
scope.permitAllPending = permitAllPending;
function renderAllPending() {
if (pendingRenderersLocks > 0) {
requestedAllPending = true;
return;
}
// TODO(arv): Order these in document order. That way we do not have to
// render something twice.
for (var i = 0; i < pendingDirtyRenderers.length; i++) {
var renderer = pendingDirtyRenderers[i];
var parentRenderer = renderer.parentRenderer;
if (parentRenderer && parentRenderer.dirty)
continue;
renderer.render();
}
pendingDirtyRenderers = [];
}
function handleRequestAnimationFrame() {
renderTimer = null;
renderAllPending();
}
/**
* Returns existing shadow renderer for a host or creates it if it is needed.
* @params {!Element} host
* @return {!ShadowRenderer}
*/
function getRendererForHost(host) {
var renderer = rendererForHostTable.get(host);
if (!renderer) {
renderer = new ShadowRenderer(host);
rendererForHostTable.set(host, renderer);
}
return renderer;
}
function getShadowRootAncestor(node) {
var root = getTreeScope(node).root;
if (root instanceof ShadowRoot)
return root;
return null;
}
function getRendererForShadowRoot(shadowRoot) {
return getRendererForHost(shadowRoot.host);
}
var spliceDiff = new ArraySplice();
spliceDiff.equals = function(renderNode, rawNode) {
return unwrap(renderNode.node) === rawNode;
};
/**
* RenderNode is used as an in memory "render tree". When we render the
* composed tree we create a tree of RenderNodes, then we diff this against
* the real DOM tree and make minimal changes as needed.
*/
function RenderNode(node) {
this.skip = false;
this.node = node;
this.childNodes = [];
}
RenderNode.prototype = {
append: function(node) {
var rv = new RenderNode(node);
this.childNodes.push(rv);
return rv;
},
sync: function(opt_added) {
if (this.skip)
return;
var nodeWrapper = this.node;
// plain array of RenderNodes
var newChildren = this.childNodes;
// plain array of real nodes.
var oldChildren = getChildNodesSnapshot(unwrap(nodeWrapper));
var added = opt_added || new WeakMap();
var splices = spliceDiff.calculateSplices(newChildren, oldChildren);
var newIndex = 0, oldIndex = 0;
var lastIndex = 0;
for (var i = 0; i < splices.length; i++) {
var splice = splices[i];
for (; lastIndex < splice.index; lastIndex++) {
oldIndex++;
newChildren[newIndex++].sync(added);
}
var removedCount = splice.removed.length;
for (var j = 0; j < removedCount; j++) {
var wrapper = wrap(oldChildren[oldIndex++]);
if (!added.get(wrapper))
remove(wrapper);
}
var addedCount = splice.addedCount;
var refNode = oldChildren[oldIndex] && wrap(oldChildren[oldIndex]);
for (var j = 0; j < addedCount; j++) {
var newChildRenderNode = newChildren[newIndex++];
var newChildWrapper = newChildRenderNode.node;
insertBefore(nodeWrapper, newChildWrapper, refNode);
// Keep track of added so that we do not remove the node after it
// has been added.
added.set(newChildWrapper, true);
newChildRenderNode.sync(added);
}
lastIndex += addedCount;
}
for (var i = lastIndex; i < newChildren.length; i++) {
newChildren[i].sync(added);
}
}
};
function ShadowRenderer(host) {
this.host = host;
this.dirty = false;
this.invalidateAttributes();
this.associateNode(host);
}
ShadowRenderer.prototype = {
// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#rendering-shadow-trees
render: function(opt_renderNode) {
if (!this.dirty)
return;
this.invalidateAttributes();
var host = this.host;
this.distribution(host);
var renderNode = opt_renderNode || new RenderNode(host);
this.buildRenderTree(renderNode, host);
var topMostRenderer = !opt_renderNode;
if (topMostRenderer)
renderNode.sync();
this.dirty = false;
},
get parentRenderer() {
return getTreeScope(this.host).renderer;
},
invalidate: function() {
if (!this.dirty) {
this.dirty = true;
pendingDirtyRenderers.push(this);
if (renderTimer)
return;
renderTimer = window[request](handleRequestAnimationFrame, 0);
}
},
// http://w3c.github.io/webcomponents/spec/shadow/#distribution-algorithms
distribution: function(root) {
this.resetAll(root);
this.distributionResolution(root);
},
resetAll: function(node) {
if (isInsertionPoint(node))
resetDistributedNodes(node);
else
resetDestinationInsertionPoints(node);
for (var child = node.firstChild; child; child = child.nextSibling) {
this.resetAll(child);
}
if (node.shadowRoot)
this.resetAll(node.shadowRoot);
if (node.olderShadowRoot)
this.resetAll(node.olderShadowRoot);
},
// http://w3c.github.io/webcomponents/spec/shadow/#distribution-results
distributionResolution: function(node) {
if (isShadowHost(node)) {
var shadowHost = node;
// 1.1
var pool = poolPopulation(shadowHost);
var shadowTrees = getShadowTrees(shadowHost);
// 1.2
for (var i = 0; i < shadowTrees.length; i++) {
// 1.2.1
this.poolDistribution(shadowTrees[i], pool);
}
// 1.3
for (var i = shadowTrees.length - 1; i >= 0; i--) {
var shadowTree = shadowTrees[i];
// 1.3.1
// TODO(arv): We should keep the shadow insertion points on the
// shadow root (or renderer) so we don't have to search the tree
// every time.
var shadow = getShadowInsertionPoint(shadowTree);
// 1.3.2
if (shadow) {
// 1.3.2.1
var olderShadowRoot = shadowTree.olderShadowRoot;
if (olderShadowRoot) {
// 1.3.2.1.1
pool = poolPopulation(olderShadowRoot);
}
// 1.3.2.2
for (var j = 0; j < pool.length; j++) {
// 1.3.2.2.1
destributeNodeInto(pool[j], shadow);
}
}
// 1.3.3
this.distributionResolution(shadowTree);
}
}
for (var child = node.firstChild; child; child = child.nextSibling) {
this.distributionResolution(child);
}
},
// http://w3c.github.io/webcomponents/spec/shadow/#dfn-pool-distribution-algorithm
poolDistribution: function (node, pool) {
if (node instanceof HTMLShadowElement)
return;
if (node instanceof HTMLContentElement) {
var content = node;
this.updateDependentAttributes(content.getAttribute('select'));
var anyDistributed = false;
// 1.1
for (var i = 0; i < pool.length; i++) {
var node = pool[i];
if (!node)
continue;
if (matches(node, content)) {
destributeNodeInto(node, content);
pool[i] = undefined;
anyDistributed = true;
}
}
// 1.2
// Fallback content
if (!anyDistributed) {
for (var child = content.firstChild;
child;
child = child.nextSibling) {
destributeNodeInto(child, content);
}
}
return;
}
for (var child = node.firstChild; child; child = child.nextSibling) {
this.poolDistribution(child, pool);
}
},
buildRenderTree: function(renderNode, node) {
var children = this.compose(node);
for (var i = 0; i < children.length; i++) {
var child = children[i];
var childRenderNode = renderNode.append(child);
this.buildRenderTree(childRenderNode, child);
}
if (isShadowHost(node)) {
var renderer = getRendererForHost(node);
renderer.dirty = false;
}
},
compose: function(node) {
var children = [];
var p = node.shadowRoot || node;
for (var child = p.firstChild; child; child = child.nextSibling) {
if (isInsertionPoint(child)) {
this.associateNode(p);
var distributedNodes = getDistributedNodes(child);
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;
},
/**
* Invalidates the attributes used to keep track of which attributes may
* cause the renderer to be invalidated.
*/
invalidateAttributes: function() {
this.attributes = Object.create(null);
},
/**
* Parses the selector and makes this renderer dependent on the attribute
* being used in the selector.
* @param {string} selector
*/
updateDependentAttributes: function(selector) {
if (!selector)
return;
var attributes = this.attributes;
// .class
if (/\.\w+/.test(selector))
attributes['class'] = true;
// #id
if (/#\w+/.test(selector))
attributes['id'] = true;
selector.replace(/\[\s*([^\s=\|~\]]+)/g, function(_, name) {
attributes[name] = true;
});
// Pseudo selectors have been removed from the spec.
},
dependsOnAttribute: function(name) {
return this.attributes[name];
},
associateNode: function(node) {
node.impl.polymerShadowRenderer_ = this;
}
};
// http://w3c.github.io/webcomponents/spec/shadow/#dfn-pool-population-algorithm
function poolPopulation(node) {
var pool = [];
for (var child = node.firstChild; child; child = child.nextSibling) {
if (isInsertionPoint(child)) {
pool.push.apply(pool, getDistributedNodes(child));
} else {
pool.push(child);
}
}
return pool;
}
function getShadowInsertionPoint(node) {
if (node instanceof HTMLShadowElement)
return node;
if (node instanceof HTMLContentElement)
return null;
for (var child = node.firstChild; child; child = child.nextSibling) {
var res = getShadowInsertionPoint(child);
if (res)
return res;
}
return null;
}
function destributeNodeInto(child, insertionPoint) {
getDistributedNodes(insertionPoint).push(child);
var points = destinationInsertionPointsTable.get(child);
if (!points)
destinationInsertionPointsTable.set(child, [insertionPoint]);
else
points.push(insertionPoint);
}
function getDestinationInsertionPoints(node) {
return destinationInsertionPointsTable.get(node);
}
function resetDestinationInsertionPoints(node) {
// IE11 crashes when delete is used.
destinationInsertionPointsTable.set(node, undefined);
}
// AllowedSelectors :
// TypeSelector
// *
// ClassSelector
// IDSelector
// AttributeSelector
var selectorStartCharRe = /^[*.#[a-zA-Z_|]/;
function matches(node, contentElement) {
var select = contentElement.getAttribute('select');
if (!select)
return true;
// Here we know the select attribute is a non empty string.
select = select.trim();
if (!select)
return true;
if (!(node instanceof Element))
return false;
if (!selectorStartCharRe.test(select))
return false;
try {
return node.matches(select);
} catch (ex) {
// Invalid selector.
return false;
}
}
function isFinalDestination(insertionPoint, node) {
var points = getDestinationInsertionPoints(node);
return points && points[points.length - 1] === insertionPoint;
}
function isInsertionPoint(node) {
return node instanceof HTMLContentElement ||
node instanceof HTMLShadowElement;
}
function isShadowHost(shadowHost) {
return shadowHost.shadowRoot;
}
// Returns the shadow trees as an array, with the youngest tree at the
// beginning of the array.
function getShadowTrees(host) {
var trees = [];
for (var tree = host.shadowRoot; tree; tree = tree.olderShadowRoot) {
trees.push(tree);
}
return trees;
}
function render(host) {
new ShadowRenderer(host).render();
};
// Need to rerender shadow host when:
//
// - a direct child to the ShadowRoot is added or removed
// - a direct child to the host is added or removed
// - a new shadow root is created
// - a direct child to a content/shadow element is added or removed
// - a sibling to a content/shadow element is added or removed
// - content[select] is changed
// - an attribute in a direct child to a host is modified
/**
* This gets called when a node was added or removed to it.
*/
Node.prototype.invalidateShadowRenderer = function(force) {
var renderer = this.impl.polymerShadowRenderer_;
if (renderer) {
renderer.invalidate();
return true;
}
return false;
};
HTMLContentElement.prototype.getDistributedNodes =
HTMLShadowElement.prototype.getDistributedNodes = function() {
// TODO(arv): We should only rerender the dirty ancestor renderers (from
// the root and down).
renderAllPending();
return getDistributedNodes(this);
};
Element.prototype.getDestinationInsertionPoints = function() {
renderAllPending();
return getDestinationInsertionPoints(this) || [];
};
HTMLContentElement.prototype.nodeIsInserted_ =
HTMLShadowElement.prototype.nodeIsInserted_ = function() {
// Invalidate old renderer if any.
this.invalidateShadowRenderer();
var shadowRoot = getShadowRootAncestor(this);
var renderer;
if (shadowRoot)
renderer = getRendererForShadowRoot(shadowRoot);
this.impl.polymerShadowRenderer_ = renderer;
if (renderer)
renderer.invalidate();
};
scope.getRendererForHost = getRendererForHost;
scope.getShadowTrees = getShadowTrees;
scope.renderAllPending = renderAllPending;
scope.getDestinationInsertionPoints = getDestinationInsertionPoints;
// Exposed for testing
scope.visual = {
insertBefore: insertBefore,
remove: remove,
};
})(window.ShadowDOMPolyfill);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment