CommonSelectItem.vue 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. /* eslint-disable vue/no-v-html */
  4. import { computed, type ConcreteComponent } from 'vue'
  5. import type {
  6. MatchedSelectOption,
  7. SelectOption,
  8. } from '#shared/components/CommonSelect/types.ts'
  9. import type { AutoCompleteOption } from '#shared/components/Form/fields/FieldAutocomplete/types'
  10. import { i18n } from '#shared/i18n.ts'
  11. import { useLocaleStore } from '#shared/stores/locale.ts'
  12. const props = defineProps<{
  13. option: AutoCompleteOption | MatchedSelectOption | SelectOption
  14. selected?: boolean
  15. multiple?: boolean
  16. noLabelTranslate?: boolean
  17. filter?: string
  18. optionIconComponent?: ConcreteComponent
  19. noSelectionIndicator?: boolean
  20. }>()
  21. const emit = defineEmits<{
  22. select: [option: SelectOption]
  23. next: [{ option: AutoCompleteOption; noFocus?: boolean }]
  24. }>()
  25. const select = (option: SelectOption) => {
  26. if (props.option.disabled) {
  27. return
  28. }
  29. emit('select', option)
  30. }
  31. const label = computed(() => {
  32. const { option } = props
  33. if (props.noLabelTranslate) return option.label || option.value.toString()
  34. return (
  35. i18n.t(option.label, ...(option.labelPlaceholder || [])) ||
  36. option.value.toString()
  37. )
  38. })
  39. const heading = computed(() => {
  40. const { option } = props
  41. if (props.noLabelTranslate) return (option as AutoCompleteOption).heading
  42. return i18n.t(
  43. (option as AutoCompleteOption).heading,
  44. ...((option as AutoCompleteOption).headingPlaceholder || []),
  45. )
  46. })
  47. const OptionIconComponent = props.optionIconComponent
  48. const locale = useLocaleStore()
  49. const goToNextPage = (option: AutoCompleteOption, noFocus?: boolean) => {
  50. emit('next', { option, noFocus })
  51. }
  52. </script>
  53. <template>
  54. <div
  55. :class="{
  56. 'cursor-pointer hover:bg-blue-600 focus:bg-blue-800 focus:text-white dark:hover:bg-blue-900 dark:hover:focus:bg-blue-800':
  57. !option.disabled,
  58. }"
  59. tabindex="0"
  60. :aria-selected="selected"
  61. :aria-disabled="option.disabled ? 'true' : undefined"
  62. class="group flex h-9 cursor-default items-center gap-1.5 self-stretch px-2.5 text-sm text-black outline-none dark:text-white"
  63. role="option"
  64. :data-value="option.value"
  65. @click="select(option)"
  66. @keypress.space.prevent="select(option)"
  67. @keypress.enter.prevent="select(option)"
  68. >
  69. <CommonIcon
  70. v-if="multiple && !noSelectionIndicator"
  71. :class="{
  72. 'fill-gray-100 group-hover:fill-black group-focus:fill-white dark:fill-neutral-400 dark:group-hover:fill-white':
  73. !option.disabled,
  74. 'fill-stone-200 dark:fill-neutral-500': option.disabled,
  75. }"
  76. size="xs"
  77. decorative
  78. :name="selected ? 'check-square' : 'square'"
  79. class="m-0.5 shrink-0"
  80. />
  81. <CommonIcon
  82. v-else-if="!noSelectionIndicator"
  83. class="shrink-0 fill-gray-100 group-hover:fill-black group-focus:fill-white dark:fill-neutral-400 dark:group-hover:fill-white"
  84. :class="{
  85. invisible: !selected,
  86. 'fill-stone-200 dark:fill-neutral-500': option.disabled,
  87. }"
  88. decorative
  89. size="tiny"
  90. name="check2"
  91. />
  92. <OptionIconComponent v-if="optionIconComponent" :option="option" />
  93. <CommonIcon
  94. v-else-if="option.icon"
  95. :name="option.icon"
  96. size="tiny"
  97. :class="{
  98. 'fill-stone-200 dark:fill-neutral-500': option.disabled,
  99. }"
  100. decorative
  101. class="shrink-0 fill-gray-100 group-hover:fill-black group-focus:fill-white dark:fill-neutral-400 dark:group-hover:fill-white"
  102. />
  103. <div
  104. v-if="filter"
  105. class="grow truncate"
  106. :title="label + (heading ? ` – ${heading}` : '')"
  107. >
  108. <span
  109. :class="{
  110. 'text-stone-200 dark:text-neutral-500':
  111. option.disabled && !(option as AutoCompleteOption).children?.length,
  112. 'text-stone-100 dark:text-neutral-400':
  113. option.disabled && (option as AutoCompleteOption).children?.length,
  114. }"
  115. v-html="(option as MatchedSelectOption).matchedLabel"
  116. />
  117. <span v-if="heading" class="text-stone-200 dark:text-neutral-500"
  118. >&nbsp;– {{ heading }}</span
  119. >
  120. </div>
  121. <span
  122. v-else
  123. :class="{
  124. 'text-stone-200 dark:text-neutral-500': option.disabled,
  125. }"
  126. class="grow truncate"
  127. :title="label + (heading ? ` – ${heading}` : '')"
  128. >
  129. {{ label }}
  130. <span v-if="heading" class="text-stone-200 dark:text-neutral-500"
  131. >– {{ heading }}</span
  132. >
  133. </span>
  134. <div
  135. v-if="(option as AutoCompleteOption).children?.length"
  136. class="group/nav -me-2 shrink-0 flex-nowrap items-center justify-center gap-x-2.5 rounded-[5px] p-2.5 hover:bg-blue-800 group-focus:hover:bg-blue-600 dark:group-focus:hover:bg-blue-900"
  137. :aria-label="$t('Has submenu')"
  138. role="button"
  139. tabindex="-1"
  140. @click.stop="goToNextPage(option as AutoCompleteOption, true)"
  141. @keypress.enter.prevent.stop="goToNextPage(option as AutoCompleteOption)"
  142. @keypress.space.prevent.stop="goToNextPage(option as AutoCompleteOption)"
  143. >
  144. <CommonIcon
  145. :class="{
  146. 'group-hover:fill-black group-focus:fill-white group-focus:group-hover/nav:!fill-black dark:group-hover:fill-white dark:group-focus:group-hover/nav:!fill-white':
  147. !option.disabled,
  148. }"
  149. class="shrink-0 fill-stone-200 group-hover/nav:!fill-white dark:fill-neutral-500"
  150. :name="
  151. locale.localeData?.dir === 'rtl' ? 'chevron-left' : 'chevron-right'
  152. "
  153. size="xs"
  154. tabindex="-1"
  155. decorative
  156. />
  157. </div>
  158. </div>
  159. </template>