/** @license Copyright (c) 2024 trading_peter This program is available under Apache License Version 2.0 */ import '@tp/tp-input/tp-input.js'; import { LitElement, html, css, svg } from 'lit'; import { FormElement } from '@tp/helpers/form-element.js'; import { EventHelpers } from '@tp/helpers/event-helpers.js'; import { DomQuery } from '@tp/helpers/dom-query.js'; import { closest } from '@tp/helpers/closest.js' class TpNumberInput extends FormElement(EventHelpers(DomQuery(LitElement))) { static get styles() { return [ css` :host { display: block; } tp-input input { text-align: center; } div[slot="prefix"], div[slot="suffix"] { display: flex; flex-direction: row; align-items: center; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } div[slot="prefix"] > tp-icon, div[slot="suffix"] > tp-icon { display: inline; --tp-icon-width: var(--tp-number-input-icon-width, 17px); --tp-icon-height: var(--tp-number-input-icon-height, 17px); } ` ]; } render() { const { disabled, invalid, errorMessage, required, autofocus, _internValue } = this; return html`
`; } static get properties() { return { disabled: { type: Boolean }, invalid: { type: Boolean }, errorMessage: { type: String }, required: { type: Boolean }, autofocus: { type: Boolean }, step: { type: Number }, value: { type: String }, min: { type: Number }, max: { type: Number }, increaseIcon: { type: Object }, decreaseIcon: { type: Object }, increaseTooltip: { type: String }, decreaseTooltip: { type: String }, // Calculation speed after medium long press time. fastInterval: { type: Number }, // Calculation speed after really long press on one of the buttons. superFastInterval: { type: Number }, // String to add after the number. Use this to show a unit for example. // `13` becomes `13px` for example. suffix: { type: String }, // If true don't include the suffix in the `value` property. ignoreSuffix: { type: Boolean }, // Value to set if the input is NaN or user tries to go under `min`. // Useful if you need to support something like `0px` -> `user tabs decrease` -> `inherit` for example. specialMin: { type: String }, // If true the controls allows to switch through time in intervals defined by step. // Most other settings are ignore if the control is in this mode. timeMode: { type: Boolean }, _internValue: { type: String } }; } static get defaultIncreaseIcon() { return svg``; } static get defaultDecreaseIcon() { return svg``; } constructor() { super(); this._internValue = '0'; this.value = '0'; this.step = 1; this.min = Number.MIN_SAFE_INTEGER; this.max = Number.MAX_SAFE_INTEGER; this.fastInterval = 50; this.superFastInterval = 100; } firstUpdated() { super.firstUpdated(); this.listen(this, 'click', '_onTap'); this.listen(this.$.innerInput, 'blur', '_onBlur'); this.listen(this.$.innerInput, 'keydown', '_onKey'); } updated(changes) { super.updated(); if (changes.has('timeMode')) { this._timeModeChanged(); } if (changes.has('value') || changes.has('min') || changes.has('max') || changes.has('suffix') || changes.has('ignoreSuffix') || changes.has('timeMode')) { this._updateValue(this.value); } } _timeModeChanged() { if (this.timeMode) { this.unlisten(this, 'mousedown', '_onMouseDown'); } else { this.listen(this, 'mousedown', '_onMouseDown'); } } _onTap(e) { let val = parseInt(this.value, 10) || 0; const op = this._getOperation(e); if(!op) { return; } if(this.timeMode) { val = this._calcTime(op); } else { if(op === 'add') { val = Math.max(val + this.step, this.min); } else { val -= this.step; } } this._updateValue(val); } _calcTime(op) { const parts = this.value.split(':'); if(parts.length) if(op === 'add') { this._updateTime(this.step); } else { this._updateTime(-this.step); } } _timeValueChanged(value) { if(!this.timeMode) return; const parts = this._parseTime(value); const h = parts[0]; const m = parts[1]; this.value = this._prefix(h) + ':' + this._prefix(m); this.$.input.value = this.value; } _parseTime(value) { if(!/[0-9]{1,2}:[0-9]{1,2}/.test(value)) { value = '00:00'; } const parts = value.split(':'); let h = parts[0]; let m = parts[1]; h = parseInt(h, 10) % 24; m = parseInt(m, 10) % 59; return [h, m]; } _updateTime(amount) { const parts = this._parseTime(this.$.input.value); let h = parts[0]; let m = parts[1]; // if (amount > 0) { // 1. Calculate how many hours were made full. const addHours = Math.floor((m + amount) / 60); // 2. Calculate the rest of the minutes after we added to hours. const minuteRest = m + amount - 60 * addHours; h += addHours; if(h < 0) { h += 24; } // 3. Update the time. this.value = h + ':' + minuteRest; } _prefix(val) { val = val.toString(); if(val.length === 1) { return '0' + val; } else { return val; } } _onMouseDown(e) { this.unlisten(window, 'mouseup', '_onMouseUp'); this.listen(window, 'mouseup', '_onMouseUp'); const op = this._getOperation(e); this._asyncJob = setTimeout(() => { let val = parseInt(this.value, 10) || 0; const func = function () { if (op == 'add') { val += this.step; } else { val -= this.step; } this._updateValue(val); }.bind(this); this._timer = setInterval(func, this.slowInterval); // Speed up the interval. this._speedUpJob = setTimeout(() => { clearInterval(this._timer); this._timer = null; this._timer = setInterval(func, this.fastInterval); }, 2000); // Switch to super fast interval. this._superSpeedUpJob = setTimeout(() => { clearInterval(this._timer); this._timer = null; this._timer = setInterval(func, this.superFastInterval); }, 5000); }, 500); } _onMouseUp(e) { this.unlisten(window, 'mouseup', '_onMouseUp'); clearTimeout(this._asyncJob); clearTimeout(this._speedUpJob); clearTimeout(this._superSpeedUpJob); clearInterval(this._timer); this._speedUpJob = null; this._superSpeedUpJob = null; this._asyncJob = null; this._timer = null; } _onBlur() { if(this.timeMode) { this._timeValueChanged(this.$.input.value); } else { this._updateValue(parseInt(this.$.input.value, 10)); } } _onKey(e) { if(e.keyCode === 13) { this._updateValue(parseInt(this.$.input.value, 10)); } } _getOperation(e) { const btn = e.composedPath()[0]; if(closest(btn, '#sub', true)) { return 'sub'; } else if(closest(btn, '#add', true)) { return 'add'; } } _internValueChanged(val) { if(val === '') return; if(this.timeMode) { this._timeValueChanged(val); } else { this._updateValue(val); } } _updateValue(val) { if(this.timeMode) return; if(typeof this.specialMin === 'string' && val === this.specialMin) { this.value = this.specialMin; this.$.input.value = this.value; return; } val = parseInt(val, 10); // If val is NaN and an specialMin is defined, then set it. // Else set the min value. if(isNaN(val)) { val = typeof this.specialMin === 'string' ? this.specialMin : this.min; } if((typeof this.specialMin === 'string' && val === this.specialMin) || (val < this.min && typeof this.specialMin === 'string')) { this.value = this.specialMin; this.$.input.value = this.value; return; } val = Math.min(this.max, val); val = Math.max(this.min, val); this.value = this.ignoreSuffix ? val : (!!this.suffix ? val + this.suffix : val); this.$.input.value = !!this.suffix ? val + this.suffix : val; } } window.customElements.define('tp-number-input', TpNumberInput);