mirror of
https://github.com/Polymer/polymer.git
synced 2025-02-25 18:55:30 -06:00
542 lines
18 KiB
JavaScript
542 lines
18 KiB
JavaScript
/**
|
|
@license
|
|
Copyright (c) 2017 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
|
|
*/
|
|
|
|
import { LegacyElementMixin } from './legacy-element-mixin.js';
|
|
import { legacyOptimizations } from '../utils/settings.js';
|
|
|
|
const lifecycleProps = {
|
|
attached: true,
|
|
detached: true,
|
|
ready: true,
|
|
created: true,
|
|
beforeRegister: true,
|
|
registered: true,
|
|
attributeChanged: true,
|
|
listeners: true,
|
|
hostAttributes: true
|
|
};
|
|
|
|
const excludeOnInfo = {
|
|
attached: true,
|
|
detached: true,
|
|
ready: true,
|
|
created: true,
|
|
beforeRegister: true,
|
|
registered: true,
|
|
attributeChanged: true,
|
|
behaviors: true,
|
|
_noAccessors: true
|
|
};
|
|
|
|
const excludeOnBehaviors = Object.assign({
|
|
listeners: true,
|
|
hostAttributes: true,
|
|
properties: true,
|
|
observers: true,
|
|
}, excludeOnInfo);
|
|
|
|
function copyProperties(source, target, excludeProps) {
|
|
const noAccessors = source._noAccessors;
|
|
const propertyNames = Object.getOwnPropertyNames(source);
|
|
for (let i = 0; i < propertyNames.length; i++) {
|
|
let p = propertyNames[i];
|
|
if (p in excludeProps) {
|
|
continue;
|
|
}
|
|
if (noAccessors) {
|
|
target[p] = source[p];
|
|
} else {
|
|
let pd = Object.getOwnPropertyDescriptor(source, p);
|
|
if (pd) {
|
|
// ensure property is configurable so that a later behavior can
|
|
// re-configure it.
|
|
pd.configurable = true;
|
|
Object.defineProperty(target, p, pd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies a "legacy" behavior or array of behaviors to the provided class.
|
|
*
|
|
* Note: this method will automatically also apply the `LegacyElementMixin`
|
|
* to ensure that any legacy behaviors can rely on legacy Polymer API on
|
|
* the underlying element.
|
|
*
|
|
* @function
|
|
* @template T
|
|
* @param {!Object|!Array<!Object>} behaviors Behavior object or array of behaviors.
|
|
* @param {function(new:T)} klass Element class.
|
|
* @return {?} Returns a new Element class extended by the
|
|
* passed in `behaviors` and also by `LegacyElementMixin`.
|
|
* @suppress {invalidCasts, checkTypes}
|
|
*/
|
|
export function mixinBehaviors(behaviors, klass) {
|
|
return GenerateClassFromInfo({}, LegacyElementMixin(klass), behaviors);
|
|
}
|
|
|
|
// NOTE:
|
|
// 1.x
|
|
// Behaviors were mixed in *in reverse order* and de-duped on the fly.
|
|
// The rule was that behavior properties were copied onto the element
|
|
// prototype if and only if the property did not already exist.
|
|
// Given: Polymer{ behaviors: [A, B, C, A, B]}, property copy order was:
|
|
// (1), B, (2), A, (3) C. This means prototype properties win over
|
|
// B properties win over A win over C. This mirrors what would happen
|
|
// with inheritance if element extended B extended A extended C.
|
|
//
|
|
// Again given, Polymer{ behaviors: [A, B, C, A, B]}, the resulting
|
|
// `behaviors` array was [C, A, B].
|
|
// Behavior lifecycle methods were called in behavior array order
|
|
// followed by the element, e.g. (1) C.created, (2) A.created,
|
|
// (3) B.created, (4) element.created. There was no support for
|
|
// super, and "super-behavior" methods were callable only by name).
|
|
//
|
|
// 2.x
|
|
// Behaviors are made into proper mixins which live in the
|
|
// element's prototype chain. Behaviors are placed in the element prototype
|
|
// eldest to youngest and de-duped youngest to oldest:
|
|
// So, first [A, B, C, A, B] becomes [C, A, B] then,
|
|
// the element prototype becomes (oldest) (1) PolymerElement, (2) class(C),
|
|
// (3) class(A), (4) class(B), (5) class(Polymer({...})).
|
|
// Result:
|
|
// This means element properties win over B properties win over A win
|
|
// over C. (same as 1.x)
|
|
// If lifecycle is called (super then me), order is
|
|
// (1) C.created, (2) A.created, (3) B.created, (4) element.created
|
|
// (again same as 1.x)
|
|
function applyBehaviors(proto, behaviors, lifecycle) {
|
|
for (let i=0; i<behaviors.length; i++) {
|
|
applyInfo(proto, behaviors[i], lifecycle, excludeOnBehaviors);
|
|
}
|
|
}
|
|
|
|
function applyInfo(proto, info, lifecycle, excludeProps) {
|
|
copyProperties(info, proto, excludeProps);
|
|
for (let p in lifecycleProps) {
|
|
if (info[p]) {
|
|
lifecycle[p] = lifecycle[p] || [];
|
|
lifecycle[p].push(info[p]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Array} behaviors List of behaviors to flatten.
|
|
* @param {Array=} list Target list to flatten behaviors into.
|
|
* @param {Array=} exclude List of behaviors to exclude from the list.
|
|
* @return {!Array} Returns the list of flattened behaviors.
|
|
*/
|
|
function flattenBehaviors(behaviors, list, exclude) {
|
|
list = list || [];
|
|
for (let i=behaviors.length-1; i >= 0; i--) {
|
|
let b = behaviors[i];
|
|
if (b) {
|
|
if (Array.isArray(b)) {
|
|
flattenBehaviors(b, list);
|
|
} else {
|
|
// dedup
|
|
if (list.indexOf(b) < 0 && (!exclude || exclude.indexOf(b) < 0)) {
|
|
list.unshift(b);
|
|
}
|
|
}
|
|
} else {
|
|
console.warn('behavior is null, check for missing or 404 import');
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/**
|
|
* Copies property descriptors from source to target, overwriting all fields
|
|
* of any previous descriptor for a property *except* for `value`, which is
|
|
* merged in from the target if it does not exist on the source.
|
|
*
|
|
* @param {*} target Target properties object
|
|
* @param {*} source Source properties object
|
|
*/
|
|
function mergeProperties(target, source) {
|
|
for (const p in source) {
|
|
const targetInfo = target[p];
|
|
const sourceInfo = source[p];
|
|
if (!('value' in sourceInfo) && targetInfo && ('value' in targetInfo)) {
|
|
target[p] = Object.assign({value: targetInfo.value}, sourceInfo);
|
|
} else {
|
|
target[p] = sourceInfo;
|
|
}
|
|
}
|
|
}
|
|
|
|
const LegacyElement = LegacyElementMixin(HTMLElement);
|
|
|
|
/* Note about construction and extension of legacy classes.
|
|
[Changed in Q4 2018 to optimize performance.]
|
|
|
|
When calling `Polymer` or `mixinBehaviors`, the generated class below is
|
|
made. The list of behaviors was previously made into one generated class per
|
|
behavior, but this is no longer the case as behaviors are now called
|
|
manually. Note, there may *still* be multiple generated classes in the
|
|
element's prototype chain if extension is used with `mixinBehaviors`.
|
|
|
|
The generated class is directly tied to the info object and behaviors
|
|
used to create it. That list of behaviors is filtered so it's only the
|
|
behaviors not active on the superclass. In order to call through to the
|
|
entire list of lifecycle methods, it's important to call `super`.
|
|
|
|
The element's `properties` and `observers` are controlled via the finalization
|
|
mechanism provided by `PropertiesMixin`. `Properties` and `observers` are
|
|
collected by manually traversing the prototype chain and merging.
|
|
|
|
To limit changes, the `_registered` method is called via `_initializeProperties`
|
|
and not `_finalizeClass`.
|
|
|
|
*/
|
|
/**
|
|
* @param {!PolymerInit} info Polymer info object
|
|
* @param {function(new:HTMLElement)} Base base class to extend with info object
|
|
* @param {Object=} behaviors behaviors to copy into the element
|
|
* @return {function(new:HTMLElement)} Generated class
|
|
* @suppress {checkTypes}
|
|
* @private
|
|
*/
|
|
function GenerateClassFromInfo(info, Base, behaviors) {
|
|
|
|
// manages behavior and lifecycle processing (filled in after class definition)
|
|
let behaviorList;
|
|
const lifecycle = {};
|
|
|
|
/** @private */
|
|
class PolymerGenerated extends Base {
|
|
|
|
// explicitly not calling super._finalizeClass
|
|
/** @nocollapse */
|
|
static _finalizeClass() {
|
|
// if calling via a subclass that hasn't been generated, pass through to super
|
|
if (!this.hasOwnProperty(JSCompiler_renameProperty('generatedFrom', this))) {
|
|
// TODO(https://github.com/google/closure-compiler/issues/3240):
|
|
// Change back to just super.methodCall()
|
|
Base._finalizeClass.call(this);
|
|
} else {
|
|
// interleave properties and observers per behavior and `info`
|
|
if (behaviorList) {
|
|
for (let i=0, b; i < behaviorList.length; i++) {
|
|
b = behaviorList[i];
|
|
if (b.properties) {
|
|
this.createProperties(b.properties);
|
|
}
|
|
if (b.observers) {
|
|
this.createObservers(b.observers, b.properties);
|
|
}
|
|
}
|
|
}
|
|
if (info.properties) {
|
|
this.createProperties(info.properties);
|
|
}
|
|
if (info.observers) {
|
|
this.createObservers(info.observers, info.properties);
|
|
}
|
|
// make sure to prepare the element template
|
|
this._prepareTemplate();
|
|
}
|
|
}
|
|
|
|
/** @nocollapse */
|
|
static get properties() {
|
|
const properties = {};
|
|
if (behaviorList) {
|
|
for (let i=0; i < behaviorList.length; i++) {
|
|
mergeProperties(properties, behaviorList[i].properties);
|
|
}
|
|
}
|
|
mergeProperties(properties, info.properties);
|
|
return properties;
|
|
}
|
|
|
|
/** @nocollapse */
|
|
static get observers() {
|
|
let observers = [];
|
|
if (behaviorList) {
|
|
for (let i=0, b; i < behaviorList.length; i++) {
|
|
b = behaviorList[i];
|
|
if (b.observers) {
|
|
observers = observers.concat(b.observers);
|
|
}
|
|
}
|
|
}
|
|
if (info.observers) {
|
|
observers = observers.concat(info.observers);
|
|
}
|
|
return observers;
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
created() {
|
|
super.created();
|
|
const list = lifecycle.created;
|
|
if (list) {
|
|
for (let i=0; i < list.length; i++) {
|
|
list[i].call(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
_registered() {
|
|
/* NOTE: `beforeRegister` is called here for bc, but the behavior
|
|
is different than in 1.x. In 1.0, the method was called *after*
|
|
mixing prototypes together but *before* processing of meta-objects.
|
|
However, dynamic effects can still be set here and can be done either
|
|
in `beforeRegister` or `registered`. It is no longer possible to set
|
|
`is` in `beforeRegister` as you could in 1.x.
|
|
*/
|
|
// only proceed if the generated class' prototype has not been registered.
|
|
const generatedProto = PolymerGenerated.prototype;
|
|
if (!generatedProto.hasOwnProperty(JSCompiler_renameProperty('__hasRegisterFinished', generatedProto))) {
|
|
generatedProto.__hasRegisterFinished = true;
|
|
// ensure superclass is registered first.
|
|
super._registered();
|
|
// copy properties onto the generated class lazily if we're optimizing,
|
|
if (legacyOptimizations) {
|
|
copyPropertiesToProto(generatedProto);
|
|
}
|
|
// make sure legacy lifecycle is called on the *element*'s prototype
|
|
// and not the generated class prototype; if the element has been
|
|
// extended, these are *not* the same.
|
|
const proto = Object.getPrototypeOf(this);
|
|
let list = lifecycle.beforeRegister;
|
|
if (list) {
|
|
for (let i=0; i < list.length; i++) {
|
|
list[i].call(proto);
|
|
}
|
|
}
|
|
list = lifecycle.registered;
|
|
if (list) {
|
|
for (let i=0; i < list.length; i++) {
|
|
list[i].call(proto);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
_applyListeners() {
|
|
super._applyListeners();
|
|
const list = lifecycle.listeners;
|
|
if (list) {
|
|
for (let i=0; i < list.length; i++) {
|
|
const listeners = list[i];
|
|
if (listeners) {
|
|
for (let l in listeners) {
|
|
this._addMethodEventListenerToNode(this, l, listeners[l]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// note: exception to "super then me" rule;
|
|
// do work before calling super so that super attributes
|
|
// only apply if not already set.
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
_ensureAttributes() {
|
|
const list = lifecycle.hostAttributes;
|
|
if (list) {
|
|
for (let i=list.length-1; i >= 0; i--) {
|
|
const hostAttributes = list[i];
|
|
for (let a in hostAttributes) {
|
|
this._ensureAttribute(a, hostAttributes[a]);
|
|
}
|
|
}
|
|
}
|
|
super._ensureAttributes();
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
ready() {
|
|
super.ready();
|
|
let list = lifecycle.ready;
|
|
if (list) {
|
|
for (let i=0; i < list.length; i++) {
|
|
list[i].call(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
attached() {
|
|
super.attached();
|
|
let list = lifecycle.attached;
|
|
if (list) {
|
|
for (let i=0; i < list.length; i++) {
|
|
list[i].call(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
detached() {
|
|
super.detached();
|
|
let list = lifecycle.detached;
|
|
if (list) {
|
|
for (let i=0; i < list.length; i++) {
|
|
list[i].call(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements native Custom Elements `attributeChangedCallback` to
|
|
* set an attribute value to a property via `_attributeToProperty`.
|
|
*
|
|
* @param {string} name Name of attribute that changed
|
|
* @param {?string} old Old attribute value
|
|
* @param {?string} value New attribute value
|
|
* @return {void}
|
|
*/
|
|
attributeChanged(name, old, value) {
|
|
super.attributeChanged();
|
|
let list = lifecycle.attributeChanged;
|
|
if (list) {
|
|
for (let i=0; i < list.length; i++) {
|
|
list[i].call(this, name, old, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// apply behaviors, note actual copying is done lazily at first instance creation
|
|
if (behaviors) {
|
|
// NOTE: ensure the behavior is extending a class with
|
|
// legacy element api. This is necessary since behaviors expect to be able
|
|
// to access 1.x legacy api.
|
|
if (!Array.isArray(behaviors)) {
|
|
behaviors = [behaviors];
|
|
}
|
|
let superBehaviors = Base.prototype.behaviors;
|
|
// get flattened, deduped list of behaviors *not* already on super class
|
|
behaviorList = flattenBehaviors(behaviors, null, superBehaviors);
|
|
PolymerGenerated.prototype.behaviors = superBehaviors ?
|
|
superBehaviors.concat(behaviors) : behaviorList;
|
|
}
|
|
|
|
const copyPropertiesToProto = (proto) => {
|
|
if (behaviorList) {
|
|
applyBehaviors(proto, behaviorList, lifecycle);
|
|
}
|
|
applyInfo(proto, info, lifecycle, excludeOnInfo);
|
|
};
|
|
|
|
// copy properties if we're not optimizing
|
|
if (!legacyOptimizations) {
|
|
copyPropertiesToProto(PolymerGenerated.prototype);
|
|
}
|
|
|
|
PolymerGenerated.generatedFrom = info;
|
|
|
|
return PolymerGenerated;
|
|
}
|
|
|
|
/**
|
|
* Generates a class that extends `LegacyElement` based on the
|
|
* provided info object. Metadata objects on the `info` object
|
|
* (`properties`, `observers`, `listeners`, `behaviors`, `is`) are used
|
|
* for Polymer's meta-programming systems, and any functions are copied
|
|
* to the generated class.
|
|
*
|
|
* Valid "metadata" values are as follows:
|
|
*
|
|
* `is`: String providing the tag name to register the element under. In
|
|
* addition, if a `dom-module` with the same id exists, the first template
|
|
* in that `dom-module` will be stamped into the shadow root of this element,
|
|
* with support for declarative event listeners (`on-...`), Polymer data
|
|
* bindings (`[[...]]` and `{{...}}`), and id-based node finding into
|
|
* `this.$`.
|
|
*
|
|
* `properties`: Object describing property-related metadata used by Polymer
|
|
* features (key: property names, value: object containing property metadata).
|
|
* Valid keys in per-property metadata include:
|
|
* - `type` (String|Number|Object|Array|...): Used by
|
|
* `attributeChangedCallback` to determine how string-based attributes
|
|
* are deserialized to JavaScript property values.
|
|
* - `notify` (boolean): Causes a change in the property to fire a
|
|
* non-bubbling event called `<property>-changed`. Elements that have
|
|
* enabled two-way binding to the property use this event to observe changes.
|
|
* - `readOnly` (boolean): Creates a getter for the property, but no setter.
|
|
* To set a read-only property, use the private setter method
|
|
* `_setProperty(property, value)`.
|
|
* - `observer` (string): Observer method name that will be called when
|
|
* the property changes. The arguments of the method are
|
|
* `(value, previousValue)`.
|
|
* - `computed` (string): String describing method and dependent properties
|
|
* for computing the value of this property (e.g. `'computeFoo(bar, zot)'`).
|
|
* Computed properties are read-only by default and can only be changed
|
|
* via the return value of the computing method.
|
|
*
|
|
* `observers`: Array of strings describing multi-property observer methods
|
|
* and their dependent properties (e.g. `'observeABC(a, b, c)'`).
|
|
*
|
|
* `listeners`: Object describing event listeners to be added to each
|
|
* instance of this element (key: event name, value: method name).
|
|
*
|
|
* `behaviors`: Array of additional `info` objects containing metadata
|
|
* and callbacks in the same format as the `info` object here which are
|
|
* merged into this element.
|
|
*
|
|
* `hostAttributes`: Object listing attributes to be applied to the host
|
|
* once created (key: attribute name, value: attribute value). Values
|
|
* are serialized based on the type of the value. Host attributes should
|
|
* generally be limited to attributes such as `tabIndex` and `aria-...`.
|
|
* Attributes in `hostAttributes` are only applied if a user-supplied
|
|
* attribute is not already present (attributes in markup override
|
|
* `hostAttributes`).
|
|
*
|
|
* In addition, the following Polymer-specific callbacks may be provided:
|
|
* - `registered`: called after first instance of this element,
|
|
* - `created`: called during `constructor`
|
|
* - `attached`: called during `connectedCallback`
|
|
* - `detached`: called during `disconnectedCallback`
|
|
* - `ready`: called before first `attached`, after all properties of
|
|
* this element have been propagated to its template and all observers
|
|
* have run
|
|
*
|
|
* @param {!PolymerInit} info Object containing Polymer metadata and functions
|
|
* to become class methods.
|
|
* @template T
|
|
* @param {function(T):T} mixin Optional mixin to apply to legacy base class
|
|
* before extending with Polymer metaprogramming.
|
|
* @return {function(new:HTMLElement)} Generated class
|
|
*/
|
|
export const Class = function(info, mixin) {
|
|
if (!info) {
|
|
console.warn('Polymer.Class requires `info` argument');
|
|
}
|
|
let klass = mixin ? mixin(LegacyElement) :
|
|
LegacyElement;
|
|
klass = GenerateClassFromInfo(info, klass, info.behaviors);
|
|
// decorate klass with registration info
|
|
klass.is = klass.prototype.is = info.is;
|
|
return klass;
|
|
};
|