1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 sc_require('views/scroll_view');
  8 
  9 
 10 /** @class
 11 
 12   Implements a scroll view for menus.  This class extends SC.ScrollView for
 13   menus.
 14 
 15   The main difference with SC.ScrollView is that there is only vertical
 16   scrolling. Value Syncing between SC.MenuScrollView and SC.MenuScrollerView
 17   is done using valueBinding.
 18 
 19   @extends SC.ScrollView
 20   @since SproutCore 1.0
 21 */
 22 SC.MenuScrollView = SC.ScrollView.extend(
 23 /** @scope SC.MenuScrollView.prototype */{
 24 
 25   // ---------------------------------------------------------------------------------------------
 26   // Properties
 27   //
 28 
 29   /**
 30     The bottom scroller view class. This will be replaced with a view instance when the
 31     MenuScrollView is created unless hasVerticalScroller is false.
 32 
 33     @type SC.View
 34     @default SC.MenuScrollerView
 35   */
 36   bottomScrollerView: SC.MenuScrollerView,
 37 
 38   /**
 39     Returns true if the view has both a vertical scroller and the scroller is visible.
 40 
 41     @field
 42     @type Boolean
 43     @readonly
 44     @see SC.ScrollView
 45   */
 46   canScrollVertical: function () {
 47     return !!(this.get('hasVerticalScroller') && // This property isn't bindable.
 48       this.get('bottomScrollerView') && // This property isn't bindable.
 49       this.get('topScrollerView') && // This property isn't bindable.
 50       this.get('isVerticalScrollerVisible'));
 51   }.property('isVerticalScrollerVisible').cacheable(),
 52 
 53   /** SC.View.prototype
 54     @type Array
 55     @default ['sc-menu-scroll-view']
 56     @see SC.View#classNames
 57   */
 58   classNames: ['sc-menu-scroll-view'],
 59 
 60   /**
 61     Control Size for Menu content: change verticalLineScroll
 62 
 63     @type String
 64     @default SC.REGULAR_CONTROL_SIZE
 65     @see SC.Control
 66   */
 67   controlSize: SC.REGULAR_CONTROL_SIZE,
 68 
 69   /**
 70     YES if the view should maintain a horizontal scroller. This property must be set when the view
 71     is created.
 72 
 73     @type Boolean
 74     @default false
 75     @see SC.ScrollView
 76   */
 77   hasHorizontalScroller: false,
 78 
 79   /**
 80     The top scroller view class. This will be replaced with a view instance when the MenuScrollView
 81     is created unless hasVerticalScroller is false.
 82 
 83     @type SC.View
 84     @default SC.MenuScrollerView
 85   */
 86   topScrollerView: SC.MenuScrollerView,
 87 
 88   // ---------------------------------------------------------------------------------------------
 89   // Methods
 90   //
 91 
 92   /** @private @see SC.ScrollView. Check frame changes for size changes. */
 93   _sc_contentViewFrameDidChange: function () {
 94     sc_super();
 95 
 96     // Unlike a normal SC.ScrollView, the visibility of the top & bottom scrollers changes as the
 97     // scrolling commences. For example, once the user scrolls a tiny bit, we need to show the
 98     // top scroller.
 99     this._sc_repositionScrollers();
100   },
101 
102   /** @private @see SC.ScrollView. When the content view's size changes, we need to update our scroll offset properties. */
103   _sc_repositionContentViewUnfiltered: function () {
104     var hasVerticalScroller = this.get('hasVerticalScroller'),
105         // UNUSED. minimumVerticalScrollOffset = this.get('minimumVerticalScrollOffset'),
106         maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset');
107 
108     if (hasVerticalScroller) {
109       var bottomScrollerView = this.get('bottomScrollerView'),
110           topScrollerView = this.get('topScrollerView');
111 
112       topScrollerView.set('maximum', maximumVerticalScrollOffset);
113       bottomScrollerView.set('maximum', maximumVerticalScrollOffset);
114 
115       // Update if the visibility of the scrollers has changed now.
116       var containerHeight = this._sc_containerHeight,
117           contentHeight = this._sc_contentHeight;
118 
119       if (this.get('autohidesVerticalScroller')) {
120         this.setIfChanged('isVerticalScrollerVisible', contentHeight > containerHeight);
121       }
122     }
123 
124     sc_super();
125   },
126 
127   /** @private @see SC.ScrollView. Re-position the scrollers and content depending on the need to scroll or not. */
128   _sc_repositionScrollersUnfiltered: function () {
129     var hasScroller = this.get('hasVerticalScroller'),
130         containerView = this.get('containerView');
131 
132     if (hasScroller && this.get('autohidesVerticalScroller')) {
133       var bottomScrollerView = this.get('bottomScrollerView'),
134           bottomScrollerThickness = bottomScrollerView.get('scrollerThickness'),
135           maximumVerticalScrollOffset = this.get('maximumVerticalScrollOffset'),
136           topScrollerView = this.get('topScrollerView'),
137           topScrollerThickness = topScrollerView.get('scrollerThickness'),
138           verticalOffset = this.get('verticalScrollOffset'),
139           isBottomScrollerVisible = bottomScrollerView.get('isVisible'),
140           isTopScrollerVisible = topScrollerView.get('isVisible');
141 
142       // This asymetric update moves the container view out of the way of the scrollers (essentially
143       // so that the scroller views can be transparent). What's important is that as the container
144       // view is adjusted, the vertical scroll offset is adjusted properly so that the content view
145       // position doesn't jump around.
146       if (isTopScrollerVisible) {
147         if (verticalOffset <= topScrollerThickness) {
148           topScrollerView.set('isVisible', false);
149           containerView.adjust('top', 0);
150           this.decrementProperty('verticalScrollOffset', topScrollerThickness);
151         }
152       } else if (verticalOffset > 0) {
153         topScrollerView.set('isVisible', true);
154         containerView.adjust('top', topScrollerThickness);
155         this.incrementProperty('verticalScrollOffset', topScrollerThickness);
156       }
157 
158       if (isBottomScrollerVisible) {
159         if (verticalOffset >= maximumVerticalScrollOffset - bottomScrollerThickness) {
160           bottomScrollerView.set('isVisible', false);
161           containerView.adjust('bottom', 0);
162           this.incrementProperty('verticalScrollOffset', bottomScrollerThickness);
163         }
164       } else if (verticalOffset < maximumVerticalScrollOffset) {
165         bottomScrollerView.set('isVisible', true);
166         containerView.adjust('bottom', bottomScrollerThickness);
167         this.decrementProperty('verticalScrollOffset', bottomScrollerThickness);
168       }
169     }
170   },
171 
172   /** @private
173     Instantiate scrollers & container views as needed.  Replace their classes
174     in the regular properties.
175   */
176   createChildViews: function () {
177     var childViews = [],
178       autohidesVerticalScroller = this.get('autohidesVerticalScroller');
179 
180     // Set up the container view.
181     var containerView = this.get('containerView');
182 
183     //@if(debug)
184     // Provide some debug-mode only developer support to prevent problems.
185     if (!containerView) {
186       throw new Error("Developer Error: SC.ScrollView must have a containerView class set before it is instantiated.");
187     }
188     //@endif
189 
190     containerView = this.containerView = this.createChildView(containerView, {
191       contentView: this.contentView // Initialize the view with the currently set container view.
192     });
193     this.contentView = containerView.get('contentView'); // Replace our content view with the instantiated version.
194     childViews.push(containerView);
195 
196     // Set up the scrollers.
197     if (!this.get('hasVerticalScroller')) {
198       // Remove the class entirely.
199       this.topScrollerView = null;
200       this.bottomScrollerView = null;
201     } else {
202       var controlSize = this.get('controlSize'),
203           topScrollerView = this.get('topScrollerView');
204 
205       // Use a default scroller view.
206       /* jshint eqnull:true */
207       if (topScrollerView == null) {
208         topScrollerView = SC.MenuScrollerView;
209       }
210 
211       // Replace the class property with an instance.
212       topScrollerView = this.topScrollerView = this.createChildView(topScrollerView, {
213         controlSize: controlSize,
214         scrollDown: false,
215         isVisible: !autohidesVerticalScroller,
216         layout: { height: 0 },
217 
218         value: this.get('verticalScrollOffset'),
219         valueBinding: '.parentView.verticalScrollOffset', // Bind the value of the scroller to our vertical offset.
220         minimum: this.get('minimumVerticalScrollOffset'),
221         maximum: this.get('maximumVerticalScrollOffset')
222       });
223 
224       var topScrollerThickness = topScrollerView.get('scrollerThickness');
225       topScrollerView.adjust('height', topScrollerThickness);
226 
227       // Add the scroller view to the child views array.
228       childViews.push(topScrollerView);
229 
230 
231       var bottomScrollerView = this.get('bottomScrollerView');
232 
233       // Use a default scroller view.
234       /* jshint eqnull:true */
235       if (bottomScrollerView == null) {
236         bottomScrollerView = SC.MenuScrollerView;
237       }
238 
239       // Replace the class property with an instance.
240       bottomScrollerView = this.bottomScrollerView = this.createChildView(bottomScrollerView, {
241         controlSize: controlSize,
242         scrollDown: true,
243         isVisible: !autohidesVerticalScroller,
244         layout: { bottom: 0, height: 0 },
245 
246         value: this.get('verticalScrollOffset'),
247         valueBinding: '.parentView.verticalScrollOffset', // Bind the value of the scroller to our vertical offset.
248         minimum: this.get('minimumVerticalScrollOffset'),
249         maximum: this.get('maximumVerticalScrollOffset')
250       });
251 
252       var bottomScrollerThickness = bottomScrollerView.get('scrollerThickness');
253       bottomScrollerView.adjust('height', bottomScrollerThickness);
254 
255       // Add the scroller view to the child views array.
256       childViews.push(bottomScrollerView);
257 
258       // If the scrollers aren't initially hidden, adjust the container.
259       if (!autohidesVerticalScroller) {
260         containerView.adjust('top', topScrollerThickness);
261         containerView.adjust('bottom', bottomScrollerThickness);
262       }
263     }
264 
265     // Set the childViews array.
266     this.childViews = childViews;
267   }
268 
269 });
270