sproutcore 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (208) hide show
  1. data/History.txt +233 -0
  2. data/Manifest.txt +67 -34
  3. data/bin/sc-build +12 -1
  4. data/bin/sc-gen +1 -1
  5. data/bin/sproutcore +14 -0
  6. data/clients/sc_docs/controllers/docs.js +38 -8
  7. data/clients/sc_docs/english.lproj/body.css +80 -127
  8. data/clients/sc_docs/english.lproj/body.rhtml +43 -23
  9. data/clients/sc_docs/english.lproj/no_docs.rhtml +2 -1
  10. data/clients/sc_docs/english.lproj/tabs.rhtml +16 -0
  11. data/clients/sc_docs/main.js +14 -9
  12. data/clients/sc_docs/models/doc.js +1 -1
  13. data/clients/sc_docs/tests/controllers/docs.rhtml +1 -2
  14. data/clients/sc_docs/tests/models/doc.rhtml +1 -2
  15. data/clients/sc_docs/tests/views/doc_frame.rhtml +1 -2
  16. data/clients/sc_docs/tests/views/doc_label_view.rhtml +1 -2
  17. data/clients/sc_docs/views/doc_frame.js +1 -1
  18. data/clients/sc_test_runner/controllers/runner.js +31 -8
  19. data/clients/sc_test_runner/english.lproj/body.css +62 -122
  20. data/clients/sc_test_runner/english.lproj/body.rhtml +62 -26
  21. data/clients/sc_test_runner/main.js +1 -6
  22. data/clients/sc_test_runner/models/test.js +14 -1
  23. data/clients/sc_test_runner/views/runner_frame.js +4 -2
  24. data/clients/view_builder/builders/builder.js +339 -0
  25. data/clients/view_builder/builders/button.js +81 -0
  26. data/clients/view_builder/controllers/document.js +21 -0
  27. data/clients/view_builder/core.js +19 -0
  28. data/clients/view_builder/english.lproj/body.css +77 -0
  29. data/clients/view_builder/english.lproj/body.rhtml +41 -0
  30. data/clients/{sc_docs → view_builder}/english.lproj/controls.css +0 -0
  31. data/clients/view_builder/english.lproj/strings.js +14 -0
  32. data/clients/view_builder/main.js +38 -0
  33. data/clients/view_builder/tests/controllers/document.rhtml +20 -0
  34. data/clients/view_builder/tests/views/builder.rhtml +20 -0
  35. data/clients/view_builder/views/builder.js +23 -0
  36. data/frameworks/prototype/prototype.js +1 -1
  37. data/frameworks/sproutcore/Core.js +32 -7
  38. data/frameworks/sproutcore/README +1 -1
  39. data/frameworks/sproutcore/animation/animation.js +411 -0
  40. data/frameworks/sproutcore/controllers/array.js +17 -9
  41. data/frameworks/sproutcore/controllers/collection.js +9 -110
  42. data/frameworks/sproutcore/controllers/controller.js +1 -1
  43. data/frameworks/sproutcore/controllers/object.js +2 -1
  44. data/frameworks/sproutcore/drag/drag.js +267 -56
  45. data/frameworks/sproutcore/drag/drag_data_source.js +24 -16
  46. data/frameworks/sproutcore/drag/drag_source.js +53 -42
  47. data/frameworks/sproutcore/drag/drop_target.js +2 -2
  48. data/frameworks/sproutcore/english.lproj/buttons.css +337 -236
  49. data/frameworks/sproutcore/english.lproj/core.css +115 -0
  50. data/frameworks/sproutcore/english.lproj/icons.css +227 -0
  51. data/{clients/sc_docs → frameworks/sproutcore}/english.lproj/images/indicator.gif +0 -0
  52. data/frameworks/sproutcore/english.lproj/images/sc-theme-sprite.png +0 -0
  53. data/frameworks/sproutcore/english.lproj/images/sc-theme-ysprite.png +0 -0
  54. data/frameworks/sproutcore/english.lproj/images/shared-icons.png +0 -0
  55. data/frameworks/sproutcore/english.lproj/menu.css +1 -1
  56. data/frameworks/sproutcore/english.lproj/strings.js +1 -1
  57. data/frameworks/sproutcore/english.lproj/theme.css +405 -31
  58. data/frameworks/sproutcore/foundation/application.js +15 -11
  59. data/frameworks/sproutcore/foundation/benchmark.js +1 -1
  60. data/frameworks/sproutcore/foundation/binding.js +2 -2
  61. data/frameworks/sproutcore/foundation/date.js +1 -1
  62. data/frameworks/sproutcore/foundation/error.js +1 -1
  63. data/frameworks/sproutcore/foundation/input_manager.js +32 -21
  64. data/frameworks/sproutcore/foundation/mock.js +1 -1
  65. data/frameworks/sproutcore/foundation/node_descriptor.js +9 -6
  66. data/frameworks/sproutcore/foundation/object.js +249 -177
  67. data/frameworks/sproutcore/foundation/page.js +5 -2
  68. data/frameworks/sproutcore/foundation/path_module.js +11 -10
  69. data/frameworks/sproutcore/foundation/responder.js +5 -2
  70. data/frameworks/sproutcore/foundation/routes.js +17 -13
  71. data/frameworks/sproutcore/foundation/run_loop.js +249 -11
  72. data/frameworks/sproutcore/foundation/server.js +1 -1
  73. data/frameworks/sproutcore/foundation/set.js +3 -3
  74. data/frameworks/sproutcore/foundation/string.js +5 -3
  75. data/frameworks/sproutcore/foundation/timer.js +371 -0
  76. data/frameworks/sproutcore/foundation/undo_manager.js +1 -1
  77. data/frameworks/sproutcore/foundation/unittest.js +3 -3
  78. data/frameworks/sproutcore/foundation/utils.js +161 -2
  79. data/frameworks/sproutcore/globals/panels.js +1 -1
  80. data/frameworks/sproutcore/globals/popups.js +4 -3
  81. data/frameworks/sproutcore/globals/window.js +44 -4
  82. data/frameworks/sproutcore/lib/button_views.rb +328 -0
  83. data/frameworks/sproutcore/lib/collection_view.rb +80 -0
  84. data/frameworks/sproutcore/lib/core_views.rb +281 -0
  85. data/frameworks/sproutcore/lib/form_views.rb +253 -0
  86. data/frameworks/sproutcore/lib/index.rhtml +2 -0
  87. data/frameworks/sproutcore/lib/menu_views.rb +88 -0
  88. data/frameworks/sproutcore/{foundation → mixins}/array.js +60 -29
  89. data/frameworks/sproutcore/mixins/control.js +265 -0
  90. data/frameworks/sproutcore/mixins/delegate_support.js +66 -0
  91. data/frameworks/sproutcore/{foundation → mixins}/observable.js +176 -6
  92. data/frameworks/sproutcore/mixins/scrollable.js +245 -0
  93. data/frameworks/sproutcore/mixins/selection_support.js +148 -0
  94. data/frameworks/sproutcore/mixins/validatable.js +152 -0
  95. data/frameworks/sproutcore/models/collection.js +5 -5
  96. data/frameworks/sproutcore/models/record.js +1 -1
  97. data/frameworks/sproutcore/models/store.js +1 -1
  98. data/frameworks/sproutcore/panes/dialog.js +1 -1
  99. data/frameworks/sproutcore/panes/manager.js +1 -1
  100. data/frameworks/sproutcore/panes/menu.js +1 -1
  101. data/frameworks/sproutcore/panes/overlay.js +2 -2
  102. data/frameworks/sproutcore/panes/panel.js +1 -1
  103. data/frameworks/sproutcore/panes/picker.js +1 -1
  104. data/frameworks/sproutcore/tests/controllers/array.rhtml +44 -4
  105. data/frameworks/sproutcore/tests/foundation/timer/invalidate.rhtml +33 -0
  106. data/frameworks/sproutcore/tests/foundation/timer/invokeLater.rhtml +145 -0
  107. data/frameworks/sproutcore/tests/foundation/timer/isPaused.rhtml +70 -0
  108. data/frameworks/sproutcore/tests/foundation/timer/schedule.rhtml +145 -0
  109. data/frameworks/sproutcore/tests/views/{scroll.rhtml → checkbox.rhtml} +3 -3
  110. data/frameworks/sproutcore/tests/views/{collection.rhtml → collection/base.rhtml} +33 -32
  111. data/frameworks/sproutcore/tests/views/collection/incremental_rendering.rhtml +260 -0
  112. data/frameworks/sproutcore/tests/views/image_cell.rhtml +19 -0
  113. data/frameworks/sproutcore/tests/views/label_item.rhtml +2 -4
  114. data/frameworks/sproutcore/tests/views/list.rhtml +2 -3
  115. data/frameworks/sproutcore/tests/views/list_item.rhtml +20 -0
  116. data/frameworks/sproutcore/tests/views/slider.rhtml +20 -0
  117. data/frameworks/sproutcore/tests/views/text_cell.rhtml +19 -0
  118. data/frameworks/sproutcore/tests/views/view/clippingFrame.rhtml +395 -0
  119. data/frameworks/sproutcore/tests/views/view/frame.rhtml +353 -0
  120. data/frameworks/sproutcore/tests/views/view/innerFrame.rhtml +347 -0
  121. data/frameworks/sproutcore/tests/views/view/isVisibleInWindow.rhtml +148 -0
  122. data/frameworks/sproutcore/tests/views/view/scrollFrame.rhtml +468 -0
  123. data/frameworks/sproutcore/validators/credit_card.js +33 -13
  124. data/frameworks/sproutcore/validators/date.js +26 -6
  125. data/frameworks/sproutcore/validators/email.js +21 -3
  126. data/frameworks/sproutcore/validators/not_empty.js +11 -1
  127. data/frameworks/sproutcore/validators/number.js +18 -4
  128. data/frameworks/sproutcore/validators/password.js +12 -1
  129. data/frameworks/sproutcore/validators/validator.js +204 -194
  130. data/frameworks/sproutcore/views/{button.js → button/button.js} +96 -94
  131. data/frameworks/sproutcore/views/button/checkbox.js +29 -0
  132. data/frameworks/sproutcore/views/button/disclosure.js +42 -0
  133. data/frameworks/sproutcore/views/button/radio.js +29 -0
  134. data/frameworks/sproutcore/views/{collection.js → collection/collection.js} +1373 -1024
  135. data/frameworks/sproutcore/views/collection/grid.js +124 -46
  136. data/frameworks/sproutcore/views/collection/image_cell.js +17 -46
  137. data/frameworks/sproutcore/views/collection/list.js +45 -35
  138. data/frameworks/sproutcore/views/collection/source_list.js +386 -0
  139. data/frameworks/sproutcore/views/collection/table.js +118 -0
  140. data/frameworks/sproutcore/views/container.js +7 -2
  141. data/frameworks/sproutcore/views/error_explanation.js +23 -10
  142. data/frameworks/sproutcore/views/{checkbox_field.js → field/checkbox_field.js} +16 -6
  143. data/frameworks/sproutcore/views/field/field.js +219 -0
  144. data/frameworks/sproutcore/views/{radio_field.js → field/radio_field.js} +27 -12
  145. data/frameworks/sproutcore/views/{select_field.js → field/select_field.js} +116 -90
  146. data/frameworks/sproutcore/views/{text_field.js → field/text_field.js} +57 -8
  147. data/frameworks/sproutcore/views/{textarea_field.js → field/textarea_field.js} +13 -3
  148. data/frameworks/sproutcore/views/filter_button.js +2 -2
  149. data/frameworks/sproutcore/views/form.js +3 -3
  150. data/frameworks/sproutcore/views/image.js +128 -21
  151. data/frameworks/sproutcore/views/inline_text_editor.js +1 -1
  152. data/frameworks/sproutcore/views/label.js +149 -92
  153. data/frameworks/sproutcore/views/list_item.js +225 -0
  154. data/frameworks/sproutcore/views/menu_item.js +10 -4
  155. data/frameworks/sproutcore/views/pagination.js +11 -4
  156. data/frameworks/sproutcore/views/popup_button.js +25 -21
  157. data/frameworks/sproutcore/views/popup_menu.js +10 -4
  158. data/frameworks/sproutcore/views/progress.js +29 -16
  159. data/frameworks/sproutcore/views/radio_group.js +1 -1
  160. data/frameworks/sproutcore/views/scroll.js +60 -20
  161. data/frameworks/sproutcore/views/segmented.js +1 -1
  162. data/frameworks/sproutcore/views/slider.js +132 -0
  163. data/frameworks/sproutcore/views/source_list_group.js +130 -0
  164. data/frameworks/sproutcore/views/spinner.js +1 -1
  165. data/frameworks/sproutcore/views/split.js +292 -0
  166. data/frameworks/sproutcore/views/split_divider.js +109 -0
  167. data/frameworks/sproutcore/views/tab.js +1 -1
  168. data/frameworks/sproutcore/views/toolbar.js +1 -1
  169. data/frameworks/sproutcore/views/view.js +1272 -591
  170. data/generators/client/templates/english.lproj/body.css +1 -1
  171. data/generators/controller/controller_generator.rb +1 -1
  172. data/generators/controller/templates/test.rhtml +2 -1
  173. data/generators/model/templates/test.rhtml +1 -1
  174. data/generators/test/templates/test.rhtml +1 -1
  175. data/generators/view/templates/test.rhtml +1 -1
  176. data/jsdoc/templates/sproutcore/class.tmpl +241 -338
  177. data/jsdoc/templates/sproutcore/default.css +105 -155
  178. data/jsdoc/templates/sproutcore/index.tmpl +43 -8
  179. data/jsdoc/templates/sproutcore/publish.js +9 -4
  180. data/lib/sproutcore/build_tools/html_builder.rb +29 -13
  181. data/lib/sproutcore/build_tools/resource_builder.rb +1 -1
  182. data/lib/sproutcore/bundle.rb +86 -25
  183. data/lib/sproutcore/jsdoc.rb +2 -0
  184. data/lib/sproutcore/version.rb +1 -1
  185. data/lib/sproutcore/view_helpers.rb +36 -3
  186. data/tasks/deployment.rake +1 -1
  187. metadata +69 -36
  188. data/clients/sc_docs/english.lproj/icons/small/next.png +0 -0
  189. data/clients/sc_docs/english.lproj/icons/small/reset.png +0 -0
  190. data/clients/sc_docs/english.lproj/images/gradients.png +0 -0
  191. data/clients/sc_docs/english.lproj/images/toolbar.png +0 -0
  192. data/clients/sc_docs/english.lproj/warning.rhtml +0 -6
  193. data/clients/sc_test_runner/english.lproj/warning.rhtml +0 -6
  194. data/frameworks/sproutcore/english.lproj/buttons.png +0 -0
  195. data/frameworks/sproutcore/english.lproj/collections.css +0 -82
  196. data/frameworks/sproutcore/english.lproj/images/buttons-sprite.png +0 -0
  197. data/frameworks/sproutcore/views/collection/collection_item.js +0 -36
  198. data/frameworks/sproutcore/views/collection/text_cell.js +0 -128
  199. data/frameworks/sproutcore/views/field.js +0 -214
  200. data/frameworks/sproutcore/views/workspace.js +0 -170
  201. data/generators/client/templates/english.lproj/controls.css +0 -0
  202. data/generators/framework/templates/english.lproj/body.css +0 -0
  203. data/generators/framework/templates/english.lproj/body.rhtml +0 -3
  204. data/generators/framework/templates/english.lproj/controls.css +0 -0
  205. data/lib/sproutcore/view_helpers/button_views.rb +0 -302
  206. data/lib/sproutcore/view_helpers/core_views.rb +0 -292
  207. data/lib/sproutcore/view_helpers/form_views.rb +0 -258
  208. data/lib/sproutcore/view_helpers/menu_views.rb +0 -94
@@ -1,17 +1,17 @@
1
1
  // ========================================================================
2
2
  // SproutCore
3
- // copyright 2006-2007 Sprout Systems, Inc.
3
+ // copyright 2006-2008 Sprout Systems, Inc.
4
4
  // ========================================================================
5
5
 
6
6
  require('views/view') ;
7
7
  require('views/label') ;
8
+ require('mixins/control') ;
8
9
 
9
10
  // Constants
10
11
  SC.TOGGLE_BEHAVIOR = 'toggle';
11
12
  SC.PUSH_BEHAVIOR = 'push';
12
13
  SC.TOGGLE_ON_BEHAVIOR = "on";
13
14
  SC.TOGGLE_OFF_BEHAVIOR = "off" ;
14
- SC.MIXED_STATE = '__MIXED__' ;
15
15
 
16
16
  /** @class
17
17
 
@@ -19,12 +19,15 @@ SC.MIXED_STATE = '__MIXED__' ;
19
19
  enabled or disabled state.
20
20
 
21
21
  @extends SC.View
22
+ @extends SC.Control
23
+ @author Charles Jolley
24
+ @version 1.0
25
+
22
26
  */
23
- SC.ButtonView = SC.View.extend(
24
- /** @scope SC.ButtonView.prototype */
25
- {
27
+ SC.ButtonView = SC.View.extend(SC.Control,
28
+ /** @scope SC.ButtonView.prototype */ {
26
29
 
27
- emptyElement: '<a href="javascript:;" class="regular"><span class="button-inner"><span class="label"></span></span></a>',
30
+ emptyElement: '<a href="javascript:;" class="sc-button-view regular"><span class="button-inner"><span class="label"></span></span></a>',
28
31
 
29
32
  // PROPERTIES
30
33
 
@@ -81,8 +84,7 @@ SC.ButtonView = SC.View.extend(
81
84
  should set a class name on the HTML with the same value to allow CSS
82
85
  styling.
83
86
 
84
- The default SproutCore theme supports "regular", "back", "checkbox", and
85
- "radio"
87
+ The default SproutCore theme supports "regular", "checkbox", and "radio"
86
88
  */
87
89
  theme: 'regular',
88
90
 
@@ -113,71 +115,75 @@ SC.ButtonView = SC.View.extend(
113
115
  buttonBehavior: SC.PUSH_BEHAVIOR,
114
116
 
115
117
  /**
116
- set to false to disable the button. clicks will be ignored.
118
+ If NO the button will be disabled.
117
119
 
118
- @type bool
120
+ @type Bool
119
121
  */
120
- isEnabled: true,
121
- isEnabledBindingDefault: SC.Binding.OneWayBool,
122
+ isEnabled: YES,
122
123
 
123
124
  /**
124
- this is the buttons selection state. Returns true, false or SC.MIXED_STATE.
125
+ button's selection state. Returns YES, NO, or SC.MIXED_STATE
125
126
  */
126
- isSelected: false,
127
- isSelectedBindingDefault: SC.Binding.OneWayBool,
127
+ isSelected: NO,
128
128
 
129
129
  /**
130
- if set to true, then this button will be triggered when you hit return
131
- while focused on a form view. This will also apply the 'def' class name
132
- to the button.
130
+ If YES, then this button will be triggered when you hit return.
131
+
132
+ This is the same as setting the keyEquivalent to 'return'. This will also
133
+ apply the "def" classname to the button.
133
134
  */
134
- isDefault: false,
135
+ isDefault: NO,
135
136
  isDefaultBindingDefault: SC.Binding.OneWayBool,
136
137
 
137
138
  /**
138
- is set to true, then this button will be triggered when you hit escape
139
- inside a pane.
139
+ If YES, then this button will be triggered when you hit escape.
140
+
141
+ This is the same as setting the keyEquivalent to 'escape'.
140
142
  */
141
- isCancel: false,
143
+ isCancel: NO,
142
144
  isCancelBindingDefault: SC.Binding.OneWayBool,
143
145
 
144
146
  /**
145
- set to true if you want the internal element with the class name
146
- 'label' to be localized on init.
147
+ If YES, then the title will be localized.
148
+ */
149
+ localize: NO,
150
+
151
+ /**
152
+ The selector path to the element that contains the button title.
153
+
154
+ This property is only used if you try to get or set the title property.
147
155
  */
148
- localize: false,
156
+ titleSelector: '.label',
149
157
 
150
158
  /**
151
- this property can be used to edit the contents of the label element,
152
- (if there is one).
159
+ The button title.
160
+
161
+ This property is observable and bindable.
162
+
163
+ @field {String}
153
164
  */
154
- labelText: function(key, value) {
165
+ title: function(key, value) {
166
+
155
167
  // set the value of the label text. Possibly localize and set innerHTML.
156
168
  if (value !== undefined) {
157
- if (this._labelText != value) {
158
- var text = this._labelText = value ;
159
- var lsel = this.get('labelSelector') ;
169
+ if (this._title != value) {
170
+ var text = this._title = value ;
171
+ var lsel = this.get('titleSelector') ;
160
172
  var el = (lsel) ? this.$sel(lsel) : this.rootElement ;
161
173
 
162
174
  if (this.get('localize')) text = text.loc() ;
163
- if (el) Element.update(el, text) ;
164
- }
165
-
166
- // lazily fetch the label text. This only happens if localization is
167
- // turned off.
168
- if (!this._labelText) {
169
- var el = this.$sel(this.labelSelector) ;
170
- this._labelText = (el) ? el.innerHTML : '' ;
175
+ el.innerHTML = text ;
171
176
  }
172
- return this._labelText ;
173
177
  }
174
-
175
- // return value.
176
- return this._labelText ;
178
+
179
+ // lazily fetch the label text.
180
+ if (!this._title) {
181
+ var el = this.$sel(this.get('titleSelector')) ;
182
+ this._title = (el) ? el.innerHTML : '' ;
183
+ }
184
+ return this._title ;
177
185
  }.property(),
178
186
 
179
- labelSelector: '.label',
180
-
181
187
  /**
182
188
  The name of the action you want triggered when the button is pressed.
183
189
 
@@ -244,8 +250,7 @@ SC.ButtonView = SC.View.extend(
244
250
  this.setClassName('active', true);
245
251
  this.didTriggerAction();
246
252
  this._action(evt);
247
- var view = this;
248
- setTimeout(function() { view.setClassName('active', false); }, 200);
253
+ this.invokeLater('setClassName', 200, 'active', false) ;
249
254
  return true;
250
255
  },
251
256
 
@@ -257,16 +262,20 @@ SC.ButtonView = SC.View.extend(
257
262
  /** @private */
258
263
  init: function() {
259
264
  arguments.callee.base.call(this) ;
260
- this._updateClassForState() ;
261
265
 
266
+ // setup initial CSS clases
267
+ this._isDefaultOrCancelObserver() ;
268
+
269
+ // If we need to localze, handle it...
262
270
  var el ;
263
- var lsel = this.get('labelSelector') ;
264
- if (this.get('localize') && (el = (lsel) ? this.$sel(lsel) : this.rootElement)) {
265
- this._labelText = el.innerHTML.strip() ;
266
- Element.update(el, this._labelText.loc()) ;
271
+ var sel = this.get('titleSelector') ;
272
+ if (this.get('localize') && sel && (el = this.$sel(sel))) {
273
+ this._title = (el.innerHTML || '').strip() ;
274
+ el.innerHTML = this._title.loc() ;
267
275
  }
268
276
  },
269
277
 
278
+ // determines the target selected state
270
279
  _selectedStateFromValue: function(value) {
271
280
  var targetValue = this.get('toggleOnValue') ;
272
281
  var state ;
@@ -275,7 +284,7 @@ SC.ButtonView = SC.View.extend(
275
284
  if (value.length == 1) {
276
285
  state = (value[0] == targetValue) ;
277
286
  } else {
278
- state = (value.include(targetValue)) ? SC.MIXED_STATE : false ;
287
+ state = (value.indexOf(targetValue) >= 0) ? SC.MIXED_STATE : false ;
279
288
  }
280
289
  } else {
281
290
  state = (value == targetValue) ;
@@ -288,53 +297,46 @@ SC.ButtonView = SC.View.extend(
288
297
  if (target != this) return ;
289
298
 
290
299
  // handle changes to the value
291
- if (key == 'value') {
300
+ switch(key) {
301
+
292
302
  // determine the new selection state.
293
- value = this.get('value') ;
294
- if (value == this._value) return ; // process value one time.
295
- this._value = value ;
296
-
297
- // if the new selected state does not match the computed value, set it.
298
- var state = this._selectedStateFromValue(value) ;
299
- if (!(this.get('isSelected') == state)) {
300
- this.set('isSelected', state) ;
301
- this._updateClassForState() ;
302
- }
303
-
304
- // handle changes to the selected state
305
- // forward to value if needed...
306
- } else if (key == "isSelected") {
307
- var newState = this.get('isSelected') ;
308
- var curState = this._selectedStateFromValue(this.get('value')) ;
309
- if (curState != newState) {
310
- var valueKey = (newState) ? 'toggleOnValue' : 'toggleOffValue' ;
311
- this.set('value', this.get(valueKey)) ;
312
- }
313
- this._updateClassForState() ;
303
+ case 'value':
304
+ value = this.get('value') ;
305
+ if (value == this._value) return ; // process value one time.
306
+ this._value = value ;
314
307
 
315
- // otherwise, handle changes to the isEnabled or isDefault states...
316
- } else if ((key == 'isEnabled') || (key == 'isDefault')) {
317
- this._updateClassForState() ;
308
+ // set the new selected state if it does not match
309
+ var state = this._selectedStateFromValue(value) ;
310
+ this.setIfChanged('isSelected', state) ;
311
+ break ;
312
+
313
+ // forward to value if needed.
314
+ case 'isSelected':
315
+ var newState = this.get('isSelected') ;
316
+ var curState = this._selectedStateFromValue(this.get('value')) ;
317
+ if (curState != newState) {
318
+ var valueKey = (newState) ? 'toggleOnValue' : 'toggleOffValue' ;
319
+ this.set('value', this.get(valueKey)) ;
320
+ }
321
+ break ;
322
+
323
+ // otherwise do nothing
324
+ default:
325
+ break ;
318
326
  }
319
327
  },
320
328
 
321
- _updateClassForState: function() {
322
- var enabled = !!this.get('isEnabled') ; // force to bool.
323
- var tagName = this.rootElement.tagName.toLowerCase() ;
324
- if (tagName == "button") {
325
- this.rootElement.disabled = !enabled ;
326
- }
327
-
328
- this.setClassName('disabled', !enabled) ;
329
- this.setClassName('def', this.get('isDefault')) ;
329
+ _isDefaultOrCancelObserver: function() {
330
+ var isDef = !!this.get('isDefault') ;
331
+ var isCancel = !isDef && this.get('isCancel') ;
332
+
333
+ this.setClassName('def', isDef) ;
334
+
335
+ var key = this.get('keyEquivalent') ;
336
+ if (isDef && key != 'return') this.set('keyEquivalent', 'return') ;
337
+ if (isCancel && key != 'escape') this.set('keyEquivalent', 'escape') ;
338
+ }.observes('isDefault', 'isCancel'),
330
339
 
331
- // handle selected state.
332
- var sel =this.get('isSelected') ;
333
- var mixed = (sel == SC.MIXED_STATE) ;
334
- this.setClassName('mixed', mixed) ;
335
- this.setClassName('sel', ((mixed) ? false : sel)) ;
336
- },
337
-
338
340
  // on mouse down, set active only if enabled.
339
341
  /** @private */
340
342
  mouseDown: function(evt) {
@@ -0,0 +1,29 @@
1
+ // ==========================================================================
2
+ // SC.CheckboxView
3
+ // ==========================================================================
4
+
5
+ require('views/button/button');
6
+
7
+ /** @class
8
+
9
+ Renders a checkbox button view specifically.
10
+
11
+ This view is basically a button view preconfigured to generate the correct
12
+ HTML and to set to use a TOGGLE_BEHAVIOR for its buttons.
13
+
14
+ This view renders a simulated checkbox that can display a mixed state and
15
+ has other features not found in platform-native controls. If you want to
16
+ use the platform native version instead, see SC.CheckboxFieldView.
17
+
18
+ @extends SC.ButtonView
19
+ @author Charles Jolley
20
+ @version 1.0
21
+ */
22
+ SC.CheckboxView = SC.ButtonView.extend(
23
+ /** @scope SC.CheckboxView.prototype */ {
24
+
25
+ emptyElement: '<a href="javascript:;" class="sc-checkbox-view sc-button-view button checkbox"><img src="%@" class="button" /><span class="label"></span></a>'.fmt(static_url('blank')),
26
+
27
+ buttonBehavior: SC.TOGGLE_BEHAVIOR
28
+
29
+ }) ;
@@ -0,0 +1,42 @@
1
+ // ==========================================================================
2
+ // SC.CheckboxView
3
+ // ==========================================================================
4
+
5
+ require('views/button/button');
6
+
7
+ /** @class
8
+
9
+ Disclosure triangle button.
10
+
11
+ @extends SC.ButtonView
12
+ @author Charles Jolley
13
+ @version 1.0
14
+ */
15
+ SC.DisclosureView = SC.ButtonView.extend(
16
+ /** @scope SC.DisclosureView.prototype */ {
17
+
18
+ emptyElement: '<a href="javascript:;" class="sc-disclosure-view sc-button-view button disclosure"><img src="%@" class="button" /><span class="label"></span></a>'.fmt(static_url('blank')),
19
+
20
+ buttonBehavior: SC.TOGGLE_BEHAVIOR,
21
+
22
+ /**
23
+ This is the value that will be set when the disclosure triangle is toggled
24
+ open.
25
+ */
26
+ toggleOnValue: YES,
27
+
28
+ /**
29
+ The value that will be set when the disclosure triangle is toggled closed.
30
+ */
31
+ toggleOffValue: NO,
32
+
33
+ valueBindingDefault: SC.Binding.Bool,
34
+
35
+ init: function() {
36
+ arguments.callee.base.apply(this,arguments) ;
37
+ if (this.get('value') == this.get('toggleOnValue')) {
38
+ this.set('isSelected', true) ;
39
+ }
40
+ }
41
+
42
+ }) ;
@@ -0,0 +1,29 @@
1
+ // ==========================================================================
2
+ // SC.RadioView
3
+ // ==========================================================================
4
+
5
+ require('views/button/button');
6
+
7
+ /** @class
8
+
9
+ Renders a radio button view.
10
+
11
+ This view is basically a button view preconfigured to generate the correct
12
+ HTML and to set to use a TOGGLE_ON_BEHAVIOR.
13
+
14
+ This view renders a simulated checkbox that can display a mixed state and
15
+ has other features not found in platform-native controls. If you want to
16
+ use the platform native version instead, see SC.RadioFieldView.
17
+
18
+ @extends SC.ButtonView
19
+ @author Charles Jolley
20
+ @version 1.0
21
+ */
22
+ SC.RadioView = SC.ButtonView.extend(
23
+ /** @scope SC.RadioView.prototype */ {
24
+
25
+ emptyElement: '<a href="javascript:;" class="sc-radio-view sc-button-view button radio"><img src="%@" class="button" /><span class="label"></span></a>'.fmt(static_url('blank')),
26
+
27
+ buttonBehavior: SC.TOGGLE_ON_BEHAVIOR
28
+
29
+ }) ;
@@ -1,11 +1,14 @@
1
1
  // ========================================================================
2
2
  // SproutCore
3
- // copyright 2006-2007 Sprout Systems, Inc.
3
+ // copyright 2006-2008 Sprout Systems, Inc.
4
4
  // ========================================================================
5
5
 
6
6
  require('views/view') ;
7
7
  require('views/label') ;
8
8
 
9
+ SC.BENCHMARK_UPDATE_CHILDREN = NO ;
10
+ SC.VALIDATE_COLLECTION_CONSISTANCY = NO ;
11
+
9
12
  /** Indicates that selection points should be selected using horizontal
10
13
  orientation.
11
14
  */
@@ -14,6 +17,14 @@ SC.HORIZONTAL_ORIENTATION = 'horizontal';
14
17
  /** Selection points should be selected using vertical orientation. */
15
18
  SC.VERTICAL_ORIENTATION = 'vertical' ;
16
19
 
20
+ /** Enables an optimization using zombie group views. This option is configurable for perf testing purposes. You should not change it. */
21
+ SC.ZOMBIE_GROUPS_ENABLED = YES ;
22
+
23
+ /** Enables an optimization that removes the root element from the DOM during
24
+ a render and then readds it when complete. This option is configurable for
25
+ perf testing purposes. You should not change it. */
26
+ SC.REMOVE_COLLECTION_ROOT_ELEMENT_DURING_RENDER = NO ;
27
+
17
28
  /**
18
29
  @class
19
30
 
@@ -31,17 +42,6 @@ SC.VERTICAL_ORIENTATION = 'vertical' ;
31
42
  property if you want to monitor selection. (be sure to set the isEnabled
32
43
  property to allow selection.)
33
44
 
34
- h4. INCREMENTAL RENDERING
35
-
36
- incremental rendering can be used in certain collection views to
37
- display only the visible views in your collection. This will yield
38
- dramatically improved performance over the typical full-rendering
39
- facility.
40
-
41
- to activate incremental rendering you need to override the two methods
42
- below to return valid values and also implement layoutChildViewsFor()
43
- above.
44
-
45
45
  @extends SC.View
46
46
  */
47
47
  SC.CollectionView = SC.View.extend(
@@ -57,8 +57,8 @@ SC.CollectionView = SC.View.extend(
57
57
 
58
58
  This array should contain the content objects you want the collection view
59
59
  to display. An item view (based on the exampleView view class) will be
60
- created for each content object, in the order the content objects appear in
61
- this array.
60
+ created for each content object, in the order the content objects appear
61
+ in this array.
62
62
 
63
63
  If you make the collection editable, the collection view will also modify
64
64
  this array using the observable array methods of SC.Array.
@@ -76,20 +76,20 @@ SC.CollectionView = SC.View.extend(
76
76
  /**
77
77
  The array of currently selected objects.
78
78
 
79
- This array should contain the currently selected content objects.
80
- It is modified automatically by the collection view when the user
81
- changes the selection on the collection.
79
+ This array should contain the currently selected content objects. It is
80
+ modified automatically by the collection view when the user changes the
81
+ selection on the collection.
82
82
 
83
- Any item views representing content objects in this array will
84
- have their isSelected property set to YES automatically.
83
+ Any item views representing content objects in this array will have their
84
+ isSelected property set to YES automatically.
85
85
 
86
- The CollectionView can deal with selection arrays that contain content
87
- objects that do not belong to the content array itself. Sometimes this
88
- will happen if you share the same selection across multiple collection
86
+ The CollectionView can deal with selection arrays that contain content
87
+ objects that do not belong to the content array itself. Sometimes this
88
+ will happen if you share the same selection across multiple collection
89
89
  views.
90
90
 
91
- Usually you will want to bind this property to a controller property
92
- that actually manages the selection for your display.
91
+ Usually you will want to bind this property to a controller property that
92
+ actually manages the selection for your display.
93
93
 
94
94
  @type Array
95
95
  */
@@ -105,7 +105,7 @@ SC.CollectionView = SC.View.extend(
105
105
  If you have items in your selection property, they will still be reflected
106
106
  visually.
107
107
 
108
- @type Boolean
108
+ @type {Bool}
109
109
  */
110
110
  isSelectable: true,
111
111
 
@@ -120,7 +120,7 @@ SC.CollectionView = SC.View.extend(
120
120
  the collection view will also be not selectable or editable, regardless of the
121
121
  settings for isEditable & isSelectable.
122
122
 
123
- @type Boolean
123
+ @type {Bool}
124
124
  */
125
125
  isEnabled: true,
126
126
 
@@ -182,30 +182,6 @@ SC.CollectionView = SC.View.extend(
182
182
  */
183
183
  useToggleSelection: false,
184
184
 
185
- /**
186
- Delete views when the content object is removed from the content array.
187
-
188
- Whenever you remove a content object from the content array, the collection view
189
- will automatically remove the corresponding item view from the display. If this
190
- property is set to true, that view will be subsequently deleted as well.
191
-
192
- If you set this property to false, then the collection view will store these
193
- unused views in a cache and reuse them later should the content object they
194
- represent reappear in the content array.
195
-
196
- In general, you want to leave this property to true in order to keep your
197
- memory usage under control. However, if you are rendering a collection of
198
- views that will change often, adding and removing the same content objects,
199
- then your collection view will be much faster if you set this to false.
200
-
201
- Most of the time, you will set this to false if you are rendering a collection
202
- of objects that may be filtered based on search criteria and you want to update
203
- the display very quickly.
204
-
205
- @type Boolean
206
- */
207
- flushUnusedViews: true,
208
-
209
185
  /**
210
186
  Trigger the action method on a single click.
211
187
 
@@ -335,7 +311,7 @@ SC.CollectionView = SC.View.extend(
335
311
 
336
312
  You can also set this to true yourself to be notified when it is completed.
337
313
  */
338
- isDirty: true,
314
+ isDirty: false,
339
315
 
340
316
  /**
341
317
  The maximum time the collection view will spend updating its
@@ -349,28 +325,23 @@ SC.CollectionView = SC.View.extend(
349
325
  */
350
326
  maxRenderTime: 0,
351
327
 
352
- /**
353
- Property returns all of the item views, regardless of group view.
354
-
355
- @property
356
- @returns {Array} the item views.
357
- */
358
- itemViews: function() {
359
- var ret = [] ;
360
- if (!this._itemViews) return ret ;
361
- for(var key in this._itemViews) {
362
- if (this._itemViews.hasOwnProperty(key)) ret.push(this._itemViews[key]);
363
- }
364
- return ret;
365
- }.property(),
366
-
367
328
  /**
368
- The property on content objects item views should display.
329
+ Property to on content items to use for display.
330
+
331
+ Built-in item views such as the LabelViews and ImageViews will use the
332
+ value of this property as a key on the content object to determine the
333
+ value they should display.
334
+
335
+ For example, if you set contentValueKey to 'name' and set the
336
+ exampleView to an SC.LabelView, then the label views created by the
337
+ colleciton view will display the value of the content.name.
369
338
 
370
- Most built-in item views will respect this property. You can also use it when writing
371
- you own item views.
339
+ If you are writing your own custom item view for a collection, you can
340
+ get this behavior automatically by including the SC.Control mixin on your
341
+ view. You can also ignore this property if you like. The collection view
342
+ itself does not use this property to impact rendering.
372
343
  */
373
- displayProperty: null,
344
+ contentValueKey: null,
374
345
 
375
346
  /**
376
347
  Enables keyboard-based navigate if set to true.
@@ -388,6 +359,54 @@ SC.CollectionView = SC.View.extend(
388
359
  to edit this property.
389
360
  */
390
361
  itemsPerRow: 1,
362
+
363
+ /**
364
+ Property returns all of the item views, regardless of group view.
365
+
366
+ @field
367
+ @returns {Array} the item views.
368
+ */
369
+ itemViews: function() {
370
+ if (!this._itemViews) {
371
+
372
+
373
+ var range = this.get('nowShowingRange') ;
374
+ var content = this.get('content') || [] ;
375
+ this._itemViews = [] ;
376
+ for(var idx=0;idx<range.length;idx++) {
377
+ var cur = content.objectAt(idx) ;
378
+ this._itemViews.push(this.itemViewForContent(cur)) ;
379
+ }
380
+ }
381
+ return this._itemViews;
382
+ }.property(),
383
+
384
+ /**
385
+ Property returns all of the rendered group views in order of their
386
+ appearance with the content.
387
+ */
388
+ groupViews: function() {
389
+ if (!this._groupViews) {
390
+ var groupBy = this.get('groupBy') ;
391
+ if (groupBy) {
392
+ var range = this.get('nowShowingRange') ;
393
+ var content = this.get('content') || [] ;
394
+ var groupValue = undefined ;
395
+ this._groupViews = [] ;
396
+
397
+ for(var idx=0;idx<range.length;idx++) {
398
+ var cur = content.objectAt(idx) ;
399
+ var curGroupValue = (cur) ? cur.get(groupBy) : null ;
400
+ if (curGroupValue != groupValue) {
401
+ groupValue = curGroupValue ;
402
+ this._groupViews.push(this.groupViewForGroupValue(groupValue)) ;
403
+ }
404
+ }
405
+
406
+ }
407
+ }
408
+ return this._groupViews;
409
+ }.property(),
391
410
 
392
411
  /**
393
412
  Returns true if the passed view belongs to the collection.
@@ -401,624 +420,355 @@ SC.CollectionView = SC.View.extend(
401
420
  @returns {Boolean} True if the view is an item view in the receiver.
402
421
  */
403
422
  hasItemView: function(view) {
404
- if (!this._itemViews) this._itemViews = {};
405
- return !!this._itemViews[SC.getGUID(view)];
423
+ if (!this._itemViewsByGuid) this._itemViewsByGuid = {} ;
424
+ return !!this._itemViewsByGuid[SC.guidFor(view)] ;
406
425
  },
407
426
 
408
- // ......................................
409
- // FIRST RESPONDER
410
- //
411
-
412
- /**
413
- Called whenever the collection becomes first responder.
414
- Adds the focused class to the element.
427
+ /**
428
+ Find the item view underneath the passed mouse location.
429
+
430
+ The default implementation of this method simply searches each item view's
431
+ frame to find one that includes the location. If you are doing your own
432
+ layout, you may be able to perform this calculation more quickly. If so,
433
+ consider overriding this method for better performance during drag
434
+ operations.
435
+
436
+ @param {Point} loc The current mouse location in the coordinate of the
437
+ collection view
438
+
439
+ @returns {SC.View} The item view under the collection
415
440
  */
416
- didBecomeFirstResponder: function() {
417
- this.addClassName('focus') ;
418
- },
419
-
420
- willLoseFirstResponder: function() {
421
- this.removeClassName('focus');
441
+ itemViewAtLocation: function(loc) {
442
+ var itemView = this._itemViewRoot ;
443
+ while(itemView) {
444
+ var frame = itemView.get('frame');
445
+ if (SC.pointInRect(loc, frame)) return itemView ;
446
+ }
447
+ return null; // not in an itemView right now.
422
448
  },
423
449
 
424
- // ......................................
425
- // DRAG AND DROP SUPPORT
426
- //
427
450
 
451
+
428
452
  /**
429
- The insertion orientation. This is used to determine which
430
- dimension we should pay attention to when determining insertion point for
431
- a mouse click.
453
+ Find the first content item view for the passed event.
432
454
 
433
- {{{
434
- SC.HORIZONTAL_ORIENTATION: look at the X dimension only
435
- SC.VERTICAL_ORIENTATION: look at the Y dimension only
436
- }}}
437
- */
438
- insertionOrientation: SC.HORIZONTAL_ORIENTATION,
439
-
440
- /**
441
- Get the preferred insertion point for the given location, including
442
- an insertion preference of before or after the named index.
455
+ This method will go up the view chain, starting with the view that was the
456
+ target of the passed event, looking for a child item. This will become
457
+ the view that is selected by the mouse event.
458
+
459
+ This method only works for mouseDown & mouseUp events. mouseMoved events
460
+ do not have a target.
461
+
462
+ @param {Event} evt An event
443
463
 
444
- The default implementation will loop through the item views looking for
445
- the first view to "switch sides" in the orientation you specify.
446
464
  */
447
- insertionIndexForLocation: function(loc) {
448
- var content = this.get('content') ;
449
- var f, itemView, curSide, lastSide = null ;
450
- var orient = this.get('insertionOrientation') ;
451
- var ret= null ;
452
- for(var idx=0; ((ret == null) && (idx<content.length)); idx++) {
453
- itemView = this.itemViewForContent(content.objectAt(idx));
454
- f = this.convertFrameFromView(itemView.get('frame'), itemView) ;
455
-
456
- // if we are a horizontal orientation, look for the first item that
457
- // will "switch sides" on the x path an the maxY is greater than Y.
458
- // This assumes you will flow top to bottom, but it should work if you
459
- // flow LTR or RTL.
460
- if (orient == SC.HORIZONTAL_ORIENTATION) {
461
- if (SC.maxY(f) > loc.y) {
462
- curSide = (SC.maxX(f) < loc.x) ? -1 : 1 ;
463
- } else curSide = null ;
464
-
465
- // if we are a vertical orientation, look for the first item that
466
- // will "swithc sides" on the y path and the maxX is greater than X.
467
- // This assumes you will flow LTR, but it should work if you flow
468
- // bottom to top or top to bottom.
469
- } else {
470
- if (SC.minX(f) < loc.x) {
471
- curSide = (SC.maxY(f) < loc.y) ? -1 : 1 ;
472
- } else curSide = null ;
473
- }
465
+ itemViewForEvent: function(evt)
466
+ {
467
+ var view = SC.window.firstViewForEvent( evt );
468
+ // work up the view hierarchy to find a match...
469
+ do {
470
+ // item clicked was the ContainerView itself... i.e. the user clicked outside the child items
471
+ // nothing to return...
472
+ if ( view == this ) return null;
474
473
 
475
- // if we "switched" sides then return this item view.
476
- if (curSide !== null) {
477
-
478
- // OK, we found an item view, while we have this data, decide if
479
- // we should insert before or after the view
480
- if ((lastSide !== null) && (curSide != lastSide)) {
481
- ret = idx ;
482
- if (orient == SC.HORIZONTAL_ORIENTATION) {
483
- if (SC.midX(f) < loc.x) ret++ ;
484
- } else {
485
- if (SC.midY(f) < loc.y) ret++ ;
486
- }
487
- }
488
- lastSide =curSide ;
489
- }
490
- }
491
-
492
- // Handle some edge cases
493
- if ((ret == null) || (ret < 0)) ret = 0 ;
494
- if (ret > content.length) ret = content.length ;
474
+ // sweet!... the view is not only in the collection, but it says we can hit it.
475
+ // hit it and quit it...
476
+ if ( this.hasItemView(view) && (!view.hitTest || view.hitTest(evt)) ) return view;
477
+ } while ( view = view.get('parentNode') );
495
478
 
496
- // Done. Phew. Return.
497
- return ret;
479
+ // nothing was found...
480
+ return null;
498
481
  },
499
-
500
- /**
501
- Override to show the insertion point during a drag.
502
-
503
- Called during a drag to show the insertion point. Passed value is the
504
- item view that you should display the insertion point before. If the
505
- passed value is null, then you should show the insertion point AFTER that
506
- last item view returned by the itemViews property.
482
+
483
+
484
+ /**
485
+ Returns the itemView that represents the passed content object.
507
486
 
508
- Once this method is called, you are guaranteed to also recieve a call to
509
- hideInsertionPoint() at some point in the future.
487
+ If no item view is currently rendered for the object, this method will
488
+ return null.
510
489
 
511
- The default implementation of this method does nothing.
490
+ @param {Object} obj The content object.
491
+ @returns {SC.View} The item view or null
492
+ */
493
+ itemViewForContent: function(obj) {
494
+ var key = (obj) ? SC.guidFor(obj) : '0';
495
+ return this._itemViewsByContent[key];
496
+ },
497
+
498
+ /**
499
+ Returns the groupView that represents the passed group value.
512
500
 
513
- @param {SC.View} itemView view the insertion point should appear directly before. If null, show insertion point at end.
501
+ If no group view is currently rendered for the gorup value, this method
502
+ will return null. If grouping is disabled, this method will also return
503
+ null.
514
504
 
515
- @returns {void}
505
+ @param {Object} value The group value.
506
+ @param {SC.View} The group view or null
516
507
  */
517
- showInsertionPointBefore: function(itemView) {},
518
-
508
+ groupViewForGroupValue: function(groupValue) {
509
+ return this._groupViewsByValue[groupValue] ;
510
+ },
511
+
519
512
  /**
520
- Override to hide the insertion point when a drag ends.
521
-
522
- Called during a drag to hide the insertion point. This will be called when the
523
- user exits the view, cancels the drag or completes the drag. It will not be
524
- called when the insertion point changes during a drag.
513
+ Returns the groupValue for the passed group view.
525
514
 
526
- You should expect to receive one or more calls to showInsertionPointBefore()
527
- during a drag followed by at least one call to this method at the end. Your
528
- method should not raise an error if it is called more than once.
515
+ Older-style groupViews expect the group value to be set directly on
516
+ their labelView while newer groupViews expect their groupValue to be set.
517
+ This method takes into account both approaches.
529
518
 
530
- @returns {void}
519
+ @param {SC.View} groupView the group view.
520
+ @returns {Object} the value of the group view or null.
531
521
  */
532
- hideInsertionPoint: function() {},
533
-
522
+ groupValueForGroupView: function(groupView) {
523
+ if (!groupView) return null ;
524
+ var ret ;
525
+ if (groupView.groupValue === undefined) {
526
+ ret = groupView.get('content') ;
527
+ } else ret = groupView.get('groupValue') ;
528
+ return ret ;
529
+ },
530
+
534
531
  /**
535
- Override this method to provide your own ghost image for a drag.
532
+ Expands the index into a range of content objects that have the same
533
+ group value.
536
534
 
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.
535
+ This method searches backward and forward through your content array for
536
+ objects that have the same group value as the object at the index you
537
+ pass in. You can use this method when implementing layoutGroupView to
538
+ determine the range of the content that belongs to the group.
539
539
 
540
- @param dragContent {Array} Array of content objects that will be used in the drag.
540
+ Since this method simply searches through the content array, it is really
541
+ only suitable for content arrays of a few hundred items or less. If you
542
+ expect to have a larger size of content array, then you may need to do
543
+ something custom in your data model to calculate this range in less time.
544
+
545
+ @param {Number} contentIndex index of a content object
546
+ @returns {Range} a range of objects
541
547
  */
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
+ groupRangeForContentIndex: function(contentIndex) {
549
+ var groupBy = this.get('groupBy') ;
550
+ if (!groupBy) return { start: contentIndex, length: 1 } ;
551
+
552
+ var min = contentIndex, max = contentIndex ;
553
+ var content = Array.from(this.get('content')) ;
554
+ var len = content.get('length') ;
555
+ var cur = content.objectAt(contentIndex) ;
556
+ var groupValue = (cur) ? cur.get(groupBy) : null ;
557
+
558
+ // find first item at bottom that does not match. add one to get start
559
+ while(--min >= 0) {
560
+ var cur = content.objectAt(min) ;
561
+ var curGroupValue = (cur) ? cur.get(groupBy) : null ;
562
+ if (curGroupValue !== groupValue) break ;
563
+ }
564
+ min++ ;
548
565
 
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) ;
566
+ // find first item at top that does not match. keep value to calc range
567
+ while(++max < len) {
568
+ var cur = content.objectAt(max) ;
569
+ var curGroupValue = (cur) ? cur.get(groupBy) : null ;
570
+ if (curGroupValue !== groupValue) break ;
568
571
  }
569
572
 
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) }) ;
573
+ return { start: min, length: max-min } ;
574
+ },
572
575
 
573
- return view ;
576
+ // Determines the group value at a specified index.
577
+ groupValueAtContentIndex: function(contentIndex) {
578
+ var groupBy = this.get('groupBy') ;
579
+ var content = Array.from(this.get('content')).objectAt(contentIndex) ;
580
+ return (groupBy && content && content.get) ? content.get(groupBy) : null;
574
581
  },
582
+
583
+ // ......................................
584
+ // GENERATING CHILDREN
585
+ //
575
586
 
576
- mouseDragged: function(ev) {
577
- // Don't do anything unless the user has been dragging for 123msec
578
- if ((Date.now() - this._mouseDownAt) < 123) return true ;
587
+ /**
588
+ Update the itemViews in the receiver to match the currently visible
589
+ content objects. Normally this method assumes the content objects
590
+ themselves have not changed and only updates the views if the range of
591
+ visible content has changed. If you pass true to the fullUpdate property,
592
+ then the entire set of itemViews will be revalidated in case any content
593
+ objects have changed.
579
594
 
580
- // OK, they must be serious, start a drag if possible.
581
- if (this.get('canReorderContent')) {
595
+ @param {Bool} fullUpdate (Optional) if set to true, assumes content has
596
+ changed and will perform a full update.
597
+
598
+ */
599
+ updateChildren: function(fullUpdate) {
582
600
 
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
- });
601
+ var f ;
594
602
 
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
600
- SC.Drag.start({
601
- event: this._mouseDownEvent,
602
- source: this,
603
- dragView: view,
604
- ghost: NO,
605
- slideBack: YES,
606
- data: { "_mouseDownContent": dragContent }
607
- }) ;
608
-
609
- // Also use this opportunity to clean up since mouseUp won't
610
- // get called.
611
- this._cleanupMouseDown() ;
612
- this._lastInsertionIndex = null ;
613
- }
614
- },
615
-
616
- // Drop Source.
617
- dragEntered: function(drag, evt) {
618
- if ((drag.get('source') == this) && this.get('canReorderContent')) {
619
- return SC.DRAG_MOVE ;
620
- } else {
621
- return SC.DRAG_NONE ;
603
+ // if the collection is not presently visible in the window, then there is
604
+ // really nothing to do here. Just mark the view as dirty and return.
605
+ if (!this.get('isVisibleInWindow')) {
606
+ this.set('isDirty', true) ;
607
+ this._needsFullUpdate = this._needsFullUpdate || fullUpdate ;
608
+ return;
622
609
  }
623
- },
624
-
625
- // If reordering is allowed, then show insertion point
626
- dragUpdated: function(drag, evt) {
627
- if (this.get('canReorderContent')) {
628
- var loc = drag.get('location') ;
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.
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
610
 
656
- if (this._lastInsertionIndex != ret) {
657
- var itemView = this.itemViewForContent(this.get('content').objectAt(ret));
658
- this.showInsertionPointBefore(itemView) ;
659
- }
660
- this._lastInsertionIndex = ret ;
661
-
611
+ if (SC.BENCHMARK_UPDATE_CHILDREN) {
612
+ var bkey = '%@.updateChildren(%@)'.fmt(this, (fullUpdate) ? 'FULL' : 'FAST') ;
613
+ SC.Benchmark.start(bkey);
662
614
  }
663
- return SC.DRAG_MOVE;
664
- },
665
-
666
- dragExited: function() {
667
- this.hideInsertionPoint() ;
668
- this._lastInsertionIndex = null ;
669
- },
670
-
671
- dragEnded: function() {
672
- this.hideInsertionPoint() ;
673
- this._lastInsertionIndex = null ;
674
- },
675
-
676
- prepareForDragOperation: function(op, drag) {
677
- return SC.DRAG_ANY;
678
- },
679
-
680
- performDragOperation: function(op, drag) {
681
-
682
- SC.Benchmark.start('%@ performDragOperation'.fmt(this._guid)) ;
683
-
684
- var loc = drag.get('location') ;
685
- loc = this.convertFrameFromView(loc, null) ;
686
-
687
- // if op is MOVE or COPY, add item to view.
688
- var objects = drag.dataForType('_mouseDownContent') ;
689
- if (objects && (op == SC.DRAG_MOVE)) {
690
-
691
- // find the index to for the new insertion
692
- var idx = this.insertionIndexForLocation(loc) ;
693
615
 
694
- var content = this.get('content') ;
695
- content.beginPropertyChanges(); // suspend notifications
696
-
697
- // debugger ;
698
- // find the old index and remove it.
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
- }
706
-
707
- // now insert objects at new location
708
- content.replace(idx, 0, objects) ;
709
- content.endPropertyChanges(); // restart notifications
710
- }
616
+ //console.log('updateChildren') ;
711
617
 
712
- SC.Benchmark.end('%@ performDragOperation'.fmt(this._guid)) ;
713
- console.log(SC.Benchmark.report()) ;
714
-
715
- return SC.DRAG_MOVE;
716
- },
717
-
718
- concludeDragOperation: function(op, drag) {
719
- this.hideInsertionPoint() ;
720
- this._lastInsertionIndex = null ;
721
- },
618
+ this.beginPropertyChanges() ; // avoid sending notifications
722
619
 
723
-
724
-
725
- // ......................................
726
- // GENERATING CHILDREN
727
- //
728
-
729
- /**
730
- Ensure that the displayed item views match the current set of content objects.
731
-
732
- This is the main entry point to the Collection View layout system. It
733
- compares the current set of item views to the content objects, adding, removing,
734
- and reordering views as necessary to bring them in sync with the set of content
735
- objects.
736
-
737
- Once it has finished running, this method will also call your layoutChildViewsFor()
738
- method if you have implemented it.
620
+ // STEP 1: Update frame size if needed. Required to compute the
621
+ // clippingFrame.
622
+ var f ;
623
+ if ((f = this.computeFrame()) && !SC.rectsEqual(f, this.get('frame'))) {
624
+ var parent = this.get('parentNode') ;
625
+ if (parent) parent.viewFrameWillChange() ;
626
+ this.set('frame', f) ;
627
+ if (parent) parent.viewFrameDidChange() ;
628
+
629
+ if ((f = this.computeFrame()) && !SC.rectsEqual(f, this.get('frame'))) {
630
+ this.set('frame', f) ;
631
+ }
632
+ }
739
633
 
740
- This method is called automatically whenever the content array changes. You will
741
- not usually need to call it yourself. If you want to refresh the item views,
742
- called rebuildChildren() instead.
743
- */
744
- updateChildren: function()
745
- {
746
- var el = this.containerElement || this.rootElement;
634
+ // Save the current clipping frame. If the frame methods are called again
635
+ // later but the frame has not actually changed, we don't want to run
636
+ // updateChildren again.
637
+ var clippingFrame = this._lastClippingFrame = this.get('clippingFrame') ;
747
638
 
748
- SC.Benchmark.start('%@: updateChildren'.fmt(this._guid)) ;
749
- // initial setup
750
- if (this._firstUpdate)
751
- {
752
- el.innerHTML = '';
753
- this._firstUpdate = false;
754
- }
755
-
756
- // before removing from parent, make sure we have retrieved the frame
757
- // size so that layout can happen.
758
- this.cacheFrame();
759
-
760
- // viewsForContent will hold all the item views we currently have rendered
761
- // keyed by content._guid. We use this to quickly determine if a view can
762
- // be reused.
763
- if (!this._viewsForContent) this._viewsForContent = {};
764
-
765
- // handle grouped items. If items are grouped, then each childNode is
766
- // a group, which contains a label and a div with the items themselves.
767
- var groupBy = this.get('groupBy');
768
- var content = this.get('content') || [];
769
-
770
- // If the number of childViews differs from the content size, remove from
771
- // DOM to improve performance while updating.
772
- this._cachedParent = null;
773
- this._cachedSibling = null;
774
- if (content.get('length') != this.childNodes.get('length'))
775
- {
776
- this._cachedParent = el.parentNode;
777
- this._cachedSibling = el.nextSibling;
778
- if (this._cachedParent) {
779
- this._cachedParent.removeChild(el);
780
- } else {
781
- //debugger;
782
- }
783
- }
784
-
785
- // this code path will render the collection of groups. This creates
786
- // group views for each distinct group it encounters and then has it
787
- // render child views in each item.
788
- if (groupBy)
789
- {
790
- var loc = 0;
791
- var group = this.firstChild;
792
- while (group || (loc < content.get('length')))
793
- {
794
- var groupValue = (loc < content.get('length')) ? content.objectAt(loc).get(groupBy) : null;
795
-
796
- // we are out of content, just remove any remaining groups (including
797
- // child nodes)
798
- if (loc >= content.get('length')) {
799
- if (group) {
800
- // this will clear out the item views in the group.
801
- loc = this.updateChildrenInGroup(group.itemView, content, loc, groupBy, null);
802
- // now remove the group.
803
- var prev = group.previousSibling ;
804
- this.removeChild(group) ;
805
- group = prev ;
806
- }
807
-
808
- // otherwise, make sure the current group matches the next group. If
809
- // it doesn't, then add a new group.
810
- } else if (!group || (group.get('groupValue') != groupValue)) {
811
-
812
- // create group view.
813
- var newGroup = this.exampleGroupView.viewFor(null) ;
814
- newGroup.owner = this ;
815
- newGroup.set('groupValue',groupValue) ;
816
-
817
- // add group label view.
818
- if (newGroup.labelView) newGroup.labelView.set('content',groupValue);
819
-
820
- // add item views to group.
821
- loc = this.updateChildrenInGroup(newGroup.itemView,content,loc,
822
- groupBy, groupValue) ;
823
-
824
- // add the new group at this point
825
- this.insertBefore(newGroup,group) ;
826
- group = newGroup ;
827
-
828
- // otherwise, if the current group does match the next group, just
829
- // update its child nodes.
830
- } else {
831
- loc = this.updateChildrenInGroup(group.itemView,content,loc,
832
- groupBy, groupValue) ;
833
- }
834
-
835
- // go to the next group. group will be nil if the first group was
836
- // removed.
837
- group = (group) ? group.nextSibling : this.firstChild ;
639
+ // STEP 2: Calculate the new range of content to display in
640
+ // the clipping frame. Determine if we need to do a full update or
641
+ // not.
642
+
643
+ var range = this.contentRangeInFrame(clippingFrame) ;
644
+ var content = Array.from(this.get('content'));
645
+
646
+ //make sure the range isn't greater than the content length
647
+ //this will prevent trying to render items that aren't really there.
648
+ range.length = Math.min(SC.maxRange(range), content.get('length')) - range.start ;
649
+
650
+ var nowShowingRange = this.get('nowShowingRange') ;
651
+ fullUpdate = fullUpdate || (SC.intersectRanges(range, nowShowingRange).length <= 0) ;
652
+ this.set('nowShowingRange', range) ;
653
+
654
+ // STEP 3: Update item views.
655
+ var groupBy = this.get('groupBy') ;
656
+ var didChange = false ;
657
+
658
+ // If this is a fullUpdate, then rebuild the itemViewsByContent hash
659
+ // from scratch. This is necessary of the content of the visible range
660
+ // might have changed.
661
+ if (fullUpdate) {
662
+
663
+ var itemViewsByContent = {} ; // this will replace the current hash.
664
+
665
+ // iterate through all of the views and insert them. If the view
666
+ // already exists, it will simply be reused.
667
+ var idx = SC.maxRange(range) ;
668
+ while(--idx >= range.start) {
669
+ var c = content.objectAt(idx) ;
670
+ var key = SC.guidFor(c) ;
671
+ var itemView = this._insertItemViewFor(c, groupBy, idx) ;
672
+
673
+ // add item view to new hash and remove from old hash.
674
+ itemViewsByContent[key] = itemView;
675
+
676
+ delete this._itemViewsByContent[key];
838
677
  }
839
678
 
840
- // grouping is not turned on.
841
- } else {
842
- this.updateChildrenInGroup(this, content, 0, null, null) ;
843
- }
844
-
845
- // Add back into DOM if optimization was used.
846
- if (this._cachedParent) {
847
- this._cachedParent.insertBefore(el,this._cachedSibling) ;
848
- }
849
-
850
- this.updateSelectionStates() ;
851
- this.flushFrameCache() ;
852
- this.set('isDirty',false);
853
- SC.Benchmark.end('%@: updateChildren'.fmt(this._guid)) ;
854
- },
855
-
856
- /**
857
- @private
858
-
859
- Step through the child nodes in the parent to match them to
860
- the content array, starting at the passed location. It will go until it
861
- runs out of content objects or until the content no longer belong to the
862
- group indicated.
863
- */
864
- updateChildrenInGroup: function(parent,content,loc,groupBy,groupValue) {
865
- // cacheing content.get('length') for optimization.
866
- var contentCount = content.get('length');
867
- var child = parent.firstChild;
868
- var inGroup = true ;
869
-
870
- if (!this._itemViews) this._itemViews = {};
871
- var itemViewsDidChange = false;
872
-
873
- this.updateComputedViewHeight(parent);
874
-
875
- // if we aren't rendering groups, then this can expire.
876
- var expired = false;
877
- var canExpire = !groupBy && loc == 0 ;
878
- if (canExpire)
879
- {
880
- loc = this._lastRenderLoc ;
881
- child = this._lastRenderChild || child;
882
- this._resetRenderClock();
883
- };
884
-
885
- var firstChild = null ;
886
-
887
- // save the first child to be modified. This will be
888
- // passed to the layout method.
889
- var firstModifiedChild = null;
890
-
891
- while (child || (inGroup && (loc < contentCount) && !expired)) {
679
+ // Now iterate through the old hash. Any left over item views should
680
+ // be removed.
681
+ for(var key in this._itemViewsByContent) {
682
+ if (!this._itemViewsByContent.hasOwnProperty(key)) continue ;
683
+ var itemView = this._itemViewsByContent[key] ;
684
+ this._removeItemView(itemView, groupBy) ;
685
+ } ;
892
686
 
893
- // get the content object.
894
- var cur = (inGroup && (loc < contentCount)) ? content.objectAt(loc) : null;
687
+ // Swap out remaining content items.
688
+ this._itemViewsByContent = itemViewsByContent ;
689
+ didChange = true;
895
690
 
896
- // verify the new cur is still in the group.
897
- if (cur && groupBy && (cur.get(groupBy) != groupValue))
898
- {
899
- inGroup = false;
900
- cur = null;
691
+ // If a fullUpdate is not required, then we assume no content has changed
692
+ // and we just need to add or remove some views to bring the ranges up
693
+ // to date.
694
+ } else {
695
+ // Find changed range at the top. Note that the length here may be
696
+ // negative. Negative means views should be removed.
697
+ var start = range.start ;
698
+ var length = (nowShowingRange.start - start) ;
699
+ if (length != 0) {
700
+ this._insertOrRemoveItemViewsInRange(start, length, groupBy) ;
701
+ didChange = true ;
901
702
  }
902
703
 
903
- // we are out of content for this group, remaining children simply need
904
- // to be removed.
905
- if (cur == null) {
906
- if (child) {
907
- if (this.flushUnusedViews) {
908
- var viewContent = child.get('content') ;
909
- if (viewContent) delete this._viewsForContent[SC.getGUID(viewContent)];
910
- child.set('content',null) ;
911
- }
912
- var prev = child.previousSibling ;
913
- parent.removeChild(child) ;
914
- if (this._itemViews[SC.getGUID(child)]) {
915
- itemViewsDidChange = true ;
916
- delete this._itemViews[SC.getGUID(child)];
917
- }
918
-
919
- child = prev;
920
- }
921
-
922
- // otherwise, make sure the current child matches the content object.
923
- // if it doesn't, get the right view (or create it) and insert it here.
924
- } else if (!child || (child.get('content') != cur)) {
925
-
926
- // find the correct view. If it doesn't exist, create it.
927
- var newChild = this._viewsForContent[SC.getGUID(cur)] ;
928
- if (!newChild) {
929
- newChild = this.exampleView.viewFor(null) ;
930
- newChild.owner = this ;
931
- newChild._isChildView = true ;
932
- newChild.set('content',cur) ;
933
- this._viewsForContent[SC.getGUID(cur)] = newChild ;
934
- }
935
-
936
- // add the view at this point in the hierarchy and make the new child
937
- // the current child.
938
- parent.insertBefore(newChild,child);
939
- this._itemViews[SC.getGUID(newChild)] = newChild;
940
- itemViewsDidChange = true;
941
- if (!firstModifiedChild) firstModifiedChild = newChild ;
942
- child = newChild;
704
+ // Find the changed range at the bottom. Note that the length here may
705
+ // also be negative. Negative means views should be removed.
706
+ var start = SC.maxRange(nowShowingRange) ;
707
+ var length = SC.maxRange(range) - start ;
708
+ if (length != 0) {
709
+ this._insertOrRemoveItemViewsInRange(start, length, groupBy) ;
710
+ didChange = true ;
943
711
  }
944
-
945
- // go to next child and content object
946
- // child would only be nil if the current child was first and was
947
- // removed
948
- if (!firstChild) firstChild = child;
949
- child = (child) ? child.nextSibling : ((inGroup) ? parent.firstChild : null);
712
+ }
713
+
714
+ // Clean out some cached items and notify their changes.
715
+ if (didChange) {
716
+ this._flushZombieGroupViews() ;
717
+ this.updateSelectionStates() ;
950
718
 
951
- // go to the next loc only if cur was used last time.
952
- if (cur) loc++;
719
+ this._itemViews = null ;
720
+ this.notifyPropertyChange('itemViews') ;
953
721
 
954
- expired = this._renderExpired();
722
+ this._groupViews = null ;
723
+ this.notifyPropertyChange('groupViews') ;
955
724
  }
956
725
 
957
-
958
- // maybe save the current render loc and reschedule.
959
- if (expired && (loc < contentCount)) {
960
- this._lastRenderLoc = loc ;
961
- this._lastRenderChild = child ;
962
- setTimeout(this.updateChildren.bind(this),1) ; // do more later.
963
- } else {
964
- this._resetExpiredRender();
965
- }
966
-
967
- // now let the collection view layout the views that changed (if
968
- // it is implemented.)
969
- if (firstModifiedChild && this.layoutChildViewsFor) {
970
- var el = this.containerElement || this.rootElement;
971
- if (this._cachedParent) {
972
- this._cachedParent.insertBefore(el,this._cachedSibling);
973
- }
974
- this.layoutChildViewsFor(parent, firstModifiedChild);
975
- if (this._cachedParent) {
976
- this._cachedParent.removeChild(el);
977
- }
978
- }
979
-
980
- // notify itemViews change if applicable.
981
- if (itemViewsDidChange) this.propertyDidChange('itemViews');
726
+ // Recache frames just in case this changed the scroll height.
727
+ this.recacheFrames() ;
982
728
 
983
- return loc;
984
- },
985
-
986
-
987
- /**
988
- Returns the itemView that represents the passed content object.
989
729
 
990
- If no item view is currently rendered for the object, this method will
991
- return null.
730
+ // Set this to true once children have been rendered. Whenever the
731
+ // content changes, we don't want resize or clipping frame changes to
732
+ // cause a refresh until the content has been rendered for the first time.
733
+ this._hasChildren = range.length>0 ;
992
734
 
993
- @param {Object} obj The content object. Should be a member of the content array.
994
- @returns {SC.View} The item view for this object or null if no match could be found.
995
- */
996
- itemViewForContent: function( obj )
997
- {
998
- return this._viewsForContent[SC.getGUID(obj)];
735
+ this.set('isDirty',false);
736
+ this.endPropertyChanges() ;
737
+ if (SC.BENCHMARK_UPDATE_CHILDREN) SC.Benchmark.end(bkey);
999
738
  },
1000
739
 
1001
740
  /**
1002
741
  Rebuild all the child item views in the collection view.
1003
742
 
1004
- This will remove all the child views from the collection view and rebuild them
1005
- from scratch. This method is generally expensive, but if you have made a
1006
- substantial number of changes to the content array and need to bring everything
1007
- up to date, this is the best way to do it.
743
+ This will remove all the child views from the collection view and rebuild
744
+ them from scratch. This method is generally expensive, but if you have
745
+ made a substantial number of changes to the content array, this may be the
746
+ most efficient way to perform the update.
1008
747
 
1009
- In general the collection view will automatically keep the item views in sync
1010
- with the content objects for you. You should not need to call this method
1011
- very often.
748
+ In general the collection view will automatically keep the item views in
749
+ sync with the content objects for you. You should not need to call this
750
+ method very often.
1012
751
 
1013
752
  @returns {void}
1014
753
  */
1015
754
  rebuildChildren: function() {
1016
- this.clear();
1017
- this._viewsForContent = {};
1018
- this._resetExpiredRender();
1019
- this.updateChildren();
755
+
756
+ this.beginPropertyChanges() ;
757
+
758
+ // iterate through itemViews and remove them
759
+ while(this._itemViewRoot) this._removeItemViewFromChain(this._itemViewRoot) ;
760
+
761
+ // iterate through groupViews and remove them .. if grouping is disabled,
762
+ // _groupViewRoot will be null anyway.
763
+ while(this._groupViewRoot) this._removeGroupView(this._groupViewRoot) ;
764
+
765
+ // now updateChildren.
766
+ this._hasChildren = false ;
767
+ this.updateChildren() ;
768
+
769
+ this.endPropertyChanges() ;
1020
770
  },
1021
-
771
+
1022
772
  /**
1023
773
  Update the selection state for the item views to reflect the selection array.
1024
774
 
@@ -1031,120 +781,480 @@ SC.CollectionView = SC.View.extend(
1031
781
  updateSelectionStates: function() {
1032
782
  if (!this._itemViews) return ;
1033
783
  var selection = this.get('selection') || [];
1034
-
784
+
1035
785
  // First, for efficiency, turn the selection into a hash by GUID. This
1036
786
  // way, we'll only have to perform a linear search over the children.
1037
- var selectionHash = {};
1038
- var numberOfSelectedItems = selection.get('length');
1039
- for( var i = 0; i < numberOfSelectedItems; i++ ) {
1040
- var item = selection.objectAt(i);
1041
- selectionHash[SC.getGUID(item)] = true;
787
+ // This hash is cached and flushed each time the selection changes.
788
+ var selectionHash = this._selectionHash ;
789
+ if (!selectionHash) {
790
+ selectionHash = {} ;
791
+ var idx = selection.get('length') ;
792
+ while(--idx >= 0) {
793
+ var cur = selection.objectAt(idx) ;
794
+ var key = SC.guidFor(cur) ;
795
+ selectionHash[key] = true ;
796
+ }
797
+ this._selectionHash = selectionHash ;
1042
798
  }
1043
799
 
1044
- for(var key in this._itemViews) {
1045
- if (!this._itemViews.hasOwnProperty(key)) continue ;
1046
- var child = this._itemViews[key] ;
1047
- var content = (child.get) ? child.get('content') : null;
1048
- var guid = (content) ? SC.getGUID(content) : null;
1049
-
1050
- if( !guid ) continue;
1051
- var childIsSelected = selectionHash[guid] ? true : false;
1052
-
1053
- // If the child's state has changed from before, set it to the new
1054
- // state. Otherwise, don't bother setting the state to the same value
1055
- // it used to have.
1056
- if( childIsSelected != child.get('isSelected') ) {
1057
- if (child.set) child.set('isSelected', childIsSelected);
800
+ // Iterate over the item views and set their selection property.
801
+ for(var key in this._itemViewsByContent) {
802
+ if (!this._itemViewsByContent.hasOwnProperty(key)) continue ;
803
+ var itemView = this._itemViewsByContent[key] ;
804
+ var isSelected = (key) ? selectionHash[key] : false ;
805
+ if (itemView.get('isSelected') != isSelected) {
806
+ itemView.set('isSelected', isSelected) ;
1058
807
  }
1059
808
  }
1060
809
  },
1061
810
 
1062
- // layoutChildViewsFor: function(parentView, startingView) { return false; },
1063
811
 
812
+ /**
813
+ Calls updateChildren whenever the view is resized, unless you have not
814
+ implemented custom layout or incremental rendering.
815
+
816
+ UPDATE:
817
+ -- add/remove any children as needed
818
+ -- update layout on all itemViews unless you have a more efficient
819
+ */
1064
820
  resizeChildrenWithOldSize: function(oldSize) {
1065
- if (this.layoutChildViewsFor && (this.layoutChildViewsFor(this, null))) {
1066
- this.updateComputedViewHeight(this) ;
1067
- } else {
1068
- arguments.callee.base.apply(this,arguments) ;
1069
- }
821
+ if (!this._hasChildren) return ;
822
+ this.updateChildren() ; // add/remove any new views.
823
+ this.layoutResize() ; // perform layout on all of the views if needed.
1070
824
  },
1071
-
1072
- _firstUpdate: true,
1073
-
1074
- _lastRenderLoc: 0,
1075
- _renderStart: null,
1076
- _resetRenderClock: function() { this._renderStart = new Date().getTime(); },
1077
825
 
1078
- _resetExpiredRender: function() {
1079
- this._lastRenderLoc = 0; this._lastRenderChild = null;
826
+ /**
827
+ Whenever your clipping frame changes, determine new range to display. If
828
+ new range is a change, then it will update the children and relayout.
829
+
830
+ UPDATE:
831
+ -- add/remove any children as needed
832
+ -- update layout on added children only
833
+ */
834
+ clippingFrameDidChange: function() {
835
+ if (!this._hasChildren) return ;
836
+ SC.Benchmark.start('%@.clippingFrameDidChange'.fmt(this.toString())) ;
837
+ if (!SC.rectsEqual(this._lastClippingFrame, this.get('clippingFrame'))) {
838
+ if (this._hasChildren) this.updateChildren() ;
839
+ }
840
+ SC.Benchmark.end('%@.clippingFrameDidChange'.fmt(this.toString())) ;
1080
841
  },
842
+
843
+ /**
844
+ Override to return the computed frame dimensions of the collection view.
845
+
846
+ These dimensions are automatically applied at the end of a call to
847
+ updateChildren() if they change at all. This method is critical for
848
+ support of incremental rendering.
1081
849
 
1082
- _renderExpired: function() {
1083
- var max = this.maxRenderTime ;
1084
- if ((this._renderStart == null) || (max == 0)) return false ;
1085
- return ((new Date().getTime()) - this._renderStart) > max ;
1086
- },
850
+ @returns {Rect} width and/or height you want this collection view to have.
851
+ */
852
+ computeFrame: function() { return null; },
1087
853
 
1088
854
  /**
1089
855
  Override to return the range of items to render for a given frame.
856
+
857
+ You can override this method to implement support for incremenetal
858
+ rendering. The range you return here will be used to limit the number of
859
+ actual item views that are created by the collection view.
1090
860
 
1091
- The range you return will be used to limit the number of actual views that are
1092
- created for the collection view. The passed frame is relative to the total frame
1093
- of the groupView.
1094
-
1095
- You should override this method if you want to support incremental rendering.
1096
- The default implementation does nothing.
1097
-
1098
- @param {SC.View} groupView The group view the requested items belong to. If
1099
- grouping is not used, this will always be null.
1100
-
1101
- @param {Frame} frame The frame you should use to determine the range.
861
+ @param {Rect} frame The frame you should use to determine the range.
1102
862
 
1103
- @returns {Range} A hash that indicates the range of content objects to render. ({ start: X, length: Y })
863
+ @returns {Range} A hash that indicates the range of content objects to
864
+ render. ({ start: X, length: Y })
1104
865
  */
1105
- itemRangeInFrame: function(groupView, frame) { return null; },
1106
-
1107
- /**
1108
- Override to return a computed height of the collection.
866
+ contentRangeInFrame: function(frame) {
867
+ var content = this.get('content') ;
868
+ var len = ((content && content.get) ? content.get('length') : 0) || 0 ;
869
+ return { start: 0, length: len };
870
+ },
1109
871
 
1110
- This will be used to set a dynamic scrollbar height if you support incremental
1111
- rendering. The default implementation does nothing.
872
+
873
+ /**
874
+ This method is called whenever a group view is added or during the
875
+ layoutResize() method. You should use this method to size and position
876
+ the group view.
877
+
878
+ The included contentIndexHint can be used to help you determine the range
879
+ of content that should be included in the group. If you are renderings a
880
+ list of items 100 or less, you can get the range of content belonging to
881
+ the group using the contentRangeForGroup() method. If you are managing
882
+ a much larger set of content, you should probably implement your own
883
+ data model.
884
+
885
+ Your layout method should can optionally also use the firstLayout to
886
+ further optimize itself. Normally, you will want to only change a view's
887
+ actual frame if it does not match your calculated size. However, if
888
+ firstLayout is true, you can simply set the new layout without checking
889
+ first.
890
+
891
+ @param {SC.View} groupView the view to size and position.
892
+ @param {Object} groupValue the value the groupView represents.
893
+ @param {Number} contentIndexHint the index of a content object.
894
+ @param {Bool} firstLayout True if this is the first the view has been laid out.
895
+
896
+ */
897
+ layoutGroupView: function(groupView, groupValue, contentIndexHint, firstLayout) {
898
+
899
+ },
1112
900
 
1113
- @param {SC.View} groupView The group view this request relates to. If grouping is
1114
- turned off, this parameter will be null.
901
+ /**
902
+ This method is called whenever an itemView is added or during the
903
+ layoutResize() method. You should use this method to size and position
904
+ the itemView.
1115
905
 
1116
- @returns {Number} The view height in pixels.
906
+ @param {SC.View} itemViewthe item view to layout
907
+ @param {Number} contentIndex the index of the content this layout represents.
908
+ @param {Bool} firstLayout true if this is the first time it has been laid out.
1117
909
  */
1118
- computedViewHeight: function(groupView) { return -1; },
910
+ layoutItemView: function(itemView, contentIndex, firstLayout) {
911
+
912
+ },
1119
913
 
1120
- // This will set the collection height.
1121
- updateComputedViewHeight: function(groupView) {
1122
- var height = this.computedViewHeight(groupView) ;
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 }) ;
914
+ /**
915
+ This method is called whenever the view is resized. The default
916
+ implementation will simply iterate through the visible content range and
917
+ call layoutItemView() and layoutGroupView() on all the views.
918
+
919
+ If you would like to provide a more efficient method for updating the
920
+ layout on a resize, you could override this method and do the iterating
921
+ yourself.
922
+ */
923
+ layoutResize: function() {
924
+ if (!this._hasChildren) return ; // ignore calls before first render
925
+ var nowShowingRange = this.get('nowShowingRange') ;
926
+ var groupBy = this.get('groupBy') ;
927
+ var groupValue = undefined ;
928
+ var content = this.get('content') || [] ;
929
+
930
+ var idx = SC.maxRange(nowShowingRange) ;
931
+ while(--idx >= nowShowingRange.start) {
932
+ var cur = content.objectAt(idx) ;
933
+ var itemView = this.itemViewForContent(cur) ;
934
+
935
+ // should never happen, but recover just in case.
936
+ if (!itemView) continue ;
937
+
938
+ // if grouping is enabled, get the group value and layout based on that.
939
+ if (groupBy && ((curGroupValue = (cur) ? cur.get(groupBy) : null) !== groupValue)) {
940
+ var groupView = this.groupViewForGroupValue(groupValue) ;
941
+ if (groupView) {
942
+ this.layoutGroupView(groupView, groupValue, idx, false) ;
943
+ }
1128
944
  }
945
+
946
+ // now layout the itemView itself.
947
+ this.layoutItemView(itemView, idx, false) ;
1129
948
  }
1130
949
  },
950
+
1131
951
 
1132
- // ......................................
1133
- // SELECTION
1134
- //
952
+ // Ordered array of item views currently on display. This array
953
+ // is reset whenever the item views are regenerated.
954
+ _itemViews: null,
1135
955
 
1136
- _indexOfSelectionTop: function() {
1137
- var content = this.get('content');
1138
- var sel = this.get('selection');
1139
- if (!content || !sel) return - 1;
956
+ // Ordered array of group views currently in the display. This array is
957
+ // reset whenever the group views are regenerated.
958
+ _groupViews: null,
959
+
960
+ // Most recent content range on display.
961
+ _visibleContentRange: null,
962
+
963
+ // Hash of itemViews to the content guids they current represent. This
964
+ // only matches views in currently in the _visibleContentRange.
965
+ _itemViewsByContent: null,
1140
966
 
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
- }
967
+ // Hash of groupViews to the group key they currently represent.
968
+ _groupViewsByValue: null,
969
+
970
+ // Hash of counts of item views contained in a group view. When the count
971
+ // of a group reaches zero, it will be removed.
972
+ _groupViewCounts: null,
973
+
974
+ // Array of unused itemViews. Push/pop only.
975
+ _itemViewPool: null,
976
+
977
+ // Array of unused groupViews. Push/pop only.
978
+ _groupViewPool: null,
979
+
980
+ // When a group view's item view count reaches zero, it is moved to this
981
+ // hash until updateChildren() completes. During that time, if the group
982
+ // is needed again, it can be reused. At the end of updateChildren() this
983
+ // hash will be flushed and its members returned to the groupView pool.
984
+ //
985
+ _zombieGroupViews: null,
986
+
987
+ /** @private
988
+ Finds or creates the itemView for the named content and inserts it into
989
+ view under the correct group if needed. Note that this method does not
990
+ take into account the actual ORDER of item views in the hierarchy. It
991
+ assumes that manual layout will ensure the items appear visually in the
992
+ proper order anyway.
993
+
994
+ @param {SC.View} itemView The item view to remove
995
+ @param {String} groupBy the value used for grouping or null if grouping is
996
+ disabled.
997
+
998
+ @returns {SC.View} the new itemView.
999
+ */
1000
+ _insertItemViewFor: function(content, groupBy, contentIndex) {
1001
+
1002
+ // first look for a matching record.
1003
+ var key = SC.guidFor(content) ;
1004
+ var ret = this._itemViewsByContent[key];
1005
+ var firstLayout = false ;
1006
+
1007
+ // if no record was found, pull an item view from the pool or create one.
1008
+ // set the content.
1009
+ if (!ret) {
1010
+ ret = this._itemViewPool.pop() || this.get('exampleView').create({
1011
+ owner: this, displayDelegate: this
1012
+ }) ;
1013
+ ret.addClassName('sc-collection-item') ; // add class name for display
1014
+
1015
+ // set content and add to content hash
1016
+ ret.set('content', content) ;
1017
+ this._itemViewsByContent[key] = ret ;
1018
+ this._itemViewsByGuid[SC.guidFor(ret)] = ret ;
1019
+ firstLayout = true ;
1020
+ }
1021
+ if (!ret) throw "Could not create itemView for content: %@".fmt(content);
1022
+
1023
+ // Determine proper parent view and insert itemView if needed.
1024
+ // Also update count of itemViews.
1025
+ var parentView = (groupBy && content) ? this._insertGroupViewFor(content.get(groupBy), contentIndex) : this ;
1026
+ if (ret.get('parentNode') != parentView) {
1027
+ parentView.appendChild(ret) ;
1028
+ if (groupBy) this._groupViewCounts[SC.guidFor(parentView)]++ ;
1029
+ }
1030
+
1031
+ // Layout itemView.
1032
+ this.layoutItemView(ret, contentIndex, firstLayout) ;
1033
+ return ret ;
1034
+ },
1035
+
1036
+ /** @private
1037
+ Removes the itemView from the receiver and returns it to the itemView pool
1038
+ for later reuse.
1039
+
1040
+ If the itemView belongs to a groupView and this leaves the groupView empty
1041
+ as well, then the groupView will be moved to the zombieGroupViews hash.
1042
+
1043
+ @param {SC.View} itemView The item view to remove
1044
+ @param {String} groupBy the value used for grouping or null if grouping is
1045
+ disabled.
1046
+
1047
+ @returns {SC.View} The itemView that was removed.
1048
+ */
1049
+ _removeItemView: function(itemView, groupBy) {
1050
+
1051
+ // If we are grouping, then decrement the groupViewCount. If the new
1052
+ // count is zero, save groupView for later removal.
1053
+ var groupView = null ; var groupValue ;
1054
+ if (groupBy && (groupView = itemView.get('parentNode'))) {
1055
+ if (--this._groupViewCounts[SC.guidFor(groupView)] > 0) groupView = null ;
1056
+ if (groupView) {
1057
+ var content = itemView.get('content') ;
1058
+ groupValue = (content) ? content.get(groupBy) : null ;
1059
+ }
1060
+ }
1061
+
1062
+ // Remove itemView from parent and remove from content hash.
1063
+ var content = itemView.get('content') ;
1064
+ var key = SC.guidFor(content) ;
1065
+ delete this._itemViewsByContent[key] ;
1066
+ delete this._itemViewsByGuid[SC.guidFor(itemView)] ;
1067
+ itemView.removeFromParent() ;
1068
+
1069
+ // Clear content and return itemView to pool
1070
+ itemView.set('content', null) ;
1071
+ this._itemViewPool.push(itemView) ;
1072
+
1073
+ // if a groupView is set, then it also needs to be returned to the pool
1074
+ if (groupView) this._removeGroupView(groupView, groupValue) ;
1075
+
1076
+ return itemView;
1077
+ },
1078
+
1079
+ /** @private
1080
+ Adds or removes itemViews for the content in the specified range.
1081
+ Note that this is not passed as a formal range because the length
1082
+ could be negative.
1083
+
1084
+ A negative length means views should be removed.
1085
+ */
1086
+ _insertOrRemoveItemViewsInRange: function(start, length, groupBy) {
1087
+ // zero length means do nothing.
1088
+ if (length == 0) return ;
1089
+
1090
+ var content = this.get('content') || [] ;
1091
+
1092
+ // negative length == remove item views
1093
+ if (length < 0) {
1094
+ while(++length < 0) {
1095
+ var c = content.objectAt(start + length) ;
1096
+ var itemView = this.itemViewForContent(c) ;
1097
+ if (itemView) this._removeItemView(itemView, groupBy) ;
1098
+ }
1099
+
1100
+ // positive length == add item views.
1101
+ } else if (length > 0) {
1102
+ while(--length >= 0) {
1103
+ var idx = start + length ;
1104
+ var c = content.objectAt(idx) ;
1105
+ this._insertItemViewFor(c, groupBy, idx) ;
1106
+ }
1107
+ }
1108
+ },
1109
+
1110
+ /** @private
1111
+ Finds or creates a groupView for the named group value and inserts it into
1112
+ the receiver. This method does not take into account the actual ORDER of
1113
+ the groupViews in the hierarchy. It assumes that manual layout will
1114
+ ensure the items appear visually in the proper order anyway.
1115
+
1116
+ @returns {SC.View} the new groupView.
1117
+ */
1118
+ _insertGroupViewFor: function(groupValue, contentIndex) {
1119
+ var ret = this._groupViewsByValue[groupValue] ;
1120
+ // if (ret) return ret ; // nothing to do
1121
+
1122
+ var firstLayout = false ;
1123
+
1124
+ // if the group was not found, check the zombie pool. If found in zombie
1125
+ // pool, restore it to the regular group view hash.
1126
+ if (!ret && this._zombieGroupViews) {
1127
+ ret = this._zombieGroupViews[groupValue] ;
1128
+ if (ret) {
1129
+ delete this._zombieGroupViews[groupValue] ;
1130
+ this._groupViewsByValue[groupValue] = ret ;
1131
+ this._groupViewCounts[SC.guidFor(ret)] = 0 ;
1132
+ }
1133
+ }
1134
+
1135
+ // If groupValue still not found, create one.
1136
+ if (!ret) {
1137
+ ret = this._groupViewPool.pop() || this.get('exampleGroupView').create({
1138
+ owner: this, displayDelegate: this
1139
+ });
1140
+ ret.addClassName('sc-collection-group') ;
1141
+
1142
+ // set the groupValue on the groupView. Older groupViews expect us to
1143
+ // set this directly on the labelView. Newer groupViews should have a
1144
+ // groupValue property.
1145
+ if (ret.groupValue !== undefined) {
1146
+ ret.set('groupValue', groupValue) ;
1147
+ } else ret.set('content', groupValue) ;
1148
+
1149
+ // save in cache
1150
+ this._groupViewsByValue[groupValue] = ret ;
1151
+ this._groupViewCounts[SC.guidFor(ret)] = 0 ;
1152
+ firstLayout = true;
1153
+ }
1154
+
1155
+ // If the group view does not already belong to the receiver, add it.
1156
+ if (!ret) throw "Could not create a groupView for value: %@".fmt(groupValue) ;
1157
+ if (ret.get('parentNode') != this) this.appendChild(ret) ;
1158
+
1159
+ // Layout the group View
1160
+ this.layoutGroupView(ret, groupValue, contentIndex, firstLayout) ;
1161
+
1162
+ return ret ;
1163
+ },
1164
+
1165
+ /** @private
1166
+ Called whenever a groupView is no longer being used.
1167
+
1168
+ Theoretically, this method removes a group view from the receiver and
1169
+ stores it in the pool for later use. In actuality, this will just moved
1170
+ the view to the zombieGroupView pool. You must call
1171
+ _flushZombieGroupViews() to actually remove them from the receiver.
1172
+ */
1173
+ _removeGroupView: function(groupView, groupValue) {
1174
+ if (SC.ZOMBIE_GROUPS_ENABLED) {
1175
+ this._zombieGroupViews[groupValue] = groupView ;
1176
+ } else {
1177
+ this._finalRemoveGroupView(groupView) ;
1178
+ }
1179
+
1180
+ delete this._groupViewsByValue[groupValue] ;
1181
+ delete this._groupViewCounts[SC.guidFor(groupView)] ;
1182
+ return groupView ;
1183
+ },
1184
+
1185
+ /** @private
1186
+ Flushes any zombie group views, removing them from their parent view and
1187
+ returning them to the groupView pool for later consumption.
1188
+ */
1189
+ _flushZombieGroupViews: function() {
1190
+ if (!SC.ZOMBIE_GROUPS_ENABLED) return ; // nothing to do
1191
+
1192
+ for(var key in this._zombieGroupViews) {
1193
+ if (!this._zombieGroupViews.hasOwnProperty(key)) continue ;
1194
+ var groupView = this._zombieGroupViews[key] ;
1195
+ this._finalRemoveGroupView(groupView) ;
1196
+ }
1197
+ this._zombieGroupViews = {} ; // reset
1198
+ },
1199
+
1200
+ /** @private
1201
+ Final method to actually remove a groupView from its parent view and
1202
+ return it to the groupView pool.
1203
+ */
1204
+ _finalRemoveGroupView: function(groupView) {
1205
+ groupView.removeFromParent() ;
1206
+
1207
+ // set the groupValue on the groupView. Older groupViews expect us to set
1208
+ // this directly on the labelView. Newer groupViews should have a
1209
+ // groupValue property.
1210
+ if (groupView.groupValue !== undefined) {
1211
+ groupView.set('groupValue', null) ;
1212
+ } else groupView.set('content', null) ;
1213
+
1214
+ this._groupViewPool.push(groupView) ;
1215
+ return groupView ;
1216
+ },
1217
+
1218
+ /** @private
1219
+ Removes the rootElement from the DOM temporarily if needed to optimize performance.
1220
+ */
1221
+ _removeRootElementFromDom: function() {
1222
+ if (!SC.REMOVE_COLLECTION_ROOT_ELEMENT_DURING_RENDER) return ;
1223
+ if (this._cachedRootElementParent === undefined) {
1224
+ var parent = this._cachedRootElementParent = this.rootElement.parentNode ;
1225
+ this._cachedRootElementNextSibling = this.rootElement.nextSibling ;
1226
+ if (parent) parent.removeChild(this.rootElement) ;
1227
+ }
1228
+ },
1229
+
1230
+ /** @private
1231
+ Re-adds root element into DOM if necessary. Inverts _removeRootElementFromDom().
1232
+ */
1233
+ _restoreRootElementInDom: function() {
1234
+ if (!SC.REMOVE_COLLECTION_ROOT_ELEMENT_DURING_RENDER) return ;
1235
+ if (this._cachedRootElementParent) {
1236
+ this._cachedRootElementParent.insertBefore(this.rootElement, this._cachedRootElementNextSibling);
1237
+ }
1238
+ this._cachedRootElementParent = this._cachedRootElementNextSibling = null ;
1239
+ },
1240
+
1241
+
1242
+ // ......................................
1243
+ // SELECTION
1244
+ //
1245
+
1246
+ _indexOfSelectionTop: function() {
1247
+ var content = this.get('content');
1248
+ var sel = this.get('selection');
1249
+ if (!content || !sel) return - 1;
1250
+
1251
+ // find the first item in the selection
1252
+ var contentLength = content.get('length') ;
1253
+ var indexOfSelected = contentLength ; var idx = sel.length ;
1254
+ while(--idx >= 0) {
1255
+ var curIndex = content.indexOf(sel[idx]) ;
1256
+ if ((curIndex >= 0) && (curIndex < indexOfSelected)) indexOfSelected = curIndex ;
1257
+ }
1148
1258
 
1149
1259
  return (indexOfSelected >= contentLength) ? -1 : indexOfSelected ;
1150
1260
  },
@@ -1191,11 +1301,11 @@ SC.CollectionView = SC.View.extend(
1191
1301
 
1192
1302
  // If the selBottom is after the anchor, then reduce the selection
1193
1303
  if (selBottom > anchor) {
1194
- selBottom-- ;
1304
+ selBottom = selBottom - numberOfItems ;
1195
1305
 
1196
1306
  // otherwise, select the previous item from the top
1197
1307
  } else {
1198
- selTop-- ;
1308
+ selTop = selTop - numberOfItems ;
1199
1309
  }
1200
1310
 
1201
1311
  // Ensure we are not out of bounds
@@ -1204,7 +1314,7 @@ SC.CollectionView = SC.View.extend(
1204
1314
 
1205
1315
  // if not extending, just select the item previous to the selTop
1206
1316
  } else {
1207
- selTop = this._indexOfSelectionTop() - 1;
1317
+ selTop = this._indexOfSelectionTop() - numberOfItems;
1208
1318
  if (selTop < 0) selTop = 0 ;
1209
1319
  selBottom = selTop ;
1210
1320
  anchor = null ;
@@ -1218,7 +1328,7 @@ SC.CollectionView = SC.View.extend(
1218
1328
 
1219
1329
  // ensure that the item is visible and set the selection
1220
1330
  if (items.length > 0) {
1221
- this.scrollToItemRecord(items.first());
1331
+ this.scrollToContent(items.first());
1222
1332
  this.selectItems(items);
1223
1333
  }
1224
1334
 
@@ -1253,11 +1363,11 @@ SC.CollectionView = SC.View.extend(
1253
1363
 
1254
1364
  // If the selTop is before the anchor, then reduce the selection
1255
1365
  if (selTop < anchor) {
1256
- selTop++ ;
1366
+ selTop = selTop + numberOfItems ;
1257
1367
 
1258
1368
  // otherwise, select the next item after the top
1259
1369
  } else {
1260
- selBottom++ ;
1370
+ selBottom = selBottom + numberOfItems ;
1261
1371
  }
1262
1372
 
1263
1373
  // Ensure we are not out of bounds
@@ -1266,7 +1376,7 @@ SC.CollectionView = SC.View.extend(
1266
1376
 
1267
1377
  // if not extending, just select the item next to the selBottom
1268
1378
  } else {
1269
- selBottom = this._indexOfSelectionBottom() + 1;
1379
+ selBottom = this._indexOfSelectionBottom() + numberOfItems;
1270
1380
  if (selBottom >= contentLength) selBottom = contentLength-1;
1271
1381
  selTop = selBottom ;
1272
1382
  anchor = null ;
@@ -1280,7 +1390,7 @@ SC.CollectionView = SC.View.extend(
1280
1390
 
1281
1391
  // ensure that the item is visible and set the selection
1282
1392
  if (items.length > 0) {
1283
- this.scrollToItemRecord(items.first());
1393
+ this.scrollToContent(items.first());
1284
1394
  this.selectItems(items);
1285
1395
  }
1286
1396
 
@@ -1292,9 +1402,16 @@ SC.CollectionView = SC.View.extend(
1292
1402
  * @param {SC.Record} record The record to scroll to
1293
1403
  * @returns {void}
1294
1404
  */
1295
- scrollToItemRecord: function( record )
1296
- {
1297
- this.scrollToItemView( this.itemViewForContent(record) );
1405
+ scrollToContent: function(record) {
1406
+ // find the itemView. if not present, add one.
1407
+ var itemView = this.itemViewForContent(record) ;
1408
+ if (!itemView) {
1409
+ var content = Array.from(this.get('content')) ;
1410
+ var contentIndex = content.indexOf(record) ;
1411
+ var groupBy = this.get('groupBy');
1412
+ itemView = this._insertItemViewFor(itemView, groupBy, contentIndex);
1413
+ }
1414
+ if (itemView) this.scrollToItemView(itemView);
1298
1415
  },
1299
1416
  /**
1300
1417
  * Scroll the rootElement (if needed) to ensure that the item is visible.
@@ -1303,24 +1420,13 @@ SC.CollectionView = SC.View.extend(
1303
1420
  */
1304
1421
  scrollToItemView: function( view )
1305
1422
  {
1306
- var visible = Element.extend(this.get('rootElement'));
1307
- var visibleTop = visible.scrollTop;
1308
- var visibleBottom = visibleTop + visible.getHeight();
1309
-
1310
- visible.makePositioned();
1311
-
1312
- var item = Element.extend(view.get('rootElement'));
1313
- var itemTop = item.positionedOffset().top;
1314
- var itemBottom = itemTop + item.getHeight();
1315
-
1316
- visible.undoPositioned();
1317
-
1318
- if (itemTop < visibleTop) {
1319
- visible.scrollTop = itemTop;
1320
- }
1321
- if (itemBottom > visibleBottom) {
1322
- visible.scrollTop += (itemBottom - visibleBottom);
1423
+ // find first scrollable view.
1424
+ var scrollable = this ;
1425
+ while(scrollable && (scrollable != SC.window) && (!scrollable.get('isScrollable'))) {
1426
+ scrollable = scrollable.get('parentNode') ;
1323
1427
  }
1428
+ if (!scrollable || (scrollable == SC.window)) return ; // no scrollable!
1429
+ scrollable.scrollToVisible(view) ;
1324
1430
  },
1325
1431
 
1326
1432
  /**
@@ -1382,7 +1488,7 @@ SC.CollectionView = SC.View.extend(
1382
1488
  Selects the previous item if itemsPerRow > 1. Otherwise does nothing.
1383
1489
  */
1384
1490
  moveLeft: function(sender, evt) {
1385
- if ((this.get('itemsPerRow') || 1) > 1) this.selectNextItem(false, 1) ;
1491
+ if ((this.get('itemsPerRow') || 1) > 1) this.selectPreviousItem(false, 1) ;
1386
1492
  return true ;
1387
1493
  },
1388
1494
 
@@ -1390,7 +1496,7 @@ SC.CollectionView = SC.View.extend(
1390
1496
  Selects the next item if itemsPerRow > 1. Otherwise does nothing.
1391
1497
  */
1392
1498
  moveRight: function(sender, evt) {
1393
- if ((this.get('itemsPerRow') || 1) > 1) this.selectPreviousItem(false, 1) ;
1499
+ if ((this.get('itemsPerRow') || 1) > 1) this.selectNextItem(false, 1) ;
1394
1500
  return true ;
1395
1501
  },
1396
1502
 
@@ -1408,7 +1514,7 @@ SC.CollectionView = SC.View.extend(
1408
1514
  Selects the previous item if itemsPerRow > 1. Otherwise does nothing.
1409
1515
  */
1410
1516
  moveLeftAndModifySelection: function(sender, evt) {
1411
- if ((this.get('itemsPerRow') || 1) > 1) this.selectNextItem(true, 1) ;
1517
+ if ((this.get('itemsPerRow') || 1) > 1) this.selectPreviousItem(true, 1) ;
1412
1518
  return true ;
1413
1519
  },
1414
1520
 
@@ -1416,82 +1522,12 @@ SC.CollectionView = SC.View.extend(
1416
1522
  Selects the next item if itemsPerRow > 1. Otherwise does nothing.
1417
1523
  */
1418
1524
  moveRightAndModifySelection: function(sender, evt) {
1419
- if ((this.get('itemsPerRow') || 1) > 1) this.selectPreviousItem(true, 1) ;
1525
+ if ((this.get('itemsPerRow') || 1) > 1) this.selectNextItem(true, 1) ;
1420
1526
  return true ;
1421
1527
  },
1422
1528
 
1423
-
1424
- /**
1425
- Find the item view underneath the passed mouse location.
1426
-
1427
- The default implementation of this method simply searches each item view's
1428
- frame to find one that includes the location. If you are doing your own
1429
- layout, you may be able to perform this calculation more quickly. If so,
1430
- consider overriding this method for better performance during drag operations.
1431
-
1432
- @param {Point} loc The current mouse location in the coordinate of the
1433
- collection view
1434
-
1435
- @returns {SC.View} The item view under the collection
1436
- */
1437
- itemViewAtLocation: function(loc) {
1438
- var content = this.get('content') ;
1439
- var idx = content.length;
1440
- while(--idx >= 0) {
1441
- var itemView = this.itemViewForContent(content.objectAt(idx));
1442
- var frame = itemView.get('frame');
1443
- if (SC.pointInRect(loc, frame)) return itemView ;
1444
- }
1445
- return null; // not in an itemView right now.
1446
- },
1447
-
1448
-
1449
-
1450
- /**
1451
- Find the first content item view for the passed event.
1452
-
1453
- This method will go up the view chain, starting with the view that was the target
1454
- of the passed event, looking for a child item. This will become the view that
1455
- is selected by the mouse event.
1456
-
1457
- This method only works for mouseDown & mouseUp events. mouseMoved events do
1458
- not have a target.
1459
-
1460
- @param {Event} evt An event
1461
-
1462
- */
1463
- itemViewForEvent: function(evt)
1464
- {
1465
- var view = SC.window.firstViewForEvent( evt );
1466
- // work up the view hierarchy to find a match...
1467
- do {
1468
- // item clicked was the ContainerView itself... i.e. the user clicked outside the child items
1469
- // nothing to return...
1470
- if ( view == this ) return null;
1471
-
1472
- // sweet!... the view is not only in the collection, but it says we can hit it.
1473
- // hit it and quit it...
1474
- if ( this.hasItemView(view) && (!view.hitTest || view.hitTest(evt)) ) return view;
1475
- } while ( view = view.get('parentNode') );
1476
-
1477
- // nothing was found...
1478
- return null;
1479
- },
1480
-
1481
-
1482
- didMouseDown: function(ev) {
1483
- console.warn("didMouseDown will be removed from CollectionView in the near future. Use mouseDown instead");
1484
- return this._mouseDown(ev, true);
1485
- },
1486
-
1487
1529
  mouseDown: function(ev) {
1488
- // older code might still use didMouseDown. Warn to give people some time to transition.
1489
- if (this.didMouseDown != SC.CollectionView.prototype.didMouseDown) {
1490
- return this.didMouseDown(ev) ;
1491
- } else return this._mouseDown(ev);
1492
- },
1493
-
1494
- _mouseDown: function(ev) {
1530
+
1495
1531
  // save for drag opt
1496
1532
  this._mouseDownEvent = ev ;
1497
1533
 
@@ -1499,11 +1535,14 @@ SC.CollectionView = SC.View.extend(
1499
1535
  if (this.useToggleSelection) return true;
1500
1536
 
1501
1537
  // Make sure that saved mouseDown state is always reset in case we do
1502
- // not get a paired mouseUp. (Only happens if subclass does not call us like it should)
1503
- this._mouseDownAt = this._shouldDeselect = this._shouldReselect = this._refreshSelection = false;
1538
+ // not get a paired mouseUp. (Only happens if subclass does not call us
1539
+ // like it should)
1540
+ this._mouseDownAt = this._shouldDeselect =
1541
+ this._shouldReselect = this._refreshSelection = false;
1504
1542
 
1505
1543
  var mouseDownView = this._mouseDownView = this.itemViewForEvent(ev);
1506
- var mouseDownContent = this._mouseDownContent = (mouseDownView) ? mouseDownView.get('content') : null;
1544
+ var mouseDownContent =
1545
+ this._mouseDownContent = (mouseDownView) ? mouseDownView.get('content') : null;
1507
1546
 
1508
1547
  // become first responder if possible.
1509
1548
  this.becomeFirstResponder() ;
@@ -1540,176 +1579,463 @@ SC.CollectionView = SC.View.extend(
1540
1579
  } else if (!modifierKeyPressed && isSelected) {
1541
1580
  this._shouldReselect = mouseDownContent;
1542
1581
 
1543
- // Otherwise, simply select the clicked on item, adding it to the current
1544
- // selection if a modifier key was pressed.
1545
- } else {
1546
- this.selectItems(mouseDownContent, modifierKeyPressed);
1582
+ // Otherwise, simply select the clicked on item, adding it to the current
1583
+ // selection if a modifier key was pressed.
1584
+ } else {
1585
+ this.selectItems(mouseDownContent, modifierKeyPressed);
1586
+ }
1587
+
1588
+ // saved for extend by shift ops.
1589
+ this._previousMouseDownContent = mouseDownContent;
1590
+ return true;
1591
+ },
1592
+
1593
+ mouseUp: function(ev) {
1594
+
1595
+ var canAct = this.get('actOnSelect') ;
1596
+ var view = this.itemViewForEvent(ev) ;
1597
+
1598
+ if (this.useToggleSelection) {
1599
+ if (!view) return ; // do nothing when clicked outside of elements
1600
+
1601
+ // determine if item is selected. If so, then go on.
1602
+ var selection = this.get('selection') || [] ;
1603
+ var content = (view) ? view.get('content') : null ;
1604
+ var isSelected = selection.include(content) ;
1605
+ if (isSelected) {
1606
+ this.deselectItems([content]) ;
1607
+ } else this.selectItems([content],true) ;
1608
+
1609
+ } else {
1610
+ if (this._shouldDeselect) this.deselectItems(this._shouldDeselect);
1611
+ if (this._shouldReselect) this.selectItems(this._shouldReselect,false) ;
1612
+
1613
+ // this is invoked if the user clicked on a checkbox. If this is not
1614
+ // done then the checkbox might not update properly.
1615
+ if (this._refreshSelection) {
1616
+ }
1617
+ this._cleanupMouseDown() ;
1618
+ }
1619
+
1620
+ this._mouseDownEvent = null ;
1621
+ if (canAct) this._action(ev, view) ;
1622
+
1623
+ return false; // bubble event to allow didDoubleClick to be called...
1624
+ },
1625
+
1626
+ _cleanupMouseDown: function() {
1627
+ this._mouseDownAt = this._shouldDeselect = this._shouldReselect = this._refreshSelection = false;
1628
+ this._mouseDownEvent = this._mouseDownContent = this._mouseDownView = null ;
1629
+ },
1630
+
1631
+ mouseMoved: function(ev) {
1632
+ var view = this.itemViewForEvent(ev) ;
1633
+ // handle hover events.
1634
+ if(this._lastHoveredItem && ((view === null) || (view != this._lastHoveredItem)) && this._lastHoveredItem.mouseOut) {
1635
+ this._lastHoveredItem.mouseOut(ev);
1636
+ }
1637
+ this._lastHoveredItem = view ;
1638
+ if (view && view.mouseOver) view.mouseOver(ev) ;
1639
+ },
1640
+
1641
+ mouseOut: function(ev) {
1642
+
1643
+ var view = this._lastHoveredItem ;
1644
+ this._lastHoveredItem = null ;
1645
+ if (view && view.didMouseOut) view.didMouseOut(ev) ;
1646
+ },
1647
+
1648
+ // invoked when the user double clicks on an item.
1649
+ didDoubleClick: function(ev) {
1650
+ console.warn("didDoubleClick will be removed from CollectionView in the near future. Use mouseOut instead");
1651
+ return this._doubleClick(ev) ;
1652
+ },
1653
+
1654
+ doubleClick: function(ev) {
1655
+ if (this.didDoubleClick != SC.CollectionView.prototype.didDoubleClick) {
1656
+ return this.didDoubleClick(ev) ;
1657
+ } else return this._doubleClick(ev) ;
1658
+ },
1659
+
1660
+ _doubleClick: function(ev) {
1661
+ console.info('_doubleClick!') ;
1662
+ var view = this.itemViewForEvent(ev) ;
1663
+ if (view) {
1664
+ this._action(view, ev) ;
1665
+ return true ;
1666
+ } else return false ;
1667
+ },
1668
+
1669
+ _findSelectionExtendedByShift: function(selection, mouseDownContent) {
1670
+ var collection = this.get('content');
1671
+
1672
+ // bounds of the collection...
1673
+ var collectionLowerBounds = 0;
1674
+ var collectionUpperBounds = (collection.get('length') - 1);
1675
+
1676
+ var selectionBeginIndex = collection.indexOf(selection.first());
1677
+ var selectionEndIndex = collection.indexOf(selection.last());
1678
+
1679
+ var previousMouseDownIndex = collection.indexOf(this._previousMouseDownContent);
1680
+ // _previousMouseDownContent couldn't be found... either it hasn't been set yet or the record has been deleted by the user
1681
+ // fall back to the first selected item.
1682
+ if (previousMouseDownIndex == -1) previousMouseDownIndex = selectionBeginIndex;
1683
+
1684
+
1685
+ var currentMouseDownIndex = collection.indexOf(mouseDownContent);
1686
+ // sanity check...
1687
+ if (currentMouseDownIndex == -1) throw "Unable to extend selection to an item that's not in the collection!";
1688
+
1689
+ // clicked before the current selection set... extend it's beginning...
1690
+ if (currentMouseDownIndex < selectionBeginIndex) selectionBeginIndex = currentMouseDownIndex;
1691
+ // clicked after the current selection set... extend it's ending...
1692
+ if (currentMouseDownIndex > selectionEndIndex) selectionEndIndex = currentMouseDownIndex;
1693
+ // clicked inside the selection set... need to determine where the las
1694
+ if ((currentMouseDownIndex > selectionBeginIndex) && (currentMouseDownIndex < selectionEndIndex))
1695
+ {
1696
+ if (currentMouseDownIndex == previousMouseDownIndex) {
1697
+ selectionBeginIndex = currentMouseDownIndex;
1698
+ selectionEndIndex = currentMouseDownIndex;
1699
+ } else if (currentMouseDownIndex > previousMouseDownIndex) {
1700
+ selectionBeginIndex = previousMouseDownIndex;
1701
+ selectionEndIndex = currentMouseDownIndex;
1702
+ } else if (currentMouseDownIndex < previousMouseDownIndex){
1703
+ selectionBeginIndex = currentMouseDownIndex;
1704
+ selectionEndIndex = previousMouseDownIndex;
1705
+ }
1706
+ }
1707
+ // slice doesn't include the last index passed... silly..
1708
+ selectionEndIndex++;
1709
+
1710
+ // shouldn't need to sanity check that the selection is in bounds due to the indexOf checks above...
1711
+ // I'll have faith that indexOf hasn't lied to me...
1712
+ return collection.slice(selectionBeginIndex, selectionEndIndex);
1713
+ },
1714
+
1715
+
1716
+ // ......................................
1717
+ // FIRST RESPONDER
1718
+ //
1719
+
1720
+ /**
1721
+ Called whenever the collection becomes first responder.
1722
+ Adds the focused class to the element.
1723
+ */
1724
+ didBecomeFirstResponder: function() {
1725
+ this.addClassName('focus') ;
1726
+ },
1727
+
1728
+ willLoseFirstResponder: function() {
1729
+ this.removeClassName('focus');
1730
+ },
1731
+
1732
+ // ......................................
1733
+ // DRAG AND DROP SUPPORT
1734
+ //
1735
+
1736
+ /**
1737
+ The insertion orientation. This is used to determine which
1738
+ dimension we should pay attention to when determining insertion point for
1739
+ a mouse click.
1740
+
1741
+ {{{
1742
+ SC.HORIZONTAL_ORIENTATION: look at the X dimension only
1743
+ SC.VERTICAL_ORIENTATION: look at the Y dimension only
1744
+ }}}
1745
+ */
1746
+ insertionOrientation: SC.HORIZONTAL_ORIENTATION,
1747
+
1748
+ /**
1749
+ Get the preferred insertion point for the given location, including
1750
+ an insertion preference of before or after the named index.
1751
+
1752
+ The default implementation will loop through the item views looking for
1753
+ the first view to "switch sides" in the orientation you specify.
1754
+ */
1755
+ insertionIndexForLocation: function(loc) {
1756
+ var content = this.get('content') ;
1757
+ var f, itemView, curSide, lastSide = null ;
1758
+ var orient = this.get('insertionOrientation') ;
1759
+ var ret= null ;
1760
+ for(var idx=0; ((ret == null) && (idx<content.length)); idx++) {
1761
+ itemView = this.itemViewForContent(content.objectAt(idx));
1762
+ f = this.convertFrameFromView(itemView.get('frame'), itemView) ;
1763
+
1764
+ // if we are a horizontal orientation, look for the first item that
1765
+ // will "switch sides" on the x path an the maxY is greater than Y.
1766
+ // This assumes you will flow top to bottom, but it should work if you
1767
+ // flow LTR or RTL.
1768
+ if (orient == SC.HORIZONTAL_ORIENTATION) {
1769
+ if (SC.maxY(f) > loc.y) {
1770
+ curSide = (SC.maxX(f) < loc.x) ? -1 : 1 ;
1771
+ } else curSide = null ;
1772
+
1773
+ // if we are a vertical orientation, look for the first item that
1774
+ // will "swithc sides" on the y path and the maxX is greater than X.
1775
+ // This assumes you will flow LTR, but it should work if you flow
1776
+ // bottom to top or top to bottom.
1777
+ } else {
1778
+ if (SC.minX(f) < loc.x) {
1779
+ curSide = (SC.maxY(f) < loc.y) ? -1 : 1 ;
1780
+ } else curSide = null ;
1781
+ }
1782
+
1783
+ // if we "switched" sides then return this item view.
1784
+ if (curSide !== null) {
1785
+
1786
+ // OK, we found an item view, while we have this data, decide if
1787
+ // we should insert before or after the view
1788
+ if ((lastSide !== null) && (curSide != lastSide)) {
1789
+ ret = idx ;
1790
+ if (orient == SC.HORIZONTAL_ORIENTATION) {
1791
+ if (SC.midX(f) < loc.x) ret++ ;
1792
+ } else {
1793
+ if (SC.midY(f) < loc.y) ret++ ;
1794
+ }
1795
+ }
1796
+ lastSide =curSide ;
1797
+ }
1798
+ }
1799
+
1800
+ // Handle some edge cases
1801
+ if ((ret == null) || (ret < 0)) ret = 0 ;
1802
+ if (ret > content.length) ret = content.length ;
1803
+
1804
+ // Done. Phew. Return.
1805
+ return ret;
1806
+ },
1807
+
1808
+ /**
1809
+ Override to show the insertion point during a drag.
1810
+
1811
+ Called during a drag to show the insertion point. Passed value is the
1812
+ item view that you should display the insertion point before. If the
1813
+ passed value is null, then you should show the insertion point AFTER that
1814
+ last item view returned by the itemViews property.
1815
+
1816
+ Once this method is called, you are guaranteed to also recieve a call to
1817
+ hideInsertionPoint() at some point in the future.
1818
+
1819
+ The default implementation of this method does nothing.
1820
+
1821
+ @param {SC.View} itemView view the insertion point should appear directly before. If null, show insertion point at end.
1822
+
1823
+ @returns {void}
1824
+ */
1825
+ showInsertionPointBefore: function(itemView) {},
1826
+
1827
+ /**
1828
+ Override to hide the insertion point when a drag ends.
1829
+
1830
+ Called during a drag to hide the insertion point. This will be called when the
1831
+ user exits the view, cancels the drag or completes the drag. It will not be
1832
+ called when the insertion point changes during a drag.
1833
+
1834
+ You should expect to receive one or more calls to showInsertionPointBefore()
1835
+ during a drag followed by at least one call to this method at the end. Your
1836
+ method should not raise an error if it is called more than once.
1837
+
1838
+ @returns {void}
1839
+ */
1840
+ hideInsertionPoint: function() {},
1841
+
1842
+ /**
1843
+ Override this method to provide your own ghost image for a drag.
1844
+
1845
+ Note that the only purpose of this view is to render a visible drag element. It is
1846
+ not critical that you make this element bindable, etc.
1847
+
1848
+ @param dragContent {Array} Array of content objects that will be used in the drag.
1849
+ */
1850
+ ghostViewFor: function(dragContent) {
1851
+ var view = SC.View.create() ;
1852
+ view.setStyle({ position: 'absolute', overflow: 'hidden' });
1853
+
1854
+ var viewFrame = this.convertFrameToView(this.get('frame'), null) ;
1855
+ view.set('frame', viewFrame) ;
1856
+
1857
+ var idx = dragContent.length ;
1858
+ var maxX = 0; var maxY = 0 ; var minX =100000; var minY = 100000 ;
1859
+
1860
+ while(--idx >= 0) {
1861
+ var itemView = this.itemViewForContent(dragContent[idx]) ;
1862
+ if (!itemView) continue ;
1863
+ var f = itemView.get('frame') ;
1864
+ var dom = itemView.rootElement ;
1865
+ if (!dom) continue ;
1866
+
1867
+ // save the maxX & maxY. This will be used to trim the size
1868
+ // of the ghost view later.
1869
+ if (SC.maxX(f) > maxX) maxX = SC.maxX(f) ;
1870
+ if (SC.maxY(f) > maxY) maxY = SC.maxY(f) ;
1871
+ if (SC.minX(f) < minX) minX = SC.minX(f) ;
1872
+ if (SC.minY(f) < minY) minY = SC.minY(f) ;
1873
+
1874
+ // Clone the contents of this node. We should probably apply the
1875
+ // computed style to the cloned nodes in order to make sure they match even if the
1876
+ // CSS styles do not match. Make sure the items are properly
1877
+ // positioned.
1878
+ dom = dom.cloneNode(true) ;
1879
+ 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) }) ;
1880
+ view.rootElement.appendChild(dom) ;
1881
+ }
1882
+
1883
+ // Now we have a view, create another view that will wrap the other view and position it
1884
+ // inside.
1885
+ var wrapper = SC.View.create() ;
1886
+ wrapper.setStyle({ position: 'absolute', overflow: 'hidden' }) ;
1887
+ wrapper.set('frame', {
1888
+ x: viewFrame.x+minX, y: viewFrame.y+minY,
1889
+ width: (maxX-minX+1), height: (maxY-minY+1)
1890
+ }) ;
1891
+ wrapper.appendChild(view) ;
1892
+ view.set('frame', { x: 0-minX, y: 0-minY }) ;
1893
+ return wrapper ;
1894
+ },
1895
+
1896
+ mouseDragged: function(ev) {
1897
+ // Don't do anything unless the user has been dragging for 123msec
1898
+ if ((Date.now() - this._mouseDownAt) < 123) return true ;
1899
+
1900
+ // OK, they must be serious, start a drag if possible.
1901
+ if (this.get('canReorderContent')) {
1902
+
1903
+ // First, get the selection to drag. Drag an array of selected
1904
+ // items appearing in this collection, in the order of the
1905
+ // collection.
1906
+ var content = this.get('content') || [] ;
1907
+ var dragContent = this.get('selection').sort(function(a,b) {
1908
+ a = content.indexOf(a) ; b = content.indexOf(b) ;
1909
+ return (a<b) ? -1 : ((a>b) ? 1 : 0) ;
1910
+ });
1911
+
1912
+ // Build the drag view to use for the ghost drag. This
1913
+ // should essentially contain any visible drag items.
1914
+ var view = this.ghostViewFor(dragContent) ;
1915
+
1916
+ // Initiate the drag
1917
+ SC.Drag.start({
1918
+ event: this._mouseDownEvent,
1919
+ source: this,
1920
+ dragView: view,
1921
+ ghost: NO,
1922
+ slideBack: YES,
1923
+ data: { "_mouseDownContent": dragContent }
1924
+ }) ;
1925
+
1926
+ // Also use this opportunity to clean up since mouseUp won't
1927
+ // get called.
1928
+ this._cleanupMouseDown() ;
1929
+ this._lastInsertionIndex = null ;
1547
1930
  }
1548
-
1549
- // saved for extend by shift ops.
1550
- this._previousMouseDownContent = mouseDownContent;
1551
- return true;
1552
1931
  },
1553
1932
 
1554
- // invoked when the user releases the mouse. based on the information saved
1555
- // during mouse down, we decide what to do.
1556
- didMouseUp: function(ev) {
1557
- console.warn("didMouseUp will be removed from CollectionView in the near future. Use mouseUp instead");
1558
- return this._mouseUp(ev);
1559
- },
1560
-
1561
- mouseUp: function(ev) {
1562
- if (this.didMouseUp != SC.CollectionView.prototype.didMouseUp) {
1563
- return this.didMouseUp(ev) ;
1564
- } else return this._mouseUp(ev) ;
1933
+ // Drop Source.
1934
+ dragEntered: function(drag, evt) {
1935
+ if ((drag.get('source') == this) && this.get('canReorderContent')) {
1936
+ return SC.DRAG_MOVE ;
1937
+ } else {
1938
+ return SC.DRAG_NONE ;
1939
+ }
1565
1940
  },
1566
1941
 
1567
- _mouseUp: function(ev) {
1568
-
1569
- var canAct = this.get('actOnSelect') ;
1570
- var view = this.itemViewForEvent(ev) ;
1571
-
1572
- if (this.useToggleSelection) {
1573
- if (!view) return ; // do nothing when clicked outside of elements
1574
-
1575
- // determine if item is selected. If so, then go on.
1576
- var selection = this.get('selection') || [] ;
1577
- var content = (view) ? view.get('content') : null ;
1578
- var isSelected = selection.include(content) ;
1579
- if (isSelected) {
1580
- this.deselectItems([content]) ;
1581
- } else this.selectItems([content],true) ;
1942
+ // If reordering is allowed, then show insertion point
1943
+ dragUpdated: function(drag, evt) {
1944
+ if (this.get('canReorderContent')) {
1945
+ var loc = drag.get('location') ;
1946
+ loc = this.convertFrameFromView(loc, null) ;
1582
1947
 
1583
- } else {
1584
- if (this._shouldDeselect) this.deselectItems(this._shouldDeselect);
1585
- if (this._shouldReselect) this.selectItems(this._shouldReselect,false) ;
1948
+ // get the insertion index for this location. This can be computed
1949
+ // by a subclass using whatever method. This method is not expected to
1950
+ // do any data valdidation, just to map the location to an insertion index.
1951
+ var ret = this.insertionIndexForLocation(loc) ;
1586
1952
 
1587
- // this is invoked if the user clicked on a checkbox. If this is not
1588
- // done then the checkbox might not update properly.
1589
- if (this._refreshSelection) {
1953
+ // now that we have an index, find the nearest index that we can
1954
+ // actually insert at, or do not allow.
1955
+ var objects = (drag.source == this) ? (drag.dataForType('_mouseDownContent') || []) : [];
1956
+ var content = this.get('content') || [] ;
1957
+
1958
+ // if the insertion index is in between two items in the drag itself,
1959
+ // then this is not allowed. Either use the last insertion index or
1960
+ // find the first index that is not in between selections.
1961
+ var isPreviousInDrag = (ret > 0) ? objects.indexOf(content.objectAt(ret-1)) : -1 ;
1962
+ var isNextInDrag = (ret < content.get('length')-1) ? objects.indexOf(content.objectAt(ret)) : -1 ;
1963
+ if (isPreviousInDrag>=0 && isNextInDrag>=0) {
1964
+ if (this._lastInsertionIndex == null) {
1965
+ while((ret > 0) && (objects.indexOf(content.objectAt(ret)) >= 0)) ret-- ;
1966
+ } else ret = this._lastInsertionIndex ;
1590
1967
  }
1591
- this._cleanupMouseDown() ;
1592
- }
1968
+
1969
+ // Now that we have verified that, check to see if a drop is allowed in the
1970
+ // insertion index with the delegate.
1971
+ // TODO
1593
1972
 
1594
- this._mouseDownEvent = null ;
1595
- if (canAct) this._action(ev, view) ;
1596
-
1597
- return false; // bubble event to allow didDoubleClick to be called...
1598
- },
1599
-
1600
- _cleanupMouseDown: function() {
1601
- this._mouseDownAt = this._shouldDeselect = this._shouldReselect = this._refreshSelection = false;
1602
- this._mouseDownEvent = this._mouseDownContent = this._mouseDownView = null ;
1603
- },
1604
-
1605
- // this can be used to initiate a drag. Only drags 100ms after mouseDown
1606
- // to avoid responding to clicks.
1607
- mouseDidMove: function(ev) {
1608
- console.warn("mouseDidMove will be removed from CollectionView in the near future. Use mouseMoved instead");
1609
- return this._mouseMoved(ev) ;
1610
- },
1611
-
1612
- mouseMoved: function(ev) {
1613
- if (this.mouseDidMove != SC.CollectionView.prototype.mouseDidMove) {
1614
- return this.mouseDidMove(ev) ;
1615
- } else return this._mouseMoved(ev) ;
1616
- },
1617
-
1618
- _mouseMoved: function(ev) {
1619
- var view = this.itemViewForEvent(ev) ;
1620
- // handle hover events.
1621
- if(this._lastHoveredItem && ((view === null) || (view != this._lastHoveredItem)) && this._lastHoveredItem.didMouseOut) {
1622
- this._lastHoveredItem.didMouseOut(ev);
1973
+ if (this._lastInsertionIndex != ret) {
1974
+ var itemView = this.itemViewForContent(this.get('content').objectAt(ret));
1975
+ this.showInsertionPointBefore(itemView) ;
1976
+ }
1977
+ this._lastInsertionIndex = ret ;
1978
+
1623
1979
  }
1624
- this._lastHoveredItem = view ;
1625
- if (view && view.didMouseOver) view.didMouseOver(ev) ;
1980
+ return SC.DRAG_MOVE;
1626
1981
  },
1627
1982
 
1628
- didMouseOut: function(ev) {
1629
- console.warn("didMouseOut will be removed from CollectionView in the near future. Use mouseOut instead");
1630
- return this._mouseOut(ev) ;
1631
- },
1632
-
1633
- mouseOut: function(ev) {
1634
- if (this.didMouseOut != SC.CollectionView.prototype.didMouseOut) {
1635
- return this.didMouseOut(ev) ;
1636
- } else return this._mouseOut(ev) ;
1637
- },
1638
-
1639
- _mouseOut: function(ev) {
1640
-
1641
- var view = this._lastHoveredItem ;
1642
- this._lastHoveredItem = null ;
1643
- if (view && view.didMouseOut) view.didMouseOut(ev) ;
1983
+ dragExited: function() {
1984
+ this.hideInsertionPoint() ;
1985
+ this._lastInsertionIndex = null ;
1644
1986
  },
1645
1987
 
1646
- // invoked when the user double clicks on an item.
1647
- didDoubleClick: function(ev) {
1648
- console.warn("didDoubleClick will be removed from CollectionView in the near future. Use mouseOut instead");
1649
- return this._doubleClick(ev) ;
1988
+ dragEnded: function() {
1989
+ this.hideInsertionPoint() ;
1990
+ this._lastInsertionIndex = null ;
1650
1991
  },
1651
1992
 
1652
- doubleClick: function(ev) {
1653
- if (this.didDoubleClick != SC.CollectionView.prototype.didDoubleClick) {
1654
- return this.didDoubleClick(ev) ;
1655
- } else return this._doubleClick(ev) ;
1993
+ prepareForDragOperation: function(op, drag) {
1994
+ return SC.DRAG_ANY;
1656
1995
  },
1657
1996
 
1658
- _doubleClick: function(ev) {
1659
- console.info('_doubleClick!') ;
1660
- var view = this.itemViewForEvent(ev) ;
1661
- if (view) {
1662
- this._action(view, ev) ;
1663
- return true ;
1664
- } else return false ;
1665
- },
1666
-
1667
- _findSelectionExtendedByShift: function(selection, mouseDownContent) {
1668
- var collection = this.get('content');
1669
-
1670
- // bounds of the collection...
1671
- var collectionLowerBounds = 0;
1672
- var collectionUpperBounds = (collection.get('length') - 1);
1673
-
1674
- var selectionBeginIndex = collection.indexOf(selection.first());
1675
- var selectionEndIndex = collection.indexOf(selection.last());
1676
-
1677
- var previousMouseDownIndex = collection.indexOf(this._previousMouseDownContent);
1678
- // _previousMouseDownContent couldn't be found... either it hasn't been set yet or the record has been deleted by the user
1679
- // fall back to the first selected item.
1680
- if (previousMouseDownIndex == -1) previousMouseDownIndex = selectionBeginIndex;
1997
+ performDragOperation: function(op, drag) {
1998
+
1999
+ SC.Benchmark.start('%@ performDragOperation'.fmt(SC.guidFor(this))) ;
2000
+
2001
+ var loc = drag.get('location') ;
2002
+ loc = this.convertFrameFromView(loc, null) ;
2003
+
2004
+ // if op is MOVE or COPY, add item to view.
2005
+ var objects = drag.dataForType('_mouseDownContent') ;
2006
+ if (objects && (op == SC.DRAG_MOVE)) {
1681
2007
 
2008
+ // find the index to for the new insertion
2009
+ var idx = this.insertionIndexForLocation(loc) ;
1682
2010
 
1683
- var currentMouseDownIndex = collection.indexOf(mouseDownContent);
1684
- // sanity check...
1685
- if (currentMouseDownIndex == -1) throw "Unable to extend selection to an item that's not in the collection!";
2011
+ var content = this.get('content') ;
2012
+ content.beginPropertyChanges(); // suspend notifications
1686
2013
 
1687
- // clicked before the current selection set... extend it's beginning...
1688
- if (currentMouseDownIndex < selectionBeginIndex) selectionBeginIndex = currentMouseDownIndex;
1689
- // clicked after the current selection set... extend it's ending...
1690
- if (currentMouseDownIndex > selectionEndIndex) selectionEndIndex = currentMouseDownIndex;
1691
- // clicked inside the selection set... need to determine where the las
1692
- if ((currentMouseDownIndex > selectionBeginIndex) && (currentMouseDownIndex < selectionEndIndex))
1693
- {
1694
- if (currentMouseDownIndex == previousMouseDownIndex) {
1695
- selectionBeginIndex = currentMouseDownIndex;
1696
- selectionEndIndex = currentMouseDownIndex;
1697
- } else if (currentMouseDownIndex > previousMouseDownIndex) {
1698
- selectionBeginIndex = previousMouseDownIndex;
1699
- selectionEndIndex = currentMouseDownIndex;
1700
- } else if (currentMouseDownIndex < previousMouseDownIndex){
1701
- selectionBeginIndex = currentMouseDownIndex;
1702
- selectionEndIndex = previousMouseDownIndex;
2014
+ // find the old index and remove it.
2015
+ var objectsIdx = objects.get('length') ;
2016
+ while(--objectsIdx >= 0) {
2017
+ var obj = objects.objectAt(objectsIdx) ;
2018
+ var old = content.indexOf(obj) ;
2019
+ if (old >= 0) content.removeAt(old) ;
2020
+ if ((old >= 0) && (old < idx)) idx--; //adjust idx
1703
2021
  }
2022
+
2023
+ // now insert objects at new location
2024
+ content.replace(idx, 0, objects) ;
2025
+ content.endPropertyChanges(); // restart notifications
1704
2026
  }
1705
- // slice doesn't include the last index passed... silly..
1706
- selectionEndIndex++;
1707
-
1708
- // shouldn't need to sanity check that the selection is in bounds due to the indexOf checks above...
1709
- // I'll have faith that indexOf hasn't lied to me...
1710
- return collection.slice(selectionBeginIndex, selectionEndIndex);
2027
+
2028
+ SC.Benchmark.end('%@ performDragOperation'.fmt(SC.guidFor(this))) ;
2029
+ console.log(SC.Benchmark.report()) ;
2030
+
2031
+ return SC.DRAG_MOVE;
1711
2032
  },
1712
2033
 
2034
+ concludeDragOperation: function(op, drag) {
2035
+ this.hideInsertionPoint() ;
2036
+ this._lastInsertionIndex = null ;
2037
+ },
2038
+
1713
2039
 
1714
2040
 
1715
2041
  // ......................................
@@ -1717,20 +2043,24 @@ SC.CollectionView = SC.View.extend(
1717
2043
  //
1718
2044
 
1719
2045
  init: function() {
2046
+
2047
+ // Initialize internal hashes and arrays. Normally the best approach to this
2048
+ // is to initialize a property only when it is used. However, these properties
2049
+ // are critical to layout and therefore will always be needed so it is faster
2050
+ // to do it once here.
2051
+ this._itemViewsByContent= {};
2052
+ this._groupViewsByValue= {};
2053
+ this._groupViewCounts= {};
2054
+ this._zombieGroupViews= {};
2055
+ this._itemViewsByGuid = {} ;
2056
+
2057
+ this._itemViewPool= [];
2058
+ this._groupViewPool= [];
2059
+
1720
2060
  arguments.callee.base.apply(this, arguments) ;
1721
2061
  this._dropTargetObserver();
1722
2062
  },
1723
2063
 
1724
- // When canReorderContent changes, add or remove drop target as necessary.
1725
- _dropTargetObserver: function() {
1726
- var canDrop = this.get('canReorderContent') || this.get('isDropTarget') ;
1727
- if (canDrop) {
1728
- SC.Drag.addDropTarget(this) ;
1729
- } else {
1730
- SC.Drag.removeDropTarget(this) ;
1731
- }
1732
- }.observes('canReorderContent', 'isDropTarget'),
1733
-
1734
2064
  // Perform the action. Supports legacy behavior as well as newer style
1735
2065
  // action dispatch.
1736
2066
  _action: function(view, evt) {
@@ -1759,76 +2089,93 @@ SC.CollectionView = SC.View.extend(
1759
2089
  return view.action(evt) ;
1760
2090
  }
1761
2091
  },
1762
-
1763
- _viewsForContent: null,
1764
- _content: [], // cached for changes.
1765
- propertyObserver: function(observing,target,key,value)
1766
- {
1767
- if (target == this)
1768
- {
1769
- // update children when content changes.
1770
- if (key == 'content')
1771
- {
1772
- // cache the observer binding
1773
- if (!this._boundObserver)
1774
- {
1775
- this._boundObserver = this._contentPropertyObserver.bind(this);
1776
- }
1777
2092
 
1778
- // don't update the content unless it has changed. Note that if we
1779
- // get a new empty array, that doesn't count as a change from a prev
1780
- // empty array.
1781
- var isEqual = (
1782
- ((value && this._content) && (value.get('length') == 0) && (this._content.get('length') == 0)) ||
1783
- SC.isEqual( value, this._content)
1784
- );
1785
-
1786
- // remove and re-add the observer for "[]" before changing the content property
1787
- // this triggers a render of the child item views whenever the array is modified.
1788
- if (this._content && this._content.removeObserver) this._content.removeObserver('[]', this._boundObserver);
1789
- this._content = value;
1790
- if (this._content && this._content.addObserver) this._content.addObserver('[]', this._boundObserver);
1791
-
1792
- // only re-render the collection if the content was actually changed to a new value.
1793
- if (!isEqual)
1794
- {
1795
- this._contentPropertyObserver(target,key,value);
1796
- }
1797
-
1798
- // update selection when selection changes. set this as a timeout so
1799
- // that a render can finish first.
1800
- }
1801
- else if (key == 'selection')
1802
- {
1803
- if (!this._updatingSel)
1804
- {
1805
- this._updatingSel = this.invokeLater('_updateSelectionState',1);
1806
- }
1807
- }
2093
+ /** Add/remove from drop targets as needed. */
2094
+ _dropTargetObserver: function() {
2095
+ var canDrop = this.get('canReorderContent') || this.get('isDropTarget') ;
2096
+ if (canDrop) {
2097
+ SC.Drag.addDropTarget(this) ;
2098
+ } else {
2099
+ SC.Drag.removeDropTarget(this) ;
1808
2100
  }
1809
- },
2101
+ }.observes('canReorderContent', 'isDropTarget'),
2102
+
2103
+ /** @private
2104
+ Whenever content changes, update children and also start observing
2105
+ new [] property.
2106
+ */
2107
+ _contentObserver: function() {
2108
+ var content = this.get('content') ;
2109
+ if (SC.isEqual(content, this._content)) return ; // nothing to do
2110
+
2111
+ if (!this._boundContentPropertyObserver) {
2112
+ this._boundContentPropertyObserver = this._contentPropertyObserver.bind(this) ;
2113
+ }
2114
+ var func = this._boundContentPropertyObserver ;
2115
+
2116
+ // remove old observer, add new observer, and trigger content property change
2117
+ if (this._content) this._content.removeObserver('[]', func) ;
2118
+ if (content) content.addObserver('[]', func) ;
2119
+ this._content = content; //cache
2120
+ this._contentPropertyRevision = null ;
2121
+ this._contentPropertyObserver(this, '[]', content, content.propertyRevision) ;
2122
+ }.observes('content'),
2123
+
2124
+ /** @private
2125
+ Whenever the selection changes, update the itemViews.
2126
+ */
2127
+ _selectionObserver: function() {
2128
+ var sel = this.get('selection') ;
2129
+ if (SC.isEqual(sel, this._selection)) return ; // nothing to do
1810
2130
 
2131
+ if (!this._boundSelectionPropertyObserver) {
2132
+ this._boundSelectionPropertyObserver = this._selectionPropertyObserver.bind(this) ;
2133
+ }
2134
+ var func = this._boundSelectionPropertyObserver ;
2135
+
2136
+ if (this._selection) this._selection.removeObserver('[]', func) ;
2137
+ if (sel) sel.addObserver('[]', func) ;
2138
+ this._selection = sel ;
2139
+ this._selectionPropertyRevision = null ;
2140
+ var propertyRevision = (sel) ? sel.propertyRevision : null;
2141
+ this._selectionPropertyObserver(this, '[]', sel, propertyRevision) ;
2142
+ }.observes('selection'),
2143
+
1811
2144
  // called on content change *and* content.[] change...
1812
- _contentPropertyObserver: function(target,key,value)
1813
- {
1814
- if (!this._updating) {
1815
- this._updating = true;
1816
- this.set('isDirty',true);
1817
- this._resetExpiredRender();
1818
- this.updateChildren();
1819
- this._updating = false;
2145
+ // update children if this is a new propertyRevision
2146
+ //
2147
+ // UPDATE:
2148
+ // -- recheck all item views, add/remove children as needed
2149
+ // -- update layout on all item views.
2150
+ // -- optional: determine the first item view that does not match.
2151
+ //
2152
+ _contentPropertyObserver: function(target, key, value, rev) {
2153
+ if (!this._updatingContent && (!rev || (rev != this._contentPropertyRevision))) {
2154
+ this._contentPropertyRevision = rev ;
2155
+ this._updatingContent = true ;
2156
+ this._hasChildren = false ;
2157
+ this.updateChildren(true) ;
2158
+ this._updatingContent = false ;
1820
2159
  }
1821
2160
  },
1822
-
1823
- _updateSelectionState: function() {
1824
- try {
2161
+
2162
+ // called on selection change and selection.[] change...
2163
+ // update selection states if this is a new propertyRevision
2164
+ _selectionPropertyObserver: function(target, key, value, rev) {
2165
+ if (!this._updatingSel && (!rev || (rev != this._selectionPropertyRevision))) {
2166
+ this._selectionPropertyRevision = rev ;
2167
+ this._updatingSel = true ;
2168
+ this._selectionHash = null ; // flush cache
1825
2169
  this.updateSelectionStates() ;
1826
- } catch(e) {
1827
- console.log('exception while updating selection states in %@: %@'.format(this,e)) ;
2170
+ this._updatingSel = false ;
1828
2171
  }
1829
- this._updatingSel = null ;
1830
2172
  },
1831
2173
 
2174
+ // If isVisibleInWindow status changes, updateChildren if we are dirty.
2175
+ _isVisibleInWindowObserver: function() {
2176
+ if (this.get('isDirty')) this.updateChildren() ;
2177
+ }.observes('isVisibleInWindow'),
2178
+
1832
2179
  // ======================================================================
1833
2180
  // DEPRECATED APIS (Still available for compatibility)
1834
2181
 
@@ -1847,3 +2194,5 @@ SC.CollectionView = SC.View.extend(
1847
2194
 
1848
2195
 
1849
2196
  }) ;
2197
+
2198
+