sproutcore 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+