HeaderResizeLine.vue 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script lang="ts" setup>
  3. import { useActiveElement, useEventListener } from '@vueuse/core'
  4. import { computed, ref, useTemplateRef } from 'vue'
  5. import { EnumTextDirection } from '#shared/graphql/types.ts'
  6. import { useLocaleStore } from '#shared/stores/locale.ts'
  7. import getUuid from '#shared/utils/getUuid.ts'
  8. import { useResizeLine } from '#desktop/components/ResizeLine/useResizeLine.ts'
  9. import { MINIMUM_COLUMN_WIDTH } from './types.ts'
  10. const emit = defineEmits<{
  11. resize: []
  12. reset: []
  13. }>()
  14. const resizeLine = useTemplateRef('resize-line')
  15. const resizing = ref(false)
  16. const currentHeader = computed(() => resizeLine.value?.parentElement)
  17. const nextHeader = computed(
  18. () =>
  19. currentHeader.value?.nextElementSibling as HTMLElement | null | undefined,
  20. )
  21. const currentHeaderWidth = ref(0)
  22. const nextHeaderWidth = ref(0)
  23. const setCurrentHeaderWidths = () => {
  24. if (!currentHeader.value || !nextHeader.value) return
  25. currentHeaderWidth.value = currentHeader.value.clientWidth
  26. nextHeaderWidth.value = nextHeader.value.clientWidth
  27. }
  28. const setHeaderWidths = (diff: number) => {
  29. if (!currentHeader.value || !nextHeader.value) return
  30. if (currentHeaderWidth.value + diff < MINIMUM_COLUMN_WIDTH)
  31. diff = -(currentHeaderWidth.value - MINIMUM_COLUMN_WIDTH)
  32. if (nextHeaderWidth.value - diff < MINIMUM_COLUMN_WIDTH)
  33. diff = nextHeaderWidth.value - MINIMUM_COLUMN_WIDTH
  34. currentHeader.value.style.width = `${currentHeaderWidth.value + diff}px`
  35. nextHeader.value.style.width = `${nextHeaderWidth.value - diff}px`
  36. }
  37. const activeElement = useActiveElement()
  38. const handleKeyStroke = (e: KeyboardEvent, diff: number) => {
  39. if (activeElement.value !== resizeLine.value) return
  40. e.preventDefault()
  41. setCurrentHeaderWidths()
  42. setHeaderWidths(diff)
  43. emit('resize')
  44. }
  45. const resizeStartX = ref(0)
  46. const { startResizing } = useResizeLine(
  47. (positionX) => {
  48. if (!currentHeader.value || !nextHeader.value) return
  49. let diff = positionX - resizeStartX.value
  50. if (useLocaleStore().localeData?.dir === EnumTextDirection.Rtl)
  51. diff = resizeStartX.value - positionX
  52. setHeaderWidths(diff)
  53. },
  54. resizeLine,
  55. handleKeyStroke,
  56. {
  57. orientation: 'vertical',
  58. },
  59. )
  60. const addRemoveResizingListener = (event: 'mouseup' | 'touchend') => {
  61. useEventListener(
  62. event,
  63. () => {
  64. resizing.value = false
  65. emit('resize')
  66. },
  67. { once: true },
  68. )
  69. }
  70. const handleMousedown = (event: MouseEvent) => {
  71. resizing.value = true
  72. resizeStartX.value = event.pageX
  73. addRemoveResizingListener('mouseup')
  74. setCurrentHeaderWidths()
  75. startResizing(event)
  76. }
  77. const handleTouchstart = (event: TouchEvent) => {
  78. resizing.value = true
  79. if (event.targetTouches[0]) resizeStartX.value = event.targetTouches[0].pageX
  80. else
  81. resizeStartX.value =
  82. event.changedTouches[event.changedTouches.length - 1].pageX
  83. addRemoveResizingListener('touchend')
  84. setCurrentHeaderWidths()
  85. startResizing(event)
  86. }
  87. const handleDoubleClick = () => {
  88. emit('reset')
  89. resizeLine.value?.blur()
  90. }
  91. const id = getUuid()
  92. </script>
  93. <template>
  94. <button
  95. ref="resize-line"
  96. v-tooltip="$t('Resize column')"
  97. :aria-describedby="id"
  98. tabindex="0"
  99. class="line"
  100. :class="{
  101. '!bg-blue-800': resizing,
  102. }"
  103. @mousedown="handleMousedown"
  104. @blur="resizing = false"
  105. @touchstart="handleTouchstart"
  106. @dblclick="handleDoubleClick"
  107. >
  108. <span
  109. :id="id"
  110. role="separator"
  111. class="invisible absolute -z-20"
  112. aria-orientation="horizontal"
  113. :aria-valuenow="currentHeader?.clientWidth"
  114. :aria-valuemin="MINIMUM_COLUMN_WIDTH"
  115. />
  116. </button>
  117. </template>
  118. <style scoped>
  119. .line {
  120. @apply absolute end-0 top-1/2 h-5 w-1 -translate-y-2.5 cursor-col-resize rounded-sm bg-neutral-100 hover:bg-blue-600 focus:outline-none dark:bg-gray-200 dark:hover:bg-blue-900;
  121. &:focus-visible {
  122. background-color: theme('colors.blue.800') !important;
  123. }
  124. }
  125. </style>