UNPKG

maz-ui

Version:

A standalone components library for Vue.Js 3 & Nuxt.Js 3

414 lines (413 loc) 20.4 kB
import { defineComponent, mergeModels, defineAsyncComponent, useTemplateRef, computed, useModel, ref, watch, createBlock, openBlock, unref, normalizeStyle, normalizeClass, withCtx, createElementVNode, createElementBlock, createCommentVNode, renderSlot, createVNode, Fragment, renderList, toDisplayString, withModifiers, mergeProps, createSlots, nextTick } from "vue"; import { MazMagnifyingGlass, MazNoSymbol, MazChevronDown } from "@maz-ui/icons"; import { useTranslations } from "@maz-ui/translations"; import { i as isClient } from "../chunks/isClient.WI4oSt66.js"; import { u as useStringMatching, n as normalizeString } from "../chunks/useStringMatching.CqudA-tS.js"; import { useInstanceUniqId } from "../composables/useInstanceUniqId.js"; import MazInput from "./MazInput.js"; import MazPopover from "./MazPopover.js"; import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper.B--vMWp3.js"; import '../assets/MazSelect.uFknehxz.css';let timeout = null; function debounceCallback(callback, delay) { timeout && clearTimeout(timeout), timeout = setTimeout(callback, delay); } const _hoisted_1 = ["disabled", "aria-label"], _hoisted_2 = ["id"], _hoisted_3 = { key: 0, class: "m-select-list__search-wrapper" }, _hoisted_4 = { class: "m-select-list__no-results" }, _hoisted_5 = { key: 2, ref: "optionListWrapper", class: "m-select-list__scroll-wrapper", tabindex: "-1" }, _hoisted_6 = { class: "m-select-list-optgroup" }, _hoisted_7 = ["tabindex", "onClick"], _sfc_main = /* @__PURE__ */ defineComponent({ inheritAttrs: !1, __name: "MazSelect", props: /* @__PURE__ */ mergeModels({ style: { default: () => { } }, class: { default: () => { } }, id: { default: () => { } }, label: {}, placeholder: {}, modelValue: { default: () => { } }, options: {}, optionValueKey: { default: "value" }, optionLabelKey: { default: "label" }, optionInputValueKey: { default: "label" }, listPosition: { default: "auto" }, preferPosition: { default: "bottom-start" }, fallbackPosition: { default: "top-start" }, itemHeight: { default: () => { } }, maxListHeight: { default: 240 }, maxListWidth: { default: () => { } }, minListHeight: { default: () => { } }, minListWidth: { default: () => { } }, size: { default: "md" }, color: { default: "primary" }, search: { type: Boolean }, searchFunction: { type: Function }, searchThreshold: { default: 0.75 }, multiple: { type: Boolean, default: !1 }, required: { type: Boolean }, disabled: { type: Boolean }, block: { type: Boolean }, autocomplete: { default: "off" }, translations: {}, formatInputValue: { type: Function }, transition: { default: "scale-fade" } }, { open: { default: !1 }, openModifiers: {} }), emits: /* @__PURE__ */ mergeModels(["close", "open", "blur", "focus", "change", "input", "update:model-value", "selected-option"], ["update:open"]), setup(__props, { expose: __expose, emit: __emit }) { const emits = __emit, MazCheckbox = defineAsyncComponent(() => import("./MazCheckbox.js")), popoverComponent = useTemplateRef("popover"), inputRef = useTemplateRef("input"), searchInputRef = useTemplateRef("searchInput"), optionListElement = useTemplateRef("optionListRef"), optionListWrapperRef = useTemplateRef("optionListWrapper"), selectedTextColor = computed(() => `hsl(var(--maz-${__props.color}))`), selectedBgColor = computed(() => `hsl(var(--maz-${__props.color}-500) / 0.1)`), { t } = useTranslations(), messages = computed(() => ({ searchPlaceholder: __props.translations?.searchPlaceholder || t("select.searchPlaceholder") })), isOpen = useModel(__props, "open"), instanceId = useInstanceUniqId({ componentName: "MazSelect", providedId: __props.id }); function getOptionPayload(option) { return { [__props.optionValueKey]: option, [__props.optionLabelKey]: option, [__props.optionInputValueKey]: option }; } function getNormalizedOptionPayload(option) { return { ...option, [__props.optionValueKey]: option[__props.optionValueKey], [__props.optionLabelKey]: option[__props.optionLabelKey], [__props.optionInputValueKey]: option[__props.optionInputValueKey] }; } function getNormalizedOptions(options) { const normalizedOptions = []; if (!options?.length) return []; for (const option of options) typeof option == "string" || typeof option == "number" || typeof option == "boolean" ? normalizedOptions.push(getOptionPayload(option)) : typeof option == "object" && "options" in option && Array.isArray(option.options) ? normalizedOptions.push( { label: option.label, isOptGroup: !0 }, ...option.options.map( (opt) => typeof opt == "string" || typeof opt == "number" || typeof opt == "boolean" ? getOptionPayload(opt) : getNormalizedOptionPayload(opt) ) ) : normalizedOptions.push(getNormalizedOptionPayload(option)); return normalizedOptions; } const optionsNormalized = computed(() => getNormalizedOptions(__props.options ?? [])); function isOptionInSelection(option) { return isNullOrUndefined(option[__props.optionValueKey]) ? !1 : __props.multiple ? Array.isArray(__props.modelValue) && __props.modelValue.includes(option[__props.optionValueKey]) : __props.modelValue === option[__props.optionValueKey]; } const selectedOptions = computed( () => optionsNormalized.value?.filter(isOptionInSelection) ?? [] ); function isNullOrUndefined(value) { return value == null; } function isSelectedOption(option) { return (selectedOptions.value?.some( (selectedOption) => selectedOption[__props.optionValueKey] === option[__props.optionValueKey] ) ?? !1) && !isNullOrUndefined(option[__props.optionValueKey]); } const inputValue = computed(() => { if (__props.multiple && __props.modelValue && Array.isArray(__props.modelValue)) { const values = __props.modelValue.map( (value2) => optionsNormalized.value?.find((option) => option[__props.optionValueKey] === value2)?.[__props.optionInputValueKey] ); return __props.formatInputValue ? __props.formatInputValue(values) : values.join(", "); } const selectedOption = optionsNormalized.value?.find( (option) => option[__props.optionValueKey] === __props.modelValue ), value = isNullOrUndefined(__props.modelValue) ? void 0 : selectedOption?.[__props.optionInputValueKey]; return __props.formatInputValue ? __props.formatInputValue(value) : value; }), searchQuery = ref(), query = ref(""); function searchInValue(value, query2) { return query2 && value && normalizeString(value).includes(normalizeString(query2)); } function getFilteredOptionWithQuery(query2) { return query2 ? optionsNormalized.value?.filter((option) => { const searchValue = option[__props.optionLabelKey], searchValue3 = option[__props.optionValueKey], searchValue2 = option[__props.optionInputValueKey], threshold = __props.searchThreshold; return searchInValue(searchValue, query2) || searchInValue(searchValue2, query2) || searchInValue(searchValue3, query2) || typeof searchValue == "string" && useStringMatching(searchValue, query2, threshold).isMatching.value || typeof searchValue2 == "string" && useStringMatching(searchValue2, query2, threshold).isMatching.value || typeof searchValue3 == "string" && useStringMatching(searchValue3, query2, threshold).isMatching.value; }) : optionsNormalized.value; } const optionList = computed(() => __props.searchFunction && __props.search && searchQuery.value ? getNormalizedOptions(__props.searchFunction(searchQuery.value, __props.options ?? []) ?? []) : getFilteredOptionWithQuery(searchQuery.value)); async function onOpenList() { const selectedIndex = optionList.value?.findIndex( (option) => isSelectedOption(option) ); await scrollToOptionIndex(selectedIndex), emits("open"); } function focusMainInput() { inputRef.value?.$el?.querySelector("input")?.focus(); } function emitInputMainInput() { inputRef.value?.$el?.querySelector("input")?.dispatchEvent(new Event("input")); } function focusSearchInputAndSetQuery(q) { searchQuery.value = q, searchInputRef.value?.$el?.querySelector("input")?.focus(); } function searchOptionWithQuery(keyPressed) { keyPressed === "Backspace" && query.value && query.value.length > 0 ? query.value = query.value.slice(0, -1) : query.value += keyPressed; const filteredOptions = getFilteredOptionWithQuery(query.value); if (!filteredOptions?.length) return; const optionIndex = optionList.value?.findIndex( (option) => option[__props.optionValueKey] === filteredOptions[0][__props.optionValueKey] ); typeof optionIndex != "number" || optionIndex === -1 || (scrollToOptionIndex(optionIndex), debounceCallback(() => { query.value = ""; }, 1e3)); } function mainInputKeyboardHandler(event) { if (event.ctrlKey || event.metaKey || event.altKey) return; const keyPressed = event.key; (keyPressed === "ArrowDown" || keyPressed === "ArrowUp" || /^[\dA-Za-z\u0400-\u04FF]$/.test(keyPressed)) && !isOpen.value && (event.preventDefault(), popoverComponent.value?.open()), /^[\dA-Za-z\u0400-\u04FF]$/.test(keyPressed) && __props.search && (event.preventDefault(), event.stopPropagation(), focusSearchInputAndSetQuery(keyPressed)); } async function scrollToOptionIndex(index) { if (await nextTick(), typeof index != "number" || index < 0) return; const item = optionListElement.value?.querySelector(`.m-select-list-item:nth-child(${index + 1})`); if (item && optionListWrapperRef.value) { const wrapperRect = optionListWrapperRef.value.getBoundingClientRect(), itemRect = item.getBoundingClientRect(), scrollTop = item.offsetTop - wrapperRect.height / 2 - itemRect.height; optionListWrapperRef.value.scrollTo?.({ top: scrollTop, behavior: "auto" }), setTimeout(() => { item.focus({ preventScroll: !0 }); }, 100); } } function updateValue(inputOption, mustCloseList = !0) { mustCloseList && !__props.multiple && nextTick(() => { popoverComponent.value?.close(); }), searchQuery.value = ""; const isAlreadySelected = selectedOptions.value?.some( (option) => option[__props.optionValueKey] === inputOption[__props.optionValueKey] ); let newValue = selectedOptions.value; isAlreadySelected && __props.multiple ? newValue = newValue?.filter( (option) => option[__props.optionValueKey] !== inputOption[__props.optionValueKey] ) : __props.multiple ? newValue.push(inputOption) : newValue = [inputOption]; const selectedValues = newValue.map((option) => option[__props.optionValueKey]); emits("update:model-value", __props.multiple ? selectedValues : selectedValues[0]), emits("selected-option", inputOption), emitInputMainInput(), focusMainInput(); } function keydownHandler(event) { const keyPressed = event.key; if (keyPressed === "ArrowDown" || keyPressed === "ArrowUp") { event.preventDefault(); const itemLength = optionList.value?.length; if (!itemLength) return; const currentElement = document.activeElement, itemsElements = document.querySelectorAll(`#${instanceId.value}-option-list .m-select-list-item`), currentIndex = Array.from(itemsElements).indexOf(currentElement); if (currentIndex === -1) { itemsElements[0]?.focus({ preventScroll: !0 }); return; } const nextIndex = keyPressed === "ArrowDown" ? (currentIndex + 1) % itemLength : (currentIndex - 1 + itemLength) % itemLength; itemsElements[nextIndex]?.focus(); } else !__props.search && /^[\dA-Za-z\u0400-\u04FF]$/.test(keyPressed) && searchOptionWithQuery(keyPressed); } function updateListPosition() { nextTick(() => { popoverComponent.value?.updatePosition(); }); } return watch( isOpen, (value) => { isClient() && (value ? document.addEventListener("keydown", keydownHandler) : document.removeEventListener("keydown", keydownHandler)); }, { immediate: !0 } ), __expose({ /** * Open the select * @description This is used to open the list options */ open: () => { popoverComponent.value?.open(); }, /** * Close the select * @description This is used to close the list options */ close: () => { popoverComponent.value?.close(); } }), (_ctx, _cache) => (openBlock(), createBlock(MazPopover, { id: `${unref(instanceId)}-popover`, ref: "popover", modelValue: isOpen.value, "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => isOpen.value = $event), class: normalizeClass(["m-select m-reset-css", [ { "--is-open": isOpen.value, "--disabled": __props.disabled }, __props.class, `--${__props.size}` ]]), style: normalizeStyle(__props.style), trigger: "click", block: __props.block, transition: __props.transition, offset: 0, position: __props.listPosition, disabled: __props.disabled, "prefer-position": __props.preferPosition, "fallback-position": __props.fallbackPosition, "position-reference": `#${unref(instanceId)}-popover .m-input-wrapper`, onClose: _cache[6] || (_cache[6] = ($event) => emits("close")), onOpen: onOpenList }, { trigger: withCtx(({ close, open: openPicker, toggle: togglePopover }) => [ createVNode(MazInput, mergeProps({ id: unref(instanceId), ref: "input", class: "m-select-input" }, _ctx.$attrs, { required: __props.required, "border-active": isOpen.value, color: __props.color, "model-value": inputValue.value, size: __props.size, block: __props.block, placeholder: __props.placeholder, label: __props.label, autocomplete: __props.autocomplete, disabled: __props.disabled, readonly: "", onChange: _cache[0] || (_cache[0] = ($event) => emits("change", $event)), onInput: _cache[1] || (_cache[1] = ($event) => emits("input", $event)), onFocus: _cache[2] || (_cache[2] = ($event) => emits("focus", $event)), onBlur: _cache[3] || (_cache[3] = ($event) => emits("blur", $event)), onKeydown: mainInputKeyboardHandler }), createSlots({ "right-icon": withCtx(() => [ renderSlot(_ctx.$slots, "right-icon", { isOpen: isOpen.value, close, open: openPicker, toggle: togglePopover }, () => [ createElementVNode("button", { tabindex: "-1", disabled: __props.disabled, type: "button", class: "m-select-input__toggle-button maz-custom", "aria-label": `${isOpen.value ? "collapse" : "expand"} list of options` }, [ createVNode(unref(MazChevronDown), { class: "m-select-chevron" }) ], 8, _hoisted_1) ], !0) ]), _: 2 }, [ _ctx.$slots["left-icon"] ? { name: "left-icon", fn: withCtx(() => [ renderSlot(_ctx.$slots, "left-icon", { isOpen: isOpen.value, close, open: openPicker, toggle: togglePopover }, void 0, !0) ]), key: "0" } : void 0 ]), 1040, ["id", "required", "border-active", "color", "model-value", "size", "block", "placeholder", "label", "autocomplete", "disabled"]) ]), default: withCtx(({ close, open: openPicker, toggle: togglePopover }) => [ createElementVNode("div", { id: `${unref(instanceId)}-option-list`, ref: "optionListRef", class: normalizeClass(["m-select-list", `--${__props.size}`]), style: normalizeStyle([{ maxHeight: `${__props.maxListHeight}px`, maxWidth: `${__props.maxListWidth}px`, minHeight: `${__props.minListHeight}px`, minWidth: `${__props.minListWidth}px`, "--selected-bg-color": selectedBgColor.value, "--selected-text-color": selectedTextColor.value }]) }, [ __props.search ? (openBlock(), createElementBlock("div", _hoisted_3, [ createVNode(MazInput, { ref: "searchInput", modelValue: searchQuery.value, "onUpdate:modelValue": [ _cache[4] || (_cache[4] = ($event) => searchQuery.value = $event), updateListPosition ], size: "sm", disabled: __props.disabled, color: __props.color, placeholder: messages.value.searchPlaceholder, name: "search", inputmode: "search", autocomplete: "off", block: "", class: "m-select-list__search-input maz-flex-none", "left-icon": unref(MazMagnifyingGlass) }, null, 8, ["modelValue", "disabled", "color", "placeholder", "left-icon"]) ])) : createCommentVNode("", !0), !optionList.value || optionList.value.length <= 0 ? renderSlot(_ctx.$slots, "no-results", { key: 1 }, () => [ createElementVNode("span", _hoisted_4, [ createVNode(unref(MazNoSymbol), { class: "maz-size-6 maz-text-foreground" }) ]) ], !0) : (openBlock(), createElementBlock("div", _hoisted_5, [ (openBlock(!0), createElementBlock(Fragment, null, renderList(optionList.value, (option, i) => (openBlock(), createElementBlock(Fragment, { key: i }, [ option.label && option.isOptGroup ? renderSlot(_ctx.$slots, "optgroup", { key: 0, label: option.label }, () => [ createElementVNode("span", _hoisted_6, toDisplayString(option.label), 1) ], !0) : (openBlock(), createElementBlock("button", { key: 1, type: "button", tabindex: __props.multiple ? -1 : 0, class: normalizeClass(["m-select-list-item maz-custom maz-flex-none", [ { "--is-selected": isSelectedOption(option), "--is-none-value": isNullOrUndefined(option[__props.optionValueKey]) } ]]), style: normalizeStyle(__props.itemHeight ? { height: `${__props.itemHeight}px` } : void 0), onClick: withModifiers(($event) => updateValue(option, !0), ["prevent", "stop"]) }, [ __props.multiple ? (openBlock(), createBlock(unref(MazCheckbox), { key: 0, "model-value": isSelectedOption(option), size: "sm", color: __props.color }, null, 8, ["model-value", "color"])) : createCommentVNode("", !0), renderSlot(_ctx.$slots, "default", { option, isSelected: isSelectedOption(option), isOpen: isOpen.value, close, open: openPicker, toggle: togglePopover }, () => [ createElementVNode("span", null, toDisplayString(option[__props.optionLabelKey]), 1) ], !0) ], 14, _hoisted_7)) ], 64))), 128)) ], 512)) ], 14, _hoisted_2) ]), _: 3 }, 8, ["id", "modelValue", "class", "style", "block", "transition", "position", "disabled", "prefer-position", "fallback-position", "position-reference"])); } }), MazSelect = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-021953e9"]]); export { MazSelect as default };