| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 part of html; | 5 part of html; |
| 6 | 6 |
| 7 // This code is a port of Model-Driven-Views: | 7 // This code is a port of Model-Driven-Views: |
| 8 // https://github.com/toolkitchen/mdv | 8 // https://github.com/toolkitchen/mdv |
| 9 // The code mostly comes from src/template_element.js | 9 // The code mostly comes from src/template_element.js |
| 10 | 10 |
| (...skipping 228 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 239 binding.cancel(); | 239 binding.cancel(); |
| 240 } | 240 } |
| 241 _bindings.clear(); | 241 _bindings.clear(); |
| 242 _values.clear(); | 242 _values.clear(); |
| 243 | 243 |
| 244 _disposed = true; | 244 _disposed = true; |
| 245 value = null; | 245 value = null; |
| 246 } | 246 } |
| 247 } | 247 } |
| 248 | 248 |
| 249 Stream<Event> _getStreamForInputType(InputElement element) { | |
| 250 switch (element.type) { | |
| 251 case 'checkbox': | |
| 252 return element.onClick; | |
| 253 case 'radio': | |
| 254 case 'select-multiple': | |
| 255 case 'select-one': | |
| 256 return element.onChange; | |
| 257 default: | |
| 258 return element.onInput; | |
| 259 } | |
| 260 } | |
| 261 | |
| 262 abstract class _InputBinding { | 249 abstract class _InputBinding { |
| 263 final InputElement element; | 250 final InputElement element; |
| 264 PathObserver binding; | 251 PathObserver binding; |
| 265 StreamSubscription _pathSub; | 252 StreamSubscription _pathSub; |
| 266 StreamSubscription _eventSub; | 253 StreamSubscription _eventSub; |
| 267 | 254 |
| 268 _InputBinding(this.element, model, String path) { | 255 _InputBinding(this.element, model, String path) { |
| 269 binding = new PathObserver(model, path); | 256 binding = new PathObserver(model, path); |
| 270 _pathSub = binding.bindSync(valueChanged); | 257 _pathSub = binding.bindSync(valueChanged); |
| 271 _eventSub = _getStreamForInputType(element).listen(updateBinding); | 258 _eventSub = _getStreamForInputType(element).listen(updateBinding); |
| 272 } | 259 } |
| 273 | 260 |
| 274 void valueChanged(newValue); | 261 void valueChanged(newValue); |
| 275 | 262 |
| 276 void updateBinding(e); | 263 void updateBinding(e); |
| 277 | 264 |
| 278 void unbind() { | 265 void unbind() { |
| 279 binding = null; | 266 binding = null; |
| 280 _pathSub.cancel(); | 267 _pathSub.cancel(); |
| 281 _eventSub.cancel(); | 268 _eventSub.cancel(); |
| 282 } | 269 } |
| 270 |
| 271 |
| 272 static Stream<Event> _getStreamForInputType(InputElement element) { |
| 273 switch (element.type) { |
| 274 case 'checkbox': |
| 275 return element.onClick; |
| 276 case 'radio': |
| 277 case 'select-multiple': |
| 278 case 'select-one': |
| 279 return element.onChange; |
| 280 default: |
| 281 return element.onInput; |
| 282 } |
| 283 } |
| 283 } | 284 } |
| 284 | 285 |
| 285 class _ValueBinding extends _InputBinding { | 286 class _ValueBinding extends _InputBinding { |
| 286 _ValueBinding(element, model, path) : super(element, model, path); | 287 _ValueBinding(element, model, path) : super(element, model, path); |
| 287 | 288 |
| 288 void valueChanged(value) { | 289 void valueChanged(value) { |
| 289 element.value = value == null ? '' : '$value'; | 290 element.value = value == null ? '' : '$value'; |
| 290 } | 291 } |
| 291 | 292 |
| 292 void updateBinding(e) { | 293 void updateBinding(e) { |
| 293 binding.value = element.value; | 294 binding.value = element.value; |
| 294 } | 295 } |
| 295 } | 296 } |
| 296 | 297 |
| 297 // TODO(jmesserly): not sure what kind of boolean conversion rules to | |
| 298 // apply for template data-binding. HTML attributes are true if they're present. | |
| 299 // However Dart only treats "true" as true. Since this is HTML we'll use | |
| 300 // something closer to the HTML rules: null (missing) and false are false, | |
| 301 // everything else is true. See: https://github.com/toolkitchen/mdv/issues/59 | |
| 302 bool _templateBooleanConversion(value) => null != value && false != value; | |
| 303 | |
| 304 class _CheckedBinding extends _InputBinding { | 298 class _CheckedBinding extends _InputBinding { |
| 305 _CheckedBinding(element, model, path) : super(element, model, path); | 299 _CheckedBinding(element, model, path) : super(element, model, path); |
| 306 | 300 |
| 307 void valueChanged(value) { | 301 void valueChanged(value) { |
| 308 element.checked = _templateBooleanConversion(value); | 302 element.checked = _Bindings._toBoolean(value); |
| 309 } | 303 } |
| 310 | 304 |
| 311 void updateBinding(e) { | 305 void updateBinding(e) { |
| 312 binding.value = element.checked; | 306 binding.value = element.checked; |
| 313 | 307 |
| 314 // Only the radio button that is getting checked gets an event. We | 308 // Only the radio button that is getting checked gets an event. We |
| 315 // therefore find all the associated radio buttons and update their | 309 // therefore find all the associated radio buttons and update their |
| 316 // CheckedBinding manually. | 310 // CheckedBinding manually. |
| 317 if (element is InputElement && element.type == 'radio') { | 311 if (element is InputElement && element.type == 'radio') { |
| 318 for (var r in _getAssociatedRadioButtons(element)) { | 312 for (var r in _getAssociatedRadioButtons(element)) { |
| 319 var checkedBinding = r._checkedBinding; | 313 var checkedBinding = r._checkedBinding; |
| 320 if (checkedBinding != null) { | 314 if (checkedBinding != null) { |
| 321 // Set the value directly to avoid an infinite call stack. | 315 // Set the value directly to avoid an infinite call stack. |
| 322 checkedBinding.binding.value = false; | 316 checkedBinding.binding.value = false; |
| 323 } | 317 } |
| 324 } | 318 } |
| 325 } | 319 } |
| 326 } | 320 } |
| 321 |
| 322 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. |
| 323 // Returns an array containing all radio buttons other than |element| that |
| 324 // have the same |name|, either in the form that |element| belongs to or, |
| 325 // if no form, in the document tree to which |element| belongs. |
| 326 // |
| 327 // This implementation is based upon the HTML spec definition of a |
| 328 // "radio button group": |
| 329 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.
html#radio-button-group |
| 330 // |
| 331 static Iterable _getAssociatedRadioButtons(element) { |
| 332 if (!_isNodeInDocument(element)) return []; |
| 333 if (element.form != null) { |
| 334 return element.form.nodes.where((el) { |
| 335 return el != element && |
| 336 el is InputElement && |
| 337 el.type == 'radio' && |
| 338 el.name == element.name; |
| 339 }); |
| 340 } else { |
| 341 var radios = element.document.queryAll( |
| 342 'input[type="radio"][name="${element.name}"]'); |
| 343 return radios.where((el) => el != element && el.form == null); |
| 344 } |
| 345 } |
| 346 |
| 347 // TODO(jmesserly): polyfill document.contains API instead of doing it here |
| 348 static bool _isNodeInDocument(Node node) { |
| 349 // On non-IE this works: |
| 350 // return node.document.contains(node); |
| 351 var document = node.document; |
| 352 if (node == document || node.parentNode == document) return true; |
| 353 return document.documentElement.contains(node); |
| 354 } |
| 327 } | 355 } |
| 328 | 356 |
| 329 // TODO(jmesserly): polyfill document.contains API instead of doing it here | 357 class _Bindings { |
| 330 bool _isNodeInDocument(Node node) { | 358 // TODO(jmesserly): not sure what kind of boolean conversion rules to |
| 331 // On non-IE this works: | 359 // apply for template data-binding. HTML attributes are true if they're |
| 332 // return node.document.contains(node); | 360 // present. However Dart only treats "true" as true. Since this is HTML we'll |
| 333 var document = node.document; | 361 // use something closer to the HTML rules: null (missing) and false are false, |
| 334 if (node == document || node.parentNode == document) return true; | 362 // everything else is true. See: https://github.com/toolkitchen/mdv/issues/59 |
| 335 return document.documentElement.contains(node); | 363 static bool _toBoolean(value) => null != value && false != value; |
| 336 } | 364 |
| 337 | 365 static Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) { |
| 338 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. | 366 var clone = node.clone(false); // Shallow clone. |
| 339 // Returns an array containing all radio buttons other than |element| that | 367 if (clone is Element && clone.isTemplate) { |
| 340 // have the same |name|, either in the form that |element| belongs to or, | 368 TemplateElement.decorate(clone, node); |
| 341 // if no form, in the document tree to which |element| belongs. | 369 if (syntax != null) { |
| 342 // | 370 clone.attributes.putIfAbsent('syntax', () => syntax); |
| 343 // This implementation is based upon the HTML spec definition of a | 371 } |
| 344 // "radio button group": | 372 } |
| 345 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.ht
ml#radio-button-group | 373 |
| 346 // | 374 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 347 Iterable _getAssociatedRadioButtons(element) { | 375 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax)); |
| 348 if (!_isNodeInDocument(element)) return []; | 376 } |
| 349 if (element.form != null) { | 377 return clone; |
| 350 return element.form.nodes.where((el) { | 378 } |
| 351 return el != element && | 379 |
| 352 el is InputElement && | 380 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#
dfn-template-contents-owner |
| 353 el.type == 'radio' && | 381 static Document _getTemplateContentsOwner(Document doc) { |
| 354 el.name == element.name; | 382 if (doc.window == null) { |
| 355 }); | 383 return doc; |
| 356 } else { | 384 } |
| 357 var radios = element.document.queryAll( | 385 var d = doc._templateContentsOwner; |
| 358 'input[type="radio"][name="${element.name}"]'); | 386 if (d == null) { |
| 359 return radios.where((el) => el != element && el.form == null); | 387 // TODO(arv): This should either be a Document or HTMLDocument depending |
| 360 } | 388 // on doc. |
| 361 } | 389 d = doc.implementation.createHtmlDocument(''); |
| 362 | 390 while (d.$dom_lastChild != null) { |
| 363 Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) { | 391 d.$dom_lastChild.remove(); |
| 364 var clone = node.clone(false); // Shallow clone. | 392 } |
| 365 if (clone is Element && clone.isTemplate) { | 393 doc._templateContentsOwner = d; |
| 366 TemplateElement.decorate(clone, node); | 394 } |
| 367 if (syntax != null) { | 395 return d; |
| 368 clone.attributes.putIfAbsent('syntax', () => syntax); | 396 } |
| 369 } | 397 |
| 370 } | 398 static Element _cloneAndSeperateAttributeTemplate(Element templateElement) { |
| 371 | 399 var clone = templateElement.clone(false); |
| 372 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { | 400 var attributes = templateElement.attributes; |
| 373 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax)); | 401 for (var name in attributes.keys.toList()) { |
| 374 } | 402 switch (name) { |
| 375 return clone; | 403 case 'template': |
| 376 } | 404 case 'repeat': |
| 377 | 405 case 'bind': |
| 378 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#df
n-template-contents-owner | 406 case 'ref': |
| 379 Document _getTemplateContentsOwner(Document doc) { | 407 clone.attributes.remove(name); |
| 380 if (doc.window == null) { | 408 break; |
| 381 return doc; | 409 default: |
| 382 } | 410 attributes.remove(name); |
| 383 var d = doc._templateContentsOwner; | 411 break; |
| 384 if (d == null) { | 412 } |
| 385 // TODO(arv): This should either be a Document or HTMLDocument depending | 413 } |
| 386 // on doc. | 414 |
| 387 d = doc.implementation.createHtmlDocument(''); | 415 return clone; |
| 388 while (d.$dom_lastChild != null) { | 416 } |
| 389 d.$dom_lastChild.remove(); | 417 |
| 390 } | 418 static void _liftNonNativeChildrenIntoContent(Element templateElement) { |
| 391 doc._templateContentsOwner = d; | 419 var content = templateElement.content; |
| 392 } | 420 |
| 393 return d; | 421 if (!templateElement._isAttributeTemplate) { |
| 394 } | 422 var child; |
| 395 | 423 while ((child = templateElement.$dom_firstChild) != null) { |
| 396 Element _cloneAndSeperateAttributeTemplate(Element templateElement) { | 424 content.append(child); |
| 397 var clone = templateElement.clone(false); | 425 } |
| 398 var attributes = templateElement.attributes; | 426 return; |
| 399 for (var name in attributes.keys.toList()) { | 427 } |
| 400 switch (name) { | 428 |
| 401 case 'template': | 429 // For attribute templates we copy the whole thing into the content and |
| 402 case 'repeat': | 430 // we move the non template attributes into the content. |
| 403 case 'bind': | 431 // |
| 404 case 'ref': | 432 // <tr foo template> |
| 405 clone.attributes.remove(name); | 433 // |
| 406 break; | 434 // becomes |
| 407 default: | 435 // |
| 408 attributes.remove(name); | 436 // <tr template> |
| 409 break; | 437 // + #document-fragment |
| 410 } | 438 // + <tr foo> |
| 411 } | 439 // |
| 412 | 440 var newRoot = _cloneAndSeperateAttributeTemplate(templateElement); |
| 413 return clone; | |
| 414 } | |
| 415 | |
| 416 void _liftNonNativeTemplateChildrenIntoContent(Element templateElement) { | |
| 417 var content = templateElement.content; | |
| 418 | |
| 419 if (!templateElement._isAttributeTemplate) { | |
| 420 var child; | 441 var child; |
| 421 while ((child = templateElement.$dom_firstChild) != null) { | 442 while ((child = templateElement.$dom_firstChild) != null) { |
| 422 content.append(child); | 443 newRoot.append(child); |
| 423 } | 444 } |
| 424 return; | 445 content.append(newRoot); |
| 425 } | 446 } |
| 426 | 447 |
| 427 // For attribute templates we copy the whole thing into the content and | 448 static void _bootstrapTemplatesRecursivelyFrom(Node node) { |
| 428 // we move the non template attributes into the content. | 449 void bootstrap(template) { |
| 429 // | 450 if (!TemplateElement.decorate(template)) { |
| 430 // <tr foo template> | 451 _bootstrapTemplatesRecursivelyFrom(template.content); |
| 431 // | 452 } |
| 432 // becomes | 453 } |
| 433 // | 454 |
| 434 // <tr template> | 455 // Need to do this first as the contents may get lifted if |node| is |
| 435 // + #document-fragment | 456 // template. |
| 436 // + <tr foo> | 457 // TODO(jmesserly): node is DocumentFragment or Element |
| 437 // | 458 var descendents = (node as dynamic).queryAll(_allTemplatesSelectors); |
| 438 var newRoot = _cloneAndSeperateAttributeTemplate(templateElement); | 459 if (node is Element && node.isTemplate) bootstrap(node); |
| 439 var child; | 460 |
| 440 while ((child = templateElement.$dom_firstChild) != null) { | 461 descendents.forEach(bootstrap); |
| 441 newRoot.append(child); | 462 } |
| 442 } | 463 |
| 443 content.append(newRoot); | 464 static final String _allTemplatesSelectors = 'template, option[template], ' + |
| 444 } | 465 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); |
| 445 | 466 |
| 446 void _bootstrapTemplatesRecursivelyFrom(Node node) { | 467 static void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { |
| 447 void bootstrap(template) { | 468 if (node is Element) { |
| 448 if (!TemplateElement.decorate(template)) { | 469 _addAttributeBindings(node, model, syntax); |
| 449 _bootstrapTemplatesRecursivelyFrom(template.content); | 470 } else if (node is Text) { |
| 450 } | 471 _parseAndBind(node, 'text', node.text, model, syntax); |
| 451 } | 472 } |
| 452 | 473 |
| 453 // Need to do this first as the contents may get lifted if |node| is | 474 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 454 // template. | 475 _addBindings(c, model, syntax); |
| 455 // TODO(jmesserly): node is DocumentFragment or Element | 476 } |
| 456 var templateDescendents = (node as dynamic).queryAll(_allTemplatesSelectors); | 477 } |
| 457 if (node is Element && node.isTemplate) bootstrap(node); | 478 |
| 458 | 479 static void _addAttributeBindings(Element element, model, syntax) { |
| 459 templateDescendents.forEach(bootstrap); | 480 element.attributes.forEach((name, value) { |
| 460 } | 481 if (value == '' && (name == 'bind' || name == 'repeat')) { |
| 461 | 482 value = '{{}}'; |
| 462 final String _allTemplatesSelectors = 'template, option[template], ' + | 483 } |
| 463 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); | 484 _parseAndBind(element, name, value, model, syntax); |
| 464 | 485 }); |
| 465 void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { | 486 } |
| 466 if (node is Element) { | 487 |
| 467 _addAttributeBindings(node, model, syntax); | 488 static void _parseAndBind(Node node, String name, String text, model, |
| 468 } else if (node is Text) { | 489 CustomBindingSyntax syntax) { |
| 469 _parseAndBind(node, 'text', node.text, model, syntax); | 490 |
| 470 } | 491 var tokens = _parseMustacheTokens(text); |
| 471 | 492 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { |
| 472 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { | 493 return; |
| 473 _addBindings(c, model, syntax); | 494 } |
| 474 } | 495 |
| 475 } | 496 if (tokens.length == 1 && tokens[0].isBinding) { |
| 476 | 497 _bindOrDelegate(node, name, model, tokens[0].value, syntax); |
| 477 | 498 return; |
| 478 void _addAttributeBindings(Element element, model, syntax) { | 499 } |
| 479 element.attributes.forEach((name, value) { | 500 |
| 480 if (value == '' && (name == 'bind' || name == 'repeat')) { | 501 var replacementBinding = new CompoundBinding(); |
| 481 value = '{{}}'; | |
| 482 } | |
| 483 _parseAndBind(element, name, value, model, syntax); | |
| 484 }); | |
| 485 } | |
| 486 | |
| 487 void _parseAndBind(Node node, String name, String text, model, | |
| 488 CustomBindingSyntax syntax) { | |
| 489 | |
| 490 var tokens = _parseMustacheTokens(text); | |
| 491 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { | |
| 492 return; | |
| 493 } | |
| 494 | |
| 495 if (tokens.length == 1 && tokens[0].isBinding) { | |
| 496 _bindOrDelegate(node, name, model, tokens[0].value, syntax); | |
| 497 return; | |
| 498 } | |
| 499 | |
| 500 var replacementBinding = new CompoundBinding(); | |
| 501 for (var i = 0; i < tokens.length; i++) { | |
| 502 var token = tokens[i]; | |
| 503 if (token.isBinding) { | |
| 504 _bindOrDelegate(replacementBinding, i, model, token.value, syntax); | |
| 505 } | |
| 506 } | |
| 507 | |
| 508 replacementBinding.combinator = (values) { | |
| 509 var newValue = new StringBuffer(); | |
| 510 | |
| 511 for (var i = 0; i < tokens.length; i++) { | 502 for (var i = 0; i < tokens.length; i++) { |
| 512 var token = tokens[i]; | 503 var token = tokens[i]; |
| 513 if (token.isText) { | 504 if (token.isBinding) { |
| 514 newValue.write(token.value); | 505 _bindOrDelegate(replacementBinding, i, model, token.value, syntax); |
| 506 } |
| 507 } |
| 508 |
| 509 replacementBinding.combinator = (values) { |
| 510 var newValue = new StringBuffer(); |
| 511 |
| 512 for (var i = 0; i < tokens.length; i++) { |
| 513 var token = tokens[i]; |
| 514 if (token.isText) { |
| 515 newValue.write(token.value); |
| 516 } else { |
| 517 var value = values[i]; |
| 518 if (value != null) { |
| 519 newValue.write(value); |
| 520 } |
| 521 } |
| 522 } |
| 523 |
| 524 return newValue.toString(); |
| 525 }; |
| 526 |
| 527 _nodeOrCustom(node).bind(name, replacementBinding, 'value'); |
| 528 } |
| 529 |
| 530 static void _bindOrDelegate(node, name, model, String path, |
| 531 CustomBindingSyntax syntax) { |
| 532 |
| 533 if (syntax != null) { |
| 534 var delegateBinding = syntax.getBinding(model, path, name, node); |
| 535 if (delegateBinding != null) { |
| 536 model = delegateBinding; |
| 537 path = 'value'; |
| 538 } |
| 539 } |
| 540 |
| 541 _nodeOrCustom(node).bind(name, model, path); |
| 542 } |
| 543 |
| 544 /** |
| 545 * Gets the [node]'s custom [Element.xtag] if present, otherwise returns |
| 546 * the node. This is used so nodes can override [Node.bind], [Node.unbind], |
| 547 * and [Node.unbindAll] like InputElement does. |
| 548 */ |
| 549 // TODO(jmesserly): remove this when we can extend Element for real. |
| 550 static _nodeOrCustom(node) => node is Element ? node.xtag : node; |
| 551 |
| 552 static List<_BindingToken> _parseMustacheTokens(String s) { |
| 553 var result = []; |
| 554 var length = s.length; |
| 555 var index = 0, lastIndex = 0; |
| 556 while (lastIndex < length) { |
| 557 index = s.indexOf('{{', lastIndex); |
| 558 if (index < 0) { |
| 559 result.add(new _BindingToken(s.substring(lastIndex))); |
| 560 break; |
| 515 } else { | 561 } else { |
| 516 var value = values[i]; | 562 // There is a non-empty text run before the next path token. |
| 517 if (value != null) { | 563 if (index > 0 && lastIndex < index) { |
| 518 newValue.write(value); | 564 result.add(new _BindingToken(s.substring(lastIndex, index))); |
| 519 } | 565 } |
| 520 } | 566 lastIndex = index + 2; |
| 521 } | 567 index = s.indexOf('}}', lastIndex); |
| 522 | 568 if (index < 0) { |
| 523 return newValue.toString(); | 569 var text = s.substring(lastIndex - 2); |
| 524 }; | 570 if (result.length > 0 && result.last.isText) { |
| 525 | 571 result.last.value += text; |
| 526 _nodeOrCustom(node).bind(name, replacementBinding, 'value'); | 572 } else { |
| 573 result.add(new _BindingToken(text)); |
| 574 } |
| 575 break; |
| 576 } |
| 577 |
| 578 var value = s.substring(lastIndex, index).trim(); |
| 579 result.add(new _BindingToken(value, isBinding: true)); |
| 580 lastIndex = index + 2; |
| 581 } |
| 582 } |
| 583 return result; |
| 584 } |
| 585 |
| 586 static void _addTemplateInstanceRecord(fragment, model) { |
| 587 if (fragment.$dom_firstChild == null) { |
| 588 return; |
| 589 } |
| 590 |
| 591 var instanceRecord = new TemplateInstance( |
| 592 fragment.$dom_firstChild, fragment.$dom_lastChild, model); |
| 593 |
| 594 var node = instanceRecord.firstNode; |
| 595 while (node != null) { |
| 596 node._templateInstance = instanceRecord; |
| 597 node = node.nextNode; |
| 598 } |
| 599 } |
| 600 |
| 601 static void _removeAllBindingsRecursively(Node node) { |
| 602 _nodeOrCustom(node).unbindAll(); |
| 603 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
| 604 _removeAllBindingsRecursively(c); |
| 605 } |
| 606 } |
| 607 |
| 608 static void _removeChild(Node parent, Node child) { |
| 609 child._templateInstance = null; |
| 610 if (child is Element && child.isTemplate) { |
| 611 // Make sure we stop observing when we remove an element. |
| 612 var templateIterator = child._templateIterator; |
| 613 if (templateIterator != null) { |
| 614 templateIterator.abandon(); |
| 615 child._templateIterator = null; |
| 616 } |
| 617 } |
| 618 child.remove(); |
| 619 _removeAllBindingsRecursively(child); |
| 620 } |
| 527 } | 621 } |
| 528 | 622 |
| 529 void _bindOrDelegate(node, name, model, String path, | |
| 530 CustomBindingSyntax syntax) { | |
| 531 | |
| 532 if (syntax != null) { | |
| 533 var delegateBinding = syntax.getBinding(model, path, name, node); | |
| 534 if (delegateBinding != null) { | |
| 535 model = delegateBinding; | |
| 536 path = 'value'; | |
| 537 } | |
| 538 } | |
| 539 | |
| 540 _nodeOrCustom(node).bind(name, model, path); | |
| 541 } | |
| 542 | |
| 543 /** | |
| 544 * Gets the [node]'s custom [Element.xtag] if present, otherwise returns | |
| 545 * the node. This is used so nodes can override [Node.bind], [Node.unbind], | |
| 546 * and [Node.unbindAll] like InputElement does. | |
| 547 */ | |
| 548 // TODO(jmesserly): remove this when we can extend Element for real. | |
| 549 _nodeOrCustom(node) => node is Element ? node.xtag : node; | |
| 550 | |
| 551 class _BindingToken { | 623 class _BindingToken { |
| 552 final String value; | 624 final String value; |
| 553 final bool isBinding; | 625 final bool isBinding; |
| 554 | 626 |
| 555 _BindingToken(this.value, {this.isBinding: false}); | 627 _BindingToken(this.value, {this.isBinding: false}); |
| 556 | 628 |
| 557 bool get isText => !isBinding; | 629 bool get isText => !isBinding; |
| 558 } | 630 } |
| 559 | 631 |
| 560 List<_BindingToken> _parseMustacheTokens(String s) { | |
| 561 var result = []; | |
| 562 var length = s.length; | |
| 563 var index = 0, lastIndex = 0; | |
| 564 while (lastIndex < length) { | |
| 565 index = s.indexOf('{{', lastIndex); | |
| 566 if (index < 0) { | |
| 567 result.add(new _BindingToken(s.substring(lastIndex))); | |
| 568 break; | |
| 569 } else { | |
| 570 // There is a non-empty text run before the next path token. | |
| 571 if (index > 0 && lastIndex < index) { | |
| 572 result.add(new _BindingToken(s.substring(lastIndex, index))); | |
| 573 } | |
| 574 lastIndex = index + 2; | |
| 575 index = s.indexOf('}}', lastIndex); | |
| 576 if (index < 0) { | |
| 577 var text = s.substring(lastIndex - 2); | |
| 578 if (result.length > 0 && result.last.isText) { | |
| 579 result.last.value += text; | |
| 580 } else { | |
| 581 result.add(new _BindingToken(text)); | |
| 582 } | |
| 583 break; | |
| 584 } | |
| 585 | |
| 586 var value = s.substring(lastIndex, index).trim(); | |
| 587 result.add(new _BindingToken(value, isBinding: true)); | |
| 588 lastIndex = index + 2; | |
| 589 } | |
| 590 } | |
| 591 return result; | |
| 592 } | |
| 593 | |
| 594 void _addTemplateInstanceRecord(fragment, model) { | |
| 595 if (fragment.$dom_firstChild == null) { | |
| 596 return; | |
| 597 } | |
| 598 | |
| 599 var instanceRecord = new TemplateInstance( | |
| 600 fragment.$dom_firstChild, fragment.$dom_lastChild, model); | |
| 601 | |
| 602 var node = instanceRecord.firstNode; | |
| 603 while (node != null) { | |
| 604 node._templateInstance = instanceRecord; | |
| 605 node = node.nextNode; | |
| 606 } | |
| 607 } | |
| 608 | |
| 609 void _removeAllBindingsRecursively(Node node) { | |
| 610 _nodeOrCustom(node).unbindAll(); | |
| 611 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { | |
| 612 _removeAllBindingsRecursively(c); | |
| 613 } | |
| 614 } | |
| 615 | |
| 616 void _removeTemplateChild(Node parent, Node child) { | |
| 617 child._templateInstance = null; | |
| 618 if (child is Element && child.isTemplate) { | |
| 619 // Make sure we stop observing when we remove an element. | |
| 620 var templateIterator = child._templateIterator; | |
| 621 if (templateIterator != null) { | |
| 622 templateIterator.abandon(); | |
| 623 child._templateIterator = null; | |
| 624 } | |
| 625 } | |
| 626 child.remove(); | |
| 627 _removeAllBindingsRecursively(child); | |
| 628 } | |
| 629 | |
| 630 | |
| 631 class _TemplateIterator { | 632 class _TemplateIterator { |
| 632 final Element _templateElement; | 633 final Element _templateElement; |
| 633 final List<Node> terminators = []; | 634 final List<Node> terminators = []; |
| 634 final CompoundBinding inputs; | 635 final CompoundBinding inputs; |
| 635 List iteratedValue; | 636 List iteratedValue; |
| 636 | 637 |
| 637 StreamSubscription _sub; | 638 StreamSubscription _sub; |
| 638 StreamSubscription _valueBinding; | 639 StreamSubscription _valueBinding; |
| 639 | 640 |
| 640 _TemplateIterator(this._templateElement) | 641 _TemplateIterator(this._templateElement) |
| 641 : inputs = new CompoundBinding(resolveInputs) { | 642 : inputs = new CompoundBinding(resolveInputs) { |
| 642 | 643 |
| 643 _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); | 644 _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); |
| 644 } | 645 } |
| 645 | 646 |
| 646 static Object resolveInputs(Map values) { | 647 static Object resolveInputs(Map values) { |
| 647 if (values.containsKey('if') && !_templateBooleanConversion(values['if'])) { | 648 if (values.containsKey('if') && !_Bindings._toBoolean(values['if'])) { |
| 648 return null; | 649 return null; |
| 649 } | 650 } |
| 650 | 651 |
| 651 if (values.containsKey('repeat')) { | 652 if (values.containsKey('repeat')) { |
| 652 return values['repeat']; | 653 return values['repeat']; |
| 653 } | 654 } |
| 654 | 655 |
| 655 if (values.containsKey('bind')) { | 656 if (values.containsKey('bind')) { |
| 656 return [values['bind']]; | 657 return [values['bind']]; |
| 657 } | 658 } |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 698 | 699 |
| 699 void removeInstanceAt(int index) { | 700 void removeInstanceAt(int index) { |
| 700 var previousTerminator = getTerminatorAt(index - 1); | 701 var previousTerminator = getTerminatorAt(index - 1); |
| 701 var terminator = getTerminatorAt(index); | 702 var terminator = getTerminatorAt(index); |
| 702 terminators.removeAt(index); | 703 terminators.removeAt(index); |
| 703 | 704 |
| 704 var parent = _templateElement.parentNode; | 705 var parent = _templateElement.parentNode; |
| 705 while (terminator != previousTerminator) { | 706 while (terminator != previousTerminator) { |
| 706 var node = terminator; | 707 var node = terminator; |
| 707 terminator = node.previousNode; | 708 terminator = node.previousNode; |
| 708 _removeTemplateChild(parent, node); | 709 _Bindings._removeChild(parent, node); |
| 709 } | 710 } |
| 710 } | 711 } |
| 711 | 712 |
| 712 void removeAllInstances() { | 713 void removeAllInstances() { |
| 713 if (terminators.length == 0) return; | 714 if (terminators.length == 0) return; |
| 714 | 715 |
| 715 var previousTerminator = _templateElement; | 716 var previousTerminator = _templateElement; |
| 716 var terminator = getTerminatorAt(terminators.length - 1); | 717 var terminator = getTerminatorAt(terminators.length - 1); |
| 717 terminators.length = 0; | 718 terminators.length = 0; |
| 718 | 719 |
| 719 var parent = _templateElement.parentNode; | 720 var parent = _templateElement.parentNode; |
| 720 while (terminator != previousTerminator) { | 721 while (terminator != previousTerminator) { |
| 721 var node = terminator; | 722 var node = terminator; |
| 722 terminator = node.previousNode; | 723 terminator = node.previousNode; |
| 723 _removeTemplateChild(parent, node); | 724 _Bindings._removeChild(parent, node); |
| 724 } | 725 } |
| 725 } | 726 } |
| 726 | 727 |
| 727 void clear() { | 728 void clear() { |
| 728 unobserve(); | 729 unobserve(); |
| 729 removeAllInstances(); | 730 removeAllInstances(); |
| 730 iteratedValue = null; | 731 iteratedValue = null; |
| 731 } | 732 } |
| 732 | 733 |
| 733 getInstanceModel(model, syntax) { | 734 getInstanceModel(model, syntax) { |
| (...skipping 21 matching lines...) Expand all Loading... |
| 755 } | 756 } |
| 756 | 757 |
| 757 for (var addIndex = splice.index; | 758 for (var addIndex = splice.index; |
| 758 addIndex < splice.index + splice.addedCount; | 759 addIndex < splice.index + splice.addedCount; |
| 759 addIndex++) { | 760 addIndex++) { |
| 760 | 761 |
| 761 var model = getInstanceModel(iteratedValue[addIndex], syntax); | 762 var model = getInstanceModel(iteratedValue[addIndex], syntax); |
| 762 | 763 |
| 763 var fragment = getInstanceFragment(syntax); | 764 var fragment = getInstanceFragment(syntax); |
| 764 | 765 |
| 765 _addBindings(fragment, model, syntax); | 766 _Bindings._addBindings(fragment, model, syntax); |
| 766 _addTemplateInstanceRecord(fragment, model); | 767 _Bindings._addTemplateInstanceRecord(fragment, model); |
| 767 | 768 |
| 768 insertInstanceAt(addIndex, fragment); | 769 insertInstanceAt(addIndex, fragment); |
| 769 } | 770 } |
| 770 } | 771 } |
| 771 } | 772 } |
| 772 | 773 |
| 773 void unobserve() { | 774 void unobserve() { |
| 774 if (_sub == null) return; | 775 if (_sub == null) return; |
| 775 _sub.cancel(); | 776 _sub.cancel(); |
| 776 _sub = null; | 777 _sub = null; |
| 777 } | 778 } |
| 778 | 779 |
| 779 void abandon() { | 780 void abandon() { |
| 780 unobserve(); | 781 unobserve(); |
| 781 _valueBinding.cancel(); | 782 _valueBinding.cancel(); |
| 782 inputs.dispose(); | 783 inputs.dispose(); |
| 783 } | 784 } |
| 784 } | 785 } |
| OLD | NEW |