Newer
Older
<template>
<ol
ref="orderFieldListEl"
class="order-filter tw-flex tw-flex-col tw-gap-3 tw-p-0 tw-m-0 tw-w-min group"
>
<li
v-for="(sortOrders, sortField) in normalizedChoices"
:key="sortField"
class="tw-flex tw-gap-3 tw-items-center tw-transition-opacity group-hover:tw-opacity-100"
:class="{ 'tw-opacity-50': !selectedFieldNames.includes(sortField) }"
>
<button
type="button"
class="btn tw-cursor-grab tw-p-1 -tw-ml-1"
:class="{ 'tw-invisible': !selectedFieldNames.includes(sortField) }"
tabindex="-1"
data-drag-handle
>
<icon-system-uicons-drag-vertical />
</button>
<label
class="btn tw-my-0 -tw-ml-3 tw-mr-6 tw-whitespace-nowrap tw-gap-3 hocus:tw-bg-gray-100"
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
>
<input
type="checkbox"
:checked="selectedFieldNames.includes(sortField)"
@input="
handleOrderSelection(
sortField,
null,
($event?.target as HTMLInputElement)?.checked === false,
)
"
/>
<span>{{ t(`${translateBase}.${sortField}.name`) }}</span>
</label>
<RadioGroup
class="tw-whitespace-nowrap tw-ml-auto"
:name="sortField"
:choices="sortOrders"
:model-value="selectedOrder[sortField] ?? null"
@update:model-value="handleOrderSelection(sortField, $event)"
/>
</li>
</ol>
</template>
<script lang="ts" setup generic="T extends string">
import { useSortable } from '@vueuse/integrations/useSortable'
import { computed, ref } from 'vue'
import { useI18n } from '@/i18n'
import RadioGroup from '@/components/generic/RadioGroup.vue'
defineOptions({ compatConfig: { MODE: 3 } })
type OrderDirection = 'asc' | 'desc'
type OrderField = { name: T; directions: OrderDirection[] }
type Value = T | `-${T}`
const modelValue = defineModel<Value[]>({ required: true })
const props = defineProps<{
choices: (T | OrderField)[]
translateBase: string
}>()
const { t } = useI18n()
const orderFieldListEl = ref<HTMLOListElement>()
const orderFields = computed<OrderField[]>(() => {
return props.choices.map((choice) => normalizeOrderField(choice))
})
const selectedFieldNames = computed(
() => modelValue.value.map((value) => (value.startsWith('-') ? value.slice(1) : value)) as T[],
)
const deselectedFieldNames = computed(
() =>
orderFields.value
.map((field) => field.name)
.filter((fieldName) => !selectedFieldNames.value.includes(fieldName)) as T[],
)
useSortable(orderFieldListEl, modelValue, {
animation: 200,
ghostClass: 'order-filter-drag-ghost',
handle: '[data-drag-handle]',
})
const normalizedChoices = computed(() => {
const orderedFieldNames = [...selectedFieldNames.value, ...deselectedFieldNames.value] as T[]
const orderedOrderFields = orderedFieldNames.map(
(fieldName) => orderFields.value.find((field) => field.name === fieldName) as OrderField,
)
return Object.fromEntries(
orderedOrderFields.map((field) => {
return [
field.name,
field.directions.map((direction) => ({
value: direction,
label: createChoiceLabel(field.name, direction),
})),
]
}),
) as Record<T, { value: OrderDirection; label: string }[]>
})
const selectedOrder = computed(
() =>
Object.fromEntries(
modelValue.value.map((value) => {
const field = (value.startsWith('-') ? value.slice(1) : value) as T
return [field, value.startsWith('-') ? 'desc' : 'asc']
}),
) as Record<T, OrderDirection>,
)
function handleOrderSelection(fieldName: T, direction: OrderDirection | null, forceOff = false) {
const currentValue = [...modelValue.value]
const newSortDirection =
direction ?? orderFields.value.find((f) => f.name === fieldName)?.directions[0] ?? 'asc'
const newSortField: Value = newSortDirection === 'asc' ? fieldName : `-${fieldName}`
let includedFieldName: Value | null = null
if (currentValue.includes(fieldName)) {
includedFieldName = fieldName
} else if (currentValue.includes(`-${fieldName}`)) {
includedFieldName = `-${fieldName}`
}
if (includedFieldName === null) {
// Field is currently not active as sorting parameter. Activating now.
currentValue.push(newSortField)
} else {
// Field is currently active as sorting parameter.
// The field may be active, but the sort direction might have changed.
// Check if we have to replace the current sorting field or have to remove it.
const replaceValue = includedFieldName !== newSortField && !forceOff ? [newSortField] : []
currentValue.splice(currentValue.indexOf(includedFieldName), 1, ...replaceValue)
}
modelValue.value = currentValue
}
function createChoiceLabel(fieldName: T, direction: OrderDirection) {
if (direction === 'asc') return t(`${props.translateBase}.${fieldName}.directions.asc`)
if (direction === 'desc') return t(`${props.translateBase}.${fieldName}.directions.desc`)
}
function normalizeOrderField(fieldName: T | OrderField): OrderField {
if (typeof fieldName === 'string') return { name: fieldName, directions: ['asc', 'desc'] }
else return fieldName
}
</script>
<style scoped lang="postcss">
.order-filter-drag-ghost {
@apply tw-rounded;
@apply tw-opacity-40;
@apply tw-border-2;
@apply tw-border-solid;
@apply tw-border-aura-primary;
}
.order-filter :deep([role='radiogroup'] .btn) {
@apply tw-min-w-[90px];
}
</style>