slider.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. /*! rangeslider.js - v2.3.2 | (c) 2018 @andreruffert | MIT license | https://github.com/andreruffert/rangeslider.js */
  2. (function(factory) {
  3. 'use strict';
  4. if (typeof define === 'function' && define.amd) {
  5. // AMD. Register as an anonymous module.
  6. define(['jquery'], factory);
  7. } else if (typeof exports === 'object') {
  8. // CommonJS
  9. module.exports = factory(require('jquery'));
  10. } else {
  11. // Browser globals
  12. factory(jQuery);
  13. }
  14. }(function($) {
  15. 'use strict';
  16. // Polyfill Number.isNaN(value)
  17. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
  18. Number.isNaN = Number.isNaN || function(value) {
  19. return typeof value === 'number' && value !== value;
  20. };
  21. /**
  22. * Range feature detection
  23. * @return {Boolean}
  24. */
  25. function supportsRange() {
  26. var input = document.createElement('input');
  27. input.setAttribute('type', 'range');
  28. return input.type !== 'text';
  29. }
  30. var pluginName = 'rangeslider',
  31. pluginIdentifier = 0,
  32. hasInputRangeSupport = supportsRange(),
  33. defaults = {
  34. polyfill: true,
  35. orientation: 'horizontal',
  36. rangeClass: 'rangeslider',
  37. disabledClass: 'rangeslider--disabled',
  38. activeClass: 'rangeslider--active',
  39. horizontalClass: 'rangeslider--horizontal',
  40. verticalClass: 'rangeslider--vertical',
  41. fillClass: 'rangeslider__fill',
  42. handleClass: 'rangeslider__handle',
  43. startEvent: ['mousedown', 'touchstart', 'pointerdown'],
  44. moveEvent: ['mousemove', 'touchmove', 'pointermove'],
  45. endEvent: ['mouseup', 'touchend', 'pointerup']
  46. },
  47. constants = {
  48. orientation: {
  49. horizontal: {
  50. dimension: 'width',
  51. direction: 'left',
  52. directionStyle: 'left',
  53. coordinate: 'x'
  54. },
  55. vertical: {
  56. dimension: 'height',
  57. direction: 'top',
  58. directionStyle: 'bottom',
  59. coordinate: 'y'
  60. }
  61. }
  62. };
  63. /**
  64. * Delays a function for the given number of milliseconds, and then calls
  65. * it with the arguments supplied.
  66. *
  67. * @param {Function} fn [description]
  68. * @param {Number} wait [description]
  69. * @return {Function}
  70. */
  71. function delay(fn, wait) {
  72. var args = Array.prototype.slice.call(arguments, 2);
  73. return setTimeout(function(){ return fn.apply(null, args); }, wait);
  74. }
  75. /**
  76. * Returns a debounced function that will make sure the given
  77. * function is not triggered too much.
  78. *
  79. * @param {Function} fn Function to debounce.
  80. * @param {Number} debounceDuration OPTIONAL. The amount of time in milliseconds for which we will debounce the function. (defaults to 100ms)
  81. * @return {Function}
  82. */
  83. function debounce(fn, debounceDuration) {
  84. debounceDuration = debounceDuration || 100;
  85. return function() {
  86. if (!fn.debouncing) {
  87. var args = Array.prototype.slice.apply(arguments);
  88. fn.lastReturnVal = fn.apply(window, args);
  89. fn.debouncing = true;
  90. }
  91. clearTimeout(fn.debounceTimeout);
  92. fn.debounceTimeout = setTimeout(function(){
  93. fn.debouncing = false;
  94. }, debounceDuration);
  95. return fn.lastReturnVal;
  96. };
  97. }
  98. /**
  99. * Check if a `element` is visible in the DOM
  100. *
  101. * @param {Element} element
  102. * @return {Boolean}
  103. */
  104. function isHidden(element) {
  105. return (
  106. element && (
  107. element.offsetWidth === 0 ||
  108. element.offsetHeight === 0 ||
  109. // Also Consider native `<details>` elements.
  110. element.open === false
  111. )
  112. );
  113. }
  114. /**
  115. * Get hidden parentNodes of an `element`
  116. *
  117. * @param {Element} element
  118. * @return {[type]}
  119. */
  120. function getHiddenParentNodes(element) {
  121. var parents = [],
  122. node = element.parentNode;
  123. while (isHidden(node)) {
  124. parents.push(node);
  125. node = node.parentNode;
  126. }
  127. return parents;
  128. }
  129. /**
  130. * Returns dimensions for an element even if it is not visible in the DOM.
  131. *
  132. * @param {Element} element
  133. * @param {String} key (e.g. offsetWidth …)
  134. * @return {Number}
  135. */
  136. function getDimension(element, key) {
  137. var hiddenParentNodes = getHiddenParentNodes(element),
  138. hiddenParentNodesLength = hiddenParentNodes.length,
  139. inlineStyle = [],
  140. dimension = element[key];
  141. // Used for native `<details>` elements
  142. function toggleOpenProperty(element) {
  143. if (typeof element.open !== 'undefined') {
  144. element.open = (element.open) ? false : true;
  145. }
  146. }
  147. if (hiddenParentNodesLength) {
  148. for (var i = 0; i < hiddenParentNodesLength; i++) {
  149. // Cache style attribute to restore it later.
  150. inlineStyle[i] = hiddenParentNodes[i].style.cssText;
  151. // visually hide
  152. if (hiddenParentNodes[i].style.setProperty) {
  153. hiddenParentNodes[i].style.setProperty('display', 'block', 'important');
  154. } else {
  155. hiddenParentNodes[i].style.cssText += ';display: block !important';
  156. }
  157. hiddenParentNodes[i].style.height = '0';
  158. hiddenParentNodes[i].style.overflow = 'hidden';
  159. hiddenParentNodes[i].style.visibility = 'hidden';
  160. toggleOpenProperty(hiddenParentNodes[i]);
  161. }
  162. // Update dimension
  163. dimension = element[key];
  164. for (var j = 0; j < hiddenParentNodesLength; j++) {
  165. // Restore the style attribute
  166. hiddenParentNodes[j].style.cssText = inlineStyle[j];
  167. toggleOpenProperty(hiddenParentNodes[j]);
  168. }
  169. }
  170. return dimension;
  171. }
  172. /**
  173. * Returns the parsed float or the default if it failed.
  174. *
  175. * @param {String} str
  176. * @param {Number} defaultValue
  177. * @return {Number}
  178. */
  179. function tryParseFloat(str, defaultValue) {
  180. var value = parseFloat(str);
  181. return Number.isNaN(value) ? defaultValue : value;
  182. }
  183. /**
  184. * Capitalize the first letter of string
  185. *
  186. * @param {String} str
  187. * @return {String}
  188. */
  189. function ucfirst(str) {
  190. return str.charAt(0).toUpperCase() + str.substr(1);
  191. }
  192. /**
  193. * Plugin
  194. * @param {String} element
  195. * @param {Object} options
  196. */
  197. function Plugin(element, options) {
  198. this.$window = $(window);
  199. this.$document = $(document);
  200. this.$element = $(element);
  201. this.options = $.extend( {}, defaults, options );
  202. this.polyfill = this.options.polyfill;
  203. this.orientation = this.$element[0].getAttribute('data-orientation') || this.options.orientation;
  204. this.onInit = this.options.onInit;
  205. this.onSlide = this.options.onSlide;
  206. this.onSlideEnd = this.options.onSlideEnd;
  207. this.DIMENSION = constants.orientation[this.orientation].dimension;
  208. this.DIRECTION = constants.orientation[this.orientation].direction;
  209. this.DIRECTION_STYLE = constants.orientation[this.orientation].directionStyle;
  210. this.COORDINATE = constants.orientation[this.orientation].coordinate;
  211. // Plugin should only be used as a polyfill
  212. if (this.polyfill) {
  213. // Input range support?
  214. if (hasInputRangeSupport) { return false; }
  215. }
  216. this.identifier = 'js-' + pluginName + '-' +(pluginIdentifier++);
  217. this.startEvent = this.options.startEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
  218. this.moveEvent = this.options.moveEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
  219. this.endEvent = this.options.endEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
  220. this.toFixed = (this.step + '').replace('.', '').length - 1;
  221. this.$fill = $('<div class="' + this.options.fillClass + '" />');
  222. this.$handle = $('<div class="' + this.options.handleClass + '" />');
  223. this.$range = $('<div class="' + this.options.rangeClass + ' ' + this.options[this.orientation + 'Class'] + '" id="' + this.identifier + '" />').insertAfter(this.$element).prepend(this.$fill, this.$handle);
  224. // visually hide the input
  225. this.$element.css({
  226. 'position': 'absolute',
  227. 'width': '1px',
  228. 'height': '1px',
  229. 'overflow': 'hidden',
  230. 'opacity': '0'
  231. });
  232. // Store context
  233. this.handleDown = $.proxy(this.handleDown, this);
  234. this.handleMove = $.proxy(this.handleMove, this);
  235. this.handleEnd = $.proxy(this.handleEnd, this);
  236. this.init();
  237. // Attach Events
  238. var _this = this;
  239. this.$window.on('resize.' + this.identifier, debounce(function() {
  240. // Simulate resizeEnd event.
  241. delay(function() { _this.update(false, false); }, 300);
  242. }, 20));
  243. this.$document.on(this.startEvent, '#' + this.identifier + ':not(.' + this.options.disabledClass + ')', this.handleDown);
  244. // Listen to programmatic value changes
  245. this.$element.on('change.' + this.identifier, function(e, data) {
  246. if (data && data.origin === _this.identifier) {
  247. return;
  248. }
  249. var value = e.target.value,
  250. pos = _this.getPositionFromValue(value);
  251. _this.setPosition(pos);
  252. });
  253. }
  254. Plugin.prototype.init = function() {
  255. this.update(true, false);
  256. if (this.onInit && typeof this.onInit === 'function') {
  257. this.onInit();
  258. }
  259. };
  260. Plugin.prototype.update = function(updateAttributes, triggerSlide) {
  261. updateAttributes = updateAttributes || false;
  262. if (updateAttributes) {
  263. this.min = tryParseFloat(this.$element[0].getAttribute('min'), 0);
  264. this.max = tryParseFloat(this.$element[0].getAttribute('max'), 100);
  265. this.value = tryParseFloat(this.$element[0].value, Math.round(this.min + (this.max-this.min)/2));
  266. this.step = tryParseFloat(this.$element[0].getAttribute('step'), 1);
  267. }
  268. this.handleDimension = getDimension(this.$handle[0], 'offset' + ucfirst(this.DIMENSION));
  269. this.rangeDimension = getDimension(this.$range[0], 'offset' + ucfirst(this.DIMENSION));
  270. this.maxHandlePos = this.rangeDimension - this.handleDimension;
  271. this.grabPos = this.handleDimension / 2;
  272. this.position = this.getPositionFromValue(this.value);
  273. // Consider disabled state
  274. if (this.$element[0].disabled) {
  275. this.$range.addClass(this.options.disabledClass);
  276. } else {
  277. this.$range.removeClass(this.options.disabledClass);
  278. }
  279. this.setPosition(this.position, triggerSlide);
  280. };
  281. Plugin.prototype.handleDown = function(e) {
  282. e.preventDefault();
  283. // Only respond to mouse main button clicks (usually the left button)
  284. if (e.button && e.button !== 0) { return; }
  285. this.$document.on(this.moveEvent, this.handleMove);
  286. this.$document.on(this.endEvent, this.handleEnd);
  287. // add active class because Firefox is ignoring
  288. // the handle:active pseudo selector because of `e.preventDefault();`
  289. this.$range.addClass(this.options.activeClass);
  290. // If we click on the handle don't set the new position
  291. if ((' ' + e.target.className + ' ').replace(/[\n\t]/g, ' ').indexOf(this.options.handleClass) > -1) {
  292. return;
  293. }
  294. var pos = this.getRelativePosition(e),
  295. rangePos = this.$range[0].getBoundingClientRect()[this.DIRECTION],
  296. handlePos = this.getPositionFromNode(this.$handle[0]) - rangePos,
  297. setPos = (this.orientation === 'vertical') ? (this.maxHandlePos - (pos - this.grabPos)) : (pos - this.grabPos);
  298. this.setPosition(setPos);
  299. if (pos >= handlePos && pos < handlePos + this.handleDimension) {
  300. this.grabPos = pos - handlePos;
  301. }
  302. };
  303. Plugin.prototype.handleMove = function(e) {
  304. e.preventDefault();
  305. var pos = this.getRelativePosition(e);
  306. var setPos = (this.orientation === 'vertical') ? (this.maxHandlePos - (pos - this.grabPos)) : (pos - this.grabPos);
  307. this.setPosition(setPos);
  308. };
  309. Plugin.prototype.handleEnd = function(e) {
  310. e.preventDefault();
  311. this.$document.off(this.moveEvent, this.handleMove);
  312. this.$document.off(this.endEvent, this.handleEnd);
  313. this.$range.removeClass(this.options.activeClass);
  314. // Ok we're done fire the change event
  315. this.$element.trigger('change', { origin: this.identifier });
  316. if (this.onSlideEnd && typeof this.onSlideEnd === 'function') {
  317. this.onSlideEnd(this.position, this.value);
  318. }
  319. };
  320. Plugin.prototype.cap = function(pos, min, max) {
  321. if (pos < min) { return min; }
  322. if (pos > max) { return max; }
  323. return pos;
  324. };
  325. Plugin.prototype.setPosition = function(pos, triggerSlide) {
  326. var value, newPos;
  327. if (triggerSlide === undefined) {
  328. triggerSlide = true;
  329. }
  330. // Snapping steps
  331. value = this.getValueFromPosition(this.cap(pos, 0, this.maxHandlePos));
  332. newPos = this.getPositionFromValue(value);
  333. // Update ui
  334. this.$fill[0].style[this.DIMENSION] = (newPos + this.grabPos) + 'px';
  335. this.$handle[0].style[this.DIRECTION_STYLE] = newPos + 'px';
  336. this.setValue(value);
  337. // Update globals
  338. this.position = newPos;
  339. this.value = value;
  340. if (triggerSlide && this.onSlide && typeof this.onSlide === 'function') {
  341. this.onSlide(newPos, value);
  342. }
  343. };
  344. // Returns element position relative to the parent
  345. Plugin.prototype.getPositionFromNode = function(node) {
  346. var i = 0;
  347. while (node !== null) {
  348. i += node.offsetLeft;
  349. node = node.offsetParent;
  350. }
  351. return i;
  352. };
  353. Plugin.prototype.getRelativePosition = function(e) {
  354. // Get the offset DIRECTION relative to the viewport
  355. var ucCoordinate = ucfirst(this.COORDINATE),
  356. rangePos = this.$range[0].getBoundingClientRect()[this.DIRECTION],
  357. pageCoordinate = 0;
  358. if (typeof e.originalEvent['client' + ucCoordinate] !== 'undefined') {
  359. pageCoordinate = e.originalEvent['client' + ucCoordinate];
  360. }
  361. else if (
  362. e.originalEvent.touches &&
  363. e.originalEvent.touches[0] &&
  364. typeof e.originalEvent.touches[0]['client' + ucCoordinate] !== 'undefined'
  365. ) {
  366. pageCoordinate = e.originalEvent.touches[0]['client' + ucCoordinate];
  367. }
  368. else if(e.currentPoint && typeof e.currentPoint[this.COORDINATE] !== 'undefined') {
  369. pageCoordinate = e.currentPoint[this.COORDINATE];
  370. }
  371. return pageCoordinate - rangePos;
  372. };
  373. Plugin.prototype.getPositionFromValue = function(value) {
  374. var percentage, pos;
  375. percentage = (value - this.min)/(this.max - this.min);
  376. pos = (!Number.isNaN(percentage)) ? percentage * this.maxHandlePos : 0;
  377. return pos;
  378. };
  379. Plugin.prototype.getValueFromPosition = function(pos) {
  380. var percentage, value;
  381. percentage = ((pos) / (this.maxHandlePos || 1));
  382. value = this.step * Math.round(percentage * (this.max - this.min) / this.step) + this.min;
  383. return Number((value).toFixed(this.toFixed));
  384. };
  385. Plugin.prototype.setValue = function(value) {
  386. if (value === this.value && this.$element[0].value !== '') {
  387. return;
  388. }
  389. // Set the new value and fire the `input` event
  390. this.$element
  391. .val(value)
  392. .trigger('input', { origin: this.identifier });
  393. };
  394. Plugin.prototype.destroy = function() {
  395. this.$document.off('.' + this.identifier);
  396. this.$window.off('.' + this.identifier);
  397. this.$element
  398. .off('.' + this.identifier)
  399. .removeAttr('style')
  400. .removeData('plugin_' + pluginName);
  401. // Remove the generated markup
  402. if (this.$range && this.$range.length) {
  403. this.$range[0].parentNode.removeChild(this.$range[0]);
  404. }
  405. };
  406. // A really lightweight plugin wrapper around the constructor,
  407. // preventing against multiple instantiations
  408. $.fn[pluginName] = function(options) {
  409. var args = Array.prototype.slice.call(arguments, 1);
  410. return this.each(function() {
  411. var $this = $(this),
  412. data = $this.data('plugin_' + pluginName);
  413. // Create a new instance.
  414. if (!data) {
  415. $this.data('plugin_' + pluginName, (data = new Plugin(this, options)));
  416. }
  417. // Make it possible to access methods from public.
  418. // e.g `$element.rangeslider('method');`
  419. if (typeof options === 'string') {
  420. data[options].apply(data, args);
  421. }
  422. });
  423. };
  424. return 'rangeslider.js is available in jQuery context e.g $(selector).rangeslider(options);';
  425. }));