web-ui-pack
Version:
Web package with UI elements
567 lines (566 loc) • 19.4 kB
JavaScript
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; ${""}
}
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;
}
(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);