diff --git a/public_html/index.html b/public_html/index.html index 6ae222f..d06b99e 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -14,6 +14,9 @@ + + + @@ -220,31 +223,49 @@
-
- - - - to - - - - -
-
- - - - -
- -
- - - - -
+
+ +
+ + + to + + +
+
+
+
+ +
+ + + to + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+ +
+ + + +
+
+
-
diff --git a/public_html/noUiSlider/CHANGELOG.MD b/public_html/noUiSlider/CHANGELOG.MD new file mode 100644 index 0000000..6e4cc72 --- /dev/null +++ b/public_html/noUiSlider/CHANGELOG.MD @@ -0,0 +1,183 @@ +# Changelog + +### 14.6.3 (*2020-11-19*) +- Fixed: Fixed removing namespaced event listeners, internal listeners getting removed (#1109); + +### 14.6.2 (*2020-09-16*) +- Fixed: Ignore erroneous mouse events on taps for iOS 13.4 (#1095); +- Added: `exactInput` argument to `set` and `setHandle` methods (#436, #1094); + +### 14.6.1 (*2020-08-17*) +- Fixed: Pips in count mode ignores pip at end of range (#1088); + +### 14.6.0 (*2020-06-27*) +- Added: `keyboardPageMultiplier` and `keyboardDefaultStep` options (#1083); +- Fixed: Ignore erroneous tap events for iOS (#1057, #1079); + +### 14.5.0 (*2020-05-20*) +- Added: Support for `margin`, `padding` and `limit` on non-linear sliders (#911, #1030, #1031, #1071); + +### 14.4.0 (*2020-05-06*) +- Added: `getOrigins` and `getTooltips` methods; +- Added: Default styling to support merging overlapping tooltips (#1032); + +### 14.3.0 (*2020-05-05*) +- Added: Default `cssClasses` are now exposed and can be modified; +- Fixed: Destroying sliders with multiple classes in `cssClasses` fails (#1069); + +### 14.2.0 (*2020-03-27*) +- Added: Slider api as event parameter (#1058); +- Added: Allow multiple classes in `cssClasses` option (#1054); +- Fixed: Slider not working within shadow DOM (#1060); +- Fixed: Last pip not rendered if it is also the first and at the slider edge (#1063); + +### 14.1.1 (*2019-12-15*) +- Fixed: Text direction is not correctly determined when the slider is not in the DOM (#1038); + +### 14.1.0 (*2019-12-04*) +- Fixed: Styling requires a root `html` node, so noUiSlider can't be used in shadow dom (#1035); +- Added: Support for PageUp/PageDown and Home/End keys in keyboard support (#1036); + +### 14.0.3 (*2019-10-10*) +- Fixed: Initialising handle values near the slider edge does not always respect `margin` (#1009); + +### 14.0.2 (*2019-06-28*) +- Fixed: Keyboard interaction uses formatter when it does not need to (#1000); + +### 14.0.1 (*2019-06-21*) +- Fixed: Visual regression in Safari caused by fixing #987 (#998); + +### 14.0.0 (*2019-06-20*) +- Fixed: `change` & `slide` events should fire on keyboard control (#994); +- Fixed: `.noUi-origin` overflows document on vertical sliders (#987); +- Fixed: Clicking to right of handle doesn't move it when it's at the same point as another (#965); +- Added: Additional documentation on number formatting (#978, #985); + +### 13.1.5 (*2019-04-24*) +- Fixed: Full-range padding (#880); + +### 13.1.4 (*2019-03-20*) +- Fixed: Keyboard interaction does not work with `snap` option (#961); + +### 13.1.3 (*2019-03-15*) +- Fixed: Keyboard interaction allows handles to "push" other handles (#960); +- Fixed: Update event fires for all handles during keyboard interaction (#960); + +### 13.1.2 (*2019-03-13*) +- Fixed: Handle disappears in Safari on tap (#927); +- Fixed: Disabled slider still accepts keyboard interaction (#953); + +### 13.1.1 (*2019-02-14*) +- Fixed: Slider hang when using a zero-length range (#948); + +### 13.1.0 (*2019-02-08*) +- Fixed: Updating `pips` using `updateOptions` (#933); +- Added: Updating `tooltips` using `updateOptions` (#946); + +### 13.0.0 (*2019-02-06*) +noUiSlider 13 does not include any breaking API changes. +Keyboard support is now built-in, so any custom implementations should be removed when upgrading. +Alternatively, built-in keyboard support can be disabled using `keyboardSupport: false`. +- Added: Built-in keyboard support (#724); +- Added: `.noUi-touch-area` element (#924); +- Fixed: Dragging a range does not check for handle disabled state (#938); +- Fixed: Incorrect CSS transform in pips (#931); + +### 12.1.0 (*2018-10-25*) +- Added: `unconstrained` behaviour (#747, #815, #913); +- Added: `setHandle` API (#917); +- Changed: point to `nouislider.js` in `package.json`.`main` (#921); + +### 12.0.0 (*2018-09-14*) +- Change: License changed to MIT; +- Change: Build process is now based on NPM scripts, phasing out the Grunt task runner. +- Fixed: Aria values are now as per spec (#889); +- Change: Pips formatting are now written as HTML (#875); +- Change: The `filter` option is now called for all pips (#754); +- Added: The `filter` option can now return `-1` to hide a pip (#754); +- Added: `keyboardSupport` option (#867, #895); +- Added: `documentElement` option (#821); + +### 11.1.0 (*2018-04-02*) +- Change: `null` options are now handled consistently (#856); +- Fixed: Missing transform origin in IE9 (#870); +- Fixed: `padding` on one side of the slider could not exceed `50%` (#865); + +### 11.0.3 (*2018-01-21*) +Refactor of source code. There are no meaningful changes in the distributed files; + +### 11.0.2 (*2018-01-20*) +- Fixed: Slider ignores clicks on `.noUi-target` outside of `.noUi-base` (#842); +- Fixed: `.noUi-origin` moving out of the page causes horizontal scrolling (#852); +- Fixed: Relative `.noUi-handle` has unintended margin (#854); + +### 11.0.0 (*2018-01-12*) +noUiSlider 11 does not include any breaking API changes. +Unless major changes were made to the stylesheet or you specifically depend +on the handle/connect order in the DOM, there should be no issues upgrading. +- Change: Use CSS transforms for handle movement, resulting in a massive performance improvement (#718); +- Change: Support multitouch by default; +- Change: Handle stacking is now on `.noUi-origin` instead of `.noUi-handle`; +- Added: A `.noUi-connects` element holding all `.noUi-connect` elements; +- Added: `[data-value]` property for `.noUi-value` in pips (#733); +- Added: `padding` option can now take an array for different padding values at both sides of a slider (#822); +- Removed: `useRequestAnimationFrame` option. No longer needed with CSS transforms; +- Removed: `multitouch` option. Now enabled by default; +- Fixed: Slider could ignore end events it should handle (#704, #805, #834); +- Fixed: Stop depending on array type (#801); +- Fixed: `set` method might bypass margin option (#823); +- Fixed: Alignment of pips for RTL sliders (#795); +- Fixed: Several issues regarding pips (#812, #826, #832); + +### 10.1.0 (*2017-07-26*) +- Added: `multitouch` option (#793); + +### 10.0.0 (*2017-05-28*) +- Change: Change event listeners to be passive (#785); +- Fixed: Pips are now updated when calling `updateOptions` (#669); +- Fixed: Content Security Policy issue with pips; +- Added: `removePips` method; +- Added: aria support (#685); +- Added: `ariaFormat` option (controls `aria-valuetext`); +- Fixed: throw a better error when mistakenly trying to initialize noUiSlider with `null` (#658); +- Fixed: Made order of events consistent and documented it (#775); +- Fixed: Border radius of connect bar, white space wrapping of tooltips (#773, #774); +- Fixed: Slider now uses `ownerDocument` instead of `document` (#767); + +### 9.2.0 (*2017-01-17*) +- Added: Version number to exceptions; +- Added: `noUiSlider.version` holds current version number; +- Added: Throw exception on invalid `pips` configuration (#721); +- Added: Merged pull request that uses less preprocessor to generate CSS (#735); + +### 9.1.0 (*2016-12-10*) +- Fixed: Slider not properly handling multitouch (#700, #704); +- Fixed: Removed a querySelector for the currently active handle (#720); +- Fixed: Removed iOS/webkit flashes on tap; +- Fixed: Incorrect error when using margin/limit with a step smaller than 0 (#736); +- Fixed: Drag option using incorrect cursor arrows (#681); +- Added: New `padding` option (#711); +- Added: Re-introduced `.noUi-handle-lower` and `.noUi-handle-upper` classes removed in 9.0.0; +- Added: Compatibility for legacy `connect` options removed in 9.0.0; + +### 9.0.0 (*2016-09-26*) +- Added: Support for **more than 2 handles**; +- Added: `format` option can be updated (#641); +- Added: `reset` method the return slider to start values (#673); +- Change: `connect` option is now implemented as a separate node; +- Change: all event arguments, including the handle number, are now in slider order; +- Change: `updateOptions` now **modifies the original options** object. The reference in `slider.noUiSlider.options` remains up to date (#678); +- Change: more events fire when using various `behaviour` options (#664); +- Change: on `rtl` sliders, handles are now visually positioned from the sliders `right`/`bottom` edge; +- Change: events for `rtl` sliders now fire in the same order as for `ltr` sliders (with incremental handleNumbers); +- Change: internal `Spectrum` component is no longer `direction` aware; +- Change: `limit` and `margin` must be divisible by `step` (if set); +- Removed: `.noUi-stacking` class. Handles now stack themselves; +- ~~Removed~~ (returned in 9.1.0): `.noUi-handle-lower` and `.noUi-handle-upper` classes; +- Removed: `.noUi-background`. Use `.noUi-target` instead; +- ~~Removed~~ (backward compatibility in 9.1.0): `connect: 'lower'` and `connect: 'upper'`. These settings are replaced by `connect: [true, false]`; +- Fixed: default tooltip color (#687); +- Fixed: `margin` and `limit` calculated improperly after calling `updateOptions` with a new `range` option; +- Fixed: `range` option was required in update, even when not updating it (#682); +- Fixed: Cursor styling is now consistent for disabled handles and sliders (#644); +- Fixed: Sliders now ignore touches when the screen is touched multiple times (#649, #663, #668); diff --git a/public_html/noUiSlider/CONTRIBUTING.md b/public_html/noUiSlider/CONTRIBUTING.md new file mode 100644 index 0000000..d15d938 --- /dev/null +++ b/public_html/noUiSlider/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Issues +Please, only use the issue tracker for **problems, bugs and feature requests**. + +For help with _implementing_ noUiSlider, please ask your question on [stackoverflow](https://stackoverflow.com/questions/tagged/nouislider). I try to look at questions posted there daily, and you will get you an answer much faster. Make sure to follow the [guidelines](https://stackoverflow.com/help/how-to-ask) of the platform. + +While I'm happy to help if you can't figure something out, please note that I: +- Can't go and debug problems just based on screenshots; +- Can't help with issues that do not include **an example with code** that reproduces it; +- Won't dig through your production site or huge chunks of unrelated code; +- Won't implement your business requirements for you; + +# Tooling + +Please run the following tooling before submitting a pull request: + +```bash +npm run lint +npm run format +``` + +# Pull requests +- Detail (in the pull request comment) what your changes do. +- When applicable, include new unit tests, and make sure existing ones pass. +- If you are introducing a new feature, update the **documentation**. +- Please don't commit `/distribute/*` files, I'll do that upon release. diff --git a/public_html/noUiSlider/LICENSE.md b/public_html/noUiSlider/LICENSE.md new file mode 100644 index 0000000..98f4ca2 --- /dev/null +++ b/public_html/noUiSlider/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Léon Gersen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/public_html/noUiSlider/README.md b/public_html/noUiSlider/README.md new file mode 100644 index 0000000..50fc731 --- /dev/null +++ b/public_html/noUiSlider/README.md @@ -0,0 +1,41 @@ +# noUiSlider + +noUiSlider is a lightweight JavaScript range slider. + +- **No dependencies** +- All modern browsers and IE > 9 are supported +- Fully **responsive** +- **Multi-touch support** on Android, iOS and Windows devices +- Accessible with `aria` and keyboard support +- Tons of [examples](https://refreshless.com/nouislider/examples) and answered [Stack Overflow questions](https://stackoverflow.com/questions/tagged/nouislider) + +License +------- +noUiSlider is licensed [MIT](https://choosealicense.com/licenses/mit/). + +It can be used **for free** and **without any attribution**, in any personal or commercial project. + +[Documentation](https://refreshless.com/nouislider/) +------- +An extensive documentation, including **examples**, **options** and **configuration details**, is available here: + +[noUiSlider documentation](https://refreshless.com/nouislider/). + +Contributing +------------ + +See [Contributing](CONTRIBUTING.md). + +Sponsorship +----------- + +noUiSlider is a stable project that still receives a lot of feature requests. A lot of these are interesting, but require a good amount of effort to implement, test and document. Sponsorship of this project will allow me to spend some more of my time on these feature requests. + +Please consider sponsoring the project by clicking the "❤ Sponsor" button above. Thanks! + +Tooling +------- + +Cross-browser testing kindly provided by BrowserStack. + +[![Tested with BrowserStack](documentation/assets/browserstack-logo-380x90.png)](http://browserstack.com/) diff --git a/public_html/noUiSlider/distribute/nouislider.css b/public_html/noUiSlider/distribute/nouislider.css new file mode 100644 index 0000000..363ad9d --- /dev/null +++ b/public_html/noUiSlider/distribute/nouislider.css @@ -0,0 +1,310 @@ +/*! nouislider - 14.6.3 - 11/19/2020 */ +/* Functional styling; + * These styles are required for noUiSlider to function. + * You don't need to change these rules to apply your design. + */ +.noUi-target, +.noUi-target * { + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-user-select: none; + -ms-touch-action: none; + touch-action: none; + -ms-user-select: none; + -moz-user-select: none; + user-select: none; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.noUi-target { + position: relative; +} +.noUi-base, +.noUi-connects { + width: 100%; + height: 100%; + position: relative; + z-index: 1; +} +/* Wrapper for all connect elements. + */ +.noUi-connects { + overflow: hidden; + z-index: 0; +} +.noUi-connect, +.noUi-origin { + will-change: transform; + position: absolute; + z-index: 1; + top: 0; + right: 0; + -ms-transform-origin: 0 0; + -webkit-transform-origin: 0 0; + -webkit-transform-style: preserve-3d; + transform-origin: 0 0; + transform-style: flat; +} +.noUi-connect { + height: 100%; + width: 100%; +} +.noUi-origin { + height: 10%; + width: 10%; +} +/* Offset direction + */ +.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin { + left: 0; + right: auto; +} +/* Give origins 0 height/width so they don't interfere with clicking the + * connect elements. + */ +.noUi-vertical .noUi-origin { + width: 0; +} +.noUi-horizontal .noUi-origin { + height: 0; +} +.noUi-handle { + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + position: absolute; +} +.noUi-touch-area { + height: 100%; + width: 100%; +} +.noUi-state-tap .noUi-connect, +.noUi-state-tap .noUi-origin { + -webkit-transition: transform 0.3s; + transition: transform 0.3s; +} +.noUi-state-drag * { + cursor: inherit !important; +} +/* Slider size and handle placement; + */ +.noUi-horizontal { + height: 18px; +} +.noUi-horizontal .noUi-handle { + width: 34px; + height: 28px; + right: -17px; + top: -6px; +} +.noUi-vertical { + width: 18px; +} +.noUi-vertical .noUi-handle { + width: 28px; + height: 34px; + right: -6px; + top: -17px; +} +.noUi-txt-dir-rtl.noUi-horizontal .noUi-handle { + left: -17px; + right: auto; +} +/* Styling; + * Giving the connect element a border radius causes issues with using transform: scale + */ +.noUi-target { + background: #FAFAFA; + border-radius: 4px; + border: 1px solid #D3D3D3; + box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB; +} +.noUi-connects { + border-radius: 3px; +} +.noUi-connect { + background: #3FB8AF; +} +/* Handles and cursors; + */ +.noUi-draggable { + cursor: ew-resize; +} +.noUi-vertical .noUi-draggable { + cursor: ns-resize; +} +.noUi-handle { + border: 1px solid #D9D9D9; + border-radius: 3px; + background: #FFF; + cursor: default; + box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #EBEBEB, 0 3px 6px -3px #BBB; +} +.noUi-active { + box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #DDD, 0 3px 6px -3px #BBB; +} +/* Handle stripes; + */ +.noUi-handle:before, +.noUi-handle:after { + content: ""; + display: block; + position: absolute; + height: 14px; + width: 1px; + background: #E8E7E6; + left: 14px; + top: 6px; +} +.noUi-handle:after { + left: 17px; +} +.noUi-vertical .noUi-handle:before, +.noUi-vertical .noUi-handle:after { + width: 14px; + height: 1px; + left: 6px; + top: 14px; +} +.noUi-vertical .noUi-handle:after { + top: 17px; +} +/* Disabled state; + */ +[disabled] .noUi-connect { + background: #B8B8B8; +} +[disabled].noUi-target, +[disabled].noUi-handle, +[disabled] .noUi-handle { + cursor: not-allowed; +} +/* Base; + * + */ +.noUi-pips, +.noUi-pips * { + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.noUi-pips { + position: absolute; + color: #999; +} +/* Values; + * + */ +.noUi-value { + position: absolute; + white-space: nowrap; + text-align: center; +} +.noUi-value-sub { + color: #ccc; + font-size: 10px; +} +/* Markings; + * + */ +.noUi-marker { + position: absolute; + background: #CCC; +} +.noUi-marker-sub { + background: #AAA; +} +.noUi-marker-large { + background: #AAA; +} +/* Horizontal layout; + * + */ +.noUi-pips-horizontal { + padding: 10px 0; + height: 80px; + top: 100%; + left: 0; + width: 100%; +} +.noUi-value-horizontal { + -webkit-transform: translate(-50%, 50%); + transform: translate(-50%, 50%); +} +.noUi-rtl .noUi-value-horizontal { + -webkit-transform: translate(50%, 50%); + transform: translate(50%, 50%); +} +.noUi-marker-horizontal.noUi-marker { + margin-left: -1px; + width: 2px; + height: 5px; +} +.noUi-marker-horizontal.noUi-marker-sub { + height: 10px; +} +.noUi-marker-horizontal.noUi-marker-large { + height: 15px; +} +/* Vertical layout; + * + */ +.noUi-pips-vertical { + padding: 0 10px; + height: 100%; + top: 0; + left: 100%; +} +.noUi-value-vertical { + -webkit-transform: translate(0, -50%); + transform: translate(0, -50%); + padding-left: 25px; +} +.noUi-rtl .noUi-value-vertical { + -webkit-transform: translate(0, 50%); + transform: translate(0, 50%); +} +.noUi-marker-vertical.noUi-marker { + width: 5px; + height: 2px; + margin-top: -1px; +} +.noUi-marker-vertical.noUi-marker-sub { + width: 10px; +} +.noUi-marker-vertical.noUi-marker-large { + width: 15px; +} +.noUi-tooltip { + display: block; + position: absolute; + border: 1px solid #D9D9D9; + border-radius: 3px; + background: #fff; + color: #000; + padding: 5px; + text-align: center; + white-space: nowrap; +} +.noUi-horizontal .noUi-tooltip { + -webkit-transform: translate(-50%, 0); + transform: translate(-50%, 0); + left: 50%; + bottom: 120%; +} +.noUi-vertical .noUi-tooltip { + -webkit-transform: translate(0, -50%); + transform: translate(0, -50%); + top: 50%; + right: 120%; +} +.noUi-horizontal .noUi-origin > .noUi-tooltip { + -webkit-transform: translate(50%, 0); + transform: translate(50%, 0); + left: auto; + bottom: 10px; +} +.noUi-vertical .noUi-origin > .noUi-tooltip { + -webkit-transform: translate(0, -18px); + transform: translate(0, -18px); + top: auto; + right: 28px; +} diff --git a/public_html/noUiSlider/distribute/nouislider.js b/public_html/noUiSlider/distribute/nouislider.js new file mode 100644 index 0000000..4247067 --- /dev/null +++ b/public_html/noUiSlider/distribute/nouislider.js @@ -0,0 +1,2693 @@ +/*! nouislider - 14.6.3 - 11/19/2020 */ +(function(factory) { + if (typeof define === "function" && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else if (typeof exports === "object") { + // Node/CommonJS + module.exports = factory(); + } else { + // Browser globals + window.noUiSlider = factory(); + } +})(function() { + "use strict"; + + var VERSION = "14.6.3"; + + //region Helper Methods + + function isValidFormatter(entry) { + return typeof entry === "object" && typeof entry.to === "function" && typeof entry.from === "function"; + } + + function removeElement(el) { + el.parentElement.removeChild(el); + } + + function isSet(value) { + return value !== null && value !== undefined; + } + + // Bindable version + function preventDefault(e) { + e.preventDefault(); + } + + // Removes duplicates from an array. + function unique(array) { + return array.filter(function(a) { + return !this[a] ? (this[a] = true) : false; + }, {}); + } + + // Round a value to the closest 'to'. + function closest(value, to) { + return Math.round(value / to) * to; + } + + // Current position of an element relative to the document. + function offset(elem, orientation) { + var rect = elem.getBoundingClientRect(); + var doc = elem.ownerDocument; + var docElem = doc.documentElement; + var pageOffset = getPageOffset(doc); + + // getBoundingClientRect contains left scroll in Chrome on Android. + // I haven't found a feature detection that proves this. Worst case + // scenario on mis-match: the 'tap' feature on horizontal sliders breaks. + if (/webkit.*Chrome.*Mobile/i.test(navigator.userAgent)) { + pageOffset.x = 0; + } + + return orientation + ? rect.top + pageOffset.y - docElem.clientTop + : rect.left + pageOffset.x - docElem.clientLeft; + } + + // Checks whether a value is numerical. + function isNumeric(a) { + return typeof a === "number" && !isNaN(a) && isFinite(a); + } + + // Sets a class and removes it after [duration] ms. + function addClassFor(element, className, duration) { + if (duration > 0) { + addClass(element, className); + setTimeout(function() { + removeClass(element, className); + }, duration); + } + } + + // Limits a value to 0 - 100 + function limit(a) { + return Math.max(Math.min(a, 100), 0); + } + + // Wraps a variable as an array, if it isn't one yet. + // Note that an input array is returned by reference! + function asArray(a) { + return Array.isArray(a) ? a : [a]; + } + + // Counts decimals + function countDecimals(numStr) { + numStr = String(numStr); + var pieces = numStr.split("."); + return pieces.length > 1 ? pieces[1].length : 0; + } + + // http://youmightnotneedjquery.com/#add_class + function addClass(el, className) { + if (el.classList && !/\s/.test(className)) { + el.classList.add(className); + } else { + el.className += " " + className; + } + } + + // http://youmightnotneedjquery.com/#remove_class + function removeClass(el, className) { + if (el.classList && !/\s/.test(className)) { + el.classList.remove(className); + } else { + el.className = el.className.replace( + new RegExp("(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", "gi"), + " " + ); + } + } + + // https://plainjs.com/javascript/attributes/adding-removing-and-testing-for-classes-9/ + function hasClass(el, className) { + return el.classList + ? el.classList.contains(className) + : new RegExp("\\b" + className + "\\b").test(el.className); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY#Notes + function getPageOffset(doc) { + var supportPageOffset = window.pageXOffset !== undefined; + var isCSS1Compat = (doc.compatMode || "") === "CSS1Compat"; + var x = supportPageOffset + ? window.pageXOffset + : isCSS1Compat + ? doc.documentElement.scrollLeft + : doc.body.scrollLeft; + var y = supportPageOffset + ? window.pageYOffset + : isCSS1Compat + ? doc.documentElement.scrollTop + : doc.body.scrollTop; + + return { + x: x, + y: y + }; + } + + // we provide a function to compute constants instead + // of accessing window.* as soon as the module needs it + // so that we do not compute anything if not needed + function getActions() { + // Determine the events to bind. IE11 implements pointerEvents without + // a prefix, which breaks compatibility with the IE10 implementation. + return window.navigator.pointerEnabled + ? { + start: "pointerdown", + move: "pointermove", + end: "pointerup" + } + : window.navigator.msPointerEnabled + ? { + start: "MSPointerDown", + move: "MSPointerMove", + end: "MSPointerUp" + } + : { + start: "mousedown touchstart", + move: "mousemove touchmove", + end: "mouseup touchend" + }; + } + + // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md + // Issue #785 + function getSupportsPassive() { + var supportsPassive = false; + + /* eslint-disable */ + try { + var opts = Object.defineProperty({}, "passive", { + get: function() { + supportsPassive = true; + } + }); + + window.addEventListener("test", null, opts); + } catch (e) {} + /* eslint-enable */ + + return supportsPassive; + } + + function getSupportsTouchActionNone() { + return window.CSS && CSS.supports && CSS.supports("touch-action", "none"); + } + + //endregion + + //region Range Calculation + + // Determine the size of a sub-range in relation to a full range. + function subRangeRatio(pa, pb) { + return 100 / (pb - pa); + } + + // (percentage) How many percent is this value of this range? + function fromPercentage(range, value, startRange) { + return (value * 100) / (range[startRange + 1] - range[startRange]); + } + + // (percentage) Where is this value on this range? + function toPercentage(range, value) { + return fromPercentage(range, range[0] < 0 ? value + Math.abs(range[0]) : value - range[0], 0); + } + + // (value) How much is this percentage on this range? + function isPercentage(range, value) { + return (value * (range[1] - range[0])) / 100 + range[0]; + } + + function getJ(value, arr) { + var j = 1; + + while (value >= arr[j]) { + j += 1; + } + + return j; + } + + // (percentage) Input a value, find where, on a scale of 0-100, it applies. + function toStepping(xVal, xPct, value) { + if (value >= xVal.slice(-1)[0]) { + return 100; + } + + var j = getJ(value, xVal); + var va = xVal[j - 1]; + var vb = xVal[j]; + var pa = xPct[j - 1]; + var pb = xPct[j]; + + return pa + toPercentage([va, vb], value) / subRangeRatio(pa, pb); + } + + // (value) Input a percentage, find where it is on the specified range. + function fromStepping(xVal, xPct, value) { + // There is no range group that fits 100 + if (value >= 100) { + return xVal.slice(-1)[0]; + } + + var j = getJ(value, xPct); + var va = xVal[j - 1]; + var vb = xVal[j]; + var pa = xPct[j - 1]; + var pb = xPct[j]; + + return isPercentage([va, vb], (value - pa) * subRangeRatio(pa, pb)); + } + + // (percentage) Get the step that applies at a certain value. + function getStep(xPct, xSteps, snap, value) { + if (value === 100) { + return value; + } + + var j = getJ(value, xPct); + var a = xPct[j - 1]; + var b = xPct[j]; + + // If 'snap' is set, steps are used as fixed points on the slider. + if (snap) { + // Find the closest position, a or b. + if (value - a > (b - a) / 2) { + return b; + } + + return a; + } + + if (!xSteps[j - 1]) { + return value; + } + + return xPct[j - 1] + closest(value - xPct[j - 1], xSteps[j - 1]); + } + + function handleEntryPoint(index, value, that) { + var percentage; + + // Wrap numerical input in an array. + if (typeof value === "number") { + value = [value]; + } + + // Reject any invalid input, by testing whether value is an array. + if (!Array.isArray(value)) { + throw new Error("noUiSlider (" + VERSION + "): 'range' contains invalid value."); + } + + // Covert min/max syntax to 0 and 100. + if (index === "min") { + percentage = 0; + } else if (index === "max") { + percentage = 100; + } else { + percentage = parseFloat(index); + } + + // Check for correct input. + if (!isNumeric(percentage) || !isNumeric(value[0])) { + throw new Error("noUiSlider (" + VERSION + "): 'range' value isn't numeric."); + } + + // Store values. + that.xPct.push(percentage); + that.xVal.push(value[0]); + + // NaN will evaluate to false too, but to keep + // logging clear, set step explicitly. Make sure + // not to override the 'step' setting with false. + if (!percentage) { + if (!isNaN(value[1])) { + that.xSteps[0] = value[1]; + } + } else { + that.xSteps.push(isNaN(value[1]) ? false : value[1]); + } + + that.xHighestCompleteStep.push(0); + } + + function handleStepPoint(i, n, that) { + // Ignore 'false' stepping. + if (!n) { + return; + } + + // Step over zero-length ranges (#948); + if (that.xVal[i] === that.xVal[i + 1]) { + that.xSteps[i] = that.xHighestCompleteStep[i] = that.xVal[i]; + + return; + } + + // Factor to range ratio + that.xSteps[i] = + fromPercentage([that.xVal[i], that.xVal[i + 1]], n, 0) / subRangeRatio(that.xPct[i], that.xPct[i + 1]); + + var totalSteps = (that.xVal[i + 1] - that.xVal[i]) / that.xNumSteps[i]; + var highestStep = Math.ceil(Number(totalSteps.toFixed(3)) - 1); + var step = that.xVal[i] + that.xNumSteps[i] * highestStep; + + that.xHighestCompleteStep[i] = step; + } + + //endregion + + //region Spectrum + + function Spectrum(entry, snap, singleStep) { + this.xPct = []; + this.xVal = []; + this.xSteps = [singleStep || false]; + this.xNumSteps = [false]; + this.xHighestCompleteStep = []; + + this.snap = snap; + + var index; + var ordered = []; // [0, 'min'], [1, '50%'], [2, 'max'] + + // Map the object keys to an array. + for (index in entry) { + if (entry.hasOwnProperty(index)) { + ordered.push([entry[index], index]); + } + } + + // Sort all entries by value (numeric sort). + if (ordered.length && typeof ordered[0][0] === "object") { + ordered.sort(function(a, b) { + return a[0][0] - b[0][0]; + }); + } else { + ordered.sort(function(a, b) { + return a[0] - b[0]; + }); + } + + // Convert all entries to subranges. + for (index = 0; index < ordered.length; index++) { + handleEntryPoint(ordered[index][1], ordered[index][0], this); + } + + // Store the actual step values. + // xSteps is sorted in the same order as xPct and xVal. + this.xNumSteps = this.xSteps.slice(0); + + // Convert all numeric steps to the percentage of the subrange they represent. + for (index = 0; index < this.xNumSteps.length; index++) { + handleStepPoint(index, this.xNumSteps[index], this); + } + } + + Spectrum.prototype.getDistance = function(value) { + var index; + var distances = []; + + for (index = 0; index < this.xNumSteps.length - 1; index++) { + // last "range" can't contain step size as it is purely an endpoint. + var step = this.xNumSteps[index]; + + if (step && (value / step) % 1 !== 0) { + throw new Error( + "noUiSlider (" + + VERSION + + "): 'limit', 'margin' and 'padding' of " + + this.xPct[index] + + "% range must be divisible by step." + ); + } + + // Calculate percentual distance in current range of limit, margin or padding + distances[index] = fromPercentage(this.xVal, value, index); + } + + return distances; + }; + + // Calculate the percentual distance over the whole scale of ranges. + // direction: 0 = backwards / 1 = forwards + Spectrum.prototype.getAbsoluteDistance = function(value, distances, direction) { + var xPct_index = 0; + + // Calculate range where to start calculation + if (value < this.xPct[this.xPct.length - 1]) { + while (value > this.xPct[xPct_index + 1]) { + xPct_index++; + } + } else if (value === this.xPct[this.xPct.length - 1]) { + xPct_index = this.xPct.length - 2; + } + + // If looking backwards and the value is exactly at a range separator then look one range further + if (!direction && value === this.xPct[xPct_index + 1]) { + xPct_index++; + } + + var start_factor; + var rest_factor = 1; + + var rest_rel_distance = distances[xPct_index]; + + var range_pct = 0; + + var rel_range_distance = 0; + var abs_distance_counter = 0; + var range_counter = 0; + + // Calculate what part of the start range the value is + if (direction) { + start_factor = (value - this.xPct[xPct_index]) / (this.xPct[xPct_index + 1] - this.xPct[xPct_index]); + } else { + start_factor = (this.xPct[xPct_index + 1] - value) / (this.xPct[xPct_index + 1] - this.xPct[xPct_index]); + } + + // Do until the complete distance across ranges is calculated + while (rest_rel_distance > 0) { + // Calculate the percentage of total range + range_pct = this.xPct[xPct_index + 1 + range_counter] - this.xPct[xPct_index + range_counter]; + + // Detect if the margin, padding or limit is larger then the current range and calculate + if (distances[xPct_index + range_counter] * rest_factor + 100 - start_factor * 100 > 100) { + // If larger then take the percentual distance of the whole range + rel_range_distance = range_pct * start_factor; + // Rest factor of relative percentual distance still to be calculated + rest_factor = (rest_rel_distance - 100 * start_factor) / distances[xPct_index + range_counter]; + // Set start factor to 1 as for next range it does not apply. + start_factor = 1; + } else { + // If smaller or equal then take the percentual distance of the calculate percentual part of that range + rel_range_distance = ((distances[xPct_index + range_counter] * range_pct) / 100) * rest_factor; + // No rest left as the rest fits in current range + rest_factor = 0; + } + + if (direction) { + abs_distance_counter = abs_distance_counter - rel_range_distance; + // Limit range to first range when distance becomes outside of minimum range + if (this.xPct.length + range_counter >= 1) { + range_counter--; + } + } else { + abs_distance_counter = abs_distance_counter + rel_range_distance; + // Limit range to last range when distance becomes outside of maximum range + if (this.xPct.length - range_counter >= 1) { + range_counter++; + } + } + + // Rest of relative percentual distance still to be calculated + rest_rel_distance = distances[xPct_index + range_counter] * rest_factor; + } + + return value + abs_distance_counter; + }; + + Spectrum.prototype.toStepping = function(value) { + value = toStepping(this.xVal, this.xPct, value); + + return value; + }; + + Spectrum.prototype.fromStepping = function(value) { + return fromStepping(this.xVal, this.xPct, value); + }; + + Spectrum.prototype.getStep = function(value) { + value = getStep(this.xPct, this.xSteps, this.snap, value); + + return value; + }; + + Spectrum.prototype.getDefaultStep = function(value, isDown, size) { + var j = getJ(value, this.xPct); + + // When at the top or stepping down, look at the previous sub-range + if (value === 100 || (isDown && value === this.xPct[j - 1])) { + j = Math.max(j - 1, 1); + } + + return (this.xVal[j] - this.xVal[j - 1]) / size; + }; + + Spectrum.prototype.getNearbySteps = function(value) { + var j = getJ(value, this.xPct); + + return { + stepBefore: { + startValue: this.xVal[j - 2], + step: this.xNumSteps[j - 2], + highestStep: this.xHighestCompleteStep[j - 2] + }, + thisStep: { + startValue: this.xVal[j - 1], + step: this.xNumSteps[j - 1], + highestStep: this.xHighestCompleteStep[j - 1] + }, + stepAfter: { + startValue: this.xVal[j], + step: this.xNumSteps[j], + highestStep: this.xHighestCompleteStep[j] + } + }; + }; + + Spectrum.prototype.countStepDecimals = function() { + var stepDecimals = this.xNumSteps.map(countDecimals); + return Math.max.apply(null, stepDecimals); + }; + + // Outside testing + Spectrum.prototype.convert = function(value) { + return this.getStep(this.toStepping(value)); + }; + + //endregion + + //region Options + + /* Every input option is tested and parsed. This'll prevent + endless validation in internal methods. These tests are + structured with an item for every option available. An + option can be marked as required by setting the 'r' flag. + The testing function is provided with three arguments: + - The provided value for the option; + - A reference to the options object; + - The name for the option; + + The testing function returns false when an error is detected, + or true when everything is OK. It can also modify the option + object, to make sure all values can be correctly looped elsewhere. */ + + //region Defaults + + var defaultFormatter = { + to: function(value) { + return value !== undefined && value.toFixed(2); + }, + from: Number + }; + + var cssClasses = { + target: "target", + base: "base", + origin: "origin", + handle: "handle", + handleLower: "handle-lower", + handleUpper: "handle-upper", + touchArea: "touch-area", + horizontal: "horizontal", + vertical: "vertical", + background: "background", + connect: "connect", + connects: "connects", + ltr: "ltr", + rtl: "rtl", + textDirectionLtr: "txt-dir-ltr", + textDirectionRtl: "txt-dir-rtl", + draggable: "draggable", + drag: "state-drag", + tap: "state-tap", + active: "active", + tooltip: "tooltip", + pips: "pips", + pipsHorizontal: "pips-horizontal", + pipsVertical: "pips-vertical", + marker: "marker", + markerHorizontal: "marker-horizontal", + markerVertical: "marker-vertical", + markerNormal: "marker-normal", + markerLarge: "marker-large", + markerSub: "marker-sub", + value: "value", + valueHorizontal: "value-horizontal", + valueVertical: "value-vertical", + valueNormal: "value-normal", + valueLarge: "value-large", + valueSub: "value-sub" + }; + + // Namespaces of internal event listeners + var INTERNAL_EVENT_NS = { + tooltips: ".__tooltips", + aria: ".__aria" + }; + + //endregion + + function validateFormat(entry) { + // Any object with a to and from method is supported. + if (isValidFormatter(entry)) { + return true; + } + + throw new Error("noUiSlider (" + VERSION + "): 'format' requires 'to' and 'from' methods."); + } + + function testStep(parsed, entry) { + if (!isNumeric(entry)) { + throw new Error("noUiSlider (" + VERSION + "): 'step' is not numeric."); + } + + // The step option can still be used to set stepping + // for linear sliders. Overwritten if set in 'range'. + parsed.singleStep = entry; + } + + function testKeyboardPageMultiplier(parsed, entry) { + if (!isNumeric(entry)) { + throw new Error("noUiSlider (" + VERSION + "): 'keyboardPageMultiplier' is not numeric."); + } + + parsed.keyboardPageMultiplier = entry; + } + + function testKeyboardDefaultStep(parsed, entry) { + if (!isNumeric(entry)) { + throw new Error("noUiSlider (" + VERSION + "): 'keyboardDefaultStep' is not numeric."); + } + + parsed.keyboardDefaultStep = entry; + } + + function testRange(parsed, entry) { + // Filter incorrect input. + if (typeof entry !== "object" || Array.isArray(entry)) { + throw new Error("noUiSlider (" + VERSION + "): 'range' is not an object."); + } + + // Catch missing start or end. + if (entry.min === undefined || entry.max === undefined) { + throw new Error("noUiSlider (" + VERSION + "): Missing 'min' or 'max' in 'range'."); + } + + // Catch equal start or end. + if (entry.min === entry.max) { + throw new Error("noUiSlider (" + VERSION + "): 'range' 'min' and 'max' cannot be equal."); + } + + parsed.spectrum = new Spectrum(entry, parsed.snap, parsed.singleStep); + } + + function testStart(parsed, entry) { + entry = asArray(entry); + + // Validate input. Values aren't tested, as the public .val method + // will always provide a valid location. + if (!Array.isArray(entry) || !entry.length) { + throw new Error("noUiSlider (" + VERSION + "): 'start' option is incorrect."); + } + + // Store the number of handles. + parsed.handles = entry.length; + + // When the slider is initialized, the .val method will + // be called with the start options. + parsed.start = entry; + } + + function testSnap(parsed, entry) { + // Enforce 100% stepping within subranges. + parsed.snap = entry; + + if (typeof entry !== "boolean") { + throw new Error("noUiSlider (" + VERSION + "): 'snap' option must be a boolean."); + } + } + + function testAnimate(parsed, entry) { + // Enforce 100% stepping within subranges. + parsed.animate = entry; + + if (typeof entry !== "boolean") { + throw new Error("noUiSlider (" + VERSION + "): 'animate' option must be a boolean."); + } + } + + function testAnimationDuration(parsed, entry) { + parsed.animationDuration = entry; + + if (typeof entry !== "number") { + throw new Error("noUiSlider (" + VERSION + "): 'animationDuration' option must be a number."); + } + } + + function testConnect(parsed, entry) { + var connect = [false]; + var i; + + // Map legacy options + if (entry === "lower") { + entry = [true, false]; + } else if (entry === "upper") { + entry = [false, true]; + } + + // Handle boolean options + if (entry === true || entry === false) { + for (i = 1; i < parsed.handles; i++) { + connect.push(entry); + } + + connect.push(false); + } + + // Reject invalid input + else if (!Array.isArray(entry) || !entry.length || entry.length !== parsed.handles + 1) { + throw new Error("noUiSlider (" + VERSION + "): 'connect' option doesn't match handle count."); + } else { + connect = entry; + } + + parsed.connect = connect; + } + + function testOrientation(parsed, entry) { + // Set orientation to an a numerical value for easy + // array selection. + switch (entry) { + case "horizontal": + parsed.ort = 0; + break; + case "vertical": + parsed.ort = 1; + break; + default: + throw new Error("noUiSlider (" + VERSION + "): 'orientation' option is invalid."); + } + } + + function testMargin(parsed, entry) { + if (!isNumeric(entry)) { + throw new Error("noUiSlider (" + VERSION + "): 'margin' option must be numeric."); + } + + // Issue #582 + if (entry === 0) { + return; + } + + parsed.margin = parsed.spectrum.getDistance(entry); + } + + function testLimit(parsed, entry) { + if (!isNumeric(entry)) { + throw new Error("noUiSlider (" + VERSION + "): 'limit' option must be numeric."); + } + + parsed.limit = parsed.spectrum.getDistance(entry); + + if (!parsed.limit || parsed.handles < 2) { + throw new Error( + "noUiSlider (" + + VERSION + + "): 'limit' option is only supported on linear sliders with 2 or more handles." + ); + } + } + + function testPadding(parsed, entry) { + var index; + + if (!isNumeric(entry) && !Array.isArray(entry)) { + throw new Error( + "noUiSlider (" + VERSION + "): 'padding' option must be numeric or array of exactly 2 numbers." + ); + } + + if (Array.isArray(entry) && !(entry.length === 2 || isNumeric(entry[0]) || isNumeric(entry[1]))) { + throw new Error( + "noUiSlider (" + VERSION + "): 'padding' option must be numeric or array of exactly 2 numbers." + ); + } + + if (entry === 0) { + return; + } + + if (!Array.isArray(entry)) { + entry = [entry, entry]; + } + + // 'getDistance' returns false for invalid values. + parsed.padding = [parsed.spectrum.getDistance(entry[0]), parsed.spectrum.getDistance(entry[1])]; + + for (index = 0; index < parsed.spectrum.xNumSteps.length - 1; index++) { + // last "range" can't contain step size as it is purely an endpoint. + if (parsed.padding[0][index] < 0 || parsed.padding[1][index] < 0) { + throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be a positive number(s)."); + } + } + + var totalPadding = entry[0] + entry[1]; + var firstValue = parsed.spectrum.xVal[0]; + var lastValue = parsed.spectrum.xVal[parsed.spectrum.xVal.length - 1]; + + if (totalPadding / (lastValue - firstValue) > 1) { + throw new Error("noUiSlider (" + VERSION + "): 'padding' option must not exceed 100% of the range."); + } + } + + function testDirection(parsed, entry) { + // Set direction as a numerical value for easy parsing. + // Invert connection for RTL sliders, so that the proper + // handles get the connect/background classes. + switch (entry) { + case "ltr": + parsed.dir = 0; + break; + case "rtl": + parsed.dir = 1; + break; + default: + throw new Error("noUiSlider (" + VERSION + "): 'direction' option was not recognized."); + } + } + + function testBehaviour(parsed, entry) { + // Make sure the input is a string. + if (typeof entry !== "string") { + throw new Error("noUiSlider (" + VERSION + "): 'behaviour' must be a string containing options."); + } + + // Check if the string contains any keywords. + // None are required. + var tap = entry.indexOf("tap") >= 0; + var drag = entry.indexOf("drag") >= 0; + var fixed = entry.indexOf("fixed") >= 0; + var snap = entry.indexOf("snap") >= 0; + var hover = entry.indexOf("hover") >= 0; + var unconstrained = entry.indexOf("unconstrained") >= 0; + + if (fixed) { + if (parsed.handles !== 2) { + throw new Error("noUiSlider (" + VERSION + "): 'fixed' behaviour must be used with 2 handles"); + } + + // Use margin to enforce fixed state + testMargin(parsed, parsed.start[1] - parsed.start[0]); + } + + if (unconstrained && (parsed.margin || parsed.limit)) { + throw new Error( + "noUiSlider (" + VERSION + "): 'unconstrained' behaviour cannot be used with margin or limit" + ); + } + + parsed.events = { + tap: tap || snap, + drag: drag, + fixed: fixed, + snap: snap, + hover: hover, + unconstrained: unconstrained + }; + } + + function testTooltips(parsed, entry) { + if (entry === false) { + return; + } + + if (entry === true) { + parsed.tooltips = []; + + for (var i = 0; i < parsed.handles; i++) { + parsed.tooltips.push(true); + } + } else { + parsed.tooltips = asArray(entry); + + if (parsed.tooltips.length !== parsed.handles) { + throw new Error("noUiSlider (" + VERSION + "): must pass a formatter for all handles."); + } + + parsed.tooltips.forEach(function(formatter) { + if ( + typeof formatter !== "boolean" && + (typeof formatter !== "object" || typeof formatter.to !== "function") + ) { + throw new Error("noUiSlider (" + VERSION + "): 'tooltips' must be passed a formatter or 'false'."); + } + }); + } + } + + function testAriaFormat(parsed, entry) { + parsed.ariaFormat = entry; + validateFormat(entry); + } + + function testFormat(parsed, entry) { + parsed.format = entry; + validateFormat(entry); + } + + function testKeyboardSupport(parsed, entry) { + parsed.keyboardSupport = entry; + + if (typeof entry !== "boolean") { + throw new Error("noUiSlider (" + VERSION + "): 'keyboardSupport' option must be a boolean."); + } + } + + function testDocumentElement(parsed, entry) { + // This is an advanced option. Passed values are used without validation. + parsed.documentElement = entry; + } + + function testCssPrefix(parsed, entry) { + if (typeof entry !== "string" && entry !== false) { + throw new Error("noUiSlider (" + VERSION + "): 'cssPrefix' must be a string or `false`."); + } + + parsed.cssPrefix = entry; + } + + function testCssClasses(parsed, entry) { + if (typeof entry !== "object") { + throw new Error("noUiSlider (" + VERSION + "): 'cssClasses' must be an object."); + } + + if (typeof parsed.cssPrefix === "string") { + parsed.cssClasses = {}; + + for (var key in entry) { + if (!entry.hasOwnProperty(key)) { + continue; + } + + parsed.cssClasses[key] = parsed.cssPrefix + entry[key]; + } + } else { + parsed.cssClasses = entry; + } + } + + // Test all developer settings and parse to assumption-safe values. + function testOptions(options) { + // To prove a fix for #537, freeze options here. + // If the object is modified, an error will be thrown. + // Object.freeze(options); + + var parsed = { + margin: 0, + limit: 0, + padding: 0, + animate: true, + animationDuration: 300, + ariaFormat: defaultFormatter, + format: defaultFormatter + }; + + // Tests are executed in the order they are presented here. + var tests = { + step: { r: false, t: testStep }, + keyboardPageMultiplier: { r: false, t: testKeyboardPageMultiplier }, + keyboardDefaultStep: { r: false, t: testKeyboardDefaultStep }, + start: { r: true, t: testStart }, + connect: { r: true, t: testConnect }, + direction: { r: true, t: testDirection }, + snap: { r: false, t: testSnap }, + animate: { r: false, t: testAnimate }, + animationDuration: { r: false, t: testAnimationDuration }, + range: { r: true, t: testRange }, + orientation: { r: false, t: testOrientation }, + margin: { r: false, t: testMargin }, + limit: { r: false, t: testLimit }, + padding: { r: false, t: testPadding }, + behaviour: { r: true, t: testBehaviour }, + ariaFormat: { r: false, t: testAriaFormat }, + format: { r: false, t: testFormat }, + tooltips: { r: false, t: testTooltips }, + keyboardSupport: { r: true, t: testKeyboardSupport }, + documentElement: { r: false, t: testDocumentElement }, + cssPrefix: { r: true, t: testCssPrefix }, + cssClasses: { r: true, t: testCssClasses } + }; + + var defaults = { + connect: false, + direction: "ltr", + behaviour: "tap", + orientation: "horizontal", + keyboardSupport: true, + cssPrefix: "noUi-", + cssClasses: cssClasses, + keyboardPageMultiplier: 5, + keyboardDefaultStep: 10 + }; + + // AriaFormat defaults to regular format, if any. + if (options.format && !options.ariaFormat) { + options.ariaFormat = options.format; + } + + // Run all options through a testing mechanism to ensure correct + // input. It should be noted that options might get modified to + // be handled properly. E.g. wrapping integers in arrays. + Object.keys(tests).forEach(function(name) { + // If the option isn't set, but it is required, throw an error. + if (!isSet(options[name]) && defaults[name] === undefined) { + if (tests[name].r) { + throw new Error("noUiSlider (" + VERSION + "): '" + name + "' is required."); + } + + return true; + } + + tests[name].t(parsed, !isSet(options[name]) ? defaults[name] : options[name]); + }); + + // Forward pips options + parsed.pips = options.pips; + + // All recent browsers accept unprefixed transform. + // We need -ms- for IE9 and -webkit- for older Android; + // Assume use of -webkit- if unprefixed and -ms- are not supported. + // https://caniuse.com/#feat=transforms2d + var d = document.createElement("div"); + var msPrefix = d.style.msTransform !== undefined; + var noPrefix = d.style.transform !== undefined; + + parsed.transformRule = noPrefix ? "transform" : msPrefix ? "msTransform" : "webkitTransform"; + + // Pips don't move, so we can place them using left/top. + var styles = [["left", "top"], ["right", "bottom"]]; + + parsed.style = styles[parsed.dir][parsed.ort]; + + return parsed; + } + + //endregion + + function scope(target, options, originalOptions) { + var actions = getActions(); + var supportsTouchActionNone = getSupportsTouchActionNone(); + var supportsPassive = supportsTouchActionNone && getSupportsPassive(); + + // All variables local to 'scope' are prefixed with 'scope_' + + // Slider DOM Nodes + var scope_Target = target; + var scope_Base; + var scope_Handles; + var scope_Connects; + var scope_Pips; + var scope_Tooltips; + + // Slider state values + var scope_Spectrum = options.spectrum; + var scope_Values = []; + var scope_Locations = []; + var scope_HandleNumbers = []; + var scope_ActiveHandlesCount = 0; + var scope_Events = {}; + + // Exposed API + var scope_Self; + + // Document Nodes + var scope_Document = target.ownerDocument; + var scope_DocumentElement = options.documentElement || scope_Document.documentElement; + var scope_Body = scope_Document.body; + + // Pips constants + var PIPS_NONE = -1; + var PIPS_NO_VALUE = 0; + var PIPS_LARGE_VALUE = 1; + var PIPS_SMALL_VALUE = 2; + + // For horizontal sliders in standard ltr documents, + // make .noUi-origin overflow to the left so the document doesn't scroll. + var scope_DirOffset = scope_Document.dir === "rtl" || options.ort === 1 ? 0 : 100; + + // Creates a node, adds it to target, returns the new node. + function addNodeTo(addTarget, className) { + var div = scope_Document.createElement("div"); + + if (className) { + addClass(div, className); + } + + addTarget.appendChild(div); + + return div; + } + + // Append a origin to the base + function addOrigin(base, handleNumber) { + var origin = addNodeTo(base, options.cssClasses.origin); + var handle = addNodeTo(origin, options.cssClasses.handle); + + addNodeTo(handle, options.cssClasses.touchArea); + + handle.setAttribute("data-handle", handleNumber); + + if (options.keyboardSupport) { + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex + // 0 = focusable and reachable + handle.setAttribute("tabindex", "0"); + handle.addEventListener("keydown", function(event) { + return eventKeydown(event, handleNumber); + }); + } + + handle.setAttribute("role", "slider"); + handle.setAttribute("aria-orientation", options.ort ? "vertical" : "horizontal"); + + if (handleNumber === 0) { + addClass(handle, options.cssClasses.handleLower); + } else if (handleNumber === options.handles - 1) { + addClass(handle, options.cssClasses.handleUpper); + } + + return origin; + } + + // Insert nodes for connect elements + function addConnect(base, add) { + if (!add) { + return false; + } + + return addNodeTo(base, options.cssClasses.connect); + } + + // Add handles to the slider base. + function addElements(connectOptions, base) { + var connectBase = addNodeTo(base, options.cssClasses.connects); + + scope_Handles = []; + scope_Connects = []; + + scope_Connects.push(addConnect(connectBase, connectOptions[0])); + + // [::::O====O====O====] + // connectOptions = [0, 1, 1, 1] + + for (var i = 0; i < options.handles; i++) { + // Keep a list of all added handles. + scope_Handles.push(addOrigin(base, i)); + scope_HandleNumbers[i] = i; + scope_Connects.push(addConnect(connectBase, connectOptions[i + 1])); + } + } + + // Initialize a single slider. + function addSlider(addTarget) { + // Apply classes and data to the target. + addClass(addTarget, options.cssClasses.target); + + if (options.dir === 0) { + addClass(addTarget, options.cssClasses.ltr); + } else { + addClass(addTarget, options.cssClasses.rtl); + } + + if (options.ort === 0) { + addClass(addTarget, options.cssClasses.horizontal); + } else { + addClass(addTarget, options.cssClasses.vertical); + } + + var textDirection = getComputedStyle(addTarget).direction; + + if (textDirection === "rtl") { + addClass(addTarget, options.cssClasses.textDirectionRtl); + } else { + addClass(addTarget, options.cssClasses.textDirectionLtr); + } + + return addNodeTo(addTarget, options.cssClasses.base); + } + + function addTooltip(handle, handleNumber) { + if (!options.tooltips[handleNumber]) { + return false; + } + + return addNodeTo(handle.firstChild, options.cssClasses.tooltip); + } + + function isSliderDisabled() { + return scope_Target.hasAttribute("disabled"); + } + + // Disable the slider dragging if any handle is disabled + function isHandleDisabled(handleNumber) { + var handleOrigin = scope_Handles[handleNumber]; + return handleOrigin.hasAttribute("disabled"); + } + + function removeTooltips() { + if (scope_Tooltips) { + removeEvent("update" + INTERNAL_EVENT_NS.tooltips); + scope_Tooltips.forEach(function(tooltip) { + if (tooltip) { + removeElement(tooltip); + } + }); + scope_Tooltips = null; + } + } + + // The tooltips option is a shorthand for using the 'update' event. + function tooltips() { + removeTooltips(); + + // Tooltips are added with options.tooltips in original order. + scope_Tooltips = scope_Handles.map(addTooltip); + + bindEvent("update" + INTERNAL_EVENT_NS.tooltips, function(values, handleNumber, unencoded) { + if (!scope_Tooltips[handleNumber]) { + return; + } + + var formattedValue = values[handleNumber]; + + if (options.tooltips[handleNumber] !== true) { + formattedValue = options.tooltips[handleNumber].to(unencoded[handleNumber]); + } + + scope_Tooltips[handleNumber].innerHTML = formattedValue; + }); + } + + function aria() { + removeEvent("update" + INTERNAL_EVENT_NS.aria); + bindEvent("update" + INTERNAL_EVENT_NS.aria, function(values, handleNumber, unencoded, tap, positions) { + // Update Aria Values for all handles, as a change in one changes min and max values for the next. + scope_HandleNumbers.forEach(function(index) { + var handle = scope_Handles[index]; + + var min = checkHandlePosition(scope_Locations, index, 0, true, true, true); + var max = checkHandlePosition(scope_Locations, index, 100, true, true, true); + + var now = positions[index]; + + // Formatted value for display + var text = options.ariaFormat.to(unencoded[index]); + + // Map to slider range values + min = scope_Spectrum.fromStepping(min).toFixed(1); + max = scope_Spectrum.fromStepping(max).toFixed(1); + now = scope_Spectrum.fromStepping(now).toFixed(1); + + handle.children[0].setAttribute("aria-valuemin", min); + handle.children[0].setAttribute("aria-valuemax", max); + handle.children[0].setAttribute("aria-valuenow", now); + handle.children[0].setAttribute("aria-valuetext", text); + }); + }); + } + + function getGroup(mode, values, stepped) { + // Use the range. + if (mode === "range" || mode === "steps") { + return scope_Spectrum.xVal; + } + + if (mode === "count") { + if (values < 2) { + throw new Error("noUiSlider (" + VERSION + "): 'values' (>= 2) required for mode 'count'."); + } + + // Divide 0 - 100 in 'count' parts. + var interval = values - 1; + var spread = 100 / interval; + + values = []; + + // List these parts and have them handled as 'positions'. + while (interval--) { + values[interval] = interval * spread; + } + + values.push(100); + + mode = "positions"; + } + + if (mode === "positions") { + // Map all percentages to on-range values. + return values.map(function(value) { + return scope_Spectrum.fromStepping(stepped ? scope_Spectrum.getStep(value) : value); + }); + } + + if (mode === "values") { + // If the value must be stepped, it needs to be converted to a percentage first. + if (stepped) { + return values.map(function(value) { + // Convert to percentage, apply step, return to value. + return scope_Spectrum.fromStepping(scope_Spectrum.getStep(scope_Spectrum.toStepping(value))); + }); + } + + // Otherwise, we can simply use the values. + return values; + } + } + + function generateSpread(density, mode, group) { + function safeIncrement(value, increment) { + // Avoid floating point variance by dropping the smallest decimal places. + return (value + increment).toFixed(7) / 1; + } + + var indexes = {}; + var firstInRange = scope_Spectrum.xVal[0]; + var lastInRange = scope_Spectrum.xVal[scope_Spectrum.xVal.length - 1]; + var ignoreFirst = false; + var ignoreLast = false; + var prevPct = 0; + + // Create a copy of the group, sort it and filter away all duplicates. + group = unique( + group.slice().sort(function(a, b) { + return a - b; + }) + ); + + // Make sure the range starts with the first element. + if (group[0] !== firstInRange) { + group.unshift(firstInRange); + ignoreFirst = true; + } + + // Likewise for the last one. + if (group[group.length - 1] !== lastInRange) { + group.push(lastInRange); + ignoreLast = true; + } + + group.forEach(function(current, index) { + // Get the current step and the lower + upper positions. + var step; + var i; + var q; + var low = current; + var high = group[index + 1]; + var newPct; + var pctDifference; + var pctPos; + var type; + var steps; + var realSteps; + var stepSize; + var isSteps = mode === "steps"; + + // When using 'steps' mode, use the provided steps. + // Otherwise, we'll step on to the next subrange. + if (isSteps) { + step = scope_Spectrum.xNumSteps[index]; + } + + // Default to a 'full' step. + if (!step) { + step = high - low; + } + + // Low can be 0, so test for false. Index 0 is already handled. + if (low === false) { + return; + } + + // If high is undefined we are at the last subrange. Make sure it iterates once (#1088) + if (high === undefined) { + high = low; + } + + // Make sure step isn't 0, which would cause an infinite loop (#654) + step = Math.max(step, 0.0000001); + + // Find all steps in the subrange. + for (i = low; i <= high; i = safeIncrement(i, step)) { + // Get the percentage value for the current step, + // calculate the size for the subrange. + newPct = scope_Spectrum.toStepping(i); + pctDifference = newPct - prevPct; + + steps = pctDifference / density; + realSteps = Math.round(steps); + + // This ratio represents the amount of percentage-space a point indicates. + // For a density 1 the points/percentage = 1. For density 2, that percentage needs to be re-divided. + // Round the percentage offset to an even number, then divide by two + // to spread the offset on both sides of the range. + stepSize = pctDifference / realSteps; + + // Divide all points evenly, adding the correct number to this subrange. + // Run up to <= so that 100% gets a point, event if ignoreLast is set. + for (q = 1; q <= realSteps; q += 1) { + // The ratio between the rounded value and the actual size might be ~1% off. + // Correct the percentage offset by the number of points + // per subrange. density = 1 will result in 100 points on the + // full range, 2 for 50, 4 for 25, etc. + pctPos = prevPct + q * stepSize; + indexes[pctPos.toFixed(5)] = [scope_Spectrum.fromStepping(pctPos), 0]; + } + + // Determine the point type. + type = group.indexOf(i) > -1 ? PIPS_LARGE_VALUE : isSteps ? PIPS_SMALL_VALUE : PIPS_NO_VALUE; + + // Enforce the 'ignoreFirst' option by overwriting the type for 0. + if (!index && ignoreFirst && i !== high) { + type = 0; + } + + if (!(i === high && ignoreLast)) { + // Mark the 'type' of this point. 0 = plain, 1 = real value, 2 = step value. + indexes[newPct.toFixed(5)] = [i, type]; + } + + // Update the percentage count. + prevPct = newPct; + } + }); + + return indexes; + } + + function addMarking(spread, filterFunc, formatter) { + var element = scope_Document.createElement("div"); + + var valueSizeClasses = []; + valueSizeClasses[PIPS_NO_VALUE] = options.cssClasses.valueNormal; + valueSizeClasses[PIPS_LARGE_VALUE] = options.cssClasses.valueLarge; + valueSizeClasses[PIPS_SMALL_VALUE] = options.cssClasses.valueSub; + + var markerSizeClasses = []; + markerSizeClasses[PIPS_NO_VALUE] = options.cssClasses.markerNormal; + markerSizeClasses[PIPS_LARGE_VALUE] = options.cssClasses.markerLarge; + markerSizeClasses[PIPS_SMALL_VALUE] = options.cssClasses.markerSub; + + var valueOrientationClasses = [options.cssClasses.valueHorizontal, options.cssClasses.valueVertical]; + var markerOrientationClasses = [options.cssClasses.markerHorizontal, options.cssClasses.markerVertical]; + + addClass(element, options.cssClasses.pips); + addClass(element, options.ort === 0 ? options.cssClasses.pipsHorizontal : options.cssClasses.pipsVertical); + + function getClasses(type, source) { + var a = source === options.cssClasses.value; + var orientationClasses = a ? valueOrientationClasses : markerOrientationClasses; + var sizeClasses = a ? valueSizeClasses : markerSizeClasses; + + return source + " " + orientationClasses[options.ort] + " " + sizeClasses[type]; + } + + function addSpread(offset, value, type) { + // Apply the filter function, if it is set. + type = filterFunc ? filterFunc(value, type) : type; + + if (type === PIPS_NONE) { + return; + } + + // Add a marker for every point + var node = addNodeTo(element, false); + node.className = getClasses(type, options.cssClasses.marker); + node.style[options.style] = offset + "%"; + + // Values are only appended for points marked '1' or '2'. + if (type > PIPS_NO_VALUE) { + node = addNodeTo(element, false); + node.className = getClasses(type, options.cssClasses.value); + node.setAttribute("data-value", value); + node.style[options.style] = offset + "%"; + node.innerHTML = formatter.to(value); + } + } + + // Append all points. + Object.keys(spread).forEach(function(offset) { + addSpread(offset, spread[offset][0], spread[offset][1]); + }); + + return element; + } + + function removePips() { + if (scope_Pips) { + removeElement(scope_Pips); + scope_Pips = null; + } + } + + function pips(grid) { + // Fix #669 + removePips(); + + var mode = grid.mode; + var density = grid.density || 1; + var filter = grid.filter || false; + var values = grid.values || false; + var stepped = grid.stepped || false; + var group = getGroup(mode, values, stepped); + var spread = generateSpread(density, mode, group); + var format = grid.format || { + to: Math.round + }; + + scope_Pips = scope_Target.appendChild(addMarking(spread, filter, format)); + + return scope_Pips; + } + + // Shorthand for base dimensions. + function baseSize() { + var rect = scope_Base.getBoundingClientRect(); + var alt = "offset" + ["Width", "Height"][options.ort]; + return options.ort === 0 ? rect.width || scope_Base[alt] : rect.height || scope_Base[alt]; + } + + // Handler for attaching events trough a proxy. + function attachEvent(events, element, callback, data) { + // This function can be used to 'filter' events to the slider. + // element is a node, not a nodeList + + var method = function(e) { + e = fixEvent(e, data.pageOffset, data.target || element); + + // fixEvent returns false if this event has a different target + // when handling (multi-) touch events; + if (!e) { + return false; + } + + // doNotReject is passed by all end events to make sure released touches + // are not rejected, leaving the slider "stuck" to the cursor; + if (isSliderDisabled() && !data.doNotReject) { + return false; + } + + // Stop if an active 'tap' transition is taking place. + if (hasClass(scope_Target, options.cssClasses.tap) && !data.doNotReject) { + return false; + } + + // Ignore right or middle clicks on start #454 + if (events === actions.start && e.buttons !== undefined && e.buttons > 1) { + return false; + } + + // Ignore right or middle clicks on start #454 + if (data.hover && e.buttons) { + return false; + } + + // 'supportsPassive' is only true if a browser also supports touch-action: none in CSS. + // iOS safari does not, so it doesn't get to benefit from passive scrolling. iOS does support + // touch-action: manipulation, but that allows panning, which breaks + // sliders after zooming/on non-responsive pages. + // See: https://bugs.webkit.org/show_bug.cgi?id=133112 + if (!supportsPassive) { + e.preventDefault(); + } + + e.calcPoint = e.points[options.ort]; + + // Call the event handler with the event [ and additional data ]. + callback(e, data); + }; + + var methods = []; + + // Bind a closure on the target for every event type. + events.split(" ").forEach(function(eventName) { + element.addEventListener(eventName, method, supportsPassive ? { passive: true } : false); + methods.push([eventName, method]); + }); + + return methods; + } + + // Provide a clean event with standardized offset values. + function fixEvent(e, pageOffset, eventTarget) { + // Filter the event to register the type, which can be + // touch, mouse or pointer. Offset changes need to be + // made on an event specific basis. + var touch = e.type.indexOf("touch") === 0; + var mouse = e.type.indexOf("mouse") === 0; + var pointer = e.type.indexOf("pointer") === 0; + + var x; + var y; + + // IE10 implemented pointer events with a prefix; + if (e.type.indexOf("MSPointer") === 0) { + pointer = true; + } + + // Erroneous events seem to be passed in occasionally on iOS/iPadOS after user finishes interacting with + // the slider. They appear to be of type MouseEvent, yet they don't have usual properties set. Ignore + // events that have no touches or buttons associated with them. (#1057, #1079, #1095) + if (e.type === "mousedown" && !e.buttons && !e.touches) { + return false; + } + + // The only thing one handle should be concerned about is the touches that originated on top of it. + if (touch) { + // Returns true if a touch originated on the target. + var isTouchOnTarget = function(checkTouch) { + return ( + checkTouch.target === eventTarget || + eventTarget.contains(checkTouch.target) || + (checkTouch.target.shadowRoot && checkTouch.target.shadowRoot.contains(eventTarget)) + ); + }; + + // In the case of touchstart events, we need to make sure there is still no more than one + // touch on the target so we look amongst all touches. + if (e.type === "touchstart") { + var targetTouches = Array.prototype.filter.call(e.touches, isTouchOnTarget); + + // Do not support more than one touch per handle. + if (targetTouches.length > 1) { + return false; + } + + x = targetTouches[0].pageX; + y = targetTouches[0].pageY; + } else { + // In the other cases, find on changedTouches is enough. + var targetTouch = Array.prototype.find.call(e.changedTouches, isTouchOnTarget); + + // Cancel if the target touch has not moved. + if (!targetTouch) { + return false; + } + + x = targetTouch.pageX; + y = targetTouch.pageY; + } + } + + pageOffset = pageOffset || getPageOffset(scope_Document); + + if (mouse || pointer) { + x = e.clientX + pageOffset.x; + y = e.clientY + pageOffset.y; + } + + e.pageOffset = pageOffset; + e.points = [x, y]; + e.cursor = mouse || pointer; // Fix #435 + + return e; + } + + // Translate a coordinate in the document to a percentage on the slider + function calcPointToPercentage(calcPoint) { + var location = calcPoint - offset(scope_Base, options.ort); + var proposal = (location * 100) / baseSize(); + + // Clamp proposal between 0% and 100% + // Out-of-bound coordinates may occur when .noUi-base pseudo-elements + // are used (e.g. contained handles feature) + proposal = limit(proposal); + + return options.dir ? 100 - proposal : proposal; + } + + // Find handle closest to a certain percentage on the slider + function getClosestHandle(clickedPosition) { + var smallestDifference = 100; + var handleNumber = false; + + scope_Handles.forEach(function(handle, index) { + // Disabled handles are ignored + if (isHandleDisabled(index)) { + return; + } + + var handlePosition = scope_Locations[index]; + var differenceWithThisHandle = Math.abs(handlePosition - clickedPosition); + + // Initial state + var clickAtEdge = differenceWithThisHandle === 100 && smallestDifference === 100; + + // Difference with this handle is smaller than the previously checked handle + var isCloser = differenceWithThisHandle < smallestDifference; + var isCloserAfter = differenceWithThisHandle <= smallestDifference && clickedPosition > handlePosition; + + if (isCloser || isCloserAfter || clickAtEdge) { + handleNumber = index; + smallestDifference = differenceWithThisHandle; + } + }); + + return handleNumber; + } + + // Fire 'end' when a mouse or pen leaves the document. + function documentLeave(event, data) { + if (event.type === "mouseout" && event.target.nodeName === "HTML" && event.relatedTarget === null) { + eventEnd(event, data); + } + } + + // Handle movement on document for handle and range drag. + function eventMove(event, data) { + // Fix #498 + // Check value of .buttons in 'start' to work around a bug in IE10 mobile (data.buttonsProperty). + // https://connect.microsoft.com/IE/feedback/details/927005/mobile-ie10-windows-phone-buttons-property-of-pointermove-event-always-zero + // IE9 has .buttons and .which zero on mousemove. + // Firefox breaks the spec MDN defines. + if (navigator.appVersion.indexOf("MSIE 9") === -1 && event.buttons === 0 && data.buttonsProperty !== 0) { + return eventEnd(event, data); + } + + // Check if we are moving up or down + var movement = (options.dir ? -1 : 1) * (event.calcPoint - data.startCalcPoint); + + // Convert the movement into a percentage of the slider width/height + var proposal = (movement * 100) / data.baseSize; + + moveHandles(movement > 0, proposal, data.locations, data.handleNumbers); + } + + // Unbind move events on document, call callbacks. + function eventEnd(event, data) { + // The handle is no longer active, so remove the class. + if (data.handle) { + removeClass(data.handle, options.cssClasses.active); + scope_ActiveHandlesCount -= 1; + } + + // Unbind the move and end events, which are added on 'start'. + data.listeners.forEach(function(c) { + scope_DocumentElement.removeEventListener(c[0], c[1]); + }); + + if (scope_ActiveHandlesCount === 0) { + // Remove dragging class. + removeClass(scope_Target, options.cssClasses.drag); + setZindex(); + + // Remove cursor styles and text-selection events bound to the body. + if (event.cursor) { + scope_Body.style.cursor = ""; + scope_Body.removeEventListener("selectstart", preventDefault); + } + } + + data.handleNumbers.forEach(function(handleNumber) { + fireEvent("change", handleNumber); + fireEvent("set", handleNumber); + fireEvent("end", handleNumber); + }); + } + + // Bind move events on document. + function eventStart(event, data) { + // Ignore event if any handle is disabled + if (data.handleNumbers.some(isHandleDisabled)) { + return false; + } + + var handle; + + if (data.handleNumbers.length === 1) { + var handleOrigin = scope_Handles[data.handleNumbers[0]]; + + handle = handleOrigin.children[0]; + scope_ActiveHandlesCount += 1; + + // Mark the handle as 'active' so it can be styled. + addClass(handle, options.cssClasses.active); + } + + // A drag should never propagate up to the 'tap' event. + event.stopPropagation(); + + // Record the event listeners. + var listeners = []; + + // Attach the move and end events. + var moveEvent = attachEvent(actions.move, scope_DocumentElement, eventMove, { + // The event target has changed so we need to propagate the original one so that we keep + // relying on it to extract target touches. + target: event.target, + handle: handle, + listeners: listeners, + startCalcPoint: event.calcPoint, + baseSize: baseSize(), + pageOffset: event.pageOffset, + handleNumbers: data.handleNumbers, + buttonsProperty: event.buttons, + locations: scope_Locations.slice() + }); + + var endEvent = attachEvent(actions.end, scope_DocumentElement, eventEnd, { + target: event.target, + handle: handle, + listeners: listeners, + doNotReject: true, + handleNumbers: data.handleNumbers + }); + + var outEvent = attachEvent("mouseout", scope_DocumentElement, documentLeave, { + target: event.target, + handle: handle, + listeners: listeners, + doNotReject: true, + handleNumbers: data.handleNumbers + }); + + // We want to make sure we pushed the listeners in the listener list rather than creating + // a new one as it has already been passed to the event handlers. + listeners.push.apply(listeners, moveEvent.concat(endEvent, outEvent)); + + // Text selection isn't an issue on touch devices, + // so adding cursor styles can be skipped. + if (event.cursor) { + // Prevent the 'I' cursor and extend the range-drag cursor. + scope_Body.style.cursor = getComputedStyle(event.target).cursor; + + // Mark the target with a dragging state. + if (scope_Handles.length > 1) { + addClass(scope_Target, options.cssClasses.drag); + } + + // Prevent text selection when dragging the handles. + // In noUiSlider <= 9.2.0, this was handled by calling preventDefault on mouse/touch start/move, + // which is scroll blocking. The selectstart event is supported by FireFox starting from version 52, + // meaning the only holdout is iOS Safari. This doesn't matter: text selection isn't triggered there. + // The 'cursor' flag is false. + // See: http://caniuse.com/#search=selectstart + scope_Body.addEventListener("selectstart", preventDefault, false); + } + + data.handleNumbers.forEach(function(handleNumber) { + fireEvent("start", handleNumber); + }); + } + + // Move closest handle to tapped location. + function eventTap(event) { + // The tap event shouldn't propagate up + event.stopPropagation(); + + var proposal = calcPointToPercentage(event.calcPoint); + var handleNumber = getClosestHandle(proposal); + + // Tackle the case that all handles are 'disabled'. + if (handleNumber === false) { + return false; + } + + // Flag the slider as it is now in a transitional state. + // Transition takes a configurable amount of ms (default 300). Re-enable the slider after that. + if (!options.events.snap) { + addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration); + } + + setHandle(handleNumber, proposal, true, true); + + setZindex(); + + fireEvent("slide", handleNumber, true); + fireEvent("update", handleNumber, true); + fireEvent("change", handleNumber, true); + fireEvent("set", handleNumber, true); + + if (options.events.snap) { + eventStart(event, { handleNumbers: [handleNumber] }); + } + } + + // Fires a 'hover' event for a hovered mouse/pen position. + function eventHover(event) { + var proposal = calcPointToPercentage(event.calcPoint); + + var to = scope_Spectrum.getStep(proposal); + var value = scope_Spectrum.fromStepping(to); + + Object.keys(scope_Events).forEach(function(targetEvent) { + if ("hover" === targetEvent.split(".")[0]) { + scope_Events[targetEvent].forEach(function(callback) { + callback.call(scope_Self, value); + }); + } + }); + } + + // Handles keydown on focused handles + // Don't move the document when pressing arrow keys on focused handles + function eventKeydown(event, handleNumber) { + if (isSliderDisabled() || isHandleDisabled(handleNumber)) { + return false; + } + + var horizontalKeys = ["Left", "Right"]; + var verticalKeys = ["Down", "Up"]; + var largeStepKeys = ["PageDown", "PageUp"]; + var edgeKeys = ["Home", "End"]; + + if (options.dir && !options.ort) { + // On an right-to-left slider, the left and right keys act inverted + horizontalKeys.reverse(); + } else if (options.ort && !options.dir) { + // On a top-to-bottom slider, the up and down keys act inverted + verticalKeys.reverse(); + largeStepKeys.reverse(); + } + + // Strip "Arrow" for IE compatibility. https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key + var key = event.key.replace("Arrow", ""); + + var isLargeDown = key === largeStepKeys[0]; + var isLargeUp = key === largeStepKeys[1]; + var isDown = key === verticalKeys[0] || key === horizontalKeys[0] || isLargeDown; + var isUp = key === verticalKeys[1] || key === horizontalKeys[1] || isLargeUp; + var isMin = key === edgeKeys[0]; + var isMax = key === edgeKeys[1]; + + if (!isDown && !isUp && !isMin && !isMax) { + return true; + } + + event.preventDefault(); + + var to; + + if (isUp || isDown) { + var multiplier = options.keyboardPageMultiplier; + var direction = isDown ? 0 : 1; + var steps = getNextStepsForHandle(handleNumber); + var step = steps[direction]; + + // At the edge of a slider, do nothing + if (step === null) { + return false; + } + + // No step set, use the default of 10% of the sub-range + if (step === false) { + step = scope_Spectrum.getDefaultStep( + scope_Locations[handleNumber], + isDown, + options.keyboardDefaultStep + ); + } + + if (isLargeUp || isLargeDown) { + step *= multiplier; + } + + // Step over zero-length ranges (#948); + step = Math.max(step, 0.0000001); + + // Decrement for down steps + step = (isDown ? -1 : 1) * step; + + to = scope_Values[handleNumber] + step; + } else if (isMax) { + // End key + to = options.spectrum.xVal[options.spectrum.xVal.length - 1]; + } else { + // Home key + to = options.spectrum.xVal[0]; + } + + setHandle(handleNumber, scope_Spectrum.toStepping(to), true, true); + + fireEvent("slide", handleNumber); + fireEvent("update", handleNumber); + fireEvent("change", handleNumber); + fireEvent("set", handleNumber); + + return false; + } + + // Attach events to several slider parts. + function bindSliderEvents(behaviour) { + // Attach the standard drag event to the handles. + if (!behaviour.fixed) { + scope_Handles.forEach(function(handle, index) { + // These events are only bound to the visual handle + // element, not the 'real' origin element. + attachEvent(actions.start, handle.children[0], eventStart, { + handleNumbers: [index] + }); + }); + } + + // Attach the tap event to the slider base. + if (behaviour.tap) { + attachEvent(actions.start, scope_Base, eventTap, {}); + } + + // Fire hover events + if (behaviour.hover) { + attachEvent(actions.move, scope_Base, eventHover, { + hover: true + }); + } + + // Make the range draggable. + if (behaviour.drag) { + scope_Connects.forEach(function(connect, index) { + if (connect === false || index === 0 || index === scope_Connects.length - 1) { + return; + } + + var handleBefore = scope_Handles[index - 1]; + var handleAfter = scope_Handles[index]; + var eventHolders = [connect]; + + addClass(connect, options.cssClasses.draggable); + + // When the range is fixed, the entire range can + // be dragged by the handles. The handle in the first + // origin will propagate the start event upward, + // but it needs to be bound manually on the other. + if (behaviour.fixed) { + eventHolders.push(handleBefore.children[0]); + eventHolders.push(handleAfter.children[0]); + } + + eventHolders.forEach(function(eventHolder) { + attachEvent(actions.start, eventHolder, eventStart, { + handles: [handleBefore, handleAfter], + handleNumbers: [index - 1, index] + }); + }); + }); + } + } + + // Attach an event to this slider, possibly including a namespace + function bindEvent(namespacedEvent, callback) { + scope_Events[namespacedEvent] = scope_Events[namespacedEvent] || []; + scope_Events[namespacedEvent].push(callback); + + // If the event bound is 'update,' fire it immediately for all handles. + if (namespacedEvent.split(".")[0] === "update") { + scope_Handles.forEach(function(a, index) { + fireEvent("update", index); + }); + } + } + + function isInternalNamespace(namespace) { + return namespace === INTERNAL_EVENT_NS.aria || namespace === INTERNAL_EVENT_NS.tooltips; + } + + // Undo attachment of event + function removeEvent(namespacedEvent) { + var event = namespacedEvent && namespacedEvent.split(".")[0]; + var namespace = event ? namespacedEvent.substring(event.length) : namespacedEvent; + + Object.keys(scope_Events).forEach(function(bind) { + var tEvent = bind.split(".")[0]; + var tNamespace = bind.substring(tEvent.length); + if ((!event || event === tEvent) && (!namespace || namespace === tNamespace)) { + // only delete protected internal event if intentional + if (!isInternalNamespace(tNamespace) || namespace === tNamespace) { + delete scope_Events[bind]; + } + } + }); + } + + // External event handling + function fireEvent(eventName, handleNumber, tap) { + Object.keys(scope_Events).forEach(function(targetEvent) { + var eventType = targetEvent.split(".")[0]; + + if (eventName === eventType) { + scope_Events[targetEvent].forEach(function(callback) { + callback.call( + // Use the slider public API as the scope ('this') + scope_Self, + // Return values as array, so arg_1[arg_2] is always valid. + scope_Values.map(options.format.to), + // Handle index, 0 or 1 + handleNumber, + // Un-formatted slider values + scope_Values.slice(), + // Event is fired by tap, true or false + tap || false, + // Left offset of the handle, in relation to the slider + scope_Locations.slice(), + // add the slider public API to an accessible parameter when this is unavailable + scope_Self + ); + }); + } + }); + } + + // Split out the handle positioning logic so the Move event can use it, too + function checkHandlePosition(reference, handleNumber, to, lookBackward, lookForward, getValue) { + var distance; + + // For sliders with multiple handles, limit movement to the other handle. + // Apply the margin option by adding it to the handle positions. + if (scope_Handles.length > 1 && !options.events.unconstrained) { + if (lookBackward && handleNumber > 0) { + distance = scope_Spectrum.getAbsoluteDistance(reference[handleNumber - 1], options.margin, 0); + to = Math.max(to, distance); + } + + if (lookForward && handleNumber < scope_Handles.length - 1) { + distance = scope_Spectrum.getAbsoluteDistance(reference[handleNumber + 1], options.margin, 1); + to = Math.min(to, distance); + } + } + + // The limit option has the opposite effect, limiting handles to a + // maximum distance from another. Limit must be > 0, as otherwise + // handles would be unmovable. + if (scope_Handles.length > 1 && options.limit) { + if (lookBackward && handleNumber > 0) { + distance = scope_Spectrum.getAbsoluteDistance(reference[handleNumber - 1], options.limit, 0); + to = Math.min(to, distance); + } + + if (lookForward && handleNumber < scope_Handles.length - 1) { + distance = scope_Spectrum.getAbsoluteDistance(reference[handleNumber + 1], options.limit, 1); + to = Math.max(to, distance); + } + } + + // The padding option keeps the handles a certain distance from the + // edges of the slider. Padding must be > 0. + if (options.padding) { + if (handleNumber === 0) { + distance = scope_Spectrum.getAbsoluteDistance(0, options.padding[0], 0); + to = Math.max(to, distance); + } + + if (handleNumber === scope_Handles.length - 1) { + distance = scope_Spectrum.getAbsoluteDistance(100, options.padding[1], 1); + to = Math.min(to, distance); + } + } + + to = scope_Spectrum.getStep(to); + + // Limit percentage to the 0 - 100 range + to = limit(to); + + // Return false if handle can't move + if (to === reference[handleNumber] && !getValue) { + return false; + } + + return to; + } + + // Uses slider orientation to create CSS rules. a = base value; + function inRuleOrder(v, a) { + var o = options.ort; + return (o ? a : v) + ", " + (o ? v : a); + } + + // Moves handle(s) by a percentage + // (bool, % to move, [% where handle started, ...], [index in scope_Handles, ...]) + function moveHandles(upward, proposal, locations, handleNumbers) { + var proposals = locations.slice(); + + var b = [!upward, upward]; + var f = [upward, !upward]; + + // Copy handleNumbers so we don't change the dataset + handleNumbers = handleNumbers.slice(); + + // Check to see which handle is 'leading'. + // If that one can't move the second can't either. + if (upward) { + handleNumbers.reverse(); + } + + // Step 1: get the maximum percentage that any of the handles can move + if (handleNumbers.length > 1) { + handleNumbers.forEach(function(handleNumber, o) { + var to = checkHandlePosition( + proposals, + handleNumber, + proposals[handleNumber] + proposal, + b[o], + f[o], + false + ); + + // Stop if one of the handles can't move. + if (to === false) { + proposal = 0; + } else { + proposal = to - proposals[handleNumber]; + proposals[handleNumber] = to; + } + }); + } + + // If using one handle, check backward AND forward + else { + b = f = [true]; + } + + var state = false; + + // Step 2: Try to set the handles with the found percentage + handleNumbers.forEach(function(handleNumber, o) { + state = setHandle(handleNumber, locations[handleNumber] + proposal, b[o], f[o]) || state; + }); + + // Step 3: If a handle moved, fire events + if (state) { + handleNumbers.forEach(function(handleNumber) { + fireEvent("update", handleNumber); + fireEvent("slide", handleNumber); + }); + } + } + + // Takes a base value and an offset. This offset is used for the connect bar size. + // In the initial design for this feature, the origin element was 1% wide. + // Unfortunately, a rounding bug in Chrome makes it impossible to implement this feature + // in this manner: https://bugs.chromium.org/p/chromium/issues/detail?id=798223 + function transformDirection(a, b) { + return options.dir ? 100 - a - b : a; + } + + // Updates scope_Locations and scope_Values, updates visual state + function updateHandlePosition(handleNumber, to) { + // Update locations. + scope_Locations[handleNumber] = to; + + // Convert the value to the slider stepping/range. + scope_Values[handleNumber] = scope_Spectrum.fromStepping(to); + + var translation = 10 * (transformDirection(to, 0) - scope_DirOffset); + var translateRule = "translate(" + inRuleOrder(translation + "%", "0") + ")"; + + scope_Handles[handleNumber].style[options.transformRule] = translateRule; + + updateConnect(handleNumber); + updateConnect(handleNumber + 1); + } + + // Handles before the slider middle are stacked later = higher, + // Handles after the middle later is lower + // [[7] [8] .......... | .......... [5] [4] + function setZindex() { + scope_HandleNumbers.forEach(function(handleNumber) { + var dir = scope_Locations[handleNumber] > 50 ? -1 : 1; + var zIndex = 3 + (scope_Handles.length + dir * handleNumber); + scope_Handles[handleNumber].style.zIndex = zIndex; + }); + } + + // Test suggested values and apply margin, step. + // if exactInput is true, don't run checkHandlePosition, then the handle can be placed in between steps (#436) + function setHandle(handleNumber, to, lookBackward, lookForward, exactInput) { + if (!exactInput) { + to = checkHandlePosition(scope_Locations, handleNumber, to, lookBackward, lookForward, false); + } + + if (to === false) { + return false; + } + + updateHandlePosition(handleNumber, to); + + return true; + } + + // Updates style attribute for connect nodes + function updateConnect(index) { + // Skip connects set to false + if (!scope_Connects[index]) { + return; + } + + var l = 0; + var h = 100; + + if (index !== 0) { + l = scope_Locations[index - 1]; + } + + if (index !== scope_Connects.length - 1) { + h = scope_Locations[index]; + } + + // We use two rules: + // 'translate' to change the left/top offset; + // 'scale' to change the width of the element; + // As the element has a width of 100%, a translation of 100% is equal to 100% of the parent (.noUi-base) + var connectWidth = h - l; + var translateRule = "translate(" + inRuleOrder(transformDirection(l, connectWidth) + "%", "0") + ")"; + var scaleRule = "scale(" + inRuleOrder(connectWidth / 100, "1") + ")"; + + scope_Connects[index].style[options.transformRule] = translateRule + " " + scaleRule; + } + + // Parses value passed to .set method. Returns current value if not parse-able. + function resolveToValue(to, handleNumber) { + // Setting with null indicates an 'ignore'. + // Inputting 'false' is invalid. + if (to === null || to === false || to === undefined) { + return scope_Locations[handleNumber]; + } + + // If a formatted number was passed, attempt to decode it. + if (typeof to === "number") { + to = String(to); + } + + to = options.format.from(to); + to = scope_Spectrum.toStepping(to); + + // If parsing the number failed, use the current value. + if (to === false || isNaN(to)) { + return scope_Locations[handleNumber]; + } + + return to; + } + + // Set the slider value. + function valueSet(input, fireSetEvent, exactInput) { + var values = asArray(input); + var isInit = scope_Locations[0] === undefined; + + // Event fires by default + fireSetEvent = fireSetEvent === undefined ? true : !!fireSetEvent; + + // Animation is optional. + // Make sure the initial values were set before using animated placement. + if (options.animate && !isInit) { + addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration); + } + + // First pass, without lookAhead but with lookBackward. Values are set from left to right. + scope_HandleNumbers.forEach(function(handleNumber) { + setHandle(handleNumber, resolveToValue(values[handleNumber], handleNumber), true, false, exactInput); + }); + + var i = scope_HandleNumbers.length === 1 ? 0 : 1; + + // Secondary passes. Now that all base values are set, apply constraints. + // Iterate all handles to ensure constraints are applied for the entire slider (Issue #1009) + for (; i < scope_HandleNumbers.length; ++i) { + scope_HandleNumbers.forEach(function(handleNumber) { + setHandle(handleNumber, scope_Locations[handleNumber], true, true, exactInput); + }); + } + + setZindex(); + + scope_HandleNumbers.forEach(function(handleNumber) { + fireEvent("update", handleNumber); + + // Fire the event only for handles that received a new value, as per #579 + if (values[handleNumber] !== null && fireSetEvent) { + fireEvent("set", handleNumber); + } + }); + } + + // Reset slider to initial values + function valueReset(fireSetEvent) { + valueSet(options.start, fireSetEvent); + } + + // Set value for a single handle + function valueSetHandle(handleNumber, value, fireSetEvent, exactInput) { + // Ensure numeric input + handleNumber = Number(handleNumber); + + if (!(handleNumber >= 0 && handleNumber < scope_HandleNumbers.length)) { + throw new Error("noUiSlider (" + VERSION + "): invalid handle number, got: " + handleNumber); + } + + // Look both backward and forward, since we don't want this handle to "push" other handles (#960); + // The exactInput argument can be used to ignore slider stepping (#436) + setHandle(handleNumber, resolveToValue(value, handleNumber), true, true, exactInput); + + fireEvent("update", handleNumber); + + if (fireSetEvent) { + fireEvent("set", handleNumber); + } + } + + // Get the slider value. + function valueGet() { + var values = scope_Values.map(options.format.to); + + // If only one handle is used, return a single value. + if (values.length === 1) { + return values[0]; + } + + return values; + } + + // Removes classes from the root and empties it. + function destroy() { + // remove protected internal listeners + removeEvent(INTERNAL_EVENT_NS.aria); + removeEvent(INTERNAL_EVENT_NS.tooltips); + + for (var key in options.cssClasses) { + if (!options.cssClasses.hasOwnProperty(key)) { + continue; + } + removeClass(scope_Target, options.cssClasses[key]); + } + + while (scope_Target.firstChild) { + scope_Target.removeChild(scope_Target.firstChild); + } + + delete scope_Target.noUiSlider; + } + + function getNextStepsForHandle(handleNumber) { + var location = scope_Locations[handleNumber]; + var nearbySteps = scope_Spectrum.getNearbySteps(location); + var value = scope_Values[handleNumber]; + var increment = nearbySteps.thisStep.step; + var decrement = null; + + // If snapped, directly use defined step value + if (options.snap) { + return [ + value - nearbySteps.stepBefore.startValue || null, + nearbySteps.stepAfter.startValue - value || null + ]; + } + + // If the next value in this step moves into the next step, + // the increment is the start of the next step - the current value + if (increment !== false) { + if (value + increment > nearbySteps.stepAfter.startValue) { + increment = nearbySteps.stepAfter.startValue - value; + } + } + + // If the value is beyond the starting point + if (value > nearbySteps.thisStep.startValue) { + decrement = nearbySteps.thisStep.step; + } else if (nearbySteps.stepBefore.step === false) { + decrement = false; + } + + // If a handle is at the start of a step, it always steps back into the previous step first + else { + decrement = value - nearbySteps.stepBefore.highestStep; + } + + // Now, if at the slider edges, there is no in/decrement + if (location === 100) { + increment = null; + } else if (location === 0) { + decrement = null; + } + + // As per #391, the comparison for the decrement step can have some rounding issues. + var stepDecimals = scope_Spectrum.countStepDecimals(); + + // Round per #391 + if (increment !== null && increment !== false) { + increment = Number(increment.toFixed(stepDecimals)); + } + + if (decrement !== null && decrement !== false) { + decrement = Number(decrement.toFixed(stepDecimals)); + } + + return [decrement, increment]; + } + + // Get the current step size for the slider. + function getNextSteps() { + return scope_HandleNumbers.map(getNextStepsForHandle); + } + + // Updateable: margin, limit, padding, step, range, animate, snap + function updateOptions(optionsToUpdate, fireSetEvent) { + // Spectrum is created using the range, snap, direction and step options. + // 'snap' and 'step' can be updated. + // If 'snap' and 'step' are not passed, they should remain unchanged. + var v = valueGet(); + + var updateAble = [ + "margin", + "limit", + "padding", + "range", + "animate", + "snap", + "step", + "format", + "pips", + "tooltips" + ]; + + // Only change options that we're actually passed to update. + updateAble.forEach(function(name) { + // Check for undefined. null removes the value. + if (optionsToUpdate[name] !== undefined) { + originalOptions[name] = optionsToUpdate[name]; + } + }); + + var newOptions = testOptions(originalOptions); + + // Load new options into the slider state + updateAble.forEach(function(name) { + if (optionsToUpdate[name] !== undefined) { + options[name] = newOptions[name]; + } + }); + + scope_Spectrum = newOptions.spectrum; + + // Limit, margin and padding depend on the spectrum but are stored outside of it. (#677) + options.margin = newOptions.margin; + options.limit = newOptions.limit; + options.padding = newOptions.padding; + + // Update pips, removes existing. + if (options.pips) { + pips(options.pips); + } else { + removePips(); + } + + // Update tooltips, removes existing. + if (options.tooltips) { + tooltips(); + } else { + removeTooltips(); + } + + // Invalidate the current positioning so valueSet forces an update. + scope_Locations = []; + valueSet(optionsToUpdate.start || v, fireSetEvent); + } + + // Initialization steps + function setupSlider() { + // Create the base element, initialize HTML and set classes. + // Add handles and connect elements. + scope_Base = addSlider(scope_Target); + + addElements(options.connect, scope_Base); + + // Attach user events. + bindSliderEvents(options.events); + + // Use the public value method to set the start values. + valueSet(options.start); + + if (options.pips) { + pips(options.pips); + } + + if (options.tooltips) { + tooltips(); + } + + aria(); + } + + setupSlider(); + + // noinspection JSUnusedGlobalSymbols + scope_Self = { + destroy: destroy, + steps: getNextSteps, + on: bindEvent, + off: removeEvent, + get: valueGet, + set: valueSet, + setHandle: valueSetHandle, + reset: valueReset, + // Exposed for unit testing, don't use this in your application. + __moveHandles: function(a, b, c) { + moveHandles(a, b, scope_Locations, c); + }, + options: originalOptions, // Issue #600, #678 + updateOptions: updateOptions, + target: scope_Target, // Issue #597 + removePips: removePips, + removeTooltips: removeTooltips, + getTooltips: function() { + return scope_Tooltips; + }, + getOrigins: function() { + return scope_Handles; + }, + pips: pips // Issue #594 + }; + + return scope_Self; + } + + // Run the standard initializer + function initialize(target, originalOptions) { + if (!target || !target.nodeName) { + throw new Error("noUiSlider (" + VERSION + "): create requires a single element, got: " + target); + } + + // Throw an error if the slider was already initialized. + if (target.noUiSlider) { + throw new Error("noUiSlider (" + VERSION + "): Slider was already initialized."); + } + + // Test the options and create the slider environment; + var options = testOptions(originalOptions, target); + var api = scope(target, options, originalOptions); + + target.noUiSlider = api; + + return api; + } + + // Use an object instead of a function for future expandability; + return { + // Exposed for unit testing, don't use this in your application. + __spectrum: Spectrum, + version: VERSION, + // A reference to the default classes, allows global changes. + // Use the cssClasses option for changes to one slider. + cssClasses: cssClasses, + create: initialize + }; +}); diff --git a/public_html/noUiSlider/distribute/nouislider.min.css b/public_html/noUiSlider/distribute/nouislider.min.css new file mode 100644 index 0000000..8181ce3 --- /dev/null +++ b/public_html/noUiSlider/distribute/nouislider.min.css @@ -0,0 +1,2 @@ +/*! nouislider - 14.6.3 - 11/19/2020 */ +.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative}.noUi-base,.noUi-connects{width:100%;height:100%;position:relative;z-index:1}.noUi-connects{overflow:hidden;z-index:0}.noUi-connect,.noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;right:0;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;-webkit-transform-style:preserve-3d;transform-origin:0 0;transform-style:flat}.noUi-connect{height:100%;width:100%}.noUi-origin{height:10%;width:10%}.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin{left:0;right:auto}.noUi-vertical .noUi-origin{width:0}.noUi-horizontal .noUi-origin{height:0}.noUi-handle{-webkit-backface-visibility:hidden;backface-visibility:hidden;position:absolute}.noUi-touch-area{height:100%;width:100%}.noUi-state-tap .noUi-connect,.noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;right:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;right:-6px;top:-17px}.noUi-txt-dir-rtl.noUi-horizontal .noUi-handle{left:-17px;right:auto}.noUi-target{background:#FAFAFA;border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-connects{border-radius:3px}.noUi-connect{background:#3FB8AF}.noUi-draggable{cursor:ew-resize}.noUi-vertical .noUi-draggable{cursor:ns-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect{background:#B8B8B8}[disabled] .noUi-handle,[disabled].noUi-handle,[disabled].noUi-target{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;color:#999}.noUi-value{position:absolute;white-space:nowrap;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-sub{background:#AAA}.noUi-marker-large{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);padding-left:25px}.noUi-rtl .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translate(0,50%)}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}.noUi-tooltip{display:block;position:absolute;border:1px solid #D9D9D9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%,0);left:50%;bottom:120%}.noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);top:50%;right:120%}.noUi-horizontal .noUi-origin>.noUi-tooltip{-webkit-transform:translate(50%,0);transform:translate(50%,0);left:auto;bottom:10px}.noUi-vertical .noUi-origin>.noUi-tooltip{-webkit-transform:translate(0,-18px);transform:translate(0,-18px);top:auto;right:28px} \ No newline at end of file diff --git a/public_html/noUiSlider/distribute/nouislider.min.js b/public_html/noUiSlider/distribute/nouislider.min.js new file mode 100644 index 0000000..0f04919 --- /dev/null +++ b/public_html/noUiSlider/distribute/nouislider.min.js @@ -0,0 +1,2 @@ +/*! nouislider - 14.6.3 - 11/19/2020 */ +!function(t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?module.exports=t():window.noUiSlider=t()}(function(){"use strict";var lt="14.6.3";function ut(t){t.parentElement.removeChild(t)}function a(t){return null!=t}function ct(t){t.preventDefault()}function o(t){return"number"==typeof t&&!isNaN(t)&&isFinite(t)}function pt(t,e,r){0=e[r];)r+=1;return r}function r(t,e,r){if(r>=t.slice(-1)[0])return 100;var n,i,o=f(r,t),s=t[o-1],a=t[o],l=e[o-1],u=e[o];return l+(i=r,p(n=[s,a],n[0]<0?i+Math.abs(n[0]):i-n[0],0)/c(l,u))}function n(t,e,r,n){if(100===n)return n;var i,o,s=f(n,t),a=t[s-1],l=t[s];return r?(l-a)/2this.xPct[i+1];)i++;else t===this.xPct[this.xPct.length-1]&&(i=this.xPct.length-2);r||t!==this.xPct[i+1]||i++;var o=1,s=e[i],a=0,l=0,u=0,c=0;for(n=r?(t-this.xPct[i])/(this.xPct[i+1]-this.xPct[i]):(this.xPct[i+1]-t)/(this.xPct[i+1]-this.xPct[i]);0= 2) required for mode 'count'.");var n=e-1,i=100/n;for(e=[];n--;)e[n]=n*i;e.push(100),t="positions"}return"positions"===t?e.map(function(t){return y.fromStepping(r?y.getStep(t):t)}):"values"===t?r?e.map(function(t){return y.fromStepping(y.getStep(y.toStepping(t)))}):e:void 0}(n,t.values||!1,t.stepped||!1),a=(m=i,g=n,v=s,b={},e=y.xVal[0],r=y.xVal[y.xVal.length-1],S=x=!1,w=0,(v=v.slice().sort(function(t,e){return t-e}).filter(function(t){return!this[t]&&(this[t]=!0)},{}))[0]!==e&&(v.unshift(e),x=!0),v[v.length-1]!==r&&(v.push(r),S=!0),v.forEach(function(t,e){var r,n,i,o,s,a,l,u,c,p,f=t,d=v[e+1],h="steps"===g;if(h&&(r=y.xNumSteps[e]),r||(r=d-f),!1!==f)for(void 0===d&&(d=f),r=Math.max(r,1e-7),n=f;n<=d;n=(n+r).toFixed(7)/1){for(u=(s=(o=y.toStepping(n))-w)/m,p=s/(c=Math.round(u)),i=1;i<=c;i+=1)b[(a=w+i*p).toFixed(5)]=[y.fromStepping(a),0];l=-1r.stepAfter.startValue&&(i=r.stepAfter.startValue-n),o=n>r.thisStep.startValue?r.thisStep.step:!1!==r.stepBefore.step&&n-r.stepBefore.highestStep,100===e?i=null:0===e&&(o=null);var s=y.countStepDecimals();return null!==i&&!1!==i&&(i=Number(i.toFixed(s))),null!==o&&!1!==o&&(o=Number(o.toFixed(s))),[o,i]}return ht(e=h,b.cssClasses.target),0===b.dir?ht(e,b.cssClasses.ltr):ht(e,b.cssClasses.rtl),0===b.ort?ht(e,b.cssClasses.horizontal):ht(e,b.cssClasses.vertical),ht(e,"rtl"===getComputedStyle(e).direction?b.cssClasses.textDirectionRtl:b.cssClasses.textDirectionLtr),l=V(e,b.cssClasses.base),function(t,e){var r=V(e,b.cssClasses.connects);u=[],(s=[]).push(M(r,t[0]));for(var n=0;n this.filter.maxAltitude; + var isFilteredByAltitude = planeAltitude < this.filter.minAltitude || planeAltitude > this.filter.maxAltitude; + if (isFilteredByAltitude) { + return true; + } + } + if (this.filter.minSpeedFilter !== undefined && this.filter.maxSpeedFilter !== undefined) { + if (this.speed === null || this.speed === undefined) { + return true; + } + + var convertedSpeed = convert_speed(this.speed, this.filter.speedUnits) + var isFilteredBySpeed = convertedSpeed < this.filter.minSpeedFilter || convertedSpeed > this.filter.maxSpeedFilter; + if (isFilteredBySpeed) { + return true; + } } // filter out ground vehicles if (typeof this.filter.groundVehicles !== 'undefined' && this.filter.groundVehicles === 'filtered') { if (typeof this.category === 'string' && this.category.startsWith('C')) { - return true; + return true; } } // filter out blocked MLAT flights if (typeof this.filter.blockedMLAT !== 'undefined' && this.filter.blockedMLAT === 'filtered') { if (typeof this.icao === 'string' && this.icao.startsWith('~')) { - return true; + return true; } } @@ -483,17 +498,16 @@ PlaneObject.prototype.updateIcon = function() { // Update our data PlaneObject.prototype.updateData = function(receiver_timestamp, data) { - // Update all of our data - this.messages = data.messages; - this.rssi = data.rssi; - this.last_message_time = receiver_timestamp - data.seen; + // Update all of our data + this.messages = data.messages; + this.rssi = data.rssi; + this.last_message_time = receiver_timestamp - data.seen; // simple fields - var fields = ["alt_baro", "alt_geom", "gs", "ias", "tas", "track", "track_rate", "mag_heading", "true_heading", "mach", - "roll", "nav_heading", "nav_modes", - "nac_p", "nac_v", "nic_baro", "sil_type", "sil", + "roll", "nav_heading", "nav_modes", + "nac_p", "nac_v", "nic_baro", "sil_type", "sil", "nav_qnh", "baro_rate", "geom_rate", "rc", "squawk", "category", "version"]; @@ -580,38 +594,38 @@ PlaneObject.prototype.updateTick = function(receiver_timestamp, last_timestamp) this.seen = receiver_timestamp - this.last_message_time; this.seen_pos = (this.last_position_time === null ? null : receiver_timestamp - this.last_position_time); - // If no packet in over 58 seconds, clear the plane. - if (this.seen > 58) { + // If no packet in over 58 seconds, clear the plane. + if (this.seen > 58) { if (this.visible) { //console.log("hiding " + this.icao); this.clearMarker(); this.visible = false; - if (SelectedPlane == this.icao) + if (SelectedPlane == this.icao) selectPlaneByHex(null,false); } - } else { + } else { if (this.position !== null && (this.selected || this.seen_pos < 60)) { - this.visible = true; - if (this.updateTrack(receiver_timestamp, last_timestamp)) { + this.visible = true; + if (this.updateTrack(receiver_timestamp, last_timestamp)) { this.updateLines(); this.updateMarker(true); } else { this.updateMarker(false); // didn't move } } else { - this.clearMarker(); - this.visible = false; - } - } + this.clearMarker(); + this.visible = false; + } + } }; PlaneObject.prototype.clearMarker = function() { - if (this.marker) { + if (this.marker) { PlaneIconFeatures.remove(this.marker); PlaneIconFeatures.remove(this.markerStatic); /* FIXME google.maps.event.clearListeners(this.marker, 'click'); */ this.marker = this.markerStatic = null; - } + } }; // Update our marker on the map diff --git a/public_html/script.js b/public_html/script.js index 4c3f123..82fe525 100644 --- a/public_html/script.js +++ b/public_html/script.js @@ -52,6 +52,9 @@ var NBSP='\u00a0'; var layers; var layerGroup; +var altitude_slider = null; +var speed_slider = null; + // piaware vs flightfeeder var isFlightFeeder = false; @@ -96,19 +99,19 @@ function processReceiverUpdate(data) { if ((now - MessageCountHistory[0].time) > 30) MessageCountHistory.shift(); - for (var j=0; j < acs.length; j++) { + for (var j=0; j < acs.length; j++) { var ac = acs[j]; var hex = ac.hex; var squawk = ac.squawk; var plane = null; - // Do we already have this plane object in Planes? - // If not make it. + // Do we already have this plane object in Planes? + // If not make it. - if (Planes[hex]) { - plane = Planes[hex]; - } else { - plane = new PlaneObject(hex); + if (Planes[hex]) { + plane = Planes[hex]; + } else { + plane = new PlaneObject(hex); plane.filter = PlaneFilter; plane.tr = PlaneRowTemplate.cloneNode(true); @@ -153,11 +156,11 @@ function processReceiverUpdate(data) { Planes[hex] = plane; PlanesOrdered.push(plane); - } + } - // Call the function update - plane.updateData(now, ac); - } + // Call the function update + plane.updateData(now, ac); + } } function fetchData() { @@ -180,11 +183,11 @@ function fetchData() { var plane = PlanesOrdered[i]; plane.updateTick(now, LastReceiverTimestamp); } - - selectNewPlanes(); - refreshTableInfo(); - refreshSelected(); - refreshHighlighted(); + + selectNewPlanes(); + refreshTableInfo(); + refreshSelected(); + refreshHighlighted(); if (ReceiverClock) { var rcv = new Date(now * 1000); @@ -203,7 +206,7 @@ function fetchData() { LastReceiverTimestamp = now; $("#update_error").css('display','none'); } - }); + }); FetchPending.fail(function(jqxhr, status, error) { $("#update_error_detail").text("AJAX call failed (" + status + (error ? (": " + error) : "") + "). Maybe dump1090 is no longer running?"); @@ -297,25 +300,35 @@ function initialize() { // Initialize other controls initializeUnitsSelector(); - // Set up altitude filter button event handlers and validation options - $("#altitude_filter_form").submit(onFilterByAltitude); - $("#altitude_filter_form").validate({ - errorPlacement: function(error, element) { - return true; - }, - - rules: { - minAltitude: { - number: true, - min: -99999, - max: 99999 + altitude_slider = document.getElementById('altitude_slider'); + + noUiSlider.create(altitude_slider, { + start: [0, 65000], + connect: true, + range: { + 'min': 0, + 'max': 65000 }, - maxAltitude: { - number: true, - min: -99999, - max: 99999 + step: 25, + format: { + to: (v) => parseFloat(v).toFixed(0), + from: (v) => parseFloat(v).toFixed(0) + } + }); + + var minAltitudeInput = document.getElementById('minAltitudeText'), + maxAltitudeInput = document.getElementById('maxAltitudeText'); + + altitude_slider.noUiSlider.on('update', function (values, handle) { + if (handle) { + maxAltitudeInput.innerHTML = values[handle]; + } else { + minAltitudeInput.innerHTML = values[handle]; } - } + }); + + altitude_slider.noUiSlider.on('set', function (values, handle) { + onFilterByAltitude(); }); // check if the altitude color values are default to enable the altitude filter @@ -323,18 +336,50 @@ function initialize() { customAltitudeColors = false; } + speed_slider = document.getElementById('speed_slider'); + noUiSlider.create(speed_slider, { + start: [0, 1000], + connect: true, + range: { + 'min': 0, + 'max': 1000 + }, + step: 5, + format: { + // 'to' the formatted value. Receives a number. + to: function (value) { + return value; + }, + // 'from' the formatted value. + // Receives a string, should return a number. + from: function (value) { + return value; + } + } + }); - $("#altitude_filter_reset_button").click(onResetAltitudeFilter); + var minSpeedInput = document.getElementById('minSpeedText'), + maxSpeedInput = document.getElementById('maxSpeedText'); + + speed_slider.noUiSlider.on('update', function (values, handle) { + if (handle) { + maxSpeedInput.innerHTML = values[handle]; + } else { + minSpeedInput.innerHTML = values[handle]; + } + }); + + speed_slider.noUiSlider.on('set', function (values, handle) { + onFilterBySpeed(); + }); $("#aircraft_type_filter_form").submit(onFilterByAircraftType); $("#aircraft_type_filter_reset_button").click(onResetAircraftTypeFilter); - $("#aircraft_ident_filter_form").submit(onFilterByAircraftIdent); $("#aircraft_ident_filter_reset_button").click(onResetAircraftIdentFilter); - $('#settingsCog').on('click', function() { $('#settings_infoblock').toggle(); }); @@ -493,20 +538,19 @@ function load_history_item(i) { dataType: 'json' }) .done(function(data) { - PositionHistoryBuffer.push(data); - HistoryItemsReturned++; - $("#loader_progress").attr('value',HistoryItemsReturned); - if (HistoryItemsReturned == PositionHistorySize) { - end_load_history(); - } + PositionHistoryBuffer.push(data); + HistoryItemsReturned++; + if (HistoryItemsReturned == PositionHistorySize) { + end_load_history(); + } }) .fail(function(jqxhr, status, error) { - //Doesn't matter if it failed, we'll just be missing a data point - HistoryItemsReturned++; - if (HistoryItemsReturned == PositionHistorySize) { - end_load_history(); - } + //Doesn't matter if it failed, we'll just be missing a data point + HistoryItemsReturned++; + if (HistoryItemsReturned == PositionHistorySize) { + end_load_history(); + } }); } @@ -2008,8 +2052,7 @@ function setAltitudeLegend(units) { } } -function onFilterByAltitude(e) { - e.preventDefault(); +function onFilterByAltitude() { updatePlaneFilter(); refreshTableInfo(); @@ -2024,6 +2067,11 @@ function onFilterByAltitude(e) { } } +function onFilterBySpeed() { + updatePlaneFilter(); + refreshTableInfo(); +} + function onFilterByAircraftType(e) { e.preventDefault(); updatePlaneFilter(); @@ -2114,35 +2162,30 @@ function toggleAltitudeChart(switchToggle) { localStorage.setItem('altitudeChart', altitudeChartDisplay); } -function onResetAltitudeFilter(e) { - $("#altitude_filter_min").val(""); - $("#altitude_filter_max").val(""); - - updatePlaneFilter(); - refreshTableInfo(); -} - function updatePlaneFilter() { - var minAltitude = parseFloat($("#altitude_filter_min").val().trim()); - var maxAltitude = parseFloat($("#altitude_filter_max").val().trim()); - - if (minAltitude === NaN) { - minAltitude = -Infinity; - } - - if (maxAltitude === NaN) { - maxAltitude = Infinity; - } + // Get min/max altitude values from slider + var minAltitude = document.getElementById('minAltitudeText').innerHTML.trim(); + var maxAltitude = document.getElementById('maxAltitudeText').innerHTML.trim(); PlaneFilter.minAltitude = minAltitude; PlaneFilter.maxAltitude = maxAltitude; PlaneFilter.altitudeUnits = DisplayUnits; + // Get min/max speed values from slider + var minSpeedFilter = document.getElementById('minSpeedText').innerHTML.trim(); + var maxSpeedFilter = document.getElementById('maxSpeedText').innerHTML.trim(); + + PlaneFilter.minSpeedFilter = minSpeedFilter; + PlaneFilter.maxSpeedFilter = maxSpeedFilter; + PlaneFilter.speedUnits = DisplayUnits; + + // Get aircraft type code filter from input box var aircraftTypeCode = $("#aircraft_type_filter").val().trim().toUpperCase() if (aircraftTypeCode === "") { aircraftTypeCode = undefined } + // Get aircraft ident filter from input box var aircraftIdent = $("#aircraft_ident_filter").val().trim().toUpperCase() if (aircraftIdent === "") { aircraftIdent = undefined diff --git a/public_html/style.css b/public_html/style.css index 84d7fe1..ddd4f91 100644 --- a/public_html/style.css +++ b/public_html/style.css @@ -230,9 +230,7 @@ div#loader { z-index: 99; position: absolute; left: 0; top: 0; bottom: 0; right: } #units_container, -#altitude_filter_form, -#aircraft_type_filter_form, -#aircraft_ident_filter_form { +#altitude_filter_form { font-size: small; margin: 10px 0 10px 0; } @@ -246,6 +244,9 @@ div#loader { z-index: 99; position: absolute; left: 0; top: 0; bottom: 0; right: font-size: smaller; padding: 5px; text-align: center; + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; } .verticalRateTriangle { @@ -852,6 +853,9 @@ select.error, textarea.error, input.error { cursor: pointer; text-align: center; width: auto; + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; } .config_button:hover, .config_button_active { @@ -866,15 +870,79 @@ select.error, textarea.error, input.error { } .panel { + background-color: white; position: relative; padding: 3px 3px 10px 3px; display: none; margin-bottom: 5px; border: solid; border-color: #002F5D; + height: auto; + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; } .wrapper { display: grid; grid-template-columns: repeat(3, 1fr); - } \ No newline at end of file + } + +#altitude_slider, #speed_slider { + position: relative; + display: block; + margin-left: 10px; + margin-right: 10px; + width: auto; + margin-top: 10px; + margin-bottom: 5px; + height: 10px; +} + +.align_right { + float: right; +} + +.noUi-horizontal .noUi-handle, .noUi-vertical .noUi-handle { + background: #FEBC11; + height: 20px !important; + width: 10px !important; + right: -5px !important; /* must be (width / 2) * -1 */ + +} + +.noUi-handle:before, .noUi-handle:after { + display: none !important; +} + +.noUi-connect { + background: #00A0E2 !important; +} + +#aircraft_type_filter_form, +#aircraft_ident_filter_form { + position: relative; + padding: 5px 5px 5px 5px; +} + +.group { + padding: 5px 5px 5px 5px; + margin-top: 5px; + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + background-clip: padding-box; + border: 1px solid #e5e9f9; + background: #fff; + color: #060d46; + width: auto; +} + +.filter_input_group { + margin-top: 10px; + margin-bottom: 5px; +} + +.ui-resizable { + min-width: 400px; +} \ No newline at end of file