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('core'); 9 sc_require('ext/function'); 10 sc_require('system/enumerator'); 11 12 /*global Prototype */ 13 14 /** 15 @class 16 17 This mixin defines the common interface implemented by enumerable objects 18 in SproutCore. Most of these methods follow the standard Array iteration 19 API defined up to JavaScript 1.8 (excluding language-specific features that 20 cannot be emulated in older versions of JavaScript). 21 22 This mixin is applied automatically to the Array class on page load, so you 23 can use any of these methods on simple arrays. If Array already implements 24 one of these methods, the mixin will not override them. 25 26 Writing Your Own Enumerable 27 ----- 28 29 To make your own custom class enumerable, you need two items: 30 31 1. You must have a length property. This property should change whenever 32 the number of items in your enumerable object changes. If you using this 33 with an SC.Object subclass, you should be sure to change the length 34 property using set(). 35 36 2. You must implement nextObject(). See documentation. 37 38 Once you have these two methods implemented, apply the SC.Enumerable mixin 39 to your class and you will be able to enumerate the contents of your object 40 like any other collection. 41 42 Using SproutCore Enumeration with Other Libraries 43 ----- 44 45 Many other libraries provide some kind of iterator or enumeration like 46 facility. This is often where the most common API conflicts occur. 47 SproutCore's API is designed to be as friendly as possible with other 48 libraries by implementing only methods that mostly correspond to the 49 JavaScript 1.8 API. 50 51 @since SproutCore 1.0 52 */ 53 SC.Enumerable = /** @scope SC.Enumerable.prototype */{ 54 55 /** 56 Walk like a duck. 57 58 @type Boolean 59 */ 60 isEnumerable: YES, 61 62 /** 63 Implement this method to make your class enumerable. 64 65 This method will be called repeatedly during enumeration. The index value 66 will always begin with 0 and increment monotonically. You don't have to 67 rely on the index value to determine what object to return, but you should 68 always check the value and start from the beginning when you see the 69 requested index is 0. 70 71 The previousObject is the object that was returned from the last call 72 to nextObject for the current iteration. This is a useful way to 73 manage iteration if you are tracing a linked list, for example. 74 75 Finally the context parameter will always contain a hash you can use as 76 a "scratchpad" to maintain any other state you need in order to iterate 77 properly. The context object is reused and is not reset between 78 iterations so make sure you setup the context with a fresh state whenever 79 the index parameter is 0. 80 81 Generally iterators will continue to call nextObject until the index 82 reaches the your current length-1. If you run out of data before this 83 time for some reason, you should simply return undefined. 84 85 The default implementation of this method simply looks up the index. 86 This works great on any Array-like objects. 87 88 @param {Number} index the current index of the iteration 89 @param {Object} previousObject the value returned by the last call to nextObject. 90 @param {Object} context a context object you can use to maintain state. 91 @returns {Object} the next object in the iteration or undefined 92 */ 93 nextObject: function (index, previousObject, context) { 94 return this.objectAt ? this.objectAt(index) : this[index]; 95 }, 96 97 /** 98 Helper method returns the first object from a collection. This is usually 99 used by bindings and other parts of the framework to extract a single 100 object if the enumerable contains only one item. 101 102 If you override this method, you should implement it so that it will 103 always return the same value each time it is called. If your enumerable 104 contains only one object, this method should always return that object. 105 If your enumerable is empty, this method should return undefined. 106 107 This property is observable if the enumerable supports it. Examples 108 of enumerables where firstObject is observable include SC.Array, 109 SC.ManyArray and SC.SparseArray. To implement a custom enumerable where 110 firstObject is observable, see {@link #enumerableContentDidChange}. 111 112 @see #enumerableContentDidChange 113 114 @returns {Object} the object or undefined 115 */ 116 firstObject: function () { 117 if (this.get('length') === 0) return undefined; 118 if (this.objectAt) return this.objectAt(0); // support arrays out of box 119 120 // handle generic enumerables 121 var context = SC.Enumerator._popContext(), ret; 122 ret = this.nextObject(0, null, context); 123 context = SC.Enumerator._pushContext(context); 124 return ret; 125 }.property().cacheable(), 126 127 /** 128 Helper method returns the last object from a collection. 129 130 This property is observable if the enumerable supports it. Examples 131 of enumerables where lastObject is observable include SC.Array, 132 SC.ManyArray and SC.SparseArray. To implement a custom enumerable where 133 lastObject is observable, see {@link #enumerableContentDidChange}. 134 135 @see #enumerableContentDidChange 136 137 @returns {Object} the object or undefined 138 */ 139 lastObject: function () { 140 var len = this.get('length'); 141 if (len === 0) return undefined; 142 if (this.objectAt) return this.objectAt(len - 1); // support arrays out of box 143 }.property().cacheable(), 144 145 /** 146 Returns a new enumerator for this object. See SC.Enumerator for 147 documentation on how to use this object. Enumeration is an alternative 148 to using one of the other iterators described here. 149 150 @returns {SC.Enumerator} an enumerator for the receiver 151 */ 152 enumerator: function () { return SC.Enumerator.create(this); }, 153 154 /** 155 Iterates through the enumerable, calling the passed function on each 156 item. This method corresponds to the forEach() method defined in 157 JavaScript 1.6. 158 159 The callback method you provide should have the following signature (all 160 parameters are optional): 161 162 function (item, index, enumerable); 163 164 - *item* is the current item in the iteration. 165 - *index* is the current index in the iteration 166 - *enumerable* is the enumerable object itself. 167 168 Note that in addition to a callback, you can also pass an optional target 169 object that will be set as "this" on the context. This is a good way 170 to give your iterator function access to the current object. 171 172 @param {Function} callback the callback to execute 173 @param {Object} target the target object to use 174 @returns {Object} this 175 */ 176 forEach: function (callback, target) { 177 if (typeof callback !== "function") throw new TypeError(); 178 var len = this.get ? this.get('length') : this.length; 179 if (target === undefined) target = null; 180 181 var last = null; 182 var context = SC.Enumerator._popContext(); 183 for (var idx = 0; idx < len;idx++) { 184 var next = this.nextObject(idx, last, context); 185 callback.call(target, next, idx, this); 186 last = next; 187 } 188 last = null; 189 context = SC.Enumerator._pushContext(context); 190 return this; 191 }, 192 193 /** 194 Retrieves the named value on each member object. This is more efficient 195 than using one of the wrapper methods defined here. Objects that 196 implement SC.Observable will use the get() method, otherwise the property 197 will be accessed directly. 198 199 @param {String} key the key to retrieve 200 @returns {Array} extracted values 201 */ 202 getEach: function (key) { 203 return this.map(function (next) { 204 return next ? (next.get ? next.get(key) : next[key]) : null; 205 }, this); 206 }, 207 208 /** 209 Sets the value on the named property for each member. This is more 210 efficient than using other methods defined on this helper. If the object 211 implements SC.Observable, the value will be changed to set(), otherwise 212 it will be set directly. null objects are skipped. 213 214 @param {String} key the key to set 215 @param {Object} value the object to set 216 @returns {Object} receiver 217 */ 218 setEach: function (key, value) { 219 this.forEach(function (next) { 220 if (next) { 221 if (next.set) next.set(key, value); 222 else next[key] = value; 223 } 224 }, this); 225 return this; 226 }, 227 228 /** 229 Maps all of the items in the enumeration to another value, returning 230 a new array. This method corresponds to map() defined in JavaScript 1.6. 231 232 The callback method you provide should have the following signature (all 233 parameters are optional): 234 235 function (item, index, enumerable); 236 237 - *item* is the current item in the iteration. 238 - *index* is the current index in the iteration 239 - *enumerable* is the enumerable object itself. 240 241 It should return the mapped value. 242 243 Note that in addition to a callback, you can also pass an optional target 244 object that will be set as "this" on the context. This is a good way 245 to give your iterator function access to the current object. 246 247 @param {Function} callback the callback to execute 248 @param {Object} target the target object to use 249 @returns {Array} The mapped array. 250 */ 251 map: function (callback, target) { 252 if (typeof callback !== "function") throw new TypeError(); 253 var len = this.get ? this.get('length') : this.length; 254 if (target === undefined) target = null; 255 256 var ret = []; 257 var last = null; 258 var context = SC.Enumerator._popContext(); 259 for (var idx = 0; idx < len;idx++) { 260 var next = this.nextObject(idx, last, context); 261 ret[idx] = callback.call(target, next, idx, this); 262 last = next; 263 } 264 last = null; 265 context = SC.Enumerator._pushContext(context); 266 return ret; 267 }, 268 269 /** 270 Similar to map, this specialized function returns the value of the named 271 property on all items in the enumeration. 272 273 @param {String} key name of the property 274 @returns {Array} The mapped array. 275 */ 276 mapProperty: function (key) { 277 return this.map(function (next) { 278 return next ? (next.get ? next.get(key) : next[key]) : null; 279 }); 280 }, 281 282 /** 283 Returns an array with all of the items in the enumeration that the passed 284 function returns YES for. This method corresponds to filter() defined in 285 JavaScript 1.6. 286 287 The callback method you provide should have the following signature (all 288 parameters are optional): 289 290 function (item, index, enumerable); 291 292 - *item* is the current item in the iteration. 293 - *index* is the current index in the iteration 294 - *enumerable* is the enumerable object itself. 295 296 It should return the YES to include the item in the results, NO otherwise. 297 298 Note that in addition to a callback, you can also pass an optional target 299 object that will be set as "this" on the context. This is a good way 300 to give your iterator function access to the current object. 301 302 @param {Function} callback the callback to execute 303 @param {Object} target the target object to use 304 @returns {Array} A filtered array. 305 */ 306 filter: function (callback, target) { 307 if (typeof callback !== "function") throw new TypeError(); 308 var len = this.get ? this.get('length') : this.length; 309 if (target === undefined) target = null; 310 311 var ret = []; 312 var last = null; 313 var context = SC.Enumerator._popContext(); 314 for (var idx = 0; idx < len;idx++) { 315 var next = this.nextObject(idx, last, context); 316 if (callback.call(target, next, idx, this)) ret.push(next); 317 last = next; 318 } 319 last = null; 320 context = SC.Enumerator._pushContext(context); 321 return ret; 322 }, 323 324 /** 325 Returns an array sorted by the value of the passed key parameters. 326 null objects will be sorted first. You can pass either an array of keys 327 or multiple parameters which will act as key names 328 329 @param {String} key one or more key names 330 @returns {Array} 331 */ 332 sortProperty: function (key) { 333 var keys = (typeof key === SC.T_STRING) ? arguments : key, 334 len = keys.length, 335 src; 336 337 // get the src array to sort 338 if (this instanceof Array) src = this; 339 else { 340 src = []; 341 this.forEach(function (i) { src.push(i); }); 342 } 343 344 if (!src) return []; 345 return src.sort(function (a, b) { 346 var idx, key, aValue, bValue, ret = 0; 347 348 for (idx = 0; ret === 0 && idx < len; idx++) { 349 key = keys[idx]; 350 aValue = a ? (a.get ? a.get(key) : a[key]) : null; 351 bValue = b ? (b.get ? b.get(key) : b[key]) : null; 352 ret = SC.compare(aValue, bValue); 353 } 354 return ret; 355 }); 356 }, 357 358 359 /** 360 Returns an array with just the items with the matched property. You 361 can pass an optional second argument with the target value. Otherwise 362 this will match any property that evaluates to true. 363 364 Note: null, undefined, false and the empty string all evaulate to false. 365 366 @param {String} key the property to test 367 @param {String} value optional value to test against. 368 @returns {Array} filtered array 369 */ 370 filterProperty: function (key, value) { 371 var len = this.get ? this.get('length') : this.length, 372 ret = [], 373 last = null, 374 context = SC.Enumerator._popContext(), 375 idx, item, cur; 376 // Although the code for value and no value are almost identical, we want to make as many decisions outside 377 // the loop as possible. 378 if (value === undefined) { 379 for (idx = 0; idx < len; idx++) { 380 item = this.nextObject(idx, last, context); 381 cur = item ? (item.get ? item.get(key) : item[key]) : null; 382 if (!!cur) ret.push(item); 383 last = item; 384 } 385 } else { 386 for (idx = 0; idx < len; idx++) { 387 item = this.nextObject(idx, last, context); 388 cur = item ? (item.get ? item.get(key) : item[key]) : null; 389 if (SC.isEqual(cur, value)) ret.push(item); 390 last = item; 391 } 392 } 393 last = null; 394 context = SC.Enumerator._pushContext(context); 395 return ret; 396 }, 397 398 /** 399 Returns the first item in the array for which the callback returns YES. 400 This method works similar to the filter() method defined in JavaScript 1.6 401 except that it will stop working on the array once a match is found. 402 403 The callback method you provide should have the following signature (all 404 parameters are optional): 405 406 function (item, index, enumerable); 407 408 - *item* is the current item in the iteration. 409 - *index* is the current index in the iteration 410 - *enumerable* is the enumerable object itself. 411 412 It should return the YES to include the item in the results, NO otherwise. 413 414 Note that in addition to a callback, you can also pass an optional target 415 object that will be set as "this" on the context. This is a good way 416 to give your iterator function access to the current object. 417 418 @param {Function} callback the callback to execute 419 @param {Object} target the target object to use 420 @returns {Object} Found item or null. 421 */ 422 find: function (callback, target) { 423 var len = this.get ? this.get('length') : this.length; 424 if (target === undefined) target = null; 425 426 var last = null, next, found = NO, ret = null; 427 var context = SC.Enumerator._popContext(); 428 for (var idx = 0; idx < len && !found;idx++) { 429 next = this.nextObject(idx, last, context); 430 if (found = callback.call(target, next, idx, this)) ret = next; 431 last = next; 432 } 433 next = last = null; 434 context = SC.Enumerator._pushContext(context); 435 return ret; 436 }, 437 438 /** 439 Returns an the first item with a property matching the passed value. You 440 can pass an optional second argument with the target value. Otherwise 441 this will match any property that evaluates to true. 442 443 This method works much like the more generic find() method. 444 445 @param {String} key the property to test 446 @param {String} value optional value to test against. 447 @returns {Object} found item or null 448 */ 449 findProperty: function (key, value) { 450 var len = this.get ? this.get('length') : this.length; 451 var found = NO, ret = null, last = null, next, cur; 452 var context = SC.Enumerator._popContext(); 453 for (var idx = 0; idx < len && !found;idx++) { 454 next = this.nextObject(idx, last, context); 455 cur = next ? (next.get ? next.get(key) : next[key]) : null; 456 found = (value === undefined) ? !!cur : SC.isEqual(cur, value); 457 if (found) ret = next; 458 last = next; 459 } 460 last = next = null; 461 context = SC.Enumerator._pushContext(context); 462 return ret; 463 }, 464 465 /** 466 Returns YES if the passed function returns YES for every item in the 467 enumeration. This corresponds with the every() method in JavaScript 1.6. 468 469 The callback method you provide should have the following signature (all 470 parameters are optional): 471 472 function (item, index, enumerable); 473 474 - *item* is the current item in the iteration. 475 - *index* is the current index in the iteration 476 - *enumerable* is the enumerable object itself. 477 478 It should return the YES or NO. 479 480 Note that in addition to a callback, you can also pass an optional target 481 object that will be set as "this" on the context. This is a good way 482 to give your iterator function access to the current object. 483 484 Example Usage: 485 486 if (people.every(isEngineer)) { Paychecks.addBigBonus(); } 487 488 @param {Function} callback the callback to execute 489 @param {Object} target the target object to use 490 @returns {Boolean} 491 */ 492 every: function (callback, target) { 493 if (typeof callback !== "function") throw new TypeError(); 494 var len = this.get ? this.get('length') : this.length; 495 if (target === undefined) target = null; 496 497 var ret = YES; 498 var last = null; 499 var context = SC.Enumerator._popContext(); 500 for (var idx = 0;ret && (idx < len);idx++) { 501 var next = this.nextObject(idx, last, context); 502 if (!callback.call(target, next, idx, this)) ret = NO; 503 last = next; 504 } 505 last = null; 506 context = SC.Enumerator._pushContext(context); 507 return ret; 508 }, 509 510 /** 511 Returns YES if the passed property resolves to true for all items in the 512 enumerable. This method is often simpler/faster than using a callback. 513 514 @param {String} key the property to test 515 @param {String} value optional value to test against. 516 @returns {Array} filtered array 517 */ 518 everyProperty: function (key, value) { 519 var len = this.get ? this.get('length') : this.length; 520 var ret = YES; 521 var last = null; 522 var context = SC.Enumerator._popContext(); 523 for (var idx = 0;ret && (idx < len);idx++) { 524 var next = this.nextObject(idx, last, context); 525 var cur = next ? (next.get ? next.get(key) : next[key]) : null; 526 ret = (value === undefined) ? !!cur : SC.isEqual(cur, value); 527 last = next; 528 } 529 last = null; 530 context = SC.Enumerator._pushContext(context); 531 return ret; 532 }, 533 534 535 /** 536 Returns YES if the passed function returns true for any item in the 537 enumeration. This corresponds with the every() method in JavaScript 1.6. 538 539 The callback method you provide should have the following signature (all 540 parameters are optional): 541 542 function (item, index, enumerable); 543 544 - *item* is the current item in the iteration. 545 - *index* is the current index in the iteration 546 - *enumerable* is the enumerable object itself. 547 548 It should return the YES to include the item in the results, NO otherwise. 549 550 Note that in addition to a callback, you can also pass an optional target 551 object that will be set as "this" on the context. This is a good way 552 to give your iterator function access to the current object. 553 554 Usage Example: 555 556 if (people.some(isManager)) { Paychecks.addBiggerBonus(); } 557 558 @param {Function} callback the callback to execute 559 @param {Object} target the target object to use 560 @returns {Boolean} YES 561 */ 562 some: function (callback, target) { 563 if (typeof callback !== "function") throw new TypeError(); 564 var len = this.get ? this.get('length') : this.length; 565 if (target === undefined) target = null; 566 567 var ret = NO; 568 var last = null; 569 var context = SC.Enumerator._popContext(); 570 for (var idx = 0;(!ret) && (idx < len);idx++) { 571 var next = this.nextObject(idx, last, context); 572 if (callback.call(target, next, idx, this)) ret = YES; 573 last = next; 574 } 575 last = null; 576 context = SC.Enumerator._pushContext(context); 577 return ret; 578 }, 579 580 /** 581 Returns YES if the passed property resolves to true for any item in the 582 enumerable. This method is often simpler/faster than using a callback. 583 584 @param {String} key the property to test 585 @param {String} value optional value to test against. 586 @returns {Boolean} YES 587 */ 588 someProperty: function (key, value) { 589 var len = this.get ? this.get('length') : this.length; 590 var ret = NO; 591 var last = null; 592 var context = SC.Enumerator._popContext(); 593 for (var idx = 0; !ret && (idx < len); idx++) { 594 var next = this.nextObject(idx, last, context); 595 var cur = next ? (next.get ? next.get(key) : next[key]) : null; 596 ret = (value === undefined) ? !!cur : SC.isEqual(cur, value); 597 last = next; 598 } 599 last = null; 600 context = SC.Enumerator._pushContext(context); 601 return ret; // return the invert 602 }, 603 604 /** 605 This will combine the values of the enumerator into a single value. It 606 is a useful way to collect a summary value from an enumeration. This 607 corresponds to the reduce() method defined in JavaScript 1.8. 608 609 The callback method you provide should have the following signature (all 610 parameters are optional): 611 612 function (previousValue, item, index, enumerable); 613 614 - *previousValue* is the value returned by the last call to the iterator. 615 - *item* is the current item in the iteration. 616 - *index* is the current index in the iteration 617 - *enumerable* is the enumerable object itself. 618 619 Return the new cumulative value. 620 621 In addition to the callback you can also pass an initialValue. An error 622 will be raised if you do not pass an initial value and the enumerator is 623 empty. 624 625 Note that unlike the other methods, this method does not allow you to 626 pass a target object to set as this for the callback. It's part of the 627 spec. Sorry. 628 629 @param {Function} callback the callback to execute 630 @param {Object} initialValue initial value for the reduce 631 @param {String} reducerProperty internal use only. May not be available. 632 @returns {Object} The reduced value. 633 */ 634 reduce: function (callback, initialValue, reducerProperty) { 635 if (typeof callback !== "function") throw new TypeError(); 636 var len = this.get ? this.get('length') : this.length; 637 638 // no value to return if no initial value & empty 639 if (len === 0 && initialValue === undefined) throw new TypeError(); 640 641 var ret = initialValue; 642 var last = null; 643 var context = SC.Enumerator._popContext(); 644 for (var idx = 0; idx < len;idx++) { 645 var next = this.nextObject(idx, last, context); 646 647 // while ret is still undefined, just set the first value we get as ret. 648 // this is not the ideal behavior actually but it matches the FireFox 649 // implementation... :( 650 if (next !== null) { 651 if (ret === undefined) { 652 ret = next; 653 } else { 654 ret = callback.call(null, ret, next, idx, this, reducerProperty); 655 } 656 } 657 last = next; 658 } 659 last = null; 660 context = SC.Enumerator._pushContext(context); 661 662 // uh oh...we never found a value! 663 if (ret === undefined) throw new TypeError(); 664 return ret; 665 }, 666 667 /** 668 Invokes the named method on every object in the receiver that 669 implements it. This method corresponds to the implementation in 670 Prototype 1.6. 671 672 @param {String} methodName the name of the method 673 @param {Object...} args optional arguments to pass as well. 674 @returns {Array} return values from calling invoke. 675 */ 676 invoke: function (methodName) { 677 var len = this.get ? this.get('length') : this.length; 678 if (len <= 0) return []; // nothing to invoke.... 679 680 var idx; 681 682 // collect the arguments 683 var args = []; 684 var alen = arguments.length; 685 if (alen > 1) { 686 for (idx = 1; idx < alen; idx++) args.push(arguments[idx]); 687 } 688 689 // call invoke 690 var ret = []; 691 var last = null; 692 var context = SC.Enumerator._popContext(); 693 for (idx = 0; idx < len; idx++) { 694 var next = this.nextObject(idx, last, context); 695 var method = next ? next[methodName] : null; 696 if (method) ret[idx] = method.apply(next, args); 697 last = next; 698 } 699 last = null; 700 context = SC.Enumerator._pushContext(context); 701 return ret; 702 }, 703 704 /** 705 Invokes the passed method and optional arguments on the receiver elements 706 as long as the methods return value matches the target value. This is 707 a useful way to attempt to apply changes to a collection of objects unless 708 or until one fails. 709 710 @param {Object} targetValue the target return value 711 @param {String} methodName the name of the method 712 @param {Object...} args optional arguments to pass as well. 713 @returns {Array} return values from calling invoke. 714 */ 715 invokeWhile: function (targetValue, methodName) { 716 var len = this.get ? this.get('length') : this.length; 717 if (len <= 0) return null; // nothing to invoke.... 718 719 var idx; 720 721 // collect the arguments 722 var args = []; 723 var alen = arguments.length; 724 if (alen > 2) { 725 for (idx = 2; idx < alen; idx++) args.push(arguments[idx]); 726 } 727 728 // call invoke 729 var ret = targetValue; 730 var last = null; 731 var context = SC.Enumerator._popContext(); 732 for (idx = 0; (ret === targetValue) && (idx < len); idx++) { 733 var next = this.nextObject(idx, last, context); 734 var method = next ? next[methodName] : null; 735 if (method) ret = method.apply(next, args); 736 last = next; 737 } 738 last = null; 739 context = SC.Enumerator._pushContext(context); 740 return ret; 741 }, 742 743 /** 744 Simply converts the enumerable into a genuine array. The order, of 745 course, is not gauranteed. Corresponds to the method implemented by 746 Prototype. 747 748 @returns {Array} the enumerable as an array. 749 */ 750 toArray: function () { 751 var ret = []; 752 this.forEach(function (o) { ret.push(o); }, this); 753 return ret; 754 }, 755 756 /** 757 Converts an enumerable into a matrix, with inner arrays grouped based 758 on a particular property of the elements of the enumerable. 759 760 @param {String} key the property to test 761 @returns {Array} matrix of arrays 762 */ 763 groupBy: function (key) { 764 var len = this.get ? this.get('length') : this.length, 765 ret = [], 766 last = null, 767 context = SC.Enumerator._popContext(), 768 grouped = [], 769 keyValues = [], 770 idx, next, cur; 771 772 for (idx = 0; idx < len;idx++) { 773 next = this.nextObject(idx, last, context); 774 cur = next ? (next.get ? next.get(key) : next[key]) : null; 775 if (SC.none(grouped[cur])) { 776 grouped[cur] = []; 777 keyValues.push(cur); 778 } 779 grouped[cur].push(next); 780 last = next; 781 } 782 last = null; 783 context = SC.Enumerator._pushContext(context); 784 785 for (idx = 0, len = keyValues.length; idx < len; idx++) { 786 ret.push(grouped[keyValues[idx]]); 787 } 788 return ret; 789 } 790 791 }; 792 793 // Build in a separate function to avoid unintentional leaks through closures... 794 SC._buildReducerFor = function (reducerKey, reducerProperty) { 795 return function (key, value) { 796 var reducer = this[reducerKey]; 797 if (SC.typeOf(reducer) !== SC.T_FUNCTION) { 798 return this.unknownProperty ? this.unknownProperty(key, value) : null; 799 } else { 800 // Invoke the reduce method defined in enumerable instead of using the 801 // one implemented in the receiver. The receiver might be a native 802 // implementation that does not support reducerProperty. 803 var ret = SC.Enumerable.reduce.call(this, reducer, null, reducerProperty); 804 return ret; 805 } 806 }.property('[]'); 807 }; 808 809 /** @class */ 810 SC.Reducers = /** @scope SC.Reducers.prototype */ { 811 /** 812 This property will trigger anytime the enumerable's content changes. 813 You can observe this property to be notified of changes to the enumerables 814 content. 815 816 For plain enumerables, this property is read only. SC.Array overrides 817 this method. 818 819 @type SC.Array 820 */ 821 '[]': function (key, value) { return this; }.property(), 822 823 /** 824 Invoke this method when the contents of your enumerable has changed. 825 This will notify any observers watching for content changes. If you are 826 implementing an ordered enumerable (such as an Array), also pass the 827 start and length values so that it can be used to notify range observers. 828 Passing start and length values will also ensure that the computed 829 properties `firstObject` and `lastObject` are updated. 830 831 @param {Number} [start] start offset for the content change 832 @param {Number} [length] length of change 833 @param {Number} [deltas] if you added or removed objects, the delta change 834 @returns {Object} receiver 835 */ 836 enumerableContentDidChange: function (start, length, deltas) { 837 // If the start & length are provided, we can also indicate if the firstObject 838 // or lastObject properties changed, thus making them independently observable. 839 if (!SC.none(start)) { 840 if (start === 0) this.notifyPropertyChange('firstObject'); 841 if (!SC.none(length) && start + length >= this.get('length') - 1) this.notifyPropertyChange('lastObject'); 842 } 843 844 this.notifyPropertyChange('[]'); 845 }, 846 847 /** 848 Call this method from your unknownProperty() handler to implement 849 automatic reduced properties. A reduced property is a property that 850 collects its contents dynamically from your array contents. Reduced 851 properties always begin with "@". Getting this property will call 852 reduce() on your array with the function matching the key name as the 853 processor. 854 855 The return value of this will be either the return value from the 856 reduced property or undefined, which means this key is not a reduced 857 property. You can call this at the top of your unknownProperty handler 858 like so: 859 860 unknownProperty: function (key, value) { 861 var ret = this.handleReduceProperty(key, value); 862 if (ret === undefined) { 863 // process like normal 864 } 865 } 866 867 @param {String} key the reduce property key 868 869 @param {Object} value a value or undefined. 870 871 @param {Boolean} generateProperty only set to false if you do not want 872 an optimized computed property handler generated for this. Not common. 873 874 @returns {Object} the reduced property or undefined 875 */ 876 reducedProperty: function (key, value, generateProperty) { 877 878 if (!key || typeof key !== SC.T_STRING || key.charAt(0) !== '@') return undefined; // not a reduced property 879 880 // get the reducer key and the reducer 881 var matches = key.match(/^@([^(]*)(\(([^)]*)\))?$/); 882 if (!matches || matches.length < 2) return undefined; // no match 883 884 var reducerKey = matches[1]; // = 'max' if key = '@max(balance)' 885 var reducerProperty = matches[3]; // = 'balance' if key = '@max(balance)' 886 reducerKey = "reduce" + reducerKey.slice(0, 1).toUpperCase() + reducerKey.slice(1); 887 var reducer = this[reducerKey]; 888 889 // if there is no reduce function defined for this key, then we can't 890 // build a reducer for it. 891 if (SC.typeOf(reducer) !== SC.T_FUNCTION) return undefined; 892 893 // if we can't generate the property, just run reduce 894 if (generateProperty === NO) { 895 return SC.Enumerable.reduce.call(this, reducer, null, reducerProperty); 896 } 897 898 // ok, found the reducer. Let's build the computed property and install 899 var func = SC._buildReducerFor(reducerKey, reducerProperty); 900 var p = this.constructor.prototype; 901 902 if (p) { 903 p[key] = func; 904 905 // add the function to the properties array so that new instances 906 // will have their dependent key registered. 907 var props = p._properties || []; 908 props.push(key); 909 p._properties = props; 910 this.registerDependentKey(key, '[]'); 911 } 912 913 // and reduce anyway... 914 return SC.Enumerable.reduce.call(this, reducer, null, reducerProperty); 915 }, 916 917 /** 918 Reducer for @max reduced property. 919 920 @param {Object} previousValue The previous value in the enumerable 921 @param {Object} item The current value in the enumerable 922 @param {Number} index The index of the current item in the enumerable 923 @param {String} reducerProperty (Optional) The property in the enumerable being reduced 924 925 @returns {Object} reduced value 926 */ 927 reduceMax: function (previousValue, item, index, e, reducerProperty) { 928 if (reducerProperty && item) { 929 item = item.get ? item.get(reducerProperty) : item[reducerProperty]; 930 } 931 if (previousValue === null) return item; 932 return (item > previousValue) ? item : previousValue; 933 }, 934 935 /** 936 Reduces an enumerable to the max of the items in the enumerable. If 937 reducerProperty is passed, it will reduce that property. Otherwise, it will 938 reduce the item itself. 939 940 @param {Object} previousValue The previous value in the enumerable 941 @param {Object} item The current value in the enumerable 942 @param {Number} index The index of the current item in the enumerable 943 @param {String} reducerProperty (Optional) The property in the enumerable being reduced 944 945 @returns {Object} reduced value 946 */ 947 reduceMaxObject: function (previousItem, item, index, e, reducerProperty) { 948 949 // get the value for both the previous and current item. If no 950 // reducerProperty was supplied, use the items themselves. 951 var previousValue = previousItem, itemValue = item; 952 if (reducerProperty) { 953 if (item) { 954 itemValue = item.get ? item.get(reducerProperty) : item[reducerProperty]; 955 } 956 957 if (previousItem) { 958 previousValue = previousItem.get ? previousItem.get(reducerProperty) : previousItem[reducerProperty]; 959 } 960 } 961 if (previousValue === null) return item; 962 return (itemValue > previousValue) ? item : previousItem; 963 }, 964 965 /** 966 Reduces an enumerable to the min of the items in the enumerable. If 967 reducerProperty is passed, it will reduce that property. Otherwise, it will 968 reduce the item itself. 969 970 @param {Object} previousValue The previous value in the enumerable 971 @param {Object} item The current value in the enumerable 972 @param {Number} index The index of the current item in the enumerable 973 @param {String} reducerProperty (Optional) The property in the enumerable being reduced 974 975 @returns {Object} reduced value 976 */ 977 reduceMin: function (previousValue, item, index, e, reducerProperty) { 978 if (reducerProperty && item) { 979 item = item.get ? item.get(reducerProperty) : item[reducerProperty]; 980 } 981 if (previousValue === null) return item; 982 return (item < previousValue) ? item : previousValue; 983 }, 984 985 /** 986 Reduces an enumerable to the max of the items in the enumerable. If 987 reducerProperty is passed, it will reduce that property. Otherwise, it will 988 reduce the item itself. 989 990 @param {Object} previousValue The previous value in the enumerable 991 @param {Object} item The current value in the enumerable 992 @param {Number} index The index of the current item in the enumerable 993 @param {String} reducerProperty (Optional) The property in the enumerable being reduced 994 995 @returns {Object} reduced value 996 */ 997 reduceMinObject: function (previousItem, item, index, e, reducerProperty) { 998 999 // get the value for both the previous and current item. If no 1000 // reducerProperty was supplied, use the items themselves. 1001 var previousValue = previousItem, itemValue = item; 1002 if (reducerProperty) { 1003 if (item) { 1004 itemValue = item.get ? item.get(reducerProperty) : item[reducerProperty]; 1005 } 1006 1007 if (previousItem) { 1008 previousValue = previousItem.get ? previousItem.get(reducerProperty) : previousItem[reducerProperty]; 1009 } 1010 } 1011 if (previousValue === null) return item; 1012 return (itemValue < previousValue) ? item : previousItem; 1013 }, 1014 1015 /** 1016 Reduces an enumerable to the average of the items in the enumerable. If 1017 reducerProperty is passed, it will reduce that property. Otherwise, it will 1018 reduce the item itself. 1019 1020 @param {Object} previousValue The previous value in the enumerable 1021 @param {Object} item The current value in the enumerable 1022 @param {Number} index The index of the current item in the enumerable 1023 @param {String} reducerProperty (Optional) The property in the enumerable being reduced 1024 1025 @returns {Object} reduced value 1026 */ 1027 reduceAverage: function (previousValue, item, index, e, reducerProperty) { 1028 if (reducerProperty && item) { 1029 item = item.get ? item.get(reducerProperty) : item[reducerProperty]; 1030 } 1031 var ret = (previousValue || 0) + item; 1032 var len = e.get ? e.get('length') : e.length; 1033 if (index >= len - 1) ret = ret / len; //avg after last item. 1034 return ret; 1035 }, 1036 1037 /** 1038 Reduces an enumerable to the sum of the items in the enumerable. If 1039 reducerProperty is passed, it will reduce that property. Otherwise, it will 1040 reduce the item itself. 1041 1042 @param {Object} previousValue The previous value in the enumerable 1043 @param {Object} item The current value in the enumerable 1044 @param {Number} index The index of the current item in the enumerable 1045 @param {String} reducerProperty (Optional) The property in the enumerable being reduced 1046 1047 @returns {Object} reduced value 1048 */ 1049 reduceSum: function (previousValue, item, index, e, reducerProperty) { 1050 if (reducerProperty && item) { 1051 item = item.get ? item.get(reducerProperty) : item[reducerProperty]; 1052 } 1053 return (previousValue === null) ? item : previousValue + item; 1054 } 1055 }; 1056 1057 // Apply reducers... 1058 SC.mixin(SC.Enumerable, SC.Reducers); 1059 SC.mixin(Array.prototype, SC.Reducers); 1060 Array.prototype.isEnumerable = YES; 1061 1062 // ...................................................... 1063 // ARRAY SUPPORT 1064 // 1065 1066 // Implement the same enhancements on Array. We use specialized methods 1067 // because working with arrays are so common. 1068 (function () { 1069 1070 // These methods will be applied even if they already exist b/c we do it 1071 // better. 1072 var alwaysMixin = { 1073 1074 // this is supported so you can get an enumerator. The rest of the 1075 // methods do not use this just to squeeze every last ounce of perf as 1076 // possible. 1077 nextObject: SC.Enumerable.nextObject, 1078 enumerator: SC.Enumerable.enumerator, 1079 firstObject: SC.Enumerable.firstObject, 1080 lastObject: SC.Enumerable.lastObject, 1081 sortProperty: SC.Enumerable.sortProperty, 1082 1083 // see above... 1084 mapProperty: function (key) { 1085 var len = this.length; 1086 var ret = []; 1087 for (var idx = 0; idx < len; idx++) { 1088 var next = this[idx]; 1089 ret[idx] = next ? (next.get ? next.get(key) : next[key]) : null; 1090 } 1091 return ret; 1092 }, 1093 1094 filterProperty: function (key, value) { 1095 var len = this.length, 1096 ret = [], 1097 idx, item, cur; 1098 // Although the code for value and no value are almost identical, we want to make as many decisions outside 1099 // the loop as possible. 1100 if (value === undefined) { 1101 for (idx = 0; idx < len; idx++) { 1102 item = this[idx]; 1103 cur = item ? (item.get ? item.get(key) : item[key]) : null; 1104 if (!!cur) ret.push(item); 1105 } 1106 } else { 1107 for (idx = 0; idx < len; idx++) { 1108 item = this[idx]; 1109 cur = item ? (item.get ? item.get(key) : item[key]) : null; 1110 if (SC.isEqual(cur, value)) ret.push(item); 1111 } 1112 } 1113 return ret; 1114 }, 1115 //returns a matrix 1116 groupBy: function (key) { 1117 var len = this.length, 1118 ret = [], 1119 grouped = [], 1120 keyValues = [], 1121 idx, next, cur; 1122 1123 for (idx = 0; idx < len; idx++) { 1124 next = this[idx]; 1125 cur = next ? (next.get ? next.get(key) : next[key]) : null; 1126 if (SC.none(grouped[cur])) { grouped[cur] = []; keyValues.push(cur); } 1127 grouped[cur].push(next); 1128 } 1129 1130 for (idx = 0, len = keyValues.length; idx < len; idx++) { 1131 ret.push(grouped[keyValues[idx]]); 1132 } 1133 return ret; 1134 }, 1135 1136 find: function (callback, target) { 1137 if (typeof callback !== "function") throw new TypeError(); 1138 var len = this.length; 1139 if (target === undefined) target = null; 1140 1141 var next, ret = null, found = NO; 1142 for (var idx = 0; idx < len && !found; idx++) { 1143 next = this[idx]; 1144 if (found = callback.call(target, next, idx, this)) ret = next; 1145 } 1146 next = null; 1147 return ret; 1148 }, 1149 1150 findProperty: function (key, value) { 1151 var len = this.length; 1152 var next, cur, found = NO, ret = null; 1153 for (var idx = 0; idx < len && !found; idx++) { 1154 cur = (next = this[idx]) ? (next.get ? next.get(key): next[key]):null; 1155 found = (value === undefined) ? !!cur : SC.isEqual(cur, value); 1156 if (found) ret = next; 1157 } 1158 next = null; 1159 return ret; 1160 }, 1161 1162 everyProperty: function (key, value) { 1163 var len = this.length; 1164 var ret = YES; 1165 for (var idx = 0; ret && (idx < len); idx++) { 1166 var next = this[idx]; 1167 var cur = next ? (next.get ? next.get(key) : next[key]) : null; 1168 ret = (value === undefined) ? !!cur : SC.isEqual(cur, value); 1169 } 1170 return ret; 1171 }, 1172 1173 someProperty: function (key, value) { 1174 var len = this.length; 1175 var ret = NO; 1176 for (var idx = 0; !ret && (idx < len); idx++) { 1177 var next = this[idx]; 1178 var cur = next ? (next.get ? next.get(key) : next[key]) : null; 1179 ret = (value === undefined) ? !!cur : SC.isEqual(cur, value); 1180 } 1181 return ret; // return the invert 1182 }, 1183 1184 invoke: function (methodName) { 1185 var len = this.length; 1186 if (len <= 0) return []; // nothing to invoke.... 1187 1188 var idx; 1189 1190 // collect the arguments 1191 var args = []; 1192 var alen = arguments.length; 1193 if (alen > 1) { 1194 for (idx = 1; idx < alen; idx++) args.push(arguments[idx]); 1195 } 1196 1197 // call invoke 1198 var ret = []; 1199 for (idx = 0; idx < len; idx++) { 1200 var next = this[idx]; 1201 var method = next ? next[methodName] : null; 1202 if (method) ret[idx] = method.apply(next, args); 1203 } 1204 return ret; 1205 }, 1206 1207 invokeWhile: function (targetValue, methodName) { 1208 var len = this.length; 1209 if (len <= 0) return null; // nothing to invoke.... 1210 1211 var idx; 1212 1213 // collect the arguments 1214 var args = []; 1215 var alen = arguments.length; 1216 if (alen > 2) { 1217 for (idx = 2; idx < alen; idx++) args.push(arguments[idx]); 1218 } 1219 1220 // call invoke 1221 var ret = targetValue; 1222 for (idx = 0; (ret === targetValue) && (idx < len); idx++) { 1223 var next = this[idx]; 1224 var method = next ? next[methodName] : null; 1225 if (method) ret = method.apply(next, args); 1226 } 1227 return ret; 1228 }, 1229 1230 toArray: function () { 1231 var len = this.length; 1232 if (len <= 0) return []; // nothing to invoke.... 1233 1234 // call invoke 1235 var ret = []; 1236 for (var idx = 0; idx < len; idx++) { 1237 var next = this[idx]; 1238 ret.push(next); 1239 } 1240 return ret; 1241 }, 1242 1243 getEach: function (key) { 1244 var ret = []; 1245 var len = this.length; 1246 for (var idx = 0; idx < len; idx++) { 1247 var obj = this[idx]; 1248 ret[idx] = obj ? (obj.get ? obj.get(key) : obj[key]) : null; 1249 } 1250 return ret; 1251 }, 1252 1253 setEach: function (key, value) { 1254 var len = this.length; 1255 for (var idx = 0; idx < len; idx++) { 1256 var obj = this[idx]; 1257 if (obj) { 1258 if (obj.set) { 1259 obj.set(key, value); 1260 } else obj[key] = value; 1261 } 1262 } 1263 return this; 1264 } 1265 1266 }; 1267 1268 // These methods will only be applied if they are not already defined b/c 1269 // the browser is probably getting it. 1270 var mixinIfMissing = { 1271 1272 // QUESTION: The lack of DRY is burning my eyes [YK] 1273 forEach: function (callback, target) { 1274 if (typeof callback !== "function") throw new TypeError(); 1275 1276 // QUESTION: Is this necessary? 1277 if (target === undefined) target = null; 1278 1279 for (var i = 0, l = this.length; i < l; i++) { 1280 var next = this[i]; 1281 callback.call(target, next, i, this); 1282 } 1283 return this; 1284 }, 1285 1286 map: function (callback, target) { 1287 if (typeof callback !== "function") throw new TypeError(); 1288 1289 if (target === undefined) target = null; 1290 1291 var ret = []; 1292 for (var i = 0, l = this.length; i < l; i++) { 1293 var next = this[i]; 1294 ret[i] = callback.call(target, next, i, this); 1295 } 1296 return ret; 1297 }, 1298 1299 filter: function (callback, target) { 1300 if (typeof callback !== "function") throw new TypeError(); 1301 1302 if (target === undefined) target = null; 1303 1304 var ret = []; 1305 for (var i = 0, l = this.length; i < l; i++) { 1306 var next = this[i]; 1307 if (callback.call(target, next, i, this)) ret.push(next); 1308 } 1309 return ret; 1310 }, 1311 1312 every: function (callback, target) { 1313 if (typeof callback !== "function") throw new TypeError(); 1314 var len = this.length; 1315 if (target === undefined) target = null; 1316 1317 var ret = YES; 1318 for (var idx = 0; ret && (idx < len); idx++) { 1319 var next = this[idx]; 1320 if (!callback.call(target, next, idx, this)) ret = NO; 1321 } 1322 return ret; 1323 }, 1324 1325 some: function (callback, target) { 1326 if (typeof callback !== "function") throw new TypeError(); 1327 var len = this.length; 1328 if (target === undefined) target = null; 1329 1330 var ret = NO; 1331 for (var idx = 0; (!ret) && (idx < len); idx++) { 1332 var next = this[idx]; 1333 if (callback.call(target, next, idx, this)) ret = YES; 1334 } 1335 return ret; 1336 }, 1337 1338 reduce: function (callback, initialValue, reducerProperty) { 1339 if (typeof callback !== "function") throw new TypeError(); 1340 var len = this.length; 1341 1342 // no value to return if no initial value & empty 1343 if (len === 0 && initialValue === undefined) throw new TypeError(); 1344 1345 var ret = initialValue; 1346 for (var idx = 0; idx < len; idx++) { 1347 var next = this[idx]; 1348 1349 // while ret is still undefined, just set the first value we get as 1350 // ret. this is not the ideal behavior actually but it matches the 1351 // FireFox implementation... :( 1352 if (next !== null) { 1353 if (ret === undefined) { 1354 ret = next; 1355 } else { 1356 ret = callback.call(null, ret, next, idx, this, reducerProperty); 1357 } 1358 } 1359 } 1360 1361 // uh oh...we never found a value! 1362 if (ret === undefined) throw new TypeError(); 1363 return ret; 1364 } 1365 }; 1366 1367 // Apply methods if missing... 1368 for (var key in mixinIfMissing) { 1369 if (!mixinIfMissing.hasOwnProperty(key)) continue; 1370 1371 // The mixinIfMissing methods should be applied if they are not defined. 1372 // If Prototype 1.6 is included, some of these methods will be defined 1373 // already, but we want to override them anyway in this special case 1374 // because our version is faster and functionally identical. 1375 if (!Array.prototype[key] || ((typeof Prototype === 'object') && Prototype.Version.match(/^1\.6/))) { 1376 Array.prototype[key] = mixinIfMissing[key]; 1377 } 1378 } 1379 1380 // Apply other methods... 1381 SC.mixin(Array.prototype, alwaysMixin); 1382 1383 })(); 1384 1385