Teren-app/resources/js/Components/app/ui/AppMultiSelect.vue

174 lines
5.4 KiB
Vue

<script setup>
import { ref, computed, watch } from "vue";
import { Popover, PopoverTrigger, PopoverContent } from "@/Components/ui/popover";
import {
Command,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
} from "@/Components/ui/command";
import { Button } from "@/Components/ui/button";
import { Badge } from "@/Components/ui/badge";
import { Check, ChevronsUpDown, X } from "lucide-vue-next";
const props = defineProps({
modelValue: { type: Array, default: () => [] },
items: { type: Array, default: () => [] }, // [{ value, label }]
placeholder: { type: String, default: "Izberi..." },
searchPlaceholder: { type: String, default: "Išči..." },
emptyText: { type: String, default: "Ni zadetkov." },
disabled: { type: Boolean, default: false },
max: { type: Number, default: null },
clearable: { type: Boolean, default: true },
contentClass: { type: String, default: "p-0 w-[300px]" },
showSelectedChips: { type: Boolean, default: true },
chipVariant: { type: String, default: "secondary" },
});
const emit = defineEmits(["update:modelValue"]);
const open = ref(false);
const query = ref("");
const internal = ref([...props.modelValue]);
watch(
() => props.modelValue,
(val) => {
if (!Array.isArray(val)) return;
internal.value = [...val];
}
);
const valueSet = computed(() => new Set(internal.value.map(String)));
const filteredItems = computed(() => {
const q = query.value.trim().toLowerCase();
if (!q) return props.items;
return props.items.filter((i) => i.label.toLowerCase().includes(q));
});
function toggle(value) {
if (props.disabled) return;
const v = String(value);
const set = new Set(internal.value.map(String));
if (set.has(v)) {
set.delete(v);
} else {
if (props.max && set.size >= props.max) return;
set.add(v);
}
internal.value = Array.from(set);
emit("update:modelValue", internal.value);
// Clear search so full list remains visible after selection
query.value = "";
}
function removeChip(value) {
const v = String(value);
internal.value = internal.value.filter((x) => String(x) !== v);
emit("update:modelValue", internal.value);
}
function clearAll() {
if (!props.clearable || props.disabled) return;
internal.value = [];
emit("update:modelValue", internal.value);
}
const summaryText = computed(() => {
if (internal.value.length === 0) return props.placeholder;
if (!props.showSelectedChips) return `${internal.value.length} izbranih`;
const labels = internal.value.map((v) => {
const found = props.items.find((i) => String(i.value) === String(v));
return found?.label || v;
});
if (labels.length <= 3) return labels.join(', ');
const firstThree = labels.slice(0, 3).join(', ');
const remaining = labels.length - 3;
return `${firstThree}, … +${remaining}`; // show ellipsis and remaining count
});
</script>
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:disabled="disabled"
class="w-full justify-between gap-2"
>
<span
class="truncate"
:class="{ 'text-muted-foreground': internal.length === 0 }"
>
{{ summaryText }}
</span>
<ChevronsUpDown class="h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent :class="contentClass">
<Command>
<CommandInput v-model="query" :placeholder="searchPlaceholder" />
<CommandList>
<CommandEmpty class="px-3 py-2 text-sm text-muted-foreground">{{
emptyText
}}</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="item in filteredItems"
:key="item.value"
:value="String(item.value)"
@select="() => toggle(item.value)"
class="flex items-center gap-2 px-2 py-1.5 rounded-sm cursor-pointer hover:bg-accent hover:text-accent-foreground"
>
<Check
class="h-4 w-4"
:class="valueSet.has(String(item.value)) ? 'opacity-100' : 'opacity-0'"
/>
<span class="flex-1">{{ item.label }}</span>
</CommandItem>
</CommandGroup>
</CommandList>
<div class="border-t p-2 flex items-center justify-between gap-2">
<Button
v-if="clearable"
variant="outline"
size="sm"
:disabled="internal.length === 0 || disabled"
@click="clearAll"
>Počisti</Button
>
<Button size="sm" :disabled="disabled" @click="open = false">Zapri</Button>
</div>
<div
v-if="showSelectedChips && internal.length"
class="border-t p-2 flex flex-wrap gap-1"
>
<Badge
v-for="val in internal"
:key="val"
:variant="chipVariant"
class="flex items-center gap-1"
>
<span class="truncate max-w-[140px]">
{{ items.find((i) => String(i.value) === String(val))?.label || val }}
</span>
<button
type="button"
class="hover:text-foreground/80"
@click.stop="removeChip(val)"
:title="'Odstrani'"
>
<X class="h-3 w-3" />
</button>
</Badge>
</div>
</Command>
</PopoverContent>
</Popover>
</template>