UNPKG

control-panel

Version:

embeddable panel of inputs for parameter setting

224 lines (176 loc) 7.71 kB
var EventEmitter = require('events').EventEmitter var inherits = require('inherits') var isnumeric = require('is-numeric') var css = require('dom-css') module.exports = Range inherits(Range, EventEmitter) function clamp (x, min, max) { return Math.min(Math.max(x, min), max) } function Range (root, opts, theme, uuid) { if (!(this instanceof Range)) return new Range(root, opts, theme, uuid) var self = this var scaleValue, scaleValueInverse, logmin, logmax, logsign, panel, input, handle var container = require('./container')(root, opts.label) require('./label')(container, opts.label, theme) if (!!opts.step && !!opts.steps) { throw new Error('Cannot specify both step and steps. Got step = ' + opts.step + ', steps = ', opts.steps) } setTimeout(function () { panel = document.getElementById('control-panel-' + uuid) }) input = container.appendChild(document.createElement('span')) input.className = 'control-panel-interval-' + uuid handle = document.createElement('span') handle.className = 'control-panel-interval-handle' input.appendChild(handle) // Create scale functions for converting to/from the desired scale: if (opts.scale === 'log') { scaleValue = function (x) { return logsign * Math.exp(Math.log(logmin) + (Math.log(logmax) - Math.log(logmin)) * x / 100) } scaleValueInverse = function (y) { return (Math.log(y * logsign) - Math.log(logmin)) * 100 / (Math.log(logmax) - Math.log(logmin)) } } else { scaleValue = scaleValueInverse = function (x) { return x } } if (!Array.isArray(opts.initial)) { opts.initial = [] } if (opts.scale === 'log') { // Get options or set defaults: opts.max = (isnumeric(opts.max)) ? opts.max : 100 opts.min = (isnumeric(opts.min)) ? opts.min : 0.1 // Check if all signs are valid: if (opts.min * opts.max <= 0) { throw new Error('Log range min/max must have the same sign and not equal zero. Got min = ' + opts.min + ', max = ' + opts.max) } else { // Pull these into separate variables so that opts can define the *slider* mapping logmin = opts.min logmax = opts.max logsign = opts.min > 0 ? 1 : -1 // Got the sign so force these positive: logmin = Math.abs(logmin) logmax = Math.abs(logmax) // These are now simply 0-100 to which we map the log range: opts.min = 0 opts.max = 100 // Step is invalid for a log range: if (isnumeric(opts.step)) { throw new Error('Log may only use steps (integer number of steps), not a step value. Got step =' + opts.step) } // Default step is simply 1 in linear slider space: opts.step = 1 } opts.initial = [ scaleValueInverse(isnumeric(opts.initial[0]) ? opts.initial[0] : scaleValue(opts.min + (opts.max - opts.min) * 0.25)), scaleValueInverse(isnumeric(opts.initial[1]) ? opts.initial[1] : scaleValue(opts.min + (opts.max - opts.min) * 0.75)) ] if (scaleValue(opts.initial[0]) * scaleValue(opts.max) <= 0 || scaleValue(opts.initial[1]) * scaleValue(opts.max) <= 0) { 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.initial[0]) + ', ' + scaleValue(opts.initial[1]) + ']') } } else { // If linear, this is much simpler: opts.max = (isnumeric(opts.max)) ? opts.max : 100 opts.min = (isnumeric(opts.min)) ? opts.min : 0 opts.step = (isnumeric(opts.step)) ? opts.step : (opts.max - opts.min) / 100 opts.initial = [ isnumeric(opts.initial[0]) ? opts.initial[0] : (opts.min + opts.max) * 0.25, isnumeric(opts.initial[1]) ? opts.initial[1] : (opts.min + opts.max) * 0.75 ] } // If we got a number of steps, use that instead: if (isnumeric(opts.steps)) { opts.step = isnumeric(opts.steps) ? (opts.max - opts.min) / opts.steps : opts.step } // Quantize the initial value to the requested step: opts.initial[0] = opts.min + opts.step * Math.round((opts.initial[0] - opts.min) / opts.step) opts.initial[1] = opts.min + opts.step * Math.round((opts.initial[1] - opts.min) / opts.step) var value = opts.initial function setHandleCSS () { css(handle, { left: ((value[0] - opts.min) / (opts.max - opts.min) * 100) + '%', right: (100 - (value[1] - opts.min) / (opts.max - opts.min) * 100) + '%' }) } // Initialize CSS: setHandleCSS() // Display the values: var lValue = require('./value')(container, scaleValue(opts.initial[0]), theme, '11%', true) var rValue = require('./value')(container, scaleValue(opts.initial[1]), theme, '11%') // An index to track what's being dragged: var activeIndex = -1 function mouseX (ev) { // Get mouse position in page coords relative to the container: return ev.pageX - input.getBoundingClientRect().left } function setActiveValue (fraction) { if (activeIndex === -1) { return } // Get the position in the range [0, 1]: var lofrac = (value[0] - opts.min) / (opts.max - opts.min) var hifrac = (value[1] - opts.min) / (opts.max - opts.min) // Clip against the other bound: if (activeIndex === 0) { fraction = Math.min(hifrac, fraction) } else { fraction = Math.max(lofrac, fraction) } // Compute and quantize the new value: var newValue = opts.min + Math.round((opts.max - opts.min) * fraction / opts.step) * opts.step // Update value, in linearized coords: value[activeIndex] = newValue // Update and send the event: setHandleCSS() input.oninput() } var mousemoveListener = function (ev) { var fraction = clamp(mouseX(ev) / input.offsetWidth, 0, 1) setActiveValue(fraction) } var mouseupListener = function (ev) { panel.classList.remove('control-panel-interval-dragging') var fraction = clamp(mouseX(ev) / input.offsetWidth, 0, 1) setActiveValue(fraction) document.removeEventListener('mousemove', mousemoveListener) document.removeEventListener('mouseup', mouseupListener) activeIndex = -1 } input.addEventListener('mousedown', function (ev) { // Tweak control to make dragging experience a little nicer: panel.classList.add('control-panel-interval-dragging') // Get mouse position fraction: var fraction = clamp(mouseX(ev) / input.offsetWidth, 0, 1) // Get the current fraction of position --> [0, 1]: var lofrac = (value[0] - opts.min) / (opts.max - opts.min) var hifrac = (value[1] - opts.min) / (opts.max - opts.min) // This is just for making decisions, so perturb it ever // so slightly just in case the bounds are numerically equal: lofrac -= Math.abs(opts.max - opts.min) * 1e-15 hifrac += Math.abs(opts.max - opts.min) * 1e-15 // Figure out which is closer: var lodiff = Math.abs(lofrac - fraction) var hidiff = Math.abs(hifrac - fraction) activeIndex = lodiff < hidiff ? 0 : 1 // Attach this to *document* so that we can still drag if the mouse // passes outside the container: document.addEventListener('mousemove', mousemoveListener) document.addEventListener('mouseup', mouseupListener) }) setTimeout(function () { var scaledLValue = scaleValue(value[0]) var scaledRValue = scaleValue(value[1]) lValue.innerHTML = scaledLValue rValue.innerHTML = scaledRValue self.emit('initialized', [scaledLValue, scaledRValue]) }) input.oninput = function () { var scaledLValue = scaleValue(value[0]) var scaledRValue = scaleValue(value[1]) lValue.innerHTML = scaledLValue rValue.innerHTML = scaledRValue self.emit('input', [scaledLValue, scaledRValue]) } }