ide.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. /* eslint-disable
  2. camelcase,
  3. max-len,
  4. no-cond-assign,
  5. no-return-assign,
  6. no-unused-vars,
  7. no-useless-escape,
  8. */
  9. // TODO: This file was created by bulk-decaffeinate.
  10. // Fix any style issues and re-enable lint.
  11. /*
  12. * decaffeinate suggestions:
  13. * DS101: Remove unnecessary use of Array.from
  14. * DS102: Remove unnecessary code created because of implicit returns
  15. * DS103: Rewrite code to no longer use __guard__
  16. * DS207: Consider shorter variations of null checks
  17. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  18. */
  19. import App from './base'
  20. import FileTreeManager from './ide/file-tree/FileTreeManager'
  21. import LoadingManager from './ide/LoadingManager'
  22. import ConnectionManager from './ide/connection/ConnectionManager'
  23. import EditorManager from './ide/editor/EditorManager'
  24. import OnlineUsersManager from './ide/online-users/OnlineUsersManager'
  25. import PermissionsManager from './ide/permissions/PermissionsManager'
  26. import BinaryFilesManager from './ide/binary-files/BinaryFilesManager'
  27. import ReferencesManager from './ide/references/ReferencesManager'
  28. import MetadataManager from './ide/metadata/MetadataManager'
  29. import './ide/review-panel/ReviewPanelManager'
  30. import './ide/cobranding/CobrandingDataService'
  31. import './ide/chat/index'
  32. import './ide/file-view/index'
  33. import './ide/toolbar/index'
  34. import './ide/directives/layout'
  35. import './ide/directives/verticalResizablePanes'
  36. import './ide/services/ide'
  37. import './services/queued-http' // used in FileTreeManager
  38. import './main/event' // used in various controllers
  39. import './main/system-messages' // used in project/editor
  40. import '../../modules/modules-ide'
  41. import './features/source-editor/ide'
  42. import './shared/context/controllers/root-context-controller'
  43. import './features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller'
  44. import './features/pdf-preview/controllers/pdf-preview-controller'
  45. import './features/share-project-modal/controllers/react-share-project-modal-controller'
  46. import './features/source-editor/controllers/grammarly-advert-controller'
  47. import './features/history/controllers/history-controller'
  48. import './features/editor-left-menu/controllers/editor-left-menu-controller'
  49. import './features/outline/controllers/outline-controller'
  50. import { cleanupServiceWorker } from './utils/service-worker-cleanup'
  51. import { reportCM6Perf } from './infrastructure/cm6-performance'
  52. import { debugConsole } from '@/utils/debugging'
  53. App.controller('IdeController', [
  54. '$scope',
  55. '$timeout',
  56. 'ide',
  57. 'localStorage',
  58. 'eventTracking',
  59. 'metadata',
  60. 'CobrandingDataService',
  61. '$window',
  62. function (
  63. $scope,
  64. $timeout,
  65. ide,
  66. localStorage,
  67. eventTracking,
  68. metadata,
  69. CobrandingDataService,
  70. $window
  71. ) {
  72. // Don't freak out if we're already in an apply callback
  73. let err, pdfLayout, userAgent
  74. $scope.$originalApply = $scope.$apply
  75. $scope.$apply = function (fn) {
  76. if (fn == null) {
  77. fn = function () {}
  78. }
  79. const phase = this.$root.$$phase
  80. if (phase === '$apply' || phase === '$digest') {
  81. return fn()
  82. } else {
  83. return this.$originalApply(fn)
  84. }
  85. }
  86. $scope.state = {
  87. loading: true,
  88. load_progress: 40,
  89. error: null,
  90. }
  91. $scope.ui = {
  92. leftMenuShown: false,
  93. view: 'editor',
  94. chatOpen: false,
  95. pdfLayout: 'sideBySide',
  96. pdfHidden: false,
  97. pdfWidth: 0,
  98. reviewPanelOpen: localStorage(`ui.reviewPanelOpen.${window.project_id}`),
  99. miniReviewPanelVisible: false,
  100. chatResizerSizeOpen: 7,
  101. chatResizerSizeClosed: 0,
  102. }
  103. $scope.user = window.user
  104. $scope.settings = window.userSettings
  105. $scope.anonymous = window.anonymous
  106. $scope.isTokenMember = window.isTokenMember
  107. $scope.isRestrictedTokenMember = window.isRestrictedTokenMember
  108. $scope.cobranding = {
  109. isProjectCobranded: CobrandingDataService.isProjectCobranded(),
  110. logoImgUrl: CobrandingDataService.getLogoImgUrl(),
  111. submitBtnHtml: CobrandingDataService.getSubmitBtnHtml(),
  112. brandVariationName: CobrandingDataService.getBrandVariationName(),
  113. brandVariationHomeUrl: CobrandingDataService.getBrandVariationHomeUrl(),
  114. }
  115. $scope.chat = {}
  116. ide.toggleReviewPanel = $scope.toggleReviewPanel = function () {
  117. $scope.$applyAsync(() => {
  118. if (!$scope.project.features.trackChangesVisible) {
  119. return
  120. }
  121. $scope.ui.reviewPanelOpen = !$scope.ui.reviewPanelOpen
  122. eventTracking.sendMB('rp-toggle-panel', {
  123. value: $scope.ui.reviewPanelOpen,
  124. })
  125. })
  126. }
  127. $scope.$watch('ui.reviewPanelOpen', function (value) {
  128. if (value != null) {
  129. return localStorage(`ui.reviewPanelOpen.${window.project_id}`, value)
  130. }
  131. })
  132. $scope.$on('layout:pdf:resize', function (_, layoutState) {
  133. $scope.ui.pdfHidden = layoutState.east.initClosed
  134. return ($scope.ui.pdfWidth = layoutState.east.size)
  135. })
  136. $scope.$watch('ui.view', function (newView, oldView) {
  137. if (newView !== oldView) {
  138. $scope.$broadcast('layout:flat-screen:toggle')
  139. }
  140. if (newView != null && newView !== 'editor' && newView !== 'pdf') {
  141. eventTracking.sendMBOnce(`ide-open-view-${newView}-once`)
  142. }
  143. })
  144. $scope.$watch('ui.chatOpen', function (isOpen) {
  145. if (isOpen) {
  146. eventTracking.sendMBOnce('ide-open-chat-once')
  147. }
  148. })
  149. $scope.$watch('ui.leftMenuShown', function (isOpen) {
  150. if (isOpen) {
  151. eventTracking.sendMBOnce('ide-open-left-menu-once')
  152. }
  153. })
  154. $scope.trackHover = feature => {
  155. eventTracking.sendMBOnce(`ide-hover-${feature}-once`)
  156. }
  157. // End of tracking code.
  158. window._ide = ide
  159. ide.validFileRegex = '^[^*/]*$' // Don't allow * and /
  160. const useFallbackWebsocket =
  161. window.location &&
  162. window.location.search &&
  163. window.location.search.match(/ws=fallback/)
  164. // if we previously failed to load the websocket fall back to null (the siteUrl)
  165. ide.wsUrl = useFallbackWebsocket ? null : window.sharelatex.wsUrl || null // websocket url (if defined)
  166. ide.project_id = $scope.project_id = window.project_id
  167. ide.$scope = $scope
  168. ide.referencesSearchManager = new ReferencesManager(ide, $scope)
  169. ide.loadingManager = new LoadingManager($scope)
  170. ide.connectionManager = new ConnectionManager(ide, $scope)
  171. ide.fileTreeManager = new FileTreeManager(ide, $scope)
  172. ide.editorManager = new EditorManager(
  173. ide,
  174. $scope,
  175. localStorage,
  176. eventTracking
  177. )
  178. ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
  179. ide.permissionsManager = new PermissionsManager(ide, $scope)
  180. ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
  181. ide.metadataManager = new MetadataManager(ide, $scope, metadata)
  182. let inited = false
  183. $scope.$on('project:joined', function () {
  184. if (inited) {
  185. return
  186. }
  187. inited = true
  188. if (
  189. __guard__(
  190. $scope != null ? $scope.project : undefined,
  191. x => x.deletedByExternalDataSource
  192. )
  193. ) {
  194. ide.showGenericMessageModal(
  195. 'Project Renamed or Deleted',
  196. `\
  197. This project has either been renamed or deleted by an external data source such as Dropbox.
  198. We don't want to delete your data on Overleaf, so this project still contains your history and collaborators.
  199. If the project has been renamed please look in your project list for a new project under the new name.\
  200. `
  201. )
  202. }
  203. return $timeout(function () {
  204. if ($scope.permissions.write) {
  205. let _labelsInitialLoadDone
  206. ide.metadataManager.loadProjectMetaFromServer()
  207. return (_labelsInitialLoadDone = true)
  208. }
  209. }, 200)
  210. })
  211. // Count the first 'doc:opened' as a sign that the ide is loaded
  212. // and broadcast a message. This is a good event to listen for
  213. // if you want to wait until the ide is fully loaded and initialized
  214. let _loaded = false
  215. $scope.$on('doc:opened', function () {
  216. if (_loaded) {
  217. return
  218. }
  219. $scope.$broadcast('ide:loaded')
  220. return (_loaded = true)
  221. })
  222. ide.editingSessionHeartbeat = () => {
  223. eventTracking.editingSessionHeartbeat(() => {
  224. const editorType = ide.editorManager.getEditorType()
  225. const segmentation = {
  226. editorType,
  227. }
  228. if (editorType === 'cm6' || editorType === 'cm6-rich-text') {
  229. const cm6PerfData = reportCM6Perf()
  230. // Ignore if no typing has happened
  231. if (cm6PerfData.numberOfEntries > 0) {
  232. const perfProps = [
  233. 'Max',
  234. 'Mean',
  235. 'Median',
  236. 'NinetyFifthPercentile',
  237. 'DocLength',
  238. 'NumberOfEntries',
  239. 'MaxUserEventsBetweenDomUpdates',
  240. 'Grammarly',
  241. 'SessionLength',
  242. 'Memory',
  243. 'Lags',
  244. 'NonLags',
  245. 'LongestLag',
  246. 'MeanLagsPerMeasure',
  247. 'MeanKeypressesPerMeasure',
  248. 'MeanKeypressPaint',
  249. 'LongTasks',
  250. 'Release',
  251. ]
  252. for (const prop of perfProps) {
  253. const perfValue =
  254. cm6PerfData[prop.charAt(0).toLowerCase() + prop.slice(1)]
  255. if (perfValue !== null) {
  256. segmentation['cm6Perf' + prop] = perfValue
  257. }
  258. }
  259. }
  260. }
  261. return segmentation
  262. })
  263. }
  264. $scope.$on('cursor:editor:update', () => {
  265. ide.editingSessionHeartbeat()
  266. })
  267. $scope.$on('scroll:editor:update', () => {
  268. ide.editingSessionHeartbeat()
  269. })
  270. angular.element($window).on('click', ide.editingSessionHeartbeat)
  271. $scope.$on('$destroy', () =>
  272. angular.element($window).off('click', ide.editingSessionHeartbeat)
  273. )
  274. const DARK_THEMES = [
  275. 'ambiance',
  276. 'chaos',
  277. 'clouds_midnight',
  278. 'cobalt',
  279. 'idle_fingers',
  280. 'merbivore',
  281. 'merbivore_soft',
  282. 'mono_industrial',
  283. 'monokai',
  284. 'pastel_on_dark',
  285. 'solarized_dark',
  286. 'terminal',
  287. 'tomorrow_night',
  288. 'tomorrow_night_blue',
  289. 'tomorrow_night_bright',
  290. 'tomorrow_night_eighties',
  291. 'twilight',
  292. 'vibrant_ink',
  293. ]
  294. $scope.darkTheme = false
  295. // Listen for settings change from React
  296. window.addEventListener('settings:change', event => {
  297. $scope.darkTheme = DARK_THEMES.includes(event.detail.editorTheme)
  298. })
  299. ide.localStorage = localStorage
  300. $scope.switchToFlatLayout = function (view) {
  301. $scope.ui.pdfLayout = 'flat'
  302. $scope.ui.view = view
  303. return ide.localStorage('pdf.layout', 'flat')
  304. }
  305. $scope.switchToSideBySideLayout = function (view) {
  306. $scope.ui.pdfLayout = 'sideBySide'
  307. $scope.ui.view = view
  308. return localStorage('pdf.layout', 'split')
  309. }
  310. if ((pdfLayout = localStorage('pdf.layout'))) {
  311. if (pdfLayout === 'split') {
  312. $scope.switchToSideBySideLayout()
  313. }
  314. if (pdfLayout === 'flat') {
  315. $scope.switchToFlatLayout()
  316. }
  317. } else {
  318. $scope.switchToSideBySideLayout()
  319. }
  320. // Update ui.pdfOpen when the layout changes.
  321. // The east pane should open when the layout changes from "Editor only" or "PDF only" to "Editor & PDF".
  322. $scope.$watch('ui.pdfLayout', value => {
  323. $scope.ui.pdfOpen = value === 'sideBySide'
  324. })
  325. // Update ui.pdfLayout when the east pane is toggled.
  326. // The layout should be set to "Editor & PDF" (sideBySide) when the east pane is opened, and "Editor only" (flat) when the east pane is closed.
  327. $scope.$watch('ui.pdfOpen', value => {
  328. $scope.ui.pdfLayout = value ? 'sideBySide' : 'flat'
  329. if (value) {
  330. window.dispatchEvent(new CustomEvent('ui:pdf-open'))
  331. }
  332. })
  333. $scope.handleKeyDown = () => {
  334. // unused?
  335. }
  336. // User can append ?ft=somefeature to url to activate a feature toggle
  337. ide.featureToggle = __guard__(
  338. __guard__(
  339. typeof location !== 'undefined' && location !== null
  340. ? location.search
  341. : undefined,
  342. x1 => x1.match(/^\?ft=(\w+)$/)
  343. ),
  344. x => x[1]
  345. )
  346. // Listen for editor:lint event from CM6 linter
  347. window.addEventListener('editor:lint', event => {
  348. $scope.hasLintingError = event.detail.hasLintingError
  349. })
  350. ide.socket.on('project:access:revoked', () => {
  351. ide.showGenericMessageModal(
  352. 'Removed From Project',
  353. 'You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.'
  354. )
  355. })
  356. return ide.socket.on('project:publicAccessLevel:changed', data => {
  357. if (data.newAccessLevel != null) {
  358. ide.$scope.project.publicAccesLevel = data.newAccessLevel
  359. return $scope.$digest()
  360. }
  361. })
  362. },
  363. ])
  364. cleanupServiceWorker()
  365. angular.module('SharelatexApp').config([
  366. '$provide',
  367. function ($provide) {
  368. $provide.decorator('$browser', [
  369. '$delegate',
  370. function ($delegate) {
  371. $delegate.onUrlChange = function () {}
  372. $delegate.url = function () {
  373. return ''
  374. }
  375. return $delegate
  376. },
  377. ])
  378. },
  379. ])
  380. export default angular.bootstrap(document.body, ['SharelatexApp'])
  381. function __guard__(value, transform) {
  382. return typeof value !== 'undefined' && value !== null
  383. ? transform(value)
  384. : undefined
  385. }