sproutcore 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (270) hide show
  1. data/History.txt +4 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +269 -0
  4. data/README.txt +67 -0
  5. data/Rakefile +4 -0
  6. data/app_generators/sproutcore/USAGE +5 -0
  7. data/app_generators/sproutcore/sproutcore_generator.rb +66 -0
  8. data/app_generators/sproutcore/templates/README +77 -0
  9. data/app_generators/sproutcore/templates/environment.yml +4 -0
  10. data/bin/sc-build +145 -0
  11. data/bin/sc-gen +24 -0
  12. data/bin/sc-server +63 -0
  13. data/bin/sproutcore +21 -0
  14. data/clients/sc_docs/controllers/docs.js +118 -0
  15. data/clients/sc_docs/core.js +19 -0
  16. data/clients/sc_docs/english.lproj/body.css +159 -0
  17. data/clients/sc_docs/english.lproj/body.rhtml +33 -0
  18. data/clients/sc_docs/english.lproj/controls.css +0 -0
  19. data/clients/sc_docs/english.lproj/icons/small/next.png +0 -0
  20. data/clients/sc_docs/english.lproj/icons/small/reset.png +0 -0
  21. data/clients/sc_docs/english.lproj/images/gradients.png +0 -0
  22. data/clients/sc_docs/english.lproj/images/indicator.gif +0 -0
  23. data/clients/sc_docs/english.lproj/images/toolbar.png +0 -0
  24. data/clients/sc_docs/english.lproj/no_docs.rhtml +7 -0
  25. data/clients/sc_docs/english.lproj/strings.js +14 -0
  26. data/clients/sc_docs/english.lproj/warning.rhtml +6 -0
  27. data/clients/sc_docs/fixtures/doc.js +11 -0
  28. data/clients/sc_docs/main.js +21 -0
  29. data/clients/sc_docs/models/doc.js +9 -0
  30. data/clients/sc_docs/tests/controllers/docs.rhtml +21 -0
  31. data/clients/sc_docs/tests/models/doc.rhtml +21 -0
  32. data/clients/sc_docs/tests/views/doc_frame.rhtml +21 -0
  33. data/clients/sc_docs/tests/views/doc_label_view.rhtml +21 -0
  34. data/clients/sc_docs/views/doc_frame.js +33 -0
  35. data/clients/sc_docs/views/doc_label.js +20 -0
  36. data/clients/sc_test_runner/controllers/runner.js +175 -0
  37. data/clients/sc_test_runner/core.js +19 -0
  38. data/clients/sc_test_runner/english.lproj/body.css +151 -0
  39. data/clients/sc_test_runner/english.lproj/body.rhtml +35 -0
  40. data/clients/sc_test_runner/english.lproj/controls.css +0 -0
  41. data/clients/sc_test_runner/english.lproj/icons/small/next.png +0 -0
  42. data/clients/sc_test_runner/english.lproj/icons/small/reset.png +0 -0
  43. data/clients/sc_test_runner/english.lproj/images/gradients.png +0 -0
  44. data/clients/sc_test_runner/english.lproj/images/indicator.gif +0 -0
  45. data/clients/sc_test_runner/english.lproj/images/toolbar.png +0 -0
  46. data/clients/sc_test_runner/english.lproj/no_tests.rhtml +6 -0
  47. data/clients/sc_test_runner/english.lproj/strings.js +14 -0
  48. data/clients/sc_test_runner/english.lproj/warning.rhtml +6 -0
  49. data/clients/sc_test_runner/fixtures/test.js +12 -0
  50. data/clients/sc_test_runner/main.js +26 -0
  51. data/clients/sc_test_runner/models/test.js +11 -0
  52. data/clients/sc_test_runner/views/runner_frame.js +72 -0
  53. data/clients/sc_test_runner/views/test_label.js +20 -0
  54. data/config/hoe.rb +70 -0
  55. data/config/requirements.rb +17 -0
  56. data/environment.yml +9 -0
  57. data/frameworks/prototype/prototype.js +4186 -0
  58. data/frameworks/sproutcore/Core.js +378 -0
  59. data/frameworks/sproutcore/README +3 -0
  60. data/frameworks/sproutcore/controllers/array.js +236 -0
  61. data/frameworks/sproutcore/controllers/collection.js +305 -0
  62. data/frameworks/sproutcore/controllers/controller.js +323 -0
  63. data/frameworks/sproutcore/controllers/object.js +372 -0
  64. data/frameworks/sproutcore/drag/drag.js +549 -0
  65. data/frameworks/sproutcore/drag/drag_data_source.js +32 -0
  66. data/frameworks/sproutcore/drag/drag_source.js +64 -0
  67. data/frameworks/sproutcore/drag/drop_target.js +153 -0
  68. data/frameworks/sproutcore/english.lproj/blank.gif +0 -0
  69. data/frameworks/sproutcore/english.lproj/buttons.css +589 -0
  70. data/frameworks/sproutcore/english.lproj/buttons.png +0 -0
  71. data/frameworks/sproutcore/english.lproj/inline_text_editor.css +21 -0
  72. data/frameworks/sproutcore/english.lproj/menu.css +121 -0
  73. data/frameworks/sproutcore/english.lproj/panels/background-fat.jpg +0 -0
  74. data/frameworks/sproutcore/english.lproj/panels/background-thin.jpg +0 -0
  75. data/frameworks/sproutcore/english.lproj/panels/bottom-edge.png +0 -0
  76. data/frameworks/sproutcore/english.lproj/panels/bottom-left-corner.png +0 -0
  77. data/frameworks/sproutcore/english.lproj/panels/bottom-right-corner.png +0 -0
  78. data/frameworks/sproutcore/english.lproj/panels/left-edge.png +0 -0
  79. data/frameworks/sproutcore/english.lproj/panels/overlay.png +0 -0
  80. data/frameworks/sproutcore/english.lproj/panels/right-edge.png +0 -0
  81. data/frameworks/sproutcore/english.lproj/panels/top-edge.png +0 -0
  82. data/frameworks/sproutcore/english.lproj/panels/top-left-corner.png +0 -0
  83. data/frameworks/sproutcore/english.lproj/panels/top-right-corner.png +0 -0
  84. data/frameworks/sproutcore/english.lproj/panes.css +155 -0
  85. data/frameworks/sproutcore/english.lproj/picker.css +22 -0
  86. data/frameworks/sproutcore/english.lproj/strings.js +15 -0
  87. data/frameworks/sproutcore/english.lproj/tab.css +23 -0
  88. data/frameworks/sproutcore/english.lproj/tests.css +67 -0
  89. data/frameworks/sproutcore/english.lproj/theme.css +77 -0
  90. data/frameworks/sproutcore/foundation/animator.js +670 -0
  91. data/frameworks/sproutcore/foundation/application.js +199 -0
  92. data/frameworks/sproutcore/foundation/array.js +348 -0
  93. data/frameworks/sproutcore/foundation/benchmark.js +211 -0
  94. data/frameworks/sproutcore/foundation/binding.js +384 -0
  95. data/frameworks/sproutcore/foundation/date.js +357 -0
  96. data/frameworks/sproutcore/foundation/error.js +39 -0
  97. data/frameworks/sproutcore/foundation/input_manager.js +153 -0
  98. data/frameworks/sproutcore/foundation/json.js +296 -0
  99. data/frameworks/sproutcore/foundation/mock.js +42 -0
  100. data/frameworks/sproutcore/foundation/node_descriptor.js +56 -0
  101. data/frameworks/sproutcore/foundation/object.js +777 -0
  102. data/frameworks/sproutcore/foundation/observable.js +451 -0
  103. data/frameworks/sproutcore/foundation/page.js +63 -0
  104. data/frameworks/sproutcore/foundation/path_module.js +413 -0
  105. data/frameworks/sproutcore/foundation/responder.js +310 -0
  106. data/frameworks/sproutcore/foundation/routes.js +371 -0
  107. data/frameworks/sproutcore/foundation/run_loop.js +21 -0
  108. data/frameworks/sproutcore/foundation/server.js +491 -0
  109. data/frameworks/sproutcore/foundation/set.js +96 -0
  110. data/frameworks/sproutcore/foundation/string.js +149 -0
  111. data/frameworks/sproutcore/foundation/undo_manager.js +186 -0
  112. data/frameworks/sproutcore/foundation/unittest.js +622 -0
  113. data/frameworks/sproutcore/foundation/utils.js +61 -0
  114. data/frameworks/sproutcore/globals/panels.js +182 -0
  115. data/frameworks/sproutcore/globals/popups.js +60 -0
  116. data/frameworks/sproutcore/globals/window.js +381 -0
  117. data/frameworks/sproutcore/lib/index.rhtml +66 -0
  118. data/frameworks/sproutcore/models/collection.js +395 -0
  119. data/frameworks/sproutcore/models/record.js +622 -0
  120. data/frameworks/sproutcore/models/store.js +295 -0
  121. data/frameworks/sproutcore/panes/dialog.js +16 -0
  122. data/frameworks/sproutcore/panes/manager.js +164 -0
  123. data/frameworks/sproutcore/panes/menu.js +45 -0
  124. data/frameworks/sproutcore/panes/overlay.js +231 -0
  125. data/frameworks/sproutcore/panes/pane.js +90 -0
  126. data/frameworks/sproutcore/panes/panel.js +19 -0
  127. data/frameworks/sproutcore/panes/picker.js +45 -0
  128. data/frameworks/sproutcore/tests/controllers/array.rhtml +86 -0
  129. data/frameworks/sproutcore/tests/controllers/controller.rhtml +273 -0
  130. data/frameworks/sproutcore/tests/controllers/object.rhtml +327 -0
  131. data/frameworks/sproutcore/tests/foundation/application.rhtml +125 -0
  132. data/frameworks/sproutcore/tests/foundation/array.rhtml +221 -0
  133. data/frameworks/sproutcore/tests/foundation/object.rhtml +69 -0
  134. data/frameworks/sproutcore/tests/globals/window.rhtml +45 -0
  135. data/frameworks/sproutcore/tests/panes/pane.rhtml +88 -0
  136. data/frameworks/sproutcore/tests/views/collection.rhtml +137 -0
  137. data/frameworks/sproutcore/tests/views/popup_button.rhtml +115 -0
  138. data/frameworks/sproutcore/tests/views/text_field.rhtml +37 -0
  139. data/frameworks/sproutcore/validators/credit_card.js +92 -0
  140. data/frameworks/sproutcore/validators/date.js +36 -0
  141. data/frameworks/sproutcore/validators/email.js +29 -0
  142. data/frameworks/sproutcore/validators/not_empty.js +24 -0
  143. data/frameworks/sproutcore/validators/number.js +55 -0
  144. data/frameworks/sproutcore/validators/password.js +78 -0
  145. data/frameworks/sproutcore/validators/validator.js +304 -0
  146. data/frameworks/sproutcore/views/button.js +425 -0
  147. data/frameworks/sproutcore/views/checkbox_field.js +30 -0
  148. data/frameworks/sproutcore/views/collection.js +1521 -0
  149. data/frameworks/sproutcore/views/container.js +62 -0
  150. data/frameworks/sproutcore/views/error_explanation.js +45 -0
  151. data/frameworks/sproutcore/views/field.js +214 -0
  152. data/frameworks/sproutcore/views/filter_button.js +29 -0
  153. data/frameworks/sproutcore/views/form.js +591 -0
  154. data/frameworks/sproutcore/views/image.js +141 -0
  155. data/frameworks/sproutcore/views/inline_text_editor.js +96 -0
  156. data/frameworks/sproutcore/views/label.js +176 -0
  157. data/frameworks/sproutcore/views/menu_item.js +90 -0
  158. data/frameworks/sproutcore/views/pagination.js +54 -0
  159. data/frameworks/sproutcore/views/popup_button.js +86 -0
  160. data/frameworks/sproutcore/views/popup_menu.js +137 -0
  161. data/frameworks/sproutcore/views/progress.js +100 -0
  162. data/frameworks/sproutcore/views/radio_field.js +107 -0
  163. data/frameworks/sproutcore/views/radio_group.js +48 -0
  164. data/frameworks/sproutcore/views/segmented.js +80 -0
  165. data/frameworks/sproutcore/views/select_field.js +272 -0
  166. data/frameworks/sproutcore/views/spinner.js +11 -0
  167. data/frameworks/sproutcore/views/tab.js +126 -0
  168. data/frameworks/sproutcore/views/text_field.js +179 -0
  169. data/frameworks/sproutcore/views/textarea_field.js +14 -0
  170. data/frameworks/sproutcore/views/toolbar.js +29 -0
  171. data/frameworks/sproutcore/views/view.js +1389 -0
  172. data/frameworks/sproutcore/views/workspace.js +170 -0
  173. data/generators/client/README +3 -0
  174. data/generators/client/USAGE +12 -0
  175. data/generators/client/client_generator.rb +53 -0
  176. data/generators/client/templates/core.js +19 -0
  177. data/generators/client/templates/english.lproj/body.css +0 -0
  178. data/generators/client/templates/english.lproj/body.rhtml +3 -0
  179. data/generators/client/templates/english.lproj/controls.css +0 -0
  180. data/generators/client/templates/english.lproj/strings.js +14 -0
  181. data/generators/client/templates/main.js +37 -0
  182. data/generators/controller/USAGE +16 -0
  183. data/generators/controller/controller_generator.rb +51 -0
  184. data/generators/controller/templates/controller.js +21 -0
  185. data/generators/controller/templates/test.rhtml +21 -0
  186. data/generators/framework/README +7 -0
  187. data/generators/framework/USAGE +12 -0
  188. data/generators/framework/framework_generator.rb +53 -0
  189. data/generators/framework/templates/core.js +20 -0
  190. data/generators/framework/templates/english.lproj/body.css +0 -0
  191. data/generators/framework/templates/english.lproj/body.rhtml +3 -0
  192. data/generators/framework/templates/english.lproj/controls.css +0 -0
  193. data/generators/framework/templates/english.lproj/strings.js +14 -0
  194. data/generators/language/USAGE +16 -0
  195. data/generators/language/language_generator.rb +47 -0
  196. data/generators/language/templates/strings.js +10 -0
  197. data/generators/model/USAGE +24 -0
  198. data/generators/model/model_generator.rb +55 -0
  199. data/generators/model/templates/fixture.js +11 -0
  200. data/generators/model/templates/model.js +20 -0
  201. data/generators/model/templates/test.rhtml +21 -0
  202. data/generators/test/USAGE +16 -0
  203. data/generators/test/templates/test.rhtml +21 -0
  204. data/generators/test/test_generator.rb +47 -0
  205. data/generators/view/USAGE +16 -0
  206. data/generators/view/templates/test.rhtml +21 -0
  207. data/generators/view/templates/view.js +20 -0
  208. data/generators/view/view_generator.rb +51 -0
  209. data/jsdoc/README.txt +119 -0
  210. data/jsdoc/app/DocFile.js +137 -0
  211. data/jsdoc/app/DocTag.js +110 -0
  212. data/jsdoc/app/Doclet.js +63 -0
  213. data/jsdoc/app/Dumper.js +143 -0
  214. data/jsdoc/app/JsDoc.js +103 -0
  215. data/jsdoc/app/JsHilite.js +45 -0
  216. data/jsdoc/app/JsIO.js +163 -0
  217. data/jsdoc/app/JsParse.js +385 -0
  218. data/jsdoc/app/JsPlate.js +130 -0
  219. data/jsdoc/app/JsTestrun.js +129 -0
  220. data/jsdoc/app/JsToke.js +564 -0
  221. data/jsdoc/app/Symbol.js +298 -0
  222. data/jsdoc/app/Transformer.js +14 -0
  223. data/jsdoc/app/Util.js +97 -0
  224. data/jsdoc/app/js.jar +0 -0
  225. data/jsdoc/app/run.js +144 -0
  226. data/jsdoc/plugins/min.js +316 -0
  227. data/jsdoc/plugins/strip.js +20 -0
  228. data/jsdoc/templates/sproutcore/class.tmpl +438 -0
  229. data/jsdoc/templates/sproutcore/default.css +241 -0
  230. data/jsdoc/templates/sproutcore/index.html +13 -0
  231. data/jsdoc/templates/sproutcore/index.tmpl +21 -0
  232. data/jsdoc/templates/sproutcore/prototype.js +4186 -0
  233. data/jsdoc/templates/sproutcore/publish.js +236 -0
  234. data/jsdoc/templates/sproutcore/splash.html +7 -0
  235. data/lib/sproutcore/build_tools/html_builder.rb +88 -0
  236. data/lib/sproutcore/build_tools/resource_builder.rb +194 -0
  237. data/lib/sproutcore/build_tools.rb +44 -0
  238. data/lib/sproutcore/bundle.rb +517 -0
  239. data/lib/sproutcore/bundle_manifest.rb +397 -0
  240. data/lib/sproutcore/generator_helper.rb +170 -0
  241. data/lib/sproutcore/helpers/capture_helper.rb +42 -0
  242. data/lib/sproutcore/helpers/static_helper.rb +80 -0
  243. data/lib/sproutcore/helpers/tag_helper.rb +110 -0
  244. data/lib/sproutcore/helpers/text_helper.rb +336 -0
  245. data/lib/sproutcore/helpers.rb +3 -0
  246. data/lib/sproutcore/jsdoc.rb +40 -0
  247. data/lib/sproutcore/jsmin.rb +247 -0
  248. data/lib/sproutcore/library.rb +258 -0
  249. data/lib/sproutcore/merb/bundle_controller.rb +179 -0
  250. data/lib/sproutcore/merb/router.rb +43 -0
  251. data/lib/sproutcore/merb.rb +27 -0
  252. data/lib/sproutcore/version.rb +9 -0
  253. data/lib/sproutcore/view_helpers/button_views.rb +302 -0
  254. data/lib/sproutcore/view_helpers/core_views.rb +284 -0
  255. data/lib/sproutcore/view_helpers/form_views.rb +258 -0
  256. data/lib/sproutcore/view_helpers/menu_views.rb +94 -0
  257. data/lib/sproutcore/view_helpers.rb +628 -0
  258. data/lib/sproutcore.rb +30 -0
  259. data/script/destroy +14 -0
  260. data/script/generate +14 -0
  261. data/script/txt2html +74 -0
  262. data/setup.rb +1585 -0
  263. data/spec/spec.opts +1 -0
  264. data/spec/spec_helper.rb +7 -0
  265. data/spec/sproutcore_spec.rb +11 -0
  266. data/tasks/deployment.rake +34 -0
  267. data/tasks/environment.rake +7 -0
  268. data/tasks/rspec.rake +21 -0
  269. data/tasks/website.rake +17 -0
  270. metadata +365 -0
@@ -0,0 +1,1521 @@
1
+ // ========================================================================
2
+ // SproutCore
3
+ // copyright 2006-2007 Sprout Systems, Inc.
4
+ // ========================================================================
5
+
6
+ require('views/view') ;
7
+ require('views/label') ;
8
+
9
+ /** Indicates that selection points should be selected using horizontal
10
+ orientation.
11
+ */
12
+ SC.HORIZONTAL_ORIENTATION = 'horizontal';
13
+
14
+ /** Selection points should be selected using vertical orientation. */
15
+ SC.VERTICAL_ORIENTATION = 'vertical' ;
16
+
17
+ /**
18
+ @class
19
+
20
+ Renders a collection of views from a source array of model objects.
21
+
22
+ The CollectionView is the root view class for rendering collections of
23
+ views based on a source array of objects. It can automatically create the
24
+ and layout the views, including displaying them in groups. It also
25
+ handles event input for the entire collection.
26
+
27
+ To use CollectionView, just create the view and set the 'content' property
28
+ to an array of objects. (Note that if you setup a binding, it will
29
+ always transform content to an array.) The view will create instances of
30
+ exampleView to render the array. You can also bind to the selection
31
+ property if you want to monitor selection. (be sure to set the isEnabled
32
+ property to allow selection.)
33
+
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
+ @extends SC.View
46
+ */
47
+ SC.CollectionView = SC.View.extend(
48
+ /** @scope SC.CollectionView.prototype */
49
+ {
50
+
51
+ // ......................................
52
+ // PROPERTIES
53
+ //
54
+
55
+ /**
56
+ An array of content objects
57
+
58
+ This array should contain the content objects you want the collection view
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.
62
+
63
+ If you make the collection editable, the collection view will also modify
64
+ this array using the observable array methods of SC.Array.
65
+
66
+ Usually you will want to bind this property to a controller property
67
+ that actually contains the array of objects you to display.
68
+
69
+ @type Array
70
+ */
71
+ content: [],
72
+
73
+ /** @private */
74
+ contentBindingDefault: SC.Binding.MultipleNotEmpty,
75
+
76
+ /**
77
+ The array of currently selected objects.
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.
82
+
83
+ Any item views representing content objects in this array will
84
+ have their isSelected property set to YES automatically.
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
89
+ views.
90
+
91
+ Usually you will want to bind this property to a controller property
92
+ that actually manages the selection for your display.
93
+
94
+ @type Array
95
+ */
96
+ selection: [],
97
+
98
+ /** @private */
99
+ selectionBindingDefault: SC.Binding.Multiple,
100
+
101
+ /**
102
+ Allow user to select content using the mouse and keyboard
103
+
104
+ Set this property to NO to disallow the user from selecting items.
105
+ If you have items in your selection property, they will still be reflected
106
+ visually.
107
+
108
+ @type Boolean
109
+ */
110
+ isSelectable: true,
111
+
112
+ /** @private */
113
+ isSelectableBindingDefault: SC.Binding.Bool,
114
+
115
+ /**
116
+ Enable or disable the view.
117
+
118
+ The collection view will set the isEnabled property of its item views to
119
+ reflect the same view of this property. Whenever isEnabled is false,
120
+ the collection view will also be not selectable or editable, regardless of the
121
+ settings for isEditable & isSelectable.
122
+
123
+ @type Boolean
124
+ */
125
+ isEnabled: true,
126
+
127
+ /** @private */
128
+ isEnabledBindingDefault: SC.Binding.Bool,
129
+
130
+ /**
131
+ Allow user to edit content views.
132
+
133
+ The collection view will set the isEditable property on its item views to
134
+ reflect the same value of this property. Whenever isEditable is false, the
135
+ user will not be able to reorder, add, or delete items regardless of the
136
+ canReorderContent and canDeleteContent and isDropTarget properties.
137
+ */
138
+ isEditable: true,
139
+
140
+ /** @private */
141
+ isEditableBindingDefault: SC.Binding.Bool,
142
+
143
+ /**
144
+ Allow user to reorder items using drag and drop.
145
+
146
+ If true, the user will can use drag and drop to reorder items in the list.
147
+ If you also accept drops, this will allow the user to drop items into
148
+ specific points in the list. Otherwise items will be added to the end.
149
+ */
150
+ canReorderContent: false,
151
+
152
+ /** @private */
153
+ canReorderContentBindingDefault: SC.Binding.Bool,
154
+
155
+ /**
156
+ Allow the user to delete items using the delete key
157
+
158
+ If true the user will be allowed to delete selected items using the delete
159
+ key. Otherwise deletes will not be permitted.
160
+ */
161
+ canDeleteContent: false,
162
+
163
+ /** @private */
164
+ canDeleteContentBindingDefault: SC.Binding.Bool,
165
+
166
+ /**
167
+ Accept drops for data other than reordering.
168
+
169
+ Setting this property to return true when the view is instantiated will cause
170
+ it to be registered as a drop target, activating the other drop machinery.
171
+ */
172
+ isDropTarget: false,
173
+
174
+ /**
175
+ Use toggle selection instead of normal click behavior.
176
+
177
+ If set to true, then selection will use a toggle instead of the normal
178
+ click behavior. Command modifiers will be ignored and instead clicking
179
+ once will enable an item and clicking on it again will disable it.
180
+
181
+ @type Boolean
182
+ */
183
+ useToggleSelection: false,
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
+ /**
210
+ Trigger the action method on a single click.
211
+
212
+ Normally, clicking on an item view in a collection will select the content
213
+ object and double clicking will trigger the action method on the collection
214
+ view.
215
+
216
+ If you set this property to true, then clicking on a view will both select it
217
+ (if isSelected is true) and trigger the action method.
218
+
219
+ Use this if you are using the collection view as a menu of items.
220
+
221
+ @type {Boolean}
222
+ */
223
+ actOnSelect: false,
224
+
225
+ /**
226
+ Property key to use to group objects.
227
+
228
+ If groupBy is set to a non-null value, then the collection view will
229
+ automatically display item views in groups based on the value of the
230
+ passed property key. The exampleGroupView will be used to display the
231
+ items in groups.
232
+
233
+ If this property is set, you MUST ensure the items in the content array are
234
+ already sorted by the group key. Otherwise item view groups might appear more
235
+ than once.
236
+
237
+ @type {String}
238
+ */
239
+ groupBy: null,
240
+
241
+ /**
242
+ The view class to use when creating new item views.
243
+
244
+ The collection view will automatically create an instance of the view class
245
+ you set here for each item in its content array. You should provide your own
246
+ subclass for this property to display the type of content you want.
247
+
248
+ For best results, the view you set here should understand the following
249
+ properties:
250
+
251
+ {{{
252
+ content: The content object from the content array your view should display
253
+ isEnabled: True if the view should appear enabled
254
+ isSelected: True if the view should appear selected
255
+ }}}
256
+
257
+ In general you do not want your child views to actually respond to mouse and
258
+ keyboard events themselves. It is better to let the collection view do that.
259
+
260
+ If you do implement your own event handlers such as mouseDown or mouseUp, you
261
+ should be sure to actually call the same method on the collection view to
262
+ give it the chance to perform its own selection housekeeping.
263
+
264
+ @type {SC.View}
265
+ */
266
+ exampleView: SC.View,
267
+
268
+ /**
269
+ The view class to use when displaying item views in groups.
270
+
271
+ If the groupBy property is not null, then the collection view will create
272
+ an instance of this view class with the item views that belong to the group
273
+ as child nodes for each distinct group value it encounters.
274
+
275
+ Your groupView should have two outlets:
276
+
277
+ {{{
278
+ labelView: The view to display the group label. The group value will be set
279
+ as the content property of this view.
280
+
281
+ itemView: This is the view the item views will be added to as children to
282
+ this view.
283
+ }}}
284
+
285
+ If groupBy is null, then this property will not be used. The default class
286
+ provided here simply displays the group value in an H1 tag.
287
+
288
+ @type {SC.View}
289
+ */
290
+ exampleGroupView: SC.View.extend({
291
+ emptyElement: '<div><h1></h1><div class="well"></div></div>',
292
+ outlets: ['labelView','itemView'],
293
+ labelView: SC.LabelView.outletFor('h1?'),
294
+ itemView: SC.View.outletFor('.well?')
295
+ }),
296
+
297
+ /**
298
+ Invoked when the user double clicks on an item (or single clicks of actOnSelect is true)
299
+
300
+ Set this to the name of the action you want to send down the
301
+ responder chain when the user double clicks on an item (or single clicks if
302
+ actOnSelect is true). You can optionally specify a specific target as well
303
+ using the target property.
304
+
305
+ If you do not specify an action, then the collection view will also try to
306
+ invoke the action named on the target item view.
307
+
308
+ Older versions of SproutCore expected the action property to contain an actual
309
+ function that would be run. This format is still supported but is deprecated
310
+ for future use. You should generally use the responder chain to handle your
311
+ action for you.
312
+
313
+ @type {String}
314
+ */
315
+ action: null,
316
+
317
+ /**
318
+ Optional target to send the action to when the user double clicks.
319
+
320
+ If you set the action property to the name of an action, you can optionally
321
+ specify the target object you want the action to be sent to. This can be
322
+ either an actual object or a property path that will resolve to an object at
323
+ the time that the action is invoked.
324
+
325
+ This property is ignored if you use the deprecated approach of making the
326
+ action property a function.
327
+
328
+ @type {String|Object}
329
+ */
330
+ target: null,
331
+
332
+ /**
333
+ Set to true whenever the content changes and remains true until
334
+ the content has been rerendered.
335
+
336
+ You can also set this to true yourself to be notified when it is completed.
337
+ */
338
+ isDirty: true,
339
+
340
+ /**
341
+ The maximum time the collection view will spend updating its
342
+ views before it takes a break from the update.
343
+
344
+ This keeps your browser from freezing or displaying a slow script
345
+ warning while the render code works. Number is in msec.
346
+
347
+ Future versions of CollectionView may ignore this property as newer
348
+ rendering techniques make it no longer necessary.
349
+ */
350
+ maxRenderTime: 0,
351
+
352
+ /**
353
+ Property returns all of the item views, regardless of group view.
354
+
355
+ @returns {Array} the item views.
356
+ */
357
+ itemViews: function() {
358
+ var ret = [] ;
359
+ if (!this._itemViews) return ret ;
360
+ for(var key in this._itemViews) {
361
+ if (this._itemViews.hasOwnProperty(key)) ret.push(this._itemViews[key]);
362
+ }
363
+ return ret;
364
+ }.property(),
365
+
366
+ /**
367
+ Returns true if the passed view belongs to the collection.
368
+
369
+ This method uses the internal hash of item views and works even if
370
+ your items are stored in group views. This is faster than searching
371
+ the child view hierarchy yourself.
372
+
373
+ @param {SC.View} view The view to search for.
374
+
375
+ @returns {Boolean} True if the view is an item view in the receiver.
376
+ */
377
+ hasItemView: function(view) {
378
+ if (!this._itemViews) this._itemViews = {};
379
+ return !!this._itemViews[SC.getGUID(view)];
380
+ },
381
+
382
+ // ......................................
383
+ // DRAG AND DROP SUPPORT
384
+ //
385
+
386
+ /**
387
+ The insertion orientation. This is used to determine which
388
+ dimension we should pay attention to when determining insertion point for
389
+ a mouse click.
390
+
391
+ {{{
392
+ SC.HORIZONTAL_ORIENTATION: look at the X dimension only
393
+ SC.VERTICAL_ORIENTATION: look at the Y dimension only
394
+ }}}
395
+ */
396
+ insertionOrientation: SC.HORIZONTAL_ORIENTATION,
397
+
398
+ /**
399
+ Get the preferred insertion point for the given location, including
400
+ an insertion preference of before or after the named index.
401
+
402
+ The default implementation will loop through the item views looking for
403
+ the first view to "switch sides" in the orientation you specify.
404
+ */
405
+ insertionIndexForLocation: function(loc) {
406
+ var content = this.get('content') ;
407
+ var f, itemView, curSide, lastSide = null ;
408
+ var orient = this.get('insertionOrientation') ;
409
+ var ret= null ;
410
+ for(var idx=0; ((ret == null) && (idx<content.length)); idx++) {
411
+ itemView = this.itemViewForContent(content.objectAt(idx));
412
+ f = this.convertFrameFromView(itemView.get('frame'), itemView) ;
413
+
414
+ // if we are a horizontal orientation, look for the first item that
415
+ // will "switch sides" on the x path an the maxY is greater than Y.
416
+ // This assumes you will flow top to bottom, but it should work if you
417
+ // flow LTR or RTL.
418
+ if (orient == SC.HORIZONTAL_ORIENTATION) {
419
+ if (SC.maxY(f) > loc.y) {
420
+ curSide = (SC.maxX(f) < loc.x) ? -1 : 1 ;
421
+ } else curSide = null ;
422
+
423
+ // if we are a vertical orientation, look for the first item that
424
+ // will "swithc sides" on the y path and the maxX is greater than X.
425
+ // This assumes you will flow LTR, but it should work if you flow
426
+ // bottom to top or top to bottom.
427
+ } else {
428
+ if (SC.maxX(f) > loc.x) {
429
+ curSide = (SC.maxY(f) < loc.y) ? -1 : 1 ;
430
+ } else curSide = null ;
431
+ }
432
+
433
+ // if we "switched" sides then return this item view.
434
+ if (curSide !== null) {
435
+
436
+ // OK, we found an item view, while we have this data, decide if
437
+ // we should insert before or after the view
438
+ if ((lastSide !== null) && (curSide != lastSide)) {
439
+ ret = idx ;
440
+ if (orient == SC.HORIZONTAL_ORIENTATION) {
441
+ if (SC.midX(f) < loc.x) ret++ ;
442
+ } else {
443
+ if (SC.midY(f) < loc.y) ret++ ;
444
+ }
445
+ }
446
+ lastSide =curSide ;
447
+ }
448
+ }
449
+
450
+ // Handle some edge cases
451
+ if ((ret == null) || (ret < 0)) ret = 0 ;
452
+ if (ret > content.length) ret = content.length ;
453
+
454
+ // Done. Phew. Return.
455
+ return ret;
456
+ },
457
+
458
+ /**
459
+ Override to show the insertion point during a drag.
460
+
461
+ Called during a drag to show the insertion point. Passed value is the
462
+ item view that you should display the insertion point before. If the
463
+ passed value is null, then you should show the insertion point AFTER that
464
+ last item view returned by the itemViews property.
465
+
466
+ Once this method is called, you are guaranteed to also recieve a call to
467
+ hideInsertionPoint() at some point in the future.
468
+
469
+ The default implementation of this method does nothing.
470
+
471
+ @param {SC.View} itemView view the insertion point should appear directly before. If null, show insertion point at end.
472
+
473
+ @returns {void}
474
+ */
475
+ showInsertionPointBefore: function(itemView) {},
476
+
477
+ /**
478
+ Override to hide the insertion point when a drag ends.
479
+
480
+ Called during a drag to hide the insertion point. This will be called when the
481
+ user exits the view, cancels the drag or completes the drag. It will not be
482
+ called when the insertion point changes during a drag.
483
+
484
+ You should expect to receive one or more calls to showInsertionPointBefore()
485
+ during a drag followed by at least one call to this method at the end. Your
486
+ method should not raise an error if it is called more than once.
487
+
488
+ @returns {void}
489
+ */
490
+ hideInsertionPoint: function() {},
491
+
492
+ // handle mouse drags. If the canReorderContent is enabled, allow the
493
+ // user to start a reorder.
494
+ mouseDragged: function(ev) {
495
+ // Don't do anything unless the user has been dragging for 123msec
496
+ if ((Date.now() - this._mouseDownAt) < 123) return true ;
497
+
498
+ // OK, they must be serious, start a drag if possible.
499
+ // Also use this opportunity to clean up since mouseUp won't
500
+ // get called.
501
+ if (this.get('canReorderContent')) {
502
+ SC.Drag.start({
503
+ event: this._mouseDownEvent,
504
+ source: this,
505
+ dragView: this._mouseDownView,
506
+ ghost: NO,
507
+ slideBack: YES,
508
+ data: { "_mouseDownContent": this._mouseDownContent }
509
+ }) ;
510
+ this._cleanupMouseDown() ;
511
+ }
512
+ },
513
+
514
+ // Drop Source.
515
+ dragEntered: function(drag, evt) {
516
+ if ((drag.get('source') == this) && this.get('canReorderContent')) {
517
+ return SC.DRAG_MOVE ;
518
+ } else {
519
+ return SC.DRAG_NONE ;
520
+ }
521
+ },
522
+
523
+ // If reordering is allowed, then show insertion point
524
+ dragUpdated: function(drag, evt) {
525
+ if (this.get('canReorderContent')) {
526
+ var loc = drag.get('location') ;
527
+ loc = this.convertFrameFromView(loc, null) ;
528
+ var ret = this.insertionIndexForLocation(loc) ;
529
+ if (this._lastInsertionIndex != ret) {
530
+ console.log("--itemView: %@".fmt(ret)) ;
531
+ var itemView = this.itemViewForContent(this.get('content').objectAt(ret));
532
+ this.showInsertionPointBefore(itemView) ;
533
+ }
534
+ this._lastInsertionIndex = ret ;
535
+
536
+ }
537
+ return SC.DRAG_MOVE;
538
+ },
539
+
540
+ dragExited: function() {
541
+ this.hideInsertionPoint() ;
542
+ this._lastInsertionIndex = null ;
543
+ },
544
+
545
+ dragEnded: function() {
546
+ this.hideInsertionPoint() ;
547
+ this._lastInsertionIndex = null ;
548
+ },
549
+
550
+ prepareForDragOperation: function(op, drag) {
551
+ return SC.DRAG_ANY;
552
+ },
553
+
554
+ performDragOperation: function(op, drag) {
555
+
556
+ var loc = drag.get('location') ;
557
+ loc = this.convertFrameFromView(loc, null) ;
558
+
559
+ // if op is MOVE or COPY, add item to view.
560
+ var obj = drag.dataForType('_mouseDownContent') ;
561
+ if (obj && (op == SC.DRAG_MOVE)) {
562
+
563
+ // find the index to for the new insertion
564
+ var idx = this.insertionIndexForLocation(loc) ;
565
+
566
+ var content = this.get('content') ;
567
+ content.beginPropertyChanges(); // suspend notifications
568
+
569
+ // find the old index and remove it.
570
+ var old = content.indexOf(obj) ;
571
+ if (old >= 0) content.removeAt(old) ;
572
+ if ((old >= 0) && (old <= idx)) idx--; //adjust idx
573
+
574
+ // now insert object at new location
575
+ content.insertAt(idx, obj) ;
576
+ content.endPropertyChanges(); // restart notifications
577
+ }
578
+
579
+ return SC.DRAG_MOVE;
580
+ },
581
+
582
+ concludeDragOperation: function(op, drag) {
583
+ this.hideInsertionPoint() ;
584
+ this._lastInsertionIndex = null ;
585
+ },
586
+
587
+
588
+
589
+ // ......................................
590
+ // GENERATING CHILDREN
591
+ //
592
+
593
+ /**
594
+ Ensure that the displayed item views match the current set of content objects.
595
+
596
+ This is the main entry point to the Collection View layout system. It
597
+ compares the current set of item views to the content objects, adding, removing,
598
+ and reordering views as necessary to bring them in sync with the set of content
599
+ objects.
600
+
601
+ Once it has finished running, this method will also call your layoutChildViewsFor()
602
+ method if you have implemented it.
603
+
604
+ This method is called automatically whenever the content array changes. You will
605
+ not usually need to call it yourself. If you want to refresh the item views,
606
+ called rebuildChildren() instead.
607
+ */
608
+ updateChildren: function()
609
+ {
610
+ var el = this.containerElement || this.rootElement;
611
+
612
+ // initial setup
613
+ if (this._firstUpdate)
614
+ {
615
+ el.innerHTML = '';
616
+ this._firstUpdate = false;
617
+ }
618
+
619
+ // before removing from parent, make sure we have retrieved the frame
620
+ // size so that layout can happen.
621
+ this.cacheFrame();
622
+
623
+ // viewsForContent will hold all the item views we currently have rendered
624
+ // keyed by content._guid. We use this to quickly determine if a view can
625
+ // be reused.
626
+ if (!this._viewsForContent) this._viewsForContent = {};
627
+
628
+ // handle grouped items. If items are grouped, then each childNode is
629
+ // a group, which contains a label and a div with the items themselves.
630
+ var groupBy = this.get('groupBy');
631
+ var content = this.get('content') || [];
632
+
633
+ // If the number of childViews differs from the content size, remove from
634
+ // DOM to improve performance while updating.
635
+ this._cachedParent = null;
636
+ this._cachedSibling = null;
637
+ if (content.get('length') != this.childNodes.get('length'))
638
+ {
639
+ this._cachedParent = el.parentNode;
640
+ this._cachedSibling = el.nextSibling;
641
+ if (this._cachedParent) {
642
+ this._cachedParent.removeChild(el);
643
+ } else {
644
+ //debugger;
645
+ }
646
+ }
647
+
648
+ // this code path will render the collection of groups. This creates
649
+ // group views for each distinct group it encounters and then has it
650
+ // render child views in each item.
651
+ if (groupBy)
652
+ {
653
+ var loc = 0;
654
+ var group = this.firstChild;
655
+ while (group || (loc < content.get('length')))
656
+ {
657
+ var groupValue = (loc < content.get('length')) ? content.objectAt(loc).get(groupBy) : null;
658
+
659
+ // we are out of content, just remove any remaining groups (including
660
+ // child nodes)
661
+ if (loc >= content.get('length')) {
662
+ if (group) {
663
+ // this will clear out the item views in the group.
664
+ loc = this.updateChildrenInGroup(group.itemView, content, loc, groupBy, null);
665
+ // now remove the group.
666
+ var prev = group.previousSibling ;
667
+ this.removeChild(group) ;
668
+ group = prev ;
669
+ }
670
+
671
+ // otherwise, make sure the current group matches the next group. If
672
+ // it doesn't, then add a new group.
673
+ } else if (!group || (group.get('groupValue') != groupValue)) {
674
+
675
+ // create group view.
676
+ var newGroup = this.exampleGroupView.viewFor(null) ;
677
+ newGroup.owner = this ;
678
+ newGroup.set('groupValue',groupValue) ;
679
+
680
+ // add group label view.
681
+ if (newGroup.labelView) newGroup.labelView.set('content',groupValue);
682
+
683
+ // add item views to group.
684
+ loc = this.updateChildrenInGroup(newGroup.itemView,content,loc,
685
+ groupBy, groupValue) ;
686
+
687
+ // add the new group at this point
688
+ this.insertBefore(newGroup,group) ;
689
+ group = newGroup ;
690
+
691
+ // otherwise, if the current group does match the next group, just
692
+ // update its child nodes.
693
+ } else {
694
+ loc = this.updateChildrenInGroup(group.itemView,content,loc,
695
+ groupBy, groupValue) ;
696
+ }
697
+
698
+ // go to the next group. group will be nil if the first group was
699
+ // removed.
700
+ group = (group) ? group.nextSibling : this.firstChild ;
701
+ }
702
+
703
+ // grouping is not turned on.
704
+ } else {
705
+ this.updateChildrenInGroup(this, content, 0, null, null) ;
706
+ }
707
+
708
+ // Add back into DOM if optimization was used.
709
+ if (this._cachedParent) {
710
+ this._cachedParent.insertBefore(el,this._cachedSibling) ;
711
+ }
712
+
713
+ this.updateSelectionStates() ;
714
+ this.flushFrameCache() ;
715
+ this.set('isDirty',false);
716
+ },
717
+
718
+ /**
719
+ @private
720
+
721
+ Step through the child nodes in the parent to match them to
722
+ the content array, starting at the passed location. It will go until it
723
+ runs out of content objects or until the content no longer belong to the
724
+ group indicated.
725
+ */
726
+ updateChildrenInGroup: function(parent,content,loc,groupBy,groupValue) {
727
+ // cacheing content.get('length') for optimization.
728
+ var contentCount = content.get('length');
729
+ var child = parent.firstChild;
730
+ var inGroup = true ;
731
+
732
+ if (!this._itemViews) this._itemViews = {};
733
+ var itemViewsDidChange = false;
734
+
735
+ this.updateComputedViewHeight(parent);
736
+
737
+ // if we aren't rendering groups, then this can expire.
738
+ var expired = false;
739
+ var canExpire = !groupBy && loc == 0 ;
740
+ if (canExpire)
741
+ {
742
+ loc = this._lastRenderLoc ;
743
+ child = this._lastRenderChild || child;
744
+ this._resetRenderClock();
745
+ };
746
+
747
+ var firstChild = null ;
748
+
749
+ while (child || (inGroup && (loc < contentCount) && !expired)) {
750
+
751
+ // get the content object.
752
+ var cur = (inGroup && (loc < contentCount)) ? content.objectAt(loc) : null;
753
+
754
+ // verify the new cur is still in the group.
755
+ if (cur && groupBy && (cur.get(groupBy) != groupValue))
756
+ {
757
+ inGroup = false;
758
+ cur = null;
759
+ }
760
+
761
+ // we are out of content for this group, remaining children simply need
762
+ // to be removed.
763
+ if (cur == null) {
764
+ if (child) {
765
+ if (this.flushUnusedViews) {
766
+ var viewContent = child.get('content') ;
767
+ if (viewContent) delete this._viewsForContent[SC.getGUID(viewContent)];
768
+ child.set('content',null) ;
769
+ }
770
+ var prev = child.previousSibling ;
771
+ parent.removeChild(child) ;
772
+ if (this._itemViews[SC.getGUID(child)]) {
773
+ itemViewsDidChange = true ;
774
+ delete this._itemViews[SC.getGUID(child)];
775
+ }
776
+
777
+ child = prev;
778
+ }
779
+
780
+ // otherwise, make sure the current child matches the content object.
781
+ // if it doesn't, get the right view (or create it) and insert it here.
782
+ } else if (!child || (child.get('content') != cur)) {
783
+
784
+ // find the correct view. If it doesn't exist, create it.
785
+ var newChild = this._viewsForContent[SC.getGUID(cur)] ;
786
+ if (!newChild) {
787
+ newChild = this.exampleView.viewFor(null) ;
788
+ newChild.owner = this ;
789
+ newChild._isChildView = true ;
790
+ newChild.set('content',cur) ;
791
+ this._viewsForContent[SC.getGUID(cur)] = newChild ;
792
+ }
793
+
794
+ // add the view at this point in the hierarchy and make the new child
795
+ // the current child.
796
+ parent.insertBefore(newChild,child);
797
+ this._itemViews[SC.getGUID(newChild)] = newChild;
798
+ itemViewsDidChange = true;
799
+ child = newChild;
800
+ }
801
+
802
+ // go to next child and content object
803
+ // child would only be nil if the current child was first and was
804
+ // removed
805
+ if (!firstChild) firstChild = child;
806
+ child = (child) ? child.nextSibling : ((inGroup) ? parent.firstChild : null);
807
+
808
+ // go to the next loc only if cur was used last time.
809
+ if (cur) loc++;
810
+
811
+ expired = this._renderExpired();
812
+ }
813
+
814
+
815
+ // maybe save the current render loc and reschedule.
816
+ if (expired && (loc < contentCount))
817
+ {
818
+ this._lastRenderLoc = loc ;
819
+ this._lastRenderChild = child ;
820
+ setTimeout(this.updateChildren.bind(this),1) ; // do more later.
821
+ }
822
+ else
823
+ {
824
+ this._resetExpiredRender();
825
+ }
826
+
827
+ // now let the collection view layout the views that changed (if
828
+ // it is implemented.)
829
+ if (this.layoutChildViewsFor)
830
+ {
831
+ var el = this.containerElement || this.rootElement;
832
+ if (this._cachedParent) {
833
+ this._cachedParent.insertBefore(el,this._cachedSibling);
834
+ }
835
+ this.layoutChildViewsFor(parent, firstChild);
836
+ if (this._cachedParent) {
837
+ this._cachedParent.removeChild(el);
838
+ }
839
+ }
840
+
841
+ // notify itemViews change if applicable.
842
+ if (itemViewsDidChange) this.propertyDidChange('itemViews');
843
+
844
+ return loc;
845
+ },
846
+
847
+
848
+ /**
849
+ Returns the itemView that represents the passed content object.
850
+
851
+ If no item view is currently rendered for the object, this method will
852
+ return null.
853
+
854
+ @param {Object} obj The content object. Should be a member of the content array.
855
+ @returns {SC.View} The item view for this object or null if no match could be found.
856
+ */
857
+ itemViewForContent: function( obj )
858
+ {
859
+ return this._viewsForContent[SC.getGUID(obj)];
860
+ },
861
+
862
+ /**
863
+ Rebuild all the child item views in the collection view.
864
+
865
+ This will remove all the child views from the collection view and rebuild them
866
+ from scratch. This method is generally expensive, but if you have made a
867
+ substantial number of changes to the content array and need to bring everything
868
+ up to date, this is the best way to do it.
869
+
870
+ In general the collection view will automatically keep the item views in sync
871
+ with the content objects for you. You should not need to call this method
872
+ very often.
873
+
874
+ @returns {void}
875
+ */
876
+ rebuildChildren: function() {
877
+ this.clear();
878
+ this._viewsForContent = {};
879
+ this._resetExpiredRender();
880
+ this.updateChildren();
881
+ },
882
+
883
+ /**
884
+ Update the selection state for the item views to reflect the selection array.
885
+
886
+ This will update the isSelected property of all item views so that only those
887
+ representing content objects found in the selection array are selected.
888
+
889
+ This method is called automatically whenever your content or selection properties
890
+ changed. You should not need to call or override it often.
891
+ */
892
+ updateSelectionStates: function() {
893
+ if (!this._itemViews) return ;
894
+ var selection = this.get('selection') || [];
895
+
896
+ // First, for efficiency, turn the selection into a hash by GUID. This
897
+ // way, we'll only have to perform a linear search over the children.
898
+ var selectionHash = {};
899
+ var numberOfSelectedItems = selection.get('length');
900
+ for( var i = 0; i < numberOfSelectedItems; i++ ) {
901
+ var item = selection.objectAt(i);
902
+ selectionHash[SC.getGUID(item)] = true;
903
+ }
904
+
905
+ for(var key in this._itemViews) {
906
+ if (!this._itemViews.hasOwnProperty(key)) continue ;
907
+ var child = this._itemViews[key] ;
908
+ var content = (child.get) ? child.get('content') : null;
909
+ var guid = (content) ? SC.getGUID(content) : null;
910
+
911
+ if( !guid ) continue;
912
+ var childIsSelected = selectionHash[guid] ? true : false;
913
+
914
+ // If the child's state has changed from before, set it to the new
915
+ // state. Otherwise, don't bother setting the state to the same value
916
+ // it used to have.
917
+ if( childIsSelected != child.get('isSelected') ) {
918
+ if (child.set) child.set('isSelected', childIsSelected);
919
+ }
920
+ }
921
+ },
922
+
923
+ // layoutChildViewsFor: function(parentView, startingView) { return false; },
924
+
925
+ resizeChildrenWithOldSize: function(oldSize) {
926
+ if (this.layoutChildViewsFor && (this.layoutChildViewsFor(this, null))) {
927
+ this.updateComputedViewHeight(this) ;
928
+ } else {
929
+ arguments.callee.base.apply(this,arguments) ;
930
+ }
931
+ },
932
+
933
+ _firstUpdate: true,
934
+
935
+ _lastRenderLoc: 0,
936
+ _renderStart: null,
937
+ _resetRenderClock: function() { this._renderStart = new Date().getTime(); },
938
+
939
+ _resetExpiredRender: function() {
940
+ this._lastRenderLoc = 0; this._lastRenderChild = null;
941
+ },
942
+
943
+ _renderExpired: function() {
944
+ var max = this.maxRenderTime ;
945
+ if ((this._renderStart == null) || (max == 0)) return false ;
946
+ return ((new Date().getTime()) - this._renderStart) > max ;
947
+ },
948
+
949
+ /**
950
+ Override to return the range of items to render for a given frame.
951
+
952
+ The range you return will be used to limit the number of actual views that are
953
+ created for the collection view. The passed frame is relative to the total frame
954
+ of the groupView.
955
+
956
+ You should override this method if you want to support incremental rendering.
957
+ The default implementation does nothing.
958
+
959
+ @param {SC.View} groupView The group view the requested items belong to. If
960
+ grouping is not used, this will always be null.
961
+
962
+ @param {Frame} frame The frame you should use to determine the range.
963
+
964
+ @returns {Range} A hash that indicates the range of content objects to render. ({ start: X, length: Y })
965
+ */
966
+ itemRangeInFrame: function(groupView, frame) { return null; },
967
+
968
+ /**
969
+ Override to return a computed height of the collection.
970
+
971
+ This will be used to set a dynamic scrollbar height if you support incremental
972
+ rendering. The default implementation does nothing.
973
+
974
+ @param {SC.View} groupView The group view this request relates to. If grouping is
975
+ turned off, this parameter will be null.
976
+
977
+ @returns {Number} The view height in pixels.
978
+ */
979
+ computedViewHeight: function(groupView) { return -1; },
980
+
981
+ // This will set the collection height.
982
+ updateComputedViewHeight: function(groupView) {
983
+ var height = this.computedViewHeight(groupView) ;
984
+ if (height <= 0) {
985
+ if (groupView._heightView) {
986
+ groupView.rootElement.removeChild(this._heightView) ;
987
+ groupView._heightView = null ;
988
+ }
989
+ } else {
990
+ if (!groupView._heightView) {
991
+ groupView._heightView = document.createElement('div') ;
992
+ groupView.rootElement.appendChild(groupView._heightView) ;
993
+ Element.setStyle(groupView._heightView,{
994
+ position: 'absolute', left: '0px', display: 'block',
995
+ width: '1px', height: '1px'
996
+ }) ;
997
+ }
998
+
999
+ if (height != groupView._lastComputedHeight) {
1000
+ Element.setStyle(groupView._heightView,{ top: height + 'px' }) ;
1001
+ groupView._lastComputedHeight = height ;
1002
+ }
1003
+ }
1004
+ },
1005
+
1006
+ // ......................................
1007
+ // SELECTION
1008
+ //
1009
+
1010
+ selectPreviousItem: function()
1011
+ {
1012
+ var extend = arguments[0] || false;
1013
+ var content = this.get('content');
1014
+ var selected = this.get('selection').first();
1015
+ var indexOfFirst = 0;
1016
+ var indexOfSelected = content.indexOf( selected );
1017
+ var indexOfPrevious = indexOfSelected - 1;
1018
+ // error check to make sure we're not out of bounds...
1019
+ if ( indexOfPrevious < indexOfFirst ) indexOfPrevious = indexOfFirst;
1020
+ // ensure that the item is visible
1021
+ this.scrollToItemRecord(content.objectAt(indexOfPrevious));
1022
+ // set the selection
1023
+ this.selectItems(content.objectAt(indexOfPrevious), extend);
1024
+ },
1025
+
1026
+ selectNextItem: function()
1027
+ {
1028
+ var extend = arguments[0] || false;
1029
+ var content = this.get('content');
1030
+ var selected = this.get('selection').last();
1031
+ var indexOfLast = (content.get('length') - 1) || 0;
1032
+ var indexOfSelected = content.indexOf( selected );
1033
+ var indexOfNext = indexOfSelected + 1;
1034
+ // error check to make sure we're not out of bounds...
1035
+ if ( indexOfNext > indexOfLast ) indexOfNext = indexOfLast;
1036
+ // ensure that the item is visible
1037
+ this.scrollToItemRecord(content.objectAt(indexOfNext));
1038
+ // set the selection
1039
+ this.selectItems(content.objectAt(indexOfNext), extend);
1040
+ },
1041
+
1042
+ /**
1043
+ * Scroll the rootElement (if needed) to ensure that the item is visible.
1044
+ * @param {SC.Record} record The record to scroll to
1045
+ * @returns {void}
1046
+ */
1047
+ scrollToItemRecord: function( record )
1048
+ {
1049
+ this.scrollToItemView( this.itemViewForContent(record) );
1050
+ },
1051
+ /**
1052
+ * Scroll the rootElement (if needed) to ensure that the item is visible.
1053
+ * @param {SC.View} view The item view to scroll to
1054
+ * @returns {void}
1055
+ */
1056
+ scrollToItemView: function( view )
1057
+ {
1058
+ var visible = Element.extend(this.get('rootElement'));
1059
+ var visibleTop = visible.scrollTop;
1060
+ var visibleBottom = visibleTop + visible.getHeight();
1061
+
1062
+ visible.makePositioned();
1063
+
1064
+ var item = Element.extend(view.get('rootElement'));
1065
+ var itemTop = item.positionedOffset().top;
1066
+ var itemBottom = itemTop + item.getHeight();
1067
+
1068
+ visible.undoPositioned();
1069
+
1070
+ if (itemTop < visibleTop) {
1071
+ visible.scrollTop = itemTop;
1072
+ }
1073
+ if (itemBottom > visibleBottom) {
1074
+ visible.scrollTop += (itemBottom - visibleBottom);
1075
+ }
1076
+ },
1077
+
1078
+ selectItems: function(items, extendSelection) {
1079
+ var base = (extendSelection) ? this.get('selection') : [] ;
1080
+ var sel = [items].concat(base).flatten().uniq() ;
1081
+ this.set('selection',sel) ;
1082
+ },
1083
+
1084
+ deselectItems: function(items) {
1085
+ items = [items].flatten() ;
1086
+ var base = this.get('selection') || [] ;
1087
+ var sel = base.map(function(i) { return (items.include(i)) ? null : i; });
1088
+ sel = sel.compact() ;
1089
+ this.set('selection',sel) ;
1090
+ },
1091
+
1092
+ // ......................................
1093
+ // EVENT HANDLING
1094
+ //
1095
+
1096
+ /**
1097
+ Find the item view underneath the passed mouse location.
1098
+
1099
+ The default implementation of this method simply searches each item view's
1100
+ frame to find one that includes the location. If you are doing your own
1101
+ layout, you may be able to perform this calculation more quickly. If so,
1102
+ consider overriding this method for better performance during drag operations.
1103
+
1104
+ @param {Point} loc The current mouse location in the coordinate of the
1105
+ collection view
1106
+
1107
+ @returns {SC.View} The item view under the collection
1108
+ */
1109
+ itemViewAtLocation: function(loc) {
1110
+ var content = this.get('content') ;
1111
+ var idx = content.length;
1112
+ while(--idx >= 0) {
1113
+ var itemView = this.itemViewForContent(content.objectAt(idx));
1114
+ var frame = itemView.get('frame');
1115
+ if (SC.pointInRect(loc, frame)) return itemView ;
1116
+ }
1117
+ return null; // not in an itemView right now.
1118
+ },
1119
+
1120
+
1121
+
1122
+ /**
1123
+ Find the first content item view for the passed event.
1124
+
1125
+ This method will go up the view chain, starting with the view that was the target
1126
+ of the passed event, looking for a child item. This will become the view that
1127
+ is selected by the mouse event.
1128
+
1129
+ This method only works for mouseDown & mouseUp events. mouseMoved events do
1130
+ not have a target.
1131
+
1132
+ @param {Event} evt An event
1133
+
1134
+ */
1135
+ itemViewForEvent: function(evt)
1136
+ {
1137
+ var view = SC.window.firstViewForEvent( evt );
1138
+ // work up the view hierarchy to find a match...
1139
+ do {
1140
+ // item clicked was the ContainerView itself... i.e. the user clicked outside the child items
1141
+ // nothing to return...
1142
+ if ( view == this ) return null;
1143
+
1144
+ // sweet!... the view is not only in the collection, but it says we can hit it.
1145
+ // hit it and quit it...
1146
+ if ( this.hasItemView(view) && (!view.hitTest || view.hitTest(evt)) ) return view;
1147
+ } while ( view = view.get('parentNode') );
1148
+
1149
+ // nothing was found...
1150
+ return null;
1151
+ },
1152
+
1153
+
1154
+ didMouseDown: function(ev) {
1155
+ console.warn("didMouseDown will be removed from CollectionView in the near future. Use mouseDown instead");
1156
+ return this._mouseDown(ev, true);
1157
+ },
1158
+
1159
+ mouseDown: function(ev) {
1160
+ // older code might still use didMouseDown. Warn to give people some time to transition.
1161
+ if (this.didMouseDown != SC.CollectionView.prototype.didMouseDown) {
1162
+ return this.didMouseDown(ev) ;
1163
+ } else return this._mouseDown(ev);
1164
+ },
1165
+
1166
+ _mouseDown: function(ev) {
1167
+ // save for drag opt
1168
+ this._mouseDownEvent = ev ;
1169
+
1170
+ // Toggle selection only triggers on mouse up. Do nothing.
1171
+ if (this.useToggleSelection) return true;
1172
+
1173
+ // Make sure that saved mouseDown state is always reset in case we do
1174
+ // not get a paired mouseUp. (Only happens if subclass does not call us like it should)
1175
+ this._mouseDownAt = this._shouldDeselect = this._shouldReselect = this._refreshSelection = false;
1176
+
1177
+ var mouseDownView = this._mouseDownView = this.itemViewForEvent(ev);
1178
+ var mouseDownContent = this._mouseDownContent = (mouseDownView) ? mouseDownView.get('content') : null;
1179
+
1180
+ // recieved a mouseDown on the collection element, but not on one of the childItems... bail
1181
+ if (!mouseDownView) {
1182
+ if (this.get('allowDeselectAll')) this.selectItems([], false);
1183
+ return true ;
1184
+ }
1185
+
1186
+ // collection some basic setup info
1187
+ var selection = this.get('selection') || [];
1188
+ var isSelected = selection.include(mouseDownContent);
1189
+ var modifierKeyPressed = ev.ctrlKey || ev.altKey || ev.metaKey;
1190
+ if (mouseDownView.checkboxView && (Event.element(ev) == el.checkboxView.rootElement)) {
1191
+ modifierKeyPressed = true ;
1192
+ }
1193
+
1194
+ this._mouseDownAt = Date.now();
1195
+
1196
+ // holding down a modifier key while clicking a selected item should deselect that item...
1197
+ // deselect and bail.
1198
+ if (modifierKeyPressed && isSelected) {
1199
+ this._shouldDeselect = mouseDownContent;
1200
+
1201
+ // if the shiftKey was pressed, then we want to extend the selection
1202
+ // from the last selected item
1203
+ } else if (ev.shiftKey && selection.get('length') > 0) {
1204
+ selection = this._findSelectionExtendedByShift(selection, mouseDownContent) ;
1205
+ this.selectItems(selection) ;
1206
+
1207
+ // If no modifier key was pressed, then clicking on the selected item should clear
1208
+ // the selection and reselect only the clicked on item.
1209
+ } else if (!modifierKeyPressed && isSelected) {
1210
+ this._shouldReselect = mouseDownContent;
1211
+
1212
+ // Otherwise, simply select the clicked on item, adding it to the current
1213
+ // selection if a modifier key was pressed.
1214
+ } else {
1215
+ this.selectItems(mouseDownContent, modifierKeyPressed);
1216
+ }
1217
+
1218
+ // saved for extend by shift ops.
1219
+ this._previousMouseDownContent = mouseDownContent;
1220
+ return true;
1221
+ },
1222
+
1223
+ // invoked when the user releases the mouse. based on the information saved
1224
+ // during mouse down, we decide what to do.
1225
+ didMouseUp: function(ev) {
1226
+ console.warn("didMouseUp will be removed from CollectionView in the near future. Use mouseUp instead");
1227
+ return this._mouseUp(ev);
1228
+ },
1229
+
1230
+ mouseUp: function(ev) {
1231
+ if (this.didMouseUp != SC.CollectionView.prototype.didMouseUp) {
1232
+ return this.didMouseUp(ev) ;
1233
+ } else return this._mouseUp(ev) ;
1234
+ },
1235
+
1236
+ _mouseUp: function(ev) {
1237
+
1238
+ console.info('_mouseUp!');
1239
+
1240
+ var canAct = this.get('actOnSelect') ;
1241
+ var view = this.itemViewForEvent(ev) ;
1242
+
1243
+ if (this.useToggleSelection) {
1244
+ if (!view) return ; // do nothing when clicked outside of elements
1245
+
1246
+ // determine if item is selected. If so, then go on.
1247
+ var selection = this.get('selection') || [] ;
1248
+ var content = (view) ? view.get('content') : null ;
1249
+ var isSelected = selection.include(content) ;
1250
+ if (isSelected) {
1251
+ this.deselectItems([content]) ;
1252
+ } else this.selectItems([content],true) ;
1253
+
1254
+ } else {
1255
+ if (this._shouldDeselect) this.deselectItems(this._shouldDeselect);
1256
+ if (this._shouldReselect) this.selectItems(this._shouldReselect,false) ;
1257
+
1258
+ // this is invoked if the user clicked on a checkbox. If this is not
1259
+ // done then the checkbox might not update properly.
1260
+ if (this._refreshSelection) {
1261
+ }
1262
+ this._cleanupMouseDown() ;
1263
+ }
1264
+
1265
+ this._mouseDownEvent = null ;
1266
+ if (canAct) this._action(ev, view) ;
1267
+
1268
+ return false; // bubble event to allow didDoubleClick to be called...
1269
+ },
1270
+
1271
+ _cleanupMouseDown: function() {
1272
+ this._mouseDownAt = this._shouldDeselect = this._shouldReselect = this._refreshSelection = false;
1273
+ this._mouseDownEvent = this._mouseDownContent = this._mouseDownView = null ;
1274
+ },
1275
+
1276
+ // this can be used to initiate a drag. Only drags 100ms after mouseDown
1277
+ // to avoid responding to clicks.
1278
+ mouseDidMove: function(ev) {
1279
+ console.warn("mouseDidMove will be removed from CollectionView in the near future. Use mouseMoved instead");
1280
+ return this._mouseMoved(ev) ;
1281
+ },
1282
+
1283
+ mouseMoved: function(ev) {
1284
+ if (this.mouseDidMove != SC.CollectionView.prototype.mouseDidMove) {
1285
+ return this.mouseDidMove(ev) ;
1286
+ } else return this._mouseMoved(ev) ;
1287
+ },
1288
+
1289
+ _mouseMoved: function(ev) {
1290
+ var view = this.itemViewForEvent(ev) ;
1291
+ // handle hover events.
1292
+ if(this._lastHoveredItem && ((view === null) || (view != this._lastHoveredItem)) && this._lastHoveredItem.didMouseOut) {
1293
+ this._lastHoveredItem.didMouseOut(ev);
1294
+ }
1295
+ this._lastHoveredItem = view ;
1296
+ if (view && view.didMouseOver) view.didMouseOver(ev) ;
1297
+ },
1298
+
1299
+ didMouseOut: function(ev) {
1300
+ console.warn("didMouseOut will be removed from CollectionView in the near future. Use mouseOut instead");
1301
+ return this._mouseOut(ev) ;
1302
+ },
1303
+
1304
+ mouseOut: function(ev) {
1305
+ if (this.didMouseOut != SC.CollectionView.prototype.didMouseOut) {
1306
+ return this.didMouseOut(ev) ;
1307
+ } else return this._mouseOut(ev) ;
1308
+ },
1309
+
1310
+ _mouseOut: function(ev) {
1311
+
1312
+ var view = this._lastHoveredItem ;
1313
+ this._lastHoveredItem = null ;
1314
+ if (view && view.didMouseOut) view.didMouseOut(ev) ;
1315
+ },
1316
+
1317
+ // invoked when the user double clicks on an item.
1318
+ didDoubleClick: function(ev) {
1319
+ console.warn("didDoubleClick will be removed from CollectionView in the near future. Use mouseOut instead");
1320
+ return this._doubleClick(ev) ;
1321
+ },
1322
+
1323
+ doubleClick: function(ev) {
1324
+ if (this.didDoubleClick != SC.CollectionView.prototype.didDoubleClick) {
1325
+ return this.didDoubleClick(ev) ;
1326
+ } else return this._doubleClick(ev) ;
1327
+ },
1328
+
1329
+ _doubleClick: function(ev) {
1330
+ console.info('_doubleClick!') ;
1331
+ var view = this.itemViewForEvent(ev) ;
1332
+ if (view) {
1333
+ this._action(view, ev) ;
1334
+ return true ;
1335
+ } else return false ;
1336
+ },
1337
+
1338
+ _findSelectionExtendedByShift: function(selection, mouseDownContent) {
1339
+ var collection = this.get('content');
1340
+
1341
+ // bounds of the collection...
1342
+ var collectionLowerBounds = 0;
1343
+ var collectionUpperBounds = (collection.get('length') - 1);
1344
+
1345
+ var selectionBeginIndex = collection.indexOf(selection.first());
1346
+ var selectionEndIndex = collection.indexOf(selection.last());
1347
+
1348
+ var previousMouseDownIndex = collection.indexOf(this._previousMouseDownContent);
1349
+ // _previousMouseDownContent couldn't be found... either it hasn't been set yet or the record has been deleted by the user
1350
+ // fall back to the first selected item.
1351
+ if (previousMouseDownIndex == -1) previousMouseDownIndex = selectionBeginIndex;
1352
+
1353
+
1354
+ var currentMouseDownIndex = collection.indexOf(mouseDownContent);
1355
+ // sanity check...
1356
+ if (currentMouseDownIndex == -1) throw "Unable to extend selection to an item that's not in the collection!";
1357
+
1358
+ // clicked before the current selection set... extend it's beginning...
1359
+ if (currentMouseDownIndex < selectionBeginIndex) selectionBeginIndex = currentMouseDownIndex;
1360
+ // clicked after the current selection set... extend it's ending...
1361
+ if (currentMouseDownIndex > selectionEndIndex) selectionEndIndex = currentMouseDownIndex;
1362
+ // clicked inside the selection set... need to determine where the las
1363
+ if ((currentMouseDownIndex > selectionBeginIndex) && (currentMouseDownIndex < selectionEndIndex))
1364
+ {
1365
+ if (currentMouseDownIndex == previousMouseDownIndex) {
1366
+ selectionBeginIndex = currentMouseDownIndex;
1367
+ selectionEndIndex = currentMouseDownIndex;
1368
+ } else if (currentMouseDownIndex > previousMouseDownIndex) {
1369
+ selectionBeginIndex = previousMouseDownIndex;
1370
+ selectionEndIndex = currentMouseDownIndex;
1371
+ } else if (currentMouseDownIndex < previousMouseDownIndex){
1372
+ selectionBeginIndex = currentMouseDownIndex;
1373
+ selectionEndIndex = previousMouseDownIndex;
1374
+ }
1375
+ }
1376
+ // slice doesn't include the last index passed... silly..
1377
+ selectionEndIndex++;
1378
+
1379
+ // shouldn't need to sanity check that the selection is in bounds due to the indexOf checks above...
1380
+ // I'll have faith that indexOf hasn't lied to me...
1381
+ return collection.slice(selectionBeginIndex, selectionEndIndex);
1382
+ },
1383
+
1384
+
1385
+
1386
+ // ......................................
1387
+ // INTERNAL
1388
+ //
1389
+
1390
+ init: function() {
1391
+ arguments.callee.base.apply(this, arguments) ;
1392
+ this._dropTargetObserver();
1393
+ },
1394
+
1395
+ // When canReorderContent changes, add or remove drop target as necessary.
1396
+ _dropTargetObserver: function() {
1397
+ var canDrop = this.get('canReorderContent') || this.get('isDropTarget') ;
1398
+ if (canDrop) {
1399
+ SC.Drag.addDropTarget(this) ;
1400
+ } else {
1401
+ SC.Drag.removeDropTarget(this) ;
1402
+ }
1403
+ }.observes('canReorderContent', 'isDropTarget'),
1404
+
1405
+ // Perform the action. Supports legacy behavior as well as newer style
1406
+ // action dispatch.
1407
+ _action: function(view, evt) {
1408
+
1409
+ var action = this.get('action');
1410
+ var target = this.get('target') || null;
1411
+ if (action) {
1412
+ // if the action is a function, just call it
1413
+ if ($type(action) == T_FUNCTION) return this.action(view, evt) ;
1414
+
1415
+ // otherwise, use the new sendAction style
1416
+ SC.app.sendAction(action, target, this) ;
1417
+
1418
+ // if no action is specified, then trigger the support action,
1419
+ // if supported.
1420
+ } else if (!view) {
1421
+ return ; // nothing to do
1422
+
1423
+ // if the target view has its own internal action handler,
1424
+ // trigger that.
1425
+ } else if ($type(view._action) == T_FUNCTION) {
1426
+ return view._action(evt) ;
1427
+
1428
+ // otherwise call the action method to support older styles.
1429
+ } else if ($type(view.action) == T_FUNCTION) {
1430
+ return view.action(evt) ;
1431
+ }
1432
+ },
1433
+
1434
+ _viewsForContent: null,
1435
+ _content: [], // cached for changes.
1436
+ propertyObserver: function(observing,target,key,value)
1437
+ {
1438
+ if (target == this)
1439
+ {
1440
+ // update children when content changes.
1441
+ if (key == 'content')
1442
+ {
1443
+ // cache the observer binding
1444
+ if (!this._boundObserver)
1445
+ {
1446
+ this._boundObserver = this._contentPropertyObserver.bind(this);
1447
+ }
1448
+
1449
+ // don't update the content unless it has changed. Note that if we
1450
+ // get a new empty array, that doesn't count as a change from a prev
1451
+ // empty array.
1452
+ var isEqual = (
1453
+ ((value && this._content) && (value.get('length') == 0) && (this._content.get('length') == 0)) ||
1454
+ SC.isEqual( value, this._content)
1455
+ );
1456
+
1457
+ // remove and re-add the observer for "[]" before changing the content property
1458
+ // this triggers a render of the child item views whenever the array is modified.
1459
+ if (this._content && this._content.removeObserver) this._content.removeObserver('[]', this._boundObserver);
1460
+ this._content = value;
1461
+ if (this._content && this._content.addObserver) this._content.addObserver('[]', this._boundObserver);
1462
+
1463
+ // only re-render the collection if the content was actually changed to a new value.
1464
+ if (!isEqual)
1465
+ {
1466
+ this._contentPropertyObserver(target,key,value);
1467
+ }
1468
+
1469
+ // update selection when selection changes. set this as a timeout so
1470
+ // that a render can finish first.
1471
+ }
1472
+ else if (key == 'selection')
1473
+ {
1474
+ if (!this._updatingSel)
1475
+ {
1476
+ this._updatingSel = this.invokeLater('_updateSelectionState',1);
1477
+ }
1478
+ }
1479
+ }
1480
+ },
1481
+
1482
+ // called on content change *and* content.[] change...
1483
+ _contentPropertyObserver: function(target,key,value)
1484
+ {
1485
+ if (!this._updating)
1486
+ {
1487
+ this._updating = true;
1488
+ this.set('isDirty',true);
1489
+ this._resetExpiredRender();
1490
+ this.updateChildren();
1491
+ this._updating = false;
1492
+ }
1493
+ },
1494
+
1495
+ _updateSelectionState: function() {
1496
+ try {
1497
+ this.updateSelectionStates() ;
1498
+ } catch(e) {
1499
+ console.log('exception while updating selection states in %@: %@'.format(this,e)) ;
1500
+ }
1501
+ this._updatingSel = null ;
1502
+ },
1503
+
1504
+ // ======================================================================
1505
+ // DEPRECATED APIS (Still available for compatibility)
1506
+
1507
+ /** @private
1508
+ If set to false, this method will prevent you from deselecting all of
1509
+ the items in your view. This is better implemented using a controller
1510
+ that prohibits empty selection.
1511
+ */
1512
+ allowDeselectAll: true,
1513
+
1514
+ /** @private */
1515
+ itemExistsInCollection: function( view ) { return this.hasItemView(view); },
1516
+
1517
+ /** @private */
1518
+ viewForContentRecord: function(rec) { return this.itemViewForContent(rec); }
1519
+
1520
+
1521
+ }) ;