1/*! Nestoria Slider - v1.0.13 - 2015-07-16 2* http://lokku.github.io/jquery-nstslider/ 3* Copyright (c) 2015 Lokku Ltd.; Licensed MIT */ 4(function($) { 5 /* 6 * These are used for user interaction. This plugin assumes the user can 7 * interact with one control at a time. For this reason it's safe to keep 8 * these global. 9 */ 10 var _$current_slider; 11 var _is_mousedown; 12 var _original_mousex; 13 14 // both for keyboard and mouse interaction 15 var _is_left_grip; 16 17 // for keyboard interaction only 18 var _before_keydown_value; 19 var _before_keydown_pixel; 20 var _before_keyup_value; 21 var _before_keyup_pixel; 22 23 // a fixed configuration for the single bar slider, used to decide where to 24 // place the naked bar. 25 var _naked_bar_deltas; // see populateNakedBarDeltas 26 27 var _methods = { 28 /* 29 * This method must be called once during initialization. 30 * It sets the behaviour of the naked bar in case of one handle. 31 */ 32 'setNakedBarDelta': function (position, handleWidth) { 33 if (position === "stickToSides") { 34 _naked_bar_deltas = { 35 toEndWidth: handleWidth, 36 toBeginLeft: 0, 37 toBeginWidth: handleWidth 38 }; 39 } 40 else if (position === "middle") { 41 // Position naked end of the bar at the middle value. 42 _naked_bar_deltas = { 43 toEndWidth: handleWidth/2, 44 toBeginLeft: handleWidth/2, 45 toBeginWidth: handleWidth/2 46 }; 47 } 48 else { 49 throw new Error('unknown position of setNakedBarDelta: ' + position); 50 } 51 }, 52 'getSliderValuesAtPositionPx' : function (leftPx, rightPx) { 53 var $this = this, 54 leftPxInValue, rightPxInValue, 55 pixel_to_value_mapping_func = $this.data('pixel_to_value_mapping'); 56 57 if (typeof pixel_to_value_mapping_func !== 'undefined') { 58 leftPxInValue = pixel_to_value_mapping_func(leftPx); 59 rightPxInValue = pixel_to_value_mapping_func(rightPx); 60 } 61 else { 62 var w = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'); 63 leftPxInValue = _methods.inverse_rangemap_0_to_n.call($this, leftPx, w); 64 rightPxInValue = _methods.inverse_rangemap_0_to_n.call($this, rightPx, w); 65 } 66 67 return [leftPxInValue, rightPxInValue]; 68 }, 69 /* 70 * Move slider grips to the specified position. This method is 71 * designed to run within the user interaction lifecycle. Only call 72 * this method if the user has interacted with the sliders 73 * actually... 74 * 75 * First the desired positions are validated. If values are ok, the 76 * move is performed, otherwise it's just ignored because weird 77 * values have been passed. 78 */ 79 'validateAndMoveGripsToPx' : function (nextLeftGripPositionPx, nextRightGripPositionPx) { 80 var $this = this; 81 82 var draggableAreaLengthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'); 83 84 // 85 // Validate & Move 86 // 87 if (nextRightGripPositionPx <= draggableAreaLengthPx && 88 nextLeftGripPositionPx >= 0 && 89 nextLeftGripPositionPx <= draggableAreaLengthPx && 90 (!$this.data('has_right_grip') || nextLeftGripPositionPx <= nextRightGripPositionPx) ) { 91 92 var prevMin = $this.data('cur_min'), 93 prevMax = $this.data('cur_max'); 94 95 // note: also stores new cur_min, cur_max 96 _methods.set_position_from_px.call($this, nextLeftGripPositionPx, nextRightGripPositionPx); 97 98 // set the style of the grips according to the highlighted range 99 _methods.refresh_grips_style.call($this); 100 101 _methods.notify_changed_implicit.call($this, 'drag_move', prevMin, prevMax); 102 } 103 104 return $this; 105 }, 106 /* 107 * Update aria attributes of the slider based on the current 108 * configuration of the slider. 109 */ 110 'updateAriaAttributes' : function () { 111 var $this = this, 112 settings = $this.data('settings'), 113 $leftGrip = $this.find(settings.left_grip_selector); 114 115 // 116 // double grips sliders is probably the most common case... 117 // ... also, the values to be set in the two cases are quite 118 // different. 119 // 120 if ($this.data('has_right_grip')) { 121 122 var $rightGrip = $this.find(settings.right_grip_selector); 123 124 // 125 // grips are mutually binding their max/min values when 2 grips 126 // are present. For example, we should imagine the left grip as 127 // being constrained between [ rangeMin, valueMax ] 128 // 129 $leftGrip 130 .attr('aria-valuemin', $this.data('range_min')) 131 .attr('aria-valuenow', methods.get_current_min_value.call($this)) 132 .attr('aria-valuemax', methods.get_current_max_value.call($this)); 133 134 $rightGrip 135 .attr('aria-valuemin', methods.get_current_min_value.call($this)) 136 .attr('aria-valuenow', methods.get_current_max_value.call($this)) 137 .attr('aria-valuemax', $this.data('range_max')); 138 } 139 else { 140 $leftGrip 141 .attr('aria-valuemin', $this.data('range_min')) 142 .attr('aria-valuenow', methods.get_current_min_value.call($this)) 143 .attr('aria-valuemax', $this.data('range_max')); 144 } 145 146 return $this; 147 }, 148 /* 149 * Return the width in pixels of the slider bar, i.e., the maximum 150 * number of pixels the user can slide the slider over. This function 151 * should always be used internally to obtain the width of the 152 * slider in pixels! 153 */ 154 'getSliderWidthPx' : function () { 155 var $this = this; 156 157 // 158 // .width() can actually return a floating point number! see 159 // jquery docs! 160 // 161 return Math.round($this.width()); 162 }, 163 /* 164 * Return the position of a given grip in pixel in integer format. 165 * Use this method internally if you are literally going to get the 166 * left CSS property from the provided grip. 167 * 168 * This method assumes a certain grip exists and will have the left 169 * property. 170 * 171 * This is generally safe for the left grip, because it is basically 172 * guaranteed to exist. But for the right grip you should be really 173 * using getRightGripPositionPx instead. 174 * 175 */ 176 'getGripPositionPx' : function ($grip) { 177 return parseInt($grip.css('left').replace('px',''), 10); 178 }, 179 /* 180 * Just the same as getGripPositionPx, but there is no need to provide 181 * the $slider. 182 */ 183 'getLeftGripPositionPx' : function () { 184 var $this = this, 185 settings = $this.data('settings'), 186 $leftGrip = $this.find(settings.left_grip_selector); 187 188 return _methods.getGripPositionPx.call($this, $leftGrip); 189 }, 190 /* 191 * Return the position of the right Grip if it exists or return the 192 * current position if not. Even if the right grip doesn't exist, its 193 * position should be defined, as it determines the position of the 194 * bar. 195 */ 196 'getRightGripPositionPx' : function () { 197 var $this = this, 198 settings = $this.data('settings'); 199 200 if ($this.data('has_right_grip')) { 201 return _methods.getGripPositionPx.call($this, 202 $this.find(settings.right_grip_selector) 203 ); 204 } 205 206 // default 207 var sliderWidthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'); 208 return _methods.rangemap_0_to_n.call($this, $this.data('cur_max'), sliderWidthPx); 209 }, 210 /* 211 * Return the width of the left grip. Like getSliderWidthPx, this 212 * method deals with .width() returning a floating point number. All 213 * the code in this plugin assumes an integer here! 214 */ 215 'getLeftGripWidth' : function () { 216 var $this = this, 217 settings = $this.data('settings'), 218 $leftGrip = $this.find(settings.left_grip_selector); 219 220 return Math.round($leftGrip.outerWidth()); 221 }, 222 /* 223 * Return the width of the right grip. The calling method should 224 * check that the right grip actually exists. This method assumes it 225 * does. 226 */ 227 'getRightGripWidth' : function () { 228 var $this = this, 229 settings = $this.data('settings'), 230 $rightGrip = $this.find(settings.right_grip_selector); 231 232 return Math.round($rightGrip.outerWidth()); 233 }, 234 'binarySearchValueToPxCompareFunc' : function (s, a, i) { 235 // Must return: 236 // 237 // s: element to search for 238 // a: array we are looking in 239 // i: position of the element we are looking for 240 // 241 // -1 (s < a[i]) 242 // 0 found (= a[i]) 243 // 1 (s > a[i]) 244 if (s === a[i]) { return 0; } // element found exactly 245 if (s < a[i] && i === 0) { return 0; } // left extreme case e.g., a = [ 3, ... ], s = 1 246 if (a[i-1] <= s && s < a[i]) { return 0; } // s is between two elements, always return the rightmost 247 if (s > a[i]) { return 1; } 248 if (s <= a[i-1]) { return -1; } 249 $.error('cannot compare s: ' + s + ' with a[' + i + ']. a is: ' + a.join(',')); 250 }, 251 /* 252 * Perform binary search to find searchElement into a generic array. 253 * It uses a customized compareFunc to perform the comparison between 254 * two elements of the array and a getElement function to pick the 255 * element from the array (e.g., in case we want to pick a field of an 256 * array of objects) 257 */ 258 'binarySearch' : function(array, searchElement, getElementFunc, compareFunc) { 259 var minIndex = 0; 260 var maxIndex = array.length - 1; 261 var currentIndex; 262 var currentElement; 263 264 while (minIndex <= maxIndex) { 265 currentIndex = (minIndex + maxIndex) / 2 | 0; 266 currentElement = getElementFunc(array, currentIndex); 267 268 // lt = -1 (searchElement < currentElement) 269 // eq = 0 270 // gt = 1 (searchElement > currentElement) 271 var lt_eq_gt = compareFunc(searchElement, array, currentIndex); 272 273 if (lt_eq_gt > 0) { 274 minIndex = currentIndex + 1; 275 } 276 else if (lt_eq_gt < 0) { 277 maxIndex = currentIndex - 1; 278 } 279 else { 280 return currentIndex; 281 } 282 } 283 284 return -1; 285 }, 286 /* 287 * Returns true if this slider has limit, false otherwise. There can be 288 * an upper limit and a lower limit for the sliders. 289 * The lower/upper limits are values that are out of the slider range, 290 * but that can be selected by the user when he moves a slider all the 291 * way down the minimum and up to the maximum value. 292 */ 293 'haveLimits' : function () { 294 var $this = this, 295 lowerLimit = $this.data('lower-limit'), 296 upperLimit = $this.data('upper-limit'), 297 haveLimits = false; 298 299 if (typeof lowerLimit !== 'undefined' && 300 typeof upperLimit !== 'undefined') { 301 302 haveLimits = true; 303 } 304 305 return haveLimits; 306 }, 307 /* 308 * This method is called whenever the style of the grips needs to get 309 * updated. 310 */ 311 'refresh_grips_style' : function () { 312 var $this = this, 313 settings = $this.data('settings'); 314 315 // Skip refreshing grips style if no hihglight is specified in 316 // construction 317 if (typeof settings.highlight === 'undefined') { 318 return; 319 } 320 321 var highlightedRangeMin = $this.data('highlightedRangeMin'); 322 323 if (typeof highlightedRangeMin === 'undefined') { 324 return; 325 } 326 327 var $leftGrip = $this.find(settings.left_grip_selector), 328 $rightGrip = $this.find(settings.right_grip_selector), 329 highlightedRangeMax = $this.data('highlightedRangeMax'), 330 curMin = $this.data('cur_min'), 331 curMax = $this.data('cur_max'), 332 highlightGripClass = settings.highlight.grip_class; 333 334 // curmin is within the highlighted range 335 if (curMin < highlightedRangeMin || curMin > highlightedRangeMax) { 336 // de-highlight grip 337 $leftGrip.removeClass(highlightGripClass); 338 } 339 else { 340 // highlight grip 341 $leftGrip.addClass(highlightGripClass); 342 } 343 344 // time to highlight right grip 345 if (curMax < highlightedRangeMin || curMax > highlightedRangeMax) { 346 // de-highlight grip 347 $rightGrip.removeClass(highlightGripClass); 348 } 349 else { 350 // highlight grip 351 $rightGrip.addClass(highlightGripClass); 352 } 353 }, 354 /* 355 * Set left and right handle at the right position on the screen (pixels) 356 * given the desired position in currency. 357 * 358 * e.g., _methods.set_position_from_val.call($this, 10000, 100000); 359 * 360 * may set the left handle at 100px and the right handle at 361 * 200px; 362 * 363 */ 364 'set_position_from_val' : function (cur_min, cur_max) { 365 var $this = this; 366 // 367 // We need to understand how much pixels cur_min and cur_max 368 // correspond. 369 // 370 var range_min = $this.data('range_min'), 371 range_max = $this.data('range_max'); 372 373 // 374 // (safety) constrain the cur_min or the cur_max value between the 375 // max/min ranges allowed for this slider. 376 // 377 if (cur_min < range_min) { cur_min = range_min; } 378 if (cur_min > range_max) { cur_min = range_max; } 379 380 if ($this.data('has_right_grip')) { 381 if (cur_max > range_max) { cur_max = range_max; } 382 if (cur_max < range_min) { cur_max = range_min; } 383 } 384 else { 385 cur_max = $this.data('cur_max'); 386 } 387 388 var leftPx = methods.value_to_px.call($this, cur_min), 389 rightPx = methods.value_to_px.call($this, cur_max); 390 391 _methods.set_handles_at_px.call($this, leftPx, rightPx); 392 393 // save this position 394 $this.data('cur_min', cur_min); 395 396 if ($this.da
396ta('has_right_grip')) { 397 $this.data('cur_max', cur_max); 398 } 399 400 return $this; 401 }, 402 /* 403 * Set the position of the handles at the specified pixel points (taking 404 * the whole slider width as a maximum point). 405 */ 406 'set_position_from_px' : function (leftPx, rightPx) { 407 var $this = this; 408 409 // 410 // we need to find a value from the given value in pixels 411 // 412 413 // now set the position as requested... 414 _methods.set_handles_at_px.call($this, leftPx, rightPx); 415 416 var valueLeftRight = _methods.getSliderValuesAtPositionPx.call($this, leftPx, rightPx), 417 leftPxInValue = valueLeftRight[0], 418 rightPxInValue = valueLeftRight[1]; 419 420 // ... and save the one we've found. 421 $this.data('cur_min', leftPxInValue); 422 423 if ($this.data('has_right_grip')) { 424 $this.data('cur_max', rightPxInValue); 425 } 426 427 return $this; 428 }, 429 /* 430 * Updates the CSS of grips and bar so that the left grip appears at 431 * leftPx and the right grip appears at rightPx. Note: leftPx can be > 432 * rightPx. 433 */ 434 'set_handles_at_px' : function (leftPx, rightPx) { 435 var $this = this; 436 var settings = $this.data('settings'); 437 438 var left_grip_selector = settings.left_grip_selector, 439 right_grip_selector = settings.right_grip_selector, 440 value_bar_selector = settings.value_bar_selector; 441 442 var handleWidth = $this.data('left_grip_width'); 443 444 // The left grip 445 $this.find(left_grip_selector).css('left', leftPx + 'px'); 446 447 // The right grip 448 $this.find(right_grip_selector).css('left', rightPx + 'px'); 449 450 // The value bar 451 if ($this.data('has_right_grip')) { 452 // If both the grips are there, the value bar must stick to 453 // beginning and the end of the grips. 454 $this.find(value_bar_selector) 455 .css('left', leftPx + 'px') 456 .css('width', (rightPx - leftPx + handleWidth) + 'px'); 457 } 458 else { 459 if (!_naked_bar_deltas) { 460 _methods.populateNakedBarDeltas.call($this, leftPx, rightPx, handleWidth); 461 } 462 463 if (rightPx > leftPx) { 464 // The naked end of the bar is on the right of the grip 465 $this.find(value_bar_selector) 466 .css('left', leftPx + 'px') 467 .css('width', rightPx - leftPx + _naked_bar_deltas.toEndWidth + 'px'); 468 } 469 else { 470 // The naked end of the bar is on the left of the grip 471 // NOTE: leftPx and rightPx are to be read swapped here. 472 $this.find(value_bar_selector) 473 .css('left', rightPx + _naked_bar_deltas.toBeginLeft + 'px') 474 .css('width', (leftPx - rightPx + _naked_bar_deltas.toBeginWidth) + 'px'); 475 } 476 } 477 478 return $this; 479 480 }, 481 'drag_start_func_touch' : function (e, settings, $left_grip, $right_grip, is_touch) { 482 var $this = this, 483 original_event = e.originalEvent, 484 touch = original_event.touches[0]; 485 486 // for touch devices we need to make sure we allow the user to scroll 487 // if the click was too far from the slider. 488 var curY = touch.pageY, 489 curX = touch.pageX; 490 491 // is the user allowed to grab if he/she tapped too far from the 492 // slider? 493 var ydelta = Math.abs($this.offset().top - curY), 494 slider_left = $this.offset().left, 495 xldelta = slider_left - curX, 496 xrdelta = curX - (slider_left + $this.width()); 497 498 if (ydelta > settings.touch_tolerance_value_bar_y || 499 xldelta > settings.touch_tolerance_value_bar_x || 500 xrdelta > settings.touch_tolerance_value_bar_x ) { 501 502 return; 503 } 504 505 original_event.preventDefault(); 506 _original_mousex = touch.pageX; 507 508 // true : is touch event 509 _methods.drag_start_func.call($this, touch, settings, $left_grip, 510 $right_grip, is_touch); 511 }, 512 'drag_start_func' : function (e, settings, $leftGrip, $rightGrip, 513 is_touch) { 514 515 var $this = this; 516 517 $this.find(settings.left_grip_selector + 518 ',' + settings.value_bar_selector + 519 ',' + settings.right_grip_selector).removeClass( 520 521 settings.animating_css_class 522 ); 523 524 if (!methods.is_enabled.call($this)) { return; } 525 526 // 527 // if the user used the finger, he/she is allowed to touch anywhere. 528 // but if the mouse is used, we want to enable the logic only for 529 // left grip, right grip, bar/panel elements. 530 // 531 var $target = $(e.target); 532 533 // ... if the highlight range was enabled we should check wether 534 // the user has tapped or clicked the highlight panel... 535 var targetIsPanelSelector = false; 536 if (typeof settings.highlight === 'object') { 537 targetIsPanelSelector = $target.is(settings.highlight.panel_selector); 538 } 539 540 if (is_touch === false && 541 !$target.is(settings.left_grip_selector) && 542 !$target.is(settings.right_grip_selector) && 543 !$target.is(settings.value_bar_selector) && 544 !targetIsPanelSelector && 545 !$target.is($this) ) { 546 547 return; 548 } 549 550 // - - - - 551 // the following logic finds the nearest slider grip and starts 552 // dragging it. 553 // - - - - 554 555 _$current_slider = $this; 556 557 var leftGripPositionPx = _methods.getGripPositionPx.call($this, $leftGrip), 558 sliderWidthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'), 559 lleft = $leftGrip.offset().left, 560 rleft, // don't compute this yet (maybe not needed if 1 grip) 561 curX, 562 ldist, 563 rdist, 564 ldelta, 565 rdelta; 566 567 var rightGripPositionPx = _methods.getRightGripPositionPx.call($this); 568 569 // 570 // We need to do as if the click happened a bit more on the left. 571 // That's because we will be setting the left CSS property at the 572 // point where the click happened, meaning the slider grip will be 573 // spanning to the right. 574 // 575 curX = Math.round(e.pageX) - ($this.data('left_grip_width') / 2); 576 577 // calculate deltas from left and right grip 578 ldist = Math.abs(lleft - curX); 579 ldelta = curX - lleft; 580 581 if ($this.data('has_right_grip')) { 582 rleft = $rightGrip.offset().left; 583 rdist = Math.abs(rleft - curX); 584 rdelta = curX - rleft; 585 } 586 else { 587 // no right grip... we make the right slider 588 // unreachable! 589 rdist = ldist * 2; 590 rdelta = ldelta * 2; 591 } 592 593 // notify the beginning of a dragging... 594 settings.user_drag_start_callback.call($this, e); 595 596 if (ldist === rdist) { 597 598 if (curX < lleft) { 599 // move the left grip 600 leftGripPositionPx += ldelta; 601 _is_left_grip = true; 602 } 603 else { 604 // move the right grip 605 rightGripPositionPx += rdelta; 606 _is_left_grip = false; 607 } 608 } 609 else if (ldist < rdist) { 610 611 // move the left grip 612 leftGripPositionPx += ldelta; 613 _is_left_grip = true; 614 } 615 else { 616 // move the right grip 617 rightGripPositionPx += rdelta; 618 _is_left_grip = false; 619 } 620 621 // 622 // Limit the right grip to the maximum allowed - as the user can 623 // actually click beyond it! 624 // 625 // ............... 626 // ^-- maximum clickable 627 // ^--- maximum allowed (i.e., sliderWidth - gripWidth) 628 // 629 // if user clicks at sliderWidth, we will be setting CSS left of 630 // right handle having: 631 // 632 // ...............R <-- out of bound :-( 633 // ^-- maximum clickable 634 // ^--- maximum allowed (i.e., sliderWidth - gripWidth) 635 // 636 // but we want: 637 // 638 // ..............R <-- within bound :-) 639 // ^-- maximum clickable 640 // ^--- maximum allowed (i.e., sliderWidth - gripWidth) 641 // 642 // Hence we limit. 643 // 644 645 if ($this.data('has_right_grip')) { 646 // here we check the right handle only, because it should 647 // always be the one that gets moved if the user clicks towards 648 // the right extremity! 649 if (rightGripPositionPx > sliderWidthPx) { 650 rightGripPositionPx = sliderWidthPx; 651 } 652 } 653 else { 654 // in case we have one handle only, we will be moving the left 655 // handle instead of the right one... hence we need to perform 656 // this check on the left handle as well! 657 if (leftGripPositionPx > sliderWidthPx) { 658 leftGripPositionPx = sliderWidthPx; 659 } 660 } 661 662 // this can happen because the user can click on the left handle! 663 // (which is out of the left boundary) 664 if (leftGripPositionPx < 0) { 665 leftGripPositionPx = 0; 666 } 667 668 _is_mousedown = true; 669 670 var prev_min = $this.data('cur_min'), 671 prev_max = $this.data('cur_max'); 672 673 _methods.set_position_from_px.call($this, leftGripPositionPx, rightGripPositionPx); 674 675 // set the style of the grips according to the highlighted range 676 _methods.refresh_grips_style.call($this); 677 678 _methods.notify_changed_implicit.call($this, 'drag_start', prev_min, prev_max); 679 680 // no need to call preventDefault on touch events, as we called 681 // preventDefault on the original event already 682 if (Object.prototype.toString.apply(e) !== "[object Touch]") { 683 e.preventDefault(); 684 } 685 }, 686 'drag_move_func_touch' : function (e) { 687 if (_is_mousedown === true) { 688 var original_event = e.originalEvent; 689 original_event.preventDefault(); 690 var touch = original_event.touches[0]; 691 _methods.drag_move_func(touch); 692 } 693 }, 694 'drag_move_func' : function (e) { 695 if (_is_mousedown) { 696 // our slider element. 697 var $this = _$current_slider, 698 settings = $this.data('settings'), 699 sliderWidthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'), 700 leftGripPositionPx = _methods.getLeftGripPositionPx.call($this); 701 702 var rightGripPositionPx = _methods.getRightGripPositionPx.call($this); 703 704 // 705 // Here we are going to set the position in pixels based on
706 // where the user has moved the mouse cursor. We obtain the 707 // position of the mouse cursors via e.pageX, which returns the 708 // absolute position of the mouse on the screen. 709 // 710 var absoluteMousePosition = Math.round(e.pageX); 711 712 // 713 // Compute the delta (in px) for the slider movement. It is the 714 // difference between the new position of the cursor and the 715 // old position of the cursor. 716 // 717 // Based on the delta we decide how to move the dragged handle. 718 // 719 // 0 : no movement 720 // -delta: move left 721 // +delta: move right 722 // 723 var delta = absoluteMousePosition - _original_mousex; 724 725 // 726 // User cannot drag the handles outside the slider bar area. 727 // 728 729 // 1) calculate the area within which the movement is 730 // considered to be valid. 731 var half_a_grip_width = $this.data('left_grip_width') / 2, 732 drag_area_start = $this.offset().left + $this.data('left_grip_width') - half_a_grip_width, 733 drag_area_end = drag_area_start + sliderWidthPx; 734 735 if (settings.crossable_handles === false && $this.data('has_right_grip')) { 736 // if handles are not crossable, we should define the left 737 // and the right boundary of the movement. 738 if (_is_left_grip) { 739 drag_area_end = drag_area_start + rightGripPositionPx; 740 } 741 else { 742 drag_area_start = drag_area_start + leftGripPositionPx; 743 } 744 } 745 746 // 2) by default we accept to move the slider according to both 747 // the deltas (i.e., left or right) 748 var ignore_positive_delta = 0, 749 ignore_negative_delta = 0; 750 751 // 3) but if the user is moving the mouse beyond the draggable 752 // area, we should only accept a movement in one direction. 753 if (absoluteMousePosition < drag_area_start) { 754 ignore_positive_delta = 1; 755 ignore_negative_delta = 0; 756 } 757 if (absoluteMousePosition > drag_area_end) { 758 ignore_negative_delta = 1; 759 ignore_positive_delta = 0; 760 } 761 762 // 763 // Here we decide whether to invert the grip being moved. 764 // 765 if (settings.crossable_handles === true && 766 $this.data('has_right_grip')) { 767 768 if (_is_left_grip) { 769 770 // ... if we are using the left grip 771 if (rightGripPositionPx <= sliderWidthPx) { 772 773 // the inversion logic should only be active when the 774 // slider is not at the extremity 775 if (leftGripPositionPx + delta > rightGripPositionPx) { 776 777 _is_left_grip = false; 778 779 // TWEAK: keep the position of the left handle fixed 780 // at the one of the right handle as the user may 781 // have moved the mouse too fast, thus giving 782 // leftGripPositionPx > rightGripPositionPx. 783 // 784 // Basically here we avoid: 785 // 786 // Initial State: 787 // 788 // ------L-R------ (leftGripPositionPx < rightGripPositionPx) 789 // 790 // Fast Mouse Move: 791 // 792 // --------R--L--- (leftGripPositionPx + delta) 793 // --------R-L---- (leftGripPositionPx [ still > rightGripPositionPx! ]) 794 // 795 // _is_left_grip becomes false (this code) 796 // 797 leftGripPositionPx = rightGripPositionPx; 798 } 799 } 800 } 801 else { 802 // ... converse logic 803 if (leftGripPositionPx >= 0) { 804 if (rightGripPositionPx + delta < leftGripPositionPx) { 805 806 // current_max = current_min; 807 _is_left_grip = true; 808 809 rightGripPositionPx = leftGripPositionPx; 810 } 811 } 812 } 813 } 814 815 // 816 // Decide the position of the new handles. 817 // 818 var nextLeftGripPositionPx = leftGripPositionPx, 819 nextRightGripPositionPx = rightGripPositionPx; 820 821 if ((delta > 0 && !ignore_positive_delta) || 822 (delta < 0 && !ignore_negative_delta)) { 823 824 if (_is_left_grip) { 825 nextLeftGripPositionPx += delta; 826 } 827 else { 828 nextRightGripPositionPx += delta; 829 } 830 } 831 832 _methods.validateAndMoveGripsToPx.call($this, nextLeftGripPositionPx, nextRightGripPositionPx); 833 834 // prepare for next movement 835 _original_mousex = absoluteMousePosition; 836 837 838 // no need to call preventDefault on touch events, as we called 839 // preventDefault on the original event already 840 if (Object.prototype.toString.apply(e) !== "[object Touch]") { 841 e.preventDefault(); 842 } 843 } 844 }, 845 'drag_end_func_touch' : function (e) { 846 var original_event = e.originalEvent; 847 original_event.preventDefault(); 848 var touch = original_event.touches[0]; 849 _methods.drag_end_func(touch); 850 }, 851 'drag_end_func' : function (/* e */) { 852 var $this = _$current_slider; 853 if (typeof $this !== 'undefined') { 854 _is_mousedown = false; 855 _original_mousex = undefined; 856 857 _methods.notify_mouse_up_implicit.call($this, _is_left_grip); 858 859 // require another click on a handler before going into here again! 860 _$current_slider = undefined; 861 862 // put back the class once user finished dragging 863 var settings = $this.data('settings'); 864 $this.find(settings.left_grip_selector + 865 ',' + settings.value_bar_selector + 866 ',' + settings.right_grip_selector).addClass( 867 868 settings.animating_css_class 869 ); 870 } 871 }, 872 'get_rounding_for_value' : function (v) { 873 var $this = this; 874 var rounding = $this.data('rounding'); 875 var rounding_ranges = $this.data('rounding_ranges'); 876 877 if (typeof rounding_ranges === 'object') { 878 879 // then it means the rounding is not fixed, we should find the 880 // value in the roundings_array. 881 var roundingIdx = _methods.binarySearch.call($this, roun
881ding_ranges, v, 882 // pick an element from the array 883 function (array, index) { return array[index].range; }, 884 885 // compare search element with current element 886 // < 0 search < current 887 // 0 equals 888 // > 0 search > current 889 function (search, array, currentIdx) { 890 891 // first check if this is our element 892 893 // this is our element if the search value is: 894 if (search < array[currentIdx].range) { 895 896 // we can have a match or search in the left half 897 if (currentIdx > 0) { 898 if (search >= array[currentIdx - 1].range) { 899 return 0; 900 } 901 else { 902 // go left 903 return -1; 904 } 905 } 906 else { 907 return 0; 908 } 909 } 910 else { 911 // we must search in the next half 912 return 1; 913 } 914 } 915 ); 916 917 rounding = 1; 918 if (roundingIdx > -1) { 919 rounding = parseInt(rounding_ranges[roundingIdx].value, 10); 920 } 921 else { 922 var lastIdx = rounding_ranges.length - 1; 923 if (v >= rounding_ranges[lastIdx].range) { 924 rounding = rounding_ranges[lastIdx].value; 925 } 926 } 927 } 928 return rounding; 929 }, 930 /* 931 * Calls the user mouseup callback with the right parameters. Relies on 932 * $data('beforestart_min/max') in addition to the isLeftGrip parameter. 933 * 934 * NOTE: saves the new beforestart_min and begforestart_max as well. 935 */ 936 'notify_mouse_up_implicit' : function(isLeftGrip) { 937 var $this = this, 938 current_min_value = methods.get_current_min_value.call($this), 939 current_max_value = methods.get_current_max_value.call($this), 940 didValuesChange = false; 941 942 // check if we changed. 943 if (($this.data('beforestart_min') !== current_min_value) || 944 ($this.data('beforestart_max') !== current_max_value) 945 ) { 946 // values have changed! 947 didValuesChange = true; 948 949 // save the new values 950 $this.data('beforestart_min', current_min_value); 951 $this.data('beforestart_max', current_max_value); 952 } 953 954 955 var settings = $this.data('settings'); 956 957 settings.user_mouseup_callback.call($this, 958 methods.get_current_min_value.call($this), 959 methods.get_current_max_value.call($this), 960 isLeftGrip, 961 didValuesChange 962 ); 963 964 return $this; 965 }, 966 /* 967 * NOTE: this method may take the previous min/max value as input. 968 * if no arguments are provided the method blindly notifies. 969 */ 970 'notify_changed_implicit' : function (cause, prevMin, prevMax) { 971 var $this = this; 972 973 var force = false; 974 if (cause === 'init' || cause === 'refresh') { 975 force = true; 976 } 977 978 var curMin = methods.get_current_min_value.call($this), 979 curMax = methods.get_current_max_value.call($this); 980 981 if (!force) { 982 prevMin = methods.round_value_according_to_rounding.call($this, prevMin); 983 prevMax = methods.round_value_according_to_rounding.call($this, prevMax); 984 } 985 986 if (force || curMin !== prevMin || curMax !== prevMax) { 987 988 _methods.notify_changed_explicit.call($this, cause, prevMin, prevMax, curMin, curMax); 989 990 force = 1; 991 } 992 993 return force; 994 }, 995 'notify_changed_explicit' : function (cause, prevMin, prevMax, curMin, curMax) { 996 var $this = this, 997 settings = $this.data('settings'); 998 999 // maybe update aria attributes for accessibility 1000 if ($this.data('aria_enabled')) { 1001 _methods.updateAriaAttributes.call($this); 1002 } 1003 1004 settings.value_changed_callback.call($this, cause, curMin, curMax, prevMin, prevMax); 1005 1006 return $this; 1007 }, 1008 'validate_params' : function (settings) { 1009 var $this = this; 1010 var min_value = $this.data('range_min'), 1011 max_value = $this.data('range_max'), 1012 cur_min = $this.data('cur_min'), 1013 lower_limit = $this.data('lower-limit'), 1014 upper_limit = $this.data('upper-limit'); 1015 1016 var have_limits = _methods.haveLimits.call($this); 1017 1018 if (typeof min_value === 'undefined') { 1019 $.error("the data-range_min attribute was not defined"); 1020 } 1021 if (typeof max_value === 'undefined') { 1022 $.error("the data-range_max attribute was not defined"); 1023 } 1024 if (typeof cur_min === 'undefined') { 1025 $.error("the data-cur_min attribute must be defined"); 1026 } 1027 if (min_value > max_value) { 1028 $.error("Invalid input parameter. must be min < max"); 1029 } 1030 1031 if (have_limits && lower_limit > upper_limit) { 1032 $.error('Invalid data-lower-limit or data-upper-limit'); 1033 } 1034 if ($this.find(settings.left_grip_selector).length === 0) { 1035 $.error("Cannot find element pointed by left_grip_selector: " + settings.left_grip_selector); 1036 } 1037 /* 1038 * NOTE: only validate right grip selector if it is not 1039 * undefined otherwise just assume that if it isn't 1040 * found isn't there. This is because we initialize the 1041 * slider at once and let the markup decide if the 1042 * slider is there or not. 1043 */ 1044 if (typeof settings.right_grip_selector !== 'undefined') { 1045 if ($this.find(settings.right_grip_selector).length === 0) { 1046 $.error("Cannot find element pointed by right_grip_selector: " + settings.right_grip_selector); 1047 } 1048 } 1049 1050 // same thing for the value bar selector 1051 if (typeof settings.value_bar_selector !== 'undefined') { 1052 if ($this.find(settings.value_bar_selector).length === 0) { 1053 $.error("Cannot find element pointed by value_bar_selector" + settings.value_bar_selector); 1054 } 1055 } 1056 }, 1057 /* 1058 * Maps a value between [minRange -- maxRange] into [0 -- n]. 1059 * The target range will be an integer number. 1060 */ 1061 'rangemap_0_to_n' : function (val, n) { 1062 var $this = this; 1063 var rangeMin = $this.data('range_min'); 1064 var rangeMax = $this.data('range_max'); 1065 1066 if (val <= rangeMin) { return 0; } 1067 if (val >= rangeMax) { return n; } 1068 1069 return Math.floor((n * val - n * rangeMin) / (rangeMax - rangeMin)); 1070 }, 1071 /* 1072 * Maps a value between [0 -- max] back into [minRange -- maxRange]. 1073 * The target range can be a floating point number. 1074 */ 1075 'inverse_rangemap_0_to_n' : function (val, max) { 1076 var $this = this; 1077 var rangeMin = $this.data('range_min'); 1078 var rangeMax = $this.data('range_max'); 1079 1080 if (val <= 0) { return rangeMin; } 1081 if (val >= max) { return rangeMax; } 1082 1083 // 1084 // To do this we first map 0 -- max relatively withing [minRange 1085 // and maxRange], that is between [0 and (maxRange-minRange)]. 1086 // 1087 var relativeMapping = (rangeMax - rangeMin) * val / max; 1088 1089 // ... then we bring this to the actual value by adding rangeMin. 1090 return relativeMapping + rangeMin; 1091 } 1092 1093 }; 1094 var methods = { 1095 1096 'teardown' : function () { 1097 var $this = this; 1098 1099 // remove all data set with .data() 1100 $this.removeData(); 1101 1102 // unbind the document as well 1103 $(document) 1104 .unbind('mousemove.nstSlider') 1105 .unbind('mouseup.nstSlider'); 1106 1107 // unbind events bound to the container element 1108 $this.parent() 1109 .unbind('mousedown.nstSlider') 1110 .unbind('touchstart.nstSlider')
1111 .unbind('touchmove.nstSlider') 1112 .unbind('touchend.nstSlider'); 1113 1114 // unbind events bound to the current element 1115 $this.unbind('keydown.nstSlider') 1116 .unbind('keyup.nstSlider'); 1117 1118 return $this; 1119 }, 1120 'init' : function(options) { 1121 var settings = $.extend({ 1122 'animating_css_class' : 'nst-animating', 1123 // this is the distance from the value bar by which we should 1124 // grab the left or the right handler. 1125 'touch_tolerance_value_bar_y': 30, // px 1126 'touch_tolerance_value_bar_x': 15, // px 1127 // where is the left grip? 1128 'left_grip_selector': '.nst-slider-grip-left', 1129 // where is the right grip? 1130 // undefined = (only left grip bar) 1131 'right_grip_selector': undefined, 1132 1133 // Specify highlight like this if you want to highlight a range 1134 // in the slider. 1135 // 1136 // 'highlight' : { 1137 // 'grip_class' : '.nsti-slider-hi', 1138 // 'panel_selector' : '.nst-slider-highlight-panel' 1139 // }, 1140 'highlight' : undefined, 1141 1142 // Lets you specify the increment rounding for the slider handles 1143 // for when the user moves them. 1144 // It can be a string, indicating a fixed increment, or an object 1145 // indicating the increment based on the value to be rounded. 1146 // 1147 // This can be specified in the following form: { 1148 // '1' : '100', 1149 // '10' : '1000', /* rounding = 10 for values in [100-999] */ 1150 // '50' : '10000', 1151 // } 1152 'rounding': undefined, 1153 1154 // if the bar is not wanted 1155 'value_bar_selector': undefined, 1156 1157 // Allow handles to cross each other while one of them is being 1158 // dragged. This option is ignored if just one handle is used. 1159 'crossable_handles': true, 1160 1161 'value_changed_callback': function(/*cause, vmin, vmax*/) { return; }, 1162 'user_mouseup_callback' : function(/*vmin, vmax, left_grip_moved*/) { return; }, 1163 'user_drag_start_callback' : function () { return; } 1164 }, options); 1165 1166 // 1167 // we need to unbind events attached to the document, 1168 // as if we replace html elements and re-initialize, we 1169 // don't want to double-bind events! 1170 // 1171 var $document = $(document); 1172 1173 // make sure only one event is bound to the document 1174 $document.unbind('mouseup.nstSlider'); 1175 $document.unbind('mousemove.nstSlider'); 1176 1177 $document.bind('mousemove.nstSlider', _methods.drag_move_func); 1178 $document.bind('mouseup.nstSlider', _methods.drag_end_func); 1179 1180 return this.each(function() { 1181 // 1182 // $this is like: 1183 // 1184 // <div class="outer-slider" data-... data-...> 1185 // <div class="bar"></div> 1186 // <div class="leftGrip"></div> 1187 // <div class="rightGrip"></div> 1188 // </div> 1189 // 1190 // It is supposed to be enclosed in a container 1191 // 1192 var $this = $(this), 1193 $container = $this.parent(); 1194 1195 // enable: the user is able to move the grips of this slider. 1196 $this.data('enabled', true); 1197 1198 // fix some values first 1199 var rangeMin = $this.data('range_min'), 1200 rangeMax = $this.data('range_max'), 1201 valueMin = $this.data('cur_min'), 1202 valueMax = $this.data('cur_max'); 1203 1204 // assume 0 if valueMax is not specified 1205 if (typeof valueMax === 'undefined') { 1206 valueMax = valueMin; 1207 } 1208 1209 if (rangeMin === '') { rangeMin = 0; } 1210 if (rangeMax === '') { rangeMax = 0; } 1211 if (valueMin === '') { valueMin = 0; } 1212 if (valueMax === '') { valueMax = 0; } 1213 1214 $this.data('range_min', rangeMin); 1215 $this.data('range_max', rangeMax); 1216 $this.data('cur_min', valueMin); 1217 $this.data('cur_max', valueMax); 1218 1219 // halt on error 1220 _methods.validate_params.call($this, settings); 1221 1222 $this.data('settings', settings); 1223 1224 // override rounding from markup if defined in configuration 1225 if (typeof settings.rounding !== 'undefined') { 1226 methods.set_rounding.call($this, settings.rounding); 1227 } 1228 else if (typeof $this.data('rounding') !== 'undefined') { 1229 methods.set_rounding.call($this, $this.data('rounding'));
1230 } 1231 else { 1232 methods.set_rounding.call($this, 1); 1233 } 1234 1235 var left_grip = $this.find(settings.left_grip_selector)[0], 1236 $left_grip = $(left_grip), 1237 $right_grip = $($this.find(settings.right_grip_selector)[0]); 1238 1239 // make sure left grip can be tabbed if the user hasn't 1240 // defined their own tab index 1241 if (typeof $left_grip.attr('tabindex') === 'undefined') { 1242 $left_grip.attr('tabindex', 0); 1243 } 1244 1245 // no right handler means single handler 1246 var has_right_grip = false; 1247 if ($this.find(settings.right_grip_selector).length > 0) { 1248 has_right_grip = true; 1249 1250 // make sure right grip can be tabbed if the user hasn't 1251 // defined their own tab index 1252 if (typeof $right_grip.attr('tabindex') === 'undefined') { 1253 $right_grip.attr('tabindex', 0); 1254 } 1255 } 1256 $this.data('has_right_grip', has_right_grip); 1257 1258 // enable aria attributes update? 1259 if ($this.data('aria_enabled') === true) { 1260 // setup aria role attributes on each grip 1261 $left_grip 1262 .attr('role', 'slider') 1263 .attr('aria-disabled', 'false'); 1264 1265 if (has_right_grip) { 1266 $right_grip 1267 .attr('role', 'slider') 1268 .attr('aria-disabled', 'false'); 1269 } 1270 } 1271 1272 // 1273 // deal with keypresses here 1274 // 1275 $this.bind('keyup.nstSlider', function (e) { 1276 if ($this.data('enabled')) { 1277 switch (e.which) { 1278 case 37: // left 1279 case 38: // up 1280 case 39: // right 1281 case 40: // down 1282 1283 if (_before_keydown_value === _before_keyup_value) { 1284 1285 // we should search for the next value change... 1286 // ... in which direction? depends on whe 1287 1288 var searchUntil = _methods.getSliderWidthPx.call($this), 1289 val, 1290 i, 1291 setAtPixel; 1292 1293 if (_before_keydown_pixel - _before_keyup_pixel < 0) { 1294 // the grip was moved towards the right 1295 1296 for (i=_before_keyup_pixel; i<=searchUntil; i++) { 1297 // if the value at pixel i is different than 1298 // the current value then we are good to go. 1299 // 1300 val = methods.round_value_according_to_rounding.call($this, 1301 _methods.getSliderValuesAtPositionPx.call($this, i, i)[1] 1302 ); 1303 if (val !== _before_keyup_value) { 1304 setAtPixel = i; 1305 break; 1306 } 1307 } 1308 } 1309 else { 1310 // the grip was moved towards the left 1311 1312 for (i=_before_keyup_pixel; i>=0; i--) { 1313 1314 // if the value at pixel i is different than 1315 // the current value then we are good to go. 1316 // 1317 val = methods.round_value_according_to_rounding.call($this, 1318 _methods.getSliderValuesAtPositionPx.call($this, i, i)[1] 1319 ); 1320 if (val !== _before_keyup_value) { 1321 setAtPixel = i; 1322 break; 1323 } 1324 } 1325 } 1326 1327 1328 // we need to set the slider at this position 1329 if (_is_left_grip) { 1330 _methods.validateAndMoveGripsToPx.call($this, setAtPixel, _methods.getRightGripPositionPx.call($this)); 1331 } 1332 else { 1333 _methods.validateAndMoveGripsToPx.call($this, _methods.getLeftGripPositionPx.call($this), setAtPixel); 1334 } 1335 1336 // 1337 // call the mouseup callback when the key is up! 1338 // 1339 _methods.notify_mouse_up_implicit.call($this, _is_left_grip); 1340 } 1341 } 1342 1343 // clear values 1344 _before_keydown_value = undefined; 1345 _before_keydown_pixel = undefined; 1346 _before_keyup_value = undefined; 1347 _before_keyup_pixel = undefined; 1348 } 1349 }); 1350 $this.bind('keydown.nstSlider', function (evt) { 1351 if ($this.data('enabled')) { 1352 1353 var moveHandleBasedOnKeysFunc = function ($grip, e) { 1354 1355 var nextLeft = _methods.getLeftGripPositionPx.call($this), 1356 nextRight = _methods.getRightGripPositionPx.call($this); 1357 1358 if (typeof _before_keydown_value === 'undefined') { 1359 _before_keydown_pixel = _is_left_grip ? nextLeft : nextRight; 1360 1361 _before_keydown_value = _is_left_grip ? methods.get_current_min_value.call($this) : methods.get_current_max_value.call($this); 1362 } 1363 1364 switch (e.which) { 1365 case 37: // left 1366 case 40: // down 1367 if (_is_left_grip) { nextLeft--; } else { nextRight--; } 1368 e.preventDefault(); 1369 break; 1370 1371 case 38: // up 1372 case 39: // right 1373 if (_is_left_grip) { nextLeft++; } else { nextRight++; } 1374 1375 e.preventDefault(); 1376 break; 1377 } 1378 1379 _before_keyup_pixel = _is_left_grip ? nextLeft : nextRight; 1380 1381 // may write into cur_min, cur_max data...
1382 _methods.validateAndMoveGripsToPx.call($this, nextLeft, 1383 nextRight); 1384 1385 _before_keyup_value = _is_left_grip ? methods.get_current_min_value.call($this) : methods.get_current_max_value.call($this); 1386 }; 1387 1388 // default 1389 if (has_right_grip && $this.find(':focus').is($right_grip)) { 1390 _is_left_grip = false; 1391 moveHandleBasedOnKeysFunc.call($this, $right_grip, evt); 1392 } 1393 else { 1394 _is_left_grip = true; 1395 moveHandleBasedOnKeysFunc.call($this, $left_grip, evt); 1396 } 1397 } 1398 }); 1399 1400 // determine size of grips 1401 var left_grip_width = _methods.getLeftGripWidth.call($this), 1402 right_grip_width = has_right_grip ? 1403 _methods.getRightGripWidth.call($this) : left_grip_width; 1404 1405 $this.data('left_grip_width', left_grip_width); 1406 $this.data('right_grip_width', right_grip_width); 1407 1408 $this.data('value_bar_selector', settings.value_bar_selector); 1409 1410 // set behaviour of naked bar in case of one handle 1411 if (!has_right_grip) { 1412 var bStickToSides = valueMax === rangeMax || valueMax === rangeMin; 1413 _methods.setNakedBarDelta.call($this,
1414 bStickToSides ? "stickToSides" : "middle", 1415 left_grip_width 1416 ); 1417 } 1418 1419 // this will set the range to the right extreme in such a case. 1420 if (rangeMin === rangeMax || valueMin === valueMax) { 1421 methods.set_range.call($this, rangeMin, rangeMax); 1422 } 1423 else { 1424 1425 // set the initial position 1426 _methods.set_position_from_val.call($this, 1427 $this.data('cur_min'), $this.data('cur_max')); 1428 } 1429 1430 _methods.notify_changed_implicit.call($this, 'init'); 1431 1432 // handle mouse movement 1433 $this.data('beforestart_min', methods.get_current_min_value.call($this)); 1434 $this.data('beforestart_max', methods.get_current_max_value.call($this)); 1435 1436 // pass a closure, so that 'this' will be the current slider bar, 1437 // not the container. 1438 $this.bind('mousedown.nstSlider', function (e) { 1439 _methods.drag_start_func.call($this, e, settings, $left_grip, $right_grip, false); 1440 }); 1441 1442 $container.bind('touchstart.nstSlider', function (e) { 1443 _methods.drag_start_func_touch.call($this, e, settings, $left_grip, $right_grip, true); 1444 }); 1445 $container.bind('touchend.nstSlider', function (e) { 1446 _methods.drag_end_func_touch.call($this, e); 1447 }); 1448 $container.bind('touchmove.nstSlider', function (e) { 1449 _methods.drag_move_func_touch.call($this, e); 1450 }); 1451 1452 // if the data-histogram attribute exists, then use this 1453 // histogram to set the step distribution 1454 var step_histogram = $this.data('histogram'); 1455 if (typeof step_histogram !== 'undefined') { 1456 methods.set_step_histogram.call($this, step_histogram); 1457 } 1458 }); // -- each slider 1459 }, 1460 'get_range_min' : function () { 1461 var $this = this; 1462 return $this.data('range_min'); 1463 }, 1464 'get_range_max' : function () { 1465 var $this = this; 1466 return $this.data('range_max'); 1467 }, 1468 'get_current_min_value' : function () { 1469 var $this = $(this); 1470 1471 var rangeMin = methods.get_range_min.call($this), 1472 rangeMax = methods.get_range_max.call($this); 1473 1474 var currentMin = $this.data('cur_min'); 1475 1476 var min; 1477 if (rangeMin >= currentMin) { 1478 min = rangeMin; 1479 } 1480 else { 1481 min = methods.round_value_according_to_rounding.call($this, currentMin); 1482 } 1483 1484 if (_methods.haveLimits.call($this)) { 1485 if (min <= rangeMin) { 1486 return $this.data('lower-limit'); 1487 } 1488 else if (min >= rangeMax) { 1489 return $this.data('upper-limit'); 1490 } 1491 } 1492 else { 1493 if (min <= rangeMin) { 1494 return rangeMin; 1495 } 1496 else if (min >= rangeMax) { 1497 return rangeMax; 1498 } 1499 } 1500 1501 return min; 1502 }, 1503 'get_current_max_value' : function () { 1504 var $this = $(this); 1505 1506 var rangeMin = methods.get_range_min.call($this), 1507 rangeMax = methods.get_range_max.call($this); 1508 1509 var currentMax = $this.data('cur_max'); 1510 1511 var max; 1512 if (rangeMax <= currentMax) { 1513 max = rangeMax; 1514 } 1515 else { 1516 max = methods.round_value_according_to_rounding.call($this, currentMax); 1517 } 1518 1519 1520 if (_methods.haveLimits.call($this)) { 1521 if (max >= rangeMax) { 1522 return $this.data('upper-limit'); 1523 } 1524 else if (max <= rangeMin) { 1525 return $this.data('lower-limit'); 1526 } 1527 } 1528 else { 1529 if (max >= rangeMax) { 1530 return rangeMax; 1531 } 1532 else if (max <= rangeMin) { 1533 return rangeMin; 1534 } 1535 } 1536 1537 return max; 1538 }, 1539 'is_handle_to_left_extreme' : function () { 1540 var $this = this; 1541 if (_methods.haveLimits.call($this)) { 1542 return $this.data('lower-limit') === methods.get_current_min_value.call($this); 1543 } 1544 else { 1545 return methods.get_range_min.call($this) === methods.get_current_min_value.call($this); 1546 } 1547 }, 1548 'is_handle_to_right_extreme' : function () { 1549 var $this = this; 1550 if (_methods.haveLimits.call($this)) { 1551 return $this.data('upper-limit') === methods.get_current_max_value.call($this); 1552 } 1553 else { 1554 return methods.get_range_max.call($this) === methods.get_current_max_value.call($this); 1555 } 1556 }, 1557 // just call set_position on the current values 1558 'refresh' : function () { 1559 var $this = this; 1560 1561 // re-set the slider step if specified 1562 var lastStepHistogram = $this.data('last_step_histogram'); 1563 if (typeof lastStepHistogram !== 'undefined') { 1564 methods.set_step_histogram.call($this, lastStepHistogram); 1565 } 1566 1567 // re-center given values 1568 _methods.set_position_from_val.call($this, 1569 methods.get_current_min_value.call($this), 1570 methods.get_current_max_value.call($this) 1571 ); 1572 1573 // re-highlight the range 1574 var highlightRangeMin = $this.data('highlightedRangeMin'); 1575 if (typeof highlightRangeMin === 'number') { 1576 // a highlight range is present, we must update it 1577 var highlightRangeMax = $this.data('highlightedRangeMax'); 1578 methods.highlight_range.call($this, highlightRangeMin, highlightRangeMax); 1579 } 1580 1581 _methods.notify_changed_implicit.call($this, 'refresh'); 1582 return $this; 1583 }, 1584 'disable' : function () { 1585 var $this = this, 1586 settings = $this.data('settings'); 1587 1588 $this.data('enabled', false) 1589 .find(settings.left_grip_selector) 1590 .attr('aria-disabled', 'true') 1591 .end() 1592 .find(settings.right_grip_selector) 1593 .attr('aria-disabled', 'true'); 1594 1595 return $this; 1596 }, 1597 'enable' : function() { 1598 var $this = this, 1599 settings = $this.data('settings'); 1600 1601 $this.data('enabled', true) 1602 .find(settings.left_grip_selector) 1603 .attr('aria-disabled', 'false') 1604 .end() 1605 .find(settings.right_grip_selector) 1606 .attr('aria-disabled', 'false'); 1607 1608 return $this; 1609 }, 1610 'is_enabled' : function() { 1611 var $this = this; 1612 return $this.data('enabled'); 1613 }, 1614 /* 1615 * This one is the public method, called externally. 1616 * It sets the position and notifies in fact. 1617 */ 1618 'set_position' : function(min, max) { 1619 var $this = this; 1620 1621 var prev_min = $this.data('cur_min'), 1622 prev_max = $this.data('cur_max'); 1623 1624 if (min > max) { 1625 _methods.set_position_from_val.call($this, max, min); 1626 } 1627 else { 1628 _methods.set_position_from_val.call($this, min, max); 1629 } 1630 1631 // set the style of the grips according to the highlighted range 1632 _methods.refresh_grips_style.call($this); 1633 1634 _methods.notify_changed_implicit.call($this, 'set_position', prev_min, prev_max); 1635 1636 // this is for the future, therefore "before the next 1637 // interaction starts" 1638 $this.data('beforestart_min', min); 1639 $this.data('beforestart_max', max); 1640 }, 1641 /* 1642 * This tells the slider to increment its step non linearly over the 1643 * current range, based on the histogram on where results are. 1644 * 1645 * the input parameter 'histogram' identifies an empirical probability 1646 * density function (PDF). 1647 * 1648 */ 1649 'set_step_histogram' : function (histogram) { 1650 var $this = this; 1651 1652 $this.data('last_step_histogram', histogram); 1653 1654 if (typeof histogram === 'undefined') { 1655 $.error('got an undefined histogram in set_step_histogram'); 1656 _methods.unset_step_histogram.call($this); 1657 } 1658 1659 var sliderWidthPx = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'), 1660 nbuckets = histogram.length; 1661 1662 if (sliderWidthPx <= 0) { 1663 // that means the slider is not visible... 1664 return; 1665 } 1666 1667 // 1668 // we need to transform this pdf into a cdf, and use it to obtain 1669 // two mappings: pixel to value and value to pixel. 1670 // 1671 // 1) normalize the pdf to sum to sliderWidthPx first 1672 var i; 1673 var histogram_sum = 0; 1674 for (i=0; i<nbuckets; i++) { 1675 histogram_sum += histogram[i]; 1676 } 1677 1678 // 1679 // if the sum of the histogram is 0 it means that all is 0 in the 1680 // histogram! (i.e, flat histogram). In this case we already know 1681 // what's going to be the answer... 1682 // 1683 if (histogram_sum === 0) { 1684 // ... and the answer is: a linear scale between min_range and 1685 // max range! 1686 methods.unset_step_histogram.call($this); 1687 1688 return $this; 1689 } 1690 1691 // coefficient for normalization
1692 var coeff = parseFloat(histogram_sum)/sliderWidthPx; 1693 1694 // go normalize the histogram using this coefficient! 1695 for (i=0; i<nbuckets; i++) { 1696 histogram[i] = histogram[i]/coeff; 1697 } 1698 1699 // 2) now that the histogram is normalized, extract the cumulative 1700 // distribution function (CDF). This is an always increasing function 1701 // that ranges between 0 and sliderWidthPx; 1702 // 1703 // We also build the inverted cdf, just the cdf read the other way 1704 // around. 1705 // 1706 var cdf = [ histogram[0] ]; // points to pixels 1707 for (i=1; i<nbuckets; i++) { 1708 var cdf_x = cdf[i-1] + histogram[i]; 1709 cdf.push(cdf_x); 1710 } 1711 cdf.push(sliderWidthPx); 1712 1713 1714 // the first value here is always min_range as the cdf is supposed 1715 // to start from 0 (also first pixel = min_range) 1716 var pixel_to_value_lookup = [ $this.data('range_min') ]; 1717 1718 var last_filled = 0; // we've already filled 0 1719 1720 // now stretch over the rest of the cdf 1721 var last_price_for_cdf_bucket = pixel_to_value_lookup[0]; 1722 1723 var cdf_bucket_count = 0; 1724 while (last_filled <= sliderWidthPx) { // do until all pixels are filled 1725 1726 // get next item from cdf 1727 var fill_up_to_px = parseInt(cdf.shift(), 10); 1728 var price_for_cdf_bucket = 1729 _methods.inverse_rangemap_0_to_n.call($this, cdf_bucket_count+1, nbuckets+1); 1730 1731 cdf_bucket_count++; 1732 1733 // how many pixels do we have to fill 1734 var fill_tot = fill_up_to_px - last_filled; 1735 1736 // interpolate and fill 1737 var diff = price_for_cdf_bucket - last_price_for_cdf_bucket; 1738 for (i = last_filled; i < fill_up_to_px; i++) { 1739 var next_price_for_cdf_bucket = 1740 last_price_for_cdf_bucket + (diff * (i-last_filled+1) / fill_tot); 1741 1742 pixel_to_value_lookup.push(next_price_for_cdf_bucket); 1743 1744 last_filled++; 1745 1746 last_price_for_cdf_bucket = next_price_for_cdf_bucket; 1747 } 1748 1749 if (last_filled === sliderWidthPx) { 1750 break; 1751 } 1752 } 1753 pixel_to_value_lookup[pixel_to_value_lookup.length-1] = $this.data('range_max'); 1754 1755 // 3) build lookup functions to extract pixels and values from the 1756 // cdf and the inverted cdf. 1757 // 1758 var pixel_to_value_mapping = function (pixel) { 1759 return pixel_to_value_lookup[parseInt(pixel, 10)]; 1760 }; 1761 1762 var value_to_pixel_mapping = function (value) { 1763 // 1764 // Binary search into the array of pixels, returns always the 1765 // rightmost pixel if there is no exact match. 1766 // 1767 var suggestedPixel = _methods.binarySearch.call($this, pixel_to_value_lookup, value, 1768 function(a, i) { return a[i]; }, // access a value in the array 1769 _methods.binarySearchValueToPxCompareFunc 1770 ); 1771 1772 // exact match 1773 if (pixel_to_value_lookup[suggestedPixel] === value) { 1774 return suggestedPixel; 1775 } 1776 1777 // approx match: we need to check if it's closer to the value 1778 // at suggestedPixel or the value at suggestedPixel-1 1779 if ( Math.abs(pixel_to_value_lookup[suggestedPixel-1] - value) < 1780 Math.abs(pixel_to_value_lookup[suggestedPixel] - value) ) { 1781 1782 return suggestedPixel-1; 1783 } 1784 return suggestedPixel; 1785 }; 1786 1787 // 1788 // these two functions will be stored and then used internally to 1789 // decide what value to display given a certain pixel, and what 1790 // pixel to put the slider on given a certain value. 1791 // 1792 $this.data('pixel_to_value_mapping', pixel_to_value_mapping); 1793 $this.data('value_to_pixel_mapping', value_to_pixel_mapping); 1794 1795 return $this; 1796 }, 1797 /* 1798 * Remove the pixel-to-value and the value-to-pixel mappings from the 1799 * slider so that the slider can follow a linear step over the current 1800 * range again. 1801 */ 1802 'unset_step_histogram' : function () { 1803 var $this = this; 1804 1805 $this.removeData('pixel_to_value_mapping'); 1806 $this.removeData('value_to_pixel_mapping'); 1807 $this.removeData('last_step_histogram'); 1808 1809 return $this; 1810 }, 1811 'set_range' : function (rangeMin, rangeMax) { 1812 var $this = this; 1813 1814 // get the current values 1815 var oldMin = methods.get_current_min_value.call($this), 1816 oldMax = methods.get_current_max_value.call($this); 1817 1818 // set range 1819 $this.data('range_min', rangeMin); 1820 $this.data('range_max', rangeMax); 1821 1822 // try to re-center old values in the new range. 1823 // NOTE: this may set different values! 1824 _methods.set_position_from_val.call($this, oldMin, oldMax); 1825 1826 /* 1827 * Re-highlight ranges if any are defined. 1828 */ 1829 // var highlightRangeMin = $this.data('highlightedRangeMin'); 1830 // if (typeof rangeMin === 'number') { 1831 // // a highlight range is present, we must update it 1832 // var highlightRangeMax = $this.data('highlightedRangeMax'); 1833 // methods.highlight_range.call($this, highlightRangeMin, highlightRangeMax); 1834 // } 1835 1836 // pass old min and max in the notify_changed_implicit method, so that we 1837 // notify if we need to 1838 _methods.notify_changed_implicit.call($this, 'set_range', oldMin, oldMax); 1839 1840 return $this; 1841 }, 1842 /* 1843 * This method highlights the range of the slider apart from the 1844 * position of the slider grips. 1845 * To work well, the slider must have background color set to 1846 * transparent in the CSS or not set. 1847 */ 1848 'highlight_range' : function(rangeMin, rangeMax) { 1849 var $this = this; 1850 var settings = $this.data('settings'); 1851 1852 if (typeof settings.highlight === "undefined") { 1853 $.error('you cannot call highlight_range if you haven\' specified the "highlight" parameter in construction!'); 1854 } 1855 1856 // avoid empty string 1857 if (!rangeMin) { rangeMin = 0; } 1858 if (!rangeMax) { rangeMax = 0; } 1859 1860 // we need to map rangeMin and rangeMax into pixels. 1861 var leftPx = methods.value_to_px.call($this, rangeMin), 1862 rightPx = methods.value_to_px.call($this, rangeMax), 1863 barWidth = rightPx - leftPx + $this.data('left_grip_width'); 1864 1865 // set position 1866 var $highlightPanel = $this.find( 1867 settings.highlight.panel_selector 1868 ); 1869 1870 $highlightPanel.css('left', leftPx + "px"); 1871 $highlightPanel.css('width', barWidth + "px"); 1872 1873 // keep the latest highlighted range, because if set_range is called 1874 // we must be able to update the highlighting. 1875 $this.data('highlightedRangeMin', rangeMin); 1876 $this.data('highlightedRangeMax', rangeMax); 1877 1878 // now decide wether the handler should be highlight 1879 _methods.refresh_grips_style.call($this); 1880 1881 return $this; 1882 }, 1883 /*
1884 * Sets the increment rounding for the slider, see input parameters section 1885 * for more information. 1886 */ 1887 'set_rounding' : function (rounding) { 1888 var $this = this; 1889 1890 if (typeof rounding === 'string' && rounding.indexOf('{') > -1) { 1891 // probably a json string 1892 rounding = $.parseJSON(rounding); 1893 } 1894 1895 $this.data('rounding', rounding); 1896 1897 // build an array of roundings and sort it by value to facilitate search 1898 // when the range is going to be set. 1899 var roundings_array = []; 1900 if (typeof rounding === 'object') { 1901 // initial object has the form { value : range } 1902 var rounding_value; 1903 for (rounding_value in rounding) { // skip_javascript_test 1904 if (rounding.hasOwnProperty(rounding_value)) { 1905 var rounding_range = rounding[rounding_value]; 1906 roundings_array.push({ 1907 'range' : rounding_range, 1908 'value' : rounding_value 1909 }); 1910 } 1911 } 1912 1913 // now sort it by rounding range 1914 roundings_array.sort(function (a, b) { return a.range - b.range; }); 1915 1916 $this.data('rounding_ranges', roundings_array); 1917 } 1918 else { 1919 $this.removeData('rounding_ranges'); 1920 } 1921 1922 return $this; 1923 }, 1924 'get_rounding' : function () { 1925 var $this = this; 1926 return $this.data('rounding'); 1927 }, 1928 /* 1929 * This method rounds a given value to the closest integer defined 1930 * according to the rounding. Examples: 1931 * rounding: 10 v: 12.3 --> 10 1932 * rounding: 1 v: 12.3 --> 12 1933 * rounding: 10 v: 12.6 --> 13 1934 */ 1935 'round_value_according_to_rounding' : function(v) { 1936 var $this = this; 1937 var rounding = _methods.get_rounding_for_value.call($this, v); 1938 1939 if (rounding > 0) { 1940 // We bring ourselves in a space of unitary roundings. You can 1941 // imagine now that sliders range between a certain minimum and 1942 // maximum, and we always increase/decrease of one. 1943 var increment = v / rounding; 1944 1945 // This is the proposed value. 1946 var increment_int = parseInt(increment, 10); 1947 1948 // delta is a positive number between 0 and 1 that tells us how 1949 // close is the slider to integer + 1 (i.e., the next rounding). 1950 // 0 means the grip is exactly on integer 1951 // 1 means the grip is on integer + 1. 1952 var delta = increment - increment_int; 1953 1954 // now use delta to modify or not the current value. 1955 if (delta > 0.5) { 1956 increment_int++; 1957 } 1958 1959 // we now move the 1960 var rounded = increment_int * rounding; 1961 1962 return rounded; 1963 } 1964 else { 1965 $.error('rounding must be > 0, got ' + rounding + ' instead'); 1966 } 1967 return v; 1968 }, 1969 /* 1970 * Utility function. Given a value within the range of the slider, 1971 * converts the value in pixels. If a value_to_pixel_mapping function 1972 * is defined it will be used, otherwise a linear mapping is used for 1973 * the conversion. 1974 */ 1975 'value_to_px' : function (value) { 1976 var $this = this, 1977 value_to_pixel_mapping_func = $this.data('value_to_pixel_mapping'); 1978 1979 // try using non-linear mapping if it's there... 1980 if (typeof value_to_pixel_mapping_func !== 'undefined') { 1981 return value_to_pixel_mapping_func(value); 1982 } 1983 1984 // ... use linear mapping otherwise 1985 var w = _methods.getSliderWidthPx.call($this) - $this.data('left_grip_width'); 1986 return _methods.rangemap_0_to_n.call($this, value, w); 1987 } 1988 }; 1989 1990 var __name__ = 'nstSlider'; 1991 1992 $.fn[__name__] = function(method) { 1993 /* 1994 * Just a router for method calls 1995 */ 1996 if (methods[method]) { 1997 if (this.data('initialized') === true) { 1998 // call a method 1999 return methods[method].apply(this, 2000 Array.prototype.slice.call(arguments, 1) 2001 ); 2002 } 2003 else { 2004 throw new Error('method ' + method + ' called on an uninitialized instance of ' + __name__); 2005 } 2006 } 2007 else if (typeof method === 'object' || !method) { 2008 // call init, user passed the settings as parameters 2009 this.data('initialized', true); 2010 return methods.init.apply(this, arguments); 2011 } 2012 else { 2013 $.error('Cannot call method ' + method); 2014 } 2015 }; 2016})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.