use-presentation-mode.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import { useCallback, useEffect, useRef, useState } from 'react'
  2. import PDFJSWrapper from '../util/pdf-js-wrapper'
  3. import { debugConsole } from '@/utils/debugging'
  4. import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
  5. type StoredPDFState = {
  6. scrollMode?: number
  7. spreadMode?: number
  8. currentScaleValue?: string
  9. }
  10. export default function usePresentationMode(
  11. pdfJsWrapper: PDFJSWrapper | null | undefined,
  12. page: number | null,
  13. handlePageChange: (page: number) => void,
  14. scale: string,
  15. setScale: (scale: string) => void
  16. ): () => void {
  17. const storedState = useRef<StoredPDFState>({})
  18. const { sendEvent } = useEditorAnalytics()
  19. const [presentationMode, setPresentationMode] = useState(false)
  20. const nextPage = useCallback(() => {
  21. if (page !== null) {
  22. handlePageChange(page + 1)
  23. }
  24. }, [handlePageChange, page])
  25. const previousPage = useCallback(() => {
  26. if (page !== null) {
  27. handlePageChange(page - 1)
  28. }
  29. }, [handlePageChange, page])
  30. const clickListener = useCallback(
  31. (event: MouseEvent) => {
  32. if ((event.target as HTMLElement).tagName === 'A') {
  33. return
  34. }
  35. if (event.shiftKey) {
  36. previousPage()
  37. } else {
  38. nextPage()
  39. }
  40. },
  41. [nextPage, previousPage]
  42. )
  43. const arrowKeyListener = useCallback(
  44. (event: KeyboardEvent) => {
  45. switch (event.key) {
  46. case 'ArrowLeft':
  47. case 'ArrowUp':
  48. case 'PageUp':
  49. case 'Backspace':
  50. previousPage()
  51. break
  52. case 'ArrowRight':
  53. case 'ArrowDown':
  54. case 'PageDown':
  55. nextPage()
  56. break
  57. case ' ':
  58. if (event.shiftKey) {
  59. previousPage()
  60. } else {
  61. nextPage()
  62. }
  63. break
  64. }
  65. },
  66. [nextPage, previousPage]
  67. )
  68. const isMouseWheelScrollingRef = useRef(false)
  69. const mouseWheelListener = useCallback(
  70. (event: WheelEvent) => {
  71. if (
  72. !isMouseWheelScrollingRef.current &&
  73. !event.ctrlKey // Avoid trackpad pinching
  74. ) {
  75. isMouseWheelScrollingRef.current = true
  76. if (event.deltaY > 0) {
  77. nextPage()
  78. } else {
  79. previousPage()
  80. }
  81. setTimeout(() => {
  82. isMouseWheelScrollingRef.current = false
  83. }, 200)
  84. }
  85. },
  86. [nextPage, previousPage]
  87. )
  88. useEffect(() => {
  89. if (presentationMode) {
  90. window.addEventListener('keydown', arrowKeyListener)
  91. window.addEventListener('click', clickListener)
  92. window.addEventListener('wheel', mouseWheelListener)
  93. return () => {
  94. window.removeEventListener('keydown', arrowKeyListener)
  95. window.removeEventListener('click', clickListener)
  96. window.removeEventListener('wheel', mouseWheelListener)
  97. }
  98. }
  99. }, [presentationMode, arrowKeyListener, clickListener, mouseWheelListener])
  100. const requestPresentationMode = useCallback(() => {
  101. sendEvent('pdf-viewer-enter-presentation-mode')
  102. if (pdfJsWrapper) {
  103. pdfJsWrapper.container.parentElement
  104. ?.requestFullscreen()
  105. .catch(debugConsole.error)
  106. }
  107. }, [pdfJsWrapper, sendEvent])
  108. const handleEnterFullscreen = useCallback(() => {
  109. if (pdfJsWrapper) {
  110. storedState.current.scrollMode = pdfJsWrapper.viewer.scrollMode
  111. storedState.current.spreadMode = pdfJsWrapper.viewer.spreadMode
  112. storedState.current.currentScaleValue = scale
  113. setScale('page-fit')
  114. pdfJsWrapper.viewer.scrollMode = 3 // page
  115. pdfJsWrapper.viewer.spreadMode = 0 // none
  116. pdfJsWrapper.fetchAllData()
  117. setPresentationMode(true)
  118. }
  119. }, [pdfJsWrapper, setScale, scale])
  120. const handleExitFullscreen = useCallback(() => {
  121. if (pdfJsWrapper) {
  122. pdfJsWrapper.viewer.scrollMode = storedState.current.scrollMode!
  123. pdfJsWrapper.viewer.spreadMode = storedState.current.spreadMode!
  124. if (storedState.current.currentScaleValue !== undefined) {
  125. setScale(storedState.current.currentScaleValue)
  126. }
  127. setPresentationMode(false)
  128. }
  129. }, [pdfJsWrapper, setScale])
  130. const handleFullscreenChange = useCallback(() => {
  131. if (pdfJsWrapper) {
  132. const fullscreen =
  133. document.fullscreenElement === pdfJsWrapper.container.parentNode
  134. if (fullscreen) {
  135. handleEnterFullscreen()
  136. } else {
  137. handleExitFullscreen()
  138. }
  139. }
  140. }, [pdfJsWrapper, handleEnterFullscreen, handleExitFullscreen])
  141. useEffect(() => {
  142. window.addEventListener('fullscreenchange', handleFullscreenChange)
  143. return () => {
  144. window.removeEventListener('fullscreenchange', handleFullscreenChange)
  145. }
  146. }, [handleFullscreenChange])
  147. return requestPresentationMode
  148. }