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