@adminforth/quick-filters 2.1.1 → 2.2.1
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 +3 -2
- package/custom/FilterDropdown.vue +148 -0
- package/custom/FiltersArea.vue +2 -2
- package/custom/QickFiltersSelect.vue +5 -5
- package/dist/custom/FilterDropdown.vue +148 -0
- package/dist/custom/FiltersArea.vue +2 -2
- package/dist/custom/QickFiltersSelect.vue +5 -5
- package/package.json +3 -3
package/build.log
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
sending incremental file list
|
|
6
6
|
custom/
|
|
7
|
+
custom/FilterDropdown.vue
|
|
7
8
|
custom/FiltersArea.vue
|
|
8
9
|
custom/QickFiltersSelect.vue
|
|
9
10
|
custom/UniversalSearchInput.vue
|
|
10
11
|
custom/tsconfig.json
|
|
11
12
|
custom/types.ts
|
|
12
13
|
|
|
13
|
-
sent
|
|
14
|
-
total size is
|
|
14
|
+
sent 10,679 bytes received 134 bytes 21,626.00 bytes/sec
|
|
15
|
+
total size is 10,173 speedup is 0.94
|
|
@@ -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>
|
package/custom/FiltersArea.vue
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
<UniversalSearchInput
|
|
5
5
|
v-if="filter.hasSearchInput"
|
|
6
6
|
:meta="{
|
|
7
|
-
placeholder:
|
|
7
|
+
placeholder: `${filter.name}`,
|
|
8
8
|
filterField: filter.name
|
|
9
9
|
}"
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
|
-
<div
|
|
12
|
+
<div v-else>
|
|
13
13
|
<QickFiltersSelect
|
|
14
14
|
:filter="filter"
|
|
15
15
|
/>
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
class="
|
|
2
|
+
<FilterDropdown
|
|
3
|
+
class="text-sm"
|
|
4
|
+
:filter="filter as { name: string; enum: any[] }"
|
|
4
5
|
:options="selectOptions"
|
|
5
6
|
v-model="selected"
|
|
6
|
-
classesForInput="py-[6px] !text-sm bg-white rounded"
|
|
7
7
|
teleportToBody
|
|
8
|
-
></
|
|
8
|
+
></FilterDropdown>
|
|
9
9
|
</template>
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
<script lang="ts" setup>
|
|
14
14
|
import { ref, onMounted, watch } from 'vue';
|
|
15
|
-
import
|
|
15
|
+
import FilterDropdown from './FilterDropdown.vue'
|
|
16
16
|
import type { Filter } from './types';
|
|
17
17
|
import { useAdminforth } from '@/adminforth';
|
|
18
18
|
import { AdminForthFilterOperators } from '@/types/Common';
|
|
@@ -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>
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
<UniversalSearchInput
|
|
5
5
|
v-if="filter.hasSearchInput"
|
|
6
6
|
:meta="{
|
|
7
|
-
placeholder:
|
|
7
|
+
placeholder: `${filter.name}`,
|
|
8
8
|
filterField: filter.name
|
|
9
9
|
}"
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
|
-
<div
|
|
12
|
+
<div v-else>
|
|
13
13
|
<QickFiltersSelect
|
|
14
14
|
:filter="filter"
|
|
15
15
|
/>
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
class="
|
|
2
|
+
<FilterDropdown
|
|
3
|
+
class="text-sm"
|
|
4
|
+
:filter="filter as { name: string; enum: any[] }"
|
|
4
5
|
:options="selectOptions"
|
|
5
6
|
v-model="selected"
|
|
6
|
-
classesForInput="py-[6px] !text-sm bg-white rounded"
|
|
7
7
|
teleportToBody
|
|
8
|
-
></
|
|
8
|
+
></FilterDropdown>
|
|
9
9
|
</template>
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
<script lang="ts" setup>
|
|
14
14
|
import { ref, onMounted, watch } from 'vue';
|
|
15
|
-
import
|
|
15
|
+
import FilterDropdown from './FilterDropdown.vue'
|
|
16
16
|
import type { Filter } from './types';
|
|
17
17
|
import { useAdminforth } from '@/adminforth';
|
|
18
18
|
import { AdminForthFilterOperators } from '@/types/Common';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/quick-filters",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
"description": "",
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "latest",
|
|
20
|
-
"adminforth": "^2.
|
|
20
|
+
"adminforth": "^2.50.0",
|
|
21
21
|
"semantic-release": "^24.2.1",
|
|
22
22
|
"semantic-release-slack-bot": "^4.0.2",
|
|
23
23
|
"typescript": "^5.7.3"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"adminforth": "^2.
|
|
26
|
+
"adminforth": "^2.50.0"
|
|
27
27
|
},
|
|
28
28
|
"release": {
|
|
29
29
|
"plugins": [
|