nouislider.js (87550B)
1 /*! nouislider - 13.0.0 - 2/6/2019 */ 2 (function(factory) { 3 if (typeof define === "function" && define.amd) { 4 // AMD. Register as an anonymous module. 5 define([], factory); 6 } else if (typeof exports === "object") { 7 // Node/CommonJS 8 module.exports = factory(); 9 } else { 10 // Browser globals 11 window.noUiSlider = factory(); 12 } 13 })(function() { 14 "use strict"; 15 16 var VERSION = "13.0.0"; 17 18 function isValidFormatter(entry) { 19 return typeof entry === "object" && typeof entry.to === "function" && typeof entry.from === "function"; 20 } 21 22 function removeElement(el) { 23 el.parentElement.removeChild(el); 24 } 25 26 function isSet(value) { 27 return value !== null && value !== undefined; 28 } 29 30 // Bindable version 31 function preventDefault(e) { 32 e.preventDefault(); 33 } 34 35 // Removes duplicates from an array. 36 function unique(array) { 37 return array.filter(function(a) { 38 return !this[a] ? (this[a] = true) : false; 39 }, {}); 40 } 41 42 // Round a value to the closest 'to'. 43 function closest(value, to) { 44 return Math.round(value / to) * to; 45 } 46 47 // Current position of an element relative to the document. 48 function offset(elem, orientation) { 49 var rect = elem.getBoundingClientRect(); 50 var doc = elem.ownerDocument; 51 var docElem = doc.documentElement; 52 var pageOffset = getPageOffset(doc); 53 54 // getBoundingClientRect contains left scroll in Chrome on Android. 55 // I haven't found a feature detection that proves this. Worst case 56 // scenario on mis-match: the 'tap' feature on horizontal sliders breaks. 57 if (/webkit.*Chrome.*Mobile/i.test(navigator.userAgent)) { 58 pageOffset.x = 0; 59 } 60 61 return orientation 62 ? rect.top + pageOffset.y - docElem.clientTop 63 : rect.left + pageOffset.x - docElem.clientLeft; 64 } 65 66 // Checks whether a value is numerical. 67 function isNumeric(a) { 68 return typeof a === "number" && !isNaN(a) && isFinite(a); 69 } 70 71 // Sets a class and removes it after [duration] ms. 72 function addClassFor(element, className, duration) { 73 if (duration > 0) { 74 addClass(element, className); 75 setTimeout(function() { 76 removeClass(element, className); 77 }, duration); 78 } 79 } 80 81 // Limits a value to 0 - 100 82 function limit(a) { 83 return Math.max(Math.min(a, 100), 0); 84 } 85 86 // Wraps a variable as an array, if it isn't one yet. 87 // Note that an input array is returned by reference! 88 function asArray(a) { 89 return Array.isArray(a) ? a : [a]; 90 } 91 92 // Counts decimals 93 function countDecimals(numStr) { 94 numStr = String(numStr); 95 var pieces = numStr.split("."); 96 return pieces.length > 1 ? pieces[1].length : 0; 97 } 98 99 // http://youmightnotneedjquery.com/#add_class 100 function addClass(el, className) { 101 if (el.classList) { 102 el.classList.add(className); 103 } else { 104 el.className += " " + className; 105 } 106 } 107 108 // http://youmightnotneedjquery.com/#remove_class 109 function removeClass(el, className) { 110 if (el.classList) { 111 el.classList.remove(className); 112 } else { 113 el.className = el.className.replace( 114 new RegExp("(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", "gi"), 115 " " 116 ); 117 } 118 } 119 120 // https://plainjs.com/javascript/attributes/adding-removing-and-testing-for-classes-9/ 121 function hasClass(el, className) { 122 return el.classList 123 ? el.classList.contains(className) 124 : new RegExp("\\b" + className + "\\b").test(el.className); 125 } 126 127 // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY#Notes 128 function getPageOffset(doc) { 129 var supportPageOffset = window.pageXOffset !== undefined; 130 var isCSS1Compat = (doc.compatMode || "") === "CSS1Compat"; 131 var x = supportPageOffset 132 ? window.pageXOffset 133 : isCSS1Compat 134 ? doc.documentElement.scrollLeft 135 : doc.body.scrollLeft; 136 var y = supportPageOffset 137 ? window.pageYOffset 138 : isCSS1Compat 139 ? doc.documentElement.scrollTop 140 : doc.body.scrollTop; 141 142 return { 143 x: x, 144 y: y 145 }; 146 } 147 148 // we provide a function to compute constants instead 149 // of accessing window.* as soon as the module needs it 150 // so that we do not compute anything if not needed 151 function getActions() { 152 // Determine the events to bind. IE11 implements pointerEvents without 153 // a prefix, which breaks compatibility with the IE10 implementation. 154 return window.navigator.pointerEnabled 155 ? { 156 start: "pointerdown", 157 move: "pointermove", 158 end: "pointerup" 159 } 160 : window.navigator.msPointerEnabled 161 ? { 162 start: "MSPointerDown", 163 move: "MSPointerMove", 164 end: "MSPointerUp" 165 } 166 : { 167 start: "mousedown touchstart", 168 move: "mousemove touchmove", 169 end: "mouseup touchend" 170 }; 171 } 172 173 // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md 174 // Issue #785 175 function getSupportsPassive() { 176 var supportsPassive = false; 177 178 /* eslint-disable */ 179 try { 180 var opts = Object.defineProperty({}, "passive", { 181 get: function() { 182 supportsPassive = true; 183 } 184 }); 185 186 window.addEventListener("test", null, opts); 187 } catch (e) {} 188 /* eslint-enable */ 189 190 return supportsPassive; 191 } 192 193 function getSupportsTouchActionNone() { 194 return window.CSS && CSS.supports && CSS.supports("touch-action", "none"); 195 } 196 197 // Value calculation 198 199 // Determine the size of a sub-range in relation to a full range. 200 function subRangeRatio(pa, pb) { 201 return 100 / (pb - pa); 202 } 203 204 // (percentage) How many percent is this value of this range? 205 function fromPercentage(range, value) { 206 return (value * 100) / (range[1] - range[0]); 207 } 208 209 // (percentage) Where is this value on this range? 210 function toPercentage(range, value) { 211 return fromPercentage(range, range[0] < 0 ? value + Math.abs(range[0]) : value - range[0]); 212 } 213 214 // (value) How much is this percentage on this range? 215 function isPercentage(range, value) { 216 return (value * (range[1] - range[0])) / 100 + range[0]; 217 } 218 219 // Range conversion 220 221 function getJ(value, arr) { 222 var j = 1; 223 224 while (value >= arr[j]) { 225 j += 1; 226 } 227 228 return j; 229 } 230 231 // (percentage) Input a value, find where, on a scale of 0-100, it applies. 232 function toStepping(xVal, xPct, value) { 233 if (value >= xVal.slice(-1)[0]) { 234 return 100; 235 } 236 237 var j = getJ(value, xVal); 238 var va = xVal[j - 1]; 239 var vb = xVal[j]; 240 var pa = xPct[j - 1]; 241 var pb = xPct[j]; 242 243 return pa + toPercentage([va, vb], value) / subRangeRatio(pa, pb); 244 } 245 246 // (value) Input a percentage, find where it is on the specified range. 247 function fromStepping(xVal, xPct, value) { 248 // There is no range group that fits 100 249 if (value >= 100) { 250 return xVal.slice(-1)[0]; 251 } 252 253 var j = getJ(value, xPct); 254 var va = xVal[j - 1]; 255 var vb = xVal[j]; 256 var pa = xPct[j - 1]; 257 var pb = xPct[j]; 258 259 return isPercentage([va, vb], (value - pa) * subRangeRatio(pa, pb)); 260 } 261 262 // (percentage) Get the step that applies at a certain value. 263 function getStep(xPct, xSteps, snap, value) { 264 if (value === 100) { 265 return value; 266 } 267 268 var j = getJ(value, xPct); 269 var a = xPct[j - 1]; 270 var b = xPct[j]; 271 272 // If 'snap' is set, steps are used as fixed points on the slider. 273 if (snap) { 274 // Find the closest position, a or b. 275 if (value - a > (b - a) / 2) { 276 return b; 277 } 278 279 return a; 280 } 281 282 if (!xSteps[j - 1]) { 283 return value; 284 } 285 286 return xPct[j - 1] + closest(value - xPct[j - 1], xSteps[j - 1]); 287 } 288 289 // Entry parsing 290 291 function handleEntryPoint(index, value, that) { 292 var percentage; 293 294 // Wrap numerical input in an array. 295 if (typeof value === "number") { 296 value = [value]; 297 } 298 299 // Reject any invalid input, by testing whether value is an array. 300 if (!Array.isArray(value)) { 301 throw new Error("noUiSlider (" + VERSION + "): 'range' contains invalid value."); 302 } 303 304 // Covert min/max syntax to 0 and 100. 305 if (index === "min") { 306 percentage = 0; 307 } else if (index === "max") { 308 percentage = 100; 309 } else { 310 percentage = parseFloat(index); 311 } 312 313 // Check for correct input. 314 if (!isNumeric(percentage) || !isNumeric(value[0])) { 315 throw new Error("noUiSlider (" + VERSION + "): 'range' value isn't numeric."); 316 } 317 318 // Store values. 319 that.xPct.push(percentage); 320 that.xVal.push(value[0]); 321 322 // NaN will evaluate to false too, but to keep 323 // logging clear, set step explicitly. Make sure 324 // not to override the 'step' setting with false. 325 if (!percentage) { 326 if (!isNaN(value[1])) { 327 that.xSteps[0] = value[1]; 328 } 329 } else { 330 that.xSteps.push(isNaN(value[1]) ? false : value[1]); 331 } 332 333 that.xHighestCompleteStep.push(0); 334 } 335 336 function handleStepPoint(i, n, that) { 337 // Ignore 'false' stepping. 338 if (!n) { 339 return true; 340 } 341 342 // Factor to range ratio 343 that.xSteps[i] = 344 fromPercentage([that.xVal[i], that.xVal[i + 1]], n) / subRangeRatio(that.xPct[i], that.xPct[i + 1]); 345 346 var totalSteps = (that.xVal[i + 1] - that.xVal[i]) / that.xNumSteps[i]; 347 var highestStep = Math.ceil(Number(totalSteps.toFixed(3)) - 1); 348 var step = that.xVal[i] + that.xNumSteps[i] * highestStep; 349 350 that.xHighestCompleteStep[i] = step; 351 } 352 353 // Interface 354 355 function Spectrum(entry, snap, singleStep) { 356 this.xPct = []; 357 this.xVal = []; 358 this.xSteps = [singleStep || false]; 359 this.xNumSteps = [false]; 360 this.xHighestCompleteStep = []; 361 362 this.snap = snap; 363 364 var index; 365 var ordered = []; // [0, 'min'], [1, '50%'], [2, 'max'] 366 367 // Map the object keys to an array. 368 for (index in entry) { 369 if (entry.hasOwnProperty(index)) { 370 ordered.push([entry[index], index]); 371 } 372 } 373 374 // Sort all entries by value (numeric sort). 375 if (ordered.length && typeof ordered[0][0] === "object") { 376 ordered.sort(function(a, b) { 377 return a[0][0] - b[0][0]; 378 }); 379 } else { 380 ordered.sort(function(a, b) { 381 return a[0] - b[0]; 382 }); 383 } 384 385 // Convert all entries to subranges. 386 for (index = 0; index < ordered.length; index++) { 387 handleEntryPoint(ordered[index][1], ordered[index][0], this); 388 } 389 390 // Store the actual step values. 391 // xSteps is sorted in the same order as xPct and xVal. 392 this.xNumSteps = this.xSteps.slice(0); 393 394 // Convert all numeric steps to the percentage of the subrange they represent. 395 for (index = 0; index < this.xNumSteps.length; index++) { 396 handleStepPoint(index, this.xNumSteps[index], this); 397 } 398 } 399 400 Spectrum.prototype.getMargin = function(value) { 401 var step = this.xNumSteps[0]; 402 403 if (step && (value / step) % 1 !== 0) { 404 throw new Error("noUiSlider (" + VERSION + "): 'limit', 'margin' and 'padding' must be divisible by step."); 405 } 406 407 return this.xPct.length === 2 ? fromPercentage(this.xVal, value) : false; 408 }; 409 410 Spectrum.prototype.toStepping = function(value) { 411 value = toStepping(this.xVal, this.xPct, value); 412 413 return value; 414 }; 415 416 Spectrum.prototype.fromStepping = function(value) { 417 return fromStepping(this.xVal, this.xPct, value); 418 }; 419 420 Spectrum.prototype.getStep = function(value) { 421 value = getStep(this.xPct, this.xSteps, this.snap, value); 422 423 return value; 424 }; 425 426 Spectrum.prototype.getDefaultStep = function(value, isDown, size) { 427 var j = getJ(value, this.xPct); 428 429 // When at the top or stepping down, look at the previous sub-range 430 if (value === 100 || (isDown && value === this.xPct[j - 1])) { 431 j = Math.max(j - 1, 1); 432 } 433 434 return (this.xVal[j] - this.xVal[j - 1]) / size; 435 }; 436 437 Spectrum.prototype.getNearbySteps = function(value) { 438 var j = getJ(value, this.xPct); 439 440 return { 441 stepBefore: { 442 startValue: this.xVal[j - 2], 443 step: this.xNumSteps[j - 2], 444 highestStep: this.xHighestCompleteStep[j - 2] 445 }, 446 thisStep: { 447 startValue: this.xVal[j - 1], 448 step: this.xNumSteps[j - 1], 449 highestStep: this.xHighestCompleteStep[j - 1] 450 }, 451 stepAfter: { 452 startValue: this.xVal[j], 453 step: this.xNumSteps[j], 454 highestStep: this.xHighestCompleteStep[j] 455 } 456 }; 457 }; 458 459 Spectrum.prototype.countStepDecimals = function() { 460 var stepDecimals = this.xNumSteps.map(countDecimals); 461 return Math.max.apply(null, stepDecimals); 462 }; 463 464 // Outside testing 465 Spectrum.prototype.convert = function(value) { 466 return this.getStep(this.toStepping(value)); 467 }; 468 469 /* Every input option is tested and parsed. This'll prevent 470 endless validation in internal methods. These tests are 471 structured with an item for every option available. An 472 option can be marked as required by setting the 'r' flag. 473 The testing function is provided with three arguments: 474 - The provided value for the option; 475 - A reference to the options object; 476 - The name for the option; 477 478 The testing function returns false when an error is detected, 479 or true when everything is OK. It can also modify the option 480 object, to make sure all values can be correctly looped elsewhere. */ 481 482 var defaultFormatter = { 483 to: function(value) { 484 return value !== undefined && value.toFixed(2); 485 }, 486 from: Number 487 }; 488 489 function validateFormat(entry) { 490 // Any object with a to and from method is supported. 491 if (isValidFormatter(entry)) { 492 return true; 493 } 494 495 throw new Error("noUiSlider (" + VERSION + "): 'format' requires 'to' and 'from' methods."); 496 } 497 498 function testStep(parsed, entry) { 499 if (!isNumeric(entry)) { 500 throw new Error("noUiSlider (" + VERSION + "): 'step' is not numeric."); 501 } 502 503 // The step option can still be used to set stepping 504 // for linear sliders. Overwritten if set in 'range'. 505 parsed.singleStep = entry; 506 } 507 508 function testRange(parsed, entry) { 509 // Filter incorrect input. 510 if (typeof entry !== "object" || Array.isArray(entry)) { 511 throw new Error("noUiSlider (" + VERSION + "): 'range' is not an object."); 512 } 513 514 // Catch missing start or end. 515 if (entry.min === undefined || entry.max === undefined) { 516 throw new Error("noUiSlider (" + VERSION + "): Missing 'min' or 'max' in 'range'."); 517 } 518 519 // Catch equal start or end. 520 if (entry.min === entry.max) { 521 throw new Error("noUiSlider (" + VERSION + "): 'range' 'min' and 'max' cannot be equal."); 522 } 523 524 parsed.spectrum = new Spectrum(entry, parsed.snap, parsed.singleStep); 525 } 526 527 function testStart(parsed, entry) { 528 entry = asArray(entry); 529 530 // Validate input. Values aren't tested, as the public .val method 531 // will always provide a valid location. 532 if (!Array.isArray(entry) || !entry.length) { 533 throw new Error("noUiSlider (" + VERSION + "): 'start' option is incorrect."); 534 } 535 536 // Store the number of handles. 537 parsed.handles = entry.length; 538 539 // When the slider is initialized, the .val method will 540 // be called with the start options. 541 parsed.start = entry; 542 } 543 544 function testSnap(parsed, entry) { 545 // Enforce 100% stepping within subranges. 546 parsed.snap = entry; 547 548 if (typeof entry !== "boolean") { 549 throw new Error("noUiSlider (" + VERSION + "): 'snap' option must be a boolean."); 550 } 551 } 552 553 function testAnimate(parsed, entry) { 554 // Enforce 100% stepping within subranges. 555 parsed.animate = entry; 556 557 if (typeof entry !== "boolean") { 558 throw new Error("noUiSlider (" + VERSION + "): 'animate' option must be a boolean."); 559 } 560 } 561 562 function testAnimationDuration(parsed, entry) { 563 parsed.animationDuration = entry; 564 565 if (typeof entry !== "number") { 566 throw new Error("noUiSlider (" + VERSION + "): 'animationDuration' option must be a number."); 567 } 568 } 569 570 function testConnect(parsed, entry) { 571 var connect = [false]; 572 var i; 573 574 // Map legacy options 575 if (entry === "lower") { 576 entry = [true, false]; 577 } else if (entry === "upper") { 578 entry = [false, true]; 579 } 580 581 // Handle boolean options 582 if (entry === true || entry === false) { 583 for (i = 1; i < parsed.handles; i++) { 584 connect.push(entry); 585 } 586 587 connect.push(false); 588 } 589 590 // Reject invalid input 591 else if (!Array.isArray(entry) || !entry.length || entry.length !== parsed.handles + 1) { 592 throw new Error("noUiSlider (" + VERSION + "): 'connect' option doesn't match handle count."); 593 } else { 594 connect = entry; 595 } 596 597 parsed.connect = connect; 598 } 599 600 function testOrientation(parsed, entry) { 601 // Set orientation to an a numerical value for easy 602 // array selection. 603 switch (entry) { 604 case "horizontal": 605 parsed.ort = 0; 606 break; 607 case "vertical": 608 parsed.ort = 1; 609 break; 610 default: 611 throw new Error("noUiSlider (" + VERSION + "): 'orientation' option is invalid."); 612 } 613 } 614 615 function testMargin(parsed, entry) { 616 if (!isNumeric(entry)) { 617 throw new Error("noUiSlider (" + VERSION + "): 'margin' option must be numeric."); 618 } 619 620 // Issue #582 621 if (entry === 0) { 622 return; 623 } 624 625 parsed.margin = parsed.spectrum.getMargin(entry); 626 627 if (!parsed.margin) { 628 throw new Error("noUiSlider (" + VERSION + "): 'margin' option is only supported on linear sliders."); 629 } 630 } 631 632 function testLimit(parsed, entry) { 633 if (!isNumeric(entry)) { 634 throw new Error("noUiSlider (" + VERSION + "): 'limit' option must be numeric."); 635 } 636 637 parsed.limit = parsed.spectrum.getMargin(entry); 638 639 if (!parsed.limit || parsed.handles < 2) { 640 throw new Error( 641 "noUiSlider (" + 642 VERSION + 643 "): 'limit' option is only supported on linear sliders with 2 or more handles." 644 ); 645 } 646 } 647 648 function testPadding(parsed, entry) { 649 if (!isNumeric(entry) && !Array.isArray(entry)) { 650 throw new Error( 651 "noUiSlider (" + VERSION + "): 'padding' option must be numeric or array of exactly 2 numbers." 652 ); 653 } 654 655 if (Array.isArray(entry) && !(entry.length === 2 || isNumeric(entry[0]) || isNumeric(entry[1]))) { 656 throw new Error( 657 "noUiSlider (" + VERSION + "): 'padding' option must be numeric or array of exactly 2 numbers." 658 ); 659 } 660 661 if (entry === 0) { 662 return; 663 } 664 665 if (!Array.isArray(entry)) { 666 entry = [entry, entry]; 667 } 668 669 // 'getMargin' returns false for invalid values. 670 parsed.padding = [parsed.spectrum.getMargin(entry[0]), parsed.spectrum.getMargin(entry[1])]; 671 672 if (parsed.padding[0] === false || parsed.padding[1] === false) { 673 throw new Error("noUiSlider (" + VERSION + "): 'padding' option is only supported on linear sliders."); 674 } 675 676 if (parsed.padding[0] < 0 || parsed.padding[1] < 0) { 677 throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be a positive number(s)."); 678 } 679 680 if (parsed.padding[0] + parsed.padding[1] >= 100) { 681 throw new Error("noUiSlider (" + VERSION + "): 'padding' option must not exceed 100% of the range."); 682 } 683 } 684 685 function testDirection(parsed, entry) { 686 // Set direction as a numerical value for easy parsing. 687 // Invert connection for RTL sliders, so that the proper 688 // handles get the connect/background classes. 689 switch (entry) { 690 case "ltr": 691 parsed.dir = 0; 692 break; 693 case "rtl": 694 parsed.dir = 1; 695 break; 696 default: 697 throw new Error("noUiSlider (" + VERSION + "): 'direction' option was not recognized."); 698 } 699 } 700 701 function testBehaviour(parsed, entry) { 702 // Make sure the input is a string. 703 if (typeof entry !== "string") { 704 throw new Error("noUiSlider (" + VERSION + "): 'behaviour' must be a string containing options."); 705 } 706 707 // Check if the string contains any keywords. 708 // None are required. 709 var tap = entry.indexOf("tap") >= 0; 710 var drag = entry.indexOf("drag") >= 0; 711 var fixed = entry.indexOf("fixed") >= 0; 712 var snap = entry.indexOf("snap") >= 0; 713 var hover = entry.indexOf("hover") >= 0; 714 var unconstrained = entry.indexOf("unconstrained") >= 0; 715 716 if (fixed) { 717 if (parsed.handles !== 2) { 718 throw new Error("noUiSlider (" + VERSION + "): 'fixed' behaviour must be used with 2 handles"); 719 } 720 721 // Use margin to enforce fixed state 722 testMargin(parsed, parsed.start[1] - parsed.start[0]); 723 } 724 725 if (unconstrained && (parsed.margin || parsed.limit)) { 726 throw new Error( 727 "noUiSlider (" + VERSION + "): 'unconstrained' behaviour cannot be used with margin or limit" 728 ); 729 } 730 731 parsed.events = { 732 tap: tap || snap, 733 drag: drag, 734 fixed: fixed, 735 snap: snap, 736 hover: hover, 737 unconstrained: unconstrained 738 }; 739 } 740 741 function testTooltips(parsed, entry) { 742 if (entry === false) { 743 return; 744 } 745 746 if (entry === true) { 747 parsed.tooltips = []; 748 749 for (var i = 0; i < parsed.handles; i++) { 750 parsed.tooltips.push(true); 751 } 752 } else { 753 parsed.tooltips = asArray(entry); 754 755 if (parsed.tooltips.length !== parsed.handles) { 756 throw new Error("noUiSlider (" + VERSION + "): must pass a formatter for all handles."); 757 } 758 759 parsed.tooltips.forEach(function(formatter) { 760 if ( 761 typeof formatter !== "boolean" && 762 (typeof formatter !== "object" || typeof formatter.to !== "function") 763 ) { 764 throw new Error("noUiSlider (" + VERSION + "): 'tooltips' must be passed a formatter or 'false'."); 765 } 766 }); 767 } 768 } 769 770 function testAriaFormat(parsed, entry) { 771 parsed.ariaFormat = entry; 772 validateFormat(entry); 773 } 774 775 function testFormat(parsed, entry) { 776 parsed.format = entry; 777 validateFormat(entry); 778 } 779 780 function testKeyboardSupport(parsed, entry) { 781 parsed.keyboardSupport = entry; 782 783 if (typeof entry !== "boolean") { 784 throw new Error("noUiSlider (" + VERSION + "): 'keyboardSupport' option must be a boolean."); 785 } 786 } 787 788 function testDocumentElement(parsed, entry) { 789 // This is an advanced option. Passed values are used without validation. 790 parsed.documentElement = entry; 791 } 792 793 function testCssPrefix(parsed, entry) { 794 if (typeof entry !== "string" && entry !== false) { 795 throw new Error("noUiSlider (" + VERSION + "): 'cssPrefix' must be a string or `false`."); 796 } 797 798 parsed.cssPrefix = entry; 799 } 800 801 function testCssClasses(parsed, entry) { 802 if (typeof entry !== "object") { 803 throw new Error("noUiSlider (" + VERSION + "): 'cssClasses' must be an object."); 804 } 805 806 if (typeof parsed.cssPrefix === "string") { 807 parsed.cssClasses = {}; 808 809 for (var key in entry) { 810 if (!entry.hasOwnProperty(key)) { 811 continue; 812 } 813 814 parsed.cssClasses[key] = parsed.cssPrefix + entry[key]; 815 } 816 } else { 817 parsed.cssClasses = entry; 818 } 819 } 820 821 // Test all developer settings and parse to assumption-safe values. 822 function testOptions(options) { 823 // To prove a fix for #537, freeze options here. 824 // If the object is modified, an error will be thrown. 825 // Object.freeze(options); 826 827 var parsed = { 828 margin: 0, 829 limit: 0, 830 padding: 0, 831 animate: true, 832 animationDuration: 300, 833 ariaFormat: defaultFormatter, 834 format: defaultFormatter 835 }; 836 837 // Tests are executed in the order they are presented here. 838 var tests = { 839 step: { r: false, t: testStep }, 840 start: { r: true, t: testStart }, 841 connect: { r: true, t: testConnect }, 842 direction: { r: true, t: testDirection }, 843 snap: { r: false, t: testSnap }, 844 animate: { r: false, t: testAnimate }, 845 animationDuration: { r: false, t: testAnimationDuration }, 846 range: { r: true, t: testRange }, 847 orientation: { r: false, t: testOrientation }, 848 margin: { r: false, t: testMargin }, 849 limit: { r: false, t: testLimit }, 850 padding: { r: false, t: testPadding }, 851 behaviour: { r: true, t: testBehaviour }, 852 ariaFormat: { r: false, t: testAriaFormat }, 853 format: { r: false, t: testFormat }, 854 tooltips: { r: false, t: testTooltips }, 855 keyboardSupport: { r: true, t: testKeyboardSupport }, 856 documentElement: { r: false, t: testDocumentElement }, 857 cssPrefix: { r: true, t: testCssPrefix }, 858 cssClasses: { r: true, t: testCssClasses } 859 }; 860 861 var defaults = { 862 connect: false, 863 direction: "ltr", 864 behaviour: "tap", 865 orientation: "horizontal", 866 keyboardSupport: true, 867 cssPrefix: "noUi-", 868 cssClasses: { 869 target: "target", 870 base: "base", 871 origin: "origin", 872 handle: "handle", 873 handleLower: "handle-lower", 874 handleUpper: "handle-upper", 875 touchArea: "touch-area", 876 horizontal: "horizontal", 877 vertical: "vertical", 878 background: "background", 879 connect: "connect", 880 connects: "connects", 881 ltr: "ltr", 882 rtl: "rtl", 883 draggable: "draggable", 884 drag: "state-drag", 885 tap: "state-tap", 886 active: "active", 887 tooltip: "tooltip", 888 pips: "pips", 889 pipsHorizontal: "pips-horizontal", 890 pipsVertical: "pips-vertical", 891 marker: "marker", 892 markerHorizontal: "marker-horizontal", 893 markerVertical: "marker-vertical", 894 markerNormal: "marker-normal", 895 markerLarge: "marker-large", 896 markerSub: "marker-sub", 897 value: "value", 898 valueHorizontal: "value-horizontal", 899 valueVertical: "value-vertical", 900 valueNormal: "value-normal", 901 valueLarge: "value-large", 902 valueSub: "value-sub" 903 } 904 }; 905 906 // AriaFormat defaults to regular format, if any. 907 if (options.format && !options.ariaFormat) { 908 options.ariaFormat = options.format; 909 } 910 911 // Run all options through a testing mechanism to ensure correct 912 // input. It should be noted that options might get modified to 913 // be handled properly. E.g. wrapping integers in arrays. 914 Object.keys(tests).forEach(function(name) { 915 // If the option isn't set, but it is required, throw an error. 916 if (!isSet(options[name]) && defaults[name] === undefined) { 917 if (tests[name].r) { 918 throw new Error("noUiSlider (" + VERSION + "): '" + name + "' is required."); 919 } 920 921 return true; 922 } 923 924 tests[name].t(parsed, !isSet(options[name]) ? defaults[name] : options[name]); 925 }); 926 927 // Forward pips options 928 parsed.pips = options.pips; 929 930 // All recent browsers accept unprefixed transform. 931 // We need -ms- for IE9 and -webkit- for older Android; 932 // Assume use of -webkit- if unprefixed and -ms- are not supported. 933 // https://caniuse.com/#feat=transforms2d 934 var d = document.createElement("div"); 935 var msPrefix = d.style.msTransform !== undefined; 936 var noPrefix = d.style.transform !== undefined; 937 938 parsed.transformRule = noPrefix ? "transform" : msPrefix ? "msTransform" : "webkitTransform"; 939 940 // Pips don't move, so we can place them using left/top. 941 var styles = [["left", "top"], ["right", "bottom"]]; 942 943 parsed.style = styles[parsed.dir][parsed.ort]; 944 945 return parsed; 946 } 947 948 function scope(target, options, originalOptions) { 949 var actions = getActions(); 950 var supportsTouchActionNone = getSupportsTouchActionNone(); 951 var supportsPassive = supportsTouchActionNone && getSupportsPassive(); 952 953 // All variables local to 'scope' are prefixed with 'scope_' 954 955 // Slider DOM Nodes 956 var scope_Target = target; 957 var scope_Base; 958 var scope_Handles; 959 var scope_Connects; 960 var scope_Pips; 961 962 // Override for the 'animate' option 963 var scope_ShouldAnimate = true; 964 965 // Slider state values 966 var scope_Spectrum = options.spectrum; 967 var scope_Values = []; 968 var scope_Locations = []; 969 var scope_HandleNumbers = []; 970 var scope_ActiveHandlesCount = 0; 971 var scope_Events = {}; 972 973 // Exposed API 974 var scope_Self; 975 976 // Document Nodes 977 var scope_Document = target.ownerDocument; 978 var scope_DocumentElement = options.documentElement || scope_Document.documentElement; 979 var scope_Body = scope_Document.body; 980 981 // Pips constants 982 var PIPS_NONE = -1; 983 var PIPS_NO_VALUE = 0; 984 var PIPS_LARGE_VALUE = 1; 985 var PIPS_SMALL_VALUE = 2; 986 987 // For horizontal sliders in standard ltr documents, 988 // make .noUi-origin overflow to the left so the document doesn't scroll. 989 var scope_DirOffset = scope_Document.dir === "rtl" || options.ort === 1 ? 0 : 100; 990 991 // Creates a node, adds it to target, returns the new node. 992 function addNodeTo(addTarget, className) { 993 var div = scope_Document.createElement("div"); 994 995 if (className) { 996 addClass(div, className); 997 } 998 999 addTarget.appendChild(div); 1000 1001 return div; 1002 } 1003 1004 // Append a origin to the base 1005 function addOrigin(base, handleNumber) { 1006 var origin = addNodeTo(base, options.cssClasses.origin); 1007 var handle = addNodeTo(origin, options.cssClasses.handle); 1008 1009 addNodeTo(handle, options.cssClasses.touchArea); 1010 1011 handle.setAttribute("data-handle", handleNumber); 1012 1013 if (options.keyboardSupport) { 1014 // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex 1015 // 0 = focusable and reachable 1016 handle.setAttribute("tabindex", "0"); 1017 handle.addEventListener("keydown", function(event) { 1018 return eventKeydown(event, handleNumber); 1019 }); 1020 } 1021 1022 handle.setAttribute("role", "slider"); 1023 handle.setAttribute("aria-orientation", options.ort ? "vertical" : "horizontal"); 1024 1025 if (handleNumber === 0) { 1026 addClass(handle, options.cssClasses.handleLower); 1027 } else if (handleNumber === options.handles - 1) { 1028 addClass(handle, options.cssClasses.handleUpper); 1029 } 1030 1031 return origin; 1032 } 1033 1034 // Insert nodes for connect elements 1035 function addConnect(base, add) { 1036 if (!add) { 1037 return false; 1038 } 1039 1040 return addNodeTo(base, options.cssClasses.connect); 1041 } 1042 1043 // Add handles to the slider base. 1044 function addElements(connectOptions, base) { 1045 var connectBase = addNodeTo(base, options.cssClasses.connects); 1046 1047 scope_Handles = []; 1048 scope_Connects = []; 1049 1050 scope_Connects.push(addConnect(connectBase, connectOptions[0])); 1051 1052 // [::::O====O====O====] 1053 // connectOptions = [0, 1, 1, 1] 1054 1055 for (var i = 0; i < options.handles; i++) { 1056 // Keep a list of all added handles. 1057 scope_Handles.push(addOrigin(base, i)); 1058 scope_HandleNumbers[i] = i; 1059 scope_Connects.push(addConnect(connectBase, connectOptions[i + 1])); 1060 } 1061 } 1062 1063 // Initialize a single slider. 1064 function addSlider(addTarget) { 1065 // Apply classes and data to the target. 1066 addClass(addTarget, options.cssClasses.target); 1067 1068 if (options.dir === 0) { 1069 addClass(addTarget, options.cssClasses.ltr); 1070 } else { 1071 addClass(addTarget, options.cssClasses.rtl); 1072 } 1073 1074 if (options.ort === 0) { 1075 addClass(addTarget, options.cssClasses.horizontal); 1076 } else { 1077 addClass(addTarget, options.cssClasses.vertical); 1078 } 1079 1080 return addNodeTo(addTarget, options.cssClasses.base); 1081 } 1082 1083 function addTooltip(handle, handleNumber) { 1084 if (!options.tooltips[handleNumber]) { 1085 return false; 1086 } 1087 1088 return addNodeTo(handle.firstChild, options.cssClasses.tooltip); 1089 } 1090 1091 // Disable the slider dragging if any handle is disabled 1092 function isHandleDisabled(handleNumber) { 1093 var handleOrigin = scope_Handles[handleNumber]; 1094 return handleOrigin.hasAttribute("disabled"); 1095 } 1096 1097 // The tooltips option is a shorthand for using the 'update' event. 1098 function tooltips() { 1099 // Tooltips are added with options.tooltips in original order. 1100 var tips = scope_Handles.map(addTooltip); 1101 1102 bindEvent("update", function(values, handleNumber, unencoded) { 1103 if (!tips[handleNumber]) { 1104 return; 1105 } 1106 1107 var formattedValue = values[handleNumber]; 1108 1109 if (options.tooltips[handleNumber] !== true) { 1110 formattedValue = options.tooltips[handleNumber].to(unencoded[handleNumber]); 1111 } 1112 1113 tips[handleNumber].innerHTML = formattedValue; 1114 }); 1115 } 1116 1117 function aria() { 1118 bindEvent("update", function(values, handleNumber, unencoded, tap, positions) { 1119 // Update Aria Values for all handles, as a change in one changes min and max values for the next. 1120 scope_HandleNumbers.forEach(function(index) { 1121 var handle = scope_Handles[index]; 1122 1123 var min = checkHandlePosition(scope_Locations, index, 0, true, true, true); 1124 var max = checkHandlePosition(scope_Locations, index, 100, true, true, true); 1125 1126 var now = positions[index]; 1127 1128 // Formatted value for display 1129 var text = options.ariaFormat.to(unencoded[index]); 1130 1131 // Map to slider range values 1132 min = scope_Spectrum.fromStepping(min).toFixed(1); 1133 max = scope_Spectrum.fromStepping(max).toFixed(1); 1134 now = scope_Spectrum.fromStepping(now).toFixed(1); 1135 1136 handle.children[0].setAttribute("aria-valuemin", min); 1137 handle.children[0].setAttribute("aria-valuemax", max); 1138 handle.children[0].setAttribute("aria-valuenow", now); 1139 handle.children[0].setAttribute("aria-valuetext", text); 1140 }); 1141 }); 1142 } 1143 1144 function getGroup(mode, values, stepped) { 1145 // Use the range. 1146 if (mode === "range" || mode === "steps") { 1147 return scope_Spectrum.xVal; 1148 } 1149 1150 if (mode === "count") { 1151 if (values < 2) { 1152 throw new Error("noUiSlider (" + VERSION + "): 'values' (>= 2) required for mode 'count'."); 1153 } 1154 1155 // Divide 0 - 100 in 'count' parts. 1156 var interval = values - 1; 1157 var spread = 100 / interval; 1158 1159 values = []; 1160 1161 // List these parts and have them handled as 'positions'. 1162 while (interval--) { 1163 values[interval] = interval * spread; 1164 } 1165 1166 values.push(100); 1167 1168 mode = "positions"; 1169 } 1170 1171 if (mode === "positions") { 1172 // Map all percentages to on-range values. 1173 return values.map(function(value) { 1174 return scope_Spectrum.fromStepping(stepped ? scope_Spectrum.getStep(value) : value); 1175 }); 1176 } 1177 1178 if (mode === "values") { 1179 // If the value must be stepped, it needs to be converted to a percentage first. 1180 if (stepped) { 1181 return values.map(function(value) { 1182 // Convert to percentage, apply step, return to value. 1183 return scope_Spectrum.fromStepping(scope_Spectrum.getStep(scope_Spectrum.toStepping(value))); 1184 }); 1185 } 1186 1187 // Otherwise, we can simply use the values. 1188 return values; 1189 } 1190 } 1191 1192 function generateSpread(density, mode, group) { 1193 function safeIncrement(value, increment) { 1194 // Avoid floating point variance by dropping the smallest decimal places. 1195 return (value + increment).toFixed(7) / 1; 1196 } 1197 1198 var indexes = {}; 1199 var firstInRange = scope_Spectrum.xVal[0]; 1200 var lastInRange = scope_Spectrum.xVal[scope_Spectrum.xVal.length - 1]; 1201 var ignoreFirst = false; 1202 var ignoreLast = false; 1203 var prevPct = 0; 1204 1205 // Create a copy of the group, sort it and filter away all duplicates. 1206 group = unique( 1207 group.slice().sort(function(a, b) { 1208 return a - b; 1209 }) 1210 ); 1211 1212 // Make sure the range starts with the first element. 1213 if (group[0] !== firstInRange) { 1214 group.unshift(firstInRange); 1215 ignoreFirst = true; 1216 } 1217 1218 // Likewise for the last one. 1219 if (group[group.length - 1] !== lastInRange) { 1220 group.push(lastInRange); 1221 ignoreLast = true; 1222 } 1223 1224 group.forEach(function(current, index) { 1225 // Get the current step and the lower + upper positions. 1226 var step; 1227 var i; 1228 var q; 1229 var low = current; 1230 var high = group[index + 1]; 1231 var newPct; 1232 var pctDifference; 1233 var pctPos; 1234 var type; 1235 var steps; 1236 var realSteps; 1237 var stepSize; 1238 var isSteps = mode === "steps"; 1239 1240 // When using 'steps' mode, use the provided steps. 1241 // Otherwise, we'll step on to the next subrange. 1242 if (isSteps) { 1243 step = scope_Spectrum.xNumSteps[index]; 1244 } 1245 1246 // Default to a 'full' step. 1247 if (!step) { 1248 step = high - low; 1249 } 1250 1251 // Low can be 0, so test for false. If high is undefined, 1252 // we are at the last subrange. Index 0 is already handled. 1253 if (low === false || high === undefined) { 1254 return; 1255 } 1256 1257 // Make sure step isn't 0, which would cause an infinite loop (#654) 1258 step = Math.max(step, 0.0000001); 1259 1260 // Find all steps in the subrange. 1261 for (i = low; i <= high; i = safeIncrement(i, step)) { 1262 // Get the percentage value for the current step, 1263 // calculate the size for the subrange. 1264 newPct = scope_Spectrum.toStepping(i); 1265 pctDifference = newPct - prevPct; 1266 1267 steps = pctDifference / density; 1268 realSteps = Math.round(steps); 1269 1270 // This ratio represents the amount of percentage-space a point indicates. 1271 // For a density 1 the points/percentage = 1. For density 2, that percentage needs to be re-divided. 1272 // Round the percentage offset to an even number, then divide by two 1273 // to spread the offset on both sides of the range. 1274 stepSize = pctDifference / realSteps; 1275 1276 // Divide all points evenly, adding the correct number to this subrange. 1277 // Run up to <= so that 100% gets a point, event if ignoreLast is set. 1278 for (q = 1; q <= realSteps; q += 1) { 1279 // The ratio between the rounded value and the actual size might be ~1% off. 1280 // Correct the percentage offset by the number of points 1281 // per subrange. density = 1 will result in 100 points on the 1282 // full range, 2 for 50, 4 for 25, etc. 1283 pctPos = prevPct + q * stepSize; 1284 indexes[pctPos.toFixed(5)] = [scope_Spectrum.fromStepping(pctPos), 0]; 1285 } 1286 1287 // Determine the point type. 1288 type = group.indexOf(i) > -1 ? PIPS_LARGE_VALUE : isSteps ? PIPS_SMALL_VALUE : PIPS_NO_VALUE; 1289 1290 // Enforce the 'ignoreFirst' option by overwriting the type for 0. 1291 if (!index && ignoreFirst) { 1292 type = 0; 1293 } 1294 1295 if (!(i === high && ignoreLast)) { 1296 // Mark the 'type' of this point. 0 = plain, 1 = real value, 2 = step value. 1297 indexes[newPct.toFixed(5)] = [i, type]; 1298 } 1299 1300 // Update the percentage count. 1301 prevPct = newPct; 1302 } 1303 }); 1304 1305 return indexes; 1306 } 1307 1308 function addMarking(spread, filterFunc, formatter) { 1309 var element = scope_Document.createElement("div"); 1310 1311 var valueSizeClasses = []; 1312 valueSizeClasses[PIPS_NO_VALUE] = options.cssClasses.valueNormal; 1313 valueSizeClasses[PIPS_LARGE_VALUE] = options.cssClasses.valueLarge; 1314 valueSizeClasses[PIPS_SMALL_VALUE] = options.cssClasses.valueSub; 1315 1316 var markerSizeClasses = []; 1317 markerSizeClasses[PIPS_NO_VALUE] = options.cssClasses.markerNormal; 1318 markerSizeClasses[PIPS_LARGE_VALUE] = options.cssClasses.markerLarge; 1319 markerSizeClasses[PIPS_SMALL_VALUE] = options.cssClasses.markerSub; 1320 1321 var valueOrientationClasses = [options.cssClasses.valueHorizontal, options.cssClasses.valueVertical]; 1322 var markerOrientationClasses = [options.cssClasses.markerHorizontal, options.cssClasses.markerVertical]; 1323 1324 addClass(element, options.cssClasses.pips); 1325 addClass(element, options.ort === 0 ? options.cssClasses.pipsHorizontal : options.cssClasses.pipsVertical); 1326 1327 function getClasses(type, source) { 1328 var a = source === options.cssClasses.value; 1329 var orientationClasses = a ? valueOrientationClasses : markerOrientationClasses; 1330 var sizeClasses = a ? valueSizeClasses : markerSizeClasses; 1331 1332 return source + " " + orientationClasses[options.ort] + " " + sizeClasses[type]; 1333 } 1334 1335 function addSpread(offset, value, type) { 1336 // Apply the filter function, if it is set. 1337 type = filterFunc ? filterFunc(value, type) : type; 1338 1339 if (type === PIPS_NONE) { 1340 return; 1341 } 1342 1343 // Add a marker for every point 1344 var node = addNodeTo(element, false); 1345 node.className = getClasses(type, options.cssClasses.marker); 1346 node.style[options.style] = offset + "%"; 1347 1348 // Values are only appended for points marked '1' or '2'. 1349 if (type > PIPS_NO_VALUE) { 1350 node = addNodeTo(element, false); 1351 node.className = getClasses(type, options.cssClasses.value); 1352 node.setAttribute("data-value", value); 1353 node.style[options.style] = offset + "%"; 1354 node.innerHTML = formatter.to(value); 1355 } 1356 } 1357 1358 // Append all points. 1359 Object.keys(spread).forEach(function(offset) { 1360 addSpread(offset, spread[offset][0], spread[offset][1]); 1361 }); 1362 1363 return element; 1364 } 1365 1366 function removePips() { 1367 if (scope_Pips) { 1368 removeElement(scope_Pips); 1369 scope_Pips = null; 1370 } 1371 } 1372 1373 function pips(grid) { 1374 // Fix #669 1375 removePips(); 1376 1377 var mode = grid.mode; 1378 var density = grid.density || 1; 1379 var filter = grid.filter || false; 1380 var values = grid.values || false; 1381 var stepped = grid.stepped || false; 1382 var group = getGroup(mode, values, stepped); 1383 var spread = generateSpread(density, mode, group); 1384 var format = grid.format || { 1385 to: Math.round 1386 }; 1387 1388 scope_Pips = scope_Target.appendChild(addMarking(spread, filter, format)); 1389 1390 return scope_Pips; 1391 } 1392 1393 // Shorthand for base dimensions. 1394 function baseSize() { 1395 var rect = scope_Base.getBoundingClientRect(); 1396 var alt = "offset" + ["Width", "Height"][options.ort]; 1397 return options.ort === 0 ? rect.width || scope_Base[alt] : rect.height || scope_Base[alt]; 1398 } 1399 1400 // Handler for attaching events trough a proxy. 1401 function attachEvent(events, element, callback, data) { 1402 // This function can be used to 'filter' events to the slider. 1403 // element is a node, not a nodeList 1404 1405 var method = function(e) { 1406 e = fixEvent(e, data.pageOffset, data.target || element); 1407 1408 // fixEvent returns false if this event has a different target 1409 // when handling (multi-) touch events; 1410 if (!e) { 1411 return false; 1412 } 1413 1414 // doNotReject is passed by all end events to make sure released touches 1415 // are not rejected, leaving the slider "stuck" to the cursor; 1416 if (scope_Target.hasAttribute("disabled") && !data.doNotReject) { 1417 return false; 1418 } 1419 1420 // Stop if an active 'tap' transition is taking place. 1421 if (hasClass(scope_Target, options.cssClasses.tap) && !data.doNotReject) { 1422 return false; 1423 } 1424 1425 // Ignore right or middle clicks on start #454 1426 if (events === actions.start && e.buttons !== undefined && e.buttons > 1) { 1427 return false; 1428 } 1429 1430 // Ignore right or middle clicks on start #454 1431 if (data.hover && e.buttons) { 1432 return false; 1433 } 1434 1435 // 'supportsPassive' is only true if a browser also supports touch-action: none in CSS. 1436 // iOS safari does not, so it doesn't get to benefit from passive scrolling. iOS does support 1437 // touch-action: manipulation, but that allows panning, which breaks 1438 // sliders after zooming/on non-responsive pages. 1439 // See: https://bugs.webkit.org/show_bug.cgi?id=133112 1440 if (!supportsPassive) { 1441 e.preventDefault(); 1442 } 1443 1444 e.calcPoint = e.points[options.ort]; 1445 1446 // Call the event handler with the event [ and additional data ]. 1447 callback(e, data); 1448 }; 1449 1450 var methods = []; 1451 1452 // Bind a closure on the target for every event type. 1453 events.split(" ").forEach(function(eventName) { 1454 element.addEventListener(eventName, method, supportsPassive ? { passive: true } : false); 1455 methods.push([eventName, method]); 1456 }); 1457 1458 return methods; 1459 } 1460 1461 // Provide a clean event with standardized offset values. 1462 function fixEvent(e, pageOffset, eventTarget) { 1463 // Filter the event to register the type, which can be 1464 // touch, mouse or pointer. Offset changes need to be 1465 // made on an event specific basis. 1466 var touch = e.type.indexOf("touch") === 0; 1467 var mouse = e.type.indexOf("mouse") === 0; 1468 var pointer = e.type.indexOf("pointer") === 0; 1469 1470 var x; 1471 var y; 1472 1473 // IE10 implemented pointer events with a prefix; 1474 if (e.type.indexOf("MSPointer") === 0) { 1475 pointer = true; 1476 } 1477 1478 // The only thing one handle should be concerned about is the touches that originated on top of it. 1479 if (touch) { 1480 // Returns true if a touch originated on the target. 1481 var isTouchOnTarget = function(checkTouch) { 1482 return checkTouch.target === eventTarget || eventTarget.contains(checkTouch.target); 1483 }; 1484 1485 // In the case of touchstart events, we need to make sure there is still no more than one 1486 // touch on the target so we look amongst all touches. 1487 if (e.type === "touchstart") { 1488 var targetTouches = Array.prototype.filter.call(e.touches, isTouchOnTarget); 1489 1490 // Do not support more than one touch per handle. 1491 if (targetTouches.length > 1) { 1492 return false; 1493 } 1494 1495 x = targetTouches[0].pageX; 1496 y = targetTouches[0].pageY; 1497 } else { 1498 // In the other cases, find on changedTouches is enough. 1499 var targetTouch = Array.prototype.find.call(e.changedTouches, isTouchOnTarget); 1500 1501 // Cancel if the target touch has not moved. 1502 if (!targetTouch) { 1503 return false; 1504 } 1505 1506 x = targetTouch.pageX; 1507 y = targetTouch.pageY; 1508 } 1509 } 1510 1511 pageOffset = pageOffset || getPageOffset(scope_Document); 1512 1513 if (mouse || pointer) { 1514 x = e.clientX + pageOffset.x; 1515 y = e.clientY + pageOffset.y; 1516 } 1517 1518 e.pageOffset = pageOffset; 1519 e.points = [x, y]; 1520 e.cursor = mouse || pointer; // Fix #435 1521 1522 return e; 1523 } 1524 1525 // Translate a coordinate in the document to a percentage on the slider 1526 function calcPointToPercentage(calcPoint) { 1527 var location = calcPoint - offset(scope_Base, options.ort); 1528 var proposal = (location * 100) / baseSize(); 1529 1530 // Clamp proposal between 0% and 100% 1531 // Out-of-bound coordinates may occur when .noUi-base pseudo-elements 1532 // are used (e.g. contained handles feature) 1533 proposal = limit(proposal); 1534 1535 return options.dir ? 100 - proposal : proposal; 1536 } 1537 1538 // Find handle closest to a certain percentage on the slider 1539 function getClosestHandle(proposal) { 1540 var closest = 100; 1541 var handleNumber = false; 1542 1543 scope_Handles.forEach(function(handle, index) { 1544 // Disabled handles are ignored 1545 if (isHandleDisabled(index)) { 1546 return; 1547 } 1548 1549 var pos = Math.abs(scope_Locations[index] - proposal); 1550 1551 if (pos < closest || (pos === 100 && closest === 100)) { 1552 handleNumber = index; 1553 closest = pos; 1554 } 1555 }); 1556 1557 return handleNumber; 1558 } 1559 1560 // Fire 'end' when a mouse or pen leaves the document. 1561 function documentLeave(event, data) { 1562 if (event.type === "mouseout" && event.target.nodeName === "HTML" && event.relatedTarget === null) { 1563 eventEnd(event, data); 1564 } 1565 } 1566 1567 // Handle movement on document for handle and range drag. 1568 function eventMove(event, data) { 1569 // Fix #498 1570 // Check value of .buttons in 'start' to work around a bug in IE10 mobile (data.buttonsProperty). 1571 // https://connect.microsoft.com/IE/feedback/details/927005/mobile-ie10-windows-phone-buttons-property-of-pointermove-event-always-zero 1572 // IE9 has .buttons and .which zero on mousemove. 1573 // Firefox breaks the spec MDN defines. 1574 if (navigator.appVersion.indexOf("MSIE 9") === -1 && event.buttons === 0 && data.buttonsProperty !== 0) { 1575 return eventEnd(event, data); 1576 } 1577 1578 // Check if we are moving up or down 1579 var movement = (options.dir ? -1 : 1) * (event.calcPoint - data.startCalcPoint); 1580 1581 // Convert the movement into a percentage of the slider width/height 1582 var proposal = (movement * 100) / data.baseSize; 1583 1584 moveHandles(movement > 0, proposal, data.locations, data.handleNumbers); 1585 } 1586 1587 // Unbind move events on document, call callbacks. 1588 function eventEnd(event, data) { 1589 // The handle is no longer active, so remove the class. 1590 if (data.handle) { 1591 removeClass(data.handle, options.cssClasses.active); 1592 scope_ActiveHandlesCount -= 1; 1593 } 1594 1595 // Unbind the move and end events, which are added on 'start'. 1596 data.listeners.forEach(function(c) { 1597 scope_DocumentElement.removeEventListener(c[0], c[1]); 1598 }); 1599 1600 if (scope_ActiveHandlesCount === 0) { 1601 // Remove dragging class. 1602 removeClass(scope_Target, options.cssClasses.drag); 1603 setZindex(); 1604 1605 // Remove cursor styles and text-selection events bound to the body. 1606 if (event.cursor) { 1607 scope_Body.style.cursor = ""; 1608 scope_Body.removeEventListener("selectstart", preventDefault); 1609 } 1610 } 1611 1612 data.handleNumbers.forEach(function(handleNumber) { 1613 fireEvent("change", handleNumber); 1614 fireEvent("set", handleNumber); 1615 fireEvent("end", handleNumber); 1616 }); 1617 } 1618 1619 // Bind move events on document. 1620 function eventStart(event, data) { 1621 // Ignore event if any handle is disabled 1622 if (data.handleNumbers.some(isHandleDisabled)) { 1623 return false; 1624 } 1625 1626 var handle; 1627 1628 if (data.handleNumbers.length === 1) { 1629 var handleOrigin = scope_Handles[data.handleNumbers[0]]; 1630 1631 handle = handleOrigin.children[0]; 1632 scope_ActiveHandlesCount += 1; 1633 1634 // Mark the handle as 'active' so it can be styled. 1635 addClass(handle, options.cssClasses.active); 1636 } 1637 1638 // A drag should never propagate up to the 'tap' event. 1639 event.stopPropagation(); 1640 1641 // Record the event listeners. 1642 var listeners = []; 1643 1644 // Attach the move and end events. 1645 var moveEvent = attachEvent(actions.move, scope_DocumentElement, eventMove, { 1646 // The event target has changed so we need to propagate the original one so that we keep 1647 // relying on it to extract target touches. 1648 target: event.target, 1649 handle: handle, 1650 listeners: listeners, 1651 startCalcPoint: event.calcPoint, 1652 baseSize: baseSize(), 1653 pageOffset: event.pageOffset, 1654 handleNumbers: data.handleNumbers, 1655 buttonsProperty: event.buttons, 1656 locations: scope_Locations.slice() 1657 }); 1658 1659 var endEvent = attachEvent(actions.end, scope_DocumentElement, eventEnd, { 1660 target: event.target, 1661 handle: handle, 1662 listeners: listeners, 1663 doNotReject: true, 1664 handleNumbers: data.handleNumbers 1665 }); 1666 1667 var outEvent = attachEvent("mouseout", scope_DocumentElement, documentLeave, { 1668 target: event.target, 1669 handle: handle, 1670 listeners: listeners, 1671 doNotReject: true, 1672 handleNumbers: data.handleNumbers 1673 }); 1674 1675 // We want to make sure we pushed the listeners in the listener list rather than creating 1676 // a new one as it has already been passed to the event handlers. 1677 listeners.push.apply(listeners, moveEvent.concat(endEvent, outEvent)); 1678 1679 // Text selection isn't an issue on touch devices, 1680 // so adding cursor styles can be skipped. 1681 if (event.cursor) { 1682 // Prevent the 'I' cursor and extend the range-drag cursor. 1683 scope_Body.style.cursor = getComputedStyle(event.target).cursor; 1684 1685 // Mark the target with a dragging state. 1686 if (scope_Handles.length > 1) { 1687 addClass(scope_Target, options.cssClasses.drag); 1688 } 1689 1690 // Prevent text selection when dragging the handles. 1691 // In noUiSlider <= 9.2.0, this was handled by calling preventDefault on mouse/touch start/move, 1692 // which is scroll blocking. The selectstart event is supported by FireFox starting from version 52, 1693 // meaning the only holdout is iOS Safari. This doesn't matter: text selection isn't triggered there. 1694 // The 'cursor' flag is false. 1695 // See: http://caniuse.com/#search=selectstart 1696 scope_Body.addEventListener("selectstart", preventDefault, false); 1697 } 1698 1699 data.handleNumbers.forEach(function(handleNumber) { 1700 fireEvent("start", handleNumber); 1701 }); 1702 } 1703 1704 // Move closest handle to tapped location. 1705 function eventTap(event) { 1706 // The tap event shouldn't propagate up 1707 event.stopPropagation(); 1708 1709 var proposal = calcPointToPercentage(event.calcPoint); 1710 var handleNumber = getClosestHandle(proposal); 1711 1712 // Tackle the case that all handles are 'disabled'. 1713 if (handleNumber === false) { 1714 return false; 1715 } 1716 1717 // Flag the slider as it is now in a transitional state. 1718 // Transition takes a configurable amount of ms (default 300). Re-enable the slider after that. 1719 if (!options.events.snap) { 1720 addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration); 1721 } 1722 1723 setHandle(handleNumber, proposal, true, true); 1724 1725 setZindex(); 1726 1727 fireEvent("slide", handleNumber, true); 1728 fireEvent("update", handleNumber, true); 1729 fireEvent("change", handleNumber, true); 1730 fireEvent("set", handleNumber, true); 1731 1732 if (options.events.snap) { 1733 eventStart(event, { handleNumbers: [handleNumber] }); 1734 } 1735 } 1736 1737 // Fires a 'hover' event for a hovered mouse/pen position. 1738 function eventHover(event) { 1739 var proposal = calcPointToPercentage(event.calcPoint); 1740 1741 var to = scope_Spectrum.getStep(proposal); 1742 var value = scope_Spectrum.fromStepping(to); 1743 1744 Object.keys(scope_Events).forEach(function(targetEvent) { 1745 if ("hover" === targetEvent.split(".")[0]) { 1746 scope_Events[targetEvent].forEach(function(callback) { 1747 callback.call(scope_Self, value); 1748 }); 1749 } 1750 }); 1751 } 1752 1753 // Handles keydown on focused handles 1754 // Don't move the document when pressing arrow keys on focused handles 1755 function eventKeydown(event, handleNumber) { 1756 if (isHandleDisabled(handleNumber)) { 1757 return false; 1758 } 1759 1760 var horizontalKeys = ["Left", "Right"]; 1761 var verticalKeys = ["Down", "Up"]; 1762 1763 if (options.dir && !options.ort) { 1764 // On an right-to-left slider, the left and right keys act inverted 1765 horizontalKeys.reverse(); 1766 } else if (options.ort && !options.dir) { 1767 // On a top-to-bottom slider, the up and down keys act inverted 1768 verticalKeys.reverse(); 1769 } 1770 1771 // Strip "Arrow" for IE compatibility. https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key 1772 var key = event.key.replace("Arrow", ""); 1773 var isDown = key === verticalKeys[0] || key === horizontalKeys[0]; 1774 var isUp = key === verticalKeys[1] || key === horizontalKeys[1]; 1775 1776 if (!isDown && !isUp) { 1777 return true; 1778 } 1779 1780 event.preventDefault(); 1781 1782 var direction = isDown ? 0 : 1; 1783 var steps = getNextStepsForHandle(handleNumber); 1784 var step = steps[direction]; 1785 1786 // At the edge of a slider, do nothing 1787 if (step === null) { 1788 return false; 1789 } 1790 1791 // No step set, use the default of 10% of the sub-range 1792 if (step === false) { 1793 step = scope_Spectrum.getDefaultStep(scope_Locations[handleNumber], isDown, 10); 1794 } 1795 1796 // Decrement for down steps 1797 step = (isDown ? -1 : 1) * step; 1798 1799 scope_ShouldAnimate = false; 1800 1801 valueSetHandle(handleNumber, scope_Values[handleNumber] + step, true); 1802 1803 scope_ShouldAnimate = true; 1804 1805 return false; 1806 } 1807 1808 // Attach events to several slider parts. 1809 function bindSliderEvents(behaviour) { 1810 // Attach the standard drag event to the handles. 1811 if (!behaviour.fixed) { 1812 scope_Handles.forEach(function(handle, index) { 1813 // These events are only bound to the visual handle 1814 // element, not the 'real' origin element. 1815 attachEvent(actions.start, handle.children[0], eventStart, { 1816 handleNumbers: [index] 1817 }); 1818 }); 1819 } 1820 1821 // Attach the tap event to the slider base. 1822 if (behaviour.tap) { 1823 attachEvent(actions.start, scope_Base, eventTap, {}); 1824 } 1825 1826 // Fire hover events 1827 if (behaviour.hover) { 1828 attachEvent(actions.move, scope_Base, eventHover, { 1829 hover: true 1830 }); 1831 } 1832 1833 // Make the range draggable. 1834 if (behaviour.drag) { 1835 scope_Connects.forEach(function(connect, index) { 1836 if (connect === false || index === 0 || index === scope_Connects.length - 1) { 1837 return; 1838 } 1839 1840 var handleBefore = scope_Handles[index - 1]; 1841 var handleAfter = scope_Handles[index]; 1842 var eventHolders = [connect]; 1843 1844 addClass(connect, options.cssClasses.draggable); 1845 1846 // When the range is fixed, the entire range can 1847 // be dragged by the handles. The handle in the first 1848 // origin will propagate the start event upward, 1849 // but it needs to be bound manually on the other. 1850 if (behaviour.fixed) { 1851 eventHolders.push(handleBefore.children[0]); 1852 eventHolders.push(handleAfter.children[0]); 1853 } 1854 1855 eventHolders.forEach(function(eventHolder) { 1856 attachEvent(actions.start, eventHolder, eventStart, { 1857 handles: [handleBefore, handleAfter], 1858 handleNumbers: [index - 1, index] 1859 }); 1860 }); 1861 }); 1862 } 1863 } 1864 1865 // Attach an event to this slider, possibly including a namespace 1866 function bindEvent(namespacedEvent, callback) { 1867 scope_Events[namespacedEvent] = scope_Events[namespacedEvent] || []; 1868 scope_Events[namespacedEvent].push(callback); 1869 1870 // If the event bound is 'update,' fire it immediately for all handles. 1871 if (namespacedEvent.split(".")[0] === "update") { 1872 scope_Handles.forEach(function(a, index) { 1873 fireEvent("update", index); 1874 }); 1875 } 1876 } 1877 1878 // Undo attachment of event 1879 function removeEvent(namespacedEvent) { 1880 var event = namespacedEvent && namespacedEvent.split(".")[0]; 1881 var namespace = event && namespacedEvent.substring(event.length); 1882 1883 Object.keys(scope_Events).forEach(function(bind) { 1884 var tEvent = bind.split(".")[0]; 1885 var tNamespace = bind.substring(tEvent.length); 1886 1887 if ((!event || event === tEvent) && (!namespace || namespace === tNamespace)) { 1888 delete scope_Events[bind]; 1889 } 1890 }); 1891 } 1892 1893 // External event handling 1894 function fireEvent(eventName, handleNumber, tap) { 1895 Object.keys(scope_Events).forEach(function(targetEvent) { 1896 var eventType = targetEvent.split(".")[0]; 1897 1898 if (eventName === eventType) { 1899 scope_Events[targetEvent].forEach(function(callback) { 1900 callback.call( 1901 // Use the slider public API as the scope ('this') 1902 scope_Self, 1903 // Return values as array, so arg_1[arg_2] is always valid. 1904 scope_Values.map(options.format.to), 1905 // Handle index, 0 or 1 1906 handleNumber, 1907 // Un-formatted slider values 1908 scope_Values.slice(), 1909 // Event is fired by tap, true or false 1910 tap || false, 1911 // Left offset of the handle, in relation to the slider 1912 scope_Locations.slice() 1913 ); 1914 }); 1915 } 1916 }); 1917 } 1918 1919 // Split out the handle positioning logic so the Move event can use it, too 1920 function checkHandlePosition(reference, handleNumber, to, lookBackward, lookForward, getValue) { 1921 // For sliders with multiple handles, limit movement to the other handle. 1922 // Apply the margin option by adding it to the handle positions. 1923 if (scope_Handles.length > 1 && !options.events.unconstrained) { 1924 if (lookBackward && handleNumber > 0) { 1925 to = Math.max(to, reference[handleNumber - 1] + options.margin); 1926 } 1927 1928 if (lookForward && handleNumber < scope_Handles.length - 1) { 1929 to = Math.min(to, reference[handleNumber + 1] - options.margin); 1930 } 1931 } 1932 1933 // The limit option has the opposite effect, limiting handles to a 1934 // maximum distance from another. Limit must be > 0, as otherwise 1935 // handles would be unmovable. 1936 if (scope_Handles.length > 1 && options.limit) { 1937 if (lookBackward && handleNumber > 0) { 1938 to = Math.min(to, reference[handleNumber - 1] + options.limit); 1939 } 1940 1941 if (lookForward && handleNumber < scope_Handles.length - 1) { 1942 to = Math.max(to, reference[handleNumber + 1] - options.limit); 1943 } 1944 } 1945 1946 // The padding option keeps the handles a certain distance from the 1947 // edges of the slider. Padding must be > 0. 1948 if (options.padding) { 1949 if (handleNumber === 0) { 1950 to = Math.max(to, options.padding[0]); 1951 } 1952 1953 if (handleNumber === scope_Handles.length - 1) { 1954 to = Math.min(to, 100 - options.padding[1]); 1955 } 1956 } 1957 1958 to = scope_Spectrum.getStep(to); 1959 1960 // Limit percentage to the 0 - 100 range 1961 to = limit(to); 1962 1963 // Return false if handle can't move 1964 if (to === reference[handleNumber] && !getValue) { 1965 return false; 1966 } 1967 1968 return to; 1969 } 1970 1971 // Uses slider orientation to create CSS rules. a = base value; 1972 function inRuleOrder(v, a) { 1973 var o = options.ort; 1974 return (o ? a : v) + ", " + (o ? v : a); 1975 } 1976 1977 // Moves handle(s) by a percentage 1978 // (bool, % to move, [% where handle started, ...], [index in scope_Handles, ...]) 1979 function moveHandles(upward, proposal, locations, handleNumbers) { 1980 var proposals = locations.slice(); 1981 1982 var b = [!upward, upward]; 1983 var f = [upward, !upward]; 1984 1985 // Copy handleNumbers so we don't change the dataset 1986 handleNumbers = handleNumbers.slice(); 1987 1988 // Check to see which handle is 'leading'. 1989 // If that one can't move the second can't either. 1990 if (upward) { 1991 handleNumbers.reverse(); 1992 } 1993 1994 // Step 1: get the maximum percentage that any of the handles can move 1995 if (handleNumbers.length > 1) { 1996 handleNumbers.forEach(function(handleNumber, o) { 1997 var to = checkHandlePosition( 1998 proposals, 1999 handleNumber, 2000 proposals[handleNumber] + proposal, 2001 b[o], 2002 f[o], 2003 false 2004 ); 2005 2006 // Stop if one of the handles can't move. 2007 if (to === false) { 2008 proposal = 0; 2009 } else { 2010 proposal = to - proposals[handleNumber]; 2011 proposals[handleNumber] = to; 2012 } 2013 }); 2014 } 2015 2016 // If using one handle, check backward AND forward 2017 else { 2018 b = f = [true]; 2019 } 2020 2021 var state = false; 2022 2023 // Step 2: Try to set the handles with the found percentage 2024 handleNumbers.forEach(function(handleNumber, o) { 2025 state = setHandle(handleNumber, locations[handleNumber] + proposal, b[o], f[o]) || state; 2026 }); 2027 2028 // Step 3: If a handle moved, fire events 2029 if (state) { 2030 handleNumbers.forEach(function(handleNumber) { 2031 fireEvent("update", handleNumber); 2032 fireEvent("slide", handleNumber); 2033 }); 2034 } 2035 } 2036 2037 // Takes a base value and an offset. This offset is used for the connect bar size. 2038 // In the initial design for this feature, the origin element was 1% wide. 2039 // Unfortunately, a rounding bug in Chrome makes it impossible to implement this feature 2040 // in this manner: https://bugs.chromium.org/p/chromium/issues/detail?id=798223 2041 function transformDirection(a, b) { 2042 return options.dir ? 100 - a - b : a; 2043 } 2044 2045 // Updates scope_Locations and scope_Values, updates visual state 2046 function updateHandlePosition(handleNumber, to) { 2047 // Update locations. 2048 scope_Locations[handleNumber] = to; 2049 2050 // Convert the value to the slider stepping/range. 2051 scope_Values[handleNumber] = scope_Spectrum.fromStepping(to); 2052 2053 var rule = "translate(" + inRuleOrder(transformDirection(to, 0) - scope_DirOffset + "%", "0") + ")"; 2054 scope_Handles[handleNumber].style[options.transformRule] = rule; 2055 2056 updateConnect(handleNumber); 2057 updateConnect(handleNumber + 1); 2058 } 2059 2060 // Handles before the slider middle are stacked later = higher, 2061 // Handles after the middle later is lower 2062 // [[7] [8] .......... | .......... [5] [4] 2063 function setZindex() { 2064 scope_HandleNumbers.forEach(function(handleNumber) { 2065 var dir = scope_Locations[handleNumber] > 50 ? -1 : 1; 2066 var zIndex = 3 + (scope_Handles.length + dir * handleNumber); 2067 scope_Handles[handleNumber].style.zIndex = zIndex; 2068 }); 2069 } 2070 2071 // Test suggested values and apply margin, step. 2072 function setHandle(handleNumber, to, lookBackward, lookForward) { 2073 to = checkHandlePosition(scope_Locations, handleNumber, to, lookBackward, lookForward, false); 2074 2075 if (to === false) { 2076 return false; 2077 } 2078 2079 updateHandlePosition(handleNumber, to); 2080 2081 return true; 2082 } 2083 2084 // Updates style attribute for connect nodes 2085 function updateConnect(index) { 2086 // Skip connects set to false 2087 if (!scope_Connects[index]) { 2088 return; 2089 } 2090 2091 var l = 0; 2092 var h = 100; 2093 2094 if (index !== 0) { 2095 l = scope_Locations[index - 1]; 2096 } 2097 2098 if (index !== scope_Connects.length - 1) { 2099 h = scope_Locations[index]; 2100 } 2101 2102 // We use two rules: 2103 // 'translate' to change the left/top offset; 2104 // 'scale' to change the width of the element; 2105 // As the element has a width of 100%, a translation of 100% is equal to 100% of the parent (.noUi-base) 2106 var connectWidth = h - l; 2107 var translateRule = "translate(" + inRuleOrder(transformDirection(l, connectWidth) + "%", "0") + ")"; 2108 var scaleRule = "scale(" + inRuleOrder(connectWidth / 100, "1") + ")"; 2109 2110 scope_Connects[index].style[options.transformRule] = translateRule + " " + scaleRule; 2111 } 2112 2113 // Parses value passed to .set method. Returns current value if not parse-able. 2114 function resolveToValue(to, handleNumber) { 2115 // Setting with null indicates an 'ignore'. 2116 // Inputting 'false' is invalid. 2117 if (to === null || to === false || to === undefined) { 2118 return scope_Locations[handleNumber]; 2119 } 2120 2121 // If a formatted number was passed, attempt to decode it. 2122 if (typeof to === "number") { 2123 to = String(to); 2124 } 2125 2126 to = options.format.from(to); 2127 to = scope_Spectrum.toStepping(to); 2128 2129 // If parsing the number failed, use the current value. 2130 if (to === false || isNaN(to)) { 2131 return scope_Locations[handleNumber]; 2132 } 2133 2134 return to; 2135 } 2136 2137 // Set the slider value. 2138 function valueSet(input, fireSetEvent) { 2139 var values = asArray(input); 2140 var isInit = scope_Locations[0] === undefined; 2141 2142 // Event fires by default 2143 fireSetEvent = fireSetEvent === undefined ? true : !!fireSetEvent; 2144 2145 // Animation is optional. 2146 // Make sure the initial values were set before using animated placement. 2147 if (options.animate && !isInit && scope_ShouldAnimate) { 2148 addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration); 2149 } 2150 2151 // First pass, without lookAhead but with lookBackward. Values are set from left to right. 2152 scope_HandleNumbers.forEach(function(handleNumber) { 2153 setHandle(handleNumber, resolveToValue(values[handleNumber], handleNumber), true, false); 2154 }); 2155 2156 // Second pass. Now that all base values are set, apply constraints 2157 scope_HandleNumbers.forEach(function(handleNumber) { 2158 setHandle(handleNumber, scope_Locations[handleNumber], true, true); 2159 }); 2160 2161 setZindex(); 2162 2163 scope_HandleNumbers.forEach(function(handleNumber) { 2164 fireEvent("update", handleNumber); 2165 2166 // Fire the event only for handles that received a new value, as per #579 2167 if (values[handleNumber] !== null && fireSetEvent) { 2168 fireEvent("set", handleNumber); 2169 } 2170 }); 2171 } 2172 2173 // Reset slider to initial values 2174 function valueReset(fireSetEvent) { 2175 valueSet(options.start, fireSetEvent); 2176 } 2177 2178 // Set value for a single handle 2179 function valueSetHandle(handleNumber, value, fireSetEvent) { 2180 var values = []; 2181 2182 // Ensure numeric input 2183 handleNumber = Number(handleNumber); 2184 2185 if (!(handleNumber >= 0 && handleNumber < scope_HandleNumbers.length)) { 2186 throw new Error("noUiSlider (" + VERSION + "): invalid handle number, got: " + handleNumber); 2187 } 2188 2189 for (var i = 0; i < scope_HandleNumbers.length; i++) { 2190 values[i] = null; 2191 } 2192 2193 values[handleNumber] = value; 2194 2195 valueSet(values, fireSetEvent); 2196 } 2197 2198 // Get the slider value. 2199 function valueGet() { 2200 var values = scope_Values.map(options.format.to); 2201 2202 // If only one handle is used, return a single value. 2203 if (values.length === 1) { 2204 return values[0]; 2205 } 2206 2207 return values; 2208 } 2209 2210 // Removes classes from the root and empties it. 2211 function destroy() { 2212 for (var key in options.cssClasses) { 2213 if (!options.cssClasses.hasOwnProperty(key)) { 2214 continue; 2215 } 2216 removeClass(scope_Target, options.cssClasses[key]); 2217 } 2218 2219 while (scope_Target.firstChild) { 2220 scope_Target.removeChild(scope_Target.firstChild); 2221 } 2222 2223 delete scope_Target.noUiSlider; 2224 } 2225 2226 function getNextStepsForHandle(handleNumber) { 2227 var location = scope_Locations[handleNumber]; 2228 var nearbySteps = scope_Spectrum.getNearbySteps(location); 2229 var value = scope_Values[handleNumber]; 2230 var increment = nearbySteps.thisStep.step; 2231 var decrement = null; 2232 2233 // If the next value in this step moves into the next step, 2234 // the increment is the start of the next step - the current value 2235 if (increment !== false) { 2236 if (value + increment > nearbySteps.stepAfter.startValue) { 2237 increment = nearbySteps.stepAfter.startValue - value; 2238 } 2239 } 2240 2241 // If the value is beyond the starting point 2242 if (value > nearbySteps.thisStep.startValue) { 2243 decrement = nearbySteps.thisStep.step; 2244 } else if (nearbySteps.stepBefore.step === false) { 2245 decrement = false; 2246 } 2247 2248 // If a handle is at the start of a step, it always steps back into the previous step first 2249 else { 2250 decrement = value - nearbySteps.stepBefore.highestStep; 2251 } 2252 2253 // Now, if at the slider edges, there is no in/decrement 2254 if (location === 100) { 2255 increment = null; 2256 } else if (location === 0) { 2257 decrement = null; 2258 } 2259 2260 // As per #391, the comparison for the decrement step can have some rounding issues. 2261 var stepDecimals = scope_Spectrum.countStepDecimals(); 2262 2263 // Round per #391 2264 if (increment !== null && increment !== false) { 2265 increment = Number(increment.toFixed(stepDecimals)); 2266 } 2267 2268 if (decrement !== null && decrement !== false) { 2269 decrement = Number(decrement.toFixed(stepDecimals)); 2270 } 2271 2272 return [decrement, increment]; 2273 } 2274 2275 // Get the current step size for the slider. 2276 function getNextSteps() { 2277 return scope_HandleNumbers.map(getNextStepsForHandle); 2278 } 2279 2280 // Updateable: margin, limit, padding, step, range, animate, snap 2281 function updateOptions(optionsToUpdate, fireSetEvent) { 2282 // Spectrum is created using the range, snap, direction and step options. 2283 // 'snap' and 'step' can be updated. 2284 // If 'snap' and 'step' are not passed, they should remain unchanged. 2285 var v = valueGet(); 2286 2287 var updateAble = ["margin", "limit", "padding", "range", "animate", "snap", "step", "format"]; 2288 2289 // Only change options that we're actually passed to update. 2290 updateAble.forEach(function(name) { 2291 if (optionsToUpdate[name] !== undefined) { 2292 originalOptions[name] = optionsToUpdate[name]; 2293 } 2294 }); 2295 2296 var newOptions = testOptions(originalOptions); 2297 2298 // Load new options into the slider state 2299 updateAble.forEach(function(name) { 2300 if (optionsToUpdate[name] !== undefined) { 2301 options[name] = newOptions[name]; 2302 } 2303 }); 2304 2305 scope_Spectrum = newOptions.spectrum; 2306 2307 // Limit, margin and padding depend on the spectrum but are stored outside of it. (#677) 2308 options.margin = newOptions.margin; 2309 options.limit = newOptions.limit; 2310 options.padding = newOptions.padding; 2311 2312 // Update pips, removes existing. 2313 if (options.pips) { 2314 pips(options.pips); 2315 } 2316 2317 // Invalidate the current positioning so valueSet forces an update. 2318 scope_Locations = []; 2319 valueSet(optionsToUpdate.start || v, fireSetEvent); 2320 } 2321 2322 // Initialization steps 2323 function setupSlider() { 2324 // Create the base element, initialize HTML and set classes. 2325 // Add handles and connect elements. 2326 scope_Base = addSlider(scope_Target); 2327 2328 addElements(options.connect, scope_Base); 2329 2330 // Attach user events. 2331 bindSliderEvents(options.events); 2332 2333 // Use the public value method to set the start values. 2334 valueSet(options.start); 2335 2336 if (options.pips) { 2337 pips(options.pips); 2338 } 2339 2340 if (options.tooltips) { 2341 tooltips(); 2342 } 2343 2344 aria(); 2345 } 2346 2347 setupSlider(); 2348 2349 // noinspection JSUnusedGlobalSymbols 2350 scope_Self = { 2351 destroy: destroy, 2352 steps: getNextSteps, 2353 on: bindEvent, 2354 off: removeEvent, 2355 get: valueGet, 2356 set: valueSet, 2357 setHandle: valueSetHandle, 2358 reset: valueReset, 2359 // Exposed for unit testing, don't use this in your application. 2360 __moveHandles: function(a, b, c) { 2361 moveHandles(a, b, scope_Locations, c); 2362 }, 2363 options: originalOptions, // Issue #600, #678 2364 updateOptions: updateOptions, 2365 target: scope_Target, // Issue #597 2366 removePips: removePips, 2367 pips: pips // Issue #594 2368 }; 2369 2370 return scope_Self; 2371 } 2372 2373 // Run the standard initializer 2374 function initialize(target, originalOptions) { 2375 if (!target || !target.nodeName) { 2376 throw new Error("noUiSlider (" + VERSION + "): create requires a single element, got: " + target); 2377 } 2378 2379 // Throw an error if the slider was already initialized. 2380 if (target.noUiSlider) { 2381 throw new Error("noUiSlider (" + VERSION + "): Slider was already initialized."); 2382 } 2383 2384 // Test the options and create the slider environment; 2385 var options = testOptions(originalOptions, target); 2386 var api = scope(target, options, originalOptions); 2387 2388 target.noUiSlider = api; 2389 2390 return api; 2391 } 2392 2393 // Use an object instead of a function for future expandability; 2394 return { 2395 // Exposed for unit testing, don't use this in your application. 2396 __spectrum: Spectrum, 2397 version: VERSION, 2398 create: initialize 2399 }; 2400 });