editor-context.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import {
  2. createContext,
  3. useCallback,
  4. useContext,
  5. useEffect,
  6. useMemo,
  7. } from 'react'
  8. import PropTypes from 'prop-types'
  9. import useScopeValue from '../hooks/use-scope-value'
  10. import useBrowserWindow from '../hooks/use-browser-window'
  11. import { useIdeContext } from './ide-context'
  12. import { useProjectContext } from './project-context'
  13. import { useDetachContext } from './detach-context'
  14. export const EditorContext = createContext()
  15. EditorContext.Provider.propTypes = {
  16. value: PropTypes.shape({
  17. cobranding: PropTypes.shape({
  18. logoImgUrl: PropTypes.string.isRequired,
  19. brandVariationName: PropTypes.string.isRequired,
  20. brandVariationId: PropTypes.number.isRequired,
  21. brandId: PropTypes.number.isRequired,
  22. brandVariationHomeUrl: PropTypes.string.isRequired,
  23. publishGuideHtml: PropTypes.string,
  24. partner: PropTypes.string,
  25. brandedMenu: PropTypes.bool,
  26. submitBtnHtml: PropTypes.string,
  27. }),
  28. hasPremiumCompile: PropTypes.bool,
  29. loading: PropTypes.bool,
  30. renameProject: PropTypes.func.isRequired,
  31. showSymbolPalette: PropTypes.bool,
  32. toggleSymbolPalette: PropTypes.func,
  33. insertSymbol: PropTypes.func,
  34. isProjectOwner: PropTypes.bool,
  35. isRestrictedTokenMember: PropTypes.bool,
  36. permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
  37. }),
  38. }
  39. export function EditorProvider({ children, settings }) {
  40. const ide = useIdeContext()
  41. const { role } = useDetachContext()
  42. const { owner, features } = useProjectContext({
  43. owner: PropTypes.shape({
  44. _id: PropTypes.string.isRequired,
  45. }),
  46. features: PropTypes.shape({
  47. compileGroup: PropTypes.string,
  48. }),
  49. })
  50. const cobranding = useMemo(() => {
  51. if (window.brandVariation) {
  52. return {
  53. logoImgUrl: window.brandVariation.logo_url,
  54. brandVariationName: window.brandVariation.name,
  55. brandVariationId: window.brandVariation.id,
  56. brandId: window.brandVariation.brand_id,
  57. brandVariationHomeUrl: window.brandVariation.home_url,
  58. publishGuideHtml: window.brandVariation.publish_guide_html,
  59. partner: window.brandVariation.partner,
  60. brandedMenu: window.brandVariation.branded_menu,
  61. submitBtnHtml: window.brandVariation.submit_button_html,
  62. }
  63. } else {
  64. return undefined
  65. }
  66. }, [])
  67. const [loading] = useScopeValue('state.loading')
  68. const [projectName, setProjectName] = useScopeValue('project.name')
  69. const [permissionsLevel] = useScopeValue('permissionsLevel')
  70. const [showSymbolPalette] = useScopeValue('editor.showSymbolPalette')
  71. const [toggleSymbolPalette] = useScopeValue('editor.toggleSymbolPalette')
  72. useEffect(() => {
  73. if (ide?.socket) {
  74. ide.socket.on('projectNameUpdated', setProjectName)
  75. return () =>
  76. ide.socket.removeListener('projectNameUpdated', setProjectName)
  77. }
  78. }, [ide?.socket, setProjectName])
  79. const renameProject = useCallback(
  80. newName => {
  81. setProjectName(oldName => {
  82. if (oldName !== newName) {
  83. settings.saveProjectSettings({ name: newName }).catch(response => {
  84. setProjectName(oldName)
  85. const { data, status } = response
  86. if (status === 400) {
  87. return ide.showGenericMessageModal('Error renaming project', data)
  88. } else {
  89. return ide.showGenericMessageModal(
  90. 'Error renaming project',
  91. 'Please try again in a moment'
  92. )
  93. }
  94. })
  95. }
  96. return newName
  97. })
  98. },
  99. [settings, ide, setProjectName]
  100. )
  101. const { setTitle } = useBrowserWindow()
  102. useEffect(() => {
  103. const parts = []
  104. if (role === 'detached') {
  105. parts.push('[PDF]')
  106. }
  107. if (projectName) {
  108. parts.push(projectName)
  109. parts.push('-')
  110. }
  111. parts.push('Online LaTeX Editor')
  112. parts.push(window.ExposedSettings.appName)
  113. const title = parts.join(' ')
  114. setTitle(title)
  115. }, [projectName, setTitle, role])
  116. const insertSymbol = useCallback(symbol => {
  117. window.dispatchEvent(
  118. new CustomEvent('editor:insert-symbol', {
  119. detail: symbol,
  120. })
  121. )
  122. }, [])
  123. const value = useMemo(
  124. () => ({
  125. cobranding,
  126. hasPremiumCompile: features?.compileGroup === 'priority',
  127. loading,
  128. renameProject,
  129. permissionsLevel,
  130. isProjectOwner: owner?._id === window.user.id,
  131. isRestrictedTokenMember: window.isRestrictedTokenMember,
  132. showSymbolPalette,
  133. toggleSymbolPalette,
  134. insertSymbol,
  135. }),
  136. [
  137. cobranding,
  138. features?.compileGroup,
  139. loading,
  140. renameProject,
  141. permissionsLevel,
  142. owner?._id,
  143. showSymbolPalette,
  144. toggleSymbolPalette,
  145. insertSymbol,
  146. ]
  147. )
  148. return (
  149. <EditorContext.Provider value={value}>{children}</EditorContext.Provider>
  150. )
  151. }
  152. EditorProvider.propTypes = {
  153. children: PropTypes.any,
  154. settings: PropTypes.object,
  155. }
  156. export function useEditorContext(propTypes) {
  157. const context = useContext(EditorContext)
  158. if (!context) {
  159. throw new Error('useEditorContext is only available inside EditorProvider')
  160. }
  161. PropTypes.checkPropTypes(propTypes, context, 'data', 'EditorContext.Provider')
  162. return context
  163. }