Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Squash merge okito/collection work

  • Loading branch information...
commit c8a287ab8b2518f19addf67053704ae1f95aef79 1 parent 7e0036b
Charles Jolley authored
Showing with 10,070 additions and 2,741 deletions.
  1. +1 −0  Buildfile
  2. +4,848 −0 design/CollectionView State Charts.graffle
  3. +21 −4 frameworks/datastore/system/store.js
  4. +145 −0 frameworks/desktop/mixins/collection_content_delegate.js
  5. +0 −22 frameworks/desktop/mixins/collection_item.js
  6. +61 −0 frameworks/desktop/mixins/collection_row_delegate.js
  7. +73 −30 frameworks/desktop/mixins/collection_view_delegate.js
  8. +168 −0 frameworks/desktop/private/tree_array.js
  9. +269 −0 frameworks/desktop/tests/views/collection/content.js
  10. +82 −0 frameworks/desktop/tests/views/collection/deleteSelection.js
  11. +199 −0 frameworks/desktop/tests/views/collection/deselect.js
  12. +240 −0 frameworks/desktop/tests/views/collection/itemViewForContentIndex.js
  13. +65 −0 frameworks/desktop/tests/views/collection/layerIdFor.js
  14. +88 −0 frameworks/desktop/tests/views/collection/length.js
  15. +163 −0 frameworks/desktop/tests/views/collection/mouse.js
  16. +121 −0 frameworks/desktop/tests/views/collection/nowShowing.js
  17. +177 −0 frameworks/desktop/tests/views/collection/reload.js
  18. +240 −0 frameworks/desktop/tests/views/collection/select.js
  19. +191 −0 frameworks/desktop/tests/views/collection/selectNextItem.js
  20. +197 −39 frameworks/desktop/tests/views/collection/selectPreviousItem.js
  21. +141 −0 frameworks/desktop/tests/views/collection/selection.js
  22. +182 −0 frameworks/desktop/tests/views/collection/ui_diagram.js
  23. +0 −10 frameworks/desktop/tests/views/list/methods.js
  24. +183 −0 frameworks/desktop/tests/views/list/rowDelegate.js
  25. +0 −110 frameworks/desktop/tests/views/list/ui.js
  26. +167 −0 frameworks/desktop/tests/views/list/ui_row_heights.js
  27. +127 −0 frameworks/desktop/tests/views/list/ui_simple.js
  28. +231 −0 frameworks/desktop/tests/views/stacked/ui_comments.js
  29. +1,240 −809 frameworks/desktop/views/collection.js
  30. +272 −590 frameworks/desktop/views/list.js
  31. +2 −4 frameworks/desktop/views/list_item.js
  32. +11 −5 frameworks/desktop/views/scroller.js
  33. +14 −1,090 frameworks/desktop/views/source_list.js
  34. +101 −0 frameworks/desktop/views/stacked.js
  35. +14 −2 frameworks/foundation/debug/control_test_pane.js
  36. +2 −0  frameworks/foundation/mixins/selection_support.js
  37. +2 −2 frameworks/foundation/system/event.js
  38. +0 −2  frameworks/foundation/views/view.js
  39. +10 −9 frameworks/runtime/core.js
  40. +3 −3 frameworks/runtime/mixins/observable.js
  41. +3 −3 frameworks/runtime/system/binding.js
  42. +0 −2  frameworks/runtime/system/index_set.js
  43. +2 −0  frameworks/runtime/system/range_observer.js
  44. +13 −4 frameworks/runtime/system/run_loop.js
  45. +1 −1  frameworks/runtime/system/set.js
View
1  Buildfile
@@ -61,3 +61,4 @@ config :standard_theme,
%w(tests docs).each do |app_target|
config app_target, :required => [:desktop], :theme => :standard_theme
end
+
View
4,848 design/CollectionView State Charts.graffle
4,848 additions, 0 deletions not shown
View
25 frameworks/datastore/system/store.js
@@ -95,12 +95,17 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
store.commitChanges().destroy();
}}}
+ @param {Hash} attrs optional attributes to set on new store
@returns {SC.NestedStore} new nested store chained to receiver
*/
- chain: function() {
- var ret = SC.NestedStore.create({ parentStore: this }) ;
- var nested = this.nestedStores;
- if (!nested) nested =this.nestedStores = [];
+ chain: function(attrs) {
+ if (!attrs) attrs = {};
+ attrs.parentStore = this;
+
+ var ret = SC.NestedStore.create(attrs),
+ nested = this.nestedStores;
+
+ if (!nested) nested = this.nestedStores = [];
nested.push(ret);
return ret ;
},
@@ -119,6 +124,18 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
return this ;
},
+ /**
+ Used to determine if a nested store belongs directly or indirectly to the
+ receiver.
+
+ @param {SC.Store} store store instance
+ @returns {Boolean} YES if belongs
+ */
+ hasNestedStore: function(store) {
+ while(store && (store !== this)) store = store.get('parentStore');
+ return store === this ;
+ },
+
// ..........................................................
// SHARED DATA STRUCTURES
//
View
145 frameworks/desktop/mixins/collection_content_delegate.js
@@ -0,0 +1,145 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// Portions ©2008-2009 Apple, Inc. All rights reserved.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+
+/**
+ Used for contentIndexDisclosureState(). Indicates open branch node.
+*/
+SC.BRANCH_OPEN = 0x0011;
+
+/**
+ Used for contentIndexDisclosureState(). Indicates closed branch node.
+*/
+SC.BRANCH_CLOSED = 0x0012;
+
+/**
+ Used for contentIndexDisclosureState(). Indicates leaf node.
+*/
+SC.LEAF_NODE = 0x0020;
+
+/**
+ @namespace
+
+ This mixin provides standard methods used by a CollectionView to provide
+ additional meta-data about content in a collection view such as selection
+ or enabled state.
+
+ You can apply this mixin to a class that you set as a delegate or to the
+ object you set as content. SC.ArrayControllers automatically implement
+ this mixin.
+
+ @since SproutCore 1.0
+*/
+SC.CollectionContentDelegate = {
+
+ /**
+ Used to detect the mixin by SC.CollectionView
+ */
+ isCollectionContent: YES,
+
+ /**
+ Return YES if the content index should be selected. Default behavior
+ looks at the selection property on the view.
+
+ @param {SC.CollectionView} view the collection view
+ @param {SC.Array} content the content object
+ @param {Number} idx the content index
+ @returns {Boolean} YES, NO, or SC.MIXED_STATE
+ */
+ contentIndexIsSelected: function(view, content, idx) {
+ var sel = view.get('selection');
+ return sel ? sel.contains(content, idx) : NO ;
+ },
+
+ /**
+ Returns YES if the content index should be enabled. Default looks at the
+ isEnabled state of the collection view.
+ looks at the selection property on the view.
+
+ @param {SC.CollectionView} view the collection view
+ @param {SC.Array} content the content object
+ @param {Number} idx the content index
+ @returns {Boolean} YES, NO, or SC.MIXED_STATE
+ */
+ contentIndexIsEnabled: function(view, content, idx) {
+ return view.get('isEnabled');
+ },
+
+ // ..........................................................
+ // GROUPING
+ //
+
+ /**
+ Optionally return an index set containing the indexes that may be group
+ views. For each group view, the delegate will actually be asked to
+ confirm the view is a group using the contentIndexIsGroup() method.
+
+ If grouping is not enabled, return null.
+
+ @param {SC.CollectionView} view the calling view
+ @param {SC.Array} content the content object
+ @return {SC.IndexSet}
+ */
+ contentGroupIndexes: function(view, content) {
+ return null;
+ },
+
+ /**
+ Returns YES if the item at the specified content index should be rendered
+ using the groupExampleView instead of the regular exampleView. Note that
+ a group view is different from a branch/leaf view. Group views often
+ appear with different layout and a different look and feel.
+
+ Default always returns NO.
+
+ @param {SC.CollectionView} view the collection view
+ @param {SC.Array} content the content object
+ @param {Number} idx the content index
+ @returns {Boolean} YES, NO, or SC.MIXED_STATE
+ */
+ contentIndexIsGroup: function(view, content, idx) {
+ return NO ;
+ },
+
+ // ..........................................................
+ // OUTLINE VIEWS
+ //
+
+ /**
+ Returns the outline level for the item at the specified index. Can be
+ used to display hierarchical lists.
+
+ Default always returns 0 (top level).
+
+ @param {SC.CollectionView} view the collection view
+ @param {SC.Array} content the content object
+ @param {Number} idx the content index
+ @returns {Boolean} YES, NO, or SC.MIXED_STATE
+ */
+ contentIndexOutlineLevel: function(view, content, idx) {
+ return 0;
+ },
+
+ /**
+ Returns a constant indicating the disclosure state of the item. Must be
+ one of SC.BRANCH_OPEN, SC.BRANCH_CLOSED, SC.LEAF_NODE. If you return one
+ of the BRANCH options then the item may be rendered with a disclosure
+ triangle open or closed. If you return SC.LEAF_NODe then the item will
+ be rendered as a leaf node.
+
+ Default returns SC.LEAF_NODE.
+
+ @param {SC.CollectionView} view the collection view
+ @param {SC.Array} content the content object
+ @param {Number} idx the content index
+ @returns {Boolean} YES, NO, or SC.MIXED_STATE
+ */
+ contentIndexDisclosureState: function(view, content, idx) {
+ return SC.LEAF_NODE;
+ }
+
+};
View
22 frameworks/desktop/mixins/collection_item.js
@@ -1,22 +0,0 @@
-// ==========================================================================
-// Project: SproutCore - JavaScript Application Framework
-// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
-// Portions ©2008-2009 Apple, Inc. All rights reserved.
-// License: Licened under MIT license (see license.js)
-// ==========================================================================
-
-/**
- @namespace
-
- TODO: Add full description of SC.CollectionItem
-
- Any view you want to use as an item view in a collection must include this
- mixin.
-
- @since SproutCore 1.0
-*/
-SC.CollectionItem = {
-
- classNames: ['sc-collection-item']
-
-};
View
61 frameworks/desktop/mixins/collection_row_delegate.js
@@ -0,0 +1,61 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// Portions ©2008-2009 Apple, Inc. All rights reserved.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+
+
+/**
+ @namespace
+
+ CollectionRowDelegates are consulted by SC.ListView and SC.TableView to
+ control the height of rows, including specifying custom heights for
+ specific rows.
+
+ You can implement a custom row height in one of two ways.
+
+*/
+SC.CollectionRowDelegate = {
+
+ /** walk like a duck */
+ isCollectionRowDelegate: YES,
+
+ /**
+ Default row height. Unless you implement some custom row height
+ support, this row height will be used for all items.
+
+ @property
+ @type Number
+ */
+ rowHeight: 18,
+
+ /**
+ Index set of rows that should have a custom row height. If you need
+ certains rows to have a custom row height, then set this property to a
+ non-null value. Otherwise leave it blank to disable custom row heights.
+
+ @property
+ @type SC.IndexSet
+ */
+ customRowHeightIndexes: null,
+
+ /**
+ Called for each index in the customRowHeightIndexes set to get the
+ actual row height for the index. This method should return the default
+ rowHeight if you don't want the row to have a custom height.
+
+ The default implementation just returns the default rowHeight.
+
+ @param {SC.CollectionView} view the calling view
+ @param {Object} content the content array
+ @param {Number} contentIndex the index
+ @returns {Number} row height
+ */
+ contentIndexRowHeight: function(view, content, contentIndex) {
+ return this.get('rowHeight');
+ }
+
+
+};
View
103 frameworks/desktop/mixins/collection_view_delegate.js
@@ -26,19 +26,30 @@ SC.DROP_ANY = 0x03 ;
/**
@namespace
- A Collection View Delegate is consulted by a SC.CollectionView's to control
- certain behaviors such as selection control and drag and drop behaviors.
+ A Collection View Delegate is consulted by a SC.CollectionView's to make
+ policy decisions about certain behaviors such as selection control and
+ drag and drop. If you need to control other aspects of your data, you may
+ also want to add the SC.CollectionContent mixin.
- To act as a Collection Delegate, the object should be set as the delegate
- property of the collection view and should implement one or more of the
- methods below.
+ To act as a Collection Delegate, just apply this mixin to your class. You
+ must then set the "delegate" property on the CollectionView to your object.
- You can also choose to mixin this delegate to get suitable default
- implementations of these methods.
+ Alternatively, if no delegate is set on a CollectionView, but the content
+ implements this mixin, the content object will be used as the delegate
+ instead.
+
+ If you set an ArrayController or its arrangedObjects property as the content
+ of a CollectionView, the ArrayController will automatically act as the
+ delegate for the view.
@since SproutCore 1.0
*/
SC.CollectionViewDelegate = {
+
+ /**
+ Used to detect the mixin by SC.CollectionView
+ */
+ isCollectionViewDelegate: YES,
/**
This method will be called anytime the collection view is about to
@@ -48,9 +59,9 @@ SC.CollectionViewDelegate = {
selected objects that cannot be selected. The default implementation of
this method simply returns the proposed selection.
- @param view {SC.CollectionView} the collection view
- @param sel {Array} Proposed array of selected objects.
- @returns The actual array allowed or null if no change is allowed.
+ @param {SC.CollectionView} view the collection view
+ @param {SC.IndexSet} sel Proposed array of selected objects.
+ @returns {SC.IndexSet} Actual allow selection index set
*/
collectionViewSelectionForProposedSelection: function(view, sel) {
return sel ;
@@ -188,39 +199,71 @@ SC.CollectionViewDelegate = {
This method is only called if canDeleteContent is YES on the collection
view.
- @param view {SC.CollectionView} the collection view
- @param item {Array} proposed array of items to delete.
- @returns {Array} items allowed to delete or null.
+ @param {SC.CollectionView} view the collection view
+ @param {SC.IndexSet} indexes proposed index set of items to delete.
+ @returns {SC.IndexSet} index set allowed to delete or null.
*/
- collectionViewShouldDeleteContent: function(view, items) { return items; },
+ collectionViewShouldDeleteIndexes: function(view, indexes) {
+ return indexes;
+ },
/**
Called by the collection view to actually delete the selected items.
- The default behavior will use standard array operators to remove the
- items from the content array. You can implement this method to provide
- your own deletion method.
+ The default behavior will use standard array operators to delete the
+ indexes from the array. You can implement this method to provide your own
+ deletion method.
- If you simply want to controls the items to be deleted, you should instead
+ If you simply want to control the items to be deleted, you should instead
implement collectionViewShouldDeleteItems(). This method will only be
- called if canDeleteContent is YES and collectionViewShouldDeleteContent()
- returns a non-empty array.
+ called if canDeleteContent is YES and collectionViewShouldDeleteIndexes()
+ returns a non-empty index set
+
+ @param {SC.CollectionView} view collection view
+ @param {SC.IndexSet} indexes the items to delete
+ @returns {Boolean} YES if the deletion was a success.
+ */
+ collectionViewDeleteContent: function(view, content, indexes) {
+ if (!content) return NO ;
+
+ if (SC.typeOf(content.destroyAt) === SC.T_FUNCTION) {
+ content.destroyAt(indexes);
+ return YES ;
+
+ } else if (SC.typeOf(content.removeAt) === SC.T_FUNCTION) {
+ content.removeAt(indexes);
+ return YES;
+
+ } else return NO ;
+ },
+
+ /**
+ Called by the collection when attempting to select an item. Return the
+ actual indexes you want to allow to be selected. Return null to disallow
+ the change. The default allows all selection.
- @param view {SC.CollectionView} the view collection view
- @param items {Array} the items to delete
- @returns {Boolean} YES if the operation succeeded, NO otherwise.
+ @param {SC.CollectionView} view the view collection view
+ @param {SC.IndexSet} indexes the indexes to be selected
+ @param {Boolean} extend YES if the indexes will extend existing sel
+ @returns {SC.IndexSet} allowed index set
*/
- collectionViewDeleteContent: function(view, items) { return NO; },
+ collectionViewShouldSelectIndexes: function (view, indexes, extend) {
+ return indexes;
+ },
/**
- Called by the collection when attempting to select an item.
+ Called by the collection when attempting to deselect an item. Return the
+ actual indexes you want to allow to be deselected. Return null to
+ disallow the change. The default allows all selection.
- The default implementation always returns YES.
+ Note that you should not modify the passed in IndexSet. clone it instead.
- @param view {SC.CollectionView} the view collection view
- @param item {Object} the item to be selected
- @returns {Boolean} YES to alow, NO to prevent it
+ @param {SC.CollectionView} view the view collection view
+ @param {SC.IndexSet} indexes the indexes to be selected
+ @returns {SC.IndexSet} allowed index set
*/
- collectionViewShouldSelectItem: function (view, item) { return YES; }
+ collectionViewShouldDeselectIndexes: function (view, indexes) {
+ return indexes;
+ }
};
View
168 frameworks/desktop/private/tree_array.js
@@ -0,0 +1,168 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// Portions ©2008-2009 Apple, Inc. All rights reserved.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+/**
+ @class
+
+ A TreeArray works with a TreeController to present a tree of objects as a
+ single flat array. This is a private class used by TreeController. You
+ should not use it in your own application code.
+
+ @extends SC.Object
+ @extends SC.Array
+ @since SproutCore 1.0
+*/
+SC._TreeArray = SC.Object.extend(SC.Array,
+/** @scope SC._TreeArray.prototype */ {
+
+ /**
+ The tree controller this array is managing.
+
+ @property
+ @type SC.TreeController
+ */
+ controller: null,
+
+ treeChildrenIsVisibleFor: function(content) {
+ return content ? content.get('showChildren') : NO;
+ },
+
+ treeChildrenFor: function(content) {
+ return content ? content.get('children') : null ;
+ },
+
+ /**
+ Compute an index by walking the tree and building a cache that can be
+ used to quickly find locations in the tree and calculate their visible
+ state.
+
+ {
+ object: obj, // the object this item represents
+ disclosureState: state,
+ childCount: x, // number of child nodes if open branch
+ }
+ */
+ treeInfo: function() {
+ var children = this.controller.get('content'),
+ len = children.get('length'),
+ ret = [], idx, loc=0;
+
+ if (!children) return ret; // nothing to do
+
+ for(idx=0;idx<len;idx++) {
+ loc = this.fillTreeInfoAt(ret, loc, children.objectAt(idx));
+ }
+ ret.length = loc; // set length incase we skipped nodes at end
+ return ret ;
+ }.property().cacheable(),
+
+ fillTreeInfoAt: function(info, loc, node) {
+ var state = node ? this.treeDisclosureStateFor(node) : SC.LEAF_NODE;
+ childCount = 0,
+ next = loc+1,
+ children, idx;
+
+ if (state === SC.BRANCH_OPEN) {
+ if (children = this.treeChildrenFor(node)) {
+ childCount = children.get('length');
+ if (this.treeChildrenAreLeafNodes(node, children)) {
+ next += childCount ; // all leaf nodes, just make room for them
+
+ } else { // otherwise fill in space for them
+ for(idx=0;idx<childCount;idx++) {
+ next = this.fillTreeInfoAt(info, next, children.objectAt(idx));
+ childCount = next - (loc+1);
+ }
+ }
+ }
+ }
+
+ // fill in node info for myself
+ info[loc] = { node: node, disclosure: state, childCount: childCount };
+ return next ;
+ },
+
+ /**
+ Returns the tree info for the node at the specified index. This will
+ compute the tree info if needed then possibly fill in a leaf node as
+ needed.
+ */
+ treeInfoAt: function(idx) {
+ var info = this.get('treeInfo'),
+ ret = info[idx],
+ loc = 0, lim = info.length;
+
+ if (idx >= lim) return null ; // nothing to do
+
+ // if no node was returned, then this is a leaf node. We need to compute
+ // the info by just walking the tree
+ if (!ret) {
+ while(loc<idx && loc<lim) {
+ ret = info[loc];
+ }
+ }
+ },
+
+ treeLengthFor: function(children) {
+ var len = children ? children.get('length') : 0,
+ ret = len, idx, content, next;
+
+ for(idx=0;idx<len;idx++) {
+ content = children.objectAt(idx);
+ if (content && this.treeChildrenIsVisibleFor(content)) {
+ len += this.treeLengthFor(this.treeChildrenFor(content));
+ }
+ }
+
+ return length;
+ },
+
+ // ..........................................................
+ // SC.ARRAY SUPPORT
+ //
+
+ length: function() {
+ var children = this.controller.get('content');
+ return children ? this.treeLengthFor(children) : 0 ;
+ }.property().cacheable(),
+
+ /**
+ Returns the object at the specified index. The value will be extracted
+ from the flattened content array.
+ */
+ objectAt: function(contentIndex) {
+ var info = this.treeInfo(),
+ node = info[contentIndex];
+
+ // if no node is found then we may not have cached it yet. Leaf nodes
+ // work this way.
+ },
+
+ /**
+ Primitive to replace specific items in the array. The range you replace
+ must lie entirely within a single parent range in order to work.
+ Otherwise this will raise an exception.
+
+ @param {Number} start
+ Starting index in the array to replace. If idx >= length, then append
+ to the end of the array.
+
+ @param {Number} length
+ Number of elements that should be removed from the array, starting at
+ *idx*.
+
+ @param {Array} objects
+ An array of zero or more objects that should be inserted into the array
+ at *idx*
+
+ @returns {SC.TreeArray} receiver
+ */
+ replace: function(start, length, objects) {
+
+ },
+
+});
View
269 frameworks/desktop/tests/views/collection/content.js
@@ -0,0 +1,269 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view, content1, content2 ;
+
+module("SC.CollectionView.content", {
+ setup: function() {
+
+ // stub in collection view to verify that proper method are called
+ view = SC.CollectionView.create({
+
+ // ..........................................................
+ // STUB: contentPropertyDidChange
+ //
+ contentPropertyDidChange: CoreTest.stub('contentPropertyDidChange'),
+
+ // ..........................................................
+ // STUB: computeLayout
+ //
+ computeLayout: CoreTest.stub('computeLayout'),
+
+ // ..........................................................
+ // STUB: RELOAD
+ //
+ reload: CoreTest.stub("reload", {
+
+ // detect if we would reload everything.
+ shouldReloadAll: function() {
+ var history = this.history,
+ loc = history.length,
+ args;
+
+ while(--loc >= 0) {
+ args = history[loc];
+ if (args <= 1) return YES ;
+ if (args[1] === null) return YES ;
+ if (args[0].get('nowShowing').contains(args[1])) return YES ;
+ }
+ return NO ;
+ },
+
+ // join all reload indexes passed excluding null or undefined
+ indexes: function() {
+ var history = this.history,
+ loc = history.length,
+ ret = SC.IndexSet.create(),
+ args;
+
+ while(--loc >= 0) {
+ args = history[loc];
+ if (args[1] && args[1].isIndexSet) ret.add(args[1]);
+ }
+
+ return ret ;
+ },
+
+ // need to save the passed indexes set as a clone because it may be
+ // reused later...
+ action: function(indexes) {
+ var history = this.reload.history; // note "this" is view
+ if (indexes) {
+ history[history.length-1][1] = indexes.clone();
+ }
+
+ // simulate calling computeLayout() to match original impl.
+ this.computeLayout();
+
+ return this ;
+ },
+
+ expect: function(indexes, callCount) {
+
+ if (indexes === YES) {
+ equals(this.shouldReloadAll(), YES, 'reload() should reload all');
+ } else if (indexes !== NO) {
+ var expected = this.indexes();
+ var passed = indexes.isEqual(expected);
+ ok(passed, "expected reload(%@), actual: reload(%@)".fmt(indexes, expected));
+ }
+
+ if (callCount !== undefined) {
+ equals(this.callCount, callCount, 'reload() should have been called X times');
+ }
+
+ this.reset();
+ }
+
+ }),
+
+ expectLength: function(len) {
+ equals(view.get('length'), len, 'view.length after change');
+
+
+ var nowShowing = view.get('nowShowing'),
+ expected = SC.IndexSet.create(0,len);
+ ok(expected.isEqual(nowShowing), 'nowShowing expected: %@, actual: %@'.fmt(expected, nowShowing));
+ },
+
+ // reset stubs
+ reset: function() {
+ this.reload.reset();
+ this.contentPropertyDidChange.reset();
+ this.computeLayout.reset();
+ }
+
+ });
+
+ content1 = "a b c d e f".w().map(function(x) {
+ return SC.Object.create({ title: x });
+ });
+
+ content2 = "d e f x y z".w().map(function(x) {
+ return SC.Object.create({ title: x });
+ });
+
+ }
+});
+
+// ..........................................................
+// BASIC EDITS
+//
+
+test("setting content for first time", function() {
+ equals(view.get('content'), null, 'precond - view.content should be null');
+
+ view.set('content', content1);
+ view.reload.expect(YES, 2); // should reload everything
+ view.contentPropertyDidChange.expect(0); // should not call
+ view.computeLayout.expect(YES);
+ view.expectLength(content1.get('length'));
+});
+
+test("changing content with different size", function() {
+
+ view.set('content', "a b".w());
+ view.reset();
+
+ view.set('content', content2);
+ view.reload.expect(YES, 2); // call twice?
+ view.contentPropertyDidChange.expect(0); // should not call
+ view.computeLayout.expect(YES);
+ view.expectLength(content2.get('length'));
+});
+
+test("changing content with same size", function() {
+
+ view.set('content', "a b".w());
+ view.reset();
+
+ view.set('content', content2);
+ view.reload.expect(YES);
+ view.contentPropertyDidChange.expect(0); // should not call
+ view.computeLayout.expect(YES);
+ view.expectLength(content2.get('length'));
+});
+
+test("changing the content of a single item should reload that item", function() {
+
+ view.set('content', content1);
+ view.reset(); // don't care about this fire
+
+ content1.replace(1,1, ["X"]);
+ view.reload.expect(SC.IndexSet.create(1));
+ view.contentPropertyDidChange.expect(0); // should not call
+ view.computeLayout.expect(YES);
+ view.expectLength(content1.get('length'));
+});
+
+test("changing the content of several items should reload each item", function() {
+
+ view.set('content', content1);
+ view.reset(); // don't care about this fire
+
+ content1.replace(1,1, ["X"]);
+ content1.replace(3,1, ["X"]);
+ view.reload.expect(SC.IndexSet.create(1).add(3));
+ view.contentPropertyDidChange.expect(0); // should not call
+ view.computeLayout.expect(YES);
+ view.expectLength(content1.get('length'));
+});
+
+test("adding to end of content should reload new items", function() {
+
+ view.set('content', content1);
+ view.reset(); // don't care about this fire
+
+ content1.pushObject("X");
+ content1.pushObject("Y");
+
+ view.reload.expect(SC.IndexSet.create(content1.get('length')-2, 2));
+ view.contentPropertyDidChange.expect(0); // should not call
+ view.expectLength(content1.get('length'));
+ view.computeLayout.expect(YES);
+});
+
+test("removing from end of content should reload removed items", function() {
+
+ view.set('content', content1);
+ view.reset(); // don't care about this fire
+
+ content1.popObject();
+ content1.popObject();
+
+ view.reload.expect(SC.IndexSet.create(content1.get('length'), 2));
+ view.contentPropertyDidChange.expect(0); // should not call
+ view.expectLength(content1.get('length'));
+ view.computeLayout.expect(YES);
+});
+
+test("inserting into middle should reload all following items", function() {
+ view.set('content', content1);
+ view.reset(); // don't care about this fire
+
+ content1.insertAt(3, 'FOO');
+
+ view.reload.expect(SC.IndexSet.create(3, content1.get('length')-3));
+ view.contentPropertyDidChange.expect(0); // should not call
+ view.expectLength(content1.get('length'));
+ view.computeLayout.expect(YES);
+});
+
+// ..........................................................
+// EDITING PROPERTIES
+//
+
+test("editing properties when observeContentProperties is NO", function() {
+ equals(view.get('observeContentProperties'), NO, 'precond - observeContentProperties should be NO');
+
+ view.set('content', content1);
+ view.reset();
+ view.contentPropertyDidChange.reset();
+
+ var obj = content1.objectAt(3);
+ ok(obj !== null, 'precond -has object to edit');
+
+ obj.set('title', 'FOO');
+ view.reload.expect(NO, 0);
+ view.contentPropertyDidChange.expect(0);
+ view.computeLayout.expect(NO);
+});
+
+test("editing properties when observeContentProperties is YES", function() {
+ // assume this only matters when content is set. normally you shouldn't
+ // change this value on a view once it is created.
+ view.set('observeContentProperties', YES);
+ equals(view.get('observeContentProperties'), YES, 'precond - observeContentProperties should be YES');
+
+ view.set('content', content1);
+ view.reset();
+ view.contentPropertyDidChange.reset();
+
+ var obj = content1.objectAt(3);
+ ok(obj !== null, 'precond -has object to edit');
+
+ obj.set('title', 'FOO');
+ view.reload.expect(NO, 0);
+ view.contentPropertyDidChange.expect(1);
+ view.computeLayout.expect(NO);
+});
+
+
+
+
+
+
View
82 frameworks/desktop/tests/views/collection/deleteSelection.js
@@ -0,0 +1,82 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view, sel, beforeLen, afterLen, content ;
+
+module("SC.CollectionView.deleteSelection", {
+ setup: function() {
+
+ content = "1 2 3 4 5 6 7 8 9 10".w().map(function(x) {
+ return SC.Object.create({ title: x });
+ });
+
+ sel = SC.SelectionSet.create().add(content,4,2);
+
+ view = SC.CollectionView.create({
+ content: content,
+ selection: sel,
+ canDeleteContent: YES
+ });
+
+ beforeLen = content.get('length');
+ afterLen = beforeLen - sel.get('length');
+ }
+});
+
+// ..........................................................
+// BASIC OPERATIONS
+//
+
+test("canDeleteContent", function() {
+
+ view.set('canDeleteContent', NO);
+ equals(view.deleteSelection(), NO, 'should return NO if not allowed');
+ equals(content.get('length'), beforeLen, 'content.length should not change');
+ equals(view.get('selection').get('length'), 2, 'should not change selection');
+
+ view.set('canDeleteContent', YES);
+ equals(view.deleteSelection(), YES, 'should return YES if allowed');
+ equals(content.get('length'), afterLen, 'content.length should change');
+ equals(view.get('selection').get('length'), 0, 'should have empty selection');
+});
+
+test("empty selection case", function() {
+ view.select(null); // clear selection
+ view.set('canDeleteContent', YES);
+ equals(view.get('selection').get('length'), 0, 'precond - should have empty selection');
+
+ equals(view.deleteSelection(), NO, 'should return NO if not allowed');
+ equals(content.get('length'), beforeLen, 'content.length should not change');
+});
+
+test("delegate.collectionViewShouldDeleteIndexes", function() {
+ view.set('canDeleteContent', YES);
+ view.delegate = SC.Object.create(SC.CollectionViewDelegate, {
+
+ v: null,
+
+ collectionViewShouldDeleteIndexes: function() { return this.v; }
+ });
+
+ // delegate returns NO
+ equals(view.deleteSelection(), NO, 'should return NO if not allowed');
+ equals(content.get('length'), beforeLen, 'content.length should not change');
+ equals(view.get('selection').get('length'), 2, 'should not change selection');
+
+ // delegate returns partial
+ view.delegate.v = SC.IndexSet.create(4,1);
+ equals(view.get('selectionDelegate'), view.delegate, 'selection delegate should be delegate object');
+ equals(view.deleteSelection(), YES, 'should return YES if allowed');
+ equals(content.get('length'), afterLen+1, 'content.length should change');
+ equals(view.get('selection').get('length'), 1, 'non-deleted parts should remain selected %@'.fmt(view.get('selection')));
+});
+
+// ..........................................................
+// EDGE CASES
+//
+
+// Add special edge cases here
View
199 frameworks/desktop/tests/views/collection/deselect.js
@@ -0,0 +1,199 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view, sel, content ;
+
+module("SC.CollectionView.deselect", {
+ setup: function() {
+
+ content = "1 2 3 4 5 6 7 8 9 10".w().map(function(x) {
+ return SC.Object.create({ title: x });
+ });
+
+ sel = SC.SelectionSet.create().add(content,4,4);
+
+ view = SC.CollectionView.create({
+ content: content,
+ selection: sel
+ });
+ }
+});
+
+// ..........................................................
+// BASIC OPERATIONS
+//
+
+test("delect(indexes=Number)", function() {
+ var expected = SC.SelectionSet.create().add(content,4,4).remove(content,6),
+ actual ;
+
+ SC.run(function() { view.deselect(6); });
+
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should remove index (expected: %@ actual: %@)'.fmt(expected, actual));
+});
+
+
+test("delect(indexes=IndexSet)", function() {
+ var actual, expected = SC.SelectionSet.create()
+ .add(content,4,4).remove(content,6,2);
+
+ SC.run(function() { view.deselect(SC.IndexSet.create(6,2)); });
+
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should remove index (expected: %@ actual: %@)'.fmt(expected, actual));
+});
+
+
+test("delect() with empty selection", function() {
+ var expected = SC.SelectionSet.create(),
+ actual ;
+
+ SC.run(function() { view.set('selection', expected); });
+ SC.run(function() { view.deselect(SC.IndexSet.create(6,2)); });
+
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'deselect should do nothing (expected: %@ actual: %@)'.fmt(expected, actual));
+});
+
+// ..........................................................
+// DELEGATE TESTS
+//
+
+var del;
+
+module("SC.CollectionView.deselect - delegate support", {
+ setup: function() {
+
+ content = "1 2 3 4 5 6 7 8 9 10".w().map(function(x) {
+ return SC.Object.create({ title: x });
+ });
+
+ del = SC.Object.create(SC.CollectionViewDelegate);
+ sel = SC.SelectionSet.create().add(content, 4,4);
+ view = SC.CollectionView.create({
+ delegate: del,
+ selection: sel,
+ content: content
+ });
+ }
+});
+
+test("should call delegate if set", function() {
+ var callCount1 = 0, callCount2 = 0 ;
+ del.collectionViewShouldDeselectIndexes = function(v, indexes, extend) {
+ callCount1++;
+ return indexes;
+ };
+
+ del.collectionViewSelectionForProposedSelection = function(v, indexes) {
+ callCount2++;
+ return indexes ;
+ };
+
+ view.deselect(3);
+ equals(callCount1, 1, 'should invoke collectionViewShouldDeselectIndexes on delegate 1x');
+ equals(callCount2, 1, 'should invoke collectionViewSelectionForProposedSelection on delegate 1x if change is allowed');
+
+});
+
+test("not calling collectionViewSelectionForProposedSelection if collectionViewShouldDeselectIndexes returns null", function() {
+ var callCount1 = 0, callCount2 = 0 ;
+ del.collectionViewShouldDeselectIndexes = function(v, indexes, extend) {
+ callCount1++;
+ return null;
+ };
+
+ del.collectionViewSelectionForProposedSelection = function(v, indexes) {
+ callCount2++;
+ return indexes ;
+ };
+
+ view.deselect(3);
+ equals(callCount1, 1, 'should invoke collectionViewShouldDeselectIndexes on delegate 1x');
+ equals(callCount2, 0, 'should NOT invoke collectionViewSelectionForProposedSelection on delegate since no change was allowed');
+
+});
+
+test("del.collectionViewShouldDeselectIndexes - replacing selection", function() {
+
+ del.collectionViewShouldDeselectIndexes = function(v, indexes, extend) {
+ return indexes.without(3);
+ };
+ view.deselect(SC.IndexSet.create(0,4));
+
+ var expected = sel.copy().remove(content,SC.IndexSet.create(0,4).remove(3)),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should only include those allowed by delegate (i.e. not index 3) (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+test("del.collectionViewShouldDeselectIndexes - returns empty index set", function() {
+
+ del.collectionViewShouldDeselectIndexes = function(v, indexes) {
+ return indexes.without(5);
+ };
+
+ view.deselect(4);
+ view.deselect(5); // should be ignored
+ var expected = sel.copy().remove(content,4),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should deselect only items returned by delegate (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+
+test("del.collectionViewShouldDeselectIndexes - delegate returns null", function() {
+
+ del.collectionViewShouldDeselectIndexes = function(v, indexes, extend) {
+ return null;
+ };
+
+ view.deselect(4); // should be ignored
+ var expected = sel,
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should not change if delegate returns null (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+
+test("del.collectionViewSelectionForProposedSelection - returns indexes", function() {
+
+ del.collectionViewSelectionForProposedSelection = function(v, indexes) {
+
+ var expected = sel.copy().remove(content,5),
+ actual = indexes;
+ ok(expected.isEqual(actual), 'should pass proposed selection to delegate (expected: %@ actual: %@)'.fmt(expected, actual));
+
+ equals(v, view, 'should pass view to delegate');
+
+ return SC.SelectionSet.create().add(content, 10,20);
+ };
+
+ view.deselect(5); // should be ignored
+ var expected = SC.SelectionSet.create().add(content, 10,20),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'should set selection to whatever is returned from delegate (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+
+test("del.collectionViewSelectionForProposedSelection - returns null", function() {
+
+ del.collectionViewSelectionForProposedSelection = function(v, indexes) {
+ return null;
+ };
+
+ view.deselect(5); // should be ignored
+ var expected = SC.SelectionSet.create(),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'should set selection to empty set if returns null (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+
+
View
240 frameworks/desktop/tests/views/collection/itemViewForContentIndex.js
@@ -0,0 +1,240 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view, del, content ;
+
+module("SC.CollectionView.itemViewForContentIndex", {
+ setup: function() {
+ content = "a b c".w().map(function(x) {
+ return SC.Object.create({ title: x });
+ });
+
+ del = {
+ fixture: {
+ isEnabled: YES,
+ isSelected: YES,
+ outlineLevel: 3,
+ disclosureState: SC.LEAF_NODE
+ },
+
+ contentIndexIsEnabled: function() {
+ return this.fixture.isEnabled;
+ },
+
+ contentIndexIsSelected: function() {
+ return this.fixture.isSelected;
+ },
+
+ contentIndexOutlineLevel: function() {
+ return this.fixture.outlineLevel;
+ },
+
+ contentIndexDisclosureState: function() {
+ return this.fixture.disclosureState ;
+ }
+ };
+
+ // NOTE: delegate methods above are added here.
+ view = SC.CollectionView.create(del, {
+ content: content,
+
+ layoutForContentIndex: function(contentIndex) {
+ return this.fixtureLayout ;
+ },
+
+ fixtureLayout: { left: 0, right: 0, top:0, bottom: 0 },
+
+ groupExampleView: SC.View.extend(), // custom for testing
+
+ exampleView: SC.View.extend(), // custom for testing
+
+ testAsGroup: NO,
+
+ contentIndexIsGroup: function() { return this.testAsGroup; },
+
+ contentGroupIndexes: function() {
+ if (this.testAsGroup) {
+ return SC.IndexSet.create(0, this.get('length'));
+ } else return null ;
+ }
+
+ });
+
+ // add in delegate mixin
+ del = SC.mixin({}, SC.CollectionContentDelegate, del);
+
+ }
+});
+
+function shouldMatchFixture(itemView, fixture) {
+ var key;
+ for(key in fixture) {
+ if (!fixture.hasOwnProperty(key)) continue;
+ equals(itemView.get(key), fixture[key], 'itemView.%@ should match delegate value'.fmt(key));
+ }
+}
+
+test("creating basic item view", function() {
+ var itemView = view.itemViewForContentIndex(1);
+
+ // should use exampleView
+ ok(itemView, 'should return itemView');
+ ok(itemView.kindOf(view.exampleView), 'itemView %@ should be kindOf %@'.fmt(itemView, view.exampleView));
+
+ // set added properties
+ equals(itemView.get('content'), content.objectAt(1), 'itemView.content should be set to content item');
+ equals(itemView.get('contentIndex'), 1, 'itemView.contentIndex should be set');
+ equals(itemView.get('owner'), view, 'itemView.owner should be collection view');
+ equals(itemView.get('displayDelegate'), view, 'itemView.displayDelegate should be collection view');
+ equals(itemView.get('parentView'), view, 'itemView.parentView should be collection view');
+
+ // test data from delegate
+ shouldMatchFixture(itemView, view.fixture);
+});
+
+test("returning item from cache", function() {
+
+ var itemView1 = view.itemViewForContentIndex(1);
+ ok(itemView1, 'precond - first call returns an item view');
+
+ var itemView2 = view.itemViewForContentIndex(1);
+ equals(itemView2, itemView1, 'retrieving multiple times should same instance');
+
+ var itemView3 = view.itemViewForContentIndex(1, YES);
+ ok(itemView1 !== itemView3, 'itemViewForContentIndex(1, YES) should return new item even if it is already cached actual :%@'.fmt(itemView3));
+
+ var itemView4 = view.itemViewForContentIndex(1);
+ equals(itemView4, itemView3, 'itemViewForContentIndex(1) [no reload] should return newly cached item after recache');
+
+});
+
+// ..........................................................
+// ALTERNATE WAYS TO GET AN EXAMPLE VIEW
+//
+
+test("contentExampleViewKey is set and content has property", function() {
+ var CustomView = SC.View.extend();
+ var obj = content.objectAt(1);
+ obj.set('foo', CustomView);
+ view.set('contentExampleViewKey', 'foo');
+
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'should return item view');
+ ok(itemView.kindOf(CustomView), 'itemView should be custom view specified on object. actual: %@'.fmt(itemView));
+});
+
+test("contentExampleViewKey is set and content is null", function() {
+ var CustomView = SC.View.extend();
+ view.set('contentExampleViewKey', 'foo');
+ content.replace(1,1,[null]);
+
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'should return item view');
+ equals(itemView.get('content'), null, 'itemView content should be null');
+ ok(itemView.kindOf(view.exampleView), 'itemView should be exampleView (%@). actual: %@'.fmt(view.exampleView, itemView));
+});
+
+test("contentExampleViewKey is set and content property is empty", function() {
+ var CustomView = SC.View.extend();
+ view.set('contentExampleViewKey', 'foo');
+
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'should return item view');
+ equals(itemView.get('content'), content.objectAt(1), 'itemView should have content');
+ ok(itemView.kindOf(view.exampleView), 'itemView should be exampleView (%@). actual: %@'.fmt(view.exampleView, itemView));
+});
+
+// ..........................................................
+// GROUP EXAMPLE VIEW
+//
+
+test("delegate says content is group", function() {
+ view.testAsGroup = YES ;
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'should return itemView');
+ ok(itemView.kindOf(view.groupExampleView), 'itemView should be groupExampleView (%@). actual: %@'.fmt(view.groupExampleView, itemView));
+ ok(itemView.isGroupView, 'itemView.isGroupView should be YES');
+});
+
+test("contentGroupExampleViewKey is set and content has property", function() {
+ view.testAsGroup = YES ;
+
+ var CustomView = SC.View.extend();
+ var obj = content.objectAt(1);
+ obj.set('foo', CustomView);
+ view.set('contentGroupExampleViewKey', 'foo');
+
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'should return item view');
+ ok(itemView.kindOf(CustomView), 'itemView should be custom view specified on object. actual: %@'.fmt(itemView));
+ ok(itemView.isGroupView, 'itemView.isGroupView should be YES');
+});
+
+test("contentGroupExampleViewKey is set and content is null", function() {
+ view.testAsGroup = YES ;
+
+ var CustomView = SC.View.extend();
+ view.set('contentGroupExampleViewKey', 'foo');
+ content.replace(1,1,[null]);
+
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'should return item view');
+ equals(itemView.get('content'), null, 'itemView content should be null');
+ ok(itemView.kindOf(view.groupExampleView), 'itemView should be exampleView (%@). actual: %@'.fmt(view.groupExampleView, itemView));
+ ok(itemView.isGroupView, 'itemView.isGroupView should be YES');
+});
+
+test("contentGroupExampleViewKey is set and content property is empty", function() {
+ view.testAsGroup = YES ;
+
+ var CustomView = SC.View.extend();
+ view.set('contentGroupExampleViewKey', 'foo');
+
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'should return item view');
+ equals(itemView.get('content'), content.objectAt(1), 'itemView should have content');
+ ok(itemView.kindOf(view.groupExampleView), 'itemView should be exampleView (%@). actual: %@'.fmt(view.groupExampleView, itemView));
+ ok(itemView.isGroupView, 'itemView.isGroupView should be YES');
+});
+
+
+// ..........................................................
+// DELEGATE SUPPORT
+//
+
+test("consults delegate if set", function() {
+ view.fixture = null; //break to make sure this is not used
+ view.delegate = del;
+
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'returns item view');
+ shouldMatchFixture(itemView, del.fixture);
+});
+
+test("consults content if implements mixin and delegate not set", function() {
+ view.fixture = null; //break to make sure this is not used
+ view.delegate = null;
+
+ SC.mixin(content, del) ; // add delegate methods to content
+
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'returns item view');
+ shouldMatchFixture(itemView, content.fixture);
+});
+
+
+test("prefers delegate over content if both implement mixin", function() {
+ view.fixture = null; //break to make sure this is not used
+ view.delegate = del;
+ SC.mixin(content, del) ; // add delegate methods to content
+ content.fixture = null ; //break
+
+ var itemView = view.itemViewForContentIndex(1);
+ ok(itemView, 'returns item view');
+ shouldMatchFixture(itemView, del.fixture);
+});
+
View
65 frameworks/desktop/tests/views/collection/layerIdFor.js
@@ -0,0 +1,65 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view ;
+
+module("SC.CollectionView.layerIdFor, contentIndexForLayerId", {
+ setup: function() {
+ view = SC.CollectionView.create();
+ }
+});
+
+// ..........................................................
+// TEST ROUND TRIP
+//
+
+test("0 index", function() {
+ var layerId = view.layerIdFor(0) ;
+ ok(layerId, 'should return string');
+ equals(view.contentIndexForLayerId(layerId), 0, 'should parse out idx');
+});
+
+test("10 index", function() {
+ var layerId = view.layerIdFor(10) ;
+ ok(layerId, 'should return string');
+ equals(view.contentIndexForLayerId(layerId), 10, 'should parse out idx');
+});
+
+// ..........................................................
+// TEST SPECIAL PARSING CASES
+//
+
+test("parse null id", function() {
+ equals(view.contentIndexForLayerId(null), null, 'should return null');
+});
+
+test("parse collection view's layerId", function() {
+ equals(view.contentIndexForLayerId(view.get('layerId')), null, 'should return null');
+});
+
+test("parse layerId from other object", function() {
+ var otherView = SC.CollectionView.create();
+ var id = otherView.layerIdFor(20);
+ equals(view.contentIndexForLayerId(id), null, 'should return null');
+});
+
+test("parse short arbitrary id", function() {
+ equals(view.contentIndexForLayerId("sc242"), null, 'should return null');
+});
+
+test("parse long arbitrary id", function() {
+ equals(view.contentIndexForLayerId("sc242-234-2453-sdf3"), null, 'should return null');
+});
+
+test("parse empty string", function() {
+ equals(view.contentIndexForLayerId(""), null, 'should return null');
+});
+
+test("parse garbage", function() {
+ equals(view.contentIndexForLayerId(234), null, 'should return null');
+});
+
View
88 frameworks/desktop/tests/views/collection/length.js
@@ -0,0 +1,88 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view, content1, content2 ;
+
+module("SC.CollectionView.length", {
+ setup: function() {
+
+ // stub in collection view to verify that proper method are called
+ view = SC.CollectionView.create({
+
+ observer: CoreTest.stub('observer(length)').observes('length'),
+ computeLayout: CoreTest.stub('computeLayout'),
+
+ reset: function(){
+ this.observer.reset();
+ this.computeLayout.reset();
+ }
+ });
+
+ content1 = "a b c".w();
+ content2 = "d e f g h".w();
+ }
+});
+
+test("no content should have length of 0", function() {
+ equals(view.get('content'), null, 'precond - content is null');
+ equals(view.get('length'), 0, 'length should be 0');
+});
+
+test("length should be set property on newly inited object with content already set", function() {
+ view = SC.CollectionView.create({ content: content1 });
+ equals(view.get('length'), content1.get('length'), 'view.length should be content.length');
+});
+
+test("setting content should update length & notify", function() {
+ view.set('content', content1);
+ equals(view.get('length'), content1.get('length'), 'view.length should equal new length');
+ view.observer.expect(1);
+});
+
+test("changing the content should update length & notify", function() {
+ view.set('content', content1);
+ view.reset(); // don't care.
+ ok(content1.get('length') !== content2.get('length'), 'precond - content1.length should not equal content2.length');
+
+ view.set('content', content2);
+
+ equals(view.get('length'), content2.get('length'), 'view.length should equal new length');
+ view.observer.expect(1);
+});
+
+test("modifying content to make it shorter should update view length and notify",function() {
+ view.set('content', content1);
+ view.reset(); // don't care.
+
+ var len = content1.get('length');
+ content1.removeAt(1);
+
+ equals(view.get('length'), len-1, 'view.length should equal new length');
+ view.observer.expect(1);
+});
+
+test("modifying content to add length should update view length and notify",function() {
+ view.set('content', content1);
+ view.reset(); // don't care.
+
+ var len = content1.get('length');
+ content1.insertAt(1, 'foo');
+
+ equals(view.get('length'), len+1, 'view.length should equal new length');
+ view.observer.expect(1);
+});
+
+test("modifying content so that it does not change the length should NOT change view length OR notify", function() {
+ view.set('content', content1);
+ view.reset(); // don't care.
+
+ var len = content1.get('length');
+ content1.replace(1, 1, 'foo');
+
+ equals(view.get('length'), len, 'view.length should equal new length');
+ view.observer.expect(0);
+});
View
163 frameworks/desktop/tests/views/collection/mouse.js
@@ -0,0 +1,163 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view, content, pane ;
+
+module("SC.CollectionView Mouse Events", {
+ setup: function() {
+
+ SC.RunLoop.begin();
+
+ content = "1 2 3 4 5 6 7 8 9 10".w().map(function(x) {
+ return SC.Object.create({ value: x });
+ });
+
+ view = SC.CollectionView.create({
+ content: content,
+
+ layout: { top: 0, left: 0, width: 300, height: 500 },
+
+ layoutForContentIndex: function(idx) {
+ return { left: 0, right: 0, top: idx * 50, height: 50 };
+ },
+
+ isVisibleInWindow: YES,
+ acceptsFirstResponder: YES
+ });
+
+ pane = SC.MainPane.create();
+ pane.appendChild(view);
+ pane.append();
+
+ SC.RunLoop.end();
+ },
+
+ teardown: function() {
+ SC.RunLoop.begin();
+ pane.remove();
+ SC.RunLoop.end();
+ }
+});
+
+/*
+ Simulates clicking on the specified index. If you pass verify as YES or NO
+ also verifies that the item view is subsequently selected or not.
+
+ @param {SC.CollectionView} view the view
+ @param {Number} index the index to click on
+ @param {Boolean} shiftKey simulate shift key pressed
+ @param {Boolean} ctrlKey simulate ctrlKey pressed
+ @param {IndexSet} expected expected selection
+ @returns {void}
+*/
+function clickOn(view, index, shiftKey, ctrlKey, expected) {
+ var itemView = view.getPath('childViews.%@'.fmt(index)),
+ layer = itemView.get('layer'),
+ opts = { shiftKey: shiftKey, ctrlKey: ctrlKey },
+ sel, ev, modifiers;
+
+ ok(layer, 'precond - itemView[%@] should have layer'.fmt(index));
+
+ ev = SC.Event.simulateEvent(layer, 'mousedown', opts);
+ SC.Event.trigger(layer, 'mousedown', [ev]);
+
+ ev = SC.Event.simulateEvent(layer, 'mouseup', opts);
+ SC.Event.trigger(layer, 'mouseup', [ev]);
+
+ if (expected !== undefined) {
+ sel = view.get('selection');
+
+ modifiers = [];
+ if (shiftKey) modifiers.push('shift');
+ if (ctrlKey) modifiers.push('ctrl');
+ modifiers = modifiers.length > 0 ? modifiers.join('+') : 'no modifiers';
+
+ expected = SC.SelectionSet.create().add(view.get('content'), expected);
+ ok(expected.isEqual(sel), 'should have selection: %@ after click with %@ on item[%@], actual: %@'.fmt(expected, modifiers, index, sel));
+ }
+
+ layer = itemView = null ;
+}
+
+// ..........................................................
+// basic click
+//
+
+test("clicking on an item should select it", function() {
+ clickOn(view, 3, NO, NO, SC.IndexSet.create(3));
+});
+
+test("clicking on a selected item should clear selection and reselect it it", function() {
+
+ view.select(SC.IndexSet.create(1,5));
+ clickOn(view, 3, NO, NO, SC.IndexSet.create(3));
+});
+
+test("clicking on unselected item should clear selection and select it", function() {
+
+ view.select(SC.IndexSet.create(1,5));;
+ clickOn(view, 7, NO, NO, SC.IndexSet.create(7));
+});
+
+test("first responder", function() {
+ clickOn(view, 3);
+ equals(view.get('isFirstResponder'), YES, 'view.isFirstResponder should be YES after mouse down');
+});
+
+// ..........................................................
+// ctrl-click mouse down
+//
+
+test("ctrl-clicking on unselected item should add to selection", function() {
+ clickOn(view,3, NO, YES, SC.IndexSet.create(3));
+ clickOn(view,5, NO, YES, SC.IndexSet.create(3).add(5));
+});
+
+test("ctrl-clicking on selected item should remove from selection", function() {
+ clickOn(view,3, NO, YES, SC.IndexSet.create(3));
+ clickOn(view,5, NO, YES, SC.IndexSet.create(3).add(5));
+ clickOn(view,3, NO, YES, SC.IndexSet.create(5));
+ clickOn(view,5, NO, YES, SC.IndexSet.create());
+});
+
+// ..........................................................
+// shift-click mouse down
+//
+
+test("shift-clicking on an item below should extend the selection", function() {
+ clickOn(view, 3, NO, NO, SC.IndexSet.create(3));
+ clickOn(view, 5, YES, NO, SC.IndexSet.create(3,3));
+});
+
+
+test("shift-clicking on an item above should extend the selection", function() {
+ clickOn(view, 3, NO, NO, SC.IndexSet.create(3));
+ clickOn(view, 1, YES, NO, SC.IndexSet.create(1,3));
+});
+
+test("shift-clicking inside selection first time should reduce selection from top", function() {
+ view.select(SC.IndexSet.create(3,4));
+ clickOn(view,4, YES, NO, SC.IndexSet.create(3,2));
+});
+
+test("shift-click below to extend selection down then shift-click inside selection should reduce selection", function() {
+ clickOn(view, 3, NO, NO, SC.IndexSet.create(3));
+ clickOn(view, 5, YES, NO, SC.IndexSet.create(3,3));
+ clickOn(view,4, YES, NO, SC.IndexSet.create(3,2));
+});
+
+test("shift-click above to extend selection down then shift-click inside selection should reduce top of selection", function() {
+ clickOn(view, 3, NO, NO, SC.IndexSet.create(3));
+ clickOn(view, 1, YES, NO, SC.IndexSet.create(1,3));
+ clickOn(view,2, YES, NO, SC.IndexSet.create(2,2));
+});
+
+test("shift-click below bottom of selection then shift click on top of selection should select only top item", function() {
+ clickOn(view, 3, NO, NO, SC.IndexSet.create(3));
+ clickOn(view, 5, YES, NO, SC.IndexSet.create(3,3));
+ clickOn(view,3, YES, NO, SC.IndexSet.create(3));
+});
View
121 frameworks/desktop/tests/views/collection/nowShowing.js
@@ -0,0 +1,121 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view, content1, content2 ;
+
+module("SC.CollectionView.nowShowing", {
+ setup: function() {
+
+ content1 = "a b c".w();
+
+ // stub in collection view to verify that proper method are called
+ view = SC.CollectionView.create({
+
+ // updateContentRangeObserver
+ updateContentRangeObserver: CoreTest.stub('updateContentRangeObserver'),
+
+ // reload()
+
+ reloadCallCount: 0,
+ reloadIndexes: "not called",
+
+ reload: function(indexes) {
+ this.reloadIndexes = indexes ? indexes.frozenCopy() : indexes;
+ this.reloadCallCount++;
+ },
+
+ expectReload: function(indexes, callCount) {
+ if (indexes !== NO) {
+ var pass = (indexes === null) ? (this.reloadIndexes === null) : indexes.isEqual(this.reloadIndexes);
+ if (!pass) {
+ indexes.isEqual(this.reloadIndexes);
+ }
+ ok(pass, 'should have called reload(%@), actual reload(%@)'.fmt(indexes, this.reloadIndexes));
+ }
+
+ if (callCount !== NO) {
+ equals(this.reloadCallCount, callCount, 'reload() should be called X times');
+ }
+ },
+
+ // GENERAL SUPPORT
+
+ observer: CoreTest.stub('nowShowing observer').observes('nowShowing'),
+
+ reset: function() {
+ this.updateContentRangeObserver.reset();
+ this.reloadCallCount = 0 ;
+ this.reloadIndexes = 'not called';
+ this.observer.reset();
+ },
+
+ nextNowShowing: SC.IndexSet.create(0,3),
+
+ // override to reeturn whatever index set is in nextNowShowing property just
+ // for testing.
+ computeNowShowing: function() {
+ return this.nextNowShowing;
+ },
+
+ content: content1
+
+ });
+
+ // some observers will fire on creation because of the content. just
+ // ignore them
+ view.reset();
+
+ }
+});
+
+// ..........................................................
+// GENERAL TESTS
+//
+
+test("nowShowing should reflect content on create", function() {
+
+ same(view.get('nowShowing'), view.nextNowShowing, 'should have now showing value');
+});
+
+test("if nowShowing changes but actual value stays the same, should do nothing", function() {
+
+ // trigger any observers
+ view.notifyPropertyChange('nowShowing');
+ view.observer.expect(1);
+ view.expectReload(NO, 0);
+ view.updateContentRangeObserver.expect(0);
+});
+
+test("nowShowing changes to new index set with some overlap", function() {
+ view.nextNowShowing = SC.IndexSet.create(2,5);
+ view.notifyPropertyChange('nowShowing');
+ view.observer.expect(1);
+
+ // expect inverse of intersection
+ view.expectReload(SC.IndexSet.create(0,2).add(3,4), 1);
+
+ view.updateContentRangeObserver.expect(1);
+});
+
+test("nowShowing changes to new index set with no overlap", function() {
+ view.nextNowShowing = SC.IndexSet.create(10,3);
+ view.notifyPropertyChange('nowShowing');
+ view.observer.expect(1);
+
+ // union of both ranges
+ view.expectReload(SC.IndexSet.create(0,3).add(10,3), 1);
+
+ view.updateContentRangeObserver.expect(1);
+});
+
+// ..........................................................
+// SPECIAL CASES
+//
+
+// Add any specific cases you find that break here
+
+
View
177 frameworks/desktop/tests/views/collection/reload.js
@@ -0,0 +1,177 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view, content ;
+
+module("SC.CollectionView.reload", {
+ setup: function() {
+ content = "1 2 3 4 5 6 7 8 9 10".w().map(function(x) {
+ return SC.Object.create({ value: x });
+ });
+
+ view = SC.CollectionView.create({
+ content: content,
+
+ isVisibleInWindow: YES
+ });
+ }
+});
+
+/*
+ Verfies that the item views for the passed collection view match exactly the
+ content array passed. If shouldShowAllContent is also YES then verifies
+ that the nowShowing range is showing the entire content range.
+
+ @param {SC.CollectionView} view the view to test
+ @param {SC.Array} content the content array
+ @param {Boolean} shouldShowAllContent
+ @param {String} testName optional test name
+ @returns {void}
+*/
+function verifyItemViews(view, content, shouldShowAllContent, testName) {
+ var nowShowing = view.get('nowShowing'),
+ childViews = view.get('childViews');
+
+ if (testName === undefined) testName='';
+
+ if (shouldShowAllContent) {
+ ok(nowShowing.isEqual(SC.IndexSet.create(0, content.get('length'))), '%@ nowShowing (%@) should equal (0..%@)'.fmt(testName, nowShowing, content.get('length')-1));
+ }
+
+ equals(childViews.get('length'), nowShowing.get('length'), '%@ view.childViews.length should match nowShowing.length'.fmt(testName));
+
+ // childViews should be in same order as nowShowing indexes at all times.
+ var iter= 0;
+ nowShowing.forEach(function(idx) {
+ var itemView = childViews.objectAt(iter),
+ item = content.objectAt(idx);
+ ok(itemView, 'childViews[%@] should have itemView'.fmt(iter));
+ if (itemView) {
+ equals(itemView.get('content'), item, '%@ childViews[%@].content should equal content[%@]'.fmt(testName, iter,idx));
+ }
+ iter++;
+ });
+}
+
+// ..........................................................
+// BASIC TESTS
+//
+
+test("should only reload when isVisibleInWindow", function() {
+
+ view.set('isVisibleInWindow', NO);
+ //view.isVisibleInWindow = NO ;
+
+ var len = view.getPath('childViews.length');
+
+ SC.run(function() {
+ view.reload();
+ });
+
+ equals(view.getPath('childViews.length'), len, 'view.childViews.length should not change while offscreen');
+
+ SC.RunLoop.begin();
+ view.set('isVisibleInWindow', YES);
+ SC.RunLoop.end();
+
+ equals(view.getPath('childViews.length'), content.get('length'), 'view.childViews.length should change when moved onscreen if reload is pending');
+});
+
+test("should automatically reload if content is set when collection view is first created", function() {
+ ok(view.get('content'), 'precond - should have content');
+ SC.RunLoop.begin();
+ SC.RunLoop.end();
+
+ verifyItemViews(view, content, YES);
+});
+
+test("reload(null) should generate item views for all items", function() {
+
+ SC.RunLoop.begin();
+ view.reload();
+ SC.RunLoop.end(); // allow reload to run
+
+ verifyItemViews(view, content, YES);
+});
+
+test("reload(index set) should update item view for items in index only", function() {
+
+ // make sure views are loaded first time
+ SC.run(function() { view.reload(); });
+
+ // now get a couple of child views.
+ var cv1 = view.childViews[1], cv2 = view.childViews[3];
+
+ // and then reload them
+ SC.run(function() { view.reload(SC.IndexSet.create(1).add(3)); });
+
+ ok(cv1 !== view.childViews[1], 'view.childViews[1] should be new instance after view.reload(<1,3>) actual: %@ expected: %@'.fmt(view.childViews[1], cv1));
+ ok(cv2 !== view.childViews[3], 'view.childViews[3] should be new instance after view.reload(<1,3>) actual: %@ expected: %@'.fmt(view.childViews[3], cv2));
+
+ // verify integrity
+ verifyItemViews(view, content, YES);
+});
+
+test("adding items to content should reload item views at end", function() {
+
+ SC.run(function() {
+ content.pushObject(SC.Object.create());
+ });
+ verifyItemViews(view, content, YES);
+});
+
+test("removing items from content should remove item views", function() {
+
+ SC.run(function() {
+ content.popObject();
+ });
+ verifyItemViews(view, content, YES);
+});
+
+// ..........................................................
+// SPECIAL CASES
+//
+
+test("remove and readd item", function() {
+ // first remove an item.
+ var item = content.objectAt(0);
+ SC.run(function() { content.removeAt(0); });
+ verifyItemViews(view, content, YES, 'after content.removeAt(0)');
+
+ // then readd the item
+ SC.run(function() { content.insertAt(0, item); });
+ verifyItemViews(view, content, YES, 'after content.insertAt(0,item)');
+
+ // then add another item
+ item = SC.Object.create();
+ SC.run(function() { content.pushObject(item); });
+ verifyItemViews(view, content, YES, 'after content.pushObject(item)');
+
+ // and remove the item
+ SC.run(function() { content.popObject(); });
+ verifyItemViews(view, content, YES, 'after content.popObject(item)');
+
+});
+
+test("reloading should only render nowShowing component", function() {
+ var expected = SC.IndexSet.create(0,2).add(6);
+
+ view = SC.CollectionView.create({
+ content: content,
+ computeNowShowing: function() {
+ return expected;
+ },
+ isVisibleInWindow: YES
+ });
+
+ SC.RunLoop.begin();
+ view.reload();
+ SC.RunLoop.end();
+
+ same(view.get('nowShowing'), expected, 'precond - should have limited now showing');
+ equals(view.get('childViews').get('length'), expected.get('length'), 'should only render number of child views in IndexSet');
+});
View
240 frameworks/desktop/tests/views/collection/select.js
@@ -0,0 +1,240 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view ;
+var content = "1 2 3 4 5 6 7 8 9 10".w().map(function(x) {
+ return SC.Object.create({ title: x });
+});
+
+module("SC.CollectionView.select", {
+ setup: function() {
+ view = SC.CollectionView.create({
+ content: content
+ });
+ }
+});
+
+// ..........................................................
+// BASIC OPERATIONS
+//
+
+test("return value", function() {
+ equals(view.select(3), view, 'should return receiver') ;
+});
+
+test("calling select(indexes=Number)", function() {
+
+ view.select(3);
+
+ var expected = SC.SelectionSet.create().add(content, 3),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should have index only (expected: %@ actual: %@)'.fmt(expected, actual));
+});
+
+test("calling select(indexes=Number, extend=YES)", function() {
+
+ var base = SC.SelectionSet.create().add(content, 3,3),
+ next = 1,
+ expected = base.copy().add(content, next),
+ actual;
+
+ view.select(SC.IndexSet.create(3,3));
+ actual = view.get('selection');
+ ok(base.isEqual(actual), 'precond - should have base selection (expected: %@ actual: %@)'.fmt(expected, actual));
+
+ view.select(1, YES);
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should add set to existing selection (expected: %@ actual: %@)'.fmt(expected, actual));
+});
+
+test("calling select(indexes=SC.IndexSet)", function() {
+
+ var expected = SC.SelectionSet.create().add(content, 3,3), actual;
+
+ view.select(SC.IndexSet.create(3,3));
+ actual = view.get('selection');
+
+ ok(expected.isEqual(actual), 'selection should have passed index set only (expected: %@ actual: %@)'.fmt(expected, actual));
+});
+
+test("calling select(indexes=SC.IndexSet, extend=YES)", function() {
+
+ var base = SC.SelectionSet.create().add(content,3,3),
+ next = SC.SelectionSet.create().add(content,0,2),
+ expected = base.copy().add(content, 0,2),
+ actual;
+
+ view.select(SC.IndexSet.create(3,3));
+ actual = view.get('selection');
+ ok(base.isEqual(actual), 'precond - should have base selection (expected: %@ actual: %@)'.fmt(base, actual));
+
+ var indexes = SC.IndexSet.create(0,2);
+ view.select(indexes, YES);
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should add set to existing selection (expected: %@ actual: %@)'.fmt(expected, actual));
+});
+
+test("calling select(indexes=null)", function() {
+ view.select(SC.IndexSet.create(4,2));
+ view.select(null);
+
+ var expected = SC.SelectionSet.create(),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should be empty (expected: %@ actual: %@)'.fmt(expected, actual));
+});
+
+// ..........................................................
+// DELEGATE TESTS
+//
+
+var del;
+
+module("SC.CollectionView.select - delegate support", {
+ setup: function() {
+ del = SC.Object.create(SC.CollectionViewDelegate);
+ view = SC.CollectionView.create({
+ delegate: del,
+ content: content
+ });
+ }
+});
+
+test("should call delegate if set", function() {
+ var callCount1 = 0, callCount2 = 0 ;
+ del.collectionViewShouldSelectIndexes = function(v, indexes, extend) {
+ callCount1++;
+ return indexes;
+ };
+
+ del.collectionViewSelectionForProposedSelection = function(v, indexes) {
+ callCount2++;
+ return indexes ;
+ };
+
+ view.select(3);
+ equals(callCount1, 1, 'should invoke collectionViewShouldSelectIndexes on delegate 1x');
+ equals(callCount2, 1, 'should invoke collectionViewSelectionForProposedSelection on delegate 1x if change is allowed');
+
+});
+
+test("calling collectionViewSelectionForProposedSelection if collectionViewShouldSelectIndexes returns null", function() {
+ var callCount1 = 0, callCount2 = 0 ;
+ del.collectionViewShouldSelectIndexes = function(v, indexes, extend) {
+ callCount1++;
+ return null;
+ };
+
+ del.collectionViewSelectionForProposedSelection = function(v, indexes) {
+ callCount2++;
+ return indexes ;
+ };
+
+ view.select(3);
+ equals(callCount1, 1, 'should invoke collectionViewShouldSelectIndexes on delegate 1x');
+ equals(callCount2, 0, 'should NOT invoke collectionViewSelectionForProposedSelection on delegate since no change was allowed');
+
+});
+
+test("del.collectionViewShouldSelectIndexes - replacing selection", function() {
+
+ del.collectionViewShouldSelectIndexes = function(v, indexes, extend) {
+ return indexes.without(3);
+ };
+ view.select(SC.IndexSet.create(0,4));
+
+ var expected = SC.SelectionSet.create().add(content, 0,4).remove(content,3),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should only include those allowed by delegate (i.e. not index 3) (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+test("del.collectionViewShouldSelectIndexes - extending selection", function() {
+
+ del.collectionViewShouldSelectIndexes = function(v, indexes, extend) {
+ return indexes.without(3);
+ };
+
+ view.select(SC.IndexSet.create(0,4));
+ view.select(SC.IndexSet.create(3,3), YES);
+
+ var expected = SC.SelectionSet.create().add(content,0,6).remove(content,3),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should extend only those allowed by delegate (i.e. not index 3) (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+test("del.collectionViewShouldSelectIndexes - returns empty index set", function() {
+
+ del.collectionViewShouldSelectIndexes = function(v, indexes, extend) {
+ return indexes.without(3);
+ };
+
+ view.select(2);
+ view.select(3); // should be ignored
+ var expected = SC.SelectionSet.create().add(content,2),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should not change if delegate does not allow any proposed selected indexes (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+
+test("del.collectionViewShouldSelectIndexes - delegate returns null", function() {
+
+ view.select(2);
+
+ del.collectionViewShouldSelectIndexes = function(v, indexes, extend) {
+ return null;
+ };
+
+ view.select(10); // should be ignored
+ var expected = SC.SelectionSet.create().add(content,2),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'selection should not change if delegate returns null (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+
+test("del.collectionViewSelectionForProposedSelection - returns indexes", function() {
+
+ del.collectionViewSelectionForProposedSelection = function(v, indexes) {
+
+ var expected = SC.SelectionSet.create().add(content,10),
+ actual = indexes;
+ ok(expected.isEqual(actual), 'should pass proposed selection to delegate (expected: %@ actual: %@)'.fmt(expected, actual));
+
+ equals(v, view, 'should pass view to delegate');
+
+ return SC.SelectionSet.create().add(content,10,20);
+ };
+
+ view.select(10); // should be ignored
+ var expected = SC.SelectionSet.create().add(content,10,20),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'should set selection to whatever is returned from delegate (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+
+test("del.collectionViewSelectionForProposedSelection - returns null", function() {
+
+ del.collectionViewSelectionForProposedSelection = function(v, indexes, extend) {
+ return null;
+ };
+
+ view.select(10); // should be ignored
+ var expected = SC.SelectionSet.create(),
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'should set selection to empty set if returns null (expected: %@ actual: %@)'.fmt(expected, actual));
+
+});
+
+
+
+
+
+
+
View
191 frameworks/desktop/tests/views/collection/selectNextItem.js
@@ -0,0 +1,191 @@
+// ==========================================================================
+// Project: SproutCore - JavaScript Application Framework
+// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
+// portions copyright @2009 Apple, Inc.
+// License: Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view ;
+var content = "1 2 3 4 5 6 7 8 9 10".w().map(function(x) {
+ return SC.Object.create({ title: x });
+});
+
+module("SC.CollectionView.selectNextItem", {
+ setup: function() {
+ view = SC.CollectionView.create({
+ content: content
+ });
+ }
+});
+
+// ..........................................................
+// BASIC OPERATIONS
+//
+
+test("selectNextItem(extend=undefined, numberOfItems=undefined)", function() {
+ var sel = SC.SelectionSet.create().add(content,4),
+ expected = SC.SelectionSet.create().add(content,5),
+ actual;
+
+ view.set('selection', sel);
+ view.selectNextItem();
+
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'should select next to %@ (expected: %@ actual: %@)'.fmt(sel, expected, actual));
+});
+
+test("selectNextItem(extend=NO, numberOfItems=undefined)", function() {
+ var sel = SC.SelectionSet.create().add(content,4),
+ expected = SC.SelectionSet.create().add(content,5),
+ actual;
+
+ view.set('selection', sel);
+ view.selectNextItem(NO);
+
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'should select next to %@ (expected: %@ actual: %@)'.fmt(sel, expected, actual));
+});
+
+test("selectNextItem(extend=YES, numberOfItems=undefined)", function() {
+ var sel = SC.SelectionSet.create().add(content,4),
+ expected = SC.SelectionSet.create().add(content,4,2),
+ actual;
+
+ view.set('selection', sel);
+ view.selectNextItem(YES);
+
+ actual = view.get('selection');
+ ok(expected.isEqual(actual), 'should extend to next of %@ (expected: %@ actual: %@)'.fmt(sel, expected, actual));
+});
+
+test("selectNextItem(extend=YES, numberOfItems=2)", function() {
+ var sel = SC.SelectionSet.create().add(content,4),
+ expected = SC.SelectionSet.create().add(content,4,3),
+ actual;
+