Skip to content

Commit a40f39c

Browse files
committed
Merge branch 'main' of github.com:devforth/adminforth
2 parents bbc57b1 + 5978f3d commit a40f39c

4 files changed

Lines changed: 155 additions & 6 deletions

File tree

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

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';

live-demo/app/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

live-demo/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"author": "",
1717
"description": "",
1818
"dependencies": {
19-
"@adminforth/agent": "^1.24.4",
19+
"@adminforth/agent": "^1.26.2",
2020
"@adminforth/audit-log": "^1.9.12",
2121
"@adminforth/bulk-ai-flow": "^1.23.14",
2222
"@adminforth/chat-gpt": "^1.0.20",

0 commit comments

Comments
 (0)