UNPKG

web-ui-pack

Version:
643 lines (642 loc) 22.7 kB
import nestedProperty from "../helpers/nestedProperty"; import onEvent from "../helpers/onEvent"; import promiseWait from "../helpers/promiseWait"; import WUPSpinElement from "../spinElement"; import { WUPcssMenu } from "../styles"; import WUPBaseComboControl from "./baseCombo"; const tagName = "wup-select"; export default class WUPSelectControl extends WUPBaseComboControl { #ctr = this.constructor; static get $style() { return `${super.$style} :host { --ctrl-icon-img: var(--wup-icon-chevron); } :host label:after { height: 100%; align-self: center; } :host[opened] label:after { transform: rotate(180deg); } ${WUPcssMenu(":host [menu]")}`; } static $textNoItems = __wupln("No Items", "content"); static $ariaLabelItems = __wupln("Items", "aria"); static $isEqual(v1, v2, c) { let isEq = super.$isEqual(v1, v2, c); if (!isEq && c._opts.multiple && Array.isArray(v1) && Array.isArray(v2)) { const isSortMatter = c._opts.sortable; isEq = v1.length === v2.length && (isSortMatter ? v1.every((v, i) => v2[i] === v) : v1.every((v) => v2.includes(v))); } return isEq; } static $filterMenuItem(menuItemText, menuItemValue, inputValue, inputRawValue) { return !inputValue || menuItemText.startsWith(inputValue) || menuItemText.includes(` ${inputValue}`); } static $defaults = { ...WUPBaseComboControl.$defaults, validationRules: { ...WUPBaseComboControl.$defaults.validationRules, minCount: (v, setV) => (v == null || v.length < setV) && __wupln(`Min count is ${setV}`, "validation"), maxCount: (v, setV) => (v == null || v.length > setV) && __wupln(`Max count is ${setV}`, "validation"), }, openCase: 8 | 2 | 64 | 32, allowNewValue: false, multiple: false, items: [], }; static cloneDefaults() { const d = super.cloneDefaults(); d.items = []; return d; } get $isPending() { return !!this.#stopPending; } parse(attrValue) { if (this._opts.multiple) { return nestedProperty.get(window, attrValue); } const arr = this.getItems(); if (arr?.length) { const r = attrValue === "" ? arr.find((o) => o.value == null) : arr.find((o) => o.value && `${o.value}` === attrValue); return r?.value; } return undefined; } parseInput(text) { let v; if (!this._opts.multiple) { const s = text.trim(); v = s ? this.findValueByText(s) ?? s : undefined; } else { v = text.split(",").map((si) => si.trim()); v = v .filter((si, i) => v.indexOf(si) === i) .map((si) => { if (!si) { return undefined; } const vi = this.findValueByText(si); return vi ?? (this._opts.allowNewValue ? si : vi); }) .filter((vi) => vi !== undefined); if (!v.length) { return undefined; } } return v; } valueToStrCompare(a) { const at = typeof a.text === "function" ? a.value?.toString() : a.text; return at?.replace(/\s/g, "") ?? null; } valueFromStorage(str, skipMultiple) { if (this._opts.multiple && !skipMultiple) { const v = str.split("_").map((si) => this.valueFromStorage(si, true)); return v; } if (str === "null") { return null; } const s = str.toLowerCase(); const items = this.getItems(); const item = items.find((a) => this.valueToStrCompare(a)?.toLowerCase() === s); if (item === undefined) { this.throwError("Not found in items (search by item.value.toString() & item.text)", { items, searchText: str, }); return undefined; } return item.value; } valueToStorage(v, skipMultiple) { if (this._opts.multiple && !skipMultiple) { return v.map((vi) => this.valueToStorage(vi, true)).join("_"); } if (v == null) { return "null"; } const items = this.getItems(); const item = items.find((o) => this.#ctr.$isEqual(o.value, v, this)) || { value: v, text: v?.toString() }; return this.valueToStrCompare(item); } findValueByText(txt) { const s = txt.toLowerCase(); const i = this._menuItems?.all.findIndex((o) => o._text === s); const r = i > -1 ? this._cachedItems[i].value : undefined; return r; } renderSpin() { WUPSpinElement.$use(); const spin = document.createElement("wup-spin"); spin.$options.fit = true; spin.$options.overflowFade = true; spin.$options.overflowTarget = this; this.appendChild(spin); return spin; } _onPendingInitValue; fetchItems() { this._cachedItems = undefined; this.#findValT && clearTimeout(this.#findValT); this.#findValT = undefined; const { items } = this._opts; let d = items; if (typeof d === "function") { d = d(); } const act = () => { this._onPendingInitValue?.call(this); delete this._onPendingInitValue; this.setInputValue(this.$value, 5); this.setupInputReadonly(); }; if (d instanceof Promise) { return promiseWait(d, 300, (v) => this.changePending(v)) .then((data) => { this._cachedItems = data || []; return data; }) .finally(() => { this._cachedItems ??= []; act(); this.$isFocused && this.goOpenMenu(2, null); }); } this._cachedItems = d; act(); return d; } gotChanges(propsChanged) { this._opts.items ??= []; if (!propsChanged || propsChanged.includes("items")) { propsChanged && this.removePopup(); this.fetchItems(); } super.gotChanges(propsChanged); } setupInputReadonly() { const r = this._opts.readOnlyInput; this.$refInput.readOnly = this.$isReadOnly || this.$isPending || r === true || (typeof r === "number" && r > (this._cachedItems?.length || 0) && !this._opts.allowNewValue); } setupInitValue(propsChanged) { if (this._cachedItems) { super.setupInitValue(propsChanged); } else { this._onPendingInitValue = () => super.setupInitValue(propsChanged); } } #stopPending; changePending(v) { if (v === !!this.#stopPending) { return; } if (v) { this.$refInput.setAttribute("aria-busy", true); const refSpin = this.renderSpin(); this.$refInput.readOnly = true; this.#stopPending = () => { this.$refInput.readOnly = this.$isReadOnly; this.#stopPending = undefined; this.$refInput.removeAttribute("aria-busy"); refSpin.remove(); }; } else { this.#stopPending(); } } _menuItems; _cachedItems; renderMenuNoItems(popup, isReset) { if (isReset) { popup.hidden = false; const liSaved = this._menuItems._refNoItems; if (liSaved) { liSaved.style.display = "none"; } } else if (this.#ctr.$textNoItems) { const liSaved = this._menuItems._refNoItems; if (liSaved) { liSaved.style.display = ""; } else { const ul = popup.children.item(0); const li = ul.appendChild(document.createElement("li")); li.textContent = this.#ctr.$textNoItems; li.setAttribute("role", "option"); li.setAttribute("aria-disabled", true); li.setAttribute("aria-selected", false); this._menuItems._refNoItems = li; } } else { popup.hidden = true; } } renderMenu(popup, menuId) { const ul = popup.appendChild(document.createElement("ul")); ul.setAttribute("id", menuId); ul.setAttribute("role", "listbox"); ul.setAttribute("aria-label", this.#ctr.$ariaLabelItems); ul.setAttribute("tabindex", -1); this.setAttr.call(ul, "aria-multiselectable", this._opts.multiple === true); const all = this.renderMenuItems(ul); this._menuItems = { all, focused: -1 }; !all.length && this.renderMenuNoItems(popup, false); onEvent(ul, "click", (e) => this.gotMenuClick(e), { passive: false }); return ul; } getItems() { if (!this._cachedItems) { this.throwError("Internal bug. No cached items", { options: this._opts, value: this.$value }); return []; } return this._cachedItems; } valueToText(v, items) { const i = items.findIndex((o) => this.#ctr.$isEqual(o.value, v, this)); if (i === -1) { if (this._opts.allowNewValue) { return v != null ? v.toString() : ""; } this.throwError("Not found in items", { items, value: v }, true); return `Error: not found for ${v != null ? v.toString() : ""}`; } const item = items[i]; if (typeof item.text === "function") { const li = document.createElement("li"); const s = item.text(item.value, li, i, this); li.remove(); return s; } return item.text; } #findValT; setInputValue(v, reason) { this.#findValT && clearTimeout(this.#findValT); this.#findValT = undefined; if (this.$isPending) { return; } if (reason === 1 || reason === 5) { this.#findValT = setTimeout(() => super.setInputValue(v, reason), 2); } else { super.setInputValue(v, reason); } } valueToInput(v) { if (v === undefined || (this._opts.multiple && !v.length)) { return ""; } const items = this.getItems(); if (this._opts.multiple) { const s = v?.map((vi) => this.valueToText(vi, items)).join(", "); return this.$isFocused ? `${s}, ` : s; } return this.valueToText(v, items); } renderMenuItems(ul) { const arr = this.getItems(); return arr.map((a, i) => { const li = ul.appendChild(document.createElement("li")); li.setAttribute("role", "option"); li._value = a.value; let s = a.text; if (typeof s === "function") { s = s(a.value, li, i, this); } else { li.textContent = s; } li._text = s.toLowerCase(); return li; }); } gotMenuClick(e) { if (e.defaultPrevented) { return; } const ul = e.currentTarget; let t = e.target; if (t === ul) { return; } while (1) { const parent = t.parentElement; if (parent === ul) { const isDisabled = t.hasAttribute("aria-disabled"); !isDisabled && this.gotMenuItemClick(e, t); break; } t = parent; } e.preventDefault(); } gotMenuItemClick(e, li) { const i = this._menuItems.all.indexOf(li); const o = this._cachedItems[i]; o.onClick?.call(e.target, e, o); if (e.defaultPrevented) { return; } const canOff = this._opts.multiple && li.getAttribute("aria-selected") === "true"; this.selectMenuItem(canOff ? null : li); canOff && li.setAttribute("aria-selected", "false"); this.selectValue(o.value, !this._opts.multiple); } selectValue(v, canCloseMenu = true) { if (this._opts.multiple) { let arr = this.$value; if (!arr?.length) { v = [v]; } else { const i = arr.indexOf(v); if (i !== -1) { if (arr.length === 1) { arr = undefined; } else { arr = [...arr]; arr.splice(i, 1); } } else { arr = [...arr, v]; } v = arr; } } super.selectValue(v, canCloseMenu); this._opts.multiple && this.clearFilterMenuItems(); } canOpenMenu(openCase, e) { if (this.$isPending) { return false; } return super.canOpenMenu(openCase, e); } async goOpenMenu(openCase, e) { if (this.$isOpened) { return this.$refPopup; } if (this.$refPopup && openCase !== 32 && this._menuItems.filtered) { this.clearFilterMenuItems(); } const popup = await super.goOpenMenu(openCase, e); popup && !this.canClearSelection && this.selectMenuItemByValue(this.$value); return popup; } getFirstSelected() { if (this._selectedMenuItem) { const items = this._selectedMenuItem.parentElement.querySelectorAll("[aria-selected=true]"); for (let i = 0, item = items[0]; i < items.length; item = items[++i]) { if (!item.style.display) { return item; } } } return undefined; } selectMenuItem(next) { if (this._opts.multiple) { this._selectedMenuItem = undefined; } super.selectMenuItem(next); } selectMenuItemByValue(v) { if (this.$isOpened) { const findAndSelect = (vi) => { const i = this._cachedItems.findIndex((item) => this.#ctr.$isEqual(item.value, vi, this)); this.selectMenuItem(this._menuItems.all[i] || null); return i; }; v === undefined && this._selectedMenuItem && this.selectMenuItem(null); if (!this._opts.multiple) { v !== undefined && findAndSelect(v); } else { const set = new Set(); v !== undefined && v.forEach((vi) => set.add(findAndSelect(vi))); this._menuItems.all.forEach((el, i) => !set.has(i) && el.hasAttribute("aria-selected") && el.removeAttribute("aria-selected")); } } } focusMenuItem(next) { next && !next.hasAttribute("aria-selected") && next.setAttribute("aria-selected", false); super.focusMenuItem(next); if (next === null && this._menuItems) { this._menuItems.focused = -1; } } focusMenuItemByIndex(index) { const { filtered } = this._menuItems; const trueIndex = filtered ? filtered[index] : index; const next = this._menuItems.all[trueIndex]; this.focusMenuItem(next); this._menuItems.focused = index; } focusMenuItemByKeydown(e) { if (!this._menuItems) { return; } const { length } = this._menuItems.filtered || this._menuItems.all; let focusIndex = null; if (this._menuItems.focused === -1) { if (e.key === "ArrowDown" || e.key === "ArrowUp") { const item = this.getFirstSelected(); if (item) { focusIndex = this._menuItems.all.indexOf(item); } } } if (focusIndex === null) { switch (e.key) { case "ArrowDown": { let cur = this._menuItems.focused; focusIndex = ++cur >= length ? 0 : cur; } break; case "ArrowUp": { let cur = this._menuItems.focused; focusIndex = --cur < 0 ? length - 1 : cur; } break; case "Home": focusIndex = 0; break; case "End": focusIndex = length - 1; break; default: break; } } if (focusIndex != null) { e.preventDefault(); this.focusMenuItemByIndex(focusIndex); } } gotKeyDown(e) { if (this._opts.allowNewValue && e.key === "Enter" && !this._focusedMenuItem) { this.setValue(this.parseInput(this.$refInput.value), 3); this._opts.multiple && e.preventDefault(); } !e.defaultPrevented && super.gotKeyDown(e); } filterMenuItems() { const rawV = this.$refInput.value; let v = rawV; if (this._opts.multiple) { v = rawV.substring(rawV.lastIndexOf(",") + 1, rawV.length); } v = v.trim().toLowerCase(); const filtered = []; this._menuItems.all.forEach((li, i) => { const isOk = this.#ctr.$filterMenuItem.call(this, li._text, li._value, v, rawV); isOk && filtered.push(i); this.filterMenuItem(li, !isOk); }); const hasFiltered = filtered.length !== this._menuItems.all.length; this._menuItems.filtered = hasFiltered ? filtered : undefined; const hasVisible = filtered.length !== 0; this.renderMenuNoItems(this.$refPopup, hasVisible); hasVisible && rawV !== "" && !this._opts.allowNewValue && this.focusMenuItemByIndex(0); } clearFilterMenuItems() { this._menuItems.all.forEach((li) => this.filterMenuItem(li, false)); delete this._menuItems.filtered; const hasVisible = this._menuItems.all.length !== 0; this.renderMenuNoItems(this.$refPopup, hasVisible); } filterMenuItem(el, hide) { el.style.display = hide ? "none" : ""; } get canClearSelection() { return !this.$refInput.value && !this._opts.multiple; } gotBeforeInput(e) { super.gotBeforeInput(e); if (this._opts.multiple) { let isDelBack = false; switch (e.inputType) { case "deleteContentBackward": isDelBack = true; break; case "deleteContentForward": isDelBack = false; break; default: return; } const pos = this.$refInput.selectionStart || 0; let pos2 = this.$refInput.selectionEnd || 0; const str = this.$refInput.value; const isItemPart = pos < str.lastIndexOf(", ") + (isDelBack ? 3 : 2); if (!isItemPart) { return; } const delimitLength = 2; const char = ",".charCodeAt(0); if (isDelBack && !pos) { return; } const start = isDelBack ? pos - 1 : pos; let i = start; for (; i !== 0; --i) { if (str.charCodeAt(i) === char) { if (!isDelBack || start - i >= delimitLength) { i += delimitLength; break; } } } if (pos === pos2) { pos2 = i; } let end = str.indexOf(",", pos2); end = end === -1 ? str.length : end + delimitLength; this.$refInput.setSelectionRange(i, end); } } tryUpdateMenu() { this.$isOpened && this.filterMenuItems(); this.canClearSelection && this.selectMenuItem(null); } clearValue() { super.clearValue(); this.tryUpdateMenu(); } gotInput(e) { this.$isOpened && this.focusMenuItem(null); super.gotInput(e, true); if (this._opts.multiple && this.canParseInput(this.$refInput.value)) { const str = this.$refInput.value; const isDeleted = e.inputType.startsWith("deleteContent"); const pos = this.$refInput.selectionStart || 0; if (pos !== str.length && !isDeleted) { this.declineInput(str.length); } else { const iEnd = str.lastIndexOf(","); const strVal = str.substring(0, iEnd); const next = this.parseInput(strVal); const nextLn = (next || []).length; const prevLn = (this.$value || []).length; const isChanged = nextLn !== prevLn; if (isChanged) { this.setValue(next, 3, true); this.setInputValueDirect(this.valueToInput(next) + str.substring(iEnd + 1).trimStart(), 3); nextLn < prevLn && this.$refInput.setSelectionRange(pos, pos); } } } this.tryUpdateMenu(); } gotFocus(e) { const r = super.gotFocus(e); if (this._opts.multiple) { if (!this.$isDisabled && !this.$isReadOnly && !this._opts.readOnlyInput && this.$refInput.value) { this.setInputValueDirect(`${this.$refInput.value}, `, 3); } } return r; } gotFocusLost() { this._opts.multiple && this.setInputValueDirect(this.$refInput.value.replace(/, *$/, ""), 3); if (this._opts.allowNewValue) { this.setValue(this.parseInput(this.$refInput.value), 3); } else { this.resetInputValue(); } super.gotFocusLost(); } setValue(v, reason, skipInput = false) { const r = super.setValue(v, reason, skipInput); r && reason !== 4 && this.selectMenuItemByValue(v); return r; } removePopup() { super.removePopup(); this._menuItems = undefined; } } customElements.define(tagName, WUPSelectControl);