2024-08-19 21:47:51 +02:00
|
|
|
/**
|
|
|
|
@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`
|
|
|
|
<tp-input id="input" exportparts="wrap" .disabled=${disabled} .invalid=${invalid} .value=${_internValue} .autofocus=${autofocus} .errorMessage=${errorMessage} .required=${required}>
|
|
|
|
<div slot="prefix">
|
|
|
|
<tp-icon id="sub" part="icons" .icon=${this.decreaseIcon || TpNumberInput.defaultDecreaseIcon} .tooltip=${this.decreaseTooltip}></tp-icon>
|
|
|
|
</div>
|
|
|
|
<input id="innerInput" type="text" pattern="[0-9\\.,]+">
|
|
|
|
<div slot="suffix">
|
|
|
|
<tp-icon id="add" part="icons" .icon=${this.increaseIcon || TpNumberInput.defaultIncreaseIcon} .tooltip=${this.increaseTooltip}></tp-icon>
|
|
|
|
</div>
|
|
|
|
</tp-input>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
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`<path fill="var(--tp-icon-color)" d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"></path>`;
|
|
|
|
}
|
|
|
|
|
|
|
|
static get defaultDecreaseIcon() {
|
|
|
|
return svg`<path fill="var(--tp-icon-color)" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10H7v-2h10v2z"></path>`;
|
|
|
|
}
|
|
|
|
|
|
|
|
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() {
|
2024-08-19 22:09:03 +02:00
|
|
|
super.firstUpdated();
|
|
|
|
|
2024-08-19 21:47:51 +02:00
|
|
|
this.listen(this, 'click', '_onTap');
|
|
|
|
this.listen(this.$.innerInput, 'blur', '_onBlur');
|
|
|
|
this.listen(this.$.innerInput, 'keydown', '_onKey');
|
|
|
|
}
|
|
|
|
|
|
|
|
updated(changes) {
|
2024-08-19 22:09:03 +02:00
|
|
|
super.updated();
|
|
|
|
|
2024-08-19 21:47:51 +02:00
|
|
|
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);
|