preview-log-entry-header.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import classNames from 'classnames'
  2. import { useState, useRef, MouseEventHandler } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import useResizeObserver from '../hooks/use-resize-observer'
  5. import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
  6. import Icon from '../../../shared/components/icon'
  7. import OLButton from '@/features/ui/components/ol/ol-button'
  8. import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
  9. import MaterialIcon from '@/shared/components/material-icon'
  10. import { ErrorLevel, SourceLocation } from '@/features/pdf-preview/util/types'
  11. function PreviewLogEntryHeader({
  12. sourceLocation,
  13. level,
  14. headerTitle,
  15. headerIcon,
  16. logType,
  17. showSourceLocationLink = true,
  18. showCloseButton = false,
  19. onSourceLocationClick,
  20. onClose,
  21. }: {
  22. headerTitle: string | React.ReactNode
  23. level: ErrorLevel
  24. headerIcon?: React.ReactElement
  25. logType?: string
  26. sourceLocation?: SourceLocation
  27. showSourceLocationLink?: boolean
  28. showCloseButton?: boolean
  29. onSourceLocationClick?: MouseEventHandler<HTMLButtonElement>
  30. onClose?: () => void
  31. }) {
  32. const { t } = useTranslation()
  33. const logLocationSpanRef = useRef<HTMLSpanElement>(null)
  34. const [locationSpanOverflown, setLocationSpanOverflown] = useState(false)
  35. useResizeObserver(
  36. logLocationSpanRef,
  37. locationSpanOverflown,
  38. checkLocationSpanOverflow
  39. )
  40. const file = sourceLocation ? sourceLocation.file : null
  41. const line = sourceLocation ? sourceLocation.line : null
  42. const logEntryHeaderClasses = classNames('log-entry-header', {
  43. 'log-entry-header-error': level === 'error',
  44. 'log-entry-header-warning': level === 'warning',
  45. 'log-entry-header-info': level === 'info',
  46. 'log-entry-header-typesetting': level === 'typesetting',
  47. 'log-entry-header-raw': level === 'raw',
  48. 'log-entry-header-success': level === 'success',
  49. })
  50. const logEntryLocationBtnClasses = classNames('log-entry-header-link', {
  51. 'log-entry-header-link-error': level === 'error',
  52. 'log-entry-header-link-warning': level === 'warning',
  53. 'log-entry-header-link-typesetting': level === 'typesetting',
  54. 'log-entry-header-link-raw': level === 'raw',
  55. 'log-entry-header-link-info': level === 'info',
  56. 'log-entry-header-link-success': level === 'success',
  57. })
  58. const headerLogLocationTitle = t('navigate_log_source', {
  59. location: file + (line ? `, ${line}` : ''),
  60. })
  61. function checkLocationSpanOverflow(observedElement: ResizeObserverEntry) {
  62. const spanEl = observedElement.target
  63. const isOverflowing = spanEl.scrollWidth > spanEl.clientWidth
  64. setLocationSpanOverflown(isOverflowing)
  65. }
  66. const locationLinkText =
  67. showSourceLocationLink && file ? `${file}${line ? `, ${line}` : ''}` : null
  68. // Because we want an ellipsis on the left-hand side (e.g. "...longfilename.tex"), the
  69. // `log-entry-header-link-location` class has text laid out from right-to-left using the CSS
  70. // rule `direction: rtl;`.
  71. // This works most of the times, except when the first character of the filename is considered
  72. // a punctuation mark, like `/` (e.g. `/foo/bar/baz.sty`). In this case, because of
  73. // right-to-left writing rules, the punctuation mark is moved to the right-side of the string,
  74. // resulting in `...bar/baz.sty/` instead of `...bar/baz.sty`.
  75. // To avoid this edge-case, we wrap the `logLocationLinkText` in two directional formatting
  76. // characters:
  77. // * \u202A LEFT-TO-RIGHT EMBEDDING Treat the following text as embedded left-to-right.
  78. // * \u202C POP DIRECTIONAL FORMATTING End the scope of the last LRE, RLE, RLO, or LRO.
  79. // This essentially tells the browser that, althought the text is laid out from right-to-left,
  80. // the wrapped portion of text should follow left-to-right writing rules.
  81. const locationLink = locationLinkText ? (
  82. <OLButton
  83. variant="ghost"
  84. className={logEntryLocationBtnClasses}
  85. aria-label={headerLogLocationTitle}
  86. onClick={onSourceLocationClick}
  87. >
  88. <BootstrapVersionSwitcher
  89. bs3={
  90. <>
  91. <Icon type="chain" />
  92. &nbsp;
  93. </>
  94. }
  95. bs5={<MaterialIcon type="link" />}
  96. />
  97. <span ref={logLocationSpanRef} className="log-entry-header-link-location">
  98. {`\u202A${locationLinkText}\u202C`}
  99. </span>
  100. </OLButton>
  101. ) : null
  102. const headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle
  103. return (
  104. <header className={logEntryHeaderClasses}>
  105. {headerIcon ? (
  106. <div className="log-entry-header-icon-container">{headerIcon}</div>
  107. ) : null}
  108. <h3 className="log-entry-header-title">{headerTitleText}</h3>
  109. {locationSpanOverflown && locationLinkText && locationLink ? (
  110. <OLTooltip
  111. id={locationLinkText}
  112. description={locationLinkText}
  113. overlayProps={{ placement: 'left' }}
  114. tooltipProps={{ className: 'log-location-tooltip' }}
  115. >
  116. {locationLink}
  117. </OLTooltip>
  118. ) : (
  119. locationLink
  120. )}
  121. {showCloseButton ? (
  122. <OLButton
  123. variant="link"
  124. className="btn-inline-link log-entry-header-link"
  125. aria-label={t('dismiss_error_popup')}
  126. onClick={onClose}
  127. >
  128. <span aria-hidden="true">&times;</span>
  129. </OLButton>
  130. ) : null}
  131. </header>
  132. )
  133. }
  134. export default PreviewLogEntryHeader