sproutcore 0.9.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +233 -0
- data/Manifest.txt +67 -34
- data/bin/sc-build +12 -1
- data/bin/sc-gen +1 -1
- data/bin/sproutcore +14 -0
- data/clients/sc_docs/controllers/docs.js +38 -8
- data/clients/sc_docs/english.lproj/body.css +80 -127
- data/clients/sc_docs/english.lproj/body.rhtml +43 -23
- data/clients/sc_docs/english.lproj/no_docs.rhtml +2 -1
- data/clients/sc_docs/english.lproj/tabs.rhtml +16 -0
- data/clients/sc_docs/main.js +14 -9
- data/clients/sc_docs/models/doc.js +1 -1
- data/clients/sc_docs/tests/controllers/docs.rhtml +1 -2
- data/clients/sc_docs/tests/models/doc.rhtml +1 -2
- data/clients/sc_docs/tests/views/doc_frame.rhtml +1 -2
- data/clients/sc_docs/tests/views/doc_label_view.rhtml +1 -2
- data/clients/sc_docs/views/doc_frame.js +1 -1
- data/clients/sc_test_runner/controllers/runner.js +31 -8
- data/clients/sc_test_runner/english.lproj/body.css +62 -122
- data/clients/sc_test_runner/english.lproj/body.rhtml +62 -26
- data/clients/sc_test_runner/main.js +1 -6
- data/clients/sc_test_runner/models/test.js +14 -1
- data/clients/sc_test_runner/views/runner_frame.js +4 -2
- data/clients/view_builder/builders/builder.js +339 -0
- data/clients/view_builder/builders/button.js +81 -0
- data/clients/view_builder/controllers/document.js +21 -0
- data/clients/view_builder/core.js +19 -0
- data/clients/view_builder/english.lproj/body.css +77 -0
- data/clients/view_builder/english.lproj/body.rhtml +41 -0
- data/clients/{sc_docs → view_builder}/english.lproj/controls.css +0 -0
- data/clients/view_builder/english.lproj/strings.js +14 -0
- data/clients/view_builder/main.js +38 -0
- data/clients/view_builder/tests/controllers/document.rhtml +20 -0
- data/clients/view_builder/tests/views/builder.rhtml +20 -0
- data/clients/view_builder/views/builder.js +23 -0
- data/frameworks/prototype/prototype.js +1 -1
- data/frameworks/sproutcore/Core.js +32 -7
- data/frameworks/sproutcore/README +1 -1
- data/frameworks/sproutcore/animation/animation.js +411 -0
- data/frameworks/sproutcore/controllers/array.js +17 -9
- data/frameworks/sproutcore/controllers/collection.js +9 -110
- data/frameworks/sproutcore/controllers/controller.js +1 -1
- data/frameworks/sproutcore/controllers/object.js +2 -1
- data/frameworks/sproutcore/drag/drag.js +267 -56
- data/frameworks/sproutcore/drag/drag_data_source.js +24 -16
- data/frameworks/sproutcore/drag/drag_source.js +53 -42
- data/frameworks/sproutcore/drag/drop_target.js +2 -2
- data/frameworks/sproutcore/english.lproj/buttons.css +337 -236
- data/frameworks/sproutcore/english.lproj/core.css +115 -0
- data/frameworks/sproutcore/english.lproj/icons.css +227 -0
- data/{clients/sc_docs → frameworks/sproutcore}/english.lproj/images/indicator.gif +0 -0
- data/frameworks/sproutcore/english.lproj/images/sc-theme-sprite.png +0 -0
- data/frameworks/sproutcore/english.lproj/images/sc-theme-ysprite.png +0 -0
- data/frameworks/sproutcore/english.lproj/images/shared-icons.png +0 -0
- data/frameworks/sproutcore/english.lproj/menu.css +1 -1
- data/frameworks/sproutcore/english.lproj/strings.js +1 -1
- data/frameworks/sproutcore/english.lproj/theme.css +405 -31
- data/frameworks/sproutcore/foundation/application.js +15 -11
- data/frameworks/sproutcore/foundation/benchmark.js +1 -1
- data/frameworks/sproutcore/foundation/binding.js +2 -2
- data/frameworks/sproutcore/foundation/date.js +1 -1
- data/frameworks/sproutcore/foundation/error.js +1 -1
- data/frameworks/sproutcore/foundation/input_manager.js +32 -21
- data/frameworks/sproutcore/foundation/mock.js +1 -1
- data/frameworks/sproutcore/foundation/node_descriptor.js +9 -6
- data/frameworks/sproutcore/foundation/object.js +249 -177
- data/frameworks/sproutcore/foundation/page.js +5 -2
- data/frameworks/sproutcore/foundation/path_module.js +11 -10
- data/frameworks/sproutcore/foundation/responder.js +5 -2
- data/frameworks/sproutcore/foundation/routes.js +17 -13
- data/frameworks/sproutcore/foundation/run_loop.js +249 -11
- data/frameworks/sproutcore/foundation/server.js +1 -1
- data/frameworks/sproutcore/foundation/set.js +3 -3
- data/frameworks/sproutcore/foundation/string.js +5 -3
- data/frameworks/sproutcore/foundation/timer.js +371 -0
- data/frameworks/sproutcore/foundation/undo_manager.js +1 -1
- data/frameworks/sproutcore/foundation/unittest.js +3 -3
- data/frameworks/sproutcore/foundation/utils.js +161 -2
- data/frameworks/sproutcore/globals/panels.js +1 -1
- data/frameworks/sproutcore/globals/popups.js +4 -3
- data/frameworks/sproutcore/globals/window.js +44 -4
- data/frameworks/sproutcore/lib/button_views.rb +328 -0
- data/frameworks/sproutcore/lib/collection_view.rb +80 -0
- data/frameworks/sproutcore/lib/core_views.rb +281 -0
- data/frameworks/sproutcore/lib/form_views.rb +253 -0
- data/frameworks/sproutcore/lib/index.rhtml +2 -0
- data/frameworks/sproutcore/lib/menu_views.rb +88 -0
- data/frameworks/sproutcore/{foundation → mixins}/array.js +60 -29
- data/frameworks/sproutcore/mixins/control.js +265 -0
- data/frameworks/sproutcore/mixins/delegate_support.js +66 -0
- data/frameworks/sproutcore/{foundation → mixins}/observable.js +176 -6
- data/frameworks/sproutcore/mixins/scrollable.js +245 -0
- data/frameworks/sproutcore/mixins/selection_support.js +148 -0
- data/frameworks/sproutcore/mixins/validatable.js +152 -0
- data/frameworks/sproutcore/models/collection.js +5 -5
- data/frameworks/sproutcore/models/record.js +1 -1
- data/frameworks/sproutcore/models/store.js +1 -1
- data/frameworks/sproutcore/panes/dialog.js +1 -1
- data/frameworks/sproutcore/panes/manager.js +1 -1
- data/frameworks/sproutcore/panes/menu.js +1 -1
- data/frameworks/sproutcore/panes/overlay.js +2 -2
- data/frameworks/sproutcore/panes/panel.js +1 -1
- data/frameworks/sproutcore/panes/picker.js +1 -1
- data/frameworks/sproutcore/tests/controllers/array.rhtml +44 -4
- data/frameworks/sproutcore/tests/foundation/timer/invalidate.rhtml +33 -0
- data/frameworks/sproutcore/tests/foundation/timer/invokeLater.rhtml +145 -0
- data/frameworks/sproutcore/tests/foundation/timer/isPaused.rhtml +70 -0
- data/frameworks/sproutcore/tests/foundation/timer/schedule.rhtml +145 -0
- data/frameworks/sproutcore/tests/views/{scroll.rhtml → checkbox.rhtml} +3 -3
- data/frameworks/sproutcore/tests/views/{collection.rhtml → collection/base.rhtml} +33 -32
- data/frameworks/sproutcore/tests/views/collection/incremental_rendering.rhtml +260 -0
- data/frameworks/sproutcore/tests/views/image_cell.rhtml +19 -0
- data/frameworks/sproutcore/tests/views/label_item.rhtml +2 -4
- data/frameworks/sproutcore/tests/views/list.rhtml +2 -3
- data/frameworks/sproutcore/tests/views/list_item.rhtml +20 -0
- data/frameworks/sproutcore/tests/views/slider.rhtml +20 -0
- data/frameworks/sproutcore/tests/views/text_cell.rhtml +19 -0
- data/frameworks/sproutcore/tests/views/view/clippingFrame.rhtml +395 -0
- data/frameworks/sproutcore/tests/views/view/frame.rhtml +353 -0
- data/frameworks/sproutcore/tests/views/view/innerFrame.rhtml +347 -0
- data/frameworks/sproutcore/tests/views/view/isVisibleInWindow.rhtml +148 -0
- data/frameworks/sproutcore/tests/views/view/scrollFrame.rhtml +468 -0
- data/frameworks/sproutcore/validators/credit_card.js +33 -13
- data/frameworks/sproutcore/validators/date.js +26 -6
- data/frameworks/sproutcore/validators/email.js +21 -3
- data/frameworks/sproutcore/validators/not_empty.js +11 -1
- data/frameworks/sproutcore/validators/number.js +18 -4
- data/frameworks/sproutcore/validators/password.js +12 -1
- data/frameworks/sproutcore/validators/validator.js +204 -194
- data/frameworks/sproutcore/views/{button.js → button/button.js} +96 -94
- data/frameworks/sproutcore/views/button/checkbox.js +29 -0
- data/frameworks/sproutcore/views/button/disclosure.js +42 -0
- data/frameworks/sproutcore/views/button/radio.js +29 -0
- data/frameworks/sproutcore/views/{collection.js → collection/collection.js} +1373 -1024
- data/frameworks/sproutcore/views/collection/grid.js +124 -46
- data/frameworks/sproutcore/views/collection/image_cell.js +17 -46
- data/frameworks/sproutcore/views/collection/list.js +45 -35
- data/frameworks/sproutcore/views/collection/source_list.js +386 -0
- data/frameworks/sproutcore/views/collection/table.js +118 -0
- data/frameworks/sproutcore/views/container.js +7 -2
- data/frameworks/sproutcore/views/error_explanation.js +23 -10
- data/frameworks/sproutcore/views/{checkbox_field.js → field/checkbox_field.js} +16 -6
- data/frameworks/sproutcore/views/field/field.js +219 -0
- data/frameworks/sproutcore/views/{radio_field.js → field/radio_field.js} +27 -12
- data/frameworks/sproutcore/views/{select_field.js → field/select_field.js} +116 -90
- data/frameworks/sproutcore/views/{text_field.js → field/text_field.js} +57 -8
- data/frameworks/sproutcore/views/{textarea_field.js → field/textarea_field.js} +13 -3
- data/frameworks/sproutcore/views/filter_button.js +2 -2
- data/frameworks/sproutcore/views/form.js +3 -3
- data/frameworks/sproutcore/views/image.js +128 -21
- data/frameworks/sproutcore/views/inline_text_editor.js +1 -1
- data/frameworks/sproutcore/views/label.js +149 -92
- data/frameworks/sproutcore/views/list_item.js +225 -0
- data/frameworks/sproutcore/views/menu_item.js +10 -4
- data/frameworks/sproutcore/views/pagination.js +11 -4
- data/frameworks/sproutcore/views/popup_button.js +25 -21
- data/frameworks/sproutcore/views/popup_menu.js +10 -4
- data/frameworks/sproutcore/views/progress.js +29 -16
- data/frameworks/sproutcore/views/radio_group.js +1 -1
- data/frameworks/sproutcore/views/scroll.js +60 -20
- data/frameworks/sproutcore/views/segmented.js +1 -1
- data/frameworks/sproutcore/views/slider.js +132 -0
- data/frameworks/sproutcore/views/source_list_group.js +130 -0
- data/frameworks/sproutcore/views/spinner.js +1 -1
- data/frameworks/sproutcore/views/split.js +292 -0
- data/frameworks/sproutcore/views/split_divider.js +109 -0
- data/frameworks/sproutcore/views/tab.js +1 -1
- data/frameworks/sproutcore/views/toolbar.js +1 -1
- data/frameworks/sproutcore/views/view.js +1272 -591
- data/generators/client/templates/english.lproj/body.css +1 -1
- data/generators/controller/controller_generator.rb +1 -1
- data/generators/controller/templates/test.rhtml +2 -1
- data/generators/model/templates/test.rhtml +1 -1
- data/generators/test/templates/test.rhtml +1 -1
- data/generators/view/templates/test.rhtml +1 -1
- data/jsdoc/templates/sproutcore/class.tmpl +241 -338
- data/jsdoc/templates/sproutcore/default.css +105 -155
- data/jsdoc/templates/sproutcore/index.tmpl +43 -8
- data/jsdoc/templates/sproutcore/publish.js +9 -4
- data/lib/sproutcore/build_tools/html_builder.rb +29 -13
- data/lib/sproutcore/build_tools/resource_builder.rb +1 -1
- data/lib/sproutcore/bundle.rb +86 -25
- data/lib/sproutcore/jsdoc.rb +2 -0
- data/lib/sproutcore/version.rb +1 -1
- data/lib/sproutcore/view_helpers.rb +36 -3
- data/tasks/deployment.rake +1 -1
- metadata +69 -36
- data/clients/sc_docs/english.lproj/icons/small/next.png +0 -0
- data/clients/sc_docs/english.lproj/icons/small/reset.png +0 -0
- data/clients/sc_docs/english.lproj/images/gradients.png +0 -0
- data/clients/sc_docs/english.lproj/images/toolbar.png +0 -0
- data/clients/sc_docs/english.lproj/warning.rhtml +0 -6
- data/clients/sc_test_runner/english.lproj/warning.rhtml +0 -6
- data/frameworks/sproutcore/english.lproj/buttons.png +0 -0
- data/frameworks/sproutcore/english.lproj/collections.css +0 -82
- data/frameworks/sproutcore/english.lproj/images/buttons-sprite.png +0 -0
- data/frameworks/sproutcore/views/collection/collection_item.js +0 -36
- data/frameworks/sproutcore/views/collection/text_cell.js +0 -128
- data/frameworks/sproutcore/views/field.js +0 -214
- data/frameworks/sproutcore/views/workspace.js +0 -170
- data/generators/client/templates/english.lproj/controls.css +0 -0
- data/generators/framework/templates/english.lproj/body.css +0 -0
- data/generators/framework/templates/english.lproj/body.rhtml +0 -3
- data/generators/framework/templates/english.lproj/controls.css +0 -0
- data/lib/sproutcore/view_helpers/button_views.rb +0 -302
- data/lib/sproutcore/view_helpers/core_views.rb +0 -292
- data/lib/sproutcore/view_helpers/form_views.rb +0 -258
- data/lib/sproutcore/view_helpers/menu_views.rb +0 -94
@@ -1,17 +1,17 @@
|
|
1
1
|
// ========================================================================
|
2
2
|
// SproutCore
|
3
|
-
// copyright 2006-
|
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
|
-
|
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", "
|
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
|
-
|
118
|
+
If NO the button will be disabled.
|
117
119
|
|
118
|
-
@type
|
120
|
+
@type Bool
|
119
121
|
*/
|
120
|
-
isEnabled:
|
121
|
-
isEnabledBindingDefault: SC.Binding.OneWayBool,
|
122
|
+
isEnabled: YES,
|
122
123
|
|
123
124
|
/**
|
124
|
-
|
125
|
+
button's selection state. Returns YES, NO, or SC.MIXED_STATE
|
125
126
|
*/
|
126
|
-
isSelected:
|
127
|
-
isSelectedBindingDefault: SC.Binding.OneWayBool,
|
127
|
+
isSelected: NO,
|
128
128
|
|
129
129
|
/**
|
130
|
-
|
131
|
-
|
132
|
-
|
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:
|
135
|
+
isDefault: NO,
|
135
136
|
isDefaultBindingDefault: SC.Binding.OneWayBool,
|
136
137
|
|
137
138
|
/**
|
138
|
-
|
139
|
-
|
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:
|
143
|
+
isCancel: NO,
|
142
144
|
isCancelBindingDefault: SC.Binding.OneWayBool,
|
143
145
|
|
144
146
|
/**
|
145
|
-
|
146
|
-
|
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
|
-
|
156
|
+
titleSelector: '.label',
|
149
157
|
|
150
158
|
/**
|
151
|
-
|
152
|
-
|
159
|
+
The button title.
|
160
|
+
|
161
|
+
This property is observable and bindable.
|
162
|
+
|
163
|
+
@field {String}
|
153
164
|
*/
|
154
|
-
|
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.
|
158
|
-
var text = this.
|
159
|
-
var lsel = this.get('
|
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
|
-
|
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
|
-
//
|
176
|
-
|
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
|
-
|
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
|
264
|
-
if (this.get('localize') && (el =
|
265
|
-
this.
|
266
|
-
|
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.
|
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
|
-
|
300
|
+
switch(key) {
|
301
|
+
|
292
302
|
// determine the new selection state.
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
316
|
-
|
317
|
-
|
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
|
-
|
322
|
-
var
|
323
|
-
var
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
this.
|
329
|
-
|
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-
|
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
|
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
|
-
|
81
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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:
|
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
|
-
|
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
|
-
|
371
|
-
|
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
|
-
|
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.
|
405
|
-
return !!this.
|
423
|
+
if (!this._itemViewsByGuid) this._itemViewsByGuid = {} ;
|
424
|
+
return !!this._itemViewsByGuid[SC.guidFor(view)] ;
|
406
425
|
},
|
407
426
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
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
|
-
|
417
|
-
this.
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
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
|
-
|
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
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
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
|
-
|
448
|
-
|
449
|
-
var
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
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
|
-
//
|
476
|
-
|
477
|
-
|
478
|
-
|
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
|
-
//
|
497
|
-
return
|
479
|
+
// nothing was found...
|
480
|
+
return null;
|
498
481
|
},
|
499
|
-
|
500
|
-
|
501
|
-
|
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
|
-
|
509
|
-
|
487
|
+
If no item view is currently rendered for the object, this method will
|
488
|
+
return null.
|
510
489
|
|
511
|
-
|
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
|
-
|
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
|
-
@
|
505
|
+
@param {Object} value The group value.
|
506
|
+
@param {SC.View} The group view or null
|
516
507
|
*/
|
517
|
-
|
518
|
-
|
508
|
+
groupViewForGroupValue: function(groupValue) {
|
509
|
+
return this._groupViewsByValue[groupValue] ;
|
510
|
+
},
|
511
|
+
|
519
512
|
/**
|
520
|
-
|
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
|
-
|
527
|
-
|
528
|
-
method
|
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
|
-
@
|
519
|
+
@param {SC.View} groupView the group view.
|
520
|
+
@returns {Object} the value of the group view or null.
|
531
521
|
*/
|
532
|
-
|
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
|
-
|
532
|
+
Expands the index into a range of content objects that have the same
|
533
|
+
group value.
|
536
534
|
|
537
|
-
|
538
|
-
|
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
|
-
|
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
|
-
|
543
|
-
var
|
544
|
-
|
545
|
-
|
546
|
-
var
|
547
|
-
var
|
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
|
-
|
550
|
-
|
551
|
-
|
552
|
-
var
|
553
|
-
|
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
|
-
|
571
|
-
|
573
|
+
return { start: min, length: max-min } ;
|
574
|
+
},
|
572
575
|
|
573
|
-
|
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
|
-
|
577
|
-
|
578
|
-
|
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
|
-
|
581
|
-
|
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
|
-
|
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
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
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
|
-
|
657
|
-
|
658
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
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
|
-
|
741
|
-
|
742
|
-
|
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
|
-
|
749
|
-
//
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
//
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
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
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
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
|
-
//
|
894
|
-
|
687
|
+
// Swap out remaining content items.
|
688
|
+
this._itemViewsByContent = itemViewsByContent ;
|
689
|
+
didChange = true;
|
895
690
|
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
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
|
-
//
|
904
|
-
//
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
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
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
712
|
+
}
|
713
|
+
|
714
|
+
// Clean out some cached items and notify their changes.
|
715
|
+
if (didChange) {
|
716
|
+
this._flushZombieGroupViews() ;
|
717
|
+
this.updateSelectionStates() ;
|
950
718
|
|
951
|
-
|
952
|
-
|
719
|
+
this._itemViews = null ;
|
720
|
+
this.notifyPropertyChange('itemViews') ;
|
953
721
|
|
954
|
-
|
722
|
+
this._groupViews = null ;
|
723
|
+
this.notifyPropertyChange('groupViews') ;
|
955
724
|
}
|
956
725
|
|
957
|
-
|
958
|
-
|
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
|
-
|
991
|
-
|
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
|
-
|
994
|
-
|
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
|
1005
|
-
from scratch. This method is generally expensive, but if you have
|
1006
|
-
substantial number of changes to the content array
|
1007
|
-
|
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
|
1010
|
-
with the content objects for you. You should not need to call this
|
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
|
-
|
1017
|
-
this.
|
1018
|
-
|
1019
|
-
|
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
|
-
|
1038
|
-
var
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
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
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
var
|
1048
|
-
var
|
1049
|
-
|
1050
|
-
|
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.
|
1066
|
-
|
1067
|
-
|
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
|
-
|
1079
|
-
|
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
|
-
|
1083
|
-
|
1084
|
-
|
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
|
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
|
863
|
+
@returns {Range} A hash that indicates the range of content objects to
|
864
|
+
render. ({ start: X, length: Y })
|
1104
865
|
*/
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
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
|
-
|
1111
|
-
|
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
|
-
|
1114
|
-
|
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
|
-
@
|
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
|
-
|
910
|
+
layoutItemView: function(itemView, contentIndex, firstLayout) {
|
911
|
+
|
912
|
+
},
|
1119
913
|
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
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
|
-
//
|
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
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
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
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
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() -
|
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.
|
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() +
|
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.
|
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
|
-
|
1296
|
-
|
1297
|
-
|
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
|
-
|
1307
|
-
var
|
1308
|
-
|
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.
|
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.
|
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.
|
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.
|
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
|
-
|
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
|
1503
|
-
|
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 =
|
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
|
-
//
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1558
|
-
|
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
|
-
|
1568
|
-
|
1569
|
-
|
1570
|
-
|
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
|
-
|
1584
|
-
|
1585
|
-
|
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
|
-
//
|
1588
|
-
//
|
1589
|
-
|
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
|
-
|
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
|
-
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
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
|
-
|
1625
|
-
if (view && view.didMouseOver) view.didMouseOver(ev) ;
|
1980
|
+
return SC.DRAG_MOVE;
|
1626
1981
|
},
|
1627
1982
|
|
1628
|
-
|
1629
|
-
|
1630
|
-
|
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
|
-
|
1647
|
-
|
1648
|
-
|
1649
|
-
return this._doubleClick(ev) ;
|
1988
|
+
dragEnded: function() {
|
1989
|
+
this.hideInsertionPoint() ;
|
1990
|
+
this._lastInsertionIndex = null ;
|
1650
1991
|
},
|
1651
1992
|
|
1652
|
-
|
1653
|
-
|
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
|
-
|
1659
|
-
|
1660
|
-
|
1661
|
-
|
1662
|
-
|
1663
|
-
|
1664
|
-
|
1665
|
-
|
1666
|
-
|
1667
|
-
|
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
|
-
|
1684
|
-
|
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
|
-
|
1688
|
-
|
1689
|
-
|
1690
|
-
|
1691
|
-
|
1692
|
-
|
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
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
|
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
|
-
|
1779
|
-
|
1780
|
-
|
1781
|
-
|
1782
|
-
|
1783
|
-
|
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
|
-
|
1813
|
-
|
1814
|
-
|
1815
|
-
|
1816
|
-
|
1817
|
-
|
1818
|
-
|
1819
|
-
|
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
|
-
|
1824
|
-
|
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
|
-
|
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
|
+
|