web-ui-pack
Version:
Web package with UI elements
433 lines (432 loc) • 15.1 kB
JavaScript
import WUPBaseElement from "./baseElement";
import { nestedProperty, promiseWait, scrollIntoView } from "./indexHelpers";
import { WUPcssButton } from "./styles";
export var SubmitActions;
(function (SubmitActions) {
SubmitActions[SubmitActions["none"] = 0] = "none";
SubmitActions[SubmitActions["goToError"] = 1] = "goToError";
SubmitActions[SubmitActions["validateUntilFirst"] = 2] = "validateUntilFirst";
SubmitActions[SubmitActions["validateChangeable"] = 4] = "validateChangeable";
SubmitActions[SubmitActions["collectChanged"] = 8] = "collectChanged";
SubmitActions[SubmitActions["reset"] = 16] = "reset";
SubmitActions[SubmitActions["lockOnPending"] = 32] = "lockOnPending";
})(SubmitActions || (SubmitActions = {}));
const tagName = "wup-form";
const formStore = [];
export default class WUPFormElement extends WUPBaseElement {
#ctr = this.constructor;
static get $styleRoot() {
return `:root {
--btn-submit-text: var(--base-btn-text);
--btn-submit-bg: var(--base-btn-bg);
--btn-submit-focus: var(--base-btn-focus);
}
[wupdark] {
--btn-submit-text: var(--base-btn-text);
--btn-submit-bg: var(--base-btn-bg);
--btn-submit-focus: var(--base-btn-focus);
}`;
}
static get $style() {
return `${super.$style}
:host {
position: relative;
display: block;
max-width: 500px;
margin: auto;
}
${WUPcssButton(":host button[type=submit]")}
:host button[type=submit] {
--base-btn-text: var(--btn-submit-text);
--base-btn-bg: var(--btn-submit-bg);
--base-btn-focus: var(--btn-submit-focus);
display: block;
position: relative;
}
:host[aria-busy] {
cursor: progress;
}`;
}
static get mappedAttributes() {
const m = super.mappedAttributes;
m.autostore.type = 2;
return m;
}
static $tryConnect(control) {
const form = formStore.find((f) => f.contains(control));
form?.$controls.push(control);
return form;
}
static $modelToControls(m, controls, prop) {
const out = { hasProp: undefined };
controls.forEach((c) => {
const key = c.$options.name;
if (key) {
const v = m && nestedProperty.get(m, key, out);
if (out.hasProp) {
c[prop] = v;
}
}
});
}
static $modelFromControls(prevModel, controls, prop, isOnlyChanged) {
if (isOnlyChanged) {
controls.forEach((c) => c.$options.name && c.$isChanged && nestedProperty.set(prevModel, c.$options.name, c[prop]));
}
else {
controls.forEach((c) => c.$options.name && nestedProperty.set(prevModel, c.$options.name, c[prop]));
}
return prevModel;
}
static get observedAttributes() {
const a = super.observedAttributes;
a.push("disabled", "readonly");
return a;
}
static $defaults = {
submitActions: 1 |
2 |
4 |
16 |
32,
autoComplete: false,
autoFocus: false,
autoStore: false,
disabled: false,
readOnly: false,
};
$onWillSubmit;
$onSubmit;
$onSubmitEnd;
$controls = [];
get $controlsAttached() {
return this.$controls.filter((c) => c.$options.name != null);
}
_model;
get $model() {
return Object.assign(this._model || {}, this.#ctr.$modelFromControls({}, this.$controls, "$value"));
}
set $model(m) {
if (m !== this._model) {
this._model = m;
this.#ctr.$modelToControls(m, this.$controls, "$value");
}
}
_initModel;
get $initModel() {
return this.#ctr.$modelFromControls(this._initModel || {}, this.$controls, "$initValue");
}
set $initModel(m) {
if (m !== this._initModel) {
this._initModel = m;
this.#ctr.$modelToControls(m, this.$controls, "$initValue");
}
}
get $isPending() {
return this.#stopPending !== undefined;
}
set $isPending(v) {
if (v !== this.$isPending) {
this.changePending(v);
}
}
get $isValid() {
return this.$controlsAttached.every((c) => c.$isValid);
}
get $isChanged() {
return this.$controls.some((c) => c.$options.name && c.$isChanged);
}
$submit() {
this.gotSubmit(null, this);
}
$validate(tillFirstInvalid, includeLocked = false) {
if (tillFirstInvalid === undefined) {
tillFirstInvalid = !!(this._opts.submitActions & 2);
}
let arrCtrl = this.$controlsAttached;
if (!includeLocked) {
arrCtrl = arrCtrl.filter((c) => c.offsetHeight > 0 && !c.$isReadOnly && !c.$isDisabled);
}
if (tillFirstInvalid) {
const ctrl = arrCtrl.find((c) => c.validateBySubmit());
return ctrl ? [ctrl] : [];
}
return arrCtrl.filter((c) => c.validateBySubmit());
}
renderSpin(target) {
target.setAttribute("aria-busy", true);
target.setAttribute("busy", "");
return {
dispose: () => {
target.removeAttribute("busy");
target.removeAttribute("aria-busy");
},
};
}
#stopPending;
changePending(v) {
if (v === !!this.#stopPending) {
return;
}
if (v) {
const wasDisabled = this._opts.readOnly;
if (this._opts.submitActions & 32) {
this.$options.readOnly = true;
this.setAttribute("aria-busy", true);
this.$controls.forEach((c) => {
c.setAttribute("busy", "");
});
}
const backArr = [];
this.querySelectorAll("[type=submit]").forEach((b) => {
backArr.push(this.renderSpin(b).dispose);
});
this.#stopPending = () => {
this.#stopPending = undefined;
this.$options.readOnly = wasDisabled;
this.removeAttribute("aria-busy");
this.$controls.forEach((c) => {
c.removeAttribute("busy");
});
backArr.forEach((s) => s());
};
}
else {
this.#stopPending();
}
}
gotSubmit(e, submitter) {
e?.preventDefault();
if (this.$isPending && this._opts.submitActions & 32) {
return;
}
const willEv = this.fireEvent("$willSubmit", {
bubbles: true,
cancelable: true,
detail: {
relatedEvent: e,
relatedForm: this,
submitter,
},
});
if (willEv.defaultPrevented) {
return;
}
let errCtrl;
let arrCtrl = this.$controlsAttached;
const ctrlsAffected = this._opts.submitActions & 4
? arrCtrl.filter((c) => c.offsetHeight > 0 && !c.$isReadOnly && !c.$isDisabled)
: arrCtrl;
if (this._opts.submitActions & 2) {
errCtrl = ctrlsAffected.find((c) => c.validateBySubmit() && c.canShowError);
}
else {
ctrlsAffected.forEach((c) => {
const err = c.validateBySubmit();
if (err && !errCtrl && c.canShowError) {
errCtrl = c;
}
});
}
if (errCtrl) {
if (this._opts.submitActions & 1) {
const el = errCtrl;
scrollIntoView(el, { offsetTop: -30, onlyIfNeeded: true }).then(() => el.focus());
}
return;
}
arrCtrl = arrCtrl.filter((c) => c.canShowError);
const onlyChanged = this._opts.submitActions & 8;
const m = this.#ctr.$modelFromControls({}, arrCtrl, "$value", !!onlyChanged);
const needReset = this._opts.submitActions & 16;
setTimeout(() => {
let p1;
const orig = this.$onSubmit;
if (orig) {
this.$onSubmit = (ev) => {
p1 = orig.call(this, ev);
this.$onSubmit = orig;
};
}
const ev = this.fireEvent("$submit", {
cancelable: true,
bubbles: true,
detail: {
model: m,
relatedEvent: e,
relatedForm: this,
submitter,
},
});
this.$onSubmit = orig;
const ev2 = new (window.SubmitEvent || Event)("submit", { submitter, cancelable: false, bubbles: true });
if (!window.SubmitEvent) {
ev2.submitter = submitter;
}
this.dispatchEvent(ev2);
let success = false;
promiseWait(Promise.all([p1, ev.detail.waitFor]), 300, (v) => this.changePending(v))
.then(() => {
this._opts.autoStore && this.storageSave(null);
if (needReset) {
arrCtrl.forEach((v) => (v.$isDirty = false));
this.$initModel = this.$model;
}
success = true;
})
.finally(() => {
!ev.defaultPrevented &&
this.fireEvent("$submitEnd", { detail: { success }, cancelable: true, bubbles: true });
});
});
}
#autoStoreT;
#autoStoreRemEv;
gotChanges(propsChanged) {
super.gotChanges(propsChanged);
this.setAttr("readonly", this._opts.readOnly, true);
this.setAttr("disabled", this._opts.disabled, true);
if (this._opts.autoStore) {
this.#autoStoreRemEv = this.appendEvent(this, "$change", () => {
if (this._preventStorageSave) {
return;
}
this.#autoStoreT && clearTimeout(this.#autoStoreT);
this.#autoStoreT = setTimeout(() => this._opts.autoStore && this.storageSave(), 700);
});
!propsChanged &&
setTimeout(() => {
const m = this.storageGet();
if (m) {
this._preventStorageSave = true;
this.$model = { ...this._initModel, ...m, ...this._model };
setTimeout(() => delete this._preventStorageSave);
}
}, 2);
}
else if (this.#autoStoreRemEv) {
this.#autoStoreRemEv.call(this);
this.#autoStoreRemEv = undefined;
}
const p = propsChanged;
if (p && (p.includes("disabled") || p.includes("autoComplete") || p.includes("readOnly"))) {
this.$controls.forEach((c) => c.gotFormChanges(propsChanged));
}
}
gotReady() {
super.gotReady();
if (["title", "aria-label", "aria-labelledby"].every((a) => !this.hasAttribute(a))) {
const meHeading = (el) => el && (/H[\d]/.test(el.tagName) || el.getAttribute("role") === "heading") ? el : null;
const h = meHeading(this.firstElementChild) ?? meHeading(this.previousElementSibling);
if (h) {
h.id ||= this.#ctr.$uniqueId;
this.setAttribute("aria-labelledby", h.id);
}
}
this.appendEvent(this, "keydown", (e) => e.key === "Enter" &&
!e.defaultPrevented &&
!e.submitPrevented &&
!e.shiftKey &&
!e.ctrlKey &&
this.gotSubmit(e, e.target instanceof HTMLElement ? e.target : this), { passive: false });
this.appendEvent(this, "click", (e) => {
if (!e.defaultPrevented) {
let t = e.target;
while (t instanceof HTMLElement && t !== this) {
if (t.type === "submit") {
this.gotSubmit(e, t);
return;
}
t = t.parentElement;
}
}
}, { passive: false });
}
connectedCallback() {
super.connectedCallback();
this.setAttribute("role", "form");
formStore.push(this);
}
get storageKey() {
const sn = this._opts.autoStore;
return typeof sn === "string"
? sn
: `${window.location.pathname}?${this.$controls
.map((c) => c.$options.name)
.filter((v) => v)
.sort()
.join(",")}`;
}
storageGet() {
try {
const key = this.storageKey;
const sv = window.localStorage.getItem(key);
if (sv) {
const m = JSON.parse(sv);
let hasVals = false;
this.$controls.forEach((c) => {
const n = c.$options.name;
if (!n) {
return;
}
const v = m[n];
if (v === undefined) {
return;
}
const parsed = typeof v === "string" ? c.parse(v) : v;
nestedProperty.set(m, n, parsed);
if (n.includes(".")) {
delete m[n];
}
hasVals = true;
});
if (!hasVals) {
return null;
}
return m;
}
}
catch (err) {
this.throwError(err);
}
return null;
}
_preventStorageSave;
storageSave(model = {}) {
try {
const storage = window.localStorage;
if (model == null) {
storage.removeItem(this.storageKey);
return null;
}
const m = {};
let hasChanges = false;
this.$controls.forEach((c) => {
if (c.$options.name && c.tagName !== "WUP-PWD") {
const v = c.$value;
if (c.$isChanged && v !== undefined) {
if (typeof v === "object" && !v.toJSON && !Array.isArray(v)) {
return;
}
m[c.$options.name] = v;
hasChanges = true;
}
}
});
if (hasChanges) {
storage.setItem(this.storageKey, JSON.stringify(m));
return m;
}
storage.removeItem(this.storageKey);
}
catch (err) {
this.throwError(err);
}
return null;
}
disconnectedCallback() {
super.disconnectedCallback();
formStore.splice(formStore.indexOf(this), 1);
}
}
customElements.define(tagName, WUPFormElement);