preview-log-entry-header.jsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import PropTypes from 'prop-types'
  2. import classNames from 'classnames'
  3. import { useState, useRef } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import useResizeObserver from '../hooks/use-resize-observer'
  6. import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
  7. import Icon from '../../../shared/components/icon'
  8. import OLButton from '@/features/ui/components/ol/ol-button'
  9. import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
  10. import MaterialIcon from '@/shared/components/material-icon'
  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. const { t } = useTranslation()
  23. const logLocationSpanRef = useRef()
  24. const [locationSpanOverflown, setLocationSpanOverflown] = useState(false)
  25. useResizeObserver(
  26. logLocationSpanRef,
  27. locationSpanOverflown,
  28. checkLocationSpanOverflow
  29. )
  30. const file = sourceLocation ? sourceLocation.file : null
  31. const line = sourceLocation ? sourceLocation.line : null
  32. const logEntryHeaderClasses = classNames('log-entry-header', {
  33. 'log-entry-header-error': level === 'error',
  34. 'log-entry-header-warning': level === 'warning',
  35. 'log-entry-header-info': level === 'info',
  36. 'log-entry-header-typesetting': level === 'typesetting',
  37. 'log-entry-header-raw': level === 'raw',
  38. 'log-entry-header-success': level === 'success',
  39. })
  40. const logEntryLocationBtnClasses = classNames('log-entry-header-link', {
  41. 'log-entry-header-link-error': level === 'error',
  42. 'log-entry-header-link-warning': level === 'warning',
  43. 'log-entry-header-link-typesetting': level === 'typesetting',
  44. 'log-entry-header-link-raw': level === 'raw',
  45. 'log-entry-header-link-info': level === 'info',
  46. 'log-entry-header-link-success': level === 'success',
  47. })
  48. const headerLogLocationTitle = t('navigate_log_source', {
  49. location: file + (line ? `, ${line}` : ''),
  50. })
  51. function checkLocationSpanOverflow(observedElement) {
  52. const spanEl = observedElement.target
  53. const isOverflowing = spanEl.scrollWidth > spanEl.clientWidth
  54. setLocationSpanOverflown(isOverflowing)
  55. }
  56. const locationLinkText =
  57. showSourceLocationLink && file ? `${file}${line ? `, ${line}` : ''}` : null
  58. // Because we want an ellipsis on the left-hand side (e.g. "...longfilename.tex"), the
  59. // `log-entry-header-link-location` class has text laid out from right-to-left using the CSS
  60. // rule `direction: rtl;`.
  61. // This works most of the times, except when the first character of the filename is considered
  62. // a punctuation mark, like `/` (e.g. `/foo/bar/baz.sty`). In this case, because of
  63. // right-to-left writing rules, the punctuation mark is moved to the right-side of the string,
  64. // resulting in `...bar/baz.sty/` instead of `...bar/baz.sty`.
  65. // To avoid this edge-case, we wrap the `logLocationLinkText` in two directional formatting
  66. // characters:
  67. // * \u202A LEFT-TO-RIGHT EMBEDDING Treat the following text as embedded left-to-right.
  68. // * \u202C POP DIRECTIONAL FORMATTING End the scope of the last LRE, RLE, RLO, or LRO.
  69. // This essentially tells the browser that, althought the text is laid out from right-to-left,
  70. // the wrapped portion of text should follow left-to-right writing rules.
  71. const locationLink = locationLinkText ? (
  72. <OLButton
  73. variant="ghost"
  74. className={logEntryLocationBtnClasses}
  75. aria-label={headerLogLocationTitle}
  76. onClick={onSourceLocationClick}
  77. >
  78. <BootstrapVersionSwitcher
  79. bs3={
  80. <>
  81. <Icon type="chain" />
  82. &nbsp;
  83. </>
  84. }
  85. bs5={<MaterialIcon type="link" />}
  86. />
  87. <span ref={logLocationSpanRef} className="log-entry-header-link-location">
  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 ? (
  100. <OLTooltip
  101. id={locationLinkText}
  102. description={locationLinkText}
  103. overlayProps={{ placement: 'left' }}
  104. tooltipProps={{ className: 'log-location-tooltip' }}
  105. >
  106. {locationLink}
  107. </OLTooltip>
  108. ) : (
  109. locationLink
  110. )}
  111. {showCloseButton ? (
  112. <OLButton
  113. variant="link"
  114. className="btn-inline-link log-entry-header-link"
  115. aria-label={t('dismiss_error_popup')}
  116. onClick={onClose}
  117. >
  118. <span aria-hidden="true">&times;</span>
  119. </OLButton>
  120. ) : null}
  121. </header>
  122. )
  123. }
  124. PreviewLogEntryHeader.propTypes = {
  125. sourceLocation: PropTypes.shape({
  126. file: PropTypes.string,
  127. // `line should be either a number or null (i.e. not required), but currently sometimes we get
  128. // an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
  129. // to PropTypes.number (not required) once we fix that.
  130. line: PropTypes.any,
  131. column: PropTypes.any,
  132. }),
  133. level: PropTypes.string.isRequired,
  134. headerTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  135. headerIcon: PropTypes.element,
  136. logType: PropTypes.string,
  137. showSourceLocationLink: PropTypes.bool,
  138. showCloseButton: PropTypes.bool,
  139. onSourceLocationClick: PropTypes.func,
  140. onClose: PropTypes.func,
  141. }
  142. export default PreviewLogEntryHeader