Newer
Older
gnexus-ui-kit / src / vue / components / GnCombobox.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 14 hours ago 3 KB Add Vue form adapter components
import { computed, defineComponent, h, nextTick, ref } from "vue";
import { cx, eventValue, iconNode } from "../utils.js";

export default defineComponent({
	name: "GnCombobox",
	inheritAttrs: false,
	props: {
		modelValue: { type: [String, Number], default: "" },
		label: { type: String, default: "" },
		icon: { type: String, default: "" },
		options: { type: Array, default: () => [] },
		placeholder: { type: String, default: "Search" },
		notFoundText: { type: String, default: "Nothing found" },
		state: { type: String, default: "" },
		help: { type: String, default: "" }
	},
	emits: ["update:modelValue", "select"],
	setup(props, { attrs, emit }) {
		const open = ref(false);
		const focused = ref(-1);
		const inputRef = ref(null);
		const normalized = computed(() => props.options.map(option => typeof option === "object" ? option : {
			value: option,
			label: option
		}));
		const query = computed(() => String(props.modelValue ?? "").toLowerCase());
		const filtered = computed(() => normalized.value.filter(option => String(option.label).toLowerCase().includes(query.value)));
		const select = option => {
			if(!option) {
				return;
			}

			emit("update:modelValue", option.label);
			emit("select", option);
			open.value = false;
			focused.value = -1;
		};
		const move = direction => {
			if(!filtered.value.length) {
				return;
			}

			open.value = true;
			focused.value = (focused.value + direction + filtered.value.length) % filtered.value.length;
			nextTick(() => {
				const container = inputRef.value?.closest(".form-group")?.querySelector(".advanced-select");
				container?.querySelector(".option.focus")?.scrollIntoView({ block: "nearest" });
			});
		};
		const onKeydown = event => {
			if(event.key === "ArrowDown") {
				event.preventDefault();
				move(1);
			} else if(event.key === "ArrowUp") {
				event.preventDefault();
				move(-1);
			} else if(event.key === "Enter") {
				event.preventDefault();
				select(filtered.value[focused.value]);
			} else if(event.key === "Escape") {
				open.value = false;
				focused.value = -1;
			}
		};

		return () => h("div", { class: "form-group" }, [
			h("label", { class: cx("label", props.state) }, [
				props.label,
				iconNode(props.icon),
				h("input", {
					...attrs,
					ref: inputRef,
					type: "text",
					value: props.modelValue,
					placeholder: props.placeholder,
					autocomplete: "off",
					class: cx("input", attrs.class),
					onFocus: () => {
						open.value = true;
					},
					onBlur: () => {
						setTimeout(() => {
							open.value = false;
						}, 120);
					},
					onInput: event => {
						focused.value = -1;
						open.value = true;
						emit("update:modelValue", eventValue(event));
					},
					onKeydown
				})
			]),
			h("div", { class: "advanced-select-container" }, [
				h("div", { class: cx("advanced-select", { "a-show": open.value }) }, [
					h("div", { class: "popup-options-container" }, [
						h("div", { class: cx("not-found", { show: !filtered.value.length }) }, props.notFoundText),
						h("div", { class: cx("options", { show: filtered.value.length }) }, filtered.value.map((option, index) => h("div", {
							class: cx("option", { focus: index === focused.value }),
							"data-value": option.value,
							"data-display-value": option.label,
							onMousedown: event => {
								event.preventDefault();
								select(option);
							}
						}, option.label)))
					])
				])
			]),
			props.help && h("div", { class: cx("input-info", props.state === "error" && "error") }, props.help)
		]);
	}
});