vue-multiselect-dropdown
Version:
A reusable Vue dropdown component with multi-select and search support
259 lines (258 loc) • 12.2 kB
JavaScript
const style = document.createElement('style'); style.innerHTML = ".dropdown-container[data-v-618a1610]{position:relative;width:100%;max-width:400px;font-family:Inter,-apple-system,BlinkMacSystemFont,sans-serif}.dropdown-label[data-v-618a1610]{display:block;margin-bottom:8px;font-size:14px;font-weight:500;color:#374151;line-height:1.5}.required[data-v-618a1610]{color:#ef4444}.dropdown-input-container[data-v-618a1610]{display:flex;flex-wrap:wrap;align-items:center;gap:6px;background-color:#fff;border:1px solid #d1d5db;border-radius:8px;padding:10px 14px;cursor:pointer;transition:all .2s ease;min-height:44px}.dropdown-input-container[data-v-618a1610]:hover{border-color:#9ca3af}.dropdown-input-container.dropdown-focused[data-v-618a1610],.dropdown-input-container.dropdown-open[data-v-618a1610]{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f626}.dropdown-input-container.dropdown-error[data-v-618a1610]{border-color:#ef4444}.tag-item[data-v-618a1610]{display:inline-flex;align-items:center;background-color:#e0e7ff;color:#1d4ed8;font-size:13px;font-weight:500;border-radius:6px;padding:4px 8px;transition:all .2s ease}.tag-item[data-v-618a1610]:hover{background-color:#d0d7ff}.tag-remove[data-v-618a1610]{display:flex;align-items:center;justify-content:center;margin-left:6px;color:#3b82f6;background:none;border:none;cursor:pointer;padding:0;width:16px;height:16px;border-radius:4px;transition:all .2s ease}.tag-remove[data-v-618a1610]:hover{color:#1e40af;background-color:#3b82f61a}.dropdown-input[data-v-618a1610]{flex:1;border:none;outline:none;background:transparent;font-size:14px;color:#111827;min-width:50px;padding:0;margin:0;height:24px}.dropdown-input[data-v-618a1610]::placeholder{color:#9ca3af}.selected-text[data-v-618a1610]{font-size:14px;color:#111827;flex:1}.placeholder-text[data-v-618a1610]{color:#9ca3af;font-size:14px;flex:1}.dropdown-icon[data-v-618a1610]{display:flex;align-items:center;justify-content:center;color:#6b7280;transition:transform .2s ease}.dropdown-open .dropdown-icon[data-v-618a1610]{transform:rotate(180deg)}.error-message[data-v-618a1610]{margin-top:6px;font-size:13px;color:#ef4444;line-height:1.5}.dropdown-enter-active[data-v-618a1610],.dropdown-leave-active[data-v-618a1610]{transition:opacity .2s ease,transform .2s ease}.dropdown-enter-from[data-v-618a1610],.dropdown-leave-to[data-v-618a1610]{opacity:0;transform:translateY(-8px)}.dropdown-list[data-v-618a1610]{position:absolute;z-index:50;margin-top:8px;width:100%;background-color:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:240px;overflow-y:auto;padding:8px 0}.dropdown-item[data-v-618a1610]{display:flex;align-items:center;padding:10px 16px;font-size:14px;color:#374151;cursor:pointer;transition:all .15s ease}.dropdown-item[data-v-618a1610]:hover,.dropdown-item.highlighted[data-v-618a1610]{background-color:#f3f4f6}.dropdown-item.selected[data-v-618a1610]{background-color:#f9fafb;color:#1d4ed8}.custom-checkbox[data-v-618a1610]{display:flex;align-items:center;justify-content:center;width:16px;height:16px;border:1px solid #d1d5db;border-radius:4px;margin-right:12px;transition:all .2s ease}.custom-checkbox.checked[data-v-618a1610]{background-color:#3b82f6;border-color:#3b82f6;color:#fff}.option-text[data-v-618a1610]{flex:1}.no-results[data-v-618a1610]{padding:12px 16px;font-size:14px;color:#6b7280;text-align:center}\n"; document.head.appendChild(style);import { createElementBlock as r, openBlock as n, createCommentVNode as h, createElementVNode as d, createVNode as g, createTextVNode as m, toDisplayString as u, normalizeClass as a, withDirectives as y, Fragment as f, renderList as w, withModifiers as k, vModelText as v, Transition as x, withCtx as O } from "vue";
const I = (e, t) => {
const s = e.__vccOpts || e;
for (const [p, i] of t)
s[p] = i;
return s;
}, _ = {
name: "MultiselectCustomDropDown",
props: {
modelValue: {
type: [Array, Object, String, Number, null],
default: () => []
},
options: { type: Array, default: () => [] },
multiple: { type: Boolean, default: !1 },
searchable: { type: Boolean, default: !1 },
placeholder: { type: String, default: "Select an option" },
label: { type: String, default: "" },
required: { type: Boolean, default: !1 },
error: { type: String, default: "" },
labelKey: { type: String, default: "name" },
valueKey: { type: String, default: "value" }
},
data() {
return {
dropdownOpen: !1,
searchTerm: "",
filteredOptions: [],
selectedItems: [],
highlightedIndex: -1,
isFocused: !1
};
},
computed: {
inputPlaceholder() {
return this.multiple && this.selectedItems.length > 0 ? "" : this.selectedItems.length === 0 ? this.placeholder : "";
}
},
watch: {
modelValue: {
immediate: !0,
handler(e) {
this.selectedItems = this.multiple ? Array.isArray(e) ? e : [] : e ? [e] : [];
}
},
options: {
immediate: !0,
handler(e) {
this.filteredOptions = Array.isArray(e) ? e : [];
}
},
dropdownOpen(e) {
e && (this.highlightedIndex = -1, this.$nextTick(() => {
this.searchable && this.$el.querySelector(".dropdown-input") && this.$el.querySelector(".dropdown-input").focus();
}));
}
},
methods: {
toggleDropdown() {
this.dropdownOpen = !this.dropdownOpen;
},
filterOptions() {
this.filteredOptions = this.options.filter(
(e) => e[this.labelKey].toLowerCase().includes(this.searchTerm.toLowerCase())
);
},
selectOption(e) {
if (this.multiple) {
const t = this.selectedItems.findIndex((s) => s[this.valueKey] === e[this.valueKey]);
t === -1 ? this.selectedItems.push(e) : this.selectedItems.splice(t, 1), this.$emit("update:modelValue", this.selectedItems);
} else
this.selectedItems = [e], this.$emit("update:modelValue", e), this.dropdownOpen = !1;
},
isSelected(e) {
return this.selectedItems.some((t) => t[this.valueKey] === e[this.valueKey]);
},
removeItem(e) {
this.selectedItems.splice(e, 1), this.$emit("update:modelValue", this.selectedItems);
},
handleClickOutside(e) {
this.$refs.dropdownRef && !this.$refs.dropdownRef.contains(e.target) && (this.dropdownOpen = !1);
},
onFocus() {
this.isFocused = !0, this.dropdownOpen = !0;
},
onBlur() {
this.isFocused = !1;
},
handleKeyDown(e) {
if (this.dropdownOpen)
switch (e.key) {
case "ArrowDown":
e.preventDefault(), this.highlightedIndex = Math.min(this.highlightedIndex + 1, this.filteredOptions.length - 1);
break;
case "ArrowUp":
e.preventDefault(), this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);
break;
case "Enter":
this.highlightedIndex >= 0 && this.highlightedIndex < this.filteredOptions.length && this.selectOption(this.filteredOptions[this.highlightedIndex]);
break;
case "Escape":
this.dropdownOpen = !1;
break;
}
}
},
mounted() {
document.addEventListener("click", this.handleClickOutside), document.addEventListener("keydown", this.handleKeyDown);
},
beforeUnmount() {
document.removeEventListener("click", this.handleClickOutside), document.removeEventListener("keydown", this.handleKeyDown);
}
}, b = {
class: "dropdown-container",
ref: "dropdownRef"
}, C = {
key: 0,
class: "dropdown-label"
}, K = {
key: 0,
class: "required"
}, D = ["onClick"], S = ["placeholder"], L = {
key: 2,
class: "selected-text"
}, B = {
key: 3,
class: "placeholder-text"
}, M = {
key: 1,
class: "error-message"
}, F = {
key: 0,
class: "dropdown-list"
}, A = ["onClick", "onMouseenter"], E = {
key: 0,
width: "12",
height: "12",
viewBox: "0 0 24 24",
fill: "none",
xmlns: "http://www.w3.org/2000/svg"
}, T = { class: "option-text" }, N = {
key: 0,
class: "no-results"
};
function V(e, t, s, p, i, o) {
return n(), r("div", b, [
s.label ? (n(), r("label", C, [
m(u(s.label) + " ", 1),
s.required ? (n(), r("span", K, "*")) : h("", !0)
])) : h("", !0),
d("div", {
class: a(["dropdown-input-container", {
"dropdown-open": i.dropdownOpen,
"dropdown-error": s.error,
"dropdown-focused": i.isFocused
}]),
onClick: t[4] || (t[4] = (...l) => o.toggleDropdown && o.toggleDropdown(...l))
}, [
s.multiple ? (n(!0), r(f, { key: 0 }, w(i.selectedItems, (l, c) => (n(), r("span", {
key: l[s.valueKey],
class: "tag-item"
}, [
m(u(l[s.labelKey]) + " ", 1),
d("button", {
onClick: k((q) => o.removeItem(c), ["stop"]),
class: "tag-remove"
}, t[5] || (t[5] = [
d("svg", {
width: "8",
height: "8",
viewBox: "0 0 24 24",
fill: "none",
xmlns: "http://www.w3.org/2000/svg"
}, [
d("path", {
d: "M18 6L6 18",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round"
}),
d("path", {
d: "M6 6L18 18",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round"
})
], -1)
]), 8, D)
]))), 128)) : h("", !0),
s.searchable ? y((n(), r("input", {
key: 1,
type: "text",
"onUpdate:modelValue": t[0] || (t[0] = (l) => i.searchTerm = l),
onFocus: t[1] || (t[1] = (...l) => o.onFocus && o.onFocus(...l)),
onBlur: t[2] || (t[2] = (...l) => o.onBlur && o.onBlur(...l)),
onInput: t[3] || (t[3] = (...l) => o.filterOptions && o.filterOptions(...l)),
placeholder: o.inputPlaceholder,
class: "dropdown-input"
}, null, 40, S)), [
[v, i.searchTerm]
]) : !s.multiple && i.selectedItems.length > 0 ? (n(), r("span", L, u(i.selectedItems[0][s.labelKey]), 1)) : !s.multiple && i.selectedItems.length === 0 ? (n(), r("span", B, u(s.placeholder), 1)) : h("", !0),
t[6] || (t[6] = d("div", { class: "dropdown-icon" }, [
d("svg", {
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
xmlns: "http://www.w3.org/2000/svg"
}, [
d("path", {
d: "M6 9L12 15L18 9",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round"
})
])
], -1))
], 2),
s.error ? (n(), r("div", M, u(s.error), 1)) : h("", !0),
g(x, { name: "dropdown" }, {
default: O(() => [
i.dropdownOpen ? (n(), r("ul", F, [
(n(!0), r(f, null, w(i.filteredOptions, (l) => (n(), r("li", {
key: l[s.valueKey],
onClick: (c) => o.selectOption(l),
class: a({
"dropdown-item": !0,
selected: o.isSelected(l),
highlighted: i.highlightedIndex === i.filteredOptions.indexOf(l)
}),
onMouseenter: (c) => i.highlightedIndex = i.filteredOptions.indexOf(l)
}, [
s.multiple ? (n(), r("span", {
key: 0,
class: a(["custom-checkbox", { checked: o.isSelected(l) }])
}, [
o.isSelected(l) ? (n(), r("svg", E, t[7] || (t[7] = [
d("path", {
d: "M20 6L9 17L4 12",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round"
}, null, -1)
]))) : h("", !0)
], 2)) : h("", !0),
d("span", T, u(l[s.labelKey]), 1)
], 42, A))), 128)),
i.filteredOptions.length === 0 ? (n(), r("li", N, " No results found ")) : h("", !0)
])) : h("", !0)
]),
_: 1
})
], 512);
}
const U = /* @__PURE__ */ I(_, [["render", V], ["__scopeId", "data-v-618a1610"]]);
export {
U as default
};