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 |
--------------------------------------------------------------------------------