maz-ui
Version:
A standalone components library for Vue.Js 3 & Nuxt.Js 3
398 lines (397 loc) • 19.5 kB
JavaScript
import { defineComponent, mergeModels, defineAsyncComponent, useTemplateRef, computed, useModel, ref, watch, createBlock, openBlock, unref, normalizeStyle, normalizeClass, withCtx, createElementVNode, createCommentVNode, renderSlot, createElementBlock, 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 { e as e$1 } from "../chunks/isClient.8V3qjGdO.js";
import { u as useStringMatching, S } from "../chunks/useStringMatching.DzSigyZ7.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.DFWUoqLA.css';let e = null;
function o(t, l) {
e && clearTimeout(e), e = setTimeout(t, l);
}
const _hoisted_1 = ["aria-label"], _hoisted_2 = ["id"], _hoisted_3 = { class: "m-select-list__no-results" }, _hoisted_4 = {
key: 2,
ref: "optionListWrapper",
class: "m-select-list__scroll-wrapper",
tabindex: "-1"
}, _hoisted_5 = { class: "m-select-list-optgroup" }, _hoisted_6 = ["tabindex", "onClick"], _sfc_main = /* @__PURE__ */ defineComponent({
inheritAttrs: !1,
__name: "MazSelect",
props: /* @__PURE__ */ mergeModels({
style: { default: void 0 },
class: { default: void 0 },
id: { default: void 0 },
label: {},
placeholder: {},
modelValue: { default: void 0 },
options: {},
optionValueKey: { default: "value" },
optionLabelKey: { default: "label" },
optionInputValueKey: { default: "label" },
listPosition: { default: void 0 },
itemHeight: { default: void 0 },
maxListHeight: { default: 240 },
maxListWidth: { default: void 0 },
minListHeight: { default: void 0 },
minListWidth: { default: void 0 },
size: { default: "md" },
color: { default: "primary" },
search: { type: Boolean },
searchFunction: {},
searchThreshold: { default: 0.75 },
multiple: { default: void 0 },
required: { type: Boolean },
disabled: { type: Boolean },
block: { type: Boolean },
autocomplete: { default: "off" },
translations: {},
formatInputValue: {},
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 props = __props, 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 && S(value).includes(S(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), o(() => {
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 && 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 / 2;
optionListWrapperRef.value.scrollTo?.({
top: scrollTop,
behavior: "auto"
}), nextTick(() => {
item.focus({ preventScroll: !0 });
});
}
}
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) => {
e$1() && (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": _ctx.disabled
},
props.class,
`--${_ctx.size}`
]]),
style: normalizeStyle(_ctx.style),
trigger: "click",
block: _ctx.block,
transition: _ctx.transition,
offset: 0,
position: _ctx.listPosition,
"prefer-position": "bottom-start",
"fallback-position": "top-start",
"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: _ctx.required,
"border-active": isOpen.value,
color: _ctx.color,
"model-value": inputValue.value,
size: _ctx.size,
block: _ctx.block,
placeholder: _ctx.placeholder,
label: _ctx.label,
autocomplete: _ctx.autocomplete,
disabled: _ctx.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",
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", `--${_ctx.size}`]),
style: normalizeStyle([{
maxHeight: `${_ctx.maxListHeight}px`,
maxWidth: `${_ctx.maxListWidth}px`,
minHeight: `${_ctx.minListHeight}px`,
minWidth: `${_ctx.minListWidth}px`,
"--selected-bg-color": selectedBgColor.value,
"--selected-text-color": selectedTextColor.value
}])
}, [
_ctx.search ? (openBlock(), createBlock(MazInput, {
key: 0,
ref: "searchInput",
modelValue: searchQuery.value,
"onUpdate:modelValue": [
_cache[4] || (_cache[4] = ($event) => searchQuery.value = $event),
updateListPosition
],
size: "sm",
disabled: _ctx.disabled,
color: _ctx.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_3, [
createVNode(unref(MazNoSymbol), { class: "maz-size-6 maz-text-foreground" })
])
], !0) : (openBlock(), createElementBlock("div", _hoisted_4, [
(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_5, toDisplayString(option.label), 1)
], !0) : (openBlock(), createElementBlock("button", {
key: 1,
type: "button",
tabindex: _ctx.multiple ? -1 : 0,
class: normalizeClass(["m-select-list-item maz-custom maz-flex-none", [
{
"--is-selected": isSelectedOption(option),
"--is-none-value": isNullOrUndefined(option[_ctx.optionValueKey])
}
]]),
style: normalizeStyle(_ctx.itemHeight ? { height: `${_ctx.itemHeight}px` } : void 0),
onClick: withModifiers(($event) => updateValue(option, !0), ["prevent", "stop"])
}, [
_ctx.multiple ? (openBlock(), createBlock(unref(MazCheckbox), {
key: 0,
"model-value": isSelectedOption(option),
size: "sm",
color: _ctx.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[_ctx.optionLabelKey]), 1)
], !0)
], 14, _hoisted_6))
], 64))), 128))
], 512))
], 14, _hoisted_2)
]),
_: 3
}, 8, ["id", "modelValue", "class", "style", "block", "transition", "position", "position-reference"]));
}
}), MazSelect = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-406e8df3"]]);
export {
MazSelect as default
};