web-ui-pack
Version:
Web package with UI elements
835 lines (834 loc) • 28.7 kB
JavaScript
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());
}
}