pdf-synctex-controls.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import classNames from 'classnames'
  2. import { memo, useCallback, useMemo } from 'react'
  3. import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
  4. import { useLayoutContext } from '../../../shared/context/layout-context'
  5. import { useTranslation } from 'react-i18next'
  6. import * as eventTracking from '../../../infrastructure/event-tracking'
  7. import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
  8. import OLButton from '@/features/ui/components/ol/ol-button'
  9. import MaterialIcon from '@/shared/components/material-icon'
  10. import { Spinner } from 'react-bootstrap'
  11. import { Placement } from 'react-bootstrap/types'
  12. import useSynctex from '../hooks/use-synctex'
  13. import { useFeatureFlag } from '@/shared/context/split-test-context'
  14. const GoToCodeButton = memo(function GoToCodeButton({
  15. syncToCode,
  16. syncToCodeInFlight,
  17. isDetachLayout,
  18. }: {
  19. syncToCode: ({ visualOffset }: { visualOffset: number }) => void
  20. syncToCodeInFlight: boolean
  21. isDetachLayout?: boolean
  22. }) {
  23. const { t } = useTranslation()
  24. const buttonClasses = classNames('synctex-control', {
  25. 'detach-synctex-control': !!isDetachLayout,
  26. })
  27. let buttonIcon = null
  28. if (syncToCodeInFlight) {
  29. buttonIcon = (
  30. <Spinner animation="border" aria-hidden="true" size="sm" role="status" />
  31. )
  32. } else if (!isDetachLayout) {
  33. buttonIcon = (
  34. <MaterialIcon type="arrow_left_alt" className="synctex-control-icon" />
  35. )
  36. }
  37. const syncToCodeWithButton = useCallback(() => {
  38. eventTracking.sendMB('jump-to-location', {
  39. direction: 'pdf-location-in-code',
  40. method: 'arrow',
  41. })
  42. syncToCode({ visualOffset: 72 })
  43. }, [syncToCode])
  44. const overlayProps = useMemo(
  45. () => ({
  46. placement: (isDetachLayout ? 'bottom' : 'right') as Placement,
  47. }),
  48. [isDetachLayout]
  49. )
  50. return (
  51. <OLTooltip
  52. id="sync-to-code"
  53. description={t('go_to_pdf_location_in_code')}
  54. overlayProps={overlayProps}
  55. >
  56. <span>
  57. <OLButton
  58. variant="secondary"
  59. size="sm"
  60. onClick={syncToCodeWithButton}
  61. disabled={syncToCodeInFlight}
  62. className={buttonClasses}
  63. aria-label={t('go_to_pdf_location_in_code')}
  64. >
  65. {buttonIcon}
  66. {isDetachLayout ? <span>&nbsp;{t('show_in_code')}</span> : ''}
  67. </OLButton>
  68. </span>
  69. </OLTooltip>
  70. )
  71. })
  72. const GoToPdfButton = memo(function GoToPdfButton({
  73. syncToPdf,
  74. syncToPdfInFlight,
  75. isDetachLayout,
  76. canSyncToPdf,
  77. }: {
  78. syncToPdf: () => void
  79. syncToPdfInFlight: boolean
  80. canSyncToPdf: boolean
  81. isDetachLayout?: boolean
  82. }) {
  83. const { t } = useTranslation()
  84. const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
  85. const buttonClasses = classNames('synctex-control', {
  86. 'detach-synctex-control': !!isDetachLayout,
  87. })
  88. let buttonIcon = null
  89. if (syncToPdfInFlight) {
  90. buttonIcon = (
  91. <Spinner animation="border" aria-hidden="true" size="sm" role="status" />
  92. )
  93. } else if (!isDetachLayout) {
  94. buttonIcon = (
  95. <MaterialIcon type="arrow_right_alt" className="synctex-control-icon" />
  96. )
  97. }
  98. return (
  99. <OLTooltip
  100. id="sync-to-pdf"
  101. description={t('go_to_code_location_in_pdf')}
  102. overlayProps={{ placement: tooltipPlacement }}
  103. >
  104. <span>
  105. <OLButton
  106. variant="secondary"
  107. size="sm"
  108. onClick={syncToPdf}
  109. disabled={syncToPdfInFlight || !canSyncToPdf}
  110. className={buttonClasses}
  111. aria-label={t('go_to_code_location_in_pdf')}
  112. >
  113. {buttonIcon}
  114. {isDetachLayout ? <span>&nbsp;{t('show_in_pdf')}</span> : ''}
  115. </OLButton>
  116. </span>
  117. </OLTooltip>
  118. )
  119. })
  120. function PdfSynctexControls() {
  121. const { detachRole } = useLayoutContext()
  122. const { pdfUrl, pdfViewer, position } = useCompileContext()
  123. const {
  124. syncToCode,
  125. syncToPdf,
  126. syncToCodeInFlight,
  127. syncToPdfInFlight,
  128. canSyncToPdf,
  129. } = useSynctex()
  130. const visualPreviewEnabled = useFeatureFlag('visual-preview')
  131. if (visualPreviewEnabled) {
  132. return null
  133. }
  134. if (!position) {
  135. return null
  136. }
  137. if (!pdfUrl || pdfViewer === 'native') {
  138. return null
  139. }
  140. if (detachRole === 'detacher') {
  141. return (
  142. <GoToPdfButton
  143. syncToPdf={syncToPdf}
  144. syncToPdfInFlight={syncToPdfInFlight}
  145. isDetachLayout
  146. canSyncToPdf={canSyncToPdf}
  147. />
  148. )
  149. } else if (detachRole === 'detached') {
  150. return (
  151. <GoToCodeButton
  152. syncToCode={syncToCode}
  153. syncToCodeInFlight={syncToCodeInFlight}
  154. isDetachLayout
  155. />
  156. )
  157. } else {
  158. return (
  159. <>
  160. <GoToPdfButton
  161. syncToPdf={syncToPdf}
  162. syncToPdfInFlight={syncToPdfInFlight}
  163. canSyncToPdf={canSyncToPdf}
  164. />
  165. <GoToCodeButton
  166. syncToCode={syncToCode}
  167. syncToCodeInFlight={syncToCodeInFlight}
  168. />
  169. </>
  170. )
  171. }
  172. }
  173. export default memo(PdfSynctexControls)