1 /*jshint multistr:true, curly: false */
2 /*global jQuery:false, define: false */
4 * jRange - Awesome range control
8 * Nitin Hayaran (nitinhayaran@gmail.com)
10 * Licensed under the MIT (MIT-LICENSE.txt).
12 * @author Nitin Hayaran
13 * @version 0.1-RELEASE
17 * jQuery (http://jquery.com)
21 (function($, window, document, undefined) {
24 var jRange = function() {
25 return this.init.apply(this, arguments);
29 onstatechange: function() {},
30 ondragend: function() {},
31 onbarclicked: function() {},
42 template: '<div class="slider-container">\
43 <div class="back-bar">\
44 <div class="selected-bar"></div>\
45 <div class="pointer low"></div><div class="pointer-label low">123456</div>\
46 <div class="pointer high"></div><div class="pointer-label high">456789</div>\
47 <div class="clickable-dummy"></div>\
49 <div class="scale"></div>\
51 init: function(node, options) {
52 this.options = $.extend({}, this.defaults, options);
53 this.inputNode = $(node);
54 this.options.value = this.inputNode.val() || (this.options.isRange ? this.options.from + ',' + this.options.from : this.options.from);
55 this.domNode = $(this.template);
56 this.domNode.addClass(this.options.theme);
57 this.inputNode.after(this.domNode);
58 this.domNode.on('change', this.onChange);
59 this.pointers = $('.pointer', this.domNode);
60 this.lowPointer = this.pointers.first();
61 this.highPointer = this.pointers.last();
62 this.labels = $('.pointer-label', this.domNode);
63 this.lowLabel = this.labels.first();
64 this.highLabel = this.labels.last();
65 this.scale = $('.scale', this.domNode);
66 this.bar = $('.selected-bar', this.domNode);
67 this.clickableBar = this.domNode.find('.clickable-dummy');
68 this.interval = this.options.to - this.options.from;
72 // Check if inputNode is visible, and have some width, so that we can set slider width accordingly.
73 if (this.inputNode.width() === 0 && !this.options.width) {
74 console.log('jRange : no width found, returning');
77 this.options.width = this.options.width || this.inputNode.width();
78 this.domNode.width(this.options.width);
79 this.inputNode.hide();
82 if (this.isSingle()) {
83 this.lowPointer.hide();
86 if (!this.options.showLabels) {
90 if (this.options.showScale) {
93 this.setValue(this.options.value);
95 isSingle: function() {
96 if (typeof(this.options.value) === 'number') {
99 return (this.options.value.indexOf(',') !== -1 || this.options.isRange) ?
102 attachEvents: function() {
103 this.clickableBar.click($.proxy(this.barClicked, this));
104 this.pointers.on('mousedown touchstart', $.proxy(this.onDragStart, this));
105 this.pointers.bind('dragstart', function(event) {
106 event.preventDefault();
109 onDragStart: function(e) {
110 if ( this.options.disable || (e.type === 'mousedown' && e.which !== 1)) {
115 var pointer = $(e.target);
116 this.pointers.removeClass('last-active');
117 pointer.addClass('focused last-active');
118 this[(pointer.hasClass('low') ? 'low' : 'high') + 'Label'].addClass('focused');
119 $(document).on('mousemove.slider touchmove.slider', $.proxy(this.onDrag, this, pointer));
120 $(document).on('mouseup.slider touchend.slider touchcancel.slider', $.proxy(this.onDragEnd, this));
122 onDrag: function(pointer, e) {
126 if (e.originalEvent.touches && e.originalEvent.touches.length) {
127 e = e.originalEvent.touches[0];
128 } else if (e.originalEvent.changedTouches && e.originalEvent.changedTouches.length) {
129 e = e.originalEvent.changedTouches[0];
132 var position = e.clientX - this.domNode.offset().left;
133 this.domNode.trigger('change', [this, pointer, position]);
135 onDragEnd: function(e) {
136 this.pointers.removeClass('focused')
137 .trigger('rangeslideend');
138 this.labels.removeClass('focused');
139 $(document).off('.slider');
140 this.options.ondragend.call(this, this.options.value);
142 barClicked: function(e) {
143 if(this.options.disable) return;
144 var x = e.pageX - this.clickableBar.offset().left;
146 this.setPosition(this.pointers.last(), x, true, true);
148 var firstLeft = Math.abs(parseFloat(this.pointers.first().css('left'), 10)),
149 firstHalfWidth = this.pointers.first().width() / 2,
150 lastLeft = Math.abs(parseFloat(this.pointers.last().css('left'), 10)),
151 lastHalfWidth = this.pointers.first().width() / 2,
152 leftSide = Math.abs(firstLeft - x + firstHalfWidth),
153 rightSide = Math.abs(lastLeft - x + lastHalfWidth),
156 if(leftSide == rightSide) {
157 pointer = x < firstLeft ? this.pointers.first() : this.pointers.last();
159 pointer = leftSide < rightSide ? this.pointers.first() : this.pointers.last();
161 this.setPosition(pointer, x, true, true);
163 this.options.onbarclicked.call(this, this.options.value);
165 onChange: function(e, self, pointer, position) {
168 max = self.domNode.width();
170 if (!self.isSingle()) {
171 min = pointer.hasClass('high') ? parseFloat(self.lowPointer.css("left")) + (self.lowPointer.width() / 2) : 0;
172 max = pointer.hasClass('low') ? parseFloat(self.highPointer.css("left")) + (self.highPointer.width() / 2) : self.domNode.width();
175 var value = Math.min(Math.max(position, min), max);
176 self.setPosition(pointer, value, true);
178 setPosition: function(pointer, position, isPx, animate) {
179 var leftPos, rightPos,
180 lowPos = parseFloat(this.lowPointer.css("left")),
181 highPos = parseFloat(this.highPointer.css("left")) || 0,
182 circleWidth = this.highPointer.width() / 2;
184 position = this.prcToPx(position);
186 if(this.options.snap){
187 var expPos = this.correctPositionForSnap(position);
194 if (pointer[0] === this.highPointer[0]) {
195 highPos = Math.round(position - circleWidth);
197 lowPos = Math.round(position - circleWidth);
199 pointer[animate ? 'animate' : 'css']({
200 'left': Math.round(position - circleWidth)
202 if (this.isSingle()) {
205 leftPos = lowPos + circleWidth;
206 rightPos = highPos + circleWidth;
208 var w = Math.round(highPos + circleWidth - leftPos);
209 this.bar[animate ? 'animate' : 'css']({
210 'width': Math.abs(w),
211 'left': (w>0) ? leftPos : leftPos + w
213 this.showPointerValue(pointer, position, animate);
216 correctPositionForSnap: function(position){
217 var currentValue = this.positionToValue(position) - this.options.from;
218 var diff = this.options.width / (this.interval / this.options.step),
219 expectedPosition = (currentValue / this.options.step) * diff;
220 if( position <= expectedPosition + diff / 2 && position >= expectedPosition - diff / 2){
221 return expectedPosition;
226 // will be called from outside
227 setValue: function(value) {
228 var values = value.toString().split(',');
229 values[0] = Math.min(Math.max(values[0], this.options.from), this.options.to) + '';
230 if (values.length > 1){
231 values[1] = Math.min(Math.max(values[1], this.options.from), this.options.to) + '';
233 this.options.value = value;
234 var prc = this.valuesToPrc(values.length === 2 ? values : [0, values[0]]);
235 if (this.isSingle()) {
236 this.setPosition(this.highPointer, prc[1]);
238 this.setPosition(this.lowPointer, prc[0]);
239 this.setPosition(this.highPointer, prc[1]);
242 renderScale: function() {
243 var s = this.options.scale || [this.options.from, this.options.to];
244 var prc = Math.round((100 / (s.length - 1)) * 10) / 10;
246 for (var i = 0; i < s.length; i++) {
247 str += '<span style="left: ' + i * prc + '%">' + (s[i] != '|' ? '<ins>' + s[i] + '</ins>' : '') + '</span>';
249 this.scale.html(str);
251 $('ins', this.scale).each(function() {
253 marginLeft: -$(this).outerWidth() / 2
257 getBarWidth: function() {
258 var values = this.options.value.split(',');
259 if (values.length > 1) {
260 return parseFloat(values[1]) - parseFloat(values[0]);
262 return parseFloat(values[0]);
265 showPointerValue: function(pointer, position, animate) {
266 var label = $('.pointer-label', this.domNode)[pointer.hasClass('low') ? 'first' : 'last']();
268 var value = this.positionToValue(position);
269 // Is it higer or lower than it should be?
271 if ($.isFunction(this.options.format)) {
272 var type = this.isSingle() ? undefined : (pointer.hasClass('low') ? 'low' : 'high');
273 text = this.options.format(value, type);
275 text = this.options.format.replace('%s', value);
278 var width = label.html(text).width(),
279 left = position - width / 2;
280 left = Math.min(Math.max(left, 0), this.options.width - width);
281 label[animate ? 'animate' : 'css']({
284 this.setInputValue(pointer, value);
286 valuesToPrc: function(values) {
287 var lowPrc = ((parseFloat(values[0]) - parseFloat(this.options.from)) * 100 / this.interval),
288 highPrc = ((parseFloat(values[1]) - parseFloat(this.options.from)) * 100 / this.interval);
289 return [lowPrc, highPrc];
291 prcToPx: function(prc) {
292 return (this.domNode.width() * prc) / 100;
294 isDecimal: function() {
295 return ((this.options.value + this.options.from + this.options.to).indexOf(".")===-1) ? false : true;
297 positionToValue: function(pos) {
298 var value = (pos / this.domNode.width()) * this.interval;
299 value = parseFloat(value, 10) + parseFloat(this.options.from, 10);
300 if (this.isDecimal()) {
301 var final = Math.round(Math.round(value / this.options.step) * this.options.step *100)/100;
304 if (final.indexOf(".")===-1) {
307 while (final.length - final.indexOf('.')<3) {
315 return Math.round(value / this.options.step) * this.options.step;
318 setInputValue: function(pointer, v) {
319 // if(!isChanged) return;
320 if (this.isSingle()) {
321 this.options.value = v.toString();
323 var values = this.options.value.split(',');
324 if (pointer.hasClass('low')) {
325 this.options.value = v + ',' + values[1];
327 this.options.value = values[0] + ',' + v;
330 if (this.inputNode.val() !== this.options.value) {
331 this.inputNode.val(this.options.value)
333 this.options.onstatechange.call(this, this.options.value);
336 getValue: function() {
337 return this.options.value;
339 getOptions: function() {
342 getRange: function() {
343 return this.options.from + "," + this.options.to;
345 isReadonly: function(){
346 this.domNode.toggleClass('slider-readonly', this.options.disable);
349 this.options.disable = true;
353 this.options.disable = false;
356 toggleDisable: function(){
357 this.options.disable = !this.options.disable;
360 updateRange: function(range, value) {
361 var values = range.toString().split(',');
362 this.interval = parseInt(values[1]) - parseInt(values[0]);
364 this.setValue(value);
366 this.setValue(this.getValue());
371 var pluginName = 'jRange';
372 // A really lightweight plugin wrapper around the constructor,
373 // preventing against multiple instantiations
374 $.fn[pluginName] = function(option) {
375 var args = arguments,
378 this.each(function() {
380 data = $.data(this, 'plugin_' + pluginName),
381 options = typeof option === 'object' && option;
383 $this.data('plugin_' + pluginName, (data = new jRange(this, options)));
384 $(window).resize(function() {
385 data.setValue(data.getValue());
386 }); // Update slider position when window is resized to keep it in sync with scale
388 // if first argument is a string, call silimarly named function
389 // this gives flexibility to call functions of the plugin e.g.
390 // - $('.dial').plugin('destroy');
391 // - $('.dial').plugin('render', $('.new-child'));
392 if (typeof option === 'string') {
393 result = data[option].apply(data, Array.prototype.slice.call(args, 1));
397 // To enable plugin returns values
398 return result || this;
401 })(jQuery, window, document);