@adminforth/quick-filters 2.2.18 → 4.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 +2 -3
- package/custom/FiltersArea.vue +6 -6
- package/custom/QickFiltersSelect.vue +26 -27
- package/custom/UniversalSearchInput.vue +18 -26
- package/dist/custom/FiltersArea.vue +6 -6
- package/dist/custom/QickFiltersSelect.vue +26 -27
- package/dist/custom/UniversalSearchInput.vue +18 -26
- package/package.json +2 -2
- package/custom/FilterDropdown.vue +0 -149
- package/dist/custom/FilterDropdown.vue +0 -149
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
|
|
15
|
-
total size is
|
|
13
|
+
sent 5,760 bytes received 115 bytes 11,750.00 bytes/sec
|
|
14
|
+
total size is 5,327 speedup is 0.91
|
package/custom/FiltersArea.vue
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
2
|
+
<!-- a narrow list header gives the filters a full row and fills it, a wide one keeps them in its single row -->
|
|
3
|
+
<div class="grid w-full grid-cols-[repeat(auto-fit,minmax(min(15rem,100%),1fr))] gap-2 @4xl/header:flex @4xl/header:w-auto @4xl/header:flex-wrap @4xl/header:items-center">
|
|
3
4
|
<template v-for="filter in meta.options" :key="filter.name" >
|
|
4
5
|
<UniversalSearchInput
|
|
5
6
|
v-if="filter.hasSearchInput"
|
|
@@ -9,11 +10,10 @@
|
|
|
9
10
|
}"
|
|
10
11
|
/>
|
|
11
12
|
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
</div>
|
|
13
|
+
<QickFiltersSelect
|
|
14
|
+
v-else
|
|
15
|
+
:filter="filter"
|
|
16
|
+
/>
|
|
17
17
|
</template>
|
|
18
18
|
</div>
|
|
19
19
|
|
|
@@ -1,46 +1,45 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
class="
|
|
4
|
-
|
|
2
|
+
<Select
|
|
3
|
+
class="w-full @4xl/header: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
|
-
|
|
10
|
+
/>
|
|
9
11
|
</template>
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
13
|
<script lang="ts" setup>
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
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
|
|
27
|
-
const selectOptions =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
43
|
-
})
|
|
44
|
-
})
|
|
41
|
+
value,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
45
44
|
|
|
46
|
-
</script>
|
|
45
|
+
</script>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="af-universal-search flex items-center gap-1">
|
|
2
|
+
<div class="af-universal-search flex w-full items-center gap-1 @4xl/header:w-auto">
|
|
3
3
|
<slot name="prefix" />
|
|
4
4
|
<!-- enter is handled on the wrapper so it fires once: Input spreads attrs on both its root and the input -->
|
|
5
|
-
<div class="w-64" @keyup.enter="applyImmediate">
|
|
5
|
+
<div class="min-w-0 flex-1 @4xl/header:w-64 @4xl/header:flex-none" @keyup.enter="applyImmediate">
|
|
6
6
|
<Input
|
|
7
7
|
v-model="localValue"
|
|
8
8
|
type="text"
|
|
@@ -10,13 +10,11 @@
|
|
|
10
10
|
:placeholder="props.meta?.placeholder ?? ''"
|
|
11
11
|
>
|
|
12
12
|
<template #rightIcon>
|
|
13
|
-
<
|
|
13
|
+
<IconX
|
|
14
14
|
v-if="localValue"
|
|
15
15
|
@click="clear"
|
|
16
|
-
class="
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
56
|
+
field,
|
|
65
57
|
operator: AdminForthFilterOperators.EQ,
|
|
66
58
|
value: term || '',
|
|
67
59
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
2
|
+
<!-- a narrow list header gives the filters a full row and fills it, a wide one keeps them in its single row -->
|
|
3
|
+
<div class="grid w-full grid-cols-[repeat(auto-fit,minmax(min(15rem,100%),1fr))] gap-2 @4xl/header:flex @4xl/header:w-auto @4xl/header:flex-wrap @4xl/header:items-center">
|
|
3
4
|
<template v-for="filter in meta.options" :key="filter.name" >
|
|
4
5
|
<UniversalSearchInput
|
|
5
6
|
v-if="filter.hasSearchInput"
|
|
@@ -9,11 +10,10 @@
|
|
|
9
10
|
}"
|
|
10
11
|
/>
|
|
11
12
|
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
</div>
|
|
13
|
+
<QickFiltersSelect
|
|
14
|
+
v-else
|
|
15
|
+
:filter="filter"
|
|
16
|
+
/>
|
|
17
17
|
</template>
|
|
18
18
|
</div>
|
|
19
19
|
|
|
@@ -1,46 +1,45 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
class="
|
|
4
|
-
|
|
2
|
+
<Select
|
|
3
|
+
class="w-full @4xl/header: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
|
-
|
|
10
|
+
/>
|
|
9
11
|
</template>
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
13
|
<script lang="ts" setup>
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
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
|
|
27
|
-
const selectOptions =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
43
|
-
})
|
|
44
|
-
})
|
|
41
|
+
value,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
45
44
|
|
|
46
|
-
</script>
|
|
45
|
+
</script>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="af-universal-search flex items-center gap-1">
|
|
2
|
+
<div class="af-universal-search flex w-full items-center gap-1 @4xl/header:w-auto">
|
|
3
3
|
<slot name="prefix" />
|
|
4
4
|
<!-- enter is handled on the wrapper so it fires once: Input spreads attrs on both its root and the input -->
|
|
5
|
-
<div class="w-64" @keyup.enter="applyImmediate">
|
|
5
|
+
<div class="min-w-0 flex-1 @4xl/header:w-64 @4xl/header:flex-none" @keyup.enter="applyImmediate">
|
|
6
6
|
<Input
|
|
7
7
|
v-model="localValue"
|
|
8
8
|
type="text"
|
|
@@ -10,13 +10,11 @@
|
|
|
10
10
|
:placeholder="props.meta?.placeholder ?? ''"
|
|
11
11
|
>
|
|
12
12
|
<template #rightIcon>
|
|
13
|
-
<
|
|
13
|
+
<IconX
|
|
14
14
|
v-if="localValue"
|
|
15
15
|
@click="clear"
|
|
16
|
-
class="
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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": "
|
|
3
|
+
"version": "4.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": "^
|
|
30
|
+
"adminforth": "^4.2.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>
|