Created
April 12, 2012 03:27
-
-
Save publickeating/2364444 to your computer and use it in GitHub Desktop.
SliderView rewrite on top of TemplateView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ========================================================================== | |
// 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) | |
// ========================================================================== | |
/** | |
This mixin should be applied to all control views. | |
@namespace | |
*/ | |
SC.Controllable = { | |
/** @private */ | |
classNameBindings: [ | |
'isActive' // .is-active | |
], | |
/** | |
The active state of the Controllable, either YES for active or NO for inactive. | |
@type Boolean | |
@default NO | |
*/ | |
isActive: NO, | |
/** | |
Duck typing. | |
@type Boolean | |
@default YES | |
@readonly | |
*/ | |
isControllable: YES | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ========================================================================== | |
// 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) | |
// ========================================================================== | |
/** | |
This adds isEnabled support to all SC.View subclasses, which essentially does | |
little more than set the 'disabled' class on the element when isEnabled is NO. | |
While isEnabled is generally associated with views that are also controls, | |
there are many views that are not controls that may need to still appear | |
disabled. Of course, views that are also controls use isEnabled to control | |
behavior as well. | |
*/ | |
// TODO: make this part of SC.CoreView | |
SC.Enablable = { | |
/** @private */ | |
'aria-disabled': function() { | |
return !this.get('isEnabled'); | |
}.property('isEnabled').cacheable(), | |
/** @private */ | |
attributeBindings: [ | |
'aria-disabled' | |
], | |
/** @private */ | |
classNameBindings: [ | |
'disabled' // .disabled | |
], | |
/** | |
Using classNameBindings on isEnabled, would insert the is-enabled class when | |
YES. Instead, we use a disabled property and the disabled classNameBinding | |
to insert the disabled class when isEnabled is NO. | |
@private | |
*/ | |
disabled: function() { | |
return !this.get('isEnabled'); | |
}.property('isEnabled').cacheable(), | |
/** | |
The enabled state of the view, either YES for enabled or NO for disabled. | |
When isEnabled is NO, the class, disabled, will be added to the element's | |
class names. The state of isEnabled may also be used to control the | |
behavior of the view. | |
@type Boolean | |
@default YES | |
*/ | |
isEnabled: YES, | |
/** @private */ | |
isEnabledBindingDefault: SC.Binding.oneWay().bool() | |
// applyAttributesToContext: function(original, context) { | |
// var isEnabled = this.get('isEnabled'); | |
// original(context); | |
// context.setClass('disabled', !isEnabled); | |
// context.attr('aria-disabled', !isEnabled ? 'true' : null); | |
// }.enhance(), | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ========================================================================== | |
// 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) | |
// ========================================================================== | |
/** | |
This control shows a horizontal or vertical slider that can be used to set | |
variable values. | |
Simply add SliderView as a childView or insert it in a template and bind its | |
value to a variable, where the value is in the range of the minimum and | |
maximum values set on the control. | |
You may also set a step value that will further constrain the value of the | |
control to step multiples greater than the minimum. | |
Theming Tips | |
============ | |
The default slider template generates the following structures, which | |
can be easily styled with CSS. With orientation set to | |
SC.ORIENTATION.horizontal: | |
<div class="sc-tui-slider-view"> | |
<div class="sc-tui-track"> | |
<div class="left"></div></div><div class="middle"></div><div class="right"> | |
</div> | |
<div class="sc-tui-knob"></div> | |
</div> | |
and with orientation set to SC.ORIENTATION.vertical: | |
<div class="sc-tui-slider-view"> | |
<div class="sc-tui-track"> | |
<div class="top"></div><div class="middle"></div><div class="bottom"></div> | |
</div> | |
<div class="sc-tui-knob"></div> | |
</div> | |
The left, right and middle or top, bottom and middle classed divs can be | |
populated with background images using the `@include slices` directive in your | |
stylesheet. See the default theme styles in progress.css for an example. | |
SliderView inherits the isEnabled property from SC.CoreView, which will insert | |
the .disabled class into the parent div when isEnabled === NO. | |
Finally, when styling the .sc-tui-knob div, be aware that in browsers that | |
don't support CSS 3D transforms, the left style will be adjusted automatically | |
by the view when orientation is horizontal and the top style will be adjusted | |
automatically by the view when orientation is vertical. The adjustment amount | |
will match the value of the progress between 0% to 100% left or top | |
respectively. For browsers that do support CSS 3D transforms, the translateX | |
and translateY values will be automatically modified. | |
@accessible aria-role:'slider' | |
@accessible aria-orientation, aria-valuemax, aria-valuemin, aria-valuenow | |
@accessible aria-valuetext | |
@class A slider control view. | |
@extends SC.TemplateView | |
@extends SC.Controllable | |
*/ | |
SC.TUI.SliderView = SC.TemplateView.extend(SC.Controllable, SC.Enablable, { | |
// ------------------------------------------------------------------------ | |
// Properties | |
// | |
/** @private */ | |
ariaRole: 'slider', | |
/** @private */ | |
'aria-orientation': function() { | |
return this.get('orientation') === SC.ORIENTATION.horizontal ? 'horizontal' : | |
'vertical'; | |
}.property('orientation').cacheable(), | |
/** @private */ | |
'aria-valuemax': function() { | |
return this.get('maximum'); | |
}.property('maximum').cacheable(), | |
/** @private */ | |
'aria-valuemin': function() { | |
return this.get('minimum'); | |
}.property('minimum').cacheable(), | |
/** @private */ | |
'aria-valuenow': function() { | |
return this.get('value'); | |
}.property('value').cacheable(), | |
/** @private */ | |
'aria-valuetext': function() { | |
return this.get('value'); | |
}.property('value').cacheable(), | |
/** @private */ | |
attributeBindings: [ | |
'aria-orientation', | |
'aria-valuemax', | |
'aria-valuemin', | |
'aria-valuenow', | |
'aria-valuetext' | |
], | |
/** @private */ | |
classNameBindings: [ | |
'direction', // .sc-direction-ltr, .sc-direction-rtl, .sc-direction-ttb, .sc-direction-btt | |
'orientation', // .sc-layout-horizontal, .sc-layout-vertical | |
'themeClassName' // User configurable String | |
], | |
/** | |
@see SC.View#classNames | |
@type Array | |
@default ['sc-tui-slider-view'] | |
*/ | |
classNames: ['sc-tui-slider-view'], | |
/** | |
The direction of the slider control. In horizontal mode, the minimum can be | |
on the left (ltr) or right (rtl) and in vertical mode, the minimum can be | |
on the top (ttb) or bottom (btt). | |
@type SC.DIRECTION | |
@default SC.DIRECTION.ltr | |
*/ | |
direction: SC.DIRECTION.ltr, | |
/** @private */ | |
isHorizontal: function() { | |
return this.get('orientation') === SC.ORIENTATION.horizontal; | |
}.property('orientation').cacheable(), | |
/** @private */ | |
knobStyle: function() { | |
var direction = this.get('direction'), | |
height, | |
isHorizontal = this.get('isHorizontal'), | |
layer, | |
maximum = this.get('maximum'), | |
minimum = this.get('minimum'), | |
valueAsPercent = this.get('valueAsPercent'), | |
style, | |
width; | |
if (SC.platform.supportsCSS3DTransforms) { | |
// Determine the distance across by pixels. | |
layer = this.$(); | |
if (isHorizontal) { | |
width = layer.innerWidth(); | |
style = Math.round(valueAsPercent * width); | |
if (direction === SC.DIRECTION.rtl) { style = width - style; } | |
style = "-webkit-transform: translate3d(" + style + "px,0,0);"; | |
} else { | |
height = layer.innerHeight(); | |
style = Math.round(valueAsPercent * height); | |
if (direction === SC.DIRECTION.btt) { style = height - style; } | |
style = "-webkit-transform: translate3d(0," + style + "px,0);"; | |
} | |
} else { | |
// Determine the distance across by percentage. | |
valueAsPercent = Math.round(valueAsPercent * 100); | |
if (isHorizontal) { | |
style = 'left: %@%'.fmt(valueAsPercent); | |
} else { | |
style = 'top: %@%'.fmt(valueAsPercent); | |
} | |
} | |
return style; | |
}.property('value').cacheable(), | |
/** | |
The maximum value of the control. | |
@type Number | |
@default 1.0 | |
*/ | |
maximum: 1.0, | |
/** @private */ | |
maximumBindingDefault: SC.Binding.single().notEmpty(), | |
/** | |
The minimum value of the control. | |
@type Number | |
@default 0.0 | |
*/ | |
minimum: 0.0, | |
/** @private */ | |
minimumBindingDefault: SC.Binding.single().notEmpty(), | |
/** | |
The orientation of the slider track. | |
@type SC.ORIENTATION | |
@default SC.ORIENTATION.horizontal | |
*/ | |
orientation: SC.ORIENTATION.horizontal, | |
/** | |
The size of each progression of the slider. Setting this value to a number | |
will constrain the slider's value to multiples of the step amount greater | |
than the minimum. Setting it to null, will create a continuous slider. | |
@type Number | |
@type null | |
@default null | |
*/ | |
step: null, | |
/** | |
@see SC.TemplateView#template | |
@type Number | |
@default 0.5 | |
*/ | |
templateName: 'slider', | |
/** | |
The theme class for this view. It will be included in the parent | |
element's class names. | |
@type String | |
@default 'sc' | |
*/ | |
themeClassName: 'sc', | |
/** | |
The current value of the slider. Bind to this property to change the | |
displayed state of the SliderView. The value will be constrained by | |
the minimum, maximum and step values. | |
@type Number | |
@default 0.5 | |
*/ | |
value: function(key, value) { | |
return this.constrainedValue(value); | |
}.property('maximum', 'minimum', 'step').cacheable(), | |
/** @private */ | |
valueBindingDefault: SC.Binding.single().notEmpty(), | |
/** | |
The current value of the slider as a percentage between minimum and | |
maximum. | |
@type Number | |
@default 0.5 | |
@readonly | |
*/ | |
valueAsPercent: function() { | |
var maximum = this.get('maximum'), | |
minimum = this.get('minimum'), | |
value = this.get('value'); | |
return (value - minimum) / (maximum - minimum); | |
}.property('value').cacheable(), | |
// ------------------------------------------------------------------------ | |
// Functions | |
// | |
/** @private */ | |
constrainedValue: function(value) { | |
var maximum = this.get('maximum'), | |
minimum = this.get('minimum'), | |
step = this.get('step') || 1; | |
if (SC.none(value)) { value = this.lastValue; } | |
// Restrict the value above minimum. | |
if (value < minimum) { value = minimum; } | |
// Limit to a step multiple above minimum, but stop at maximum first. | |
value = (value >= maximum) ? maximum : | |
Math.round((value - minimum) / step) * step + minimum; | |
// Restrict the value below maximum. The step could have rounded over | |
// maximum. | |
if (value > maximum) { value = maximum; } | |
// Cache the value so that we can support value being dependent on minimum, | |
// maximum and step value. | |
this.lastValue = value; | |
return value; | |
}, | |
/** @private */ | |
init: function() { | |
sc_super(); | |
// Set up private properties | |
this.lastValue = 0.5; | |
this.isMouseDownInView = NO; | |
this.notifyOnRawChange = YES; | |
this.rawValueOffset = 0; | |
}, | |
/** @private */ | |
mouseDown: function(evt) { | |
var halfHeight, halfWidth, | |
knob, | |
point, | |
positionInDocument; | |
// Fast path. | |
if (!this.get('isEnabled')) { return YES; } | |
this.set('isActive', YES); | |
this.isMouseDownInView = YES; | |
// This is an important optimization. | |
// Determine if the evt was within the frame of the knob and if so, use a | |
// slight offset on the rawValue so that the knob doesn't jump to a new | |
// position on mouseDown when the step is relatively small. | |
knob = this.$('.sc-tui-knob'); | |
point = { x: evt.pageX, y: evt.pageY }; | |
if (SC.pointInElement(point, knob)) { | |
// Determine the view's frame based on document coordinates. | |
positionInDocument = SC.offset(knob); | |
if (this.get('isHorizontal')) { | |
halfWidth = Math.round(knob.innerWidth() * 0.5); | |
this.rawValueOffset = evt.pageX - (positionInDocument.x + halfWidth); | |
} else { | |
halfHeight = Math.round(knob.innerHeight() * 0.5); | |
this.rawValueOffset = evt.pageY - (positionInDocument.y + halfHeight); | |
} | |
} | |
this.set('value', this.rawValueAtPosition(evt)); | |
return YES; | |
}, | |
/** @private */ | |
mouseDragged: function(evt) { | |
// Fast path. | |
if (!this.isMouseDownInView) { return NO; } | |
this.set('value', this.rawValueAtPosition(evt)); | |
return YES; | |
}, | |
/** @private */ | |
mouseUp: function(evt) { | |
// Fast path. | |
if (!this.isMouseDownInView) { return NO; } | |
this.set('isActive', NO); | |
this.isMouseDownInView = NO; | |
this.rawValueOffset = 0; | |
return YES; | |
}, | |
/** @private */ | |
mouseWheel: function(evt) { | |
// Fast path. | |
if (!this.get('isEnabled')) { return NO; } | |
var step = this.get('step'), | |
value = this.get('value'); | |
if (evt.wheelDeltaY > 0 || evt.wheelDeltaX < 0) { | |
this.set('value', value + step); | |
} else { | |
this.set('value', value - step); | |
} | |
return YES; | |
}, | |
/** | |
This function returns the rawValue of the slider based on the position of | |
the event. | |
@private | |
*/ | |
rawValueAtPosition: function(evt) { | |
var direction = this.get('direction'), | |
isHorizontal = this.get('isHorizontal'), | |
layer = this.$(), | |
maximum = this.get('maximum'), | |
maximumLeft, maximumTop, | |
minimum = this.get('minimum'), | |
percentage, | |
positionInDocument, | |
rawValue, | |
relativeLeft, relativeTop; | |
// Determine the view's frame based on document coordinates. | |
positionInDocument = SC.offset(layer); | |
if (isHorizontal) { | |
// The maximum left value is the width of the element | |
maximumLeft = layer.innerWidth(); | |
// Determine the relative left position of the event. Taking into account | |
// that it might be offset slightly by the mouse/touch grabbing the slider | |
// off center. | |
relativeLeft = evt.pageX - positionInDocument.x - this.rawValueOffset; | |
// Adjust for rtl direction | |
if (direction === SC.DIRECTION.rtl) { | |
relativeLeft = maximumLeft - relativeLeft; | |
} | |
// Determine the percentage of the slider's value the event is at. | |
percentage = relativeLeft / maximumLeft; | |
} else { | |
// The maximum top value is the height of the element | |
maximumTop = layer.innerHeight(); | |
// Determine the relative top position of the event. Taking into account | |
// that it might be offset slightly by the mouse/touch grabbing the slider | |
// off center. | |
relativeTop = evt.pageY - positionInDocument.y - this.rawValueOffset; | |
// Adjust for btt direction | |
if (direction === SC.DIRECTION.btt) { | |
relativeTop = maximumTop - relativeTop; | |
} | |
// Determine the percentage of the slider's value the event is at. | |
percentage = relativeTop / maximumTop; | |
} | |
// Determine the raw value that the event is at. | |
rawValue = minimum + (maximum - minimum) * percentage; | |
return rawValue; | |
}, | |
/** @private */ | |
touchEnd: function(evt) { | |
return this.mouseUp(evt); | |
}, | |
/** @private */ | |
touchStart: function(evt) { | |
return this.mouseDown(evt); | |
}, | |
/** @private */ | |
touchesDragged: function(evt) { | |
return this.mouseDragged(evt); | |
}, | |
/** tied to the isEnabled state */ | |
acceptsFirstResponder: function() { | |
if (SC.FOCUS_ALL_CONTROLS) { return this.get('isEnabled'); } | |
return NO; | |
}.property('isEnabled'), | |
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(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(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; | |
} | |
}); |
That’s good to know, I didn’t realize that.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@publickeating, you can do:
to change the class from
'.is-active'
to'.active'