layout-context.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import {
  2. createContext,
  3. useContext,
  4. useCallback,
  5. useMemo,
  6. useEffect,
  7. useRef,
  8. } from 'react'
  9. import PropTypes from 'prop-types'
  10. import { debounce } from 'lodash'
  11. import useScopeValue from '../hooks/use-scope-value'
  12. import useDetachLayout from '../hooks/use-detach-layout'
  13. import { useIdeContext } from './ide-context'
  14. import localStorage from '../../infrastructure/local-storage'
  15. import getMeta from '../../utils/meta'
  16. const debugPdfDetach = getMeta('ol-debugPdfDetach')
  17. export const LayoutContext = createContext()
  18. LayoutContext.Provider.propTypes = {
  19. value: PropTypes.shape({
  20. reattach: PropTypes.func.isRequired,
  21. detach: PropTypes.func.isRequired,
  22. detachIsLinked: PropTypes.bool,
  23. detachRole: PropTypes.string,
  24. changeLayout: PropTypes.func.isRequired,
  25. view: PropTypes.string,
  26. setView: PropTypes.func.isRequired,
  27. chatIsOpen: PropTypes.bool,
  28. setChatIsOpen: PropTypes.func.isRequired,
  29. reviewPanelOpen: PropTypes.bool,
  30. setReviewPanelOpen: PropTypes.func.isRequired,
  31. leftMenuShown: PropTypes.bool,
  32. setLeftMenuShown: PropTypes.func.isRequired,
  33. pdfLayout: PropTypes.oneOf(['sideBySide', 'flat']).isRequired,
  34. }).isRequired,
  35. }
  36. function setLayoutInLocalStorage(pdfLayout) {
  37. localStorage.setItem(
  38. 'pdf.layout',
  39. pdfLayout === 'sideBySide' ? 'split' : 'flat'
  40. )
  41. }
  42. export function LayoutProvider({ children }) {
  43. const { $scope } = useIdeContext()
  44. // what to show in the "flat" view (editor or pdf)
  45. const [view, _setView] = useScopeValue('ui.view')
  46. const setView = useCallback(
  47. value => {
  48. _setView(oldValue => {
  49. // ensure that the "history:toggle" event is broadcast when switching in or out of history view
  50. if (value === 'history' || oldValue === 'history') {
  51. $scope.toggleHistory()
  52. }
  53. return value
  54. })
  55. },
  56. [$scope, _setView]
  57. )
  58. // whether the chat pane is open
  59. const [chatIsOpen, setChatIsOpen] = useScopeValue('ui.chatOpen')
  60. // whether the review pane is open
  61. const [reviewPanelOpen, setReviewPanelOpen] = useScopeValue(
  62. 'ui.reviewPanelOpen'
  63. )
  64. // whether the menu pane is open
  65. const [leftMenuShown, setLeftMenuShown] = useScopeValue('ui.leftMenuShown')
  66. // whether to display the editor and preview side-by-side or full-width ("flat")
  67. const [pdfLayout, setPdfLayout] = useScopeValue('ui.pdfLayout')
  68. // switch to either side-by-side or flat (full-width) layout
  69. const switchLayout = useCallback(() => {
  70. setPdfLayout(layout => {
  71. const newLayout = layout === 'sideBySide' ? 'flat' : 'sideBySide'
  72. setView(newLayout === 'sideBySide' ? 'editor' : 'pdf')
  73. setPdfLayout(newLayout)
  74. setLayoutInLocalStorage(newLayout)
  75. })
  76. }, [setPdfLayout, setView])
  77. const changeLayout = useCallback(
  78. (newLayout, newView) => {
  79. setPdfLayout(newLayout)
  80. setView(newLayout === 'sideBySide' ? 'editor' : newView)
  81. setLayoutInLocalStorage(newLayout)
  82. },
  83. [setPdfLayout, setView]
  84. )
  85. // helper to avoid changing layout multiple times in rapid succession. This is
  86. // especially useful for calling `changeLayout` as a side-effect. Calling
  87. // `changeLayout` multiple times on page load cause layout rendering issues do
  88. // to timming clash with Angular.
  89. const debouncedChangeLayout = useRef(
  90. debounce((newLayout, newView) => changeLayout(newLayout, newView), 1000, {
  91. leading: true,
  92. })
  93. ).current
  94. const {
  95. reattach,
  96. detach,
  97. isLinking: detachIsLinking,
  98. isLinked: detachIsLinked,
  99. role: detachRole,
  100. } = useDetachLayout()
  101. useEffect(() => {
  102. if (debugPdfDetach) {
  103. console.log('Layout Effect', {
  104. detachRole,
  105. detachIsLinking,
  106. detachIsLinked,
  107. })
  108. }
  109. if (detachRole !== 'detacher') return // not in a PDF detacher layout
  110. if (detachIsLinking || detachIsLinked) {
  111. // the tab is linked to a detached tab (or about to be linked); show
  112. // editor only
  113. debouncedChangeLayout('flat', 'editor')
  114. } else {
  115. debouncedChangeLayout('sideBySide')
  116. }
  117. }, [detachRole, detachIsLinking, detachIsLinked, debouncedChangeLayout])
  118. const value = useMemo(
  119. () => ({
  120. reattach,
  121. detach,
  122. detachIsLinked,
  123. detachRole,
  124. changeLayout,
  125. chatIsOpen,
  126. leftMenuShown,
  127. pdfLayout,
  128. reviewPanelOpen,
  129. setChatIsOpen,
  130. setLeftMenuShown,
  131. setPdfLayout,
  132. setReviewPanelOpen,
  133. setView,
  134. switchLayout,
  135. view,
  136. }),
  137. [
  138. reattach,
  139. detach,
  140. detachIsLinked,
  141. detachRole,
  142. changeLayout,
  143. chatIsOpen,
  144. leftMenuShown,
  145. pdfLayout,
  146. reviewPanelOpen,
  147. setChatIsOpen,
  148. setLeftMenuShown,
  149. setPdfLayout,
  150. setReviewPanelOpen,
  151. setView,
  152. switchLayout,
  153. view,
  154. ]
  155. )
  156. return (
  157. <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>
  158. )
  159. }
  160. LayoutProvider.propTypes = {
  161. children: PropTypes.any,
  162. }
  163. export function useLayoutContext(propTypes) {
  164. const data = useContext(LayoutContext)
  165. PropTypes.checkPropTypes(propTypes, data, 'data', 'LayoutContext.Provider')
  166. return data
  167. }