@playcanvas/pcui
Version:
User interface component library for the web
354 lines (351 loc) • 13.8 kB
JavaScript
import { CLASS_MULTIPLE_VALUES } from '../../class.mjs';
import { Element } from '../Element/index.mjs';
import { InputElement } from '../InputElement/index.mjs';
const CLASS_NUMERIC_INPUT = 'pcui-numeric-input';
const CLASS_NUMERIC_INPUT_SLIDER_CONTROL = `${CLASS_NUMERIC_INPUT}-slider-control`;
const CLASS_NUMERIC_INPUT_SLIDER_CONTROL_ACTIVE = `${CLASS_NUMERIC_INPUT_SLIDER_CONTROL}-active`;
const CLASS_NUMERIC_INPUT_SLIDER_CONTROL_HIDDEN = `${CLASS_NUMERIC_INPUT_SLIDER_CONTROL}-hidden`;
const REGEX_COMMA = /,/g;
/**
* The NumericInput represents an input element that holds numbers.
*
* NumericInput accepts `number` values. It also accepts strings that contain mathematical
* expressions which are then evaluated to a number. Here are some examples:
*
* | Input String | Evaluated Number Value |
* | ------------------ | ---------------------- |
* | `"10 + 20"` | `30` |
* | `"10 - 20"` | `-10` |
* | `"10 * 20"` | `200` |
* | `"10 / 20"` | `0.5` |
* | `"10 * (20 + 30)"` | `500` |
*
* By default, a NumericInput displays a slider input that can be used to quickly change the value
* via a click and drag. The slider can be disabled by setting the `hideSlider` argument to `true`.
*/
class NumericInput extends InputElement {
/**
* Creates a new NumericInput.
*
* @param args - The arguments.
*/
constructor(args = {}) {
var _a, _b, _c, _d, _e;
const textInputArgs = Object.assign({}, args);
// delete value because we want to set it after the other arguments
delete textInputArgs.value;
delete textInputArgs.renderChanges;
super(textInputArgs);
this._sliderUsed = false;
this._onSliderMouseWheel = (evt) => {
this._updatePosition(evt.deltaY, evt.shiftKey);
};
this._onSliderMouseMove = (evt) => {
this._updatePosition(evt.movementX, evt.shiftKey);
};
this._onSliderMouseDown = (evt) => {
this._sliderControl.dom.requestPointerLock();
this._sliderMovement = 0.0;
this._sliderPrevValue = this.value;
this._sliderUsed = true;
if (this.binding) {
this._historyCombine = this.binding.historyCombine;
this._historyPostfix = this.binding.historyPostfix;
this.binding.historyCombine = true;
this.binding.historyPostfix = `(${Date.now()})`;
}
this.emit('slider:mousedown', evt);
};
this._onSliderMouseUp = () => {
document.exitPointerLock();
if (!this._sliderUsed)
return;
this._sliderUsed = false;
this.value = this._sliderPrevValue + this._sliderMovement;
if (this.binding) {
this.binding.historyCombine = this._historyCombine;
this.binding.historyPostfix = this._historyPostfix;
this._historyCombine = false;
this._historyPostfix = null;
}
this.focus();
this.emit('slider:mouseup');
};
this._onPointerLockChange = () => {
if (this._isScrolling()) {
this._sliderControl.dom.addEventListener('mousemove', this._onSliderMouseMove, false);
this._sliderControl.dom.addEventListener('wheel', this._onSliderMouseWheel, { passive: true });
this._sliderControl.class.add(CLASS_NUMERIC_INPUT_SLIDER_CONTROL_ACTIVE);
}
else {
this._sliderControl.dom.removeEventListener('mousemove', this._onSliderMouseMove, false);
this._sliderControl.dom.removeEventListener('wheel', this._onSliderMouseWheel);
this._sliderControl.class.remove(CLASS_NUMERIC_INPUT_SLIDER_CONTROL_ACTIVE);
}
};
this.class.add(CLASS_NUMERIC_INPUT);
this._min = (_a = args.min) !== null && _a !== void 0 ? _a : null;
this._max = (_b = args.max) !== null && _b !== void 0 ? _b : null;
this._allowNull = (_c = args.allowNull) !== null && _c !== void 0 ? _c : false;
this._precision = (_d = args.precision) !== null && _d !== void 0 ? _d : 7;
if (Number.isFinite(args.step)) {
this._step = args.step;
}
else if (args.precision) {
this._step = 10 / Math.pow(10, args.precision);
}
else {
this._step = 1;
}
if (Number.isFinite(args.stepPrecision)) {
this._stepPrecision = args.stepPrecision;
}
else {
this._stepPrecision = this._step * 0.1;
}
this._oldValue = undefined;
if (Number.isFinite(args.value)) {
this.value = args.value;
}
else if (!this._allowNull) {
this.value = 0;
}
this._historyCombine = false;
this._historyPostfix = null;
this._sliderPrevValue = 0;
this.renderChanges = (_e = args.renderChanges) !== null && _e !== void 0 ? _e : false;
if (!args.hideSlider) {
this._sliderControl = new Element();
this._sliderControl.class.add(CLASS_NUMERIC_INPUT_SLIDER_CONTROL);
this.dom.append(this._sliderControl.dom);
this._sliderControl.dom.addEventListener('mousedown', this._onSliderMouseDown);
this._sliderControl.dom.addEventListener('mouseup', this._onSliderMouseUp);
document.addEventListener('pointerlockchange', this._onPointerLockChange, false);
}
}
destroy() {
if (this.destroyed)
return;
if (this._sliderControl) {
this._sliderControl.dom.removeEventListener('mousedown', this._onSliderMouseDown);
this._sliderControl.dom.removeEventListener('mouseup', this._onSliderMouseUp);
this._sliderControl.dom.removeEventListener('mousemove', this._onSliderMouseMove, false);
this._sliderControl.dom.removeEventListener('wheel', this._onSliderMouseWheel);
document.removeEventListener('pointerlockchange', this._onPointerLockChange, false);
}
super.destroy();
}
_updatePosition(movement, shiftKey) {
// move one step or stepPrecision every 100 pixels
this._sliderMovement += movement / 100 * (shiftKey ? this._stepPrecision : this._step);
this.value = this._sliderPrevValue + this._sliderMovement;
}
_onInputChange(evt) {
// get the content of the input, normalize it and set it as the current value
this.value = this._normalizeValue(this._domInput.value);
}
_onInputKeyDown(evt) {
if (!this.enabled || this.readOnly)
return;
// increase / decrease value with arrow keys
if (evt.key === 'ArrowUp' || evt.key === 'ArrowDown') {
const inc = evt.key === 'ArrowDown' ? -1 : 1;
this.value += (evt.shiftKey ? this._stepPrecision : this._step) * inc;
}
super._onInputKeyDown(evt);
}
_getPointerLockElementByShadowRoot(pointerLockElement) {
const shadowRoot = pointerLockElement.shadowRoot;
if (shadowRoot) {
const pointerLockElement = shadowRoot.pointerLockElement;
return this._getPointerLockElementByShadowRoot(pointerLockElement);
}
return pointerLockElement === this._sliderControl.dom;
}
_isScrolling() {
if (!this._sliderControl)
return false;
if (document.pointerLockElement && document.pointerLockElement.shadowRoot) {
return this._getPointerLockElementByShadowRoot(document.pointerLockElement);
}
return document.pointerLockElement === this._sliderControl.dom;
}
_normalizeValue(value) {
try {
if (typeof value === 'string') {
// check for 0
if (value === '0')
return 0;
// replace commas with dots (for some international keyboards)
value = value.replace(REGEX_COMMA, '.');
// remove spaces
value = value.replace(/\s/g, '');
// sanitize input to only allow short mathematical expressions
value = value.match(/^[*/+\-0-9().]+$/);
if (value !== null && value[0].length < 20) {
let expression = value[0];
const operators = ['+', '-', '/', '*'];
operators.forEach((operator) => {
const expressionArr = expression.split(operator);
expressionArr.forEach((_, i) => {
expressionArr[i] = expressionArr[i].replace(/^0+/, '');
});
expression = expressionArr.join(operator);
});
// eslint-disable-next-line no-new-func
value = Function(`"use strict";return (${expression})`)();
}
}
if (value === null || value === undefined || value === '') {
if (this._allowNull) {
return null;
}
value = 0;
}
value = Number(value);
if (isNaN(value)) {
if (this._allowNull) {
return null;
}
value = 0;
}
// clamp between min max
if (this.min !== null && value < this.min) {
value = this.min;
}
if (this.max !== null && value > this.max) {
value = this.max;
}
// fix precision
if (this.precision !== null) {
value = parseFloat(Number(value).toFixed(this.precision));
}
return value;
}
catch (error) {
if (this._allowNull) {
return null;
}
return 0;
}
}
_updateValue(value, force) {
const different = (value !== this._oldValue || force);
// always set the value to the input because
// we always want it to show an actual number or nothing
this._oldValue = value;
if (value === null) {
this._domInput.value = '';
}
else {
this._domInput.value = String(value);
}
this.class.remove(CLASS_MULTIPLE_VALUES);
if (different) {
this.emit('change', value);
}
return different;
}
set value(value) {
value = this._normalizeValue(value);
const forceUpdate = this.class.contains(CLASS_MULTIPLE_VALUES) && value === null && this._allowNull;
const changed = this._updateValue(value, forceUpdate);
if (changed && this.binding) {
this.binding.setValue(value);
}
if (this._sliderControl) {
this._sliderControl.class.remove(CLASS_NUMERIC_INPUT_SLIDER_CONTROL_HIDDEN);
}
}
get value() {
const val = this._domInput.value;
return val !== '' ? parseFloat(val) : null;
}
/* eslint accessor-pairs: 0 */
set values(values) {
const normalizedValues = values.map(v => this._normalizeValue(v));
const different = normalizedValues.some(v => v !== normalizedValues[0]);
if (different) {
this._updateValue(null);
this.class.add(CLASS_MULTIPLE_VALUES);
if (this._sliderControl) {
this._sliderControl.class.add(CLASS_NUMERIC_INPUT_SLIDER_CONTROL_HIDDEN);
}
}
else {
this._updateValue(normalizedValues[0]);
if (this._sliderControl) {
this._sliderControl.class.remove(CLASS_NUMERIC_INPUT_SLIDER_CONTROL_HIDDEN);
}
}
}
/**
* Sets the minimum value this field can take.
*/
set min(value) {
if (this._min === value)
return;
this._min = value;
// reset value
if (this._min !== null) {
this.value = this.value; // eslint-disable-line no-self-assign
}
}
/**
* Gets the minimum value this field can take.
*/
get min() {
return this._min;
}
/**
* Sets the maximum value this field can take.
*/
set max(value) {
if (this._max === value)
return;
this._max = value;
// reset value
if (this._max !== null) {
this.value = this.value; // eslint-disable-line no-self-assign
}
}
/**
* Gets the maximum value this field can take.
*/
get max() {
return this._max;
}
/**
* Sets the precision of the input.
*/
set precision(value) {
if (this._precision === value)
return;
this._precision = value;
// reset value
if (this._precision !== null) {
this.value = this.value; // eslint-disable-line no-self-assign
}
}
/**
* Gets the precision of the input.
*/
get precision() {
return this._precision;
}
/**
* Sets the amount that the value will be increased or decreased when using the arrow keys and the slider input.
*/
set step(value) {
this._step = value;
}
/**
* Gets the amount that the value will be increased or decreased when using the arrow keys and the slider input.
*/
get step() {
return this._step;
}
}
Element.register('number', NumericInput, { renderChanges: true });
export { NumericInput };
//# sourceMappingURL=index.mjs.map