1 // ========================================================================== 2 // Project: SproutCore Costello - Property Observing Library 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('ext/function'); 9 sc_require('private/observer_set'); 10 sc_require('private/chain_observer'); 11 12 //@if(debug) 13 /** 14 Set to YES to have all observing activity logged to the console. 15 16 This is only available in debug mode. 17 18 @type Boolean 19 */ 20 SC.LOG_OBSERVERS = false; 21 //@endif 22 23 SC.OBSERVES_HANDLER_ADD = 0; 24 SC.OBSERVES_HANDLER_REMOVE = 1; 25 26 /** 27 @class 28 29 Key-Value-Observing (KVO) simply allows one object to observe changes to a 30 property on another object. It is one of the fundamental ways that models, 31 controllers and views communicate with each other in a SproutCore 32 application. Any object that has this module applied to it can be used in 33 KVO-operations. 34 35 This module is applied automatically to all objects that inherit from 36 SC.Object, which includes most objects bundled with the SproutCore 37 framework. You will not generally apply this module to classes yourself, 38 but you will use the features provided by this module frequently, so it is 39 important to understand how to use it. 40 41 Enabling Key Value Observing 42 --- 43 44 With KVO, you can write functions that will be called automatically whenever 45 a property on a particular object changes. You can use this feature to 46 reduce the amount of "glue code" that you often write to tie the various 47 parts of your application together. 48 49 To use KVO, just use the KVO-aware methods get() and set() to access 50 properties instead of accessing properties directly. Instead of writing: 51 52 var aName = contact.firstName; 53 contact.firstName = 'Charles'; 54 55 use: 56 57 var aName = contact.get('firstName'); 58 contact.set('firstName', 'Charles'); 59 60 get() and set() work just like the normal "dot operators" provided by 61 JavaScript but they provide you with much more power, including not only 62 observing but computed properties as well. 63 64 Observing Property Changes 65 --- 66 67 You typically observe property changes simply by adding the observes() 68 call to the end of your method declarations in classes that you write. For 69 example: 70 71 SC.Object.create({ 72 valueObserver: function () { 73 // Executes whenever the "Value" property changes 74 }.observes('value') 75 }); 76 77 Although this is the most common way to add an observer, this capability is 78 actually built into the SC.Object class on top of two methods defined in 79 this mixin called addObserver() and removeObserver(). You can use these two 80 methods to add and remove observers yourself if you need to do so at run 81 time. 82 83 To add an observer for a property, just call: 84 85 object.addObserver('propertyKey', targetObject, targetAction); 86 87 This will call the 'targetAction' method on the targetObject to be called 88 whenever the value of the propertyKey changes. 89 90 Observer Parameters 91 --- 92 93 An observer function typically does not need to accept any parameters, 94 however you can accept certain arguments when writing generic observers. 95 An observer function can have the following arguments: 96 97 propertyObserver(target, key, value, revision); 98 99 - *target* - This is the object whose value changed. Usually this. 100 - *key* - The key of the value that changed 101 - *value* - this property is no longer used. It will always be null 102 - *revision* - this is the revision of the target object 103 104 Implementing Manual Change Notifications 105 --- 106 107 Sometimes you may want to control the rate at which notifications for 108 a property are delivered, for example by checking first to make sure 109 that the value has changed. 110 111 To do this, you need to implement a computed property for the property 112 you want to change and override automaticallyNotifiesObserversFor(). 113 114 The example below will only notify if the "balance" property value actually 115 changes: 116 117 118 automaticallyNotifiesObserversFor: function (key) { 119 return (key === 'balance') ? NO : sc_super(); 120 }, 121 122 balance: function (key, value) { 123 var balance = this._balance; 124 if ((value !== undefined) && (balance !== value)) { 125 this.propertyWillChange(key); 126 balance = this._balance = value; 127 this.propertyDidChange(key); 128 } 129 return balance; 130 } 131 132 133 Implementation Details 134 --- 135 136 Internally, SproutCore keeps track of observable information by adding a 137 number of properties to the object adopting the observable. All of these 138 properties begin with "_kvo_" to separate them from the rest of your object. 139 140 @since SproutCore 1.0 141 */ 142 SC.Observable = /** @scope SC.Observable.prototype */ { 143 144 //@if(debug) 145 /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */ 146 147 /** 148 Allows you to inspect a property for changes. Whenever the named property 149 changes, a log will be printed to the console. This (along with removeProbe) 150 are convenience methods meant for debugging purposes. 151 152 @param {String} key The name of the property you want probed for changes 153 */ 154 addProbe: function (key) { this.addObserver(key, SC.logChange); }, 155 156 /** 157 Stops a running probe from observing changes to the observer. 158 159 @param {String} key The name of the property you want probed for changes 160 */ 161 removeProbe: function (key) { this.removeObserver(key, SC.logChange); }, 162 163 /** 164 Logs the named properties to the console. 165 166 @param {String...} propertyNames one or more property names 167 */ 168 logProperty: function () { 169 var props = SC.$A(arguments), 170 prop, propsLen, idx; 171 for (idx = 0, propsLen = props.length; idx < propsLen; idx++) { 172 prop = props[idx]; 173 console.log('%@:%@: '.fmt(SC.guidFor(this), prop), this.get(prop)); 174 } 175 }, 176 177 /* END DEBUG ONLY PROPERTIES AND METHODS */ 178 //@endif 179 180 /** @private Property cache. */ 181 _kvo_cache: null, 182 183 /** @private Whether properties of the object will be cacheable. Becomes true if any one computed property is .cacheable() */ 184 _kvo_cacheable: false, 185 186 /** @private Cache of dependepents for a property. */ 187 _kvo_cachedep: null, 188 189 /** @private */ 190 _kvo_changeLevel: 0, 191 192 /** @private */ 193 _kvo_changes: null, 194 195 /** @private */ 196 _kvo_cloned: null, 197 198 /** @private */ 199 _kvo_revision: 0, 200 201 /** @private */ 202 _observableInited: false, 203 204 /** 205 Walk like that ol' duck 206 207 @type Boolean 208 */ 209 isObservable: YES, 210 211 /** 212 Determines whether observers should be automatically notified of changes 213 to a key. 214 215 If you are manually implementing change notifications for a property, you 216 can override this method to return NO for properties you do not want the 217 observing system to automatically notify for. 218 219 The default implementation always returns YES. 220 221 @param {String} key the key that is changing 222 @returns {Boolean} YES if automatic notification should occur. 223 */ 224 automaticallyNotifiesObserversFor: function (key) { 225 return YES; 226 }, 227 228 // .......................................... 229 // PROPERTIES 230 // 231 // Use these methods to get/set properties. This will handle observing 232 // notifications as well as allowing you to define functions that can be 233 // used as properties. 234 235 /** 236 Retrieves the value of key from the object. 237 238 This method is generally very similar to using object[key] or object.key, 239 however it supports both computed properties and the unknownProperty 240 handler. 241 242 Computed Properties 243 --- 244 245 Computed properties are methods defined with the property() modifier 246 declared at the end, such as: 247 248 fullName: function () { 249 return this.getEach('firstName', 'lastName').compact().join(' '); 250 }.property('firstName', 'lastName') 251 252 When you call get() on a computed property, the property function will be 253 called and the return value will be returned instead of the function 254 itself. 255 256 Unknown Properties 257 --- 258 259 Likewise, if you try to call get() on a property whose values is 260 undefined, the unknownProperty() method will be called on the object. 261 If this method returns any value other than undefined, it will be returned 262 instead. This allows you to implement "virtual" properties that are 263 not defined upfront. 264 265 @param {String} key the property to retrieve 266 @returns {Object} the property value or undefined. 267 268 */ 269 get: function (key) { 270 var ret = this[key], cache; 271 if (ret === undefined) { 272 return this.unknownProperty(key); 273 } else if (ret && ret.isProperty) { 274 if (ret.isCacheable) { 275 cache = this._kvo_cache; 276 if (!cache) cache = this._kvo_cache = {}; 277 278 return (cache[ret.cacheKey] !== undefined) ? cache[ret.cacheKey] : (cache[ret.cacheKey] = ret.call(this, key)); 279 } else return ret.call(this, key); 280 } else return ret; 281 }, 282 283 /** 284 Sets the key equal to value. 285 286 This method is generally very similar to calling object[key] = value or 287 object.key = value, except that it provides support for computed 288 properties, the unknownProperty() method and property observers. 289 290 Computed Properties 291 --- 292 293 If you try to set a value on a key that has a computed property handler 294 defined (see the get() method for an example), then set() will call 295 that method, passing both the value and key instead of simply changing 296 the value itself. This is useful for those times when you need to 297 implement a property that is composed of one or more member 298 properties. 299 300 Unknown Properties 301 --- 302 303 If you try to set a value on a key that is undefined in the target 304 object, then the unknownProperty() handler will be called instead. This 305 gives you an opportunity to implement complex "virtual" properties that 306 are not predefined on the object. If unknownProperty() returns 307 undefined, then set() will simply set the value on the object. 308 309 Property Observers 310 --- 311 312 In addition to changing the property, set() will also register a 313 property change with the object. Unless you have placed this call 314 inside of a beginPropertyChanges() and endPropertyChanges(), any "local" 315 observers (i.e. observer methods declared on the same object), will be 316 called immediately. Any "remote" observers (i.e. observer methods 317 declared on another object) will be placed in a queue and called at a 318 later time in a coalesced manner. 319 320 Chaining 321 --- 322 323 In addition to property changes, set() returns the value of the object 324 itself so you can do chaining like this: 325 326 record.set('firstName', 'Charles').set('lastName', 'Jolley'); 327 328 @param {String|Hash} key the property to set 329 @param {Object} value the value to set or null. 330 @returns {SC.Observable} 331 */ 332 set: function (key, value) { 333 var func = this[key], 334 notify = this.automaticallyNotifiesObserversFor(key), 335 ret = value, 336 cachedep, cache, idx, dfunc; 337 338 if (value === undefined && SC.typeOf(key) === SC.T_HASH) { 339 var hash = key; 340 341 for (var hashKey in hash) { 342 if (!hash.hasOwnProperty(hashKey)) continue; 343 this.set(hashKey, hash[hashKey]); 344 } 345 346 return this; 347 } 348 349 // if there are any dependent keys and they use caching, then clear the 350 // cache. (If we're notifying, then propertyDidChange will do this for 351 // us.) 352 if (!notify && this._kvo_cacheable && (cache = this._kvo_cache)) { 353 // lookup the cached dependents for this key. if undefined, compute. 354 // note that if cachdep is set to null is means we figure out it has no 355 // cached dependencies already. this is different from undefined. 356 cachedep = this._kvo_cachedep; 357 if (!cachedep || (cachedep = cachedep[key]) === undefined) { 358 cachedep = this._kvo_computeCachedDependentsFor(key); 359 } 360 361 if (cachedep) { 362 idx = cachedep.length; 363 while (--idx >= 0) { 364 dfunc = cachedep[idx]; 365 cache[dfunc.cacheKey] = cache[dfunc.lastSetValueKey] = undefined; 366 } 367 } 368 } 369 370 // set the value. 371 if (func && func.isProperty) { 372 cache = this._kvo_cache; 373 if (func.isVolatile || !cache || (cache[func.lastSetValueKey] !== value)) { 374 if (!cache) cache = this._kvo_cache = {}; 375 376 cache[func.lastSetValueKey] = value; 377 if (notify) this.propertyWillChange(key); 378 ret = func.call(this, key, value); 379 380 // update cached value 381 if (func.isCacheable) cache[func.cacheKey] = ret; 382 if (notify) this.propertyDidChange(key, ret, YES); 383 } 384 385 } else if (func === undefined) { 386 if (notify) this.propertyWillChange(key); 387 this.unknownProperty(key, value); 388 if (notify) this.propertyDidChange(key, ret); 389 390 } else { 391 if (this[key] !== value) { 392 if (notify) this.propertyWillChange(key); 393 ret = this[key] = value; 394 if (notify) this.propertyDidChange(key, ret); 395 } 396 } 397 398 return this; 399 }, 400 401 /** 402 Called whenever you try to get or set an undefined property. 403 404 This is a generic property handler. If you define it, it will be called 405 when the named property is not yet set in the object. The default does 406 nothing. 407 408 @param {String} key the key that was requested 409 @param {Object} value The value if called as a setter, undefined if called as a getter. 410 @returns {Object} The new value for key. 411 */ 412 unknownProperty: function (key, value) { 413 if (value !== undefined) { this[key] = value; } 414 return value; 415 }, 416 417 /** 418 Begins a grouping of property changes. 419 420 You can use this method to group property changes so that notifications 421 will not be sent until the changes are finished. If you plan to make a 422 large number of changes to an object at one time, you should call this 423 method at the beginning of the changes to suspend change notifications. 424 When you are done making changes, call endPropertyChanges() to allow 425 notification to resume. 426 427 @returns {SC.Observable} 428 */ 429 beginPropertyChanges: function () { 430 this._kvo_changeLevel = this._kvo_changeLevel + 1; 431 return this; 432 }, 433 434 /** 435 Ends a grouping of property changes. 436 437 You can use this method to group property changes so that notifications 438 will not be sent until the changes are finished. If you plan to make a 439 large number of changes to an object at one time, you should call 440 beginPropertyChanges() at the beginning of the changes to suspend change 441 notifications. When you are done making changes, call this method to allow 442 notification to resume. 443 444 @returns {SC.Observable} 445 */ 446 endPropertyChanges: function () { 447 this._kvo_changeLevel = (this._kvo_changeLevel || 1) - 1; 448 var level = this._kvo_changeLevel, changes = this._kvo_changes; 449 if ((level <= 0) && changes && (changes.length > 0) && !SC.Observers.isObservingSuspended) { 450 this._notifyPropertyObservers(); 451 } 452 return this; 453 }, 454 455 /** 456 Notify the observer system that a property is about to change. 457 458 Sometimes you need to change a value directly or indirectly without 459 actually calling get() or set() on it. In this case, you can use this 460 method and propertyDidChange() instead. Calling these two methods 461 together will notify all observers that the property has potentially 462 changed value. 463 464 Note that you must always call propertyWillChange and propertyDidChange as 465 a pair. If you do not, it may get the property change groups out of order 466 and cause notifications to be delivered more often than you would like. 467 468 @param {String} key The property key that is about to change. 469 @returns {SC.Observable} 470 */ 471 propertyWillChange: function (key) { 472 return this; 473 }, 474 475 /** 476 Notify the observer system that a property has just changed. 477 478 Sometimes you need to change a value directly or indirectly without 479 actually calling get() or set() on it. In this case, you can use this 480 method and propertyWillChange() instead. Calling these two methods 481 together will notify all observers that the property has potentially 482 changed value. 483 484 Note that you must always call propertyWillChange and propertyDidChange as 485 a pair. If you do not, it may get the property change groups out of order 486 and cause notifications to be delivered more often than you would like. 487 488 @param {String} key The property key that has just changed. 489 @param {Object} value The new value of the key. May be null. 490 @param {Boolean} _keepCache Private property 491 @returns {SC.Observable} 492 */ 493 propertyDidChange: function (key, value, _keepCache) { 494 this._kvo_revision = this._kvo_revision + 1; 495 var level = this._kvo_changeLevel, 496 cachedep, idx, dfunc, func; 497 498 //@if(debug) 499 var log = SC.LOG_OBSERVERS && (this.LOG_OBSERVING !== NO); 500 //@endif 501 502 // If any dependent keys contain this property in their path, 503 // invalidate the cache of the computed property and re-setup chain with 504 // new value. 505 var chains = this._kvo_property_chains; 506 if (chains) { 507 var keyChains = chains[key]; 508 509 if (keyChains) { 510 this.beginPropertyChanges(); 511 keyChains = SC.clone(keyChains); 512 keyChains.forEach(function (chain) { 513 // Invalidate the property that depends on the changed key. 514 chain.notifyPropertyDidChange(); 515 }); 516 this.endPropertyChanges(); 517 } 518 } 519 520 var cache = this._kvo_cache; 521 if (cache) { 522 523 // clear any cached value 524 if (!_keepCache) { 525 func = this[key]; 526 if (func && func.isProperty) { 527 cache[func.cacheKey] = cache[func.lastSetValueKey] = undefined; 528 } 529 } 530 531 if (this._kvo_cacheable) { 532 // if there are any dependent keys and they use caching, then clear the 533 // cache. This is the same code as is in set. It is inlined for perf. 534 cachedep = this._kvo_cachedep; 535 if (!cachedep || (cachedep = cachedep[key]) === undefined) { 536 cachedep = this._kvo_computeCachedDependentsFor(key); 537 } 538 539 if (cachedep) { 540 idx = cachedep.length; 541 while (--idx >= 0) { 542 dfunc = cachedep[idx]; 543 cache[dfunc.cacheKey] = cache[dfunc.lastSetValueKey] = undefined; 544 } 545 } 546 } 547 } 548 549 // save in the change set if queuing changes 550 var suspended = SC.Observers.isObservingSuspended; 551 if ((level > 0) || suspended) { 552 var changes = this._kvo_changes; 553 if (!changes) changes = this._kvo_changes = SC.CoreSet.create(); 554 changes.add(key); 555 556 if (suspended) { 557 //@if(debug) 558 if (log) console.log("%@%@: will not notify observers because observing is suspended".fmt(SC.KVO_SPACES, this)); 559 //@endif 560 SC.Observers.objectHasPendingChanges(this); 561 } 562 563 // otherwise notify property observers immediately 564 } else { 565 this._notifyPropertyObservers(key); 566 } 567 568 return this; 569 }, 570 571 // .......................................... 572 // DEPENDENT KEYS 573 // 574 575 /** 576 Use this to indicate that one key changes if other keys it depends on 577 change. Pass the key that is dependent and additional keys it depends 578 upon. You can either pass the additional keys inline as arguments or 579 in a single array. 580 581 You generally do not call this method, but instead pass dependent keys to 582 your property() method when you declare a computed property. 583 584 You can call this method during your init to register the keys that should 585 trigger a change notification for your computed properties. 586 587 @param {String} key the dependent key 588 @param {Array|String} dependentKeys one or more dependent keys 589 @returns {Object} this 590 */ 591 registerDependentKey: function (key, dependentKeys) { 592 var dependents = this._kvo_dependents, 593 // chainDependents = this._kvo_chain_dependents, 594 keys, idx, lim, dep, queue; 595 596 // normalize input. 597 if (typeof dependentKeys === "object" && (dependentKeys instanceof Array)) { 598 keys = dependentKeys; 599 lim = 0; 600 } else { 601 keys = arguments; 602 lim = 1; 603 } 604 idx = keys.length; 605 606 // define dependents if not defined already. 607 if (!dependents) this._kvo_dependents = dependents = {}; 608 609 // for each key, build array of dependents, add this key... 610 // note that we ignore the first argument since it is the key... 611 while (--idx >= lim) { 612 dep = keys[idx]; 613 614 if (dep.indexOf('.') >= 0) { 615 SC._PropertyChain.createChain(dep, this, key).activate(); 616 } else { 617 // add dependent key to dependents array of key it depends on 618 queue = dependents[dep]; 619 if (!queue) { queue = dependents[dep] = []; } 620 queue.push(key); 621 } 622 } 623 }, 624 625 /** @private 626 Register a property chain so that dependent keys can be invalidated 627 when a property on this object changes. 628 629 @param {String} property the property on this object that invalidates the chain 630 @param {SC._PropertyChain} chain the chain to notify 631 */ 632 registerDependentKeyWithChain: function (property, chain) { 633 var chains = this._chainsFor(property); 634 chains.add(chain); 635 }, 636 637 /** @private 638 Removes a property chain from the object. 639 640 @param {String} property the property on this object that invalidates the chain 641 @param {SC._PropertyChain} chain the chain to notify 642 */ 643 removeDependentKeyWithChain: function (property, chain) { 644 var chains = this._chainsFor(property); 645 chains.remove(chain); 646 647 if (chains.get('length') === 0) { 648 delete this._kvo_property_chains[property]; 649 } 650 }, 651 652 /** @private 653 Returns an instance of SC.CoreSet in which to save SC._PropertyChains. 654 655 @param {String} property the property associated with the SC._PropertyChain 656 @returns {SC.CoreSet} 657 */ 658 _chainsFor: function (property) { 659 this._kvo_property_chains = this._kvo_property_chains || {}; 660 var chains = this._kvo_property_chains[property] || SC.CoreSet.create(); 661 this._kvo_property_chains[property] = chains; 662 663 return chains; 664 }, 665 666 /** @private 667 668 Helper method used by computeCachedDependents. Just loops over the 669 array of dependent keys. If the passed function is cacheable, it will 670 be added to the queue. Also, recursively call on each keys dependent 671 keys. 672 673 @param {Array} queue the queue to add functions to 674 @param {Array} keys the array of dependent keys for this key 675 @param {Hash} dependents the _kvo_dependents cache 676 @param {SC.Set} seen already seen keys 677 @returns {void} 678 */ 679 _kvo_addCachedDependents: function (queue, keys, dependents, seen) { 680 var idx = keys.length, 681 func, key, deps; 682 683 while (--idx >= 0) { 684 key = keys[idx]; 685 seen.add(key); 686 687 // if the value for this key is a computed property, then add it to the 688 // set if it is cacheable, and process any of its dependent keys also. 689 func = this[key]; 690 if (func && (func instanceof Function) && func.isProperty) { 691 if (func.isCacheable) queue.push(func); // handle this func 692 if ((deps = dependents[key]) && deps.length > 0) { // and any dependents 693 this._kvo_addCachedDependents(queue, deps, dependents, seen); 694 } 695 } 696 } 697 698 }, 699 700 /** @private 701 702 Called by set() whenever it needs to determine which cached dependent 703 keys to clear. Recursively searches dependent keys to determine all 704 cached property directly or indirectly affected. 705 706 The return value is also saved for future reference 707 708 @param {String} key the key to compute 709 @returns {Array} 710 */ 711 _kvo_computeCachedDependentsFor: function (key) { 712 var cached = this._kvo_cachedep, 713 dependents = this._kvo_dependents, 714 keys = dependents ? dependents[key] : null, 715 queue, seen; 716 if (!cached) cached = this._kvo_cachedep = {}; 717 718 // if there are no dependent keys, then just set and return null to avoid 719 // this mess again. 720 if (!keys || keys.length === 0) return cached[key] = null; 721 722 // there are dependent keys, so we need to do the work to find out if 723 // any of them or their dependent keys are cached. 724 queue = cached[key] = []; 725 seen = SC._TMP_SEEN_SET = (SC._TMP_SEEN_SET || SC.CoreSet.create()); 726 seen.add(key); 727 this._kvo_addCachedDependents(queue, keys, dependents, seen); 728 seen.clear(); // reset 729 730 if (queue.length === 0) queue = cached[key] = null; // turns out nothing 731 return queue; 732 }, 733 734 // .......................................... 735 // OBSERVERS 736 // 737 738 _kvo_for: function (kvoKey, type) { 739 var ret = this[kvoKey]; 740 741 if (!this._kvo_cloned) this._kvo_cloned = {}; 742 743 // if the item does not exist, create it. Unless type is passed, 744 // assume array. 745 if (!ret) { 746 ret = this[kvoKey] = (type === undefined) ? [] : type.create(); 747 this._kvo_cloned[kvoKey] = YES; 748 749 // if item does exist but has not been cloned, then clone it. Note 750 // that all types must implement copy().0 751 } else if (!this._kvo_cloned[kvoKey]) { 752 ret = this[kvoKey] = ret.copy(); 753 this._kvo_cloned[kvoKey] = YES; 754 } 755 756 return ret; 757 }, 758 759 /** 760 Adds an observer on a property. 761 762 This is the core method used to register an observer for a property. 763 764 Once you call this method, anytime the key's value is set, your observer 765 will be notified. Note that the observers are triggered anytime the 766 value is set, regardless of whether it has actually changed. Your 767 observer should be prepared to handle that. 768 769 You can also pass an optional context parameter to this method. The 770 context will be passed to your observer method whenever it is triggered. 771 Note that if you add the same target/method pair on a key multiple times 772 with different context parameters, your observer will only be called once 773 with the last context you passed. 774 775 Observer Methods 776 --- 777 778 Observer methods you pass should generally have the following signature if 779 you do not pass a "context" parameter: 780 781 fooDidChange: function (sender, key, value, rev); 782 783 The sender is the object that changed. The key is the property that 784 changes. The value property is currently reserved and unused. The rev 785 is the last property revision of the object when it changed, which you can 786 use to detect if the key value has really changed or not. 787 788 If you pass a "context" parameter, the context will be passed before the 789 revision like so: 790 791 fooDidChange: function (sender, key, value, context, rev); 792 793 Usually you will not need the value, context or revision parameters at 794 the end. In this case, it is common to write observer methods that take 795 only a sender and key value as parameters or, if you aren't interested in 796 any of these values, to write an observer that has no parameters at all. 797 798 @param {String} key the key to observer 799 @param {Object} target the target object to invoke 800 @param {String|Function} method the method to invoke. 801 @param {Object} context optional context 802 @returns {SC.Object} self 803 */ 804 addObserver: function (key, target, method, context) { 805 var kvoKey, chain; 806 807 // normalize. if a function is passed to target, make it the method. 808 if (method === undefined) { 809 method = target; 810 target = this; 811 } 812 if (!target) target = this; 813 814 if (typeof method === "string") method = target[method]; 815 if (!method) throw new Error("You must pass a method to addObserver()"); 816 817 // Normalize key... 818 key = key.toString(); 819 if (key.indexOf('.') >= 0) { 820 821 // create the chain and save it for later so we can tear it down if 822 // needed. 823 chain = SC._ChainObserver.createChain(this, key, target, method, context); 824 chain.masterTarget = target; 825 chain.masterMethod = method; 826 827 // Save in set for chain observers. 828 this._kvo_for(SC.keyFor('_kvo_chains', key)).push(chain); 829 830 // Create observers if needed... 831 } else { 832 833 // Special case to support reduced properties. If the property 834 // key begins with '@' and its value is unknown, then try to get its 835 // value. This will configure the dependent keys if needed. 836 if ((this[key] === undefined) && (key.indexOf('@') === 0)) { 837 this.get(key); 838 } 839 840 if (target === this) target = null; // use null for observers only. 841 kvoKey = SC.keyFor('_kvo_observers', key); 842 this._kvo_for(kvoKey, SC.ObserverSet).add(target, method, context); 843 this._kvo_for('_kvo_observed_keys', SC.CoreSet).add(key); 844 } 845 846 if (this.didAddObserver) this.didAddObserver(key, target, method); 847 return this; 848 }, 849 850 /** 851 Remove an observer you have previously registered on this object. Pass 852 the same key, target, and method you passed to addObserver() and your 853 target will no longer receive notifications. 854 855 @param {String} key the key to observer 856 @param {Object} target the target object to invoke 857 @param {String|Function} method the method to invoke. 858 @returns {SC.Observable} receiver 859 */ 860 removeObserver: function (key, target, method) { 861 862 var kvoKey, chains, chain, observers, idx; 863 864 // normalize. if a function is passed to target, make it the method. 865 if (method === undefined) { 866 method = target; 867 target = this; 868 } 869 if (!target) target = this; 870 871 if (typeof method === "string") method = target[method]; 872 if (!method) throw new Error("You must pass a method to removeObserver()"); 873 874 // if the key contains a '.', this is a chained observer. 875 key = key.toString(); 876 if (key.indexOf('.') >= 0) { 877 878 // try to find matching chains 879 kvoKey = SC.keyFor('_kvo_chains', key); 880 if (chains = this[kvoKey]) { 881 882 // if chains have not been cloned yet, do so now. 883 chains = this._kvo_for(kvoKey); 884 885 // remove any chains 886 idx = chains.length; 887 while (--idx >= 0) { 888 chain = chains[idx]; 889 if (chain && (chain.masterTarget === target) && (chain.masterMethod === method)) { 890 chains[idx] = chain.destroyChain(); 891 } 892 } 893 } 894 895 // otherwise, just like a normal observer. 896 } else { 897 if (target === this) target = null; // use null for observers only. 898 kvoKey = SC.keyFor('_kvo_observers', key); 899 if (observers = this[kvoKey]) { 900 // if observers have not been cloned yet, do so now 901 observers = this._kvo_for(kvoKey); 902 observers.remove(target, method); 903 904 // Remove the key when no members remain. 905 if (observers.getMembers().length === 0) { 906 this._kvo_for('_kvo_observed_keys', SC.CoreSet).remove(key); 907 } 908 } 909 } 910 911 if (this.didRemoveObserver) this.didRemoveObserver(key, target, method); 912 return this; 913 }, 914 915 /** 916 Returns YES if the object currently has observers registered for a 917 particular key. You can use this method to potentially defer performing 918 an expensive action until someone begins observing a particular property 919 on the object. 920 921 Optionally, you may pass a target and method to check for the 922 presence of a particular observer. You can use this to avoid creating 923 duplicate observers in situations where that's likely. 924 925 @param {String} key key to check 926 @param {Object} [target] the target that the observer uses 927 @param {Function|String} [method]) the method on the target that the observer uses 928 @returns {Boolean} 929 */ 930 hasObserverFor: function (key, target, method) { 931 SC.Observers.flush(this); // hookup as many observers as possible. 932 933 var observers = this[SC.keyFor('_kvo_observers', key)], 934 chains = this._kvo_for(SC.keyFor('_kvo_chains', key)), 935 locals = this[SC.keyFor('_kvo_local', key)], 936 isChain = key.indexOf('.') >= 0; 937 938 // Fast path: no target/method. 939 if (target === undefined) { 940 if (isChain) { 941 if (chains && chains.length > 0) return YES; 942 } else { 943 // Found locally. 944 if (locals && locals.length > 0) return YES; 945 if (observers && observers.getMembers().length > 0) return YES; 946 } 947 return NO; 948 949 // Slow path: target/method. 950 } else { 951 if (method === undefined) { 952 method = target; 953 target = this; 954 } 955 if (typeof method === "string") method = target[method]; 956 if (!method) throw new Error("Developer Error: If present, the `method` argument of hasObserverFor must be (or refer to) a function."); 957 958 // Declare our iterators. 959 var i, len; 960 961 // Check remote chains. 962 if (isChain) { 963 if (!chains || !chains.length) return NO; 964 len = chains.length; 965 for (i = 0; i < len; i++) { 966 if (chains[i].masterTarget === target && chains[i].masterMethod === method) return YES; 967 } 968 return NO; 969 970 // Check locals. 971 } else if (target === this) { 972 if (!locals) return NO; 973 len = locals.length; 974 for (i = 0; i < len; i++) { 975 if (this[locals[i]] === method) return YES; 976 } 977 return NO; 978 979 // Check remotes. 980 } else { 981 if (!observers || !observers.members) return NO; 982 983 len = observers.members.length; 984 var member; 985 for (i = 0; i < len; i++) { 986 member = observers.members[i]; 987 // If this is a non-chained observer, the first item is the target and the second is the method. 988 if (member[0] === target && member[1] === method) return YES; 989 } 990 return NO; 991 } 992 // TODO: Remote chains. 993 } 994 }, 995 996 /** 997 This method will register any observers and computed properties saved on 998 the object. Normally you do not need to call this method yourself. It 999 is invoked automatically just before property notifications are sent and 1000 from the init() method of SC.Object. You may choose to call this 1001 from your own initialization method if you are using SC.Observable in 1002 a non-SC.Object-based object. 1003 1004 This method looks for several private variables, which you can setup, 1005 to initialize: 1006 1007 - _observers: this should contain an array of key names for observers 1008 you need to configure. 1009 1010 - _bindings: this should contain an array of key names that configure 1011 bindings. 1012 1013 - _properties: this should contain an array of key names for computed 1014 properties. 1015 1016 @returns {Object} this 1017 */ 1018 initObservable: function () { 1019 if (this._observableInited) return; 1020 this._observableInited = YES; 1021 1022 var loc, keys, key, value, observer, propertyPaths, propertyPathsLength, 1023 len, ploc, path, propertyKey, keysLen; 1024 1025 // Loop through observer functions and register them 1026 if (keys = this._observers) { 1027 len = keys.length; 1028 for (loc = 0; loc < len; loc++) { 1029 key = keys[loc]; 1030 observer = this[key]; 1031 propertyPaths = observer.propertyPaths; 1032 propertyPathsLength = (propertyPaths) ? propertyPaths.length : 0; 1033 for (ploc = 0 ; ploc < propertyPathsLength; ploc++) { 1034 path = propertyPaths[ploc]; 1035 this.addObservesHandler(observer, path); 1036 } 1037 } 1038 } 1039 1040 // Add Bindings 1041 this.bindings = []; // will be filled in by the bind() method. 1042 if (keys = this._bindings) { 1043 for (loc = 0, keysLen = keys.length; loc < keysLen; loc++) { 1044 // get propertyKey 1045 key = keys[loc]; 1046 value = this[key]; 1047 propertyKey = key.slice(0, -7); // contentBinding => content 1048 1049 // Replace the short form property with the new binding object. 1050 this[key] = this.bind(propertyKey, value); 1051 } 1052 } 1053 1054 // Add Properties 1055 if (keys = this._properties) { 1056 for (loc = 0, keysLen = keys.length; loc < keysLen; loc++) { 1057 key = keys[loc]; 1058 if (value = this[key]) { 1059 1060 // activate cacheable only if needed for perf reasons 1061 if (value.isCacheable) this._kvo_cacheable = YES; 1062 1063 // register dependent keys 1064 if (value.dependentKeys && (value.dependentKeys.length > 0)) { 1065 this.registerDependentKey(key, value.dependentKeys); 1066 } 1067 } 1068 } 1069 } 1070 1071 // Clean up these properties once they have been used. 1072 delete this._bindings; 1073 delete this._properties; 1074 1075 return this; 1076 }, 1077 1078 /** 1079 This method will destroy the observable. 1080 1081 @returns {Object} this 1082 */ 1083 destroyObservable: function () { 1084 var key, keys, 1085 len, 1086 observer, 1087 path, 1088 propertyPaths, 1089 propertyPathsLength; 1090 1091 // Destroy bindings 1092 this.bindings.invoke('destroy'); 1093 delete this.bindings; 1094 1095 // Loop through observer functions and remove them 1096 if (keys = this._observers) { 1097 len = keys.length; 1098 for (var loc = 0; loc < len; loc++) { 1099 key = keys[loc]; 1100 observer = this[key]; 1101 propertyPaths = observer.propertyPaths; 1102 propertyPathsLength = (propertyPaths) ? propertyPaths.length : 0; 1103 1104 for (var ploc = 0; ploc < propertyPathsLength; ploc++) { 1105 path = propertyPaths[ploc]; 1106 this.removeObservesHandler(observer, path); 1107 } 1108 } 1109 } 1110 1111 delete this._observers; 1112 1113 return this; 1114 }, 1115 1116 /** 1117 Will add an observes handler to this object for a given property path. 1118 1119 In most cases, the path provided is relative to this object. However, 1120 if the path begins with a capital character then the path is considered 1121 relative to the window object. 1122 1123 @param {Function} observer the function on this object that will be 1124 notified of changes 1125 @param {String} path a property path string 1126 @return {Object} returns this 1127 */ 1128 addObservesHandler: function (observer, path) { 1129 this._configureObservesHandler(SC.OBSERVES_HANDLER_ADD, observer, path); 1130 return this; 1131 }, 1132 1133 /** 1134 Will remove an observes handler from this object for a given property path. 1135 1136 In most cases, the path provided is relative to this object. However, 1137 if the path begins with a capital character then the path is considered 1138 relative to the window object. 1139 1140 @param {Function} observer the function on this object that will be 1141 notified of changes 1142 @param {String} path a property path string 1143 @return {Object} returns this 1144 */ 1145 removeObservesHandler: function (observer, path) { 1146 this._configureObservesHandler(SC.OBSERVES_HANDLER_REMOVE, observer, path); 1147 return this; 1148 }, 1149 1150 /** @private 1151 1152 Used to either add or remove an observer handler on this object 1153 for a given property path. 1154 1155 In most cases, the path provided is relative to this object. However, 1156 if the path begins with a capital character then the path is considered 1157 relative to the window object. 1158 1159 You must supply an action that is to be performed by this method. The 1160 action can either be `SC.OBSERVES_HANDLER_ADD` or `SC.OBSERVES_HANDLER_REMOVE`. 1161 1162 @param {Function} observer the function on this object that will be 1163 notified of changes 1164 @param {String} path a property path string 1165 @param {String} path a dot-notation property path string 1166 */ 1167 _configureObservesHandler: function (action, observer, path) { 1168 var dotIndex, root; 1169 1170 switch (action) { 1171 case SC.OBSERVES_HANDLER_ADD: 1172 action = "addObserver"; 1173 break; 1174 case SC.OBSERVES_HANDLER_REMOVE: 1175 action = "removeObserver"; 1176 break; 1177 default: 1178 throw new Error("invalid action provided: " + action); 1179 } 1180 1181 dotIndex = path.indexOf('.'); 1182 1183 if (dotIndex < 0) { 1184 this[action](path, this, observer); 1185 } else if (path.indexOf('*') === 0) { 1186 this[action](path.slice(1), this, observer); 1187 } else { 1188 root = null; 1189 1190 if (dotIndex === 0) { 1191 root = this; 1192 path = path.slice(1); 1193 } else if (dotIndex === 4 && path.slice(0, 5) === 'this.') { 1194 root = this; 1195 path = path.slice(5); 1196 } else if (dotIndex < 0 && path.length === 4 && path === 'this') { 1197 root = this; 1198 path = ''; 1199 } else if (dotIndex > 0 && path[0] === path.charAt(0).toLowerCase()) { 1200 // if the first character for the given path is lower case 1201 // then we assume the path is relative to this 1202 root = this; 1203 } 1204 1205 SC.Observers[action](path, this, observer, root); 1206 } 1207 }, 1208 1209 // .......................................... 1210 // NOTIFICATION 1211 // 1212 1213 /** 1214 Returns an array with all of the observers registered for the specified 1215 key. This is intended for debugging purposes only. You generally do not 1216 want to rely on this method for production code. 1217 1218 @param {String} key the key to evaluate 1219 @returns {Array} array of Observer objects, describing the observer. 1220 */ 1221 observersForKey: function (key) { 1222 SC.Observers.flush(this); // hookup as many observers as possible. 1223 1224 var observers = this[SC.keyFor('_kvo_observers', key)]; 1225 return observers ? observers.getMembers() : []; 1226 }, 1227 1228 /** @private 1229 This private method actually notifies the observers for any keys in the observer queue. If you 1230 pass a key it will be added to the queue. 1231 */ 1232 _notifyPropertyObservers: function (key) { 1233 // Ensure that this object has been initialized. 1234 if (!this._observableInited) this.initObservable(); 1235 1236 SC.Observers.flush(this); // hookup as many observers as possible. 1237 1238 var observers, changes, dependents, starObservers, idx, keys, rev, 1239 members, membersLength, member, memberLoc, target, method, loc, func, 1240 context, spaces, cache; 1241 1242 //@if(debug) 1243 var log = SC.LOG_OBSERVERS && this.LOG_OBSERVING !== NO; 1244 if (log) { 1245 spaces = SC.KVO_SPACES = (SC.KVO_SPACES || '') + ' '; 1246 console.log('%@%@: notifying observers after change to key "%@"'.fmt(spaces, this, key)); 1247 } 1248 //@endif 1249 1250 // Get any starObservers -- they will be notified of all changes. 1251 starObservers = this['_kvo_observers_*']; 1252 1253 // prevent notifications from being sent until complete 1254 this._kvo_changeLevel = this._kvo_changeLevel + 1; 1255 1256 // keep sending notifications as long as there are changes 1257 while (((changes = this._kvo_changes) && (changes.length > 0)) || key) { 1258 1259 // increment revision 1260 rev = ++this.propertyRevision; 1261 1262 // save the current set of changes and swap out the kvo_changes so that 1263 // any set() calls by observers will be saved in a new set. 1264 if (!changes) changes = SC.CoreSet.create(); 1265 this._kvo_changes = null; 1266 1267 // Add the passed key to the changes set. If a '*' was passed, then 1268 // add all keys in the observers to the set... 1269 // once finished, clear the key so the loop will end. 1270 if (key === '*') { 1271 changes.add('*'); 1272 changes.addEach(this._kvo_for('_kvo_observed_keys', SC.CoreSet)); 1273 1274 } else if (key) { 1275 changes.add(key); 1276 } 1277 1278 // Now go through the set and add all dependent keys... 1279 dependents = this._kvo_dependents; 1280 if (dependents) { 1281 1282 // NOTE: each time we loop, we check the changes length, this 1283 // way any dependent keys added to the set will also be evaluated... 1284 for (idx = 0; idx < changes.length; idx++) { 1285 key = changes[idx]; 1286 keys = dependents[key]; 1287 1288 // for each dependent key, add to set of changes. Also, if key 1289 // value is a cacheable property, clear the cached value... 1290 if (keys && (loc = keys.length)) { 1291 //@if(debug) 1292 if (log) { 1293 console.log("%@...including dependent keys for %@: %@".fmt(spaces, key, keys)); 1294 } 1295 //@endif 1296 cache = this._kvo_cache; 1297 if (!cache) cache = this._kvo_cache = {}; 1298 while (--loc >= 0) { 1299 changes.add(key = keys[loc]); 1300 if (func = this[key]) { 1301 this[func.cacheKey] = undefined; 1302 cache[func.cacheKey] = cache[func.lastSetValueKey] = undefined; 1303 } // if (func=) 1304 } // while (--loc) 1305 } // if (keys && 1306 } // for(idx... 1307 } // if (dependents...) 1308 1309 // now iterate through all changed keys and notify observers. 1310 while (changes.length > 0) { 1311 key = changes.pop(); // the changed key 1312 1313 // find any observers and notify them... 1314 observers = this[SC.keyFor('_kvo_observers', key)]; 1315 1316 if (observers) { 1317 // We need to clone the 'members' structure here in case any of the 1318 // observers we're about to notify happen to remove observers for 1319 // this key, which would mutate the structure underneath us. 1320 // (Cloning it rather than mutating gives us a clear policy: if you 1321 // were registered as an observer at the time notification begins, 1322 // you will be notified, regardless of whether you're removed as an 1323 // observer during that round of notification. Similarly, if you're 1324 // added as an observer during the notification round by another 1325 // observer, you will not be notified until the next time.) 1326 members = observers.getMembers(); 1327 membersLength = members.length; 1328 1329 for (memberLoc = 0; memberLoc < membersLength; memberLoc++) { 1330 member = members[memberLoc]; 1331 1332 if (member[3] === rev) continue; // skip notified items. 1333 1334 if (!member[1]) console.log(member); 1335 1336 target = member[0] || this; 1337 method = member[1]; 1338 context = member[2]; 1339 member[3] = rev; 1340 1341 //@if(debug) 1342 if (log) console.log('%@...firing observer on %@ for key "%@"'.fmt(spaces, target, key)); 1343 //@endif 1344 if (context !== undefined) { 1345 method.call(target, this, key, null, context, rev); 1346 } else { 1347 method.call(target, this, key, null, rev); 1348 } 1349 } 1350 } 1351 1352 // look for local observers. Local observers are added by SC.Object 1353 // as an optimization to avoid having to add observers for every 1354 // instance when you are just observing your local object. 1355 members = this[SC.keyFor('_kvo_local', key)]; 1356 if (members) { 1357 // Note: Since, unlike above, we don't expect local observers to be 1358 // removed in general, we will not clone 'members'. 1359 membersLength = members.length; 1360 for (memberLoc = 0; memberLoc < membersLength; memberLoc++) { 1361 member = members[memberLoc]; 1362 method = this[member]; // try to find observer function 1363 if (method) { 1364 //@if(debug) 1365 if (log) console.log('%@...firing local observer %@.%@ for key "%@"'.fmt(spaces, this, member, key)); 1366 //@endif 1367 1368 method.call(this, this, key, null, rev); 1369 } 1370 } 1371 } 1372 1373 // if there are starObservers, do the same thing for them 1374 if (starObservers && key !== '*') { 1375 // We clone the structure per the justification, above, for regular 1376 // observers. 1377 members = starObservers.getMembers(); 1378 membersLength = members.length; 1379 for (memberLoc = 0; memberLoc < membersLength; memberLoc++) { 1380 member = members[memberLoc]; 1381 target = member[0] || this; 1382 method = member[1]; 1383 context = member[2]; 1384 1385 //@if(debug) 1386 if (log) console.log('%@...firing * observer on %@ for key "%@"'.fmt(spaces, target, key)); 1387 //@endif 1388 if (context !== undefined) { 1389 method.call(target, this, key, null, context, rev); 1390 } else { 1391 method.call(target, this, key, null, rev); 1392 } 1393 } 1394 } 1395 1396 // if there is a default property observer, call that also 1397 if (this.propertyObserver) { 1398 //@if(debug) 1399 if (log) console.log('%@...firing %@.propertyObserver for key "%@"'.fmt(spaces, this, key)); 1400 //@endif 1401 this.propertyObserver(this, key, null, rev); 1402 } 1403 } // while(changes.length>0) 1404 1405 // changes set should be empty. release it for reuse 1406 if (changes) changes.destroy(); 1407 1408 // key is no longer needed; clear it to avoid infinite loops 1409 key = null; 1410 1411 } // while (changes) 1412 1413 // done with loop, reduce change level so that future sets can resume 1414 this._kvo_changeLevel = (this._kvo_changeLevel || 1) - 1; 1415 1416 //@if(debug) 1417 if (log) SC.KVO_SPACES = spaces.slice(0, -2); 1418 //@endif 1419 1420 return YES; // finished successfully 1421 }, 1422 1423 // .......................................... 1424 // BINDINGS 1425 // 1426 1427 /** 1428 Manually add a new binding to an object. This is the same as doing 1429 the more familiar propertyBinding: 'property.path' approach. 1430 1431 @param {String} toKey the key to bind to 1432 @param {Object} target target or property path to bind from 1433 @param {String|Function} method method for target to bind from 1434 @returns {SC.Binding} new binding instance 1435 */ 1436 bind: function (toKey, target, method) { 1437 var binding, pathType; 1438 1439 //@if(debug) 1440 // Developer support. 1441 if (!target) { 1442 throw new Error("Developer Error: Attempt to bind key `%@` to null or undefined target".fmt(toKey)); 1443 } 1444 //@endif 1445 1446 // normalize... 1447 if (method !== undefined) target = [target, method]; 1448 1449 pathType = typeof target; 1450 1451 // if a string or array (i.e. tuple) is passed, convert this into a 1452 // binding. If a binding default was provided, use that. 1453 if (pathType === "string" || (pathType === "object" && (target instanceof Array))) { 1454 binding = this[toKey + 'BindingDefault'] || SC.Binding; 1455 binding = binding.beget().from(target); 1456 } else { 1457 // If a binding object was provided, clone it so that it gets 1458 // connected again if the original example binding was already 1459 // connected. 1460 binding = target.beget(); 1461 } 1462 1463 // finish configuring the binding and then connect it. 1464 binding = binding.to(toKey, this).connect(); 1465 this.bindings.push(binding); 1466 1467 return binding; 1468 }, 1469 1470 /** 1471 didChangeFor is a very important method which allows you to tell whether 1472 a property or properties have changed. 1473 1474 The key to using didChangeFor is to pass a unique string as the first argument, 1475 which signals, "Has anything changed since the last time this was called with 1476 this unique key?" The string can be anything you want, as long as it's unique 1477 and stays the same from call to call. 1478 1479 After the key argument, you can pass as many property arguments as you like; 1480 didChangeFor will only return `true` if any of those properties have changed 1481 since the last call. 1482 1483 For example, in your view's update method, you might want to gate DOM changes 1484 (generally a slow operation) on whether the root values have changed. You might 1485 ask the following: 1486 1487 if (this.didChangeFor('updateOnDisplayValue', 'displayValue')) { 1488 // Update the DOM. 1489 } 1490 1491 In another method on the same view, you might send an event if that same value 1492 has changed: 1493 1494 if (this.didChangeFor('otherMethodDisplayValue', 'displayValue')) { 1495 // Send a statechart action. 1496 } 1497 1498 Each call will correctly return whether the property has changed since the last 1499 time displayDidChange was called *with that key*. The following sequence of calls 1500 will return the following values: 1501 1502 - this.set('displayValue', 'value1'); 1503 - this.didChangeFor('updateOnDisplayValue', 'displayValue'); 1504 > true; 1505 - this.didChangeFor('updateOnDisplayValue', 'displayValue'); 1506 > false; 1507 - this.didChangeFor('otherMethodDisplayValue', 'displayValue'); 1508 > true; 1509 - this.set('displayValue', 'value2'); 1510 - this.didChangeFor('updateOnDisplayValue', 'displayValue'); 1511 > true; 1512 - this.didChangeFor('updateOnDisplayValue', 'displayValue'); 1513 > false; 1514 - this.didChangeFor('updateOnDisplayValue', 'displayValue'); 1515 > false; 1516 - this.didChangeFor('otherMethodDisplayValue', 'displayValue'); 1517 > false; 1518 1519 This method works by comparing property revision counts. Every time a 1520 property changes, an internal counter is incremented. When didChangeFor is 1521 invoked, the current revision count of the property is compared to the 1522 revision count from the last time this method was called. 1523 1524 @param {String|Object} context a unique identifier 1525 @param {String…} propertyNames one or more property names 1526 */ 1527 didChangeFor: function (context) { 1528 var valueCache, revisionCache, seenValues, seenRevisions, ret, 1529 currentRevision, idx, key, value; 1530 context = SC.hashFor(context); // get a hash key we can use in caches. 1531 1532 // setup caches... 1533 valueCache = this._kvo_didChange_valueCache; 1534 if (!valueCache) valueCache = this._kvo_didChange_valueCache = {}; 1535 revisionCache = this._kvo_didChange_revisionCache; 1536 if (!revisionCache) revisionCache = this._kvo_didChange_revisionCache = {}; 1537 1538 // get the cache of values and revisions already seen in this context 1539 seenValues = valueCache[context] || {}; 1540 seenRevisions = revisionCache[context] || {}; 1541 1542 // prepare to loop! 1543 ret = false; 1544 currentRevision = this._kvo_revision; 1545 idx = arguments.length; 1546 while (--idx >= 1) { // NB: loop only to 1 to ignore context arg. 1547 key = arguments[idx]; 1548 1549 // has the kvo revision changed since the last time we did this? 1550 if (seenRevisions[key] != currentRevision) { 1551 // yes, check the value with the last seen value 1552 value = this.get(key); 1553 if (seenValues[key] !== value) { 1554 ret = true; // did change! 1555 seenValues[key] = value; 1556 } 1557 } 1558 seenRevisions[key] = currentRevision; 1559 } 1560 1561 valueCache[context] = seenValues; 1562 revisionCache[context] = seenRevisions; 1563 return ret; 1564 }, 1565 1566 /** 1567 Sets the property only if the passed value is different from the 1568 current value. Depending on how expensive a get() is on this property, 1569 this may be more efficient. 1570 1571 NOTE: By default, the set() method will not set the value unless it has 1572 changed. However, this check can skipped by setting .property().idempotent(NO) 1573 setIfChanged() may be useful in this case. 1574 1575 @param {String|Hash} key the key to change 1576 @param {Object} value the value to change 1577 @returns {SC.Observable} 1578 */ 1579 setIfChanged: function (key, value) { 1580 if (value === undefined && SC.typeOf(key) === SC.T_HASH) { 1581 var hash = key; 1582 1583 for (key in hash) { 1584 if (!hash.hasOwnProperty(key)) continue; 1585 this.setIfChanged(key, hash[key]); 1586 } 1587 1588 return this; 1589 } 1590 1591 return (this.get(key) !== value) ? this.set(key, value) : this; 1592 }, 1593 1594 /** 1595 Navigates the property path, returning the value at that point. 1596 1597 If any object in the path is undefined, returns undefined. 1598 @param {String} path The property path you want to retrieve 1599 */ 1600 getPath: function (path) { 1601 var tuple = SC.tupleForPropertyPath(path, this); 1602 if (tuple === null || tuple[0] === null) return undefined; 1603 return SC.get(tuple[0], tuple[1]); 1604 }, 1605 1606 /** 1607 Navigates the property path, finally setting the value. 1608 1609 @param {String} path the property path to set 1610 @param {Object} value the value to set 1611 @returns {SC.Observable} 1612 */ 1613 setPath: function (path, value) { 1614 if (path.indexOf('.') >= 0) { 1615 var tuple = SC.tupleForPropertyPath(path, this); 1616 if (!tuple || !tuple[0]) return null; 1617 tuple[0].set(tuple[1], value); 1618 } else this.set(path, value); // shortcut 1619 return this; 1620 }, 1621 1622 /** 1623 Navigates the property path, finally setting the value but only if 1624 the value does not match the current value. This will avoid sending 1625 unnecessary change notifications. 1626 1627 @param {String} path the property path to set 1628 @param {Object} value the value to set 1629 @returns {Object} this 1630 */ 1631 setPathIfChanged: function (path, value) { 1632 if (path.indexOf('.') >= 0) { 1633 var tuple = SC.tupleForPropertyPath(path, this); 1634 if (!tuple || !tuple[0]) return null; 1635 if (tuple[0].get(tuple[1]) !== value) { 1636 tuple[0].set(tuple[1], value); 1637 } 1638 } else this.setIfChanged(path, value); // shortcut 1639 return this; 1640 }, 1641 1642 /** 1643 Convenience method to get an array of properties. 1644 1645 Pass in multiple property keys or an array of property keys. This 1646 method uses getPath() so you can also pass key paths. 1647 1648 @returns {Array} Values of property keys. 1649 */ 1650 getEach: function () { 1651 var ret = [], idx, idxLen; 1652 1653 for (idx = 0, idxLen = arguments.length; idx < idxLen; idx++) { 1654 ret[ret.length] = this.getPath(arguments[idx]); 1655 } 1656 return ret; 1657 }, 1658 1659 1660 /** 1661 Increments the value of a property. 1662 1663 @param {String} key property name 1664 @param {Number} increment the amount to increment (optional) 1665 @returns {Number} new value of property 1666 */ 1667 incrementProperty: function (key, increment) { 1668 if (!increment) increment = 1; 1669 this.set(key, (this.get(key) || 0) + increment); 1670 return this.get(key); 1671 }, 1672 1673 /** 1674 Decrements the value of a property. 1675 1676 @param {String} key property name 1677 @param {Number} increment the amount to decrement (optional) 1678 @returns {Number} new value of property 1679 */ 1680 decrementProperty: function (key, increment) { 1681 if (!increment) increment = 1; 1682 this.set(key, (this.get(key) || 0) - increment); 1683 return this.get(key); 1684 }, 1685 1686 /** 1687 Inverts a property. Property should be a bool. 1688 1689 @param {String} key property name 1690 @param {Object} value optional parameter for "true" value 1691 @param {Object} alt optional parameter for "false" value 1692 @returns {Object} new value 1693 */ 1694 toggleProperty: function (key, value, alt) { 1695 if (value === undefined) value = true; 1696 if (alt === undefined) alt = false; 1697 value = (this.get(key) == value) ? alt : value; 1698 this.set(key, value); 1699 return this.get(key); 1700 }, 1701 1702 /** 1703 Convenience method to call propertyWillChange/propertyDidChange. 1704 1705 Sometimes you need to notify observers that a property has changed value 1706 without actually changing this value. In those cases, you can use this 1707 method as a convenience instead of calling propertyWillChange() and 1708 propertyDidChange(). 1709 1710 @param {String} key The property key that has just changed. 1711 @param {Object} value The new value of the key. May be null. 1712 @returns {SC.Observable} 1713 */ 1714 notifyPropertyChange: function (key, value) { 1715 this.propertyWillChange(key); 1716 this.propertyDidChange(key, value); 1717 return this; 1718 }, 1719 1720 /** 1721 Notifies observers of all possible property changes. 1722 1723 Sometimes when you make a major update to your object, it is cheaper to 1724 simply notify all observers that their property might have changed than 1725 to figure out specifically which properties actually did change. 1726 1727 In those cases, you can simply call this method to notify all property 1728 observers immediately. Note that this ignores property groups. 1729 1730 @returns {SC.Observable} 1731 */ 1732 allPropertiesDidChange: function () { 1733 this._kvo_cache = null; //clear cached props 1734 this._notifyPropertyObservers('*'); 1735 return this; 1736 }, 1737 1738 propertyRevision: 1 1739 1740 }; 1741 1742 //@if(debug) 1743 /** @private used by addProbe/removeProbe. Debug mode only. */ 1744 SC.logChange = function logChange(target, key, value) { 1745 console.log("CHANGE: %@[%@] => %@".fmt(target, key, target.get(key))); 1746 }; 1747 //@endif 1748 1749 /** 1750 Retrieves a property from an object, using get() if the 1751 object implements SC.Observable. 1752 1753 @param {Object} object the object to query 1754 @param {String} key the property to retrieve 1755 */ 1756 SC.mixin(SC, { 1757 1758 get: function (object, key) { 1759 if (!object) return undefined; 1760 if (key === undefined) return this[object]; 1761 if (object.get) return object.get(key); 1762 return object[key]; 1763 }, 1764 1765 /** 1766 Retrieves a property from an object at a specified path, using get() if 1767 the object implements SC.Observable. 1768 1769 @param {Object} object the object to query 1770 @param {String} path the path to the property to retrieve 1771 */ 1772 getPath: function (object, path) { 1773 if (path === undefined) { 1774 path = object; 1775 object = window; 1776 } 1777 return SC.objectForPropertyPath(path, object); 1778 } 1779 1780 }); 1781 1782 // Make all Array's observable 1783 SC.mixin(Array.prototype, SC.Observable); 1784