UNPKG

web-ui-pack

Version:
290 lines (289 loc) 9.42 kB
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);