web-ui-pack
Version:
Web package with UI elements
290 lines (289 loc) • 9.42 kB
JavaScript
import onScroll from "../helpers/onScroll";
import { mathFixFP, onEvent } from "../indexHelpers";
import localeInfo from "../objects/localeInfo";
import WUPBaseControl from "./baseControl";
import WUPTextControl from "./text";
const tagName = "wup-num";
class TextAnyControl extends WUPTextControl {
}
export default class WUPNumberControl extends TextAnyControl {
#ctr = this.constructor;
static $defaults = {
...WUPTextControl.$defaults,
validationRules: {
...WUPBaseControl.$defaults.validationRules,
min: (v, setV, c) => (v == null || v < setV) && __wupln(`Min value is ${c.valueToInput(setV)}`, "validation"),
max: (v, setV, c) => (v == null || v > setV) && __wupln(`Max value is ${c.valueToInput(setV)}`, "validation"),
},
format: null,
scale: 1,
offset: 0,
};
static $parse(s, format) {
let v = 0;
let ok = false;
let dec = null;
let decLn = 1;
let decI = 0;
const f = format;
for (let i = 0; i < s.length; ++i) {
const ascii = s.charCodeAt(i);
const num = ascii - 48;
if (num >= 0 && num <= 9) {
ok = true;
if (dec === null) {
v = v * 10 + num;
}
else if (f.maxDecimal >= ++decI) {
dec = dec * 10 + num;
decLn *= 10;
}
}
else if (ascii === f.sepDecimal.charCodeAt(0)) {
dec = 0;
}
}
if (dec) {
v += dec / decLn;
}
if (!ok) {
v = undefined;
}
else if (s.charCodeAt(0) === 45) {
v *= -1;
}
return v;
}
static $stringify(v, format) {
if (v == null) {
return "";
}
let [int, dec] = v.toString().split(".");
const f = format;
if (f.sep1000) {
const to = v > 0 ? 0 : 1;
for (let i = int.length - 3; i > to; i -= 3) {
int = `${int.substring(0, i)}${f.sep1000}${int.substring(i)}`;
}
}
if (dec || f.minDecimal || f.maxDecimal) {
dec = dec === undefined ? "" : dec.substring(0, Math.min(f.maxDecimal, dec.length));
dec += "0".repeat(Math.max(f.minDecimal - dec.length, 0));
if (dec.length) {
return int + f.sepDecimal + dec;
}
}
return int;
}
get $format() {
const f = this._opts.format;
return {
sepDecimal: localeInfo.sepDecimal,
sep1000: localeInfo.sep1000,
...f,
maxDecimal: f?.maxDecimal ?? f?.minDecimal ?? 0,
minDecimal: f?.minDecimal ?? 0,
};
}
valueToInput(v, skipScaling) {
if (v == null) {
return "";
}
if (!skipScaling) {
const { scale, offset } = this._opts;
v /= scale || 1;
v += offset || 0;
v = mathFixFP(v);
}
if (this._opts.mask) {
return v.toString();
}
return this.#ctr.$stringify(v, this.$format);
}
parse(text) {
const v = Number(text);
if (!text || Number.isNaN(v)) {
return undefined;
}
return v;
}
canParseInput(text) {
const f = this.$format;
if (f.maxDecimal > 0 && text.endsWith(f.sepDecimal)) {
return false;
}
return true;
}
parseInput(text) {
let v = this.#ctr.$parse(text, this.$format);
if (v != null && (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER)) {
this.declineInput();
throw new RangeError("Out of range");
}
const rawValue = v;
if (v != null) {
const { scale, offset } = this._opts;
v -= offset || 0;
v = mathFixFP(v * (scale || 1));
}
if (!this._opts.mask) {
const next = this.#ctr.$stringify(rawValue, this.$format);
const hasDeclined = this._canShowDeclined && text.length > next.length;
if (hasDeclined && next === this.#ctr.$stringify(this.$value, this.$format)) {
this.declineInput();
}
else if (text !== next) {
const el = this.$refInput;
const pos = el.selectionStart || 0;
let dp = 0;
for (let i = 0, k = 0; k < pos && i < next.length; ++i, ++k) {
const ascii = text.charCodeAt(k);
if (!(ascii >= 48 && ascii <= 57)) {
--i;
--dp;
}
else if (next[i] !== text[k]) {
++dp;
--k;
}
}
this.setInputValue(v, 3);
const end = pos + dp;
el.setSelectionRange(end, end);
}
}
return v;
}
setInputValue(v, reason) {
const txt = this.valueToInput(v);
super.setInputValue(txt, reason);
}
renderControl() {
super.renderControl();
this.$refInput.setAttribute("role", "spinbutton");
}
gotChanges(propsChanged) {
super.gotChanges(propsChanged);
if ((!propsChanged && this._opts.format) || propsChanged?.includes("format")) {
this.setInputValue(this.$value, 3);
}
}
gotBeforeInput(e) {
super.gotBeforeInput(e);
if (!this._opts.mask) {
const el = e.target;
let pos = el.selectionStart || 0;
if (pos === el.selectionEnd) {
let isBefore = false;
switch (e.inputType) {
case "deleteContentBackward":
--pos;
isBefore = true;
case "deleteContentForward":
if (el.value[pos] === this.$format.sep1000) {
pos += isBefore ? 0 : 1;
el.setSelectionRange(pos, pos);
}
break;
default:
break;
}
}
}
}
_canShowDeclined;
gotInput(e) {
if (!this._opts.mask) {
switch (e.inputType) {
case "insertText":
case "insertFromPaste":
this._canShowDeclined = true;
break;
default:
delete this._canShowDeclined;
break;
}
}
super.gotInput(e);
}
gotFocus(ev) {
const r = super.gotFocus(ev);
this.$refInput.setAttribute("inputmode", "numeric");
r.push(onScroll(this, (v) => this.gotIncrement(-1 * v), { skip: () => this.$isReadOnly || this.$isDisabled }));
this.style.overflow = "";
r.push(onEvent(this, "keyup", (e) => {
if (!e.altKey) {
delete this._isAltDown;
e.key === "Alt" && this._wasInc && e.preventDefault();
delete this._wasInc;
}
if (!e.shiftKey)
delete this._isShiftDown;
if (!e.ctrlKey)
delete this._isCtrlDown;
}, { passive: false }));
r.push(() => {
delete this._wasInc;
delete this._isAltDown;
delete this._isShiftDown;
delete this._isCtrlDown;
});
return r;
}
_wasInc;
_isAltDown;
_isShiftDown;
_isCtrlDown;
gotKeyDown(e) {
super.gotKeyDown(e);
if (e.altKey)
this._isAltDown = true;
if (e.shiftKey)
this._isShiftDown = true;
if (e.ctrlKey)
this._isCtrlDown = true;
let v = 0;
switch (e.key) {
case "ArrowUp":
++v;
break;
case "ArrowDown":
--v;
break;
default:
break;
}
if (v) {
e.preventDefault();
this.gotIncrement(v);
}
}
gotIncrement(dval) {
this._wasInc = true;
if (this._isAltDown) {
dval *= 0.1;
if (this.$format.maxDecimal < 1) {
return;
}
}
else if (this._isShiftDown)
dval *= 10;
else if (this._isCtrlDown)
dval *= 100;
const tv = this.$refInput.value;
const v = (tv && this.#ctr.$parse(tv, this.$format)) || 0;
const hasFloat = this._isAltDown || v % 1 !== 0;
const next = hasFloat ? mathFixFP(v + dval) : v + dval;
const el = this.$refInput;
const inputType = dval > 0 ? "_inc" : "_dec";
const data = this.valueToInput(next, true);
setTimeout(() => {
const isPrevented = !el.dispatchEvent(new InputEvent("beforeinput", { inputType, data, bubbles: true, cancelable: true }));
if (!isPrevented) {
el.value = data;
el.dispatchEvent(new InputEvent("input", { inputType, bubbles: true }));
}
});
}
}
customElements.define(tagName, WUPNumberControl);