UNPKG

web-ui-pack

Version:
433 lines (432 loc) 15.1 kB
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);