├── 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 | [](https://www.npmjs.com/package/rangeable)
4 | [](https://badge.fury.io/js/rangeable)
5 | [](https://github.com/Mobius1/Rangeable/blob/master/LICENSE)
6 | [](http://isitmaintained.com/project/Mobius1/Rangeable "Average time to resolve an issue")
7 | [](http://isitmaintained.com/project/Mobius1/Rangeable "Percentage of issues still open")
8 |  
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 | 
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 | }
--------------------------------------------------------------------------------