UNPKG

8.76 kBJavaScriptView Raw
1const isNumeric = require('is-numeric')
2const css = require('dom-css')
3const isMobile = require('is-mobile')()
4const format = require('param-case')
5const clamp = require('mumath/clamp')
6const EventEmitter = require('events').EventEmitter
7const inherits = require('inherits');
8const precision = require('mumath/precision');
9
10module.exports = Range
11
12inherits(Range, EventEmitter);
13
14function Range (opts) {
15 if (!(this instanceof Range)) return new Range(opts);
16
17 this.update(opts);
18}
19
20Range.prototype.update = function (opts) {
21 var self = this
22 var scaleValue, scaleValueInverse, logmin, logmax, logsign, input, handle, panel;
23
24 if (!!opts.step && !!opts.steps) {
25 throw new Error('Cannot specify both step and steps. Got step = ' + opts.step + ', steps = ', opts.steps)
26 }
27
28 opts.container.innerHTML = '';
29
30 if (opts.step) {
31 var prec = precision(opts.step) || 1;
32 }
33 else {
34 var prec = precision( (opts.max - opts.min) / opts.steps ) || 1;
35 }
36
37 // Create scale functions for converting to/from the desired scale:
38 if (opts.scale === 'log' || opts.log) {
39 scaleValue = function (x) {
40 return logsign * Math.exp(Math.log(logmin) + (Math.log(logmax) - Math.log(logmin)) * x / 100)
41 }
42 scaleValueInverse = function (y) {
43 return (Math.log(y * logsign) - Math.log(logmin)) * 100 / (Math.log(logmax) - Math.log(logmin))
44 }
45 } else {
46 scaleValue = scaleValueInverse = function (x) { return x }
47 }
48
49 if (!Array.isArray(opts.value)) {
50 opts.value = []
51 }
52 if (opts.scale === 'log' || opts.log) {
53 // Get options or set defaults:
54 opts.max = (isNumeric(opts.max)) ? opts.max : 100
55 opts.min = (isNumeric(opts.min)) ? opts.min : 0.1
56
57 // Check if all signs are valid:
58 if (opts.min * opts.max <= 0) {
59 throw new Error('Log range min/max must have the same sign and not equal zero. Got min = ' + opts.min + ', max = ' + opts.max)
60 } else {
61 // Pull these into separate variables so that opts can define the *slider* mapping
62 logmin = opts.min
63 logmax = opts.max
64 logsign = opts.min > 0 ? 1 : -1
65
66 // Got the sign so force these positive:
67 logmin = Math.abs(logmin)
68 logmax = Math.abs(logmax)
69
70 // These are now simply 0-100 to which we map the log range:
71 opts.min = 0
72 opts.max = 100
73
74 // Step is invalid for a log range:
75 if (isNumeric(opts.step)) {
76 throw new Error('Log may only use steps (integer number of steps), not a step value. Got step =' + opts.step)
77 }
78 // Default step is simply 1 in linear slider space:
79 opts.step = 1
80 }
81
82 opts.value = [
83 scaleValueInverse(isNumeric(opts.value[0]) ? opts.value[0] : scaleValue(opts.min + (opts.max - opts.min) * 0.25)),
84 scaleValueInverse(isNumeric(opts.value[1]) ? opts.value[1] : scaleValue(opts.min + (opts.max - opts.min) * 0.75))
85 ]
86
87 if (scaleValue(opts.value[0]) * scaleValue(opts.max) <= 0 || scaleValue(opts.value[1]) * scaleValue(opts.max) <= 0) {
88 throw new Error('Log range initial value must have the same sign as min/max and must not equal zero. Got initial value = [' + scaleValue(opts.value[0]) + ', ' + scaleValue(opts.value[1]) + ']')
89 }
90 } else {
91 // If linear, this is much simpler:
92 opts.max = (isNumeric(opts.max)) ? opts.max : 100
93 opts.min = (isNumeric(opts.min)) ? opts.min : 0
94 opts.step = (isNumeric(opts.step)) ? opts.step : (opts.max - opts.min) / 100
95
96 opts.value = [
97 isNumeric(opts.value[0]) ? opts.value[0] : (opts.min + opts.max) * 0.25,
98 isNumeric(opts.value[1]) ? opts.value[1] : (opts.min + opts.max) * 0.75
99 ]
100 }
101
102 // If we got a number of steps, use that instead:
103 if (isNumeric(opts.steps)) {
104 opts.step = isNumeric(opts.steps) ? (opts.max - opts.min) / opts.steps : opts.step
105 }
106
107 // Quantize the initial value to the requested step:
108 opts.value[0] = opts.min + opts.step * Math.round((opts.value[0] - opts.min) / opts.step)
109 opts.value[1] = opts.min + opts.step * Math.round((opts.value[1] - opts.min) / opts.step)
110
111
112 //create DOM
113 var lValue = require('./value')({
114 container: opts.container,
115 value: scaleValue(opts.value[0]).toFixed(prec),
116 type: 'text',
117 left: true,
118 disabled: opts.disabled,
119 id: opts.id,
120 className: 'settings-panel-interval-value settings-panel-interval-value--left',
121 input: v => {
122 //TODO
123 }
124 })
125
126 panel = opts.container.parentNode;
127
128 input = opts.container.appendChild(document.createElement('span'))
129 input.id = 'settings-panel-interval'
130 input.className = 'settings-panel-interval'
131
132 handle = document.createElement('span')
133 handle.className = 'settings-panel-interval-handle'
134 handle.value = 50;
135 handle.min = 0;
136 handle.max = 50;
137 input.appendChild(handle)
138
139 var value = opts.value
140
141 // Display the values:
142 var rValue = require('./value')({
143 container: opts.container,
144 disabled: opts.disabled,
145 value: scaleValue(opts.value[1]).toFixed(prec),
146 type: 'text',
147 className: 'settings-panel-interval-value settings-panel-interval-value--right',
148 input: v => {
149 //TODO
150 }
151 })
152
153 function setHandleCSS () {
154 let left = ((value[0] - opts.min) / (opts.max - opts.min) * 100);
155 let right = (100 - (value[1] - opts.min) / (opts.max - opts.min) * 100);
156 css(handle, {
157 left: left + '%',
158 width: (100 - left - right) + '%'
159 });
160 opts.container.style.setProperty('--low', left + '%');
161 opts.container.style.setProperty('--high', 100 - right + '%');
162 lValue.style.setProperty('--value', left + '%');
163 rValue.style.setProperty('--value', 100 - right + '%');
164 }
165
166 // Initialize CSS:
167 setHandleCSS()
168 // An index to track what's being dragged:
169 var activeIndex = -1
170
171 function mouseX (ev) {
172 // Get mouse/touch position in page coords relative to the container:
173 return (ev.touches && ev.touches[0] || ev).pageX - input.getBoundingClientRect().left
174 }
175
176 function setActiveValue (fraction) {
177 if (activeIndex === -1) {
178 return
179 }
180
181 // Get the position in the range [0, 1]:
182 var lofrac = (value[0] - opts.min) / (opts.max - opts.min)
183 var hifrac = (value[1] - opts.min) / (opts.max - opts.min)
184
185 // Clip against the other bound:
186 if (activeIndex === 0) {
187 fraction = Math.min(hifrac, fraction)
188 } else {
189 fraction = Math.max(lofrac, fraction)
190 }
191
192 // Compute and quantize the new value:
193 var newValue = opts.min + Math.round((opts.max - opts.min) * fraction / opts.step) * opts.step
194
195 // Update value, in linearized coords:
196 value[activeIndex] = newValue
197
198 // Update and send the event:
199 setHandleCSS()
200 input.oninput()
201 }
202
203 var mousemoveListener = function (ev) {
204 if (ev.target === input || ev.target === handle) ev.preventDefault()
205
206 var fraction = clamp(mouseX(ev) / input.offsetWidth, 0, 1)
207
208 setActiveValue(fraction)
209 }
210
211 var mouseupListener = function (ev) {
212 panel.classList.remove('settings-panel-interval-dragging')
213
214 document.removeEventListener(isMobile ? 'touchmove' : 'mousemove', mousemoveListener)
215 document.removeEventListener(isMobile ? 'touchend' : 'mouseup', mouseupListener)
216
217 activeIndex = -1
218 }
219
220 input.addEventListener(isMobile ? 'touchstart' : 'mousedown', function (ev) {
221 // Tweak control to make dragging experience a little nicer:
222 panel.classList.add('settings-panel-interval-dragging')
223
224 // Get mouse position fraction:
225 var fraction = clamp(mouseX(ev) / input.offsetWidth, 0, 1)
226
227 // Get the current fraction of position --> [0, 1]:
228 var lofrac = (value[0] - opts.min) / (opts.max - opts.min)
229 var hifrac = (value[1] - opts.min) / (opts.max - opts.min)
230
231 // This is just for making decisions, so perturb it ever
232 // so slightly just in case the bounds are numerically equal:
233 lofrac -= Math.abs(opts.max - opts.min) * 1e-15
234 hifrac += Math.abs(opts.max - opts.min) * 1e-15
235
236 // Figure out which is closer:
237 var lodiff = Math.abs(lofrac - fraction)
238 var hidiff = Math.abs(hifrac - fraction)
239
240 activeIndex = lodiff < hidiff ? 0 : 1
241
242 // Attach this to *document* so that we can still drag if the mouse
243 // passes outside the container:
244 document.addEventListener(isMobile ? 'touchmove' : 'mousemove', mousemoveListener)
245 document.addEventListener(isMobile ? 'touchend' : 'mouseup', mouseupListener)
246 })
247
248 setTimeout(() => {
249 var scaledLValue = scaleValue(value[0])
250 var scaledRValue = scaleValue(value[1])
251 lValue.value = scaledLValue.toFixed(prec)
252 rValue.value = scaledRValue.toFixed(prec)
253 this.emit('init', [scaledLValue, scaledRValue])
254 })
255
256 input.oninput = () => {
257 var scaledLValue = scaleValue(value[0])
258 var scaledRValue = scaleValue(value[1])
259 lValue.value = scaledLValue.toFixed(prec)
260 rValue.value = scaledRValue.toFixed(prec)
261 this.emit('input', [scaledLValue, scaledRValue])
262 }
263
264 return this;
265}
\No newline at end of file