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 
  8 /** @class
  9 
 10   SliderView displays a horizontal slider control that you can use to choose
 11   from a spectrum (or a sequence) of values.
 12 
 13   The property `value` holds the slider's current value. You can set the
 14   `minimum`, `maximum` and `step` properties as well.
 15 
 16   @extends SC.View
 17   @extends SC.Control
 18   @since SproutCore 1.0
 19 */
 20 SC.SliderView = SC.View.extend(SC.Control,
 21 /** @scope SC.SliderView.prototype */ {
 22 
 23   /** @private */
 24   classNames: 'sc-slider-view',
 25 
 26   /** @private
 27     The WAI-ARIA role for slider view. This property's value should not be
 28     changed.
 29 
 30     @type String
 31   */
 32   ariaRole: 'slider',
 33 
 34   /**
 35     The current value of the slider.
 36   */
 37   value: 0.50,
 38   valueBindingDefault: SC.Binding.single().notEmpty(),
 39 
 40   /**
 41     The minimum value of the slider.
 42 
 43     @type Number
 44     @default 0
 45   */
 46   minimum: 0,
 47   minimumBindingDefault: SC.Binding.single().notEmpty(),
 48 
 49   /**
 50     Optionally specify the key used to extract the minimum slider value
 51     from the content object.  If this is set to null then the minimum value
 52     will not be derived from the content object.
 53 
 54     @type String
 55   */
 56   contentMinimumKey: null,
 57 
 58   /**
 59     The maximum value of the slider bar.
 60 
 61     @type Number
 62     @default 1
 63   */
 64   maximum: 1,
 65   maximumBindingDefault: SC.Binding.single().notEmpty(),
 66 
 67   /**
 68     Optionally specify the key used to extract the maximum slider value
 69     from the content object.  If this is set to null then the maximum value
 70     will not be derived from the content object.
 71 
 72     @type String
 73   */
 74   contentMaximumKey: null,
 75 
 76   /**
 77     Optionally set to the minimum step size allowed.
 78 
 79     All values will be rounded to this step size when displayed.
 80 
 81     @type Number
 82     @default 0.1
 83   */
 84   step: 0.1,
 85 
 86   /*
 87     When set to true, this draws and positions an element for each step, giving
 88     your theme the opportunity to show a mark at each step.
 89 
 90     @type Boolean
 91     @default false
 92   */
 93   markSteps: false,
 94 
 95   /*
 96     When set to true, this view handles mouse-wheel scroll events by changing the
 97     value. Set to false to prevent a slider in a scroll view from hijacking scroll
 98     events mid-scroll, for example.
 99 
100     @type Boolean
101     @default true
102   */
103   updateOnScroll: true,
104 
105   // ..........................................................
106   // INTERNAL
107   //
108 
109   /* @private The full list includes min, max, and stepPositions, but those are redundant with displayValue. */
110   displayProperties: ['displayValue', 'markSteps'],
111 
112   /** @private
113    @type Number
114    The raw, unchanged value to be provided to screen readers and the like.
115   */
116   ariaValue: function() {
117     return this.get('value');
118   }.property('value').cacheable(),
119 
120   /* @private
121     The name of the render delegate which is creating and maintaining
122     the DOM associated with instances of this view.
123   */
124   renderDelegateName: 'sliderRenderDelegate',
125 
126   /*
127     The value, converted to a percent out of 100 between maximum and minimum.
128 
129     @type Number
130     @readonly
131   */
132   displayValue: function() {
133     return this._displayValueForValue(this.get('value'));
134   }.property('value', 'minimum', 'maximum', 'step').cacheable(),
135 
136   /*
137     If a nonzero step is specified, this property contains an array of each step's value between
138     min and max (inclusive).
139 
140     @type Array
141     @default null
142     @readonly
143   */
144   steps: function() {
145     var step = this.get('step');
146     // FAST PATH: No step.
147     if (!step) return null;
148     var min = this.get('minimum'),
149       max = this.get('maximum'),
150       cur = min,
151       ret = [];
152     while (cur < max) {
153       ret.push(cur);
154       cur += step;
155       cur = Math.round(cur / step) * step;
156     }
157     ret.push(max);
158     return ret;
159   }.property('minimum', 'maximum', 'step').cacheable(),
160 
161   /*
162     If a nonzero step is specified, this property contains an array of each step's position,
163     expressed as a fraction between 0 and 1 (inclusive). You can use these values to generate
164     and position labels for each step, for example.
165 
166     @type Array
167     @default null
168     @readonly
169   */
170   stepPositions: function() {
171     var steps = this.get('steps');
172     // FAST PATH: No steps.
173     if (!steps) return null;
174     var min = steps[0],
175       max = steps[steps.length - 1],
176       ret = [],
177       len = steps.length, i;
178     for (i = 0; i < len; i++) {
179       ret[i] = Math.round((steps[i] - min) / (max - min) * 1000) / 1000;
180     }
181     return ret;
182   }.property('steps').cacheable(),
183 
184   /** @private Given a particular value, returns the percentage value. */
185   _displayValueForValue: function(value) {
186     var min = this.get('minimum'),
187         max = this.get('maximum'),
188         step = this.get('step');
189 
190     // determine the constrained value.  Must fit within min & max
191     value = Math.min(Math.max(value, min), max);
192 
193     // limit to step value
194     if (!SC.none(step) && step !== 0) {
195       value = Math.round(value / step) * step;
196     }
197 
198     // determine the percent across
199     value = Math.round((value - min) / (max - min) * 100);
200 
201     return value;
202   },
203 
204   /** @private Clears the mouse just down flag. */
205   _sc_clearMouseJustDown: function () {
206     this._sc_isMouseJustDown = NO;
207   },
208 
209   /** @private Flag used to track when the mouse is pressed. */
210   _isMouseDown: NO,
211 
212   /** @private Flag used to track when mouse was just down so that mousewheel events firing as the finger is lifted don't shoot the slider over. */
213   _sc_isMouseJustDown: NO,
214 
215   /** @private Timer used to track time immediately after a mouse up event. */
216   _sc_clearMouseJustDownTimer: null,
217 
218   /* @private */
219   mouseDown: function(evt) {
220     // Fast path, reject secondary clicks.
221     if (evt.which && evt.which !== 1) return false;
222 
223     if (!this.get('isEnabledInPane')) return YES; // nothing to do...
224     this.set('isActive', YES);
225     this._isMouseDown = YES ;
226 
227     // Clear existing mouse just down timer.
228     if (this._sc_clearMouseJustDownTimer) {
229       this._sc_clearMouseJustDownTimer.invalidate();
230       this._sc_clearMouseJustDownTimer = null;
231     }
232 
233     this._sc_isMouseJustDown = NO;
234 
235     return this._triggerHandle(evt, YES);
236   },
237 
238   /* @private mouseDragged uses same technique as mouseDown. */
239   mouseDragged: function(evt) {
240     return this._isMouseDown ? this._triggerHandle(evt) : YES;
241   },
242 
243   /* @private remove active class */
244   mouseUp: function(evt) {
245     if (this._isMouseDown) this.set('isActive', NO);
246     var ret = this._isMouseDown ? this._triggerHandle(evt) : YES ;
247     this._isMouseDown = NO;
248 
249     // To avoid annoying jitter from Magic Mouse (which sends mousewheel events while trying
250     // to lift your finger after a drag), ignore mousewheel events for a small period of time.
251     this._sc_isMouseJustDown = YES;
252     this._sc_clearMouseJustDownTimer = this.invokeLater(this._sc_clearMouseJustDown, 250);
253 
254     return ret ;
255   },
256 
257   /* @private */
258   mouseWheel: function(evt) {
259     if (!this.get('isEnabledInPane')) return NO;
260     if (!this.get('updateOnScroll')) return NO;
261 
262     // If the Magic Mouse is pressed, it still sends mousewheel events rapidly, we don't want errant wheel
263     // events to move the slider.
264     if (this._isMouseDown || this._sc_isMouseJustDown) return NO;
265 
266     var min = this.get('minimum'),
267         max = this.get('maximum'),
268         step = this.get('step') || ((max - min) / 20),
269         newVal = this.get('value') + ((evt.wheelDeltaX+evt.wheelDeltaY)*step),
270         value = Math.round(newVal / step) * step;
271     if (newVal< min) this.setIfChanged('value', min);
272     else if (newVal> max) this.setIfChanged('value', max);
273     else this.setIfChanged('value', newVal);
274     return YES ;
275   },
276 
277   /* @private */
278   touchStart: function(evt){
279     return this.mouseDown(evt);
280   },
281 
282   /* @private */
283   touchEnd: function(evt){
284     return this.mouseUp(evt);
285   },
286 
287   /* @private */
288   touchesDragged: function(evt){
289     return this.mouseDragged(evt);
290   },
291 
292   /** @private
293     Updates the handle based on the mouse location of the handle in the
294     event.
295   */
296   _triggerHandle: function(evt, firstEvent) {
297     var width = this.get('frame').width,
298         min = this.get('minimum'), max=this.get('maximum'),
299         step = this.get('step'), v=this.get('value'), loc;
300 
301     if(firstEvent){
302       loc = this.convertFrameFromView({ x: evt.pageX }).x;
303       this._evtDiff = evt.pageX - loc;
304     }else{
305       loc = evt.pageX-this._evtDiff;
306     }
307 
308     // convert to percentage
309     loc = Math.max(0, Math.min(loc / width, 1));
310 
311     // if the location is NOT in the general vicinity of the slider, we assume
312     // that the mouse pointer or touch is in the center of where the knob should be.
313     // otherwise, if we are starting, we need to do extra to add an offset
314     if (firstEvent) {
315       var value = this.get("value");
316       value = (value - min) / (max - min);
317 
318       // if the value and the loc are within 16px
319       if (Math.abs(value * width - loc * width) < 16) this._offset = value - loc;
320       else this._offset = 0;
321     }
322 
323     // add offset and constrain
324     loc = Math.max(0, Math.min(loc + this._offset, 1));
325 
326     // convert to value using minimum/maximum then constrain to steps
327     loc = min + ((max-min)*loc);
328     if (!SC.none(step) && step !== 0) loc = Math.round(loc / step) * step ;
329 
330     // if changes by more than a rounding amount, set v.
331     if (Math.abs(v-loc)>=0.01) {
332       this.set('value', loc); // adjust
333     }
334 
335     return YES ;
336   },
337 
338   /** @private tied to the isEnabledInPane state */
339   acceptsFirstResponder: function() {
340     if (SC.FOCUS_ALL_CONTROLS) { return this.get('isEnabledInPane'); }
341     return NO;
342   }.property('isEnabledInPane'),
343 
344   /* @private TODO: Update to use interpretKeyEvents. */
345   keyDown: function(evt) {
346      // handle tab key
347      if (evt.which === 9 || evt.keyCode === 9) {
348        var view = evt.shiftKey ? this.get('previousValidKeyView') : this.get('nextValidKeyView');
349        if(view) view.becomeFirstResponder();
350        else evt.allowDefault();
351        return YES ; // handled
352      }
353      if (evt.which >= 33 && evt.which <= 40){
354        var min = this.get('minimum'),max=this.get('maximum'),
355           step = this.get('step'),
356           size = max-min, val=0, calculateStep, current=this.get('value');
357 
358        if (evt.which === 37 || evt.which === 38 || evt.which === 34 ){
359          if (SC.none(step) || step === 0) {
360            if(size<100){
361              val = current-1;
362            }else{
363              calculateStep = Math.abs(size/100);
364              if(calculateStep<2) calculateStep = 2;
365              val = current-calculateStep;
366            }
367          }else{
368            val = current-step;
369          }
370        }
371        if (evt.which === 39 || evt.which === 40 || evt.which === 33 ){
372            if (SC.none(step) || step === 0) {
373               if(size<100){
374                 val = current + 2;
375               }else{
376                 calculateStep = Math.abs(size/100);
377                 if(calculateStep<2) calculateStep =2;
378                 val = current+calculateStep;
379               }
380             }else{
381               val = current+step;
382             }
383        }
384        if (evt.which === 36){
385          val=max;
386        }
387        if (evt.which === 35){
388           val=min;
389        }
390        if(val>=min && val<=max) this.set('value', val);
391      }else{
392        evt.allowDefault();
393        return NO;
394      }
395      return YES;
396    },
397 
398   /* @private */
399    contentKeys: {
400      'contentValueKey': 'value',
401      'contentMinimumKey': 'minimum',
402      'contentMaximumKey': 'maximum',
403      'contentIsIndeterminateKey': 'isIndeterminate'
404    }
405 });
406