Skip to content
Snippets Groups Projects
OrderFilter.vue 5.37 KiB
Newer Older
  • Learn to ignore specific revisions
  • <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"
    
          >
            <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>