UNPKG

web-ui-pack

Version:
567 lines (566 loc) 19.4 kB
import MaskTextInput from "./text.mask"; import { onEvent } from "../indexHelpers"; import { WUPcssIcon } from "../styles"; import WUPBaseControl from "./baseControl"; import TextHistory from "./text.history"; const emailReg = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const tagName = "wup-text"; export default class WUPTextControl extends WUPBaseControl { #ctr = this.constructor; static get $styleRoot() { return `:root { --ctrl-autofill: #00869e; --ctrl-clear: red; --ctrl-clear-hover: rgba(255,0,0,0.1); --ctrl-label-active-pos: translateY(calc(-100% - 0.2em)) scale(0.9); } [wupdark] { --ctrl-clear-hover: rgba(255,0,0,0.2); --ctrl-autofill: #89bc55; --ctrl-autofill-caret: #fff; }`; } static get $style() { return `${super.$style} :host { cursor: text; text-align: start; } :host label > span { width: 100%; position: relative;${""} display: flex; flex-direction: row-reverse;${""} } :host input, :host textarea, :host [maskholder], :host [prefix], :host [postfix] { padding: var(--ctrl-padding); padding-left: 0; padding-right: 0; font: inherit; text-align: inherit; color: inherit; margin: 0; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } :host input, :host textarea, :host [contenteditable=true], :host [maskholder], :host [postfix] { width: 100%; box-sizing: border-box; border: none; background: none; outline: none; } :host [prefix], :host [postfix] { color: inherit; flex-shrink: 0; } :host [maskholder], :host [prefix], :host [postfix] { display: none; opacity: 0; pointer-events: none; text-overflow: initial; white-space: pre; } :host [postfix] { position: absolute; } :host [maskholder] { position: absolute; opacity: 0.65; } :host [maskholder]>i, :host [postfix]>i { visibility: hidden; font: inherit; white-space: pre; } :host input:-webkit-autofill, :host textarea:-webkit-autofill, :host [contenteditable=true]:-webkit-autofill { font: inherit; -webkit-background-clip: text; -webkit-text-fill-color: var(--ctrl-autofill); caret-color: var(--ctrl-autofill-caret, auto); } :host input:autofill, :host textarea:autofill, :host [contenteditable=true]:autofill { font: inherit; background-clip: text; text-fill-color: var(--ctrl-autofill); caret-color: var(--ctrl-autofill-caret, auto); } :host strong { display: block; position: absolute; top: 50%; left: 0; padding: 0; margin: 0; box-sizing: border-box; max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; transform-origin: top left; transform: translateY(-50%); font-weight: normal; text-decoration: none; -webkit-backface-visibility: hidden; backface-visibility: hidden; ${""} } @media not all and (prefers-reduced-motion) { :host strong { transition: top var(--anim), transform var(--anim), color var(--anim); } } :host input:not(:focus):placeholder, :host textarea:not(:focus):placeholder { color: transparent; } :host:focus-within strong, :host input:not(:placeholder-shown) + strong, :host textarea:not(:placeholder-shown) + strong, :host [contenteditable=true]:not(:empty) + strong, :host legend { transform: var(--ctrl-label-active-pos); } :host input:-webkit-autofill + strong :host textarea:-webkit-autofill + strong :host [contenteditable=true]:-webkit-autofill + strong { transform: var(--ctrl-label-active-pos); } :host input:autofill + strong, :host textarea:autofill + strong, :host [contenteditable=true]:autofill + strong { transform: var(--ctrl-label-active-pos); } :host:focus-within [maskholder], :host:focus-within [prefix], :host:focus-within [postfix], :host input:not(:placeholder-shown) ~ [prefix], :host input:not(:placeholder-shown) ~ [postfix], :host textarea:not(:placeholder-shown) ~ [prefix], :host textarea:not(:placeholder-shown) ~ [postfix] { display: inline-block; opacity: 1; } /* style for icons */ :host label:after, :host label:before { ${WUPcssIcon} cursor: pointer; -webkit-mask-image: none; mask-image: none; } :host label:after { margin-right: calc(var(--ctrl-icon-size) / -2); } :host label:before { margin-left: calc(var(--ctrl-icon-size) / -2); } :host label>button { z-index: 1; contain: strict; font-size: inherit; flex: 0 0 auto; align-self: center; } :host button[clear] { --icon-size: var(--ctrl-icon-size); --icon-hover-r: 24px; --icon-img: var(--wup-icon-cross); --icon: var(--ctrl-label); --icon-hover: var(--ctrl-clear); --icon-hover-bg: var(--ctrl-clear-hover); display: none; opacity: 0; margin-right: -0.5em; } :host button[clear=back] { --icon-img: var(--wup-icon-back); } :host:focus-within button[clear] { display: inline-block; opacity: 1; } @media (hover: hover) and (pointer: fine) { :host:hover button[clear] { display: inline-block; opacity: 1; } } :host[disabled] button[clear], :host[readonly] button[clear] { display: none; pointer-events: none; }`; } static $errorParse = __wupln("Invalid value", "validation"); static $errorMask = __wupln("Incomplete value", "validation"); static $defaults = { ...WUPBaseControl.$defaults, selectOnFocus: false, clearButton: true, validationRules: { ...WUPBaseControl.$defaults.validationRules, min: (v, setV) => (v === undefined || v.length < setV) && __wupln(`Min length is ${setV} characters`, "validation"), max: (v, setV) => (v === undefined || v.length > setV) && __wupln(`Max length is ${setV} characters`, "validation"), email: (v, setV) => setV && (!v || !emailReg.test(v)) && __wupln("Invalid email address", "validation"), }, debounceMs: 0, mask: "", maskholder: "", prefix: "", postfix: "", }; $refBtnClear; $refMaskholder; $refPrefix; $refPostfix; constructor() { super(); this.$refInput.placeholder = " "; } parse(text) { return (text || undefined); } canParseInput(_text) { return true; } parseInput(text) { return this.parse(text.trim()); } canHandleUndo() { return true; } get validations() { const vls = super.validations || {}; vls._invalidInput = (_v, c) => c._inputError || false; return vls; } renderControl() { this.$refInput.type = "text"; this.$refInput.id = this.#ctr.$uniqueId; this.$refLabel.setAttribute("for", this.$refInput.id); const s = this.$refLabel.appendChild(document.createElement("span")); s.appendChild(this.$refInput); s.appendChild(this.$refTitle); this.appendChild(this.$refLabel); this.$refInput.addEventListener("beforeinput", (e) => this.gotBeforeInput(e)); this.$refInput.addEventListener("input", (e) => this.gotInput(e), { passive: true }); } renderBtnClear() { const bc = document.createElement("button"); const span = this.$refLabel.querySelector("span"); if (span.nextElementSibling) { this.$refLabel.insertBefore(bc, span.nextElementSibling); } else { this.$refLabel.appendChild(bc); } bc.setAttribute(this.#ctr.classNameBtnIcon, ""); bc.setAttribute("clear", ""); bc.setAttribute("tabindex", -1); bc.setAttribute("aria-hidden", true); bc.setAttribute("type", "button"); onEvent(bc, "click", (e) => { e.preventDefault(); this.clearValue(); }, { passive: false }); return bc; } renderPrefix(text) { let el = this.$refPrefix; if (!text) { if (el) { el.remove(); delete this.$refPrefix; } } else { if (!el) { el = document.createElement("span"); el.setAttribute("prefix", ""); this.$refLabel.firstElementChild.append(el); this.$refPrefix = el; } el.textContent = text; } this.renderPostfix(this._opts.postfix); } renderPostfix(text) { let el = this.$refPostfix; if (!text) { if (el) { el.remove(); delete this.$refPostfix; } } else { if (!el) { el = document.createElement("span"); el.setAttribute("postfix", ""); el.appendChild(document.createElement("i")); el.append(""); this.$refLabel.firstElementChild.append(el); this.$refPostfix = el; } el.firstChild.textContent = (this.$isFocused && this.$refMaskholder?.textContent) || (this.$refPrefix?.textContent || "") + this.$refInput.value; el.lastChild.textContent = text; } } _refHistory; gotFocus(ev) { const arr = super.gotFocus(ev); if (this.canHandleUndo()) { this._refHistory ??= new TextHistory(this.$refInput); } if (!this.$refInput.readOnly) { let canSelectAll = this._opts.selectOnFocus; if (this._opts.mask) { this.maskInputProcess(null); canSelectAll &&= this.refMask.isCompleted; this.renderPostfix(this._opts.postfix); if (!canSelectAll) { const end = this.$refInput.value.length; this.$refInput.setSelectionRange(end, end); } } canSelectAll && this.$refInput.select(); } const hasOnlyNums = this.refMask?.chunks.every((c) => !c.isVar || c.pattern[0] !== "*"); this.setAttr.call(this.$refInput, "inputmode", hasOnlyNums ? "numeric" : ""); return arr; } gotFocusLost() { this.#declineInputEnd?.call(this); if (this.refMask) { if (this.refMask.prefix && this.refMask.value === this.refMask.prefix) { this.$refInput.value = ""; delete this.$refInput._maskPrev; } this.renderPostfix(this._opts.postfix); } super.gotFocusLost(); } gotChanges(propsChanged) { if (!this._opts.mask || this._opts.mask !== this.refMask?.pattern) { delete this.refMask; } if (this._opts.mask && !this._opts.maskholder && this._opts.maskholder !== false) { this.refMask ??= new MaskTextInput(this._opts.mask, ""); this._opts.maskholder = this.refMask.chunks.map((c) => c.pattern).join(""); } if ((!this._opts.maskholder || !this._opts.mask) && this.$refMaskholder) { this.$refMaskholder.remove(); delete this.$refMaskholder; } super.gotChanges(propsChanged); if (this._opts.clearButton) { this.$refBtnClear = this.$refBtnClear || this.renderBtnClear(); } else if (this.$refBtnClear) { this.$refBtnClear.remove(); this.$refBtnClear = undefined; } if (!propsChanged || propsChanged.includes("prefix")) { this.renderPrefix(this._opts.prefix); } if (!propsChanged || propsChanged.includes("postfix")) { this.renderPostfix(this._opts.postfix); } } gotKeyDown(e) { super.gotKeyDown(e); this._refHistory?.handleKeyDown(e); } _beforeSnap; gotBeforeInput(e) { const isUndoRedo = this._refHistory?.handleBeforeInput(e); this.#declineInputEnd?.call(this); this._beforeSnap = TextHistory.historyToSnapshot(this.$refInput.value, this.$refInput.selectionStart || 0); setTimeout(() => delete this._beforeSnap); if (!isUndoRedo && this._opts.mask) { this.refMask = this.refMask ?? new MaskTextInput(this._opts.mask, e.target.value); this.refMask.handleBeforeInput(e); } } _inputError; #inputTimer; gotInput(e) { const isBrowserAutofill = e.isTrusted && e.inputType == null; if (isBrowserAutofill && !this._refHistory && this.canHandleUndo()) { this._refHistory = new TextHistory(this.$refInput); } this._refHistory?.handleInput(e); const el = e.target; let txt = el.value; let errMsg; if (this._opts.mask) { const prev = el._maskPrev?.value; txt = this.maskInputProcess(e); if (txt === prev) { return; } const m = this.refMask; if (m.value !== m.prefix && !m.isCompleted) { errMsg = this.#ctr.$errorMask; } } let v = this.$value; let isParsedOrEmpty = false; if (txt) { if (this.canParseInput(txt)) { try { v = this.parseInput(txt); isParsedOrEmpty = true; } catch (err) { v = this.$value; errMsg ||= this.#ctr.$errorParse; } } } else { isParsedOrEmpty = true; v = undefined; } this.renderPostfix(this._opts.postfix); const wasErr = !errMsg && !!this._inputError; this._inputError = errMsg; const act = () => { if (this._inputError || wasErr) { this.goValidate(3); } isParsedOrEmpty && this.setValue(v, 3, true); }; this._validTimer && clearTimeout(this._validTimer); this.#inputTimer && clearTimeout(this.#inputTimer); if (this._opts.debounceMs) { this.#inputTimer = setTimeout(act, this._opts.debounceMs); } else { act(); } } refMask; maskInputProcess(e) { const el = this.$refInput; const v = el.value; const { mask } = this._opts; this.refMask = this.refMask ?? new MaskTextInput(mask, ""); const mi = this.refMask; const isFocused = this.$isFocused; if (!v && !isFocused) { el.value = v; mi.parse(v); return v; } let declinedAdd = 0; let pos = el.selectionStart || 0; if (!e) { mi.parse(v); pos = mi.value.length; } else { const r = mi.handleInput(e); declinedAdd = r.declinedAdd; pos = r.position; } if (declinedAdd) { this.declineInput(pos, mi.value); } else if (el.value !== mi.value) { el.value = mi.value; document.activeElement === el && el.setSelectionRange(pos, pos); } isFocused && this.renderMaskHolder(this._opts.maskholder, mi.leftLength - declinedAdd); return mi.value; } renderMaskHolder(text, leftLength) { if (!text) { return; } if (!this.$refMaskholder) { const m = document.createElement("span"); m.setAttribute("aria-hidden", "true"); m.setAttribute("maskholder", ""); m.appendChild(document.createElement("i")); m.append(""); this.$refInput.parentElement.prepend(m); this.$refMaskholder = m; } this.$refMaskholder.firstChild.textContent = (this._opts.prefix || "") + this.$refInput.value; this.$refMaskholder.lastChild.textContent = text.substring(text.length - leftLength); } #declineInputEnd; declineInput(nextCaret, nextValue) { if (!this._beforeSnap) { return; } const { v, pos } = TextHistory.historyFromSnapshot(this._beforeSnap); nextCaret ??= pos; nextValue ??= v; delete this._beforeSnap; const isChanged = v !== nextValue; isChanged && nextValue != null && this._refHistory?.save(v, nextValue); this.#declineInputEnd = () => { this.#declineInputEnd = undefined; clearTimeout(t); this.$refInput.value = nextValue; this.$refInput.setSelectionRange(nextCaret, nextCaret); this.refMask && this.renderMaskHolder(this._opts.maskholder, this.refMask.leftLength); this.renderPostfix(this._opts.postfix); v === nextValue && this._refHistory?.removeLast(); }; const t = setTimeout(this.#declineInputEnd, 100); } setValue(v, reason, skipInput = false) { !skipInput && this.setInputValue(v, reason); const isChanged = super.setValue(v, reason); this._isValid !== false && this.goHideError(); return isChanged; } setInputValue(v, reason) { this._inputError && this.goHideError(); delete this._inputError; const prev = this.$refInput.value; const str = v != null ? v.toString() : ""; this.$refInput.value = str; if (this.$isReady) { this._opts.mask && this.maskInputProcess(null); str !== prev && this._refHistory?.save(prev, this.$refInput.value); this.renderPostfix(this._opts.postfix); } } setClearState() { const next = super.setClearState(); if (this.$refBtnClear) { this.$refBtnClear.setAttribute("clear", this.#ctr.$isEmpty(next) ? "" : "back"); } return next; } focus() { if (this.$isDisabled) { return false; } this.$refInput.focus(); return document.activeElement === this.$refInput; } } customElements.define(tagName, WUPTextControl);