diff --git a/README.md b/README.md
index 1ab27b7..ada099a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-# tp-element
+# tp-date-input
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..2cc17fa
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,132 @@
+{
+ "name": "@tp/tp-date-input",
+ "version": "0.0.1",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@tp/tp-date-input",
+ "version": "0.0.1",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "date-fns": "^2.28.0",
+ "date-fns-tz": "^1.3.3",
+ "lit": "^2.2.0"
+ }
+ },
+ "node_modules/@lit/reactive-element": {
+ "version": "1.3.1",
+ "resolved": "https://verdaccio.codeblob.work/@lit%2freactive-element/-/reactive-element-1.3.1.tgz",
+ "integrity": "sha512-nOJARIr3pReqK3hfFCSW2Zg/kFcFsSAlIE7z4a0C9D2dPrgD/YSn3ZP2ET/rxKB65SXyG7jJbkynBRm+tGlacw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.2",
+ "resolved": "https://verdaccio.codeblob.work/@types%2ftrusted-types/-/trusted-types-2.0.2.tgz",
+ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
+ "license": "MIT"
+ },
+ "node_modules/date-fns": {
+ "version": "2.28.0",
+ "resolved": "https://verdaccio.codeblob.work/date-fns/-/date-fns-2.28.0.tgz",
+ "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
+ "node_modules/date-fns-tz": {
+ "version": "1.3.3",
+ "resolved": "https://verdaccio.codeblob.work/date-fns-tz/-/date-fns-tz-1.3.3.tgz",
+ "integrity": "sha512-Gks46gwbSauBQnV3Oofluj1wTm8J0tM7sbSJ9P+cJq/ZnTCpMohTKmmO5Tn+jQ7dyn0+b8G7cY4O2DZ5P/LXcA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "date-fns": ">=2.0.0"
+ }
+ },
+ "node_modules/lit": {
+ "version": "2.2.1",
+ "resolved": "https://verdaccio.codeblob.work/lit/-/lit-2.2.1.tgz",
+ "integrity": "sha512-dSe++R50JqrvNGXmI9OE13de1z5U/Y3J2dTm/9GC86vedI8ILoR8ZGnxfThFpvQ9m0lR0qRnIR4IiKj/jDCfYw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-element": "^3.2.0",
+ "lit-html": "^2.2.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "3.2.0",
+ "resolved": "https://verdaccio.codeblob.work/lit-element/-/lit-element-3.2.0.tgz",
+ "integrity": "sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-html": "^2.2.0"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "2.2.1",
+ "resolved": "https://verdaccio.codeblob.work/lit-html/-/lit-html-2.2.1.tgz",
+ "integrity": "sha512-AiJ/Rs0awjICs2FioTnHSh+Np5dhYSkyRczKy3wKjp8qjLhr1Ov+GiHrUQNdX8ou1LMuznpIME990AZsa/tR8g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ }
+ },
+ "dependencies": {
+ "@lit/reactive-element": {
+ "version": "1.3.1",
+ "resolved": "https://verdaccio.codeblob.work/@lit%2freactive-element/-/reactive-element-1.3.1.tgz",
+ "integrity": "sha512-nOJARIr3pReqK3hfFCSW2Zg/kFcFsSAlIE7z4a0C9D2dPrgD/YSn3ZP2ET/rxKB65SXyG7jJbkynBRm+tGlacw=="
+ },
+ "@types/trusted-types": {
+ "version": "2.0.2",
+ "resolved": "https://verdaccio.codeblob.work/@types%2ftrusted-types/-/trusted-types-2.0.2.tgz",
+ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
+ },
+ "date-fns": {
+ "version": "2.28.0",
+ "resolved": "https://verdaccio.codeblob.work/date-fns/-/date-fns-2.28.0.tgz",
+ "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
+ },
+ "date-fns-tz": {
+ "version": "1.3.3",
+ "resolved": "https://verdaccio.codeblob.work/date-fns-tz/-/date-fns-tz-1.3.3.tgz",
+ "integrity": "sha512-Gks46gwbSauBQnV3Oofluj1wTm8J0tM7sbSJ9P+cJq/ZnTCpMohTKmmO5Tn+jQ7dyn0+b8G7cY4O2DZ5P/LXcA==",
+ "requires": {}
+ },
+ "lit": {
+ "version": "2.2.1",
+ "resolved": "https://verdaccio.codeblob.work/lit/-/lit-2.2.1.tgz",
+ "integrity": "sha512-dSe++R50JqrvNGXmI9OE13de1z5U/Y3J2dTm/9GC86vedI8ILoR8ZGnxfThFpvQ9m0lR0qRnIR4IiKj/jDCfYw==",
+ "requires": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-element": "^3.2.0",
+ "lit-html": "^2.2.0"
+ }
+ },
+ "lit-element": {
+ "version": "3.2.0",
+ "resolved": "https://verdaccio.codeblob.work/lit-element/-/lit-element-3.2.0.tgz",
+ "integrity": "sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==",
+ "requires": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-html": "^2.2.0"
+ }
+ },
+ "lit-html": {
+ "version": "2.2.1",
+ "resolved": "https://verdaccio.codeblob.work/lit-html/-/lit-html-2.2.1.tgz",
+ "integrity": "sha512-AiJ/Rs0awjICs2FioTnHSh+Np5dhYSkyRczKy3wKjp8qjLhr1Ov+GiHrUQNdX8ou1LMuznpIME990AZsa/tR8g==",
+ "requires": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index c39fdff..252c2d6 100644
--- a/package.json
+++ b/package.json
@@ -1,18 +1,20 @@
{
- "name": "@tp/tp-element",
- "version": "0.0.1",
+ "name": "@tp/tp-date-input",
+ "version": "0.1.0",
"description": "",
- "main": "tp-element.js",
+ "main": "tp-date-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-date-input.git"
},
"author": "trading_peter",
"license": "Apache-2.0",
"dependencies": {
+ "date-fns": "^2.28.0",
+ "date-fns-tz": "^1.3.3",
"lit": "^2.2.0"
}
}
diff --git a/tp-date-input.js b/tp-date-input.js
new file mode 100644
index 0000000..fffc4eb
--- /dev/null
+++ b/tp-date-input.js
@@ -0,0 +1,428 @@
+/**
+@license
+Copyright (c) 2022 trading_peter
+This program is available under Apache License Version 2.0
+*/
+
+import '@tp/tp-input/tp-input.js';
+import { ControlState } from '@tp/helpers/control-state.js';
+import { EventHelpers } from '@tp/helpers/event-helpers.js';
+import { FormElement } from '@tp/helpers/form-element.js';
+import { LitElement, html, css } from 'lit';
+import { format, parse, parseISO, isAfter, isValid, endOfDay } from 'date-fns/esm';
+
+class TpDateInput extends EventHelpers(ControlState(FormElement(LitElement))) {
+ static get styles() {
+ return [
+ css`
+ :host {
+ display: block;
+ }
+
+ .wrap {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .wrap > div {
+ align-self: flex-end;
+ }
+
+ tp-input {
+ width: 30px;
+ text-align: center;
+ border: none;
+ }
+
+ tp-input.bigger {
+ width: 50px;
+ }
+
+ .under {
+ position: relative;
+ }
+
+ .error-message {
+ position: absolute;
+ top: -5px;
+ left: 0;
+ font-size: 10px;
+ color: var(--tp-input-text-color-invalid, #B71C1C);
+ transition: opacity 0.3s;
+ opacity: 0;
+ will-change: opacity;
+ }
+
+ :host([invalid]) .error-message {
+ opacity: 1;
+ }
+ `
+ ];
+ }
+
+ render() {
+ const { _delemiter } = this;
+
+ return html`
+
+ `;
+ }
+
+ static get properties() {
+ return {
+ // Format of the date.
+ // Supports MM, dd and y. The order specifies how the input fields are labeled.
+ // The delimiter is also taken from the supplied format.
+ format: { type: String },
+
+ // Date object with the currently selected date.
+ date: { type: Object },
+
+ // If true, let value default to the date of today.
+ today: { type: Boolean },
+
+ required: { type: Boolean },
+
+ autoValidate: { type: Boolean },
+
+ // If true, the entered date is invalid.
+ invalid: { type: Boolean, reflect: true },
+
+ // Error message to show if the date is invalid.
+ errorMessage: { type: String },
+
+ optional: { type: Boolean },
+
+ // Range of years in the future that are selectable in the date-picker.
+ maxYear: { type: Number },
+
+ // Range of years in the past that are selectable in the date-picker.
+ minYear: { type: Number },
+
+ // Maximum date that can be selected. Everything after will be disabled.
+ // Set it to `today` to automatically allow dates till today (inclusive).
+ // Expects 'today' or a ISO Date string.
+ maxDate: { type: String },
+ };
+ }
+
+ constructor() {
+ super();
+ this.format = 'MM-dd-y';
+ this.required = false;
+ this.autoValidate = false;
+ this.invalid = false;
+ this.optional = false;
+ this.maxYear = 10;
+ this.minYear = 10;
+ }
+
+ shouldUpdate(changes) {
+ if (changes.has('format')) {
+ this._formatChanged();
+ }
+
+ if (changes.has('value')) {
+ this._onValueChanged();
+ }
+
+ return true;
+ }
+
+ firstUpdated() {
+ this.listen(this, 'input', '_autoMoveCursor');
+ }
+
+ /**
+ * Returns current validation state of the control.
+ */
+ validate() {
+ this.inputs[0].validate();
+ this.inputs[1].validate();
+ this.inputs[2].validate();
+
+ if (this.optional && this.inputs[0].value === '' && this.inputs[1].value === '' && this.inputs[2].value === '') {
+ this.invalid = false;
+ return true;
+ }
+
+ const maxDate = this._getMaxDate(this.maxDate);
+
+ if ((this.inputs[0].invalid || this.inputs[1].invalid || this.inputs[2].invalid) ||
+ !this.dateValid(this.value, maxDate)) {
+ this.invalid = true;
+ return false;
+ }
+
+ this.invalid = false;
+ return true;
+ }
+
+ focus() {
+ this.inputs[0].select();
+ }
+
+ /**
+ * Test is a date is selectable when considering all restricting options
+ * of the control, like enabledDates, maxDate, ...
+ */
+ dateValid(date, maxDate, enabledDates) {
+ date = this._toDate(date);
+
+ if (isValid(date) === false) {
+ return false;
+ }
+
+ maxDate = maxDate || this.maxDate;
+ enabledDates = enabledDates || [];
+
+ if ((enabledDates.length > 0 && enabledDates.indexOf(date) === -1) ||
+ (maxDate && isAfter(date, maxDate))) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Reset the control if a parent era-form is reset.
+ */
+ reset() {
+ this._input0 = '';
+ this._input1 = '';
+ this._input2 = '';
+ this.shadowRoot.querySelectorAll('era-input').forEach(el => el.invalid = false);
+ this.invalid = false;
+ this.value = null;
+
+ if (this.today) {
+ this._setToday();
+ }
+ }
+
+ _inputChanged() {
+ const i0 = this._input0;
+ const i1 = this._input1;
+ const i2 = this._input2;
+
+ if (i0 === '' && i1 === '' && i2 === '' && this.optional) {
+ this.date = null;
+ this.value = null;
+ this.invalid = false;
+ return;
+ }
+
+ if (
+ i0 === '' || i1 === '' || i2 === '' ||
+ !this.inputs[0].validate() || !this.inputs[1].validate() || !this.inputs[2].validate()
+ ) {
+ if (this.focused) {
+ this.date = null;
+ this.value = null;
+ if (this.autoValidate) {
+ this.invalid = true;
+ }
+ }
+ return;
+ }
+
+ const date = parse(i0 + '-' + i1 + '-' + i2, this._inputAssign.join('-'), new Date());
+
+ if (isValid(date)) {
+ this.inputs[0].invalid = false;
+ this.inputs[1].invalid = false;
+ this.inputs[2].invalid = false;
+ this.date = date;
+ this.value = date.toISOString();
+ this.invalid = false;
+ } else {
+ this.inputs[0].invalid = true;
+ this.inputs[1].invalid = true;
+ this.inputs[2].invalid = true;
+ this.date = null;
+ this.value = null;
+ if (this.autoValidate) {
+ this.invalid = true;
+ }
+ }
+ }
+
+ _setValidator(idx) {
+ switch (this._inputAssign[idx]) {
+ case 'dd':
+ return this._validateDay;
+ case 'MM':
+ return this._validateMonth;
+ case 'y':
+ return this._validateYear;
+ }
+ }
+
+ _validateDay(value) {
+ if (typeof value !== 'string') {
+ return false;
+ }
+
+ if (/^[0-9]+$/.test(value) !== true) {
+ return false;
+ }
+
+ var v = parseInt(value, 10);
+ return v >= 1 && v <= 31;
+ }
+
+ _validateMonth(value) {
+ if (typeof value !== 'string') {
+ return false;
+ }
+
+ if (/^[0-9]+$/.test(value) !== true) {
+ return false;
+ }
+
+ var v = parseInt(value, 10);
+ return v >= 1 && v <= 12;
+ }
+
+ _validateYear(value) {
+ if (typeof value !== 'string') {
+ return false;
+ }
+
+ if (/^[0-9]+$/.test(value) !== true) {
+ return false;
+ }
+
+ var v = parseInt(value, 10);
+ return v >= 1900;
+ }
+
+ _setPlaceholder(idx) {
+ switch (this._inputAssign[idx]) {
+ case 'dd':
+ return 'DD';
+ case 'MM':
+ return 'MM';
+ case 'y':
+ return 'YYYYY';
+ }
+ }
+
+ _formatChanged(format) {
+ if (!format) return;
+
+ let _realFormat = format;
+
+ const types = ['MM', 'dd', 'y'];
+ this._inputAssign = [];
+
+ this._delimiter = this._determineDelimiter(_realFormat);
+
+ const parts = _realFormat.split(this._delimiter);
+ if (parts.length < 2) {
+ console.warn(this.tagName + ': Unknown format. Fallback to format MM-dd-y');
+ this.format = 'MM-dd-y';
+ return;
+ }
+
+ for (let i = 0; i <= 2; ++i) {
+ for (let a = 0, la = types.length; a < la; ++a) {
+ if (types[a] === parts[i] || types[a] === parts[i].toLowerCase()) {
+ parts[i] = parts[i] !== 'MM' ? parts[i].toLowerCase() : parts[i];
+ this._inputAssign.push(parts[i]);
+ }
+ }
+ }
+
+ if (this.value) {
+ this._onValueChanged();
+ }
+ }
+
+ _determineDelimiter(format) {
+ if (format.indexOf('-') > -1) {
+ return '-';
+ }
+ if (format.indexOf('/') > -1) {
+ return '/';
+ }
+ if (format.indexOf('.') > -1) {
+ return '.';
+ }
+ return '/';
+ }
+
+ // Reset invalid state if value was changed.
+ // This clears up old invalid states if the value was changed programmatically.
+ _onValueChanged() {
+ // If the control is focused we ignore a programmatically set value because
+ // the user may works with the element right now.
+ if (this.focused) {
+ return;
+ }
+
+ this.invalid = false;
+
+ if (this.value === null || this.value === undefined || this.value === 'Invalid date') {
+ this.date = null;
+ this.value = null;
+ this._input0 = '';
+ this._input1 = '';
+ this._input2 = '';
+ return;
+ }
+
+ const date = this._toDate(this.value);
+ this._input0 = format(date, this._inputAssign[0]);
+ this._input1 = format(date, this._inputAssign[1]);
+ this._input2 = format(date, this._inputAssign[2]);
+ this.date = date;
+ }
+
+ _setToday() {
+ if (this.today) {
+ setTimeout(() => {
+ if (!this.value) {
+ this.value = new Date().toISOString();
+ }
+ });
+ }
+ }
+
+ _getMaxDate(maxDate) {
+ if (typeof maxDate !== 'string') return null;
+ if (maxDate.toLowerCase() === 'today') {
+ return endOfDay(new Date());
+ }
+ return this._toDate(maxDate);
+ }
+
+ _autoMoveCursor(e) {
+ const target = e.composedPath().find(node => node.tagName === 'ERA-INPUT');
+ const idx = Array.from(this.inputs).findIndex(node => node === target);
+ if (target.value.length === 2 && idx < 2) {
+ this.inputs[idx + 1].select();
+ }
+ }
+
+ _toDate(value) {
+ if (typeof value === 'string') {
+ return parseISO(value);
+ } else {
+ return value;
+ }
+ }
+}
+
+window.customElements.define('tp-date-input', TpDateInput);
diff --git a/tp-element.js b/tp-element.js
deleted file mode 100644
index 6a92a2f..0000000
--- a/tp-element.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
-@license
-Copyright (c) 2022 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);