Remove Angular (#17534)

GitOrigin-RevId: 7a0d45e17d9905fa75569e2d19ca59caa4a41565
This commit is contained in:
Alf Eaton 2024-06-05 09:33:11 +01:00 committed by Copybot
parent b3d33fe813
commit c24ace801b
149 changed files with 665 additions and 21004 deletions

224
package-lock.json generated
View file

@ -12243,12 +12243,6 @@
"@types/node": "*"
}
},
"node_modules/@types/angular": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.4.tgz",
"integrity": "sha512-wPS/ncJWhyxJsndsW1B6Ta8D4mi97x1yItSu+rkLDytU3oRIh2CFAjMuJceYwFAh9+DIohndWM0QBA9OU2Hv0g==",
"dev": true
},
"node_modules/@types/aria-query": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
@ -12691,15 +12685,6 @@
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
"dev": true
},
"node_modules/@types/lodash.frompairs": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz",
"integrity": "sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
@ -14733,25 +14718,6 @@
"integrity": "sha512-VJK1SRmXBpjwsB4YOHYSturx48rLKMzHgCqDH2ZDa6ZbMS/N5huoNqyQdK5Fj/xayu3fqbXckn5SeCS1EbMDZg==",
"dev": true
},
"node_modules/angular": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==",
"deprecated": "For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward.",
"dev": true
},
"node_modules/angular-mocks": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.8.2.tgz",
"integrity": "sha512-I5L3P0l21HPdVsP4A4qWmENt4ePjjbkDFdAzOaM7QiibFySbt14DptPbt2IjeG4vFBr4vSLbhIz8Fk03DISl8Q==",
"dev": true
},
"node_modules/angular-sanitize": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.8.2.tgz",
"integrity": "sha512-OB6Goa+QN3byf5asQ7XRl7DKZejm/F/ZOqa9z1skqYVOWA2hoBxoCmt9E7+i7T/TbxZP5zYzKxNZVVJNu860Hg==",
"dev": true
},
"node_modules/ansi-color": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz",
@ -15617,17 +15583,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/babel-plugin-angularjs-annotate": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/babel-plugin-angularjs-annotate/-/babel-plugin-angularjs-annotate-0.10.0.tgz",
"integrity": "sha512-NPE7FOAxcLPCUR/kNkrhHIjoScR3RyIlRH3yRn79j8EZWtpILVnCOdA9yKfsOmRh6BHnLHKl8ZAThc+YDd/QwQ==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.0.0",
"@babel/types": "^7.2.0",
"simple-is": "~0.2.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@ -27997,12 +27952,6 @@
"lodash.keys": "~2.4.1"
}
},
"node_modules/lodash.frompairs": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz",
"integrity": "sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=",
"dev": true
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@ -30011,18 +29960,6 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
},
"node_modules/ngcomponent": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-4.1.0.tgz",
"integrity": "sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==",
"dev": true,
"dependencies": {
"@types/angular": "^1.6.39",
"@types/lodash": "^4.14.85",
"angular": ">=1.5.0",
"lodash": "^4.17.4"
}
},
"node_modules/nise": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
@ -34807,52 +34744,6 @@
"react-dom": ">=15.0.0"
}
},
"node_modules/react2angular": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/react2angular/-/react2angular-4.0.6.tgz",
"integrity": "sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==",
"dev": true,
"dependencies": {
"@types/lodash.frompairs": "^4.0.5",
"angular": ">=1.5",
"lodash.frompairs": "^4.0.1",
"ngcomponent": "^4.1.0"
},
"peerDependencies": {
"@types/angular": ">=1.5",
"@types/prop-types": ">=15",
"@types/react": ">=16",
"@types/react-dom": ">=16",
"prop-types": ">=15",
"react": ">=15",
"react-dom": ">=15"
}
},
"node_modules/react2angular-shared-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/react2angular-shared-context/-/react2angular-shared-context-1.1.2.tgz",
"integrity": "sha512-0zrxBjmBs+et5zYNknx/jvrJCzGz6KbF8BHfzXHTl9ms6iMsbmmXkZiQQksVT1Og5wnkmVq9nlLVfWYJLSXF0w==",
"dev": true,
"dependencies": {
"uuid": "^8.3.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16.0.0 || ^17",
"react-dom": "^16.0.0 || ^17"
}
},
"node_modules/react2angular-shared-context/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
@ -36673,12 +36564,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/simple-is": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz",
"integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=",
"dev": true
},
"node_modules/simple-oauth2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz",
@ -44616,12 +44501,8 @@
"acorn": "^7.1.1",
"acorn-walk": "^7.1.1",
"algoliasearch": "^3.35.1",
"angular": "~1.8.0",
"angular-mocks": "~1.8.0",
"angular-sanitize": "~1.8.0",
"autoprefixer": "^10.4.16",
"babel-loader": "^9.1.2",
"babel-plugin-angularjs-annotate": "^0.10.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"backbone": "^1.3.3",
@ -44698,8 +44579,6 @@
"react-linkify": "^1.0.0-alpha",
"react-refresh": "^0.14.0",
"react-resizable-panels": "^1.0.3",
"react2angular": "^4.0.6",
"react2angular-shared-context": "^1.1.0",
"requirejs": "^2.3.6",
"resolve-url-loader": "^5.0.0",
"samlp": "^7.0.2",
@ -53003,14 +52882,10 @@
"acorn": "^7.1.1",
"acorn-walk": "^7.1.1",
"algoliasearch": "^3.35.1",
"angular": "~1.8.0",
"angular-mocks": "~1.8.0",
"angular-sanitize": "~1.8.0",
"archiver": "^5.3.0",
"async": "3.2.2",
"autoprefixer": "^10.4.16",
"babel-loader": "^9.1.2",
"babel-plugin-angularjs-annotate": "^0.10.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"backbone": "^1.3.3",
@ -53152,8 +53027,6 @@
"react-linkify": "^1.0.0-alpha",
"react-refresh": "^0.14.0",
"react-resizable-panels": "^1.0.3",
"react2angular": "^4.0.6",
"react2angular-shared-context": "^1.1.0",
"recurly": "^4.0.0",
"referer-parser": "github:overleaf/nodejs-referer-parser#8b8b103762d05b7be4cfa2f810e1d408be67d7bb",
"request": "^2.88.2",
@ -57040,12 +56913,6 @@
"@types/node": "*"
}
},
"@types/angular": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.4.tgz",
"integrity": "sha512-wPS/ncJWhyxJsndsW1B6Ta8D4mi97x1yItSu+rkLDytU3oRIh2CFAjMuJceYwFAh9+DIohndWM0QBA9OU2Hv0g==",
"dev": true
},
"@types/aria-query": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
@ -57487,15 +57354,6 @@
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
"dev": true
},
"@types/lodash.frompairs": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz",
"integrity": "sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
@ -59081,24 +58939,6 @@
}
}
},
"angular": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==",
"dev": true
},
"angular-mocks": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.8.2.tgz",
"integrity": "sha512-I5L3P0l21HPdVsP4A4qWmENt4ePjjbkDFdAzOaM7QiibFySbt14DptPbt2IjeG4vFBr4vSLbhIz8Fk03DISl8Q==",
"dev": true
},
"angular-sanitize": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.8.2.tgz",
"integrity": "sha512-OB6Goa+QN3byf5asQ7XRl7DKZejm/F/ZOqa9z1skqYVOWA2hoBxoCmt9E7+i7T/TbxZP5zYzKxNZVVJNu860Hg==",
"dev": true
},
"ansi-color": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz",
@ -59740,17 +59580,6 @@
}
}
},
"babel-plugin-angularjs-annotate": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/babel-plugin-angularjs-annotate/-/babel-plugin-angularjs-annotate-0.10.0.tgz",
"integrity": "sha512-NPE7FOAxcLPCUR/kNkrhHIjoScR3RyIlRH3yRn79j8EZWtpILVnCOdA9yKfsOmRh6BHnLHKl8ZAThc+YDd/QwQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@babel/types": "^7.2.0",
"simple-is": "~0.2.0"
}
},
"babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@ -70105,12 +69934,6 @@
"lodash.keys": "~2.4.1"
}
},
"lodash.frompairs": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz",
"integrity": "sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=",
"dev": true
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@ -71596,18 +71419,6 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
},
"ngcomponent": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-4.1.0.tgz",
"integrity": "sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==",
"dev": true,
"requires": {
"@types/angular": "^1.6.39",
"@types/lodash": "^4.14.85",
"angular": ">=1.5.0",
"lodash": "^4.17.4"
}
},
"nise": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
@ -75149,35 +74960,6 @@
"react-lifecycles-compat": "^3.0.4"
}
},
"react2angular": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/react2angular/-/react2angular-4.0.6.tgz",
"integrity": "sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==",
"dev": true,
"requires": {
"@types/lodash.frompairs": "^4.0.5",
"angular": ">=1.5",
"lodash.frompairs": "^4.0.1",
"ngcomponent": "^4.1.0"
}
},
"react2angular-shared-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/react2angular-shared-context/-/react2angular-shared-context-1.1.2.tgz",
"integrity": "sha512-0zrxBjmBs+et5zYNknx/jvrJCzGz6KbF8BHfzXHTl9ms6iMsbmmXkZiQQksVT1Og5wnkmVq9nlLVfWYJLSXF0w==",
"dev": true,
"requires": {
"uuid": "^8.3.2"
},
"dependencies": {
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true
}
}
},
"reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
@ -76578,12 +76360,6 @@
}
}
},
"simple-is": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz",
"integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=",
"dev": true
},
"simple-oauth2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz",

View file

@ -1,9 +0,0 @@
diff --git a/node_modules/ngcomponent/index.ts b/node_modules/ngcomponent/index.ts
index 5fe33c5..8e1c6fc 100644
--- a/node_modules/ngcomponent/index.ts
+++ b/node_modules/ngcomponent/index.ts
@@ -1,3 +1,4 @@
+// @ts-nocheck
import { IChangesObject } from 'angular'
import assign = require('lodash/assign')
import mapValues = require('lodash/mapValues')

View file

@ -1,9 +0,0 @@
diff --git a/node_modules/react2angular/index.tsx b/node_modules/react2angular/index.tsx
index 5cee831..a07e040 100644
--- a/node_modules/react2angular/index.tsx
+++ b/node_modules/react2angular/index.tsx
@@ -1,3 +1,4 @@
+// @ts-nocheck
import { IAugmentedJQuery, IComponentOptions } from 'angular'
import fromPairs = require('lodash.frompairs')
import NgComponent from 'ngcomponent'

View file

@ -603,15 +603,10 @@ const _ProjectController = {
!showPersonalAccessToken &&
splitTestAssignments['personal-access-token'].variant === 'enabled' // `?personal-access-token=enabled`
const idePageReact = splitTestAssignments['ide-page'].variant === 'react'
const template =
detachRole === 'detached'
? // TODO: Create React version of detached page
'project/editor_detached'
: idePageReact
? 'project/ide-react'
: 'project/editor'
? 'project/ide-react-detached'
: 'project/ide-react'
res.render(template, {
title: project.name,
@ -681,7 +676,6 @@ const _ProjectController = {
showUpgradePrompt,
fixedSizeDocument: true,
useOpenTelemetry: Settings.useOpenTelemetryClient,
idePageReact,
showPersonalAccessToken,
optionalPersonalAccessToken,
hasTrackChangesFeature: Features.hasFeature('track-changes'),

View file

@ -12,7 +12,6 @@ const {
handleAdminDomainRedirect,
} = require('../Authorization/AuthorizationMiddleware')
const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const orderedPrivilegeLevels = [
PrivilegeLevels.NONE,
@ -98,18 +97,7 @@ async function tokenAccessPage(req, res, next) {
}
}
const { variant } = await SplitTestHandler.promises.getAssignment(
req,
res,
'token-access-page'
)
const view =
variant === 'react'
? 'project/token/access-react'
: 'project/token/access'
res.render(view, {
res.render('project/token/access-react', {
postUrl: makePostUrl(token),
})
} catch (err) {

View file

@ -72,7 +72,7 @@ html(
block head-scripts
body(ng-csp=(cspEnabled ? "no-unsafe-eval" : false) class=(showThinFooter ? 'thin-footer' : undefined))
body(class=(showThinFooter ? 'thin-footer' : undefined))
if(settings.recaptcha && settings.recaptcha.siteKeyV3)
script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render="+settings.recaptcha.siteKeyV3, defer=deferScripts)

View file

@ -1,21 +0,0 @@
extends ./layout-base
block entrypointVar
- entrypoint = 'main'
block body
if (typeof(suppressNavbar) == "undefined")
include layout/navbar
block content
if (typeof(suppressFooter) == "undefined")
if showThinFooter
include layout/footer
else
include layout/fat-footer
if (typeof(suppressCookieBanner) == 'undefined')
include _cookie_banner
!= moduleIncludes("contactModal", locals)

View file

@ -1,42 +0,0 @@
footer.site-footer
- var showLanguagePicker = Object.keys(settings.i18n.subdomainLang).length > 1
- var hasCustomLeftNav = nav.left_footer && nav.left_footer.length > 0
.site-footer-content.hidden-print
.row
ul.col-md-9
if hasFeature('saas')
li © #{new Date().getFullYear()} Overleaf
else if !settings.nav.hide_powered_by
li
//- year of Server Pro release, static
| © 2024
|
a(href='https://www.overleaf.com/for/enterprises') Powered by Overleaf
if showLanguagePicker || hasCustomLeftNav
li
strong.text-muted |
if showLanguagePicker
include language-picker
if showLanguagePicker && hasCustomLeftNav
li
strong.text-muted |
each item in nav.left_footer
li
if item.url
a(href=item.url, class=item.class) !{translate(item.text)}
else
| !{item.text}
ul.col-md-3.text-right
each item in nav.right_footer
li
if item.url
a(href=item.url, class=item.class, aria-label=item.label) !{item.text}
else
| !{item.text}

View file

@ -1,147 +0,0 @@
nav.navbar.navbar-default.navbar-main
.container-fluid
.navbar-header
if (typeof(suppressNavbarRight) == "undefined")
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation'))
i.fa.fa-bars(aria-hidden="true")
if settings.nav.custom_logo
a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
else if (nav.title)
a(href='/', aria-label=settings.appName, ng-non-bindable).navbar-title #{nav.title}
else
a(href='/', aria-label=settings.appName).navbar-brand
- var canDisplayAdminMenu = hasAdminAccess()
- var canDisplayAdminRedirect = canRedirectToAdminDomain()
- var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
if (typeof(suppressNavbarRight) == "undefined")
.navbar-collapse.collapse(collapse="navCollapsed")
ul.nav.navbar-nav.navbar-right
if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu)
li.dropdown(class="subdued", dropdown)
a.dropdown-toggle(href, dropdown-toggle)
| Admin
b.caret
ul.dropdown-menu
if canDisplayAdminMenu
li
a(href="/admin") Manage Site
li
a(href="/admin/user") Manage Users
li
a(href="/admin/project") Project URL Lookup
li
a(href="/admin/saml/logs") SAML logs
if canDisplayAdminRedirect
li
a(href=settings.adminUrl) Switch to Admin
if canDisplaySplitTestMenu
li
a(href="/admin/split-test") Manage Feature Flags
if canDisplaySurveyMenu
li
a(href="/admin/survey") Manage Surveys
// loop over header_extras
each item in nav.header_extras
-
if ((item.only_when_logged_in && getSessionUser())
|| (item.only_when_logged_out && (!getSessionUser()))
|| (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
|| (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
){
var showNavItem = true
} else {
var showNavItem = false
}
if showNavItem
if item.dropdown
li.dropdown(class=item.class, dropdown)
a.dropdown-toggle(href, dropdown-toggle)
| !{translate(item.text)}
b.caret
ul.dropdown-menu
each child in item.dropdown
if child.divider
li.divider
else if child.isContactUs
li
a(ng-controller="ContactModal" ng-click="contactUsModal()" href)
span(event-tracking="menu-clicked-contact" event-tracking-mb="true" event-tracking-trigger="click")
| #{translate("contact_us")}
else
li
if child.url
a(
href=child.url,
class=child.class,
event-tracking=child.event
event-tracking-mb="true"
event-tracking-trigger="click"
event-segmentation=child.eventSegmentation
) !{translate(child.text)}
else
| !{translate(child.text)}
else
li(class=item.class)
if item.url
a(
href=item.url,
class=item.class,
event-tracking=item.event
event-tracking-mb="true"
event-tracking-trigger="click"
) !{translate(item.text)}
else
| !{translate(item.text)}
// logged out
if !getSessionUser()
// register link
if hasFeature('registration-page')
li
a(
href="/register"
event-tracking="menu-clicked-register"
event-tracking-action="clicked"
event-tracking-trigger="click"
event-tracking-mb="true"
event-segmentation={ page: currentUrl }
) #{translate('register')}
// login link
li
a(
href="/login"
event-tracking="menu-clicked-login"
event-tracking-action="clicked"
event-tracking-trigger="click"
event-tracking-mb="true"
event-segmentation={ page: currentUrl }
).text-capitalize #{translate('log_in')}
// projects link and account menu
if getSessionUser()
li
a(href="/project") #{translate('Projects')}
li.dropdown(dropdown)
a.dropdown-toggle(href, dropdown-toggle)
| #{translate('Account')}
b.caret
ul.dropdown-menu
li
div.subdued {{ usersEmail }}
li.divider.hidden-xs.hidden-sm
li
a(href="/user/settings") #{translate('Account Settings')}
if nav.showSubscriptionLink
li
a(href="/user/subscription") #{translate('subscription')}
li.divider.hidden-xs.hidden-sm
li
form(method="POST" action="/logout")
input(name='_csrf', type='hidden', value=csrfToken)
button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}

View file

@ -1,124 +0,0 @@
extends ../layout
block vars
- var suppressNavbar = true
- var suppressFooter = true
- var suppressSkipToContent = true
- var suppressCookieBanner = true
- metadata.robotsNoindexNofollow = true
block entrypointVar
- entrypoint = 'ide'
block content
.editor(ng-controller="IdeController").full-size
//- required by react2angular-shared-context, must be rendered as a top level component
shared-context-react()
.loading-screen(ng-if="state.loading")
.loading-screen-brand-container
.loading-screen-brand(
style="height: 20%;"
ng-style="{ 'height': state.load_progress + '%' }"
)
h3.loading-screen-label(ng-if="!state.error") #{translate("loading")}
span.loading-screen-ellip .
span.loading-screen-ellip .
span.loading-screen-ellip .
p.loading-screen-error(ng-if="state.error").ng-cloak
span(ng-bind-html="state.error")
.global-alerts(ng-cloak ng-hide="editor.error_state")
.alert.alert-danger.small(ng-if="connection.forced_disconnect")
strong #{translate("disconnected")}
.alert.alert-warning.small(ng-if="connection.reconnection_countdown")
strong #{translate("lost_connection")}.
| #{translate("reconnecting_in_x_secs", {seconds:"{{ connection.reconnection_countdown }}"})}.
a#try-reconnect-now-button.alert-link-as-btn.pull-right(href, ng-click="tryReconnectNow()") #{translate("try_now")}
.alert.alert-warning.small(ng-if="connection.reconnecting && connection.stillReconnecting")
strong #{translate("reconnecting")}…
.alert.alert-warning.small(ng-if="sync_tex_error")
strong #{translate("synctex_failed")}.
a#synctex-more-info-button.alert-link-as-btn.pull-right(
href="/learn/how-to/SyncTeX_Errors"
target="_blank"
) #{translate("more_info")}
.alert.alert-warning.small(ng-if="connection.inactive_disconnect")
strong #{translate("editor_disconected_click_to_reconnect")}
.alert.alert-warning.small(ng-if="connection.debug") {{ connection.state }}
.div(ng-controller="SavingNotificationController")
.alert.alert-warning.small(ng-repeat="(doc_id, state) in docSavingStatus" ng-if="state.unsavedSeconds > 8") #{translate("saving_notification_with_seconds", {docname:"{{ state.doc.name }}", seconds:"{{ state.unsavedSeconds }}"})}
.div(ng-controller="SystemMessagesController")
.alert.alert-warning.system-message(
ng-repeat="message in messages"
ng-controller="SystemMessageController"
ng-hide="hidden"
)
button(ng-hide="protected" ng-click="hide()").close.pull-right
span(aria-hidden="true") ×
span.sr-only #{translate("close")}
.system-message-content
| {{htmlContent}}
if hasFeature('saas')
legacy-editor-warning(delay=10000)
include ./editor/main
script(type="text/ng-template" id="genericMessageModalTemplate")
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="done()"
aria-label="Close"
)
span(aria-hidden="true") ×
h3 {{ title }}
.modal-body(ng-bind-html="message")
.modal-footer
button.btn.btn-info(ng-click="done()") #{translate("ok")}
script(type="text/ng-template" id="outOfSyncModalTemplate")
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="done()"
aria-label="Close"
)
span(aria-hidden="true") ×
h3 {{ title }}
.modal-body(ng-bind-html="message")
.modal-body
button.btn.btn-info(
ng-init="showFileContents = false"
ng-click="showFileContents = !showFileContents"
)
| {{showFileContents ? "Hide" : "Show"}} Local File Contents
.text-preview(ng-show="showFileContents")
textarea.scroll-container(readonly="readonly" rows="{{editorContentRows}}")
| {{editorContent}}
.modal-footer
button.btn.btn-info(ng-click="done()") #{translate("reload_editor")}
script(type="text/ng-template" id="lockEditorModalTemplate")
.modal-header
h3 {{ title }}
.modal-body(ng-bind-html="message")
block append meta
include ./editor/meta
block prepend foot-scripts
each file in (useOpenTelemetry ? entrypointScripts("tracing") : [])
script(type="text/javascript", nonce=scriptNonce, src=file)
script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js')

View file

@ -1,52 +0,0 @@
.ui-layout-center(
ng-controller="ReviewPanelController",
ng-class="{\
'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\
'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
'rp-size-mini': ui.miniReviewPanelVisible,\
'rp-size-expanded': ui.reviewPanelOpen,\
'rp-layout-left': reviewPanel.layoutToLeft,\
'rp-loading-threads': loadingThreads,\
}"
)
.multi-selection-ongoing(
ng-show="editor.multiSelectedCount > 0"
)
.multi-selection-message
h4 {{ editor.multiSelectedCount }} #{translate('files_selected')}
include ./file-view
.editor-container.full-size(
ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0"
vertical-resizable-panes="south-pane-resizer"
vertical-resizable-panes-hidden-externally-on="south-pane-toggled"
vertical-resizable-panes-hidden-initially="true"
vertical-resizable-panes-default-size="250"
vertical-resizable-panes-min-size="250"
vertical-resizable-panes-max-size="336"
vertical-resizable-panes-resize-on="layout:flat-screen:toggle,south-pane-toggled"
)
.div(vertical-resizable-top)
.loading-panel(
ng-show="(!editor.sharejs_doc || editor.opening) && !editor.error_state",
)
span(ng-show="editor.open_doc_id")
i.fa.fa-spin.fa-refresh
|   #{translate("loading")}…
span(ng-show="!editor.open_doc_id")
i.fa.fa-arrow-left
|   #{translate("open_a_file_on_the_left")}
div(ng-controller="EditorLoaderController")
include ../../source-editor/source-editor
if moduleIncludesAvailable('editor:symbol-palette')
.div(vertical-resizable-bottom)
if moduleIncludesAvailable('editor:symbol-palette')
!= moduleIncludes('editor:symbol-palette', locals)

View file

@ -1,53 +0,0 @@
div.full-size(
ng-show="ui.view == 'editor' || ui.view === 'file'"
layout="pdf"
open-east="ui.pdfOpen"
mask-iframes-on-resize="true"
resize-on="layout:main:resize"
resize-proportionally="true"
initial-size-east="'50%'"
minimum-restore-size-east="300"
allow-overflow-on="'center'"
custom-toggler-pane="east"
custom-toggler-msg-when-open=translate("tooltip_hide_pdf")
custom-toggler-msg-when-closed=translate("tooltip_show_pdf")
)
include ./editor-pane
.ui-layout-east
div(ng-if="ui.pdfLayout == 'sideBySide'")
pdf-preview()
.ui-layout-resizer-controls.synctex-controls(
ng-show="settings.pdfViewer !== 'native'"
)
pdf-synctex-controls()
div.full-size(
ng-if="ui.pdfLayout == 'flat'"
ng-show="ui.view == 'pdf'"
)
pdf-preview()
// fallback, shown when no file/view is selected
div.full-size.no-file-selection(
ng-if="!ui.view"
)
.no-file-selection-message(
ng-if="rootFolder.children && rootFolder.children.length > 0"
)
h3
| #{translate('no_selection_select_file')}
.no-file-selection-message(
ng-if="rootFolder.children && rootFolder.children.length === 0"
)
h3
| #{translate('no_selection_create_new_file')}
div(
ng-controller="FileTreeController"
)
button.btn.btn-primary(
ng-click="openNewDocModal()"
)
| #{translate('new_file')}

View file

@ -1,3 +0,0 @@
aside.editor-sidebar.full-size.history-file-tree#history-file-tree(
ng-show="ui.view == 'history'"
)

View file

@ -1,27 +0,0 @@
aside.editor-sidebar.full-size(
ng-show="ui.view != 'history'"
vertical-resizable-panes="outline-resizer"
vertical-resizable-panes-toggled-externally-on="outline-toggled"
vertical-resizable-panes-default-size="350"
vertical-resizable-panes-min-size="32"
vertical-resizable-panes-max-size="'75%'"
vertical-resizable-panes-resize-on="left-pane-resize-all"
)
div(
ng-controller="ReactFileTreeController"
vertical-resizable-top
)
file-tree-root(
on-select="onSelect"
on-init="onInit"
is-connected="isConnected"
ref-providers="refProviders"
reindex-references="reindexReferences"
set-ref-provider-enabled="setRefProviderEnabled"
set-started-free-trial="setStartedFreeTrial"
)
outline-container(
vertical-resizable-bottom
)

View file

@ -1,9 +0,0 @@
div(
ng-controller="FileViewController"
ng-show="ui.view == 'file'"
ng-if="openFile && editor.multiSelectedCount === 0"
)
file-view(
file='file'
store-references-keys='storeReferencesKeys'
)

View file

@ -1,12 +0,0 @@
div(ng-controller="ReactShareProjectModalController")
share-project-modal(
handle-hide="handleHide"
show="show"
)
div(ng-controller="EditorNavigationToolbarController")
editor-navigation-toolbar-root(
open-doc="openDoc"
online-users-array="onlineUsersArray"
open-share-project-modal="openShareProjectModal"
)

View file

@ -1 +0,0 @@
editor-left-menu()

View file

@ -1,39 +0,0 @@
include ./left-menu-react
#chat-wrapper.full-size(
layout="chat",
spacing-open="{{ui.chatResizerSizeOpen}}",
spacing-closed="{{ui.chatResizerSizeClosed}}",
ng-hide="state.loading",
ng-cloak
)
.ui-layout-center
include ./header-react
main#ide-body(
ng-cloak,
role="main",
layout="main",
ng-hide="state.loading",
resize-on="layout:chat:resize,history:toggle,layout:flat-screen:toggle,south-pane-toggled",
minimum-restore-size-west="130"
custom-toggler-pane="west"
custom-toggler-msg-when-open=translate("tooltip_hide_filetree")
custom-toggler-msg-when-closed=translate("tooltip_show_filetree")
tabindex="0"
initial-size-east="250"
init-closed-east="true"
open-east="ui.chatOpen"
)
.ui-layout-west
include ./file-tree-react
include ./file-tree-history-react
.ui-layout-center
include ./editor
history-root()
if !isRestrictedTokenMember
.ui-layout-east
aside.chat
chat()

View file

@ -35,7 +35,6 @@ meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optional
meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature)
meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials)
meta(name="ol-projectTags" data-type="json" content=projectTags)
meta(name="ol-idePageReact", data-type="boolean" content=idePageReact)
meta(name="ol-loadingText", data-type="string" content=translate("loading"))
meta(name="ol-translationLoadErrorMessage", data-type="string" content=translate("could_not_load_translations"))

View file

@ -1,4 +1,4 @@
extends ../layout
extends ../layout-marketing
block entrypointVar
- entrypoint = 'ide-detached'

View file

@ -1,4 +1,4 @@
extends ../layout
extends ../layout-marketing
block vars
- var suppressNavbar = true

View file

@ -1,127 +0,0 @@
extends ../../layout
block vars
- var suppressFooter = true
- var suppressCookieBanner = true
- var suppressSkipToContent = true
block content
script(type="template", id="overleaf-token-access-data")!= StringHelper.stringifyJsonForScript({ postUrl: postUrl, csrfToken: csrfToken})
div(
ng-controller="TokenAccessPageController",
ng-init="post()"
)
.editor.full-size
div
|  
a(href="/project", style="font-size: 2rem; margin-left: 1rem; color: #ddd;")
i.fa.fa-arrow-left
.loading-screen(
ng-show="mode == 'accessAttempt'"
)
.loading-screen-brand-container
.loading-screen-brand()
h3.loading-screen-label.text-center
| #{translate('join_project')}
span(ng-show="accessInFlight == true")
span.loading-screen-ellip .
span.loading-screen-ellip .
span.loading-screen-ellip .
.global-alerts.text-center(ng-cloak)
div(ng-show="accessError", ng-cloak)
br
div(ng-switch="accessError", ng-cloak)
div(ng-switch-when="not_found")
h4(aria-live="assertive")
| Project not found
div(ng-switch-default)
.alert.alert-danger(aria-live="assertive") #{translate('token_access_failure')}
p
a(href="/") #{translate('home')}
.loading-screen(
ng-show="mode == 'v1Import'"
)
.container
.row
.col-sm-8.col-sm-offset-2
h1.text-center
span(ng-if="v1ImportData.status != 'mustLogin'") Overleaf v1 Project
span(ng-if="v1ImportData.status == 'mustLogin'") Please Log In
img.v2-import__img(
src="/img/v1-import/v2-editor.png"
alt="The new V2 editor."
)
div(ng-if="v1ImportData.status == 'cannotImport'")
h2.text-center
| Cannot Access Overleaf v1 Project
p.text-center.row-spaced-small
| Please contact the project owner or
|
a(href="/contact") contact support
|
| for assistance.
div(ng-if="v1ImportData.status == 'mustLogin'")
p.text-center.row-spaced-small
| You will need to log in to access this project.
.row-spaced.text-center
a.btn.btn-primary(
href="/login?redir={{ currentPath() }}"
) Log In To Access Project
div(ng-if="v1ImportData.status == 'canDownloadZip'")
p.text-center.row-spaced.small
| #[strong() {{ getProjectName() }}] has not yet been moved into
| the new version of Overleaf. This project was created
| anonymously and therefore cannot be automatically imported.
| Please download a zip file of the project and upload that to
| continue editing it. If you would like to delete this project
| after you have made a copy, please contact support.
.row-spaced.text-center
a.btn.btn-primary(ng-href="{{ buildZipDownloadPath(v1ImportData.projectId) }}")
| Download project zip file
.loading-screen(
ng-show="mode == 'requireAccept'"
)
.container
.row
.col-md-8.col-md-offset-2
.card
.page-header.text-centered
h1 #{translate("invited_to_join")}
br
em {{ getProjectName() }}
.row.text-center
.col-md-12
p
if user
| #{translate("accepting_invite_as")}
|
em #{user.email}
.row.text-center
.col-md-12
button.btn.btn-lg.btn-primary(
type='submit'
ng-click="postConfirmedByUser()"
) #{translate("join_project")}
block append foot-scripts
script(type="text/javascript", nonce=scriptNonce).
$(document).ready(function () {
setTimeout(function() {
$('.loading-screen-brand').css('height', '20%')
}, 500);
});

View file

@ -1,3 +0,0 @@
source-editor.review-panel-react#editor(
ng-show="!!editor.sharejs_doc && !editor.opening && multiSelectedCount === 0 && !editor.error_state"
)

View file

@ -1,10 +1,11 @@
extends ../layout
extends ../layout-marketing
include ./plans/_mixins
include ../_mixins/bootstrap_js
block entrypointVar
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
block vars
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
- var suppressFooter = true
- var suppressNavbarRight = true
- var suppressCookieBanner = true
@ -66,6 +67,3 @@ block content
| #{translate("continue_with_free_plan")}
!= moduleIncludes("contactModalGeneral-marketing", locals)
block prepend foot-scripts
+bootstrap-js(bootstrapVersion)

View file

@ -1,6 +1,6 @@
extends ../layout-marketing
block vars
block entrypointVar
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
block append meta

View file

@ -1,4 +1,4 @@
extends ../../layout
extends ../../layout-marketing
block content
main.content.content-alt.team-invite#main-content

View file

@ -1,47 +0,0 @@
script(type="text/ng-template", id="BonusLinkToUsModal")
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="cancel()"
aria-label="Close"
)
span(aria-hidden="true") ×
h3 Dropbox link
.modal-body.modal-body-share
div(ng-show="dbState.gotLinkStatus")
div(ng-hide="dbState.userIsLinkedToDropbox || !dbState.hasDropboxFeature")
span(ng-hide="dbState.startedLinkProcess") Your account is not linked to dropbox
|    
a(ng-click="linkToDropbox()").btn.btn-info Update Dropbox Settings
p.small.text-center(ng-show="dbState.startedLinkProcess")
| Please refresh this page after starting your free trial.
div(ng-show="dbState.hasDropboxFeature && dbState.userIsLinkedToDropbox")
progressbar.progress-striped.active(value='dbState.percentageLeftTillNextPoll', type="info")
span
strong {{dbState.minsTillNextPoll}} minutes
span until dropbox is next checked for changes.
div.text-center(ng-hide="dbState.hasDropboxFeature")
p You need to upgrade your account to link to dropbox.
p
a.btn(ng-click="startFreeTrial('dropbox')", ng-class="buttonClass") Start Free Trial
p.small(ng-show="startedFreeTrial")
| Please refresh this page after starting your free trial.
div(ng-hide="dbState.gotLinkStatus")
span.small   checking dropbox status  
i.fa.fa-refresh.fa-spin(aria-hidden="true")
.modal-footer()
button.btn.btn-default(
ng-click="cancel()",
)
span Dismiss

View file

@ -27,7 +27,7 @@
["@babel/react", { "runtime": "automatic" }],
"@babel/typescript"
],
"plugins": ["angularjs-annotate", "macros"],
"plugins": ["macros"],
"overrides": [
// treat .cjs files (e.g. libraries symlinked into node_modules) as commonjs
{

View file

@ -1,33 +1,6 @@
const fs = require('fs')
const Path = require('path')
const { merge } = require('@overleaf/settings/merge')
// Automatically detect module imports that are included in this version of the application (SaaS, Server-CE, Server Pro).
// E.g. during a Server-CE build, we will not find imports for proprietary modules.
//
// Restart webpack after adding/removing modules.
const MODULES_PATH = Path.join(__dirname, '../modules')
const entryPointsIde = []
const entryPointsMain = []
fs.readdirSync(MODULES_PATH).forEach(module => {
const entryPathIde = Path.join(
MODULES_PATH,
module,
'/frontend/js/ide/index.js'
)
if (fs.existsSync(entryPathIde)) {
entryPointsIde.push(entryPathIde)
}
const entryPathMain = Path.join(
MODULES_PATH,
module,
'/frontend/js/main/index.js'
)
if (fs.existsSync(entryPathMain)) {
entryPointsMain.push(entryPathMain)
}
})
let defaultFeatures, siteUrl
// Make time interval config easier.
@ -918,9 +891,6 @@ module.exports = {
managedGroupSubscriptionEnrollmentNotification: [],
managedGroupEnrollmentInvite: [],
ssoCertificateInfo: [],
// See comment at the definition of these variables.
entryPointsIde,
entryPointsMain,
},
moduleImportSequence: [
@ -935,7 +905,7 @@ module.exports = {
reportOnly: process.env.CSP_REPORT_ONLY === 'true',
reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0,
reportUri: process.env.CSP_REPORT_URI,
exclude: ['app/views/project/editor'],
exclude: [],
},
unsupportedBrowsers: {

View file

@ -11,6 +11,7 @@ import { interceptFileUpload } from './upload'
import { interceptProjectListing } from './project-list'
import { interceptLinkedFile } from './linked-file'
import { interceptMathJax } from './mathjax'
import { interceptMetadata } from './metadata'
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-namespace
declare global {
@ -21,6 +22,7 @@ declare global {
interceptAsync: typeof interceptAsync
interceptCompile: typeof interceptCompile
interceptEvents: typeof interceptEvents
interceptMetadata: typeof interceptMetadata
interceptSpelling: typeof interceptSpelling
waitForCompile: typeof waitForCompile
interceptDeferredCompile: typeof interceptDeferredCompile
@ -35,6 +37,7 @@ declare global {
Cypress.Commands.add('interceptAsync', interceptAsync)
Cypress.Commands.add('interceptCompile', interceptCompile)
Cypress.Commands.add('interceptEvents', interceptEvents)
Cypress.Commands.add('interceptMetadata', interceptMetadata)
Cypress.Commands.add('interceptSpelling', interceptSpelling)
Cypress.Commands.add('waitForCompile', waitForCompile)
Cypress.Commands.add('interceptDeferredCompile', interceptDeferredCompile)

View file

@ -0,0 +1,3 @@
export const interceptMetadata = () => {
cy.intercept('POST', '/project/*/doc/*/metadata', {})
}

View file

@ -1,61 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-useless-escape,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import './utils/webpack-public-path'
import './libraries'
import './infrastructure/error-reporter'
import './modules/recursionHelper'
import './modules/errorCatcher'
import './modules/localStorage'
import './modules/sessionStorage'
import getMeta from './utils/meta'
const App = angular
.module('OverleafApp', [
'ui.bootstrap',
'RecursionHelper',
'ngSanitize',
'ErrorCatcher',
'localStorage',
'sessionStorage',
'ui.select',
])
.config([
'$qProvider',
'uiSelectConfig',
function ($qProvider, uiSelectConfig) {
$qProvider.errorOnUnhandledRejections(false)
uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin'
},
])
App.run([
'$rootScope',
'$templateCache',
function ($rootScope, $templateCache) {
$rootScope.usersEmail = getMeta('ol-usersEmail')
// UI Select templates are hard-coded and use Glyphicon icons (which we don't import).
// The line below simply overrides the hard-coded template with our own, which is
// basically the same but using Font Awesome icons.
$templateCache.put(
'bootstrap/match.tpl.html',
'<div class="ui-select-match" ng-hide="$select.open && $select.searchEnabled" ng-disabled="$select.disabled" ng-class="{\'btn-default-focus\':$select.focus}"><span tabindex="-1" class="btn btn-default form-control ui-select-toggle" aria-label="{{ $select.baseTitle }} activate" ng-disabled="$select.disabled" ng-click="$select.activate()" style="outline: 0;"><span ng-show="$select.isEmpty()" class="ui-select-placeholder text-muted">{{$select.placeholder}}</span> <span ng-hide="$select.isEmpty()" class="ui-select-match-text pull-left" ng-class="{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}" ng-transclude=""></span> <i class="caret pull-right" ng-click="$select.toggle($event)"></i> <a ng-show="$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)" aria-label="{{ $select.baseTitle }} clear" style="margin-right: 10px" ng-click="$select.clear($event)" class="btn btn-xs btn-link pull-right"><i class="fa fa-times" aria-hidden="true"></i></a></span></div>'
)
},
])
export default App

View file

@ -1,134 +0,0 @@
import _ from 'lodash'
/* eslint-disable
camelcase,
max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
// For sending event data to metabase and google analytics
// ---
// by default,
// event not sent to MB.
// for MB, add event-tracking-mb='true'
// by default, event sent to MB via sendMB
// event not sent to GA.
// for GA, add event-tracking-ga attribute, where the value is the GA category
// Either GA or MB can use the attribute event-tracking-send-once='true' to
// send event just once
// MB will use the key and GA will use the action to determine if the event
// has been sent
// event-tracking-trigger attribute is required to send event
/* eslint-disable
camelcase,
max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
// For sending event data to metabase and google analytics
// ---
// by default,
// event not sent to MB.
// for MB, add event-tracking-mb='true'
// by default, event sent to MB via sendMB
// event not sent to GA.
// for GA, add event-tracking-ga attribute, where the value is the GA category
// Either GA or MB can use the attribute event-tracking-send-once='true' to
// send event just once
// MB will use the key and GA will use the action to determine if the event
// has been sent
// event-tracking-trigger attribute is required to send event
import App from '../base'
const isInViewport = function (element) {
const elTop = element.offset().top
const elBtm = elTop + element.outerHeight()
const viewportTop = $(window).scrollTop()
const viewportBtm = viewportTop + $(window).height()
return elBtm > viewportTop && elTop < viewportBtm
}
export default App.directive('eventTracking', [
'eventTracking',
function (eventTracking) {
return {
scope: {
eventTracking: '@',
eventSegmentation: '=?',
},
link(scope, element, attrs) {
const sendGA = attrs.eventTrackingGa || false
const sendMB = attrs.eventTrackingMb || false
const sendMBFunction = attrs.eventTrackingSendOnce
? 'sendMBOnce'
: 'sendMB'
const sendGAFunction = attrs.eventTrackingSendOnce
? 'sendGAOnce'
: 'send'
const segmentation = scope.eventSegmentation || {}
segmentation.page = window.location.pathname
const sendEvent = function (scrollEvent) {
/*
@param {boolean} scrollEvent Use to unbind scroll event
*/
if (sendMB) {
eventTracking[sendMBFunction](scope.eventTracking, segmentation)
}
if (sendGA) {
eventTracking[sendGAFunction](
attrs.eventTrackingGa,
attrs.eventTrackingAction || scope.eventTracking,
attrs.eventTrackingLabel || ''
)
}
if (scrollEvent) {
return $(window).unbind('resize scroll')
}
}
if (attrs.eventTrackingTrigger === 'load') {
return sendEvent()
} else if (attrs.eventTrackingTrigger === 'click') {
return element.on('click', e => sendEvent())
} else if (attrs.eventTrackingTrigger === 'hover') {
let timer = null
let timeoutAmt = 500
if (attrs.eventHoverAmt) {
timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
}
return element
.on('mouseenter', function () {
timer = setTimeout(() => sendEvent(), timeoutAmt)
})
.on('mouseleave', () => clearTimeout(timer))
} else if (
attrs.eventTrackingTrigger === 'scroll' &&
!eventTracking.eventInCache(scope.eventTracking)
) {
$(window).on(
'resize scroll',
_.throttle(() => {
if (isInViewport(element)) {
sendEvent(true)
}
}, 500)
)
}
},
}
},
])

View file

@ -1,6 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import { rootContext } from '../../../shared/context/root-context'
import EditorLeftMenu from '../components/editor-left-menu'
App.component('editorLeftMenu', react2angular(rootContext.use(EditorLeftMenu)))

View file

@ -7,8 +7,7 @@ export default function useProjectWideSettingsSocketListener() {
const ide = useIdeContext()
const [project, setProject] = useScopeValue<ProjectSettings | undefined>(
'project',
true
'project'
)
const setCompiler = useCallback(

View file

@ -8,7 +8,7 @@ import { debugConsole } from '@/utils/debugging'
export default function useProjectWideSettings() {
// The value will be undefined on mount
const [project] = useScopeValue<ProjectSettings | undefined>('project', true)
const [project] = useScopeValue<ProjectSettings | undefined>('project')
const saveProjectSettings = useSaveProjectSettings()
const setCompiler = useCallback(

View file

@ -6,7 +6,7 @@ export default function useSaveProjectSettings() {
// projectSettings value will be undefined on mount
const [projectSettings, setProjectSettings] = useScopeValue<
ProjectSettings | undefined
>('project', true)
>('project')
const { _id: projectId } = useProjectContext()
return async (

View file

@ -1,30 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import EditorNavigationToolbarRoot from '../components/editor-navigation-toolbar-root'
import { rootContext } from '../../../shared/context/root-context'
App.controller('EditorNavigationToolbarController', [
'$scope',
'ide',
function ($scope, ide) {
// wrapper is required to avoid scope problems with `this` inside `EditorManager`
$scope.openDoc = (doc, args) => ide.editorManager.openDoc(doc, args)
},
])
App.component(
'editorNavigationToolbarRoot',
react2angular(rootContext.use(EditorNavigationToolbarRoot), [
'openDoc',
// `$scope.onlineUsersArray` is already populated by `OnlineUsersManager`, which also creates
// a new array instance every time the list of online users change (which should refresh the
// value passed to React as a prop, triggering a re-render)
'onlineUsersArray',
// We're still including ShareController as part fo the React navigation toolbar. The reason is
// the coupling between ShareController's $scope and Angular's ShareProjectModal. Once ShareProjectModal
// is fully ported to React we should be able to repli
'openShareProjectModal',
])
)

View file

@ -1,123 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import { cloneDeep } from 'lodash'
import FileTreeRoot from '../components/file-tree-root'
import { rootContext } from '../../../shared/context/root-context'
App.controller('ReactFileTreeController', [
'$scope',
'$timeout',
'ide',
function ($scope, $timeout, ide) {
$scope.isConnected = true
$scope.$on('project:joined', () => {
$scope.$emit('file-tree:initialized')
})
$scope.$watch('editor.open_doc_id', openDocId => {
window.dispatchEvent(
new CustomEvent('editor.openDoc', { detail: openDocId })
)
})
$scope.$on('file-tree.reselectDoc', (ev, docId) => {
window.dispatchEvent(new CustomEvent('editor.openDoc', { detail: docId }))
})
// Set isConnected to true if:
// - connection state is 'ready', OR
// - connection state is 'waitingCountdown' and reconnection_countdown is null
// The added complexity is needed because in Firefox on page reload the
// connection state goes into 'waitingCountdown' before being hidden and we
// don't want to show a disconnect UI.
function updateIsConnected() {
if ($scope.connection) {
const isReady = $scope.connection.state === 'ready'
const willStartCountdown =
$scope.connection.state === 'waitingCountdown' &&
$scope.connection.reconnection_countdown === null
$scope.isConnected = isReady || willStartCountdown
} else {
$scope.isConnected = false
}
}
$scope.$watch('connection.state', updateIsConnected)
$scope.$watch('connection.reconnection_countdown', updateIsConnected)
$scope.onInit = () => {
// HACK: resize the vertical pane on init after a 0ms timeout. We do not
// understand why this is necessary but without this the resized handle is
// stuck at the bottom. The vertical resize will soon be migrated to React
// so we accept to live with this hack for now.
$timeout(() => {
$scope.$emit('left-pane-resize-all')
})
}
$scope.onSelect = selectedEntities => {
if (selectedEntities.length === 1) {
const selectedEntity = selectedEntities[0]
const type =
selectedEntity.type === 'fileRef' ? 'file' : selectedEntity.type
$scope.$emit('entity:selected', {
...selectedEntity.entity,
id: selectedEntity.entity._id,
type,
})
// in the react implementation there is no such concept as "1
// multi-selected entity" so here we pass a count of 0
$scope.$emit('entities:multiSelected', { count: 0 })
} else if (selectedEntities.length > 1) {
$scope.$emit('entities:multiSelected', {
count: selectedEntities.length,
})
} else {
$scope.$emit('entity:no-selection')
}
}
$scope.refProviders = ide.$scope.user.refProviders || {}
ide.$scope.$watch(
'user.refProviders',
refProviders => {
$scope.refProviders = cloneDeep(refProviders)
},
true
)
$scope.setRefProviderEnabled = (provider, value = true) => {
ide.$scope.$applyAsync(() => {
ide.$scope.user.refProviders[provider] = value
})
}
$scope.setStartedFreeTrial = started => {
$scope.$applyAsync(() => {
$scope.startedFreeTrial = started
})
}
$scope.reindexReferences = () => {
ide.$scope.$emit('references:should-reindex', {})
}
},
])
App.component(
'fileTreeRoot',
react2angular(rootContext.use(FileTreeRoot), [
'onSelect',
'onDelete',
'onInit',
'isConnected',
'setRefProviderEnabled',
'setStartedFreeTrial',
'reindexReferences',
'refProviders',
])
)

View file

@ -1,24 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import _ from 'lodash'
import { rootContext } from '../../../shared/context/root-context'
import FileView from '../components/file-view'
export default App.controller('FileViewController', [
'$scope',
'$rootScope',
function ($scope, $rootScope) {
$scope.file = $scope.openFile
$scope.storeReferencesKeys = newKeys => {
const oldKeys = $rootScope._references.keys
return ($rootScope._references.keys = _.union(oldKeys, newKeys))
}
},
])
App.component(
'fileView',
react2angular(rootContext.use(FileView), ['storeReferencesKeys', 'file'])
)

View file

@ -1,6 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import HistoryRoot from '../components/history-root'
import { rootContext } from '../../../shared/context/root-context'
App.component('historyRoot', react2angular(rootContext.use(HistoryRoot)))

View file

@ -27,9 +27,9 @@ type ConnectionContextValue = {
disconnect: () => void
}
const ConnectionContext = createContext<ConnectionContextValue | undefined>(
undefined
)
export const ConnectionContext = createContext<
ConnectionContextValue | undefined
>(undefined)
export const ConnectionProvider: FC = ({ children }) => {
const location = useLocation()

View file

@ -87,7 +87,9 @@ export type EditorScopeValue = {
error_state: boolean
}
const EditorManagerContext = createContext<EditorManager | undefined>(undefined)
export const EditorManagerContext = createContext<EditorManager | undefined>(
undefined
)
export const EditorManagerProvider: FC = ({ children }) => {
const { t } = useTranslation()

View file

@ -40,7 +40,7 @@ type IdeReactContextValue = {
projectJoined: boolean
}
const IdeReactContext = createContext<IdeReactContextValue | undefined>(
export const IdeReactContext = createContext<IdeReactContextValue | undefined>(
undefined
)
@ -63,7 +63,7 @@ function populatePdfScope(store: ReactScopeValueStore) {
store.allowNonExistentPath('pdf', true)
}
function createReactScopeValueStore(projectId: string) {
export function createReactScopeValueStore(projectId: string) {
const scopeStore = new ReactScopeValueStore()
// Populate the scope value store with default values that will be used by

View file

@ -15,7 +15,6 @@ import _ from 'lodash'
import { getJSON, postJSON } from '@/infrastructure/fetch-json'
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
import { useEditorContext } from '@/shared/context/editor-context'
import { useIdeContext } from '@/shared/context/ide-context'
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
import useEventListener from '@/shared/hooks/use-event-listener'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
@ -42,13 +41,12 @@ type MetadataContextValue = {
type DocMetadataResponse = { docId: string; meta: DocumentMetadata }
const MetadataContext = createContext<MetadataContextValue | undefined>(
export const MetadataContext = createContext<MetadataContextValue | undefined>(
undefined
)
export const MetadataProvider: FC = ({ children }) => {
const { t } = useTranslation()
const ide = useIdeContext()
const { eventEmitter, projectId } = useIdeReactContext()
const { socket } = useConnectionContext()
const { onlineUsersCount } = useOnlineUsersContext()
@ -225,10 +223,6 @@ export const MetadataProvider: FC = ({ children }) => {
[documents, getAllLabels, getAllPackages]
)
// Expose metadataManager via ide object because useCodeMirrorScope relies on
// it, for now
ide.metadataManager = value
return (
<MetadataContext.Provider value={value}>
{children}

View file

@ -9,7 +9,9 @@ import {
import useScopeValue from '@/shared/hooks/use-scope-value'
import { DeepReadonly } from '../../../../../types/utils'
const PermissionsContext = createContext<Permissions | undefined>(undefined)
export const PermissionsContext = createContext<Permissions | undefined>(
undefined
)
const permissionsMap: DeepReadonly<Record<PermissionsLevel, Permissions>> = {
readOnly: {

View file

@ -40,10 +40,10 @@ export const ReactContextRoot: FC = ({ children }) => {
<PermissionsProvider>
<ProjectSettingsProvider>
<LayoutProvider>
<EditorManagerProvider>
<LocalCompileProvider>
<DetachCompileProvider>
<ChatProvider>
<EditorManagerProvider>
<FileTreeOpenProvider>
<OnlineUsersProvider>
<MetadataProvider>
@ -53,10 +53,10 @@ export const ReactContextRoot: FC = ({ children }) => {
</MetadataProvider>
</OnlineUsersProvider>
</FileTreeOpenProvider>
</EditorManagerProvider>
</ChatProvider>
</DetachCompileProvider>
</LocalCompileProvider>
</EditorManagerProvider>
</LayoutProvider>
</ProjectSettingsProvider>
</PermissionsProvider>

View file

@ -170,10 +170,7 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
ReviewPanel.Value<'commentThreads'>
>({})
const [entries, setEntries] = useState<ReviewPanel.Value<'entries'>>({})
const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>(
'users',
true
)
const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>('users')
const [resolvedComments, setResolvedComments] = useState<
ReviewPanel.Value<'resolvedComments'>
>({})
@ -532,7 +529,6 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
if (!user) {
return 'anonymous'
}
// FIXME: check this
if (project.owner._id === user.id) {
return 'member'
}

View file

@ -1,22 +0,0 @@
import {
ScopeEventEmitter,
ScopeEventName,
} from '../../../../../types/ide/scope-event-emitter'
import { Scope } from '../../../../../types/angular/scope'
export class AngularScopeEventEmitter implements ScopeEventEmitter {
// eslint-disable-next-line no-useless-constructor
constructor(readonly $scope: Scope) {}
emit(eventName: ScopeEventName, broadcast: boolean, ...detail: unknown[]) {
if (broadcast) {
this.$scope.$broadcast(eventName, ...detail)
} else {
this.$scope.$emit(eventName, ...detail)
}
}
on(eventName: ScopeEventName, listener: (...args: unknown[]) => void) {
return this.$scope.$on(eventName, listener)
}
}

View file

@ -1,28 +0,0 @@
import { ScopeValueStore } from '../../../../../types/ide/scope-value-store'
import { Scope } from '../../../../../types/angular/scope'
import _ from 'lodash'
export class AngularScopeValueStore implements ScopeValueStore {
// eslint-disable-next-line no-useless-constructor
constructor(readonly $scope: Scope) {}
get(path: string) {
return _.get(this.$scope, path)
}
set(path: string, value: unknown): void {
this.$scope.$applyAsync(() => _.set(this.$scope, path, value))
}
watch<T>(
path: string,
callback: (newValue: T) => void,
deep: boolean
): () => void {
return this.$scope.$watch(
path,
(newValue: T) => callback(deep ? _.cloneDeep(newValue) : newValue),
deep
)
}
}

View file

@ -1,9 +0,0 @@
import App from '@/base'
import { react2angular } from 'react2angular'
import { rootContext } from '@/shared/context/root-context'
import { OutlineContainer } from '@/features/outline/components/outline-container'
App.component(
'outlineContainer',
react2angular(rootContext.use(OutlineContainer))
)

View file

@ -1,7 +1,7 @@
import ReactDOM from 'react-dom'
import PdfPreview from './pdf-preview'
import { ContextRoot } from '../../../shared/context/root-context'
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
function PdfPreviewDetachedRoot() {
const { isReady } = useWaitForI18n()
@ -11,9 +11,9 @@ function PdfPreviewDetachedRoot() {
}
return (
<ContextRoot>
<ReactContextRoot>
<PdfPreview />
</ContextRoot>
</ReactContextRoot>
)
}

View file

@ -1,19 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import PdfPreview from '../components/pdf-preview'
import { rootContext } from '../../../shared/context/root-context'
import {
DefaultSynctexControl,
DetacherSynctexControl,
} from '../components/detach-synctex-control'
App.component('pdfPreview', react2angular(rootContext.use(PdfPreview), []))
App.component(
'pdfSynctexControls',
react2angular(rootContext.use(DefaultSynctexControl), [])
)
App.component(
'detacherSynctexControl',
react2angular(rootContext.use(DetacherSynctexControl), [])
)

View file

@ -1,68 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import ShareProjectModal from '../components/share-project-modal'
import { rootContext } from '../../../shared/context/root-context'
import { listProjectInvites, listProjectMembers } from '../utils/api'
import { debugConsole } from '@/utils/debugging'
App.component(
'shareProjectModal',
react2angular(rootContext.use(ShareProjectModal), [
'animation',
'handleHide',
'show',
])
)
export default App.controller('ReactShareProjectModalController', [
'$scope',
'eventTracking',
'ide',
function ($scope, eventTracking, ide) {
$scope.show = false
$scope.handleHide = () => {
$scope.$applyAsync(() => {
$scope.show = false
})
}
$scope.openShareProjectModal = () => {
eventTracking.sendMBOnce('ide-open-share-modal-once')
$scope.$applyAsync(() => {
$scope.show = true
})
}
ide.socket.on('project:membership:changed', data => {
if (data.members) {
listProjectMembers($scope.project._id)
.then(({ members }) => {
if (members) {
$scope.$applyAsync(() => {
$scope.project.members = members
})
}
})
.catch(err => {
debugConsole.error('Error fetching members for project', err)
})
}
if (data.invites) {
listProjectInvites($scope.project._id)
.then(({ invites }) => {
if (invites) {
$scope.$applyAsync(() => {
$scope.project.invites = invites
})
}
})
.catch(err => {
debugConsole.error('Error fetching invites for project', err)
})
}
})
},
])

View file

@ -8,12 +8,10 @@ import {
useReviewPanelUpdaterFnsContext,
useReviewPanelValueContext,
} from '../../../context/review-panel/review-panel-context'
import { useIdeContext } from '@/shared/context/ide-context'
import { useEditorContext } from '@/shared/context/editor-context'
import { useCodeMirrorViewContext } from '../../codemirror-editor'
import Modal, { useBulkActionsModal } from '../entries/bulk-actions-entry/modal'
import getMeta from '../../../../../utils/meta'
import useScopeValue from '../../../../../shared/hooks/use-scope-value'
import useScopeEventListener from '@/shared/hooks/use-scope-event-listener'
import { memo, useCallback } from 'react'
import { useLayoutContext } from '@/shared/context/layout-context'
@ -29,14 +27,8 @@ function EditorWidgets() {
handleShowBulkRejectDialog,
handleConfirmDialog,
} = useBulkActionsModal()
const { setIsAddingComment, handleSetSubview } =
useReviewPanelUpdaterFnsContext()
const { isReactIde } = useIdeContext()
const { setIsAddingComment } = useReviewPanelUpdaterFnsContext()
const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext()
const [addNewComment] =
useScopeValue<(e: React.MouseEvent<HTMLButtonElement>) => void>(
'addNewComment'
)
const view = useCodeMirrorViewContext()
const { reviewPanelOpen } = useLayoutContext()
const { isRestrictedTokenMember } = useEditorContext()
@ -55,20 +47,9 @@ function EditorWidgets() {
openDocId && openDocId in entries ? entries[openDocId] : undefined
const handleAddNewCommentClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (isReactIde) {
e.preventDefault()
setIsAddingComment(true)
toggleReviewPanel()
return
}
addNewComment(e)
setTimeout(() => {
// Re-render the comment box in order to add autofocus every time
handleSetSubview('cur_file')
setIsAddingComment(false)
setIsAddingComment(true)
}, 0)
}
useScopeEventListener(

View file

@ -8,26 +8,14 @@ import { isCurrentFileView } from '../../utils/sub-view'
import { useLayoutContext } from '@/shared/context/layout-context'
import classnames from 'classnames'
import { lazy, memo } from 'react'
import getMeta from '@/utils/meta'
import { SubView } from '../../../../../../types/review-panel/review-panel'
const isReactIde: boolean = getMeta('ol-idePageReact')
type ReviewPanelViewProps = {
parentDomNode: Element
}
function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
const { subView } = useReviewPanelValueContext()
return ReactDOM.createPortal(
isReactIde ? (
<ReviewPanelContainer />
) : (
<ReviewPanelContent subView={subView} />
),
parentDomNode
)
return ReactDOM.createPortal(<ReviewPanelContainer />, parentDomNode)
}
const ReviewPanelContainer = memo(() => {
@ -65,10 +53,9 @@ const ReviewPanelContent = memo<{ subView: SubView }>(({ subView }) => (
))
ReviewPanelContent.displayName = 'ReviewPanelContent'
const ReviewPanelProvider = lazy(() =>
isReactIde
? import('@/features/ide-react/context/review-panel/review-panel-provider')
: import('../../context/review-panel/review-panel-provider')
const ReviewPanelProvider = lazy(
() =>
import('@/features/ide-react/context/review-panel/review-panel-provider')
)
function ReviewPanel() {

View file

@ -1,275 +0,0 @@
import { useState, useMemo, useCallback } from 'react'
import useScopeValue from '../../../../../shared/hooks/use-scope-value'
import useLayoutToLeft from '@/features/ide-react/context/review-panel/hooks/useLayoutToLeft'
import { sendMB } from '../../../../../infrastructure/event-tracking'
import type * as ReviewPanel from '../types/review-panel-state'
import {
SubView,
ThreadId,
} from '../../../../../../../types/review-panel/review-panel'
import { DocId } from '../../../../../../../types/project-settings'
import { dispatchReviewPanelLayout as handleLayoutChange } from '../../../extensions/changes/change-manager'
function useAngularReviewPanelState(): ReviewPanel.ReviewPanelState {
const [subView, setSubView] = useScopeValue<ReviewPanel.Value<'subView'>>(
'reviewPanel.subView'
)
const [isOverviewLoading] = useScopeValue<
ReviewPanel.Value<'isOverviewLoading'>
>('reviewPanel.overview.loading')
const [nVisibleSelectedChanges] = useScopeValue<
ReviewPanel.Value<'nVisibleSelectedChanges'>
>('reviewPanel.nVisibleSelectedChanges')
const [collapsed, setCollapsed] = useScopeValue<
ReviewPanel.Value<'collapsed'>
>('reviewPanel.overview.docsCollapsedState')
const [commentThreads] = useScopeValue<ReviewPanel.Value<'commentThreads'>>(
'reviewPanel.commentThreads',
true
)
const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>(
'reviewPanel.entries',
true
)
const [loadingThreads] =
useScopeValue<ReviewPanel.Value<'loadingThreads'>>('loadingThreads')
const [permissions] =
useScopeValue<ReviewPanel.Value<'permissions'>>('permissions')
const [users] = useScopeValue<ReviewPanel.Value<'users'>>('users', true)
const [resolvedComments] = useScopeValue<
ReviewPanel.Value<'resolvedComments'>
>('reviewPanel.resolvedComments', true)
const [wantTrackChanges] = useScopeValue<
ReviewPanel.Value<'wantTrackChanges'>
>('editor.wantTrackChanges')
const [openDocId] =
useScopeValue<ReviewPanel.Value<'openDocId'>>('editor.open_doc_id')
const [shouldCollapse, setShouldCollapse] = useScopeValue<
ReviewPanel.Value<'shouldCollapse'>
>('reviewPanel.fullTCStateCollapsed')
const [lineHeight] = useScopeValue<number>(
'reviewPanel.rendererData.lineHeight'
)
const [toggleTrackChangesForEveryone] = useScopeValue<
ReviewPanel.UpdaterFn<'toggleTrackChangesForEveryone'>
>('toggleTrackChangesForEveryone')
const [toggleTrackChangesForUser] = useScopeValue<
ReviewPanel.UpdaterFn<'toggleTrackChangesForUser'>
>('toggleTrackChangesForUser')
const [toggleTrackChangesForGuests] = useScopeValue<
ReviewPanel.UpdaterFn<'toggleTrackChangesForGuests'>
>('toggleTrackChangesForGuests')
const [trackChangesState] = useScopeValue<
ReviewPanel.Value<'trackChangesState'>
>('reviewPanel.trackChangesState')
const [trackChangesOnForEveryone] = useScopeValue<
ReviewPanel.Value<'trackChangesOnForEveryone'>
>('reviewPanel.trackChangesOnForEveryone')
const [trackChangesOnForGuests] = useScopeValue<
ReviewPanel.Value<'trackChangesOnForGuests'>
>('reviewPanel.trackChangesOnForGuests')
const [trackChangesForGuestsAvailable] = useScopeValue<
ReviewPanel.Value<'trackChangesForGuestsAvailable'>
>('reviewPanel.trackChangesForGuestsAvailable')
const [resolveComment] =
useScopeValue<ReviewPanel.UpdaterFn<'resolveComment'>>('resolveComment')
const [submitNewComment] =
useScopeValue<ReviewPanel.UpdaterFn<'submitNewComment'>>('submitNewComment')
const [deleteComment] =
useScopeValue<ReviewPanel.UpdaterFn<'deleteComment'>>('deleteComment')
const [gotoEntry] =
useScopeValue<ReviewPanel.UpdaterFn<'gotoEntry'>>('gotoEntry')
const [saveEdit] =
useScopeValue<ReviewPanel.UpdaterFn<'saveEdit'>>('saveEdit')
const [submitReplyAngular] =
useScopeValue<
(entry: { thread_id: ThreadId; replyContent: string }) => void
>('submitReply')
const [formattedProjectMembers] = useScopeValue<
ReviewPanel.Value<'formattedProjectMembers'>
>('reviewPanel.formattedProjectMembers')
const [toggleReviewPanel] =
useScopeValue<ReviewPanel.UpdaterFn<'toggleReviewPanel'>>(
'toggleReviewPanel'
)
const [unresolveComment] =
useScopeValue<ReviewPanel.UpdaterFn<'unresolveComment'>>('unresolveComment')
const [deleteThreadAngular] =
useScopeValue<
(
_: unknown,
...args: [...Parameters<ReviewPanel.UpdaterFn<'deleteThread'>>]
) => ReturnType<ReviewPanel.UpdaterFn<'deleteThread'>>
>('deleteThread')
const deleteThread = useCallback(
(docId: DocId, threadId: ThreadId) => {
deleteThreadAngular(undefined, docId, threadId)
},
[deleteThreadAngular]
)
const [refreshResolvedCommentsDropdown] = useScopeValue<
ReviewPanel.UpdaterFn<'refreshResolvedCommentsDropdown'>
>('refreshResolvedCommentsDropdown')
const [acceptChanges] =
useScopeValue<ReviewPanel.UpdaterFn<'acceptChanges'>>('acceptChanges')
const [rejectChanges] =
useScopeValue<ReviewPanel.UpdaterFn<'rejectChanges'>>('rejectChanges')
const [bulkAcceptActions] =
useScopeValue<ReviewPanel.UpdaterFn<'bulkAcceptActions'>>(
'bulkAcceptActions'
)
const [bulkRejectActions] =
useScopeValue<ReviewPanel.UpdaterFn<'bulkRejectActions'>>(
'bulkRejectActions'
)
const layoutToLeft = useLayoutToLeft('#editor')
const handleSetSubview = useCallback(
(subView: SubView) => {
setSubView(subView)
sendMB('rp-subview-change', { subView })
},
[setSubView]
)
const submitReply = useCallback(
(threadId: ThreadId, replyContent: string) => {
submitReplyAngular({ thread_id: threadId, replyContent })
},
[submitReplyAngular]
)
const [isAddingComment, setIsAddingComment] = useState(false)
const [navHeight, setNavHeight] = useState(0)
const [toolbarHeight, setToolbarHeight] = useState(0)
const [layoutSuspended, setLayoutSuspended] = useState(false)
const [unsavedComment, setUnsavedComment] = useState('')
const values = useMemo<ReviewPanel.ReviewPanelState['values']>(
() => ({
collapsed,
commentThreads,
entries,
isAddingComment,
loadingThreads,
nVisibleSelectedChanges,
permissions,
users,
resolvedComments,
shouldCollapse,
navHeight,
toolbarHeight,
subView,
wantTrackChanges,
isOverviewLoading,
openDocId,
lineHeight,
trackChangesState,
trackChangesOnForEveryone,
trackChangesOnForGuests,
trackChangesForGuestsAvailable,
formattedProjectMembers,
layoutSuspended,
unsavedComment,
layoutToLeft,
}),
[
collapsed,
commentThreads,
entries,
isAddingComment,
loadingThreads,
nVisibleSelectedChanges,
permissions,
users,
resolvedComments,
shouldCollapse,
navHeight,
toolbarHeight,
subView,
wantTrackChanges,
isOverviewLoading,
openDocId,
lineHeight,
trackChangesState,
trackChangesOnForEveryone,
trackChangesOnForGuests,
trackChangesForGuestsAvailable,
formattedProjectMembers,
layoutSuspended,
unsavedComment,
layoutToLeft,
]
)
const updaterFns = useMemo<ReviewPanel.ReviewPanelState['updaterFns']>(
() => ({
handleSetSubview,
handleLayoutChange,
gotoEntry,
resolveComment,
submitReply,
acceptChanges,
rejectChanges,
toggleReviewPanel,
bulkAcceptActions,
bulkRejectActions,
saveEdit,
submitNewComment,
deleteComment,
unresolveComment,
refreshResolvedCommentsDropdown,
deleteThread,
toggleTrackChangesForEveryone,
toggleTrackChangesForUser,
toggleTrackChangesForGuests,
setCollapsed,
setShouldCollapse,
setIsAddingComment,
setNavHeight,
setToolbarHeight,
setLayoutSuspended,
setUnsavedComment,
}),
[
handleSetSubview,
gotoEntry,
resolveComment,
submitReply,
acceptChanges,
rejectChanges,
toggleReviewPanel,
bulkAcceptActions,
bulkRejectActions,
saveEdit,
submitNewComment,
deleteComment,
unresolveComment,
refreshResolvedCommentsDropdown,
deleteThread,
toggleTrackChangesForEveryone,
toggleTrackChangesForUser,
toggleTrackChangesForGuests,
setCollapsed,
setShouldCollapse,
setIsAddingComment,
setNavHeight,
setToolbarHeight,
setLayoutSuspended,
setUnsavedComment,
]
)
return { values, updaterFns }
}
export default useAngularReviewPanelState

View file

@ -1,20 +0,0 @@
import useAngularReviewPanelState from '@/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state'
import { FC } from 'react'
import {
ReviewPanelUpdaterFnsContext,
ReviewPanelValueContext,
} from './review-panel-context'
const ReviewPanelProvider: FC = ({ children }) => {
const { values, updaterFns } = useAngularReviewPanelState()
return (
<ReviewPanelValueContext.Provider value={values}>
<ReviewPanelUpdaterFnsContext.Provider value={updaterFns}>
{children}
</ReviewPanelUpdaterFnsContext.Provider>
</ReviewPanelValueContext.Provider>
)
}
export default ReviewPanelProvider

View file

@ -1,9 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import { rootContext } from '../../../shared/context/root-context'
import GrammarlyAdvert from '../components/grammarly-advert'
App.component(
'grammarlyAdvert',
react2angular(rootContext.use(GrammarlyAdvert))
)

View file

@ -1,6 +0,0 @@
import { react2angular } from 'react2angular'
import SourceEditor from '../components/source-editor'
import App from '../../../base'
import { rootContext } from '../../../shared/context/root-context'
App.component('sourceEditor', react2angular(rootContext.use(SourceEditor), []))

View file

@ -26,7 +26,6 @@ import {
setMetadata,
setSyntaxValidation,
} from '../extensions/language'
import { useIdeContext } from '../../../shared/context/ide-context'
import { restoreScrollPosition } from '../extensions/scroll-position'
import { setEditable } from '../extensions/editable'
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
@ -59,10 +58,9 @@ import grammarlyExtensionPresent from '@/shared/utils/grammarly'
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
import { useLayoutContext } from '@/shared/context/layout-context'
import { debugConsole } from '@/utils/debugging'
import { useMetadataContext } from '@/features/ide-react/context/metadata-context'
function useCodeMirrorScope(view: EditorView) {
const ide = useIdeContext()
const { fileTreeData } = useFileTreeData()
const [permissions] = useScopeValue<{ write: boolean }>('permissions')
@ -74,6 +72,8 @@ function useCodeMirrorScope(view: EditorView) {
const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext()
const { metadata } = useMetadataContext()
const [loadingThreads] = useScopeValue<boolean>('loadingThreads')
const [currentDoc] = useScopeValue<DocumentContainer | null>(
@ -211,22 +211,16 @@ function useCodeMirrorScope(view: EditorView) {
// set the project metadata, mostly for use in autocomplete
// TODO: read this data from the scope?
const metadataRef = useRef({
documents: ide.metadataManager.metadata.state.documents,
documents: metadata.state.documents,
references: references.keys,
fileTreeData,
})
// listen to project metadata (docs + packages) updates
useEffect(() => {
const listener = (event: Event) => {
metadataRef.current.documents = (
event as CustomEvent<Record<string, any>>
).detail
metadataRef.current.documents = metadata.state.documents
view.dispatch(setMetadata(metadataRef.current))
}
window.addEventListener('project:metadata', listener)
return () => window.removeEventListener('project:metadata', listener)
}, [view])
}, [view, metadata.state.documents])
// listen to project reference keys updates
useEffect(() => {

View file

@ -1 +0,0 @@
import '../controllers/source-editor-controller'

View file

@ -1,422 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-cond-assign,
no-return-assign,
no-unused-vars,
no-useless-escape,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from './base'
import FileTreeManager from './ide/file-tree/FileTreeManager'
import LoadingManager from './ide/LoadingManager'
import ConnectionManager from './ide/connection/ConnectionManager'
import EditorManager from './ide/editor/EditorManager'
import OnlineUsersManager from './ide/online-users/OnlineUsersManager'
import PermissionsManager from './ide/permissions/PermissionsManager'
import BinaryFilesManager from './ide/binary-files/BinaryFilesManager'
import ReferencesManager from './ide/references/ReferencesManager'
import MetadataManager from './ide/metadata/MetadataManager'
import './ide/review-panel/ReviewPanelManager'
import './ide/cobranding/CobrandingDataService'
import './ide/chat/index'
import './ide/file-view/index'
import './ide/toolbar/index'
import './ide/directives/layout'
import './ide/directives/verticalResizablePanes'
import './ide/services/ide'
import './services/queued-http' // used in FileTreeManager
import './main/event' // used in various controllers
import './main/system-messages' // used in project/editor
import '../../modules/modules-ide'
import './features/source-editor/ide'
import './shared/context/controllers/root-context-controller'
import './features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller'
import './features/pdf-preview/controllers/pdf-preview-controller'
import './features/share-project-modal/controllers/react-share-project-modal-controller'
import './features/source-editor/controllers/grammarly-advert-controller'
import './features/history/controllers/history-controller'
import './features/editor-left-menu/controllers/editor-left-menu-controller'
import './features/outline/controllers/outline-controller'
import { cleanupServiceWorker } from './utils/service-worker-cleanup'
import { reportCM6Perf } from './infrastructure/cm6-performance'
import { debugConsole } from '@/utils/debugging'
App.controller('IdeController', [
'$scope',
'$timeout',
'ide',
'localStorage',
'eventTracking',
'metadata',
'CobrandingDataService',
'$window',
function (
$scope,
$timeout,
ide,
localStorage,
eventTracking,
metadata,
CobrandingDataService,
$window
) {
// Don't freak out if we're already in an apply callback
let err, pdfLayout, userAgent
$scope.$originalApply = $scope.$apply
$scope.$apply = function (fn) {
if (fn == null) {
fn = function () {}
}
const phase = this.$root.$$phase
if (phase === '$apply' || phase === '$digest') {
return fn()
} else {
return this.$originalApply(fn)
}
}
$scope.state = {
loading: true,
load_progress: 40,
error: null,
}
$scope.ui = {
leftMenuShown: false,
view: 'editor',
chatOpen: false,
pdfLayout: 'sideBySide',
pdfHidden: false,
pdfWidth: 0,
reviewPanelOpen: localStorage(`ui.reviewPanelOpen.${window.project_id}`),
miniReviewPanelVisible: false,
chatResizerSizeOpen: 7,
chatResizerSizeClosed: 0,
}
$scope.user = window.user
$scope.settings = window.userSettings
$scope.anonymous = window.anonymous
$scope.isTokenMember = window.isTokenMember
$scope.isRestrictedTokenMember = window.isRestrictedTokenMember
$scope.cobranding = {
isProjectCobranded: CobrandingDataService.isProjectCobranded(),
logoImgUrl: CobrandingDataService.getLogoImgUrl(),
submitBtnHtml: CobrandingDataService.getSubmitBtnHtml(),
brandVariationName: CobrandingDataService.getBrandVariationName(),
brandVariationHomeUrl: CobrandingDataService.getBrandVariationHomeUrl(),
}
$scope.chat = {}
ide.toggleReviewPanel = $scope.toggleReviewPanel = function () {
$scope.$applyAsync(() => {
if (!$scope.project.features.trackChangesVisible) {
return
}
$scope.ui.reviewPanelOpen = !$scope.ui.reviewPanelOpen
eventTracking.sendMB('rp-toggle-panel', {
value: $scope.ui.reviewPanelOpen,
})
})
}
$scope.$watch('ui.reviewPanelOpen', function (value) {
if (value != null) {
return localStorage(`ui.reviewPanelOpen.${window.project_id}`, value)
}
})
$scope.$on('layout:pdf:resize', function (_, layoutState) {
$scope.ui.pdfHidden = layoutState.east.initClosed
return ($scope.ui.pdfWidth = layoutState.east.size)
})
$scope.$watch('ui.view', function (newView, oldView) {
if (newView !== oldView) {
$scope.$broadcast('layout:flat-screen:toggle')
}
if (newView != null && newView !== 'editor' && newView !== 'pdf') {
eventTracking.sendMBOnce(`ide-open-view-${newView}-once`)
}
})
$scope.$watch('ui.chatOpen', function (isOpen) {
if (isOpen) {
eventTracking.sendMBOnce('ide-open-chat-once')
}
})
$scope.$watch('ui.leftMenuShown', function (isOpen) {
if (isOpen) {
eventTracking.sendMBOnce('ide-open-left-menu-once')
}
})
$scope.trackHover = feature => {
eventTracking.sendMBOnce(`ide-hover-${feature}-once`)
}
// End of tracking code.
window._ide = ide
ide.validFileRegex = '^[^*/]*$' // Don't allow * and /
ide.project_id = $scope.project_id = window.project_id
ide.$scope = $scope
ide.referencesSearchManager = new ReferencesManager(ide, $scope)
ide.loadingManager = new LoadingManager($scope)
ide.connectionManager = new ConnectionManager(ide, $scope)
ide.fileTreeManager = new FileTreeManager(ide, $scope)
ide.editorManager = new EditorManager(
ide,
$scope,
localStorage,
eventTracking
)
ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
ide.permissionsManager = new PermissionsManager(ide, $scope)
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
ide.metadataManager = new MetadataManager(ide, $scope, metadata)
let inited = false
$scope.$on('project:joined', function () {
if (inited) {
return
}
inited = true
if (
__guard__(
$scope != null ? $scope.project : undefined,
x => x.deletedByExternalDataSource
)
) {
ide.showGenericMessageModal(
'Project Renamed or Deleted',
`\
This project has either been renamed or deleted by an external data source such as Dropbox.
We don't want to delete your data on Overleaf, so this project still contains your history and collaborators.
If the project has been renamed please look in your project list for a new project under the new name.\
`
)
}
return $timeout(function () {
if ($scope.permissions.write) {
let _labelsInitialLoadDone
ide.metadataManager.loadProjectMetaFromServer()
return (_labelsInitialLoadDone = true)
}
}, 200)
})
// Count the first 'doc:opened' as a sign that the ide is loaded
// and broadcast a message. This is a good event to listen for
// if you want to wait until the ide is fully loaded and initialized
let _loaded = false
$scope.$on('doc:opened', function () {
if (_loaded) {
return
}
$scope.$broadcast('ide:loaded')
return (_loaded = true)
})
ide.editingSessionHeartbeat = () => {
eventTracking.editingSessionHeartbeat(() => {
const editorType = ide.editorManager.getEditorType()
const segmentation = {
editorType,
}
if (editorType === 'cm6' || editorType === 'cm6-rich-text') {
const cm6PerfData = reportCM6Perf()
// Ignore if no typing has happened
if (cm6PerfData.numberOfEntries > 0) {
const perfProps = [
'Max',
'Mean',
'Median',
'NinetyFifthPercentile',
'DocLength',
'NumberOfEntries',
'MaxUserEventsBetweenDomUpdates',
'Grammarly',
'SessionLength',
'Memory',
'Lags',
'NonLags',
'LongestLag',
'MeanLagsPerMeasure',
'MeanKeypressesPerMeasure',
'MeanKeypressPaint',
'LongTasks',
'Release',
]
for (const prop of perfProps) {
const perfValue =
cm6PerfData[prop.charAt(0).toLowerCase() + prop.slice(1)]
if (perfValue !== null) {
segmentation['cm6Perf' + prop] = perfValue
}
}
}
}
return segmentation
})
}
$scope.$on('cursor:editor:update', () => {
ide.editingSessionHeartbeat()
})
$scope.$on('scroll:editor:update', () => {
ide.editingSessionHeartbeat()
})
angular.element($window).on('click', ide.editingSessionHeartbeat)
$scope.$on('$destroy', () =>
angular.element($window).off('click', ide.editingSessionHeartbeat)
)
const DARK_THEMES = [
'ambiance',
'chaos',
'clouds_midnight',
'cobalt',
'idle_fingers',
'merbivore',
'merbivore_soft',
'mono_industrial',
'monokai',
'pastel_on_dark',
'solarized_dark',
'terminal',
'tomorrow_night',
'tomorrow_night_blue',
'tomorrow_night_bright',
'tomorrow_night_eighties',
'twilight',
'vibrant_ink',
]
$scope.darkTheme = false
// Listen for settings change from React
window.addEventListener('settings:change', event => {
$scope.darkTheme = DARK_THEMES.includes(event.detail.editorTheme)
})
ide.localStorage = localStorage
$scope.switchToFlatLayout = function (view) {
$scope.ui.pdfLayout = 'flat'
$scope.ui.view = view
return ide.localStorage('pdf.layout', 'flat')
}
$scope.switchToSideBySideLayout = function (view) {
$scope.ui.pdfLayout = 'sideBySide'
$scope.ui.view = view
return localStorage('pdf.layout', 'split')
}
if ((pdfLayout = localStorage('pdf.layout'))) {
if (pdfLayout === 'split') {
$scope.switchToSideBySideLayout()
}
if (pdfLayout === 'flat') {
$scope.switchToFlatLayout()
}
} else {
$scope.switchToSideBySideLayout()
}
// Update ui.pdfOpen when the layout changes.
// The east pane should open when the layout changes from "Editor only" or "PDF only" to "Editor & PDF".
$scope.$watch('ui.pdfLayout', value => {
$scope.ui.pdfOpen = value === 'sideBySide'
})
// Update ui.pdfLayout when the east pane is toggled.
// 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.
$scope.$watch('ui.pdfOpen', value => {
$scope.ui.pdfLayout = value ? 'sideBySide' : 'flat'
if (value) {
window.dispatchEvent(new CustomEvent('ui:pdf-open'))
}
})
$scope.handleKeyDown = () => {
// unused?
}
// User can append ?ft=somefeature to url to activate a feature toggle
ide.featureToggle = __guard__(
__guard__(
typeof location !== 'undefined' && location !== null
? location.search
: undefined,
x1 => x1.match(/^\?ft=(\w+)$/)
),
x => x[1]
)
// Listen for editor:lint event from CM6 linter
window.addEventListener('editor:lint', event => {
$scope.hasLintingError = event.detail.hasLintingError
})
ide.socket.on('project:access:revoked', () => {
ide.showGenericMessageModal(
'Removed From Project',
'You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.'
)
})
return ide.socket.on('project:publicAccessLevel:changed', data => {
if (data.newAccessLevel != null) {
ide.$scope.project.publicAccesLevel = data.newAccessLevel
return $scope.$digest()
}
})
},
])
cleanupServiceWorker()
angular.module('OverleafApp').config([
'$provide',
function ($provide) {
$provide.decorator('$browser', [
'$delegate',
function ($delegate) {
$delegate.onUrlChange = function () {}
$delegate.url = function () {
return ''
}
return $delegate
},
])
},
])
export default angular.bootstrap(document.body, ['OverleafApp'])
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View file

@ -1,37 +0,0 @@
import i18n from '../i18n'
// Control the editor loading screen. We want to show the loading screen until
// both the websocket connection has been established (so that the editor is in
// the correct state) and the translations have been loaded (so we don't see a
// flash of untranslated text).
class LoadingManager {
constructor($scope) {
this.$scope = $scope
const socketPromise = new Promise(resolve => {
this.resolveSocketPromise = resolve
})
Promise.all([socketPromise, i18n])
.then(() => {
this.$scope.$apply(() => {
this.$scope.state.load_progress = 100
this.$scope.state.loading = false
this.$scope.$emit('editor:loaded')
})
})
// Note: this will only catch errors in from i18n setup. ConnectionManager
// handles errors for the socket connection
.catch(() => {
this.$scope.$apply(() => {
this.$scope.state.error = 'Could not load translations.'
})
})
}
socketLoaded() {
this.resolveSocketPromise()
}
}
export default LoadingManager

View file

@ -1,73 +0,0 @@
/* eslint-disable
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let BinaryFilesManager
export default BinaryFilesManager = class BinaryFilesManager {
constructor(ide, $scope) {
this.ide = ide
this.$scope = $scope
this.$scope.$on('entity:selected', (event, entity) => {
if (this.$scope.ui.view !== 'track-changes' && entity.type === 'file') {
return this.openFile(entity)
} else if (entity.type === 'doc') {
return this.closeFile()
}
})
}
openFile(file) {
if (this.$scope.ui.view === 'editor') {
// store position before switching to binary view
this.$scope.$broadcast('store-doc-position')
}
this.ide.fileTreeManager.selectEntity(file)
if (this.$scope.ui.view !== 'history') {
this.$scope.ui.view = 'file'
}
this.$scope.openFile = null
this.$scope.$apply()
return window.setTimeout(
() => {
this.$scope.openFile = file
this.$scope.$apply()
this.$scope.$broadcast('file-view:file-opened')
window.dispatchEvent(new Event('file-view:file-opened'))
},
0,
this
)
}
openFileWithId(id) {
const entity = this.ide.fileTreeManager.findEntityById(id)
if (entity?.type === 'file') {
this.openFile(entity)
}
}
closeFile() {
return window.setTimeout(
() => {
this.$scope.openFile = null
if (this.$scope.ui.view !== 'history') {
this.$scope.ui.view = 'editor'
}
this.$scope.$apply()
},
0,
this
)
}
}

View file

@ -1,6 +0,0 @@
import App from '../../base'
import { rootContext } from '../../shared/context/root-context'
import ChatPane from '../../features/chat/components/chat-pane'
import { react2angular } from 'react2angular'
App.component('chat', react2angular(rootContext.use(ChatPane)))

View file

@ -1,59 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from '../../base'
const _cobrandingData = window.brandVariation
export default App.factory('CobrandingDataService', function () {
const isProjectCobranded = () => _cobrandingData != null
const getLogoImgUrl = () =>
_cobrandingData != null ? _cobrandingData.logo_url : undefined
const getSubmitBtnHtml = () =>
_cobrandingData != null ? _cobrandingData.submit_button_html : undefined
const getBrandVariationName = () =>
_cobrandingData != null ? _cobrandingData.name : undefined
const getBrandVariationHomeUrl = () =>
_cobrandingData != null ? _cobrandingData.home_url : undefined
const getPublishGuideHtml = () =>
_cobrandingData != null ? _cobrandingData.publish_guide_html : undefined
const getPartner = () =>
_cobrandingData != null ? _cobrandingData.partner : undefined
const hasBrandedMenu = () =>
_cobrandingData != null ? _cobrandingData.branded_menu : undefined
const getBrandId = () =>
_cobrandingData != null ? _cobrandingData.brand_id : undefined
const getBrandVariationId = () =>
_cobrandingData != null ? _cobrandingData.id : undefined
return {
isProjectCobranded,
getLogoImgUrl,
getSubmitBtnHtml,
getBrandVariationName,
getBrandVariationHomeUrl,
getPublishGuideHtml,
getPartner,
hasBrandedMenu,
getBrandId,
getBrandVariationId,
}
})

View file

@ -1,630 +0,0 @@
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import SocketIoShim from './SocketIoShim'
import getMeta from '../../utils/meta'
import { debugConsole, debugging } from '@/utils/debugging'
let ConnectionManager
const ONEHOUR = 1000 * 60 * 60
export default ConnectionManager = (function () {
ConnectionManager = class ConnectionManager {
static initClass() {
this.prototype.disconnectAfterMs = ONEHOUR * 24
this.prototype.lastUserAction = new Date()
this.prototype.MIN_RETRY_INTERVAL = 1000 // ms, rate limit on reconnects for user clicking "try now"
this.prototype.BACKGROUND_RETRY_INTERVAL = 5 * 1000
this.prototype.RECONNECT_GRACEFULLY_RETRY_INTERVAL = 5000 // ms
this.prototype.MAX_RECONNECT_GRACEFULLY_INTERVAL = 45 * 1000
}
constructor(ide, $scope) {
this.ide = ide
this.$scope = $scope
if (typeof window.io !== 'object') {
this.switchToWsFallbackIfPossible()
debugConsole.error(
'Socket.io javascript not loaded. Please check that the real-time service is running and accessible.'
)
this.ide.socket = SocketIoShim.stub()
this.$scope.$apply(() => {
return (this.$scope.state.error =
'Could not connect to websocket server :(')
})
return
}
setInterval(() => {
return this.disconnectIfInactive()
}, ONEHOUR)
// trigger a reconnect immediately if network comes back online
window.addEventListener('online', () => {
debugConsole.log('[online] browser notified online')
if (!this.connected) {
return this.tryReconnectWithRateLimit({ force: true })
}
})
this.userIsLeavingPage = false
window.addEventListener('beforeunload', () => {
this.userIsLeavingPage = true
}) // Don't return true or it will show a pop up
this.connected = false
this.userIsInactive = false
this.gracefullyReconnecting = false
this.shuttingDown = false
this.$scope.connection = {
debug: debugging,
reconnecting: false,
stillReconnecting: false,
// If we need to force everyone to reload the editor
forced_disconnect: false,
inactive_disconnect: false,
jobId: 0,
}
this.$scope.tryReconnectNow = () => {
// user manually requested reconnection via "Try now" button
return this.tryReconnectWithRateLimit({ force: true })
}
this.$scope.$on('cursor:editor:update', () => {
this.lastUserAction = new Date() // time of last edit
if (!this.connected) {
// user is editing, try to reconnect
return this.tryReconnectWithRateLimit()
}
})
document.querySelector('body').addEventListener('click', e => {
if (
!this.shuttingDown &&
!this.connected &&
e.target.id !== 'try-reconnect-now-button'
) {
// user is editing, try to reconnect
return this.tryReconnectWithRateLimit()
}
})
// initial connection attempt
this.updateConnectionManagerState('connecting')
const parsedURL = new URL(
getMeta('ol-wsUrl') || '/socket.io',
window.origin
)
const query = new URLSearchParams({
projectId: getMeta('ol-project_id'),
}).toString()
this.ide.socket = SocketIoShim.connect(parsedURL.origin, {
resource: parsedURL.pathname.slice(1),
reconnect: false,
'connect timeout': 30 * 1000,
'force new connection': true,
query,
})
// handle network-level websocket errors (e.g. failed dns lookups)
let connectionAttempt = 1
const connectionErrorHandler = err => {
if (
window.wsRetryHandshake &&
connectionAttempt++ < window.wsRetryHandshake
) {
return setTimeout(
() => this.ide.socket.socket.connect(),
// add jitter to spread reconnects
connectionAttempt * (1 + Math.random()) * 1000
)
}
this.updateConnectionManagerState('error')
debugConsole.log('socket.io error', err)
if (!this.switchToWsFallbackIfPossible()) {
this.connected = false
return this.$scope.$apply(() => {
return (this.$scope.state.error =
"Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue.")
})
}
}
this.ide.socket.on('error', connectionErrorHandler)
// The "connect" event is the first event we get back. It only
// indicates that the websocket is connected, we still need to
// pass authentication to join a project.
this.ide.socket.on('connect', () => {
// state should be 'connecting'...
// remove connection error handler when connected, avoid unwanted fallbacks
this.ide.socket.removeListener('error', connectionErrorHandler)
debugConsole.log('[socket.io connect] Connected')
this.updateConnectionManagerState('authenticating')
})
// The next event we should get is an authentication response
// from the server, either "joinProjectResponse" or "connectionRejected".
this.ide.socket.on(
'joinProjectResponse',
({ publicId, project, permissionsLevel, protocolVersion }) => {
this.ide.socket.publicId = publicId
debugConsole.log('[socket.io bootstrap] ready for joinDoc')
this.connected = true
this.gracefullyReconnecting = false
this.ide.pushEvent('connected')
this.ide.pushEvent('joinProjectResponse')
this.updateConnectionManagerState('joining')
this.$scope.$apply(() => {
if (this.$scope.state.loading) {
this.$scope.state.load_progress = 70
}
})
this.handleJoinProjectResponse({
project,
permissionsLevel,
protocolVersion,
})
}
)
this.ide.socket.on('connectionRejected', err => {
// state should be 'authenticating'...
debugConsole.log(
'[socket.io connectionRejected] session not valid or other connection error'
)
// real-time sends a 'retry' message if the process was shutting down
// real-time sends TooManyRequests if joinProject was rate-limited.
if (err?.message === 'retry' || err?.code === 'TooManyRequests') {
return this.tryReconnectWithRateLimit()
}
if (err?.code === 'ProjectNotFound') {
// A stale browser tab tried to join a deleted project.
// Reloading the page will render a 404.
this.ide
.showGenericMessageModal(
'Project has been deleted',
'This project has been deleted by the owner.'
)
.result.then(() => location.reload(true))
return
}
// we have failed authentication, usually due to an invalid session cookie
return this.reportConnectionError(err)
})
// Alternatively the attempt to connect can fail completely, so
// we never get into the "connect" state.
this.ide.socket.on('connect_failed', () => {
this.updateConnectionManagerState('error')
this.connected = false
return this.$scope.$apply(() => {
return (this.$scope.state.error =
"Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue.")
})
})
// We can get a "disconnect" event at any point after the
// "connect" event.
this.ide.socket.on('disconnect', () => {
debugConsole.log('[socket.io disconnect] Disconnected')
this.connected = false
this.ide.pushEvent('disconnected')
if (!this.$scope.connection.state.match(/^waiting/)) {
if (
!this.$scope.connection.forced_disconnect &&
!this.userIsInactive &&
!this.shuttingDown
) {
this.startAutoReconnectCountdown()
} else {
this.updateConnectionManagerState('inactive')
}
}
})
// Site administrators can send the forceDisconnect event to all users
this.ide.socket.on('forceDisconnect', (message, delay = 10) => {
this.updateConnectionManagerState('inactive')
this.shuttingDown = true // prevent reconnection attempts
this.$scope.$apply(() => {
this.$scope.permissions = { ...this.$scope.permissions, write: false }
return (this.$scope.connection.forced_disconnect = true)
})
// flush changes before disconnecting
this.ide.$scope.$broadcast('flush-changes')
window.setTimeout(() => this.ide.socket.disconnect(), 1000 * delay)
this.ide.showLockEditorMessageModal(
'Please wait',
`\
We're performing maintenance on Overleaf and you need to wait a moment.
Sorry for any inconvenience.
The editor will refresh automatically in ${delay} seconds.\
`
)
return setTimeout(() => location.reload(), delay * 1000)
})
this.ide.socket.on('reconnectGracefully', () => {
debugConsole.log('Reconnect gracefully')
this.reconnectGracefully()
})
}
switchToWsFallbackIfPossible() {
const search = new URLSearchParams(window.location.search)
if (getMeta('ol-wsUrl') && search.get('ws') !== 'fallback') {
// if we tried to boot from a custom real-time backend and failed,
// try reloading and falling back to the siteUrl
search.set('ws', 'fallback')
window.location.search = search.toString()
return true
}
return false
}
updateConnectionManagerState(state) {
this.$scope.$apply(() => {
this.$scope.connection.jobId += 1
const jobId = this.$scope.connection.jobId
debugConsole.log(
`[updateConnectionManagerState ${jobId}] from ${this.$scope.connection.state} to ${state}`
)
this.$scope.connection.state = state
this.$scope.connection.reconnecting = false
this.$scope.connection.stillReconnecting = false
this.$scope.connection.inactive_disconnect = false
this.$scope.connection.joining = false
this.$scope.connection.reconnection_countdown = null
if (state === 'connecting') {
// initial connection
} else if (state === 'reconnecting') {
// reconnection after a connection has failed
this.stopReconnectCountdownTimer()
this.$scope.connection.reconnecting = true
// if reconnecting takes more than 1s (it doesn't, usually) show the
// 'reconnecting...' warning
setTimeout(() => {
if (
this.$scope.connection.reconnecting &&
this.$scope.connection.jobId === jobId
) {
this.$scope.connection.stillReconnecting = true
}
}, 1000)
} else if (state === 'reconnectFailed') {
// reconnect attempt failed
} else if (state === 'authenticating') {
// socket connection has been established, trying to authenticate
} else if (state === 'joining') {
// authenticated, joining project
this.$scope.connection.joining = true
} else if (state === 'ready') {
// project has been joined
} else if (state === 'waitingCountdown') {
// disconnected and waiting to reconnect via the countdown timer
this.stopReconnectCountdownTimer()
} else if (state === 'waitingGracefully') {
// disconnected and waiting to reconnect gracefully
this.stopReconnectCountdownTimer()
} else if (state === 'inactive') {
// disconnected and not trying to reconnect (inactive)
} else if (state === 'error') {
// something is wrong
} else {
debugConsole.log(
`[WARN] [updateConnectionManagerState ${jobId}] got unrecognised state ${state}`
)
}
})
}
expectConnectionManagerState(state, jobId) {
if (
this.$scope.connection.state === state &&
(!jobId || jobId === this.$scope.connection.jobId)
) {
return true
}
debugConsole.log(
`[WARN] [state mismatch] expected state ${state}${
jobId ? '/' + jobId : ''
} when in ${this.$scope.connection.state}/${
this.$scope.connection.jobId
}`
)
return false
}
// Error reporting, which can reload the page if appropriate
reportConnectionError(err) {
debugConsole.log('[socket.io] reporting connection error')
this.updateConnectionManagerState('error')
if (
(err != null ? err.message : undefined) === 'not authorized' ||
(err != null ? err.message : undefined) === 'invalid session'
) {
return (window.location = `/login?redir=${encodeURI(
window.location.pathname
)}`)
} else {
this.ide.socket.disconnect()
return this.ide.showGenericMessageModal(
'Something went wrong connecting',
`\
Something went wrong connecting to your project. Please refresh if this continues to happen.\
`
)
}
}
handleJoinProjectResponse({ project, permissionsLevel, protocolVersion }) {
if (
this.$scope.protocolVersion != null &&
this.$scope.protocolVersion !== protocolVersion
) {
location.reload(true)
}
this.$scope.$apply(() => {
this.updateConnectionManagerState('ready')
this.$scope.protocolVersion = protocolVersion
const defaultProjectAttributes = { rootDoc_id: null }
this.$scope.project = { ...defaultProjectAttributes, ...project }
this.$scope.permissionsLevel = permissionsLevel
this.ide.loadingManager.socketLoaded()
window.dispatchEvent(
new CustomEvent('project:joined', { detail: this.$scope.project })
)
this.$scope.$broadcast('project:joined')
})
}
reconnectImmediately() {
this.disconnect()
return this.tryReconnect()
}
disconnect(options) {
if (options && options.permanent) {
debugConsole.log('[disconnect] shutting down ConnectionManager')
this.updateConnectionManagerState('inactive')
this.shuttingDown = true // prevent reconnection attempts
} else if (this.ide.socket.socket && !this.ide.socket.socket.connected) {
debugConsole.log(
'[socket.io] skipping disconnect because socket.io has not connected'
)
return
}
debugConsole.log('[socket.io] disconnecting client')
return this.ide.socket.disconnect()
}
startAutoReconnectCountdown() {
this.updateConnectionManagerState('waitingCountdown')
const connectionId = this.$scope.connection.jobId
let countdown
debugConsole.log('[ConnectionManager] starting autoreconnect countdown')
const twoMinutes = 2 * 60 * 1000
if (
this.lastUserAction != null &&
new Date() - this.lastUserAction > twoMinutes
) {
// between 1 minute and 3 minutes
countdown = 60 + Math.floor(Math.random() * 120)
} else {
countdown = 3 + Math.floor(Math.random() * 7)
}
if (this.userIsLeavingPage) {
// user will have pressed refresh or back etc
return
}
this.$scope.$apply(() => {
this.$scope.connection.reconnecting = false
this.$scope.connection.stillReconnecting = false
this.$scope.connection.joining = false
this.$scope.connection.reconnection_countdown = countdown
})
setTimeout(() => {
if (!this.connected && !this.countdownTimeoutId) {
this.countdownTimeoutId = setTimeout(
() => this.decreaseCountdown(connectionId),
1000
)
}
}, 200)
}
stopReconnectCountdownTimer() {
// clear timeout and set to null so we know there is no countdown running
if (this.countdownTimeoutId != null) {
debugConsole.log(
'[ConnectionManager] cancelling existing reconnect timer'
)
clearTimeout(this.countdownTimeoutId)
this.countdownTimeoutId = null
}
}
decreaseCountdown(connectionId) {
this.countdownTimeoutId = null
if (this.$scope.connection.reconnection_countdown == null) {
return
}
if (
!this.expectConnectionManagerState('waitingCountdown', connectionId)
) {
debugConsole.log(
`[ConnectionManager] Aborting stale countdown ${connectionId}`
)
return
}
debugConsole.log(
'[ConnectionManager] decreasing countdown',
this.$scope.connection.reconnection_countdown
)
this.$scope.$apply(() => {
this.$scope.connection.reconnection_countdown--
})
if (this.$scope.connection.reconnection_countdown <= 0) {
this.$scope.connection.reconnecting = false
this.$scope.$apply(() => {
this.tryReconnect()
})
} else {
this.countdownTimeoutId = setTimeout(
() => this.decreaseCountdown(connectionId),
1000
)
}
}
tryReconnect() {
debugConsole.log('[ConnectionManager] tryReconnect')
if (
this.connected ||
this.shuttingDown ||
this.$scope.connection.reconnecting
) {
return
}
this.updateConnectionManagerState('reconnecting')
debugConsole.log('[ConnectionManager] Starting new connection')
const removeHandler = () => {
this.ide.socket.removeListener('error', handleFailure)
this.ide.socket.removeListener('connect', handleSuccess)
}
const handleFailure = () => {
debugConsole.log('[ConnectionManager] tryReconnect: failed')
removeHandler()
this.updateConnectionManagerState('reconnectFailed')
this.tryReconnectWithRateLimit({ force: true })
}
const handleSuccess = () => {
debugConsole.log('[ConnectionManager] tryReconnect: success')
removeHandler()
}
this.ide.socket.on('error', handleFailure)
this.ide.socket.on('connect', handleSuccess)
// use socket.io connect() here to make a single attempt, the
// reconnect() method makes multiple attempts
this.ide.socket.socket.connect()
// record the time of the last attempt to connect
this.lastConnectionAttempt = new Date()
}
tryReconnectWithRateLimit(options) {
// bail out if the reconnect is already in progress
if (this.$scope.connection.reconnecting || this.connected) {
return
}
// bail out if we are going to reconnect soon anyway
const reconnectingSoon =
this.$scope.connection.reconnection_countdown != null &&
this.$scope.connection.reconnection_countdown <= 5
const clickedTryNow = options != null ? options.force : undefined // user requested reconnection
if (reconnectingSoon && !clickedTryNow) {
return
}
// bail out if we tried reconnecting recently
const allowedInterval = clickedTryNow
? this.MIN_RETRY_INTERVAL
: this.BACKGROUND_RETRY_INTERVAL
if (
this.lastConnectionAttempt != null &&
new Date() - this.lastConnectionAttempt < allowedInterval
) {
if (this.$scope.connection.state !== 'waitingCountdown') {
this.startAutoReconnectCountdown()
}
return
}
this.tryReconnect()
}
disconnectIfInactive() {
this.userIsInactive =
new Date() - this.lastUserAction > this.disconnectAfterMs
if (this.userIsInactive && this.connected) {
this.disconnect()
return this.$scope.$apply(() => {
return (this.$scope.connection.inactive_disconnect = true)
}) // 5 minutes
}
}
reconnectGracefully(force) {
if (this.reconnectGracefullyStarted == null) {
this.reconnectGracefullyStarted = new Date()
} else {
if (!force) {
debugConsole.log(
'[reconnectGracefully] reconnection is already in process, so skipping'
)
return
}
}
const userIsInactive =
new Date() - this.lastUserAction >
this.RECONNECT_GRACEFULLY_RETRY_INTERVAL
const maxIntervalReached =
new Date() - this.reconnectGracefullyStarted >
this.MAX_RECONNECT_GRACEFULLY_INTERVAL
if (userIsInactive || maxIntervalReached) {
debugConsole.log(
"[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
)
this._reconnectGracefullyNow()
} else {
debugConsole.log(
'[reconnectGracefully] User is working, will try again in 5 seconds'
)
this.updateConnectionManagerState('waitingGracefully')
setTimeout(() => {
this.reconnectGracefully(true)
}, this.RECONNECT_GRACEFULLY_RETRY_INTERVAL)
}
}
_reconnectGracefullyNow() {
this.gracefullyReconnecting = true
this.reconnectGracefullyStarted = null
// Clear cookie so we don't go to the same backend server
$.cookie('SERVERID', '', { expires: -1, path: '/' })
return this.reconnectImmediately()
}
}
ConnectionManager.initClass()
return ConnectionManager
})()

View file

@ -1,236 +0,0 @@
/*
EditorWatchdogManager is used for end-to-end checks of edits.
The editor UI is backed by Ace and CodeMirrors, which in turn are connected
to ShareJs documents in the frontend.
Edits propagate from the editor to ShareJs and are send through socket.io
and real-time to document-updater.
In document-updater edits are integrated into the document history and
a confirmation/rejection is sent back to the frontend.
Along the way things can get lost.
We have certain safe-guards in place, but are still getting occasional
reports of lost edits.
EditorWatchdogManager is implementing the basis for end-to-end checks on
two levels:
- local/ShareJsDoc: edits that pass-by a ShareJs document shall get
acknowledged eventually.
- global: any edits made in the editor shall get acknowledged eventually,
independent for which ShareJs document (potentially none) sees it.
How does this work?
===================
The global check is using a global EditorWatchdogManager that is available
via the angular factory 'ide'.
Local/ShareJsDoc level checks will connect to the global instance.
Each EditorWatchdogManager keeps track of the oldest un-acknowledged edit.
When ever a ShareJs document receives an acknowledgement event, a local
EditorWatchdogManager will see it and also notify the global instance about
it.
The next edit cycle will clear the oldest un-acknowledged timestamp in case
a new ack has arrived, otherwise it will bark loud! via the timeout handler.
Scenarios
=========
- User opens the CodeMirror editor
- attach global check to new CM instance
- detach Ace from the local EditorWatchdogManager
- when the frontend attaches the CM instance to ShareJs, we also
attach it to the local EditorWatchdogManager
- the internal attach process writes the document content to the editor,
which in turn emits 'change' events. These event need to be excluded
from the watchdog. EditorWatchdogManager.ignoreEditsFor takes care
of that.
- User opens the Ace editor (again)
- (attach global check to the Ace editor, only one copy of Ace is around)
- detach local EditorWatchdogManager from CM
- likewise with CM, attach Ace to the local EditorWatchdogManager
- User makes an edit
- the editor will emit a 'change' event
- the global EditorWatchdogManager will process it first
- the local EditorWatchdogManager will process it next
- Document-updater confirms an edit
- the local EditorWatchdogManager will process it first, it passes it on to
- the global EditorWatchdogManager will process it next
Time
====
The delay between edits and acks is measured using a monotonic clock:
`performance.now()`.
It is agnostic to system clock changes in either direction and timezone
changes do not affect it as well.
Roughly speaking, it is initialized with `0` when the `window` context is
created, before our JS app boots.
As per canIUse.com and MDN `performance.now()` is available to all supported
Browsers, including IE11.
See also: https://caniuse.com/?search=performance.now
See also: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
*/
import { debugConsole } from '@/utils/debugging'
// TIMEOUT specifies the timeout for edits into a single ShareJsDoc.
const TIMEOUT = 60 * 1000
// GLOBAL_TIMEOUT specifies the timeout for edits into any ShareJSDoc.
const GLOBAL_TIMEOUT = TIMEOUT
// REPORT_EVERY specifies how often we send events/report errors.
const REPORT_EVERY = 60 * 1000
const SCOPE_LOCAL = 'ShareJsDoc'
const SCOPE_GLOBAL = 'global'
class Reporter {
constructor(onTimeoutHandler) {
this._onTimeoutHandler = onTimeoutHandler
this._lastReport = undefined
this._queue = []
}
_getMetaPreferLocal() {
for (const meta of this._queue) {
if (meta.scope === SCOPE_LOCAL) {
return meta
}
}
return this._queue.pop()
}
onTimeout(meta) {
// Collect all 'meta's for this update.
// global arrive before local ones, but we are eager to report local ones.
this._queue.push(meta)
setTimeout(() => {
// Another handler processed the 'meta' entry already
if (!this._queue.length) return
const maybeLocalMeta = this._getMetaPreferLocal()
// Discard other, newly arrived 'meta's
this._queue.length = 0
const now = Date.now()
// Do not flood the server with losing-edits events
const reportedRecently = now - this._lastReport < REPORT_EVERY
if (!reportedRecently) {
this._lastReport = now
this._onTimeoutHandler(maybeLocalMeta)
}
})
}
}
export default class EditorWatchdogManager {
constructor({ parent, onTimeoutHandler }) {
this.scope = parent ? SCOPE_LOCAL : SCOPE_GLOBAL
this.timeout = parent ? TIMEOUT : GLOBAL_TIMEOUT
this.parent = parent
if (parent) {
this.reporter = parent.reporter
} else {
this.reporter = new Reporter(onTimeoutHandler)
}
this.lastAck = null
this.lastUnackedEdit = null
}
onAck() {
this.lastAck = performance.now()
// bubble up to globalEditorWatchdogManager
if (this.parent) this.parent.onAck()
}
onEdit() {
// Use timestamps to track the high-water mark of unacked edits
const now = performance.now()
// Discard the last unacked edit if there are now newer acks
if (this.lastAck > this.lastUnackedEdit) {
this.lastUnackedEdit = null
}
// Start tracking for this keypress if we aren't already tracking an
// unacked edit
if (!this.lastUnackedEdit) {
this.lastUnackedEdit = now
}
// Report an error if the last tracked edit hasn't been cleared by an
// ack from the server after a long time
const delay = now - this.lastUnackedEdit
if (delay > this.timeout) {
const timeOrigin = Date.now() - now
const scope = this.scope
const lastAck = new Date(this.lastAck ? timeOrigin + this.lastAck : 0)
const lastUnackedEdit = new Date(timeOrigin + this.lastUnackedEdit)
const meta = { scope, delay, lastAck, lastUnackedEdit }
this._log('timedOut', meta)
this.reporter.onTimeout(meta)
}
}
attachToEditor(editorName, editor) {
let onChange
if (editorName === 'CM6') {
// Code Mirror 6
this._log('attach to editor', editorName)
onChange = (_editor, changeDescription) => {
if (changeDescription.origin === 'remote') return
if (!(changeDescription.removed || changeDescription.inserted)) return
this.onEdit()
}
editor.on('change', onChange)
const detachFromEditor = () => {
this._log('detach from editor', editorName)
editor.off('change', onChange)
}
return detachFromEditor
}
if (editorName === 'CM') {
// CM is passing the CM instance as first parameter, then the change.
onChange = (editor, change) => {
// Ignore remote changes.
if (change.origin === 'remote') return
// sharejs only looks at DEL or INSERT change events.
// NOTE: Keep in sync with sharejs.
if (!(change.removed || change.text)) return
this.onEdit()
}
} else {
// ACE is passing the change object as first parameter.
onChange = change => {
// Ignore remote changes.
if (change.origin === 'remote') return
// sharejs only looks at DEL or INSERT change events.
// NOTE: Keep in sync with sharejs.
if (!(change.action === 'remove' || change.action === 'insert')) return
this.onEdit()
}
}
this._log('attach to editor', editorName)
editor.on('change', onChange)
const detachFromEditor = () => {
this._log('detach from editor', editorName)
editor.off('change', onChange)
}
return detachFromEditor
}
_log() {
debugConsole.log(`[EditorWatchdogManager] ${this.scope}:`, ...arguments)
}
}

View file

@ -1,99 +0,0 @@
/* eslint-disable
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
// This file is shared between the frontend and server code of web, so that
// filename validation is the same in both implementations.
// The logic in all copies must be kept in sync:
// app/src/Features/Project/SafePath.js
// frontend/js/ide/directives/SafePath.js
// frontend/js/features/file-tree/util/safe-path.js
let SafePath
// eslint-disable-next-line prefer-regex-literals
const BADCHAR_RX = new RegExp(
`\
[\
\\/\
\\\\\
\\*\
\\u0000-\\u001F\
\\u007F\
\\u0080-\\u009F\
\\uD800-\\uDFFF\
]\
`,
'g'
)
// eslint-disable-next-line prefer-regex-literals
const BADFILE_RX = new RegExp(
`\
(^\\.$)\
|(^\\.\\.$)\
|(^\\s+)\
|(\\s+$)\
`,
'g'
)
// Put a block on filenames which match javascript property names, as they
// can cause exceptions where the code puts filenames into a hash. This is a
// temporary workaround until the code in other places is made safe against
// property names.
//
// The list of property names is taken from
// ['prototype'].concat(Object.getOwnPropertyNames(Object.prototype))
// eslint-disable-next-line prefer-regex-literals
const BLOCKEDFILE_RX = new RegExp(`\
^(\
prototype\
|constructor\
|toString\
|toLocaleString\
|valueOf\
|hasOwnProperty\
|isPrototypeOf\
|propertyIsEnumerable\
|__defineGetter__\
|__lookupGetter__\
|__defineSetter__\
|__lookupSetter__\
|__proto__\
)$\
`)
const MAX_PATH = 1024 // Maximum path length, in characters. This is fairly arbitrary.
export default SafePath = {
clean(filename) {
filename = filename.replace(BADCHAR_RX, '_')
// for BADFILE_RX replace any matches with an equal number of underscores
filename = filename.replace(BADFILE_RX, match =>
new Array(match.length + 1).join('_')
)
// replace blocked filenames 'prototype' with '@prototype'
filename = filename.replace(BLOCKEDFILE_RX, '@$1')
return filename
},
isCleanFilename(filename) {
return (
SafePath.isAllowedLength(filename) &&
!filename.match(BADCHAR_RX) &&
!filename.match(BADFILE_RX)
)
},
isAllowedLength(pathname) {
return pathname.length > 0 && pathname.length <= MAX_PATH
},
}

View file

@ -1,323 +0,0 @@
/* eslint-disable
max-len,
no-return-assign,
no-useless-escape,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from '../../base'
import _ from 'lodash'
import '../../vendor/libs/jquery-layout'
import '../../vendor/libs/jquery.ui.touch-punch'
export default App.directive('layout', [
'$parse',
'$compile',
'ide',
function ($parse, $compile, ide) {
return {
compile() {
return {
pre(scope, element, attrs) {
let customTogglerEl, spacingClosed, spacingOpen, state
const name = attrs.layout
const { customTogglerPane } = attrs
const { customTogglerMsgWhenOpen } = attrs
const { customTogglerMsgWhenClosed } = attrs
const hasCustomToggler =
customTogglerPane != null &&
customTogglerMsgWhenOpen != null &&
customTogglerMsgWhenClosed != null
if (attrs.spacingOpen != null) {
spacingOpen = parseInt(attrs.spacingOpen, 10)
} else {
spacingOpen = 7
}
if (attrs.spacingClosed != null) {
spacingClosed = parseInt(attrs.spacingClosed, 10)
} else {
spacingClosed = 7
}
const options = {
spacing_open: spacingOpen,
spacing_closed: spacingClosed,
slidable: false,
enableCursorHotkey: false,
onopen: pane => {
return onPaneOpen(pane)
},
onclose: pane => {
return onPaneClose(pane)
},
onresize: () => {
return onInternalResize()
},
maskIframesOnResize: scope.$eval(
attrs.maskIframesOnResize || 'false'
),
east: {
size: scope.$eval(attrs.initialSizeEast),
initClosed: scope.$eval(attrs.initClosedEast),
},
west: {
size: scope.$eval(attrs.initialSizeWest),
initClosed: scope.$eval(attrs.initClosedWest),
},
}
// Restore previously recorded state
if ((state = ide.localStorage(`layout.${name}`)) != null) {
if (state.east != null) {
if (
attrs.minimumRestoreSizeEast == null ||
(state.east.size >= attrs.minimumRestoreSizeEast &&
!state.east.initClosed)
) {
options.east = state.east
}
options.east.initClosed = state.east.initClosed
}
if (state.west != null) {
if (
attrs.minimumRestoreSizeWest == null ||
(state.west.size >= attrs.minimumRestoreSizeWest &&
!state.west.initClosed)
) {
options.west = state.west
}
// NOTE: disabled so that the file tree re-opens on page load
// options.west.initClosed = state.west.initClosed
}
}
options.east.resizerCursor = 'ew-resize'
options.west.resizerCursor = 'ew-resize'
function repositionControls() {
state = layout.readState()
if (state.east != null) {
const controls = element.find('> .ui-layout-resizer-controls')
if (state.east.initClosed) {
return controls.hide()
} else {
controls.show()
return controls.css({
right: state.east.size,
})
}
}
}
function repositionCustomToggler() {
if (customTogglerEl == null) {
return
}
state = layout.readState()
const positionAnchor =
customTogglerPane === 'east' ? 'right' : 'left'
const paneState = state[customTogglerPane]
if (paneState != null) {
return customTogglerEl.css(
positionAnchor,
paneState.initClosed ? 0 : paneState.size
)
}
}
function resetOpenStates() {
state = layout.readState()
if (attrs.openEast != null && state.east != null) {
const openEast = $parse(attrs.openEast)
return openEast.assign(scope, !state.east.initClosed)
}
}
// Someone moved the resizer
function onInternalResize() {
state = layout.readState()
scope.$broadcast(`layout:${name}:resize`, state)
repositionControls()
if (hasCustomToggler) {
repositionCustomToggler()
}
return resetOpenStates()
}
let oldWidth = element.width()
// Something resized our parent element
const onExternalResize = function () {
if (
attrs.resizeProportionally != null &&
scope.$eval(attrs.resizeProportionally)
) {
const eastState = layout.readState().east
if (eastState != null) {
const currentWidth = element.width()
if (currentWidth > 0) {
const newInternalWidth =
(eastState.size / oldWidth) * currentWidth
oldWidth = currentWidth
layout.sizePane('east', newInternalWidth)
}
return
}
}
ide.$timeout(() => {
layout.resizeAll()
})
}
const layout = element.layout(options)
layout.resizeAll()
if (attrs.resizeOn != null) {
for (const event of Array.from(attrs.resizeOn.split(','))) {
scope.$on(event, () => onExternalResize())
}
}
if (hasCustomToggler) {
state = layout.readState()
const customTogglerScope = scope.$new()
customTogglerScope.isOpen = true
customTogglerScope.isVisible = true
if (
(state[customTogglerPane] != null
? state[customTogglerPane].initClosed
: undefined) === true
) {
customTogglerScope.isOpen = false
}
customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
customTogglerScope.tooltipMsgWhenClosed =
customTogglerMsgWhenClosed
customTogglerScope.tooltipPlacement =
customTogglerPane === 'east' ? 'left' : 'right'
customTogglerScope.handleClick = function () {
layout.toggle(customTogglerPane)
return repositionCustomToggler()
}
customTogglerEl = $compile(`\
<a href \
ng-show=\"isVisible\" \
class=\"custom-toggler ${`custom-toggler-${customTogglerPane}`}\" \
ng-class=\"isOpen ? 'custom-toggler-open' : 'custom-toggler-closed'\" \
tooltip=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\" \
tooltip-placement=\"{{ tooltipPlacement }}\" \
ng-click=\"handleClick()\" \
aria-label=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\">\
`)(customTogglerScope)
element.append(customTogglerEl)
}
function onPaneOpen(pane) {
if (!hasCustomToggler || pane !== customTogglerPane) {
return
}
return customTogglerEl
.scope()
.$applyAsync(() => (customTogglerEl.scope().isOpen = true))
}
function onPaneClose(pane) {
if (!hasCustomToggler || pane !== customTogglerPane) {
return
}
return customTogglerEl
.scope()
.$applyAsync(() => (customTogglerEl.scope().isOpen = false))
}
// Ensure editor resizes after loading. This is to handle the case where
// the window has been resized while the editor is loading
scope.$on('editor:loaded', () => {
ide.$timeout(() => layout.resizeAll())
})
// Save state when exiting
$(window).unload(() => {
// Save only the state properties for the current layout, ignoring sublayouts inside it.
// If we save sublayouts state (`children`), the layout library will use it when
// initializing. This raises errors when the sublayout elements aren't available (due to
// being loaded at init or just not existing for the current project/user).
const stateToSave = _.mapValues(layout.readState(), pane =>
_.omit(pane, 'children')
)
ide.localStorage(`layout.${name}`, stateToSave)
})
if (attrs.openEast != null) {
scope.$watch(attrs.openEast, function (value, oldValue) {
if (value != null && value !== oldValue) {
if (value) {
layout.open('east')
} else {
layout.close('east')
}
if (hasCustomToggler && customTogglerPane === 'east') {
repositionCustomToggler()
customTogglerEl.scope().$applyAsync(function () {
customTogglerEl.scope().isOpen = value
})
}
}
return setTimeout(() => scope.$digest(), 0)
})
}
if (attrs.allowOverflowOn != null) {
const overflowPane = scope.$eval(attrs.allowOverflowOn)
const overflowPaneEl = layout.panes[overflowPane]
// Set the panel as overflowing (gives it higher z-index and sets overflow rules)
layout.allowOverflow(overflowPane)
// Read the given z-index value and increment it, so that it's higher than synctex controls.
const overflowPaneZVal = overflowPaneEl.zIndex()
overflowPaneEl.css('z-index', overflowPaneZVal + 1)
}
resetOpenStates()
onInternalResize()
if (attrs.layoutDisabled != null) {
return scope.$watch(attrs.layoutDisabled, function (value) {
if (value) {
layout.hide('east')
} else {
layout.show('east')
}
if (hasCustomToggler) {
return customTogglerEl.scope().$applyAsync(function () {
customTogglerEl.scope().isOpen = !value
return (customTogglerEl.scope().isVisible = !value)
})
}
})
}
},
post(scope, element, attrs) {
const name = attrs.layout
const state = element.layout().readState()
return scope.$broadcast(`layout:${name}:linked`, state)
},
}
},
}
},
])

View file

@ -1,139 +0,0 @@
import App from '../../base'
export default App.directive('verticalResizablePanes', [
'localStorage',
'ide',
function (localStorage, ide) {
return {
restrict: 'A',
link(scope, element, attrs) {
const name = attrs.verticalResizablePanes
const minSize = scope.$eval(attrs.verticalResizablePanesMinSize)
const maxSize = scope.$eval(attrs.verticalResizablePanesMaxSize)
const defaultSize = scope.$eval(attrs.verticalResizablePanesDefaultSize)
let storedSize = null
let manualResizeIncoming = false
if (name) {
const storageKey = `vertical-resizable:${name}:south-size`
storedSize = localStorage(storageKey)
$(window).unload(() => {
if (storedSize) {
localStorage(storageKey, storedSize)
}
})
}
const layoutOptions = {
center: {
paneSelector: '[vertical-resizable-top]',
paneClass: 'vertical-resizable-top',
size: 'auto',
},
south: {
paneSelector: '[vertical-resizable-bottom]',
paneClass: 'vertical-resizable-bottom',
resizerClass: 'vertical-resizable-resizer',
resizerCursor: 'ns-resize',
size: 'auto',
resizable: true,
closable: false,
slidable: false,
spacing_open: 6,
spacing_closed: 6,
maxSize: '75%',
},
}
const toggledExternally =
attrs.verticalResizablePanesToggledExternallyOn
const hiddenExternally = attrs.verticalResizablePanesHiddenExternallyOn
const hiddenInitially = attrs.verticalResizablePanesHiddenInitially
const resizeOn = attrs.verticalResizablePanesResizeOn
const resizerDisabledClass = `${layoutOptions.south.resizerClass}-disabled`
function enableResizer() {
if (layoutHandle.resizers && layoutHandle.resizers.south) {
layoutHandle.resizers.south.removeClass(resizerDisabledClass)
}
}
function disableResizer() {
if (layoutHandle.resizers && layoutHandle.resizers.south) {
layoutHandle.resizers.south.addClass(resizerDisabledClass)
}
}
function handleDragEnd() {
manualResizeIncoming = true
}
function handleResize(paneName, paneEl, paneState) {
if (manualResizeIncoming) {
storedSize = paneState.size
}
manualResizeIncoming = false
}
if (toggledExternally) {
scope.$on(toggledExternally, (e, open) => {
if (open) {
enableResizer()
layoutHandle.sizePane(
'south',
storedSize ?? defaultSize ?? 'auto'
)
} else {
disableResizer()
layoutHandle.sizePane('south', 'auto')
}
})
}
if (hiddenExternally) {
ide.$scope.$on(hiddenExternally, (e, open) => {
if (open) {
layoutHandle.show('south')
} else {
layoutHandle.hide('south')
}
})
}
if (resizeOn) {
ide.$scope.$on(resizeOn, () => {
ide.$timeout(() => {
layoutHandle.resizeAll()
})
})
}
if (maxSize) {
layoutOptions.south.maxSize = maxSize
}
if (minSize) {
layoutOptions.south.minSize = minSize
}
if (defaultSize) {
layoutOptions.south.size = defaultSize
}
// The `drag` event fires only when the user manually resizes the panes; the `resize` event fires even when
// the layout library internally resizes itself. In order to get explicit user-initiated resizes, we need to
// listen to `drag` events. However, when the `drag` event fires, the panes aren't yet finished sizing so we
// get the pane size *before* the resize happens. We do get the correct size in the next `resize` event.
// The solution to work around this is to set up a flag in `drag` events which tells the next `resize` event
// that it was user-initiated (therefore, storing the value).
layoutOptions.south.ondrag_end = handleDragEnd
layoutOptions.south.onresize = handleResize
const layoutHandle = element.layout(layoutOptions)
if (hiddenInitially === 'true') {
layoutHandle.hide('south')
}
},
}
},
])

View file

@ -1,781 +0,0 @@
/* eslint-disable
camelcase,
n/handle-callback-err,
max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS001: Remove Babel/TypeScript constructor workaround
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS205: Consider reworking code to avoid use of IIFEs
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import RangesTracker from '@overleaf/ranges-tracker'
import EventEmitter from '../../utils/EventEmitter'
import ShareJsDoc from './ShareJsDoc'
import { debugConsole } from '@/utils/debugging'
let Document
export default Document = (function () {
Document = class Document extends EventEmitter {
static initClass() {
this.prototype.MAX_PENDING_OP_SIZE = 64
}
static getDocument(ide, doc_id) {
if (!this.openDocs) {
this.openDocs = {}
}
// Try to clean up existing docs before reopening them. If the doc has no
// buffered ops then it will be deleted by _cleanup() and a new instance
// of the document created below. This prevents us trying to follow the
// joinDoc:existing code path on an existing doc that doesn't have any
// local changes and getting an error if its version is too old.
if (this.openDocs[doc_id]) {
debugConsole.log(
`[getDocument] Cleaning up existing document instance for ${doc_id}`
)
this.openDocs[doc_id]._cleanUp()
}
if (this.openDocs[doc_id] == null) {
debugConsole.log(
`[getDocument] Creating new document instance for ${doc_id}`
)
this.openDocs[doc_id] = new Document(ide, doc_id)
} else {
debugConsole.log(
`[getDocument] Returning existing document instance for ${doc_id}`
)
}
return this.openDocs[doc_id]
}
static hasUnsavedChanges() {
const object = this.openDocs || {}
for (const doc_id in object) {
const doc = object[doc_id]
if (doc.hasBufferedOps()) {
return true
}
}
return false
}
static flushAll() {
return (() => {
const result = []
for (const doc_id in this.openDocs) {
const doc = this.openDocs[doc_id]
result.push(doc.flush())
}
return result
})()
}
constructor(ide, doc_id) {
super()
this.ide = ide
this.doc_id = doc_id
this.connected = this.ide.socket.socket.connected
this.joined = false
this.wantToBeJoined = false
this._checkCM6Consistency = () => this._checkConsistency(this.cm6)
this._bindToEditorEvents()
this._bindToSocketEvents()
}
editorType() {
if (this.cm6) {
return 'cm6'
} else {
return null
}
}
attachToCM6(cm6) {
this.cm6 = cm6
if (this.doc != null) {
this.doc.attachToCM6(this.cm6)
}
if (this.cm6 != null) {
this.cm6.on('change', this._checkCM6Consistency)
}
return this.ide.$scope.$emit('document:opened', this.doc)
}
detachFromCM6() {
if (this.doc != null) {
this.doc.detachFromCM6()
}
if (this.cm6 != null) {
this.cm6.off('change', this._checkCM6Consistency)
}
delete this.cm6
this.clearChaosMonkey()
return this.ide.$scope.$emit('document:closed', this.doc)
}
submitOp(...args) {
return this.doc != null
? this.doc.submitOp(...Array.from(args || []))
: undefined
}
_checkConsistency(editor) {
// We've been seeing a lot of errors when I think there shouldn't be
// any, which may be related to this check happening before the change is
// applied. If we use a timeout, hopefully we can reduce this.
return setTimeout(() => {
const editorValue = editor != null ? editor.getValue() : undefined
const sharejsValue =
this.doc != null ? this.doc.getSnapshot() : undefined
if (editorValue !== sharejsValue) {
return this._onError(
new Error('Editor text does not match server text'),
{},
editorValue
)
}
}, 0)
}
getSnapshot() {
return this.doc != null ? this.doc.getSnapshot() : undefined
}
getType() {
return this.doc != null ? this.doc.getType() : undefined
}
getInflightOp() {
return this.doc != null ? this.doc.getInflightOp() : undefined
}
getPendingOp() {
return this.doc != null ? this.doc.getPendingOp() : undefined
}
getRecentAck() {
return this.doc != null ? this.doc.getRecentAck() : undefined
}
getOpSize(op) {
return this.doc != null ? this.doc.getOpSize(op) : undefined
}
hasBufferedOps() {
return this.doc != null ? this.doc.hasBufferedOps() : undefined
}
setTrackingChanges(track_changes) {
return (this.doc.track_changes = track_changes)
}
getTrackingChanges() {
return !!this.doc.track_changes
}
setTrackChangesIdSeeds(id_seeds) {
return (this.doc.track_changes_id_seeds = id_seeds)
}
_bindToSocketEvents() {
this._onUpdateAppliedHandler = update => this._onUpdateApplied(update)
this.ide.socket.on('otUpdateApplied', this._onUpdateAppliedHandler)
this._onErrorHandler = (error, message) => {
// 'otUpdateError' are emitted per doc socket.io room, hence we can be
// sure that message.doc_id exists.
if (message.doc_id !== this.doc_id) {
// This error is for another doc. Do not action it. We could open
// a modal that has the wrong context on it.
return
}
this._onError(error, message)
}
this.ide.socket.on('otUpdateError', this._onErrorHandler)
this._onDisconnectHandler = error => this._onDisconnect(error)
return this.ide.socket.on('disconnect', this._onDisconnectHandler)
}
_bindToEditorEvents() {
const onReconnectHandler = update => {
return this._onReconnect(update)
}
return (this._unsubscribeReconnectHandler = this.ide.$scope.$on(
'project:joined',
onReconnectHandler
))
}
_unBindFromEditorEvents() {
return this._unsubscribeReconnectHandler()
}
_unBindFromSocketEvents() {
this.ide.socket.removeListener(
'otUpdateApplied',
this._onUpdateAppliedHandler
)
this.ide.socket.removeListener('otUpdateError', this._onErrorHandler)
return this.ide.socket.removeListener(
'disconnect',
this._onDisconnectHandler
)
}
leaveAndCleanUp(cb) {
return this.leave(error => {
this._cleanUp()
if (cb) cb(error)
})
}
join(callback) {
if (callback == null) {
callback = function () {}
}
this.wantToBeJoined = true
this._cancelLeave()
if (this.connected) {
return this._joinDoc(callback)
} else {
if (!this._joinCallbacks) {
this._joinCallbacks = []
}
return this._joinCallbacks.push(callback)
}
}
leave(callback) {
if (callback == null) {
callback = function () {}
}
this.flush() // force an immediate flush when leaving document
this.wantToBeJoined = false
this._cancelJoin()
if (this.doc != null && this.doc.hasBufferedOps()) {
debugConsole.log(
'[leave] Doc has buffered ops, pushing callback for later'
)
if (!this._leaveCallbacks) {
this._leaveCallbacks = []
}
return this._leaveCallbacks.push(callback)
} else if (!this.connected) {
debugConsole.log('[leave] Not connected, returning now')
return callback()
} else {
debugConsole.log('[leave] Leaving now')
return this._leaveDoc(callback)
}
}
flush() {
return this.doc != null ? this.doc.flushPendingOps() : undefined
}
chaosMonkey(line, char) {
if (line == null) {
line = 0
}
if (char == null) {
char = 'a'
}
const orig = char
let copy = null
let pos = 0
const timer = () => {
if (copy == null || !copy.length) {
copy = orig.slice() + ' ' + new Date() + '\n'
line += Math.random() > 0.1 ? 1 : -2
if (line < 0) {
line = 0
}
pos = 0
}
char = copy[0]
copy = copy.slice(1)
if (this.cm6) {
this.cm6.view.dispatch({
changes: {
from: Math.min(pos, this.cm6.view.state.doc.length),
insert: char,
},
})
}
pos += 1
return (this._cm = setTimeout(
timer,
100 + (Math.random() < 0.1 ? 1000 : 0)
))
}
return (this._cm = timer())
}
clearChaosMonkey() {
const timer = this._cm
if (timer) {
delete this._cm
return clearTimeout(timer)
}
}
pollSavedStatus() {
// returns false if doc has ops waiting to be acknowledged or
// sent that haven't changed since the last time we checked.
// Otherwise returns true.
let saved
const inflightOp = this.getInflightOp()
const pendingOp = this.getPendingOp()
const recentAck = this.getRecentAck()
const pendingOpSize = pendingOp != null && this.getOpSize(pendingOp)
if (inflightOp == null && pendingOp == null) {
// there's nothing going on, this is ok.
saved = true
debugConsole.log('[pollSavedStatus] no inflight or pending ops')
} else if (inflightOp != null && inflightOp === this.oldInflightOp) {
// The same inflight op has been sitting unacked since we
// last checked, this is bad.
saved = false
debugConsole.log('[pollSavedStatus] inflight op is same as before')
} else if (
pendingOp != null &&
recentAck &&
pendingOpSize < this.MAX_PENDING_OP_SIZE
) {
// There is an op waiting to go to server but it is small and
// within the flushDelay, this is ok for now.
saved = true
debugConsole.log(
'[pollSavedStatus] pending op (small with recent ack) assume ok',
pendingOp,
pendingOpSize
)
} else {
// In any other situation, assume the document is unsaved.
saved = false
debugConsole.log(
`[pollSavedStatus] assuming not saved (inflightOp?: ${
inflightOp != null
}, pendingOp?: ${pendingOp != null})`
)
}
this.oldInflightOp = inflightOp
return saved
}
_cancelLeave() {
if (this._leaveCallbacks != null) {
return delete this._leaveCallbacks
}
}
_cancelJoin() {
if (this._joinCallbacks != null) {
return delete this._joinCallbacks
}
}
_onUpdateApplied(update) {
this.ide.pushEvent('received-update', {
doc_id: this.doc_id,
remote_doc_id: update != null ? update.doc : undefined,
wantToBeJoined: this.wantToBeJoined,
update,
hasDoc: this.doc != null,
})
if (
window.disconnectOnAck != null &&
Math.random() < window.disconnectOnAck
) {
debugConsole.log('Disconnecting on ack', update)
window._ide.socket.socket.disconnect()
// Pretend we never received the ack
return
}
if (window.dropAcks != null && Math.random() < window.dropAcks) {
if (update.op == null) {
// Only drop our own acks, not collaborator updates
debugConsole.log('Simulating a lost ack', update)
return
}
}
if (
(update != null ? update.doc : undefined) === this.doc_id &&
this.doc != null
) {
this.ide.pushEvent('received-update:processing', {
update,
})
// FIXME: change this back to processUpdateFromServer when redis fixed
this.doc.processUpdateFromServerInOrder(update)
if (!this.wantToBeJoined) {
return this.leave()
}
}
}
_onDisconnect() {
debugConsole.log('[onDisconnect] disconnecting')
this.connected = false
this.joined = false
return this.doc != null
? this.doc.updateConnectionState('disconnected')
: undefined
}
_onReconnect() {
debugConsole.log('[onReconnect] reconnected (joined project)')
this.ide.pushEvent('reconnected:afterJoinProject')
this.connected = true
if (
this.wantToBeJoined ||
(this.doc != null ? this.doc.hasBufferedOps() : undefined)
) {
debugConsole.log(
`[onReconnect] Rejoining (wantToBeJoined: ${
this.wantToBeJoined
} OR hasBufferedOps: ${
this.doc != null ? this.doc.hasBufferedOps() : undefined
})`
)
return this._joinDoc(error => {
if (error != null) {
return this._onError(error)
}
this.doc.updateConnectionState('ok')
this.doc.flushPendingOps()
return this._callJoinCallbacks()
})
}
}
_callJoinCallbacks() {
for (const callback of Array.from(this._joinCallbacks || [])) {
callback()
}
return delete this._joinCallbacks
}
_joinDoc(callback) {
if (callback == null) {
callback = function () {}
}
if (this.doc != null) {
this.ide.pushEvent('joinDoc:existing', {
doc_id: this.doc_id,
version: this.doc.getVersion(),
})
return this.ide.socket.emit(
'joinDoc',
this.doc_id,
this.doc.getVersion(),
{ encodeRanges: true },
(error, docLines, version, updates, ranges) => {
if (error != null) {
return callback(error)
}
this.joined = true
this.doc.catchUp(updates)
this._decodeRanges(ranges)
this._catchUpRanges(
ranges != null ? ranges.changes : undefined,
ranges != null ? ranges.comments : undefined
)
return callback()
}
)
} else {
this.ide.pushEvent('joinDoc:new', {
doc_id: this.doc_id,
})
return this.ide.socket.emit(
'joinDoc',
this.doc_id,
{ encodeRanges: true },
(error, docLines, version, updates, ranges) => {
if (error != null) {
return callback(error)
}
this.joined = true
this.ide.pushEvent('joinDoc:inited', {
doc_id: this.doc_id,
version,
})
this.doc = new ShareJsDoc(
this.doc_id,
docLines,
version,
this.ide.socket,
this.ide.globalEditorWatchdogManager
)
this._decodeRanges(ranges)
this.ranges = new RangesTracker(
ranges != null ? ranges.changes : undefined,
ranges != null ? ranges.comments : undefined
)
this._bindToShareJsDocEvents()
return callback()
}
)
}
}
_decodeRanges(ranges) {
const decodeFromWebsockets = text => decodeURIComponent(escape(text))
try {
for (const change of Array.from(ranges.changes || [])) {
if (change.op.i != null) {
change.op.i = decodeFromWebsockets(change.op.i)
}
if (change.op.d != null) {
change.op.d = decodeFromWebsockets(change.op.d)
}
}
return (() => {
const result = []
for (const comment of Array.from(ranges.comments || [])) {
if (comment.op.c != null) {
result.push((comment.op.c = decodeFromWebsockets(comment.op.c)))
} else {
result.push(undefined)
}
}
return result
})()
} catch (err) {
debugConsole.error(err)
}
}
_leaveDoc(callback) {
if (callback == null) {
callback = function () {}
}
this.ide.pushEvent('leaveDoc', {
doc_id: this.doc_id,
})
debugConsole.log('[_leaveDoc] Sending leaveDoc request')
return this.ide.socket.emit('leaveDoc', this.doc_id, error => {
if (error != null) {
return callback(error)
}
this.joined = false
for (callback of Array.from(this._leaveCallbacks || [])) {
debugConsole.log('[_leaveDoc] Calling buffered callback', callback)
callback(error)
}
delete this._leaveCallbacks
return callback(error)
})
}
_cleanUp() {
// if we arrive here from _onError the pending and inflight ops will have been cleared
if (this.hasBufferedOps()) {
debugConsole.log(
`[_cleanUp] Document (${this.doc_id}) has buffered ops, refusing to remove from openDocs`
)
return // return immediately, do not unbind from events
} else if (Document.openDocs[this.doc_id] === this) {
debugConsole.log(
`[_cleanUp] Removing self (${this.doc_id}) from in openDocs`
)
delete Document.openDocs[this.doc_id]
} else {
// It's possible that this instance has error, and the doc has been reloaded.
// This creates a new instance in Document.openDoc with the same id. We shouldn't
// clear it because it's not this instance.
debugConsole.log(
`[_cleanUp] New instance of (${this.doc_id}) created. Not removing`
)
}
this._unBindFromEditorEvents()
return this._unBindFromSocketEvents()
}
_bindToShareJsDocEvents() {
this.doc.on('error', (error, meta) => this._onError(error, meta))
this.doc.on('externalUpdate', update => {
this.ide.pushEvent('externalUpdate', { doc_id: this.doc_id })
return this.trigger('externalUpdate', update)
})
this.doc.on('remoteop', (...args) => {
this.ide.pushEvent('remoteop', { doc_id: this.doc_id })
return this.trigger('remoteop', ...Array.from(args))
})
this.doc.on('op:sent', op => {
this.ide.pushEvent('op:sent', {
doc_id: this.doc_id,
op,
})
return this.trigger('op:sent')
})
this.doc.on('op:acknowledged', op => {
this.ide.pushEvent('op:acknowledged', {
doc_id: this.doc_id,
op,
})
this.ide.$scope.$emit('ide:opAcknowledged', {
doc_id: this.doc_id,
op,
})
return this.trigger('op:acknowledged')
})
this.doc.on('op:timeout', op => {
this.ide.pushEvent('op:timeout', {
doc_id: this.doc_id,
op,
})
this.trigger('op:timeout')
return this._onError(new Error('op timed out'))
})
this.doc.on('flush', (inflightOp, pendingOp, version) => {
return this.ide.pushEvent('flush', {
doc_id: this.doc_id,
inflightOp,
pendingOp,
v: version,
})
})
let docChangedTimeout
this.doc.on('change', (ops, oldSnapshot, msg) => {
this._applyOpsToRanges(ops, oldSnapshot, msg)
if (docChangedTimeout) {
window.clearTimeout(docChangedTimeout)
}
docChangedTimeout = window.setTimeout(() => {
window.dispatchEvent(
new CustomEvent('doc:changed', { detail: { id: this.doc_id } })
)
this.ide.$scope.$emit('doc:changed', { doc_id: this.doc_id })
}, 50)
})
this.doc.on('flipped_pending_to_inflight', () => {
return this.trigger('flipped_pending_to_inflight')
})
let docSavedTimeout
this.doc.on('saved', () => {
if (docSavedTimeout) {
window.clearTimeout(docSavedTimeout)
}
docSavedTimeout = window.setTimeout(() => {
window.dispatchEvent(
new CustomEvent('doc:saved', { detail: { id: this.doc_id } })
)
this.ide.$scope.$emit('doc:saved', { doc_id: this.doc_id })
}, 50)
})
}
_onError(error, meta, editorContent) {
if (meta == null) {
meta = {}
}
meta.doc_id = this.doc_id
debugConsole.log('ShareJS error', error, meta)
if (error.message === 'no project_id found on client') {
debugConsole.log('ignoring error, will wait to join project')
return
}
if (this.doc != null) {
this.doc.clearInflightAndPendingOps()
}
this.trigger('error', error, meta, editorContent)
// The clean up should run after the error is triggered because the error triggers a
// disconnect. If we run the clean up first, we remove our event handlers and miss
// the disconnect event, which means we try to leaveDoc when the connection comes back.
// This could intefere with the new connection of a new instance of this document.
return this._cleanUp()
}
_applyOpsToRanges(ops, oldSnapshot, msg) {
let old_id_seed
if (ops == null) {
ops = []
}
let track_changes_as = null
const remote_op = msg != null
if (__guard__(msg != null ? msg.meta : undefined, x => x.tc) != null) {
old_id_seed = this.ranges.getIdSeed()
this.ranges.setIdSeed(msg.meta.tc)
}
if (remote_op && (msg.meta != null ? msg.meta.tc : undefined)) {
track_changes_as = msg.meta.user_id
} else if (!remote_op && this.track_changes_as != null) {
;({ track_changes_as } = this)
}
this.ranges.track_changes = track_changes_as != null
for (const op of this._filterOps(ops)) {
this.ranges.applyOp(op, { user_id: track_changes_as })
}
if (old_id_seed != null) {
this.ranges.setIdSeed(old_id_seed)
}
if (remote_op) {
// With remote ops, the editor hasn't been updated when we receive this op,
// so defer updating track changes until it has
return setTimeout(() => this.emit('ranges:dirty'))
} else {
return this.emit('ranges:dirty')
}
}
_catchUpRanges(changes, comments) {
// We've just been given the current server's ranges, but need to apply any local ops we have.
// Reset to the server state then apply our local ops again.
if (changes == null) {
changes = []
}
if (comments == null) {
comments = []
}
this.emit('ranges:clear')
this.ranges.changes = changes
this.ranges.comments = comments
this.ranges.track_changes = this.doc.track_changes
for (const op of this._filterOps(this.doc.getInflightOp() || [])) {
this.ranges.setIdSeed(this.doc.track_changes_id_seeds.inflight)
this.ranges.applyOp(op, { user_id: this.track_changes_as })
}
for (const op of this._filterOps(this.doc.getPendingOp() || [])) {
this.ranges.setIdSeed(this.doc.track_changes_id_seeds.pending)
this.ranges.applyOp(op, { user_id: this.track_changes_as })
}
return this.emit('ranges:redraw')
}
_filterOps(ops) {
// Read-only token users can't see/edit comment, so we filter out comment
// ops to avoid highlighting comment ranges.
if (window.isRestrictedTokenMember) {
return ops.filter(op => op.c == null)
} else {
return ops
}
}
}
Document.initClass()
return Document
})()
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View file

@ -1,510 +0,0 @@
import _ from 'lodash'
/* eslint-disable
camelcase,
n/handle-callback-err,
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import Document from './Document'
import './directives/formattingButtons'
import './directives/toggleSwitch'
import './controllers/SavingNotificationController'
import './controllers/CompileButton'
import './controllers/SwitchToPDFButton'
import '../metadata/services/metadata'
import { debugConsole } from '@/utils/debugging'
import customLocalStorage from '@/infrastructure/local-storage'
let EditorManager
export default EditorManager = (function () {
EditorManager = class EditorManager {
static initClass() {
this.prototype._syncTimeout = null
}
constructor(ide, $scope, localStorage, eventTracking) {
this.ide = ide
this.editorOpenDocEpoch = 0 // track pending document loads
this.$scope = $scope
this.localStorage = localStorage
this.$scope.editor = {
sharejs_doc: null,
open_doc_id: null,
open_doc_name: null,
opening: true,
trackChanges: false,
wantTrackChanges: false,
docTooLongErrorShown: false,
showVisual: this.showVisual(),
showSymbolPalette: false,
toggleSymbolPalette: () => {
const newValue = !this.$scope.editor.showSymbolPalette
this.$scope.editor.showSymbolPalette = newValue
ide.$scope.$emit('south-pane-toggled', newValue)
eventTracking.sendMB(
newValue ? 'symbol-palette-show' : 'symbol-palette-hide'
)
},
insertSymbol: symbol => {
ide.$scope.$emit('editor:replace-selection', symbol.command)
eventTracking.sendMB('symbol-palette-insert')
},
multiSelectedCount: 0,
}
window.addEventListener('editor:insert-symbol', event => {
this.$scope.editor.insertSymbol(event.detail)
})
this.$scope.$on('entity:selected', (event, entity) => {
if (this.$scope.ui.view !== 'history' && entity.type === 'doc') {
return this.openDoc(entity)
}
})
this.$scope.$on('entity:no-selection', () => {
this.$scope.$apply(() => {
this.$scope.ui.view = null
})
})
this.$scope.$on('entity:deleted', (event, entity) => {
if (this.$scope.editor.open_doc_id === entity.id) {
if (!this.$scope.project.rootDoc_id) {
this.$scope.ui.view = null
return
}
const doc = this.ide.fileTreeManager.findEntityById(
this.$scope.project.rootDoc_id
)
if (doc == null) {
this.$scope.ui.view = null
return
}
return this.openDoc(doc)
}
})
let initialized = false
this.$scope.$on('file-tree:initialized', () => {
if (!initialized) {
initialized = true
return this.autoOpenDoc()
}
})
this.$scope.$on('flush-changes', () => {
return Document.flushAll()
})
// event dispatched by pdf preview
window.addEventListener('flush-changes', () => {
Document.flushAll()
})
window.addEventListener('blur', () => {
// The browser may put the tab into sleep as it looses focus.
// Flushing the documents should help with keeping the documents in
// sync: we can use any new version of the doc that the server may
// present us. There should be no need to insert local changes into
// the doc history as the user comes back.
debugConsole.log('[EditorManager] forcing flush onblur')
Document.flushAll()
})
this.$scope.$watch('editor.wantTrackChanges', value => {
if (value == null) {
return
}
return this._syncTrackChangesState(this.$scope.editor.sharejs_doc)
})
window.addEventListener('editor:open-doc', event => {
const { doc, ...options } = event.detail
this.openDoc(doc, options)
})
window.addEventListener('editor:open-file', event => {
const { name, ...options } = event.detail
for (const extension of ['', '.tex']) {
const path = `${name}${extension}`
const doc = ide.fileTreeManager.findEntityByPath(path)
if (doc) {
this.openDoc(doc, options)
break
}
}
})
}
getEditorType() {
if (!this.$scope.editor.sharejs_doc) {
return null
}
let editorType = this.$scope.editor.sharejs_doc.editorType()
if (editorType === 'cm6' && this.$scope.editor.showVisual) {
editorType = 'cm6-rich-text'
}
return editorType
}
showVisual() {
const editorModeKey = `editor.mode.${this.$scope.project_id}`
const editorModeVal = this.localStorage(editorModeKey)
if (editorModeVal) {
// clean up the old key
customLocalStorage.removeItem(editorModeKey)
}
const lastUsedMode = this.localStorage(`editor.lastUsedMode`)
if (lastUsedMode) {
return lastUsedMode === 'visual'
} else {
return editorModeVal === 'rich-text'
}
}
autoOpenDoc() {
const open_doc_id =
this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) ||
this.$scope.project.rootDoc_id
if (open_doc_id == null) {
return
}
const doc = this.ide.fileTreeManager.findEntityById(open_doc_id)
if (doc == null) {
return
}
return this.openDoc(doc)
}
openDocId(doc_id, options) {
if (options == null) {
options = {}
}
const doc = this.ide.fileTreeManager.findEntityById(doc_id)
if (doc == null) {
return
}
return this.openDoc(doc, options)
}
jumpToLine(options) {
return this.$scope.$broadcast(
'editor:gotoLine',
options.gotoLine,
options.gotoColumn,
options.syncToPdf
)
}
openDoc(doc, options) {
if (options == null) {
options = {}
}
debugConsole.log(`[openDoc] Opening ${doc.id}`)
if (this.$scope.ui.view === 'editor') {
// store position of previous doc before switching docs
this.$scope.$broadcast('store-doc-position')
}
this.$scope.ui.view = 'editor'
const done = isNewDoc => {
const eventName = 'doc:after-opened'
this.$scope.$broadcast(eventName, { isNewDoc })
window.dispatchEvent(new CustomEvent(eventName, { detail: isNewDoc }))
if (options.gotoLine != null) {
// allow Ace to display document before moving, delay until next tick
// added delay to make this happen later that gotoStoredPosition in
// CursorPositionManager
setTimeout(() => this.jumpToLine(options))
// when opening a doc in CM6, jump to the line again after a stored scroll position has been restored
if (isNewDoc) {
window.addEventListener(
'editor:scroll-position-restored',
() => this.jumpToLine(options),
{ once: true }
)
}
} else if (options.gotoOffset != null) {
setTimeout(() => {
this.$scope.$broadcast('editor:gotoOffset', options.gotoOffset)
})
}
}
// If we already have the document open we can return at this point.
// Note: only use forceReopen:true to override this when the document is
// is out of sync and needs to be reloaded from the server.
if (doc.id === this.$scope.editor.open_doc_id && !options.forceReopen) {
// automatically update the file tree whenever the file is opened
this.ide.fileTreeManager.selectEntity(doc)
this.$scope.$broadcast('file-tree.reselectDoc', doc.id)
this.$scope.$apply(() => {
return done(false)
})
return
}
this.$scope.$applyAsync(() => {
// We're now either opening a new document or reloading a broken one.
this.$scope.editor.open_doc_id = doc.id
this.$scope.editor.open_doc_name = doc.name
this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`, doc.id)
this.ide.fileTreeManager.selectEntity(doc)
this.$scope.editor.opening = true
return this._openNewDocument(doc, (error, sharejs_doc) => {
if (error && error.message === 'another document was loaded') {
debugConsole.log(
`[openDoc] another document was loaded while ${doc.id} was loading`
)
return
}
if (error != null) {
this.ide.showGenericMessageModal(
'Error opening document',
'Sorry, something went wrong opening this document. Please try again.'
)
return
}
this._syncTrackChangesState(sharejs_doc)
this.$scope.$broadcast('doc:opened')
return this.$scope.$applyAsync(() => {
this.$scope.editor.opening = false
this.$scope.editor.sharejs_doc = sharejs_doc
return done(true)
})
})
})
}
_openNewDocument(doc, callback) {
// Leave the current document
// - when we are opening a different new one, to avoid race conditions
// between leaving and joining the same document
// - when the current one has pending ops that need flushing, to avoid
// race conditions from cleanup
const current_sharejs_doc = this.$scope.editor.sharejs_doc
const currentDocId = current_sharejs_doc && current_sharejs_doc.doc_id
const hasBufferedOps =
current_sharejs_doc && current_sharejs_doc.hasBufferedOps()
const changingDoc = current_sharejs_doc && currentDocId !== doc.id
if (changingDoc || hasBufferedOps) {
debugConsole.log('[_openNewDocument] Leaving existing open doc...')
// Do not trigger any UI changes from remote operations
this._unbindFromDocumentEvents(current_sharejs_doc)
// Keep listening for out-of-sync and similar errors.
this._attachErrorHandlerToDocument(doc, current_sharejs_doc)
// Teardown the Document -> ShareJsDoc -> sharejs doc
// By the time this completes, the Document instance is no longer
// registered in Document.openDocs and _doOpenNewDocument can start
// from scratch -- read: no corrupted internal state.
const editorOpenDocEpoch = ++this.editorOpenDocEpoch
current_sharejs_doc.leaveAndCleanUp(error => {
if (error) {
debugConsole.log(
`[_openNewDocument] error leaving doc ${currentDocId}`,
error
)
return callback(error)
}
if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
debugConsole.log(
`[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
)
return callback(new Error('another document was loaded'))
}
this._doOpenNewDocument(doc, callback)
})
} else {
this._doOpenNewDocument(doc, callback)
}
}
_doOpenNewDocument(doc, callback) {
if (callback == null) {
callback = function () {}
}
debugConsole.log('[_doOpenNewDocument] Opening...')
const new_sharejs_doc = Document.getDocument(this.ide, doc.id)
const editorOpenDocEpoch = ++this.editorOpenDocEpoch
return new_sharejs_doc.join(error => {
if (error != null) {
debugConsole.log(
`[_doOpenNewDocument] error joining doc ${doc.id}`,
error
)
return callback(error)
}
if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
debugConsole.log(
`[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
)
new_sharejs_doc.leaveAndCleanUp()
return callback(new Error('another document was loaded'))
}
this._bindToDocumentEvents(doc, new_sharejs_doc)
return callback(null, new_sharejs_doc)
})
}
_attachErrorHandlerToDocument(doc, sharejs_doc) {
sharejs_doc.on('error', (error, meta, editorContent) => {
let message
if ((error != null ? error.message : undefined) != null) {
;({ message } = error)
} else if (typeof error === 'string') {
message = error
} else {
message = ''
}
if (/maxDocLength/.test(message)) {
this.$scope.docTooLongErrorShown = true
this.openDoc(doc, { forceReopen: true })
const genericMessageModal = this.ide.showGenericMessageModal(
'Document Too Long',
'Sorry, this file is too long to be edited manually. Please upload it directly.'
)
genericMessageModal.result.finally(() => {
this.$scope.docTooLongErrorShown = false
})
} else if (/too many comments or tracked changes/.test(message)) {
this.ide.showGenericMessageModal(
'Too many comments or tracked changes',
'Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments.'
)
} else if (!this.$scope.docTooLongErrorShown) {
// Do not allow this doc to open another error modal.
sharejs_doc.off('error')
// Preserve the sharejs contents before the teardown.
editorContent =
typeof editorContent === 'string'
? editorContent
: sharejs_doc.doc._doc.snapshot
// Tear down the ShareJsDoc.
if (sharejs_doc.doc) sharejs_doc.doc.clearInflightAndPendingOps()
// Do not re-join after re-connecting.
sharejs_doc.leaveAndCleanUp()
this.ide.connectionManager.disconnect({ permanent: true })
this.ide.reportError(error, meta)
// Tell the user about the error state.
this.$scope.editor.error_state = true
this.ide.showOutOfSyncModal(
'Out of sync',
"Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a target='_blank' rel='noopener noreferrer' href='/learn/Kb/Editor_out_of_sync_problems'>Please see this help guide for more information</a>",
editorContent
)
// Do not forceReopen the document.
return
}
const removeHandler = this.$scope.$on('project:joined', () => {
this.openDoc(doc, { forceReopen: true })
removeHandler()
})
})
}
_bindToDocumentEvents(doc, sharejs_doc) {
this._attachErrorHandlerToDocument(doc, sharejs_doc)
return sharejs_doc.on('externalUpdate', update => {
if (this._ignoreExternalUpdates) {
return
}
if (
_.property(['meta', 'type'])(update) === 'external' &&
_.property(['meta', 'source'])(update) === 'git-bridge'
) {
return
}
if (update?.meta?.source === 'file-revert') {
return
}
return this.ide.showGenericMessageModal(
'Document Updated Externally',
'This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history.'
)
})
}
_unbindFromDocumentEvents(document) {
return document.off()
}
getCurrentDocValue() {
return this.$scope.editor.sharejs_doc != null
? this.$scope.editor.sharejs_doc.getSnapshot()
: undefined
}
getCurrentDocId() {
return this.$scope.editor.open_doc_id
}
startIgnoringExternalUpdates() {
return (this._ignoreExternalUpdates = true)
}
stopIgnoringExternalUpdates() {
return (this._ignoreExternalUpdates = false)
}
_syncTrackChangesState(doc) {
let tryToggle
if (doc == null) {
return
}
if (this._syncTimeout != null) {
clearTimeout(this._syncTimeout)
this._syncTimeout = null
}
const want = this.$scope.editor.wantTrackChanges
const have = doc.getTrackingChanges()
if (want === have) {
this.$scope.editor.trackChanges = want
return
}
return (tryToggle = () => {
const saved = doc.getInflightOp() == null && doc.getPendingOp() == null
if (saved) {
doc.setTrackingChanges(want)
return this.$scope.$apply(() => {
return (this.$scope.editor.trackChanges = want)
})
} else {
return (this._syncTimeout = setTimeout(tryToggle, 100))
}
})()
}
}
EditorManager.initClass()
return EditorManager
})()

View file

@ -1,50 +0,0 @@
/* eslint-disable
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let EditorShareJsCodec
export default EditorShareJsCodec = {
rangeToShareJs(range, lines) {
let offset = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
offset += i < range.row ? line.length : range.column
}
offset += range.row // Include newlines
return offset
},
changeToShareJs(delta, lines) {
const offset = EditorShareJsCodec.rangeToShareJs(delta.start, lines)
const text = delta.lines.join('\n')
switch (delta.action) {
case 'insert':
return { i: text, p: offset }
case 'remove':
return { d: text, p: offset }
default:
throw new Error(`unknown action: ${delta.action}`)
}
},
shareJsOffsetToRowColumn(offset, lines) {
let row = 0
for (row = 0; row < lines.length; row++) {
const line = lines[row]
if (offset <= line.length) {
break
}
offset -= lines[row].length + 1
} // + 1 for newline char
return { row, column: offset }
},
}

View file

@ -1,471 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS001: Remove Babel/TypeScript constructor workaround
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS205: Consider reworking code to avoid use of IIFEs
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import EventEmitter from '../../utils/EventEmitter'
import ShareJs from '../../vendor/libs/sharejs'
import EditorWatchdogManager from '../connection/EditorWatchdogManager'
import { debugConsole } from '@/utils/debugging'
import { recordDocumentFirstChangeEvent } from '@/features/event-tracking/document-first-change-event'
let ShareJsDoc
const SINGLE_USER_FLUSH_DELAY = 2000 // ms
const MULTI_USER_FLUSH_DELAY = 500 // ms
export default ShareJsDoc = (function () {
ShareJsDoc = class ShareJsDoc extends EventEmitter {
static initClass() {
this.prototype.INFLIGHT_OP_TIMEOUT = 5000 // Retry sending ops after 5 seconds without an ack
this.prototype.WAIT_FOR_CONNECTION_TIMEOUT = 500
this.prototype.FATAL_OP_TIMEOUT = 30000
}
constructor(
doc_id,
docLines,
version,
socket,
globalEditorWatchdogManager
) {
super()
// Dencode any binary bits of data
// See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
this.doc_id = doc_id
this.socket = socket
this.type = 'text'
docLines = Array.from(docLines).map(line =>
decodeURIComponent(escape(line))
)
const snapshot = docLines.join('\n')
this.track_changes = false
this.connection = {
send: update => {
this._startInflightOpTimeout(update)
if (
window.disconnectOnUpdate != null &&
Math.random() < window.disconnectOnUpdate
) {
debugConsole.log('Disconnecting on update', update)
window._ide.socket.socket.disconnect()
}
if (
window.dropUpdates != null &&
Math.random() < window.dropUpdates
) {
debugConsole.log('Simulating a lost update', update)
return
}
if (this.track_changes) {
if (update.meta == null) {
update.meta = {}
}
update.meta.tc = this.track_changes_id_seeds.inflight
}
return this.socket.emit(
'applyOtUpdate',
this.doc_id,
update,
error => {
if (error != null) {
return this._handleError(error)
}
}
)
},
state: 'ok',
id: this.socket.publicId,
}
this._doc = new ShareJs.Doc(this.connection, this.doc_id, {
type: this.type,
})
this._doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
this._doc.on('change', (...args) => {
return this.trigger('change', ...Array.from(args))
})
this.EditorWatchdogManager = new EditorWatchdogManager({
parent: globalEditorWatchdogManager,
})
this._doc.on('acknowledge', () => {
this.lastAcked = new Date() // note time of last ack from server for an op we sent
this.EditorWatchdogManager.onAck() // keep track of last ack globally
return this.trigger('acknowledge')
})
this._doc.on('remoteop', (...args) => {
// As soon as we're working with a collaborator, start sending
// ops more frequently for low latency.
this._doc.setFlushDelay(MULTI_USER_FLUSH_DELAY)
return this.trigger('remoteop', ...Array.from(args))
})
this._doc.on('flipped_pending_to_inflight', () => {
return this.trigger('flipped_pending_to_inflight')
})
this._doc.on('saved', () => {
return this.trigger('saved')
})
this._doc.on('error', e => {
return this._handleError(e)
})
this._bindToDocChanges(this._doc)
this.processUpdateFromServer({
open: true,
v: version,
snapshot,
})
this._removeCarriageReturnCharFromShareJsDoc()
}
_removeCarriageReturnCharFromShareJsDoc() {
const doc = this._doc
if (doc.snapshot.indexOf('\r') === -1) {
return
}
window._ide.pushEvent('remove-carriage-return-char', {
doc_id: this.doc_id,
})
let nextPos
while ((nextPos = doc.snapshot.indexOf('\r')) !== -1) {
debugConsole.log('[ShareJsDoc] remove-carriage-return-char', nextPos)
doc.del(nextPos, 1)
}
}
submitOp(...args) {
return this._doc.submitOp(...Array.from(args || []))
}
// The following code puts out of order messages into a queue
// so that they can be processed in order. This is a workaround
// for messages being delayed by redis cluster.
// FIXME: REMOVE THIS WHEN REDIS PUBSUB IS SENDING MESSAGES IN ORDER
_isAheadOfExpectedVersion(message) {
return this._doc.version > 0 && message.v > this._doc.version
}
_pushOntoQueue(message) {
debugConsole.log(`[processUpdate] push onto queue ${message.v}`)
// set a timer so that we never leave messages in the queue indefinitely
if (!this.queuedMessageTimer) {
this.queuedMessageTimer = setTimeout(() => {
debugConsole.log(
`[processUpdate] queue timeout fired for ${message.v}`
)
// force the message to be processed after the timeout,
// it will cause an error if the missing update has not arrived
this.processUpdateFromServer(message)
}, this.INFLIGHT_OP_TIMEOUT)
}
this.queuedMessages.push(message)
// keep the queue in order, lowest version first
this.queuedMessages.sort(function (a, b) {
return a.v - b.v
})
}
_clearQueue() {
this.queuedMessages = []
}
_processQueue() {
if (this.queuedMessages.length > 0) {
const nextAvailableVersion = this.queuedMessages[0].v
if (nextAvailableVersion > this._doc.version) {
// there are updates we still can't apply yet
} else {
// there's a version we can accept on the queue, apply it
debugConsole.log(
`[processUpdate] taken from queue ${nextAvailableVersion}`
)
this.processUpdateFromServerInOrder(this.queuedMessages.shift())
// clear the pending timer if the queue has now been cleared
if (this.queuedMessages.length === 0 && this.queuedMessageTimer) {
debugConsole.log('[processUpdate] queue is empty, cleared timeout')
clearTimeout(this.queuedMessageTimer)
this.queuedMessageTimer = null
}
}
}
}
// FIXME: This is the new method which reorders incoming updates if needed
// called from Document.js
processUpdateFromServerInOrder(message) {
// Create an array to hold queued messages
if (!this.queuedMessages) {
this.queuedMessages = []
}
// Is this update ahead of the next expected update?
// If so, put it on a queue to be handled later.
if (this._isAheadOfExpectedVersion(message)) {
this._pushOntoQueue(message)
return // defer processing this update for now
}
const error = this.processUpdateFromServer(message)
if (
error instanceof Error &&
error.message === 'Invalid version from server'
) {
// if there was an error, abandon the queued updates ahead of this one
this._clearQueue()
return
}
// Do we have any messages queued up?
// find the next message if available
this._processQueue()
}
// FIXME: This is the original method. Switch back to this when redis
// issues are resolved.
processUpdateFromServer(message) {
try {
this._doc._onMessage(message)
} catch (error) {
// Version mismatches are thrown as errors
debugConsole.error(error)
this._handleError(error)
return error // return the error for queue handling
}
if (
__guard__(message != null ? message.meta : undefined, x => x.type) ===
'external'
) {
return this.trigger('externalUpdate', message)
}
}
catchUp(updates) {
return (() => {
const result = []
for (let i = 0; i < updates.length; i++) {
const update = updates[i]
update.v = this._doc.version
update.doc = this.doc_id
result.push(this.processUpdateFromServer(update))
}
return result
})()
}
getSnapshot() {
return this._doc.snapshot
}
getVersion() {
return this._doc.version
}
getType() {
return this.type
}
clearInflightAndPendingOps() {
this._clearFatalTimeoutTimer()
this._doc.inflightOp = null
this._doc.inflightCallbacks = []
this._doc.pendingOp = null
return (this._doc.pendingCallbacks = [])
}
flushPendingOps() {
// This will flush any ops that are pending.
// If there is an inflight op it will do nothing.
return this._doc.flush()
}
updateConnectionState(state) {
debugConsole.log(`[updateConnectionState] Setting state to ${state}`)
this.connection.state = state
this.connection.id = this.socket.publicId
this._doc.autoOpen = false
this._doc._connectionStateChanged(state)
return (this.lastAcked = null) // reset the last ack time when connection changes
}
hasBufferedOps() {
return this._doc.inflightOp != null || this._doc.pendingOp != null
}
getInflightOp() {
return this._doc.inflightOp
}
getPendingOp() {
return this._doc.pendingOp
}
getRecentAck() {
// check if we have received an ack recently (within a factor of two of the single user flush delay)
return (
this.lastAcked != null &&
new Date() - this.lastAcked < 2 * SINGLE_USER_FLUSH_DELAY
)
}
getOpSize(op) {
// compute size of an op from its components
// (total number of characters inserted and deleted)
let size = 0
for (const component of Array.from(op || [])) {
if ((component != null ? component.i : undefined) != null) {
size += component.i.length
}
if ((component != null ? component.d : undefined) != null) {
size += component.d.length
}
}
return size
}
_attachEditorWatchdogManager(editorName, editor) {
// end-to-end check for edits -> acks, for this very ShareJsdoc
// This will catch a broken connection and missing UX-blocker for the
// user, allowing them to keep editing.
this._detachEditorWatchdogManager =
this.EditorWatchdogManager.attachToEditor(editorName, editor)
}
_attachToEditor(editorName, editor, attachToShareJs) {
this._attachEditorWatchdogManager(editorName, editor)
attachToShareJs()
}
_maybeDetachEditorWatchdogManager() {
// a failed attach attempt may lead to a missing cleanup handler
if (this._detachEditorWatchdogManager) {
this._detachEditorWatchdogManager()
delete this._detachEditorWatchdogManager
}
}
attachToCM6(cm6) {
this._attachToEditor('CM6', cm6, () => {
cm6.attachShareJs(this._doc, window.maxDocLength)
})
}
detachFromCM6() {
this._maybeDetachEditorWatchdogManager()
if (this._doc.detach_cm6) {
this._doc.detach_cm6()
}
}
_startInflightOpTimeout(update) {
this._startFatalTimeoutTimer(update)
const retryOp = () => {
// Only send the update again if inflightOp is still populated
// This can be cleared when hard reloading the document in which
// case we don't want to keep trying to send it.
debugConsole.log('[inflightOpTimeout] Trying op again')
if (this._doc.inflightOp != null) {
// When there is a socket.io disconnect, @_doc.inflightSubmittedIds
// is updated with the socket.io client id of the current op in flight
// (meta.source of the op).
// @connection.id is the client id of the current socket.io session.
// So we need both depending on whether the op was submitted before
// one or more disconnects, or if it was submitted during the current session.
update.dupIfSource = [
this.connection.id,
...Array.from(this._doc.inflightSubmittedIds),
]
// We must be joined to a project for applyOtUpdate to work on the real-time
// service, so don't send an op if we're not. Connection state is set to 'ok'
// when we've joined the project
if (this.connection.state !== 'ok') {
let timer
debugConsole.log(
'[inflightOpTimeout] Not connected, retrying in 0.5s'
)
return (timer = setTimeout(
retryOp,
this.WAIT_FOR_CONNECTION_TIMEOUT
))
} else {
debugConsole.log('[inflightOpTimeout] Sending')
return this.connection.send(update)
}
}
}
const timer = setTimeout(retryOp, this.INFLIGHT_OP_TIMEOUT)
return this._doc.inflightCallbacks.push(() => {
this._clearFatalTimeoutTimer()
return clearTimeout(timer)
}) // 30 seconds
}
_startFatalTimeoutTimer(update) {
// If an op doesn't get acked within FATAL_OP_TIMEOUT, something has
// gone unrecoverably wrong (the op will have been retried multiple times)
if (this._timeoutTimer != null) {
return
}
return (this._timeoutTimer = setTimeout(() => {
this._clearFatalTimeoutTimer()
return this.trigger('op:timeout', update)
}, this.FATAL_OP_TIMEOUT))
}
_clearFatalTimeoutTimer() {
if (this._timeoutTimer == null) {
return
}
clearTimeout(this._timeoutTimer)
return (this._timeoutTimer = null)
}
_handleError(error, meta) {
if (meta == null) {
meta = {}
}
return this.trigger('error', error, meta)
}
_bindToDocChanges(doc) {
const { submitOp } = doc
doc.submitOp = (...args) => {
recordDocumentFirstChangeEvent()
this.trigger('op:sent', ...Array.from(args))
doc.pendingCallbacks.push(() => {
return this.trigger('op:acknowledged', ...Array.from(args))
})
return submitOp.apply(doc, args)
}
const { flush } = doc
return (doc.flush = (...args) => {
this.trigger('flush', doc.inflightOp, doc.pendingOp, doc.version)
return flush.apply(doc, args)
})
}
}
ShareJsDoc.initClass()
return ShareJsDoc
})()
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View file

@ -1,9 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import { rootContext } from '../../../shared/context/root-context'
import DetachCompileButtonWrapper from '../../../features/pdf-preview/components/detach-compile-button-wrapper'
App.component(
'editorCompileButton',
react2angular(rootContext.use(DetachCompileButtonWrapper))
)

View file

@ -1,103 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from '../../../base'
import Document from '../Document'
export default App.controller('SavingNotificationController', [
'$scope',
'ide',
function ($scope, ide) {
let warnAboutUnsavedChanges
setInterval(() => pollSavedStatus(), 1000)
$(window).bind('beforeunload', () => {
return warnAboutUnsavedChanges()
})
let lockEditorModal = null // modal showing "connection lost"
let originalPermissionsLevel
const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved
$scope.docSavingStatus = {}
function pollSavedStatus() {
let t
const oldStatus = $scope.docSavingStatus
const oldUnsavedCount = $scope.docSavingStatusCount
const newStatus = {}
let newUnsavedCount = 0
let maxUnsavedSeconds = 0
for (const doc_id in Document.openDocs) {
const doc = Document.openDocs[doc_id]
const saving = doc.pollSavedStatus()
if (!saving) {
newUnsavedCount++
if (oldStatus[doc_id] != null) {
newStatus[doc_id] = oldStatus[doc_id]
t = newStatus[doc_id].unsavedSeconds += 1
if (t > maxUnsavedSeconds) {
maxUnsavedSeconds = t
}
} else {
newStatus[doc_id] = {
unsavedSeconds: 0,
doc: ide.fileTreeManager.findEntityById(doc_id),
}
}
}
}
if (newUnsavedCount > 0 && t > MAX_UNSAVED_SECONDS && !lockEditorModal) {
lockEditorModal = ide.showLockEditorMessageModal(
'Connection lost',
'Sorry, the connection to the server is down.'
)
// put editor in readOnly mode
originalPermissionsLevel = ide.$scope.permissionsLevel
ide.$scope.permissionsLevel = 'readOnly'
lockEditorModal.result.finally(() => {
lockEditorModal = null // unset the modal if connection comes back
// restore original permissions
ide.$scope.permissionsLevel = originalPermissionsLevel
})
}
if (lockEditorModal && newUnsavedCount === 0) {
lockEditorModal.dismiss('connection back up')
// restore original permissions if they were changed
if (originalPermissionsLevel) {
ide.$scope.permissionsLevel = originalPermissionsLevel
}
}
// for performance, only update the display if the old or new
// counts of unsaved files are nonzeror. If both old and new
// unsaved counts are zero then we know we are in a good state
// and don't need to do anything to the UI.
if (newUnsavedCount || oldUnsavedCount) {
$scope.docSavingStatus = newStatus
$scope.docSavingStatusCount = newUnsavedCount
return $scope.$apply()
}
}
return (warnAboutUnsavedChanges = function () {
if (Document.hasUnsavedChanges()) {
return 'You have unsaved changes. If you leave now they will not be saved.'
}
})
},
])

View file

@ -1,9 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import { rootContext } from '../../../shared/context/root-context'
import SwitchToPDFButton from '../../../features/source-editor/components/switch-to-pdf-button'
App.component(
'switchToPdfButton',
react2angular(rootContext.use(SwitchToPDFButton))
)

View file

@ -1,19 +0,0 @@
import App from '../../../base'
export default App.directive('formattingButtons', function () {
return {
scope: {
buttons: '=',
opening: '=',
isFullscreenEditor: '=',
},
link(scope, element, attrs) {
scope.showMore = false
scope.shownButtons = scope.buttons
scope.overflowedButtons = []
},
templateUrl: 'formattingButtonsTpl',
}
})

View file

@ -1,38 +0,0 @@
import App from '../../../base'
export default App.directive('toggleSwitch', function () {
return {
restrict: 'E',
scope: {
description: '@',
labelFalse: '@',
labelTrue: '@',
ngModel: '=',
},
template: `\
<fieldset class="toggle-switch">
<legend class="sr-only">{{description}}</legend>
<input
type="radio"
name="toggle-switch-{{$id}}"
class="toggle-switch-input"
id="toggle-switch-false-{{$id}}"
ng-value="false"
ng-model="ngModel"
>
<label for="toggle-switch-false-{{$id}}" class="toggle-switch-label"><span>{{labelFalse}}</span></label>
<input
type="radio"
class="toggle-switch-input"
name="toggle-switch-{{$id}}"
id="toggle-switch-true-{{$id}}"
ng-value="true"
ng-model="ngModel"
>
<label for="toggle-switch-true-{{$id}}" class="toggle-switch-label"><span>{{labelTrue}}</span></label>
</fieldset>\
`,
}
})

View file

@ -1,740 +0,0 @@
/* eslint-disable
camelcase,
n/handle-callback-err,
max-len,
no-dupe-class-members,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS202: Simplify dynamic range loops
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import './controllers/FileTreeController'
import '../../features/file-tree/controllers/file-tree-controller'
import { debugConsole } from '@/utils/debugging'
let FileTreeManager
export default FileTreeManager = class FileTreeManager {
constructor(ide, $scope) {
this.ide = ide
this.$scope = $scope
this.$scope.$on('project:joined', () => {
this.loadRootFolder()
this.loadDeletedDocs()
return this.$scope.$emit('file-tree:initialized')
})
this.$scope.$on('entities:multiSelected', (_event, data) => {
this.$scope.$apply(() => {
this.$scope.multiSelectedCount = data.count
this.$scope.editor.multiSelectedCount = data.count
})
})
this.$scope.$watch('rootFolder', rootFolder => {
if (rootFolder != null) {
return this.recalculateDocList()
}
})
this._bindToSocketEvents()
this.$scope.multiSelectedCount = 0
$(document).on('click', () => {
this.clearMultiSelectedEntities()
setTimeout(() => this.$scope.$digest(), 0)
})
}
_bindToSocketEvents() {
this.ide.socket.on('reciveNewDoc', (parent_folder_id, doc) => {
const parent_folder =
this.findEntityById(parent_folder_id) || this.$scope.rootFolder
return this.$scope.$apply(() => {
parent_folder.children.push({
name: doc.name,
id: doc._id,
type: 'doc',
})
return this.recalculateDocList()
})
})
this.ide.socket.on(
'reciveNewFile',
(parent_folder_id, file, source, linkedFileData) => {
const parent_folder =
this.findEntityById(parent_folder_id) || this.$scope.rootFolder
return this.$scope.$apply(() => {
parent_folder.children.push({
name: file.name,
id: file._id,
type: 'file',
linkedFileData,
created: file.created,
})
return this.recalculateDocList()
})
}
)
this.ide.socket.on('reciveNewFolder', (parent_folder_id, folder) => {
const parent_folder =
this.findEntityById(parent_folder_id) || this.$scope.rootFolder
return this.$scope.$apply(() => {
parent_folder.children.push({
name: folder.name,
id: folder._id,
type: 'folder',
children: [],
})
return this.recalculateDocList()
})
})
this.ide.socket.on('reciveEntityRename', (entity_id, name) => {
const entity = this.findEntityById(entity_id)
if (entity == null) {
return
}
return this.$scope.$apply(() => {
entity.name = name
return this.recalculateDocList()
})
})
this.ide.socket.on('removeEntity', entity_id => {
const entity = this.findEntityById(entity_id)
if (entity == null) {
return
}
this.$scope.$apply(() => {
this._deleteEntityFromScope(entity)
return this.recalculateDocList()
})
return this.$scope.$broadcast('entity:deleted', entity)
})
return this.ide.socket.on('reciveEntityMove', (entity_id, folder_id) => {
const entity = this.findEntityById(entity_id)
const folder = this.findEntityById(folder_id)
return this.$scope.$apply(() => {
this._moveEntityInScope(entity, folder)
return this.recalculateDocList()
})
})
}
selectEntity(entity) {
this.selected_entity_id = entity.id // For reselecting after a reconnect
this.ide.fileTreeManager.forEachEntity(entity => (entity.selected = false))
return (entity.selected = true)
}
getMultiSelectedEntities() {
const entities = []
this.forEachEntity(function (e) {
if (e.multiSelected) {
return entities.push(e)
}
})
return entities
}
getFullCount() {
const entities = []
this.forEachEntity(function (e) {
if (!e.deleted) entities.push(e)
})
return entities.length
}
getMultiSelectedEntityChildNodes() {
// use pathnames with a leading slash to avoid
// problems with reserved Object properties
const entities = this.getMultiSelectedEntities()
const paths = {}
for (const entity of Array.from(entities)) {
paths['/' + this.getEntityPath(entity)] = entity
}
const prefixes = {}
for (const path in paths) {
const entity = paths[path]
const parts = path.split('/')
if (parts.length <= 2) {
continue
} else {
// Record prefixes a/b/c.tex -> 'a' and 'a/b'
for (
let i = 1, end = parts.length - 1, asc = end >= 1;
asc ? i <= end : i >= end;
asc ? i++ : i--
) {
prefixes['/' + parts.slice(0, i).join('/')] = true
}
}
}
const child_entities = []
for (const path in paths) {
// If the path is in the prefixes, then it's a parent folder and
// should be ignore
const entity = paths[path]
if (prefixes[path] == null) {
child_entities.push(entity)
}
}
return child_entities
}
clearMultiSelectedEntities() {
if (this.$scope.multiSelectedCount === 0) {
return
} // Be efficient, this is called a lot on 'click'
this.forEachEntity(entity => (entity.multiSelected = false))
return (this.$scope.multiSelectedCount = 0)
}
existsInFolder(folder_id, name) {
const folder = this.findEntityById(folder_id)
if (folder == null) {
return false
}
const entity = this._findEntityByPathInFolder(folder, name)
return entity != null
}
findSelectedEntity() {
let selected = null
this.forEachEntity(function (entity) {
if (entity.selected) {
return (selected = entity)
}
})
return selected
}
findEntityById(id, options) {
if (options == null) {
options = {}
}
if (this.$scope.rootFolder.id === id) {
return this.$scope.rootFolder
}
let entity = this._findEntityByIdInFolder(this.$scope.rootFolder, id)
if (entity != null) {
return entity
}
if (options.includeDeleted) {
for (entity of Array.from(this.$scope.deletedDocs)) {
if (entity.id === id) {
return entity
}
}
}
return null
}
_findEntityByIdInFolder(folder, id) {
for (const entity of Array.from(folder.children || [])) {
if (entity.id === id) {
return entity
} else if (entity.children != null) {
const result = this._findEntityByIdInFolder(entity, id)
if (result != null) {
return result
}
}
}
return null
}
findEntityByPath(path) {
return this._findEntityByPathInFolder(this.$scope.rootFolder, path)
}
getPreviewByPath(path) {
for (const suffix of [
'',
'.png',
'.jpg',
'.jpeg',
'.pdf',
'.PNG',
'.JPG',
'.JPEG',
'.PDF',
]) {
const entity = this.findEntityByPath(path + suffix)
if (entity) {
return {
url: `/project/${this.$scope.project._id}/file/${entity.id}`,
extension: entity.name.split('.').pop(),
}
}
}
return null
}
_findEntityByPathInFolder(folder, path) {
if (path == null || folder == null) {
return null
}
if (path === '') {
return folder
}
const parts = path.split('/')
const name = parts.shift()
const rest = parts.join('/')
if (name === '.') {
return this._findEntityByPathInFolder(folder, rest)
}
for (const entity of Array.from(folder.children)) {
if (entity.name === name) {
if (rest === '') {
return entity
} else if (entity.type === 'folder') {
return this._findEntityByPathInFolder(entity, rest)
}
}
}
return null
}
forEachEntity(callback) {
if (callback == null) {
callback = function () {}
}
this._forEachEntityInFolder(this.$scope.rootFolder, null, callback)
return (() => {
const result = []
for (const entity of Array.from(this.$scope.deletedDocs || [])) {
result.push(callback(entity))
}
return result
})()
}
_forEachEntityInFolder(folder, path, callback) {
return (() => {
const result = []
for (const entity of Array.from(folder.children || [])) {
let childPath
if (path != null) {
childPath = path + '/' + entity.name
} else {
childPath = entity.name
}
callback(entity, folder, childPath)
if (entity.children != null) {
result.push(this._forEachEntityInFolder(entity, childPath, callback))
} else {
result.push(undefined)
}
}
return result
})()
}
getEntityPath(entity) {
return this._getEntityPathInFolder(this.$scope.rootFolder, entity)
}
_getEntityPathInFolder(folder, entity) {
for (const child of Array.from(folder.children || [])) {
if (child === entity) {
return entity.name
} else if (child.type === 'folder') {
const path = this._getEntityPathInFolder(child, entity)
if (path != null) {
return child.name + '/' + path
}
}
}
return null
}
getRootDocDirname() {
const rootDoc = this.findEntityById(this.$scope.project.rootDoc_id)
if (rootDoc == null) {
return
}
return this._getEntityDirname(rootDoc)
}
_getEntityDirname(entity) {
const path = this.getEntityPath(entity)
if (path == null) {
return
}
return path.split('/').slice(0, -1).join('/')
}
_findParentFolder(entity) {
const dirname = this._getEntityDirname(entity)
if (dirname == null) {
return
}
return this.findEntityByPath(dirname)
}
loadRootFolder() {
return (this.$scope.rootFolder = this._parseFolder(
__guard__(
this.$scope != null ? this.$scope.project : undefined,
x => x.rootFolder[0]
)
))
}
_parseFolder(rawFolder) {
const folder = {
name: rawFolder.name,
id: rawFolder._id,
type: 'folder',
children: [],
selected: rawFolder._id === this.selected_entity_id,
}
for (const doc of Array.from(rawFolder.docs || [])) {
folder.children.push({
name: doc.name,
id: doc._id,
type: 'doc',
selected: doc._id === this.selected_entity_id,
})
}
for (const file of Array.from(rawFolder.fileRefs || [])) {
folder.children.push({
name: file.name,
id: file._id,
type: 'file',
selected: file._id === this.selected_entity_id,
linkedFileData: file.linkedFileData,
created: file.created,
})
}
for (const childFolder of Array.from(rawFolder.folders || [])) {
folder.children.push(this._parseFolder(childFolder))
}
return folder
}
loadDeletedDocs() {
this.$scope.deletedDocs = []
return Array.from(this.$scope.project.deletedDocs || []).map(doc =>
this.$scope.deletedDocs.push({
name: doc.name,
id: doc._id,
type: 'doc',
deleted: true,
})
)
}
recalculateDocList() {
this.$scope.docs = []
this.forEachEntity((entity, parentFolder, path) => {
if (entity.type === 'doc' && !entity.deleted) {
return this.$scope.docs.push({
doc: entity,
path,
})
}
})
// Keep list ordered by folders, then name
return this.$scope.docs.sort(function (a, b) {
const aDepth = (a.path.match(/\//g) || []).length
const bDepth = (b.path.match(/\//g) || []).length
if (aDepth - bDepth !== 0) {
return -(aDepth - bDepth) // Deeper path == folder first
} else if (a.path < b.path) {
return -1
} else if (a.path > b.path) {
return 1
} else {
return 0
}
})
}
getCurrentFolder() {
// Return the root folder if nothing is selected
return (
this._getCurrentFolder(this.$scope.rootFolder) || this.$scope.rootFolder
)
}
_getCurrentFolder(startFolder) {
if (startFolder == null) {
startFolder = this.$scope.rootFolder
}
for (const entity of Array.from(startFolder.children || [])) {
// The 'current' folder is either the one selected, or
// the one containing the selected doc/file
if (entity.selected) {
if (entity.type === 'folder') {
return entity
} else {
return startFolder
}
}
if (entity.type === 'folder') {
const result = this._getCurrentFolder(entity)
if (result != null) {
return result
}
}
}
return null
}
projectContainsFolder() {
for (const entity of Array.from(this.$scope.rootFolder.children)) {
if (entity.type === 'folder') {
return true
}
}
return false
}
existsInThisFolder(folder, name) {
for (const entity of Array.from(
(folder != null ? folder.children : undefined) || []
)) {
if (entity.name === name) {
return true
}
}
return false
}
nameExistsError(message) {
if (message == null) {
message = 'already exists'
}
const nameExists = this.ide.$q.defer()
nameExists.reject({ data: message })
return nameExists.promise
}
createDoc(name, parent_folder) {
// check if a doc/file/folder already exists with this name
if (parent_folder == null) {
parent_folder = this.getCurrentFolder()
}
if (this.existsInThisFolder(parent_folder, name)) {
return this.nameExistsError()
}
// We'll wait for the socket.io notification to actually
// add the doc for us.
return this.ide.$http.post(`/project/${this.ide.project_id}/doc`, {
name,
parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
_csrf: window.csrfToken,
})
}
createFolder(name, parent_folder) {
// check if a doc/file/folder already exists with this name
if (parent_folder == null) {
parent_folder = this.getCurrentFolder()
}
if (this.existsInThisFolder(parent_folder, name)) {
return this.nameExistsError()
}
// We'll wait for the socket.io notification to actually
// add the folder for us.
return this.ide.$http.post(`/project/${this.ide.project_id}/folder`, {
name,
parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
_csrf: window.csrfToken,
})
}
createLinkedFile(name, parent_folder, provider, data) {
// check if a doc/file/folder already exists with this name
if (parent_folder == null) {
parent_folder = this.getCurrentFolder()
}
if (this.existsInThisFolder(parent_folder, name)) {
return this.nameExistsError()
}
// We'll wait for the socket.io notification to actually
// add the file for us.
return this.ide.$http.post(
`/project/${this.ide.project_id}/linked_file`,
{
name,
parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
provider,
data,
_csrf: window.csrfToken,
},
{
disableAutoLoginRedirect: true,
}
)
}
refreshLinkedFile(file) {
const parent_folder = this._findParentFolder(file)
const provider =
file.linkedFileData != null ? file.linkedFileData.provider : undefined
if (provider == null) {
debugConsole.warn(`>> no provider for ${file.name}`, file)
return
}
return this.ide.$http.post(
`/project/${this.ide.project_id}/linked_file/${file.id}/refresh`,
{
_csrf: window.csrfToken,
},
{
disableAutoLoginRedirect: true,
}
)
}
renameEntity(entity, name, callback) {
if (callback == null) {
callback = function () {}
}
if (entity.name === name) {
return
}
if (name.length >= 150) {
return
}
// check if a doc/file/folder already exists with this name
const parent_folder = this.getCurrentFolder()
if (this.existsInThisFolder(parent_folder, name)) {
return this.nameExistsError()
}
entity.renamingToName = name
return this.ide.$http
.post(
`/project/${this.ide.project_id}/${entity.type}/${entity.id}/rename`,
{
name,
_csrf: window.csrfToken,
}
)
.then(() => (entity.name = name))
.finally(() => (entity.renamingToName = null))
}
deleteEntity(entity, callback) {
// We'll wait for the socket.io notification to
// delete from scope.
if (callback == null) {
callback = function () {}
}
return this.ide.queuedHttp({
method: 'DELETE',
url: `/project/${this.ide.project_id}/${entity.type}/${entity.id}`,
headers: {
'X-Csrf-Token': window.csrfToken,
},
})
}
moveEntity(entity, parent_folder) {
// Abort move if the folder being moved (entity) has the parent_folder as child
// since that would break the tree structure.
if (this._isChildFolder(entity, parent_folder)) {
return
}
// check if a doc/file/folder already exists with this name
if (this.existsInThisFolder(parent_folder, entity.name)) {
throw new Error('file exists in this location')
}
// Wait for the http response before doing the move
this.ide.queuedHttp
.post(
`/project/${this.ide.project_id}/${entity.type}/${entity.id}/move`,
{
folder_id: parent_folder.id,
_csrf: window.csrfToken,
}
)
.then(() => {
this._moveEntityInScope(entity, parent_folder)
})
}
_isChildFolder(parent_folder, child_folder) {
const parent_path = this.getEntityPath(parent_folder) || '' // null if root folder
const child_path = this.getEntityPath(child_folder) || '' // null if root folder
// is parent path the beginning of child path?
return child_path.slice(0, parent_path.length) === parent_path
}
_deleteEntityFromScope(entity, options) {
if (options == null) {
options = { moveToDeleted: true }
}
if (entity == null) {
return
}
let parent_folder = null
this.forEachEntity(function (possible_entity, folder) {
if (possible_entity === entity) {
return (parent_folder = folder)
}
})
if (parent_folder != null) {
const index = parent_folder.children.indexOf(entity)
if (index > -1) {
parent_folder.children.splice(index, 1)
}
}
if (entity.type !== 'folder' && entity.selected) {
this.$scope.ui.view = null
}
if (entity.type === 'doc' && options.moveToDeleted) {
entity.deleted = true
return this.$scope.deletedDocs.push(entity)
}
}
_moveEntityInScope(entity, parent_folder) {
if (Array.from(parent_folder.children).includes(entity)) {
return
}
this._deleteEntityFromScope(entity, { moveToDeleted: false })
return parent_folder.children.push(entity)
}
}
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View file

@ -1,18 +0,0 @@
import App from '../../../base'
App.controller('FileTreeController', [
'$scope',
function ($scope) {
$scope.openNewDocModal = () => {
window.dispatchEvent(
new CustomEvent('file-tree.start-creating', { detail: { mode: 'doc' } })
)
}
$scope.orderByFoldersFirst = function (entity) {
if ((entity != null ? entity.type : undefined) === 'folder') {
return '0'
}
return '1'
}
},
])

View file

@ -1 +0,0 @@
import '../../features/file-view/controllers/file-view-controller'

View file

@ -1,28 +0,0 @@
/* eslint-disable
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let MetadataManager
export default MetadataManager = class MetadataManager {
constructor(ide, $scope, metadata) {
this.ide = ide
this.$scope = $scope
this.metadata = metadata
this.ide.socket.on('broadcastDocMeta', data => {
return this.metadata.onBroadcastDocMeta(data)
})
this.$scope.$on('entity:deleted', this.metadata.onEntityDeleted)
}
loadProjectMetaFromServer() {
return this.metadata.loadProjectMetaFromServer()
}
}

View file

@ -1,129 +0,0 @@
import _ from 'lodash'
/* eslint-disable
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from '../../../base'
export default App.factory('metadata', [
'$http',
'ide',
function ($http, ide) {
const debouncer = {} // DocId => Timeout
const state = { documents: {} }
const metadata = { state }
metadata.onBroadcastDocMeta = function (data) {
if (data.docId != null && data.meta != null) {
state.documents[data.docId] = data.meta
window.dispatchEvent(
new CustomEvent('project:metadata', { detail: state.documents })
)
}
}
metadata.onEntityDeleted = function (e, entity) {
if (entity.type === 'doc') {
return delete state.documents[entity.id]
}
}
metadata.getAllLabels = () =>
_.flattenDeep(
(() => {
const result = []
for (const docId in state.documents) {
const meta = state.documents[docId]
result.push(meta.labels)
}
return result
})()
)
metadata.getAllPackages = function () {
const packageCommandMapping = {}
for (const _docId in state.documents) {
const meta = state.documents[_docId]
for (const packageName in meta.packages) {
const commandSnippets = meta.packages[packageName]
packageCommandMapping[packageName] = commandSnippets
}
}
return packageCommandMapping
}
metadata.loadProjectMetaFromServer = () =>
$http
.get(`/project/${window.project_id}/metadata`)
.then(function (response) {
const { data } = response
if (data.projectMeta) {
return (() => {
const result = []
for (const docId in data.projectMeta) {
const docMeta = data.projectMeta[docId]
result.push((state.documents[docId] = docMeta))
}
window.dispatchEvent(
new CustomEvent('project:metadata', { detail: state.documents })
)
return result
})()
}
})
metadata.loadDocMetaFromServer = docId =>
$http
.post(`/project/${window.project_id}/doc/${docId}/metadata`, {
// Don't broadcast metadata when there are no other users in the
// project.
broadcast: ide.$scope.onlineUsersCount > 0,
_csrf: window.csrfToken,
})
.then(function (response) {
const { data } = response
// handle the POST response like a broadcast event when there are no
// other users in the project.
metadata.onBroadcastDocMeta(data)
})
metadata.scheduleLoadDocMetaFromServer = function (docId) {
if (ide.$scope.permissionsLevel === 'readOnly') {
// The POST request is blocked for users without write permission.
// The user will not be able to consume the meta data for edits anyways.
return
}
// De-bounce loading labels with a timeout
const existingTimeout = debouncer[docId]
if (existingTimeout != null) {
clearTimeout(existingTimeout)
delete debouncer[docId]
}
return (debouncer[docId] = setTimeout(() => {
// TODO: wait for the document to be saved?
metadata.loadDocMetaFromServer(docId)
return delete debouncer[docId]
}, 2000))
}
window.addEventListener('editor:metadata-outdated', () => {
metadata.scheduleLoadDocMetaFromServer(
ide.$scope.editor.sharejs_doc.doc_id
)
})
return metadata
},
])

View file

@ -1,176 +0,0 @@
/* eslint-disable
camelcase,
n/handle-callback-err,
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import ColorManager from '../colors/ColorManager'
let OnlineUsersManager
export default OnlineUsersManager = (function () {
OnlineUsersManager = class OnlineUsersManager {
static initClass() {
this.prototype.cursorUpdateInterval = 500
}
constructor(ide, $scope) {
this.ide = ide
this.$scope = $scope
this.$scope.onlineUsers = {}
this.$scope.onlineUserCursorHighlights = {}
this.$scope.onlineUsersArray = []
this.$scope.onlineUsersCount = 0
this.$scope.$on('cursor:editor:update', (event, position) => {
return this.sendCursorPositionUpdate(position)
})
this.$scope.$on('project:joined', () => {
return this.ide.socket.emit(
'clientTracking.getConnectedUsers',
(error, connectedUsers) => {
this.$scope.onlineUsers = {}
for (const user of Array.from(connectedUsers || [])) {
if (user.client_id === this.ide.socket.publicId) {
// Don't store myself
continue
}
// Store data in the same format returned by clientTracking.clientUpdated
this.$scope.onlineUsers[user.client_id] = {
id: user.client_id,
user_id: user.user_id,
email: user.email,
name: `${user.first_name} ${user.last_name}`,
doc_id:
user.cursorData != null ? user.cursorData.doc_id : undefined,
row: user.cursorData != null ? user.cursorData.row : undefined,
column:
user.cursorData != null ? user.cursorData.column : undefined,
}
}
return this.refreshOnlineUsers()
}
)
})
this.ide.socket.on('clientTracking.clientUpdated', client => {
if (client.id !== this.ide.socket.publicId) {
// Check it's not me!
return this.$scope.$apply(() => {
this.$scope.onlineUsers[client.id] = client
return this.refreshOnlineUsers()
})
}
})
this.ide.socket.on('clientTracking.clientDisconnected', client_id => {
return this.$scope.$apply(() => {
delete this.$scope.onlineUsers[client_id]
return this.refreshOnlineUsers()
})
})
this.$scope.getHueForUserId = user_id => {
return ColorManager.getHueForUserId(user_id)
}
}
refreshOnlineUsers() {
this.$scope.onlineUsersArray = []
for (const client_id in this.$scope.onlineUsers) {
const user = this.$scope.onlineUsers[client_id]
if (user.doc_id != null) {
user.doc = this.ide.fileTreeManager.findEntityById(user.doc_id)
}
// If the user's name is empty use their email as display name
// Otherwise they're probably an anonymous user
if (user.name === null || user.name.trim().length === 0) {
if (user.email) {
user.name = user.email.trim()
} else if (user.user_id === 'anonymous-user') {
user.name = 'Anonymous'
}
}
user.initial = user.name != null ? user.name[0] : undefined
if (!user.initial || user.initial === ' ') {
user.initial = '?'
}
this.$scope.onlineUsersArray.push(user)
}
// keep a count of the other online users
this.$scope.onlineUsersCount = this.$scope.onlineUsersArray.length
this.$scope.onlineUserCursorHighlights = {}
for (const client_id in this.$scope.onlineUsers) {
const client = this.$scope.onlineUsers[client_id]
const { doc_id } = client
if (doc_id == null || client.row == null || client.column == null) {
continue
}
if (!this.$scope.onlineUserCursorHighlights[doc_id]) {
this.$scope.onlineUserCursorHighlights[doc_id] = []
}
this.$scope.onlineUserCursorHighlights[doc_id].push({
label: client.name,
cursor: {
row: client.row,
column: client.column,
},
hue: ColorManager.getHueForUserId(client.user_id),
})
}
if (this.$scope.onlineUsersArray.length > 0) {
delete this.cursorUpdateTimeout
return (this.cursorUpdateInterval = 500)
} else {
delete this.cursorUpdateTimeout
return (this.cursorUpdateInterval = 60 * 1000 * 5)
}
}
sendCursorPositionUpdate(position) {
if (position != null) {
this.$scope.currentPosition = position // keep track of the latest position
}
if (this.cursorUpdateTimeout == null) {
return (this.cursorUpdateTimeout = setTimeout(() => {
const doc_id = this.$scope.editor.open_doc_id
// always send the latest position to other clients
this.ide.socket.emit('clientTracking.updatePosition', {
row:
this.$scope.currentPosition != null
? this.$scope.currentPosition.row
: undefined,
column:
this.$scope.currentPosition != null
? this.$scope.currentPosition.column
: undefined,
doc_id,
})
return delete this.cursorUpdateTimeout
}, this.cursorUpdateInterval))
}
}
}
OnlineUsersManager.initClass()
return OnlineUsersManager
})()

View file

@ -1,49 +0,0 @@
/* eslint-disable
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let PermissionsManager
export default PermissionsManager = class PermissionsManager {
constructor(ide, $scope) {
this.ide = ide
this.$scope = $scope
this.$scope.permissions = {
read: false,
write: false,
admin: false,
comment: false,
}
this.$scope.$watch('permissionsLevel', permissionsLevel => {
if (permissionsLevel != null) {
if (permissionsLevel === 'readOnly') {
this.$scope.permissions.read = true
this.$scope.permissions.write = false
this.$scope.permissions.admin = false
this.$scope.permissions.comment = true
} else if (permissionsLevel === 'readAndWrite') {
this.$scope.permissions.read = true
this.$scope.permissions.write = true
this.$scope.permissions.comment = true
} else if (permissionsLevel === 'owner') {
this.$scope.permissions.read = true
this.$scope.permissions.write = true
this.$scope.permissions.admin = true
this.$scope.permissions.comment = true
}
}
if (this.$scope.anonymous) {
return (this.$scope.permissions.comment = false)
}
})
}
}

View file

@ -1,110 +0,0 @@
import _ from 'lodash'
/* eslint-disable
max-len,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import CryptoJSSHA1 from 'crypto-js/sha1'
let ReferencesManager
export default ReferencesManager = class ReferencesManager {
constructor(ide, $scope) {
this.ide = ide
this.$scope = $scope
this.$scope.$root._references = this.state = { keys: [] }
this.existingIndexHash = {}
this.$scope.$on('document:closed', (e, doc) => {
let entity
if (doc.doc_id) {
entity = this.ide.fileTreeManager.findEntityById(doc.doc_id)
}
if (
__guard__(entity != null ? entity.name : undefined, x =>
x.match(/.*\.bib$/)
)
) {
return this.indexReferencesIfDocModified(doc, true)
}
})
this.$scope.$on('references:should-reindex', (e, data) => {
return this.indexAllReferences(true)
})
// When we join the project:
// index all references files
// and don't broadcast to all clients
this.inited = false
this.$scope.$on('project:joined', e => {
// We only need to grab the references when the editor first loads,
// not on every reconnect
if (!this.inited) {
this.inited = true
this.ide.socket.on('references:keys:updated', (keys, allDocs) =>
this._storeReferencesKeys(keys, allDocs)
)
this.indexAllReferences(false)
}
})
}
_storeReferencesKeys(newKeys, replaceExistingKeys) {
const oldKeys = this.$scope.$root._references.keys
const keys = replaceExistingKeys ? newKeys : _.union(oldKeys, newKeys)
window.dispatchEvent(
new CustomEvent('project:references', {
detail: keys,
})
)
return (this.$scope.$root._references.keys = keys)
}
indexReferencesIfDocModified(doc, shouldBroadcast) {
// avoid reindexing references if the bib file has not changed since the
// last time they were indexed
const docId = doc.doc_id
const snapshot = doc._doc.snapshot
const now = Date.now()
const sha1 = CryptoJSSHA1(
'blob ' + snapshot.length + '\x00' + snapshot
).toString()
const CACHE_LIFETIME = 6 * 3600 * 1000 // allow reindexing every 6 hours
const cacheEntry = this.existingIndexHash[docId]
const isCached =
cacheEntry &&
cacheEntry.timestamp > now - CACHE_LIFETIME &&
cacheEntry.hash === sha1
if (!isCached) {
this.indexAllReferences(shouldBroadcast)
this.existingIndexHash[docId] = { hash: sha1, timestamp: now }
}
}
indexAllReferences(shouldBroadcast) {
const opts = {
shouldBroadcast,
_csrf: window.csrfToken,
}
return this.ide.$http
.post(`/project/${this.$scope.project_id}/references/indexAll`, opts)
.then(response => {
return this._storeReferencesKeys(response.data.keys, true)
})
}
}
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View file

@ -1 +0,0 @@
import './controllers/ReviewPanelController'

View file

@ -1,181 +0,0 @@
/* eslint-disable
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from '../../base'
import EditorWatchdogManager from '../connection/EditorWatchdogManager'
import { debugConsole } from '@/utils/debugging'
// We create and provide this as service so that we can access the global ide
// from within other parts of the angular app.
App.factory('ide', [
'$http',
'queuedHttp',
'$modal',
'$q',
'$filter',
'$timeout',
'eventTracking',
function ($http, queuedHttp, $modal, $q, $filter, $timeout, eventTracking) {
const ide = {}
ide.$http = $http
ide.queuedHttp = queuedHttp
ide.$q = $q
ide.$filter = $filter
ide.$timeout = $timeout
ide.globalEditorWatchdogManager = new EditorWatchdogManager({
onTimeoutHandler: meta => {
eventTracking.sendMB('losing-edits', meta)
// clone the meta object, reportError adds additional fields into it
ide.reportError('losing-edits', Object.assign({}, meta))
},
})
this.recentEvents = []
ide.pushEvent = (type, meta) => {
if (meta == null) {
meta = {}
}
debugConsole.log('event', type, meta)
this.recentEvents.push({ type, meta, date: new Date() })
if (this.recentEvents.length > 100) {
return this.recentEvents.shift()
}
}
ide.reportError = (error, meta) => {
if (meta == null) {
meta = {}
}
meta.user_id = window.user_id
meta.project_id = window.project_id
meta.client_id = __guard__(
ide.socket != null ? ide.socket.socket : undefined,
x => x.sessionid
)
meta.transport = __guard__(
__guard__(
ide.socket != null ? ide.socket.socket : undefined,
x2 => x2.transport
),
x1 => x1.name
)
meta.client_now = new Date()
const errorObj = {}
if (typeof error === 'object') {
for (const key of Array.from(Object.getOwnPropertyNames(error))) {
errorObj[key] = error[key]
}
} else if (typeof error === 'string') {
errorObj.message = error
}
return $http.post('/error/client', {
error: errorObj,
meta,
_csrf: window.csrfToken,
})
}
ide.showGenericMessageModal = (title, message) =>
$modal.open({
templateUrl: 'genericMessageModalTemplate',
controller: 'GenericMessageModalController',
resolve: {
title() {
return title
},
message() {
return message
},
},
})
ide.showOutOfSyncModal = (title, message, editorContent) =>
$modal.open({
templateUrl: 'outOfSyncModalTemplate',
controller: 'OutOfSyncModalController',
backdrop: false, // not dismissable by clicking background
keyboard: false, // prevent dismiss via keyboard
resolve: {
title() {
return title
},
message() {
return message
},
editorContent() {
return editorContent
},
},
windowClass: 'out-of-sync-modal',
})
ide.showLockEditorMessageModal = (title, message) =>
// modal to block the editor when connection is down
$modal.open({
templateUrl: 'lockEditorModalTemplate',
controller: 'GenericMessageModalController',
backdrop: false, // not dismissable by clicking background
keyboard: false, // prevent dismiss via keyboard
resolve: {
title() {
return title
},
message() {
return message
},
},
windowClass: 'lock-editor-modal',
})
return ide
},
])
App.controller('GenericMessageModalController', [
'$scope',
'$modalInstance',
'title',
'message',
function ($scope, $modalInstance, title, message) {
$scope.title = title
$scope.message = message
return ($scope.done = () => $modalInstance.close())
},
])
App.controller('OutOfSyncModalController', [
'$scope',
'$window',
'title',
'message',
'editorContent',
function ($scope, $window, title, message, editorContent) {
$scope.title = title
$scope.message = message
$scope.editorContent = editorContent
$scope.editorContentRows = editorContent.split('\n').length
$scope.done = () => {
// Reload the page to avoid staying in an inconsistent state.
// https://github.com/overleaf/issues/issues/3694
$window.location.reload()
}
},
])
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View file

@ -1,11 +0,0 @@
import App from '../../base'
App.controller('EditorLoaderController', [
'$scope',
'localStorage',
function ($scope, localStorage) {
$scope.$watch('editor.showVisual', function (val) {
localStorage(`editor.lastUsedMode`, val === true ? 'visual' : 'code')
})
},
])

View file

@ -1 +0,0 @@
import './EditorLoaderController'

View file

@ -1,13 +0,0 @@
import 'jquery'
import 'angular'
import 'angular-sanitize'
import 'lodash'
import './vendor/libs/ui-bootstrap'
import './vendor/libs/jquery.storage'
import './vendor/libs/select/select'
// CSS
import 'angular/angular-csp.css'
// Rewrite meta elements
import './utils/meta'

View file

@ -1,32 +0,0 @@
/* eslint-disable
max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import './main/token-access' // used in project/token/access
import './main/event' // used in various controllers
import './main/system-messages' // used in project/editor
import './directives/eventTracking' // used in lots of places
import './features/cookie-banner'
import '../../modules/modules-main'
import { debugConsole } from '@/utils/debugging'
angular.module('OverleafApp').config([
'$locationProvider',
function ($locationProvider) {
try {
return $locationProvider.html5Mode({
enabled: true,
requireBase: false,
rewriteLinks: false,
})
} catch (e) {
debugConsole.error("Error while trying to fix '#' links: ", e)
}
},
])
export default angular.bootstrap(document.body, ['OverleafApp'])

View file

@ -1,129 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import moment from 'moment'
import App from '../base'
import '../modules/localStorage'
import { sendMB } from '../infrastructure/event-tracking'
import { debugConsole } from '@/utils/debugging'
const CACHE_KEY = 'mbEvents'
// keep track of how many heartbeats we've sent so we can calculate how
// long wait until the next one
let heartbeatsSent = 0
let nextHeartbeat = new Date()
App.factory('eventTracking', [
'$http',
'localStorage',
function ($http, localStorage) {
const _getEventCache = function () {
let eventCache = localStorage(CACHE_KEY)
// Initialize as an empy object if the event cache is still empty.
if (eventCache == null) {
eventCache = {}
localStorage(CACHE_KEY, eventCache)
}
return eventCache
}
const _eventInCache = function (key) {
const curCache = _getEventCache()
return curCache[key] || false
}
const _addEventToCache = function (key) {
const curCache = _getEventCache()
curCache[key] = true
return localStorage(CACHE_KEY, curCache)
}
const _sendEditingSessionHeartbeat = segmentation =>
$http({
url: `/editingSession/${window.project_id}`,
method: 'PUT',
data: { segmentation },
headers: {
'X-CSRF-Token': window.csrfToken,
},
})
return {
send(category, action, label, value) {
return ga('send', 'event', category, action, label, value)
},
sendGAOnce(category, action, label, value) {
if (!_eventInCache(action)) {
_addEventToCache(action)
return this.send(category, action, label, value)
}
},
editingSessionHeartbeat(segmentationCb = () => {}) {
debugConsole.log('[Event] heartbeat trigger')
// If the next heartbeat is in the future, stop
if (nextHeartbeat > new Date()) return
const segmentation = segmentationCb()
debugConsole.log('[Event] send heartbeat request', segmentation)
_sendEditingSessionHeartbeat(segmentation)
heartbeatsSent++
// send two first heartbeats at 0 and 30s then increase the backoff time
// 1min per call until we reach 5 min
const backoffSecs =
heartbeatsSent <= 2
? 30
: heartbeatsSent <= 6
? (heartbeatsSent - 2) * 60
: 300
nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate()
},
sendMB,
sendMBSampled(key, segmentation, rate = 0.01) {
if (Math.random() < rate) {
this.sendMB(key, segmentation)
}
},
sendMBOnce(key, segmentation) {
if (!_eventInCache(key)) {
_addEventToCache(key)
this.sendMB(key, segmentation)
}
},
eventInCache(key) {
return _eventInCache(key)
},
}
},
])
export default $('.navbar a').on('click', function (e) {
const href = $(e.target).attr('href')
if (href != null) {
return ga('send', 'event', 'navigation', 'top menu bar', href)
}
})

View file

@ -1,21 +0,0 @@
import App from '../base'
App.controller('ImportingController', [
'$interval',
'$scope',
'$timeout',
'$window',
function ($interval, $scope, $timeout, $window) {
$interval(function () {
$scope.state.load_progress += 5
if ($scope.state.load_progress > 100) {
$scope.state.load_progress = 20
}
}, 500)
$timeout(function () {
$window.location.reload()
}, 5000)
$scope.state = {
load_progress: 20,
}
},
])

View file

@ -1,63 +0,0 @@
/* eslint-disable
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from '../base'
const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000
// Controller for messages (array)
App.controller('SystemMessagesController', [
'$http',
'$scope',
function ($http, $scope) {
$scope.messages = []
function pollSystemMessages() {
// Ignore polling if tab is hidden or browser is offline
if (document.hidden || !navigator.onLine) {
return
}
$http
.get('/system/messages')
.then(response => {
// Ignore if content-type is anything but JSON, prevents a bug where
// the user logs out in another tab, then a 302 redirect was returned,
// which is transparently resolved by the browser to the login (HTML)
// page.
// This then caused an Angular error where it was attempting to loop
// through the HTML as a string
if (response.headers('content-type').includes('json')) {
$scope.messages = response.data
}
})
.catch(() => {
// ignore errors
})
}
pollSystemMessages()
setInterval(pollSystemMessages, MESSAGE_POLL_INTERVAL)
},
])
export default App.controller('SystemMessageController', [
'$scope',
function ($scope) {
$scope.hidden = $.localStorage(`systemMessage.hide.${$scope.message._id}`)
$scope.protected = $scope.message._id === 'protected'
$scope.htmlContent = $scope.message.content
return ($scope.hide = function () {
if (!$scope.protected) {
// do not allow protected messages to be hidden
$scope.hidden = true
return $.localStorage(`systemMessage.hide.${$scope.message._id}`, true)
}
})
},
])

View file

@ -1,92 +0,0 @@
import App from '../base'
import { debugConsole } from '@/utils/debugging'
App.controller('TokenAccessPageController', [
'$scope',
'$http',
'$location',
function ($scope, $http, $location) {
window.S = $scope
$scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import' | 'requireAccept'
$scope.v1ImportData = null
$scope.requireAccept = null
$scope.accessInFlight = false
$scope.accessSuccess = false
$scope.accessError = false
$scope.currentPath = () => {
return $location.path()
}
$scope.buildZipDownloadPath = projectId => {
return `/overleaf/project/${projectId}/download/zip`
}
$scope.getProjectName = () => {
if ($scope.v1ImportData?.name) {
return $scope.v1ImportData.name
} else if ($scope.requireAccept?.projectName) {
return $scope.requireAccept.projectName
} else {
return 'This project'
}
}
$scope.postConfirmedByUser = () => {
$scope.post(true)
}
$scope.post = (confirmedByUser = false) => {
$scope.mode = 'accessAttempt'
const textData = $('#overleaf-token-access-data').text()
const parsedData = JSON.parse(textData)
const { postUrl, csrfToken } = parsedData
$scope.accessInFlight = true
$http({
method: 'POST',
url: postUrl,
data: {
_csrf: csrfToken,
confirmedByUser,
tokenHashPrefix: window.location.hash,
},
}).then(
function successCallback(response) {
$scope.accessInFlight = false
$scope.accessError = false
const { data } = response
if (data.redirect) {
const redirect = response.data.redirect
if (!redirect) {
debugConsole.warn(
'no redirect supplied in success response data',
response
)
$scope.accessError = true
return
}
window.location.replace(redirect)
} else if (data.v1Import) {
$scope.mode = 'v1Import'
$scope.v1ImportData = data.v1Import
} else if (data.requireAccept) {
$scope.mode = 'requireAccept'
$scope.requireAccept = data.requireAccept
} else {
debugConsole.warn(
'invalid data from server in success response',
response
)
$scope.accessError = true
}
},
function errorCallback(response) {
debugConsole.warn('error response from server', response)
$scope.accessInFlight = false
$scope.accessError = response.status === 404 ? 'not_found' : 'error'
}
)
}
},
])

Some files were not shown because too many files have changed in this diff Show more