UNPKG

web-ui-pack

Version:
835 lines (834 loc) 28.7 kB
import WUPBaseElement from "../baseElement"; import WUPFormElement from "../formElement"; import isEqual from "../helpers/isEqual"; import nestedProperty from "../helpers/nestedProperty"; import onFocusLostEv from "../helpers/onFocusLost"; import onFocusGot from "../helpers/onFocusGot"; import { stringPrettify } from "../helpers/string"; import WUPPopupElement from "../popup/popupElement"; import { WUPcssIcon } from "../styles"; export var SetValueReasons; (function (SetValueReasons) { SetValueReasons[SetValueReasons["manual"] = 1] = "manual"; SetValueReasons[SetValueReasons["clear"] = 2] = "clear"; SetValueReasons[SetValueReasons["userInput"] = 3] = "userInput"; SetValueReasons[SetValueReasons["userSelect"] = 4] = "userSelect"; SetValueReasons[SetValueReasons["initValue"] = 5] = "initValue"; SetValueReasons[SetValueReasons["storage"] = 6] = "storage"; })(SetValueReasons || (SetValueReasons = {})); export var ValidationCases; (function (ValidationCases) { ValidationCases[ValidationCases["none"] = 0] = "none"; ValidationCases[ValidationCases["onChangeSmart"] = 1] = "onChangeSmart"; ValidationCases[ValidationCases["onChange"] = 2] = "onChange"; ValidationCases[ValidationCases["onFocusLost"] = 4] = "onFocusLost"; ValidationCases[ValidationCases["onFocusWithValue"] = 8] = "onFocusWithValue"; ValidationCases[ValidationCases["onInit"] = 16] = "onInit"; })(ValidationCases || (ValidationCases = {})); export var ClearActions; (function (ClearActions) { ClearActions[ClearActions["clear"] = 0] = "clear"; ClearActions[ClearActions["clearBack"] = 1] = "clearBack"; ClearActions[ClearActions["initClearBack"] = 2] = "initClearBack"; })(ClearActions || (ClearActions = {})); export var ValidateFromCases; (function (ValidateFromCases) { ValidateFromCases[ValidateFromCases["onInit"] = 0] = "onInit"; ValidateFromCases[ValidateFromCases["onFocus"] = 1] = "onFocus"; ValidateFromCases[ValidateFromCases["onFocusLost"] = 2] = "onFocusLost"; ValidateFromCases[ValidateFromCases["onChange"] = 3] = "onChange"; ValidateFromCases[ValidateFromCases["onSubmit"] = 4] = "onSubmit"; ValidateFromCases[ValidateFromCases["onManualCall"] = 5] = "onManualCall"; })(ValidateFromCases || (ValidateFromCases = {})); export default class WUPBaseControl extends WUPBaseElement { #ctr = this.constructor; static $ariaCleared = __wupln("cleared", "aria"); static $ariaError = __wupln("Error for", "aria"); static get $styleRoot() { return `:root { --ctrl-padding: 1.5em 1em 0.5em 1em; --ctrl-focus: var(--base-focus); --ctrl-focus-label: var(--ctrl-focus); --ctrl-selected: var(--ctrl-focus); --ctrl-label: #5e5e5e; --ctrl-icon: var(--ctrl-label); --ctrl-icon-size: 1em; --ctrl-text: inherit; --ctrl-bg: #fff; --ctrl-border: #e6e6e6; --ctrl-border-radius: var(--border-radius); --ctrl-err: #ad0000; --ctrl-err-bg: #fff4fa; --ctrl-err-valid: green; --ctrl-invalid-border: red; } [wupdark] { --ctrl-bg: none; --ctrl-border: #104652; --ctrl-label: #919191; --ctrl-icon: var(--ctrl-label); --ctrl-focus-label: #25a1b6; --ctrl-selected: #25a1b6; --ctrl-err: #ff6666; --ctrl-err-bg: #000; --ctrl-err-valid: #54d754; }`; } static get $style() { return `${super.$style} :host { --ctrl-focus-border: var(--ctrl-focus); contain: style; display: block; flex: 1; margin: var(--base-margin) 0; box-shadow: 0 0 0 1px var(--ctrl-border); border-radius: var(--ctrl-border-radius); color: var(--ctrl-text); background: var(--ctrl-bg); cursor: pointer; -webkit-tap-highlight-color: transparent;${""} } :host strong, :host legend { color: var(--ctrl-label); pointer-events: none; } :host[invalid], :host[invalid] > [menu] { --ctrl-focus-border: var( --ctrl-invalid-border); box-shadow: 0 0 3px 0 var(--ctrl-focus-border); } :host:focus-within, :host:focus-within > [menu] { box-shadow: 0 0 2px 1px var(--ctrl-focus-border); } :host:focus-within strong, :host:focus-within legend { color: var(--ctrl-focus-label); pointer-events: initial; } [disabled] :host, :host[disabled] { opacity: 0.6; cursor: not-allowed; -webkit-user-select: none; user-select: none; } :host[busy] { opacity: 0.8; cursor: inherit; -webkit-user-select: none; user-select: none; } [disabled] :host > label, :host[disabled] > label, :host[busy] > label { pointer-events: none; } :host label { display: flex; align-items: center; box-sizing: border-box; font: inherit; cursor: inherit; padding: var(--ctrl-padding); padding-top: 0; padding-bottom: 0; } :host input, :host textarea { padding: 0; margin: 0; cursor: inherit; } :host [contenteditable=true] { margin: var(--ctrl-padding); margin-left: 0; margin-right: 0; cursor: inherit; } :host strong { cursor: inherit; } :host strong:empty { display: none; } :host [aria-required=true] + strong:after, :host fieldset[aria-required=true] > legend:after { content: "*"; font-size: larger; font-weight: bolder; line-height: 0; } :host [error] { cursor: pointer; font-size: small; color: var(--ctrl-err); background: var(--ctrl-err-bg); overflow: auto; overflow: overlay; // text-shadow: 0 0 0 var(--ctrl-err); } :host [error] ul { margin:0; padding:2px 4px 2px; } :host [error] li { display: flex; align-items: center; margin-left: -5px; } :host [error] li:before { content: ''; --ctrl-icon-img: var(--wup-icon-dot); --ctrl-icon: var(--ctrl-err); ${WUPcssIcon} } :host [error] li[valid] { color: var(--ctrl-err-valid); } :host [error] li[valid]:before { content: ''; --ctrl-icon-img: var(--wup-icon-check); --ctrl-icon: var(--ctrl-err-valid); } :host:focus-within [error] { max-height: none; } @media (hover: hover) and (pointer: fine) { :host:hover strong, :host[hovered] strong{ color: var(--ctrl-focus-label); } :host:hover, :host:hover>[menu], :host[hovered], :host[hovered]>[menu] { box-shadow: 0 0 2px 1px var(--ctrl-focus-border); } } @media not all and (prefers-reduced-motion) { :host { transition: box-shadow var(--anim); } }`; } static $isEqual(v1, v2, control) { return isEqual(v1, v2) || (v1 != null && v1.id != null && v2 != null && v1.id === v2.id); } static $isEmpty(v) { return v === "" || v === undefined; } static get observedAttributes() { const a = super.observedAttributes; a.push("w-initvalue", "disabled", "readonly"); return a; } static get mappedAttributes() { const m = super.mappedAttributes; m.autocomplete.type = 2; m.label.type = 2; m.name.type = 2; m.initvalue = { type: 5, parse: (v) => v, }; return m; } static $defaults = { autoComplete: null, autoFocus: false, clearActions: 2, focusDebounceMs: 100, validateDebounceMs: 500, validationCase: 1 | 4 | 8, validationRules: { required: (v, setV) => setV === true && this.$isEmpty(v) && __wupln("This field is required", "validation"), }, validations: null, validationShowAll: false, disabled: false, readOnly: false, label: null, name: null, storage: "local", storageKey: "", }; static cloneDefaults() { const d = super.cloneDefaults(); d.validationRules = undefined; return d; } $onChange; #value; get $value() { return this.#value; } set $value(v) { this.setValue(v, 1); } #initValue; get $initValue() { return this.#initValue; } set $initValue(v) { const was = this.#initValue; const canUpdate = (!this.$isReady && this.$value === undefined) || (!this.$isDirty && !this.$isChanged); this.#initValue = v; let needUpdate = !this.#ctr.$isEqual(v, was, this); if (canUpdate && needUpdate) { needUpdate = !this.setValue(v, 5); } needUpdate && this.setClearState(); if (!this._noDelInitValueAttr) { this._isStopChanges = true; this.removeAttribute("w-initvalue"); this._isStopChanges = false; } } #isDirty = false; get $isDirty() { return this.#isDirty; } set $isDirty(v) { this.#isDirty = v; } get $isEmpty() { return this.#ctr.$isEmpty(this.#value); } get $isChanged() { return !this.#ctr.$isEqual(this.$value, this.#initValue, this); } _isValid; get $isValid() { if (this._isValid == null) { this.goValidate(0, true); } return this._isValid; } get $isDisabled() { const o = this.$form?._opts; return o?.disabled || this._opts.disabled || false; } get $isReadOnly() { const o = this.$form?._opts; return o?.readOnly || this._opts.readOnly || false; } get $isRequired() { return !!this.validations?.required; } get $autoComplete() { const o = this.$form?._opts; const af = this._opts.autoComplete ?? (o?.autoComplete || false); return (af === true ? this._opts.name : af) || false; } $validate(silent = false) { return this.goValidate(5, silent); } validateBySubmit() { return this.goValidate(4); } $showError(err) { return this.goShowError(err, this.$refInput); } $hideError() { return this.goHideError(); } #refDetails; $ariaDetails(text) { let el = this.#refDetails; if (!text) { if (el) { el.remove(); this.#refDetails = undefined; } } else { if (!el) { el = document.createElement("span"); el.className = this.#ctr.classNameHidden; this.#refDetails = this.$refTitle.parentElement.appendChild(el); } el.textContent = text; } } $ariaSpeak(text, delayMs = 100) { const el = document.createElement("section"); el.setAttribute("aria-live", "off"); el.setAttribute("aria-atomic", true); el.className = this.#ctr.classNameHidden; el.id = this.#ctr.$uniqueId; const i = this.$refInput; const an = "aria-describedby"; i.setAttribute(an, `${i.getAttribute(an) || ""} ${el.id}`.trimStart()); this.appendChild(el); setTimeout(() => (el.textContent = text), delayMs); setTimeout(() => { el.remove(); const a = i.getAttribute(an); if (a != null) { const aNext = a.replace(el.id, "").replace(" ", " ").trim(); aNext ? i.setAttribute(an, aNext) : i.removeAttribute(an); } }, 500); } $form; $refLabel = document.createElement("label"); $refInput = document.createElement("input"); $refTitle = document.createElement("strong"); $refError; gotChanges(propsChanged) { super.gotChanges(propsChanged); const i = this.$refInput; const label = (this._opts.label ?? (this._opts.name && __wupln(stringPrettify(this._opts.name), "content"))) || null; this.$refTitle.textContent = label; const n = !label && this._opts.name ? __wupln(stringPrettify(this._opts.name), "aria") : null; this.setAttr.call(i, "aria-label", n); this.setAttr("disabled", this._opts.disabled, true); this.setAttr("readonly", this._opts.readOnly, true); const isReq = this.$isRequired; this.setAttr("required", isReq, true); this.setAttr.call(this.$refInput, "aria-required", isReq); this.setupInitValue(propsChanged); propsChanged?.includes("clearActions") && this.setClearState(); this.gotFormChanges(propsChanged); } gotFormChanges(propsChanged) { this.setupInput(); this.$refError && !this.canShowError && this.goHideError(); } setupInput() { const i = this.$refInput; i.disabled = this.$isDisabled; i.autocomplete = this.$autoComplete || "off"; this.setupInputReadonly(); } setupInputReadonly() { this.$refInput.readOnly = this.$isReadOnly; } setupInitValue(propsChanged) { if (!propsChanged || propsChanged.includes("initvalue")) { const attr = this._opts.initvalue; if (attr != null) { this._noDelInitValueAttr = true; try { this.$initValue = this.parse(attr); } catch (err) { this.throwError(err); } delete this._noDelInitValueAttr; } else if (propsChanged) { this.$initValue = undefined; } } if (this.$initValue === undefined && this.$form && this._opts.name && !this.hasAttribute("initvalue")) { if (!propsChanged || propsChanged.includes("name")) { this.$initValue = nestedProperty.get(this.$form._initModel, this._opts.name); } } if (!propsChanged && this._opts.storageKey) { this.setValue(this.storageGet(), 6); } } get canShowError() { return !this.$isDisabled; } gotReady() { super.gotReady(); const onFocusGotHandler = (e) => { const arr = this.gotFocus(e); const r = onFocusLostEv(this, () => { this.gotFocusLost(); arr.forEach((f) => f()); r(); this.disposeLst.splice(this.disposeLst.indexOf(r), 1); }); this.disposeLst.push(r); }; this.$isFocused && setTimeout(onFocusGotHandler); const r = onFocusGot(this, onFocusGotHandler, { debounceMs: this._opts.focusDebounceMs }); this.disposeLst.push(r); this.appendEvent(this, "mousedown", (e) => { if (this.$isDisabled) { return; } const el = e.target; !(el.tagName === "INPUT" || el.tagName === "TEXTAREA" || this.$refInput === el || this.includes.call(this.$refInput, el)) && e.preventDefault(); this.appendEvent(this, "mouseup", () => !this.$isFocused && this.focus(), { once: true }); }, { passive: false }); if (this._opts.validationCase & 16) { !this.$isEmpty && this.goValidate(0); } } gotRender() { this.renderControl(); } connectedCallback() { delete this._errName; delete this._errMsg; delete this._wasValidNotEmpty; super.connectedCallback(); this.$form = WUPFormElement.$tryConnect(this); this.#isDirty = false; } gotRemoved() { super.gotRemoved(); this.$form?.$controls.splice(this.$form.$controls.indexOf(this), 1); } get validations() { const def = this.#ctr.$defaults.validations; const opt = this._opts.validations; return def ? { ...def, ...opt } : opt; } get validationsRules() { const vls = this.validations; if (!vls) { return []; } const rules = this.#ctr.$defaults.validationRules; const check = (v, k, reason) => { const vl = vls[k]; let err; if (typeof vl === "function") { err = vl(v, this, reason); } else { const r = rules[k]; if (!r) { const n = this._opts.name ? `.[${this._opts.name}]` : ""; throw new Error(`${this.tagName}${n}. Validation rule [${k}] is not found`); } err = r.call(this, v, vl, this, reason); } if (err !== false) { return err; } return false; }; const self = this; const arr = Object.keys(vls) .sort((k1, k2) => { const p1 = k1[0] === "_" ? 1 : k1 === "required" ? 2 : 3; const p2 = k2[0] === "_" ? 1 : k2 === "required" ? 2 : 3; return p1 - p2; }) .map((key) => ({ [key]: (v, reason) => check.call(self, v, key, reason) }[key])); return arr; } _errName; _wasValidNotEmpty; _validTimer; goValidate(fromCase, silent = false) { this._errName = undefined; const vls = this.validationsRules; if (!vls.length) { this._isValid = true; if (!this.$isEmpty) { this._wasValidNotEmpty = true; } return false; } const v = this.$value; let errMsg = ""; const isEmpty = this.$isEmpty; this._isValid = !vls.some((fn) => { const skipRule = isEmpty && fn.name[0] !== "_" && fn.name !== "required"; const err = !skipRule && fn(v, fromCase); if (err) { this._errName = fn.name; errMsg = err; return true; } return false; }); this._validTimer && clearTimeout(this._validTimer); let canShowError = (!silent || this.$refError) && this.canShowError; if (fromCase === 3 && this._opts.validationCase & 1) { if (errMsg && !this._wasValidNotEmpty && !this.$refError) { canShowError = false; } } if (this._isValid && !isEmpty) { this._wasValidNotEmpty = true; } if (errMsg) { if (canShowError) { const waitMs = this.$refError || fromCase !== 3 ? 0 : this._opts.validateDebounceMs; const saved = this._errName; const act = () => { this.goShowError(errMsg, this.$refInput); this._errName = saved; }; this._validTimer = waitMs ? setTimeout(act, waitMs) : (act(), undefined); } return errMsg; } this.goHideError(); return false; } renderValidations(parent, skipRules = ["required"]) { const vls = this.validationsRules.filter((vl) => !skipRules.includes(vl.name) && vl.name[0] !== "_"); if (!vls.length) { return; } let p = parent; if (!p._wupVldItems) { p = parent; p._wupVldItems = []; const ul = p.appendChild(document.createElement("ul")); ul.setAttribute("aria-hidden", true); for (let i = 0; i < vls.length; ++i) { const li = ul.appendChild(document.createElement("li")); li._wupVld = vls[i]; p._wupVldItems.push(li); const err = vls[i](undefined, null); if (err) { li.textContent = err; } else { setTimeout(() => { const n = this._opts.name ? `.[${this._opts.name}]` : ""; throw new Error(`${this.tagName}${n}. Can't get error message for validationRule [${vls[i].name}]. Calling rule with value [undefined] must return error message`); }); } } } const v = this.$value; p._wupVldItems.forEach((li) => this.setAttr.call(li, "valid", li._wupVld(v, null) === false, true)); } renderError() { const p = document.createElement("wup-popup"); p.$options.openCase = 1; p.$options.target = this; p.$options.placement = [ WUPPopupElement.$placements.$bottom.$start.$resizeWidth, WUPPopupElement.$placements.$top.$start.$resizeWidth, ]; p.$options.offset = [-2, 0]; p.setAttribute("error", ""); p.setAttribute("aria-live", "off"); p.setAttribute("aria-atomic", true); p.id = this.#ctr.$uniqueId; const hiddenLbl = p.appendChild(document.createElement("span")); hiddenLbl.className = this.#ctr.classNameHidden; p.appendChild(document.createElement("span")); this.$refError = this.appendChild(p); return p; } _errMsg; #refErrTarget; goShowError(err, target) { if (!err) { this.throwError("Error message missed"); return; } if (this._errMsg === err) { return; } if (!this.isConnected) { return; } this._errName = undefined; this._errMsg = err; if (!this.$refError) { this.$refError = this.renderError(); } this._opts.validationShowAll && this.renderValidations(this.$refError); this.#refErrTarget = target; target.setCustomValidity?.call(target, err); this.setAttribute("invalid", ""); const lbl = this.$refTitle.textContent; this.$refError.firstElementChild.textContent = lbl ? `${this.#ctr.$ariaError} ${lbl}:` : ""; const el = this.$refError.children.item(1); el.textContent = err; const renderedErr = this.$refError._wupVldItems?.find((li) => li.textContent === err); this.setAttr.call(el, "class", renderedErr ? this.#ctr.classNameHidden : null); target.setAttribute("aria-describedby", this.$refError.id); if (!this.$isFocused) { this.$refError.setAttribute("role", "alert"); renderedErr?.scrollIntoView(); } } goHideError() { this._errMsg = undefined; if (this.$refError) { const p = this.$refError; p.addEventListener("$close", p.remove, { passive: true, once: true }); p.$close(); this.$refError = undefined; this.#refErrTarget.setCustomValidity?.call(this.#refErrTarget, ""); this.#refErrTarget.removeAttribute("aria-describedby"); this.removeAttribute("invalid"); this.#refErrTarget = undefined; } } valueFromStorage(str) { return str === "null" ? null : this.parse(str); } valueToStorage(v) { if (v == null) { return "null"; } if (typeof v === "object") { this.throwError(new Error("Option `storageKey` with object-values are not supported. Override `valueToStorage` & `valueFromStorage` to fix it")); return null; } return v.toString(); } get storageKey() { return this._opts.storageKey === true ? this._opts.name : this._opts.storageKey; } storageGet() { const key = this.storageKey; if (key) { let v; switch (this._opts.storage) { case "session": v = window.sessionStorage.getItem(key); break; case "url": v = new URLSearchParams(window.location.search).get(decodeURIComponent(key).replace(/\+/g, " ")); v = v != null ? decodeURIComponent(v) : v; break; default: v = window.localStorage.getItem(key); break; } if (v != null) { return this.valueFromStorage(v); } } return this.$initValue; } storageSet(v) { const key = this.storageKey; if (!key) { return; } try { let strg; switch (this._opts.storage) { case "session": strg = window.sessionStorage; break; case "url": { const url = new URL(window.location.href); strg = { removeItem: (k) => { url.searchParams.delete(k); window.history.replaceState(null, "", url); }, setItem: (k, val) => { url.searchParams.set(k, val); window.history.replaceState(null, "", url); }, }; } break; default: strg = window.localStorage; break; } if (this.#ctr.$isEmpty(v)) { strg.removeItem(key); } else { const sv = this.valueToStorage(v); sv === null ? strg.removeItem(key) : strg.setItem(key, sv); } } catch (err) { this.throwError(err); } } setValue(v, reason) { const prev = this.#value; this.#value = v; if (!this.$isReady) { this.setClearState(); return null; } if (reason !== 5 && reason !== 1 && reason !== 6) { this.$isDirty = true; } const isChanged = !this.#ctr.$isEqual(v, prev, this); if (!isChanged) { return false; } this.setClearState(); this._isValid = undefined; const canVld = reason !== 1; (canVld || this.$refError) && this.validateAfterChange(); if (reason !== 5) { reason !== 6 && this._opts.storageKey && this.storageSet(v); setTimeout(() => this.fireEvent("$change", { cancelable: false, bubbles: true, detail: { reason } })); } return true; } validateAfterChange() { const c = this._opts.validationCase; if (c & 2 || c & 1) { this.goValidate(3); } } _nextClearValue; setClearState() { const clr = this._opts.clearActions; switch (clr) { case 1: if (!this.$isEmpty) { delete this._nextClearValue; } break; case 2: if (this.$isChanged) { this._nextClearValue = this.$initValue; } else if (!this.$isEmpty) { delete this._nextClearValue; } break; default: delete this._nextClearValue; break; } return this._nextClearValue; } clearValue() { const next = this._nextClearValue; this._nextClearValue = this.#value; this.setValue(next, 2); this.$isEmpty ? this.$ariaSpeak(this.#ctr.$ariaCleared) : this.$refInput.select(); } gotFocus(ev) { this.$refError?.$refresh(); const c = this._opts.validationCase; if (c & 8 && !this.$isEmpty) { this.goValidate(1); } else if (this._wasValidNotEmpty == null && c & 1) { this.goValidate(3, true); } const r = this.appendEvent(this, "keydown", (e) => !this.$isDisabled && !this.$isReadOnly && this.gotKeyDown(e), { passive: false, }); return [r]; } gotFocusLost() { if (this._opts.validationCase & 4) { this.goValidate(2); } } gotKeyDown(e) { const canClear = !e.defaultPrevented && e.key === "Escape" && !e.shiftKey && !e.altKey && !e.ctrlKey; canClear && setTimeout(() => !e.defaultPrevented && this.clearValue()); } }