// ========================================================================== // Project: SproutCore
- JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ==========================================================================
/** @class
SliderView displays a horizontal slider control that you can use to choose from a spectrum (or a sequence) of values. The property `value` holds the slider's current value. You can set the `minimum`, `maximum` and `step` properties as well. @extends SC.View @extends SC.Control @since SproutCore 1.0
*/ SC
.SliderView = SC
.View.extend(SC
.Control, /** @scope SC
.SliderView.prototype */ {
/** @private */ classNames: 'sc-slider-view', /** @private The WAI-ARIA role for slider view. This property's value should not be changed. @type String */ ariaRole: 'slider', /** The current value of the slider. */ value: 0.50, valueBindingDefault: SC.Binding.single().notEmpty(), /** The minimum value of the slider. @type Number @default 0 */ minimum: 0, minimumBindingDefault: SC.Binding.single().notEmpty(), /** Optionally specify the key used to extract the minimum slider value from the content object. If this is set to null then the minimum value will not be derived from the content object. @type String */ contentMinimumKey: null, /** The maximum value of the slider bar. @type Number @default 1 */ maximum: 1, maximumBindingDefault: SC.Binding.single().notEmpty(), /** Optionally specify the key used to extract the maximum slider value from the content object. If this is set to null then the maximum value will not be derived from the content object. @type String */ contentMaximumKey: null, /** Optionally set to the minimum step size allowed. All values will be rounded to this step size when displayed. @type Number @default 0.1 */ step: 0.1, /* When set to true, this draws and positions an element for each step, giving your theme the opportunity to show a mark at each step. @type Boolean @default false */ markSteps: false, /* When set to true, this view handles mouse-wheel scroll events by changing the value. Set to false to prevent a slider in a scroll view from hijacking scroll events mid-scroll, for example. @type Boolean @default true */ updateOnScroll: true, // .......................................................... // INTERNAL // /* @private The full list includes min, max, and stepPositions, but those are redundant with displayValue. */ displayProperties: ['displayValue', 'markSteps'], /** @private @type Number The raw, unchanged value to be provided to screen readers and the like. */ ariaValue: function() { return this.get('value'); }.property('value').cacheable(), /* @private The name of the render delegate which is creating and maintaining the DOM associated with instances of this view. */ renderDelegateName: 'sliderRenderDelegate', /* The value, converted to a percent out of 100 between maximum and minimum. @type Number @readonly */ displayValue: function() { return this._displayValueForValue(this.get('value')); }.property('value', 'minimum', 'maximum', 'step').cacheable(), /* If a nonzero step is specified, this property contains an array of each step's value between min and max (inclusive). @type Array @default null @readonly */ steps: function() { var step = this.get('step'); // FAST PATH: No step. if (!step) return null; var min = this.get('minimum'), max = this.get('maximum'), cur = min, ret = []; while (cur < max) { ret.push(cur); cur += step; cur = Math.round(cur / step) * step; } ret.push(max); return ret; }.property('minimum', 'maximum', 'step').cacheable(), /* If a nonzero step is specified, this property contains an array of each step's position, expressed as a fraction between 0 and 1 (inclusive). You can use these values to generate and position labels for each step, for example. @type Array @default null @readonly */ stepPositions: function() { var steps = this.get('steps'); // FAST PATH: No steps. if (!steps) return null; var min = steps[0], max = steps[steps.length - 1], ret = [], len = steps.length, i; for (i = 0; i < len; i++) { ret[i] = Math.round((steps[i] - min) / (max - min) * 1000) / 1000; } return ret; }.property('steps').cacheable(), /** @private Given a particular value, returns the percentage value. */ _displayValueForValue: function(value) { var min = this.get('minimum'), max = this.get('maximum'), step = this.get('step'); // determine the constrained value. Must fit within min & max value = Math.min(Math.max(value, min), max); // limit to step value if (!SC.none(step) && step !== 0) { value = Math.round(value / step) * step; } // determine the percent across value = Math.round((value - min) / (max - min) * 100); return value; }, /** @private Clears the mouse just down flag. */ _sc_clearMouseJustDown: function () { this._sc_isMouseJustDown = NO; }, /** @private Flag used to track when the mouse is pressed. */ _isMouseDown: NO, /** @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. */ _sc_isMouseJustDown: NO, /** @private Timer used to track time immediately after a mouse up event. */ _sc_clearMouseJustDownTimer: null, /* @private */ mouseDown: function(evt) { // Fast path, reject secondary clicks. if (evt.which && evt.which !== 1) return false; if (!this.get('isEnabledInPane')) return YES; // nothing to do... this.set('isActive', YES); this._isMouseDown = YES ; // Clear existing mouse just down timer. if (this._sc_clearMouseJustDownTimer) { this._sc_clearMouseJustDownTimer.invalidate(); this._sc_clearMouseJustDownTimer = null; } this._sc_isMouseJustDown = NO; return this._triggerHandle(evt, YES); }, /* @private mouseDragged uses same technique as mouseDown. */ mouseDragged: function(evt) { return this._isMouseDown ? this._triggerHandle(evt) : YES; }, /* @private remove active class */ mouseUp: function(evt) { if (this._isMouseDown) this.set('isActive', NO); var ret = this._isMouseDown ? this._triggerHandle(evt) : YES ; this._isMouseDown = NO; // To avoid annoying jitter from Magic Mouse (which sends mousewheel events while trying // to lift your finger after a drag), ignore mousewheel events for a small period of time. this._sc_isMouseJustDown = YES; this._sc_clearMouseJustDownTimer = this.invokeLater(this._sc_clearMouseJustDown, 250); return ret ; }, /* @private */ mouseWheel: function(evt) { if (!this.get('isEnabledInPane')) return NO; if (!this.get('updateOnScroll')) return NO; // If the Magic Mouse is pressed, it still sends mousewheel events rapidly, we don't want errant wheel // events to move the slider. if (this._isMouseDown || this._sc_isMouseJustDown) return NO; var min = this.get('minimum'), max = this.get('maximum'), step = this.get('step') || ((max - min) / 20), newVal = this.get('value') + ((evt.wheelDeltaX+evt.wheelDeltaY)*step), value = Math.round(newVal / step) * step; if (newVal< min) this.setIfChanged('value', min); else if (newVal> max) this.setIfChanged('value', max); else this.setIfChanged('value', newVal); return YES ; }, /* @private */ touchStart: function(evt){ return this.mouseDown(evt); }, /* @private */ touchEnd: function(evt){ return this.mouseUp(evt); }, /* @private */ touchesDragged: function(evt){ return this.mouseDragged(evt); }, /** @private Updates the handle based on the mouse location of the handle in the event. */ _triggerHandle: function(evt, firstEvent) { var width = this.get('frame').width, min = this.get('minimum'), max=this.get('maximum'), step = this.get('step'), v=this.get('value'), loc; if(firstEvent){ loc = this.convertFrameFromView({ x: evt.pageX }).x; this._evtDiff = evt.pageX - loc; }else{ loc = evt.pageX-this._evtDiff; } // convert to percentage loc = Math.max(0, Math.min(loc / width, 1)); // if the location is NOT in the general vicinity of the slider, we assume // that the mouse pointer or touch is in the center of where the knob should be. // otherwise, if we are starting, we need to do extra to add an offset if (firstEvent) { var value = this.get("value"); value = (value - min) / (max - min); // if the value and the loc are within 16px if (Math.abs(value * width - loc * width) < 16) this._offset = value - loc; else this._offset = 0; } // add offset and constrain loc = Math.max(0, Math.min(loc + this._offset, 1)); // convert to value using minimum/maximum then constrain to steps loc = min + ((max-min)*loc); if (!SC.none(step) && step !== 0) loc = Math.round(loc / step) * step ; // if changes by more than a rounding amount, set v. if (Math.abs(v-loc)>=0.01) { this.set('value', loc); // adjust } return YES ; }, /** @private tied to the isEnabledInPane state */ acceptsFirstResponder: function() { if (SC.FOCUS_ALL_CONTROLS) { return this.get('isEnabledInPane'); } return NO; }.property('isEnabledInPane'), /* @private TODO: Update to use interpretKeyEvents. */ keyDown: function(evt) { // handle tab key if (evt.which === 9 || evt.keyCode === 9) { var view = evt.shiftKey ? this.get('previousValidKeyView') : this.get('nextValidKeyView'); if(view) view.becomeFirstResponder(); else evt.allowDefault(); return YES ; // handled } if (evt.which >= 33 && evt.which <= 40){ var min = this.get('minimum'),max=this.get('maximum'), step = this.get('step'), size = max-min, val=0, calculateStep, current=this.get('value'); if (evt.which === 37 || evt.which === 38 || evt.which === 34 ){ if (SC.none(step) || step === 0) { if(size<100){ val = current-1; }else{ calculateStep = Math.abs(size/100); if(calculateStep<2) calculateStep = 2; val = current-calculateStep; } }else{ val = current-step; } } if (evt.which === 39 || evt.which === 40 || evt.which === 33 ){ if (SC.none(step) || step === 0) { if(size<100){ val = current + 2; }else{ calculateStep = Math.abs(size/100); if(calculateStep<2) calculateStep =2; val = current+calculateStep; } }else{ val = current+step; } } if (evt.which === 36){ val=max; } if (evt.which === 35){ val=min; } if(val>=min && val<=max) this.set('value', val); }else{ evt.allowDefault(); return NO; } return YES; }, /* @private */ contentKeys: { 'contentValueKey': 'value', 'contentMinimumKey': 'minimum', 'contentMaximumKey': 'maximum', 'contentIsIndeterminateKey': 'isIndeterminate' }
});