web-ui-pack
Version:
Web package with UI elements
648 lines (647 loc) • 22.8 kB
JavaScript
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) {
let at = null;
if (a.value != null) {
at = (a.value.id ?? a.value).toString();
}
else {
at = typeof a.text === "function" ? a.value?.toString() : a.text;
}
return at;
}
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 items = this.getItems();
const item = items.find((a) => this.valueToStrCompare(a) === str);
if (item === undefined) {
this.throwError("Not found in items (search by item.value.id | 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);