Files
webcomponentsjs/src/CustomElements/register.js
Peter Burns 4346c37fec Rationalize our isIE variables.
In CustomElements we were setting scope.isIE11OrOlder in boot.js rather than
base.js, such that it was undefined in register.js

Also switched to a more consistent naming scheme. isIE matches IE and
isIEOrEdge matches IE or Edge.

Fixes https://github.com/Polymer/polymer/issues/2310
2015-08-20 14:50:55 -07:00

387 lines
13 KiB
JavaScript

/**
* @license
* Copyright (c) 2014 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
*/
/**
* Implements `document.registerElement`
* @module register
*/
/**
* Polyfilled extensions to the `document` object.
* @class Document
*/
window.CustomElements.addModule(function(scope) {
// imports
var isIE = scope.isIE;
var upgradeDocumentTree = scope.upgradeDocumentTree;
var upgradeAll = scope.upgradeAll;
var upgradeWithDefinition = scope.upgradeWithDefinition;
var implementPrototype = scope.implementPrototype;
var useNative = scope.useNative;
/**
* 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) {
throw new Error('document.registerElement: first argument `name` must not be empty');
}
if (name.indexOf('-') < 0) {
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');
}
// prototype is optional, default to an extension of HTMLElement
if (!definition.prototype) {
definition.prototype = Object.create(HTMLElement.prototype);
}
// 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
upgradeDocumentTree(document);
}
return definition.ctor;
}
// 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) {
name = name.toLowerCase();
var oldValue = this.getAttribute(name);
operation.apply(this, arguments);
var newValue = this.getAttribute(name);
if (this.attributeChangedCallback &&
(newValue !== oldValue)) {
this.attributeChangedCallback(name, oldValue, newValue);
}
}
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;
var foundPrototype = false;
while (proto) {
if (proto == nativePrototype) {
foundPrototype = true;
}
ancestor = Object.getPrototypeOf(proto);
if (ancestor) {
proto.__proto__ = ancestor;
}
proto = ancestor;
}
if (!foundPrototype) {
// Note the spec actually allows this, but it results in broken elements
// and is difficult to polyfill correctly, so we throw
console.warn(definition.tag + ' prototype not found in prototype chain for ' +
definition.is);
}
// 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 upgradeWithDefinition(domCreateElement(definition.tag), definition);
}
// 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
if (tag) {
tag = tag.toLowerCase();
}
if (typeExtension) {
typeExtension = typeExtension.toLowerCase();
}
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();
}
}
var element;
if (typeExtension) {
element = createElement(tag);
element.setAttribute('is', typeExtension);
return element;
}
element = domCreateElement(tag);
// Custom tags should be HTMLElements even if not upgraded.
if (tag.indexOf('-') >= 0) {
implementPrototype(element, HTMLElement);
}
return element;
}
// capture native createElement before we override it
var domCreateElement = document.createElement.bind(document);
var domCreateElementNS = document.createElementNS.bind(document);
// 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) {
// Allows instanceof(<div>, HTMLElement.prototype) to work
if (obj instanceof ctor) {
return true;
}
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;
};
}
// wrap a dom object method that works on nodes such that it forces upgrade
function wrapDomMethodToForceUpgrade(obj, methodName) {
var orig = obj[methodName];
obj[methodName] = function() {
var n = orig.apply(this, arguments);
upgradeAll(n);
return n;
};
}
wrapDomMethodToForceUpgrade(Node.prototype, 'cloneNode');
wrapDomMethodToForceUpgrade(document, 'importNode');
// Patch document.importNode to work around IE11 bug that
// casues children of a document fragment imported while
// there is a mutation observer to not have a parentNode (!?!)
if (isIE) {
(function() {
var importNode = document.importNode;
document.importNode = function() {
var n = importNode.apply(document, arguments);
// Copy all children to a new document fragment since
// this one may be broken
if (n.nodeType == n.DOCUMENT_FRAGMENT_NODE) {
var f = document.createDocumentFragment();
f.appendChild(n);
return f;
} else {
return n;
}
};
})();
}
// exports
document.registerElement = register;
document.createElement = createElement; // override
document.createElementNS = createElementNS; // override
scope.registry = registry;
scope.instanceof = isInstance;
scope.reservedTagList = reservedTagList;
scope.getRegisteredDefinition = getRegisteredDefinition;
// bc
document.register = document.registerElement;
});