CommonSelectItem.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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 && !option.labelPlaceholder)
  34. return option.label || option.value.toString()
  35. return (
  36. i18n.t(option.label, ...(option.labelPlaceholder || [])) ||
  37. option.value.toString()
  38. )
  39. })
  40. const heading = computed(() => {
  41. const { option } = props
  42. if (
  43. props.noLabelTranslate &&
  44. !(option as AutoCompleteOption).headingPlaceholder
  45. )
  46. return (option as AutoCompleteOption).heading
  47. return i18n.t(
  48. (option as AutoCompleteOption).heading,
  49. ...((option as AutoCompleteOption).headingPlaceholder || []),
  50. )
  51. })
  52. const OptionIconComponent = props.optionIconComponent
  53. const locale = useLocaleStore()
  54. const goToNextPage = (option: AutoCompleteOption, noFocus?: boolean) => {
  55. emit('next', { option, noFocus })
  56. }
  57. </script>
  58. <template>
  59. <div
  60. :class="{
  61. 'cursor-pointer hover:bg-blue-600 focus:bg-blue-800 focus:text-white dark:hover:bg-blue-900 dark:hover:focus:bg-blue-800':
  62. !option.disabled,
  63. }"
  64. tabindex="0"
  65. :aria-selected="selected"
  66. :aria-disabled="option.disabled ? 'true' : undefined"
  67. 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"
  68. role="option"
  69. :data-value="option.value"
  70. @click="select(option)"
  71. @keypress.space.prevent="select(option)"
  72. @keypress.enter.prevent="select(option)"
  73. >
  74. <CommonIcon
  75. v-if="multiple && !noSelectionIndicator"
  76. :class="{
  77. 'fill-gray-100 group-hover:fill-black group-focus:fill-white dark:fill-neutral-400 dark:group-hover:fill-white':
  78. !option.disabled,
  79. 'fill-stone-200 dark:fill-neutral-500': option.disabled,
  80. }"
  81. size="xs"
  82. decorative
  83. :name="selected ? 'check-square' : 'square'"
  84. class="m-0.5 shrink-0"
  85. />
  86. <CommonIcon
  87. v-else-if="!noSelectionIndicator"
  88. class="shrink-0 fill-gray-100 group-hover:fill-black group-focus:fill-white dark:fill-neutral-400 dark:group-hover:fill-white"
  89. :class="{
  90. invisible: !selected,
  91. 'fill-stone-200 dark:fill-neutral-500': option.disabled,
  92. }"
  93. decorative
  94. size="tiny"
  95. name="check2"
  96. />
  97. <OptionIconComponent v-if="optionIconComponent" :option="option" />
  98. <CommonIcon
  99. v-else-if="option.icon"
  100. :name="option.icon"
  101. size="tiny"
  102. :class="{
  103. 'fill-stone-200 dark:fill-neutral-500': option.disabled,
  104. }"
  105. decorative
  106. class="shrink-0 fill-gray-100 group-hover:fill-black group-focus:fill-white dark:fill-neutral-400 dark:group-hover:fill-white"
  107. />
  108. <div
  109. v-if="filter"
  110. v-tooltip="label + (heading ? ` – ${heading}` : '')"
  111. class="grow truncate"
  112. >
  113. <span
  114. :class="{
  115. 'text-stone-200 dark:text-neutral-500':
  116. option.disabled && !(option as AutoCompleteOption).children?.length,
  117. 'text-stone-100 dark:text-neutral-400':
  118. option.disabled && (option as AutoCompleteOption).children?.length,
  119. }"
  120. v-html="(option as MatchedSelectOption).matchedLabel"
  121. />
  122. <span
  123. v-if="heading"
  124. class="text-stone-200 dark:text-neutral-500"
  125. :class="{
  126. 'group-hover:text-black group-focus:text-black group-hover:dark:text-white group-focus:dark:text-white':
  127. !option.disabled,
  128. }"
  129. >&nbsp;– {{ heading }}</span
  130. >
  131. </div>
  132. <span
  133. v-else
  134. v-tooltip="label + (heading ? ` – ${heading}` : '')"
  135. :class="{
  136. 'text-stone-200 dark:text-neutral-500': option.disabled,
  137. }"
  138. class="grow truncate"
  139. >
  140. {{ label }}
  141. <span
  142. v-if="heading"
  143. class="text-stone-200 dark:text-neutral-500"
  144. :class="{
  145. 'group-hover:text-black group-focus:text-black group-hover:dark:text-white group-focus:dark:text-white':
  146. !option.disabled,
  147. }"
  148. >– {{ heading }}</span
  149. >
  150. </span>
  151. <div
  152. v-if="(option as AutoCompleteOption).children?.length"
  153. 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"
  154. :aria-label="$t('Has submenu')"
  155. role="button"
  156. tabindex="-1"
  157. @click.stop="goToNextPage(option as AutoCompleteOption, true)"
  158. @keypress.enter.prevent.stop="goToNextPage(option as AutoCompleteOption)"
  159. @keypress.space.prevent.stop="goToNextPage(option as AutoCompleteOption)"
  160. >
  161. <CommonIcon
  162. :class="{
  163. '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':
  164. !option.disabled,
  165. }"
  166. class="shrink-0 fill-stone-200 group-hover/nav:!fill-white dark:fill-neutral-500"
  167. :name="
  168. locale.localeData?.dir === 'rtl' ? 'chevron-left' : 'chevron-right'
  169. "
  170. size="xs"
  171. tabindex="-1"
  172. decorative
  173. />
  174. </div>
  175. </div>
  176. </template>