guify
Version:
A simple GUI for inspecting and changing JS variables
208 lines (169 loc) • 7.68 kB
JavaScript
import ComponentBase from "../component-base.js";
import css from "dom-css";
import isnumeric from "is-numeric";
import "./range.css";
import { default as LabelPartial } from "../partials/label";
import { default as ValuePartial } from "../partials/value";
export default class Range extends ComponentBase {
constructor (root, opts, theme) {
super(root, opts, theme);
this.scale = opts.scale;
this.label = LabelPartial(this.container, opts.label, theme);
this.input = this.container.appendChild(document.createElement("input"));
this.input.type = "range";
this.input.classList.add("guify-range");
// Add ARIA attribute to input based on label text
if(opts.label) this.input.setAttribute("aria-label", opts.label + " input");
// Get initial value:
if (opts.scale === "log") {
// If logarithmic, we're going to set the slider to a known linear range. Then we'll
// map that linear range to the user-set range using a log scale.
// 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);
}
// Step is invalid for log scale slider
if (isnumeric(opts.step)) {
console.warn("Step is unused for log scale sliders.");
}
// Warn that `steps` was removed
if (isnumeric(opts.steps)) {
console.warn("\"steps\" option for log scale sliders has been removed.");
}
// Min/max are forced to a known range, and log value will be derived from slider position within.
this.minPos = 0;
this.maxPos = 1000000;
this.min = Math.log( (isnumeric(opts.min)) ? opts.min : 0.000001 ); // This cannot be 0
this.max = Math.log( (isnumeric(opts.max)) ? opts.max : 100 );
this.precision = (isnumeric(opts.precision)) ? opts.precision : 3;
this.logScale = (this.max - this.min) / (this.maxPos - this.minPos);
this.initial = isnumeric(opts.initial) ? opts.initial : this.min;
if (opts.initial < 0) {
throw new Error(`Log range initial value must be > 0. Got initial value = ${opts.initial}`);
}
} else {
// If linear, this is much simpler. Pos and value can directly match.
this.minPos = (isnumeric(opts.min)) ? opts.min : 0;
this.maxPos = (isnumeric(opts.max)) ? opts.max : 100;
this.min = this.minPos;
this.max = this.maxPos;
this.precision = (isnumeric(opts.precision)) ? opts.precision : 3;
this.step = (isnumeric(opts.step)) ? opts.step : (10 / Math.pow(10, 3)); // Default is the lowest possible number given the precision. When precision = 3, step = 0.01.
this.initial = isnumeric(opts.initial) ? opts.initial : this.min;
// Quantize the initial value to the nearest step:
if (this.step != 0) {
var initialStep = Math.round((this.initial - this.min) / this.step);
this.initial = this.min + this.step * initialStep;
}
}
// Set value on the this.input itself:
this.input.min = this.minPos;
this.input.max = this.maxPos;
if (isnumeric(this.step)) {
this.input.step = this.step;
}
this.input.value = this._Position(this.initial);
css(this.input, {
width: `calc(100% - ${theme.sizing.labelWidth} - 16% - 0.5em)`
});
this.valueComponent = ValuePartial(this.container, this.initial, theme, "16%");
// Add ARIA attribute to input based on label text
if(opts.label) this.valueComponent.setAttribute("aria-label", opts.label + " value");
setTimeout(() => {
this.emit("initialized", parseFloat(this.input.value));
});
this.userIsModifying = false;
// Gain focus
this.input.addEventListener("focus", () => {
this.focused = true;
});
// Lose focus
this.input.addEventListener("blur", () => {
this.focused = false;
});
// Defocus on mouse up (for non-accessibility users)
this.input.addEventListener("mouseup", () => {
this.input.blur();
});
this.input.oninput = (data) => {
let position = parseFloat(data.target.value);
var scaledValue = this._Value(position);
this.valueComponent.value = this._FormatNumber(scaledValue, this.precision);
this.emit("input", scaledValue);
};
this.valueComponent.onchange = () => {
let rawValue = this.valueComponent.value;
if(Number(parseFloat(rawValue)) == rawValue){
// Input number is valid
var value = parseFloat(rawValue);
// Ensure number fits slider properties
value = this._ValidatedInputValue(value);
this.valueComponent.value = value;
this.emit("input", value);
this.lastValue = value;
} else {
// Input number is invalid
// Go back to before input change
this.valueComponent.value = this.lastValue;
}
};
}
/**
* Calculate value from slider position
*/
_Value(position) {
if (this.scale === "log") {
// Map from slider position range to log value range
// Map from slider range to min-max value range
let rangePos = (position - this.minPos) * this.logScale + this.min;
// Now convert to log space
return Math.exp(rangePos);
} else {
// Position and value are equivalent
return position;
}
}
/**
* Calculate slider position from value
*/
_Position(value) {
if (this.scale === "log") {
// Map from log value range to the slider's position range
return this.minPos + (Math.log(value) - this.min) / this.logScale;
} else {
// Value and position are equivalent
return value;
}
}
_ValidatedInputValue(value) {
var newValue;
if (this.scale === "log") {
// Clamp to input range, turning logmin and logmax back into min/max in linear space
newValue = Math.min(Math.max(value, Math.exp(this.min)), Math.exp(this.max));
} else {
// Clamp to input range
newValue = Math.min(Math.max(value, this.min), this.max);
// Quantize to step
newValue = Math.ceil((newValue - this.min) / this.step) * this.step + this.min;
}
return this._FormatNumber(newValue, this.precision);
}
SetValue(value) {
let validated = this._ValidatedInputValue(value);
if(this.focused !== true) {
this.valueComponent.value = this._FormatNumber(validated, this.precision);
this.input.value = this._Position(validated);
this.lastValue = validated;
}
}
GetValue() {
return this._Value(this.input.value);
}
// Formats the number for display.
// `opts.precision` lets you customize how many decimal places you want here.
// The default is 3.
_FormatNumber(value, precision) {
// https://stackoverflow.com/a/29249277
return +parseFloat(value).toFixed(precision);
}
}