useSelectOptions.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. import { computed, type ComputedRef, ref, type Ref } from 'vue'
  3. import { i18n } from '@shared/i18n'
  4. import type { TicketState } from '@shared/entities/ticket/types'
  5. import type { SelectOptionSorting, SelectOption } from '../fields/FieldSelect'
  6. import type { FormFieldContext } from '../types/field'
  7. import type { FlatSelectOption } from '../fields/FieldTreeSelect'
  8. import type { AutoCompleteOption } from '../fields/FieldAutoComplete'
  9. import useValue from './useValue'
  10. const useSelectOptions = (
  11. options: Ref<SelectOption[] | FlatSelectOption[] | AutoCompleteOption[]>,
  12. context: Ref<
  13. FormFieldContext<{
  14. multiple?: boolean
  15. noOptionsLabelTranslation?: boolean
  16. sorting?: SelectOptionSorting
  17. }>
  18. >,
  19. arrowLeftCallback?: (
  20. option?: SelectOption | FlatSelectOption | AutoCompleteOption,
  21. getDialogFocusTargets?: (optionsOnly?: boolean) => HTMLElement[],
  22. ) => void,
  23. arrowRightCallback?: (
  24. option?: SelectOption | FlatSelectOption | AutoCompleteOption,
  25. getDialogFocusTargets?: (optionsOnly?: boolean) => HTMLElement[],
  26. ) => void,
  27. ) => {
  28. const dialog = ref(null)
  29. const { currentValue } = useValue(context)
  30. const hasStatusProperty = computed(
  31. () =>
  32. options.value &&
  33. options.value.some(
  34. (option) => (option as SelectOption | FlatSelectOption).status,
  35. ),
  36. )
  37. const translatedOptions = computed(
  38. () =>
  39. options.value &&
  40. options.value.map(
  41. (option: SelectOption | FlatSelectOption | AutoCompleteOption) =>
  42. ({
  43. ...option,
  44. label: context.value.noOptionsLabelTranslation
  45. ? option.label
  46. : i18n.t(option.label, option.labelPlaceholder as never),
  47. ...((option as AutoCompleteOption).heading
  48. ? {
  49. heading: context.value.noOptionsLabelTranslation
  50. ? (option as AutoCompleteOption).heading
  51. : i18n.t(
  52. (option as AutoCompleteOption).heading,
  53. (option as AutoCompleteOption)
  54. .headingPlaceholder as never,
  55. ),
  56. }
  57. : {}),
  58. } as unknown as SelectOption | FlatSelectOption | AutoCompleteOption),
  59. ),
  60. )
  61. const optionValueLookup: ComputedRef<
  62. Record<
  63. string | number,
  64. SelectOption | FlatSelectOption | AutoCompleteOption
  65. >
  66. > = computed(
  67. () =>
  68. translatedOptions.value &&
  69. translatedOptions.value.reduce(
  70. (options, option) => ({
  71. ...options,
  72. [option.value]: option,
  73. }),
  74. {},
  75. ),
  76. )
  77. const sortedOptions = computed(() => {
  78. if (!context.value.sorting) return translatedOptions.value
  79. if (
  80. context.value.sorting !== 'label' &&
  81. context.value.sorting !== 'value'
  82. ) {
  83. console.warn('Unsupported sorting option')
  84. return translatedOptions.value
  85. }
  86. return [...translatedOptions.value]?.sort((a, b) => {
  87. const aLabelOrValue =
  88. a[context.value.sorting as SelectOptionSorting] || a.value
  89. const bLabelOrValue =
  90. b[context.value.sorting as SelectOptionSorting] || b.value
  91. return aLabelOrValue.toString().localeCompare(bLabelOrValue.toString())
  92. })
  93. })
  94. const getSelectedOptionIcon = (selectedValue: string | number) =>
  95. optionValueLookup.value[selectedValue] &&
  96. (optionValueLookup.value[selectedValue].icon as string)
  97. const getSelectedOptionLabel = (selectedValue: string | number) =>
  98. optionValueLookup.value[selectedValue] &&
  99. optionValueLookup.value[selectedValue].label
  100. const getSelectedOptionStatus = (selectedValue: string | number) =>
  101. optionValueLookup.value[selectedValue] &&
  102. ((optionValueLookup.value[selectedValue] as SelectOption | FlatSelectOption)
  103. .status as TicketState)
  104. const selectOption = (
  105. option: SelectOption | FlatSelectOption | AutoCompleteOption,
  106. ) => {
  107. if (context.value.multiple) {
  108. const selectedValue = currentValue.value ?? []
  109. const optionIndex = selectedValue.indexOf(option.value)
  110. if (optionIndex !== -1) selectedValue.splice(optionIndex, 1)
  111. else selectedValue.push(option.value)
  112. selectedValue.sort(
  113. (a: string | number, b: string | number) =>
  114. sortedOptions.value.findIndex((option) => option.value === a) -
  115. sortedOptions.value.findIndex((option) => option.value === b),
  116. )
  117. context.value.node.input(selectedValue)
  118. return
  119. }
  120. context.value.node.input(option.value)
  121. }
  122. const getDialogFocusTargets = (optionsOnly?: boolean): HTMLElement[] => {
  123. const containerElement =
  124. dialog.value && (dialog.value as HTMLElement).parentElement
  125. if (!containerElement) return []
  126. const targetElements = Array.from(
  127. (containerElement as HTMLElement).querySelectorAll('[tabindex="0"]'),
  128. ) as HTMLElement[]
  129. if (!targetElements) return []
  130. if (optionsOnly)
  131. return targetElements.filter(
  132. (targetElement) =>
  133. targetElement.attributes.getNamedItem('role')?.value === 'option',
  134. )
  135. return targetElements
  136. }
  137. const advanceDialogFocus = (
  138. event: KeyboardEvent,
  139. option?: SelectOption | FlatSelectOption,
  140. ) => {
  141. const originElement = event.target as HTMLElement
  142. const targetElements = getDialogFocusTargets()
  143. if (!targetElements.length) return
  144. const originElementIndex =
  145. targetElements.indexOf(
  146. (document.activeElement as HTMLElement) || originElement,
  147. ) || 0
  148. let targetElement
  149. switch (event.key) {
  150. case 'ArrowLeft':
  151. if (typeof arrowLeftCallback === 'function')
  152. arrowLeftCallback(option, getDialogFocusTargets)
  153. break
  154. case 'ArrowUp':
  155. targetElement =
  156. targetElements[originElementIndex - 1] ||
  157. targetElements[targetElements.length - 1]
  158. break
  159. case 'ArrowRight':
  160. if (typeof arrowRightCallback === 'function')
  161. arrowRightCallback(option, getDialogFocusTargets)
  162. break
  163. case 'ArrowDown':
  164. targetElement =
  165. targetElements[originElementIndex + 1] || targetElements[0]
  166. break
  167. default:
  168. }
  169. if (targetElement) targetElement.focus()
  170. }
  171. return {
  172. dialog,
  173. hasStatusProperty,
  174. translatedOptions,
  175. optionValueLookup,
  176. sortedOptions,
  177. getSelectedOptionIcon,
  178. getSelectedOptionLabel,
  179. getSelectedOptionStatus,
  180. selectOption,
  181. getDialogFocusTargets,
  182. advanceDialogFocus,
  183. }
  184. }
  185. export default useSelectOptions