| OLD | NEW |
| 1 /// The Dart HTML library. | 1 /// The Dart HTML library. |
| 2 library dart.dom.html; | 2 library dart.dom.html; |
| 3 | 3 |
| 4 import 'dart:async'; | 4 import 'dart:async'; |
| 5 import 'dart:collection'; | 5 import 'dart:collection'; |
| 6 import 'dart:_collection-dev' hide Symbol; | 6 import 'dart:_collection-dev' hide Symbol; |
| 7 import 'dart:html_common'; | 7 import 'dart:html_common'; |
| 8 import 'dart:indexed_db'; | 8 import 'dart:indexed_db'; |
| 9 import 'dart:isolate'; | 9 import 'dart:isolate'; |
| 10 import 'dart:json' as json; | 10 import 'dart:json' as json; |
| (...skipping 7619 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 7630 self._attributeBindings = new Map<String, StreamSubscription>(); | 7630 self._attributeBindings = new Map<String, StreamSubscription>(); |
| 7631 } | 7631 } |
| 7632 | 7632 |
| 7633 self.xtag.attributes.remove(name); | 7633 self.xtag.attributes.remove(name); |
| 7634 | 7634 |
| 7635 var changed; | 7635 var changed; |
| 7636 if (name.endsWith('?')) { | 7636 if (name.endsWith('?')) { |
| 7637 name = name.substring(0, name.length - 1); | 7637 name = name.substring(0, name.length - 1); |
| 7638 | 7638 |
| 7639 changed = (value) { | 7639 changed = (value) { |
| 7640 if (_templateBooleanConversion(value)) { | 7640 if (_Bindings._toBoolean(value)) { |
| 7641 self.xtag.attributes[name] = ''; | 7641 self.xtag.attributes[name] = ''; |
| 7642 } else { | 7642 } else { |
| 7643 self.xtag.attributes.remove(name); | 7643 self.xtag.attributes.remove(name); |
| 7644 } | 7644 } |
| 7645 }; | 7645 }; |
| 7646 } else { | 7646 } else { |
| 7647 changed = (value) { | 7647 changed = (value) { |
| 7648 // TODO(jmesserly): escape value if needed to protect against XSS. | 7648 // TODO(jmesserly): escape value if needed to protect against XSS. |
| 7649 // See https://github.com/toolkitchen/mdv/issues/58 | 7649 // See https://github.com/toolkitchen/mdv/issues/58 |
| 7650 self.xtag.attributes[name] = value == null ? '' : '$value'; | 7650 self.xtag.attributes[name] = value == null ? '' : '$value'; |
| (...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 7763 * Creates an instance of the template. | 7763 * Creates an instance of the template. |
| 7764 * This is only supported if [isTemplate] is true. | 7764 * This is only supported if [isTemplate] is true. |
| 7765 */ | 7765 */ |
| 7766 @Experimental | 7766 @Experimental |
| 7767 DocumentFragment createInstance() { | 7767 DocumentFragment createInstance() { |
| 7768 _ensureTemplate(); | 7768 _ensureTemplate(); |
| 7769 | 7769 |
| 7770 var template = ref; | 7770 var template = ref; |
| 7771 if (template == null) template = this; | 7771 if (template == null) template = this; |
| 7772 | 7772 |
| 7773 var instance = _createDeepCloneAndDecorateTemplates(template.content, | 7773 var instance = _Bindings._createDeepCloneAndDecorateTemplates( |
| 7774 attributes['syntax']); | 7774 template.content, attributes['syntax']); |
| 7775 | 7775 |
| 7776 if (TemplateElement._instanceCreated != null) { | 7776 if (TemplateElement._instanceCreated != null) { |
| 7777 TemplateElement._instanceCreated.add(instance); | 7777 TemplateElement._instanceCreated.add(instance); |
| 7778 } | 7778 } |
| 7779 return instance; | 7779 return instance; |
| 7780 } | 7780 } |
| 7781 | 7781 |
| 7782 /** | 7782 /** |
| 7783 * The data model which is inherited through the tree. | 7783 * The data model which is inherited through the tree. |
| 7784 * This is only supported if [isTemplate] is true. | 7784 * This is only supported if [isTemplate] is true. |
| 7785 * | 7785 * |
| 7786 * Setting this will destructive propagate the value to all descendant nodes, | 7786 * Setting this will destructive propagate the value to all descendant nodes, |
| 7787 * and reinstantiate all of the nodes expanded by this template. | 7787 * and reinstantiate all of the nodes expanded by this template. |
| 7788 * | 7788 * |
| 7789 * Currently this does not support propagation through Shadow DOMs. | 7789 * Currently this does not support propagation through Shadow DOMs. |
| 7790 */ | 7790 */ |
| 7791 @Experimental | 7791 @Experimental |
| 7792 get model => _model; | 7792 get model => _model; |
| 7793 | 7793 |
| 7794 @Experimental | 7794 @Experimental |
| 7795 void set model(value) { | 7795 void set model(value) { |
| 7796 _ensureTemplate(); | 7796 _ensureTemplate(); |
| 7797 | 7797 |
| 7798 var syntax = TemplateElement.syntax[attributes['syntax']]; | 7798 var syntax = TemplateElement.syntax[attributes['syntax']]; |
| 7799 _model = value; | 7799 _model = value; |
| 7800 _addBindings(this, model, syntax); | 7800 _Bindings._addBindings(this, model, syntax); |
| 7801 } | 7801 } |
| 7802 | 7802 |
| 7803 // TODO(jmesserly): const set would be better | 7803 // TODO(jmesserly): const set would be better |
| 7804 static const _TABLE_TAGS = const { | 7804 static const _TABLE_TAGS = const { |
| 7805 'caption': null, | 7805 'caption': null, |
| 7806 'col': null, | 7806 'col': null, |
| 7807 'colgroup': null, | 7807 'colgroup': null, |
| 7808 'tbody': null, | 7808 'tbody': null, |
| 7809 'td': null, | 7809 'td': null, |
| 7810 'tfoot': null, | 7810 'tfoot': null, |
| (...skipping 11513 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 19324 static bool decorate(Element template, [Element instanceRef]) { | 19324 static bool decorate(Element template, [Element instanceRef]) { |
| 19325 // == true check because it starts as a null field. | 19325 // == true check because it starts as a null field. |
| 19326 if (template._templateIsDecorated == true) return false; | 19326 if (template._templateIsDecorated == true) return false; |
| 19327 | 19327 |
| 19328 template._templateIsDecorated = true; | 19328 template._templateIsDecorated = true; |
| 19329 | 19329 |
| 19330 _injectStylesheet(); | 19330 _injectStylesheet(); |
| 19331 | 19331 |
| 19332 // Create content | 19332 // Create content |
| 19333 if (template is! TemplateElement) { | 19333 if (template is! TemplateElement) { |
| 19334 var doc = _getTemplateContentsOwner(template.document); | 19334 var doc = _Bindings._getTemplateContentsOwner(template.document); |
| 19335 template._templateContent = doc.createDocumentFragment(); | 19335 template._templateContent = doc.createDocumentFragment(); |
| 19336 } | 19336 } |
| 19337 | 19337 |
| 19338 if (instanceRef != null) { | 19338 if (instanceRef != null) { |
| 19339 template._templateInstanceRef = instanceRef; | 19339 template._templateInstanceRef = instanceRef; |
| 19340 return true; // content is empty. | 19340 return true; // content is empty. |
| 19341 } | 19341 } |
| 19342 | 19342 |
| 19343 if (template is TemplateElement) { | 19343 if (template is TemplateElement) { |
| 19344 _bootstrapTemplatesRecursivelyFrom(template.content); | 19344 bootstrap(template.content); |
| 19345 } else { | 19345 } else { |
| 19346 _liftNonNativeTemplateChildrenIntoContent(template); | 19346 _Bindings._liftNonNativeChildrenIntoContent(template); |
| 19347 } | 19347 } |
| 19348 | 19348 |
| 19349 return true; | 19349 return true; |
| 19350 } | 19350 } |
| 19351 | 19351 |
| 19352 /** | 19352 /** |
| 19353 * This used to decorate recursively all templates from a given node. | 19353 * This used to decorate recursively all templates from a given node. |
| 19354 * | 19354 * |
| 19355 * By default [decorate] will be called on templates lazily when certain | 19355 * By default [decorate] will be called on templates lazily when certain |
| 19356 * properties such as [model] are accessed, but it can be run eagerly to | 19356 * properties such as [model] are accessed, but it can be run eagerly to |
| 19357 * decorate an entire tree recursively. | 19357 * decorate an entire tree recursively. |
| 19358 */ | 19358 */ |
| 19359 // TODO(rafaelw): Review whether this is the right public API. | 19359 // TODO(rafaelw): Review whether this is the right public API. |
| 19360 @Experimental | 19360 @Experimental |
| 19361 static void bootstrap(Node content) { | 19361 static void bootstrap(Node content) { |
| 19362 _bootstrapTemplatesRecursivelyFrom(content); | 19362 _Bindings._bootstrapTemplatesRecursivelyFrom(content); |
| 19363 } | 19363 } |
| 19364 | 19364 |
| 19365 static bool _initStyles; | 19365 static bool _initStyles; |
| 19366 | 19366 |
| 19367 static void _injectStylesheet() { | 19367 static void _injectStylesheet() { |
| 19368 if (_initStyles == true) return; | 19368 if (_initStyles == true) return; |
| 19369 _initStyles = true; | 19369 _initStyles = true; |
| 19370 | 19370 |
| 19371 var style = new StyleElement(); | 19371 var style = new StyleElement(); |
| 19372 style.text = r''' | 19372 style.text = r''' |
| (...skipping 6831 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 26204 binding.cancel(); | 26204 binding.cancel(); |
| 26205 } | 26205 } |
| 26206 _bindings.clear(); | 26206 _bindings.clear(); |
| 26207 _values.clear(); | 26207 _values.clear(); |
| 26208 | 26208 |
| 26209 _disposed = true; | 26209 _disposed = true; |
| 26210 value = null; | 26210 value = null; |
| 26211 } | 26211 } |
| 26212 } | 26212 } |
| 26213 | 26213 |
| 26214 Stream<Event> _getStreamForInputType(InputElement element) { | |
| 26215 switch (element.type) { | |
| 26216 case 'checkbox': | |
| 26217 return element.onClick; | |
| 26218 case 'radio': | |
| 26219 case 'select-multiple': | |
| 26220 case 'select-one': | |
| 26221 return element.onChange; | |
| 26222 default: | |
| 26223 return element.onInput; | |
| 26224 } | |
| 26225 } | |
| 26226 | |
| 26227 abstract class _InputBinding { | 26214 abstract class _InputBinding { |
| 26228 final InputElement element; | 26215 final InputElement element; |
| 26229 PathObserver binding; | 26216 PathObserver binding; |
| 26230 StreamSubscription _pathSub; | 26217 StreamSubscription _pathSub; |
| 26231 StreamSubscription _eventSub; | 26218 StreamSubscription _eventSub; |
| 26232 | 26219 |
| 26233 _InputBinding(this.element, model, String path) { | 26220 _InputBinding(this.element, model, String path) { |
| 26234 binding = new PathObserver(model, path); | 26221 binding = new PathObserver(model, path); |
| 26235 _pathSub = binding.bindSync(valueChanged); | 26222 _pathSub = binding.bindSync(valueChanged); |
| 26236 _eventSub = _getStreamForInputType(element).listen(updateBinding); | 26223 _eventSub = _getStreamForInputType(element).listen(updateBinding); |
| 26237 } | 26224 } |
| 26238 | 26225 |
| 26239 void valueChanged(newValue); | 26226 void valueChanged(newValue); |
| 26240 | 26227 |
| 26241 void updateBinding(e); | 26228 void updateBinding(e); |
| 26242 | 26229 |
| 26243 void unbind() { | 26230 void unbind() { |
| 26244 binding = null; | 26231 binding = null; |
| 26245 _pathSub.cancel(); | 26232 _pathSub.cancel(); |
| 26246 _eventSub.cancel(); | 26233 _eventSub.cancel(); |
| 26247 } | 26234 } |
| 26235 |
| 26236 |
| 26237 static Stream<Event> _getStreamForInputType(InputElement element) { |
| 26238 switch (element.type) { |
| 26239 case 'checkbox': |
| 26240 return element.onClick; |
| 26241 case 'radio': |
| 26242 case 'select-multiple': |
| 26243 case 'select-one': |
| 26244 return element.onChange; |
| 26245 default: |
| 26246 return element.onInput; |
| 26247 } |
| 26248 } |
| 26248 } | 26249 } |
| 26249 | 26250 |
| 26250 class _ValueBinding extends _InputBinding { | 26251 class _ValueBinding extends _InputBinding { |
| 26251 _ValueBinding(element, model, path) : super(element, model, path); | 26252 _ValueBinding(element, model, path) : super(element, model, path); |
| 26252 | 26253 |
| 26253 void valueChanged(value) { | 26254 void valueChanged(value) { |
| 26254 element.value = value == null ? '' : '$value'; | 26255 element.value = value == null ? '' : '$value'; |
| 26255 } | 26256 } |
| 26256 | 26257 |
| 26257 void updateBinding(e) { | 26258 void updateBinding(e) { |
| 26258 binding.value = element.value; | 26259 binding.value = element.value; |
| 26259 } | 26260 } |
| 26260 } | 26261 } |
| 26261 | 26262 |
| 26262 // TODO(jmesserly): not sure what kind of boolean conversion rules to | |
| 26263 // apply for template data-binding. HTML attributes are true if they're present. | |
| 26264 // However Dart only treats "true" as true. Since this is HTML we'll use | |
| 26265 // something closer to the HTML rules: null (missing) and false are false, | |
| 26266 // everything else is true. See: https://github.com/toolkitchen/mdv/issues/59 | |
| 26267 bool _templateBooleanConversion(value) => null != value && false != value; | |
| 26268 | |
| 26269 class _CheckedBinding extends _InputBinding { | 26263 class _CheckedBinding extends _InputBinding { |
| 26270 _CheckedBinding(element, model, path) : super(element, model, path); | 26264 _CheckedBinding(element, model, path) : super(element, model, path); |
| 26271 | 26265 |
| 26272 void valueChanged(value) { | 26266 void valueChanged(value) { |
| 26273 element.checked = _templateBooleanConversion(value); | 26267 element.checked = _Bindings._toBoolean(value); |
| 26274 } | 26268 } |
| 26275 | 26269 |
| 26276 void updateBinding(e) { | 26270 void updateBinding(e) { |
| 26277 binding.value = element.checked; | 26271 binding.value = element.checked; |
| 26278 | 26272 |
| 26279 // Only the radio button that is getting checked gets an event. We | 26273 // Only the radio button that is getting checked gets an event. We |
| 26280 // therefore find all the associated radio buttons and update their | 26274 // therefore find all the associated radio buttons and update their |
| 26281 // CheckedBinding manually. | 26275 // CheckedBinding manually. |
| 26282 if (element is InputElement && element.type == 'radio') { | 26276 if (element is InputElement && element.type == 'radio') { |
| 26283 for (var r in _getAssociatedRadioButtons(element)) { | 26277 for (var r in _getAssociatedRadioButtons(element)) { |
| 26284 var checkedBinding = r._checkedBinding; | 26278 var checkedBinding = r._checkedBinding; |
| 26285 if (checkedBinding != null) { | 26279 if (checkedBinding != null) { |
| 26286 // Set the value directly to avoid an infinite call stack. | 26280 // Set the value directly to avoid an infinite call stack. |
| 26287 checkedBinding.binding.value = false; | 26281 checkedBinding.binding.value = false; |
| 26288 } | 26282 } |
| 26289 } | 26283 } |
| 26290 } | 26284 } |
| 26291 } | 26285 } |
| 26286 |
| 26287 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. |
| 26288 // Returns an array containing all radio buttons other than |element| that |
| 26289 // have the same |name|, either in the form that |element| belongs to or, |
| 26290 // if no form, in the document tree to which |element| belongs. |
| 26291 // |
| 26292 // This implementation is based upon the HTML spec definition of a |
| 26293 // "radio button group": |
| 26294 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.
html#radio-button-group |
| 26295 // |
| 26296 static Iterable _getAssociatedRadioButtons(element) { |
| 26297 if (!_isNodeInDocument(element)) return []; |
| 26298 if (element.form != null) { |
| 26299 return element.form.nodes.where((el) { |
| 26300 return el != element && |
| 26301 el is InputElement && |
| 26302 el.type == 'radio' && |
| 26303 el.name == element.name; |
| 26304 }); |
| 26305 } else { |
| 26306 var radios = element.document.queryAll( |
| 26307 'input[type="radio"][name="${element.name}"]'); |
| 26308 return radios.where((el) => el != element && el.form == null); |
| 26309 } |
| 26310 } |
| 26311 |
| 26312 // TODO(jmesserly): polyfill document.contains API instead of doing it here |
| 26313 static bool _isNodeInDocument(Node node) { |
| 26314 // On non-IE this works: |
| 26315 // return node.document.contains(node); |
| 26316 var document = node.document; |
| 26317 if (node == document || node.parentNode == document) return true; |
| 26318 return document.documentElement.contains(node); |
| 26319 } |
| 26292 } | 26320 } |
| 26293 | 26321 |
| 26294 // TODO(jmesserly): polyfill document.contains API instead of doing it here | 26322 class _Bindings { |
| 26295 bool _isNodeInDocument(Node node) { | 26323 // TODO(jmesserly): not sure what kind of boolean conversion rules to |
| 26296 // On non-IE this works: | 26324 // apply for template data-binding. HTML attributes are true if they're |
| 26297 // return node.document.contains(node); | 26325 // present. However Dart only treats "true" as true. Since this is HTML we'll |
| 26298 var document = node.document; | 26326 // use something closer to the HTML rules: null (missing) and false are false, |
| 26299 if (node == document || node.parentNode == document) return true; | 26327 // everything else is true. See: https://github.com/toolkitchen/mdv/issues/59 |
| 26300 return document.documentElement.contains(node); | 26328 static bool _toBoolean(value) => null != value && false != value; |
| 26301 } | 26329 |
| 26302 | 26330 static Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) { |
| 26303 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. | 26331 var clone = node.clone(false); // Shallow clone. |
| 26304 // Returns an array containing all radio buttons other than |element| that | 26332 if (clone is Element && clone.isTemplate) { |
| 26305 // have the same |name|, either in the form that |element| belongs to or, | 26333 TemplateElement.decorate(clone, node); |
| 26306 // if no form, in the document tree to which |element| belongs. | 26334 if (syntax != null) { |
| 26307 // | 26335 clone.attributes.putIfAbsent('syntax', () => syntax); |
| 26308 // This implementation is based upon the HTML spec definition of a | 26336 } |
| 26309 // "radio button group": | 26337 } |
| 26310 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.ht
ml#radio-button-group | 26338 |
| 26311 // | 26339 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 26312 Iterable _getAssociatedRadioButtons(element) { | 26340 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax)); |
| 26313 if (!_isNodeInDocument(element)) return []; | 26341 } |
| 26314 if (element.form != null) { | 26342 return clone; |
| 26315 return element.form.nodes.where((el) { | 26343 } |
| 26316 return el != element && | 26344 |
| 26317 el is InputElement && | 26345 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#
dfn-template-contents-owner |
| 26318 el.type == 'radio' && | 26346 static Document _getTemplateContentsOwner(Document doc) { |
| 26319 el.name == element.name; | 26347 if (doc.window == null) { |
| 26320 }); | 26348 return doc; |
| 26321 } else { | 26349 } |
| 26322 var radios = element.document.queryAll( | 26350 var d = doc._templateContentsOwner; |
| 26323 'input[type="radio"][name="${element.name}"]'); | 26351 if (d == null) { |
| 26324 return radios.where((el) => el != element && el.form == null); | 26352 // TODO(arv): This should either be a Document or HTMLDocument depending |
| 26325 } | 26353 // on doc. |
| 26326 } | 26354 d = doc.implementation.createHtmlDocument(''); |
| 26327 | 26355 while (d.$dom_lastChild != null) { |
| 26328 Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) { | 26356 d.$dom_lastChild.remove(); |
| 26329 var clone = node.clone(false); // Shallow clone. | 26357 } |
| 26330 if (clone is Element && clone.isTemplate) { | 26358 doc._templateContentsOwner = d; |
| 26331 TemplateElement.decorate(clone, node); | 26359 } |
| 26332 if (syntax != null) { | 26360 return d; |
| 26333 clone.attributes.putIfAbsent('syntax', () => syntax); | 26361 } |
| 26334 } | 26362 |
| 26335 } | 26363 static Element _cloneAndSeperateAttributeTemplate(Element templateElement) { |
| 26336 | 26364 var clone = templateElement.clone(false); |
| 26337 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { | 26365 var attributes = templateElement.attributes; |
| 26338 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax)); | 26366 for (var name in attributes.keys.toList()) { |
| 26339 } | 26367 switch (name) { |
| 26340 return clone; | 26368 case 'template': |
| 26341 } | 26369 case 'repeat': |
| 26342 | 26370 case 'bind': |
| 26343 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#df
n-template-contents-owner | 26371 case 'ref': |
| 26344 Document _getTemplateContentsOwner(Document doc) { | 26372 clone.attributes.remove(name); |
| 26345 if (doc.window == null) { | 26373 break; |
| 26346 return doc; | 26374 default: |
| 26347 } | 26375 attributes.remove(name); |
| 26348 var d = doc._templateContentsOwner; | 26376 break; |
| 26349 if (d == null) { | 26377 } |
| 26350 // TODO(arv): This should either be a Document or HTMLDocument depending | 26378 } |
| 26351 // on doc. | 26379 |
| 26352 d = doc.implementation.createHtmlDocument(''); | 26380 return clone; |
| 26353 while (d.$dom_lastChild != null) { | 26381 } |
| 26354 d.$dom_lastChild.remove(); | 26382 |
| 26355 } | 26383 static void _liftNonNativeChildrenIntoContent(Element templateElement) { |
| 26356 doc._templateContentsOwner = d; | 26384 var content = templateElement.content; |
| 26357 } | 26385 |
| 26358 return d; | 26386 if (!templateElement._isAttributeTemplate) { |
| 26359 } | 26387 var child; |
| 26360 | 26388 while ((child = templateElement.$dom_firstChild) != null) { |
| 26361 Element _cloneAndSeperateAttributeTemplate(Element templateElement) { | 26389 content.append(child); |
| 26362 var clone = templateElement.clone(false); | 26390 } |
| 26363 var attributes = templateElement.attributes; | 26391 return; |
| 26364 for (var name in attributes.keys.toList()) { | 26392 } |
| 26365 switch (name) { | 26393 |
| 26366 case 'template': | 26394 // For attribute templates we copy the whole thing into the content and |
| 26367 case 'repeat': | 26395 // we move the non template attributes into the content. |
| 26368 case 'bind': | 26396 // |
| 26369 case 'ref': | 26397 // <tr foo template> |
| 26370 clone.attributes.remove(name); | 26398 // |
| 26371 break; | 26399 // becomes |
| 26372 default: | 26400 // |
| 26373 attributes.remove(name); | 26401 // <tr template> |
| 26374 break; | 26402 // + #document-fragment |
| 26375 } | 26403 // + <tr foo> |
| 26376 } | 26404 // |
| 26377 | 26405 var newRoot = _cloneAndSeperateAttributeTemplate(templateElement); |
| 26378 return clone; | |
| 26379 } | |
| 26380 | |
| 26381 void _liftNonNativeTemplateChildrenIntoContent(Element templateElement) { | |
| 26382 var content = templateElement.content; | |
| 26383 | |
| 26384 if (!templateElement._isAttributeTemplate) { | |
| 26385 var child; | 26406 var child; |
| 26386 while ((child = templateElement.$dom_firstChild) != null) { | 26407 while ((child = templateElement.$dom_firstChild) != null) { |
| 26387 content.append(child); | 26408 newRoot.append(child); |
| 26388 } | 26409 } |
| 26389 return; | 26410 content.append(newRoot); |
| 26390 } | 26411 } |
| 26391 | 26412 |
| 26392 // For attribute templates we copy the whole thing into the content and | 26413 static void _bootstrapTemplatesRecursivelyFrom(Node node) { |
| 26393 // we move the non template attributes into the content. | 26414 void bootstrap(template) { |
| 26394 // | 26415 if (!TemplateElement.decorate(template)) { |
| 26395 // <tr foo template> | 26416 _bootstrapTemplatesRecursivelyFrom(template.content); |
| 26396 // | 26417 } |
| 26397 // becomes | 26418 } |
| 26398 // | 26419 |
| 26399 // <tr template> | 26420 // Need to do this first as the contents may get lifted if |node| is |
| 26400 // + #document-fragment | 26421 // template. |
| 26401 // + <tr foo> | 26422 // TODO(jmesserly): node is DocumentFragment or Element |
| 26402 // | 26423 var descendents = (node as dynamic).queryAll(_allTemplatesSelectors); |
| 26403 var newRoot = _cloneAndSeperateAttributeTemplate(templateElement); | 26424 if (node is Element && node.isTemplate) bootstrap(node); |
| 26404 var child; | 26425 |
| 26405 while ((child = templateElement.$dom_firstChild) != null) { | 26426 descendents.forEach(bootstrap); |
| 26406 newRoot.append(child); | 26427 } |
| 26407 } | 26428 |
| 26408 content.append(newRoot); | 26429 static final String _allTemplatesSelectors = 'template, option[template], ' + |
| 26409 } | 26430 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); |
| 26410 | 26431 |
| 26411 void _bootstrapTemplatesRecursivelyFrom(Node node) { | 26432 static void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { |
| 26412 void bootstrap(template) { | 26433 if (node is Element) { |
| 26413 if (!TemplateElement.decorate(template)) { | 26434 _addAttributeBindings(node, model, syntax); |
| 26414 _bootstrapTemplatesRecursivelyFrom(template.content); | 26435 } else if (node is Text) { |
| 26415 } | 26436 _parseAndBind(node, 'text', node.text, model, syntax); |
| 26416 } | 26437 } |
| 26417 | 26438 |
| 26418 // Need to do this first as the contents may get lifted if |node| is | 26439 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 26419 // template. | 26440 _addBindings(c, model, syntax); |
| 26420 // TODO(jmesserly): node is DocumentFragment or Element | 26441 } |
| 26421 var templateDescendents = (node as dynamic).queryAll(_allTemplatesSelectors); | 26442 } |
| 26422 if (node is Element && node.isTemplate) bootstrap(node); | 26443 |
| 26423 | 26444 static void _addAttributeBindings(Element element, model, syntax) { |
| 26424 templateDescendents.forEach(bootstrap); | 26445 element.attributes.forEach((name, value) { |
| 26425 } | 26446 if (value == '' && (name == 'bind' || name == 'repeat')) { |
| 26426 | 26447 value = '{{}}'; |
| 26427 final String _allTemplatesSelectors = 'template, option[template], ' + | 26448 } |
| 26428 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); | 26449 _parseAndBind(element, name, value, model, syntax); |
| 26429 | 26450 }); |
| 26430 void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { | 26451 } |
| 26431 if (node is Element) { | 26452 |
| 26432 _addAttributeBindings(node, model, syntax); | 26453 static void _parseAndBind(Node node, String name, String text, model, |
| 26433 } else if (node is Text) { | 26454 CustomBindingSyntax syntax) { |
| 26434 _parseAndBind(node, 'text', node.text, model, syntax); | 26455 |
| 26435 } | 26456 var tokens = _parseMustacheTokens(text); |
| 26436 | 26457 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { |
| 26437 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { | 26458 return; |
| 26438 _addBindings(c, model, syntax); | 26459 } |
| 26439 } | 26460 |
| 26440 } | 26461 if (tokens.length == 1 && tokens[0].isBinding) { |
| 26441 | 26462 _bindOrDelegate(node, name, model, tokens[0].value, syntax); |
| 26442 | 26463 return; |
| 26443 void _addAttributeBindings(Element element, model, syntax) { | 26464 } |
| 26444 element.attributes.forEach((name, value) { | 26465 |
| 26445 if (value == '' && (name == 'bind' || name == 'repeat')) { | 26466 var replacementBinding = new CompoundBinding(); |
| 26446 value = '{{}}'; | |
| 26447 } | |
| 26448 _parseAndBind(element, name, value, model, syntax); | |
| 26449 }); | |
| 26450 } | |
| 26451 | |
| 26452 void _parseAndBind(Node node, String name, String text, model, | |
| 26453 CustomBindingSyntax syntax) { | |
| 26454 | |
| 26455 var tokens = _parseMustacheTokens(text); | |
| 26456 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { | |
| 26457 return; | |
| 26458 } | |
| 26459 | |
| 26460 if (tokens.length == 1 && tokens[0].isBinding) { | |
| 26461 _bindOrDelegate(node, name, model, tokens[0].value, syntax); | |
| 26462 return; | |
| 26463 } | |
| 26464 | |
| 26465 var replacementBinding = new CompoundBinding(); | |
| 26466 for (var i = 0; i < tokens.length; i++) { | |
| 26467 var token = tokens[i]; | |
| 26468 if (token.isBinding) { | |
| 26469 _bindOrDelegate(replacementBinding, i, model, token.value, syntax); | |
| 26470 } | |
| 26471 } | |
| 26472 | |
| 26473 replacementBinding.combinator = (values) { | |
| 26474 var newValue = new StringBuffer(); | |
| 26475 | |
| 26476 for (var i = 0; i < tokens.length; i++) { | 26467 for (var i = 0; i < tokens.length; i++) { |
| 26477 var token = tokens[i]; | 26468 var token = tokens[i]; |
| 26478 if (token.isText) { | 26469 if (token.isBinding) { |
| 26479 newValue.write(token.value); | 26470 _bindOrDelegate(replacementBinding, i, model, token.value, syntax); |
| 26471 } |
| 26472 } |
| 26473 |
| 26474 replacementBinding.combinator = (values) { |
| 26475 var newValue = new StringBuffer(); |
| 26476 |
| 26477 for (var i = 0; i < tokens.length; i++) { |
| 26478 var token = tokens[i]; |
| 26479 if (token.isText) { |
| 26480 newValue.write(token.value); |
| 26481 } else { |
| 26482 var value = values[i]; |
| 26483 if (value != null) { |
| 26484 newValue.write(value); |
| 26485 } |
| 26486 } |
| 26487 } |
| 26488 |
| 26489 return newValue.toString(); |
| 26490 }; |
| 26491 |
| 26492 _nodeOrCustom(node).bind(name, replacementBinding, 'value'); |
| 26493 } |
| 26494 |
| 26495 static void _bindOrDelegate(node, name, model, String path, |
| 26496 CustomBindingSyntax syntax) { |
| 26497 |
| 26498 if (syntax != null) { |
| 26499 var delegateBinding = syntax.getBinding(model, path, name, node); |
| 26500 if (delegateBinding != null) { |
| 26501 model = delegateBinding; |
| 26502 path = 'value'; |
| 26503 } |
| 26504 } |
| 26505 |
| 26506 _nodeOrCustom(node).bind(name, model, path); |
| 26507 } |
| 26508 |
| 26509 /** |
| 26510 * Gets the [node]'s custom [Element.xtag] if present, otherwise returns |
| 26511 * the node. This is used so nodes can override [Node.bind], [Node.unbind], |
| 26512 * and [Node.unbindAll] like InputElement does. |
| 26513 */ |
| 26514 // TODO(jmesserly): remove this when we can extend Element for real. |
| 26515 static _nodeOrCustom(node) => node is Element ? node.xtag : node; |
| 26516 |
| 26517 static List<_BindingToken> _parseMustacheTokens(String s) { |
| 26518 var result = []; |
| 26519 var length = s.length; |
| 26520 var index = 0, lastIndex = 0; |
| 26521 while (lastIndex < length) { |
| 26522 index = s.indexOf('{{', lastIndex); |
| 26523 if (index < 0) { |
| 26524 result.add(new _BindingToken(s.substring(lastIndex))); |
| 26525 break; |
| 26480 } else { | 26526 } else { |
| 26481 var value = values[i]; | 26527 // There is a non-empty text run before the next path token. |
| 26482 if (value != null) { | 26528 if (index > 0 && lastIndex < index) { |
| 26483 newValue.write(value); | 26529 result.add(new _BindingToken(s.substring(lastIndex, index))); |
| 26484 } | 26530 } |
| 26485 } | 26531 lastIndex = index + 2; |
| 26486 } | 26532 index = s.indexOf('}}', lastIndex); |
| 26487 | 26533 if (index < 0) { |
| 26488 return newValue.toString(); | 26534 var text = s.substring(lastIndex - 2); |
| 26489 }; | 26535 if (result.length > 0 && result.last.isText) { |
| 26490 | 26536 result.last.value += text; |
| 26491 _nodeOrCustom(node).bind(name, replacementBinding, 'value'); | 26537 } else { |
| 26538 result.add(new _BindingToken(text)); |
| 26539 } |
| 26540 break; |
| 26541 } |
| 26542 |
| 26543 var value = s.substring(lastIndex, index).trim(); |
| 26544 result.add(new _BindingToken(value, isBinding: true)); |
| 26545 lastIndex = index + 2; |
| 26546 } |
| 26547 } |
| 26548 return result; |
| 26549 } |
| 26550 |
| 26551 static void _addTemplateInstanceRecord(fragment, model) { |
| 26552 if (fragment.$dom_firstChild == null) { |
| 26553 return; |
| 26554 } |
| 26555 |
| 26556 var instanceRecord = new TemplateInstance( |
| 26557 fragment.$dom_firstChild, fragment.$dom_lastChild, model); |
| 26558 |
| 26559 var node = instanceRecord.firstNode; |
| 26560 while (node != null) { |
| 26561 node._templateInstance = instanceRecord; |
| 26562 node = node.nextNode; |
| 26563 } |
| 26564 } |
| 26565 |
| 26566 static void _removeAllBindingsRecursively(Node node) { |
| 26567 _nodeOrCustom(node).unbindAll(); |
| 26568 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 26569 _removeAllBindingsRecursively(c); |
| 26570 } |
| 26571 } |
| 26572 |
| 26573 static void _removeChild(Node parent, Node child) { |
| 26574 child._templateInstance = null; |
| 26575 if (child is Element && child.isTemplate) { |
| 26576 // Make sure we stop observing when we remove an element. |
| 26577 var templateIterator = child._templateIterator; |
| 26578 if (templateIterator != null) { |
| 26579 templateIterator.abandon(); |
| 26580 child._templateIterator = null; |
| 26581 } |
| 26582 } |
| 26583 child.remove(); |
| 26584 _removeAllBindingsRecursively(child); |
| 26585 } |
| 26492 } | 26586 } |
| 26493 | 26587 |
| 26494 void _bindOrDelegate(node, name, model, String path, | |
| 26495 CustomBindingSyntax syntax) { | |
| 26496 | |
| 26497 if (syntax != null) { | |
| 26498 var delegateBinding = syntax.getBinding(model, path, name, node); | |
| 26499 if (delegateBinding != null) { | |
| 26500 model = delegateBinding; | |
| 26501 path = 'value'; | |
| 26502 } | |
| 26503 } | |
| 26504 | |
| 26505 _nodeOrCustom(node).bind(name, model, path); | |
| 26506 } | |
| 26507 | |
| 26508 /** | |
| 26509 * Gets the [node]'s custom [Element.xtag] if present, otherwise returns | |
| 26510 * the node. This is used so nodes can override [Node.bind], [Node.unbind], | |
| 26511 * and [Node.unbindAll] like InputElement does. | |
| 26512 */ | |
| 26513 // TODO(jmesserly): remove this when we can extend Element for real. | |
| 26514 _nodeOrCustom(node) => node is Element ? node.xtag : node; | |
| 26515 | |
| 26516 class _BindingToken { | 26588 class _BindingToken { |
| 26517 final String value; | 26589 final String value; |
| 26518 final bool isBinding; | 26590 final bool isBinding; |
| 26519 | 26591 |
| 26520 _BindingToken(this.value, {this.isBinding: false}); | 26592 _BindingToken(this.value, {this.isBinding: false}); |
| 26521 | 26593 |
| 26522 bool get isText => !isBinding; | 26594 bool get isText => !isBinding; |
| 26523 } | 26595 } |
| 26524 | 26596 |
| 26525 List<_BindingToken> _parseMustacheTokens(String s) { | |
| 26526 var result = []; | |
| 26527 var length = s.length; | |
| 26528 var index = 0, lastIndex = 0; | |
| 26529 while (lastIndex < length) { | |
| 26530 index = s.indexOf('{{', lastIndex); | |
| 26531 if (index < 0) { | |
| 26532 result.add(new _BindingToken(s.substring(lastIndex))); | |
| 26533 break; | |
| 26534 } else { | |
| 26535 // There is a non-empty text run before the next path token. | |
| 26536 if (index > 0 && lastIndex < index) { | |
| 26537 result.add(new _BindingToken(s.substring(lastIndex, index))); | |
| 26538 } | |
| 26539 lastIndex = index + 2; | |
| 26540 index = s.indexOf('}}', lastIndex); | |
| 26541 if (index < 0) { | |
| 26542 var text = s.substring(lastIndex - 2); | |
| 26543 if (result.length > 0 && result.last.isText) { | |
| 26544 result.last.value += text; | |
| 26545 } else { | |
| 26546 result.add(new _BindingToken(text)); | |
| 26547 } | |
| 26548 break; | |
| 26549 } | |
| 26550 | |
| 26551 var value = s.substring(lastIndex, index).trim(); | |
| 26552 result.add(new _BindingToken(value, isBinding: true)); | |
| 26553 lastIndex = index + 2; | |
| 26554 } | |
| 26555 } | |
| 26556 return result; | |
| 26557 } | |
| 26558 | |
| 26559 void _addTemplateInstanceRecord(fragment, model) { | |
| 26560 if (fragment.$dom_firstChild == null) { | |
| 26561 return; | |
| 26562 } | |
| 26563 | |
| 26564 var instanceRecord = new TemplateInstance( | |
| 26565 fragment.$dom_firstChild, fragment.$dom_lastChild, model); | |
| 26566 | |
| 26567 var node = instanceRecord.firstNode; | |
| 26568 while (node != null) { | |
| 26569 node._templateInstance = instanceRecord; | |
| 26570 node = node.nextNode; | |
| 26571 } | |
| 26572 } | |
| 26573 | |
| 26574 void _removeAllBindingsRecursively(Node node) { | |
| 26575 _nodeOrCustom(node).unbindAll(); | |
| 26576 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { | |
| 26577 _removeAllBindingsRecursively(c); | |
| 26578 } | |
| 26579 } | |
| 26580 | |
| 26581 void _removeTemplateChild(Node parent, Node child) { | |
| 26582 child._templateInstance = null; | |
| 26583 if (child is Element && child.isTemplate) { | |
| 26584 // Make sure we stop observing when we remove an element. | |
| 26585 var templateIterator = child._templateIterator; | |
| 26586 if (templateIterator != null) { | |
| 26587 templateIterator.abandon(); | |
| 26588 child._templateIterator = null; | |
| 26589 } | |
| 26590 } | |
| 26591 child.remove(); | |
| 26592 _removeAllBindingsRecursively(child); | |
| 26593 } | |
| 26594 | |
| 26595 | |
| 26596 class _TemplateIterator { | 26597 class _TemplateIterator { |
| 26597 final Element _templateElement; | 26598 final Element _templateElement; |
| 26598 final List<Node> terminators = []; | 26599 final List<Node> terminators = []; |
| 26599 final CompoundBinding inputs; | 26600 final CompoundBinding inputs; |
| 26600 List iteratedValue; | 26601 List iteratedValue; |
| 26601 | 26602 |
| 26602 StreamSubscription _sub; | 26603 StreamSubscription _sub; |
| 26603 StreamSubscription _valueBinding; | 26604 StreamSubscription _valueBinding; |
| 26604 | 26605 |
| 26605 _TemplateIterator(this._templateElement) | 26606 _TemplateIterator(this._templateElement) |
| 26606 : inputs = new CompoundBinding(resolveInputs) { | 26607 : inputs = new CompoundBinding(resolveInputs) { |
| 26607 | 26608 |
| 26608 _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); | 26609 _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); |
| 26609 } | 26610 } |
| 26610 | 26611 |
| 26611 static Object resolveInputs(Map values) { | 26612 static Object resolveInputs(Map values) { |
| 26612 if (values.containsKey('if') && !_templateBooleanConversion(values['if'])) { | 26613 if (values.containsKey('if') && !_Bindings._toBoolean(values['if'])) { |
| 26613 return null; | 26614 return null; |
| 26614 } | 26615 } |
| 26615 | 26616 |
| 26616 if (values.containsKey('repeat')) { | 26617 if (values.containsKey('repeat')) { |
| 26617 return values['repeat']; | 26618 return values['repeat']; |
| 26618 } | 26619 } |
| 26619 | 26620 |
| 26620 if (values.containsKey('bind')) { | 26621 if (values.containsKey('bind')) { |
| 26621 return [values['bind']]; | 26622 return [values['bind']]; |
| 26622 } | 26623 } |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 26663 | 26664 |
| 26664 void removeInstanceAt(int index) { | 26665 void removeInstanceAt(int index) { |
| 26665 var previousTerminator = getTerminatorAt(index - 1); | 26666 var previousTerminator = getTerminatorAt(index - 1); |
| 26666 var terminator = getTerminatorAt(index); | 26667 var terminator = getTerminatorAt(index); |
| 26667 terminators.removeAt(index); | 26668 terminators.removeAt(index); |
| 26668 | 26669 |
| 26669 var parent = _templateElement.parentNode; | 26670 var parent = _templateElement.parentNode; |
| 26670 while (terminator != previousTerminator) { | 26671 while (terminator != previousTerminator) { |
| 26671 var node = terminator; | 26672 var node = terminator; |
| 26672 terminator = node.previousNode; | 26673 terminator = node.previousNode; |
| 26673 _removeTemplateChild(parent, node); | 26674 _Bindings._removeChild(parent, node); |
| 26674 } | 26675 } |
| 26675 } | 26676 } |
| 26676 | 26677 |
| 26677 void removeAllInstances() { | 26678 void removeAllInstances() { |
| 26678 if (terminators.length == 0) return; | 26679 if (terminators.length == 0) return; |
| 26679 | 26680 |
| 26680 var previousTerminator = _templateElement; | 26681 var previousTerminator = _templateElement; |
| 26681 var terminator = getTerminatorAt(terminators.length - 1); | 26682 var terminator = getTerminatorAt(terminators.length - 1); |
| 26682 terminators.length = 0; | 26683 terminators.length = 0; |
| 26683 | 26684 |
| 26684 var parent = _templateElement.parentNode; | 26685 var parent = _templateElement.parentNode; |
| 26685 while (terminator != previousTerminator) { | 26686 while (terminator != previousTerminator) { |
| 26686 var node = terminator; | 26687 var node = terminator; |
| 26687 terminator = node.previousNode; | 26688 terminator = node.previousNode; |
| 26688 _removeTemplateChild(parent, node); | 26689 _Bindings._removeChild(parent, node); |
| 26689 } | 26690 } |
| 26690 } | 26691 } |
| 26691 | 26692 |
| 26692 void clear() { | 26693 void clear() { |
| 26693 unobserve(); | 26694 unobserve(); |
| 26694 removeAllInstances(); | 26695 removeAllInstances(); |
| 26695 iteratedValue = null; | 26696 iteratedValue = null; |
| 26696 } | 26697 } |
| 26697 | 26698 |
| 26698 getInstanceModel(model, syntax) { | 26699 getInstanceModel(model, syntax) { |
| (...skipping 21 matching lines...) Expand all Loading... |
| 26720 } | 26721 } |
| 26721 | 26722 |
| 26722 for (var addIndex = splice.index; | 26723 for (var addIndex = splice.index; |
| 26723 addIndex < splice.index + splice.addedCount; | 26724 addIndex < splice.index + splice.addedCount; |
| 26724 addIndex++) { | 26725 addIndex++) { |
| 26725 | 26726 |
| 26726 var model = getInstanceModel(iteratedValue[addIndex], syntax); | 26727 var model = getInstanceModel(iteratedValue[addIndex], syntax); |
| 26727 | 26728 |
| 26728 var fragment = getInstanceFragment(syntax); | 26729 var fragment = getInstanceFragment(syntax); |
| 26729 | 26730 |
| 26730 _addBindings(fragment, model, syntax); | 26731 _Bindings._addBindings(fragment, model, syntax); |
| 26731 _addTemplateInstanceRecord(fragment, model); | 26732 _Bindings._addTemplateInstanceRecord(fragment, model); |
| 26732 | 26733 |
| 26733 insertInstanceAt(addIndex, fragment); | 26734 insertInstanceAt(addIndex, fragment); |
| 26734 } | 26735 } |
| 26735 } | 26736 } |
| 26736 } | 26737 } |
| 26737 | 26738 |
| 26738 void unobserve() { | 26739 void unobserve() { |
| 26739 if (_sub == null) return; | 26740 if (_sub == null) return; |
| 26740 _sub.cancel(); | 26741 _sub.cancel(); |
| 26741 _sub = null; | 26742 _sub = null; |
| (...skipping 1226 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 27968 _position = nextPosition; | 27969 _position = nextPosition; |
| 27969 return true; | 27970 return true; |
| 27970 } | 27971 } |
| 27971 _current = null; | 27972 _current = null; |
| 27972 _position = _array.length; | 27973 _position = _array.length; |
| 27973 return false; | 27974 return false; |
| 27974 } | 27975 } |
| 27975 | 27976 |
| 27976 T get current => _current; | 27977 T get current => _current; |
| 27977 } | 27978 } |
| OLD | NEW |