UNPKG

@polymer/polymer

Version:

The Polymer library makes it easy to create your own web components. Give your element some markup and properties, and then use it on a site. Polymer provides features like dynamic templates and data binding to reduce the amount of boilerplate you need to

623 lines (590 loc) 24.8 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 */ import '../utils/boot.js'; import { dedupingMixin } from '../utils/mixin.js'; // 1.x backwards-compatible auto-wrapper for template type extensions // This is a clear layering violation and gives favored-nation status to // dom-if and dom-repeat templates. This is a conceit we're choosing to keep // a.) to ease 1.x backwards-compatibility due to loss of `is`, and // b.) to maintain if/repeat capability in parser-constrained elements // (e.g. table, select) in lieu of native CE type extensions without // massive new invention in this space (e.g. directive system) const templateExtensions = { 'dom-if': true, 'dom-repeat': true }; let placeholderBugDetect = false; let placeholderBug = false; function hasPlaceholderBug() { if (!placeholderBugDetect) { placeholderBugDetect = true; const t = document.createElement('textarea'); t.placeholder = 'a'; placeholderBug = t.placeholder === t.textContent; } return placeholderBug; } /** * Some browsers have a bug with textarea, where placeholder text is copied as * a textnode child of the textarea. * * If the placeholder is a binding, this can break template stamping in two * ways. * * One issue is that when the `placeholder` attribute is removed when the * binding is processed, the textnode child of the textarea is deleted, and the * template info tries to bind into that node. * * With `legacyOptimizations` in use, when the template is stamped and the * `textarea.textContent` binding is processed, no corresponding node is found * because it was removed during parsing. An exception is generated when this * binding is updated. * * With `legacyOptimizations` not in use, the template is cloned before * processing and this changes the above behavior. The cloned template also has * a value property set to the placeholder and textContent. This prevents the * removal of the textContent when the placeholder attribute is removed. * Therefore the exception does not occur. However, there is an extra * unnecessary binding. * * @param {!Node} node Check node for placeholder bug * @return {void} */ function fixPlaceholder(node) { if (hasPlaceholderBug() && node.localName === 'textarea' && node.placeholder && node.placeholder === node.textContent) { node.textContent = null; } } /** * Copies an attribute from one element to another, converting the value to a * `TrustedScript` if it is named like a Polymer template event listener. * * @param {!Element} dest The element to set the attribute on * @param {!Element} src The element to read the attribute from * @param {string} name The name of the attribute */ const copyAttributeWithTemplateEventPolicy = (() => { /** * This `TrustedTypePolicy` is used to work around a Chrome bug in the Trusted * Types API where any attribute that starts with `on` may only be set to a * `TrustedScript` value, even if that attribute would not cause an event * listener to be created. (See https://crbug.com/993268 for details.) * * Polymer's template system allows `<dom-if>` and `<dom-repeat>` to be * written using the `<template is="...">` syntax, even if there is no UA * support for custom element extensions of built-in elements. In doing so, it * copies attributes from the original `<template>` to a newly created * `<dom-if>` or `<dom-repeat>`, which can trigger the bug mentioned above if * any of those attributes uses Polymer's `on-` syntax for event listeners. * (Note, the value of these `on-` listeners is not evaluated as script: it is * the name of a member function of a component that will be used as the event * listener.) * * @type {!TrustedTypePolicy|undefined} */ const polymerTemplateEventAttributePolicy = window.trustedTypes && window.trustedTypes.createPolicy( 'polymer-template-event-attribute-policy', { createScript: x => x, }); return (dest, src, name) => { const value = src.getAttribute(name); if (polymerTemplateEventAttributePolicy && name.startsWith('on-')) { dest.setAttribute( name, polymerTemplateEventAttributePolicy.createScript(value, name)); return; } dest.setAttribute(name, value); }; })(); function wrapTemplateExtension(node) { let is = node.getAttribute('is'); if (is && templateExtensions[is]) { let t = node; t.removeAttribute('is'); node = t.ownerDocument.createElement(is); t.parentNode.replaceChild(node, t); node.appendChild(t); while(t.attributes.length) { const {name} = t.attributes[0]; copyAttributeWithTemplateEventPolicy(node, t, name); t.removeAttribute(name); } } return node; } function findTemplateNode(root, nodeInfo) { // recursively ascend tree until we hit root let parent = nodeInfo.parentInfo && findTemplateNode(root, nodeInfo.parentInfo); // unwind the stack, returning the indexed node at each level if (parent) { // note: marginally faster than indexing via childNodes // (http://jsperf.com/childnodes-lookup) for (let n=parent.firstChild, i=0; n; n=n.nextSibling) { if (nodeInfo.parentIndex === i++) { return n; } } } else { return root; } } // construct `$` map (from id annotations) function applyIdToMap(inst, map, node, nodeInfo) { if (nodeInfo.id) { map[nodeInfo.id] = node; } } // install event listeners (from event annotations) function applyEventListener(inst, node, nodeInfo) { if (nodeInfo.events && nodeInfo.events.length) { for (let j=0, e$=nodeInfo.events, e; (j<e$.length) && (e=e$[j]); j++) { inst._addMethodEventListenerToNode(node, e.name, e.value, inst); } } } // push configuration references at configure time function applyTemplateInfo(inst, node, nodeInfo, parentTemplateInfo) { if (nodeInfo.templateInfo) { // Give the node an instance of this templateInfo and set its parent node._templateInfo = nodeInfo.templateInfo; node._parentTemplateInfo = parentTemplateInfo; } } function createNodeEventHandler(context, eventName, methodName) { // Instances can optionally have a _methodHost which allows redirecting where // to find methods. Currently used by `templatize`. context = context._methodHost || context; let handler = function(e) { if (context[methodName]) { context[methodName](e, e.detail); } else { console.warn('listener method `' + methodName + '` not defined'); } }; return handler; } /** * Element mixin that provides basic template parsing and stamping, including * the following template-related features for stamped templates: * * - Declarative event listeners (`on-eventname="listener"`) * - Map of node id's to stamped node instances (`this.$.id`) * - Nested template content caching/removal and re-installation (performance * optimization) * * @mixinFunction * @polymer * @summary Element class mixin that provides basic template parsing and stamping */ export const TemplateStamp = dedupingMixin( /** * @template T * @param {function(new:T)} superClass Class to apply mixin to. * @return {function(new:T)} superClass with mixin applied. */ (superClass) => { /** * @polymer * @mixinClass * @implements {Polymer_TemplateStamp} */ class TemplateStamp extends superClass { /** * Scans a template to produce template metadata. * * Template-specific metadata are stored in the object returned, and node- * specific metadata are stored in objects in its flattened `nodeInfoList` * array. Only nodes in the template that were parsed as nodes of * interest contain an object in `nodeInfoList`. Each `nodeInfo` object * contains an `index` (`childNodes` index in parent) and optionally * `parent`, which points to node info of its parent (including its index). * * The template metadata object returned from this method has the following * structure (many fields optional): * * ```js * { * // Flattened list of node metadata (for nodes that generated metadata) * nodeInfoList: [ * { * // `id` attribute for any nodes with id's for generating `$` map * id: {string}, * // `on-event="handler"` metadata * events: [ * { * name: {string}, // event name * value: {string}, // handler method name * }, ... * ], * // Notes when the template contained a `<slot>` for shady DOM * // optimization purposes * hasInsertionPoint: {boolean}, * // For nested `<template>`` nodes, nested template metadata * templateInfo: {object}, // nested template metadata * // Metadata to allow efficient retrieval of instanced node * // corresponding to this metadata * parentInfo: {number}, // reference to parent nodeInfo> * parentIndex: {number}, // index in parent's `childNodes` collection * infoIndex: {number}, // index of this `nodeInfo` in `templateInfo.nodeInfoList` * }, * ... * ], * // When true, the template had the `strip-whitespace` attribute * // or was nested in a template with that setting * stripWhitespace: {boolean}, * // For nested templates, nested template content is moved into * // a document fragment stored here; this is an optimization to * // avoid the cost of nested template cloning * content: {DocumentFragment} * } * ``` * * This method kicks off a recursive treewalk as follows: * * ``` * _parseTemplate <---------------------+ * _parseTemplateContent | * _parseTemplateNode <------------|--+ * _parseTemplateNestedTemplate --+ | * _parseTemplateChildNodes ---------+ * _parseTemplateNodeAttributes * _parseTemplateNodeAttribute * * ``` * * These methods may be overridden to add custom metadata about templates * to either `templateInfo` or `nodeInfo`. * * Note that this method may be destructive to the template, in that * e.g. event annotations may be removed after being noted in the * template metadata. * * @param {!HTMLTemplateElement} template Template to parse * @param {TemplateInfo=} outerTemplateInfo Template metadata from the outer * template, for parsing nested templates * @return {!TemplateInfo} Parsed template metadata * @nocollapse */ static _parseTemplate(template, outerTemplateInfo) { // since a template may be re-used, memo-ize metadata if (!template._templateInfo) { // TODO(rictic): fix typing let /** ? */ templateInfo = template._templateInfo = {}; templateInfo.nodeInfoList = []; templateInfo.nestedTemplate = Boolean(outerTemplateInfo); templateInfo.stripWhiteSpace = (outerTemplateInfo && outerTemplateInfo.stripWhiteSpace) || (template.hasAttribute && template.hasAttribute('strip-whitespace')); // TODO(rictic): fix typing this._parseTemplateContent( template, templateInfo, /** @type {?} */ ({parent: null})); } return template._templateInfo; } /** * See docs for _parseTemplateNode. * * @param {!HTMLTemplateElement} template . * @param {!TemplateInfo} templateInfo . * @param {!NodeInfo} nodeInfo . * @return {boolean} . * @nocollapse */ static _parseTemplateContent(template, templateInfo, nodeInfo) { return this._parseTemplateNode(template.content, templateInfo, nodeInfo); } /** * Parses template node and adds template and node metadata based on * the current node, and its `childNodes` and `attributes`. * * This method may be overridden to add custom node or template specific * metadata based on this node. * * @param {Node} node Node to parse * @param {!TemplateInfo} templateInfo Template metadata for current template * @param {!NodeInfo} nodeInfo Node metadata for current template. * @return {boolean} `true` if the visited node added node-specific * metadata to `nodeInfo` * @nocollapse */ static _parseTemplateNode(node, templateInfo, nodeInfo) { let noted = false; let element = /** @type {!HTMLTemplateElement} */ (node); if (element.localName == 'template' && !element.hasAttribute('preserve-content')) { noted = this._parseTemplateNestedTemplate(element, templateInfo, nodeInfo) || noted; } else if (element.localName === 'slot') { // For ShadyDom optimization, indicating there is an insertion point templateInfo.hasInsertionPoint = true; } fixPlaceholder(element); if (element.firstChild) { this._parseTemplateChildNodes(element, templateInfo, nodeInfo); } if (element.hasAttributes && element.hasAttributes()) { noted = this._parseTemplateNodeAttributes(element, templateInfo, nodeInfo) || noted; } // Checking `nodeInfo.noted` allows a child node of this node (who gets // access to `parentInfo`) to cause the parent to be noted, which // otherwise has no return path via `_parseTemplateChildNodes` (used by // some optimizations) return noted || nodeInfo.noted; } /** * Parses template child nodes for the given root node. * * This method also wraps whitelisted legacy template extensions * (`is="dom-if"` and `is="dom-repeat"`) with their equivalent element * wrappers, collapses text nodes, and strips whitespace from the template * if the `templateInfo.stripWhitespace` setting was provided. * * @param {Node} root Root node whose `childNodes` will be parsed * @param {!TemplateInfo} templateInfo Template metadata for current template * @param {!NodeInfo} nodeInfo Node metadata for current template. * @return {void} */ static _parseTemplateChildNodes(root, templateInfo, nodeInfo) { if (root.localName === 'script' || root.localName === 'style') { return; } for (let node=root.firstChild, parentIndex=0, next; node; node=next) { // Wrap templates if (node.localName == 'template') { node = wrapTemplateExtension(node); } // collapse adjacent textNodes: fixes an IE issue that can cause // text nodes to be inexplicably split =( // note that root.normalize() should work but does not so we do this // manually. next = node.nextSibling; if (node.nodeType === Node.TEXT_NODE) { let /** Node */ n = next; while (n && (n.nodeType === Node.TEXT_NODE)) { node.textContent += n.textContent; next = n.nextSibling; root.removeChild(n); n = next; } // optionally strip whitespace if (templateInfo.stripWhiteSpace && !node.textContent.trim()) { root.removeChild(node); continue; } } let childInfo = /** @type {!NodeInfo} */ ({parentIndex, parentInfo: nodeInfo}); if (this._parseTemplateNode(node, templateInfo, childInfo)) { childInfo.infoIndex = templateInfo.nodeInfoList.push(childInfo) - 1; } // Increment if not removed if (node.parentNode) { parentIndex++; } } } /** * Parses template content for the given nested `<template>`. * * Nested template info is stored as `templateInfo` in the current node's * `nodeInfo`. `template.content` is removed and stored in `templateInfo`. * It will then be the responsibility of the host to set it back to the * template and for users stamping nested templates to use the * `_contentForTemplate` method to retrieve the content for this template * (an optimization to avoid the cost of cloning nested template content). * * @param {HTMLTemplateElement} node Node to parse (a <template>) * @param {TemplateInfo} outerTemplateInfo Template metadata for current template * that includes the template `node` * @param {!NodeInfo} nodeInfo Node metadata for current template. * @return {boolean} `true` if the visited node added node-specific * metadata to `nodeInfo` * @nocollapse */ static _parseTemplateNestedTemplate(node, outerTemplateInfo, nodeInfo) { // TODO(rictic): the type of node should be non-null let element = /** @type {!HTMLTemplateElement} */ (node); let templateInfo = this._parseTemplate(element, outerTemplateInfo); let content = templateInfo.content = element.content.ownerDocument.createDocumentFragment(); content.appendChild(element.content); nodeInfo.templateInfo = templateInfo; return true; } /** * Parses template node attributes and adds node metadata to `nodeInfo` * for nodes of interest. * * @param {Element} node Node to parse * @param {!TemplateInfo} templateInfo Template metadata for current * template * @param {!NodeInfo} nodeInfo Node metadata for current template. * @return {boolean} `true` if the visited node added node-specific * metadata to `nodeInfo` * @nocollapse */ static _parseTemplateNodeAttributes(node, templateInfo, nodeInfo) { // Make copy of original attribute list, since the order may change // as attributes are added and removed let noted = false; let attrs = Array.from(node.attributes); for (let i=attrs.length-1, a; (a=attrs[i]); i--) { noted = this._parseTemplateNodeAttribute(node, templateInfo, nodeInfo, a.name, a.value) || noted; } return noted; } /** * Parses a single template node attribute and adds node metadata to * `nodeInfo` for attributes of interest. * * This implementation adds metadata for `on-event="handler"` attributes * and `id` attributes. * * @param {Element} node Node to parse * @param {!TemplateInfo} templateInfo Template metadata for current template * @param {!NodeInfo} nodeInfo Node metadata for current template. * @param {string} name Attribute name * @param {string} value Attribute value * @return {boolean} `true` if the visited node added node-specific * metadata to `nodeInfo` * @nocollapse */ static _parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value) { // events (on-*) if (name.slice(0, 3) === 'on-') { node.removeAttribute(name); nodeInfo.events = nodeInfo.events || []; nodeInfo.events.push({ name: name.slice(3), value }); return true; } // static id else if (name === 'id') { nodeInfo.id = value; return true; } return false; } /** * Returns the `content` document fragment for a given template. * * For nested templates, Polymer performs an optimization to cache nested * template content to avoid the cost of cloning deeply nested templates. * This method retrieves the cached content for a given template. * * @param {HTMLTemplateElement} template Template to retrieve `content` for * @return {DocumentFragment} Content fragment * @nocollapse */ static _contentForTemplate(template) { let templateInfo = /** @type {HTMLTemplateElementWithInfo} */ (template)._templateInfo; return (templateInfo && templateInfo.content) || template.content; } /** * Clones the provided template content and returns a document fragment * containing the cloned dom. * * The template is parsed (once and memoized) using this library's * template parsing features, and provides the following value-added * features: * * Adds declarative event listeners for `on-event="handler"` attributes * * Generates an "id map" for all nodes with id's under `$` on returned * document fragment * * Passes template info including `content` back to templates as * `_templateInfo` (a performance optimization to avoid deep template * cloning) * * Note that the memoized template parsing process is destructive to the * template: attributes for bindings and declarative event listeners are * removed after being noted in notes, and any nested `<template>.content` * is removed and stored in notes as well. * * @param {!HTMLTemplateElement} template Template to stamp * @param {TemplateInfo=} templateInfo Optional template info associated * with the template to be stamped; if omitted the template will be * automatically parsed. * @return {!StampedTemplate} Cloned template content * @override */ _stampTemplate(template, templateInfo) { // Polyfill support: bootstrap the template if it has not already been if (template && !template.content && window.HTMLTemplateElement && HTMLTemplateElement.decorate) { HTMLTemplateElement.decorate(template); } // Accepting the `templateInfo` via an argument allows for creating // instances of the `templateInfo` by the caller, useful for adding // instance-time information to the prototypical data templateInfo = templateInfo || this.constructor._parseTemplate(template); let nodeInfo = templateInfo.nodeInfoList; let content = templateInfo.content || template.content; let dom = /** @type {DocumentFragment} */ (document.importNode(content, true)); // NOTE: ShadyDom optimization indicating there is an insertion point dom.__noInsertionPoint = !templateInfo.hasInsertionPoint; let nodes = dom.nodeList = new Array(nodeInfo.length); dom.$ = {}; for (let i=0, l=nodeInfo.length, info; (i<l) && (info=nodeInfo[i]); i++) { let node = nodes[i] = findTemplateNode(dom, info); applyIdToMap(this, dom.$, node, info); applyTemplateInfo(this, node, info, templateInfo); applyEventListener(this, node, info); } dom = /** @type {!StampedTemplate} */(dom); // eslint-disable-line no-self-assign return dom; } /** * Adds an event listener by method name for the event provided. * * This method generates a handler function that looks up the method * name at handling time. * * @param {!EventTarget} node Node to add listener on * @param {string} eventName Name of event * @param {string} methodName Name of method * @param {*=} context Context the method will be called on (defaults * to `node`) * @return {Function} Generated handler function * @override */ _addMethodEventListenerToNode(node, eventName, methodName, context) { context = context || node; let handler = createNodeEventHandler(context, eventName, methodName); this._addEventListenerToNode(node, eventName, handler); return handler; } /** * Override point for adding custom or simulated event handling. * * @param {!EventTarget} node Node to add event listener to * @param {string} eventName Name of event * @param {function(!Event):void} handler Listener function to add * @return {void} * @override */ _addEventListenerToNode(node, eventName, handler) { node.addEventListener(eventName, handler); } /** * Override point for adding custom or simulated event handling. * * @param {!EventTarget} node Node to remove event listener from * @param {string} eventName Name of event * @param {function(!Event):void} handler Listener function to remove * @return {void} * @override */ _removeEventListenerFromNode(node, eventName, handler) { node.removeEventListener(eventName, handler); } } return TemplateStamp; });