OLD | NEW |
(Empty) | |
| 1 <!-- |
| 2 @license |
| 3 Copyright (c) 2014 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 <link rel="import" href="../module.html"> |
| 11 <link rel="import" href="../case-map.html"> |
| 12 |
| 13 <script> |
| 14 /** |
| 15 * Scans a template to produce an annotation list that that associates |
| 16 * metadata culled from markup with tree locations |
| 17 * metadata and information to associate the metadata with nodes in an instance. |
| 18 * |
| 19 * Supported expressions include: |
| 20 * |
| 21 * Double-mustache annotations in text content. The annotation must be the only |
| 22 * content in the tag, compound expressions are not supported. |
| 23 * |
| 24 * <[tag]>{{annotation}}<[tag]> |
| 25 * |
| 26 * Double-escaped annotations in an attribute, either {{}} or [[]]. |
| 27 * |
| 28 * <[tag] someAttribute="{{annotation}}" another="[[annotation]]"><[tag]> |
| 29 * |
| 30 * `on-` style event declarations. |
| 31 * |
| 32 * <[tag] on-<event-name>="annotation"><[tag]> |
| 33 * |
| 34 * Note that the `annotations` feature does not implement any behaviors |
| 35 * associated with these expressions, it only captures the data. |
| 36 * |
| 37 * Generated data-structure: |
| 38 * |
| 39 * [ |
| 40 * { |
| 41 * id: '<id>', |
| 42 * events: [ |
| 43 * { |
| 44 * name: '<name>' |
| 45 * value: '<annotation>' |
| 46 * }, ... |
| 47 * ], |
| 48 * bindings: [ |
| 49 * { |
| 50 * kind: ['text'|'attribute'], |
| 51 * mode: ['{'|'['], |
| 52 * name: '<name>' |
| 53 * value: '<annotation>' |
| 54 * }, ... |
| 55 * ], |
| 56 * // TODO(sjmiles): this is annotation-parent, not node-parent |
| 57 * parent: <reference to parent annotation object>, |
| 58 * index: <integer index in parent's childNodes collection> |
| 59 * }, |
| 60 * ... |
| 61 * ] |
| 62 * |
| 63 * @class Template feature |
| 64 */ |
| 65 |
| 66 // null-array (shared empty array to avoid null-checks) |
| 67 Polymer.nar = []; |
| 68 |
| 69 Polymer.Annotations = { |
| 70 |
| 71 // preprocess-time |
| 72 |
| 73 // construct and return a list of annotation records |
| 74 // by scanning `template`'s content |
| 75 // |
| 76 parseAnnotations: function(template) { |
| 77 var list = []; |
| 78 var content = template._content || template.content; |
| 79 this._parseNodeAnnotations(content, list); |
| 80 return list; |
| 81 }, |
| 82 |
| 83 // add annotations gleaned from subtree at `node` to `list` |
| 84 _parseNodeAnnotations: function(node, list) { |
| 85 return node.nodeType === Node.TEXT_NODE ? |
| 86 this._parseTextNodeAnnotation(node, list) : |
| 87 // TODO(sjmiles): are there other nodes we may encounter |
| 88 // that are not TEXT_NODE but also not ELEMENT? |
| 89 this._parseElementAnnotations(node, list); |
| 90 }, |
| 91 |
| 92 // add annotations gleaned from TextNode `node` to `list` |
| 93 _parseTextNodeAnnotation: function(node, list) { |
| 94 var v = node.textContent, escape = v.slice(0, 2); |
| 95 if (escape === '{{' || escape === '[[') { |
| 96 // NOTE: use a space here so the textNode remains; some browsers |
| 97 // (IE) evacipate an empty textNode. |
| 98 node.textContent = ' '; |
| 99 var annote = { |
| 100 bindings: [{ |
| 101 kind: 'text', |
| 102 mode: escape[0], |
| 103 value: v.slice(2, -2) |
| 104 }] |
| 105 }; |
| 106 list.push(annote); |
| 107 return annote; |
| 108 } |
| 109 }, |
| 110 |
| 111 // add annotations gleaned from Element `node` to `list` |
| 112 _parseElementAnnotations: function(element, list) { |
| 113 var annote = { |
| 114 bindings: [], |
| 115 events: [] |
| 116 }; |
| 117 this._parseChildNodesAnnotations(element, annote, list); |
| 118 // TODO(sjmiles): is this for non-ELEMENT nodes? If so, we should |
| 119 // change the contract of this method, or filter these out above. |
| 120 if (element.attributes) { |
| 121 this._parseNodeAttributeAnnotations(element, annote, list); |
| 122 // TODO(sorvell): ad hoc callback for doing work on elements while |
| 123 // leveraging annotator's tree walk. |
| 124 // Consider adding an node callback registry and moving specific |
| 125 // processing out of this module. |
| 126 if (this.prepElement) { |
| 127 this.prepElement(element); |
| 128 } |
| 129 } |
| 130 if (annote.bindings.length || annote.events.length || annote.id) { |
| 131 list.push(annote); |
| 132 } |
| 133 return annote; |
| 134 }, |
| 135 |
| 136 // add annotations gleaned from children of `root` to `list`, `root`'s |
| 137 // `annote` is supplied as it is the annote.parent of added annotations |
| 138 _parseChildNodesAnnotations: function(root, annote, list, callback) { |
| 139 if (root.firstChild) { |
| 140 for (var i=0, node=root.firstChild; node; node=node.nextSibling, i++){ |
| 141 if (node.localName === 'template' && |
| 142 !node.hasAttribute('preserve-content')) { |
| 143 this._parseTemplate(node, i, list, annote); |
| 144 } |
| 145 // |
| 146 var childAnnotation = this._parseNodeAnnotations(node, list, callback)
; |
| 147 if (childAnnotation) { |
| 148 childAnnotation.parent = annote; |
| 149 childAnnotation.index = i; |
| 150 } |
| 151 } |
| 152 } |
| 153 }, |
| 154 |
| 155 // 1. Parse annotations from the template and memoize them on |
| 156 // content._notes (recurses into nested templates) |
| 157 // 2. Parse template bindings for parent.* properties and memoize them on |
| 158 // content._parentProps |
| 159 // 3. Create bindings in current scope's annotation list to template for |
| 160 // parent props found in template |
| 161 // 4. Remove template.content and store it in annotation list, where it |
| 162 // will be the responsibility of the host to set it back to the template |
| 163 // (this is both an optimization to avoid re-stamping nested template |
| 164 // children and avoids a bug in Chrome where nested template children |
| 165 // upgrade) |
| 166 _parseTemplate: function(node, index, list, parent) { |
| 167 // TODO(sjmiles): simply altering the .content reference didn't |
| 168 // work (there was some confusion, might need verification) |
| 169 var content = document.createDocumentFragment(); |
| 170 content._notes = this.parseAnnotations(node); |
| 171 content.appendChild(node.content); |
| 172 // Special-case treatment of 'parent.*' props for nested templates |
| 173 // Automatically bind `prop` on host to `_parent_prop` on template |
| 174 // for any `parent.prop`'s encountered in template binding; it is |
| 175 // responsibility of the template implementation to forward |
| 176 // these properties as appropriate |
| 177 var bindings = []; |
| 178 this._discoverTemplateParentProps(content); |
| 179 for (var prop in content._parentProps) { |
| 180 bindings.push({ |
| 181 index: index, |
| 182 kind: 'property', |
| 183 mode: '{', |
| 184 name: '_parent_' + prop, |
| 185 value: prop |
| 186 }); |
| 187 } |
| 188 // TODO(sjmiles): using `nar` to avoid unnecessary allocation; |
| 189 // in general the handling of these arrays needs some cleanup |
| 190 // in this module |
| 191 list.push({ |
| 192 bindings: bindings, |
| 193 events: Polymer.nar, |
| 194 templateContent: content, |
| 195 parent: parent, |
| 196 index: index |
| 197 }); |
| 198 }, |
| 199 |
| 200 // Finds all parent.* properties in template content and stores |
| 201 // the path members in content._parentPropChain, which is an array |
| 202 // of maps listing the properties of parent templates required at |
| 203 // each level. Each outer template merges inner _parentPropChains to |
| 204 // propagate inner parent property needs to outer templates. |
| 205 // The top-level parent props from the chain (corresponding to this |
| 206 // template) are stored in content._parentProps. |
| 207 _discoverTemplateParentProps: function(content) { |
| 208 var chain = content._parentPropChain = []; |
| 209 content._notes.forEach(function(n) { |
| 210 // Find all bindings to parent.* and spread them into _parentPropChain |
| 211 n.bindings.forEach(function(b) { |
| 212 var m; |
| 213 if (m = b.value.match(/parent\.((parent\.)*[^.]*)/)) { |
| 214 var parts = m[1].split('.'); |
| 215 for (var i=0; i<parts.length; i++) { |
| 216 var pp = chain[i] || (chain[i] = {}); |
| 217 pp[parts[i]] = true; |
| 218 } |
| 219 } |
| 220 }); |
| 221 // Merge child _parentPropChain[n+1] into this _parentPropChain[n] |
| 222 if (n.templateContent) { |
| 223 var tpp = n.templateContent._parentPropChain; |
| 224 for (var i=1; i<tpp.length; i++) { |
| 225 if (tpp[i]) { |
| 226 var pp = chain[i-1] || (chain[i-1] = {}); |
| 227 Polymer.Base.simpleMixin(pp, tpp[i]); |
| 228 } |
| 229 } |
| 230 } |
| 231 }); |
| 232 // Store this template's parentProps map |
| 233 content._parentProps = chain[0]; |
| 234 }, |
| 235 |
| 236 // add annotation data from attributes to the `annotation` for node `node` |
| 237 // TODO(sjmiles): the distinction between an `annotation` and |
| 238 // `annotation data` is not as clear as it could be |
| 239 // Walk attributes backwards, since removeAttribute can be vetoed by |
| 240 // IE in certain cases (e.g. <input value="foo">), resulting in the |
| 241 // attribute staying in the attributes list |
| 242 _parseNodeAttributeAnnotations: function(node, annotation) { |
| 243 for (var i=node.attributes.length-1, a; (a=node.attributes[i]); i--) { |
| 244 var n = a.name, v = a.value; |
| 245 // id |
| 246 if (n === 'id') { |
| 247 annotation.id = v; |
| 248 } |
| 249 // events (on-*) |
| 250 else if (n.slice(0, 3) === 'on-') { |
| 251 node.removeAttribute(n); |
| 252 annotation.events.push({ |
| 253 name: n.slice(3), |
| 254 value: v |
| 255 }); |
| 256 } |
| 257 // bindings (other attributes) |
| 258 else { |
| 259 var b = this._parseNodeAttributeAnnotation(node, n, v); |
| 260 if (b) { |
| 261 annotation.bindings.push(b); |
| 262 } |
| 263 } |
| 264 } |
| 265 }, |
| 266 |
| 267 // construct annotation data from a generic attribute, or undefined |
| 268 _parseNodeAttributeAnnotation: function(node, n, v) { |
| 269 var mode = '', escape = v.slice(0, 2), name = n; |
| 270 if (escape === '{{' || escape === '[[') { |
| 271 // Mode (one-way or two) |
| 272 mode = escape[0]; |
| 273 v = v.slice(2, -2); |
| 274 // Negate |
| 275 var not = false; |
| 276 if (v[0] == '!') { |
| 277 v = v.substring(1); |
| 278 not = true; |
| 279 } |
| 280 // Attribute or property |
| 281 var kind = 'property'; |
| 282 if (n[n.length-1] == '$') { |
| 283 name = n.slice(0, -1); |
| 284 kind = 'attribute'; |
| 285 } |
| 286 // Custom notification event |
| 287 var notifyEvent, colon; |
| 288 if (mode == '{' && (colon = v.indexOf('::')) > 0) { |
| 289 notifyEvent = v.substring(colon + 2); |
| 290 v = v.substring(0, colon); |
| 291 } |
| 292 // Remove annotation |
| 293 node.removeAttribute(n); |
| 294 // Case hackery: attributes are lower-case, but bind targets |
| 295 // (properties) are case sensitive. Gambit is to map dash-case to |
| 296 // camel-case: `foo-bar` becomes `fooBar`. |
| 297 // Attribute bindings are excepted. |
| 298 if (kind === 'property') { |
| 299 name = Polymer.CaseMap.dashToCamelCase(name); |
| 300 } |
| 301 return { |
| 302 kind: kind, |
| 303 mode: mode, |
| 304 name: name, |
| 305 value: v, |
| 306 negate: not, |
| 307 event: notifyEvent |
| 308 }; |
| 309 } |
| 310 }, |
| 311 |
| 312 // instance-time |
| 313 |
| 314 _localSubTree: function(node, host) { |
| 315 return (node === host) ? node.childNodes : |
| 316 (node.lightChildren || node.childNodes); |
| 317 }, |
| 318 |
| 319 findAnnotatedNode: function(root, annote) { |
| 320 // recursively ascend tree until we hit root |
| 321 var parent = annote.parent && |
| 322 Polymer.Annotations.findAnnotatedNode(root, annote.parent); |
| 323 // unwind the stack, returning the indexed node at each level |
| 324 return !parent ? root : |
| 325 Polymer.Annotations._localSubTree(parent, root)[annote.index]; |
| 326 } |
| 327 |
| 328 }; |
| 329 |
| 330 |
| 331 </script> |
OLD | NEW |