preview-log-entry-header.tsx 4.7 KB

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