/*! Nestoria Slider - v1.0.13 - 2015-07-16 * http://lokku.github.io/jquery-nstslider/ * Copyright (c) 2015 Lokku Ltd.; Licensed MIT */ (function($) { /* * These are used for user interaction. This plugin assumes the user can * interact with one control at a time. For this reason it's safe to keep * these global. */ var _$current_slider; var _is_mousedown; var _original_mousex; // both for keyboard and mouse interaction var _is_left_grip; // for keyboard interaction only var _before_keydown_value; var _before_keydown_pixel; var _before_keyup_value; var _before_keyup_pixel; // a fixed configuration for the single bar slider, used to decide where to // place the naked bar. var _naked_bar_deltas; // see populateNakedBarDeltas var _methods = { /* * This method must be called once during initialization. * It sets the behaviour of the naked bar in case of one handle. */ 'setNakedBarDelta': function (position, handleWidth) { if (position === "stickToSides") { _naked_bar_deltas = { toEndWidth: handleWidth, toBeginLeft: 0, toBeginWidth: handleWidth }; } else if (position === "middle") { // Position naked end of the bar at the middle value. _naked_bar_deltas = { toEndWidth: handleWidth/2, toBeginLeft: handleWidth/2, toBeginWidth: handleWidth/2 }; } else { throw new Error('unknown position of setNakedBarDelta: ' + position); } }, 'getSliderValuesAtPositionPx' : function (leftPx, rightPx) { var $this = this, leftPxInValue, rightPxInValue, pixel_to_value_mapping_func = $this.data('pixel_to_value_mapping'); if (typeof pixel_to_value_mapping_func !== 'undefined') { leftPxInValue = pixel_to_value_mapping_func(leftPx); rightPxInValue = pixel_to_value_mapping_func(rightPx); } else { var w = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'); leftPxInValue = _methods.inverse_rangemap_0_to_n.call($this, leftPx, w); rightPxInValue = _methods.inverse_rangemap_0_to_n.call($this, rightPx, w); } return [leftPxInValue, rightPxInValue]; }, /* * Move slider grips to the specified position. This method is * designed to run within the user interaction lifecycle. Only call * this method if the user has interacted with the sliders * actually... * * First the desired positions are validated. If values are ok, the * move is performed, otherwise it's just ignored because weird * values have been passed. */ 'validateAndMoveGripsToPx' : function (nextLeftGripPositionPx, nextRightGripPositionPx) { var $this = this; var draggableAreaLengthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'); // // Validate & Move // if (nextRightGripPositionPx <= draggableAreaLengthPx && nextLeftGripPositionPx >= 0 && nextLeftGripPositionPx <= draggableAreaLengthPx && (!$this.data('has_right_grip') || nextLeftGripPositionPx <= nextRightGripPositionPx) ) { var prevMin = $this.data('cur_min'), prevMax = $this.data('cur_max'); // note: also stores new cur_min, cur_max _methods.set_position_from_px.call($this, nextLeftGripPositionPx, nextRightGripPositionPx); // set the style of the grips according to the highlighted range _methods.refresh_grips_style.call($this); _methods.notify_changed_implicit.call($this, 'drag_move', prevMin, prevMax); } return $this; }, /* * Update aria attributes of the slider based on the current * configuration of the slider. */ 'updateAriaAttributes' : function () { var $this = this, settings = $this.data('settings'), $leftGrip = $this.find(settings.left_grip_selector); // // double grips sliders is probably the most common case... // ... also, the values to be set in the two cases are quite // different. // if ($this.data('has_right_grip')) { var $rightGrip = $this.find(settings.right_grip_selector); // // grips are mutually binding their max/min values when 2 grips // are present. For example, we should imagine the left grip as // being constrained between [ rangeMin, valueMax ] // $leftGrip .attr('aria-valuemin', $this.data('range_min')) .attr('aria-valuenow', methods.get_current_min_value.call($this)) .attr('aria-valuemax', methods.get_current_max_value.call($this)); $rightGrip .attr('aria-valuemin', methods.get_current_min_value.call($this)) .attr('aria-valuenow', methods.get_current_max_value.call($this)) .attr('aria-valuemax', $this.data('range_max')); } else { $leftGrip .attr('aria-valuemin', $this.data('range_min')) .attr('aria-valuenow', methods.get_current_min_value.call($this)) .attr('aria-valuemax', $this.data('range_max')); } return $this; }, /* * Return the width in pixels of the slider bar, i.e., the maximum * number of pixels the user can slide the slider over. This function * should always be used internally to obtain the width of the * slider in pixels! */ 'getSliderWidthPx' : function () { var $this = this; // // .width() can actually return a floating point number! see // jquery docs! // return Math.round($this.width()); }, /* * Return the position of a given grip in pixel in integer format. * Use this method internally if you are literally going to get the * left CSS property from the provided grip. * * This method assumes a certain grip exists and will have the left * property. * * This is generally safe for the left grip, because it is basically * guaranteed to exist. But for the right grip you should be really * using getRightGripPositionPx instead. * */ 'getGripPositionPx' : function ($grip) { return parseInt($grip.css('left').replace('px',''), 10); }, /* * Just the same as getGripPositionPx, but there is no need to provide * the $slider. */ 'getLeftGripPositionPx' : function () { var $this = this, settings = $this.data('settings'), $leftGrip = $this.find(settings.left_grip_selector); return _methods.getGripPositionPx.call($this, $leftGrip); }, /* * Return the position of the right Grip if it exists or return the * current position if not. Even if the right grip doesn't exist, its * position should be defined, as it determines the position of the * bar. */ 'getRightGripPositionPx' : function () { var $this = this, settings = $this.data('settings'); if ($this.data('has_right_grip')) { return _methods.getGripPositionPx.call($this, $this.find(settings.right_grip_selector) ); } // default var sliderWidthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'); return _methods.rangemap_0_to_n.call($this, $this.data('cur_max'), sliderWidthPx); }, /* * Return the width of the left grip. Like getSliderWidthPx, this * method deals with .width() returning a floating point number. All * the code in this plugin assumes an integer here! */ 'getLeftGripWidth' : function () { var $this = this, settings = $this.data('settings'), $leftGrip = $this.find(settings.left_grip_selector); return Math.round($leftGrip.outerWidth()); }, /* * Return the width of the right grip. The calling method should * check that the right grip actually exists. This method assumes it * does. */ 'getRightGripWidth' : function () { var $this = this, settings = $this.data('settings'), $rightGrip = $this.find(settings.right_grip_selector); return Math.round($rightGrip.outerWidth()); }, 'binarySearchValueToPxCompareFunc' : function (s, a, i) { // Must return: // // s: element to search for // a: array we are looking in // i: position of the element we are looking for // // -1 (s < a[i]) // 0 found (= a[i]) // 1 (s > a[i]) if (s === a[i]) { return 0; } // element found exactly if (s < a[i] && i === 0) { return 0; } // left extreme case e.g., a = [ 3, ... ], s = 1 if (a[i-1] <= s && s < a[i]) { return 0; } // s is between two elements, always return the rightmost if (s > a[i]) { return 1; } if (s <= a[i-1]) { return -1; } $.error('cannot compare s: ' + s + ' with a[' + i + ']. a is: ' + a.join(',')); }, /* * Perform binary search to find searchElement into a generic array. * It uses a customized compareFunc to perform the comparison between * two elements of the array and a getElement function to pick the * element from the array (e.g., in case we want to pick a field of an * array of objects) */ 'binarySearch' : function(array, searchElement, getElementFunc, compareFunc) { var minIndex = 0; var maxIndex = array.length - 1; var currentIndex; var currentElement; while (minIndex <= maxIndex) { currentIndex = (minIndex + maxIndex) / 2 | 0; currentElement = getElementFunc(array, currentIndex); // lt = -1 (searchElement < currentElement) // eq = 0 // gt = 1 (searchElement > currentElement) var lt_eq_gt = compareFunc(searchElement, array, currentIndex); if (lt_eq_gt > 0) { minIndex = currentIndex + 1; } else if (lt_eq_gt < 0) { maxIndex = currentIndex - 1; } else { return currentIndex; } } return -1; }, /* * Returns true if this slider has limit, false otherwise. There can be * an upper limit and a lower limit for the sliders. * The lower/upper limits are values that are out of the slider range, * but that can be selected by the user when he moves a slider all the * way down the minimum and up to the maximum value. */ 'haveLimits' : function () { var $this = this, lowerLimit = $this.data('lower-limit'), upperLimit = $this.data('upper-limit'), haveLimits = false; if (typeof lowerLimit !== 'undefined' && typeof upperLimit !== 'undefined') { haveLimits = true; } return haveLimits; }, /* * This method is called whenever the style of the grips needs to get * updated. */ 'refresh_grips_style' : function () { var $this = this, settings = $this.data('settings'); // Skip refreshing grips style if no hihglight is specified in // construction if (typeof settings.highlight === 'undefined') { return; } var highlightedRangeMin = $this.data('highlightedRangeMin'); if (typeof highlightedRangeMin === 'undefined') { return; } var $leftGrip = $this.find(settings.left_grip_selector), $rightGrip = $this.find(settings.right_grip_selector), highlightedRangeMax = $this.data('highlightedRangeMax'), curMin = $this.data('cur_min'), curMax = $this.data('cur_max'), highlightGripClass = settings.highlight.grip_class; // curmin is within the highlighted range if (curMin < highlightedRangeMin || curMin > highlightedRangeMax) { // de-highlight grip $leftGrip.removeClass(highlightGripClass); } else { // highlight grip $leftGrip.addClass(highlightGripClass); } // time to highlight right grip if (curMax < highlightedRangeMin || curMax > highlightedRangeMax) { // de-highlight grip $rightGrip.removeClass(highlightGripClass); } else { // highlight grip $rightGrip.addClass(highlightGripClass); } }, /* * Set left and right handle at the right position on the screen (pixels) * given the desired position in currency. * * e.g., _methods.set_position_from_val.call($this, 10000, 100000); * * may set the left handle at 100px and the right handle at * 200px; * */ 'set_position_from_val' : function (cur_min, cur_max) { var $this = this; // // We need to understand how much pixels cur_min and cur_max // correspond. // var range_min = $this.data('range_min'), range_max = $this.data('range_max'); // // (safety) constrain the cur_min or the cur_max value between the // max/min ranges allowed for this slider. // if (cur_min < range_min) { cur_min = range_min; } if (cur_min > range_max) { cur_min = range_max; } if ($this.data('has_right_grip')) { if (cur_max > range_max) { cur_max = range_max; } if (cur_max < range_min) { cur_max = range_min; } } else { cur_max = $this.data('cur_max'); } var leftPx = methods.value_to_px.call($this, cur_min), rightPx = methods.value_to_px.call($this, cur_max); _methods.set_handles_at_px.call($this, leftPx, rightPx); // save this position $this.data('cur_min', cur_min); if ($this.data('has_right_grip')) { $this.data('cur_max', cur_max); } return $this; }, /* * Set the position of the handles at the specified pixel points (taking * the whole slider width as a maximum point). */ 'set_position_from_px' : function (leftPx, rightPx) { var $this = this; // // we need to find a value from the given value in pixels // // now set the position as requested... _methods.set_handles_at_px.call($this, leftPx, rightPx); var valueLeftRight = _methods.getSliderValuesAtPositionPx.call($this, leftPx, rightPx), leftPxInValue = valueLeftRight[0], rightPxInValue = valueLeftRight[1]; // ... and save the one we've found. $this.data('cur_min', leftPxInValue); if ($this.data('has_right_grip')) { $this.data('cur_max', rightPxInValue); } return $this; }, /* * Updates the CSS of grips and bar so that the left grip appears at * leftPx and the right grip appears at rightPx. Note: leftPx can be > * rightPx. */ 'set_handles_at_px' : function (leftPx, rightPx) { var $this = this; var settings = $this.data('settings'); var left_grip_selector = settings.left_grip_selector, right_grip_selector = settings.right_grip_selector, value_bar_selector = settings.value_bar_selector; var handleWidth = $this.data('left_grip_width'); // The left grip $this.find(left_grip_selector).css('left', leftPx + 'px'); // The right grip $this.find(right_grip_selector).css('left', rightPx + 'px'); // The value bar if ($this.data('has_right_grip')) { // If both the grips are there, the value bar must stick to // beginning and the end of the grips. $this.find(value_bar_selector) .css('left', leftPx + 'px') .css('width', (rightPx - leftPx + handleWidth) + 'px'); } else { if (!_naked_bar_deltas) { _methods.populateNakedBarDeltas.call($this, leftPx, rightPx, handleWidth); } if (rightPx > leftPx) { // The naked end of the bar is on the right of the grip $this.find(value_bar_selector) .css('left', leftPx + 'px') .css('width', rightPx - leftPx + _naked_bar_deltas.toEndWidth + 'px'); } else { // The naked end of the bar is on the left of the grip // NOTE: leftPx and rightPx are to be read swapped here. $this.find(value_bar_selector) .css('left', rightPx + _naked_bar_deltas.toBeginLeft + 'px') .css('width', (leftPx - rightPx + _naked_bar_deltas.toBeginWidth) + 'px'); } } return $this; }, 'drag_start_func_touch' : function (e, settings, $left_grip, $right_grip, is_touch) { var $this = this, original_event = e.originalEvent, touch = original_event.touches[0]; // for touch devices we need to make sure we allow the user to scroll // if the click was too far from the slider. var curY = touch.pageY, curX = touch.pageX; // is the user allowed to grab if he/she tapped too far from the // slider? var ydelta = Math.abs($this.offset().top - curY), slider_left = $this.offset().left, xldelta = slider_left - curX, xrdelta = curX - (slider_left + $this.width()); if (ydelta > settings.touch_tolerance_value_bar_y || xldelta > settings.touch_tolerance_value_bar_x || xrdelta > settings.touch_tolerance_value_bar_x ) { return; } original_event.preventDefault(); _original_mousex = touch.pageX; // true : is touch event _methods.drag_start_func.call($this, touch, settings, $left_grip, $right_grip, is_touch); }, 'drag_start_func' : function (e, settings, $leftGrip, $rightGrip, is_touch) { var $this = this; $this.find(settings.left_grip_selector + ',' + settings.value_bar_selector + ',' + settings.right_grip_selector).removeClass( settings.animating_css_class ); if (!methods.is_enabled.call($this)) { return; } // // if the user used the finger, he/she is allowed to touch anywhere. // but if the mouse is used, we want to enable the logic only for // left grip, right grip, bar/panel elements. // var $target = $(e.target); // ... if the highlight range was enabled we should check wether // the user has tapped or clicked the highlight panel... var targetIsPanelSelector = false; if (typeof settings.highlight === 'object') { targetIsPanelSelector = $target.is(settings.highlight.panel_selector); } if (is_touch === false && !$target.is(settings.left_grip_selector) && !$target.is(settings.right_grip_selector) && !$target.is(settings.value_bar_selector) && !targetIsPanelSelector && !$target.is($this) ) { return; } // - - - - // the following logic finds the nearest slider grip and starts // dragging it. // - - - - _$current_slider = $this; var leftGripPositionPx = _methods.getGripPositionPx.call($this, $leftGrip), sliderWidthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'), lleft = $leftGrip.offset().left, rleft, // don't compute this yet (maybe not needed if 1 grip) curX, ldist, rdist, ldelta, rdelta; var rightGripPositionPx = _methods.getRightGripPositionPx.call($this); // // We need to do as if the click happened a bit more on the left. // That's because we will be setting the left CSS property at the // point where the click happened, meaning the slider grip will be // spanning to the right. // curX = Math.round(e.pageX) - ($this.data('left_grip_width') / 2); // calculate deltas from left and right grip ldist = Math.abs(lleft - curX); ldelta = curX - lleft; if ($this.data('has_right_grip')) { rleft = $rightGrip.offset().left; rdist = Math.abs(rleft - curX); rdelta = curX - rleft; } else { // no right grip... we make the right slider // unreachable! rdist = ldist * 2; rdelta = ldelta * 2; } // notify the beginning of a dragging... settings.user_drag_start_callback.call($this, e); if (ldist === rdist) { if (curX < lleft) { // move the left grip leftGripPositionPx += ldelta; _is_left_grip = true; } else { // move the right grip rightGripPositionPx += rdelta; _is_left_grip = false; } } else if (ldist < rdist) { // move the left grip leftGripPositionPx += ldelta; _is_left_grip = true; } else { // move the right grip rightGripPositionPx += rdelta; _is_left_grip = false; } // // Limit the right grip to the maximum allowed - as the user can // actually click beyond it! // // ............... // ^-- maximum clickable // ^--- maximum allowed (i.e., sliderWidth - gripWidth) // // if user clicks at sliderWidth, we will be setting CSS left of // right handle having: // // ...............R <-- out of bound :-( // ^-- maximum clickable // ^--- maximum allowed (i.e., sliderWidth - gripWidth) // // but we want: // // ..............R <-- within bound :-) // ^-- maximum clickable // ^--- maximum allowed (i.e., sliderWidth - gripWidth) // // Hence we limit. // if ($this.data('has_right_grip')) { // here we check the right handle only, because it should // always be the one that gets moved if the user clicks towards // the right extremity! if (rightGripPositionPx > sliderWidthPx) { rightGripPositionPx = sliderWidthPx; } } else { // in case we have one handle only, we will be moving the left // handle instead of the right one... hence we need to perform // this check on the left handle as well! if (leftGripPositionPx > sliderWidthPx) { leftGripPositionPx = sliderWidthPx; } } // this can happen because the user can click on the left handle! // (which is out of the left boundary) if (leftGripPositionPx < 0) { leftGripPositionPx = 0; } _is_mousedown = true; var prev_min = $this.data('cur_min'), prev_max = $this.data('cur_max'); _methods.set_position_from_px.call($this, leftGripPositionPx, rightGripPositionPx); // set the style of the grips according to the highlighted range _methods.refresh_grips_style.call($this); _methods.notify_changed_implicit.call($this, 'drag_start', prev_min, prev_max); // no need to call preventDefault on touch events, as we called // preventDefault on the original event already if (Object.prototype.toString.apply(e) !== "[object Touch]") { e.preventDefault(); } }, 'drag_move_func_touch' : function (e) { if (_is_mousedown === true) { var original_event = e.originalEvent; original_event.preventDefault(); var touch = original_event.touches[0]; _methods.drag_move_func(touch); } }, 'drag_move_func' : function (e) { if (_is_mousedown) { // our slider element. var $this = _$current_slider, settings = $this.data('settings'), sliderWidthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'), leftGripPositionPx = _methods.getLeftGripPositionPx.call($this); var rightGripPositionPx = _methods.getRightGripPositionPx.call($this); // // Here we are going to set the position in pixels based on // where the user has moved the mouse cursor. We obtain the // position of the mouse cursors via e.pageX, which returns the // absolute position of the mouse on the screen. // var absoluteMousePosition = Math.round(e.pageX); // // Compute the delta (in px) for the slider movement. It is the // difference between the new position of the cursor and the // old position of the cursor. // // Based on the delta we decide how to move the dragged handle. // // 0 : no movement // -delta: move left // +delta: move right // var delta = absoluteMousePosition - _original_mousex; // // User cannot drag the handles outside the slider bar area. // // 1) calculate the area within which the movement is // considered to be valid. var half_a_grip_width = $this.data('left_grip_width') / 2, drag_area_start = $this.offset().left + $this.data('left_grip_width') - half_a_grip_width, drag_area_end = drag_area_start + sliderWidthPx; if (settings.crossable_handles === false && $this.data('has_right_grip')) { // if handles are not crossable, we should define the left // and the right boundary of the movement. if (_is_left_grip) { drag_area_end = drag_area_start + rightGripPositionPx; } else { drag_area_start = drag_area_start + leftGripPositionPx; } } // 2) by default we accept to move the slider according to both // the deltas (i.e., left or right) var ignore_positive_delta = 0, ignore_negative_delta = 0; // 3) but if the user is moving the mouse beyond the draggable // area, we should only accept a movement in one direction. if (absoluteMousePosition < drag_area_start) { ignore_positive_delta = 1; ignore_negative_delta = 0; } if (absoluteMousePosition > drag_area_end) { ignore_negative_delta = 1; ignore_positive_delta = 0; } // // Here we decide whether to invert the grip being moved. // if (settings.crossable_handles === true && $this.data('has_right_grip')) { if (_is_left_grip) { // ... if we are using the left grip if (rightGripPositionPx <= sliderWidthPx) { // the inversion logic should only be active when the // slider is not at the extremity if (leftGripPositionPx + delta > rightGripPositionPx) { _is_left_grip = false; // TWEAK: keep the position of the left handle fixed // at the one of the right handle as the user may // have moved the mouse too fast, thus giving // leftGripPositionPx > rightGripPositionPx. // // Basically here we avoid: // // Initial State: // // ------L-R------ (leftGripPositionPx < rightGripPositionPx) // // Fast Mouse Move: // // --------R--L--- (leftGripPositionPx + delta) // --------R-L---- (leftGripPositionPx [ still > rightGripPositionPx! ]) // // _is_left_grip becomes false (this code) // leftGripPositionPx = rightGripPositionPx; } } } else { // ... converse logic if (leftGripPositionPx >= 0) { if (rightGripPositionPx + delta < leftGripPositionPx) { // current_max = current_min; _is_left_grip = true; rightGripPositionPx = leftGripPositionPx; } } } } // // Decide the position of the new handles. // var nextLeftGripPositionPx = leftGripPositionPx, nextRightGripPositionPx = rightGripPositionPx; if ((delta > 0 && !ignore_positive_delta) || (delta < 0 && !ignore_negative_delta)) { if (_is_left_grip) { nextLeftGripPositionPx += delta; } else { nextRightGripPositionPx += delta; } } _methods.validateAndMoveGripsToPx.call($this, nextLeftGripPositionPx, nextRightGripPositionPx); // prepare for next movement _original_mousex = absoluteMousePosition; // no need to call preventDefault on touch events, as we called // preventDefault on the original event already if (Object.prototype.toString.apply(e) !== "[object Touch]") { e.preventDefault(); } } }, 'drag_end_func_touch' : function (e) { var original_event = e.originalEvent; original_event.preventDefault(); var touch = original_event.touches[0]; _methods.drag_end_func(touch); }, 'drag_end_func' : function (/* e */) { var $this = _$current_slider; if (typeof $this !== 'undefined') { _is_mousedown = false; _original_mousex = undefined; _methods.notify_mouse_up_implicit.call($this, _is_left_grip); // require another click on a handler before going into here again! _$current_slider = undefined; // put back the class once user finished dragging var settings = $this.data('settings'); $this.find(settings.left_grip_selector + ',' + settings.value_bar_selector + ',' + settings.right_grip_selector).addClass( settings.animating_css_class ); } }, 'get_rounding_for_value' : function (v) { var $this = this; var rounding = $this.data('rounding'); var rounding_ranges = $this.data('rounding_ranges'); if (typeof rounding_ranges === 'object') { // then it means the rounding is not fixed, we should find the // value in the roundings_array. var roundingIdx = _methods.binarySearch.call($this, rounding_ranges, v, // pick an element from the array function (array, index) { return array[index].range; }, // compare search element with current element // < 0 search < current // 0 equals // > 0 search > current function (search, array, currentIdx) { // first check if this is our element // this is our element if the search value is: if (search < array[currentIdx].range) { // we can have a match or search in the left half if (currentIdx > 0) { if (search >= array[currentIdx - 1].range) { return 0; } else { // go left return -1; } } else { return 0; } } else { // we must search in the next half return 1; } } ); rounding = 1; if (roundingIdx > -1) { rounding = parseInt(rounding_ranges[roundingIdx].value, 10); } else { var lastIdx = rounding_ranges.length - 1; if (v >= rounding_ranges[lastIdx].range) { rounding = rounding_ranges[lastIdx].value; } } } return rounding; }, /* * Calls the user mouseup callback with the right parameters. Relies on * $data('beforestart_min/max') in addition to the isLeftGrip parameter. * * NOTE: saves the new beforestart_min and begforestart_max as well. */ 'notify_mouse_up_implicit' : function(isLeftGrip) { var $this = this, current_min_value = methods.get_current_min_value.call($this), current_max_value = methods.get_current_max_value.call($this), didValuesChange = false; // check if we changed. if (($this.data('beforestart_min') !== current_min_value) || ($this.data('beforestart_max') !== current_max_value) ) { // values have changed! didValuesChange = true; // save the new values $this.data('beforestart_min', current_min_value); $this.data('beforestart_max', current_max_value); } var settings = $this.data('settings'); settings.user_mouseup_callback.call($this, methods.get_current_min_value.call($this), methods.get_current_max_value.call($this), isLeftGrip, didValuesChange ); return $this; }, /* * NOTE: this method may take the previous min/max value as input. * if no arguments are provided the method blindly notifies. */ 'notify_changed_implicit' : function (cause, prevMin, prevMax) { var $this = this; var force = false; if (cause === 'init' || cause === 'refresh') { force = true; } var curMin = methods.get_current_min_value.call($this), curMax = methods.get_current_max_value.call($this); if (!force) { prevMin = methods.round_value_according_to_rounding.call($this, prevMin); prevMax = methods.round_value_according_to_rounding.call($this, prevMax); } if (force || curMin !== prevMin || curMax !== prevMax) { _methods.notify_changed_explicit.call($this, cause, prevMin, prevMax, curMin, curMax); force = 1; } return force; }, 'notify_changed_explicit' : function (cause, prevMin, prevMax, curMin, curMax) { var $this = this, settings = $this.data('settings'); // maybe update aria attributes for accessibility if ($this.data('aria_enabled')) { _methods.updateAriaAttributes.call($this); } settings.value_changed_callback.call($this, cause, curMin, curMax, prevMin, prevMax); return $this; }, 'validate_params' : function (settings) { var $this = this; var min_value = $this.data('range_min'), max_value = $this.data('range_max'), cur_min = $this.data('cur_min'), lower_limit = $this.data('lower-limit'), upper_limit = $this.data('upper-limit'); var have_limits = _methods.haveLimits.call($this); if (typeof min_value === 'undefined') { $.error("the data-range_min attribute was not defined"); } if (typeof max_value === 'undefined') { $.error("the data-range_max attribute was not defined"); } if (typeof cur_min === 'undefined') { $.error("the data-cur_min attribute must be defined"); } if (min_value > max_value) { $.error("Invalid input parameter. must be min < max"); } if (have_limits && lower_limit > upper_limit) { $.error('Invalid data-lower-limit or data-upper-limit'); } if ($this.find(settings.left_grip_selector).length === 0) { $.error("Cannot find element pointed by left_grip_selector: " + settings.left_grip_selector); } /* * NOTE: only validate right grip selector if it is not * undefined otherwise just assume that if it isn't * found isn't there. This is because we initialize the * slider at once and let the markup decide if the * slider is there or not. */ if (typeof settings.right_grip_selector !== 'undefined') { if ($this.find(settings.right_grip_selector).length === 0) { $.error("Cannot find element pointed by right_grip_selector: " + settings.right_grip_selector); } } // same thing for the value bar selector if (typeof settings.value_bar_selector !== 'undefined') { if ($this.find(settings.value_bar_selector).length === 0) { $.error("Cannot find element pointed by value_bar_selector" + settings.value_bar_selector); } } }, /* * Maps a value between [minRange -- maxRange] into [0 -- n]. * The target range will be an integer number. */ 'rangemap_0_to_n' : function (val, n) { var $this = this; var rangeMin = $this.data('range_min'); var rangeMax = $this.data('range_max'); if (val <= rangeMin) { return 0; } if (val >= rangeMax) { return n; } return Math.floor((n * val - n * rangeMin) / (rangeMax - rangeMin)); }, /* * Maps a value between [0 -- max] back into [minRange -- maxRange]. * The target range can be a floating point number. */ 'inverse_rangemap_0_to_n' : function (val, max) { var $this = this; var rangeMin = $this.data('range_min'); var rangeMax = $this.data('range_max'); if (val <= 0) { return rangeMin; } if (val >= max) { return rangeMax; } // // To do this we first map 0 -- max relatively withing [minRange // and maxRange], that is between [0 and (maxRange-minRange)]. // var relativeMapping = (rangeMax - rangeMin) * val / max; // ... then we bring this to the actual value by adding rangeMin. return relativeMapping + rangeMin; } }; var methods = { 'teardown' : function () { var $this = this; // remove all data set with .data() $this.removeData(); // unbind the document as well $(document) .unbind('mousemove.nstSlider') .unbind('mouseup.nstSlider'); // unbind events bound to the container element $this.parent() .unbind('mousedown.nstSlider') .unbind('touchstart.nstSlider') .unbind('touchmove.nstSlider') .unbind('touchend.nstSlider'); // unbind events bound to the current element $this.unbind('keydown.nstSlider') .unbind('keyup.nstSlider'); return $this; }, 'init' : function(options) { var settings = $.extend({ 'animating_css_class' : 'nst-animating', // this is the distance from the value bar by which we should // grab the left or the right handler. 'touch_tolerance_value_bar_y': 30, // px 'touch_tolerance_value_bar_x': 15, // px // where is the left grip? 'left_grip_selector': '.nst-slider-grip-left', // where is the right grip? // undefined = (only left grip bar) 'right_grip_selector': undefined, // Specify highlight like this if you want to highlight a range // in the slider. // // 'highlight' : { // 'grip_class' : '.nsti-slider-hi', // 'panel_selector' : '.nst-slider-highlight-panel' // }, 'highlight' : undefined, // Lets you specify the increment rounding for the slider handles // for when the user moves them. // It can be a string, indicating a fixed increment, or an object // indicating the increment based on the value to be rounded. // // This can be specified in the following form: { // '1' : '100', // '10' : '1000', /* rounding = 10 for values in [100-999] */ // '50' : '10000', // } 'rounding': undefined, // if the bar is not wanted 'value_bar_selector': undefined, // Allow handles to cross each other while one of them is being // dragged. This option is ignored if just one handle is used. 'crossable_handles': true, 'value_changed_callback': function(/*cause, vmin, vmax*/) { return; }, 'user_mouseup_callback' : function(/*vmin, vmax, left_grip_moved*/) { return; }, 'user_drag_start_callback' : function () { return; } }, options); // // we need to unbind events attached to the document, // as if we replace html elements and re-initialize, we // don't want to double-bind events! // var $document = $(document); // make sure only one event is bound to the document $document.unbind('mouseup.nstSlider'); $document.unbind('mousemove.nstSlider'); $document.bind('mousemove.nstSlider', _methods.drag_move_func); $document.bind('mouseup.nstSlider', _methods.drag_end_func); return this.each(function() { // // $this is like: // //
// // It is supposed to be enclosed in a container // var $this = $(this), $container = $this.parent(); // enable: the user is able to move the grips of this slider. $this.data('enabled', true); // fix some values first var rangeMin = $this.data('range_min'), rangeMax = $this.data('range_max'), valueMin = $this.data('cur_min'), valueMax = $this.data('cur_max'); // assume 0 if valueMax is not specified if (typeof valueMax === 'undefined') { valueMax = valueMin; } if (rangeMin === '') { rangeMin = 0; } if (rangeMax === '') { rangeMax = 0; } if (valueMin === '') { valueMin = 0; } if (valueMax === '') { valueMax = 0; } $this.data('range_min', rangeMin); $this.data('range_max', rangeMax); $this.data('cur_min', valueMin); $this.data('cur_max', valueMax); // halt on error _methods.validate_params.call($this, settings); $this.data('settings', settings); // override rounding from markup if defined in configuration if (typeof settings.rounding !== 'undefined') { methods.set_rounding.call($this, settings.rounding); } else if (typeof $this.data('rounding') !== 'undefined') { methods.set_rounding.call($this, $this.data('rounding')); } else { methods.set_rounding.call($this, 1); } var left_grip = $this.find(settings.left_grip_selector)[0], $left_grip = $(left_grip), $right_grip = $($this.find(settings.right_grip_selector)[0]); // make sure left grip can be tabbed if the user hasn't // defined their own tab index if (typeof $left_grip.attr('tabindex') === 'undefined') { $left_grip.attr('tabindex', 0); } // no right handler means single handler var has_right_grip = false; if ($this.find(settings.right_grip_selector).length > 0) { has_right_grip = true; // make sure right grip can be tabbed if the user hasn't // defined their own tab index if (typeof $right_grip.attr('tabindex') === 'undefined') { $right_grip.attr('tabindex', 0); } } $this.data('has_right_grip', has_right_grip); // enable aria attributes update? if ($this.data('aria_enabled') === true) { // setup aria role attributes on each grip $left_grip .attr('role', 'slider') .attr('aria-disabled', 'false'); if (has_right_grip) { $right_grip .attr('role', 'slider') .attr('aria-disabled', 'false'); } } // // deal with keypresses here // $this.bind('keyup.nstSlider', function (e) { if ($this.data('enabled')) { switch (e.which) { case 37: // left case 38: // up case 39: // right case 40: // down if (_before_keydown_value === _before_keyup_value) { // we should search for the next value change... // ... in which direction? depends on whe var searchUntil = _methods.getSliderWidthPx.call($this), val, i, setAtPixel; if (_before_keydown_pixel - _before_keyup_pixel < 0) { // the grip was moved towards the right for (i=_before_keyup_pixel; i<=searchUntil; i++) { // if the value at pixel i is different than // the current value then we are good to go. // val = methods.round_value_according_to_rounding.call($this, _methods.getSliderValuesAtPositionPx.call($this, i, i)[1] ); if (val !== _before_keyup_value) { setAtPixel = i; break; } } } else { // the grip was moved towards the left for (i=_before_keyup_pixel; i>=0; i--) { // if the value at pixel i is different than // the current value then we are good to go. // val = methods.round_value_according_to_rounding.call($this, _methods.getSliderValuesAtPositionPx.call($this, i, i)[1] ); if (val !== _before_keyup_value) { setAtPixel = i; break; } } } // we need to set the slider at this position if (_is_left_grip) { _methods.validateAndMoveGripsToPx.call($this, setAtPixel, _methods.getRightGripPositionPx.call($this)); } else { _methods.validateAndMoveGripsToPx.call($this, _methods.getLeftGripPositionPx.call($this), setAtPixel); } // // call the mouseup callback when the key is up! // _methods.notify_mouse_up_implicit.call($this, _is_left_grip); } } // clear values _before_keydown_value = undefined; _before_keydown_pixel = undefined; _before_keyup_value = undefined; _before_keyup_pixel = undefined; } }); $this.bind('keydown.nstSlider', function (evt) { if ($this.data('enabled')) { var moveHandleBasedOnKeysFunc = function ($grip, e) { var nextLeft = _methods.getLeftGripPositionPx.call($this), nextRight = _methods.getRightGripPositionPx.call($this); if (typeof _before_keydown_value === 'undefined') { _before_keydown_pixel = _is_left_grip ? nextLeft : nextRight; _before_keydown_value = _is_left_grip ? methods.get_current_min_value.call($this) : methods.get_current_max_value.call($this); } switch (e.which) { case 37: // left case 40: // down if (_is_left_grip) { nextLeft--; } else { nextRight--; } e.preventDefault(); break; case 38: // up case 39: // right if (_is_left_grip) { nextLeft++; } else { nextRight++; } e.preventDefault(); break; } _before_keyup_pixel = _is_left_grip ? nextLeft : nextRight; // may write into cur_min, cur_max data... _methods.validateAndMoveGripsToPx.call($this, nextLeft, nextRight); _before_keyup_value = _is_left_grip ? methods.get_current_min_value.call($this) : methods.get_current_max_value.call($this); }; // default if (has_right_grip && $this.find(':focus').is($right_grip)) { _is_left_grip = false; moveHandleBasedOnKeysFunc.call($this, $right_grip, evt); } else { _is_left_grip = true; moveHandleBasedOnKeysFunc.call($this, $left_grip, evt); } } }); // determine size of grips var left_grip_width = _methods.getLeftGripWidth.call($this), right_grip_width = has_right_grip ? _methods.getRightGripWidth.call($this) : left_grip_width; $this.data('left_grip_width', left_grip_width); $this.data('right_grip_width', right_grip_width); $this.data('value_bar_selector', settings.value_bar_selector); // set behaviour of naked bar in case of one handle if (!has_right_grip) { var bStickToSides = valueMax === rangeMax || valueMax === rangeMin; _methods.setNakedBarDelta.call($this, bStickToSides ? "stickToSides" : "middle", left_grip_width ); } // this will set the range to the right extreme in such a case. if (rangeMin === rangeMax || valueMin === valueMax) { methods.set_range.call($this, rangeMin, rangeMax); } else { // set the initial position _methods.set_position_from_val.call($this, $this.data('cur_min'), $this.data('cur_max')); } _methods.notify_changed_implicit.call($this, 'init'); // handle mouse movement $this.data('beforestart_min', methods.get_current_min_value.call($this)); $this.data('beforestart_max', methods.get_current_max_value.call($this)); // pass a closure, so that 'this' will be the current slider bar, // not the container. $this.bind('mousedown.nstSlider', function (e) { _methods.drag_start_func.call($this, e, settings, $left_grip, $right_grip, false); }); $container.bind('touchstart.nstSlider', function (e) { _methods.drag_start_func_touch.call($this, e, settings, $left_grip, $right_grip, true); }); $container.bind('touchend.nstSlider', function (e) { _methods.drag_end_func_touch.call($this, e); }); $container.bind('touchmove.nstSlider', function (e) { _methods.drag_move_func_touch.call($this, e); }); // if the data-histogram attribute exists, then use this // histogram to set the step distribution var step_histogram = $this.data('histogram'); if (typeof step_histogram !== 'undefined') { methods.set_step_histogram.call($this, step_histogram); } }); // -- each slider }, 'get_range_min' : function () { var $this = this; return $this.data('range_min'); }, 'get_range_max' : function () { var $this = this; return $this.data('range_max'); }, 'get_current_min_value' : function () { var $this = $(this); var rangeMin = methods.get_range_min.call($this), rangeMax = methods.get_range_max.call($this); var currentMin = $this.data('cur_min'); var min; if (rangeMin >= currentMin) { min = rangeMin; } else { min = methods.round_value_according_to_rounding.call($this, currentMin); } if (_methods.haveLimits.call($this)) { if (min <= rangeMin) { return $this.data('lower-limit'); } else if (min >= rangeMax) { return $this.data('upper-limit'); } } else { if (min <= rangeMin) { return rangeMin; } else if (min >= rangeMax) { return rangeMax; } } return min; }, 'get_current_max_value' : function () { var $this = $(this); var rangeMin = methods.get_range_min.call($this), rangeMax = methods.get_range_max.call($this); var currentMax = $this.data('cur_max'); var max; if (rangeMax <= currentMax) { max = rangeMax; } else { max = methods.round_value_according_to_rounding.call($this, currentMax); } if (_methods.haveLimits.call($this)) { if (max >= rangeMax) { return $this.data('upper-limit'); } else if (max <= rangeMin) { return $this.data('lower-limit'); } } else { if (max >= rangeMax) { return rangeMax; } else if (max <= rangeMin) { return rangeMin; } } return max; }, 'is_handle_to_left_extreme' : function () { var $this = this; if (_methods.haveLimits.call($this)) { return $this.data('lower-limit') === methods.get_current_min_value.call($this); } else { return methods.get_range_min.call($this) === methods.get_current_min_value.call($this); } }, 'is_handle_to_right_extreme' : function () { var $this = this; if (_methods.haveLimits.call($this)) { return $this.data('upper-limit') === methods.get_current_max_value.call($this); } else { return methods.get_range_max.call($this) === methods.get_current_max_value.call($this); } }, // just call set_position on the current values 'refresh' : function () { var $this = this; // re-set the slider step if specified var lastStepHistogram = $this.data('last_step_histogram'); if (typeof lastStepHistogram !== 'undefined') { methods.set_step_histogram.call($this, lastStepHistogram); } // re-center given values _methods.set_position_from_val.call($this, methods.get_current_min_value.call($this), methods.get_current_max_value.call($this) ); // re-highlight the range var highlightRangeMin = $this.data('highlightedRangeMin'); if (typeof highlightRangeMin === 'number') { // a highlight range is present, we must update it var highlightRangeMax = $this.data('highlightedRangeMax'); methods.highlight_range.call($this, highlightRangeMin, highlightRangeMax); } _methods.notify_changed_implicit.call($this, 'refresh'); return $this; }, 'disable' : function () { var $this = this, settings = $this.data('settings'); $this.data('enabled', false) .find(settings.left_grip_selector) .attr('aria-disabled', 'true') .end() .find(settings.right_grip_selector) .attr('aria-disabled', 'true'); return $this; }, 'enable' : function() { var $this = this, settings = $this.data('settings'); $this.data('enabled', true) .find(settings.left_grip_selector) .attr('aria-disabled', 'false') .end() .find(settings.right_grip_selector) .attr('aria-disabled', 'false'); return $this; }, 'is_enabled' : function() { var $this = this; return $this.data('enabled'); }, /* * This one is the public method, called externally. * It sets the position and notifies in fact. */ 'set_position' : function(min, max) { var $this = this; var prev_min = $this.data('cur_min'), prev_max = $this.data('cur_max'); if (min > max) { _methods.set_position_from_val.call($this, max, min); } else { _methods.set_position_from_val.call($this, min, max); } // set the style of the grips according to the highlighted range _methods.refresh_grips_style.call($this); _methods.notify_changed_implicit.call($this, 'set_position', prev_min, prev_max); // this is for the future, therefore "before the next // interaction starts" $this.data('beforestart_min', min); $this.data('beforestart_max', max); }, /* * This tells the slider to increment its step non linearly over the * current range, based on the histogram on where results are. * * the input parameter 'histogram' identifies an empirical probability * density function (PDF). * */ 'set_step_histogram' : function (histogram) { var $this = this; $this.data('last_step_histogram', histogram); if (typeof histogram === 'undefined') { $.error('got an undefined histogram in set_step_histogram'); _methods.unset_step_histogram.call($this); } var sliderWidthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'), nbuckets = histogram.length; if (sliderWidthPx <= 0) { // that means the slider is not visible... return; } // // we need to transform this pdf into a cdf, and use it to obtain // two mappings: pixel to value and value to pixel. // // 1) normalize the pdf to sum to sliderWidthPx first var i; var histogram_sum = 0; for (i=0; i