172 lines
5.1 KiB
Vue
172 lines
5.1 KiB
Vue
<script setup lang="ts">
|
|
import { SelectRoot } from 'radix-vue'
|
|
import { watch } from 'vue'
|
|
import SelectContent from '~/components/pub/ui/select/SelectContent.vue'
|
|
import SelectGroup from '~/components/pub/ui/select/SelectGroup.vue'
|
|
import SelectItem from '~/components/pub/ui/select/SelectItem.vue'
|
|
import SelectLabel from '~/components/pub/ui/select/SelectLabel.vue'
|
|
import SelectSeparator from '~/components/pub/ui/select/SelectSeparator.vue'
|
|
import SelectTrigger from '~/components/pub/ui/select/SelectTrigger.vue'
|
|
import SelectValue from '~/components/pub/ui/select/SelectValue.vue'
|
|
import { cn } from '~/lib/utils'
|
|
|
|
export interface SelectItem {
|
|
value: string
|
|
label: string
|
|
code?: string
|
|
priority?: number // Priority untuk sorting: negatif = bawah, positif = atas, 0/undefined = normal sorting
|
|
}
|
|
|
|
const props = defineProps<{
|
|
modelValue?: string
|
|
items: SelectItem[]
|
|
placeholder?: string
|
|
label?: string
|
|
separator?: boolean
|
|
class?: string
|
|
isSelectedFirst?: boolean
|
|
preserveOrder?: boolean
|
|
isDisabled?: boolean
|
|
autoWidth?: boolean
|
|
autoFill?: boolean
|
|
// otherPlacement sudah tidak digunakan, diganti dengan priority system di Item interface
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string]
|
|
}>()
|
|
|
|
const processedItems = computed(() => {
|
|
const itemsWithSelection = props.items.map((item) => ({
|
|
...item,
|
|
isSelected: item.value === props.modelValue,
|
|
}))
|
|
|
|
// Jika preserveOrder true, kembalikan urutan array asli
|
|
if (props.preserveOrder) {
|
|
return itemsWithSelection
|
|
}
|
|
|
|
// Sorting dengan priority system
|
|
return itemsWithSelection.sort((a, b) => {
|
|
const aPriority = a.priority ?? 0
|
|
const bPriority = b.priority ?? 0
|
|
|
|
// Jika ada priority, sort berdasarkan priority (descending: tinggi ke rendah)
|
|
if (aPriority !== bPriority) {
|
|
return bPriority - aPriority
|
|
}
|
|
|
|
// Jika priority sama (termasuk 0/undefined), lakukan sorting normal
|
|
if (props.isSelectedFirst) {
|
|
if (a.isSelected && !b.isSelected) return -1
|
|
if (!a.isSelected && b.isSelected) return 1
|
|
}
|
|
|
|
return a.label.localeCompare(b.label)
|
|
})
|
|
})
|
|
|
|
// Computed property untuk menghitung width optimal berdasarkan konten terpanjang
|
|
const optimalWidth = computed(() => {
|
|
// Jika autoWidth false atau undefined, gunakan full width
|
|
if (!props.autoWidth) return '100%'
|
|
|
|
if (!props.items.length) return 'auto'
|
|
|
|
// Mencari label terpanjang
|
|
const longestLabel = props.items.reduce((longest, item) => {
|
|
const itemText = item.code ? `${item.label} ${item.code}` : item.label
|
|
return itemText.length > longest.length ? itemText : longest
|
|
}, '')
|
|
|
|
// Menghitung width berdasarkan panjang karakter (estimasi)
|
|
// Setiap karakter ~0.6em, ditambah padding dan space untuk icon
|
|
const estimatedWidth = Math.max(longestLabel.length * 0.6 + 3, 8) // minimum 8em
|
|
|
|
return `${Math.min(estimatedWidth, 25)}em` // maksimum 25em
|
|
})
|
|
|
|
function onValueChange(value: string) {
|
|
emit('update:modelValue', value)
|
|
}
|
|
|
|
// Auto fill logic - automatically select first item if autoFill is enabled
|
|
watch(
|
|
() => props.items,
|
|
(newItems) => {
|
|
if (props.autoFill && newItems.length > 0 && !props.modelValue) {
|
|
// Auto select first item only if no value is currently selected
|
|
const firstItem = newItems[0]
|
|
if (firstItem?.value) {
|
|
emit('update:modelValue', firstItem.value)
|
|
}
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<SelectRoot
|
|
:model-value="modelValue"
|
|
:disabled="isDisabled"
|
|
@update:model-value="onValueChange"
|
|
>
|
|
<SelectTrigger
|
|
:class="
|
|
cn(
|
|
'rounded-md focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
|
|
{
|
|
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
|
|
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,
|
|
'w-full': !autoWidth,
|
|
},
|
|
props.class,
|
|
)
|
|
"
|
|
:style="autoWidth ? { width: optimalWidth, minWidth: '8em' } : {}"
|
|
icon-name="i-radix-icons-chevron-down"
|
|
icon-class="text-gray-500 dark:text-gray-300"
|
|
>
|
|
<SelectValue
|
|
:placeholder="placeholder || 'Pilih item'"
|
|
:class="
|
|
cn('', {
|
|
'text-gray-400': !props.modelValue,
|
|
'text-black dark:text-white': props.modelValue,
|
|
'text-gray-500': isDisabled,
|
|
})
|
|
"
|
|
/>
|
|
</SelectTrigger>
|
|
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectLabel v-if="label">
|
|
{{ label }}
|
|
</SelectLabel>
|
|
|
|
<SelectItem
|
|
v-for="item in processedItems"
|
|
:key="item.value"
|
|
:value="item.value"
|
|
class="cursor-pointer hover:bg-primary hover:text-white focus:bg-primary focus:text-white"
|
|
>
|
|
<div class="flex w-full items-center justify-between">
|
|
<span>{{ item.label }}</span>
|
|
<span
|
|
v-if="item.code"
|
|
class="ml-2 text-xs text-muted-foreground"
|
|
>
|
|
{{ item.code }}
|
|
</span>
|
|
</div>
|
|
</SelectItem>
|
|
|
|
<SelectSeparator v-if="separator" />
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</SelectRoot>
|
|
</template>
|