- Implement dialog form with validation schema for division creation - Add combobox component for parent division selection - Include form submission handling with reset and error states - a11y
144 lines
5.1 KiB
Vue
144 lines
5.1 KiB
Vue
<script setup lang="ts">
|
|
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
|
|
import { refDebounced } from '@vueuse/core'
|
|
|
|
const props = defineProps<{
|
|
prep: HeaderPrep
|
|
modelValue?: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string]
|
|
'search': [value: string]
|
|
}>()
|
|
|
|
// Internal search state
|
|
const searchInput = ref(props.modelValue || '')
|
|
const debouncedSearch = refDebounced(searchInput, props.prep.refSearchNav?.debounceMs || 500)
|
|
|
|
// Computed search model for v-model
|
|
const searchModel = computed({
|
|
get: () => searchInput.value,
|
|
set: (value: string) => {
|
|
searchInput.value = value
|
|
emit('update:modelValue', value)
|
|
},
|
|
})
|
|
|
|
// Watch for external changes to modelValue
|
|
watch(() => props.modelValue, (newValue) => {
|
|
if (newValue !== searchInput.value) {
|
|
searchInput.value = newValue || ''
|
|
}
|
|
})
|
|
|
|
// Watch debounced search and emit search event
|
|
watch(debouncedSearch, (newValue) => {
|
|
const minLength = props.prep.refSearchNav?.minLength || 3
|
|
// Only search if meets minimum length or empty (to clear search)
|
|
if (newValue.length === 0 || newValue.length >= minLength) {
|
|
emit('search', newValue)
|
|
props.prep.refSearchNav?.onInput(newValue)
|
|
}
|
|
})
|
|
|
|
// Handle clear search
|
|
function clearSearch() {
|
|
searchModel.value = ''
|
|
props.prep.refSearchNav?.onClear()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<header>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<div class="ml-3 text-lg font-bold text-gray-900">
|
|
<Icon :name="props.prep.icon!" class="mr-2 size-4 md:size-6 align-middle" />
|
|
{{ props.prep.title }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<!-- Search Section -->
|
|
<div v-if="props.prep.refSearchNav" class="ml-3 text-lg text-gray-900 relative">
|
|
<div class="relative">
|
|
<Input
|
|
id="search-table"
|
|
v-model="searchModel"
|
|
name="search-table"
|
|
type="text"
|
|
class="w-full rounded-md border bg-white px-4 py-2 text-gray-900 sm:text-sm"
|
|
:class="[
|
|
props.prep.refSearchNav.inputClass,
|
|
{
|
|
'border-amber-300 bg-amber-50': searchInput.length > 0 && searchInput.length < (props.prep.refSearchNav.minLength || 3),
|
|
'border-green-300 bg-green-50': searchInput.length >= (props.prep.refSearchNav.minLength || 3),
|
|
},
|
|
]"
|
|
:placeholder="props.prep.refSearchNav.placeholder || 'Cari (min. 3 karakter)...'"
|
|
/>
|
|
|
|
<!-- Clear button -->
|
|
<button
|
|
v-if="searchInput.length > 0"
|
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
type="button"
|
|
@click="clearSearch"
|
|
>
|
|
<Icon name="i-lucide-x" class="h-4 w-4" />
|
|
</button>
|
|
|
|
<!-- Validation feedback -->
|
|
<div
|
|
v-if="props.prep.refSearchNav.showValidationFeedback !== false && searchInput.length > 0 && searchInput.length < (props.prep.refSearchNav.minLength || 3)"
|
|
class="absolute -bottom-6 left-0 text-xs text-amber-600 whitespace-nowrap"
|
|
>
|
|
Minimal {{ props.prep.refSearchNav.minLength || 3 }} karakter untuk mencari
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Button -->
|
|
<div v-if="props.prep.addNav" class="m-2 flex items-center">
|
|
<Button
|
|
class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm"
|
|
:class="props.prep.addNav.classVal"
|
|
:variant="(props.prep.addNav.variant as any) || 'default'"
|
|
@click="props.prep.addNav?.onClick"
|
|
>
|
|
<Icon :name="props.prep.addNav.icon || 'i-lucide-plus'" class="mr-2 h-4 w-4 align-middle" />
|
|
{{ props.prep.addNav.label }}
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Filter Button -->
|
|
<div v-if="props.prep.filterNav" class="m-2 flex items-center">
|
|
<Button
|
|
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
|
:class="props.prep.filterNav.classVal"
|
|
:variant="(props.prep.filterNav.variant as any) || 'default'"
|
|
@click="props.prep.filterNav?.onClick"
|
|
>
|
|
<Icon :name="props.prep.filterNav.icon || 'i-lucide-filter'" class="mr-2 h-4 w-4 align-middle" />
|
|
{{ props.prep.filterNav.label }}
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Print Button -->
|
|
<div v-if="props.prep.printNav" class="m-2 flex items-center">
|
|
<Button
|
|
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
|
:class="props.prep.printNav.classVal"
|
|
:variant="(props.prep.printNav.variant as any) || 'default'"
|
|
@click="props.prep.printNav?.onClick"
|
|
>
|
|
<Icon :name="props.prep.printNav.icon || 'i-lucide-printer'" class="mr-2 h-4 w-4 align-middle" />
|
|
{{ props.prep.printNav.label }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
</template>
|