useResizeLine.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import {
  3. type MaybeComputedElementRef,
  4. type MaybeElement,
  5. onKeyStroke,
  6. useElementBounding,
  7. useWindowSize,
  8. } from '@vueuse/core'
  9. import { ref, type Ref, onBeforeUnmount } from 'vue'
  10. import { EnumTextDirection } from '#shared/graphql/types.ts'
  11. import { useLocaleStore } from '#shared/stores/locale.ts'
  12. export const useResizeLine = (
  13. resizeCallback: (positionX: number) => void,
  14. resizeLineElementRef: MaybeComputedElementRef<MaybeElement>,
  15. keyStrokeCallback: (e: KeyboardEvent, adjustment: number) => void,
  16. options?: {
  17. calculateFromRight?: boolean
  18. /**
  19. * @default 'horizontal'
  20. * */
  21. orientation: 'horizontal' | 'vertical'
  22. offsetThreshold?: number
  23. },
  24. ) => {
  25. const isResizing = ref(false)
  26. const locale = useLocaleStore()
  27. const { height: screenHeight } = useWindowSize()
  28. const { width, height } = useElementBounding(
  29. resizeLineElementRef as MaybeComputedElementRef<MaybeElement>,
  30. )
  31. const { width: screenWidth } = useWindowSize()
  32. const handleVerticalResize = (event: MouseEvent | TouchEvent) => {
  33. // Position the cursor as close to the handle center as possible.
  34. let positionX = Math.round(width.value / 2)
  35. if (event instanceof MouseEvent) {
  36. positionX += event.pageX
  37. } else if (event.targetTouches[0]) {
  38. positionX += event.targetTouches[0].pageX
  39. }
  40. // In case of RTL locale, subtract the reported position from the current screen width.
  41. if (
  42. locale.localeData?.dir === EnumTextDirection.Rtl &&
  43. !options?.calculateFromRight // If the option is set, do not calculate from the right.
  44. )
  45. positionX = screenWidth.value - positionX
  46. // In case of LTR locale and resizer is used from right side of the window, subtract the reported position from the current screen width.
  47. if (
  48. locale.localeData?.dir === EnumTextDirection.Ltr &&
  49. options?.calculateFromRight
  50. )
  51. positionX = screenWidth.value - positionX
  52. resizeCallback(positionX)
  53. }
  54. const handleHorizontalResize = (event: MouseEvent | TouchEvent) => {
  55. // Position the cursor as close to the handle center as possible.
  56. let positionY = Math.round(height.value / 2)
  57. if (event instanceof MouseEvent) {
  58. positionY += event.pageY
  59. } else if (event.targetTouches[0]) {
  60. positionY += event.targetTouches[0].pageY
  61. }
  62. positionY = screenHeight.value - positionY - (options?.offsetThreshold ?? 0)
  63. resizeCallback(positionY)
  64. }
  65. const resize = (event: MouseEvent | TouchEvent) => {
  66. if (options?.orientation === 'vertical') return handleVerticalResize(event)
  67. handleHorizontalResize(event)
  68. }
  69. const endResizing = () => {
  70. // eslint-disable-next-line no-use-before-define
  71. removeListeners()
  72. isResizing.value = false
  73. }
  74. const removeListeners = () => {
  75. document.removeEventListener('touchmove', resize)
  76. document.removeEventListener('touchend', endResizing)
  77. document.removeEventListener('mousemove', resize)
  78. document.removeEventListener('mouseup', endResizing)
  79. }
  80. const addEventListeners = () => {
  81. document.addEventListener('touchend', endResizing)
  82. document.addEventListener('touchmove', resize)
  83. document.addEventListener('mouseup', endResizing)
  84. document.addEventListener('mousemove', resize)
  85. }
  86. const startResizing = (e: MouseEvent | TouchEvent) => {
  87. // Do not react on double click event.
  88. if (e.detail > 1) return
  89. e.preventDefault()
  90. isResizing.value = true
  91. addEventListeners()
  92. }
  93. onBeforeUnmount(() => {
  94. removeListeners()
  95. })
  96. // a11y keyboard navigation horizontal resize
  97. if (options?.orientation === 'vertical') {
  98. onKeyStroke(
  99. 'ArrowLeft',
  100. (e: KeyboardEvent) => {
  101. if (options?.calculateFromRight) {
  102. keyStrokeCallback(e, locale.localeData?.dir === 'rtl' ? -5 : 5)
  103. } else {
  104. keyStrokeCallback(e, locale.localeData?.dir === 'rtl' ? 5 : -5)
  105. }
  106. },
  107. { target: resizeLineElementRef as Ref<EventTarget> },
  108. )
  109. onKeyStroke(
  110. 'ArrowRight',
  111. (e: KeyboardEvent) => {
  112. if (options?.calculateFromRight) {
  113. keyStrokeCallback(e, locale.localeData?.dir === 'rtl' ? 5 : -5)
  114. } else {
  115. keyStrokeCallback(e, locale.localeData?.dir === 'rtl' ? -5 : 5)
  116. }
  117. },
  118. { target: resizeLineElementRef as Ref<EventTarget> },
  119. )
  120. } else {
  121. onKeyStroke(
  122. 'ArrowUp',
  123. (e: KeyboardEvent) => {
  124. keyStrokeCallback(e, 5)
  125. },
  126. { target: resizeLineElementRef as Ref<EventTarget> },
  127. )
  128. onKeyStroke(
  129. 'ArrowDown',
  130. (e: KeyboardEvent) => {
  131. keyStrokeCallback(e, -5)
  132. },
  133. { target: resizeLineElementRef as Ref<EventTarget> },
  134. )
  135. }
  136. return {
  137. isResizing,
  138. startResizing,
  139. }
  140. }