diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c2fe7b1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,227 @@ +{ + "name": "@tp/tp-element", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@tp/tp-element", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@tp/helpers": "^2.3.1", + "@tp/tp-input": "^1.0.6", + "lit": "^3.0.0" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", + "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==" + }, + "node_modules/@lit/reactive-element": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0" + } + }, + "node_modules/@tp/helpers": { + "version": "2.3.1", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/2.3.1/helpers-2.3.1.tgz", + "integrity": "sha512-LjwqF6wHy6SrdY+0m5+fMP0CG8mwb85rVd7ZkMTpmkJXn4PiTYMaLXNcCjq6k4R5AwhzWUasUOteBJNyl9pynw==", + "license": "Apache-2.0" + }, + "node_modules/@tp/tp-input": { + "version": "1.0.6", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-input/-/1.0.6/tp-input-1.0.6.tgz", + "integrity": "sha512-i24LTVNX8taafWEcO4FII1YwrLQO5RvA0f4KDGpxU0Ca+WWfSkq3xwJEbiC9eovP/wrB9e2XoLQ1mjm0S79+mg==", + "license": "Apache-2.0", + "dependencies": { + "@tp/helpers": "^1.0.0", + "lit": "^2.2.0" + } + }, + "node_modules/@tp/tp-input/node_modules/@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, + "node_modules/@tp/tp-input/node_modules/@tp/helpers": { + "version": "1.3.0", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/1.3.0/helpers-1.3.0.tgz", + "integrity": "sha512-mOAVP45kkEYXwonaOd5jkFQLX1nbeKtl8YX8FpL2ytON0cOSsh6TUAbCEcMU5xqgyD6L1ZEZNvxCjhOKOKdGyA==", + "license": "Apache-2.0" + }, + "node_modules/@tp/tp-input/node_modules/lit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "dependencies": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/@tp/tp-input/node_modules/lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/@tp/tp-input/node_modules/lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, + "node_modules/lit": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.4.tgz", + "integrity": "sha512-q6qKnKXHy2g1kjBaNfcoLlgbI3+aSOZ9Q4tiGa9bGYXq5RBXxkVTqTIVmP2VWMp29L4GyvCFm8ZQ2o56eUAMyA==", + "dependencies": { + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.0.4", + "lit-html": "^3.1.2" + } + }, + "node_modules/lit-element": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.6.tgz", + "integrity": "sha512-U4sdJ3CSQip7sLGZ/uJskO5hGiqtlpxndsLr6mt3IQIjheg93UKYeGQjWMRql1s/cXNOaRrCzC2FQwjIwSUqkg==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.1.2" + } + }, + "node_modules/lit-html": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.4.tgz", + "integrity": "sha512-yKKO2uVv7zYFHlWMfZmqc+4hkmSbFp8jgjdZY9vvR9jr4J8fH6FUMXhr+ljfELgmjpvlF7Z1SJ5n5/Jeqtc9YA==", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + } + }, + "dependencies": { + "@lit-labs/ssr-dom-shim": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", + "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==" + }, + "@lit/reactive-element": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", + "requires": { + "@lit-labs/ssr-dom-shim": "^1.2.0" + } + }, + "@tp/helpers": { + "version": "2.3.1", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/2.3.1/helpers-2.3.1.tgz", + "integrity": "sha512-LjwqF6wHy6SrdY+0m5+fMP0CG8mwb85rVd7ZkMTpmkJXn4PiTYMaLXNcCjq6k4R5AwhzWUasUOteBJNyl9pynw==" + }, + "@tp/tp-input": { + "version": "1.0.6", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-input/-/1.0.6/tp-input-1.0.6.tgz", + "integrity": "sha512-i24LTVNX8taafWEcO4FII1YwrLQO5RvA0f4KDGpxU0Ca+WWfSkq3xwJEbiC9eovP/wrB9e2XoLQ1mjm0S79+mg==", + "requires": { + "@tp/helpers": "^1.0.0", + "lit": "^2.2.0" + }, + "dependencies": { + "@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "requires": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, + "@tp/helpers": { + "version": "1.3.0", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/1.3.0/helpers-1.3.0.tgz", + "integrity": "sha512-mOAVP45kkEYXwonaOd5jkFQLX1nbeKtl8YX8FpL2ytON0cOSsh6TUAbCEcMU5xqgyD6L1ZEZNvxCjhOKOKdGyA==" + }, + "lit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "requires": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.8.0" + } + }, + "lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "requires": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "requires": { + "@types/trusted-types": "^2.0.2" + } + } + } + }, + "@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, + "lit": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.4.tgz", + "integrity": "sha512-q6qKnKXHy2g1kjBaNfcoLlgbI3+aSOZ9Q4tiGa9bGYXq5RBXxkVTqTIVmP2VWMp29L4GyvCFm8ZQ2o56eUAMyA==", + "requires": { + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.0.4", + "lit-html": "^3.1.2" + } + }, + "lit-element": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.6.tgz", + "integrity": "sha512-U4sdJ3CSQip7sLGZ/uJskO5hGiqtlpxndsLr6mt3IQIjheg93UKYeGQjWMRql1s/cXNOaRrCzC2FQwjIwSUqkg==", + "requires": { + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.1.2" + } + }, + "lit-html": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.4.tgz", + "integrity": "sha512-yKKO2uVv7zYFHlWMfZmqc+4hkmSbFp8jgjdZY9vvR9jr4J8fH6FUMXhr+ljfELgmjpvlF7Z1SJ5n5/Jeqtc9YA==", + "requires": { + "@types/trusted-types": "^2.0.2" + } + } + } +} diff --git a/package.json b/package.json index 24f0225..28da72f 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,20 @@ { - "name": "@tp/tp-element", - "version": "0.0.1", + "name": "@tp/tp-number-input", + "version": "1.0.0", "description": "", - "main": "tp-element.js", + "main": "tp-number-input.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "https://gitea.codeblob.work/tp-elements/tp-element.git" + "url": "https://gitea.codeblob.work/tp-elements/tp-number-input.git" }, "author": "trading_peter", "license": "Apache-2.0", "dependencies": { + "@tp/helpers": "^2.3.1", + "@tp/tp-input": "^1.0.6", "lit": "^3.0.0" } } diff --git a/tp-element.js b/tp-element.js deleted file mode 100644 index 6195006..0000000 --- a/tp-element.js +++ /dev/null @@ -1,35 +0,0 @@ -/** -@license -Copyright (c) 2024 trading_peter -This program is available under Apache License Version 2.0 -*/ - -import { LitElement, html, css } from 'lit'; - -class TpElement extends LitElement { - static get styles() { - return [ - css` - :host { - display: block; - } - ` - ]; - } - - render() { - const { } = this; - - return html` - - `; - } - - static get properties() { - return { }; - } - - -} - -window.customElements.define('tp-element', TpElement); diff --git a/tp-number-input.js b/tp-number-input.js new file mode 100644 index 0000000..8e79f84 --- /dev/null +++ b/tp-number-input.js @@ -0,0 +1,345 @@ +/** +@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() { + this.listen(this, 'click', '_onTap'); + this.listen(this.$.innerInput, 'blur', '_onBlur'); + this.listen(this.$.innerInput, 'keydown', '_onKey'); + } + + updated(changes) { + 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);