UNPKG

webdash-readme-preview

Version:
580 lines (555 loc) 24.6 kB
<!-- @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 --> <link rel="import" href="boot.html"> <link rel="import" href="../mixins/property-effects.html"> <link rel="import" href="../mixins/mutable-data.html"> <script> (function() { 'use strict'; // Base class for HTMLTemplateElement extension that has property effects // machinery for propagating host properties to children. This is an ES5 // class only because Babel (incorrectly) requires super() in the class // constructor even though no `this` is used and it returns an instance. let newInstance = null; /** * @constructor * @extends {HTMLTemplateElement} */ function HTMLTemplateElementExtension() { return newInstance; } HTMLTemplateElementExtension.prototype = Object.create(HTMLTemplateElement.prototype, { constructor: { value: HTMLTemplateElementExtension, writable: true } }); /** * @constructor * @implements {Polymer_PropertyEffects} * @extends {HTMLTemplateElementExtension} */ const DataTemplate = Polymer.PropertyEffects(HTMLTemplateElementExtension); /** * @constructor * @implements {Polymer_MutableData} * @extends {DataTemplate} */ const MutableDataTemplate = Polymer.MutableData(DataTemplate); // Applies a DataTemplate subclass to a <template> instance function upgradeTemplate(template, constructor) { newInstance = template; Object.setPrototypeOf(template, constructor.prototype); new constructor(); newInstance = null; } // Base class for TemplateInstance's /** * @constructor * @implements {Polymer_PropertyEffects} */ const base = Polymer.PropertyEffects(class {}); /** * @polymer * @customElement * @appliesMixin Polymer.PropertyEffects * @unrestricted */ class TemplateInstanceBase extends base { constructor(props) { super(); this._configureProperties(props); this.root = this._stampTemplate(this.__dataHost); // Save list of stamped children let children = this.children = []; for (let n = this.root.firstChild; n; n=n.nextSibling) { children.push(n); n.__templatizeInstance = this; } if (this.__templatizeOwner && this.__templatizeOwner.__hideTemplateChildren__) { this._showHideChildren(true); } // Flush props only when props are passed if instance props exist // or when there isn't instance props. let options = this.__templatizeOptions; if ((props && options.instanceProps) || !options.instanceProps) { this._enableProperties(); } } /** * Configure the given `props` by calling `_setPendingProperty`. Also * sets any properties stored in `__hostProps`. * @private * @param {Object} props Object of property name-value pairs to set. * @return {void} */ _configureProperties(props) { let options = this.__templatizeOptions; if (options.forwardHostProp) { for (let hprop in this.__hostProps) { this._setPendingProperty(hprop, this.__dataHost['_host_' + hprop]); } } // Any instance props passed in the constructor will overwrite host props; // normally this would be a user error but we don't specifically filter them for (let iprop in props) { this._setPendingProperty(iprop, props[iprop]); } } /** * Forwards a host property to this instance. This method should be * called on instances from the `options.forwardHostProp` callback * to propagate changes of host properties to each instance. * * Note this method enqueues the change, which are flushed as a batch. * * @param {string} prop Property or path name * @param {*} value Value of the property to forward * @return {void} */ forwardHostProp(prop, value) { if (this._setPendingPropertyOrPath(prop, value, false, true)) { this.__dataHost._enqueueClient(this); } } /** * Override point for adding custom or simulated event handling. * * @param {!Node} node Node to add event listener to * @param {string} eventName Name of event * @param {function(!Event):void} handler Listener function to add * @return {void} */ _addEventListenerToNode(node, eventName, handler) { if (this._methodHost && this.__templatizeOptions.parentModel) { // If this instance should be considered a parent model, decorate // events this template instance as `model` this._methodHost._addEventListenerToNode(node, eventName, (e) => { e.model = this; handler(e); }); } else { // Otherwise delegate to the template's host (which could be) // another template instance let templateHost = this.__dataHost.__dataHost; if (templateHost) { templateHost._addEventListenerToNode(node, eventName, handler); } } } /** * Shows or hides the template instance top level child elements. For * text nodes, `textContent` is removed while "hidden" and replaced when * "shown." * @param {boolean} hide Set to true to hide the children; * set to false to show them. * @return {void} * @protected */ _showHideChildren(hide) { let c = this.children; for (let i=0; i<c.length; i++) { let n = c[i]; // Ignore non-changes if (Boolean(hide) != Boolean(n.__hideTemplateChildren__)) { if (n.nodeType === Node.TEXT_NODE) { if (hide) { n.__polymerTextContent__ = n.textContent; n.textContent = ''; } else { n.textContent = n.__polymerTextContent__; } // remove and replace slot } else if (n.localName === 'slot') { if (hide) { n.__polymerReplaced__ = document.createComment('hidden-slot'); n.parentNode.replaceChild(n.__polymerReplaced__, n); } else { const replace = n.__polymerReplaced__; if (replace) { replace.parentNode.replaceChild(n, replace); } } } else if (n.style) { if (hide) { n.__polymerDisplay__ = n.style.display; n.style.display = 'none'; } else { n.style.display = n.__polymerDisplay__; } } } n.__hideTemplateChildren__ = hide; if (n._showHideChildren) { n._showHideChildren(hide); } } } /** * Overrides default property-effects implementation to intercept * textContent bindings while children are "hidden" and cache in * private storage for later retrieval. * * @param {!Node} node The node to set a property on * @param {string} prop The property to set * @param {*} value The value to set * @return {void} * @protected */ _setUnmanagedPropertyToNode(node, prop, value) { if (node.__hideTemplateChildren__ && node.nodeType == Node.TEXT_NODE && prop == 'textContent') { node.__polymerTextContent__ = value; } else { super._setUnmanagedPropertyToNode(node, prop, value); } } /** * Find the parent model of this template instance. The parent model * is either another templatize instance that had option `parentModel: true`, * or else the host element. * * @return {!Polymer_PropertyEffects} The parent model of this instance */ get parentModel() { let model = this.__parentModel; if (!model) { let options; model = this; do { // A template instance's `__dataHost` is a <template> // `model.__dataHost.__dataHost` is the template's host model = model.__dataHost.__dataHost; } while ((options = model.__templatizeOptions) && !options.parentModel); this.__parentModel = model; } return model; } /** * Stub of HTMLElement's `dispatchEvent`, so that effects that may * dispatch events safely no-op. * * @param {Event} event Event to dispatch * @return {boolean} Always true. */ dispatchEvent(event) { // eslint-disable-line no-unused-vars return true; } } /** @type {!DataTemplate} */ TemplateInstanceBase.prototype.__dataHost; /** @type {!TemplatizeOptions} */ TemplateInstanceBase.prototype.__templatizeOptions; /** @type {!Polymer_PropertyEffects} */ TemplateInstanceBase.prototype._methodHost; /** @type {!Object} */ TemplateInstanceBase.prototype.__templatizeOwner; /** @type {!Object} */ TemplateInstanceBase.prototype.__hostProps; /** * @constructor * @extends {TemplateInstanceBase} * @implements {Polymer_MutableData} */ const MutableTemplateInstanceBase = Polymer.MutableData(TemplateInstanceBase); function findMethodHost(template) { // Technically this should be the owner of the outermost template. // In shadow dom, this is always getRootNode().host, but we can // approximate this via cooperation with our dataHost always setting // `_methodHost` as long as there were bindings (or id's) on this // instance causing it to get a dataHost. let templateHost = template.__dataHost; return templateHost && templateHost._methodHost || templateHost; } /* eslint-disable valid-jsdoc */ /** * @suppress {missingProperties} class.prototype is not defined for some reason */ function createTemplatizerClass(template, templateInfo, options) { // Anonymous class created by the templatize let base = options.mutableData ? MutableTemplateInstanceBase : TemplateInstanceBase; /** * @constructor * @extends {base} * @private */ let klass = class extends base { }; klass.prototype.__templatizeOptions = options; klass.prototype._bindTemplate(template); addNotifyEffects(klass, template, templateInfo, options); return klass; } /** * @suppress {missingProperties} class.prototype is not defined for some reason */ function addPropagateEffects(template, templateInfo, options) { let userForwardHostProp = options.forwardHostProp; if (userForwardHostProp) { // Provide data API and property effects on memoized template class let klass = templateInfo.templatizeTemplateClass; if (!klass) { let base = options.mutableData ? MutableDataTemplate : DataTemplate; klass = templateInfo.templatizeTemplateClass = class TemplatizedTemplate extends base {}; // Add template - >instances effects // and host <- template effects let hostProps = templateInfo.hostProps; for (let prop in hostProps) { klass.prototype._addPropertyEffect('_host_' + prop, klass.prototype.PROPERTY_EFFECT_TYPES.PROPAGATE, {fn: createForwardHostPropEffect(prop, userForwardHostProp)}); klass.prototype._createNotifyingProperty('_host_' + prop); } } upgradeTemplate(template, klass); // Mix any pre-bound data into __data; no need to flush this to // instances since they pull from the template at instance-time if (template.__dataProto) { // Note, generally `__dataProto` could be chained, but it's guaranteed // to not be since this is a vanilla template we just added effects to Object.assign(template.__data, template.__dataProto); } // Clear any pending data for performance template.__dataTemp = {}; template.__dataPending = null; template.__dataOld = null; template._enableProperties(); } } /* eslint-enable valid-jsdoc */ function createForwardHostPropEffect(hostProp, userForwardHostProp) { return function forwardHostProp(template, prop, props) { userForwardHostProp.call(template.__templatizeOwner, prop.substring('_host_'.length), props[prop]); }; } function addNotifyEffects(klass, template, templateInfo, options) { let hostProps = templateInfo.hostProps || {}; for (let iprop in options.instanceProps) { delete hostProps[iprop]; let userNotifyInstanceProp = options.notifyInstanceProp; if (userNotifyInstanceProp) { klass.prototype._addPropertyEffect(iprop, klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY, {fn: createNotifyInstancePropEffect(iprop, userNotifyInstanceProp)}); } } if (options.forwardHostProp && template.__dataHost) { for (let hprop in hostProps) { klass.prototype._addPropertyEffect(hprop, klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY, {fn: createNotifyHostPropEffect()}); } } } function createNotifyInstancePropEffect(instProp, userNotifyInstanceProp) { return function notifyInstanceProp(inst, prop, props) { userNotifyInstanceProp.call(inst.__templatizeOwner, inst, prop, props[prop]); }; } function createNotifyHostPropEffect() { return function notifyHostProp(inst, prop, props) { inst.__dataHost._setPendingPropertyOrPath('_host_' + prop, props[prop], true, true); }; } /** * Module for preparing and stamping instances of templates that utilize * Polymer's data-binding and declarative event listener features. * * Example: * * // Get a template from somewhere, e.g. light DOM * let template = this.querySelector('template'); * // Prepare the template * let TemplateClass = Polymer.Templatize.templatize(template); * // Instance the template with an initial data model * let instance = new TemplateClass({myProp: 'initial'}); * // Insert the instance's DOM somewhere, e.g. element's shadow DOM * this.shadowRoot.appendChild(instance.root); * // Changing a property on the instance will propagate to bindings * // in the template * instance.myProp = 'new value'; * * The `options` dictionary passed to `templatize` allows for customizing * features of the generated template class, including how outer-scope host * properties should be forwarded into template instances, how any instance * properties added into the template's scope should be notified out to * the host, and whether the instance should be decorated as a "parent model" * of any event handlers. * * // Customize property forwarding and event model decoration * let TemplateClass = Polymer.Templatize.templatize(template, this, { * parentModel: true, * forwardHostProp(property, value) {...}, * instanceProps: {...}, * notifyInstanceProp(instance, property, value) {...}, * }); * * @namespace * @memberof Polymer * @summary Module for preparing and stamping instances of templates * utilizing Polymer templating features. */ Polymer.Templatize = { /** * Returns an anonymous `Polymer.PropertyEffects` class bound to the * `<template>` provided. Instancing the class will result in the * template being stamped into a document fragment stored as the instance's * `root` property, after which it can be appended to the DOM. * * Templates may utilize all Polymer data-binding features as well as * declarative event listeners. Event listeners and inline computing * functions in the template will be called on the host of the template. * * The constructor returned takes a single argument dictionary of initial * property values to propagate into template bindings. Additionally * host properties can be forwarded in, and instance properties can be * notified out by providing optional callbacks in the `options` dictionary. * * Valid configuration in `options` are as follows: * * - `forwardHostProp(property, value)`: Called when a property referenced * in the template changed on the template's host. As this library does * not retain references to templates instanced by the user, it is the * templatize owner's responsibility to forward host property changes into * user-stamped instances. The `instance.forwardHostProp(property, value)` * method on the generated class should be called to forward host * properties into the template to prevent unnecessary property-changed * notifications. Any properties referenced in the template that are not * defined in `instanceProps` will be notified up to the template's host * automatically. * - `instanceProps`: Dictionary of property names that will be added * to the instance by the templatize owner. These properties shadow any * host properties, and changes within the template to these properties * will result in `notifyInstanceProp` being called. * - `mutableData`: When `true`, the generated class will skip strict * dirty-checking for objects and arrays (always consider them to be * "dirty"). * - `notifyInstanceProp(instance, property, value)`: Called when * an instance property changes. Users may choose to call `notifyPath` * on e.g. the owner to notify the change. * - `parentModel`: When `true`, events handled by declarative event listeners * (`on-event="handler"`) will be decorated with a `model` property pointing * to the template instance that stamped it. It will also be returned * from `instance.parentModel` in cases where template instance nesting * causes an inner model to shadow an outer model. * * All callbacks are called bound to the `owner`. Any context * needed for the callbacks (such as references to `instances` stamped) * should be stored on the `owner` such that they can be retrieved via * `this`. * * When `options.forwardHostProp` is declared as an option, any properties * referenced in the template will be automatically forwarded from the host of * the `<template>` to instances, with the exception of any properties listed in * the `options.instanceProps` object. `instanceProps` are assumed to be * managed by the owner of the instances, either passed into the constructor * or set after the fact. Note, any properties passed into the constructor will * always be set to the instance (regardless of whether they would normally * be forwarded from the host). * * Note that `templatize()` can be run only once for a given `<template>`. * Further calls will result in an error. Also, there is a special * behavior if the template was duplicated through a mechanism such as * `<dom-repeat>` or `<test-fixture>`. In this case, all calls to * `templatize()` return the same class for all duplicates of a template. * The class returned from `templatize()` is generated only once using * the `options` from the first call. This means that any `options` * provided to subsequent calls will be ignored. Therefore, it is very * important not to close over any variables inside the callbacks. Also, * arrow functions must be avoided because they bind the outer `this`. * Inside the callbacks, any contextual information can be accessed * through `this`, which points to the `owner`. * * @memberof Polymer.Templatize * @param {!HTMLTemplateElement} template Template to templatize * @param {Polymer_PropertyEffects=} owner Owner of the template instances; * any optional callbacks will be bound to this owner. * @param {Object=} options Options dictionary (see summary for details) * @return {function(new:TemplateInstanceBase)} Generated class bound to the template * provided * @suppress {invalidCasts} */ templatize(template, owner, options) { options = /** @type {!TemplatizeOptions} */(options || {}); if (template.__templatizeOwner) { throw new Error('A <template> can only be templatized once'); } template.__templatizeOwner = owner; const ctor = owner ? owner.constructor : TemplateInstanceBase; let templateInfo = ctor._parseTemplate(template); // Get memoized base class for the prototypical template, which // includes property effects for binding template & forwarding let baseClass = templateInfo.templatizeInstanceClass; if (!baseClass) { baseClass = createTemplatizerClass(template, templateInfo, options); templateInfo.templatizeInstanceClass = baseClass; } // Host property forwarding must be installed onto template instance addPropagateEffects(template, templateInfo, options); // Subclass base class and add reference for this specific template /** @private */ let klass = class TemplateInstance extends baseClass {}; klass.prototype._methodHost = findMethodHost(template); klass.prototype.__dataHost = template; klass.prototype.__templatizeOwner = owner; klass.prototype.__hostProps = templateInfo.hostProps; klass = /** @type {function(new:TemplateInstanceBase)} */(klass); //eslint-disable-line no-self-assign return klass; }, /** * Returns the template "model" associated with a given element, which * serves as the binding scope for the template instance the element is * contained in. A template model is an instance of * `TemplateInstanceBase`, and should be used to manipulate data * associated with this template instance. * * Example: * * let model = modelForElement(el); * if (model.index < 10) { * model.set('item.checked', true); * } * * @memberof Polymer.Templatize * @param {HTMLTemplateElement} template The model will be returned for * elements stamped from this template * @param {Node=} node Node for which to return a template model. * @return {TemplateInstanceBase} Template instance representing the * binding scope for the element */ modelForElement(template, node) { let model; while (node) { // An element with a __templatizeInstance marks the top boundary // of a scope; walk up until we find one, and then ensure that // its __dataHost matches `this`, meaning this dom-repeat stamped it if ((model = node.__templatizeInstance)) { // Found an element stamped by another template; keep walking up // from its __dataHost if (model.__dataHost != template) { node = model.__dataHost; } else { return model; } } else { // Still in a template scope, keep going up until // a __templatizeInstance is found node = node.parentNode; } } return null; } }; Polymer.TemplateInstanceBase = TemplateInstanceBase; })(); </script>