PluginProbe
Elementor Website Builder – more than just a page builder / 3.0.13
Elementor Website Builder – more than just a page builder v3.0.13
4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
elementor / assets / lib / nouislider / nouislider.js

nouislider.js in Elementor Website Builder – more than just a page builder 3.0.13, at assets/lib/nouislider/nouislider.js

2,401 lines 85.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 });
2401