sproutcore 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/Manifest.txt +42 -29
  2. data/README.txt +1 -1
  3. data/Rakefile +5 -0
  4. data/app_generators/sproutcore/sproutcore_generator.rb +4 -4
  5. data/app_generators/sproutcore/templates/sc-config.rb +72 -0
  6. data/bin/sc-build +2 -0
  7. data/clients/sc_docs/controllers/docs.js +1 -0
  8. data/clients/sc_docs/english.lproj/body.rhtml +5 -0
  9. data/clients/sc_docs/views/doc_frame.js +1 -1
  10. data/clients/sc_test_runner/controllers/runner.js +2 -2
  11. data/clients/sc_test_runner/english.lproj/body.rhtml +5 -0
  12. data/clients/sc_test_runner/main.js +12 -12
  13. data/clients/sc_test_runner/models/test.js +3 -0
  14. data/clients/sc_test_runner/views/test_label.js +1 -1
  15. data/config/hoe.rb +2 -0
  16. data/frameworks/sproutcore/controllers/array.js +132 -125
  17. data/frameworks/sproutcore/drag/drag.js +5 -2
  18. data/frameworks/sproutcore/english.lproj/buttons.css +64 -64
  19. data/frameworks/sproutcore/english.lproj/collections.css +82 -0
  20. data/frameworks/sproutcore/english.lproj/images/buttons-sprite.png +0 -0
  21. data/frameworks/sproutcore/english.lproj/images/sproutcore-logo.png +0 -0
  22. data/frameworks/sproutcore/english.lproj/images/sticky-note.png +0 -0
  23. data/frameworks/sproutcore/english.lproj/menu.css +1 -1
  24. data/frameworks/sproutcore/english.lproj/theme.css +13 -4
  25. data/frameworks/sproutcore/foundation/array.js +24 -2
  26. data/frameworks/sproutcore/foundation/benchmark.js +12 -5
  27. data/frameworks/sproutcore/foundation/observable.js +62 -1
  28. data/frameworks/sproutcore/foundation/utils.js +16 -0
  29. data/frameworks/sproutcore/tests/views/label_item.rhtml +21 -0
  30. data/frameworks/sproutcore/tests/views/list.rhtml +21 -0
  31. data/frameworks/sproutcore/tests/views/scroll.rhtml +21 -0
  32. data/frameworks/sproutcore/views/collection.js +401 -73
  33. data/frameworks/sproutcore/views/collection/collection_item.js +36 -0
  34. data/frameworks/sproutcore/views/collection/grid.js +149 -0
  35. data/frameworks/sproutcore/views/collection/image_cell.js +154 -0
  36. data/frameworks/sproutcore/views/collection/list.js +115 -0
  37. data/frameworks/sproutcore/views/collection/text_cell.js +128 -0
  38. data/frameworks/sproutcore/views/image.js +1 -1
  39. data/frameworks/sproutcore/views/label.js +6 -2
  40. data/frameworks/sproutcore/views/scroll.js +34 -0
  41. data/frameworks/sproutcore/views/view.js +12 -4
  42. data/generators/client/client_generator.rb +3 -11
  43. data/generators/client/templates/english.lproj/body.css +75 -0
  44. data/generators/client/templates/english.lproj/body.rhtml +17 -2
  45. data/generators/model/templates/fixture.js +32 -0
  46. data/lib/sproutcore/build_tools/html_builder.rb +29 -11
  47. data/lib/sproutcore/build_tools/resource_builder.rb +1 -1
  48. data/lib/sproutcore/bundle.rb +19 -7
  49. data/lib/sproutcore/library.rb +39 -21
  50. data/lib/sproutcore/merb/bundle_controller.rb +3 -8
  51. data/lib/sproutcore/version.rb +1 -1
  52. data/lib/sproutcore/view_helpers.rb +7 -5
  53. data/lib/sproutcore/view_helpers/core_views.rb +11 -3
  54. data/sc-config.rb +7 -0
  55. data/tasks/deployment.rake +15 -2
  56. metadata +44 -31
  57. data/app_generators/sproutcore/templates/environment.yml +0 -4
  58. data/environment.yml +0 -9
@@ -0,0 +1,82 @@
1
+ /* @override http://localhost:4020/static/sproutcore/en/_cache/collections-1205614097.css */
2
+
3
+ /* @group Common */
4
+
5
+ body {
6
+ overflow: hidden;
7
+ }
8
+
9
+ .collection-item {
10
+ cursor: pointer ;
11
+ }
12
+
13
+ /* @end */
14
+
15
+ /* @group List View */
16
+
17
+ .sc-theme .sc-collection-view {
18
+ background-color: white ;
19
+ }
20
+
21
+ .sc-theme .list-insertion-point {
22
+ border: 1px #4e4977 solid;
23
+ position: absolute ;
24
+ }
25
+
26
+ .sc-theme .list-insertion-point .anchor {
27
+ position: absolute ;
28
+ width: 7px;
29
+ height: 7px;
30
+ left: -6px;
31
+ top: -4px;
32
+ background: static_url('images/buttons-sprite.png') no-repeat 0px -931px;
33
+ }
34
+
35
+ /* @end */
36
+
37
+ /* @group Grid View */
38
+
39
+ .sc-theme .grid-insertion-point {
40
+ border: 1px #4e4977 solid;
41
+ position: absolute ;
42
+ }
43
+
44
+ .sc-theme .grid-insertion-point .anchor {
45
+ position: absolute ;
46
+ width: 7px;
47
+ height: 7px;
48
+ left: -4px;
49
+ top: -6px;
50
+ background: static_url('images/buttons-sprite.png') no-repeat 0px -931px;
51
+ }
52
+
53
+ /* @end */
54
+
55
+ /* @group Text Cell */
56
+
57
+ .sc-theme .collection-item {
58
+ text-decoration: none ;
59
+ color: #000;
60
+ border-top: 1px white solid;
61
+ background-color: white ;
62
+ }
63
+
64
+ .sc-theme .collection-item.text-cell {
65
+ display: block ;
66
+ padding: 0 6px;
67
+ line-height: 22px;
68
+ }
69
+ .sc-theme .collection-item.sel {
70
+ background-color: #ddd;
71
+ border-top: 1px solid #eee;
72
+ }
73
+
74
+ .sc-theme .sc-collection-view.focus .collection-item.sel {
75
+ background-color: #40007e;
76
+ color: white ;
77
+ border-top: 1px solid #84788f;
78
+ }
79
+
80
+ /* @end */
81
+
82
+
@@ -49,7 +49,7 @@
49
49
  }
50
50
 
51
51
  .menu-pane li.menu-item.active a {
52
- background: static_url('buttons.png') repeat-x left -67px;
52
+ background: static_url('images/buttons-sprite.png') repeat-x left -67px;
53
53
  color: white ;
54
54
  border-top-width: 1px;
55
55
  border-top-color: #a9a9a9;
@@ -1,3 +1,12 @@
1
+ /* @override http://localhost:4020/static/sproutcore/en/_cache/theme-1205438214.css */
2
+
3
+ /* @group Root Container */
4
+
5
+ .sc-scroll-view {
6
+ overflow: auto ;
7
+ }
8
+
9
+ /* @end */
1
10
 
2
11
  /* @group Root Form */
3
12
 
@@ -24,16 +33,16 @@ input.show-hint {
24
33
  /* @group sc-theme */
25
34
 
26
35
  body.sc-theme {
27
- font: 12px "Helvetica Neue", Helvetica, Verdana, sans-serif;
36
+ font: 13px Helvetica, Verdana, sans-serif;
37
+ line-height: 18px;
28
38
  background-color: #f0f0f0 ;
29
- padding: 10px;
39
+ padding: 0px;
30
40
  }
31
41
 
32
42
  .sc-theme h1 {
33
- margin: 0 10%;
43
+ margin: 0;
34
44
  padding: 0 ;
35
45
  margin-bottom: 10px;
36
- border-bottom: 1px #888 dotted ;
37
46
  }
38
47
 
39
48
  .sc-theme label {
@@ -68,6 +68,7 @@ SC.Array = {
68
68
  */
69
69
  objectAt: function(idx)
70
70
  {
71
+ if (idx < 0) return undefined ;
71
72
  if (idx >= this.get('length')) return undefined;
72
73
  return this.get(idx);
73
74
  },
@@ -86,7 +87,8 @@ SC.Array = {
86
87
  notified.
87
88
  */
88
89
  arrayContentDidChange: function() {
89
- this.propertyDidChange('[]') ;
90
+ var kvo = (this._kvo) ? this._kvo().changes : '(null)';
91
+ this.notifyPropertyChange('[]') ;
90
92
  },
91
93
 
92
94
  /**
@@ -204,8 +206,27 @@ SC.Array = {
204
206
  // enumerable methods since Arrays are already enumerable.
205
207
  Object.extend(Array.prototype, SC.Array) ;
206
208
 
207
- // Now make SC.Array enumerable.
209
+ // Now make SC.Array enumerable and add other array method we did not want to
210
+ // override in Array itself.
208
211
  Object.extend(SC.Array, Enumerable) ;
212
+ Object.extend(SC.Array, {
213
+ /**
214
+ Returns a new array that is a slice of the receiver. This implementation
215
+ uses the observable array methods to retrieve the objects for the new slice.
216
+
217
+ @param beginIndex {Integer} (Optional) index to begin slicing from. Default: 0
218
+ @param endIndex {Integer} (Optional) index to end the slice at. Default: 0
219
+ */
220
+ slice: function(beginIndex, endIndex) {
221
+ var ret = [];
222
+ var length = this.get('length') ;
223
+ if (beginIndex == null) beginIndex = 0 ;
224
+ if ((endIndex == null) || (endIndex > length)) endIndex = length ;
225
+ while(beginIndex < endIndex) ret[ret.length] = this.objectAt(beginIndex++) ;
226
+ return ret ;
227
+ }
228
+
229
+ }) ;
209
230
 
210
231
  // ........................................................
211
232
  // A few basic enhancements to the Array class.
@@ -298,6 +319,7 @@ Object.extend(Array.prototype, {
298
319
  if (value !== undefined) return null ;
299
320
  return this.invoke('get', key) ;
300
321
  }
322
+
301
323
  }) ;
302
324
 
303
325
  Array.prototype.collect = Array.prototype.map ;
@@ -58,23 +58,30 @@ SC.Benchmark = {
58
58
  Call this method at the start of whatever you want to collect.
59
59
  if topLevelOnly is passed, then recursive calls to the start will be
60
60
  ignored and only the top level call will be benchmarked.
61
+
62
+ @param key {String} A unique key that identifies this benchmark. All calls to start/end with the same key will be groups together.
63
+ @param topLevelOnly {Boolean} If true then recursive calls to this method with the same key will be ignored.
64
+ @param time {Integer} Only pass if you want to explicitly set the start time. Otherwise the start time is now.
61
65
  */
62
- start: function(key, topLevelOnly) {
66
+ start: function(key, topLevelOnly, time) {
63
67
  if (!this.enabled) return ;
64
68
  var stat = this._statFor(key) ;
65
69
 
66
70
  if (topLevelOnly && stat._starts.length > 0) {
67
71
  stat._starts.push('ignore') ;
68
72
  } else {
69
- stat._starts.push(Date.now()) ;
73
+ stat._starts.push(time || Date.now()) ;
70
74
  }
71
75
  },
72
76
 
73
77
  /**
74
78
  Call this method at the end of whatever you want to collect. This will
75
79
  save the collected benchmark.
80
+
81
+ @param key {String} The benchmark key you used when you called start()
82
+ @param time {Integer} Only pass if you want to explicitly set the end time. Otherwise start time is now.
76
83
  */
77
- end: function(key) {
84
+ end: function(key, time) {
78
85
  if (!this.enabled) return ;
79
86
  var stat = this._statFor(key) ;
80
87
  var start = stat._starts.pop() ;
@@ -86,7 +93,7 @@ SC.Benchmark = {
86
93
  // top level only.
87
94
  if (start == 'ignore') return ;
88
95
 
89
- stat.amt += Date.now() - start ;
96
+ stat.amt += (time || Date.now()) - start ;
90
97
  stat.runs++ ;
91
98
 
92
99
  if (this.verbose) this.log(key) ;
@@ -198,7 +205,7 @@ SC.Benchmark = {
198
205
  return ret ;
199
206
  },
200
207
 
201
- reset: function() { this.stats = {} ; },
208
+ reset: function() { this.stats = {} ; debugger;},
202
209
 
203
210
  // This is private, but it is used in some places, so we are keeping this for
204
211
  // compatibility.
@@ -174,6 +174,25 @@ SC.Observable = {
174
174
  if (tuple[0] == null) return null ;
175
175
  return tuple[0].set(tuple[1], value) ;
176
176
  },
177
+
178
+
179
+ /**
180
+ Convenience method to get an array of properties.
181
+
182
+ Pass in multiple property keys or an array of property keys. This
183
+ method uses getPath() so you can also pass key paths.
184
+
185
+ @returns {Array} Values of property keys.
186
+ */
187
+ getEach: function() {
188
+ var keys = $A(arguments).flatten() ;
189
+ var ret = [];
190
+ for(var idx=0; idx<keys.length;idx++) {
191
+ ret[ret.length] = this.getPath(keys[idx]);
192
+ }
193
+ return ret ;
194
+ },
195
+
177
196
 
178
197
  /**
179
198
  Increments the value of a property.
@@ -228,18 +247,60 @@ SC.Observable = {
228
247
  },
229
248
 
230
249
  /**
231
- If you are not going to use get/set, you can notify observers this way.
250
+ Notify the observer system that a property is about to change.
251
+
252
+ Sometimes you need to change a value directly or indirectly without actually
253
+ calling get() or set() on it. In this case, you can use this method and
254
+ propertyDidChange() instead. Calling these two methods together will notify all
255
+ observers that the property has potentially changed value.
256
+
257
+ Note that you must always call propertyWillChange and propertyDidChange as a pair.
258
+ If you do not, it may get the property change groups out of order and cause
259
+ notifications to be delivered more often than you would like.
260
+
261
+ @param key {String} The property key that is about to change.
232
262
  */
233
263
  propertyWillChange: function(key) {
234
264
  this._kvo().changes++ ;
235
265
  },
236
266
 
267
+ /**
268
+ Notify the observer system that a property has just changed.
269
+
270
+ Sometimes you need to change a value directly or indirectly without actually
271
+ calling get() or set() on it. In this case, you can use this method and
272
+ propertyWillChange() instead. Calling these two methods together will notify all
273
+ observers that the property has potentially changed value.
274
+
275
+ Note that you must always call propertyWillChange and propertyDidChange as a pair.
276
+ If you do not, it may get the property change groups out of order and cause
277
+ notifications to be delivered more often than you would like.
278
+
279
+ @param key {String} The property key that has just changed.
280
+ @param value {Object} The new value of the key. May be null.
281
+ */
237
282
  propertyDidChange: function(key,value) {
238
283
  this._kvo().changed[key] = value ;
239
284
  var kvo = this._kvo() ; kvo.changes--; kvo.revision++ ;
240
285
  if (kvo.changes <= 0) this._notifyPropertyObservers() ;
241
286
  },
242
287
 
288
+ /**
289
+ Convenience method to call propertyWillChange/propertyDidChange.
290
+
291
+ Sometimes you need to notify observers that a property has changed value without
292
+ actually changing this value. In those cases, you can use this method as a
293
+ convenience instead of calling propertyWillChange() and propertyDidChange().
294
+
295
+ @param key {String} The property key that has just changed.
296
+ @param value {Object} The new value of the key. May be null.
297
+ @returns {void}
298
+ */
299
+ notifyPropertyChange: function(key, value) {
300
+ this.propertyWillChange(key) ;
301
+ this.propertyDidChange(key, value) ;
302
+ },
303
+
243
304
  /**
244
305
  This may be a simpler way to notify of changes if you are making a major
245
306
  update or don't know exactly which properties have changed. This ignores
@@ -56,6 +56,22 @@ Object.extend(SC,
56
56
  (point.y >= SC.minY(f)) &&
57
57
  (point.x <= SC.maxX(f)) &&
58
58
  (point.y <= SC.maxY(f)) ;
59
+ },
60
+
61
+ /** Return true if the two frames match.
62
+
63
+ @param r1 {Rect} the first rect
64
+ @param r2 {Rect} the second rect
65
+ @param delta {Float} an optional delta that allows for rects that do not match exactly. Defaults to 0.1
66
+ @returns {Boolean} true if rects match
67
+ */
68
+ rectsEqual: function(r1, r2, delta) {
69
+ if (delta == null) delta = 0.1;
70
+ if (Math.abs(r1.y - r2.y) > delta) return false ;
71
+ if (Math.abs(r1.x - r2.x) > delta) return false ;
72
+ if (Math.abs(r1.width - r2.width) > delta) return false ;
73
+ if (Math.abs(r1.height - r2.height) > delta) return false ;
74
+ return true ;
59
75
  }
60
76
 
61
77
  }) ;
@@ -0,0 +1,21 @@
1
+ <% # ========================================================================
2
+ # Sproutcore.LabelItemView Unit Test
3
+ # ========================================================================
4
+ %>
5
+ <% content_for('final') do %>
6
+
7
+ <script>
8
+
9
+ Test.context("Sproutcore.LabelItemView",{
10
+
11
+ "TODO: Add your own tests here": function() {
12
+ true.shouldEqual(true) ;
13
+ }
14
+
15
+ }) ;
16
+
17
+ appMain = main; main = null ; // Cancel main() so app does not start
18
+
19
+ </script>
20
+
21
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <% # ========================================================================
2
+ # Sproutcore.ListView Unit Test
3
+ # ========================================================================
4
+ %>
5
+ <% content_for('final') do %>
6
+
7
+ <script>
8
+
9
+ Test.context("Sproutcore.ListView",{
10
+
11
+ "TODO: Add your own tests here": function() {
12
+ true.shouldEqual(true) ;
13
+ }
14
+
15
+ }) ;
16
+
17
+ appMain = main; main = null ; // Cancel main() so app does not start
18
+
19
+ </script>
20
+
21
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <% # ========================================================================
2
+ # Sproutcore.ScrollView Unit Test
3
+ # ========================================================================
4
+ %>
5
+ <% content_for('final') do %>
6
+
7
+ <script>
8
+
9
+ Test.context("Sproutcore.ScrollView",{
10
+
11
+ "TODO: Add your own tests here": function() {
12
+ true.shouldEqual(true) ;
13
+ }
14
+
15
+ }) ;
16
+
17
+ appMain = main; main = null ; // Cancel main() so app does not start
18
+
19
+ </script>
20
+
21
+ <% end %>
@@ -351,7 +351,8 @@ SC.CollectionView = SC.View.extend(
351
351
 
352
352
  /**
353
353
  Property returns all of the item views, regardless of group view.
354
-
354
+
355
+ @property
355
356
  @returns {Array} the item views.
356
357
  */
357
358
  itemViews: function() {
@@ -363,6 +364,31 @@ SC.CollectionView = SC.View.extend(
363
364
  return ret;
364
365
  }.property(),
365
366
 
367
+ /**
368
+ The property on content objects item views should display.
369
+
370
+ Most built-in item views will respect this property. You can also use it when writing
371
+ you own item views.
372
+ */
373
+ displayProperty: null,
374
+
375
+ /**
376
+ Enables keyboard-based navigate if set to true.
377
+ */
378
+ acceptsFirstResponder: false,
379
+
380
+ /**
381
+ If your layout uses a grid or horizontal-based layout, then make sure this
382
+ property is always up to date with the current number of items per row.
383
+
384
+ The CollectionView will use this property to support keyboard navigation
385
+ using the arrow keys.
386
+
387
+ If your collection view is simply a vertical list of items then you do not need
388
+ to edit this property.
389
+ */
390
+ itemsPerRow: 1,
391
+
366
392
  /**
367
393
  Returns true if the passed view belongs to the collection.
368
394
 
@@ -379,6 +405,22 @@ SC.CollectionView = SC.View.extend(
379
405
  return !!this._itemViews[SC.getGUID(view)];
380
406
  },
381
407
 
408
+ // ......................................
409
+ // FIRST RESPONDER
410
+ //
411
+
412
+ /**
413
+ Called whenever the collection becomes first responder.
414
+ Adds the focused class to the element.
415
+ */
416
+ didBecomeFirstResponder: function() {
417
+ this.addClassName('focus') ;
418
+ },
419
+
420
+ willLoseFirstResponder: function() {
421
+ this.removeClassName('focus');
422
+ },
423
+
382
424
  // ......................................
383
425
  // DRAG AND DROP SUPPORT
384
426
  //
@@ -425,7 +467,7 @@ SC.CollectionView = SC.View.extend(
425
467
  // This assumes you will flow LTR, but it should work if you flow
426
468
  // bottom to top or top to bottom.
427
469
  } else {
428
- if (SC.maxX(f) > loc.x) {
470
+ if (SC.minX(f) < loc.x) {
429
471
  curSide = (SC.maxY(f) < loc.y) ? -1 : 1 ;
430
472
  } else curSide = null ;
431
473
  }
@@ -488,26 +530,86 @@ SC.CollectionView = SC.View.extend(
488
530
  @returns {void}
489
531
  */
490
532
  hideInsertionPoint: function() {},
533
+
534
+ /**
535
+ Override this method to provide your own ghost image for a drag.
536
+
537
+ Note that the only purpose of this view is to render a visible drag element. It is
538
+ not critical that you make this element bindable, etc.
539
+
540
+ @param dragContent {Array} Array of content objects that will be used in the drag.
541
+ */
542
+ ghostViewFor: function(dragContent) {
543
+ var view = SC.View.create() ;
544
+ view.set('frame', this.get('frame')) ;
545
+ view.set('isPositioned', true) ;
546
+ var idx = dragContent.length ;
547
+ var maxX = 0; var maxY = 0 ;
548
+
549
+ while(--idx >= 0) {
550
+ var itemView = this.itemViewForContent(dragContent[idx]) ;
551
+ if (!itemView) continue ;
552
+ var f = itemView.get('frame') ;
553
+ var dom = itemView.rootElement ;
554
+ if (!dom) continue ;
555
+
556
+ // save the maxX & maxY. This will be used to trim the size
557
+ // of the ghost view later.
558
+ if (SC.maxX(f) > maxX) maxX = SC.maxX(f) ;
559
+ if (SC.maxY(f) > maxY) maxY = SC.maxY(f) ;
560
+
561
+ // Clone the contents of this node. We should probably apply the
562
+ // computed style to the cloned nodes in order to make sure they match even if the
563
+ // CSS styles do not match. Make sure the items are properly
564
+ // positioned.
565
+ dom = dom.cloneNode(true) ;
566
+ Element.setStyle(dom, { position: "absolute", left: "%@px".fmt(f.x), top: "%@px".fmt(f.y), width: "%@px".fmt(f.width), height: "%@px".fmt(f.height) }) ;
567
+ view.rootElement.appendChild(dom) ;
568
+ }
569
+
570
+ // Trim the size of the view to match the maxX & maxY as well as overflow
571
+ view.setStyle({ overflow: 'hidden', width: "%@px.".fmt(maxX+1), height: "%@px".fmt(maxY+1) }) ;
572
+
573
+ return view ;
574
+ },
491
575
 
492
- // handle mouse drags. If the canReorderContent is enabled, allow the
493
- // user to start a reorder.
494
576
  mouseDragged: function(ev) {
495
577
  // Don't do anything unless the user has been dragging for 123msec
496
578
  if ((Date.now() - this._mouseDownAt) < 123) return true ;
497
579
 
498
580
  // OK, they must be serious, start a drag if possible.
499
- // Also use this opportunity to clean up since mouseUp won't
500
- // get called.
501
581
  if (this.get('canReorderContent')) {
582
+
583
+ // we need to recalculate the frame at this point.
584
+ this.flushFrameCache();
585
+
586
+ // First, get the selection to drag. Drag an array of selected
587
+ // items appearing in this collection, in the order of the
588
+ // collection.
589
+ var content = this.get('content') || [] ;
590
+ var dragContent = this.get('selection').sort(function(a,b) {
591
+ a = content.indexOf(a) ; b = content.indexOf(b) ;
592
+ return (a<b) ? -1 : ((a>b) ? 1 : 0) ;
593
+ });
594
+
595
+ // Build the drag view to use for the ghost drag. This
596
+ // should essentially contain any visible drag items.
597
+ var view = this.ghostViewFor(dragContent) ;
598
+
599
+ // Initiate the drag
502
600
  SC.Drag.start({
503
601
  event: this._mouseDownEvent,
504
602
  source: this,
505
- dragView: this._mouseDownView,
603
+ dragView: view,
506
604
  ghost: NO,
507
605
  slideBack: YES,
508
- data: { "_mouseDownContent": this._mouseDownContent }
606
+ data: { "_mouseDownContent": dragContent }
509
607
  }) ;
608
+
609
+ // Also use this opportunity to clean up since mouseUp won't
610
+ // get called.
510
611
  this._cleanupMouseDown() ;
612
+ this._lastInsertionIndex = null ;
511
613
  }
512
614
  },
513
615
 
@@ -525,9 +627,33 @@ SC.CollectionView = SC.View.extend(
525
627
  if (this.get('canReorderContent')) {
526
628
  var loc = drag.get('location') ;
527
629
  loc = this.convertFrameFromView(loc, null) ;
630
+
631
+ // get the insertion index for this location. This can be computed
632
+ // by a subclass using whatever method. This method is not expected to
633
+ // do any data valdidation, just to map the location to an insertion index.
528
634
  var ret = this.insertionIndexForLocation(loc) ;
635
+
636
+ // now that we have an index, find the nearest index that we can actually
637
+ // insert at, or do not allow.
638
+ var objects = (drag.source == this) ? (drag.dataForType('_mouseDownContent') || []) : [];
639
+ var content = this.get('content') || [] ;
640
+
641
+ // if the insertion index is in between two items in the drag itself, then this is
642
+ // not allowed. Either use the last insertion index or find the first index that is not
643
+ // in between selections.
644
+ var isPreviousInDrag = (ret > 0) ? objects.indexOf(content.objectAt(ret-1)) : -1 ;
645
+ var isNextInDrag = (ret < content.get('length')-1) ? objects.indexOf(content.objectAt(ret)) : -1 ;
646
+ if (isPreviousInDrag>=0 && isNextInDrag>=0) {
647
+ if (this._lastInsertionIndex == null) {
648
+ while((ret > 0) && (objects.indexOf(content.objectAt(ret)) >= 0)) ret-- ;
649
+ } else ret = this._lastInsertionIndex ;
650
+ }
651
+
652
+ // Now that we have verified that, check to see if a drop is allowed in the
653
+ // insertion index with the delegate.
654
+ // TODO
655
+
529
656
  if (this._lastInsertionIndex != ret) {
530
- console.log("--itemView: %@".fmt(ret)) ;
531
657
  var itemView = this.itemViewForContent(this.get('content').objectAt(ret));
532
658
  this.showInsertionPointBefore(itemView) ;
533
659
  }
@@ -553,29 +679,39 @@ SC.CollectionView = SC.View.extend(
553
679
 
554
680
  performDragOperation: function(op, drag) {
555
681
 
682
+ SC.Benchmark.start('%@ performDragOperation'.fmt(this._guid)) ;
683
+
556
684
  var loc = drag.get('location') ;
557
685
  loc = this.convertFrameFromView(loc, null) ;
558
686
 
559
687
  // if op is MOVE or COPY, add item to view.
560
- var obj = drag.dataForType('_mouseDownContent') ;
561
- if (obj && (op == SC.DRAG_MOVE)) {
688
+ var objects = drag.dataForType('_mouseDownContent') ;
689
+ if (objects && (op == SC.DRAG_MOVE)) {
562
690
 
563
691
  // find the index to for the new insertion
564
692
  var idx = this.insertionIndexForLocation(loc) ;
565
-
693
+
566
694
  var content = this.get('content') ;
567
695
  content.beginPropertyChanges(); // suspend notifications
568
-
696
+
697
+ // debugger ;
569
698
  // find the old index and remove it.
570
- var old = content.indexOf(obj) ;
571
- if (old >= 0) content.removeAt(old) ;
572
- if ((old >= 0) && (old <= idx)) idx--; //adjust idx
699
+ var objectsIdx = objects.get('length') ;
700
+ while(--objectsIdx >= 0) {
701
+ var obj = objects.objectAt(objectsIdx) ;
702
+ var old = content.indexOf(obj) ;
703
+ if (old >= 0) content.removeAt(old) ;
704
+ if ((old >= 0) && (old < idx)) idx--; //adjust idx
705
+ }
573
706
 
574
- // now insert object at new location
575
- content.insertAt(idx, obj) ;
707
+ // now insert objects at new location
708
+ content.replace(idx, 0, objects) ;
576
709
  content.endPropertyChanges(); // restart notifications
577
710
  }
578
711
 
712
+ SC.Benchmark.end('%@ performDragOperation'.fmt(this._guid)) ;
713
+ console.log(SC.Benchmark.report()) ;
714
+
579
715
  return SC.DRAG_MOVE;
580
716
  },
581
717
 
@@ -609,6 +745,7 @@ SC.CollectionView = SC.View.extend(
609
745
  {
610
746
  var el = this.containerElement || this.rootElement;
611
747
 
748
+ SC.Benchmark.start('%@: updateChildren'.fmt(this._guid)) ;
612
749
  // initial setup
613
750
  if (this._firstUpdate)
614
751
  {
@@ -713,6 +850,7 @@ SC.CollectionView = SC.View.extend(
713
850
  this.updateSelectionStates() ;
714
851
  this.flushFrameCache() ;
715
852
  this.set('isDirty',false);
853
+ SC.Benchmark.end('%@: updateChildren'.fmt(this._guid)) ;
716
854
  },
717
855
 
718
856
  /**
@@ -746,6 +884,10 @@ SC.CollectionView = SC.View.extend(
746
884
 
747
885
  var firstChild = null ;
748
886
 
887
+ // save the first child to be modified. This will be
888
+ // passed to the layout method.
889
+ var firstModifiedChild = null;
890
+
749
891
  while (child || (inGroup && (loc < contentCount) && !expired)) {
750
892
 
751
893
  // get the content object.
@@ -796,6 +938,7 @@ SC.CollectionView = SC.View.extend(
796
938
  parent.insertBefore(newChild,child);
797
939
  this._itemViews[SC.getGUID(newChild)] = newChild;
798
940
  itemViewsDidChange = true;
941
+ if (!firstModifiedChild) firstModifiedChild = newChild ;
799
942
  child = newChild;
800
943
  }
801
944
 
@@ -813,26 +956,22 @@ SC.CollectionView = SC.View.extend(
813
956
 
814
957
 
815
958
  // maybe save the current render loc and reschedule.
816
- if (expired && (loc < contentCount))
817
- {
959
+ if (expired && (loc < contentCount)) {
818
960
  this._lastRenderLoc = loc ;
819
961
  this._lastRenderChild = child ;
820
962
  setTimeout(this.updateChildren.bind(this),1) ; // do more later.
821
- }
822
- else
823
- {
963
+ } else {
824
964
  this._resetExpiredRender();
825
965
  }
826
966
 
827
967
  // now let the collection view layout the views that changed (if
828
968
  // it is implemented.)
829
- if (this.layoutChildViewsFor)
830
- {
969
+ if (firstModifiedChild && this.layoutChildViewsFor) {
831
970
  var el = this.containerElement || this.rootElement;
832
971
  if (this._cachedParent) {
833
972
  this._cachedParent.insertBefore(el,this._cachedSibling);
834
973
  }
835
- this.layoutChildViewsFor(parent, firstChild);
974
+ this.layoutChildViewsFor(parent, firstModifiedChild);
836
975
  if (this._cachedParent) {
837
976
  this._cachedParent.removeChild(el);
838
977
  }
@@ -981,24 +1120,11 @@ SC.CollectionView = SC.View.extend(
981
1120
  // This will set the collection height.
982
1121
  updateComputedViewHeight: function(groupView) {
983
1122
  var height = this.computedViewHeight(groupView) ;
984
- if (height <= 0) {
985
- if (groupView._heightView) {
986
- groupView.rootElement.removeChild(this._heightView) ;
987
- groupView._heightView = null ;
988
- }
989
- } else {
990
- if (!groupView._heightView) {
991
- groupView._heightView = document.createElement('div') ;
992
- groupView.rootElement.appendChild(groupView._heightView) ;
993
- Element.setStyle(groupView._heightView,{
994
- position: 'absolute', left: '0px', display: 'block',
995
- width: '1px', height: '1px'
996
- }) ;
997
- }
998
-
999
- if (height != groupView._lastComputedHeight) {
1000
- Element.setStyle(groupView._heightView,{ top: height + 'px' }) ;
1001
- groupView._lastComputedHeight = height ;
1123
+ if (height >= 0) {
1124
+ var f = this.get('frame') ;
1125
+ if (Math.abs(f.height - height) > 0.1) {
1126
+ f.height = height ;
1127
+ this.set('frame', { height: height }) ;
1002
1128
  }
1003
1129
  }
1004
1130
  },
@@ -1007,36 +1133,158 @@ SC.CollectionView = SC.View.extend(
1007
1133
  // SELECTION
1008
1134
  //
1009
1135
 
1010
- selectPreviousItem: function()
1136
+ _indexOfSelectionTop: function() {
1137
+ var content = this.get('content');
1138
+ var sel = this.get('selection');
1139
+ if (!content || !sel) return - 1;
1140
+
1141
+ // find the first item in the selection
1142
+ var contentLength = content.get('length') ;
1143
+ var indexOfSelected = contentLength ; var idx = sel.length ;
1144
+ while(--idx >= 0) {
1145
+ var curIndex = content.indexOf(sel[idx]) ;
1146
+ if ((curIndex >= 0) && (curIndex < indexOfSelected)) indexOfSelected = curIndex ;
1147
+ }
1148
+
1149
+ return (indexOfSelected >= contentLength) ? -1 : indexOfSelected ;
1150
+ },
1151
+
1152
+ _indexOfSelectionBottom: function() {
1153
+ var content = this.get('content');
1154
+ var sel = this.get('selection');
1155
+ if (!content || !sel) return - 1;
1156
+
1157
+ var indexOfSelected = -1 ; var idx = sel.length ;
1158
+ while(--idx >= 0) {
1159
+ var curIndex = content.indexOf(sel[idx]) ;
1160
+ if (curIndex > indexOfSelected) indexOfSelected = curIndex ;
1161
+ }
1162
+
1163
+ return (indexOfSelected < 0) ? -1 : indexOfSelected ;
1164
+ },
1165
+
1166
+ /**
1167
+ Select one or more items before the current selection, optionally
1168
+ extending the current selection. Also scrolls the selected item into view.
1169
+
1170
+ Selection does not wrap around.
1171
+
1172
+ @param extend {Boolean} (Optional) If true, the selection will be extended instead of replaced. Defaults to false.
1173
+ @param numberOfItems {Integer} (Optional) The number of previous to be selected. Defaults to 1
1174
+ @returns {void}
1175
+ */
1176
+ selectPreviousItem: function(extend, numberOfItems)
1011
1177
  {
1012
- var extend = arguments[0] || false;
1178
+ if (numberOfItems == null) numberOfItems = 1 ;
1179
+ if (extend == null) extend = false ;
1180
+
1013
1181
  var content = this.get('content');
1014
- var selected = this.get('selection').first();
1015
- var indexOfFirst = 0;
1016
- var indexOfSelected = content.indexOf( selected );
1017
- var indexOfPrevious = indexOfSelected - 1;
1018
- // error check to make sure we're not out of bounds...
1019
- if ( indexOfPrevious < indexOfFirst ) indexOfPrevious = indexOfFirst;
1020
- // ensure that the item is visible
1021
- this.scrollToItemRecord(content.objectAt(indexOfPrevious));
1022
- // set the selection
1023
- this.selectItems(content.objectAt(indexOfPrevious), extend);
1182
+ var contentLength = content.get('length') ;
1183
+
1184
+ // if extending, then we need to do some fun stuff to build the array
1185
+ var selTop, selBottom, anchor ;
1186
+ if (extend) {
1187
+ selTop = this._indexOfSelectionTop() ;
1188
+ selBottom = this._indexOfSelectionBottom() ;
1189
+ anchor = (this._selectionAnchor == null) ? selTop : this._selectionAnchor ;
1190
+ this._selectionAnchor = anchor ;
1191
+
1192
+ // If the selBottom is after the anchor, then reduce the selection
1193
+ if (selBottom > anchor) {
1194
+ selBottom-- ;
1195
+
1196
+ // otherwise, select the previous item from the top
1197
+ } else {
1198
+ selTop-- ;
1199
+ }
1200
+
1201
+ // Ensure we are not out of bounds
1202
+ if (selTop < 0) selTop = 0 ;
1203
+ if (selBottom < selTop) selBottom = selTop ;
1204
+
1205
+ // if not extending, just select the item previous to the selTop
1206
+ } else {
1207
+ selTop = this._indexOfSelectionTop() - 1;
1208
+ if (selTop < 0) selTop = 0 ;
1209
+ selBottom = selTop ;
1210
+ anchor = null ;
1211
+ }
1212
+
1213
+ // now build array of new items to select
1214
+ var items = [] ;
1215
+ while(selTop <= selBottom) {
1216
+ items[items.length] = content.objectAt(selTop++) ;
1217
+ }
1218
+
1219
+ // ensure that the item is visible and set the selection
1220
+ if (items.length > 0) {
1221
+ this.scrollToItemRecord(items.first());
1222
+ this.selectItems(items);
1223
+ }
1224
+
1225
+ this._selectionAnchor = anchor ;
1024
1226
  },
1025
1227
 
1026
- selectNextItem: function()
1228
+ /**
1229
+ Select one or more items folling the current selection, optionally
1230
+ extending the current selection. Also scrolls to selected item.
1231
+
1232
+ Selection does not wrap around.
1233
+
1234
+ @param extend {Boolean} (Optional) If true, the selection will be extended instead of replaced. Defaults to false.
1235
+ @param numberOfItems {Integer} (Optional) The number of items to be selected. Defaults to 1.
1236
+ @returns {void}
1237
+ */
1238
+ selectNextItem: function(extend, numberOfItems)
1027
1239
  {
1028
- var extend = arguments[0] || false;
1240
+ if (numberOfItems == null) numberOfItems = 1 ;
1241
+ if (extend == null) extend = false ;
1242
+
1029
1243
  var content = this.get('content');
1030
- var selected = this.get('selection').last();
1031
- var indexOfLast = (content.get('length') - 1) || 0;
1032
- var indexOfSelected = content.indexOf( selected );
1033
- var indexOfNext = indexOfSelected + 1;
1034
- // error check to make sure we're not out of bounds...
1035
- if ( indexOfNext > indexOfLast ) indexOfNext = indexOfLast;
1036
- // ensure that the item is visible
1037
- this.scrollToItemRecord(content.objectAt(indexOfNext));
1038
- // set the selection
1039
- this.selectItems(content.objectAt(indexOfNext), extend);
1244
+ var contentLength = content.get('length') ;
1245
+
1246
+ // if extending, then we need to do some fun stuff to build the array
1247
+ var selTop, selBottom, anchor ;
1248
+ if (extend) {
1249
+ selTop = this._indexOfSelectionTop() ;
1250
+ selBottom = this._indexOfSelectionBottom() ;
1251
+ anchor = (this._selectionAnchor == null) ? selTop : this._selectionAnchor ;
1252
+ this._selectionAnchor = anchor ;
1253
+
1254
+ // If the selTop is before the anchor, then reduce the selection
1255
+ if (selTop < anchor) {
1256
+ selTop++ ;
1257
+
1258
+ // otherwise, select the next item after the top
1259
+ } else {
1260
+ selBottom++ ;
1261
+ }
1262
+
1263
+ // Ensure we are not out of bounds
1264
+ if (selBottom >= contentLength) selBottom = contentLength-1;
1265
+ if (selTop > selBottom) selTop = selBottom ;
1266
+
1267
+ // if not extending, just select the item next to the selBottom
1268
+ } else {
1269
+ selBottom = this._indexOfSelectionBottom() + 1;
1270
+ if (selBottom >= contentLength) selBottom = contentLength-1;
1271
+ selTop = selBottom ;
1272
+ anchor = null ;
1273
+ }
1274
+
1275
+ // now build array of new items to select
1276
+ var items = [] ;
1277
+ while(selTop <= selBottom) {
1278
+ items[items.length] = content.objectAt(selTop++) ;
1279
+ }
1280
+
1281
+ // ensure that the item is visible and set the selection
1282
+ if (items.length > 0) {
1283
+ this.scrollToItemRecord(items.first());
1284
+ this.selectItems(items);
1285
+ }
1286
+
1287
+ this._selectionAnchor = anchor ;
1040
1288
  },
1041
1289
 
1042
1290
  /**
@@ -1075,12 +1323,25 @@ SC.CollectionView = SC.View.extend(
1075
1323
  }
1076
1324
  },
1077
1325
 
1326
+ /**
1327
+ Selects the passed array of items, optionally extending the
1328
+ current selection.
1329
+
1330
+ @param items {Array} The item or items to select.
1331
+ @param extendSelection {Boolean} If true, extends the selection instead of replacing it.
1332
+ */
1078
1333
  selectItems: function(items, extendSelection) {
1079
1334
  var base = (extendSelection) ? this.get('selection') : [] ;
1080
1335
  var sel = [items].concat(base).flatten().uniq() ;
1336
+
1337
+ // if you are not extending the selection, then clear the selection anchor.
1338
+ this._selectionAnchor = null ;
1081
1339
  this.set('selection',sel) ;
1082
1340
  },
1083
-
1341
+
1342
+ /**
1343
+ Removes the items from the selection.
1344
+ */
1084
1345
  deselectItems: function(items) {
1085
1346
  items = [items].flatten() ;
1086
1347
  var base = this.get('selection') || [] ;
@@ -1093,6 +1354,73 @@ SC.CollectionView = SC.View.extend(
1093
1354
  // EVENT HANDLING
1094
1355
  //
1095
1356
 
1357
+ keyDown: function(evt) {
1358
+ return this.interpretKeyEvents(evt) ;
1359
+ },
1360
+
1361
+ keyUp: function() { return true; },
1362
+
1363
+ /** @private
1364
+ Selects the same item on the next row. Or moves down one if
1365
+ itemsPerRow = 1
1366
+ */
1367
+ moveDown: function(sender, evt) {
1368
+ this.selectNextItem(false, this.get('itemsPerRow') || 1) ;
1369
+ return true ;
1370
+ },
1371
+
1372
+ /** @private
1373
+ Selects the same item on the next row. Or moves up one if
1374
+ itemsPerRow = 1
1375
+ */
1376
+ moveUp: function(sender, evt) {
1377
+ this.selectPreviousItem(false, this.get('itemsPerRow') || 1) ;
1378
+ return true ;
1379
+ },
1380
+
1381
+ /** @private
1382
+ Selects the previous item if itemsPerRow > 1. Otherwise does nothing.
1383
+ */
1384
+ moveLeft: function(sender, evt) {
1385
+ if ((this.get('itemsPerRow') || 1) > 1) this.selectNextItem(false, 1) ;
1386
+ return true ;
1387
+ },
1388
+
1389
+ /** @private
1390
+ Selects the next item if itemsPerRow > 1. Otherwise does nothing.
1391
+ */
1392
+ moveRight: function(sender, evt) {
1393
+ if ((this.get('itemsPerRow') || 1) > 1) this.selectPreviousItem(false, 1) ;
1394
+ return true ;
1395
+ },
1396
+
1397
+ moveDownAndModifySelection: function(sender, evt) {
1398
+ this.selectNextItem(true, this.get('itemsPerRow') || 1) ;
1399
+ return true ;
1400
+ },
1401
+
1402
+ moveUpAndModifySelection: function(sender, evt) {
1403
+ this.selectPreviousItem(true, this.get('itemsPerRow') || 1) ;
1404
+ return true ;
1405
+ },
1406
+
1407
+ /** @private
1408
+ Selects the previous item if itemsPerRow > 1. Otherwise does nothing.
1409
+ */
1410
+ moveLeftAndModifySelection: function(sender, evt) {
1411
+ if ((this.get('itemsPerRow') || 1) > 1) this.selectNextItem(true, 1) ;
1412
+ return true ;
1413
+ },
1414
+
1415
+ /** @private
1416
+ Selects the next item if itemsPerRow > 1. Otherwise does nothing.
1417
+ */
1418
+ moveRightAndModifySelection: function(sender, evt) {
1419
+ if ((this.get('itemsPerRow') || 1) > 1) this.selectPreviousItem(true, 1) ;
1420
+ return true ;
1421
+ },
1422
+
1423
+
1096
1424
  /**
1097
1425
  Find the item view underneath the passed mouse location.
1098
1426
 
@@ -1177,6 +1505,9 @@ SC.CollectionView = SC.View.extend(
1177
1505
  var mouseDownView = this._mouseDownView = this.itemViewForEvent(ev);
1178
1506
  var mouseDownContent = this._mouseDownContent = (mouseDownView) ? mouseDownView.get('content') : null;
1179
1507
 
1508
+ // become first responder if possible.
1509
+ this.becomeFirstResponder() ;
1510
+
1180
1511
  // recieved a mouseDown on the collection element, but not on one of the childItems... bail
1181
1512
  if (!mouseDownView) {
1182
1513
  if (this.get('allowDeselectAll')) this.selectItems([], false);
@@ -1235,8 +1566,6 @@ SC.CollectionView = SC.View.extend(
1235
1566
 
1236
1567
  _mouseUp: function(ev) {
1237
1568
 
1238
- console.info('_mouseUp!');
1239
-
1240
1569
  var canAct = this.get('actOnSelect') ;
1241
1570
  var view = this.itemViewForEvent(ev) ;
1242
1571
 
@@ -1482,8 +1811,7 @@ SC.CollectionView = SC.View.extend(
1482
1811
  // called on content change *and* content.[] change...
1483
1812
  _contentPropertyObserver: function(target,key,value)
1484
1813
  {
1485
- if (!this._updating)
1486
- {
1814
+ if (!this._updating) {
1487
1815
  this._updating = true;
1488
1816
  this.set('isDirty',true);
1489
1817
  this._resetExpiredRender();