Last active
August 29, 2015 14:01
-
-
Save jcmoore/3432b06621e3c3ccd39c to your computer and use it in GitHub Desktop.
polymer shadow DOM missed events
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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