1+ <template >
2+ <div class =" afcl-select afcl-select-wrapper relative inline-block af-button-shadow rounded" ref =" internalSelect"
3+ :class =" {'opacity-50': readonly}"
4+ >
5+ <div class =" relative" >
6+ <button
7+ ref =" inputEl"
8+ type =" button"
9+ @click =" inputClick"
10+ class =" group block w-full pl-3 pr-10 text-left cursor-pointer
11+ text-sm font-medium transition-all rounded border af-button-shadow outline-none
12+ bg-lightListViewButtonBackground text-lightListViewButtonText border-lightListViewButtonBorder
13+ dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:border-darkListViewButtonBorder
14+ hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover
15+ dark:hover:text-darkListViewButtonTextHover dark:hover:bg-darkListViewButtonBackgroundHover"
16+ :class =" classesForInput"
17+ >
18+ <span v-if =" displayLabel" >
19+ {{ displayLabel }}
20+ </span >
21+ <span
22+ v-else
23+ class =" opacity-100 transition-colors"
24+ :class =" [
25+ 'text-lightListViewButtonText dark:text-darkListViewButtonText',
26+ 'group-hover:text-lightListViewButtonTextHover dark:group-hover:text-darkListViewButtonTextHover'
27+ ]"
28+ >
29+ {{ filter?.name || placeholder || $t('Select...') }}
30+ </span >
31+ </button >
32+
33+ <div class =" absolute inset-y-0 right-2 flex items-center pointer-events-none" >
34+ <IconCaretDownSolid class =" h-5 w-5 text-lightPrimary dark:text-darkPrimary opacity-50 transition duration-150 ease-in"
35+ :class =" { 'transform rotate-180': showDropdown }"
36+ />
37+ </div >
38+ </div >
39+
40+ <teleport to =" body" v-if =" teleportToBody && showDropdown" >
41+ <div
42+ ref =" dropdownEl"
43+ :style =" getDropdownPosition"
44+ class =" fixed z-[1000] bg-lightDropdownOptionsBackground shadow-lg dark:shadow-black dark:bg-darkDropdownOptionsBackground
45+ dark:border-gray-600 rounded-md text-base ring-1 ring-black ring-opacity-5 overflow-hidden focus:outline-none sm:text-sm max-h-64 flex flex-col"
46+ >
47+ <div class =" py-1 overflow-y-auto grow" @scroll =" handleDropdownScroll" >
48+ <div
49+ v-for =" item in options"
50+ :key =" item.value"
51+ class =" px-4 py-2 cursor-pointer hover:bg-lightDropdownOptionsHoverBackground dark:hover:bg-darkDropdownOptionsHoverBackground text-lightDropdownOptionsText dark:text-darkDropdownOptionsText"
52+ :class =" { 'bg-lightDropdownPicked dark:bg-darkDropdownPicked': isItemSelected(item) }"
53+ @click =" toggleItem(item)"
54+ >
55+ <slot name =" item" :option =" item" >
56+ {{ item.label }}
57+ </slot >
58+ </div >
59+
60+ <div v-if =" !options?.length" class =" px-4 py-2 text-gray-500 italic text-center" >
61+ {{ $t('No items here') }}
62+ </div >
63+
64+ <div
65+ v-if =" modelValue !== null && modelValue !== undefined && modelValue !== ''"
66+ class =" px-4 py-2 cursor-pointer hover:bg-lightDropdownOptionsHoverBackground dark:hover:bg-darkDropdownOptionsHoverBackground text-lightDropdownOptionsText dark:text-darkDropdownOptionsText"
67+ @click =" clearSelection"
68+ >
69+ {{ $t('Clear selection') }}
70+ </div >
71+ </div >
72+ </div >
73+ </teleport >
74+ </div >
75+ </template >
76+
77+ <script setup lang="ts">
78+ import { ref , computed , onMounted , onUnmounted , type PropType } from ' vue' ;
79+ import { IconCaretDownSolid } from ' @iconify-prerendered/vue-flowbite' ;
80+
81+ const props = defineProps ({
82+ filter: {
83+ type: Object as PropType <{ name: string ; enum: any [] }>,
84+ default: null ,
85+ },
86+ options: {
87+ type: Array as PropType <{label: string , value: any }[]>,
88+ default : () => [],
89+ },
90+ modelValue: [String , Number , Boolean , Array ] as PropType <any >,
91+ placeholder: String ,
92+ readonly: Boolean ,
93+ teleportToBody: Boolean ,
94+ classesForInput: String ,
95+ });
96+
97+ const emit = defineEmits ([' update:modelValue' , ' scroll-near-end' ]);
98+
99+ const showDropdown = ref (false );
100+ const inputEl = ref <HTMLElement | null >(null );
101+ const dropdownEl = ref <HTMLElement | null >(null );
102+ const internalSelect = ref <HTMLElement | null >(null );
103+
104+ const displayLabel = computed (() => {
105+ const selected = props .options .find (o => o .value === props .modelValue );
106+ return selected ? selected .label : ' ' ;
107+ });
108+
109+ const isItemSelected = (item : any ) => props .modelValue === item .value ;
110+
111+ const toggleItem = (item : any ) => {
112+ emit (' update:modelValue' , item .value );
113+ showDropdown .value = false ;
114+ };
115+
116+ const clearSelection = () => {
117+ emit (' update:modelValue' , null );
118+ showDropdown .value = false ;
119+ };
120+
121+ const inputClick = () => {
122+ if (! props .readonly ) showDropdown .value = ! showDropdown .value ;
123+ };
124+
125+ const getDropdownPosition = computed (() => {
126+ if (! inputEl .value ) return {};
127+ const rect = inputEl .value .getBoundingClientRect ();
128+ return {
129+ top: ` ${rect .bottom + window .scrollY + 4 }px ` ,
130+ left: ` ${rect .left + window .scrollX }px ` ,
131+ width: ` ${rect .width }px `
132+ };
133+ });
134+
135+ const handleDropdownScroll = (event : Event ) => {
136+ const target = event .target as HTMLElement ;
137+ if (target .scrollTop + target .clientHeight >= target .scrollHeight - 10 ) {
138+ emit (' scroll-near-end' );
139+ }
140+ };
141+
142+ const handleClickOutside = (event : MouseEvent ) => {
143+ const target = event .target as HTMLElement ;
144+ if (internalSelect .value ?.contains (target )) return ;
145+ if (dropdownEl .value ?.contains (target )) return ;
146+
147+ showDropdown .value = false ;
148+ };
149+
150+ onMounted (() => {
151+ document .addEventListener (' click' , handleClickOutside );
152+ });
153+
154+ onUnmounted (() => {
155+ document .removeEventListener (' click' , handleClickOutside );
156+ });
157+ </script >
0 commit comments