492 lines
20 KiB
HTML
492 lines
20 KiB
HTML
<!--
|
|
@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="../utils/boot.html">
|
|
<link rel="import" href="../utils/mixin.html">
|
|
|
|
<script>
|
|
(function() {
|
|
|
|
'use strict';
|
|
|
|
// 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
|
|
};
|
|
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) {
|
|
node.setAttribute(t.attributes[0].name, t.attributes[0].value);
|
|
t.removeAttribute(t.attributes[0].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 applyTemplateContent(inst, node, nodeInfo) {
|
|
if (nodeInfo.templateInfo) {
|
|
node._templateInfo = nodeInfo.templateInfo;
|
|
}
|
|
}
|
|
|
|
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
|
|
* @memberof Polymer
|
|
* @summary Element class mixin that provides basic template parsing and stamping
|
|
*/
|
|
Polymer.TemplateStamp = Polymer.dedupingMixin(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
|
|
*/
|
|
static _parseTemplate(template, outerTemplateInfo) {
|
|
// since a template may be re-used, memo-ize metadata
|
|
if (!template._templateInfo) {
|
|
let templateInfo = template._templateInfo = {};
|
|
templateInfo.nodeInfoList = [];
|
|
templateInfo.stripWhiteSpace =
|
|
(outerTemplateInfo && outerTemplateInfo.stripWhiteSpace) ||
|
|
template.hasAttribute('strip-whitespace');
|
|
this._parseTemplateContent(template, templateInfo, {parent: null});
|
|
}
|
|
return template._templateInfo;
|
|
}
|
|
|
|
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`
|
|
*/
|
|
static _parseTemplateNode(node, templateInfo, nodeInfo) {
|
|
let noted;
|
|
let element = /** @type {Element} */(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;
|
|
}
|
|
if (element.firstChild) {
|
|
noted = this._parseTemplateChildNodes(element, templateInfo, nodeInfo) || noted;
|
|
}
|
|
if (element.hasAttributes && element.hasAttributes()) {
|
|
noted = this._parseTemplateNodeAttributes(element, templateInfo, nodeInfo) || noted;
|
|
}
|
|
return 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 = { parentIndex, parentInfo: nodeInfo };
|
|
if (this._parseTemplateNode(node, templateInfo, childInfo)) {
|
|
childInfo.infoIndex = templateInfo.nodeInfoList.push(/** @type {!NodeInfo} */(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`
|
|
*/
|
|
static _parseTemplateNestedTemplate(node, outerTemplateInfo, nodeInfo) {
|
|
let templateInfo = this._parseTemplate(node, outerTemplateInfo);
|
|
let content = templateInfo.content =
|
|
node.content.ownerDocument.createDocumentFragment();
|
|
content.appendChild(node.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`
|
|
*/
|
|
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`
|
|
*/
|
|
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
|
|
*/
|
|
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
|
|
* @return {!StampedTemplate} Cloned template content
|
|
*/
|
|
_stampTemplate(template) {
|
|
// Polyfill support: bootstrap the template if it has not already been
|
|
if (template && !template.content &&
|
|
window.HTMLTemplateElement && HTMLTemplateElement.decorate) {
|
|
HTMLTemplateElement.decorate(template);
|
|
}
|
|
let 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);
|
|
applyTemplateContent(this, node, info);
|
|
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 {!Node} 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
|
|
*/
|
|
_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 {!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) {
|
|
node.addEventListener(eventName, handler);
|
|
}
|
|
|
|
/**
|
|
* Override point for adding custom or simulated event handling.
|
|
*
|
|
* @param {Node} node Node to remove event listener from
|
|
* @param {string} eventName Name of event
|
|
* @param {function(!Event):void} handler Listener function to remove
|
|
* @return {void}
|
|
*/
|
|
_removeEventListenerFromNode(node, eventName, handler) {
|
|
node.removeEventListener(eventName, handler);
|
|
}
|
|
|
|
}
|
|
|
|
return TemplateStamp;
|
|
|
|
});
|
|
|
|
})();
|
|
</script>
|