/**
@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);