balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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 });