├── README.md ├── html5slider.js └── index.html /README.md: -------------------------------------------------------------------------------- 1 | ## html5slider 2 | 3 | #### a JavaScript implementation of HTML5 `` for Firefox 4 | 5 | html5slider is a polyfill that: 6 | * transforms all `` elements from plain textboxes to draggable sliders on-the-fly automatically, 7 | * supports `min`, `max`, `step`, `oninput`, and `onchange`, 8 | * and is the only polyfill that provides **native styling** for the slider widgets. 9 | 10 | **Firefox 23 added built-in support for this widget, so this polyfill only applies to Firefox 16 - 22.** 11 | 12 | Try out a [live demo](http://fryn.github.io/html5slider/)! 13 | 14 | To use html5slider, simply reference html5slider.js in your page as an external script. For example: 15 | `` 16 | 17 | The source code is available under the MIT license. 18 | 19 | For more information about HTML5 and ``, check out the 20 | fantastic online guide "Dive Into HTML5" by Mark Pilgrim: 21 | [http://diveintohtml5.info/forms.html#type-range](http://diveintohtml5.info/forms.html#type-range) 22 | 23 | For more HTML5 polyfills and shims, check out this collection: 24 | [http://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills](http://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills) 25 | -------------------------------------------------------------------------------- /html5slider.js: -------------------------------------------------------------------------------- 1 | /* 2 | html5slider - a JS implementation of for Firefox 16 and up 3 | https://github.com/fryn/html5slider 4 | 5 | Copyright (c) 2010-2013 Frank Yan, 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | (function() { 27 | 28 | // test for native support 29 | var test = document.createElement('input'); 30 | try { 31 | test.type = 'range'; 32 | if (test.type == 'range') 33 | return; 34 | } catch (e) { 35 | return; 36 | } 37 | 38 | // test for required property support 39 | test.style.background = 'linear-gradient(red, red)'; 40 | if (!test.style.backgroundImage || !('MozAppearance' in test.style)) 41 | return; 42 | 43 | var scale; 44 | var isMac = navigator.platform == 'MacIntel'; 45 | var thumb = { 46 | radius: isMac ? 9 : 6, 47 | width: isMac ? 22 : 12, 48 | height: isMac ? 16 : 20 49 | }; 50 | var track = 'linear-gradient(transparent ' + (isMac ? 51 | '6px, #999 6px, #999 7px, #ccc 8px, #bbb 9px, #bbb 10px, transparent 10px' : 52 | '9px, #999 9px, #bbb 10px, #fff 11px, transparent 11px') + 53 | ', transparent)'; 54 | var styles = { 55 | 'min-width': thumb.width + 'px', 56 | 'min-height': thumb.height + 'px', 57 | 'max-height': thumb.height + 'px', 58 | padding: '0 0 ' + (isMac ? '2px' : '1px'), 59 | border: 0, 60 | 'border-radius': 0, 61 | cursor: 'default', 62 | 'text-indent': '-999999px' // -moz-user-select: none; breaks mouse capture 63 | }; 64 | var options = { 65 | attributes: true, 66 | attributeFilter: ['min', 'max', 'step', 'value'] 67 | }; 68 | var onInput = document.createEvent('HTMLEvents'); 69 | onInput.initEvent('input', true, false); 70 | var onChange = document.createEvent('HTMLEvents'); 71 | onChange.initEvent('change', true, false); 72 | 73 | if (document.readyState == 'loading') 74 | document.addEventListener('DOMContentLoaded', initialize, true); 75 | else 76 | initialize(); 77 | addEventListener('pageshow', recreate, true); 78 | 79 | function initialize() { 80 | // create initial sliders 81 | recreate(); 82 | // create sliders on-the-fly 83 | new MutationObserver(function(mutations) { 84 | mutations.forEach(function(mutation) { 85 | if (mutation.addedNodes) 86 | Array.forEach(mutation.addedNodes, function(node) { 87 | if (!(node instanceof Element)) 88 | ; 89 | else if (node.childElementCount) 90 | Array.forEach(node.querySelectorAll('input[type=range]'), check); 91 | else if (node.mozMatchesSelector('input[type=range]')) 92 | check(node); 93 | }); 94 | }); 95 | }).observe(document, { childList: true, subtree: true }); 96 | } 97 | 98 | function recreate() { 99 | Array.forEach(document.querySelectorAll('input[type=range]'), check); 100 | } 101 | 102 | function check(input) { 103 | if (input.type != 'range') 104 | transform(input); 105 | } 106 | 107 | function transform(slider) { 108 | 109 | var isValueSet, areAttrsSet, isUI, isClick, prevValue, rawValue, prevX; 110 | var min, max, step, range, value = slider.value; 111 | 112 | // lazily create shared slider affordance 113 | if (!scale) { 114 | scale = document.body.appendChild(document.createElement('hr')); 115 | style(scale, { 116 | '-moz-appearance': isMac ? 'scale-horizontal' : 'scalethumb-horizontal', 117 | display: 'block', 118 | visibility: 'visible', 119 | opacity: 1, 120 | position: 'fixed', 121 | top: '-999999px' 122 | }); 123 | document.mozSetImageElement('__sliderthumb__', scale); 124 | } 125 | 126 | // reimplement value and type properties 127 | var getValue = function() { return '' + value; }; 128 | var setValue = function setValue(val) { 129 | value = '' + val; 130 | isValueSet = true; 131 | draw(); 132 | delete slider.value; 133 | slider.value = value; 134 | slider.__defineGetter__('value', getValue); 135 | slider.__defineSetter__('value', setValue); 136 | }; 137 | slider.__defineGetter__('value', getValue); 138 | slider.__defineSetter__('value', setValue); 139 | Object.defineProperty(slider, 'type', { 140 | get: function() { return 'range'; } 141 | }); 142 | 143 | // sync properties with attributes 144 | ['min', 'max', 'step'].forEach(function(name) { 145 | if (slider.hasAttribute(name)) 146 | areAttrsSet = true; 147 | Object.defineProperty(slider, name, { 148 | get: function() { 149 | return this.hasAttribute(name) ? this.getAttribute(name) : ''; 150 | }, 151 | set: function(val) { 152 | val === null ? 153 | this.removeAttribute(name) : 154 | this.setAttribute(name, val); 155 | } 156 | }); 157 | }); 158 | 159 | // initialize slider 160 | slider.readOnly = true; 161 | style(slider, styles); 162 | update(); 163 | 164 | new MutationObserver(function(mutations) { 165 | mutations.forEach(function(mutation) { 166 | if (mutation.attributeName != 'value') { 167 | update(); 168 | areAttrsSet = true; 169 | } 170 | // note that value attribute only sets initial value 171 | else if (!isValueSet) { 172 | value = slider.getAttribute('value'); 173 | draw(); 174 | } 175 | }); 176 | }).observe(slider, options); 177 | 178 | slider.addEventListener('mousedown', onDragStart, true); 179 | slider.addEventListener('keydown', onKeyDown, true); 180 | slider.addEventListener('focus', onFocus, true); 181 | slider.addEventListener('blur', onBlur, true); 182 | 183 | function onDragStart(e) { 184 | isClick = true; 185 | setTimeout(function() { isClick = false; }, 0); 186 | if (e.button || !range) 187 | return; 188 | var width = parseFloat(getComputedStyle(this).width); 189 | var multiplier = (width - thumb.width) / range; 190 | if (!multiplier) 191 | return; 192 | // distance between click and center of thumb 193 | var dev = e.clientX - this.getBoundingClientRect().left - thumb.width / 2 - 194 | (value - min) * multiplier; 195 | // if click was not on thumb, move thumb to click location 196 | if (Math.abs(dev) > thumb.radius) { 197 | isUI = true; 198 | this.value -= -dev / multiplier; 199 | } 200 | rawValue = value; 201 | prevX = e.clientX; 202 | this.addEventListener('mousemove', onDrag, true); 203 | this.addEventListener('mouseup', onDragEnd, true); 204 | } 205 | 206 | function onDrag(e) { 207 | var width = parseFloat(getComputedStyle(this).width); 208 | var multiplier = (width - thumb.width) / range; 209 | if (!multiplier) 210 | return; 211 | rawValue += (e.clientX - prevX) / multiplier; 212 | prevX = e.clientX; 213 | isUI = true; 214 | this.value = rawValue; 215 | } 216 | 217 | function onDragEnd() { 218 | this.removeEventListener('mousemove', onDrag, true); 219 | this.removeEventListener('mouseup', onDragEnd, true); 220 | slider.dispatchEvent(onInput); 221 | slider.dispatchEvent(onChange); 222 | } 223 | 224 | function onKeyDown(e) { 225 | if (e.keyCode > 36 && e.keyCode < 41) { // 37-40: left, up, right, down 226 | onFocus.call(this); 227 | isUI = true; 228 | this.value = value + (e.keyCode == 38 || e.keyCode == 39 ? step : -step); 229 | } 230 | } 231 | 232 | function onFocus() { 233 | if (!isClick) 234 | this.style.boxShadow = !isMac ? '0 0 0 2px #fb0' : 235 | 'inset 0 0 20px rgba(0,127,255,.1), 0 0 1px rgba(0,127,255,.4)'; 236 | } 237 | 238 | function onBlur() { 239 | this.style.boxShadow = ''; 240 | } 241 | 242 | // determines whether value is valid number in attribute form 243 | function isAttrNum(value) { 244 | return !isNaN(value) && +value == parseFloat(value); 245 | } 246 | 247 | // validates min, max, and step attributes and redraws 248 | function update() { 249 | min = isAttrNum(slider.min) ? +slider.min : 0; 250 | max = isAttrNum(slider.max) ? +slider.max : 100; 251 | if (max < min) 252 | max = min > 100 ? min : 100; 253 | step = isAttrNum(slider.step) && slider.step > 0 ? +slider.step : 1; 254 | range = max - min; 255 | draw(true); 256 | } 257 | 258 | // recalculates value property 259 | function calc() { 260 | if (!isValueSet && !areAttrsSet) 261 | value = slider.getAttribute('value'); 262 | if (!isAttrNum(value)) 263 | value = (min + max) / 2;; 264 | // snap to step intervals (WebKit sometimes does not - bug?) 265 | value = Math.round((value - min) / step) * step + min; 266 | if (value < min) 267 | value = min; 268 | else if (value > max) 269 | value = min + ~~(range / step) * step; 270 | } 271 | 272 | // renders slider using CSS background ;) 273 | function draw(attrsModified) { 274 | calc(); 275 | var wasUI = isUI; 276 | isUI = false; 277 | if (wasUI && value != prevValue) 278 | slider.dispatchEvent(onInput); 279 | if (!attrsModified && value == prevValue) 280 | return; 281 | prevValue = value; 282 | var position = range ? (value - min) / range * 100 : 0; 283 | var bg = '-moz-element(#__sliderthumb__) ' + position + '% no-repeat, '; 284 | style(slider, { background: bg + track }); 285 | } 286 | 287 | } 288 | 289 | function style(element, styles) { 290 | for (var prop in styles) 291 | element.style.setProperty(prop, styles[prop], 'important'); 292 | } 293 | 294 | })(); 295 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | html5slider: <input type=range> for Firefox 6 | 42 | 43 | 56 | 57 | 58 |

html5slider

59 |

<input type=range> polyfill for Firefox

60 |
61 | 62 |
 
63 |

64 | 65 |
 
66 |
 
67 |

68 | github.com/fryn/html5slider 69 | — 70 | frankyan.com 71 | 72 | 73 | 74 | --------------------------------------------------------------------------------