Skip to content

Commit 63a809a

Browse files
committed
feat: add FilterDropdown component and update index export
1 parent 709c8b4 commit 63a809a

2 files changed

Lines changed: 159 additions & 1 deletion

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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>

adminforth/spa/src/afcl/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ export { default as DatePicker } from './DatePicker.vue';
2626
export { default as Textarea } from './Textarea.vue';
2727
export { default as ButtonGroup } from './ButtonGroup.vue';
2828
export { default as Card } from './Card.vue';
29-
export { default as Modal } from './Modal.vue';
29+
export { default as Modal } from './Modal.vue';
30+
export { default as FilterDropdown } from './FilterDropdown.vue';

0 commit comments

Comments
 (0)