1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 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 /** @class 9 10 A SelectionSet contains a set of objects that represent the current 11 selection. You can select objects by either adding them to the set directly 12 or indirectly by selecting a range of indexes on a source object. 13 14 @extends SC.Object 15 @extends SC.Enumerable 16 @extends SC.Freezable 17 @extends SC.Copyable 18 @since SproutCore 1.0 19 */ 20 SC.SelectionSet = SC.Object.extend(SC.Enumerable, SC.Freezable, SC.Copyable, 21 /** @scope SC.SelectionSet.prototype */ { 22 23 /** 24 Walk like a duck. 25 26 @type Boolean 27 */ 28 isSelectionSet: YES, 29 30 /** 31 Total number of indexes in the selection set 32 33 @type Number 34 */ 35 length: function() { 36 var ret = 0, 37 sets = this._sets, 38 objects = this._objects; 39 if (objects) ret += objects.get('length'); 40 if (sets) sets.forEach(function(s) { ret += s.get('length'); }); 41 return ret ; 42 }.property().cacheable(), 43 44 // .......................................................... 45 // INDEX-BASED SELECTION 46 // 47 48 /** 49 A set of all the source objects used in the selection set. This 50 property changes automatically as you add or remove index sets. 51 52 @type SC.Array 53 */ 54 sources: function() { 55 var ret = [], 56 sets = this._sets, 57 len = sets ? sets.length : 0, 58 idx, set, source; 59 60 for(idx=0;idx<len;idx++) { 61 set = sets[idx]; 62 if (set && set.get('length')>0 && set.source) ret.push(set.source); 63 } 64 return ret ; 65 }.property().cacheable(), 66 67 /** 68 Returns the index set for the passed source object or null if no items are 69 seleted in the source. 70 71 @param {SC.Array} source the source object 72 @returns {SC.IndexSet} index set or null 73 */ 74 indexSetForSource: function(source) { 75 if (!source || !source.isSCArray) return null; // nothing to do 76 77 var cache = this._indexSetCache, 78 objects = this._objects, 79 ret, idx; 80 81 // try to find in cache 82 if (!cache) cache = this._indexSetCache = {}; 83 ret = cache[SC.guidFor(source)]; 84 if (ret && ret._sourceRevision && (ret._sourceRevision !== source.propertyRevision)) { 85 ret = null; 86 } 87 88 // not in cache. generate from index sets and any saved objects 89 if (!ret) { 90 ret = this._indexSetForSource(source, NO); 91 if (ret && ret.get('length')===0) ret = null; 92 93 if (objects) { 94 if (ret) ret = ret.copy(); 95 objects.forEach(function(o) { 96 if ((idx = source.indexOf(o)) >= 0) { 97 if (!ret) ret = SC.IndexSet.create(); 98 ret.add(idx); 99 } 100 }, this); 101 } 102 103 if (ret) { 104 ret = cache[SC.guidFor(source)] = ret.frozenCopy(); 105 ret._sourceRevision = source.propertyRevision; 106 } 107 } 108 109 return ret; 110 }, 111 112 /** 113 @private 114 115 Internal method gets the index set for the source, ignoring objects 116 that have been added directly. 117 */ 118 _indexSetForSource: function(source, canCreate) { 119 if (canCreate === undefined) canCreate = YES; 120 121 var guid = SC.guidFor(source), 122 index = this[guid], 123 sets = this._sets, 124 len = sets ? sets.length : 0, 125 ret = null; 126 127 if (index >= len) index = null; 128 if (SC.none(index)) { 129 if (canCreate && !this.isFrozen) { 130 this.propertyWillChange('sources'); 131 if (!sets) sets = this._sets = []; 132 ret = sets[len] = SC.IndexSet.create(); 133 ret.source = source ; 134 this[guid] = len; 135 this.propertyDidChange('sources'); 136 } 137 138 } else ret = sets ? sets[index] : null; 139 return ret ; 140 }, 141 142 /** 143 Add the passed index, range of indexSet belonging to the passed source 144 object to the selection set. 145 146 The first parameter you pass must be the source array you are selecting 147 from. The following parameters may be one of a start/length pair, a 148 single index, a range object or an IndexSet. If some or all of the range 149 you are selecting is already in the set, it will not be selected again. 150 151 You can also pass an SC.SelectionSet to this method and all the selected 152 sets will be added from their instead. 153 154 @param {SC.Array} source source object or object to add. 155 @param {Number} start index, start of range, range or IndexSet 156 @param {Number} length length if passing start/length pair. 157 @returns {SC.SelectionSet} receiver 158 */ 159 add: function(source, start, length) { 160 161 if (this.isFrozen) throw new Error(SC.FROZEN_ERROR); 162 163 var sets, len, idx, set, oldlen, newlen, setlen, objects; 164 165 // normalize 166 if (start === undefined && length === undefined) { 167 if (!source) throw new Error("Must pass params to SC.SelectionSet.add()"); 168 if (source.isIndexSet) return this.add(source.source, source); 169 if (source.isSelectionSet) { 170 sets = source._sets; 171 objects = source._objects; 172 len = sets ? sets.length : 0; 173 174 this.beginPropertyChanges(); 175 for(idx=0;idx<len;idx++) { 176 set = sets[idx]; 177 if (set && set.get('length')>0) this.add(set.source, set); 178 } 179 if (objects) this.addObjects(objects); 180 this.endPropertyChanges(); 181 return this ; 182 183 } 184 } 185 186 set = this._indexSetForSource(source, YES); 187 oldlen = this.get('length'); 188 setlen = set.get('length'); 189 newlen = oldlen - setlen; 190 191 set.add(start, length); 192 193 this._indexSetCache = null; 194 195 newlen += set.get('length'); 196 if (newlen !== oldlen) { 197 this.propertyDidChange('length'); 198 this.enumerableContentDidChange(); 199 if (setlen === 0) this.notifyPropertyChange('sources'); 200 } 201 202 return this ; 203 }, 204 205 /** 206 Removes the passed index, range of indexSet belonging to the passed source 207 object from the selection set. 208 209 The first parameter you pass must be the source array you are selecting 210 from. The following parameters may be one of a start/length pair, a 211 single index, a range object or an IndexSet. If some or all of the range 212 you are selecting is already in the set, it will not be selected again. 213 214 @param {SC.Array} source source object. must not be null 215 @param {Number} start index, start of range, range or IndexSet 216 @param {Number} length length if passing start/length pair. 217 @returns {SC.SelectionSet} receiver 218 */ 219 remove: function(source, start, length) { 220 221 if (this.isFrozen) throw new Error(SC.FROZEN_ERROR); 222 223 var sets, len, idx, i, set, oldlen, newlen, setlen, objects, object; 224 225 // normalize 226 if (start === undefined && length === undefined) { 227 if (!source) throw new Error("Must pass params to SC.SelectionSet.remove()"); 228 if (source.isIndexSet) return this.remove(source.source, source); 229 if (source.isSelectionSet) { 230 sets = source._sets; 231 objects = source._objects; 232 len = sets ? sets.length : 0; 233 234 this.beginPropertyChanges(); 235 for(idx=0;idx<len;idx++) { 236 set = sets[idx]; 237 if (set && set.get('length')>0) this.remove(set.source, set); 238 } 239 if (objects) this.removeObjects(objects); 240 this.endPropertyChanges(); 241 return this ; 242 } 243 } 244 245 // save starter info 246 set = this._indexSetForSource(source, YES); 247 oldlen = this.get('length'); 248 newlen = oldlen - set.get('length'); 249 250 // if we have objects selected, determine if they are in the index 251 // set and remove them as well. 252 if (set && (objects = this._objects)) { 253 254 // convert start/length to index set so the iterator below will work... 255 if (length !== undefined) { 256 start = SC.IndexSet.create(start, length); 257 length = undefined; 258 } 259 260 for (i = objects.get('length') - 1; i >= 0; --i) { 261 object = objects[i]; 262 idx = source.indexOf(object); 263 if (start.contains(idx)) { 264 objects.remove(object); 265 newlen--; 266 } 267 } 268 } 269 270 // remove indexes from source index set 271 set.remove(start, length); 272 setlen = set.get('length'); 273 newlen += setlen; 274 275 // update caches; change enumerable... 276 this._indexSetCache = null; 277 if (newlen !== oldlen) { 278 this.propertyDidChange('length'); 279 this.enumerableContentDidChange(); 280 if (setlen === 0) this.notifyPropertyChange('sources'); 281 } 282 283 return this ; 284 }, 285 286 287 /** 288 Returns YES if the selection contains the named index, range of indexes. 289 290 @param {Object} source source object for range 291 @param {Number} start index, start of range, range object, or indexSet 292 @param {Number} length optional range length 293 @returns {Boolean} 294 */ 295 contains: function(source, start, length) { 296 if (start === undefined && length === undefined) { 297 return this.containsObject(source); 298 } 299 300 var set = this.indexSetForSource(source); 301 if (!set) return NO ; 302 return set.contains(start, length); 303 }, 304 305 /** 306 Returns YES if the index set contains any of the passed indexes. You 307 can pass a single index, a range or an index set. 308 309 @param {Object} source source object for range 310 @param {Number} start index, range, or IndexSet 311 @param {Number} length optional range length 312 @returns {Boolean} 313 */ 314 intersects: function(source, start, length) { 315 var set = this.indexSetForSource(source, NO); 316 if (!set) return NO ; 317 return set.intersects(start, length); 318 }, 319 320 321 // .......................................................... 322 // OBJECT-BASED API 323 // 324 325 _TMP_ARY: [], 326 327 /** 328 Adds the object to the selection set. Unlike adding an index set, the 329 selection will actually track the object independent of its location in 330 the array. 331 332 @param {Object} object 333 @returns {SC.SelectionSet} receiver 334 */ 335 addObject: function(object) { 336 var ary = this._TMP_ARY, ret; 337 ary[0] = object; 338 339 ret = this.addObjects(ary); 340 ary.length = 0; 341 342 return ret; 343 }, 344 345 /** 346 Adds objects in the passed enumerable to the selection set. Unlike adding 347 an index set, the seleciton will actually track the object independent of 348 its location the array. 349 350 @param {SC.Enumerable} objects 351 @returns {SC.SelectionSet} receiver 352 */ 353 addObjects: function(objects) { 354 var cur = this._objects, 355 oldlen, newlen; 356 if (!cur) cur = this._objects = SC.CoreSet.create(); 357 oldlen = cur.get('length'); 358 359 cur.addEach(objects); 360 newlen = cur.get('length'); 361 362 this._indexSetCache = null; 363 if (newlen !== oldlen) { 364 this.propertyDidChange('length'); 365 this.enumerableContentDidChange(); 366 } 367 return this; 368 }, 369 370 /** 371 Removes the object from the selection set. Note that if the selection 372 set also selects a range of indexes that includes this object, it may 373 still be in the selection set. 374 375 @param {Object} object 376 @returns {SC.SelectionSet} receiver 377 */ 378 removeObject: function(object) { 379 var ary = this._TMP_ARY, ret; 380 ary[0] = object; 381 382 ret = this.removeObjects(ary); 383 ary.length = 0; 384 385 return ret; 386 }, 387 388 /** 389 Removes the objects from the selection set. Note that if the selection 390 set also selects a range of indexes that includes this object, it may 391 still be in the selection set. 392 393 @param {Object} object 394 @returns {SC.SelectionSet} receiver 395 */ 396 removeObjects: function(objects) { 397 var cur = this._objects, 398 oldlen, newlen, sets; 399 400 if (!cur) return this; 401 402 oldlen = cur.get('length'); 403 404 cur.removeEach(objects); 405 newlen = cur.get('length'); 406 407 // also remove from index sets, if present 408 if (sets = this._sets) { 409 sets.forEach(function(set) { 410 oldlen += set.get('length'); 411 set.removeObjects(objects); 412 newlen += set.get('length'); 413 }, this); 414 } 415 416 this._indexSetCache = null; 417 if (newlen !== oldlen) { 418 this.propertyDidChange('length'); 419 this.enumerableContentDidChange(); 420 } 421 return this; 422 }, 423 424 /** 425 Returns YES if the selection contains the passed object. This will search 426 selected ranges in all source objects. 427 428 @param {Object} object the object to search for 429 @returns {Boolean} 430 */ 431 containsObject: function(object) { 432 // fast path 433 var objects = this._objects ; 434 if (objects && objects.contains(object)) return YES ; 435 436 var sets = this._sets, 437 len = sets ? sets.length : 0, 438 idx, set; 439 for(idx=0;idx<len;idx++) { 440 set = sets[idx]; 441 if (set && set.indexOf(object)>=0) return YES; 442 } 443 444 return NO ; 445 }, 446 447 448 // .......................................................... 449 // GENERIC HELPER METHODS 450 // 451 452 /** 453 Constrains the selection set to only objects found in the passed source 454 object. This will remove any indexes selected in other sources, any 455 indexes beyond the length of the content, and any objects not found in the 456 set. 457 458 @param {Object} source the source to limit 459 @returns {SC.SelectionSet} receiver 460 */ 461 constrain: function (source) { 462 var set, len, max, objects; 463 464 this.beginPropertyChanges(); 465 466 // remove sources other than this one 467 this.get('sources').forEach(function(cur) { 468 if (cur === source) return; //skip 469 var set = this._indexSetForSource(source, NO); 470 if (set) this.remove(source, set); 471 },this); 472 473 // remove indexes beyond end of source length 474 set = this._indexSetForSource(source, NO); 475 if (set && ((max=set.get('max'))>(len=source.get('length')))) { 476 this.remove(source, len, max-len); 477 } 478 479 // remove objects not in source 480 if (objects = this._objects) { 481 var i, cur; 482 for (i = objects.length - 1; i >= 0; i--) { 483 cur = objects[i]; 484 if (source.indexOf(cur) < 0) this.removeObject(cur); 485 } 486 } 487 488 this.endPropertyChanges(); 489 return this ; 490 }, 491 492 /** 493 Returns YES if the passed index set or selection set contains the exact 494 same source objects and indexes as the receiver. If you pass any object 495 other than an IndexSet or SelectionSet, returns NO. 496 497 @param {Object} obj another object. 498 @returns {Boolean} 499 */ 500 isEqual: function(obj) { 501 var left, right, idx, len, sources, source; 502 503 // fast paths 504 if (!obj || !obj.isSelectionSet) return NO ; 505 if (obj === this) return YES; 506 if ((this._sets === obj._sets) && (this._objects === obj._objects)) return YES; 507 if (this.get('length') !== obj.get('length')) return NO; 508 509 // check objects 510 left = this._objects; 511 right = obj._objects; 512 if (left || right) { 513 if ((left ? left.get('length'):0) !== (right ? right.get('length'):0)) { 514 return NO; 515 } 516 if (left && !left.isEqual(right)) return NO ; 517 } 518 519 // now go through the sets 520 sources = this.get('sources'); 521 len = sources.get('length'); 522 for(idx=0;idx<len;idx++) { 523 source = sources.objectAt(idx); 524 left = this._indexSetForSource(source, NO); 525 right = this._indexSetForSource(source, NO); 526 if (!!right !== !!left) return NO ; 527 if (left && !left.isEqual(right)) return NO ; 528 } 529 530 return YES ; 531 }, 532 533 /** 534 Clears the set. Removes all IndexSets from the object 535 536 @returns {SC.SelectionSet} 537 */ 538 clear: function() { 539 if (this.isFrozen) throw new Error(SC.FROZEN_ERROR); 540 if (this._sets) this._sets.length = 0 ; // truncate 541 if (this._objects) this._objects = null; 542 543 this._indexSetCache = null; 544 this.propertyDidChange('length'); 545 this.enumerableContentDidChange(); 546 this.notifyPropertyChange('sources'); 547 548 return this ; 549 }, 550 551 /** 552 Clones the set into a new set. 553 554 @returns {SC.SelectionSet} 555 */ 556 copy: function() { 557 var ret = this.constructor.create(), 558 sets = this._sets, 559 len = sets ? sets.length : 0 , 560 idx, set; 561 562 if (sets && len>0) { 563 sets = ret._sets = sets.slice(); 564 for(idx=0;idx<len;idx++) { 565 if (!(set = sets[idx])) continue ; 566 set = sets[idx] = set.copy(); 567 ret[SC.guidFor(set.source)] = idx; 568 } 569 } 570 571 if (this._objects) ret._objects = this._objects.copy(); 572 return ret ; 573 }, 574 575 /** 576 @private 577 578 Freezing a SelectionSet also freezes its internal sets. 579 */ 580 freeze: function() { 581 if (this.get('isFrozen')) { return this ; } 582 var sets = this._sets, 583 loc = sets ? sets.length : 0, 584 set ; 585 586 while(--loc >= 0) { 587 set = sets[loc]; 588 if (set) { set.freeze(); } 589 } 590 591 if (this._objects) { this._objects.freeze(); } 592 this.set('isFrozen', YES); 593 return this; 594 // return sc_super(); 595 }, 596 597 // .......................................................... 598 // ITERATORS 599 // 600 601 /** @private */ 602 toString: function() { 603 var sets = this._sets || []; 604 sets = sets.map(function(set) { 605 return set.toString().replace("SC.IndexSet", SC.guidFor(set.source)); 606 }, this); 607 if (this._objects) sets.push(this._objects.toString()); 608 return "SC.SelectionSet:%@<%@>".fmt(SC.guidFor(this), sets.join(',')); 609 }, 610 611 /** @private */ 612 firstObject: function() { 613 var sets = this._sets, 614 objects = this._objects; 615 616 // if we have sets, get the first one 617 if (sets && sets.get('length')>0) { 618 var set = sets ? sets[0] : null, 619 src = set ? set.source : null, 620 idx = set ? set.firstObject() : -1; 621 if (src && idx>=0) return src.objectAt(idx); 622 } 623 624 // otherwise if we have objects, get the first one 625 return objects ? objects.firstObject() : undefined; 626 627 }.property(), 628 629 /** @private 630 Implement primitive enumerable support. Returns each object in the 631 selection. 632 */ 633 nextObject: function(count, lastObject, context) { 634 var objects, ret; 635 636 // TODO: Make this more efficient. Right now it collects all objects 637 // first. 638 639 if (count === 0) { 640 objects = context.objects = []; 641 this.forEach(function(o) { objects.push(o); }, this); 642 context.max = objects.length; 643 } 644 645 objects = context.objects ; 646 ret = objects[count]; 647 648 if (count+1 >= context.max) { 649 context.objects = context.max = null; 650 } 651 652 return ret ; 653 }, 654 655 /** 656 Iterates over the selection, invoking your callback with each __object__. 657 This will actually find the object referenced by each index in the 658 selection, not just the index. 659 660 The callback must have the following signature: 661 662 function callback(object, index, source, indexSet) { ... } 663 664 If you pass a target, it will be used when the callback is called. 665 666 @param {Function} callback function to invoke. 667 @param {Object} target optional content. otherwise uses window 668 @returns {SC.SelectionSet} receiver 669 */ 670 forEach: function(callback, target) { 671 var sets = this._sets, 672 objects = this._objects, 673 len = sets ? sets.length : 0, 674 set, idx; 675 676 for(idx=0;idx<len;idx++) { 677 set = sets[idx]; 678 if (set) set.forEachObject(callback, target); 679 } 680 681 if (objects) objects.forEach(callback, target); 682 return this ; 683 } 684 685 }); 686 687 /** @private */ 688 SC.SelectionSet.prototype.clone = SC.SelectionSet.prototype.copy; 689 690 /** 691 Default frozen empty selection set 692 693 @property {SC.SelectionSet} 694 */ 695 SC.SelectionSet.EMPTY = SC.SelectionSet.create().freeze(); 696 697