Skip to content

Instantly share code, notes, and snippets.

@publickeating
Created April 12, 2012 03:27
Show Gist options
  • Save publickeating/2364444 to your computer and use it in GitHub Desktop.
Save publickeating/2364444 to your computer and use it in GitHub Desktop.
SliderView rewrite on top of TemplateView
// ==========================================================================
// 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
};
// ==========================================================================
// 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(),
};
{{#if isHorizontal}}
<div class="sc-tui-track">
<div class="left"></div><div class="middle"></div><div class="right"></div>
</div>
{{else}}
<div class="sc-tui-track">
<div class="top"></div><div class="middle"></div><div class="bottom"></div>
</div>
{{/if}}
<div class="sc-tui-knob" {{bindAttr style="knobStyle"}}></div>
// ==========================================================================
// 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;
}
});
@tim-evans
Copy link

@publickeating, you can do:

classNameBindings: ['isActive:active']

to change the class from '.is-active' to '.active'

@publickeating
Copy link
Author

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