| OLD | NEW |
| (Empty) | |
| 1 <!-- |
| 2 @license |
| 3 Copyright (c) 2017 The Polymer Project Authors. All rights reserved. |
| 4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt |
| 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| 6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt |
| 7 Code distributed by Google as part of the polymer project is also |
| 8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt |
| 9 --> |
| 10 |
| 11 <link rel="import" href="../utils/boot.html"> |
| 12 <link rel="import" href="../utils/mixin.html"> |
| 13 |
| 14 <script> |
| 15 (function() { |
| 16 |
| 17 'use strict'; |
| 18 |
| 19 // 1.x backwards-compatible auto-wrapper for template type extensions |
| 20 // This is a clear layering violation and gives favored-nation status to |
| 21 // dom-if and dom-repeat templates. This is a conceit we're choosing to keep |
| 22 // a.) to ease 1.x backwards-compatibility due to loss of `is`, and |
| 23 // b.) to maintain if/repeat capability in parser-constrained elements |
| 24 // (e.g. table, select) in lieu of native CE type extensions without |
| 25 // massive new invention in this space (e.g. directive system) |
| 26 const templateExtensions = { |
| 27 'dom-if': true, |
| 28 'dom-repeat': true |
| 29 }; |
| 30 function wrapTemplateExtension(node) { |
| 31 let is = node.getAttribute('is'); |
| 32 if (is && templateExtensions[is]) { |
| 33 let t = node; |
| 34 t.removeAttribute('is'); |
| 35 node = t.ownerDocument.createElement(is); |
| 36 t.parentNode.replaceChild(node, t); |
| 37 node.appendChild(t); |
| 38 while(t.attributes.length) { |
| 39 node.setAttribute(t.attributes[0].name, t.attributes[0].value); |
| 40 t.removeAttribute(t.attributes[0].name); |
| 41 } |
| 42 } |
| 43 return node; |
| 44 } |
| 45 |
| 46 function findTemplateNode(root, nodeInfo) { |
| 47 // recursively ascend tree until we hit root |
| 48 let parent = nodeInfo.parentInfo && findTemplateNode(root, nodeInfo.parentIn
fo); |
| 49 // unwind the stack, returning the indexed node at each level |
| 50 if (parent) { |
| 51 // note: marginally faster than indexing via childNodes |
| 52 // (http://jsperf.com/childnodes-lookup) |
| 53 for (let n=parent.firstChild, i=0; n; n=n.nextSibling) { |
| 54 if (nodeInfo.parentIndex === i++) { |
| 55 return n; |
| 56 } |
| 57 } |
| 58 } else { |
| 59 return root; |
| 60 } |
| 61 } |
| 62 |
| 63 // construct `$` map (from id annotations) |
| 64 function applyIdToMap(inst, map, node, nodeInfo) { |
| 65 if (nodeInfo.id) { |
| 66 map[nodeInfo.id] = node; |
| 67 } |
| 68 } |
| 69 |
| 70 // install event listeners (from event annotations) |
| 71 function applyEventListener(inst, node, nodeInfo) { |
| 72 if (nodeInfo.events && nodeInfo.events.length) { |
| 73 for (let j=0, e$=nodeInfo.events, e; (j<e$.length) && (e=e$[j]); j++) { |
| 74 inst._addMethodEventListenerToNode(node, e.name, e.value, inst); |
| 75 } |
| 76 } |
| 77 } |
| 78 |
| 79 // push configuration references at configure time |
| 80 function applyTemplateContent(inst, node, nodeInfo) { |
| 81 if (nodeInfo.templateInfo) { |
| 82 node._templateInfo = nodeInfo.templateInfo; |
| 83 } |
| 84 } |
| 85 |
| 86 function createNodeEventHandler(context, eventName, methodName) { |
| 87 // Instances can optionally have a _methodHost which allows redirecting wher
e |
| 88 // to find methods. Currently used by `templatize`. |
| 89 context = context._methodHost || context; |
| 90 let handler = function(e) { |
| 91 if (context[methodName]) { |
| 92 context[methodName](e, e.detail); |
| 93 } else { |
| 94 console.warn('listener method `' + methodName + '` not defined'); |
| 95 } |
| 96 }; |
| 97 return handler; |
| 98 } |
| 99 |
| 100 /** |
| 101 * Element mixin that provides basic template parsing and stamping, including |
| 102 * the following template-related features for stamped templates: |
| 103 * |
| 104 * - Declarative event listeners (`on-eventname="listener"`) |
| 105 * - Map of node id's to stamped node instances (`this.$.id`) |
| 106 * - Nested template content caching/removal and re-installation (performance |
| 107 * optimization) |
| 108 * |
| 109 * @polymerMixin |
| 110 * @memberof Polymer |
| 111 * @summary Element class mixin that provides basic template parsing and stamp
ing |
| 112 */ |
| 113 Polymer.TemplateStamp = Polymer.dedupingMixin(superClass => { |
| 114 |
| 115 /** |
| 116 * @polymerMixinClass |
| 117 * @implements {Polymer_TemplateStamp} |
| 118 */ |
| 119 class TemplateStamp extends superClass { |
| 120 |
| 121 /** |
| 122 * Scans a template to produce template metadata. |
| 123 * |
| 124 * Template-specific metadata are stored in the object returned, and node- |
| 125 * specific metadata are stored in objects in its flattened `nodeInfoList` |
| 126 * array. Only nodes in the template that were parsed as nodes of |
| 127 * interest contain an object in `nodeInfoList`. Each `nodeInfo` object |
| 128 * contains an `index` (`childNodes` index in parent) and optionally |
| 129 * `parent`, which points to node info of its parent (including its index)
. |
| 130 * |
| 131 * The template metadata object returned from this method has the followin
g |
| 132 * structure (many fields optional): |
| 133 * |
| 134 * ```js |
| 135 * { |
| 136 * // Flattened list of node metadata (for nodes that generated metada
ta) |
| 137 * nodeInfoList: [ |
| 138 * { |
| 139 * // `id` attribute for any nodes with id's for generating `$` ma
p |
| 140 * id: {string}, |
| 141 * // `on-event="handler"` metadata |
| 142 * events: [ |
| 143 * { |
| 144 * name: {string}, // event name |
| 145 * value: {string}, // handler method name |
| 146 * }, ... |
| 147 * ], |
| 148 * // Notes when the template contained a `<slot>` for shady DOM |
| 149 * // optimization purposes |
| 150 * hasInsertionPoint: {boolean}, |
| 151 * // For nested `<template>`` nodes, nested template metadata |
| 152 * templateInfo: {object}, // nested template metadata |
| 153 * // Metadata to allow efficient retrieval of instanced node |
| 154 * // corresponding to this metadata |
| 155 * parentInfo: {number}, // reference to parent nodeInfo> |
| 156 * parentIndex: {number}, // index in parent's `childNodes` colle
ction |
| 157 * infoIndex: {number}, // index of this `nodeInfo` in `templat
eInfo.nodeInfoList` |
| 158 * }, |
| 159 * ... |
| 160 * ], |
| 161 * // When true, the template had the `strip-whitespace` attribute |
| 162 * // or was nested in a template with that setting |
| 163 * stripWhitespace: {boolean}, |
| 164 * // For nested templates, nested template content is moved into |
| 165 * // a document fragment stored here; this is an optimization to |
| 166 * // avoid the cost of nested template cloning |
| 167 * content: {DocumentFragment} |
| 168 * } |
| 169 * ``` |
| 170 * |
| 171 * This method kicks off a recursive treewalk as follows: |
| 172 * |
| 173 * ``` |
| 174 * _parseTemplate <---------------------+ |
| 175 * _parseTemplateContent | |
| 176 * _parseTemplateNode <------------|--+ |
| 177 * _parseTemplateNestedTemplate --+ | |
| 178 * _parseTemplateChildNodes ---------+ |
| 179 * _parseTemplateNodeAttributes |
| 180 * _parseTemplateNodeAttribute |
| 181 * |
| 182 * ``` |
| 183 * |
| 184 * These methods may be overridden to add custom metadata about templates |
| 185 * to either `templateInfo` or `nodeInfo`. |
| 186 * |
| 187 * Note that this method may be destructive to the template, in that |
| 188 * e.g. event annotations may be removed after being noted in the |
| 189 * template metadata. |
| 190 * |
| 191 * @param {HTMLTemplateElement} template Template to parse |
| 192 * @param {Object=} outerTemplateInfo Template metadata from the outer |
| 193 * template, for parsing nested templates |
| 194 * @return {Object} Parsed template metadata |
| 195 */ |
| 196 static _parseTemplate(template, outerTemplateInfo) { |
| 197 // since a template may be re-used, memo-ize metadata |
| 198 if (!template._templateInfo) { |
| 199 let templateInfo = template._templateInfo = {}; |
| 200 templateInfo.nodeInfoList = []; |
| 201 templateInfo.stripWhiteSpace = |
| 202 (outerTemplateInfo && outerTemplateInfo.stripWhiteSpace) || |
| 203 template.hasAttribute('strip-whitespace'); |
| 204 this._parseTemplateContent(template, templateInfo, {parent: null}); |
| 205 } |
| 206 return template._templateInfo; |
| 207 } |
| 208 |
| 209 static _parseTemplateContent(template, templateInfo, nodeInfo) { |
| 210 return this._parseTemplateNode(template.content, templateInfo, nodeInfo)
; |
| 211 } |
| 212 |
| 213 /** |
| 214 * Parses template node and adds template and node metadata based on |
| 215 * the current node, and its `childNodes` and `attributes`. |
| 216 * |
| 217 * This method may be overridden to add custom node or template specific |
| 218 * metadata based on this node. |
| 219 * |
| 220 * @param {Node} node Node to parse |
| 221 * @param {Object} templateInfo Template metadata for current template |
| 222 * @param {Object} nodeInfo Node metadata for current template. |
| 223 * @return {boolean} `true` if the visited node added node-specific |
| 224 * metadata to `nodeInfo` |
| 225 */ |
| 226 static _parseTemplateNode(node, templateInfo, nodeInfo) { |
| 227 let noted; |
| 228 if (node.localName == 'template' && !node.hasAttribute('preserve-content
')) { |
| 229 noted = this._parseTemplateNestedTemplate(node, templateInfo, nodeInfo
) || noted; |
| 230 } else if (node.localName === 'slot') { |
| 231 // For ShadyDom optimization, indicating there is an insertion point |
| 232 templateInfo.hasInsertionPoint = true; |
| 233 } |
| 234 if (node.firstChild) { |
| 235 noted = this._parseTemplateChildNodes(node, templateInfo, nodeInfo) ||
noted; |
| 236 } |
| 237 if (node.hasAttributes && node.hasAttributes()) { |
| 238 noted = this._parseTemplateNodeAttributes(node, templateInfo, nodeInfo
) || noted; |
| 239 } |
| 240 return noted; |
| 241 } |
| 242 |
| 243 /** |
| 244 * Parses template child nodes for the given root node. |
| 245 * |
| 246 * This method also wraps whitelisted legacy template extensions |
| 247 * (`is="dom-if"` and `is="dom-repeat"`) with their equivalent element |
| 248 * wrappers, collapses text nodes, and strips whitespace from the template |
| 249 * if the `templateInfo.stripWhitespace` setting was provided. |
| 250 * |
| 251 * @param {Node} root Root node whose `childNodes` will be parsed |
| 252 * @param {Object} templateInfo Template metadata for current template |
| 253 * @param {Object} nodeInfo Node metadata for current template. |
| 254 */ |
| 255 static _parseTemplateChildNodes(root, templateInfo, nodeInfo) { |
| 256 for (let node=root.firstChild, parentIndex=0, next; node; node=next) { |
| 257 // Wrap templates |
| 258 if (node.localName == 'template') { |
| 259 node = wrapTemplateExtension(node); |
| 260 } |
| 261 // collapse adjacent textNodes: fixes an IE issue that can cause |
| 262 // text nodes to be inexplicably split =( |
| 263 // note that root.normalize() should work but does not so we do this |
| 264 // manually. |
| 265 next = node.nextSibling; |
| 266 if (node.nodeType === Node.TEXT_NODE) { |
| 267 let n = next; |
| 268 while (n && (n.nodeType === Node.TEXT_NODE)) { |
| 269 node.textContent += n.textContent; |
| 270 next = n.nextSibling; |
| 271 root.removeChild(n); |
| 272 n = next; |
| 273 } |
| 274 // optionally strip whitespace |
| 275 if (templateInfo.stripWhiteSpace && !node.textContent.trim()) { |
| 276 root.removeChild(node); |
| 277 continue; |
| 278 } |
| 279 } |
| 280 let childInfo = { parentIndex, parentInfo: nodeInfo }; |
| 281 if (this._parseTemplateNode(node, templateInfo, childInfo)) { |
| 282 childInfo.infoIndex = templateInfo.nodeInfoList.push(childInfo) - 1; |
| 283 } |
| 284 // Increment if not removed |
| 285 if (node.parentNode) { |
| 286 parentIndex++; |
| 287 } |
| 288 } |
| 289 } |
| 290 |
| 291 /** |
| 292 * Parses template content for the given nested `<template>`. |
| 293 * |
| 294 * Nested template info is stored as `templateInfo` in the current node's |
| 295 * `nodeInfo`. `template.content` is removed and stored in `templateInfo`. |
| 296 * It will then be the responsibility of the host to set it back to the |
| 297 * template and for users stamping nested templates to use the |
| 298 * `_contentForTemplate` method to retrieve the content for this template |
| 299 * (an optimization to avoid the cost of cloning nested template content). |
| 300 * |
| 301 * @param {HTMLTemplateElement} node Node to parse (a <template>) |
| 302 * @param {Object} outerTemplateInfo Template metadata for current templat
e |
| 303 * that includes the template `node` |
| 304 * @param {Object} nodeInfo Node metadata for current template. |
| 305 * @return {boolean} `true` if the visited node added node-specific |
| 306 * metadata to `nodeInfo` |
| 307 */ |
| 308 static _parseTemplateNestedTemplate(node, outerTemplateInfo, nodeInfo) { |
| 309 let templateInfo = this._parseTemplate(node, outerTemplateInfo); |
| 310 let content = templateInfo.content = |
| 311 node.content.ownerDocument.createDocumentFragment(); |
| 312 content.appendChild(node.content); |
| 313 nodeInfo.templateInfo = templateInfo; |
| 314 return true; |
| 315 } |
| 316 |
| 317 /** |
| 318 * Parses template node attributes and adds node metadata to `nodeInfo` |
| 319 * for nodes of interest. |
| 320 * |
| 321 * @param {Node} node Node to parse |
| 322 * @param {Object} templateInfo Template metadata for current template |
| 323 * @param {Object} nodeInfo Node metadata for current template. |
| 324 * @return {boolean} `true` if the visited node added node-specific |
| 325 * metadata to `nodeInfo` |
| 326 */ |
| 327 static _parseTemplateNodeAttributes(node, templateInfo, nodeInfo) { |
| 328 // Make copy of original attribute list, since the order may change |
| 329 // as attributes are added and removed |
| 330 let noted; |
| 331 let attrs = Array.from(node.attributes); |
| 332 for (let i=attrs.length-1, a; (a=attrs[i]); i--) { |
| 333 noted = this._parseTemplateNodeAttribute(node, templateInfo, nodeInfo,
a.name, a.value) || noted; |
| 334 } |
| 335 return noted; |
| 336 } |
| 337 |
| 338 /** |
| 339 * Parses a single template node attribute and adds node metadata to |
| 340 * `nodeInfo` for attributes of interest. |
| 341 * |
| 342 * This implementation adds metadata for `on-event="handler"` attributes |
| 343 * and `id` attributes. |
| 344 * |
| 345 * @param {Node} node Node to parse |
| 346 * @param {Object} templateInfo Template metadata for current template |
| 347 * @param {Object} nodeInfo Node metadata for current template. |
| 348 * @param {string} name Attribute name |
| 349 * @param {*} value Attribute value |
| 350 * @return {boolean} `true` if the visited node added node-specific |
| 351 * metadata to `nodeInfo` |
| 352 */ |
| 353 static _parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, val
ue) { |
| 354 // events (on-*) |
| 355 if (name.slice(0, 3) === 'on-') { |
| 356 node.removeAttribute(name); |
| 357 nodeInfo.events = nodeInfo.events || []; |
| 358 nodeInfo.events.push({ |
| 359 name: name.slice(3), |
| 360 value |
| 361 }); |
| 362 return true; |
| 363 } |
| 364 // static id |
| 365 else if (name === 'id') { |
| 366 nodeInfo.id = value; |
| 367 return true; |
| 368 } |
| 369 } |
| 370 |
| 371 /** |
| 372 * Returns the `content` document fragment for a given template. |
| 373 * |
| 374 * For nested templates, Polymer performs an optimization to cache nested |
| 375 * template content to avoid the cost of cloning deeply nested templates. |
| 376 * This method retrieves the cached content for a given template. |
| 377 * |
| 378 * @param {HTMLTemplateElement} template Template to retrieve `content` fo
r |
| 379 * @return {DocumentFragment} Content fragment |
| 380 */ |
| 381 static _contentForTemplate(template) { |
| 382 let templateInfo = template.__templateInfo; |
| 383 return (templateInfo && templateInfo.content) || template.content; |
| 384 } |
| 385 |
| 386 /** |
| 387 * Clones the provided template content and returns a document fragment |
| 388 * containing the cloned dom. |
| 389 * |
| 390 * The template is parsed (once and memoized) using this library's |
| 391 * template parsing features, and provides the following value-added |
| 392 * features: |
| 393 * * Adds declarative event listeners for `on-event="handler"` attributes |
| 394 * * Generates an "id map" for all nodes with id's under `$` on returned |
| 395 * document fragment |
| 396 * * Passes template info including `content` back to templates as |
| 397 * `_templateInfo` (a performance optimization to avoid deep template |
| 398 * cloning) |
| 399 * |
| 400 * Note that the memoized template parsing process is destructive to the |
| 401 * template: attributes for bindings and declarative event listeners are |
| 402 * removed after being noted in notes, and any nested `<template>.content` |
| 403 * is removed and stored in notes as well. |
| 404 * |
| 405 * @param {HTMLTemplateElement} template Template to stamp |
| 406 * @return {DocumentFragment} Cloned template content |
| 407 */ |
| 408 _stampTemplate(template) { |
| 409 // Polyfill support: bootstrap the template if it has not already been |
| 410 if (template && !template.content && |
| 411 window.HTMLTemplateElement && HTMLTemplateElement.decorate) { |
| 412 HTMLTemplateElement.decorate(template); |
| 413 } |
| 414 let templateInfo = this.constructor._parseTemplate(template); |
| 415 let nodeInfo = templateInfo.nodeInfoList; |
| 416 let content = templateInfo.content || template.content; |
| 417 let dom = document.importNode(content, true); |
| 418 // NOTE: ShadyDom optimization indicating there is an insertion point |
| 419 dom.__noInsertionPoint = !templateInfo.hasInsertionPoint; |
| 420 let nodes = dom.nodeList = new Array(nodeInfo.length); |
| 421 dom.$ = {}; |
| 422 for (let i=0, l=nodeInfo.length, info; (i<l) && (info=nodeInfo[i]); i++)
{ |
| 423 let node = nodes[i] = findTemplateNode(dom, info); |
| 424 applyIdToMap(this, dom.$, node, info); |
| 425 applyTemplateContent(this, node, info); |
| 426 applyEventListener(this, node, info); |
| 427 } |
| 428 return dom; |
| 429 } |
| 430 |
| 431 /** |
| 432 * Adds an event listener by method name for the event provided. |
| 433 * |
| 434 * This method generates a handler function that looks up the method |
| 435 * name at handling time. |
| 436 * |
| 437 * @param {Node} node Node to add listener on |
| 438 * @param {string} eventName Name of event |
| 439 * @param {string} methodName Name of method |
| 440 * @param {*=} context Context the method will be called on (defaults |
| 441 * to `node`) |
| 442 * @return {Function} Generated handler function |
| 443 */ |
| 444 _addMethodEventListenerToNode(node, eventName, methodName, context) { |
| 445 context = context || node; |
| 446 let handler = createNodeEventHandler(context, eventName, methodName); |
| 447 this._addEventListenerToNode(node, eventName, handler); |
| 448 return handler; |
| 449 } |
| 450 |
| 451 /** |
| 452 * Override point for adding custom or simulated event handling. |
| 453 * |
| 454 * @param {Node} node Node to add event listener to |
| 455 * @param {string} eventName Name of event |
| 456 * @param {Function} handler Listener function to add |
| 457 */ |
| 458 _addEventListenerToNode(node, eventName, handler) { |
| 459 node.addEventListener(eventName, handler); |
| 460 } |
| 461 |
| 462 /** |
| 463 * Override point for adding custom or simulated event handling. |
| 464 * |
| 465 * @param {Node} node Node to remove event listener from |
| 466 * @param {string} eventName Name of event |
| 467 * @param {Function} handler Listener function to remove |
| 468 */ |
| 469 _removeEventListenerFromNode(node, eventName, handler) { |
| 470 node.removeEventListener(eventName, handler); |
| 471 } |
| 472 |
| 473 } |
| 474 |
| 475 return TemplateStamp; |
| 476 |
| 477 }); |
| 478 |
| 479 })(); |
| 480 </script> |
| OLD | NEW |