1 /* =========================================================
2 * bootstrap-slider.js v3.0.0
3 * =========================================================
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 * ========================================================= */
21 formatInvalidInputErrorMsg : function(input) {
22 return "Invalid input value '" + input + "' passed in";
24 callingContextNotSliderInstance : "Calling context element does not have instance of Slider bound to it. Check your code to make sure the JQuery object returned from the call to the slider() initializer is calling the method"
27 var Slider = function(element, options) {
28 var el = this.element = $(element).hide();
29 var origWidth = $(element)[0].style.width;
31 var updateSlider = false;
32 var parent = this.element.parent();
35 if (parent.hasClass('slider') === true) {
39 this.picker = $('<div class="slider">'+
40 '<div class="slider-track">'+
41 '<div class="slider-selection"></div>'+
42 '<div class="slider-handle min-slider-handle"></div>'+
43 '<div class="slider-handle max-slider-handle"></div>'+
45 '<div id="tooltip" class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'+
46 '<div id="tooltip_min" class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'+
47 '<div id="tooltip_max" class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'+
49 .insertBefore(this.element)
50 .append(this.element);
53 this.id = this.element.data('slider-id')||options.id;
55 this.picker[0].id = this.id;
58 if (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch) {
59 this.touchCapable = true;
62 var tooltip = this.element.data('slider-tooltip')||options.tooltip;
64 this.tooltip = this.picker.find('#tooltip');
65 this.tooltipInner = this.tooltip.find('div.tooltip-inner');
67 this.tooltip_min = this.picker.find('#tooltip_min');
68 this.tooltipInner_min = this.tooltip_min.find('div.tooltip-inner');
70 this.tooltip_max = this.picker.find('#tooltip_max');
71 this.tooltipInner_max= this.tooltip_max.find('div.tooltip-inner');
73 if (updateSlider === true) {
75 this.picker.removeClass('slider-horizontal');
76 this.picker.removeClass('slider-vertical');
77 this.tooltip.removeClass('hide');
78 this.tooltip_min.removeClass('hide');
79 this.tooltip_max.removeClass('hide');
83 this.orientation = this.element.data('slider-orientation')||options.orientation;
84 switch(this.orientation) {
86 this.picker.addClass('slider-vertical');
87 this.stylePos = 'top';
88 this.mousePos = 'pageY';
89 this.sizePos = 'offsetHeight';
90 this.tooltip.addClass('right')[0].style.left = '100%';
91 this.tooltip_min.addClass('right')[0].style.left = '100%';
92 this.tooltip_max.addClass('right')[0].style.left = '100%';
96 .addClass('slider-horizontal')
97 .css('width', origWidth);
98 this.orientation = 'horizontal';
99 this.stylePos = 'left';
100 this.mousePos = 'pageX';
101 this.sizePos = 'offsetWidth';
102 this.tooltip.addClass('top')[0].style.top = -this.tooltip.outerHeight() - 14 + 'px';
103 this.tooltip_min.addClass('top')[0].style.top = -this.tooltip_min.outerHeight() - 14 + 'px';
104 this.tooltip_max.addClass('top')[0].style.top = -this.tooltip_max.outerHeight() - 14 + 'px';
116 ], function(i, attr) {
117 if (typeof el.data('slider-' + attr) !== 'undefined') {
118 self[attr] = el.data('slider-' + attr);
119 } else if (typeof options[attr] !== 'undefined') {
120 self[attr] = options[attr];
121 } else if (typeof el.prop(attr) !== 'undefined') {
122 self[attr] = el.prop(attr);
124 self[attr] = 0; // to prevent empty string issues in calculations in IE
128 if (this.value instanceof Array) {
129 if (updateSlider && !this.range) {
130 this.value = this.value[0];
134 } else if (this.range) {
135 // User wants a range, but value is not an array
136 this.value = [this.value, this.max];
139 this.selection = this.element.data('slider-selection')||options.selection;
140 this.selectionEl = this.picker.find('.slider-selection');
141 if (this.selection === 'none') {
142 this.selectionEl.addClass('hide');
145 this.selectionElStyle = this.selectionEl[0].style;
147 this.handle1 = this.picker.find('.slider-handle:first');
148 this.handle1Stype = this.handle1[0].style;
150 this.handle2 = this.picker.find('.slider-handle:last');
151 this.handle2Stype = this.handle2[0].style;
153 if (updateSlider === true) {
155 this.handle1.removeClass('round triangle');
156 this.handle2.removeClass('round triangle hide');
159 var availableHandleModifiers = ['round', 'triangle', 'custom'];
160 if (availableHandleModifiers.indexOf(this.handle) !== -1){
161 this.handle1.addClass(this.handle);
162 this.handle2.addClass(this.handle);
165 this.offset = this.picker.offset();
166 this.size = this.picker[0][this.sizePos];
167 this.formater = options.formater;
169 this.tooltip_separator = options.tooltip_separator;
170 this.tooltip_split = options.tooltip_split;
172 this.setValue(this.value);
175 keydown: $.proxy(this.keydown, this, 0)
178 keydown: $.proxy(this.keydown, this, 1)
181 if (this.touchCapable) {
182 // Touch: Bind touch events:
184 touchstart: $.proxy(this.mousedown, this)
187 // Bind mouse events:
189 mousedown: $.proxy(this.mousedown, this)
192 if(tooltip === 'hide') {
193 this.tooltip.addClass('hide');
194 this.tooltip_min.addClass('hide');
195 this.tooltip_max.addClass('hide');
196 } else if(tooltip === 'always') {
198 this.alwaysShowTooltip = true;
201 mouseenter: $.proxy(this.showTooltip, this),
202 mouseleave: $.proxy(this.hideTooltip, this)
205 focus: $.proxy(this.showTooltip, this),
206 blur: $.proxy(this.hideTooltip, this)
209 focus: $.proxy(this.showTooltip, this),
210 blur: $.proxy(this.hideTooltip, this)
214 this.enabled = options.enabled &&
215 (this.element.data('slider-enabled') === undefined || this.element.data('slider-enabled') === true);
221 this.natural_arrow_keys = this.element.data('slider-natural_arrow_keys') || options.natural_arrow_keys;
230 showTooltip: function(){
231 if (this.tooltip_split === false ){
232 this.tooltip.addClass('in');
234 this.tooltip_min.addClass('in');
235 this.tooltip_max.addClass('in');
241 hideTooltip: function(){
242 if (this.inDrag === false && this.alwaysShowTooltip !== true) {
243 this.tooltip.removeClass('in');
244 this.tooltip_min.removeClass('in');
245 this.tooltip_max.removeClass('in');
251 var positionPercentages;
254 positionPercentages = [ 100 - this.percentage[0], this.percentage[1] ];
256 positionPercentages = [ this.percentage[0], this.percentage[1] ];
259 this.handle1Stype[this.stylePos] = positionPercentages[0]+'%';
260 this.handle2Stype[this.stylePos] = positionPercentages[1]+'%';
262 if (this.orientation === 'vertical') {
263 this.selectionElStyle.top = Math.min(positionPercentages[0], positionPercentages[1]) +'%';
264 this.selectionElStyle.height = Math.abs(positionPercentages[0] - positionPercentages[1]) +'%';
266 this.selectionElStyle.left = Math.min(positionPercentages[0], positionPercentages[1]) +'%';
267 this.selectionElStyle.width = Math.abs(positionPercentages[0] - positionPercentages[1]) +'%';
269 var offset_min = this.tooltip_min[0].getBoundingClientRect();
270 var offset_max = this.tooltip_max[0].getBoundingClientRect();
272 if (offset_min.right > offset_max.left) {
273 this.tooltip_max.removeClass('top');
274 this.tooltip_max.addClass('bottom')[0].style.top = 18 + 'px';
276 this.tooltip_max.removeClass('bottom');
277 this.tooltip_max.addClass('top')[0].style.top = -30 + 'px';
282 this.tooltipInner.text(
283 this.formater(this.value[0]) + this.tooltip_separator + this.formater(this.value[1])
285 this.tooltip[0].style[this.stylePos] = (positionPercentages[1] + positionPercentages[0])/2 + '%';
286 if (this.orientation === 'vertical') {
287 this.tooltip.css('margin-top', -this.tooltip.outerHeight() / 2 + 'px');
289 this.tooltip.css('margin-left', -this.tooltip.outerWidth() / 2 + 'px');
292 if (this.orientation === 'vertical') {
293 this.tooltip.css('margin-top', -this.tooltip.outerHeight() / 2 + 'px');
295 this.tooltip.css('margin-left', -this.tooltip.outerWidth() / 2 + 'px');
297 this.tooltipInner_min.text(
298 this.formater(this.value[0])
300 this.tooltipInner_max.text(
301 this.formater(this.value[1])
304 this.tooltip_min[0].style[this.stylePos] = positionPercentages[0] + '%';
305 if (this.orientation === 'vertical') {
306 this.tooltip_min.css('margin-top', -this.tooltip_min.outerHeight() / 2 + 'px');
308 this.tooltip_min.css('margin-left', -this.tooltip_min.outerWidth() / 2 + 'px');
310 this.tooltip_max[0].style[this.stylePos] = positionPercentages[1] + '%';
311 if (this.orientation === 'vertical') {
312 this.tooltip_max.css('margin-top', -this.tooltip_max.outerHeight() / 2 + 'px');
314 this.tooltip_max.css('margin-left', -this.tooltip_max.outerWidth() / 2 + 'px');
317 this.tooltipInner.text(
318 this.formater(this.value[0])
320 this.tooltip[0].style[this.stylePos] = positionPercentages[0] + '%';
321 if (this.orientation === 'vertical') {
322 this.tooltip.css('margin-top', -this.tooltip.outerHeight() / 2 + 'px');
324 this.tooltip.css('margin-left', -this.tooltip.outerWidth() / 2 + 'px');
329 mousedown: function(ev) {
330 if(!this.isEnabled()) {
333 // Touch: Get the original event:
334 if (this.touchCapable && ev.type === 'touchstart') {
335 ev = ev.originalEvent;
338 this.triggerFocusOnHandle();
340 this.offset = this.picker.offset();
341 this.size = this.picker[0][this.sizePos];
343 var percentage = this.getPercentage(ev);
346 var diff1 = Math.abs(this.percentage[0] - percentage);
347 var diff2 = Math.abs(this.percentage[1] - percentage);
348 this.dragged = (diff1 < diff2) ? 0 : 1;
353 this.percentage[this.dragged] = this.reversed ? 100 - percentage : percentage;
356 if (this.touchCapable) {
357 // Touch: Bind touch events:
359 touchmove: $.proxy(this.mousemove, this),
360 touchend: $.proxy(this.mouseup, this)
363 // Bind mouse events:
365 mousemove: $.proxy(this.mousemove, this),
366 mouseup: $.proxy(this.mouseup, this)
370 var val = this.calculateValue();
371 this.element.trigger({
381 triggerFocusOnHandle: function(handleIdx) {
382 if(handleIdx === 0) {
383 this.handle1.focus();
385 if(handleIdx === 1) {
386 this.handle2.focus();
390 keydown: function(handleIdx, ev) {
391 if(!this.isEnabled()) {
410 // use natural arrow keys instead of from min to max
411 if (this.natural_arrow_keys) {
412 if ((this.orientation === 'vertical' && !this.reversed) || (this.orientation === 'horizontal' && this.reversed)) {
417 var oneStepValuePercentageChange = dir * this.percentage[2];
418 var percentage = this.percentage[handleIdx] + oneStepValuePercentageChange;
420 if (percentage > 100) {
422 } else if (percentage < 0) {
426 this.dragged = handleIdx;
427 this.adjustPercentageForRangeSliders(percentage);
428 this.percentage[this.dragged] = percentage;
431 var val = this.calculateValue();
433 this.element.trigger({
440 this.setValue(val, true);
452 mousemove: function(ev) {
453 if(!this.isEnabled()) {
456 // Touch: Get the original event:
457 if (this.touchCapable && ev.type === 'touchmove') {
458 ev = ev.originalEvent;
461 var percentage = this.getPercentage(ev);
462 this.adjustPercentageForRangeSliders(percentage);
463 this.percentage[this.dragged] = this.reversed ? 100 - percentage : percentage;
466 var val = this.calculateValue();
467 this.setValue(val, true);
471 adjustPercentageForRangeSliders: function(percentage) {
473 if (this.dragged === 0 && this.percentage[1] < percentage) {
474 this.percentage[0] = this.percentage[1];
476 } else if (this.dragged === 1 && this.percentage[0] > percentage) {
477 this.percentage[1] = this.percentage[0];
483 mouseup: function() {
484 if(!this.isEnabled()) {
487 if (this.touchCapable) {
488 // Touch: Unbind touch event handlers:
490 touchmove: this.mousemove,
491 touchend: this.mouseup
494 // Unbind mouse event handlers:
496 mousemove: this.mousemove,
497 mouseup: this.mouseup
501 if (this.over === false) {
504 var val = this.calculateValue();
516 calculateValue: function() {
519 val = [this.min,this.max];
520 if (this.percentage[0] !== 0){
521 val[0] = (Math.max(this.min, this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step));
522 val[0] = this.applyPrecision(val[0]);
524 if (this.percentage[1] !== 100){
525 val[1] = (Math.min(this.max, this.min + Math.round((this.diff * this.percentage[1]/100)/this.step)*this.step));
526 val[1] = this.applyPrecision(val[1]);
530 val = (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step);
531 if (val < this.min) {
534 else if (val > this.max) {
537 val = parseFloat(val);
538 val = this.applyPrecision(val);
539 this.value = [val, this.value[1]];
543 applyPrecision: function(val) {
544 var precision = this.precision || this.getNumDigitsAfterDecimalPlace(this.step);
545 return this.applyToFixedAndParseFloat(val, precision);
548 Credits to Mike Samuel for the following method!
549 Source: http://stackoverflow.com/questions/10454518/javascript-how-to-retrieve-the-number-of-decimals-of-a-string-number
551 getNumDigitsAfterDecimalPlace: function(num) {
552 var match = (''+num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
553 if (!match) { return 0; }
554 return Math.max(0, (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0));
557 applyToFixedAndParseFloat: function(num, toFixedInput) {
558 var truncatedNum = num.toFixed(toFixedInput);
559 return parseFloat(truncatedNum);
562 getPercentage: function(ev) {
563 if (this.touchCapable && (ev.type === 'touchstart' || ev.type === 'touchmove')) {
566 var percentage = (ev[this.mousePos] - this.offset[this.stylePos])*100/this.size;
567 percentage = Math.round(percentage/this.percentage[2])*this.percentage[2];
568 return Math.max(0, Math.min(100, percentage));
571 getValue: function() {
575 return this.value[0];
578 setValue: function(val, triggerSlideEvent) {
582 this.value = this.validateInputValue(val);
585 this.value[0] = this.applyPrecision(this.value[0]);
586 this.value[1] = this.applyPrecision(this.value[1]);
588 this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0]));
589 this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1]));
591 this.value = this.applyPrecision(this.value);
592 this.value = [ Math.max(this.min, Math.min(this.max, this.value))];
593 this.handle2.addClass('hide');
594 if (this.selection === 'after') {
595 this.value[1] = this.max;
597 this.value[1] = this.min;
601 this.diff = this.max - this.min;
604 (this.value[0] - this.min) * 100 / this.diff,
605 (this.value[1] - this.min) * 100 / this.diff,
606 this.step * 100 / this.diff
609 this.percentage = [0, 0, 100];
615 if(triggerSlideEvent === true) {
616 var slideEventValue = this.range ? this.value : this.value[0];
620 'value': slideEventValue
622 .data('value', slideEventValue)
623 .prop('value', slideEventValue);
627 validateInputValue : function(val) {
628 if(typeof val === 'number') {
630 } else if(val instanceof Array) {
631 $.each(val, function(i, input) { if (typeof input !== 'number') { throw new Error( ErrorMsgs.formatInvalidInputErrorMsg(input) ); }});
634 throw new Error( ErrorMsgs.formatInvalidInputErrorMsg(val) );
641 this.element.off().show().insertBefore(this.picker);
642 this.picker.off().remove();
643 $(this.element).removeData('slider');
646 disable: function() {
647 this.enabled = false;
648 this.handle1.removeAttr("tabindex");
649 this.handle2.removeAttr("tabindex");
650 this.picker.addClass('slider-disabled');
651 this.element.trigger('slideDisabled');
656 this.handle1.attr("tabindex", 0);
657 this.handle2.attr("tabindex", 0);
658 this.picker.removeClass('slider-disabled');
659 this.element.trigger('slideEnabled');
670 isEnabled: function() {
674 setAttribute: function(attribute, value) {
675 this[attribute] = value;
678 getAttribute: function(attribute) {
679 return this[attribute];
684 var publicMethods = {
685 getValue : Slider.prototype.getValue,
686 setValue : Slider.prototype.setValue,
687 setAttribute : Slider.prototype.setAttribute,
688 getAttribute : Slider.prototype.getAttribute,
689 destroy : Slider.prototype.destroy,
690 disable : Slider.prototype.disable,
691 enable : Slider.prototype.enable,
692 toggle : Slider.prototype.toggle,
693 isEnabled: Slider.prototype.isEnabled
696 $.fn.slider = function (option) {
697 if (typeof option === 'string' && option !== 'refresh') {
698 var args = Array.prototype.slice.call(arguments, 1);
699 return invokePublicMethod.call(this, option, args);
701 return createNewSliderInstance.call(this, option);
705 function invokePublicMethod(methodName, args) {
706 if(publicMethods[methodName]) {
707 var sliderObject = retrieveSliderObjectFromElement(this);
708 var result = publicMethods[methodName].apply(sliderObject, args);
710 if (typeof result === "undefined") {
716 throw new Error("method '" + methodName + "()' does not exist for slider.");
720 function retrieveSliderObjectFromElement(element) {
721 var sliderObject = $(element).data('slider');
722 if(sliderObject && sliderObject instanceof Slider) {
725 throw new Error(ErrorMsgs.callingContextNotSliderInstance);
729 function createNewSliderInstance(opts) {
731 $this.each(function() {
733 slider = $this.data('slider'),
734 options = typeof opts === 'object' && opts;
736 // If slider already exists, use its attributes
737 // as options so slider refreshes properly
738 if (slider && !options) {
741 $.each($.fn.slider.defaults, function(key) {
742 options[key] = slider[key];
746 $this.data('slider', (new Slider(this, $.extend({}, $.fn.slider.defaults, options))));
751 $.fn.slider.defaults = {
756 orientation: 'horizontal',
761 tooltip_separator: ':',
762 tooltip_split: false,
763 natural_arrow_keys: false,
767 formater: function(value) {
772 $.fn.slider.Constructor = Slider;
776 /* vim: set noexpandtab tabstop=4 shiftwidth=4 autoindent: */