tools/third_party/webcomponents/customelements.js

598 lines
18 KiB
JavaScript

/**
* @license
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* 2.3
* http://w3c.github.io/webcomponents/spec/custom/#dfn-element-definition
* @typedef {{
* name: string,
* localName: string,
* constructor: Function,
* connectedCallback: Function,
* disconnectedCallback: Function,
* attributeChangedCallback: Function,
* observedAttributes: Array<string>,
* }}
*/
var CustomElementDefinition;
(function() {
'use strict';
var doc = document;
var win = window;
// name validation
// https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name
/**
* @const
* @type {Array<string>}
*/
var reservedTagList = [
'annotation-xml',
'color-profile',
'font-face',
'font-face-src',
'font-face-uri',
'font-face-format',
'font-face-name',
'missing-glyph',
];
/** @const */
var customNameValidation = /^[a-z][.0-9_a-z]*-[\-.0-9_a-z]*$/;
function isValidCustomElementName(name) {
return customNameValidation.test(name) && reservedTagList.indexOf(name) === -1;
}
function createTreeWalker(root) {
// IE 11 requires the third and fourth arguments be present. If the third
// arg is null, it applies the default behaviour. However IE also requires
// the fourth argument be present even though the other browsers ignore it.
return doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
}
function isElement(node) {
return node.nodeType === Node.ELEMENT_NODE
}
/**
* A registry of custom element definitions.
*
* See https://html.spec.whatwg.org/multipage/scripting.html#customelementsregistry
*
* @constructor
* @property {boolean} polyfilled Whether this registry is polyfilled
* @property {boolean} enableFlush Set to true to enable the flush() method
* to work. This should only be done for tests, as it causes a memory leak.
*/
function CustomElementsRegistry() {
/** @private {Map<string, CustomElementDefinition>} **/
this._definitions = new Map();
/** @private {Map<Function, CustomElementDefinition>} **/
this._constructors = new Map();
this._whenDefinedMap = new Map();
/** @private {Set<MutationObserver>} **/
this._observers = new Set();
/** @private {MutationObserver} **/
this._attributeObserver =
new MutationObserver(this._handleAttributeChange.bind(this));
/** @private {HTMLElement} **/
this._newInstance = null;
this.polyfilled = true;
this.enableFlush = false;
this._observeRoot(document);
}
CustomElementsRegistry.prototype = {
// HTML spec part 4.13.4
// https://html.spec.whatwg.org/multipage/scripting.html#dom-customelementsregistry-define
define: function(name, constructor, options) {
name = name.toString().toLowerCase();
// 1:
if (typeof constructor !== 'function') {
throw new TypeError('constructor must be a Constructor');
}
// 2. If constructor is an interface object whose corresponding interface
// either is HTMLElement or has HTMLElement in its set of inherited
// interfaces, throw a TypeError and abort these steps.
//
// It doesn't appear possible to check this condition from script
// 3:
if (!isValidCustomElementName(name)) {
throw new SyntaxError(`The element name '${name}' is not valid.`);
}
// 4, 5:
// Note: we don't track being-defined names and constructors because
// define() isn't normally reentrant. The only time user code can run
// during define() is when getting callbacks off the prototype, which
// would be highly-unusual. We can make define() reentrant-safe if needed.
if (this._definitions.has(name)) {
throw new Error(`An element with name '${name}' is already defined`);
}
// 6, 7:
if (this._constructors.has(constructor)) {
throw new Error(`Definition failed for '${name}': ` +
`The constructor is already used.`);
}
// 8:
var localName = name;
// 9, 10: We do not support extends currently.
// 11, 12, 13: Our define() isn't rentrant-safe
// 14.1:
var prototype = constructor.prototype;
// 14.2:
if (typeof prototype !== 'object') {
throw new TypeError(`Definition failed for '${name}': ` +
`constructor.prototype must be an object`);
}
function getCallback(calllbackName) {
var callback = prototype[calllbackName];
if (callback !== undefined && typeof callback !== 'function') {
throw new Error(`${localName} '${calllbackName}' is not a Function`);
}
return callback;
}
// 3, 4:
var connectedCallback = getCallback('connectedCallback');
// 5, 6:
var disconnectedCallback = getCallback('disconnectedCallback');
// Divergence from spec: we always throw if attributeChangedCallback is
// not a function, and always get observedAttributes.
// 7, 9.1:
var attributeChangedCallback = getCallback('attributeChangedCallback');
// 8, 9.2, 9.3:
var observedAttributes = constructor['observedAttributes'] || [];
// 15:
// @type {CustomElementDefinition}
var definition = {
name: name,
localName: localName,
constructor: constructor,
connectedCallback: connectedCallback,
disconnectedCallback: disconnectedCallback,
attributeChangedCallback: attributeChangedCallback,
observedAttributes: observedAttributes,
};
// 16:
this._definitions.set(localName, definition);
this._constructors.set(constructor, localName);
// 17, 18, 19:
this._addNodes(doc.childNodes);
// 20:
var deferred = this._whenDefinedMap.get(localName);
if (deferred) {
deferred.resolve(undefined);
this._whenDefinedMap.delete(localName);
}
},
/**
* Returns the constructor defined for `name`, or `null`.
*
* @param {string} name
* @return {Function|undefined}
*/
get: function(name) {
// https://html.spec.whatwg.org/multipage/scripting.html#custom-elements-api
var def = this._definitions.get(name);
return def ? def.constructor : undefined;
},
/**
* Returns a `Promise` that resolves when a custom element for `name` has
* been defined.
*
* @param {string} name
* @return {Promise}
*/
whenDefined: function(name) {
// https://html.spec.whatwg.org/multipage/scripting.html#dom-customelementsregistry-whendefined
if (!customNameValidation.test(name)) {
return Promise.reject(
new SyntaxError(`The element name '${name}' is not valid.`));
}
if (this._definitions.has(name)) {
return Promise.resolve();
}
var deferred = {
promise: null,
};
deferred.promise = new Promise(function(resolve, _) {
deferred.resolve = resolve;
});
this._whenDefinedMap.set(name, deferred);
return deferred.promise;
},
/**
* Causes all pending mutation records to be processed, and thus all
* customization, upgrades and custom element reactions to be called.
* `enableFlush` must be true for this to work. Only use during tests!
*/
flush: function() {
if (this.enableFlush) {
console.warn("flush!!!");
this._observers.forEach(function(observer) {
this._handleMutations(observer.takeRecords());
}, this);
}
},
_setNewInstance: function(instance) {
this._newInstance = instance;
},
/**
* Observes a DOM root for mutations that trigger upgrades and reactions.
* @private
*/
_observeRoot: function(root) {
root.__observer = new MutationObserver(this._handleMutations.bind(this));
root.__observer.observe(root, {childList: true, subtree: true});
if (this.enableFlush) {
// this is memory leak, only use in tests
this._observers.add(root.__observer);
}
},
/**
* @private
*/
_unobserveRoot: function(root) {
if (root.__observer) {
root.__observer.disconnect();
root.__observer = null;
if (this.enableFlush) {
this._observers.delete(root.__observer);
}
}
},
/**
* @private
*/
_handleMutations: function(mutations) {
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
if (mutation.type === 'childList') {
// Note: we can't get an ordering between additions and removals, and
// so might diverge from spec reaction ordering
this._addNodes(mutation.addedNodes);
this._removeNodes(mutation.removedNodes);
}
}
},
/**
* @param {NodeList} nodeList
* @private
*/
_addNodes: function(nodeList) {
for (var i = 0; i < nodeList.length; i++) {
var root = nodeList[i];
if (!isElement(root)) {
continue;
}
// Since we're adding this node to an observed tree, we can unobserve
this._unobserveRoot(root);
var walker = createTreeWalker(root);
do {
var node = /** @type {HTMLElement} */ (walker.currentNode);
var definition = this._definitions.get(node.localName);
if (definition) {
if (!node.__upgraded) {
this._upgradeElement(node, definition, true);
}
if (node.__upgraded && !node.__attached) {
node.__attached = true;
if (definition && definition.connectedCallback) {
definition.connectedCallback.call(node);
}
}
}
if (node.shadowRoot) {
// TODO(justinfagnani): do we need to check that the shadowRoot
// is observed?
this._addNodes(node.shadowRoot.childNodes);
}
if (node.tagName === 'LINK') {
var onLoad = (function() {
var link = node;
return function() {
link.removeEventListener('load', onLoad);
this._observeRoot(link.import);
this._addNodes(link.import.childNodes);
}.bind(this);
}).bind(this)();
if (node.import) {
onLoad();
} else {
node.addEventListener('load', onLoad);
}
}
} while (walker.nextNode())
}
},
/**
* @param {NodeList} nodeList
* @private
*/
_removeNodes: function(nodeList) {
for (var i = 0; i < nodeList.length; i++) {
var root = nodeList[i];
if (!isElement(root)) {
continue;
}
// Since we're detatching this element from an observed root, we need to
// reobserve it.
// TODO(justinfagnani): can we do this in a microtask so we don't thrash
// on creating and destroying MutationObservers on batch DOM mutations?
this._observeRoot(root);
var walker = createTreeWalker(root);
do {
var node = walker.currentNode;
if (node.__upgraded && node.__attached) {
node.__attached = false;
var definition = this._definitions.get(node.localName);
if (definition && definition.disconnectedCallback) {
definition.disconnectedCallback.call(node);
}
}
} while (walker.nextNode())
}
},
/**
* Upgrades or customizes a custom element.
*
* @param {HTMLElement} element
* @param {CustomElementDefinition} definition
* @param {boolean} callConstructor
* @private
*/
_upgradeElement: function(element, definition, callConstructor) {
var prototype = definition.constructor.prototype;
element.__proto__ = prototype;
if (callConstructor) {
this._setNewInstance(element);
element.__upgraded = true;
new (definition.constructor)();
console.assert(this._newInstance == null);
}
var observedAttributes = definition.observedAttributes;
if (definition.attributeChangedCallback && observedAttributes.length > 0) {
this._attributeObserver.observe(element, {
attributes: true,
attributeOldValue: true,
attributeFilter: observedAttributes,
});
// Trigger attributeChangedCallback for existing attributes.
// https://html.spec.whatwg.org/multipage/scripting.html#upgrades
for (var i = 0; i < observedAttributes.length; i++) {
var name = observedAttributes[i];
if (element.hasAttribute(name)) {
var value = element.getAttribute(name);
element.attributeChangedCallback(name, null, value);
}
}
}
},
/**
* @private
*/
_handleAttributeChange: function(mutations) {
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
if (mutation.type === 'attributes') {
var name = mutation.attributeName;
var oldValue = mutation.oldValue;
var target = mutation.target;
var newValue = target.getAttribute(name);
var namespace = mutation.attributeNamespace;
target['attributeChangedCallback'](name, oldValue, newValue, namespace);
}
}
},
}
// Closure Compiler Exports
window['CustomElementsRegistry'] = CustomElementsRegistry;
CustomElementsRegistry.prototype['define'] = CustomElementsRegistry.prototype.define;
CustomElementsRegistry.prototype['get'] = CustomElementsRegistry.prototype.get;
CustomElementsRegistry.prototype['whenDefined'] = CustomElementsRegistry.prototype.whenDefined;
CustomElementsRegistry.prototype['flush'] = CustomElementsRegistry.prototype.flush;
CustomElementsRegistry.prototype['polyfilled'] = CustomElementsRegistry.prototype.polyfilled;
CustomElementsRegistry.prototype['enableFlush'] = CustomElementsRegistry.prototype.enableFlush;
// patch window.HTMLElement
var origHTMLElement = win.HTMLElement;
win.HTMLElement = function HTMLElement() {
var customElements = win['customElements'];
if (customElements._newInstance) {
var i = customElements._newInstance;
customElements._newInstance = null;
return i;
}
if (this.constructor) {
var tagName = customElements._constructors.get(this.constructor);
return doc._createElement(tagName, false);
}
throw new Error('unknown constructor. Did you call customElements.define()?');
}
win.HTMLElement.prototype = Object.create(origHTMLElement.prototype);
Object.defineProperty(win.HTMLElement.prototype, 'constructor', {value: win.HTMLElement});
// patch all built-in subclasses of HTMLElement to inherit from the new HTMLElement
// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
/** @const */
var htmlElementSubclasses = [
'Button',
'Canvas',
'Data',
'Head',
'Mod',
'TableCell',
'TableCol',
'Anchor',
'Area',
'Base',
'Body',
'BR',
'DataList',
'Details',
'Dialog',
'Div',
'DList',
'Embed',
'FieldSet',
'Form',
'Heading',
'HR',
'Html',
'IFrame',
'Image',
'Input',
'Keygen',
'Label',
'Legend',
'LI',
'Link',
'Map',
'Media',
'Menu',
'MenuItem',
'Meta',
'Meter',
'Object',
'OList',
'OptGroup',
'Option',
'Output',
'Paragraph',
'Param',
'Picture',
'Pre',
'Progress',
'Quote',
'Script',
'Select',
'Slot',
'Source',
'Span',
'Style',
'TableCaption',
'Table',
'TableRow',
'TableSection',
'Template',
'TextArea',
'Time',
'Title',
'Track',
'UList',
'Unknown',
];
for (var i = 0; i < htmlElementSubclasses.length; i++) {
var ctor = window['HTML' + htmlElementSubclasses[i] + 'Element'];
if (ctor) {
ctor.prototype.__proto__ = win.HTMLElement.prototype;
}
}
// patch doc.createElement
var rawCreateElement = doc.createElement;
doc._createElement = function(tagName, callConstructor) {
var customElements = win['customElements'];
var element = rawCreateElement.call(doc, tagName);
var definition = customElements._definitions.get(tagName.toLowerCase());
if (definition) {
customElements._upgradeElement(element, definition, callConstructor);
}
customElements._observeRoot(element);
return element;
};
doc.createElement = function(tagName) {
return doc._createElement(tagName, true);
}
// patch doc.createElementNS
var HTMLNS = 'http://www.w3.org/1999/xhtml';
var _origCreateElementNS = doc.createElementNS;
doc.createElementNS = function(namespaceURI, qualifiedName) {
if (namespaceURI === 'http://www.w3.org/1999/xhtml') {
return doc.createElement(qualifiedName);
} else {
return _origCreateElementNS.call(document, namespaceURI, qualifiedName);
}
};
// patch Element.attachShadow
var _origAttachShadow = Element.prototype['attachShadow'];
if (_origAttachShadow) {
Object.defineProperty(Element.prototype, 'attachShadow', {
value: function(options) {
var root = _origAttachShadow.call(this, options);
var customElements = win['customElements'];
customElements._observeRoot(root);
return root;
},
});
}
/** @type {CustomElementsRegistry} */
window['customElements'] = new CustomElementsRegistry();
})();