├── LICENSE ├── README.md ├── dist ├── rangeable.min.css └── rangeable.min.js ├── docs └── rangeable.png ├── package.json └── src ├── index.js └── scss └── app.scss /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present, Karl Saunders (mobius1[at]gmx[dot]com). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rangeable 2 | 3 | [![npm](https://img.shields.io/npm/dt/rangeable.svg)](https://www.npmjs.com/package/rangeable) 4 | [![npm version](https://badge.fury.io/js/rangeable.svg)](https://badge.fury.io/js/rangeable) 5 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/Mobius1/Rangeable/blob/master/LICENSE) 6 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/Mobius1/Rangeable.svg)](http://isitmaintained.com/project/Mobius1/Rangeable "Average time to resolve an issue") 7 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/Mobius1/Rangeable.svg)](http://isitmaintained.com/project/Mobius1/Rangeable "Percentage of issues still open") 8 | ![](http://img.badgesize.io/Mobius1/Rangeable/master/dist/rangeable.min.js) ![](http://img.badgesize.io/Mobius1/Rangeable/master/dist/rangeable.min.js?compression=gzip&label=gzipped) 9 | 10 | 11 | 12 | 13 | A dependency-free, responsive and touch-enabled vanilla javascript range slider to make `` elements prettier and more configurable. 14 | 15 | - [x] No dependencies 16 | - [x] 3kb gzipped 17 | - [x] Touch enabled 18 | - [x] Responsive 19 | - [x] Single or double range layouts. 20 | - [x] Horizontal and vertical orientations. 21 | - [x] Fully stylable to fit your app. 22 | 23 | ![Rangeable](/docs/rangeable.png?raw=true "Rangeable") 24 | 25 | ** Rangeable is still in active development and therefore the API is in constant flux until `v1.0.0`. Check back regularly for any changes and make sure you have the latest version installed.** 26 | 27 | ## [Live Demos](https://codepen.io/collection/AEWWkz/) 28 | 29 | --- 30 | 31 | ## Install 32 | 33 | ### npm 34 | ``` 35 | npm install rangeable --save 36 | ``` 37 | 38 | --- 39 | 40 | ### Browser 41 | 42 | Grab the files from one of the CDNs and include them in your page: 43 | 44 | ``` 45 | https://unpkg.com/rangeable@latest/dist/rangeable.min.css 46 | https://unpkg.com/rangeable@latest/dist/rangeable.min.js 47 | ``` 48 | 49 | You can replace `latest` with the required release number if needed. 50 | 51 | --- 52 | 53 | ### Default 54 | 55 | Create a new instance: 56 | 57 | ```javascript 58 | const rangeable = new Rangeable(input, { 59 | type: "single", 60 | tooltips: "always", 61 | min: 0, 62 | max: 100, 63 | step: 1, 64 | value: 50, 65 | vertical: false, 66 | controls: undefined, 67 | onInit: function() { 68 | // do something when the instance has loaded 69 | }, 70 | onStart: function() { 71 | // do something on mousedown/touchstart 72 | }, 73 | onChange: function() { 74 | // do something when the value changes 75 | }, 76 | onEnd: function() { 77 | // do something on mouseup/touchend 78 | } 79 | }); 80 | ``` 81 | 82 | You can pass either a reference to the input or a CSS3 selector string: 83 | 84 | ```javascript 85 | const myRangeInput = document.getElementById('myRangeInput'); 86 | const rangeable = new Rangeable(myRangeInput); 87 | 88 | // or 89 | 90 | const rangeable = new Rangeable('#myRangeInput'); 91 | ``` 92 | 93 | ## Options 94 | 95 | [See Options](https://github.com/Mobius1/Rangeable/wiki/Options) 96 | 97 | --- 98 | 99 | ## Public Methods 100 | 101 | [See Public Methods](https://github.com/Mobius1/Rangeable/wiki/Public-Methods) 102 | 103 | --- 104 | 105 | Copyright © 2018 Karl Saunders | BSD & MIT license -------------------------------------------------------------------------------- /dist/rangeable.min.css: -------------------------------------------------------------------------------- 1 | /* 2 | Rangeable 3 | Copyright (c) 2018 Karl Saunders 4 | Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 5 | and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 6 | 7 | Version: 0.1.6 8 | 9 | */ 10 | .rangeable-container.combined-tooltip.dragging .rangeable-progress>.rangeable-tooltip,.rangeable-container.combined-tooltip.rangeable-tooltips--visible .rangeable-progress>.rangeable-tooltip,.rangeable-container.dragging.rangeable-tooltips .rangeable-handle .rangeable-tooltip,.rangeable-container.rangeable-tooltips--visible.rangeable-tooltips .rangeable-handle .rangeable-tooltip,.rangeable-container.rangeable-vertical.combined-tooltip .rangeable-progress>.rangeable-tooltip::before{display:block}.rangeable-container{cursor:pointer;width:100%}.rangeable-container.rangeable-disabled{opacity:.6;cursor:not-allowed}.rangeable-container.rangeable-multiple.combined-tooltip .rangeable-handle .rangeable-tooltip,.rangeable-container.rangeable-vertical.combined-tooltip .rangeable-handle .rangeable-tooltip{opacity:0}.rangeable-container.focus .rangeable-handle{border:1px solid #74b9ff}.rangeable-container.rangeable-multiple .rangeable-handle:nth-child(1){left:0;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.rangeable-container.rangeable-multiple .rangeable-handle:nth-child(2){right:0}.rangeable-container.rangeable-vertical{height:100%;width:auto}.rangeable-container.rangeable-vertical .rangeable-track{width:8px;height:100%}.rangeable-container.rangeable-vertical .rangeable-progress{width:8px;height:100%;top:auto;bottom:0;-webkit-transform-origin:0 100% 0;transform-origin:0 100% 0}.rangeable-container.rangeable-vertical .rangeable-handle{right:auto;left:50%;top:0;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.rangeable-container.rangeable-vertical .rangeable-tooltip{position:absolute;top:50%;left:calc(100% + 6px + 4px + 5px);right:auto;bottom:auto;-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0)}.rangeable-container.rangeable-vertical .rangeable-tooltip::before{right:100%;left:auto;top:50%;-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);border-width:4px 4px 4px 0;border-color:transparent #3db13d transparent transparent}.rangeable-container.rangeable-vertical .rangeable-buffer{width:100%;height:0}.rangeable-container.rangeable-vertical.rangeable-multiple .rangeable-handle:nth-child(1){top:0;left:50%}.rangeable-container.rangeable-vertical.rangeable-multiple .rangeable-handle:nth-child(2){bottom:0;top:auto;-webkit-transform:translate3d(-50%,50%,0);transform:translate3d(-50%,50%,0)}.rangeable-input{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0}.rangeable-input:focus+.rangeable-track .rangeable-handle::after{position:absolute;width:22px;height:22px;bottom:-6px;right:-6px;outline:#000 dotted 1px;content:""}.rangeable-progress,.rangeable-track{height:8px;width:100%;border-radius:4px}.rangeable-track{background-color:#ccc;position:relative}.rangeable-progress{background-color:#3db13d;position:absolute;left:0;top:0;-webkit-transform-origin:0 0 0;transform-origin:0 0 0}.rangeable-progress>.rangeable-tooltip{display:none;z-index:11;top:auto;bottom:calc(100% + 7px + 9px);white-space:nowrap}.rangeable-handle{box-sizing:border-box;width:22px;height:22px;border:6px solid #3db13d;border-radius:50%;background-color:#fff;position:absolute;top:50%;right:0;-webkit-transform:translate3d(50%,-50%,0);transform:translate3d(50%,-50%,0)}.rangeable-handle:focus{outline:0}.rangeable-handle:focus::after{position:absolute;width:22px;height:22px;bottom:-6px;right:-6px;outline:#000 dotted 1px;content:""}.rangeable-handle.active{z-index:10}.rangeable-handle .rangeable-tooltip{display:none}.rangeable-tooltip{position:absolute;right:50%;bottom:calc(100% + 6px + 4px + 5px);-webkit-transform:translate3d(50%,0,0);transform:translate3d(50%,0,0);text-align:center;padding:2px 13px;background-color:#3db13d;border-radius:4px;font-weight:700;font-size:16px;color:#fff;font-family:Inconsolata,Consolas,Courier New,Lucida Console,sans-serif}.rangeable-tooltip::before{width:0;height:0;border-width:4px 4px 0;border-style:solid;border-color:#3db13d transparent transparent;position:absolute;left:50%;top:100%;-webkit-transform:translate3d(-50%,0,0);transform:translate3d(-50%,0,0);content:""}.rangeable-buffers{position:absolute;left:0;top:0;height:100%;width:100%}.rangeable-buffer{position:absolute;background-color:rgba(0,0,0,.2);border-radius:4px;height:100%} -------------------------------------------------------------------------------- /dist/rangeable.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Rangeable 4 | * Copyright (c) 2018 Karl Saunders (mobius1(at)gmx(dot)com) 5 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 6 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 7 | * 8 | * Version: 0.1.6 9 | * 10 | */ 11 | (function(i,j){"object"==typeof exports?module.exports=j("Rangeable"):"function"==typeof define&&define.amd?define([],j):i.Rangeable=j("Rangeable")})("undefined"==typeof global?this.window||this.global:global,function(){var i=function(s,t){var u=document.createElement(s);return t&&u.classList.add(t),u},j=function(s){return s&&"function"==typeof s},o=function(s,t,u){var v;return function(){if(u=u||this,!v)return s.apply(u,arguments),v=!0,setTimeout(function(){v=!1},t)}},q=function(s,t){this.plugins=["ruler"],"string"==typeof s&&(s=document.querySelector(s)),this.input=s,this.config=Object.assign({},{type:"single",tooltips:"always",updateThrottle:30,formatTooltip:function(u){return u},classes:{input:"rangeable-input",container:"rangeable-container",vertical:"rangeable-vertical",progress:"rangeable-progress",handle:"rangeable-handle",track:"rangeable-track",multiple:"rangeable-multiple",disabled:"rangeable-disabled",tooltips:"rangeable-tooltips",tooltip:"rangeable-tooltip",visible:"rangeable-tooltips--visible"}},t),this.mouseAxis={x:"clientX",y:"clientY"},this.trackSize={x:"width",y:"height"},this.trackPos={x:"left",y:"top"},this.lastPos=0,this.double="double"===this.config.type||Array.isArray(this.config.value),this.touch="ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch,this.version="0.1.6",this.init(),this.onInit()},r=q.prototype;return r.init=function(){if(!this.input.rangeable){var t,s={min:0,max:100,step:1,value:this.input.value};for(t in s)this.input[t]||(this.input[t]=s[t]),void 0!==this.config[t]&&(this.input[t]=this.config[t]);this.axis=this.config.vertical?"y":"x",this.input.rangeable=this,this.double?(this.input.values=this.config.value?this.config.value:[this.input.min,this.input.max],this.input.defaultValues=this.input.values.slice()):this.input.defaultValue||(this.input.defaultValue=this.input.value),this.render(),this.initialised=!0}},r.render=function(){var s=this,t=this.config,u=t.classes,v=i("div",u.container),w=i("div",u.track),x=i("div",u.progress),y=i("div",u.handle),z=i("div",u.tooltip);if(this.input.tabIndex=-1,this.double){y=[i("div",u.handle),i("div",u.handle)],z=[];for(var A=0;3>A;A++)z[A]=i("div",u.tooltip);y.forEach(function(C,D){C.index=D,x.appendChild(C),C.appendChild(z[D]),C.tabIndex=1,t.controls&&t.controls[D]&&t.controls[D].locked&&!0===t.controls[D].locked&&(C.locked=!0)}),t.vertical&&x.appendChild(y[0]),x.appendChild(z[2]),v.classList.add(u.multiple)}else x.appendChild(y),y.appendChild(z),y.tabIndex=1,t.controls&&t.controls.locked&&!0===t.controls.locked&&(y.locked=!0);if(v.appendChild(w),t.vertical&&v.classList.add(u.vertical),t.size&&(v.style[this.trackSize[this.axis]]=isNaN(t.size)?t.size:t.size+"px"),t.tooltips&&(v.classList.add(u.tooltips),"string"==typeof t.tooltips&&"always"===t.tooltips&&v.classList.add(u.visible)),this.nodes={container:v,track:w,progress:x,handle:y,tooltip:z},this.double){this.nodes.buffer=[];var B=i("div","rangeable-buffers");this.input.values.forEach(function(C,D){var E=i("div","rangeable-buffer");B.appendChild(E),s.nodes.buffer.push(E),w.appendChild(B),t.controls&&(s.limits=[{},{}],void 0!==t.controls[D].min&&(s.limits[D].min=t.controls[D].min),void 0!==t.controls[D].max&&(s.limits[D].max=t.controls[D].max))})}else y=i("div","rangeable-buffer"),w.appendChild(y),this.nodes.buffer=y,w.appendChild(y),t.controls&&(this.limits={},void 0!==t.controls.min&&(this.limits.min=t.controls.min),void 0!==t.controls.max&&(this.limits.max=t.controls.max));this.setLimits(t.controls),w.appendChild(x),this.input.parentNode.insertBefore(v,this.input),v.insertBefore(this.input,w),this.input.classList.add(u.input),this.bind(),this.update()},r.reset=function(){this.double?this.input.defaultValues.forEach(this.setValue,this):this.setValue(this.input.defaultValue),this.onEnd()},r.setValueFromPosition=function(s){var t=this.getLimits(),u=parseFloat(this.input.step),v=this.touch?s.touches[0][this.mouseAxis[this.axis]]:s[this.mouseAxis[this.axis]],w=v-this.rects.container[this.trackPos[this.axis]],x=this.rects.container[this.trackSize[this.axis]];return"mousedown"===s.type&&(!this.double&&this.nodes.handle.contains(s.target)||this.double&&(this.nodes.handle[0].contains(s.target)||this.nodes.handle[1].contains(s.target)))?!1:(s=(this.config.vertical?100*((x-w)/x):100*(w/x))*(t.max-t.min)/100+t.min,s=Math.ceil(s/u)*u,v>=this.lastPos&&(s-=u),parseFloat(s)!==parseFloat(this.startValue)&&void(u=!1,this.double&&(u=this.activeHandle.index),s=this.limit(s,u),this.setValue(s,u)))},r.start=function(s){return s.preventDefault(),this.startValue=this.getValue(),this.onStart(),this.nodes.container.classList.add("dragging"),this.recalculate(),this.activeHandle=this.getHandle(s),!!this.activeHandle&&void(this.activeHandle.classList.add("active"),this.setValueFromPosition(s),this.touch?(document.addEventListener("touchmove",this.events.move,!1),document.addEventListener("touchend",this.events.stop,!1),document.addEventListener("touchcancel",this.events.stop,!1)):(document.addEventListener("mousemove",this.events.move,!1),document.addEventListener("mouseup",this.events.stop,!1)))},r.move=function(s){this.setValueFromPosition(s),this.lastPos=this.touch?s.touches[0][this.mouseAxis[this.axis]]:s[this.mouseAxis[this.axis]]},r.stop=function(){this.stopValue=this.getValue(),this.nodes.container.classList.remove("dragging"),this.onEnd(),this.activeHandle.classList.remove("active"),this.activeHandle=!1,this.touch?(document.removeEventListener("touchmove",this.events.move),document.removeEventListener("touchend",this.events.stop),document.removeEventListener("touchcancel",this.events.stop)):(document.removeEventListener("mousemove",this.events.move),document.removeEventListener("mouseup",this.events.stop)),this.startValue!==this.stopValue&&this.input.dispatchEvent(new Event("change")),this.startValue=null},r.keydown=function(s){var t=this,u=function(v){switch(s.key){case"ArrowRight":case"ArrowUp":t.stepUp(v);break;case"ArrowLeft":case"ArrowDown":t.stepDown(v);}};this.double?this.nodes.handle.forEach(function(v){v===document.activeElement&&u(v.index)}):this.nodes.handle===document.activeElement&&u()},r.stepUp=function(s){var t=parseFloat(this.input.step),u=this.getValue();this.double&&void 0!==s&&(u=u[s]),t=this.limit(parseFloat(u)+t,s),this.setValue(t,s)},r.stepDown=function(s){var t=parseFloat(this.input.step),u=this.getValue();this.double&&void 0!==s&&(u=u[s]),t=this.limit(parseFloat(u)-t,s),this.setValue(t,s)},r.limit=function(s,t){var u=this.input,v=this.getLimits();return s=parseFloat(s),this.double&&void 0!==t?(!t&&s>u.values[1]?s=u.values[1]:0this.limits[1].max?s=this.limits[1].max:sthis.limits[0].max?s=this.limits[0].max:sthis.limits.max?s=this.limits.max:sv.max?s=v.max:sz.right||y.bottomz.bottom),u.container.classList.toggle("combined-tooltip",y),y&&(u.tooltip[2].textContent=x[0]===x[1]?w.call(this,x[0]):w.call(this,x[0])+" - "+w.call(this,x[1]))}}else this.input.value=s,u.tooltip.textContent=w.call(this,s);this.setPosition(s,t),v&&(this.onChange(),this.nativeEvent||this.input.dispatchEvent(new Event("input")),this.nativeEvent=!1)},r.native=function(){this.nativeEvent=!0,this.setValue()},r.getLimits=function(){return{min:parseFloat(this.input.min),max:parseFloat(this.input.max)}},r.setLimits=function(s){var t=this;if(void 0===s)return!1;this.limits||(this.limits=s);var u=function(v,w){void 0!==w.min&&(v.min=w.min),void 0!==w.max&&(v.max=w.max)};this.double?s.forEach(function(v,w){u(t.limits[w],v)}):u(this.limits,s),this.update()},r.setPosition=function(s){if(this.double){s=this.getPosition(this.input.values[0]);var t=this.getPosition(this.input.values[1]);this.nodes.progress.style[this.config.vertical?"bottom":"left"]=s+"px",s=t-s}else s=this.getPosition();this.nodes.progress.style[this.trackSize[this.axis]]=s+"px"},r.getPosition=function(s){void 0===s&&(s=this.input.value);var t=this.getLimits();return(s-t.min)/(t.max-t.min)*this.rects.container[this.trackSize[this.axis]]},r.getHandle=function(s){if(!this.double)return!this.nodes.handle.locked&&this.nodes.handle;var t=this.rects,u=Math.abs(s[this.mouseAxis[this.axis]]-t.handle[0][this.trackPos[this.axis]]);return t=Math.abs(s[this.mouseAxis[this.axis]]-t.handle[1][this.trackPos[this.axis]]),(s=s.target.closest("."+this.config.classes.handle))||(s=u>t?this.nodes.handle[1]:this.nodes.handle[0]),!s.locked&&s},r.enable=function(){this.input.disabled&&(this.nodes.container.addEventListener(this.touch?"touchstart":"mousedown",this.events.start,!1),this.double?this.nodes.handle.forEach(function(s){return s.tabIndex=1}):this.nodes.handle.tabIndex=1,this.nodes.container.classList.remove(this.config.classes.disabled),this.input.disabled=!1)},r.disable=function(){this.input.disabled||(this.nodes.container.removeEventListener(this.touch?"touchstart":"mousedown",this.events.start),this.double?this.nodes.handle.forEach(function(s){return s.removeAttribute("tabindex")}):this.nodes.handle.removeAttribute("tabindex"),this.nodes.container.classList.add(this.config.classes.disabled),this.input.disabled=!0)},r.bind=function(){var s=this;this.events={},"start move stop update reset native keydown".split(" ").forEach(function(t){s.events[t]=s[t].bind(s)}),this.events.scroll=o(this.events.update,this.config.updateThrottle),this.events.resize=o(this.events.update,this.config.updateThrottle),document.addEventListener("scroll",this.events.scroll,!1),window.addEventListener("resize",this.events.resize,!1),document.addEventListener("keydown",this.events.keydown,!1),this.nodes.container.addEventListener(this.touch?"touchstart":"mousedown",this.events.start,!1),this.input.addEventListener("input",this.events.native,!1),this.input.form&&this.input.form.addEventListener("reset",this.events.reset,!1)},r.unbind=function(){document.removeEventListener("scroll",this.events.scroll),window.removeEventListener("resize",this.events.resize),document.removeEventListener("keydown",this.events.keydown),this.nodes.container.removeEventListener(this.touch?"touchstart":"mousedown",this.events.start),this.input.removeEventListener("input",this.events.native),this.input.form&&this.input.form.removeEventListener("reset",this.events.reset),this.events=null},r.destroy=function(){this.input.rangeable&&(this.unbind(),this.input.classList.remove(this.config.classes.input),this.nodes.container.parentNode.replaceChild(this.input,this.nodes.container),delete this.input.rangeable,this.initialised=!1)},r.onInit=function(){j(this.config.onInit)&&this.config.onInit.call(this,this.getValue())},r.onStart=function(){j(this.config.onStart)&&this.config.onStart.call(this,this.getValue())},r.onChange=function(){j(this.config.onChange)&&this.config.onChange.call(this,this.getValue())},r.onEnd=function(){j(this.config.onEnd)&&this.config.onEnd.call(this,this.getValue())},q}); -------------------------------------------------------------------------------- /docs/rangeable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mobius1/Rangeable/21f1e21ba24273739897d0c46c3924c2649fd73a/docs/rangeable.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rangeable", 3 | "version": "0.1.6", 4 | "description": "A dependency-free, responsive and touch-enabled javascript range slider written in vanilla javascript.", 5 | "main": "dist/rangeable.min.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/karma start --single-run --reporters=progress" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Mobius1/Rangeable.git" 12 | }, 13 | "keywords": [ 14 | "range", 15 | "input", 16 | "slider" 17 | ], 18 | "author": "Karl Saunders", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/Mobius1/Rangeable/issues" 22 | }, 23 | "homepage": "https://github.com/Mobius1/Rangeable#readme", 24 | "devDependencies": { 25 | "jasmine": "^3.0.0", 26 | "karma": "^2.0.0", 27 | "karma-chrome-launcher": "^2.2.0", 28 | "karma-coverage": "^1.1.1", 29 | "karma-firefox-launcher": "^1.1.0", 30 | "karma-jasmine": "^1.1.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Rangeable 4 | * Copyright (c) 2018 Karl Saunders (mobius1(at)gmx(dot)com) 5 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 6 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 7 | * 8 | * Version: 0.1.6 9 | * 10 | */ 11 | (function(root, factory) { 12 | var plugin = "Rangeable"; 13 | 14 | if (typeof exports === "object") { 15 | module.exports = factory(plugin); 16 | } else if (typeof define === "function" && define.amd) { 17 | define([], factory); 18 | } else { 19 | root[plugin] = factory(plugin); 20 | } 21 | })(typeof global !== "undefined" ? global : this.window || this.global, function() { 22 | "use strict"; 23 | 24 | const version = "0.1.6"; 25 | 26 | /* HELPERS*/ 27 | 28 | /** 29 | * addEventListener shortcut 30 | * @param {HTMLElement} el 31 | * @param {String} type 32 | * @param {Function} callback 33 | * @return {Void} 34 | */ 35 | const on = (el, type, callback) => { 36 | el.addEventListener(type, callback, false); 37 | }; 38 | 39 | /** 40 | * removeEventListener shortcut 41 | * @param {HTMLElement} el 42 | * @param {String} type 43 | * @param {Function} callback 44 | * @return {Void} 45 | */ 46 | const off = (el, type, callback) => { 47 | el.removeEventListener(type, callback); 48 | }; 49 | 50 | /** 51 | * createElement helper 52 | * @param {String} type 53 | * @param {String} obj 54 | * @return {HTMLElement} 55 | */ 56 | const createElement = function(type, obj) { 57 | const el = document.createElement(type); 58 | if (obj) { 59 | el.classList.add(obj); 60 | } 61 | return el; 62 | }; 63 | 64 | /** 65 | * Check prop is defined and it is a function 66 | * @param {Mixed} func 67 | * @return {Boolean} 68 | */ 69 | const isFunction = function(func) { 70 | return func && typeof func === "function"; 71 | }; 72 | 73 | /** 74 | * Throttle for resize / scroll 75 | * @param {Function} fn 76 | * @param {Number} limit 77 | * @param {Object} context 78 | * @return {Function} 79 | */ 80 | const throttle = function(fn, limit, context) { 81 | let wait; 82 | return function() { 83 | context = context || this; 84 | if (!wait) { 85 | fn.apply(context, arguments); 86 | wait = true; 87 | return setTimeout(function() { 88 | wait = false; 89 | }, limit); 90 | } 91 | }; 92 | }; 93 | 94 | /** 95 | * Rangeable 96 | * @param {String|HTMLElement} input 97 | * @param {Object} config 98 | */ 99 | const Rangeable = function(input, config) { 100 | this.plugins = ["ruler"]; 101 | const defaultConfig = { 102 | type: "single", 103 | tooltips: "always", 104 | updateThrottle: 30, 105 | formatTooltip: (value => value), 106 | classes: { 107 | input: "rangeable-input", 108 | container: "rangeable-container", 109 | vertical: "rangeable-vertical", 110 | progress: "rangeable-progress", 111 | handle: "rangeable-handle", 112 | track: "rangeable-track", 113 | multiple: "rangeable-multiple", 114 | disabled: "rangeable-disabled", 115 | tooltips: "rangeable-tooltips", 116 | tooltip: "rangeable-tooltip", 117 | visible: "rangeable-tooltips--visible" 118 | } 119 | }; 120 | 121 | // user has passed a CSS3 selector string 122 | if (typeof input === "string") { 123 | input = document.querySelector(input); 124 | } 125 | 126 | this.input = input; 127 | this.config = Object.assign({}, defaultConfig, config); 128 | 129 | this.mouseAxis = { 130 | x: "clientX", 131 | y: "clientY" 132 | }; 133 | this.trackSize = { 134 | x: "width", 135 | y: "height" 136 | }; 137 | this.trackPos = { 138 | x: "left", 139 | y: "top" 140 | }; 141 | this.lastPos = 0; 142 | this.double = 143 | this.config.type === "double" || Array.isArray(this.config.value); 144 | this.touch = 145 | "ontouchstart" in window || 146 | (window.DocumentTouch && document instanceof DocumentTouch); 147 | this.version = version; 148 | 149 | this.init(); 150 | 151 | this.onInit(); 152 | }; 153 | 154 | /** 155 | * Initialise the instance 156 | * @return {Void} 157 | */ 158 | Rangeable.prototype.init = function() { 159 | if (!this.input.rangeable) { 160 | const props = { 161 | min: 0, 162 | max: 100, 163 | step: 1, 164 | value: this.input.value 165 | }; 166 | 167 | for (let prop in props) { 168 | // prop is missing, so add it 169 | if (!this.input[prop]) { 170 | this.input[prop] = props[prop]; 171 | } 172 | 173 | // prop set in config 174 | if (this.config[prop] !== undefined) { 175 | this.input[prop] = this.config[prop]; 176 | } 177 | } 178 | 179 | this.axis = !this.config.vertical ? "x" : "y"; 180 | 181 | this.input.rangeable = this; 182 | 183 | if (this.double) { 184 | this.input.values = this.config.value ? 185 | this.config.value : 186 | [this.input.min, this.input.max]; 187 | this.input.defaultValues = this.input.values.slice(); 188 | } else { 189 | if (!this.input.defaultValue) { 190 | this.input.defaultValue = this.input.value; 191 | } 192 | } 193 | 194 | this.render(); 195 | 196 | if ( this.input.disabled ) { 197 | this.disable(); 198 | } 199 | 200 | this.initialised = true; 201 | } 202 | }; 203 | 204 | /** 205 | * Render the instance 206 | * @return {Void} 207 | */ 208 | Rangeable.prototype.render = function() { 209 | const o = this.config; 210 | const c = o.classes; 211 | 212 | const container = createElement("div", c.container); 213 | const track = createElement("div", c.track); 214 | const progress = createElement("div", c.progress); 215 | 216 | let handle = createElement("div", c.handle); 217 | let tooltip = createElement("div", c.tooltip); 218 | 219 | this.input.tabIndex = -1; 220 | 221 | if (this.double) { 222 | handle = [createElement("div", c.handle), createElement("div", c.handle)]; 223 | tooltip = []; 224 | 225 | for (let i = 0; i < 3; i++) tooltip[i] = createElement("div", c.tooltip); 226 | 227 | handle.forEach((node, i) => { 228 | node.index = i; 229 | progress.appendChild(node); 230 | node.appendChild(tooltip[i]); 231 | node.tabIndex = 1; 232 | 233 | // locked handles? 234 | if (o.controls && o.controls[i]) { 235 | if (o.controls[i].locked && o.controls[i].locked === true) { 236 | node.locked = true; 237 | } 238 | } 239 | }); 240 | 241 | if (o.vertical) { 242 | progress.appendChild(handle[0]); 243 | } 244 | 245 | progress.appendChild(tooltip[2]); 246 | 247 | container.classList.add(c.multiple); 248 | } else { 249 | progress.appendChild(handle); 250 | handle.appendChild(tooltip); 251 | 252 | handle.tabIndex = 1; 253 | 254 | // locked handle? 255 | if (o.controls) { 256 | if (o.controls.locked && o.controls.locked === true) { 257 | handle.locked = true; 258 | } 259 | } 260 | } 261 | 262 | container.appendChild(track); 263 | 264 | if (o.vertical) { 265 | container.classList.add(c.vertical); 266 | } 267 | 268 | if (o.size) { 269 | container.style[this.trackSize[this.axis]] = !isNaN(o.size) ? 270 | `${o.size}px` : 271 | o.size; 272 | } 273 | 274 | if (o.tooltips) { 275 | container.classList.add(c.tooltips); 276 | 277 | if (typeof o.tooltips === "string" && o.tooltips === "always") { 278 | container.classList.add(c.visible); 279 | } 280 | } 281 | 282 | this.nodes = { 283 | container, 284 | track, 285 | progress, 286 | handle, 287 | tooltip 288 | }; 289 | 290 | if (this.double) { 291 | this.nodes.buffer = []; 292 | const buffers = createElement("div", "rangeable-buffers"); 293 | 294 | this.input.values.forEach((val, i) => { 295 | const buffer = createElement("div", "rangeable-buffer"); 296 | buffers.appendChild(buffer); 297 | this.nodes.buffer.push(buffer); 298 | 299 | track.appendChild(buffers); 300 | 301 | if (o.controls) { 302 | this.limits = [{}, {}]; 303 | if (o.controls[i].min !== undefined) { 304 | this.limits[i].min = o.controls[i].min; 305 | } 306 | if (o.controls[i].max !== undefined) { 307 | this.limits[i].max = o.controls[i].max; 308 | } 309 | } 310 | }); 311 | this.setLimits(o.controls); 312 | } else { 313 | const buffer = createElement("div", "rangeable-buffer"); 314 | 315 | track.appendChild(buffer); 316 | 317 | this.nodes.buffer = buffer; 318 | 319 | track.appendChild(buffer); 320 | 321 | if (o.controls) { 322 | this.limits = {}; 323 | if (o.controls.min !== undefined) { 324 | this.limits.min = o.controls.min; 325 | } 326 | if (o.controls.max !== undefined) { 327 | this.limits.max = o.controls.max; 328 | } 329 | } 330 | this.setLimits(o.controls); 331 | } 332 | 333 | track.appendChild(progress); 334 | 335 | this.input.parentNode.insertBefore(container, this.input); 336 | container.insertBefore(this.input, track); 337 | 338 | this.input.classList.add(c.input); 339 | 340 | this.bind(); 341 | 342 | this.update(); 343 | }; 344 | 345 | /** 346 | * Reset the value(s) to default 347 | * @return {Void} 348 | */ 349 | Rangeable.prototype.reset = function() { 350 | if (this.double) { 351 | this.input.defaultValues.forEach(this.setValue, this); 352 | } else { 353 | this.setValue(this.input.defaultValue); 354 | } 355 | this.onEnd(); 356 | }; 357 | 358 | /** 359 | * Set the value from the position of pointer over the track 360 | * @param {Object} e 361 | */ 362 | Rangeable.prototype.setValueFromPosition = function(e) { 363 | const limits = this.getLimits(); 364 | const step = parseFloat(this.input.step); 365 | const rect = this.rects; 366 | const axis = this.touch ? 367 | e.touches[0][this.mouseAxis[this.axis]] : 368 | e[this.mouseAxis[this.axis]]; 369 | const point = axis - this.rects.container[this.trackPos[this.axis]]; 370 | const size = rect.container[this.trackSize[this.axis]]; 371 | 372 | if (e.type === "mousedown") { 373 | if ( 374 | (!this.double && this.nodes.handle.contains(e.target)) || 375 | (this.double && 376 | (this.nodes.handle[0].contains(e.target) || 377 | this.nodes.handle[1].contains(e.target))) 378 | ) { 379 | return false; 380 | } 381 | } 382 | 383 | // get the position of the cursor over the bar as a percentage 384 | let position = this.config.vertical ? 385 | (size - point) / size * 100 : 386 | point / size * 100; 387 | 388 | // work out the value from the position 389 | let val = position * (limits.max - limits.min) / 100 + limits.min; 390 | 391 | // apply granularity (step) 392 | val = Math.ceil(val / step) * step; 393 | 394 | if (axis >= this.lastPos) { 395 | val -= step; 396 | } 397 | 398 | // prevent change event from firing if slider hasn't moved 399 | if (parseFloat(val) === parseFloat(this.startValue)) { 400 | return false; 401 | } 402 | 403 | let index = false; 404 | 405 | if (this.double) { 406 | index = this.activeHandle.index; 407 | } 408 | 409 | val = this.limit(val, index); 410 | 411 | this.setValue(val, index); 412 | }; 413 | 414 | /** 415 | * Mousedown / touchstart callback 416 | * @param {Object} e 417 | * @return {Void} 418 | */ 419 | Rangeable.prototype.start = function(e) { 420 | e.preventDefault(); 421 | 422 | this.startValue = this.getValue(); 423 | 424 | this.onStart(); 425 | // show the tip now so we can get the dimensions later 426 | this.nodes.container.classList.add("dragging"); 427 | 428 | this.recalculate(); 429 | 430 | this.activeHandle = this.getHandle(e); 431 | 432 | if (!this.activeHandle) { 433 | return false; 434 | } 435 | 436 | this.activeHandle.classList.add("active"); 437 | 438 | this.setValueFromPosition(e); 439 | 440 | if (this.touch) { 441 | on(document, "touchmove", this.events.move); 442 | on(document, "touchend", this.events.stop); 443 | on(document, "touchcancel", this.events.stop); 444 | } else { 445 | on(document, "mousemove", this.events.move); 446 | on(document, "mouseup", this.events.stop); 447 | } 448 | }; 449 | 450 | /** 451 | * Mousemove / touchmove callback 452 | * @param {Object} e 453 | * @return {Void} 454 | */ 455 | Rangeable.prototype.move = function(e) { 456 | this.setValueFromPosition(e); 457 | this.lastPos = this.touch ? 458 | e.touches[0][this.mouseAxis[this.axis]] : 459 | e[this.mouseAxis[this.axis]]; 460 | }; 461 | 462 | /** 463 | * Mouseup / touchend callback 464 | * @param {Object} e 465 | * @return {Void} 466 | */ 467 | Rangeable.prototype.stop = function(e) { 468 | this.stopValue = this.getValue(); 469 | 470 | this.nodes.container.classList.remove("dragging"); 471 | 472 | this.onEnd(); 473 | 474 | this.activeHandle.classList.remove("active"); 475 | this.activeHandle = false; 476 | 477 | if (this.touch) { 478 | off(document, "touchmove", this.events.move); 479 | off(document, "touchend", this.events.stop); 480 | off(document, "touchcancel", this.events.stop); 481 | } else { 482 | off(document, "mousemove", this.events.move); 483 | off(document, "mouseup", this.events.stop); 484 | } 485 | 486 | if (this.startValue !== this.stopValue) { 487 | this.input.dispatchEvent(new Event("change")); 488 | } 489 | 490 | this.startValue = null; 491 | }; 492 | 493 | /** 494 | * Keydown callback 495 | * @param {Object} e 496 | * @return {Void} 497 | */ 498 | Rangeable.prototype.keydown = function(e) { 499 | const step = index => { 500 | switch (e.key) { 501 | case "ArrowRight": 502 | case "ArrowUp": 503 | this.stepUp(index); 504 | break; 505 | case "ArrowLeft": 506 | case "ArrowDown": 507 | this.stepDown(index); 508 | break; 509 | } 510 | }; 511 | 512 | if (this.double) { 513 | this.nodes.handle.forEach(node => { 514 | if (node === document.activeElement) { 515 | step(node.index); 516 | } 517 | }); 518 | } else { 519 | if (this.nodes.handle === document.activeElement) { 520 | step(); 521 | } 522 | } 523 | }; 524 | 525 | /** 526 | * Increase the value by step 527 | * @param {Number} index 528 | * @return {Void} 529 | */ 530 | Rangeable.prototype.stepUp = function(index) { 531 | const step = parseFloat(this.input.step); 532 | 533 | let val = this.getValue(); 534 | 535 | if (this.double && index !== undefined) { 536 | val = val[index]; 537 | } 538 | 539 | let newval = this.limit(parseFloat(val) + step, index); 540 | 541 | this.setValue(newval, index); 542 | }; 543 | 544 | /** 545 | * Decrease the value by step 546 | * @param {Number} index 547 | * @return {Void} 548 | */ 549 | Rangeable.prototype.stepDown = function(index) { 550 | const step = parseFloat(this.input.step); 551 | 552 | let val = this.getValue(); 553 | 554 | if (this.double && index !== undefined) { 555 | val = val[index]; 556 | } 557 | 558 | let newval = this.limit(parseFloat(val) - step, index); 559 | 560 | this.setValue(newval, index); 561 | }; 562 | 563 | /** 564 | * Check the value is within the limits 565 | * @param {Number} value 566 | * @param {Number} index 567 | * @return {Number} 568 | */ 569 | Rangeable.prototype.limit = function(value, index) { 570 | const el = this.input; 571 | const limits = this.getLimits(); 572 | 573 | value = parseFloat(value); 574 | 575 | if (this.double && index !== undefined) { 576 | if (!index && value > el.values[1]) { 577 | value = el.values[1]; 578 | } else if (index > 0 && value < el.values[0]) { 579 | value = el.values[0]; 580 | } 581 | 582 | if (this.limits) { 583 | if (!index) { 584 | if (value > this.limits[0].max) { 585 | value = this.limits[0].max; 586 | } else if (value < this.limits[0].min) { 587 | value = this.limits[0].min; 588 | } 589 | } else { 590 | if (value > this.limits[1].max) { 591 | value = this.limits[1].max; 592 | } else if (value < this.limits[1].min) { 593 | value = this.limits[1].min; 594 | } 595 | } 596 | } 597 | } else { 598 | if (this.limits) { 599 | if (value > this.limits.max) { 600 | value = this.limits.max; 601 | } else if (value < this.limits.min) { 602 | value = this.limits.min; 603 | } 604 | } 605 | } 606 | 607 | if (value > limits.max) { 608 | value = limits.max; 609 | } else if (value < limits.min) { 610 | value = limits.min; 611 | } 612 | 613 | value = parseFloat(value); 614 | 615 | value = value.toFixed(this.accuracy); 616 | 617 | return value; 618 | }; 619 | 620 | /** 621 | * Recache dimensions 622 | * @return {Void} 623 | */ 624 | Rangeable.prototype.recalculate = function() { 625 | let handle = []; 626 | 627 | if (this.double) { 628 | this.nodes.handle.forEach((node, i) => { 629 | handle[i] = node.getBoundingClientRect(); 630 | }); 631 | } else { 632 | handle = this.nodes.handle.getBoundingClientRect(); 633 | } 634 | 635 | this.rects = { 636 | handle: handle, 637 | container: this.nodes.container.getBoundingClientRect() 638 | }; 639 | }; 640 | 641 | /** 642 | * Update the instance 643 | * @return {Void} 644 | */ 645 | Rangeable.prototype.update = function() { 646 | this.recalculate(); 647 | 648 | this.accuracy = 0; 649 | 650 | // detect float 651 | if (this.input.step.includes(".")) { 652 | this.accuracy = (this.input.step.split(".")[1] || []).length; 653 | } 654 | 655 | const value = this.getValue(); 656 | const limits = this.getLimits(); 657 | const size = this.rects.container[this.trackSize[this.axis]]; 658 | 659 | const setStyle = (el, offset, m) => { 660 | el.style[this.config.vertical ? "bottom" : "left"] = `${offset}px`; 661 | el.style[this.trackSize[this.axis]] = `${m / limits.max * size - offset}px`; 662 | }; 663 | 664 | if (this.double) { 665 | // set buffers 666 | if (this.limits) { 667 | this.limits.forEach((o, i) => { 668 | setStyle(this.nodes.buffer[i], o.min / limits.max * size, o.max); 669 | }); 670 | } 671 | 672 | this.input.values.forEach((val, i) => { 673 | this.setValue(this.limit(val, i), i); 674 | }); 675 | } else { 676 | // set buffer 677 | if (this.limits) { 678 | setStyle( 679 | this.nodes.buffer, 680 | this.limits.min / limits.max * size, 681 | this.limits.max 682 | ); 683 | } 684 | this.setValue(this.limit(value)); 685 | } 686 | }; 687 | 688 | /** 689 | * Get the current value(s) 690 | * @return {Number|Array} 691 | */ 692 | Rangeable.prototype.getValue = function() { 693 | return this.double ? this.input.values : this.input.value; 694 | }; 695 | 696 | /** 697 | * Set the current value(s) 698 | * @param {Number} value 699 | * @param {Number} index 700 | */ 701 | Rangeable.prototype.setValue = function(value, index) { 702 | const rects = this.rects; 703 | const nodes = this.nodes; 704 | 705 | let handle = nodes.handle; 706 | 707 | if (this.double) { 708 | if (index === undefined) { 709 | return false; 710 | } 711 | 712 | handle = this.activeHandle ? this.activeHandle : nodes.handle[index]; 713 | } 714 | 715 | if (value === undefined) { 716 | value = this.input.value; 717 | } 718 | 719 | value = this.limit(value, index); 720 | 721 | const doChange = 722 | this.initialised && (value !== this.input.value || this.nativeEvent); 723 | 724 | const format = this.config.formatTooltip; 725 | // update the value 726 | if (this.double) { 727 | const values = this.input.values; 728 | values[index] = value; 729 | 730 | if (this.config.tooltips) { 731 | // update the node so we can get the width / height 732 | nodes.tooltip[index].textContent = format.call(this, value); 733 | 734 | // check if tips are intersecting... 735 | const a = nodes.tooltip[0].getBoundingClientRect(); 736 | const b = nodes.tooltip[1].getBoundingClientRect(); 737 | const intersect = !( 738 | a.right < b.left || 739 | a.left > b.right || 740 | a.bottom < b.top || 741 | a.top > b.bottom 742 | ); 743 | 744 | // ... and set the className where appropriate 745 | nodes.container.classList.toggle("combined-tooltip", intersect); 746 | 747 | if (intersect) { 748 | // Format the combined tooltip. 749 | // Only show single value if they both match, otherwise show both seperated by a hyphen 750 | nodes.tooltip[2].textContent = 751 | values[0] === values[1] ? format.call(this, values[0]) : `${format.call(this, values[0])} - ${format.call(this, values[1])}`; 752 | } 753 | } 754 | } else { 755 | this.input.value = value; 756 | nodes.tooltip.textContent = format.call(this, value); 757 | } 758 | 759 | // set bar size 760 | this.setPosition(value, index); 761 | 762 | if (doChange) { 763 | this.onChange(); 764 | 765 | if (!this.nativeEvent) { 766 | this.input.dispatchEvent(new Event("input")); 767 | } 768 | 769 | this.nativeEvent = false; 770 | } 771 | }; 772 | 773 | /** 774 | * Native callback 775 | * @return {Void} 776 | */ 777 | Rangeable.prototype.native = function() { 778 | this.nativeEvent = true; 779 | 780 | this.setValue(); 781 | }; 782 | 783 | Rangeable.prototype.getLimits = function() { 784 | return { 785 | min: parseFloat(this.input.min), 786 | max: parseFloat(this.input.max) 787 | }; 788 | }; 789 | 790 | /** 791 | * Set the buffer 792 | * @param {[type]} value [description] 793 | */ 794 | Rangeable.prototype.setLimits = function(config) { 795 | if (config === undefined) return false; 796 | 797 | if (!this.limits) { 798 | this.limits = config; 799 | } 800 | 801 | const setLimit = (limit, o) => { 802 | if (o.min !== undefined) { 803 | limit.min = o.min; 804 | } 805 | if (o.max !== undefined) { 806 | limit.max = o.max; 807 | } 808 | }; 809 | 810 | if (this.double) { 811 | config.forEach((o, i) => { 812 | setLimit(this.limits[i], o); 813 | }); 814 | } else { 815 | setLimit(this.limits, config); 816 | } 817 | 818 | this.update(); 819 | }; 820 | 821 | /** 822 | * Set the postion / size of the progress bar. 823 | * @param {[type]} value [description] 824 | */ 825 | Rangeable.prototype.setPosition = function(value) { 826 | let width; 827 | 828 | if (this.double) { 829 | let start = this.getPosition(this.input.values[0]); 830 | let end = this.getPosition(this.input.values[1]); 831 | 832 | // set the start point of the bar 833 | this.nodes.progress.style[ 834 | this.config.vertical ? "bottom" : "left" 835 | ] = `${start}px`; 836 | 837 | width = end - start; 838 | } else { 839 | width = this.getPosition(); 840 | } 841 | 842 | // set the end point of the bar 843 | this.nodes.progress.style[this.trackSize[this.axis]] = `${width}px`; 844 | }; 845 | 846 | /** 847 | * Get the position along the track from a value. 848 | * @param {Number} value 849 | * @return {Number} 850 | */ 851 | Rangeable.prototype.getPosition = function(value) { 852 | if (value === undefined) { 853 | value = this.input.value; 854 | } 855 | const limits = this.getLimits(); 856 | 857 | return ( 858 | (value - limits.min) / 859 | (limits.max - limits.min) * 860 | this.rects.container[this.trackSize[this.axis]] 861 | ); 862 | }; 863 | 864 | /** 865 | * Get the correct handle on mousedown / touchstart 866 | * @param {Object} e 867 | * @return {Boolean|HTMLElement} 868 | */ 869 | Rangeable.prototype.getHandle = function(e) { 870 | if (!this.double) { 871 | return this.nodes.handle.locked ? false : this.nodes.handle; 872 | } 873 | 874 | const r = this.rects; 875 | const distA = Math.abs( 876 | e[this.mouseAxis[this.axis]] - r.handle[0][this.trackPos[this.axis]] 877 | ); 878 | const distB = Math.abs( 879 | e[this.mouseAxis[this.axis]] - r.handle[1][this.trackPos[this.axis]] 880 | ); 881 | let handle = e.target.closest(`.${this.config.classes.handle}`); 882 | 883 | if (!handle) { 884 | if (distA > distB) { 885 | handle = this.nodes.handle[1]; 886 | } else { 887 | handle = this.nodes.handle[0]; 888 | } 889 | } 890 | 891 | return handle.locked ? false : handle; 892 | }; 893 | 894 | /** 895 | * Enable the instance 896 | * @return {Void} 897 | */ 898 | Rangeable.prototype.enable = function() { 899 | on( 900 | this.nodes.container, 901 | this.touch ? "touchstart" : "mousedown", 902 | this.events.start 903 | ); 904 | 905 | if (this.double) { 906 | this.nodes.handle.forEach(el => (el.tabIndex = 1)); 907 | } else { 908 | this.nodes.handle.tabIndex = 1; 909 | } 910 | 911 | this.nodes.container.classList.remove(this.config.classes.disabled); 912 | 913 | this.input.disabled = false; 914 | }; 915 | 916 | /** 917 | * Disable the instance 918 | * @return {Void} 919 | */ 920 | Rangeable.prototype.disable = function() { 921 | off( 922 | this.nodes.container, 923 | this.touch ? "touchstart" : "mousedown", 924 | this.events.start 925 | ); 926 | 927 | if (this.double) { 928 | this.nodes.handle.forEach(el => el.removeAttribute("tabindex")); 929 | } else { 930 | this.nodes.handle.removeAttribute("tabindex"); 931 | } 932 | 933 | this.nodes.container.classList.add(this.config.classes.disabled); 934 | 935 | this.input.disabled = true; 936 | }; 937 | 938 | /** 939 | * Add event listeners 940 | * @return {Void} 941 | */ 942 | Rangeable.prototype.bind = function() { 943 | this.events = {}; 944 | const events = [ 945 | "start", 946 | "move", 947 | "stop", 948 | "update", 949 | "reset", 950 | "native", 951 | "keydown" 952 | ]; 953 | 954 | // bind so we can remove later 955 | events.forEach(event => { 956 | this.events[event] = this[event].bind(this); 957 | }); 958 | 959 | this.events.scroll = throttle( 960 | this.events.update, 961 | this.config.updateThrottle 962 | ); 963 | this.events.resize = throttle( 964 | this.events.update, 965 | this.config.updateThrottle 966 | ); 967 | 968 | // throttle the scroll callback for performance 969 | on(document, "scroll", this.events.scroll); 970 | 971 | // throttle the resize callback for performance 972 | on(window, "resize", this.events.resize); 973 | 974 | // key control 975 | on(document, "keydown", this.events.keydown); 976 | 977 | // touchstart/mousedown 978 | on( 979 | this.nodes.container, 980 | this.touch ? "touchstart" : "mousedown", 981 | this.events.start 982 | ); 983 | 984 | // listen for native input to allow keyboard control on focus 985 | on(this.input, "input", this.events.native); 986 | 987 | // detect form reset 988 | if (this.input.form) { 989 | on(this.input.form, "reset", this.events.reset); 990 | } 991 | }; 992 | 993 | /** 994 | * Remove event listeners 995 | * @return {Void} 996 | */ 997 | Rangeable.prototype.unbind = function() { 998 | // throttle the scroll callback for performance 999 | off(document, "scroll", this.events.scroll); 1000 | 1001 | // throttle the resize callback for performance 1002 | off(window, "resize", this.events.resize); 1003 | 1004 | off(document, "keydown", this.events.keydown); 1005 | 1006 | off( 1007 | this.nodes.container, 1008 | this.touch ? "touchstart" : "mousedown", 1009 | this.events.start 1010 | ); 1011 | 1012 | // listen for native input to allow keyboard control on focus 1013 | off(this.input, "input", this.events.native); 1014 | 1015 | // detect form reset 1016 | if (this.input.form) { 1017 | off(this.input.form, "reset", this.events.reset); 1018 | } 1019 | 1020 | this.events = null; 1021 | }; 1022 | 1023 | /** 1024 | * Destroy the instance 1025 | * @return {Void} 1026 | */ 1027 | Rangeable.prototype.destroy = function() { 1028 | if (this.input.rangeable) { 1029 | // remove all event events 1030 | this.unbind(); 1031 | 1032 | // remove the className from the input 1033 | this.input.classList.remove(this.config.classes.input); 1034 | 1035 | // kill all nodes 1036 | this.nodes.container.parentNode.replaceChild( 1037 | this.input, 1038 | this.nodes.container 1039 | ); 1040 | 1041 | // remove the reference from the input 1042 | delete this.input.rangeable; 1043 | 1044 | this.initialised = false; 1045 | } 1046 | }; 1047 | 1048 | /** 1049 | * onInit callback 1050 | * @return {Void} 1051 | */ 1052 | Rangeable.prototype.onInit = function() { 1053 | if (isFunction(this.config.onInit)) { 1054 | this.config.onInit.call(this, this.getValue()); 1055 | } 1056 | }; 1057 | 1058 | /** 1059 | * onStart callback 1060 | * @return {Void} 1061 | */ 1062 | Rangeable.prototype.onStart = function() { 1063 | if (isFunction(this.config.onStart)) { 1064 | this.config.onStart.call(this, this.getValue()); 1065 | } 1066 | }; 1067 | 1068 | /** 1069 | * onChange callback 1070 | * @return {Void} 1071 | */ 1072 | Rangeable.prototype.onChange = function() { 1073 | if (isFunction(this.config.onChange)) { 1074 | this.config.onChange.call(this, this.getValue()); 1075 | } 1076 | }; 1077 | 1078 | /** 1079 | * onEnd callback 1080 | * @return {Void} 1081 | */ 1082 | Rangeable.prototype.onEnd = function() { 1083 | if (isFunction(this.config.onEnd)) { 1084 | this.config.onEnd.call(this, this.getValue()); 1085 | } 1086 | }; 1087 | 1088 | return Rangeable; 1089 | }); -------------------------------------------------------------------------------- /src/scss/app.scss: -------------------------------------------------------------------------------- 1 | $color-primary: #3db13d; 2 | $color-secondary: #ccc; 3 | 4 | $tip-arrow: 4px; 5 | $tip-offset: 5px; 6 | $tip-radius: 4px; 7 | $tip-padding: 2px 13px; 8 | $tip-font: 16px; 9 | 10 | $bar-size: 8px; 11 | 12 | $handle-border: 6px; 13 | $handle-size: 22px; 14 | 15 | .rangeable-container { 16 | cursor: pointer; 17 | width: 100%; 18 | 19 | &.rangeable-disabled { 20 | opacity: 0.6; 21 | cursor: not-allowed; 22 | } 23 | 24 | &.focus { 25 | .rangeable-handle { 26 | border: 1px solid #74b9ff; 27 | } 28 | } 29 | 30 | &.rangeable-multiple { 31 | 32 | &.combined-tooltip { 33 | .rangeable-handle { 34 | .rangeable-tooltip { 35 | opacity: 0; 36 | } 37 | } 38 | } 39 | 40 | .rangeable-handle { 41 | &:nth-child(1) { 42 | left: 0; 43 | transform: translate3d(-50%, -50%, 0); 44 | } 45 | 46 | &:nth-child(2) { 47 | right: 0; 48 | } 49 | } 50 | } 51 | 52 | &.rangeable-vertical { 53 | height: 100%; 54 | width: auto; 55 | 56 | .rangeable-track { 57 | width: $bar-size; 58 | height: 100%; 59 | } 60 | 61 | /* progress bar */ 62 | .rangeable-progress { 63 | width: $bar-size; 64 | height: 100%; 65 | top: auto; 66 | bottom: 0; 67 | transform-origin: 0 100% 0; 68 | } 69 | 70 | /* handle */ 71 | .rangeable-handle { 72 | right: auto; 73 | left: 50%; 74 | top: 0; 75 | transform: translate3d(-50%,-50%,0); 76 | } 77 | 78 | .rangeable-tooltip { 79 | position: absolute; 80 | 81 | top: 50%; 82 | left: calc(100% + #{$handle-border} + #{$tip-arrow} + #{$tip-offset}); 83 | right: auto; 84 | bottom: auto; 85 | transform: translate3d(0,-50%,0); 86 | 87 | &::before { 88 | right: 100%; 89 | left: auto; 90 | top: 50%; 91 | transform: translate3d(0,-50%,0); 92 | 93 | border-width: $tip-arrow $tip-arrow $tip-arrow 0; 94 | border-color: transparent $color-primary transparent transparent; 95 | } 96 | } 97 | 98 | .rangeable-buffer { 99 | width: 100%; 100 | height: 0; 101 | } 102 | 103 | &.combined-tooltip { 104 | 105 | .rangeable-progress > .rangeable-tooltip::before { 106 | display:block; 107 | } 108 | 109 | .rangeable-handle .rangeable-tooltip { 110 | opacity: 0; 111 | } 112 | } 113 | 114 | &.rangeable-multiple { 115 | .rangeable-handle { 116 | &:nth-child(1) { 117 | top: 0; 118 | left: 50%; 119 | } 120 | 121 | &:nth-child(2) { 122 | bottom: 0; 123 | top: auto; 124 | transform: translate3d(-50%, 50%, 0); 125 | } 126 | } 127 | } 128 | } 129 | 130 | &.combined-tooltip { 131 | &.dragging, &.rangeable-tooltips--visible { 132 | .rangeable-progress > .rangeable-tooltip { 133 | display:block; 134 | } 135 | } 136 | } 137 | 138 | &.dragging, &.rangeable-tooltips--visible { 139 | &.rangeable-tooltips { 140 | .rangeable-handle { 141 | .rangeable-tooltip { 142 | display: block; 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | .rangeable-input { 150 | position: absolute; 151 | overflow: hidden; 152 | clip: rect(0px, 0px, 0px, 0px); 153 | width: 1px; 154 | height: 1px; 155 | margin: -1px; 156 | padding: 0; 157 | border: 0 none; 158 | 159 | &:focus { 160 | & + .rangeable-track { 161 | .rangeable-handle { 162 | &::after { 163 | position: absolute; 164 | width: $handle-size; 165 | height: $handle-size; 166 | bottom: -$handle-border; 167 | right: -$handle-border; 168 | outline: 1px dotted #000; 169 | content: ""; 170 | } 171 | } 172 | } 173 | } 174 | } 175 | 176 | /* track */ 177 | .rangeable-track { 178 | width: 100%; 179 | height: $bar-size; 180 | background-color: $color-secondary; 181 | position: relative; 182 | border-radius: $bar-size / 2; 183 | } 184 | 185 | /* progress bar */ 186 | .rangeable-progress { 187 | height: $bar-size; 188 | width: 100%; 189 | background-color: $color-primary; 190 | position: absolute; 191 | left: 0; 192 | top: 0; 193 | border-radius: $bar-size / 2; 194 | transform-origin: 0 0 0; 195 | 196 | & > .rangeable-tooltip { 197 | display: none; 198 | z-index: 11; 199 | top: auto; 200 | bottom: calc(100% + #{($handle-size - $bar-size) / 2} + #{$tip-offset + $tip-arrow}); 201 | white-space: nowrap; 202 | } 203 | } 204 | 205 | /* handle */ 206 | .rangeable-handle { 207 | box-sizing: border-box; 208 | width: $handle-size; 209 | height: $handle-size; 210 | border: $handle-border solid $color-primary; 211 | border-radius: 50%; 212 | background-color: #fff; 213 | position: absolute; 214 | top: 50%; 215 | right: 0; 216 | transform: translate3d(50%, -50%, 0); 217 | 218 | // opacity: 0.4; 219 | 220 | &:focus{ 221 | outline: none; 222 | &::after { 223 | position: absolute; 224 | width: $handle-size; 225 | height: $handle-size; 226 | bottom: -$handle-border; 227 | right: -$handle-border; 228 | outline: 1px dotted #000; 229 | content: ""; 230 | } 231 | } 232 | 233 | &.active { 234 | z-index: 10; 235 | } 236 | 237 | .rangeable-tooltip { 238 | display: none; 239 | } 240 | } 241 | 242 | /* tooltip */ 243 | .rangeable-tooltip { 244 | position: absolute; 245 | right: 50%; 246 | bottom: calc(100% + #{$handle-border} + #{$tip-arrow} + #{$tip-offset}); 247 | transform: translate3d(50%,0,0); 248 | 249 | text-align: center; 250 | 251 | padding: $tip-padding; 252 | background-color: $color-primary; 253 | border-radius: $tip-radius; 254 | font-weight: bold; 255 | font-size: $tip-font; 256 | color: #fff; 257 | font-family: "Inconsolata", Consolas, Courier New, Lucida Console, sans-serif; 258 | 259 | /* tooltip arrow */ 260 | &::before { 261 | width: 0; 262 | height: 0; 263 | border-width: $tip-arrow $tip-arrow 0 $tip-arrow; 264 | border-style: solid; 265 | border-color: $color-primary transparent transparent transparent; 266 | position: absolute; 267 | left: 50%; 268 | top: 100%; 269 | transform: translate3d(-50%,0,0); 270 | content: ""; 271 | } 272 | } 273 | 274 | .rangeable-buffers { 275 | position: absolute; 276 | left: 0; 277 | top: 0; 278 | height: 100%; 279 | width: 100%; 280 | } 281 | 282 | .rangeable-buffer { 283 | position: absolute; 284 | background-color: rgba(0,0,0,0.2); 285 | border-radius: 4px; 286 | height: 100%; 287 | } --------------------------------------------------------------------------------