@adminforth/quick-filters 2.2.18 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build.log CHANGED
@@ -4,12 +4,11 @@
4
4
 
5
5
  sending incremental file list
6
6
  custom/
7
- custom/FilterDropdown.vue
8
7
  custom/FiltersArea.vue
9
8
  custom/QickFiltersSelect.vue
10
9
  custom/UniversalSearchInput.vue
11
10
  custom/tsconfig.json
12
11
  custom/types.ts
13
12
 
14
- sent 10,905 bytes received 134 bytes 22,078.00 bytes/sec
15
- total size is 10,405 speedup is 0.94
13
+ sent 5,435 bytes received 115 bytes 11,100.00 bytes/sec
14
+ total size is 5,002 speedup is 0.90
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="flex flex-wrap gap-1">
2
+ <div class="flex flex-wrap items-center gap-2 gap-y-1">
3
3
  <template v-for="filter in meta.options" :key="filter.name" >
4
4
  <UniversalSearchInput
5
5
  v-if="filter.hasSearchInput"
@@ -9,11 +9,10 @@
9
9
  }"
10
10
  />
11
11
 
12
- <div v-else>
13
- <QickFiltersSelect
14
- :filter="filter"
15
- />
16
- </div>
12
+ <QickFiltersSelect
13
+ v-else
14
+ :filter="filter"
15
+ />
17
16
  </template>
18
17
  </div>
19
18
 
@@ -1,46 +1,45 @@
1
1
  <template>
2
- <FilterDropdown
3
- class="text-sm"
4
- :filter="filter as { name: string; enum: any[] }"
2
+ <Select
3
+ class="w-48"
4
+ classesForInput="af-filter-dropdown-button"
5
5
  :options="selectOptions"
6
+ :placeholder="filter.name"
6
7
  v-model="selected"
8
+ searchDisabled
7
9
  teleportToBody
8
- ></FilterDropdown>
10
+ />
9
11
  </template>
10
12
 
11
-
12
-
13
13
  <script lang="ts" setup>
14
- import { ref, onMounted, watch } from 'vue';
15
- import FilterDropdown from './FilterDropdown.vue'
14
+ import { computed } from 'vue';
15
+ import { Select } from '@/afcl';
16
16
  import type { Filter } from './types';
17
17
  import { useAdminforth } from '@/adminforth';
18
+ import { useFiltersStore } from '@/stores/filters';
18
19
  import { AdminForthFilterOperators } from '@/types/Common';
19
20
 
20
21
  const { list } = useAdminforth();
22
+ const filtersStore = useFiltersStore();
21
23
 
22
24
  const props = defineProps<{
23
25
  filter: Filter
24
26
  }>();
25
27
 
26
- const selected = ref(null);
27
- const selectOptions = ref<{ label: string, value: any }[]>([]);
28
-
29
- onMounted(() => {
30
- selectOptions.value = props.filter.enum.map(option => ({
31
- label: option.label,
32
- value: option.label
33
- }));
34
- });
35
-
36
-
37
- watch(selected, (newValue) => {
38
- console.log('Selected value changed:', newValue);
39
- list?.updateFilter?.({
40
- field: `_qf_${props.filter.name}`,
28
+ const field = `_qf_${props.filter.name}`;
29
+ const selectOptions = props.filter.enum!.map(option => ({
30
+ label: option.label,
31
+ value: option.label
32
+ }));
33
+
34
+ // the filters store is the source of truth: it is restored from the URL and emptied by "clear filters"
35
+ const selected = computed({
36
+ get: () => filtersStore.filters.find(f => f.field === field)?.value ?? null,
37
+ // Select emits undefined when the picked option is clicked again, updateFilter then removes the filter
38
+ set: (value) => list.updateFilter({
39
+ field,
41
40
  operator: AdminForthFilterOperators.EQ,
42
- value: newValue,
43
- });
44
- })
41
+ value,
42
+ }),
43
+ });
45
44
 
46
- </script>
45
+ </script>
@@ -10,13 +10,11 @@
10
10
  :placeholder="props.meta?.placeholder ?? ''"
11
11
  >
12
12
  <template #rightIcon>
13
- <p
13
+ <IconX
14
14
  v-if="localValue"
15
15
  @click="clear"
16
- class="hover:cursor-pointer hover:text-gray-600 dark:text-white dark:hover:text-gray-400"
17
- >
18
- ✕
19
- </p>
16
+ class="w-4 h-4 cursor-pointer text-lightInputIconColor hover:text-lightInputText dark:text-darkInputIconColor dark:hover:text-darkInputText transition-colors"
17
+ />
20
18
  </template>
21
19
  </Input>
22
20
  </div>
@@ -24,13 +22,12 @@
24
22
  </div>
25
23
  </template>
26
24
  <script setup lang="ts">
27
- import { ref, watch, onMounted } from 'vue';
25
+ import { ref, computed, watch } from 'vue';
28
26
  import adminforth from '@/adminforth';
29
27
  import { Input } from '@/afcl';
28
+ import { IconX } from '@iconify-prerendered/vue-lucide';
30
29
  import { AdminForthFilterOperators } from '@/types/Common';
31
- import { useRoute } from 'vue-router';
32
-
33
- const route = useRoute();
30
+ import { useFiltersStore } from '@/stores/filters';
34
31
 
35
32
  const props = defineProps<{
36
33
  meta?: {
@@ -39,29 +36,24 @@ const props = defineProps<{
39
36
  filterField: string,
40
37
  };
41
38
  }>();
42
- const localValue = ref('');
39
+ const filtersStore = useFiltersStore();
40
+ const field = `_universal_search_${props.meta?.filterField}`;
41
+ const appliedTerm = computed(() => filtersStore.filters.find(f => f.field === field)?.value ?? '');
42
+ const localValue = ref(appliedTerm.value);
43
43
  let t: any = null;
44
44
  let blockFilterUpdate = false;
45
45
 
46
- onMounted(() => {
47
- const filters = Object.keys(route.query).filter(k => k.startsWith('filter__')).map(k => {
48
- const [_, field, operator] = k.split('__');
49
- return {
50
- field,
51
- operator,
52
- value: JSON.parse(decodeURIComponent(route.query[k] as string))
53
- }
54
- });
55
- const isUniversalSearchFilterApplied = filters.find(f => f.field === `_universal_search_${props.meta?.filterField}`);
56
- if (isUniversalSearchFilterApplied) {
57
- localValue.value = isUniversalSearchFilterApplied.value;
46
+ // follow filter changes made outside of the input: restore from the URL, "clear filters"
47
+ watch(appliedTerm, (term) => {
48
+ if (term !== localValue.value.trim()) {
58
49
  blockFilterUpdate = true;
59
- };
50
+ localValue.value = term;
51
+ }
60
52
  });
61
53
 
62
54
  function send(term?: string) {
63
55
  adminforth?.list?.updateFilter?.({
64
- field: `_universal_search_${props.meta?.filterField}`,
56
+ field,
65
57
  operator: AdminForthFilterOperators.EQ,
66
58
  value: term || '',
67
59
  });
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="flex flex-wrap gap-1">
2
+ <div class="flex flex-wrap items-center gap-2 gap-y-1">
3
3
  <template v-for="filter in meta.options" :key="filter.name" >
4
4
  <UniversalSearchInput
5
5
  v-if="filter.hasSearchInput"
@@ -9,11 +9,10 @@
9
9
  }"
10
10
  />
11
11
 
12
- <div v-else>
13
- <QickFiltersSelect
14
- :filter="filter"
15
- />
16
- </div>
12
+ <QickFiltersSelect
13
+ v-else
14
+ :filter="filter"
15
+ />
17
16
  </template>
18
17
  </div>
19
18
 
@@ -1,46 +1,45 @@
1
1
  <template>
2
- <FilterDropdown
3
- class="text-sm"
4
- :filter="filter as { name: string; enum: any[] }"
2
+ <Select
3
+ class="w-48"
4
+ classesForInput="af-filter-dropdown-button"
5
5
  :options="selectOptions"
6
+ :placeholder="filter.name"
6
7
  v-model="selected"
8
+ searchDisabled
7
9
  teleportToBody
8
- ></FilterDropdown>
10
+ />
9
11
  </template>
10
12
 
11
-
12
-
13
13
  <script lang="ts" setup>
14
- import { ref, onMounted, watch } from 'vue';
15
- import FilterDropdown from './FilterDropdown.vue'
14
+ import { computed } from 'vue';
15
+ import { Select } from '@/afcl';
16
16
  import type { Filter } from './types';
17
17
  import { useAdminforth } from '@/adminforth';
18
+ import { useFiltersStore } from '@/stores/filters';
18
19
  import { AdminForthFilterOperators } from '@/types/Common';
19
20
 
20
21
  const { list } = useAdminforth();
22
+ const filtersStore = useFiltersStore();
21
23
 
22
24
  const props = defineProps<{
23
25
  filter: Filter
24
26
  }>();
25
27
 
26
- const selected = ref(null);
27
- const selectOptions = ref<{ label: string, value: any }[]>([]);
28
-
29
- onMounted(() => {
30
- selectOptions.value = props.filter.enum.map(option => ({
31
- label: option.label,
32
- value: option.label
33
- }));
34
- });
35
-
36
-
37
- watch(selected, (newValue) => {
38
- console.log('Selected value changed:', newValue);
39
- list?.updateFilter?.({
40
- field: `_qf_${props.filter.name}`,
28
+ const field = `_qf_${props.filter.name}`;
29
+ const selectOptions = props.filter.enum!.map(option => ({
30
+ label: option.label,
31
+ value: option.label
32
+ }));
33
+
34
+ // the filters store is the source of truth: it is restored from the URL and emptied by "clear filters"
35
+ const selected = computed({
36
+ get: () => filtersStore.filters.find(f => f.field === field)?.value ?? null,
37
+ // Select emits undefined when the picked option is clicked again, updateFilter then removes the filter
38
+ set: (value) => list.updateFilter({
39
+ field,
41
40
  operator: AdminForthFilterOperators.EQ,
42
- value: newValue,
43
- });
44
- })
41
+ value,
42
+ }),
43
+ });
45
44
 
46
- </script>
45
+ </script>
@@ -10,13 +10,11 @@
10
10
  :placeholder="props.meta?.placeholder ?? ''"
11
11
  >
12
12
  <template #rightIcon>
13
- <p
13
+ <IconX
14
14
  v-if="localValue"
15
15
  @click="clear"
16
- class="hover:cursor-pointer hover:text-gray-600 dark:text-white dark:hover:text-gray-400"
17
- >
18
- ✕
19
- </p>
16
+ class="w-4 h-4 cursor-pointer text-lightInputIconColor hover:text-lightInputText dark:text-darkInputIconColor dark:hover:text-darkInputText transition-colors"
17
+ />
20
18
  </template>
21
19
  </Input>
22
20
  </div>
@@ -24,13 +22,12 @@
24
22
  </div>
25
23
  </template>
26
24
  <script setup lang="ts">
27
- import { ref, watch, onMounted } from 'vue';
25
+ import { ref, computed, watch } from 'vue';
28
26
  import adminforth from '@/adminforth';
29
27
  import { Input } from '@/afcl';
28
+ import { IconX } from '@iconify-prerendered/vue-lucide';
30
29
  import { AdminForthFilterOperators } from '@/types/Common';
31
- import { useRoute } from 'vue-router';
32
-
33
- const route = useRoute();
30
+ import { useFiltersStore } from '@/stores/filters';
34
31
 
35
32
  const props = defineProps<{
36
33
  meta?: {
@@ -39,29 +36,24 @@ const props = defineProps<{
39
36
  filterField: string,
40
37
  };
41
38
  }>();
42
- const localValue = ref('');
39
+ const filtersStore = useFiltersStore();
40
+ const field = `_universal_search_${props.meta?.filterField}`;
41
+ const appliedTerm = computed(() => filtersStore.filters.find(f => f.field === field)?.value ?? '');
42
+ const localValue = ref(appliedTerm.value);
43
43
  let t: any = null;
44
44
  let blockFilterUpdate = false;
45
45
 
46
- onMounted(() => {
47
- const filters = Object.keys(route.query).filter(k => k.startsWith('filter__')).map(k => {
48
- const [_, field, operator] = k.split('__');
49
- return {
50
- field,
51
- operator,
52
- value: JSON.parse(decodeURIComponent(route.query[k] as string))
53
- }
54
- });
55
- const isUniversalSearchFilterApplied = filters.find(f => f.field === `_universal_search_${props.meta?.filterField}`);
56
- if (isUniversalSearchFilterApplied) {
57
- localValue.value = isUniversalSearchFilterApplied.value;
46
+ // follow filter changes made outside of the input: restore from the URL, "clear filters"
47
+ watch(appliedTerm, (term) => {
48
+ if (term !== localValue.value.trim()) {
58
49
  blockFilterUpdate = true;
59
- };
50
+ localValue.value = term;
51
+ }
60
52
  });
61
53
 
62
54
  function send(term?: string) {
63
55
  adminforth?.list?.updateFilter?.({
64
- field: `_universal_search_${props.meta?.filterField}`,
56
+ field,
65
57
  operator: AdminForthFilterOperators.EQ,
66
58
  value: term || '',
67
59
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/quick-filters",
3
- "version": "2.2.18",
3
+ "version": "3.0.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "typescript": "^5.7.3"
28
28
  },
29
29
  "peerDependencies": {
30
- "adminforth": "^3.7.1"
30
+ "adminforth": "^4.0.0"
31
31
  },
32
32
  "release": {
33
33
  "plugins": [
@@ -1,149 +0,0 @@
1
- <template>
2
- <div class="afcl-select afcl-select-wrapper relative inline-block af-button-shadow rounded-default" 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="af-filter-dropdown-button 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 border outline-none gap-x-2
12
- bg-lightListViewButtonBackground text-lightListViewButtonText border-lightListViewButtonBorder
13
- dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:border-darkListViewButtonBorder
14
- dark:hover:text-darkListViewButtonTextHover dark:hover:bg-darkListViewButtonBackgroundHover rounded-default
15
- hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover rounded-default"
16
- >
17
- <span v-if="displayLabel" class="whitespace-nowrap">
18
- {{ displayLabel }}
19
- </span>
20
- <span
21
- v-else
22
- class="opacity-100 transition-colors whitespace-nowrap"
23
- >
24
- {{ filter?.name || placeholder || $t('Select...') }}
25
- </span>
26
-
27
- <IconCaretDownSolid class="h-4 w-4 text-lightPrimary dark:text-darkPrimary transition duration-150 ease-in flex-shrink-0 brightness-150"
28
- :class="{ 'transform rotate-180': showDropdown }"
29
- />
30
- </button>
31
- </div>
32
-
33
- <teleport to="body" v-if="teleportToBody && showDropdown">
34
- <div
35
- ref="dropdownEl"
36
- :style="getDropdownPosition"
37
- class="fixed z-[1000] w-max min-w-fit bg-lightDropdownOptionsBackground shadow-lg dark:shadow-black dark:bg-darkDropdownOptionsBackground
38
- 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"
39
- >
40
- <div class="py-1 overflow-y-auto grow" @scroll="handleDropdownScroll">
41
- <div
42
- v-for="item in options"
43
- :key="item.value"
44
- class="px-4 py-2 cursor-pointer hover:bg-lightDropdownOptionsHoverBackground dark:hover:bg-darkDropdownOptionsHoverBackground text-lightDropdownOptionsText dark:text-darkDropdownOptionsText"
45
- :class="{ 'bg-lightDropdownPicked dark:bg-darkDropdownPicked': isItemSelected(item) }"
46
- @click="toggleItem(item)"
47
- >
48
- <slot name="item" :option="item">
49
- {{ item.label }}
50
- </slot>
51
- </div>
52
-
53
- <div v-if="!options?.length" class="px-4 py-2 text-gray-500 italic text-center">
54
- {{ $t('No items here') }}
55
- </div>
56
-
57
- <div
58
- v-if="modelValue !== null && modelValue !== undefined && modelValue !== ''"
59
- class="px-4 py-2 cursor-pointer hover:bg-lightDropdownOptionsHoverBackground dark:hover:bg-darkDropdownOptionsHoverBackground text-lightDropdownOptionsText dark:text-darkDropdownOptionsText"
60
- @click="clearSelection"
61
- >
62
- {{ $t('Clear selection') }}
63
- </div>
64
- </div>
65
- </div>
66
- </teleport>
67
- </div>
68
- </template>
69
-
70
- <script setup lang="ts">
71
- import { ref, computed, onMounted, onUnmounted, type PropType } from 'vue';
72
- import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
73
-
74
- const props = defineProps({
75
- filter: {
76
- type: Object as PropType<{ name: string; enum: any[] }>,
77
- default: null,
78
- },
79
- options: {
80
- type: Array as PropType<{label: string, value: any}[]>,
81
- default: () => [],
82
- },
83
- modelValue: [String, Number, Boolean, Array] as PropType<any>,
84
- placeholder: String,
85
- readonly: Boolean,
86
- teleportToBody: Boolean,
87
- });
88
-
89
- const emit = defineEmits(['update:modelValue', 'scroll-near-end']);
90
-
91
- const showDropdown = ref(false);
92
- const dropdownFilterEl = ref<HTMLElement | null>(null);
93
- const dropdownEl = ref<HTMLElement | null>(null);
94
- const internalSelect = ref<HTMLElement | null>(null);
95
-
96
- const displayLabel = computed(() => {
97
- const selected = props.options.find(o => o.value === props.modelValue);
98
- return selected ? selected.label : '';
99
- });
100
-
101
- const isItemSelected = (item: any) => props.modelValue === item.value;
102
-
103
- const toggleItem = (item: any) => {
104
- emit('update:modelValue', item.value);
105
- showDropdown.value = false;
106
- };
107
-
108
- const clearSelection = () => {
109
- emit('update:modelValue', null);
110
- showDropdown.value = false;
111
- };
112
-
113
- const dropdownClick = () => {
114
- if (!props.readonly) showDropdown.value = !showDropdown.value;
115
- };
116
-
117
- const getDropdownPosition = computed(() => {
118
- if (!dropdownFilterEl.value) return {};
119
- const rect = dropdownFilterEl.value.getBoundingClientRect();
120
- return {
121
- top: `${rect.bottom + window.scrollY + 4}px`,
122
- left: `${rect.right + window.scrollX - (dropdownEl.value?.offsetWidth || rect.width)}px`,
123
- minWidth: `${rect.width}px`
124
- };
125
- });
126
-
127
- const handleDropdownScroll = (event: Event) => {
128
- const target = event.target as HTMLElement;
129
- if (target.scrollTop + target.clientHeight >= target.scrollHeight - 10) {
130
- emit('scroll-near-end');
131
- }
132
- };
133
-
134
- const handleClickOutside = (event: MouseEvent) => {
135
- const target = event.target as HTMLElement;
136
- if (internalSelect.value?.contains(target)) return;
137
- if (dropdownEl.value?.contains(target)) return;
138
-
139
- showDropdown.value = false;
140
- };
141
-
142
- onMounted(() => {
143
- document.addEventListener('click', handleClickOutside);
144
- });
145
-
146
- onUnmounted(() => {
147
- document.removeEventListener('click', handleClickOutside);
148
- });
149
- </script>
@@ -1,149 +0,0 @@
1
- <template>
2
- <div class="afcl-select afcl-select-wrapper relative inline-block af-button-shadow rounded-default" 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="af-filter-dropdown-button 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 border outline-none gap-x-2
12
- bg-lightListViewButtonBackground text-lightListViewButtonText border-lightListViewButtonBorder
13
- dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:border-darkListViewButtonBorder
14
- dark:hover:text-darkListViewButtonTextHover dark:hover:bg-darkListViewButtonBackgroundHover rounded-default
15
- hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover rounded-default"
16
- >
17
- <span v-if="displayLabel" class="whitespace-nowrap">
18
- {{ displayLabel }}
19
- </span>
20
- <span
21
- v-else
22
- class="opacity-100 transition-colors whitespace-nowrap"
23
- >
24
- {{ filter?.name || placeholder || $t('Select...') }}
25
- </span>
26
-
27
- <IconCaretDownSolid class="h-4 w-4 text-lightPrimary dark:text-darkPrimary transition duration-150 ease-in flex-shrink-0 brightness-150"
28
- :class="{ 'transform rotate-180': showDropdown }"
29
- />
30
- </button>
31
- </div>
32
-
33
- <teleport to="body" v-if="teleportToBody && showDropdown">
34
- <div
35
- ref="dropdownEl"
36
- :style="getDropdownPosition"
37
- class="fixed z-[1000] w-max min-w-fit bg-lightDropdownOptionsBackground shadow-lg dark:shadow-black dark:bg-darkDropdownOptionsBackground
38
- 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"
39
- >
40
- <div class="py-1 overflow-y-auto grow" @scroll="handleDropdownScroll">
41
- <div
42
- v-for="item in options"
43
- :key="item.value"
44
- class="px-4 py-2 cursor-pointer hover:bg-lightDropdownOptionsHoverBackground dark:hover:bg-darkDropdownOptionsHoverBackground text-lightDropdownOptionsText dark:text-darkDropdownOptionsText"
45
- :class="{ 'bg-lightDropdownPicked dark:bg-darkDropdownPicked': isItemSelected(item) }"
46
- @click="toggleItem(item)"
47
- >
48
- <slot name="item" :option="item">
49
- {{ item.label }}
50
- </slot>
51
- </div>
52
-
53
- <div v-if="!options?.length" class="px-4 py-2 text-gray-500 italic text-center">
54
- {{ $t('No items here') }}
55
- </div>
56
-
57
- <div
58
- v-if="modelValue !== null && modelValue !== undefined && modelValue !== ''"
59
- class="px-4 py-2 cursor-pointer hover:bg-lightDropdownOptionsHoverBackground dark:hover:bg-darkDropdownOptionsHoverBackground text-lightDropdownOptionsText dark:text-darkDropdownOptionsText"
60
- @click="clearSelection"
61
- >
62
- {{ $t('Clear selection') }}
63
- </div>
64
- </div>
65
- </div>
66
- </teleport>
67
- </div>
68
- </template>
69
-
70
- <script setup lang="ts">
71
- import { ref, computed, onMounted, onUnmounted, type PropType } from 'vue';
72
- import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
73
-
74
- const props = defineProps({
75
- filter: {
76
- type: Object as PropType<{ name: string; enum: any[] }>,
77
- default: null,
78
- },
79
- options: {
80
- type: Array as PropType<{label: string, value: any}[]>,
81
- default: () => [],
82
- },
83
- modelValue: [String, Number, Boolean, Array] as PropType<any>,
84
- placeholder: String,
85
- readonly: Boolean,
86
- teleportToBody: Boolean,
87
- });
88
-
89
- const emit = defineEmits(['update:modelValue', 'scroll-near-end']);
90
-
91
- const showDropdown = ref(false);
92
- const dropdownFilterEl = ref<HTMLElement | null>(null);
93
- const dropdownEl = ref<HTMLElement | null>(null);
94
- const internalSelect = ref<HTMLElement | null>(null);
95
-
96
- const displayLabel = computed(() => {
97
- const selected = props.options.find(o => o.value === props.modelValue);
98
- return selected ? selected.label : '';
99
- });
100
-
101
- const isItemSelected = (item: any) => props.modelValue === item.value;
102
-
103
- const toggleItem = (item: any) => {
104
- emit('update:modelValue', item.value);
105
- showDropdown.value = false;
106
- };
107
-
108
- const clearSelection = () => {
109
- emit('update:modelValue', null);
110
- showDropdown.value = false;
111
- };
112
-
113
- const dropdownClick = () => {
114
- if (!props.readonly) showDropdown.value = !showDropdown.value;
115
- };
116
-
117
- const getDropdownPosition = computed(() => {
118
- if (!dropdownFilterEl.value) return {};
119
- const rect = dropdownFilterEl.value.getBoundingClientRect();
120
- return {
121
- top: `${rect.bottom + window.scrollY + 4}px`,
122
- left: `${rect.right + window.scrollX - (dropdownEl.value?.offsetWidth || rect.width)}px`,
123
- minWidth: `${rect.width}px`
124
- };
125
- });
126
-
127
- const handleDropdownScroll = (event: Event) => {
128
- const target = event.target as HTMLElement;
129
- if (target.scrollTop + target.clientHeight >= target.scrollHeight - 10) {
130
- emit('scroll-near-end');
131
- }
132
- };
133
-
134
- const handleClickOutside = (event: MouseEvent) => {
135
- const target = event.target as HTMLElement;
136
- if (internalSelect.value?.contains(target)) return;
137
- if (dropdownEl.value?.contains(target)) return;
138
-
139
- showDropdown.value = false;
140
- };
141
-
142
- onMounted(() => {
143
- document.addEventListener('click', handleClickOutside);
144
- });
145
-
146
- onUnmounted(() => {
147
- document.removeEventListener('click', handleClickOutside);
148
- });
149
- </script>