From ebc7a2f8cfaeb253920ec8d9bdcbde0751726a35 Mon Sep 17 00:00:00 2001 From: trading_peter Date: Fri, 11 Mar 2022 23:35:49 +0100 Subject: [PATCH] Initial version --- .editorconfig | 21 +++ .gitnore | 1 + package.json | 15 ++ tp-input.js | 465 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 502 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitnore create mode 100644 package.json create mode 100644 tp-input.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c2cdfb8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitnore b/.gitnore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.gitnore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..811f29a --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "tp-input", + "version": "1.0.0", + "description": "", + "main": "tp-input.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://gitea.codeblob.work/tp-elements/tp-input.git" + }, + "author": "trading_peter", + "license": "Apache-2.0" +} diff --git a/tp-input.js b/tp-input.js new file mode 100644 index 0000000..942fcd4 --- /dev/null +++ b/tp-input.js @@ -0,0 +1,465 @@ +/** +@license +Copyright (c) 2022 trading_peter +This program is available under Apache License Version 2.0 +*/ + +import { LitElement, html, css } from 'lit'; +import { FormElement } from '../helpers/form-element.js'; +import { EventHelpers } from '../helpers/event-helpers.js'; +import { ControlState } from '../helpers/control-state.js'; +import { Inert } from '../helpers/inert.js'; + +const mixins = [ + FormElement, + EventHelpers, + ControlState, + Inert +]; + +const BaseElement = mixins.reduce((baseClass, mixin) => { + return mixin(baseClass); +}, LitElement); + +class TpInput extends BaseElement { + static get styles() { + return [ + css` + :host { + display: block; + position: relative; + outline: none; + font-size: 14px; + } + + .wrap ::slotted(input) { + outline: none; + box-shadow: none; + padding: 0; + width: 100%; + min-width: 0; /** Because of FF **/ + background: transparent; + border: none; + font-family: inherit; + font-size: inherit; + text-align: inherit; + color: inherit; /** FF seems to need this **/ + } + + .error-message { + position: absolute; + z-index: 1; + left: 0; + right: 0; + font-size: 10px; + color: var(--tp-input-text-color-invalid, #B71C1C); + transition: opacity 0.3s; + opacity: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + } + + :host([invalid]) .error-message { + opacity: 1; + } + + .wrap { + display: flex; + flex-direction: row; + padding: 5px; + border-radius: 2px; + border: solid 1px #000; + } + + .prefix, .suffix { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + } + + .prefix ::slotted([slot="prefix"]) { + padding-right: 5px; + } + + .suffix ::slotted([slot="suffix"]) { + padding-left: 5px; + } + ` + ]; + } + + render() { + const { errorMessage } = this; + + return html` +
+
+ +
+ +
+ +
+
+ ${errorMessage ? html` +
${errorMessage}
+ ` : null} + `; + } + + static get properties() { + return { + // The value for this element. + value: { type: String }, + + // If true the control can't be edited. + disabled: { type: Boolean, reflect: true }, + + // If true, something invalid was entered. + invalid: { type: Boolean, reflect: true }, + + /* + * Force invalid state no matter what. + * Useful if the input must be invalid even if the value itself would be valid. + * For example: Event if a valid email address was entered, an external test that makes a DNS check for the domain may fail. + * In this case we still want to force the invalid state. + */ + forceInvalid: { type: Boolean }, + + /* + * Regex pattern to live check the input. + * Invalid input is blocked and never shown. + * If you wan't live validation without blocking input use `pattern` and `auto-validate`. + */ + allowedPattern: { type: String }, + + /* + * Error message to show if the value is invalid. + */ + errorMessage: { type: String }, + + /* + * Validate while the control receives input. + */ + autoValidate: { type: Boolean }, + + /* + * A custom validator function for checking the value. + */ + validator: { type: Object }, + + /* + * Query selector to another input element. + * The input's value must the be equal to the other input in order to be valid. + */ + equalTo: { type: String }, + + type: { type: String }, + + optional: { type: Boolean }, + + readonly: { type: Boolean, reflect: true }, + + /* @private */ + _previousValidInput: { type: String, value: '' } + }; + } + + constructor() { + super(); + this.value = ''; + } + + get inputEl() { + const slot = this.shadowRoot.querySelector('#content'); + return slot.assignedNodes({ flatten: true }).filter(n => n.tagName === 'INPUT')[0]; + } + + disconnectedCallback() { + super.disconnectedCallback(); + + this.unlisten(this._equalToTarget, 'input', '_onInput'); + } + + firstUpdated() { + super.firstUpdated(); + + if (!this.inputEl) { + console.warn(this.tagName + ': Cannot find input child!'); + return; + } + + this.listen(this.inputEl, 'input', '_onInput'); + this.listen(this.inputEl, 'keypress', '_onKeypress'); + + if (this.value !== '' && this.value !== undefined && this.inputEl.value === '') { + this._onInput(); // Force validation + } + + if (this.name === undefined && this.inputEl.name) { + this.name = this.inputEl.name; + } + + if (this.inputEl.name) { + console.warn(this.tagName + ': Can\'t have a name on the inner input.'); + this.inputEl.removeAttribute('name'); + } + + if (this.value === undefined) { + this.value = this.inputEl.value; + } + + if (this.equalTo) { + this.listen(this._equalToTarget, 'input', '_onInput'); + } + } + + updated(changes) { + if (changes.has('optional')) { + this._optionalChanged(changes.get('optional')); + } + + if (changes.has('forceInvalid')) { + this._forceInvalidChanged(); + } + + if (changes.has('value')) { + this._syncValue(); + } + } + + get _equalToTarget() { + if (this._eqTarget) { + return this._eqTarget; + } + + // Make sure the input wants a `equalTo` target. + if (!this.equalTo) { + return; + } + + const root = this.getRootNode(); + this._eqTarget = root.querySelector(this.equalTo) || root.host.querySelector(this.equalTo); + if (!this._eqTarget || this._eqTarget.value == undefined) { + console.warn(this.tagName + ': Unable to find element to match against or target doesn\'t have a value property.', this); + } + return this._eqTarget; + } + + get _valueRegEx() { + if (this.allowedPattern) { + return new RegExp(this.allowedPattern); + } else { + switch (this.type) { + case 'number': { + return /[0-9.,e-]/; + } + } + } + } + + _onInput() { + this._inputWasChanged = true; + if (this.allowedPattern && !this._patternAlreadyChecked) { + const valid = this._checkPatternValidity(); + if (!valid) { + this.inputEl.value = this._previousValidInput; + } + } + + this.value = this._previousValidInput = this.inputEl.value; + this._patternAlreadyChecked = false; + + if (this.autoValidate) { + this.validate(); + } + } + + select() { + this.inputEl.select(); + } + + _onKeypress(e) { + // Submit form if `Enter` key is pressed. + if (e.keyCode === 13 && this._parentForm) { + this._parentForm.submit(); + e.preventDefault(); + return; + } + + if (!this.allowedPattern && this.type !== 'number') { + return; + } + + const regex = this._valueRegEx; + if (!regex) { + return; + } + + // Handle special keys and backspace + if (e.metaKey || e.ctrlKey || e.altKey) { + return; + } + + // Check the pattern either here or in `_onInput`, but not in both. + this._patternAlreadyChecked = true; + + const thisChar = String.fromCharCode(e.charCode); + if (this._isPrintable(e) && !regex.test(thisChar)) { + e.preventDefault(); + this._announceInvalidCharacter('Invalid character ' + thisChar + ' not entered.'); + } + } + + _checkPatternValidity() { + const regex = this._valueRegEx; + if (!regex) { + return true; + } + for (let i = 0; i < this.inputEl.value.length; i++) { + if (!regex.test(this.inputEl.value[i])) { + return false; + } + } + return true; + } + + _isPrintable(e) { + // What a control/printable character is varies wildly based on the browser. + // - most control characters (arrows, backspace) do not send a `keypress` event + // in Chrome, but the *do* on Firefox + // - in Firefox, when they do send a `keypress` event, control chars have + // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) + // - printable characters always send a keypress event. + // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the keyCode + // always matches the charCode. + // None of this makes any sense. + + // For these keys, ASCII code == browser keycode. + const anyNonPrintable = + (e.keyCode == 8) || // backspace + (e.keyCode == 9) || // tab + (e.keyCode == 13) || // enter + (e.keyCode == 27); // escape + + // For these keys, make sure it's a browser keycode and not an ASCII code. + const mozNonPrintable = + (e.keyCode == 19) || // pause + (e.keyCode == 20) || // caps lock + (e.keyCode == 45) || // insert + (e.keyCode == 46) || // delete + (e.keyCode == 144) || // num lock + (e.keyCode == 145) || // scroll lock + (e.keyCode > 32 && e.keyCode < 41) || // page up/down, end, home, arrows + (e.keyCode > 111 && e.keyCode < 124); // fn keys + + return !anyNonPrintable && !(e.charCode == 0 && mozNonPrintable); + } + + _syncValue() { + if (this.inputEl === undefined) return; + + if (this.inputEl.value !== this.value) { + this.inputEl.value = this.value === undefined || this.value === null ? '' : this.value; + } + } + + _forceInvalidChanged() { + if (!this.autoValidate || !this.inputEl || !this._inputWasChanged) return; + + if (this.forceInvalid) { + this.invalid = true; + } + + if (!this.forceInvalid) { + this.invalid = this.validate(); + } + } + + /** + * Validate the controls value. + */ + validate() { + if (this.forceInvalid) { + this.invalid = true; + return false; + } + + const valueIsFalsy = this.value === null || this.value === undefined || this.value === ''; + + // Run native validation first. + let valid = this.inputEl.checkValidity(); + + // Then check if control is optional. If so and the value is falsy, assume valid. + if (this.optional && valueIsFalsy) { + valid = true; + } else if (this.required && valueIsFalsy) { + valid = false; + } else if (typeof this.validator === 'function' && !this.validator(this, this.value)) { + valid = false; + } + + if (this.equalTo && this._equalToTarget && this.value !== this._equalToTarget.value) { + valid = false; + } + + this.invalid = !valid; + + return valid; + } + + // Reset invalid state if value was changed. + // This clears up old invalid states if the value was changed programmatically. + _onValueChanged() { + this.invalid = false; + } + + focus() { + this.inputEl.focus(); + } + + /** + * Reset the control if a parent era-form is reset. + */ + reset() { + this.invalid = false; + } + + _equalToChanged() { + this.required = Boolean(this.equalTo); + } + + // If the control is dynamically set completely optional, clear invalid state. + _requiredChanged(newValue, oldValue) { + if (oldValue !== undefined && (this.invalid || this.autoValidate)) { + this.validate(); + } + } + + _syncReadonly() { + if (this.inputEl) { + if (this.readonly) { + this.inputEl.setAttribute('readonly', ''); + } else { + this.inputEl.removeAttribute('readonly'); + } + } + } + + _announceInvalidCharacter(message) { + this.fire('iron-announce', { text: message }); + } + + _optionalChanged(oldValue) { + if (oldValue === undefined) return; + + if (this.required === true && this.autoValidate === true) { + this.validate(); + } + } +} + +window.customElements.define('tp-input', TpInput); \ No newline at end of file