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