Просмотр исходного кода

Merge pull request #1925 from overleaf/as-babel-plugin-angular-annotate

Enable Angular "annotations" via Babel plugin

GitOrigin-RevId: 343610025e3ba791fb284a0b696a1ea33e125a80
Eric Mc Sween 7 лет назад
Родитель
Сommit
65849456d0
43 измененных файлов с 4455 добавлено и 4564 удалено
  1. 2 1
      services/web/.babelrc
  2. 0 3
      services/web/Gruntfile.js
  3. 201 103
      services/web/npm-shrinkwrap.json
  4. 1 0
      services/web/package.json
  5. 12 14
      services/web/public/src/directives/equals.js
  6. 56 61
      services/web/public/src/directives/eventTracking.js
  7. 172 176
      services/web/public/src/ide/binary-files/controllers/BinaryFileController.js
  8. 39 43
      services/web/public/src/ide/chat/controllers/ChatMessageController.js
  9. 222 227
      services/web/public/src/ide/directives/layout.js
  10. 61 66
      services/web/public/src/ide/editor/controllers/SavingNotificationController.js
  11. 547 568
      services/web/public/src/ide/file-tree/controllers/FileTreeController.js
  12. 91 97
      services/web/public/src/ide/file-tree/controllers/FileTreeEntityController.js
  13. 19 23
      services/web/public/src/ide/file-tree/controllers/FileTreeRootFolderController.js
  14. 29 32
      services/web/public/src/ide/file-tree/directives/fileEntity.js
  15. 161 167
      services/web/public/src/ide/history/controllers/HistoryListController.js
  16. 34 35
      services/web/public/src/ide/history/controllers/HistoryV2AddLabelModalController.js
  17. 30 31
      services/web/public/src/ide/history/controllers/HistoryV2DeleteLabelModalController.js
  18. 4 8
      services/web/public/src/ide/history/controllers/HistoryV2FileTreeController.js
  19. 30 38
      services/web/public/src/ide/history/controllers/HistoryV2ListController.js
  20. 3 7
      services/web/public/src/ide/history/controllers/HistoryV2ToolbarController.js
  21. 166 180
      services/web/public/src/ide/pdf/controllers/PdfController.js
  22. 32 34
      services/web/public/src/ide/pdfng/directives/pdfHighlights.js
  23. 121 127
      services/web/public/src/ide/pdfng/directives/pdfJs.js
  24. 127 132
      services/web/public/src/ide/pdfng/directives/pdfPage.js
  25. 430 439
      services/web/public/src/ide/pdfng/directives/pdfRenderer.js
  26. 26 28
      services/web/public/src/ide/pdfng/directives/pdfSpinner.js
  27. 50 52
      services/web/public/src/ide/pdfng/directives/pdfTextLayer.js
  28. 728 744
      services/web/public/src/ide/pdfng/directives/pdfViewer.js
  29. 92 94
      services/web/public/src/ide/services/ide.js
  30. 56 57
      services/web/public/src/ide/settings/controllers/ProjectNameController.js
  31. 162 174
      services/web/public/src/ide/settings/controllers/SettingsController.js
  32. 44 48
      services/web/public/src/ide/settings/services/settings.js
  33. 52 60
      services/web/public/src/ide/share/controllers/ShareController.js
  34. 35 39
      services/web/public/src/ide/share/services/projectInvites.js
  35. 26 30
      services/web/public/src/ide/share/services/projectMembers.js
  36. 104 113
      services/web/public/src/main/account-settings.js
  37. 209 210
      services/web/public/src/main/affiliations/controllers/UserAffiliationsController.js
  38. 120 126
      services/web/public/src/main/affiliations/factories/UserAffiliationsDataService.js
  39. 20 24
      services/web/public/src/main/clear-sessions.js
  40. 83 84
      services/web/public/src/main/oauth/controllers/UserOauthController.js
  41. 12 15
      services/web/public/src/main/oauth/factories/UserOauthDataService.js
  42. 24 26
      services/web/public/src/main/project-list/services/project-list.js
  43. 22 28
      services/web/public/src/modules/errorCatcher.js

+ 2 - 1
services/web/.babelrc

@@ -2,5 +2,6 @@
   "presets": [
     "react",
     ["env", { "modules": false }]
-  ]
+  ],
+  "plugins": ["angularjs-annotate"]
 }

+ 0 - 3
services/web/Gruntfile.js

@@ -23,9 +23,6 @@ module.exports = function(grunt) {
       compile: {
         options: {
           optimize: 'uglify2',
-          uglify2: {
-            mangle: false
-          },
           appDir: 'public/js',
           baseUrl: './',
           dir: 'public/minjs',

Разница между файлами не показана из-за своего большого размера
+ 201 - 103
services/web/npm-shrinkwrap.json


+ 1 - 0
services/web/package.json

@@ -119,6 +119,7 @@
     "babel-cli": "^6.26.0",
     "babel-core": "^6.26.0",
     "babel-loader": "^7.1.2",
+    "babel-plugin-angularjs-annotate": "^0.10.0",
     "babel-preset-env": "^1.6.1",
     "babel-preset-react": "^6.16.0",
     "bunyan": "0.22.1",

+ 12 - 14
services/web/public/src/directives/equals.js

@@ -9,17 +9,15 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.directive('equals', [
-    () => ({
-      require: 'ngModel',
-      link(scope, elem, attrs, ctrl) {
-        const firstField = `#${attrs.equals}`
-        return elem.add(firstField).on('keyup', () =>
-          scope.$apply(function() {
-            const equal = elem.val() === $(firstField).val()
-            return ctrl.$setValidity('areEqual', equal)
-          })
-        )
-      }
-    })
-  ]))
+  App.directive('equals', () => ({
+    require: 'ngModel',
+    link(scope, elem, attrs, ctrl) {
+      const firstField = `#${attrs.equals}`
+      return elem.add(firstField).on('keyup', () =>
+        scope.$apply(function() {
+          const equal = elem.val() === $(firstField).val()
+          return ctrl.$setValidity('areEqual', equal)
+        })
+      )
+    }
+  })))

+ 56 - 61
services/web/public/src/directives/eventTracking.js

@@ -35,72 +35,67 @@ const isInViewport = function(element) {
 }
 
 define(['base'], App =>
-  App.directive('eventTracking', [
-    'event_tracking',
-    event_tracking => ({
-      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
+  App.directive('eventTracking', event_tracking => ({
+    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) {
-          /*
+      const sendEvent = function(scrollEvent) {
+        /*
 						@param {boolean}		scrollEvent		Use to unbind scroll event
 					*/
-          if (sendMB) {
-            event_tracking[sendMBFunction](scope.eventTracking, segmentation)
-          }
-          if (sendGA) {
-            event_tracking[sendGAFunction](
-              attrs.eventTrackingGa,
-              attrs.eventTrackingAction || scope.eventTracking,
-              attrs.eventTrackingLabel || ''
-            )
-          }
-          if (scrollEvent) {
-            return $(window).unbind('resize scroll')
-          }
+        if (sendMB) {
+          event_tracking[sendMBFunction](scope.eventTracking, segmentation)
+        }
+        if (sendGA) {
+          event_tracking[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') {
-          if (!event_tracking.eventInCache(scope.eventTracking)) {
-            return $(window).on('resize scroll', () =>
-              _.throttle(
-                isInViewport(element) &&
-                !event_tracking.eventInCache(scope.eventTracking)
-                  ? sendEvent(true)
-                  : undefined,
-                500
-              )
+      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') {
+        if (!event_tracking.eventInCache(scope.eventTracking)) {
+          return $(window).on('resize scroll', () =>
+            _.throttle(
+              isInViewport(element) &&
+              !event_tracking.eventInCache(scope.eventTracking)
+                ? sendEvent(true)
+                : undefined,
+              500
             )
-          }
+          )
         }
       }
-    })
-  ]))
+    }
+  })))

+ 172 - 176
services/web/public/src/ide/binary-files/controllers/BinaryFileController.js

@@ -1,197 +1,193 @@
 define(['base', 'moment'], (App, moment) =>
-  App.controller('BinaryFileController', [
-    '$scope',
-    '$rootScope',
-    '$http',
-    '$timeout',
-    '$element',
-    'ide',
-    'waitFor',
-    function($scope, $rootScope, $http, $timeout, $element, ide, waitFor) {
-      const MAX_FILE_SIZE = 2 * 1024 * 1024
-      const MAX_URL_LENGTH = 60
-      const FRONT_OF_URL_LENGTH = 35
-      const FILLER = '...'
-      const TAIL_OF_URL_LENGTH =
-        MAX_URL_LENGTH - FRONT_OF_URL_LENGTH - FILLER.length
-
-      const textExtensions = ['bib', 'tex', 'txt', 'cls', 'sty']
-      const imageExtensions = ['png', 'jpg', 'jpeg', 'gif']
-      const previewableExtensions = []
-
-      const extension = file =>
-        file.name
-          .split('.')
-          .pop()
-          .toLowerCase()
-
-      $scope.isTextFile = () =>
-        textExtensions.indexOf(extension($scope.openFile)) > -1
-      $scope.isImageFile = () =>
-        imageExtensions.indexOf(extension($scope.openFile)) > -1
-      $scope.isPreviewableFile = () =>
-        previewableExtensions.indexOf(extension($scope.openFile)) > -1
-      $scope.isUnpreviewableFile = () =>
-        !$scope.isTextFile() &&
-        !$scope.isImageFile() &&
-        !$scope.isPreviewableFile()
-
-      $scope.textPreview = {
-        loading: false,
-        shouldShowDots: false,
-        error: false,
-        data: null
-      }
+  App.controller('BinaryFileController', function(
+    $scope,
+    $rootScope,
+    $http,
+    $timeout,
+    $element,
+    ide,
+    waitFor
+  ) {
+    const MAX_FILE_SIZE = 2 * 1024 * 1024
+    const MAX_URL_LENGTH = 60
+    const FRONT_OF_URL_LENGTH = 35
+    const FILLER = '...'
+    const TAIL_OF_URL_LENGTH =
+      MAX_URL_LENGTH - FRONT_OF_URL_LENGTH - FILLER.length
+
+    const textExtensions = ['bib', 'tex', 'txt', 'cls', 'sty']
+    const imageExtensions = ['png', 'jpg', 'jpeg', 'gif']
+    const previewableExtensions = []
+
+    const extension = file =>
+      file.name
+        .split('.')
+        .pop()
+        .toLowerCase()
+
+    $scope.isTextFile = () =>
+      textExtensions.indexOf(extension($scope.openFile)) > -1
+    $scope.isImageFile = () =>
+      imageExtensions.indexOf(extension($scope.openFile)) > -1
+    $scope.isPreviewableFile = () =>
+      previewableExtensions.indexOf(extension($scope.openFile)) > -1
+    $scope.isUnpreviewableFile = () =>
+      !$scope.isTextFile() &&
+      !$scope.isImageFile() &&
+      !$scope.isPreviewableFile()
+
+    $scope.textPreview = {
+      loading: false,
+      shouldShowDots: false,
+      error: false,
+      data: null
+    }
 
-      $scope.refreshing = false
-      $scope.refreshError = null
+    $scope.refreshing = false
+    $scope.refreshError = null
 
-      $scope.displayUrl = function(url) {
-        if (url == null) {
-          return
-        }
-        if (url.length > MAX_URL_LENGTH) {
-          const front = url.slice(0, FRONT_OF_URL_LENGTH)
-          const tail = url.slice(url.length - TAIL_OF_URL_LENGTH)
-          return front + FILLER + tail
-        }
-        return url
+    $scope.displayUrl = function(url) {
+      if (url == null) {
+        return
       }
-
-      $scope.refreshFile = function(file) {
-        $scope.refreshing = true
-        $scope.refreshError = null
-        ide.fileTreeManager
-          .refreshLinkedFile(file)
-          .then(function(response) {
-            const { data } = response
-            const newFileId = data.new_file_id
-            $timeout(
-              () =>
-                waitFor(
-                  () => ide.fileTreeManager.findEntityById(newFileId),
-                  5000
-                )
-                  .then(newFile => ide.binaryFilesManager.openFile(newFile))
-                  .catch(err => console.warn(err)),
-
-              0
-            )
-            $scope.refreshError = null
-          })
-          .catch(response => ($scope.refreshError = response.data))
-          .finally(() => {
-            $scope.refreshing = false
-            const provider = file.linkedFileData.provider
-            if (
-              provider === 'mendeley' ||
-              provider === 'zotero' ||
-              file.name.match(/^.*\.bib$/)
-            ) {
-              ide.$scope.$emit('references:should-reindex', {})
-            }
-          })
+      if (url.length > MAX_URL_LENGTH) {
+        const front = url.slice(0, FRONT_OF_URL_LENGTH)
+        const tail = url.slice(url.length - TAIL_OF_URL_LENGTH)
+        return front + FILLER + tail
       }
+      return url
+    }
 
-      // Callback fired when the `img` tag fails to load,
-      // `failedLoad` used to show the "No Preview" message
-      $scope.failedLoad = false
-      window.sl_binaryFilePreviewError = () => {
-        $scope.failedLoad = true
-        $scope.$apply()
-      }
+    $scope.refreshFile = function(file) {
+      $scope.refreshing = true
+      $scope.refreshError = null
+      ide.fileTreeManager
+        .refreshLinkedFile(file)
+        .then(function(response) {
+          const { data } = response
+          const newFileId = data.new_file_id
+          $timeout(
+            () =>
+              waitFor(() => ide.fileTreeManager.findEntityById(newFileId), 5000)
+                .then(newFile => ide.binaryFilesManager.openFile(newFile))
+                .catch(err => console.warn(err)),
+
+            0
+          )
+          $scope.refreshError = null
+        })
+        .catch(response => ($scope.refreshError = response.data))
+        .finally(() => {
+          $scope.refreshing = false
+          const provider = file.linkedFileData.provider
+          if (
+            provider === 'mendeley' ||
+            provider === 'zotero' ||
+            file.name.match(/^.*\.bib$/)
+          ) {
+            ide.$scope.$emit('references:should-reindex', {})
+          }
+        })
+    }
 
-      // Callback fired when the `img` tag is done loading,
-      // `imgLoaded` is used to show the spinner gif while loading
-      $scope.imgLoaded = false
-      window.sl_binaryFilePreviewLoaded = () => {
-        $scope.imgLoaded = true
-        $scope.$apply()
-      }
+    // Callback fired when the `img` tag fails to load,
+    // `failedLoad` used to show the "No Preview" message
+    $scope.failedLoad = false
+    window.sl_binaryFilePreviewError = () => {
+      $scope.failedLoad = true
+      $scope.$apply()
+    }
 
-      if ($scope.isTextFile()) {
-        loadTextFilePreview()
-      }
+    // Callback fired when the `img` tag is done loading,
+    // `imgLoaded` is used to show the spinner gif while loading
+    $scope.imgLoaded = false
+    window.sl_binaryFilePreviewLoaded = () => {
+      $scope.imgLoaded = true
+      $scope.$apply()
+    }
 
-      function loadTextFilePreview() {
-        const url = `/project/${window.project_id}/file/${$scope.openFile.id}`
-        let truncated = false
-        displayPreviewLoading()
-        getFileSize(url)
-          .then(fileSize => {
-            const opts = {}
-            if (fileSize > MAX_FILE_SIZE) {
-              truncated = true
-              opts.maxSize = MAX_FILE_SIZE
-            }
-            return getFileContents(url, opts)
-          })
-          .then(contents => {
-            const displayedContents = truncated
-              ? truncateFileContents(contents)
-              : contents
-            displayPreview(displayedContents, truncated)
-          })
-          .catch(err => {
-            console.error(err)
-            displayPreviewError()
-          })
-      }
+    if ($scope.isTextFile()) {
+      loadTextFilePreview()
+    }
 
-      function getFileSize(url) {
-        return $http.head(url).then(response => {
-          const size = parseInt(response.headers('Content-Length'), 10)
-          if (isNaN(size)) {
-            throw new Error('Could not parse Content-Length header')
+    function loadTextFilePreview() {
+      const url = `/project/${window.project_id}/file/${$scope.openFile.id}`
+      let truncated = false
+      displayPreviewLoading()
+      getFileSize(url)
+        .then(fileSize => {
+          const opts = {}
+          if (fileSize > MAX_FILE_SIZE) {
+            truncated = true
+            opts.maxSize = MAX_FILE_SIZE
           }
-          return size
+          return getFileContents(url, opts)
         })
-      }
+        .then(contents => {
+          const displayedContents = truncated
+            ? truncateFileContents(contents)
+            : contents
+          displayPreview(displayedContents, truncated)
+        })
+        .catch(err => {
+          console.error(err)
+          displayPreviewError()
+        })
+    }
 
-      function getFileContents(url, opts = {}) {
-        const { maxSize } = opts
-        if (maxSize != null) {
-          url += `?range=0-${maxSize}`
+    function getFileSize(url) {
+      return $http.head(url).then(response => {
+        const size = parseInt(response.headers('Content-Length'), 10)
+        if (isNaN(size)) {
+          throw new Error('Could not parse Content-Length header')
         }
-        return $http
-          .get(url, {
-            transformResponse: null // Don't parse JSON
-          })
-          .then(response => {
-            return response.data
-          })
-      }
+        return size
+      })
+    }
 
-      function truncateFileContents(contents) {
-        return contents.replace(/\n.*$/, '')
+    function getFileContents(url, opts = {}) {
+      const { maxSize } = opts
+      if (maxSize != null) {
+        url += `?range=0-${maxSize}`
       }
+      return $http
+        .get(url, {
+          transformResponse: null // Don't parse JSON
+        })
+        .then(response => {
+          return response.data
+        })
+    }
 
-      function displayPreviewLoading() {
-        $scope.textPreview.data = null
-        $scope.textPreview.loading = true
-        $scope.textPreview.shouldShowDots = false
-        $scope.$apply()
-      }
+    function truncateFileContents(contents) {
+      return contents.replace(/\n.*$/, '')
+    }
 
-      function displayPreview(contents, truncated) {
-        $scope.textPreview.error = false
-        $scope.textPreview.data = contents
-        $scope.textPreview.shouldShowDots = truncated
-        $timeout(setPreviewHeight, 0)
-      }
+    function displayPreviewLoading() {
+      $scope.textPreview.data = null
+      $scope.textPreview.loading = true
+      $scope.textPreview.shouldShowDots = false
+      $scope.$apply()
+    }
 
-      function displayPreviewError() {
-        $scope.textPreview.error = true
-        $scope.textPreview.loading = false
-      }
+    function displayPreview(contents, truncated) {
+      $scope.textPreview.error = false
+      $scope.textPreview.data = contents
+      $scope.textPreview.shouldShowDots = truncated
+      $timeout(setPreviewHeight, 0)
+    }
 
-      function setPreviewHeight() {
-        const $preview = $element.find('.text-preview .scroll-container')
-        const $footer = $element.find('.binary-file-footer')
-        const maxHeight = $element.height() - $footer.height() - 14 // borders + margin
-        $preview.css({ 'max-height': maxHeight })
-        // Don't show the preview until we've set the height, otherwise we jump around
-        $scope.textPreview.loading = false
-      }
+    function displayPreviewError() {
+      $scope.textPreview.error = true
+      $scope.textPreview.loading = false
+    }
+
+    function setPreviewHeight() {
+      const $preview = $element.find('.text-preview .scroll-container')
+      const $footer = $element.find('.binary-file-footer')
+      const maxHeight = $element.height() - $footer.height() - 14 // borders + margin
+      $preview.css({ 'max-height': maxHeight })
+      // Don't show the preview until we've set the height, otherwise we jump around
+      $scope.textPreview.loading = false
     }
-  ]))
+  }))

+ 39 - 43
services/web/public/src/ide/chat/controllers/ChatMessageController.js

@@ -12,50 +12,46 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base', 'ide/colors/ColorManager'], (App, ColorManager) =>
-  App.controller('ChatMessageController', [
-    '$scope',
-    'ide',
-    function($scope, ide) {
-      const hslColorConfigs = {
-        borderSaturation:
-          (window.uiConfig != null
-            ? window.uiConfig.chatMessageBorderSaturation
-            : undefined) || '70%',
-        borderLightness:
-          (window.uiConfig != null
-            ? window.uiConfig.chatMessageBorderLightness
-            : undefined) || '70%',
-        bgSaturation:
-          (window.uiConfig != null
-            ? window.uiConfig.chatMessageBgSaturation
-            : undefined) || '60%',
-        bgLightness:
-          (window.uiConfig != null
-            ? window.uiConfig.chatMessageBgLightness
-            : undefined) || '97%'
-      }
+  App.controller('ChatMessageController', function($scope, ide) {
+    const hslColorConfigs = {
+      borderSaturation:
+        (window.uiConfig != null
+          ? window.uiConfig.chatMessageBorderSaturation
+          : undefined) || '70%',
+      borderLightness:
+        (window.uiConfig != null
+          ? window.uiConfig.chatMessageBorderLightness
+          : undefined) || '70%',
+      bgSaturation:
+        (window.uiConfig != null
+          ? window.uiConfig.chatMessageBgSaturation
+          : undefined) || '60%',
+      bgLightness:
+        (window.uiConfig != null
+          ? window.uiConfig.chatMessageBgLightness
+          : undefined) || '97%'
+    }
 
-      const hue = function(user) {
-        if (user == null) {
-          return 0
-        } else {
-          return ColorManager.getHueForUserId(user.id)
-        }
+    const hue = function(user) {
+      if (user == null) {
+        return 0
+      } else {
+        return ColorManager.getHueForUserId(user.id)
       }
+    }
 
-      $scope.getMessageStyle = user => ({
-        'border-color': `hsl(${hue(user)}, ${
-          hslColorConfigs.borderSaturation
-        }, ${hslColorConfigs.borderLightness})`,
-        'background-color': `hsl(${hue(user)}, ${
-          hslColorConfigs.bgSaturation
-        }, ${hslColorConfigs.bgLightness})`
-      })
+    $scope.getMessageStyle = user => ({
+      'border-color': `hsl(${hue(user)}, ${hslColorConfigs.borderSaturation}, ${
+        hslColorConfigs.borderLightness
+      })`,
+      'background-color': `hsl(${hue(user)}, ${hslColorConfigs.bgSaturation}, ${
+        hslColorConfigs.bgLightness
+      })`
+    })
 
-      return ($scope.getArrowStyle = user => ({
-        'border-color': `hsl(${hue(user)}, ${
-          hslColorConfigs.borderSaturation
-        }, ${hslColorConfigs.borderLightness})`
-      }))
-    }
-  ]))
+    return ($scope.getArrowStyle = user => ({
+      'border-color': `hsl(${hue(user)}, ${hslColorConfigs.borderSaturation}, ${
+        hslColorConfigs.borderLightness
+      })`
+    }))
+  }))

+ 222 - 227
services/web/public/src/ide/directives/layout.js

@@ -14,198 +14,194 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base', 'libs/jquery-layout', 'libs/jquery.ui.touch-punch'], App =>
-  App.directive('layout', [
-    '$parse',
-    '$compile',
-    'ide',
-    ($parse, $compile, ide) => ({
-      compile() {
-        return {
-          pre(scope, element, attrs) {
-            let customTogglerEl, spacingClosed, spacingOpen, state
-            const name = attrs.layout
+  App.directive('layout', ($parse, $compile, ide) => ({
+    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
+          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 = window.uiConfig.defaultResizerSizeOpen
-            }
+          if (attrs.spacingOpen != null) {
+            spacingOpen = parseInt(attrs.spacingOpen, 10)
+          } else {
+            spacingOpen = window.uiConfig.defaultResizerSizeOpen
+          }
 
-            if (attrs.spacingClosed != null) {
-              spacingClosed = parseInt(attrs.spacingClosed, 10)
-            } else {
-              spacingClosed = window.uiConfig.defaultResizerSizeClosed
-            }
+          if (attrs.spacingClosed != null) {
+            spacingClosed = parseInt(attrs.spacingClosed, 10)
+          } else {
+            spacingClosed = window.uiConfig.defaultResizerSizeClosed
+          }
 
-            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.initialSizeEast),
-                initClosed: scope.$eval(attrs.initClosedWest)
-              }
+          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.initialSizeEast),
+              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
-                }
+          // 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
               }
-              if (state.west != null) {
-                if (
-                  attrs.minimumRestoreSizeWest == null ||
-                  (state.west.size >= attrs.minimumRestoreSizeWest &&
-                    !state.west.initClosed)
-                ) {
-                  options.west = state.west
-                }
+            }
+            if (state.west != null) {
+              if (
+                attrs.minimumRestoreSizeWest == null ||
+                (state.west.size >= attrs.minimumRestoreSizeWest &&
+                  !state.west.initClosed)
+              ) {
+                options.west = state.west
               }
             }
+          }
 
-            if (window.uiConfig.eastResizerCursor != null) {
-              options.east.resizerCursor = window.uiConfig.eastResizerCursor
-            }
+          if (window.uiConfig.eastResizerCursor != null) {
+            options.east.resizerCursor = window.uiConfig.eastResizerCursor
+          }
 
-            if (window.uiConfig.westResizerCursor != null) {
-              options.west.resizerCursor = window.uiConfig.westResizerCursor
-            }
+          if (window.uiConfig.westResizerCursor != null) {
+            options.west.resizerCursor = window.uiConfig.westResizerCursor
+          }
 
-            const repositionControls = function() {
-              state = element.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
-                  })
-                }
+          const repositionControls = function() {
+            state = element.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
+                })
               }
             }
+          }
 
-            const repositionCustomToggler = function() {
-              if (customTogglerEl == null) {
-                return
-              }
-              state = element.layout().readState()
-              const positionAnchor =
-                customTogglerPane === 'east' ? 'right' : 'left'
-              const paneState = state[customTogglerPane]
-              if (paneState != null) {
-                return customTogglerEl.css(
-                  positionAnchor,
-                  paneState.initClosed ? 0 : paneState.size
-                )
-              }
+          const repositionCustomToggler = function() {
+            if (customTogglerEl == null) {
+              return
             }
+            state = element.layout().readState()
+            const positionAnchor =
+              customTogglerPane === 'east' ? 'right' : 'left'
+            const paneState = state[customTogglerPane]
+            if (paneState != null) {
+              return customTogglerEl.css(
+                positionAnchor,
+                paneState.initClosed ? 0 : paneState.size
+              )
+            }
+          }
 
-            const resetOpenStates = function() {
-              state = element.layout().readState()
-              if (attrs.openEast != null && state.east != null) {
-                const openEast = $parse(attrs.openEast)
-                return openEast.assign(scope, !state.east.initClosed)
-              }
+          const resetOpenStates = function() {
+            state = element.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
-            var onInternalResize = function() {
-              state = element.layout().readState()
-              scope.$broadcast(`layout:${name}:resize`, state)
-              repositionControls()
-              if (hasCustomToggler) {
-                repositionCustomToggler()
-              }
-              return resetOpenStates()
+          // Someone moved the resizer
+          var onInternalResize = function() {
+            state = element.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 = element.layout().readState().east
-                if (eastState != null) {
-                  const newInternalWidth =
-                    (eastState.size / oldWidth) * element.width()
-                  oldWidth = element.width()
-                  element.layout().sizePane('east', newInternalWidth)
-                  return
-                }
+          let oldWidth = element.width()
+          // Something resized our parent element
+          const onExternalResize = function() {
+            if (
+              attrs.resizeProportionally != null &&
+              scope.$eval(attrs.resizeProportionally)
+            ) {
+              const eastState = element.layout().readState().east
+              if (eastState != null) {
+                const newInternalWidth =
+                  (eastState.size / oldWidth) * element.width()
+                oldWidth = element.width()
+                element.layout().sizePane('east', newInternalWidth)
+                return
               }
-
-              return element.layout().resizeAll()
             }
 
-            element.layout(options)
-            element.layout().resizeAll()
+            return element.layout().resizeAll()
+          }
 
-            if (attrs.resizeOn != null) {
-              for (let event of Array.from(attrs.resizeOn.split(','))) {
-                scope.$on(event, () => onExternalResize())
-              }
+          element.layout(options)
+          element.layout().resizeAll()
+
+          if (attrs.resizeOn != null) {
+            for (let event of Array.from(attrs.resizeOn.split(','))) {
+              scope.$on(event, () => onExternalResize())
             }
+          }
 
-            if (hasCustomToggler) {
-              state = element.layout().readState()
-              const customTogglerScope = scope.$new()
+          if (hasCustomToggler) {
+            state = element.layout().readState()
+            const customTogglerScope = scope.$new()
 
-              customTogglerScope.isOpen = true
-              customTogglerScope.isVisible = true
+            customTogglerScope.isOpen = true
+            customTogglerScope.isVisible = true
 
-              if (
-                (state[customTogglerPane] != null
-                  ? state[customTogglerPane].initClosed
-                  : undefined) === true
-              ) {
-                customTogglerScope.isOpen = false
-              }
+            if (
+              (state[customTogglerPane] != null
+                ? state[customTogglerPane].initClosed
+                : undefined) === true
+            ) {
+              customTogglerScope.isOpen = false
+            }
 
-              customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
-              customTogglerScope.tooltipMsgWhenClosed = customTogglerMsgWhenClosed
+            customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
+            customTogglerScope.tooltipMsgWhenClosed = customTogglerMsgWhenClosed
 
-              customTogglerScope.tooltipPlacement =
-                customTogglerPane === 'east' ? 'left' : 'right'
-              customTogglerScope.handleClick = function() {
-                element.layout().toggle(customTogglerPane)
-                return repositionCustomToggler()
-              }
-              customTogglerEl = $compile(`\
+            customTogglerScope.tooltipPlacement =
+              customTogglerPane === 'east' ? 'left' : 'right'
+            customTogglerScope.handleClick = function() {
+              element.layout().toggle(customTogglerPane)
+              return repositionCustomToggler()
+            }
+            customTogglerEl = $compile(`\
 <a href \
 ng-show=\"isVisible\" \
 class=\"custom-toggler ${`custom-toggler-${customTogglerPane}`}\" \
@@ -214,82 +210,81 @@ tooltip=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\" \
 tooltip-placement=\"{{ tooltipPlacement }}\" \
 ng-click=\"handleClick()\">\
 `)(customTogglerScope)
-              element.append(customTogglerEl)
-            }
-
-            var onPaneOpen = function(pane) {
-              if (!hasCustomToggler && pane !== customTogglerPane) {
-                return
-              }
-              return customTogglerEl
-                .scope()
-                .$applyAsync(() => (customTogglerEl.scope().isOpen = true))
-            }
-
-            var onPaneClose = function(pane) {
-              if (!hasCustomToggler && pane !== customTogglerPane) {
-                return
-              }
-              return customTogglerEl
-                .scope()
-                .$applyAsync(() => (customTogglerEl.scope().isOpen = false))
-            }
-
-            // Save state when exiting
-            $(window).unload(() =>
-              ide.localStorage(`layout.${name}`, element.layout().readState())
-            )
+            element.append(customTogglerEl)
+          }
 
-            if (attrs.openEast != null) {
-              scope.$watch(attrs.openEast, function(value, oldValue) {
-                if (value != null && value !== oldValue) {
-                  if (value) {
-                    element.layout().open('east')
-                  } else {
-                    element.layout().close('east')
-                  }
-                }
-                return setTimeout(() => scope.$digest(), 0)
-              })
+          var onPaneOpen = function(pane) {
+            if (!hasCustomToggler && pane !== customTogglerPane) {
+              return
             }
+            return customTogglerEl
+              .scope()
+              .$applyAsync(() => (customTogglerEl.scope().isOpen = true))
+          }
 
-            if (attrs.allowOverflowOn != null) {
-              const layoutObj = element.layout()
-              const overflowPane = scope.$eval(attrs.allowOverflowOn)
-              const overflowPaneEl = layoutObj.panes[overflowPane]
-              // Set the panel as overflowing (gives it higher z-index and sets overflow rules)
-              layoutObj.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)
+          var onPaneClose = function(pane) {
+            if (!hasCustomToggler && pane !== customTogglerPane) {
+              return
             }
+            return customTogglerEl
+              .scope()
+              .$applyAsync(() => (customTogglerEl.scope().isOpen = false))
+          }
 
-            resetOpenStates()
-            onInternalResize()
+          // Save state when exiting
+          $(window).unload(() =>
+            ide.localStorage(`layout.${name}`, element.layout().readState())
+          )
 
-            if (attrs.layoutDisabled != null) {
-              return scope.$watch(attrs.layoutDisabled, function(value) {
+          if (attrs.openEast != null) {
+            scope.$watch(attrs.openEast, function(value, oldValue) {
+              if (value != null && value !== oldValue) {
                 if (value) {
-                  element.layout().hide('east')
+                  element.layout().open('east')
                 } else {
-                  element.layout().show('east')
+                  element.layout().close('east')
                 }
-                if (hasCustomToggler) {
-                  return customTogglerEl.scope().$applyAsync(function() {
-                    customTogglerEl.scope().isOpen = !value
-                    return (customTogglerEl.scope().isVisible = !value)
-                  })
-                }
-              })
-            }
-          },
+              }
+              return setTimeout(() => scope.$digest(), 0)
+            })
+          }
 
-          post(scope, element, attrs) {
-            const name = attrs.layout
-            const state = element.layout().readState()
-            return scope.$broadcast(`layout:${name}:linked`, state)
+          if (attrs.allowOverflowOn != null) {
+            const layoutObj = element.layout()
+            const overflowPane = scope.$eval(attrs.allowOverflowOn)
+            const overflowPaneEl = layoutObj.panes[overflowPane]
+            // Set the panel as overflowing (gives it higher z-index and sets overflow rules)
+            layoutObj.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) {
+                element.layout().hide('east')
+              } else {
+                element.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)
         }
       }
-    })
-  ]))
+    }
+  })))

+ 61 - 66
services/web/public/src/ide/editor/controllers/SavingNotificationController.js

@@ -13,81 +13,76 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base', 'ide/editor/Document'], (App, Document) =>
-  App.controller('SavingNotificationController', [
-    '$scope',
-    '$interval',
-    'ide',
-    function($scope, $interval, ide) {
-      let warnAboutUnsavedChanges
-      setInterval(() => pollSavedStatus(), 1000)
+  App.controller('SavingNotificationController', function(
+    $scope,
+    $interval,
+    ide
+  ) {
+    let warnAboutUnsavedChanges
+    setInterval(() => pollSavedStatus(), 1000)
 
-      $(window).bind('beforeunload', () => {
-        return warnAboutUnsavedChanges()
-      })
+    $(window).bind('beforeunload', () => {
+      return warnAboutUnsavedChanges()
+    })
 
-      let lockEditorModal = null // modal showing "connection lost"
-      const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved
+    let lockEditorModal = null // modal showing "connection lost"
+    const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved
 
-      $scope.docSavingStatus = {}
-      var pollSavedStatus = function() {
-        let t
-        const oldStatus = $scope.docSavingStatus
-        const oldUnsavedCount = $scope.docSavingStatusCount
-        const newStatus = {}
-        let newUnsavedCount = 0
-        let maxUnsavedSeconds = 0
+    $scope.docSavingStatus = {}
+    var pollSavedStatus = function() {
+      let t
+      const oldStatus = $scope.docSavingStatus
+      const oldUnsavedCount = $scope.docSavingStatusCount
+      const newStatus = {}
+      let newUnsavedCount = 0
+      let maxUnsavedSeconds = 0
 
-        for (let 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)
-              }
+      for (let 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.'
-          )
-          lockEditorModal.result.finally(() => (lockEditorModal = null)) // unset the modal if connection comes back
-        }
-
-        if (lockEditorModal && newUnsavedCount === 0) {
-          lockEditorModal.dismiss('connection back up')
-        }
+      if (newUnsavedCount > 0 && t > MAX_UNSAVED_SECONDS && !lockEditorModal) {
+        lockEditorModal = ide.showLockEditorMessageModal(
+          'Connection lost',
+          'Sorry, the connection to the server is down.'
+        )
+        lockEditorModal.result.finally(() => (lockEditorModal = null)) // unset the modal if connection comes back
+      }
 
-        // 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()
-        }
+      if (lockEditorModal && newUnsavedCount === 0) {
+        lockEditorModal.dismiss('connection back up')
       }
 
-      return (warnAboutUnsavedChanges = function() {
-        if (Document.hasUnsavedChanges()) {
-          return 'You have unsaved changes. If you leave now they will not be saved.'
-        }
-      })
+      // 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.'
+      }
+    })
+  }))

+ 547 - 568
services/web/public/src/ide/file-tree/controllers/FileTreeController.js

@@ -16,636 +16,615 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], function(App) {
-  App.controller('FileTreeController', [
-    '$scope',
-    '$modal',
-    'ide',
-    '$rootScope',
-    function($scope, $modal, ide, $rootScope) {
-      $scope.openNewDocModal = () =>
-        $modal.open({
-          templateUrl: 'newFileModalTemplate',
-          controller: 'NewFileModalController',
-          size: 'lg',
-          resolve: {
-            parent_folder() {
-              return ide.fileTreeManager.getCurrentFolder()
-            },
-            projectFeatures() {
-              return ide.$scope.project.features
-            },
-            type() {
-              return 'doc'
-            },
-            userFeatures() {
-              return ide.$scope.user.features
-            }
+  App.controller('FileTreeController', function(
+    $scope,
+    $modal,
+    ide,
+    $rootScope
+  ) {
+    $scope.openNewDocModal = () =>
+      $modal.open({
+        templateUrl: 'newFileModalTemplate',
+        controller: 'NewFileModalController',
+        size: 'lg',
+        resolve: {
+          parent_folder() {
+            return ide.fileTreeManager.getCurrentFolder()
+          },
+          projectFeatures() {
+            return ide.$scope.project.features
+          },
+          type() {
+            return 'doc'
+          },
+          userFeatures() {
+            return ide.$scope.user.features
           }
-        })
+        }
+      })
 
-      $scope.openNewFolderModal = () =>
-        $modal.open({
-          templateUrl: 'newFolderModalTemplate',
-          controller: 'NewFolderModalController',
-          resolve: {
-            parent_folder() {
-              return ide.fileTreeManager.getCurrentFolder()
-            }
+    $scope.openNewFolderModal = () =>
+      $modal.open({
+        templateUrl: 'newFolderModalTemplate',
+        controller: 'NewFolderModalController',
+        resolve: {
+          parent_folder() {
+            return ide.fileTreeManager.getCurrentFolder()
           }
-        })
+        }
+      })
 
-      $scope.openUploadFileModal = () =>
-        $modal.open({
-          templateUrl: 'newFileModalTemplate',
-          controller: 'NewFileModalController',
-          size: 'lg',
-          resolve: {
-            projectFeatures() {
-              return ide.$scope.project.features
-            },
-            parent_folder() {
-              return ide.fileTreeManager.getCurrentFolder()
-            },
-            type() {
-              return 'upload'
-            },
-            userFeatures() {
-              return ide.$scope.user.features
-            }
+    $scope.openUploadFileModal = () =>
+      $modal.open({
+        templateUrl: 'newFileModalTemplate',
+        controller: 'NewFileModalController',
+        size: 'lg',
+        resolve: {
+          projectFeatures() {
+            return ide.$scope.project.features
+          },
+          parent_folder() {
+            return ide.fileTreeManager.getCurrentFolder()
+          },
+          type() {
+            return 'upload'
+          },
+          userFeatures() {
+            return ide.$scope.user.features
           }
-        })
-
-      $scope.orderByFoldersFirst = function(entity) {
-        if ((entity != null ? entity.type : undefined) === 'folder') {
-          return '0'
         }
-        return '1'
+      })
+
+    $scope.orderByFoldersFirst = function(entity) {
+      if ((entity != null ? entity.type : undefined) === 'folder') {
+        return '0'
       }
+      return '1'
+    }
 
-      $scope.startRenamingSelected = () => $scope.$broadcast('rename:selected')
+    $scope.startRenamingSelected = () => $scope.$broadcast('rename:selected')
 
-      return ($scope.openDeleteModalForSelected = () =>
-        $scope.$broadcast('delete:selected'))
-    }
-  ])
-
-  App.controller('NewFolderModalController', [
-    '$scope',
-    'ide',
-    '$modalInstance',
-    '$timeout',
-    'parent_folder',
-    function($scope, ide, $modalInstance, $timeout, parent_folder) {
-      $scope.inputs = { name: 'name' }
-      $scope.state = { inflight: false }
-
-      $modalInstance.opened.then(() =>
-        $timeout(() => $scope.$broadcast('open'), 200)
-      )
+    return ($scope.openDeleteModalForSelected = () =>
+      $scope.$broadcast('delete:selected'))
+  })
 
-      $scope.create = function() {
-        const { name } = $scope.inputs
-        if (name == null || name.length === 0) {
-          return
-        }
-        $scope.state.inflight = true
-        return ide.fileTreeManager
-          .createFolder(name, $scope.parent_folder)
-          .then(function() {
-            $scope.state.inflight = false
-            return $modalInstance.dismiss('done')
-          })
-          .catch(function(response) {
-            const { data } = response
-            $scope.error = data
-            return ($scope.state.inflight = false)
-          })
+  App.controller('NewFolderModalController', function(
+    $scope,
+    ide,
+    $modalInstance,
+    $timeout,
+    parent_folder
+  ) {
+    $scope.inputs = { name: 'name' }
+    $scope.state = { inflight: false }
+
+    $modalInstance.opened.then(() =>
+      $timeout(() => $scope.$broadcast('open'), 200)
+    )
+
+    $scope.create = function() {
+      const { name } = $scope.inputs
+      if (name == null || name.length === 0) {
+        return
       }
+      $scope.state.inflight = true
+      return ide.fileTreeManager
+        .createFolder(name, $scope.parent_folder)
+        .then(function() {
+          $scope.state.inflight = false
+          return $modalInstance.dismiss('done')
+        })
+        .catch(function(response) {
+          const { data } = response
+          $scope.error = data
+          return ($scope.state.inflight = false)
+        })
+    }
 
-      return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
+    return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
+  })
+
+  App.controller('NewFileModalController', function(
+    $scope,
+    ide,
+    type,
+    parent_folder,
+    $modalInstance,
+    event_tracking,
+    projectFeatures,
+    userFeatures
+  ) {
+    $scope.type = type
+    $scope.parent_folder = parent_folder
+    $scope.state = {
+      inflight: false,
+      valid: true
     }
-  ])
-
-  App.controller('NewFileModalController', [
-    '$scope',
-    'ide',
-    'type',
-    'parent_folder',
-    '$modalInstance',
-    'event_tracking',
-    'projectFeatures',
-    'userFeatures',
-    function(
-      $scope,
-      ide,
-      type,
-      parent_folder,
-      $modalInstance,
-      event_tracking,
-      projectFeatures,
-      userFeatures
-    ) {
-      $scope.type = type
-      $scope.parent_folder = parent_folder
-      $scope.state = {
-        inflight: false,
-        valid: true
+    $scope.cancel = () => $modalInstance.dismiss('cancel')
+    $scope.create = () => $scope.$broadcast('create')
+
+    const hasMendeleyFeature =
+      (projectFeatures && projectFeatures.references) ||
+      (projectFeatures && projectFeatures.mendeley) ||
+      (userFeatures && userFeatures.references) ||
+      (userFeatures && userFeatures.mendeley)
+
+    const hasZoteroFeature =
+      (projectFeatures && projectFeatures.references) ||
+      (projectFeatures && projectFeatures.zotero) ||
+      (userFeatures && userFeatures.references) ||
+      (userFeatures && userFeatures.zotero)
+
+    $scope.$watch('type', function() {
+      if ($scope.type === 'mendeley' && !hasMendeleyFeature) {
+        event_tracking.send(
+          'subscription-funnel',
+          'editor-click-feature',
+          $scope.type
+        )
       }
-      $scope.cancel = () => $modalInstance.dismiss('cancel')
-      $scope.create = () => $scope.$broadcast('create')
-
-      const hasMendeleyFeature =
-        (projectFeatures && projectFeatures.references) ||
-        (projectFeatures && projectFeatures.mendeley) ||
-        (userFeatures && userFeatures.references) ||
-        (userFeatures && userFeatures.mendeley)
-
-      const hasZoteroFeature =
-        (projectFeatures && projectFeatures.references) ||
-        (projectFeatures && projectFeatures.zotero) ||
-        (userFeatures && userFeatures.references) ||
-        (userFeatures && userFeatures.zotero)
-
-      $scope.$watch('type', function() {
-        if ($scope.type === 'mendeley' && !hasMendeleyFeature) {
-          event_tracking.send(
-            'subscription-funnel',
-            'editor-click-feature',
-            $scope.type
-          )
-        }
-        if ($scope.type === 'zotero' && !hasZoteroFeature) {
-          event_tracking.send(
-            'subscription-funnel',
-            'editor-click-feature',
-            $scope.type
-          )
-        }
-      })
-      return $scope.$on('done', (e, opts = {}) => {
-        isBibFile = opts.name && /^.*\.bib$/.test(opts.name)
-        if (opts.shouldReindexReferences || isBibFile) {
-          ide.$scope.$emit('references:should-reindex', {})
-        }
-        $modalInstance.dismiss('done')
-      })
-    }
-  ])
-
-  App.controller('NewDocModalController', [
-    '$scope',
-    'ide',
-    '$timeout',
-    function($scope, ide, $timeout) {
-      $scope.inputs = { name: 'name.tex' }
-
-      const validate = function() {
-        const { name } = $scope.inputs
-        $scope.state.valid = name != null && name.length > 0
+      if ($scope.type === 'zotero' && !hasZoteroFeature) {
+        event_tracking.send(
+          'subscription-funnel',
+          'editor-click-feature',
+          $scope.type
+        )
+      }
+    })
+    return $scope.$on('done', (e, opts = {}) => {
+      isBibFile = opts.name && /^.*\.bib$/.test(opts.name)
+      if (opts.shouldReindexReferences || isBibFile) {
+        ide.$scope.$emit('references:should-reindex', {})
       }
-      $scope.$watch('inputs.name', validate)
+      $modalInstance.dismiss('done')
+    })
+  })
 
-      $timeout(() => $scope.$broadcast('open'), 200)
+  App.controller('NewDocModalController', function($scope, ide, $timeout) {
+    $scope.inputs = { name: 'name.tex' }
 
-      return $scope.$on('create', function() {
-        const { name } = $scope.inputs
-        if (name == null || name.length === 0) {
-          return
-        }
-        $scope.state.inflight = true
-        return ide.fileTreeManager
-          .createDoc(name, $scope.parent_folder)
-          .then(function() {
-            $scope.state.inflight = false
-            return $scope.$emit('done')
-          })
-          .catch(function(response) {
-            const { data } = response
-            $scope.error = data
-            $scope.state.inflight = false
-          })
-      })
+    const validate = function() {
+      const { name } = $scope.inputs
+      $scope.state.valid = name != null && name.length > 0
     }
-  ])
-
-  App.controller('UploadFileModalController', [
-    '$scope',
-    '$rootScope',
-    'ide',
-    '$timeout',
-    '$window',
-    function($scope, $rootScope, ide, $timeout, $window) {
-      $scope.parent_folder_id =
-        $scope.parent_folder != null ? $scope.parent_folder.id : undefined
-      $scope.project_id = ide.project_id
-      $scope.tooManyFiles = false
-      $scope.rateLimitHit = false
-      $scope.secondsToRedirect = 10
-      $scope.notLoggedIn = false
-      $scope.conflicts = []
-      $scope.control = {}
-
-      const needToLogBackIn = function() {
-        $scope.notLoggedIn = true
-        var decreseTimeout = () =>
-          $timeout(function() {
-            if ($scope.secondsToRedirect === 0) {
-              return ($window.location.href = `/login?redir=/project/${
-                ide.project_id
-              }`)
-            } else {
-              decreseTimeout()
-              return ($scope.secondsToRedirect = $scope.secondsToRedirect - 1)
-            }
-          }, 1000)
-
-        return decreseTimeout()
-      }
+    $scope.$watch('inputs.name', validate)
+
+    $timeout(() => $scope.$broadcast('open'), 200)
 
-      $scope.max_files = 40
-      $scope.onComplete = (error, name, response) =>
+    return $scope.$on('create', function() {
+      const { name } = $scope.inputs
+      if (name == null || name.length === 0) {
+        return
+      }
+      $scope.state.inflight = true
+      return ide.fileTreeManager
+        .createDoc(name, $scope.parent_folder)
+        .then(function() {
+          $scope.state.inflight = false
+          return $scope.$emit('done')
+        })
+        .catch(function(response) {
+          const { data } = response
+          $scope.error = data
+          $scope.state.inflight = false
+        })
+    })
+  })
+
+  App.controller('UploadFileModalController', function(
+    $scope,
+    $rootScope,
+    ide,
+    $timeout,
+    $window
+  ) {
+    $scope.parent_folder_id =
+      $scope.parent_folder != null ? $scope.parent_folder.id : undefined
+    $scope.project_id = ide.project_id
+    $scope.tooManyFiles = false
+    $scope.rateLimitHit = false
+    $scope.secondsToRedirect = 10
+    $scope.notLoggedIn = false
+    $scope.conflicts = []
+    $scope.control = {}
+
+    const needToLogBackIn = function() {
+      $scope.notLoggedIn = true
+      var decreseTimeout = () =>
         $timeout(function() {
-          uploadCount--
-          if (response.success) {
-            $rootScope.$broadcast('file:upload:complete', response)
-          }
-          if (uploadCount === 0 && response != null && response.success) {
-            return $scope.$emit('done', { name: name })
+          if ($scope.secondsToRedirect === 0) {
+            return ($window.location.href = `/login?redir=/project/${
+              ide.project_id
+            }`)
+          } else {
+            decreseTimeout()
+            return ($scope.secondsToRedirect = $scope.secondsToRedirect - 1)
           }
-        }, 250)
-
-      $scope.onValidateBatch = function(files) {
-        if (files.length > $scope.max_files) {
-          $timeout(() => ($scope.tooManyFiles = true), 1)
-          return false
-        } else {
-          return true
-        }
-      }
+        }, 1000)
+
+      return decreseTimeout()
+    }
 
-      $scope.onError = function(id, name, reason) {
-        console.log(id, name, reason)
-        if (reason.indexOf('429') !== -1) {
-          return ($scope.rateLimitHit = true)
-        } else if (reason.indexOf('403') !== -1) {
-          return needToLogBackIn()
+    $scope.max_files = 40
+    $scope.onComplete = (error, name, response) =>
+      $timeout(function() {
+        uploadCount--
+        if (response.success) {
+          $rootScope.$broadcast('file:upload:complete', response)
+        }
+        if (uploadCount === 0 && response != null && response.success) {
+          return $scope.$emit('done', { name: name })
         }
+      }, 250)
+
+    $scope.onValidateBatch = function(files) {
+      if (files.length > $scope.max_files) {
+        $timeout(() => ($scope.tooManyFiles = true), 1)
+        return false
+      } else {
+        return true
       }
+    }
 
-      let _uploadTimer = null
-      const uploadIfNoConflicts = function() {
-        if ($scope.conflicts.length === 0) {
-          return $scope.doUpload()
-        }
+    $scope.onError = function(id, name, reason) {
+      console.log(id, name, reason)
+      if (reason.indexOf('429') !== -1) {
+        return ($scope.rateLimitHit = true)
+      } else if (reason.indexOf('403') !== -1) {
+        return needToLogBackIn()
       }
+    }
 
-      var uploadCount = 0
-      $scope.onSubmit = function(id, name) {
-        uploadCount++
-        if (ide.fileTreeManager.existsInFolder($scope.parent_folder_id, name)) {
-          $scope.conflicts.push(name)
-          $scope.$apply()
-        }
-        if (_uploadTimer == null) {
-          _uploadTimer = setTimeout(function() {
-            _uploadTimer = null
-            return uploadIfNoConflicts()
-          }, 0)
-        }
-        return true
+    let _uploadTimer = null
+    const uploadIfNoConflicts = function() {
+      if ($scope.conflicts.length === 0) {
+        return $scope.doUpload()
       }
+    }
 
-      $scope.onCancel = function(id, name) {
-        uploadCount--
-        const index = $scope.conflicts.indexOf(name)
-        if (index > -1) {
-          $scope.conflicts.splice(index, 1)
-        }
+    var uploadCount = 0
+    $scope.onSubmit = function(id, name) {
+      uploadCount++
+      if (ide.fileTreeManager.existsInFolder($scope.parent_folder_id, name)) {
+        $scope.conflicts.push(name)
         $scope.$apply()
-        return uploadIfNoConflicts()
       }
+      if (_uploadTimer == null) {
+        _uploadTimer = setTimeout(function() {
+          _uploadTimer = null
+          return uploadIfNoConflicts()
+        }, 0)
+      }
+      return true
+    }
+
+    $scope.onCancel = function(id, name) {
+      uploadCount--
+      const index = $scope.conflicts.indexOf(name)
+      if (index > -1) {
+        $scope.conflicts.splice(index, 1)
+      }
+      $scope.$apply()
+      return uploadIfNoConflicts()
+    }
 
-      return ($scope.doUpload = () =>
-        __guard__($scope.control != null ? $scope.control.q : undefined, x =>
-          x.uploadStoredFiles()
-        ))
+    return ($scope.doUpload = () =>
+      __guard__($scope.control != null ? $scope.control.q : undefined, x =>
+        x.uploadStoredFiles()
+      ))
+  })
+
+  App.controller('ProjectLinkedFileModalController', function(
+    $scope,
+    ide,
+    $timeout
+  ) {
+    $scope.data = {
+      projects: null, // or []
+      selectedProjectId: null,
+      projectEntities: null, // or []
+      projectOutputFiles: null, // or []
+      selectedProjectEntity: null,
+      selectedProjectOutputFile: null,
+      buildId: null,
+      name: null
     }
-  ])
-
-  App.controller('ProjectLinkedFileModalController', [
-    '$scope',
-    'ide',
-    '$timeout',
-    function($scope, ide, $timeout) {
-      $scope.data = {
-        projects: null, // or []
-        selectedProjectId: null,
-        projectEntities: null, // or []
-        projectOutputFiles: null, // or []
-        selectedProjectEntity: null,
-        selectedProjectOutputFile: null,
-        buildId: null,
-        name: null
+    $scope.state.inFlight = {
+      projects: false,
+      entities: false,
+      compile: false
+    }
+    $scope.state.isOutputFilesMode = false
+    $scope.state.error = false
+
+    $scope.$watch('data.selectedProjectId', function(newVal, oldVal) {
+      if (!newVal) {
+        return
       }
-      $scope.state.inFlight = {
-        projects: false,
-        entities: false,
-        compile: false
+      $scope.data.selectedProjectEntity = null
+      $scope.data.selectedProjectOutputFile = null
+      if ($scope.state.isOutputFilesMode) {
+        return $scope.compileProjectAndGetOutputFiles(
+          $scope.data.selectedProjectId
+        )
+      } else {
+        return $scope.getProjectEntities($scope.data.selectedProjectId)
       }
-      $scope.state.isOutputFilesMode = false
-      $scope.state.error = false
+    })
 
-      $scope.$watch('data.selectedProjectId', function(newVal, oldVal) {
-        if (!newVal) {
-          return
-        }
-        $scope.data.selectedProjectEntity = null
-        $scope.data.selectedProjectOutputFile = null
-        if ($scope.state.isOutputFilesMode) {
-          return $scope.compileProjectAndGetOutputFiles(
-            $scope.data.selectedProjectId
-          )
-        } else {
-          return $scope.getProjectEntities($scope.data.selectedProjectId)
-        }
-      })
+    $scope.$watch('state.isOutputFilesMode', function(newVal, oldVal) {
+      if (!newVal && !oldVal) {
+        return
+      }
+      $scope.data.selectedProjectOutputFile = null
+      if (newVal === true) {
+        return $scope.compileProjectAndGetOutputFiles(
+          $scope.data.selectedProjectId
+        )
+      } else {
+        return $scope.getProjectEntities($scope.data.selectedProjectId)
+      }
+    })
 
-      $scope.$watch('state.isOutputFilesMode', function(newVal, oldVal) {
-        if (!newVal && !oldVal) {
-          return
-        }
-        $scope.data.selectedProjectOutputFile = null
-        if (newVal === true) {
-          return $scope.compileProjectAndGetOutputFiles(
-            $scope.data.selectedProjectId
-          )
-        } else {
-          return $scope.getProjectEntities($scope.data.selectedProjectId)
-        }
-      })
+    // auto-set filename based on selected file
+    $scope.$watch('data.selectedProjectEntity', function(newVal, oldVal) {
+      if (!newVal) {
+        return
+      }
+      const fileName = newVal.split('/').reverse()[0]
+      if (fileName) {
+        $scope.data.name = fileName
+      }
+    })
 
-      // auto-set filename based on selected file
-      $scope.$watch('data.selectedProjectEntity', function(newVal, oldVal) {
-        if (!newVal) {
-          return
-        }
+    // auto-set filename based on selected file
+    $scope.$watch('data.selectedProjectOutputFile', function(newVal, oldVal) {
+      if (!newVal) {
+        return
+      }
+      if (newVal === 'output.pdf') {
+        const project = _.find(
+          $scope.data.projects,
+          p => p._id === $scope.data.selectedProjectId
+        )
+        $scope.data.name =
+          (project != null ? project.name : undefined) != null
+            ? `${project.name}.pdf`
+            : 'output.pdf'
+      } else {
         const fileName = newVal.split('/').reverse()[0]
         if (fileName) {
           $scope.data.name = fileName
         }
-      })
+      }
+    })
 
-      // auto-set filename based on selected file
-      $scope.$watch('data.selectedProjectOutputFile', function(newVal, oldVal) {
-        if (!newVal) {
-          return
-        }
-        if (newVal === 'output.pdf') {
-          const project = _.find(
-            $scope.data.projects,
-            p => p._id === $scope.data.selectedProjectId
-          )
-          $scope.data.name =
-            (project != null ? project.name : undefined) != null
-              ? `${project.name}.pdf`
-              : 'output.pdf'
-        } else {
-          const fileName = newVal.split('/').reverse()[0]
-          if (fileName) {
-            $scope.data.name = fileName
-          }
-        }
-      })
+    const _setInFlight = type => ($scope.state.inFlight[type] = true)
 
-      const _setInFlight = type => ($scope.state.inFlight[type] = true)
+    const _reset = function(opts) {
+      const isError = opts.err === true
+      const { inFlight } = $scope.state
+      inFlight.projects = inFlight.entities = inFlight.compile = false
+      $scope.state.inflight = false
+      return ($scope.state.error = isError)
+    }
 
-      const _reset = function(opts) {
-        const isError = opts.err === true
-        const { inFlight } = $scope.state
-        inFlight.projects = inFlight.entities = inFlight.compile = false
-        $scope.state.inflight = false
-        return ($scope.state.error = isError)
+    $scope.toggleOutputFilesMode = function() {
+      if (!$scope.data.selectedProjectId) {
+        return
       }
+      return ($scope.state.isOutputFilesMode = !$scope.state.isOutputFilesMode)
+    }
 
-      $scope.toggleOutputFilesMode = function() {
-        if (!$scope.data.selectedProjectId) {
-          return
-        }
-        return ($scope.state.isOutputFilesMode = !$scope.state
-          .isOutputFilesMode)
-      }
+    $scope.shouldEnableProjectSelect = function() {
+      const { state, data } = $scope
+      return !state.inFlight.projects && data.projects
+    }
 
-      $scope.shouldEnableProjectSelect = function() {
-        const { state, data } = $scope
-        return !state.inFlight.projects && data.projects
-      }
+    $scope.hasNoProjects = function() {
+      const { state, data } = $scope
+      return (
+        !state.inFlight.projects &&
+        (data.projects == null || data.projects.length === 0)
+      )
+    }
 
-      $scope.hasNoProjects = function() {
-        const { state, data } = $scope
-        return (
-          !state.inFlight.projects &&
-          (data.projects == null || data.projects.length === 0)
-        )
-      }
+    $scope.shouldEnableProjectEntitySelect = function() {
+      const { state, data } = $scope
+      return (
+        !state.inFlight.projects &&
+        !state.inFlight.entities &&
+        data.projects &&
+        data.selectedProjectId
+      )
+    }
 
-      $scope.shouldEnableProjectEntitySelect = function() {
-        const { state, data } = $scope
-        return (
-          !state.inFlight.projects &&
-          !state.inFlight.entities &&
-          data.projects &&
-          data.selectedProjectId
-        )
-      }
+    $scope.shouldEnableProjectOutputFileSelect = function() {
+      const { state, data } = $scope
+      return (
+        !state.inFlight.projects &&
+        !state.inFlight.compile &&
+        data.projects &&
+        data.selectedProjectId
+      )
+    }
 
-      $scope.shouldEnableProjectOutputFileSelect = function() {
-        const { state, data } = $scope
-        return (
-          !state.inFlight.projects &&
-          !state.inFlight.compile &&
-          data.projects &&
-          data.selectedProjectId
-        )
-      }
+    const validate = function() {
+      const { state } = $scope
+      const { data } = $scope
+      $scope.state.valid =
+        !state.inFlight.projects &&
+        !state.inFlight.entities &&
+        data.projects &&
+        data.selectedProjectId &&
+        ((!$scope.state.isOutputFilesMode &&
+          data.projectEntities &&
+          data.selectedProjectEntity) ||
+          ($scope.state.isOutputFilesMode &&
+            data.projectOutputFiles &&
+            data.selectedProjectOutputFile)) &&
+        data.name
+    }
+    $scope.$watch('state', validate, true)
+    $scope.$watch('data', validate, true)
+
+    $scope.getUserProjects = function() {
+      _setInFlight('projects')
+      return ide.$http
+        .get('/user/projects', {
+          _csrf: window.csrfToken
+        })
+        .then(function(resp) {
+          $scope.data.projectEntities = null
+          $scope.data.projects = resp.data.projects.filter(
+            p => p._id !== ide.project_id
+          )
+          return _reset({ err: false })
+        })
+        .catch(err => _reset({ err: true }))
+    }
 
-      const validate = function() {
-        const { state } = $scope
-        const { data } = $scope
-        $scope.state.valid =
-          !state.inFlight.projects &&
-          !state.inFlight.entities &&
-          data.projects &&
-          data.selectedProjectId &&
-          ((!$scope.state.isOutputFilesMode &&
-            data.projectEntities &&
-            data.selectedProjectEntity) ||
-            ($scope.state.isOutputFilesMode &&
-              data.projectOutputFiles &&
-              data.selectedProjectOutputFile)) &&
-          data.name
-      }
-      $scope.$watch('state', validate, true)
-      $scope.$watch('data', validate, true)
-
-      $scope.getUserProjects = function() {
-        _setInFlight('projects')
-        return ide.$http
-          .get('/user/projects', {
-            _csrf: window.csrfToken
-          })
-          .then(function(resp) {
-            $scope.data.projectEntities = null
-            $scope.data.projects = resp.data.projects.filter(
-              p => p._id !== ide.project_id
-            )
+    $scope.getProjectEntities = project_id => {
+      _setInFlight('entities')
+      return ide.$http
+        .get(`/project/${project_id}/entities`, {
+          _csrf: window.csrfToken
+        })
+        .then(function(resp) {
+          if ($scope.data.selectedProjectId === resp.data.project_id) {
+            $scope.data.projectEntities = resp.data.entities
             return _reset({ err: false })
-          })
-          .catch(err => _reset({ err: true }))
-      }
-
-      $scope.getProjectEntities = project_id => {
-        _setInFlight('entities')
-        return ide.$http
-          .get(`/project/${project_id}/entities`, {
-            _csrf: window.csrfToken
-          })
-          .then(function(resp) {
-            if ($scope.data.selectedProjectId === resp.data.project_id) {
-              $scope.data.projectEntities = resp.data.entities
-              return _reset({ err: false })
-            }
-          })
-          .catch(err => _reset({ err: true }))
-      }
+          }
+        })
+        .catch(err => _reset({ err: true }))
+    }
 
-      $scope.compileProjectAndGetOutputFiles = project_id => {
-        _setInFlight('compile')
-        return ide.$http
-          .post(`/project/${project_id}/compile`, {
-            check: 'silent',
-            draft: false,
-            incrementalCompilesEnabled: false,
-            _csrf: window.csrfToken
-          })
-          .then(function(resp) {
-            if (resp.data.status === 'success') {
-              const filteredFiles = resp.data.outputFiles.filter(f =>
-                f.path.match(/.*\.(pdf|png|jpeg|jpg|gif)/)
-              )
-              $scope.data.projectOutputFiles = filteredFiles
-              $scope.data.buildId = __guard__(
-                filteredFiles != null ? filteredFiles[0] : undefined,
-                x => x.build
-              )
-              console.log('>> build_id', $scope.data.buildId)
-              return _reset({ err: false })
-            } else {
-              $scope.data.projectOutputFiles = null
-              return _reset({ err: true })
-            }
-          })
-          .catch(function(err) {
-            console.error(err)
+    $scope.compileProjectAndGetOutputFiles = project_id => {
+      _setInFlight('compile')
+      return ide.$http
+        .post(`/project/${project_id}/compile`, {
+          check: 'silent',
+          draft: false,
+          incrementalCompilesEnabled: false,
+          _csrf: window.csrfToken
+        })
+        .then(function(resp) {
+          if (resp.data.status === 'success') {
+            const filteredFiles = resp.data.outputFiles.filter(f =>
+              f.path.match(/.*\.(pdf|png|jpeg|jpg|gif)/)
+            )
+            $scope.data.projectOutputFiles = filteredFiles
+            $scope.data.buildId = __guard__(
+              filteredFiles != null ? filteredFiles[0] : undefined,
+              x => x.build
+            )
+            console.log('>> build_id', $scope.data.buildId)
+            return _reset({ err: false })
+          } else {
+            $scope.data.projectOutputFiles = null
             return _reset({ err: true })
-          })
-      }
-
-      $scope.init = () => $scope.getUserProjects()
-      $timeout($scope.init, 0)
-
-      return $scope.$on('create', function() {
-        let payload, provider
-        const projectId = $scope.data.selectedProjectId
-        const { name } = $scope.data
-        if ($scope.state.isOutputFilesMode) {
-          provider = 'project_output_file'
-          payload = {
-            source_project_id: projectId,
-            source_output_file_path: $scope.data.selectedProjectOutputFile,
-            build_id: $scope.data.buildId
-          }
-        } else {
-          provider = 'project_file'
-          payload = {
-            source_project_id: projectId,
-            source_entity_path: $scope.data.selectedProjectEntity
           }
-        }
-        _setInFlight('create')
-        ide.fileTreeManager
-          .createLinkedFile(name, $scope.parent_folder, provider, payload)
-          .then(function() {
-            _reset({ err: false })
-            return $scope.$emit('done', { name: name })
-          })
-          .catch(function(response) {
-            const { data } = response
-            $scope.error = data
-          })
-      })
+        })
+        .catch(function(err) {
+          console.error(err)
+          return _reset({ err: true })
+        })
     }
-  ])
-
-  return App.controller('UrlLinkedFileModalController', [
-    '$scope',
-    'ide',
-    '$timeout',
-    function($scope, ide, $timeout) {
-      $scope.inputs = {
-        name: '',
-        url: ''
-      }
-      $scope.nameChangedByUser = false
 
-      $timeout(() => $scope.$broadcast('open'), 200)
-
-      const validate = function() {
-        const { name, url } = $scope.inputs
-        if (name == null || name.length === 0) {
-          return ($scope.state.valid = false)
-        } else if (url == null || url.length === 0) {
-          return ($scope.state.valid = false)
-        } else {
-          return ($scope.state.valid = true)
+    $scope.init = () => $scope.getUserProjects()
+    $timeout($scope.init, 0)
+
+    return $scope.$on('create', function() {
+      let payload, provider
+      const projectId = $scope.data.selectedProjectId
+      const { name } = $scope.data
+      if ($scope.state.isOutputFilesMode) {
+        provider = 'project_output_file'
+        payload = {
+          source_project_id: projectId,
+          source_output_file_path: $scope.data.selectedProjectOutputFile,
+          build_id: $scope.data.buildId
+        }
+      } else {
+        provider = 'project_file'
+        payload = {
+          source_project_id: projectId,
+          source_entity_path: $scope.data.selectedProjectEntity
         }
       }
-      $scope.$watch('inputs.name', validate)
-      $scope.$watch('inputs.url', validate)
-
-      $scope.$watch('inputs.url', function(url) {
-        if (url != null && url !== '' && !$scope.nameChangedByUser) {
-          url = url.replace('://', '') // Ignore http:// etc
-          const parts = url.split('/').reverse()
-          if (parts.length > 1) {
-            // Wait for at one /
-            return ($scope.inputs.name = parts[0])
-          }
+      _setInFlight('create')
+      ide.fileTreeManager
+        .createLinkedFile(name, $scope.parent_folder, provider, payload)
+        .then(function() {
+          _reset({ err: false })
+          return $scope.$emit('done', { name: name })
+        })
+        .catch(function(response) {
+          const { data } = response
+          $scope.error = data
+        })
+    })
+  })
+
+  return App.controller('UrlLinkedFileModalController', function(
+    $scope,
+    ide,
+    $timeout
+  ) {
+    $scope.inputs = {
+      name: '',
+      url: ''
+    }
+    $scope.nameChangedByUser = false
+
+    $timeout(() => $scope.$broadcast('open'), 200)
+
+    const validate = function() {
+      const { name, url } = $scope.inputs
+      if (name == null || name.length === 0) {
+        return ($scope.state.valid = false)
+      } else if (url == null || url.length === 0) {
+        return ($scope.state.valid = false)
+      } else {
+        return ($scope.state.valid = true)
+      }
+    }
+    $scope.$watch('inputs.name', validate)
+    $scope.$watch('inputs.url', validate)
+
+    $scope.$watch('inputs.url', function(url) {
+      if (url != null && url !== '' && !$scope.nameChangedByUser) {
+        url = url.replace('://', '') // Ignore http:// etc
+        const parts = url.split('/').reverse()
+        if (parts.length > 1) {
+          // Wait for at one /
+          return ($scope.inputs.name = parts[0])
         }
-      })
+      }
+    })
 
-      return $scope.$on('create', function() {
-        const { name, url } = $scope.inputs
-        if (name == null || name.length === 0) {
-          return
-        }
-        if (url == null || url.length === 0) {
-          return
-        }
-        $scope.state.inflight = true
-        return ide.fileTreeManager
-          .createLinkedFile(name, $scope.parent_folder, 'url', { url })
-          .then(function() {
-            $scope.state.inflight = false
-            return $scope.$emit('done', { name: name })
-          })
-          .catch(function(response) {
-            const { data } = response
-            $scope.error = data
-            return ($scope.state.inflight = false)
-          })
-      })
-    }
-  ])
+    return $scope.$on('create', function() {
+      const { name, url } = $scope.inputs
+      if (name == null || name.length === 0) {
+        return
+      }
+      if (url == null || url.length === 0) {
+        return
+      }
+      $scope.state.inflight = true
+      return ide.fileTreeManager
+        .createLinkedFile(name, $scope.parent_folder, 'url', { url })
+        .then(function() {
+          $scope.state.inflight = false
+          return $scope.$emit('done', { name: name })
+        })
+        .catch(function(response) {
+          const { data } = response
+          $scope.error = data
+          return ($scope.state.inflight = false)
+        })
+    })
+  })
 })
 
 function __guard__(value, transform) {

+ 91 - 97
services/web/public/src/ide/file-tree/controllers/FileTreeEntityController.js

@@ -17,125 +17,119 @@ define(['base', 'ide/file-tree/util/iconTypeFromName'], function(
   App,
   iconTypeFromName
 ) {
-  App.controller('FileTreeEntityController', [
-    '$scope',
-    'ide',
-    '$modal',
-    function($scope, ide, $modal) {
-      $scope.select = function(e) {
-        if (e.ctrlKey || e.metaKey) {
-          e.stopPropagation()
-          const initialMultiSelectCount = ide.fileTreeManager.multiSelectedCount()
-          ide.fileTreeManager.toggleMultiSelectEntity($scope.entity) === 0
-          if (initialMultiSelectCount === 0) {
-            // On first multi selection, also include the current active/open file.
-            return ide.fileTreeManager.multiSelectSelectedEntity()
-          }
-        } else {
-          ide.fileTreeManager.selectEntity($scope.entity)
-          return $scope.$emit('entity:selected', $scope.entity)
+  App.controller('FileTreeEntityController', function($scope, ide, $modal) {
+    $scope.select = function(e) {
+      if (e.ctrlKey || e.metaKey) {
+        e.stopPropagation()
+        const initialMultiSelectCount = ide.fileTreeManager.multiSelectedCount()
+        ide.fileTreeManager.toggleMultiSelectEntity($scope.entity) === 0
+        if (initialMultiSelectCount === 0) {
+          // On first multi selection, also include the current active/open file.
+          return ide.fileTreeManager.multiSelectSelectedEntity()
         }
+      } else {
+        ide.fileTreeManager.selectEntity($scope.entity)
+        return $scope.$emit('entity:selected', $scope.entity)
       }
+    }
 
-      $scope.draggableHelper = function() {
-        if (ide.fileTreeManager.multiSelectedCount() > 0) {
-          return $(
-            `<strong style='z-index:100'>${ide.fileTreeManager.multiSelectedCount()} Files</strong>`
-          )
-        } else {
-          return $(`<strong style='z-index:100'>${$scope.entity.name}</strong>`)
-        }
+    $scope.draggableHelper = function() {
+      if (ide.fileTreeManager.multiSelectedCount() > 0) {
+        return $(
+          `<strong style='z-index:100'>${ide.fileTreeManager.multiSelectedCount()} Files</strong>`
+        )
+      } else {
+        return $(`<strong style='z-index:100'>${$scope.entity.name}</strong>`)
       }
+    }
 
-      $scope.inputs = { name: $scope.entity.name }
-
-      $scope.startRenaming = () => ($scope.entity.renaming = true)
+    $scope.inputs = { name: $scope.entity.name }
 
-      let invalidModalShowing = false
-      $scope.finishRenaming = function() {
-        // avoid double events when blur and on-enter fire together
-        if (!$scope.entity.renaming) {
-          return
-        }
+    $scope.startRenaming = () => ($scope.entity.renaming = true)
 
-        const { name } = $scope.inputs
+    let invalidModalShowing = false
+    $scope.finishRenaming = function() {
+      // avoid double events when blur and on-enter fire together
+      if (!$scope.entity.renaming) {
+        return
+      }
 
-        // validator will set name to undefined for invalid filenames
-        if (name == null) {
-          // Showing the modal blurs the rename box which calls us again
-          // so track this with the invalidModalShowing flag
-          if (invalidModalShowing) {
-            return
-          }
-          invalidModalShowing = true
-          const modal = $modal.open({
-            templateUrl: 'invalidFileNameModalTemplate'
-          })
-          modal.result.then(() => (invalidModalShowing = false))
-          return
-        }
+      const { name } = $scope.inputs
 
-        delete $scope.entity.renaming
-        if (name == null || name.length === 0) {
-          $scope.inputs.name = $scope.entity.name
+      // validator will set name to undefined for invalid filenames
+      if (name == null) {
+        // Showing the modal blurs the rename box which calls us again
+        // so track this with the invalidModalShowing flag
+        if (invalidModalShowing) {
           return
         }
-        return ide.fileTreeManager.renameEntity($scope.entity, name)
+        invalidModalShowing = true
+        const modal = $modal.open({
+          templateUrl: 'invalidFileNameModalTemplate'
+        })
+        modal.result.then(() => (invalidModalShowing = false))
+        return
       }
 
-      $scope.$on('rename:selected', function() {
-        if ($scope.entity.selected) {
-          return $scope.startRenaming()
-        }
-      })
+      delete $scope.entity.renaming
+      if (name == null || name.length === 0) {
+        $scope.inputs.name = $scope.entity.name
+        return
+      }
+      return ide.fileTreeManager.renameEntity($scope.entity, name)
+    }
 
-      $scope.openDeleteModal = function() {
-        let entities
-        if (ide.fileTreeManager.multiSelectedCount() > 0) {
-          entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
-        } else {
-          entities = [$scope.entity]
-        }
-        return $modal.open({
-          templateUrl: 'deleteEntityModalTemplate',
-          controller: 'DeleteEntityModalController',
-          resolve: {
-            entities() {
-              return entities
-            }
-          }
-        })
+    $scope.$on('rename:selected', function() {
+      if ($scope.entity.selected) {
+        return $scope.startRenaming()
       }
+    })
 
-      $scope.$on('delete:selected', function() {
-        if ($scope.entity.selected) {
-          return $scope.openDeleteModal()
+    $scope.openDeleteModal = function() {
+      let entities
+      if (ide.fileTreeManager.multiSelectedCount() > 0) {
+        entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
+      } else {
+        entities = [$scope.entity]
+      }
+      return $modal.open({
+        templateUrl: 'deleteEntityModalTemplate',
+        controller: 'DeleteEntityModalController',
+        resolve: {
+          entities() {
+            return entities
+          }
         }
       })
-
-      return ($scope.iconTypeFromName = iconTypeFromName)
     }
-  ])
 
-  return App.controller('DeleteEntityModalController', [
-    '$scope',
-    'ide',
-    '$modalInstance',
-    'entities',
-    function($scope, ide, $modalInstance, entities) {
-      $scope.state = { inflight: false }
+    $scope.$on('delete:selected', function() {
+      if ($scope.entity.selected) {
+        return $scope.openDeleteModal()
+      }
+    })
 
-      $scope.entities = entities
+    return ($scope.iconTypeFromName = iconTypeFromName)
+  })
 
-      $scope.delete = function() {
-        $scope.state.inflight = true
-        for (let entity of Array.from($scope.entities)) {
-          ide.fileTreeManager.deleteEntity(entity)
-        }
-        return $modalInstance.close()
-      }
+  return App.controller('DeleteEntityModalController', function(
+    $scope,
+    ide,
+    $modalInstance,
+    entities
+  ) {
+    $scope.state = { inflight: false }
 
-      return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
+    $scope.entities = entities
+
+    $scope.delete = function() {
+      $scope.state.inflight = true
+      for (let entity of Array.from($scope.entities)) {
+        ide.fileTreeManager.deleteEntity(entity)
+      }
+      return $modalInstance.close()
     }
-  ])
+
+    return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
+  })
 })

+ 19 - 23
services/web/public/src/ide/file-tree/controllers/FileTreeRootFolderController.js

@@ -13,26 +13,22 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.controller('FileTreeRootFolderController', [
-    '$scope',
-    'ide',
-    function($scope, ide) {
-      const { rootFolder } = $scope
-      return ($scope.onDrop = function(events, ui) {
-        let entities
-        if (ide.fileTreeManager.multiSelectedCount()) {
-          entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
-        } else {
-          entities = [$(ui.draggable).scope().entity]
-        }
-        for (let dropped_entity of Array.from(entities)) {
-          ide.fileTreeManager.moveEntity(dropped_entity, rootFolder)
-        }
-        $scope.$digest()
-        // clear highlight explicitly
-        return $('.file-tree-inner .droppable-hover').removeClass(
-          'droppable-hover'
-        )
-      })
-    }
-  ]))
+  App.controller('FileTreeRootFolderController', function($scope, ide) {
+    const { rootFolder } = $scope
+    return ($scope.onDrop = function(events, ui) {
+      let entities
+      if (ide.fileTreeManager.multiSelectedCount()) {
+        entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
+      } else {
+        entities = [$(ui.draggable).scope().entity]
+      }
+      for (let dropped_entity of Array.from(entities)) {
+        ide.fileTreeManager.moveEntity(dropped_entity, rootFolder)
+      }
+      $scope.$digest()
+      // clear highlight explicitly
+      return $('.file-tree-inner .droppable-hover').removeClass(
+        'droppable-hover'
+      )
+    })
+  }))

+ 29 - 32
services/web/public/src/ide/file-tree/directives/fileEntity.js

@@ -12,36 +12,33 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.directive('fileEntity', [
-    'RecursionHelper',
-    RecursionHelper => ({
-      restrict: 'E',
-      scope: {
-        entity: '=',
-        permissions: '='
-      },
-      templateUrl: 'entityListItemTemplate',
-      compile(element) {
-        return RecursionHelper.compile(element, function(
-          scope,
-          element,
-          attrs,
-          ctrl
-        ) {
-          // Don't freak out if we're already in an apply callback
-          scope.$originalApply = scope.$apply
-          return (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)
-            }
-          })
+  App.directive('fileEntity', RecursionHelper => ({
+    restrict: 'E',
+    scope: {
+      entity: '=',
+      permissions: '='
+    },
+    templateUrl: 'entityListItemTemplate',
+    compile(element) {
+      return RecursionHelper.compile(element, function(
+        scope,
+        element,
+        attrs,
+        ctrl
+      ) {
+        // Don't freak out if we're already in an apply callback
+        scope.$originalApply = scope.$apply
+        return (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)
+          }
         })
-      }
-    })
-  ]))
+      })
+    }
+  })))

+ 161 - 167
services/web/public/src/ide/history/controllers/HistoryListController.js

@@ -18,206 +18,200 @@ define(['base', 'ide/history/util/displayNameForUser'], function(
   App,
   displayNameForUser
 ) {
-  App.controller('HistoryListController', [
-    '$scope',
-    '$modal',
-    'ide',
-    function($scope, $modal, ide) {
-      $scope.hoveringOverListSelectors = false
-
-      $scope.projectUsers = []
-
-      $scope.$watch('project.members', function(newVal) {
-        if (newVal != null) {
-          return ($scope.projectUsers = newVal.concat($scope.project.owner))
-        }
+  App.controller('HistoryListController', function($scope, $modal, ide) {
+    $scope.hoveringOverListSelectors = false
+
+    $scope.projectUsers = []
+
+    $scope.$watch('project.members', function(newVal) {
+      if (newVal != null) {
+        return ($scope.projectUsers = newVal.concat($scope.project.owner))
+      }
+    })
+
+    // This method (and maybe the one below) will be removed soon. User details data will be
+    // injected into the history API responses, so we won't need to fetch user data from other
+    // local data structures.
+    const _getUserById = id =>
+      _.find($scope.projectUsers, function(user) {
+        const curUserId =
+          (user != null ? user._id : undefined) ||
+          (user != null ? user.id : undefined)
+        return curUserId === id
       })
 
-      // This method (and maybe the one below) will be removed soon. User details data will be
-      // injected into the history API responses, so we won't need to fetch user data from other
-      // local data structures.
-      const _getUserById = id =>
-        _.find($scope.projectUsers, function(user) {
-          const curUserId =
-            (user != null ? user._id : undefined) ||
-            (user != null ? user.id : undefined)
-          return curUserId === id
-        })
-
-      $scope.getDisplayNameById = id => displayNameForUser(_getUserById(id))
-
-      $scope.deleteLabel = labelDetails =>
-        $modal.open({
-          templateUrl: 'historyV2DeleteLabelModalTemplate',
-          controller: 'HistoryV2DeleteLabelModalController',
-          resolve: {
-            labelDetails() {
-              return labelDetails
-            }
+    $scope.getDisplayNameById = id => displayNameForUser(_getUserById(id))
+
+    $scope.deleteLabel = labelDetails =>
+      $modal.open({
+        templateUrl: 'historyV2DeleteLabelModalTemplate',
+        controller: 'HistoryV2DeleteLabelModalController',
+        resolve: {
+          labelDetails() {
+            return labelDetails
           }
-        })
+        }
+      })
 
-      $scope.loadMore = () => {
-        return ide.historyManager.fetchNextBatchOfUpdates()
-      }
+    $scope.loadMore = () => {
+      return ide.historyManager.fetchNextBatchOfUpdates()
+    }
 
-      $scope.recalculateSelectedUpdates = function() {
-        let beforeSelection = true
-        let afterSelection = false
-        $scope.history.selection.updates = []
-        return (() => {
-          const result = []
-          for (let update of Array.from($scope.history.updates)) {
-            var inSelection
-            if (update.selectedTo) {
-              inSelection = true
-              beforeSelection = false
-            }
+    $scope.recalculateSelectedUpdates = function() {
+      let beforeSelection = true
+      let afterSelection = false
+      $scope.history.selection.updates = []
+      return (() => {
+        const result = []
+        for (let update of Array.from($scope.history.updates)) {
+          var inSelection
+          if (update.selectedTo) {
+            inSelection = true
+            beforeSelection = false
+          }
 
-            update.beforeSelection = beforeSelection
-            update.inSelection = inSelection
-            update.afterSelection = afterSelection
+          update.beforeSelection = beforeSelection
+          update.inSelection = inSelection
+          update.afterSelection = afterSelection
 
-            if (inSelection) {
-              $scope.history.selection.updates.push(update)
-            }
+          if (inSelection) {
+            $scope.history.selection.updates.push(update)
+          }
 
-            if (update.selectedFrom) {
-              inSelection = false
-              result.push((afterSelection = true))
-            } else {
-              result.push(undefined)
-            }
+          if (update.selectedFrom) {
+            inSelection = false
+            result.push((afterSelection = true))
+          } else {
+            result.push(undefined)
           }
-          return result
-        })()
+        }
+        return result
+      })()
+    }
+
+    $scope.recalculateHoveredUpdates = function() {
+      let inHoverSelection
+      let hoverSelectedFrom = false
+      let hoverSelectedTo = false
+      for (var update of Array.from($scope.history.updates)) {
+        // Figure out whether the to or from selector is hovered over
+        if (update.hoverSelectedFrom) {
+          hoverSelectedFrom = true
+        }
+        if (update.hoverSelectedTo) {
+          hoverSelectedTo = true
+        }
       }
 
-      $scope.recalculateHoveredUpdates = function() {
-        let inHoverSelection
-        let hoverSelectedFrom = false
-        let hoverSelectedTo = false
-        for (var update of Array.from($scope.history.updates)) {
-          // Figure out whether the to or from selector is hovered over
-          if (update.hoverSelectedFrom) {
-            hoverSelectedFrom = true
+      if (hoverSelectedFrom) {
+        // We want to 'hover select' everything between hoverSelectedFrom and selectedTo
+        inHoverSelection = false
+        for (update of Array.from($scope.history.updates)) {
+          if (update.selectedTo) {
+            update.hoverSelectedTo = true
+            inHoverSelection = true
           }
-          if (update.hoverSelectedTo) {
-            hoverSelectedTo = true
+          update.inHoverSelection = inHoverSelection
+          if (update.hoverSelectedFrom) {
+            inHoverSelection = false
           }
         }
-
-        if (hoverSelectedFrom) {
-          // We want to 'hover select' everything between hoverSelectedFrom and selectedTo
-          inHoverSelection = false
+      }
+      if (hoverSelectedTo) {
+        // We want to 'hover select' everything between hoverSelectedTo and selectedFrom
+        inHoverSelection = false
+        return (() => {
+          const result = []
           for (update of Array.from($scope.history.updates)) {
-            if (update.selectedTo) {
-              update.hoverSelectedTo = true
+            if (update.hoverSelectedTo) {
               inHoverSelection = true
             }
             update.inHoverSelection = inHoverSelection
-            if (update.hoverSelectedFrom) {
-              inHoverSelection = false
-            }
-          }
-        }
-        if (hoverSelectedTo) {
-          // We want to 'hover select' everything between hoverSelectedTo and selectedFrom
-          inHoverSelection = false
-          return (() => {
-            const result = []
-            for (update of Array.from($scope.history.updates)) {
-              if (update.hoverSelectedTo) {
-                inHoverSelection = true
-              }
-              update.inHoverSelection = inHoverSelection
-              if (update.selectedFrom) {
-                update.hoverSelectedFrom = true
-                result.push((inHoverSelection = false))
-              } else {
-                result.push(undefined)
-              }
+            if (update.selectedFrom) {
+              update.hoverSelectedFrom = true
+              result.push((inHoverSelection = false))
+            } else {
+              result.push(undefined)
             }
-            return result
-          })()
-        }
-      }
-
-      $scope.resetHoverState = () =>
-        (() => {
-          const result = []
-          for (let update of Array.from($scope.history.updates)) {
-            delete update.hoverSelectedFrom
-            delete update.hoverSelectedTo
-            result.push(delete update.inHoverSelection)
           }
           return result
         })()
-
-      return $scope.$watch('history.updates.length', () =>
-        $scope.recalculateSelectedUpdates()
-      )
+      }
     }
-  ])
-
-  return App.controller('HistoryListItemController', [
-    '$scope',
-    'event_tracking',
-    function($scope, event_tracking) {
-      $scope.$watch('update.selectedFrom', function(
-        selectedFrom,
-        oldSelectedFrom
-      ) {
-        if (selectedFrom) {
-          for (let update of Array.from($scope.history.updates)) {
-            if (update !== $scope.update) {
-              update.selectedFrom = false
-            }
+
+    $scope.resetHoverState = () =>
+      (() => {
+        const result = []
+        for (let update of Array.from($scope.history.updates)) {
+          delete update.hoverSelectedFrom
+          delete update.hoverSelectedTo
+          result.push(delete update.inHoverSelection)
+        }
+        return result
+      })()
+
+    return $scope.$watch('history.updates.length', () =>
+      $scope.recalculateSelectedUpdates()
+    )
+  })
+
+  return App.controller('HistoryListItemController', function(
+    $scope,
+    event_tracking
+  ) {
+    $scope.$watch('update.selectedFrom', function(
+      selectedFrom,
+      oldSelectedFrom
+    ) {
+      if (selectedFrom) {
+        for (let update of Array.from($scope.history.updates)) {
+          if (update !== $scope.update) {
+            update.selectedFrom = false
           }
-          return $scope.recalculateSelectedUpdates()
         }
-      })
+        return $scope.recalculateSelectedUpdates()
+      }
+    })
 
-      $scope.$watch('update.selectedTo', function(selectedTo, oldSelectedTo) {
-        if (selectedTo) {
-          for (let update of Array.from($scope.history.updates)) {
-            if (update !== $scope.update) {
-              update.selectedTo = false
-            }
+    $scope.$watch('update.selectedTo', function(selectedTo, oldSelectedTo) {
+      if (selectedTo) {
+        for (let update of Array.from($scope.history.updates)) {
+          if (update !== $scope.update) {
+            update.selectedTo = false
           }
-          return $scope.recalculateSelectedUpdates()
         }
-      })
-
-      $scope.select = function() {
-        event_tracking.sendMB('history-view-change')
-        $scope.update.selectedTo = true
-        return ($scope.update.selectedFrom = true)
+        return $scope.recalculateSelectedUpdates()
       }
+    })
 
-      $scope.mouseOverSelectedFrom = function() {
-        $scope.history.hoveringOverListSelectors = true
-        $scope.update.hoverSelectedFrom = true
-        return $scope.recalculateHoveredUpdates()
-      }
+    $scope.select = function() {
+      event_tracking.sendMB('history-view-change')
+      $scope.update.selectedTo = true
+      return ($scope.update.selectedFrom = true)
+    }
 
-      $scope.mouseOutSelectedFrom = function() {
-        $scope.history.hoveringOverListSelectors = false
-        return $scope.resetHoverState()
-      }
+    $scope.mouseOverSelectedFrom = function() {
+      $scope.history.hoveringOverListSelectors = true
+      $scope.update.hoverSelectedFrom = true
+      return $scope.recalculateHoveredUpdates()
+    }
 
-      $scope.mouseOverSelectedTo = function() {
-        $scope.history.hoveringOverListSelectors = true
-        $scope.update.hoverSelectedTo = true
-        return $scope.recalculateHoveredUpdates()
-      }
+    $scope.mouseOutSelectedFrom = function() {
+      $scope.history.hoveringOverListSelectors = false
+      return $scope.resetHoverState()
+    }
 
-      $scope.mouseOutSelectedTo = function() {
-        $scope.history.hoveringOverListSelectors = false
-        return $scope.resetHoverState()
-      }
+    $scope.mouseOverSelectedTo = function() {
+      $scope.history.hoveringOverListSelectors = true
+      $scope.update.hoverSelectedTo = true
+      return $scope.recalculateHoveredUpdates()
+    }
 
-      return ($scope.displayName = displayNameForUser)
+    $scope.mouseOutSelectedTo = function() {
+      $scope.history.hoveringOverListSelectors = false
+      return $scope.resetHoverState()
     }
-  ])
+
+    return ($scope.displayName = displayNameForUser)
+  })
 })

+ 34 - 35
services/web/public/src/ide/history/controllers/HistoryV2AddLabelModalController.js

@@ -11,40 +11,39 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.controller('HistoryV2AddLabelModalController', [
-    '$scope',
-    '$modalInstance',
-    'ide',
-    'update',
-    function($scope, $modalInstance, ide, update) {
-      $scope.update = update
-      $scope.inputs = { labelName: null }
-      $scope.state = {
-        inflight: false,
-        error: false
-      }
+  App.controller('HistoryV2AddLabelModalController', function(
+    $scope,
+    $modalInstance,
+    ide,
+    update
+  ) {
+    $scope.update = update
+    $scope.inputs = { labelName: null }
+    $scope.state = {
+      inflight: false,
+      error: false
+    }
 
-      $modalInstance.opened.then(() =>
-        $scope.$applyAsync(() => $scope.$broadcast('open'))
-      )
+    $modalInstance.opened.then(() =>
+      $scope.$applyAsync(() => $scope.$broadcast('open'))
+    )
 
-      return ($scope.addLabelModalFormSubmit = function() {
-        $scope.state.inflight = true
-        return ide.historyManager
-          .labelCurrentVersion($scope.inputs.labelName)
-          .then(function(response) {
-            $scope.state.inflight = false
-            return $modalInstance.close()
-          })
-          .catch(function(response) {
-            const { data, status } = response
-            $scope.state.inflight = false
-            if (status === 400) {
-              return ($scope.state.error = { message: data })
-            } else {
-              return ($scope.state.error = true)
-            }
-          })
-      })
-    }
-  ]))
+    return ($scope.addLabelModalFormSubmit = function() {
+      $scope.state.inflight = true
+      return ide.historyManager
+        .labelCurrentVersion($scope.inputs.labelName)
+        .then(function(response) {
+          $scope.state.inflight = false
+          return $modalInstance.close()
+        })
+        .catch(function(response) {
+          const { data, status } = response
+          $scope.state.inflight = false
+          if (status === 400) {
+            return ($scope.state.error = { message: data })
+          } else {
+            return ($scope.state.error = true)
+          }
+        })
+    })
+  }))

+ 30 - 31
services/web/public/src/ide/history/controllers/HistoryV2DeleteLabelModalController.js

@@ -11,35 +11,34 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.controller('HistoryV2DeleteLabelModalController', [
-    '$scope',
-    '$modalInstance',
-    'ide',
-    'labelDetails',
-    function($scope, $modalInstance, ide, labelDetails) {
-      $scope.labelDetails = labelDetails
-      $scope.state = {
-        inflight: false,
-        error: false
-      }
-
-      return ($scope.deleteLabel = function() {
-        $scope.state.inflight = true
-        return ide.historyManager
-          .deleteLabel(labelDetails)
-          .then(function(response) {
-            $scope.state.inflight = false
-            return $modalInstance.close()
-          })
-          .catch(function(response) {
-            const { data, status } = response
-            $scope.state.inflight = false
-            if (status === 400) {
-              return ($scope.state.error = { message: data })
-            } else {
-              return ($scope.state.error = true)
-            }
-          })
-      })
+  App.controller('HistoryV2DeleteLabelModalController', function(
+    $scope,
+    $modalInstance,
+    ide,
+    labelDetails
+  ) {
+    $scope.labelDetails = labelDetails
+    $scope.state = {
+      inflight: false,
+      error: false
     }
-  ]))
+
+    return ($scope.deleteLabel = function() {
+      $scope.state.inflight = true
+      return ide.historyManager
+        .deleteLabel(labelDetails)
+        .then(function(response) {
+          $scope.state.inflight = false
+          return $modalInstance.close()
+        })
+        .catch(function(response) {
+          const { data, status } = response
+          $scope.state.inflight = false
+          if (status === 400) {
+            return ($scope.state.error = { message: data })
+          } else {
+            return ($scope.state.error = true)
+          }
+        })
+    })
+  }))

+ 4 - 8
services/web/public/src/ide/history/controllers/HistoryV2FileTreeController.js

@@ -12,12 +12,8 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.controller('HistoryV2FileTreeController', [
-    '$scope',
-    'ide',
-    function($scope, ide) {
-      $scope.handleFileSelection = file => {
-        ide.historyManager.selectFile(file)
-      }
+  App.controller('HistoryV2FileTreeController', function($scope, ide) {
+    $scope.handleFileSelection = file => {
+      ide.historyManager.selectFile(file)
     }
-  ]))
+  }))

+ 30 - 38
services/web/public/src/ide/history/controllers/HistoryV2ListController.js

@@ -15,48 +15,40 @@ define(['base', 'ide/history/util/displayNameForUser'], (
   App,
   displayNameForUser
 ) =>
-  App.controller('HistoryV2ListController', [
-    '$scope',
-    '$modal',
-    'ide',
-    function($scope, $modal, ide) {
-      $scope.hoveringOverListSelectors = false
-      $scope.listConfig = { showOnlyLabelled: false }
+  App.controller('HistoryV2ListController', function($scope, $modal, ide) {
+    $scope.hoveringOverListSelectors = false
+    $scope.listConfig = { showOnlyLabelled: false }
 
-      $scope.projectUsers = []
+    $scope.projectUsers = []
 
-      $scope.$watch('project.members', function(newVal) {
-        if (newVal != null) {
-          return ($scope.projectUsers = newVal.concat($scope.project.owner))
-        }
-      })
-
-      $scope.loadMore = () => {
-        return ide.historyManager.fetchNextBatchOfUpdates()
+    $scope.$watch('project.members', function(newVal) {
+      if (newVal != null) {
+        return ($scope.projectUsers = newVal.concat($scope.project.owner))
       }
+    })
+
+    $scope.loadMore = () => {
+      return ide.historyManager.fetchNextBatchOfUpdates()
+    }
 
-      $scope.handleVersionSelect = version =>
-        $scope.$applyAsync(() =>
-          ide.historyManager.selectVersionForPointInTime(version)
-        )
+    $scope.handleVersionSelect = version =>
+      $scope.$applyAsync(() =>
+        ide.historyManager.selectVersionForPointInTime(version)
+      )
 
-      $scope.handleRangeSelect = (selectedToV, selectedFromV) =>
-        $scope.$applyAsync(() =>
-          ide.historyManager.selectVersionsForCompare(
-            selectedToV,
-            selectedFromV
-          )
-        )
+    $scope.handleRangeSelect = (selectedToV, selectedFromV) =>
+      $scope.$applyAsync(() =>
+        ide.historyManager.selectVersionsForCompare(selectedToV, selectedFromV)
+      )
 
-      return ($scope.handleLabelDelete = labelDetails =>
-        $modal.open({
-          templateUrl: 'historyV2DeleteLabelModalTemplate',
-          controller: 'HistoryV2DeleteLabelModalController',
-          resolve: {
-            labelDetails() {
-              return labelDetails
-            }
+    return ($scope.handleLabelDelete = labelDetails =>
+      $modal.open({
+        templateUrl: 'historyV2DeleteLabelModalTemplate',
+        controller: 'HistoryV2DeleteLabelModalController',
+        resolve: {
+          labelDetails() {
+            return labelDetails
           }
-        }))
-    }
-  ]))
+        }
+      }))
+  }))

+ 3 - 7
services/web/public/src/ide/history/controllers/HistoryV2ToolbarController.js

@@ -12,12 +12,8 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.controller('HistoryV2ToolbarController', [
-    '$scope',
-    '$modal',
-    'ide',
-    'event_tracking',
-    'waitFor',
+  App.controller(
+    'HistoryV2ToolbarController',
     ($scope, $modal, ide, event_tracking, waitFor) => {
       let openEntity
 
@@ -126,4 +122,4 @@ define(['base'], App =>
           .catch(err => console.warn(err))
       }
     }
-  ]))
+  ))

+ 166 - 180
services/web/public/src/ide/pdf/controllers/PdfController.js

@@ -30,7 +30,7 @@ define([
   // and then again on ack.
   const AUTO_COMPILE_DEBOUNCE = 2000
 
-  App.filter('trusted', ['$sce', $sce => url => $sce.trustAsResourceUrl(url)])
+  App.filter('trusted', $sce => url => $sce.trustAsResourceUrl(url))
 
   App.controller('PdfController', function(
     $scope,
@@ -898,188 +898,175 @@ define([
       }))
   })
 
-  App.factory('synctex', [
-    'ide',
-    '$http',
-    '$q',
-    function(ide, $http, $q) {
-      // enable per-user containers by default
-      const perUserCompile = true
-
-      const synctex = {
-        syncToPdf(cursorPosition) {
-          const deferred = $q.defer()
-
-          const doc_id = ide.editorManager.getCurrentDocId()
-          if (doc_id == null) {
-            deferred.reject()
-            return deferred.promise
-          }
-          const doc = ide.fileTreeManager.findEntityById(doc_id)
-          if (doc == null) {
-            deferred.reject()
-            return deferred.promise
-          }
-          let path = ide.fileTreeManager.getEntityPath(doc)
-          if (path == null) {
-            deferred.reject()
-            return deferred.promise
-          }
+  App.factory('synctex', function(ide, $http, $q) {
+    // enable per-user containers by default
+    const perUserCompile = true
 
-          // If the root file is folder/main.tex, then synctex sees the
-          // path as folder/./main.tex
-          const rootDocDirname = ide.fileTreeManager.getRootDocDirname()
-          if (rootDocDirname != null && rootDocDirname !== '') {
-            path = path.replace(
-              RegExp(`^${rootDocDirname}`),
-              `${rootDocDirname}/.`
-            )
-          }
+    const synctex = {
+      syncToPdf(cursorPosition) {
+        const deferred = $q.defer()
+
+        const doc_id = ide.editorManager.getCurrentDocId()
+        if (doc_id == null) {
+          deferred.reject()
+          return deferred.promise
+        }
+        const doc = ide.fileTreeManager.findEntityById(doc_id)
+        if (doc == null) {
+          deferred.reject()
+          return deferred.promise
+        }
+        let path = ide.fileTreeManager.getEntityPath(doc)
+        if (path == null) {
+          deferred.reject()
+          return deferred.promise
+        }
 
-          const { row, column } = cursorPosition
+        // If the root file is folder/main.tex, then synctex sees the
+        // path as folder/./main.tex
+        const rootDocDirname = ide.fileTreeManager.getRootDocDirname()
+        if (rootDocDirname != null && rootDocDirname !== '') {
+          path = path.replace(
+            RegExp(`^${rootDocDirname}`),
+            `${rootDocDirname}/.`
+          )
+        }
 
-          $http({
-            url: `/project/${ide.project_id}/sync/code`,
-            method: 'GET',
-            params: {
-              file: path,
-              line: row + 1,
-              column,
-              clsiserverid: ide.clsiServerId
-            }
+        const { row, column } = cursorPosition
+
+        $http({
+          url: `/project/${ide.project_id}/sync/code`,
+          method: 'GET',
+          params: {
+            file: path,
+            line: row + 1,
+            column,
+            clsiserverid: ide.clsiServerId
+          }
+        })
+          .then(function(response) {
+            const { data } = response
+            return deferred.resolve(data.pdf || [])
           })
-            .then(function(response) {
-              const { data } = response
-              return deferred.resolve(data.pdf || [])
-            })
-            .catch(function(response) {
-              const error = response.data
-              return deferred.reject(error)
-            })
+          .catch(function(response) {
+            const error = response.data
+            return deferred.reject(error)
+          })
+
+        return deferred.promise
+      },
 
+      syncToCode(position, options) {
+        let v
+        if (options == null) {
+          options = {}
+        }
+        const deferred = $q.defer()
+        if (position == null) {
+          deferred.reject()
           return deferred.promise
-        },
+        }
 
-        syncToCode(position, options) {
-          let v
-          if (options == null) {
-            options = {}
-          }
-          const deferred = $q.defer()
-          if (position == null) {
-            deferred.reject()
-            return deferred.promise
-          }
+        // FIXME: this actually works better if it's halfway across the
+        // page (or the visible part of the page). Synctex doesn't
+        // always find the right place in the file when the point is at
+        // the edge of the page, it sometimes returns the start of the
+        // next paragraph instead.
+        const h = position.offset.left
+
+        // Compute the vertical position to pass to synctex, which
+        // works with coordinates increasing from the top of the page
+        // down.  This matches the browser's DOM coordinate of the
+        // click point, but the pdf position is measured from the
+        // bottom of the page so we need to invert it.
+        if (
+          options.fromPdfPosition &&
+          (position.pageSize != null ? position.pageSize.height : undefined) !=
+            null
+        ) {
+          v = position.pageSize.height - position.offset.top || 0 // measure from pdf point (inverted)
+        } else {
+          v = position.offset.top || 0 // measure from html click position
+        }
 
-          // FIXME: this actually works better if it's halfway across the
-          // page (or the visible part of the page). Synctex doesn't
-          // always find the right place in the file when the point is at
-          // the edge of the page, it sometimes returns the start of the
-          // next paragraph instead.
-          const h = position.offset.left
-
-          // Compute the vertical position to pass to synctex, which
-          // works with coordinates increasing from the top of the page
-          // down.  This matches the browser's DOM coordinate of the
-          // click point, but the pdf position is measured from the
-          // bottom of the page so we need to invert it.
-          if (
-            options.fromPdfPosition &&
-            (position.pageSize != null
-              ? position.pageSize.height
-              : undefined) != null
-          ) {
-            v = position.pageSize.height - position.offset.top || 0 // measure from pdf point (inverted)
-          } else {
-            v = position.offset.top || 0 // measure from html click position
-          }
+        // It's not clear exactly where we should sync to if it wasn't directly
+        // clicked on, but a little bit down from the very top seems best.
+        if (options.includeVisualOffset) {
+          v += 72 // use the same value as in pdfViewer highlighting visual offset
+        }
 
-          // It's not clear exactly where we should sync to if it wasn't directly
-          // clicked on, but a little bit down from the very top seems best.
-          if (options.includeVisualOffset) {
-            v += 72 // use the same value as in pdfViewer highlighting visual offset
+        $http({
+          url: `/project/${ide.project_id}/sync/pdf`,
+          method: 'GET',
+          params: {
+            page: position.page + 1,
+            h: h.toFixed(2),
+            v: v.toFixed(2),
+            clsiserverid: ide.clsiServerId
           }
-
-          $http({
-            url: `/project/${ide.project_id}/sync/pdf`,
-            method: 'GET',
-            params: {
-              page: position.page + 1,
-              h: h.toFixed(2),
-              v: v.toFixed(2),
-              clsiserverid: ide.clsiServerId
+        })
+          .then(function(response) {
+            const { data } = response
+            if (
+              data.code != null &&
+              data.code.length > 0 &&
+              data.code[0].file !== ''
+            ) {
+              const doc = ide.fileTreeManager.findEntityByPath(
+                data.code[0].file
+              )
+              if (doc == null) {
+                return
+              }
+              return deferred.resolve({ doc, line: data.code[0].line })
+            } else if (data.code[0].file === '') {
+              ide.$scope.sync_tex_error = true
+              setTimeout(() => (ide.$scope.sync_tex_error = false), 4000)
             }
           })
-            .then(function(response) {
-              const { data } = response
-              if (
-                data.code != null &&
-                data.code.length > 0 &&
-                data.code[0].file !== ''
-              ) {
-                const doc = ide.fileTreeManager.findEntityByPath(
-                  data.code[0].file
-                )
-                if (doc == null) {
-                  return
-                }
-                return deferred.resolve({ doc, line: data.code[0].line })
-              } else if (data.code[0].file === '') {
-                ide.$scope.sync_tex_error = true
-                setTimeout(() => (ide.$scope.sync_tex_error = false), 4000)
-              }
-            })
-            .catch(function(response) {
-              const error = response.data
-              return deferred.reject(error)
-            })
+          .catch(function(response) {
+            const error = response.data
+            return deferred.reject(error)
+          })
 
-          return deferred.promise
-        }
+        return deferred.promise
       }
-
-      return synctex
     }
-  ])
-
-  App.controller('PdfSynctexController', [
-    '$scope',
-    'synctex',
-    'ide',
-    function($scope, synctex, ide) {
-      this.cursorPosition = null
-      ide.$scope.$on('cursor:editor:update', (event, cursorPosition) => {
-        this.cursorPosition = cursorPosition
-      })
 
-      $scope.syncToPdf = () => {
-        if (this.cursorPosition == null) {
-          return
-        }
-        return synctex
-          .syncToPdf(this.cursorPosition)
-          .then(highlights => ($scope.pdf.highlights = highlights))
-      }
+    return synctex
+  })
 
-      ide.$scope.$on('cursor:editor:syncToPdf', $scope.syncToPdf)
+  App.controller('PdfSynctexController', function($scope, synctex, ide) {
+    this.cursorPosition = null
+    ide.$scope.$on('cursor:editor:update', (event, cursorPosition) => {
+      this.cursorPosition = cursorPosition
+    })
 
-      return ($scope.syncToCode = () =>
-        synctex
-          .syncToCode($scope.pdf.position, {
-            includeVisualOffset: true,
-            fromPdfPosition: true
-          })
-          .then(function(data) {
-            const { doc, line } = data
-            return ide.editorManager.openDoc(doc, { gotoLine: line })
-          }))
+    $scope.syncToPdf = () => {
+      if (this.cursorPosition == null) {
+        return
+      }
+      return synctex
+        .syncToPdf(this.cursorPosition)
+        .then(highlights => ($scope.pdf.highlights = highlights))
     }
-  ])
 
-  App.controller('PdfLogEntryController', [
-    '$scope',
-    'ide',
-    'event_tracking',
+    ide.$scope.$on('cursor:editor:syncToPdf', $scope.syncToPdf)
+
+    return ($scope.syncToCode = () =>
+      synctex
+        .syncToCode($scope.pdf.position, {
+          includeVisualOffset: true,
+          fromPdfPosition: true
+        })
+        .then(function(data) {
+          const { doc, line } = data
+          return ide.editorManager.openDoc(doc, { gotoLine: line })
+        }))
+  })
+
+  App.controller(
+    'PdfLogEntryController',
     ($scope, ide, event_tracking) =>
       ($scope.openInEditor = function(entry) {
         let column, line
@@ -1099,25 +1086,24 @@ define([
           gotoColumn: column
         })
       })
-  ])
-
-  return App.controller('ClearCacheModalController', [
-    '$scope',
-    '$modalInstance',
-    function($scope, $modalInstance) {
-      $scope.state = { inflight: false }
-
-      $scope.clear = function() {
-        $scope.state.inflight = true
-        return $scope.clearCache().then(function() {
-          $scope.state.inflight = false
-          return $modalInstance.close()
-        })
-      }
+  )
 
-      return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
+  return App.controller('ClearCacheModalController', function(
+    $scope,
+    $modalInstance
+  ) {
+    $scope.state = { inflight: false }
+
+    $scope.clear = function() {
+      $scope.state.inflight = true
+      return $scope.clearCache().then(function() {
+        $scope.state.inflight = false
+        return $modalInstance.close()
+      })
     }
-  ])
+
+    return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
+  })
 })
 
 function __guard__(value, transform) {

+ 32 - 34
services/web/public/src/ide/pdfng/directives/pdfHighlights.js

@@ -16,41 +16,39 @@
 define(['base', 'pdfjs-dist/build/pdf'], (App, PDFJS) =>
   // app = angular.module 'pdfHighlights', []
 
-  App.factory('pdfHighlights', [
-    function() {
-      let pdfHighlights
-      return (pdfHighlights = class pdfHighlights {
-        constructor(options) {
-          this.highlightsLayerDiv = options.highlights[0]
-          this.highlightElements = []
-        }
+  App.factory('pdfHighlights', function() {
+    let pdfHighlights
+    return (pdfHighlights = class pdfHighlights {
+      constructor(options) {
+        this.highlightsLayerDiv = options.highlights[0]
+        this.highlightElements = []
+      }
 
-        addHighlight(viewport, left, top, width, height) {
-          let rect = viewport.convertToViewportRectangle([
-            left,
-            top,
-            left + width,
-            top + height
-          ])
-          rect = PDFJS.Util.normalizeRect(rect)
-          const element = document.createElement('div')
-          element.style.left = Math.floor(rect[0]) + 'px'
-          element.style.top = Math.floor(rect[1]) + 'px'
-          element.style.width = Math.ceil(rect[2] - rect[0]) + 'px'
-          element.style.height = Math.ceil(rect[3] - rect[1]) + 'px'
-          this.highlightElements.push(element)
-          this.highlightsLayerDiv.appendChild(element)
-          return element
-        }
+      addHighlight(viewport, left, top, width, height) {
+        let rect = viewport.convertToViewportRectangle([
+          left,
+          top,
+          left + width,
+          top + height
+        ])
+        rect = PDFJS.Util.normalizeRect(rect)
+        const element = document.createElement('div')
+        element.style.left = Math.floor(rect[0]) + 'px'
+        element.style.top = Math.floor(rect[1]) + 'px'
+        element.style.width = Math.ceil(rect[2] - rect[0]) + 'px'
+        element.style.height = Math.ceil(rect[3] - rect[1]) + 'px'
+        this.highlightElements.push(element)
+        this.highlightsLayerDiv.appendChild(element)
+        return element
+      }
 
-        clearHighlights() {
-          for (let h of Array.from(this.highlightElements)) {
-            if (h != null) {
-              h.parentNode.removeChild(h)
-            }
+      clearHighlights() {
+        for (let h of Array.from(this.highlightElements)) {
+          if (h != null) {
+            h.parentNode.removeChild(h)
           }
-          return (this.highlightElements = [])
         }
-      })
-    }
-  ]))
+        return (this.highlightElements = [])
+      }
+    })
+  }))

+ 121 - 127
services/web/public/src/ide/pdfng/directives/pdfJs.js

@@ -14,148 +14,143 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base', 'ide/pdfng/directives/pdfViewer'], (App, pdfViewer) =>
-  App.directive('pdfng', [
-    '$timeout',
-    'localStorage',
-    ($timeout, localStorage) => ({
-      scope: {
-        pdfSrc: '=',
-        highlights: '=',
-        position: '=',
-        dblClickCallback: '='
-      },
-      link(scope, element, attrs) {
-        scope.loading = false
-        scope.pleaseJumpTo = null
-        scope.scale = null
-        let initializedPosition = false
-        const initializePosition = function() {
-          let position, scale
-          if (initializedPosition) {
-            return
-          }
-          initializedPosition = true
+  App.directive('pdfng', ($timeout, localStorage) => ({
+    scope: {
+      pdfSrc: '=',
+      highlights: '=',
+      position: '=',
+      dblClickCallback: '='
+    },
+    link(scope, element, attrs) {
+      scope.loading = false
+      scope.pleaseJumpTo = null
+      scope.scale = null
+      let initializedPosition = false
+      const initializePosition = function() {
+        let position, scale
+        if (initializedPosition) {
+          return
+        }
+        initializedPosition = true
 
-          if ((scale = localStorage('pdf.scale')) != null) {
-            scope.scale = { scaleMode: scale.scaleMode, scale: +scale.scale }
-          } else {
-            scope.scale = { scaleMode: 'scale_mode_fit_width' }
-          }
+        if ((scale = localStorage('pdf.scale')) != null) {
+          scope.scale = { scaleMode: scale.scaleMode, scale: +scale.scale }
+        } else {
+          scope.scale = { scaleMode: 'scale_mode_fit_width' }
+        }
 
-          if ((position = localStorage(`pdf.position.${attrs.key}`))) {
-            scope.position = {
-              page: +position.page,
-              offset: {
-                top: +position.offset.top,
-                left: +position.offset.left
-              }
+        if ((position = localStorage(`pdf.position.${attrs.key}`))) {
+          scope.position = {
+            page: +position.page,
+            offset: {
+              top: +position.offset.top,
+              left: +position.offset.left
             }
           }
-
-          scope.$on('$destroy', () => {
-            localStorage('pdf.scale', scope.scale)
-            return localStorage(`pdf.position.${attrs.key}`, scope.position)
-          })
-
-          return $(window).unload(() => {
-            localStorage('pdf.scale', scope.scale)
-            return localStorage(`pdf.position.${attrs.key}`, scope.position)
-          })
         }
 
-        const flashControls = () =>
-          scope.$evalAsync(function() {
-            scope.flashControls = true
-            return $timeout(() => (scope.flashControls = false), 1000)
-          })
-
-        scope.$on(
-          'pdfDoubleClick',
-          (event, e) =>
-            typeof scope.dblClickCallback === 'function'
-              ? scope.dblClickCallback({
-                  page: e.page - 1,
-                  offset: { top: e.y, left: e.x }
-                })
-              : undefined
-        )
-
-        scope.$on('flash-controls', () => flashControls())
-
-        scope.$watch('pdfSrc', function(url) {
-          if (url) {
-            scope.loading = true
-            scope.loaded = false
-            scope.progress = 1
-            initializePosition()
-            return flashControls()
-          }
+        scope.$on('$destroy', () => {
+          localStorage('pdf.scale', scope.scale)
+          return localStorage(`pdf.position.${attrs.key}`, scope.position)
         })
 
-        scope.$on('loaded', function() {
-          scope.loaded = true
-          scope.progress = 100
-          return $timeout(function() {
-            scope.loading = false
-            return delete scope.progress
-          }, 500)
+        return $(window).unload(() => {
+          localStorage('pdf.scale', scope.scale)
+          return localStorage(`pdf.position.${attrs.key}`, scope.position)
         })
+      }
 
-        scope.fitToHeight = function() {
-          const scale = angular.copy(scope.scale)
-          scale.scaleMode = 'scale_mode_fit_height'
-          return (scope.scale = scale)
-        }
-
-        scope.fitToWidth = function() {
-          const scale = angular.copy(scope.scale)
-          scale.scaleMode = 'scale_mode_fit_width'
-          return (scope.scale = scale)
-        }
+      const flashControls = () =>
+        scope.$evalAsync(function() {
+          scope.flashControls = true
+          return $timeout(() => (scope.flashControls = false), 1000)
+        })
 
-        scope.zoomIn = function() {
-          const scale = angular.copy(scope.scale)
-          scale.scaleMode = 'scale_mode_value'
-          scale.scale = scale.scale * 1.2
-          return (scope.scale = scale)
+      scope.$on(
+        'pdfDoubleClick',
+        (event, e) =>
+          typeof scope.dblClickCallback === 'function'
+            ? scope.dblClickCallback({
+                page: e.page - 1,
+                offset: { top: e.y, left: e.x }
+              })
+            : undefined
+      )
+
+      scope.$on('flash-controls', () => flashControls())
+
+      scope.$watch('pdfSrc', function(url) {
+        if (url) {
+          scope.loading = true
+          scope.loaded = false
+          scope.progress = 1
+          initializePosition()
+          return flashControls()
         }
-
-        scope.zoomOut = function() {
-          const scale = angular.copy(scope.scale)
-          scale.scaleMode = 'scale_mode_value'
-          scale.scale = scale.scale / 1.2
-          return (scope.scale = scale)
+      })
+
+      scope.$on('loaded', function() {
+        scope.loaded = true
+        scope.progress = 100
+        return $timeout(function() {
+          scope.loading = false
+          return delete scope.progress
+        }, 500)
+      })
+
+      scope.fitToHeight = function() {
+        const scale = angular.copy(scope.scale)
+        scale.scaleMode = 'scale_mode_fit_height'
+        return (scope.scale = scale)
+      }
+
+      scope.fitToWidth = function() {
+        const scale = angular.copy(scope.scale)
+        scale.scaleMode = 'scale_mode_fit_width'
+        return (scope.scale = scale)
+      }
+
+      scope.zoomIn = function() {
+        const scale = angular.copy(scope.scale)
+        scale.scaleMode = 'scale_mode_value'
+        scale.scale = scale.scale * 1.2
+        return (scope.scale = scale)
+      }
+
+      scope.zoomOut = function() {
+        const scale = angular.copy(scope.scale)
+        scale.scaleMode = 'scale_mode_value'
+        scale.scale = scale.scale / 1.2
+        return (scope.scale = scale)
+      }
+
+      if (attrs.resizeOn != null) {
+        for (let event of Array.from(attrs.resizeOn.split(','))) {
+          scope.$on(event, function(e) {})
         }
+      }
+      // console.log 'got a resize event', event, e
 
-        if (attrs.resizeOn != null) {
-          for (let event of Array.from(attrs.resizeOn.split(','))) {
-            scope.$on(event, function(e) {})
+      scope.$on('progress', (event, progress) =>
+        scope.$apply(function() {
+          if (scope.loaded) {
+            return
           }
-        }
-        // console.log 'got a resize event', event, e
-
-        scope.$on('progress', (event, progress) =>
-          scope.$apply(function() {
-            if (scope.loaded) {
-              return
-            }
-            scope.progress = Math.floor(
-              (progress.loaded / progress.total) * 100
-            )
-            if (scope.progress > 100) {
-              scope.progress = 100
-            }
-            if (scope.progress < 0) {
-              return (scope.progress = 0)
-            }
-          })
-        )
+          scope.progress = Math.floor((progress.loaded / progress.total) * 100)
+          if (scope.progress > 100) {
+            scope.progress = 100
+          }
+          if (scope.progress < 0) {
+            return (scope.progress = 0)
+          }
+        })
+      )
 
-        return scope.$on('$destroy', function() {})
-      },
-      // console.log 'pdfjs destroy event'
+      return scope.$on('$destroy', function() {})
+    },
+    // console.log 'pdfjs destroy event'
 
-      template: `\
+    template: `\
 <div data-pdf-viewer class="pdfjs-viewer" pdf-src='pdfSrc' position='position' scale='scale' highlights='highlights' please-jump-to='pleaseJumpTo'></div>
 <div class="pdfjs-controls" ng-class="{'flash': flashControls }">
 	<div class="btn-group">
@@ -201,5 +196,4 @@ define(['base', 'ide/pdfng/directives/pdfViewer'], (App, pdfViewer) =>
 	<div class="progress-bar" ng-style="{ 'width': progress + '%' }"></div>
 </div>\
 `
-    })
-  ]))
+  })))

+ 127 - 132
services/web/public/src/ide/pdfng/directives/pdfPage.js

@@ -17,13 +17,9 @@
 define(['base'], App =>
   // App = angular.module 'pdfPage', ['pdfHighlights']
 
-  App.directive('pdfPage', [
-    '$timeout',
-    'pdfHighlights',
-    'pdfSpinner',
-    ($timeout, pdfHighlights, pdfSpinner) => ({
-      require: '^pdfViewer',
-      template: `\
+  App.directive('pdfPage', ($timeout, pdfHighlights, pdfSpinner) => ({
+    require: '^pdfViewer',
+    template: `\
 <div class="plv-page-view page-view">
 	<div class="pdf-canvas pdfng-empty"></div>
 	<div class="plv-text-layer text-layer"></div>
@@ -31,141 +27,140 @@ define(['base'], App =>
 	<div class="plv-highlights-layer highlights-layer"></div>
 </div>\
 `,
-      link(scope, element, attrs, ctrl) {
-        const canvasElement = $(element).find('.pdf-canvas')
-        const textElement = $(element).find('.text-layer')
-        const annotationsElement = $(element).find('.annotations-layer')
-        const highlightsElement = $(element).find('.highlights-layer')
+    link(scope, element, attrs, ctrl) {
+      const canvasElement = $(element).find('.pdf-canvas')
+      const textElement = $(element).find('.text-layer')
+      const annotationsElement = $(element).find('.annotations-layer')
+      const highlightsElement = $(element).find('.highlights-layer')
 
-        const updatePageSize = function(size) {
-          const h = Math.floor(size[0])
-          const w = Math.floor(size[1])
-          element.height(h)
-          element.width(w)
-          canvasElement.height(h)
-          canvasElement.width(w)
-          return (scope.page.sized = true)
-        }
+      const updatePageSize = function(size) {
+        const h = Math.floor(size[0])
+        const w = Math.floor(size[1])
+        element.height(h)
+        element.width(w)
+        canvasElement.height(h)
+        canvasElement.width(w)
+        return (scope.page.sized = true)
+      }
 
-        // keep track of our page element, so we can access it in the
-        // parent with scope.pages[i].element, and the contained
-        // elements for each part
-        scope.page.element = element
-        scope.page.elementChildren = {
-          canvas: canvasElement,
-          text: textElement,
-          annotations: annotationsElement,
-          highlights: highlightsElement,
-          container: element
-        }
+      // keep track of our page element, so we can access it in the
+      // parent with scope.pages[i].element, and the contained
+      // elements for each part
+      scope.page.element = element
+      scope.page.elementChildren = {
+        canvas: canvasElement,
+        text: textElement,
+        annotations: annotationsElement,
+        highlights: highlightsElement,
+        container: element
+      }
 
-        if (!scope.page.sized) {
-          if (scope.defaultPageSize != null) {
-            updatePageSize(scope.defaultPageSize)
-          } else {
-            // shouldn't get here - the default page size should now
-            // always be set before redraw is called
-            var handler = scope.$watch('defaultPageSize', function(
-              defaultPageSize
-            ) {
-              if (defaultPageSize == null) {
-                return
-              }
-              updatePageSize(defaultPageSize)
-              return handler()
-            })
-          }
+      if (!scope.page.sized) {
+        if (scope.defaultPageSize != null) {
+          updatePageSize(scope.defaultPageSize)
+        } else {
+          // shouldn't get here - the default page size should now
+          // always be set before redraw is called
+          var handler = scope.$watch('defaultPageSize', function(
+            defaultPageSize
+          ) {
+            if (defaultPageSize == null) {
+              return
+            }
+            updatePageSize(defaultPageSize)
+            return handler()
+          })
         }
+      }
 
-        if (scope.page.current) {
-          // console.log 'we must scroll to this page', scope.page.pageNum, 'at position', scope.page.position
-          // this is the current page, we want to scroll it into view
-          // and render it immediately
-          scope.document.renderPage(scope.page)
-          ctrl.setPdfPosition(scope.page, scope.page.position)
-        }
+      if (scope.page.current) {
+        // console.log 'we must scroll to this page', scope.page.pageNum, 'at position', scope.page.position
+        // this is the current page, we want to scroll it into view
+        // and render it immediately
+        scope.document.renderPage(scope.page)
+        ctrl.setPdfPosition(scope.page, scope.page.position)
+      }
 
-        element.on('dblclick', function(e) {
-          const offset = $(element)
-            .find('.pdf-canvas')
-            .offset()
-          const dx = e.pageX - offset.left
-          const dy = e.pageY - offset.top
-          return scope.document
-            .getPdfViewport(scope.page.pageNum)
-            .then(function(viewport) {
-              const pdfPoint = viewport.convertToPdfPoint(dx, dy)
-              const event = {
-                page: scope.page.pageNum,
-                x: pdfPoint[0],
-                y: viewport.viewBox[3] - pdfPoint[1]
-              }
-              return scope.$emit('pdfDoubleClick', event)
-            })
-        })
+      element.on('dblclick', function(e) {
+        const offset = $(element)
+          .find('.pdf-canvas')
+          .offset()
+        const dx = e.pageX - offset.left
+        const dy = e.pageY - offset.top
+        return scope.document
+          .getPdfViewport(scope.page.pageNum)
+          .then(function(viewport) {
+            const pdfPoint = viewport.convertToPdfPoint(dx, dy)
+            const event = {
+              page: scope.page.pageNum,
+              x: pdfPoint[0],
+              y: viewport.viewBox[3] - pdfPoint[1]
+            }
+            return scope.$emit('pdfDoubleClick', event)
+          })
+      })
 
-        const highlightsLayer = new pdfHighlights({
-          highlights: highlightsElement
-        })
+      const highlightsLayer = new pdfHighlights({
+        highlights: highlightsElement
+      })
 
-        scope.$on('pdf:highlights', function(event, highlights) {
-          let h
-          if (highlights == null) {
-            return
-          }
-          if (!(highlights.length > 0)) {
-            return
-          }
-          if (scope.timeoutHandler) {
-            $timeout.cancel(scope.timeoutHandler)
-            highlightsLayer.clearHighlights()
-            scope.timeoutHandler = null
-          }
+      scope.$on('pdf:highlights', function(event, highlights) {
+        let h
+        if (highlights == null) {
+          return
+        }
+        if (!(highlights.length > 0)) {
+          return
+        }
+        if (scope.timeoutHandler) {
+          $timeout.cancel(scope.timeoutHandler)
+          highlightsLayer.clearHighlights()
+          scope.timeoutHandler = null
+        }
 
-          // console.log 'got highlight watch in pdfPage', scope.page
-          const pageHighlights = (() => {
-            const result = []
-            for (h of Array.from(highlights)) {
-              if (h.page === scope.page.pageNum) {
-                result.push(h)
-              }
+        // console.log 'got highlight watch in pdfPage', scope.page
+        const pageHighlights = (() => {
+          const result = []
+          for (h of Array.from(highlights)) {
+            if (h.page === scope.page.pageNum) {
+              result.push(h)
             }
-            return result
-          })()
-          if (!pageHighlights.length) {
-            return
           }
-          scope.document.getPdfViewport(scope.page.pageNum).then(viewport =>
-            (() => {
-              const result1 = []
-              for (let hl of Array.from(pageHighlights)) {
-                // console.log 'adding highlight', h, viewport
-                const top = viewport.viewBox[3] - hl.v
-                result1.push(
-                  highlightsLayer.addHighlight(
-                    viewport,
-                    hl.h,
-                    top,
-                    hl.width,
-                    hl.height
-                  )
+          return result
+        })()
+        if (!pageHighlights.length) {
+          return
+        }
+        scope.document.getPdfViewport(scope.page.pageNum).then(viewport =>
+          (() => {
+            const result1 = []
+            for (let hl of Array.from(pageHighlights)) {
+              // console.log 'adding highlight', h, viewport
+              const top = viewport.viewBox[3] - hl.v
+              result1.push(
+                highlightsLayer.addHighlight(
+                  viewport,
+                  hl.h,
+                  top,
+                  hl.width,
+                  hl.height
                 )
-              }
-              return result1
-            })()
-          )
-          return (scope.timeoutHandler = $timeout(function() {
-            highlightsLayer.clearHighlights()
-            return (scope.timeoutHandler = null)
-          }, 1000))
-        })
+              )
+            }
+            return result1
+          })()
+        )
+        return (scope.timeoutHandler = $timeout(function() {
+          highlightsLayer.clearHighlights()
+          return (scope.timeoutHandler = null)
+        }, 1000))
+      })
 
-        return scope.$on('$destroy', function() {
-          if (scope.timeoutHandler != null) {
-            $timeout.cancel(scope.timeoutHandler)
-            return highlightsLayer.clearHighlights()
-          }
-        })
-      }
-    })
-  ]))
+      return scope.$on('$destroy', function() {
+        if (scope.timeoutHandler != null) {
+          $timeout.cancel(scope.timeoutHandler)
+          return highlightsLayer.clearHighlights()
+        }
+      })
+    }
+  })))

+ 430 - 439
services/web/public/src/ide/pdfng/directives/pdfRenderer.js

@@ -21,512 +21,503 @@
 define(['base', 'pdfjs-dist/build/pdf'], (App, PDFJS) =>
   // App = angular.module 'PDFRenderer', ['pdfAnnotations', 'pdfTextLayer']
 
-  App.factory('PDFRenderer', [
-    '$q',
-    '$timeout',
-    'pdfAnnotations',
-    'pdfTextLayer',
-    'pdfSpinner',
-    function($q, $timeout, pdfAnnotations, pdfTextLayer, pdfSpinner) {
-      let PDFRenderer
-      return (PDFRenderer = (function() {
-        PDFRenderer = class PDFRenderer {
-          static initClass() {
-            this.prototype.JOB_QUEUE_INTERVAL = 25
-            this.prototype.PAGE_LOAD_TIMEOUT = 60 * 1000
-            this.prototype.INDICATOR_DELAY1 = 100 // time to delay before showing the indicator
-            this.prototype.INDICATOR_DELAY2 = 250 // time until the indicator starts animating
-            this.prototype.TEXTLAYER_TIMEOUT = 100
-          }
+  App.factory('PDFRenderer', function(
+    $q,
+    $timeout,
+    pdfAnnotations,
+    pdfTextLayer,
+    pdfSpinner
+  ) {
+    let PDFRenderer
+    return (PDFRenderer = (function() {
+      PDFRenderer = class PDFRenderer {
+        static initClass() {
+          this.prototype.JOB_QUEUE_INTERVAL = 25
+          this.prototype.PAGE_LOAD_TIMEOUT = 60 * 1000
+          this.prototype.INDICATOR_DELAY1 = 100 // time to delay before showing the indicator
+          this.prototype.INDICATOR_DELAY2 = 250 // time until the indicator starts animating
+          this.prototype.TEXTLAYER_TIMEOUT = 100
+        }
 
-          constructor(url, options) {
-            // set up external character mappings - needed for Japanese etc
-            this.url = url
-            this.options = options
-
-            this.scale = this.options.scale || 1
-
-            let disableFontFace
-            if (
-              __guard__(
-                window.location != null ? window.location.search : undefined,
-                x => x.indexOf('disable-font-face=true')
-              ) >= 0
-            ) {
-              disableFontFace = true
-            } else {
-              disableFontFace = false
-            }
-            this.pdfjs = PDFJS.getDocument({
-              url: this.url,
-              cMapUrl: window.pdfCMapsPath,
-              cMapPacked: true,
-              disableFontFace,
-              // Enable fetching with Range headers to restrict individual
-              // requests to 128kb.
-              // To do this correctly we must:
-              // a) disable auto-fetching of the whole file upfront
-              // b) disable streaming (which in this context means streaming of
-              // the response into memory). This isn't supported when using
-              // Range headers, but shouldn't be a problem since we are already
-              // limiting individual response size through chunked range
-              // requests
-              rangeChunkSize: 128 * 1024,
-              disableAutoFetch: !!this.options.disableAutoFetch,
-              disableStream: !!this.options.disableAutoFetch
-            })
-            this.pdfjs.onProgress = this.options.progressCallback
-            this.document = $q.when(this.pdfjs)
-            this.navigateFn = this.options.navigateFn
-            this.spinner = new pdfSpinner()
-            this.resetState()
-            this.document.then(pdfDocument => {
-              return pdfDocument.getDownloadInfo().then(() => {
-                return this.options.loadedCallback()
-              })
-            })
-            this.errorCallback = this.options.errorCallback
-            this.pageSizeChangeCallback = this.options.pageSizeChangeCallback
-            this.pdfjs.promise.catch(exception => {
-              // error getting document
-              return this.errorCallback(exception)
-            })
+        constructor(url, options) {
+          // set up external character mappings - needed for Japanese etc
+          this.url = url
+          this.options = options
+
+          this.scale = this.options.scale || 1
+
+          let disableFontFace
+          if (
+            __guard__(
+              window.location != null ? window.location.search : undefined,
+              x => x.indexOf('disable-font-face=true')
+            ) >= 0
+          ) {
+            disableFontFace = true
+          } else {
+            disableFontFace = false
           }
+          this.pdfjs = PDFJS.getDocument({
+            url: this.url,
+            cMapUrl: window.pdfCMapsPath,
+            cMapPacked: true,
+            disableFontFace,
+            // Enable fetching with Range headers to restrict individual
+            // requests to 128kb.
+            // To do this correctly we must:
+            // a) disable auto-fetching of the whole file upfront
+            // b) disable streaming (which in this context means streaming of
+            // the response into memory). This isn't supported when using
+            // Range headers, but shouldn't be a problem since we are already
+            // limiting individual response size through chunked range
+            // requests
+            rangeChunkSize: 128 * 1024,
+            disableAutoFetch: !!this.options.disableAutoFetch,
+            disableStream: !!this.options.disableAutoFetch
+          })
+          this.pdfjs.onProgress = this.options.progressCallback
+          this.document = $q.when(this.pdfjs)
+          this.navigateFn = this.options.navigateFn
+          this.spinner = new pdfSpinner()
+          this.resetState()
+          this.document.then(pdfDocument => {
+            return pdfDocument.getDownloadInfo().then(() => {
+              return this.options.loadedCallback()
+            })
+          })
+          this.errorCallback = this.options.errorCallback
+          this.pageSizeChangeCallback = this.options.pageSizeChangeCallback
+          this.pdfjs.promise.catch(exception => {
+            // error getting document
+            return this.errorCallback(exception)
+          })
+        }
 
-          resetState() {
-            this.renderQueue = []
-            if (this.queueTimer != null) {
-              clearTimeout(this.queueTimer)
-            }
-            // clear any existing timers, render tasks
-            for (let timer of Array.from(this.spinTimer || [])) {
-              clearTimeout(timer)
-            }
-            for (let page of Array.from(this.pageState || [])) {
-              __guard__(page != null ? page.loadTask : undefined, x =>
-                x.cancel()
-              )
-              __guard__(page != null ? page.renderTask : undefined, x1 =>
-                x1.cancel()
-              )
-            }
-            // initialise/reset the state
-            this.pageState = []
-            this.spinTimer = [] // timers for starting the spinners (to avoid jitter)
-            this.spinTimerDone = [] // array of pages where the spinner has activated
-            return (this.jobs = 0)
+        resetState() {
+          this.renderQueue = []
+          if (this.queueTimer != null) {
+            clearTimeout(this.queueTimer)
           }
-
-          getNumPages() {
-            return this.document.then(pdfDocument => pdfDocument.numPages)
+          // clear any existing timers, render tasks
+          for (let timer of Array.from(this.spinTimer || [])) {
+            clearTimeout(timer)
           }
-
-          getPage(pageNum) {
-            return this.document.then(pdfDocument =>
-              pdfDocument.getPage(pageNum)
+          for (let page of Array.from(this.pageState || [])) {
+            __guard__(page != null ? page.loadTask : undefined, x => x.cancel())
+            __guard__(page != null ? page.renderTask : undefined, x1 =>
+              x1.cancel()
             )
           }
+          // initialise/reset the state
+          this.pageState = []
+          this.spinTimer = [] // timers for starting the spinners (to avoid jitter)
+          this.spinTimerDone = [] // array of pages where the spinner has activated
+          return (this.jobs = 0)
+        }
 
-          getPdfViewport(pageNum, scale) {
-            if (scale == null) {
-              ;({ scale } = this)
-            }
-            return this.document.then(pdfDocument => {
-              return pdfDocument.getPage(pageNum).then(
-                function(page) {
-                  let viewport
-                  return (viewport = page.getViewport(scale))
-                },
-                error => {
-                  return typeof this.errorCallback === 'function'
-                    ? this.errorCallback(error)
-                    : undefined
-                }
-              )
-            })
-          }
+        getNumPages() {
+          return this.document.then(pdfDocument => pdfDocument.numPages)
+        }
 
-          getDestinations() {
-            return this.document.then(pdfDocument =>
-              pdfDocument.getDestinations()
-            )
+        getPage(pageNum) {
+          return this.document.then(pdfDocument => pdfDocument.getPage(pageNum))
+        }
+
+        getPdfViewport(pageNum, scale) {
+          if (scale == null) {
+            ;({ scale } = this)
           }
+          return this.document.then(pdfDocument => {
+            return pdfDocument.getPage(pageNum).then(
+              function(page) {
+                let viewport
+                return (viewport = page.getViewport(scale))
+              },
+              error => {
+                return typeof this.errorCallback === 'function'
+                  ? this.errorCallback(error)
+                  : undefined
+              }
+            )
+          })
+        }
 
-          getDestination(dest) {
-            return this.document.then(
-              pdfDocument => pdfDocument.getDestination(dest),
+        getDestinations() {
+          return this.document.then(pdfDocument =>
+            pdfDocument.getDestinations()
+          )
+        }
+
+        getDestination(dest) {
+          return this.document.then(
+            pdfDocument => pdfDocument.getDestination(dest),
+            error => {
+              return typeof this.errorCallback === 'function'
+                ? this.errorCallback(error)
+                : undefined
+            }
+          )
+        }
+
+        getPageIndex(ref) {
+          return this.document.then(pdfDocument => {
+            return pdfDocument.getPageIndex(ref).then(
+              idx => idx,
               error => {
                 return typeof this.errorCallback === 'function'
                   ? this.errorCallback(error)
                   : undefined
               }
             )
-          }
+          })
+        }
 
-          getPageIndex(ref) {
-            return this.document.then(pdfDocument => {
-              return pdfDocument.getPageIndex(ref).then(
-                idx => idx,
-                error => {
-                  return typeof this.errorCallback === 'function'
-                    ? this.errorCallback(error)
-                    : undefined
-                }
-              )
-            })
-          }
+        getScale() {
+          return this.scale
+        }
 
-          getScale() {
-            return this.scale
-          }
+        setScale(scale) {
+          this.scale = scale
+          return this.resetState()
+        }
 
-          setScale(scale) {
-            this.scale = scale
-            return this.resetState()
+        triggerRenderQueue(interval) {
+          if (interval == null) {
+            interval = this.JOB_QUEUE_INTERVAL
           }
-
-          triggerRenderQueue(interval) {
-            if (interval == null) {
-              interval = this.JOB_QUEUE_INTERVAL
-            }
-            if (this.queueTimer != null) {
-              clearTimeout(this.queueTimer)
-            }
-            return (this.queueTimer = setTimeout(() => {
-              this.queueTimer = null
-              return this.processRenderQueue()
-            }, interval))
+          if (this.queueTimer != null) {
+            clearTimeout(this.queueTimer)
           }
+          return (this.queueTimer = setTimeout(() => {
+            this.queueTimer = null
+            return this.processRenderQueue()
+          }, interval))
+        }
 
-          removeCompletedJob(pagenum) {
-            this.jobs = this.jobs - 1
-            return this.triggerRenderQueue(0)
-          }
+        removeCompletedJob(pagenum) {
+          this.jobs = this.jobs - 1
+          return this.triggerRenderQueue(0)
+        }
 
-          renderPages(pages) {
-            if (this.shuttingDown) {
-              return
-            }
-            this.renderQueue = Array.from(pages).map(page => ({
-              element: page.elementChildren,
-              pagenum: page.pageNum
-            }))
-            return this.triggerRenderQueue()
+        renderPages(pages) {
+          if (this.shuttingDown) {
+            return
           }
+          this.renderQueue = Array.from(pages).map(page => ({
+            element: page.elementChildren,
+            pagenum: page.pageNum
+          }))
+          return this.triggerRenderQueue()
+        }
 
-          renderPage(page) {
-            if (this.shuttingDown) {
-              return
-            }
-            const current = {
-              element: page.elementChildren,
-              pagenum: page.pageNum
-            }
-            this.renderQueue.push(current)
-            return this.processRenderQueue()
+        renderPage(page) {
+          if (this.shuttingDown) {
+            return
           }
-
-          getPageDetails(page) {
-            return [page.element.canvas, page.pagenum]
+          const current = {
+            element: page.elementChildren,
+            pagenum: page.pageNum
           }
+          this.renderQueue.push(current)
+          return this.processRenderQueue()
+        }
 
-          // handle the loading indicators for each page
+        getPageDetails(page) {
+          return [page.element.canvas, page.pagenum]
+        }
+
+        // handle the loading indicators for each page
 
-          startIndicators() {
-            // make an array of the pages in the queue
-            this.queuedPages = []
-            for (var page of Array.from(this.renderQueue)) {
-              this.queuedPages[page.pagenum] = true
+        startIndicators() {
+          // make an array of the pages in the queue
+          this.queuedPages = []
+          for (var page of Array.from(this.renderQueue)) {
+            this.queuedPages[page.pagenum] = true
+          }
+          // clear any unfinished spinner timers on pages that aren't in the queue any more
+          for (let pagenum in this.spinTimer) {
+            if (!this.queuedPages[pagenum]) {
+              clearTimeout(this.spinTimer[pagenum])
+              delete this.spinTimer[pagenum]
             }
-            // clear any unfinished spinner timers on pages that aren't in the queue any more
-            for (let pagenum in this.spinTimer) {
-              if (!this.queuedPages[pagenum]) {
-                clearTimeout(this.spinTimer[pagenum])
-                delete this.spinTimer[pagenum]
+          }
+          // add indicators for any new pages in the current queue
+          return (() => {
+            const result = []
+            for (page of Array.from(this.renderQueue)) {
+              if (
+                !this.spinTimer[page.pagenum] &&
+                !this.spinTimerDone[page.pagenum]
+              ) {
+                result.push(this.startIndicator(page))
               }
             }
-            // add indicators for any new pages in the current queue
-            return (() => {
-              const result = []
-              for (page of Array.from(this.renderQueue)) {
-                if (
-                  !this.spinTimer[page.pagenum] &&
-                  !this.spinTimerDone[page.pagenum]
-                ) {
-                  result.push(this.startIndicator(page))
-                }
+            return result
+          })()
+        }
+
+        startIndicator(page) {
+          const [canvas, pagenum] = Array.from(this.getPageDetails(page))
+          canvas.addClass('pdfng-loading')
+          return (this.spinTimer[pagenum] = setTimeout(() => {
+            for (let queuedPage of Array.from(this.renderQueue)) {
+              if (pagenum === queuedPage.pagenum) {
+                this.spinner.add(canvas, { static: true })
+                this.spinTimerDone[pagenum] = true
+                break
               }
-              return result
-            })()
-          }
+            }
+            return delete this.spinTimer[pagenum]
+          }, this.INDICATOR_DELAY1))
+        }
 
-          startIndicator(page) {
-            const [canvas, pagenum] = Array.from(this.getPageDetails(page))
-            canvas.addClass('pdfng-loading')
+        updateIndicator(page) {
+          const [canvas, pagenum] = Array.from(this.getPageDetails(page))
+          // did the spinner insert itself already?
+          if (this.spinTimerDone[pagenum]) {
             return (this.spinTimer[pagenum] = setTimeout(() => {
-              for (let queuedPage of Array.from(this.renderQueue)) {
-                if (pagenum === queuedPage.pagenum) {
-                  this.spinner.add(canvas, { static: true })
-                  this.spinTimerDone[pagenum] = true
-                  break
-                }
-              }
+              this.spinner.start(canvas)
               return delete this.spinTimer[pagenum]
-            }, this.INDICATOR_DELAY1))
-          }
-
-          updateIndicator(page) {
-            const [canvas, pagenum] = Array.from(this.getPageDetails(page))
-            // did the spinner insert itself already?
-            if (this.spinTimerDone[pagenum]) {
+            }, this.INDICATOR_DELAY2))
+          } else {
+            // stop the existing spin timer
+            clearTimeout(this.spinTimer[pagenum])
+            // start a new one which will also start spinning
+            return (this.spinTimer[pagenum] = setTimeout(() => {
+              this.spinner.add(canvas, { static: true })
+              this.spinTimerDone[pagenum] = true
               return (this.spinTimer[pagenum] = setTimeout(() => {
                 this.spinner.start(canvas)
                 return delete this.spinTimer[pagenum]
               }, this.INDICATOR_DELAY2))
-            } else {
-              // stop the existing spin timer
-              clearTimeout(this.spinTimer[pagenum])
-              // start a new one which will also start spinning
-              return (this.spinTimer[pagenum] = setTimeout(() => {
-                this.spinner.add(canvas, { static: true })
-                this.spinTimerDone[pagenum] = true
-                return (this.spinTimer[pagenum] = setTimeout(() => {
-                  this.spinner.start(canvas)
-                  return delete this.spinTimer[pagenum]
-                }, this.INDICATOR_DELAY2))
-              }, this.INDICATOR_DELAY1))
-            }
+            }, this.INDICATOR_DELAY1))
           }
+        }
 
-          clearIndicator(page) {
-            const [canvas, pagenum] = Array.from(this.getPageDetails(page))
-            this.spinner.stop(canvas)
-            clearTimeout(this.spinTimer[pagenum])
-            delete this.spinTimer[pagenum]
-            return (this.spinTimerDone[pagenum] = true)
+        clearIndicator(page) {
+          const [canvas, pagenum] = Array.from(this.getPageDetails(page))
+          this.spinner.stop(canvas)
+          clearTimeout(this.spinTimer[pagenum])
+          delete this.spinTimer[pagenum]
+          return (this.spinTimerDone[pagenum] = true)
+        }
+
+        // handle the queue of pages to be rendered
+
+        processRenderQueue() {
+          let pageState
+          if (this.shuttingDown) {
+            return
+          }
+          // mark all pages in the queue as loading
+          this.startIndicators()
+          // bail out if there is already a render job running
+          if (this.jobs > 0) {
+            return
           }
+          // take the first page in the queue
+          let page = this.renderQueue.shift()
+          // check if it is in action already
+          while (page != null && this.pageState[page.pagenum] != null) {
+            page = this.renderQueue.shift()
+          }
+          if (page == null) {
+            return
+          }
+          const [element, pagenum] = Array.from([page.element, page.pagenum])
+          this.jobs = this.jobs + 1
 
-          // handle the queue of pages to be rendered
+          // update the spinner to make it spinning (signifies loading has begun)
+          this.updateIndicator(page)
 
-          processRenderQueue() {
-            let pageState
-            if (this.shuttingDown) {
+          let timedOut = false
+          const timer = $timeout(() => {
+            // page load timed out
+            if (loadTask.cancelled) {
               return
-            }
-            // mark all pages in the queue as loading
-            this.startIndicators()
-            // bail out if there is already a render job running
-            if (this.jobs > 0) {
-              return
-            }
-            // take the first page in the queue
-            let page = this.renderQueue.shift()
-            // check if it is in action already
-            while (page != null && this.pageState[page.pagenum] != null) {
-              page = this.renderQueue.shift()
-            }
-            if (page == null) {
-              return
-            }
-            const [element, pagenum] = Array.from([page.element, page.pagenum])
-            this.jobs = this.jobs + 1
+            } // return from cancelled page load
+            __guardMethod__(window.Raven, 'captureMessage', o =>
+              o.captureMessage(
+                `pdfng page load timed out after ${this.PAGE_LOAD_TIMEOUT}ms`
+              )
+            )
+            timedOut = true
+            this.clearIndicator(page)
+            // @jobs = @jobs - 1
+            // @triggerRenderQueue(0)
+            return typeof this.errorCallback === 'function'
+              ? this.errorCallback('timeout')
+              : undefined
+          }, this.PAGE_LOAD_TIMEOUT)
+
+          var loadTask = this.getPage(pagenum)
+
+          loadTask.cancel = function() {
+            return (this.cancelled = true)
+          }
 
-            // update the spinner to make it spinning (signifies loading has begun)
-            this.updateIndicator(page)
+          this.pageState[pagenum] = pageState = { loadTask }
 
-            let timedOut = false
-            const timer = $timeout(() => {
-              // page load timed out
+          return loadTask
+            .then(pageObject => {
+              // page load success
+              $timeout.cancel(timer)
               if (loadTask.cancelled) {
                 return
               } // return from cancelled page load
-              __guardMethod__(window.Raven, 'captureMessage', o =>
-                o.captureMessage(
-                  `pdfng page load timed out after ${this.PAGE_LOAD_TIMEOUT}ms`
-                )
+              pageState.renderTask = this.doRender(element, pagenum, pageObject)
+              return pageState.renderTask.then(
+                () => {
+                  // render task success
+                  this.clearIndicator(page)
+                  pageState.complete = true
+                  delete pageState.renderTask
+                  return this.removeCompletedJob(pagenum)
+                },
+                () => {
+                  // render task failed
+                  // could display an error icon
+                  pageState.complete = false
+                  delete pageState.renderTask
+                  return this.removeCompletedJob(pagenum)
+                }
               )
-              timedOut = true
-              this.clearIndicator(page)
-              // @jobs = @jobs - 1
-              // @triggerRenderQueue(0)
-              return typeof this.errorCallback === 'function'
-                ? this.errorCallback('timeout')
-                : undefined
-            }, this.PAGE_LOAD_TIMEOUT)
-
-            var loadTask = this.getPage(pagenum)
+            })
+            .catch(error => {
+              // page load error
+              $timeout.cancel(timer)
+              return this.clearIndicator(page)
+            })
+        }
 
-            loadTask.cancel = function() {
-              return (this.cancelled = true)
-            }
+        doRender(element, pagenum, page) {
+          const self = this
+          const { scale } = this
 
-            this.pageState[pagenum] = pageState = { loadTask }
-
-            return loadTask
-              .then(pageObject => {
-                // page load success
-                $timeout.cancel(timer)
-                if (loadTask.cancelled) {
-                  return
-                } // return from cancelled page load
-                pageState.renderTask = this.doRender(
-                  element,
-                  pagenum,
-                  pageObject
-                )
-                return pageState.renderTask.then(
-                  () => {
-                    // render task success
-                    this.clearIndicator(page)
-                    pageState.complete = true
-                    delete pageState.renderTask
-                    return this.removeCompletedJob(pagenum)
-                  },
-                  () => {
-                    // render task failed
-                    // could display an error icon
-                    pageState.complete = false
-                    delete pageState.renderTask
-                    return this.removeCompletedJob(pagenum)
-                  }
-                )
-              })
-              .catch(error => {
-                // page load error
-                $timeout.cancel(timer)
-                return this.clearIndicator(page)
-              })
+          if (scale == null) {
+            // scale is undefined, returning
+            return
           }
 
-          doRender(element, pagenum, page) {
-            const self = this
-            const { scale } = this
-
-            if (scale == null) {
-              // scale is undefined, returning
-              return
-            }
-
-            const canvas = $(
-              '<canvas class="pdf-canvas pdfng-rendering"></canvas>'
-            )
-            // In Windows+IE we must have the canvas in the DOM during
-            // rendering to see the fonts defined in the DOM. If we try to
-            // render 'offscreen' then all the text will be sans-serif.
-            // Previously we rendered offscreen and added in the canvas
-            // when rendering was complete.
-            element.canvas.replaceWith(canvas)
-
-            const viewport = page.getViewport(scale)
-
-            const devicePixelRatio = window.devicePixelRatio || 1
-
-            const ctx = canvas[0].getContext('2d')
-            const backingStoreRatio =
-              ctx.webkitBackingStorePixelRatio ||
-              ctx.mozBackingStorePixelRatio ||
-              ctx.msBackingStorePixelRatio ||
-              ctx.oBackingStorePixelRatio ||
-              ctx.backingStorePixelRatio ||
-              1
-            const pixelRatio = devicePixelRatio / backingStoreRatio
-
-            const scaledWidth = (Math.floor(viewport.width) * pixelRatio) | 0
-            const scaledHeight = (Math.floor(viewport.height) * pixelRatio) | 0
-
-            const newWidth = Math.floor(viewport.width)
-            const newHeight = Math.floor(viewport.height)
-
-            canvas[0].height = scaledHeight
-            canvas[0].width = scaledWidth
-
-            canvas.height(newHeight + 'px')
-            canvas.width(newWidth + 'px')
-
-            const oldHeight = element.canvas.height()
-            const oldWidth = element.canvas.width()
-            if (newHeight !== oldHeight || newWidth !== oldWidth) {
-              element.canvas.height(newHeight + 'px')
-              element.canvas.width(newWidth + 'px')
-              element.container.height(newHeight + 'px')
-              element.container.width(newWidth + 'px')
-              if (typeof this.pageSizeChangeCallback === 'function') {
-                this.pageSizeChangeCallback(pagenum, newHeight - oldHeight)
-              }
+          const canvas = $(
+            '<canvas class="pdf-canvas pdfng-rendering"></canvas>'
+          )
+          // In Windows+IE we must have the canvas in the DOM during
+          // rendering to see the fonts defined in the DOM. If we try to
+          // render 'offscreen' then all the text will be sans-serif.
+          // Previously we rendered offscreen and added in the canvas
+          // when rendering was complete.
+          element.canvas.replaceWith(canvas)
+
+          const viewport = page.getViewport(scale)
+
+          const devicePixelRatio = window.devicePixelRatio || 1
+
+          const ctx = canvas[0].getContext('2d')
+          const backingStoreRatio =
+            ctx.webkitBackingStorePixelRatio ||
+            ctx.mozBackingStorePixelRatio ||
+            ctx.msBackingStorePixelRatio ||
+            ctx.oBackingStorePixelRatio ||
+            ctx.backingStorePixelRatio ||
+            1
+          const pixelRatio = devicePixelRatio / backingStoreRatio
+
+          const scaledWidth = (Math.floor(viewport.width) * pixelRatio) | 0
+          const scaledHeight = (Math.floor(viewport.height) * pixelRatio) | 0
+
+          const newWidth = Math.floor(viewport.width)
+          const newHeight = Math.floor(viewport.height)
+
+          canvas[0].height = scaledHeight
+          canvas[0].width = scaledWidth
+
+          canvas.height(newHeight + 'px')
+          canvas.width(newWidth + 'px')
+
+          const oldHeight = element.canvas.height()
+          const oldWidth = element.canvas.width()
+          if (newHeight !== oldHeight || newWidth !== oldWidth) {
+            element.canvas.height(newHeight + 'px')
+            element.canvas.width(newWidth + 'px')
+            element.container.height(newHeight + 'px')
+            element.container.width(newWidth + 'px')
+            if (typeof this.pageSizeChangeCallback === 'function') {
+              this.pageSizeChangeCallback(pagenum, newHeight - oldHeight)
             }
+          }
 
-            const textLayer = new pdfTextLayer({
-              textLayerDiv: element.text[0],
-              viewport,
-              renderer: PDFJS.renderTextLayer
-            })
-
-            const annotationsLayer = new pdfAnnotations({
-              annotations: element.annotations[0],
-              viewport,
-              navigateFn: this.navigateFn
-            })
-
-            const result = page.render({
-              canvasContext: ctx,
-              viewport,
-              transform: [pixelRatio, 0, 0, pixelRatio, 0, 0]
-            })
-
-            const textLayerTimeout = this.TEXTLAYER_TIMEOUT
-
-            result
-              .then(function() {
-                // page render success
-                canvas.removeClass('pdfng-rendering')
-                page.getTextContent({ normalizeWhitespace: true }).then(
-                  function(textContent) {
-                    textLayer.setTextContent(textContent)
-                    return textLayer.render(textLayerTimeout)
-                  },
+          const textLayer = new pdfTextLayer({
+            textLayerDiv: element.text[0],
+            viewport,
+            renderer: PDFJS.renderTextLayer
+          })
+
+          const annotationsLayer = new pdfAnnotations({
+            annotations: element.annotations[0],
+            viewport,
+            navigateFn: this.navigateFn
+          })
+
+          const result = page.render({
+            canvasContext: ctx,
+            viewport,
+            transform: [pixelRatio, 0, 0, pixelRatio, 0, 0]
+          })
+
+          const textLayerTimeout = this.TEXTLAYER_TIMEOUT
+
+          result
+            .then(function() {
+              // page render success
+              canvas.removeClass('pdfng-rendering')
+              page.getTextContent({ normalizeWhitespace: true }).then(
+                function(textContent) {
+                  textLayer.setTextContent(textContent)
+                  return textLayer.render(textLayerTimeout)
+                },
+                error =>
+                  typeof self.errorCallback === 'function'
+                    ? self.errorCallback(error)
+                    : undefined
+              )
+              return page
+                .getAnnotations()
+                .then(
+                  annotations => annotationsLayer.setAnnotations(annotations),
                   error =>
                     typeof self.errorCallback === 'function'
                       ? self.errorCallback(error)
                       : undefined
                 )
-                return page
-                  .getAnnotations()
-                  .then(
-                    annotations => annotationsLayer.setAnnotations(annotations),
-                    error =>
-                      typeof self.errorCallback === 'function'
-                        ? self.errorCallback(error)
-                        : undefined
-                  )
-              })
-              .catch(function(error) {
-                // page render failed
-                if (error.name === 'RenderingCancelledException') {
-                  // do nothing when cancelled
-                } else {
-                  return typeof self.errorCallback === 'function'
-                    ? self.errorCallback(error)
-                    : undefined
-                }
-              })
+            })
+            .catch(function(error) {
+              // page render failed
+              if (error.name === 'RenderingCancelledException') {
+                // do nothing when cancelled
+              } else {
+                return typeof self.errorCallback === 'function'
+                  ? self.errorCallback(error)
+                  : undefined
+              }
+            })
 
-            return result
-          }
+          return result
+        }
 
-          destroy() {
-            this.shuttingDown = true
-            this.resetState()
-            return this.pdfjs.then(function(document) {
-              document.cleanup()
-              return document.destroy()
-            })
-          }
+        destroy() {
+          this.shuttingDown = true
+          this.resetState()
+          return this.pdfjs.then(function(document) {
+            document.cleanup()
+            return document.destroy()
+          })
         }
-        PDFRenderer.initClass()
-        return PDFRenderer
-      })())
-    }
-  ]))
+      }
+      PDFRenderer.initClass()
+      return PDFRenderer
+    })())
+  }))
 
 function __guard__(value, transform) {
   return typeof value !== 'undefined' && value !== null

+ 26 - 28
services/web/public/src/ide/pdfng/directives/pdfSpinner.js

@@ -13,35 +13,33 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.factory('pdfSpinner', [
-    function() {
-      let pdfSpinner
-      return (pdfSpinner = class pdfSpinner {
-        constructor() {}
-        // handler for spinners
+  App.factory('pdfSpinner', function() {
+    let pdfSpinner
+    return (pdfSpinner = class pdfSpinner {
+      constructor() {}
+      // handler for spinners
 
-        add(element, options) {
-          const size = 64
-          const spinner = $(
-            `<div class="pdfng-spinner" style="position: absolute; top: 50%; left:50%; transform: translateX(-50%) translateY(-50%);"><i class="fa fa-spinner${
-              (options != null ? options.static : undefined) ? '' : ' fa-spin'
-            }" style="color: #999"></i></div>`
-          )
-          spinner.css({ 'font-size': size + 'px' })
-          return element.append(spinner)
-        }
+      add(element, options) {
+        const size = 64
+        const spinner = $(
+          `<div class="pdfng-spinner" style="position: absolute; top: 50%; left:50%; transform: translateX(-50%) translateY(-50%);"><i class="fa fa-spinner${
+            (options != null ? options.static : undefined) ? '' : ' fa-spin'
+          }" style="color: #999"></i></div>`
+        )
+        spinner.css({ 'font-size': size + 'px' })
+        return element.append(spinner)
+      }
 
-        start(element) {
-          return element.find('.fa-spinner').addClass('fa-spin')
-        }
+      start(element) {
+        return element.find('.fa-spinner').addClass('fa-spin')
+      }
 
-        stop(element) {
-          return element.find('.fa-spinner').removeClass('fa-spin')
-        }
+      stop(element) {
+        return element.find('.fa-spinner').removeClass('fa-spin')
+      }
 
-        remove(element) {
-          return element.find('.fa-spinner').remove()
-        }
-      })
-    }
-  ]))
+      remove(element) {
+        return element.find('.fa-spinner').remove()
+      }
+    })
+  }))

+ 50 - 52
services/web/public/src/ide/pdfng/directives/pdfTextLayer.js

@@ -16,65 +16,63 @@ define(['base'], App =>
   // uses the PDFJS text layer renderer to provide invisible overlayed
   // text for searching
 
-  App.factory('pdfTextLayer', [
-    function() {
-      let pdfTextLayer
-      return (pdfTextLayer = class pdfTextLayer {
-        constructor(options) {
-          this.textLayerDiv = options.textLayerDiv
-          this.divContentDone = false
-          this.viewport = options.viewport
-          this.textDivs = []
-          this.renderer = options.renderer
-          this.renderingDone = false
-        }
-
-        render(timeout) {
-          if (this.renderingDone || !this.divContentDone) {
-            return
-          }
+  App.factory('pdfTextLayer', function() {
+    let pdfTextLayer
+    return (pdfTextLayer = class pdfTextLayer {
+      constructor(options) {
+        this.textLayerDiv = options.textLayerDiv
+        this.divContentDone = false
+        this.viewport = options.viewport
+        this.textDivs = []
+        this.renderer = options.renderer
+        this.renderingDone = false
+      }
 
-          if (this.textLayerRenderTask != null) {
-            this.textLayerRenderTask.cancel()
-            this.textLayerRenderTask = null
-          }
+      render(timeout) {
+        if (this.renderingDone || !this.divContentDone) {
+          return
+        }
 
-          this.textDivs = []
-          const textLayerFrag = document.createDocumentFragment()
+        if (this.textLayerRenderTask != null) {
+          this.textLayerRenderTask.cancel()
+          this.textLayerRenderTask = null
+        }
 
-          this.textLayerRenderTask = this.renderer({
-            textContent: this.textContent,
-            container: textLayerFrag,
-            viewport: this.viewport,
-            textDivs: this.textDivs,
-            timeout,
-            enhanceTextSelection: this.enhanceTextSelection
-          })
+        this.textDivs = []
+        const textLayerFrag = document.createDocumentFragment()
 
-          const textLayerSuccess = () => {
-            this.textLayerDiv.appendChild(textLayerFrag)
-            return (this.renderingDone = true)
-          }
+        this.textLayerRenderTask = this.renderer({
+          textContent: this.textContent,
+          container: textLayerFrag,
+          viewport: this.viewport,
+          textDivs: this.textDivs,
+          timeout,
+          enhanceTextSelection: this.enhanceTextSelection
+        })
 
-          const textLayerFailure = function() {
-            // canceled or failed to render text layer -- skipping errors
-          }
+        const textLayerSuccess = () => {
+          this.textLayerDiv.appendChild(textLayerFrag)
+          return (this.renderingDone = true)
+        }
 
-          return this.textLayerRenderTask.promise.then(
-            textLayerSuccess,
-            textLayerFailure
-          )
+        const textLayerFailure = function() {
+          // canceled or failed to render text layer -- skipping errors
         }
 
-        setTextContent(textContent) {
-          if (this.textLayerRenderTask) {
-            this.textLayerRenderTask.cancel()
-            this.textLayerRenderTask = null
-          }
+        return this.textLayerRenderTask.promise.then(
+          textLayerSuccess,
+          textLayerFailure
+        )
+      }
 
-          this.textContent = textContent
-          return (this.divContentDone = true)
+      setTextContent(textContent) {
+        if (this.textLayerRenderTask) {
+          this.textLayerRenderTask.cancel()
+          this.textLayerRenderTask = null
         }
-      })
-    }
-  ]))
+
+        this.textContent = textContent
+        return (this.divContentDone = true)
+      }
+    })
+  }))

+ 728 - 744
services/web/public/src/ide/pdfng/directives/pdfViewer.js

@@ -38,825 +38,809 @@ define([
 ) {
   // App = angular.module 'pdfViewerApp', ['pdfPage', 'PDFRenderer', 'pdfHighlights']
 
-  App.controller('pdfViewerController', [
-    '$scope',
-    '$q',
-    '$timeout',
-    'PDFRenderer',
-    '$element',
-    'pdfHighlights',
-    'pdfSpinner',
-    function(
-      $scope,
-      $q,
-      $timeout,
-      PDFRenderer,
-      $element,
-      pdfHighlights,
-      pdfSpinner
-    ) {
-      this.load = function() {
-        // $scope.pages = []
-
-        if ($scope.document != null) {
-          $scope.document.destroy()
-        }
-        $scope.loadCount = $scope.loadCount != null ? $scope.loadCount + 1 : 1
-        // TODO need a proper url manipulation library to add to query string
-        let url = $scope.pdfSrc
-        // add 'pdfng=true' to show that we are using the angular pdfjs viewer
-        const queryStringExists = /\?/.test(url)
-        url = url + (!queryStringExists ? '?' : '&') + 'pdfng=true'
-        // for isolated compiles, load the pdf on-demand because nobody will overwrite it
-        const onDemandLoading = true
-        $scope.document = new PDFRenderer(url, {
-          scale: 1,
-          disableAutoFetch: onDemandLoading ? true : undefined,
-          navigateFn(ref) {
-            // this function captures clicks on the annotation links
-            $scope.navigateTo = ref
-            return $scope.$apply()
-          },
-          progressCallback(progress) {
-            return $scope.$emit('progress', progress)
-          },
-          loadedCallback() {
-            return $scope.$emit('loaded')
-          },
-          errorCallback(error) {
-            __guardMethod__(window.Raven, 'captureMessage', o =>
-              o.captureMessage(`pdfng error ${error}`)
-            )
-            return $scope.$emit('pdf:error', error)
-          },
-          pageSizeChangeCallback(pageNum, deltaH) {
-            return $scope.$broadcast('pdf:page:size-change', pageNum, deltaH)
-          }
-        })
+  App.controller('pdfViewerController', function(
+    $scope,
+    $q,
+    $timeout,
+    PDFRenderer,
+    $element,
+    pdfHighlights,
+    pdfSpinner
+  ) {
+    this.load = function() {
+      // $scope.pages = []
 
-        // we will have all the main information needed to start display
-        // after the following promise is resolved
-        return ($scope.loaded = $q
-          .all({
-            numPages: $scope.document.getNumPages(),
-            // get size of first page as default @ scale 1
-            pdfViewport: $scope.document.getPdfViewport(1, 1)
-          })
-          .then(function(result) {
-            $scope.pdfViewport = result.pdfViewport
-            $scope.pdfPageSize = [
-              result.pdfViewport.height,
-              result.pdfViewport.width
-            ]
-            // console.log 'resolved q.all, page size is', result
-            $scope.$emit('loaded')
-            return ($scope.numPages = result.numPages)
-          })
-          .catch(function(error) {
-            $scope.$emit('pdf:error', error)
-            return $q.reject(error)
-          }))
+      if ($scope.document != null) {
+        $scope.document.destroy()
       }
+      $scope.loadCount = $scope.loadCount != null ? $scope.loadCount + 1 : 1
+      // TODO need a proper url manipulation library to add to query string
+      let url = $scope.pdfSrc
+      // add 'pdfng=true' to show that we are using the angular pdfjs viewer
+      const queryStringExists = /\?/.test(url)
+      url = url + (!queryStringExists ? '?' : '&') + 'pdfng=true'
+      // for isolated compiles, load the pdf on-demand because nobody will overwrite it
+      const onDemandLoading = true
+      $scope.document = new PDFRenderer(url, {
+        scale: 1,
+        disableAutoFetch: onDemandLoading ? true : undefined,
+        navigateFn(ref) {
+          // this function captures clicks on the annotation links
+          $scope.navigateTo = ref
+          return $scope.$apply()
+        },
+        progressCallback(progress) {
+          return $scope.$emit('progress', progress)
+        },
+        loadedCallback() {
+          return $scope.$emit('loaded')
+        },
+        errorCallback(error) {
+          __guardMethod__(window.Raven, 'captureMessage', o =>
+            o.captureMessage(`pdfng error ${error}`)
+          )
+          return $scope.$emit('pdf:error', error)
+        },
+        pageSizeChangeCallback(pageNum, deltaH) {
+          return $scope.$broadcast('pdf:page:size-change', pageNum, deltaH)
+        }
+      })
+
+      // we will have all the main information needed to start display
+      // after the following promise is resolved
+      return ($scope.loaded = $q
+        .all({
+          numPages: $scope.document.getNumPages(),
+          // get size of first page as default @ scale 1
+          pdfViewport: $scope.document.getPdfViewport(1, 1)
+        })
+        .then(function(result) {
+          $scope.pdfViewport = result.pdfViewport
+          $scope.pdfPageSize = [
+            result.pdfViewport.height,
+            result.pdfViewport.width
+          ]
+          // console.log 'resolved q.all, page size is', result
+          $scope.$emit('loaded')
+          return ($scope.numPages = result.numPages)
+        })
+        .catch(function(error) {
+          $scope.$emit('pdf:error', error)
+          return $q.reject(error)
+        }))
+    }
 
-      this.setScale = (scale, containerHeight, containerWidth) =>
-        $scope.loaded
-          .then(function() {
-            let numScale
-            if (scale == null) {
-              scale = {}
-            }
-            if (containerHeight === 0 || containerWidth === 0) {
-              numScale = 1
-            } else if (scale.scaleMode === 'scale_mode_fit_width') {
-              // TODO make this dynamic
-              numScale = (containerWidth - 40) / $scope.pdfPageSize[1]
-            } else if (scale.scaleMode === 'scale_mode_fit_height') {
-              // TODO magic numbers for jquery ui layout
-              numScale = (containerHeight - 20) / $scope.pdfPageSize[0]
-            } else if (scale.scaleMode === 'scale_mode_value') {
-              numScale = scale.scale
-            } else if (scale.scaleMode === 'scale_mode_auto') {
-              // TODO
-            } else {
-              scale.scaleMode = 'scale_mode_fit_width'
-              numScale = (containerWidth - 40) / $scope.pdfPageSize[1]
-            }
+    this.setScale = (scale, containerHeight, containerWidth) =>
+      $scope.loaded
+        .then(function() {
+          let numScale
+          if (scale == null) {
+            scale = {}
+          }
+          if (containerHeight === 0 || containerWidth === 0) {
+            numScale = 1
+          } else if (scale.scaleMode === 'scale_mode_fit_width') {
+            // TODO make this dynamic
+            numScale = (containerWidth - 40) / $scope.pdfPageSize[1]
+          } else if (scale.scaleMode === 'scale_mode_fit_height') {
+            // TODO magic numbers for jquery ui layout
+            numScale = (containerHeight - 20) / $scope.pdfPageSize[0]
+          } else if (scale.scaleMode === 'scale_mode_value') {
+            numScale = scale.scale
+          } else if (scale.scaleMode === 'scale_mode_auto') {
             // TODO
-            $scope.scale.scale = numScale
-            $scope.document.setScale(numScale)
-            return ($scope.defaultPageSize = [
-              numScale * $scope.pdfPageSize[0],
-              numScale * $scope.pdfPageSize[1]
-            ])
-          })
-          // console.log 'in setScale result', $scope.scale.scale, $scope.defaultPageSize
-          .catch(function(error) {
-            $scope.$emit('pdf:error', error)
-            return $q.reject(error)
-          })
+          } else {
+            scale.scaleMode = 'scale_mode_fit_width'
+            numScale = (containerWidth - 40) / $scope.pdfPageSize[1]
+          }
+          // TODO
+          $scope.scale.scale = numScale
+          $scope.document.setScale(numScale)
+          return ($scope.defaultPageSize = [
+            numScale * $scope.pdfPageSize[0],
+            numScale * $scope.pdfPageSize[1]
+          ])
+        })
+        // console.log 'in setScale result', $scope.scale.scale, $scope.defaultPageSize
+        .catch(function(error) {
+          $scope.$emit('pdf:error', error)
+          return $q.reject(error)
+        })
 
-      this.redraw = function(position) {
-        // console.log 'in redraw'
-        // console.log 'reseting pages array for', $scope.numPages
-        $scope.pages = __range__(0, $scope.numPages - 1, true).map(i => ({
-          pageNum: i + 1
-        }))
-        if (position != null && position.page != null) {
-          // console.log 'position is', position.page, position.offset
-          // console.log 'setting current page', position.page
-          let pagenum = position.page
-          if (pagenum > $scope.numPages - 1) {
-            pagenum = $scope.numPages - 1
-          }
-          $scope.pages[pagenum].current = true
-          return ($scope.pages[pagenum].position = position)
+    this.redraw = function(position) {
+      // console.log 'in redraw'
+      // console.log 'reseting pages array for', $scope.numPages
+      $scope.pages = __range__(0, $scope.numPages - 1, true).map(i => ({
+        pageNum: i + 1
+      }))
+      if (position != null && position.page != null) {
+        // console.log 'position is', position.page, position.offset
+        // console.log 'setting current page', position.page
+        let pagenum = position.page
+        if (pagenum > $scope.numPages - 1) {
+          pagenum = $scope.numPages - 1
         }
+        $scope.pages[pagenum].current = true
+        return ($scope.pages[pagenum].position = position)
       }
+    }
 
-      this.zoomIn = function() {
-        // console.log 'zoom in'
-        const newScale = $scope.scale.scale * 1.2
-        return ($scope.forceScale = {
-          scaleMode: 'scale_mode_value',
-          scale: newScale
-        })
-      }
+    this.zoomIn = function() {
+      // console.log 'zoom in'
+      const newScale = $scope.scale.scale * 1.2
+      return ($scope.forceScale = {
+        scaleMode: 'scale_mode_value',
+        scale: newScale
+      })
+    }
 
-      this.zoomOut = function() {
-        // console.log 'zoom out'
-        const newScale = $scope.scale.scale / 1.2
-        return ($scope.forceScale = {
-          scaleMode: 'scale_mode_value',
-          scale: newScale
-        })
-      }
+    this.zoomOut = function() {
+      // console.log 'zoom out'
+      const newScale = $scope.scale.scale / 1.2
+      return ($scope.forceScale = {
+        scaleMode: 'scale_mode_value',
+        scale: newScale
+      })
+    }
 
-      this.fitWidth = () =>
-        // console.log 'fit width'
-        ($scope.forceScale = { scaleMode: 'scale_mode_fit_width' })
-
-      this.fitHeight = () =>
-        // console.log 'fit height'
-        ($scope.forceScale = { scaleMode: 'scale_mode_fit_height' })
-
-      this.checkPosition = () =>
-        // console.log 'check position'
-        ($scope.forceCheck = ($scope.forceCheck || 0) + 1)
-
-      this.showRandomHighlights = () =>
-        // console.log 'show highlights'
-        ($scope.highlights = [
-          {
-            page: 3,
-            h: 100,
-            v: 100,
-            height: 30,
-            width: 200
-          }
-        ])
-
-      // we work with (pagenumber, % of height down page from top)
-      // pdfListView works with (pagenumber, vertical position up page from
-      // bottom measured in pts)
-
-      this.getPdfPosition = function() {
-        // console.log 'in getPdfPosition'
-        let canvasOffset, pdfOffset, viewport
-        let topPageIdx = 0
-        let topPage = $scope.pages[0]
-        // find first visible page
-        const visible = $scope.pages.some(function(page, i) {
-          if (page.visible) {
-            let ref
-            return ([topPageIdx, topPage] = Array.from((ref = [i, page]))), ref
-          }
-        })
-        if (visible && topPage.element != null) {
-          // console.log 'found it', topPageIdx
-        } else {
-          // console.log 'CANNOT FIND TOP PAGE'
-          return
+    this.fitWidth = () =>
+      // console.log 'fit width'
+      ($scope.forceScale = { scaleMode: 'scale_mode_fit_width' })
+
+    this.fitHeight = () =>
+      // console.log 'fit height'
+      ($scope.forceScale = { scaleMode: 'scale_mode_fit_height' })
+
+    this.checkPosition = () =>
+      // console.log 'check position'
+      ($scope.forceCheck = ($scope.forceCheck || 0) + 1)
+
+    this.showRandomHighlights = () =>
+      // console.log 'show highlights'
+      ($scope.highlights = [
+        {
+          page: 3,
+          h: 100,
+          v: 100,
+          height: 30,
+          width: 200
         }
-
-        // console.log 'top page is', topPage.pageNum, topPage.elemTop, topPage.elemBottom, topPage
-        const { top } = topPage.element.offset()
-        const bottom = top + topPage.element.innerHeight()
-        const viewportTop = $element.offset().top
-        const viewportBottom = viewportTop + $element.height()
-        const topVisible = top >= viewportTop && top < viewportBottom
-        const someContentVisible = top < viewportTop && bottom > viewportTop
-        // console.log 'in PdfListView', top, topVisible, someContentVisible, viewportTop
-        if (topVisible) {
-          canvasOffset = 0
-        } else if (someContentVisible) {
-          canvasOffset = viewportTop - top
-        } else {
-          canvasOffset = null
-        }
-        // console.log 'pdfListview position = ', canvasOffset
-        // instead of using promise, check if size is known and revert to
-        // default otherwise
-        // console.log 'looking up viewport', topPage.viewport, $scope.pdfViewport
-        if (topPage.viewport) {
-          ;({ viewport } = topPage)
-          pdfOffset = viewport.convertToPdfPoint(0, canvasOffset)
-        } else {
-          // console.log 'WARNING: had to default to global page size'
-          viewport = $scope.pdfViewport
-          const scaledOffset = canvasOffset / $scope.scale.scale
-          pdfOffset = viewport.convertToPdfPoint(0, scaledOffset)
-        }
-        // console.log 'converted to offset = ', pdfOffset
-        const newPosition = {
-          page: topPageIdx,
-          offset: { top: pdfOffset[1], left: 0 },
-          pageSize: { height: viewport.viewBox[3], width: viewport.viewBox[2] }
-        }
-        return newPosition
+      ])
+
+    // we work with (pagenumber, % of height down page from top)
+    // pdfListView works with (pagenumber, vertical position up page from
+    // bottom measured in pts)
+
+    this.getPdfPosition = function() {
+      // console.log 'in getPdfPosition'
+      let canvasOffset, pdfOffset, viewport
+      let topPageIdx = 0
+      let topPage = $scope.pages[0]
+      // find first visible page
+      const visible = $scope.pages.some(function(page, i) {
+        if (page.visible) {
+          let ref
+          return ([topPageIdx, topPage] = Array.from((ref = [i, page]))), ref
+        }
+      })
+      if (visible && topPage.element != null) {
+        // console.log 'found it', topPageIdx
+      } else {
+        // console.log 'CANNOT FIND TOP PAGE'
+        return
       }
 
-      this.computeOffset = function(page, position) {
-        // console.log 'computing offset for', page, position
-        const { element } = page
-        // console.log 'element =', $(element), 'parent =', $(element).parent()
-        const t1 = __guard__($(element).offset(), x => x.top)
-        const t2 = __guard__(
-          $(element)
-            .parent()
-            .offset(),
-          x1 => x1.top
-        )
-        if (!(t1 != null && t2 != null)) {
-          return $q((resolve, reject) => reject('elements destroyed'))
-        }
-        const pageTop =
-          $(element).offset().top -
-          $(element)
-            .parent()
-            .offset().top
-        // console.log('top of page scroll is', pageTop, 'vs', page.elemTop)
-        // console.log('inner height is', $(element).innerHeight())
-        const currentScroll = $(element)
-          .parent()
-          .scrollTop()
-        const { offset } = position
-        // convert offset to pixels
-        return $scope.document
-          .getPdfViewport(page.pageNum)
-          .then(function(viewport) {
-            page.viewport = viewport
-            const pageOffset = viewport.convertToViewportPoint(
-              offset.left,
-              offset.top
-            )
-            // if the passed-in position doesn't have the page height/width add them now
-            if (position.pageSize == null) {
-              position.pageSize = {
-                height: viewport.viewBox[3],
-                width: viewport.viewBox[2]
-              }
-            }
-            // console.log 'addition offset =', pageOffset
-            // console.log 'total', pageTop + pageOffset[1]
-            return Math.round(pageTop + pageOffset[1] + currentScroll)
-          }) // # 10 is margin
+      // console.log 'top page is', topPage.pageNum, topPage.elemTop, topPage.elemBottom, topPage
+      const { top } = topPage.element.offset()
+      const bottom = top + topPage.element.innerHeight()
+      const viewportTop = $element.offset().top
+      const viewportBottom = viewportTop + $element.height()
+      const topVisible = top >= viewportTop && top < viewportBottom
+      const someContentVisible = top < viewportTop && bottom > viewportTop
+      // console.log 'in PdfListView', top, topVisible, someContentVisible, viewportTop
+      if (topVisible) {
+        canvasOffset = 0
+      } else if (someContentVisible) {
+        canvasOffset = viewportTop - top
+      } else {
+        canvasOffset = null
       }
+      // console.log 'pdfListview position = ', canvasOffset
+      // instead of using promise, check if size is known and revert to
+      // default otherwise
+      // console.log 'looking up viewport', topPage.viewport, $scope.pdfViewport
+      if (topPage.viewport) {
+        ;({ viewport } = topPage)
+        pdfOffset = viewport.convertToPdfPoint(0, canvasOffset)
+      } else {
+        // console.log 'WARNING: had to default to global page size'
+        viewport = $scope.pdfViewport
+        const scaledOffset = canvasOffset / $scope.scale.scale
+        pdfOffset = viewport.convertToPdfPoint(0, scaledOffset)
+      }
+      // console.log 'converted to offset = ', pdfOffset
+      const newPosition = {
+        page: topPageIdx,
+        offset: { top: pdfOffset[1], left: 0 },
+        pageSize: { height: viewport.viewBox[3], width: viewport.viewBox[2] }
+      }
+      return newPosition
+    }
 
-      this.setPdfPosition = function(page, position) {
-        // console.log 'required pdf Position is', position
-        return this.computeOffset(page, position).then(function(offset) {
-          $scope.pleaseScrollTo = offset
-          return ($scope.position = position)
-        })
+    this.computeOffset = function(page, position) {
+      // console.log 'computing offset for', page, position
+      const { element } = page
+      // console.log 'element =', $(element), 'parent =', $(element).parent()
+      const t1 = __guard__($(element).offset(), x => x.top)
+      const t2 = __guard__(
+        $(element)
+          .parent()
+          .offset(),
+        x1 => x1.top
+      )
+      if (!(t1 != null && t2 != null)) {
+        return $q((resolve, reject) => reject('elements destroyed'))
       }
+      const pageTop =
+        $(element).offset().top -
+        $(element)
+          .parent()
+          .offset().top
+      // console.log('top of page scroll is', pageTop, 'vs', page.elemTop)
+      // console.log('inner height is', $(element).innerHeight())
+      const currentScroll = $(element)
+        .parent()
+        .scrollTop()
+      const { offset } = position
+      // convert offset to pixels
+      return $scope.document
+        .getPdfViewport(page.pageNum)
+        .then(function(viewport) {
+          page.viewport = viewport
+          const pageOffset = viewport.convertToViewportPoint(
+            offset.left,
+            offset.top
+          )
+          // if the passed-in position doesn't have the page height/width add them now
+          if (position.pageSize == null) {
+            position.pageSize = {
+              height: viewport.viewBox[3],
+              width: viewport.viewBox[2]
+            }
+          }
+          // console.log 'addition offset =', pageOffset
+          // console.log 'total', pageTop + pageOffset[1]
+          return Math.round(pageTop + pageOffset[1] + currentScroll)
+        }) // # 10 is margin
+    }
 
-      return this
+    this.setPdfPosition = function(page, position) {
+      // console.log 'required pdf Position is', position
+      return this.computeOffset(page, position).then(function(offset) {
+        $scope.pleaseScrollTo = offset
+        return ($scope.position = position)
+      })
     }
-  ])
-
-  return App.directive('pdfViewer', [
-    '$q',
-    '$timeout',
-    'pdfSpinner',
-    ($q, $timeout, pdfSpinner) => ({
-      controller: 'pdfViewerController',
-      controllerAs: 'ctrl',
-      scope: {
-        pdfSrc: '=',
-        highlights: '=',
-        position: '=',
-        scale: '=',
-        pleaseJumpTo: '='
-      },
-      template: `\
+
+    return this
+  })
+
+  return App.directive('pdfViewer', ($q, $timeout, pdfSpinner) => ({
+    controller: 'pdfViewerController',
+    controllerAs: 'ctrl',
+    scope: {
+      pdfSrc: '=',
+      highlights: '=',
+      position: '=',
+      scale: '=',
+      pleaseJumpTo: '='
+    },
+    template: `\
 <div data-pdf-page class='pdf-page-container page-container' ng-repeat='page in pages'></div>\
 `,
-      link(scope, element, attrs, ctrl) {
-        // console.log 'in pdfViewer element is', element
-        // console.log 'attrs', attrs
-        const spinner = new pdfSpinner()
-        let layoutReady = $q.defer()
-        layoutReady.notify('waiting for layout')
-        layoutReady.promise.then(function() {})
-        // console.log 'layoutReady was resolved'
-
-        const renderVisiblePages = function() {
-          const visiblePages = getVisiblePages()
-          const pages = getExtraPages(visiblePages)
-          return scope.document.renderPages(pages)
-        }
-
-        var getVisiblePages = function() {
-          const top = element[0].scrollTop
-          const bottom = top + element[0].clientHeight
-          const visiblePages = _.filter(scope.pages, function(page) {
-            if (page.element == null) {
-              return false
-            }
-            const pageElement = page.element[0]
-            const pageTop = pageElement.offsetTop
-            const pageBottom = pageTop + pageElement.clientHeight
-            page.visible = pageTop < bottom && pageBottom > top
-            return page.visible
-          })
-          return visiblePages
-        }
-
-        var getExtraPages = function(visiblePages) {
-          const extra = []
-          if (visiblePages.length > 0) {
-            const firstVisiblePage = visiblePages[0].pageNum
-            const firstVisiblePageIdx = firstVisiblePage - 1
-            const len = visiblePages.length
-            const lastVisiblePage = visiblePages[len - 1].pageNum
-            const lastVisiblePageIdx = lastVisiblePage - 1
-            // first page after
-            if (lastVisiblePageIdx + 1 < scope.pages.length) {
-              extra.push(scope.pages[lastVisiblePageIdx + 1])
-            }
-            // page before
-            if (firstVisiblePageIdx > 0) {
-              extra.push(scope.pages[firstVisiblePageIdx - 1])
-            }
-            // second page after
-            if (lastVisiblePageIdx + 2 < scope.pages.length) {
-              extra.push(scope.pages[lastVisiblePageIdx + 2])
-            }
+    link(scope, element, attrs, ctrl) {
+      // console.log 'in pdfViewer element is', element
+      // console.log 'attrs', attrs
+      const spinner = new pdfSpinner()
+      let layoutReady = $q.defer()
+      layoutReady.notify('waiting for layout')
+      layoutReady.promise.then(function() {})
+      // console.log 'layoutReady was resolved'
+
+      const renderVisiblePages = function() {
+        const visiblePages = getVisiblePages()
+        const pages = getExtraPages(visiblePages)
+        return scope.document.renderPages(pages)
+      }
+
+      var getVisiblePages = function() {
+        const top = element[0].scrollTop
+        const bottom = top + element[0].clientHeight
+        const visiblePages = _.filter(scope.pages, function(page) {
+          if (page.element == null) {
+            return false
           }
-          return visiblePages.concat(extra)
-        }
+          const pageElement = page.element[0]
+          const pageTop = pageElement.offsetTop
+          const pageBottom = pageTop + pageElement.clientHeight
+          page.visible = pageTop < bottom && pageBottom > top
+          return page.visible
+        })
+        return visiblePages
+      }
 
-        let rescaleTimer = null
-        const queueRescale = function(scale) {
-          // console.log 'call to queueRescale'
-          if (
-            rescaleTimer != null ||
-            layoutTimer != null ||
-            elementTimer != null
-          ) {
-            return
+      var getExtraPages = function(visiblePages) {
+        const extra = []
+        if (visiblePages.length > 0) {
+          const firstVisiblePage = visiblePages[0].pageNum
+          const firstVisiblePageIdx = firstVisiblePage - 1
+          const len = visiblePages.length
+          const lastVisiblePage = visiblePages[len - 1].pageNum
+          const lastVisiblePageIdx = lastVisiblePage - 1
+          // first page after
+          if (lastVisiblePageIdx + 1 < scope.pages.length) {
+            extra.push(scope.pages[lastVisiblePageIdx + 1])
+          }
+          // page before
+          if (firstVisiblePageIdx > 0) {
+            extra.push(scope.pages[firstVisiblePageIdx - 1])
+          }
+          // second page after
+          if (lastVisiblePageIdx + 2 < scope.pages.length) {
+            extra.push(scope.pages[lastVisiblePageIdx + 2])
           }
-          // console.log 'adding to rescale queue'
-          return (rescaleTimer = setTimeout(function() {
-            doRescale(scale)
-            return (rescaleTimer = null)
-          }, 0))
         }
+        return visiblePages.concat(extra)
+      }
 
-        let spinnerTimer = null
-        var doRescale = function(scale) {
-          // console.log 'doRescale', scale
-          if (scale == null) {
-            return
-          }
-          const origposition = angular.copy(scope.position)
-          // console.log 'origposition', origposition
-
-          if (spinnerTimer == null) {
-            spinnerTimer = setTimeout(function() {
-              spinner.add(element)
-              return (spinnerTimer = null)
-            }, 100)
-          }
-          return layoutReady.promise.then(function(parentSize) {
-            const [h, w] = Array.from(parentSize)
-            // console.log 'in promise', h, w
-            return ctrl
-              .setScale(scale, h, w)
-              .then(() =>
-                // console.log 'in setscale then', scale, h, w
-                scope.$evalAsync(function() {
-                  if (spinnerTimer) {
-                    clearTimeout(spinnerTimer)
-                  } else {
-                    spinner.remove(element)
-                  }
-                  // stop displaying the text layer
-                  element.removeClass('pdfjs-viewer-show-text')
-                  ctrl.redraw(origposition)
-                  $timeout(renderVisiblePages)
-                  return (scope.loadSuccess = true)
-                })
-              )
-              .catch(error => scope.$emit('pdf:error', error))
-          })
+      let rescaleTimer = null
+      const queueRescale = function(scale) {
+        // console.log 'call to queueRescale'
+        if (
+          rescaleTimer != null ||
+          layoutTimer != null ||
+          elementTimer != null
+        ) {
+          return
         }
+        // console.log 'adding to rescale queue'
+        return (rescaleTimer = setTimeout(function() {
+          doRescale(scale)
+          return (rescaleTimer = null)
+        }, 0))
+      }
 
-        var elementTimer = null
-        var updateLayout = function() {
-          // if element is zero-sized keep checking until it is ready
-          // console.log 'checking element ready', element.height(), element.width()
-          if (element.height() === 0 || element.width() === 0) {
-            if (elementTimer != null) {
-              return
-            }
-            return (elementTimer = setTimeout(function() {
-              elementTimer = null
-              return updateLayout()
-            }, 1000))
-          } else {
-            scope.parentSize = [element.innerHeight(), element.innerWidth()]
-            // console.log 'resolving layoutReady with', scope.parentSize
-            return $timeout(function() {
-              layoutReady.resolve(scope.parentSize)
-              return scope.$emit('flash-controls')
-            })
-          }
+      let spinnerTimer = null
+      var doRescale = function(scale) {
+        // console.log 'doRescale', scale
+        if (scale == null) {
+          return
+        }
+        const origposition = angular.copy(scope.position)
+        // console.log 'origposition', origposition
+
+        if (spinnerTimer == null) {
+          spinnerTimer = setTimeout(function() {
+            spinner.add(element)
+            return (spinnerTimer = null)
+          }, 100)
         }
+        return layoutReady.promise.then(function(parentSize) {
+          const [h, w] = Array.from(parentSize)
+          // console.log 'in promise', h, w
+          return ctrl
+            .setScale(scale, h, w)
+            .then(() =>
+              // console.log 'in setscale then', scale, h, w
+              scope.$evalAsync(function() {
+                if (spinnerTimer) {
+                  clearTimeout(spinnerTimer)
+                } else {
+                  spinner.remove(element)
+                }
+                // stop displaying the text layer
+                element.removeClass('pdfjs-viewer-show-text')
+                ctrl.redraw(origposition)
+                $timeout(renderVisiblePages)
+                return (scope.loadSuccess = true)
+              })
+            )
+            .catch(error => scope.$emit('pdf:error', error))
+        })
+      }
 
-        var layoutTimer = null
-        const queueLayout = function() {
-          // console.log 'call to queue layout'
-          if (layoutTimer != null) {
+      var elementTimer = null
+      var updateLayout = function() {
+        // if element is zero-sized keep checking until it is ready
+        // console.log 'checking element ready', element.height(), element.width()
+        if (element.height() === 0 || element.width() === 0) {
+          if (elementTimer != null) {
             return
           }
-          // console.log 'added to queue layoyt'
-          layoutReady = $q.defer()
-          return (layoutTimer = setTimeout(function() {
-            // console.log 'calling update layout'
-            updateLayout()
-            // console.log 'setting layout timer to null'
-            return (layoutTimer = null)
-          }, 0))
+          return (elementTimer = setTimeout(function() {
+            elementTimer = null
+            return updateLayout()
+          }, 1000))
+        } else {
+          scope.parentSize = [element.innerHeight(), element.innerWidth()]
+          // console.log 'resolving layoutReady with', scope.parentSize
+          return $timeout(function() {
+            layoutReady.resolve(scope.parentSize)
+            return scope.$emit('flash-controls')
+          })
         }
+      }
 
-        queueLayout()
+      var layoutTimer = null
+      const queueLayout = function() {
+        // console.log 'call to queue layout'
+        if (layoutTimer != null) {
+          return
+        }
+        // console.log 'added to queue layoyt'
+        layoutReady = $q.defer()
+        return (layoutTimer = setTimeout(function() {
+          // console.log 'calling update layout'
+          updateLayout()
+          // console.log 'setting layout timer to null'
+          return (layoutTimer = null)
+        }, 0))
+      }
 
-        // scope.$on 'layout:pdf:view', (e, args) ->
-        //	console.log 'pdf view change', element, e, args
-        //	queueLayout()
+      queueLayout()
 
-        scope.$on('layout:main:resize', () =>
-          // console.log 'GOT LAYOUT-MAIN-RESIZE EVENT'
-          queueLayout()
-        )
+      // scope.$on 'layout:pdf:view', (e, args) ->
+      //	console.log 'pdf view change', element, e, args
+      //	queueLayout()
 
-        scope.$on('layout:pdf:resize', () =>
-          // FIXME we get this event twice
-          // also we need to start a new layout when we get it
-          // console.log 'GOT LAYOUT-PDF-RESIZE EVENT'
-          queueLayout()
-        )
+      scope.$on('layout:main:resize', () =>
+        // console.log 'GOT LAYOUT-MAIN-RESIZE EVENT'
+        queueLayout()
+      )
 
-        scope.$on('pdf:error', function(event, error) {
-          if (error.name === 'RenderingCancelledException') {
-            return
-          }
-          // check if too many retries or file is missing
-          const message = (error != null ? error.message : undefined) || error
-          if (
-            scope.loadCount > 3 ||
-            /^Missing PDF/i.test(message) ||
-            /^loading/i.test(message)
-          ) {
-            scope.$emit('pdf:error:display')
-            return
-          }
-          if (scope.loadSuccess) {
-            return ctrl
-              .load()
-              .then(
-                () =>
-                  // trigger a redraw
-                  (scope.scale = angular.copy(scope.scale))
-              )
-              .catch(error => scope.$emit('pdf:error:display'))
-          } else {
-            scope.$emit('pdf:error:display')
-          }
-        })
+      scope.$on('layout:pdf:resize', () =>
+        // FIXME we get this event twice
+        // also we need to start a new layout when we get it
+        // console.log 'GOT LAYOUT-PDF-RESIZE EVENT'
+        queueLayout()
+      )
 
-        scope.$on('pdf:page:size-change', function(event, pageNum, delta) {
-          // console.log 'page size change event', pageNum, delta
-          const origposition = angular.copy(scope.position)
-          // console.log 'orig position', JSON.stringify(origposition)
-          if (
-            origposition != null &&
-            pageNum - 1 < origposition.page &&
-            delta !== 0
-          ) {
-            const currentScrollTop = element.scrollTop()
-            // console.log 'adjusting scroll from', currentScrollTop, 'by', delta
-            scope.adjustingScroll = true
-            return element.scrollTop(currentScrollTop + delta)
-          }
-        })
+      scope.$on('pdf:error', function(event, error) {
+        if (error.name === 'RenderingCancelledException') {
+          return
+        }
+        // check if too many retries or file is missing
+        const message = (error != null ? error.message : undefined) || error
+        if (
+          scope.loadCount > 3 ||
+          /^Missing PDF/i.test(message) ||
+          /^loading/i.test(message)
+        ) {
+          scope.$emit('pdf:error:display')
+          return
+        }
+        if (scope.loadSuccess) {
+          return ctrl
+            .load()
+            .then(
+              () =>
+                // trigger a redraw
+                (scope.scale = angular.copy(scope.scale))
+            )
+            .catch(error => scope.$emit('pdf:error:display'))
+        } else {
+          scope.$emit('pdf:error:display')
+        }
+      })
+
+      scope.$on('pdf:page:size-change', function(event, pageNum, delta) {
+        // console.log 'page size change event', pageNum, delta
+        const origposition = angular.copy(scope.position)
+        // console.log 'orig position', JSON.stringify(origposition)
+        if (
+          origposition != null &&
+          pageNum - 1 < origposition.page &&
+          delta !== 0
+        ) {
+          const currentScrollTop = element.scrollTop()
+          // console.log 'adjusting scroll from', currentScrollTop, 'by', delta
+          scope.adjustingScroll = true
+          return element.scrollTop(currentScrollTop + delta)
+        }
+      })
+
+      element.on('mousedown', function(e) {
+        // We're checking that the event target isn't the directive root element
+        // to make sure that the click was within a PDF page - no point in showing
+        // the text layer when the click is outside.
+        // If the user clicks a PDF page, the mousedown target will be the canvas
+        // element (or the text layer one). Alternatively, if the event target is
+        // the root element, we can assume that the user has clicked either the
+        // grey background area or the scrollbars.
+        if (e.target !== element[0] && !_hasSelection()) {
+          element.addClass('pdfjs-viewer-show-text')
+          return _setMouseUpHandler()
+        }
+      })
 
-        element.on('mousedown', function(e) {
-          // We're checking that the event target isn't the directive root element
-          // to make sure that the click was within a PDF page - no point in showing
-          // the text layer when the click is outside.
-          // If the user clicks a PDF page, the mousedown target will be the canvas
-          // element (or the text layer one). Alternatively, if the event target is
-          // the root element, we can assume that the user has clicked either the
-          // grey background area or the scrollbars.
-          if (e.target !== element[0] && !_hasSelection()) {
-            element.addClass('pdfjs-viewer-show-text')
-            return _setMouseUpHandler()
-          }
-        })
+      let mouseUpHandler = null // keep track of the handler to avoid adding multiple times
 
-        let mouseUpHandler = null // keep track of the handler to avoid adding multiple times
+      var _setMouseUpHandler = function() {
+        if (mouseUpHandler == null) {
+          return (mouseUpHandler = $(document.body).one(
+            'mouseup',
+            _handleSelectionMouseUp
+          ))
+        }
+      }
 
-        var _setMouseUpHandler = function() {
-          if (mouseUpHandler == null) {
-            return (mouseUpHandler = $(document.body).one(
-              'mouseup',
-              _handleSelectionMouseUp
-            ))
+      var _handleSelectionMouseUp = function() {
+        mouseUpHandler = null // reset handler, has now fired
+        window.setTimeout(function() {
+          const removedClass = _removeClassIfNoSelection()
+          // if we still have a selection we need to keep the handler going
+          if (!removedClass) {
+            return _setMouseUpHandler()
           }
-        }
+        }, 10)
+        return true
+      }
 
-        var _handleSelectionMouseUp = function() {
-          mouseUpHandler = null // reset handler, has now fired
-          window.setTimeout(function() {
-            const removedClass = _removeClassIfNoSelection()
-            // if we still have a selection we need to keep the handler going
-            if (!removedClass) {
-              return _setMouseUpHandler()
-            }
-          }, 10)
+      var _removeClassIfNoSelection = function() {
+        if (_hasSelection()) {
+          return false // didn't remove the text layer
+        } else {
+          element.removeClass('pdfjs-viewer-show-text')
           return true
         }
+      }
 
-        var _removeClassIfNoSelection = function() {
-          if (_hasSelection()) {
-            return false // didn't remove the text layer
-          } else {
-            element.removeClass('pdfjs-viewer-show-text')
-            return true
-          }
-        }
-
-        var _hasSelection = function() {
-          const selection =
-            typeof window.getSelection === 'function'
-              ? window.getSelection()
-              : undefined
-          // check the selection collapsed state in preference to
-          // using selection.toString() as the latter is "" when
-          // the selection is hidden (e.g. while viewing logs)
-          return (
-            selection != null &&
-            _isSelectionWithinPDF(selection) &&
-            !selection.isCollapsed
-          )
-        }
+      var _hasSelection = function() {
+        const selection =
+          typeof window.getSelection === 'function'
+            ? window.getSelection()
+            : undefined
+        // check the selection collapsed state in preference to
+        // using selection.toString() as the latter is "" when
+        // the selection is hidden (e.g. while viewing logs)
+        return (
+          selection != null &&
+          _isSelectionWithinPDF(selection) &&
+          !selection.isCollapsed
+        )
+      }
 
-        var _isSelectionWithinPDF = function(selection) {
-          if (selection.rangeCount === 0) {
-            return false
-          }
-          const selectionAncestorNode = selection.getRangeAt(0)
-            .commonAncestorContainer
-          return (
-            element.find(selectionAncestorNode).length > 0 ||
-            element.is(selectionAncestorNode)
-          )
+      var _isSelectionWithinPDF = function(selection) {
+        if (selection.rangeCount === 0) {
+          return false
         }
+        const selectionAncestorNode = selection.getRangeAt(0)
+          .commonAncestorContainer
+        return (
+          element.find(selectionAncestorNode).length > 0 ||
+          element.is(selectionAncestorNode)
+        )
+      }
 
-        element.on('scroll', function() {
-          // console.log 'scroll event', element.scrollTop(), 'adjusting?', scope.adjustingScroll
-          // scope.scrollPosition = element.scrollTop()
-          if (scope.adjustingScroll) {
-            renderVisiblePages()
-            scope.adjustingScroll = false
-            return
-          }
-          if (scope.scrollHandlerTimeout) {
-            clearTimeout(scope.scrollHandlerTimeout)
-          }
-          return (scope.scrollHandlerTimeout = setTimeout(scrollHandler, 25))
-        })
-
-        var scrollHandler = function() {
+      element.on('scroll', function() {
+        // console.log 'scroll event', element.scrollTop(), 'adjusting?', scope.adjustingScroll
+        // scope.scrollPosition = element.scrollTop()
+        if (scope.adjustingScroll) {
           renderVisiblePages()
-          const newPosition = ctrl.getPdfPosition()
-          if (newPosition != null) {
-            scope.position = newPosition
-          }
-          return (scope.scrollHandlerTimeout = null)
+          scope.adjustingScroll = false
+          return
+        }
+        if (scope.scrollHandlerTimeout) {
+          clearTimeout(scope.scrollHandlerTimeout)
         }
+        return (scope.scrollHandlerTimeout = setTimeout(scrollHandler, 25))
+      })
+
+      var scrollHandler = function() {
+        renderVisiblePages()
+        const newPosition = ctrl.getPdfPosition()
+        if (newPosition != null) {
+          scope.position = newPosition
+        }
+        return (scope.scrollHandlerTimeout = null)
+      }
 
-        scope.$watch('pdfSrc', function(newVal, oldVal) {
-          // console.log 'loading pdf', newVal, oldVal
-          if (newVal == null) {
-            return
-          }
-          scope.loadCount = 0 // new pdf, so reset load count
-          scope.loadSuccess = false
-          return ctrl
-            .load()
-            .then(
-              () =>
-                // trigger a redraw
-                (scope.scale = angular.copy(scope.scale))
-            )
-            .catch(error => scope.$emit('pdf:error', error))
-        })
+      scope.$watch('pdfSrc', function(newVal, oldVal) {
+        // console.log 'loading pdf', newVal, oldVal
+        if (newVal == null) {
+          return
+        }
+        scope.loadCount = 0 // new pdf, so reset load count
+        scope.loadSuccess = false
+        return ctrl
+          .load()
+          .then(
+            () =>
+              // trigger a redraw
+              (scope.scale = angular.copy(scope.scale))
+          )
+          .catch(error => scope.$emit('pdf:error', error))
+      })
 
-        scope.$watch('scale', function(newVal, oldVal) {
-          // no need to set scale when initialising, done in pdfSrc
-          if (newVal === oldVal) {
-            return
-          }
-          // console.log 'XXX calling Setscale in scale watch'
-          return queueRescale(newVal)
-        })
+      scope.$watch('scale', function(newVal, oldVal) {
+        // no need to set scale when initialising, done in pdfSrc
+        if (newVal === oldVal) {
+          return
+        }
+        // console.log 'XXX calling Setscale in scale watch'
+        return queueRescale(newVal)
+      })
 
-        scope.$watch('forceScale', function(newVal, oldVal) {
-          // console.log 'got change in numscale watcher', newVal, oldVal
-          if (newVal == null) {
-            return
-          }
-          return queueRescale(newVal)
-        })
+      scope.$watch('forceScale', function(newVal, oldVal) {
+        // console.log 'got change in numscale watcher', newVal, oldVal
+        if (newVal == null) {
+          return
+        }
+        return queueRescale(newVal)
+      })
 
-        //				scope.$watch 'position', (newVal, oldVal) ->
-        //					console.log 'got change in position watcher', newVal, oldVal
+      //				scope.$watch 'position', (newVal, oldVal) ->
+      //					console.log 'got change in position watcher', newVal, oldVal
 
-        scope.$watch('forceCheck', function(newVal, oldVal) {
-          // console.log 'forceCheck', newVal, oldVal
+      scope.$watch('forceCheck', function(newVal, oldVal) {
+        // console.log 'forceCheck', newVal, oldVal
+        if (newVal == null) {
+          return
+        }
+        scope.adjustingScroll = true // temporarily disable scroll
+        return queueRescale(scope.scale)
+      })
+
+      scope.$watch(
+        'parentSize',
+        function(newVal, oldVal) {
+          // console.log 'XXX in parentSize watch', newVal, oldVal
+          // if newVal == oldVal
+          // 	console.log 'returning because old and new are the same'
+          // 	return
+          // return unless oldVal?
+          // console.log 'XXX calling setScale in parentSize watcher'
           if (newVal == null) {
             return
           }
-          scope.adjustingScroll = true // temporarily disable scroll
           return queueRescale(scope.scale)
-        })
-
-        scope.$watch(
-          'parentSize',
-          function(newVal, oldVal) {
-            // console.log 'XXX in parentSize watch', newVal, oldVal
-            // if newVal == oldVal
-            // 	console.log 'returning because old and new are the same'
-            // 	return
-            // return unless oldVal?
-            // console.log 'XXX calling setScale in parentSize watcher'
-            if (newVal == null) {
-              return
-            }
-            return queueRescale(scope.scale)
-          },
-          true
-        )
+        },
+        true
+      )
 
-        // scope.$watch 'elementWidth', (newVal, oldVal) ->
-        // 	console.log '*** watch INTERVAL element width is', newVal, oldVal
+      // scope.$watch 'elementWidth', (newVal, oldVal) ->
+      // 	console.log '*** watch INTERVAL element width is', newVal, oldVal
 
-        scope.$watch('pleaseScrollTo', function(newVal, oldVal) {
-          // console.log 'got request to ScrollTo', newVal, 'oldVal', oldVal
-          if (newVal == null) {
-            return
-          }
-          scope.adjustingScroll = true // temporarily disable scroll
-          // handler while we reposition
-          $(element).scrollTop(newVal)
-          return (scope.pleaseScrollTo = undefined)
-        })
-
-        scope.$watch('pleaseJumpTo', function(newPosition, oldPosition) {
-          // console.log 'in pleaseJumpTo', newPosition, oldPosition
-          if (newPosition == null) {
-            return
-          }
-          return ctrl.setPdfPosition(
-            scope.pages[newPosition.page - 1],
-            newPosition
-          )
-        })
+      scope.$watch('pleaseScrollTo', function(newVal, oldVal) {
+        // console.log 'got request to ScrollTo', newVal, 'oldVal', oldVal
+        if (newVal == null) {
+          return
+        }
+        scope.adjustingScroll = true // temporarily disable scroll
+        // handler while we reposition
+        $(element).scrollTop(newVal)
+        return (scope.pleaseScrollTo = undefined)
+      })
+
+      scope.$watch('pleaseJumpTo', function(newPosition, oldPosition) {
+        // console.log 'in pleaseJumpTo', newPosition, oldPosition
+        if (newPosition == null) {
+          return
+        }
+        return ctrl.setPdfPosition(
+          scope.pages[newPosition.page - 1],
+          newPosition
+        )
+      })
 
-        scope.$watch('navigateTo', function(newVal, oldVal) {
-          if (newVal == null) {
-            return
-          }
-          // console.log 'got request to navigate to', newVal, 'oldVal', oldVal
-          scope.navigateTo = undefined
-          // console.log 'navigate to', newVal
-          // console.log 'look up page num'
-          return scope.document.getDestination(newVal.dest).then(r =>
-            // console.log 'need to go to', r
-            // console.log 'page ref is', r[0]
-            scope.document.getPageIndex(r[0]).then(function(pidx) {
-              // console.log 'page num is', pidx
-              const page = scope.pages[pidx]
-              return scope.document
-                .getPdfViewport(page.pageNum)
-                .then(function(viewport) {
-                  // console.log 'got viewport', viewport
-                  const coords = viewport.convertToViewportPoint(r[2], r[3])
-                  // console.log	'viewport position', coords
-                  // console.log 'r is', r, 'r[1]', r[1], 'r[1].name', r[1].name
-                  if (r[1].name === 'XYZ') {
-                    // console.log 'XYZ:', r[2], r[3]
-                    const newPosition = {
-                      page: pidx,
-                      offset: { top: r[3], left: r[2] }
-                    }
-                    return ctrl.setPdfPosition(scope.pages[pidx], newPosition)
+      scope.$watch('navigateTo', function(newVal, oldVal) {
+        if (newVal == null) {
+          return
+        }
+        // console.log 'got request to navigate to', newVal, 'oldVal', oldVal
+        scope.navigateTo = undefined
+        // console.log 'navigate to', newVal
+        // console.log 'look up page num'
+        return scope.document.getDestination(newVal.dest).then(r =>
+          // console.log 'need to go to', r
+          // console.log 'page ref is', r[0]
+          scope.document.getPageIndex(r[0]).then(function(pidx) {
+            // console.log 'page num is', pidx
+            const page = scope.pages[pidx]
+            return scope.document
+              .getPdfViewport(page.pageNum)
+              .then(function(viewport) {
+                // console.log 'got viewport', viewport
+                const coords = viewport.convertToViewportPoint(r[2], r[3])
+                // console.log	'viewport position', coords
+                // console.log 'r is', r, 'r[1]', r[1], 'r[1].name', r[1].name
+                if (r[1].name === 'XYZ') {
+                  // console.log 'XYZ:', r[2], r[3]
+                  const newPosition = {
+                    page: pidx,
+                    offset: { top: r[3], left: r[2] }
                   }
-                })
-            })
-          )
-        }) // XXX?
-
-        scope.$watch('highlights', function(areas) {
-          // console.log 'got HIGHLIGHTS in pdfViewer', areas
-          if (areas == null) {
-            return
-          }
-          // console.log 'areas are', areas
-          const highlights = Array.from(areas || []).map(area => ({
-            page: area.page - 1,
-            highlight: {
-              left: area.h,
-              top: area.v,
-              height: area.height,
-              width: area.width
-            }
-          }))
-          // console.log 'highlights', highlights
+                  return ctrl.setPdfPosition(scope.pages[pidx], newPosition)
+                }
+              })
+          })
+        )
+      }) // XXX?
 
-          if (!highlights.length) {
-            return
+      scope.$watch('highlights', function(areas) {
+        // console.log 'got HIGHLIGHTS in pdfViewer', areas
+        if (areas == null) {
+          return
+        }
+        // console.log 'areas are', areas
+        const highlights = Array.from(areas || []).map(area => ({
+          page: area.page - 1,
+          highlight: {
+            left: area.h,
+            top: area.v,
+            height: area.height,
+            width: area.width
           }
+        }))
+        // console.log 'highlights', highlights
 
-          scope.$broadcast('pdf:highlights', areas)
+        if (!highlights.length) {
+          return
+        }
 
-          const first = highlights[0]
+        scope.$broadcast('pdf:highlights', areas)
 
-          // switching between split and full pdf views can cause
-          // highlights to appear before rendering
-          if (!scope.pages) {
-            return // ignore highlight scroll if still rendering
-          }
+        const first = highlights[0]
 
-          const pageNum =
-            scope.pages[first.page] != null
-              ? scope.pages[first.page].pageNum
-              : undefined
+        // switching between split and full pdf views can cause
+        // highlights to appear before rendering
+        if (!scope.pages) {
+          return // ignore highlight scroll if still rendering
+        }
 
-          if (pageNum == null) {
-            return // ignore highlight scroll if page not found
-          }
+        const pageNum =
+          scope.pages[first.page] != null
+            ? scope.pages[first.page].pageNum
+            : undefined
 
-          // use a visual offset of 72pt to match the offset in PdfController syncToCode
-          return scope.document
-            .getPdfViewport(pageNum)
-            .then(function(viewport) {
-              const position = {
-                page: first.page,
-                offset: {
-                  left: first.highlight.left,
-                  top:
-                    viewport.viewBox[3] -
-                    first.highlight.top +
-                    first.highlight.height +
-                    72
-                }
-              }
-              return ctrl.setPdfPosition(scope.pages[first.page], position)
-            })
-        })
+        if (pageNum == null) {
+          return // ignore highlight scroll if page not found
+        }
 
-        return scope.$on('$destroy', function() {
-          // console.log 'handle pdfng directive destroy'
-          if (elementTimer != null) {
-            clearTimeout(elementTimer)
-          }
-          if (layoutTimer != null) {
-            clearTimeout(layoutTimer)
-          }
-          if (rescaleTimer != null) {
-            clearTimeout(rescaleTimer)
-          }
-          if (spinnerTimer != null) {
-            return clearTimeout(spinnerTimer)
+        // use a visual offset of 72pt to match the offset in PdfController syncToCode
+        return scope.document.getPdfViewport(pageNum).then(function(viewport) {
+          const position = {
+            page: first.page,
+            offset: {
+              left: first.highlight.left,
+              top:
+                viewport.viewBox[3] -
+                first.highlight.top +
+                first.highlight.height +
+                72
+            }
           }
+          return ctrl.setPdfPosition(scope.pages[first.page], position)
         })
-      }
-    })
-  ])
+      })
+
+      return scope.$on('$destroy', function() {
+        // console.log 'handle pdfng directive destroy'
+        if (elementTimer != null) {
+          clearTimeout(elementTimer)
+        }
+        if (layoutTimer != null) {
+          clearTimeout(layoutTimer)
+        }
+        if (rescaleTimer != null) {
+          clearTimeout(rescaleTimer)
+        }
+        if (spinnerTimer != null) {
+          return clearTimeout(spinnerTimer)
+        }
+      })
+    }
+  }))
 })
 
 function __guardMethod__(obj, methodName, transform) {

+ 92 - 94
services/web/public/src/ide/services/ide.js

@@ -16,113 +16,111 @@
 define(['base'], function(App) {
   // 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',
-    function($http, queuedHttp, $modal, $q, $filter, $timeout) {
-      const ide = {}
-      ide.$http = $http
-      ide.queuedHttp = queuedHttp
-      ide.$q = $q
-      ide.$filter = $filter
-      ide.$timeout = $timeout
+  App.factory('ide', function(
+    $http,
+    queuedHttp,
+    $modal,
+    $q,
+    $filter,
+    $timeout
+  ) {
+    const ide = {}
+    ide.$http = $http
+    ide.queuedHttp = queuedHttp
+    ide.$q = $q
+    ide.$filter = $filter
+    ide.$timeout = $timeout
 
-      this.recentEvents = []
-      ide.pushEvent = (type, meta) => {
-        if (meta == null) {
-          meta = {}
-        }
-        sl_console.log('event', type, meta)
-        this.recentEvents.push({ type, meta, date: new Date() })
-        if (this.recentEvents.length > 100) {
-          return this.recentEvents.shift()
-        }
+    this.recentEvents = []
+    ide.pushEvent = (type, meta) => {
+      if (meta == null) {
+        meta = {}
       }
+      sl_console.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.client_id = __guard__(
+    ide.reportError = (error, meta) => {
+      if (meta == null) {
+        meta = {}
+      }
+      meta.client_id = __guard__(
+        this.socket != null ? this.socket.socket : undefined,
+        x => x.sessionid
+      )
+      meta.transport = __guard__(
+        __guard__(
           this.socket != null ? this.socket.socket : undefined,
-          x => x.sessionid
-        )
-        meta.transport = __guard__(
-          __guard__(
-            this.socket != null ? this.socket.socket : undefined,
-            x2 => x2.transport
-          ),
-          x1 => x1.name
-        )
-        meta.client_now = new Date()
-        meta.recent_events = this.recentEvents
-        const errorObj = {}
-        if (typeof error === 'object') {
-          for (let key of Array.from(Object.getOwnPropertyNames(error))) {
-            errorObj[key] = error[key]
-          }
-        } else if (typeof error === 'string') {
-          errorObj.message = error
+          x2 => x2.transport
+        ),
+        x1 => x1.name
+      )
+      meta.client_now = new Date()
+      meta.recent_events = this.recentEvents
+      const errorObj = {}
+      if (typeof error === 'object') {
+        for (let key of Array.from(Object.getOwnPropertyNames(error))) {
+          errorObj[key] = error[key]
         }
-        return $http.post('/error/client', {
-          error: errorObj,
-          meta,
-          _csrf: window.csrfToken
-        })
+      } 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.showGenericMessageModal = (title, message) =>
+      $modal.open({
+        templateUrl: 'genericMessageModalTemplate',
+        controller: 'GenericMessageModalController',
+        resolve: {
+          title() {
+            return title
+          },
+          message() {
+            return message
           }
-        })
+        }
+      })
 
-      ide.showLockEditorMessageModal = (title, message) =>
-        // modal to block the editor when connection is down
-        $modal.open({
-          templateUrl: 'lockEditorModalTemplate',
-          controller: 'GenericMessageModalController',
-          backdrop: 'static', // prevent dismiss by click on background
-          keyboard: false, // prevent dismiss via keyboard
-          resolve: {
-            title() {
-              return title
-            },
-            message() {
-              return message
-            }
+    ide.showLockEditorMessageModal = (title, message) =>
+      // modal to block the editor when connection is down
+      $modal.open({
+        templateUrl: 'lockEditorModalTemplate',
+        controller: 'GenericMessageModalController',
+        backdrop: 'static', // prevent dismiss by click on background
+        keyboard: false, // prevent dismiss via keyboard
+        resolve: {
+          title() {
+            return title
           },
-          windowClass: 'lock-editor-modal'
-        })
+          message() {
+            return message
+          }
+        },
+        windowClass: 'lock-editor-modal'
+      })
 
-      return ide
-    }
-  ])
+    return ide
+  })
 
-  return App.controller('GenericMessageModalController', [
-    '$scope',
-    '$modalInstance',
-    'title',
-    'message',
-    function($scope, $modalInstance, title, message) {
-      $scope.title = title
-      $scope.message = message
+  return App.controller('GenericMessageModalController', function(
+    $scope,
+    $modalInstance,
+    title,
+    message
+  ) {
+    $scope.title = title
+    $scope.message = message
 
-      return ($scope.done = () => $modalInstance.close())
-    }
-  ])
+    return ($scope.done = () => $modalInstance.close())
+  })
 })
 
 function __guard__(value, transform) {

+ 56 - 57
services/web/public/src/ide/settings/controllers/ProjectNameController.js

@@ -14,68 +14,67 @@
  */
 define(['base'], function(App) {
   const MAX_PROJECT_NAME_LENGTH = 150
-  return App.controller('ProjectNameController', [
-    '$scope',
-    '$element',
-    'settings',
-    'ide',
-    function($scope, $element, settings, ide) {
-      const projectNameReadOnlyEl = $element.find('.name')[0]
+  return App.controller('ProjectNameController', function(
+    $scope,
+    $element,
+    settings,
+    ide
+  ) {
+    const projectNameReadOnlyEl = $element.find('.name')[0]
 
-      $scope.state = {
-        renaming: false,
-        overflowed: false
-      }
+    $scope.state = {
+      renaming: false,
+      overflowed: false
+    }
 
-      $scope.inputs = {}
+    $scope.inputs = {}
 
-      $scope.startRenaming = function() {
-        $scope.inputs.name = $scope.project.name
-        $scope.state.renaming = true
-        return $scope.$emit('project:rename:start')
-      }
+    $scope.startRenaming = function() {
+      $scope.inputs.name = $scope.project.name
+      $scope.state.renaming = true
+      return $scope.$emit('project:rename:start')
+    }
 
-      $scope.finishRenaming = function() {
-        $scope.state.renaming = false
-        const newName = $scope.inputs.name
-        if ($scope.project.name === newName) {
-          return
-        }
-        const oldName = $scope.project.name
-        $scope.project.name = newName
-        return settings
-          .saveProjectSettings({ name: $scope.project.name })
-          .catch(function(response) {
-            const { data, status } = response
-            $scope.project.name = oldName
-            if (status === 400) {
-              return ide.showGenericMessageModal('Error renaming project', data)
-            } else {
-              return ide.showGenericMessageModal(
-                'Error renaming project',
-                'Please try again in a moment'
-              )
-            }
-          })
+    $scope.finishRenaming = function() {
+      $scope.state.renaming = false
+      const newName = $scope.inputs.name
+      if ($scope.project.name === newName) {
+        return
       }
+      const oldName = $scope.project.name
+      $scope.project.name = newName
+      return settings
+        .saveProjectSettings({ name: $scope.project.name })
+        .catch(function(response) {
+          const { data, status } = response
+          $scope.project.name = oldName
+          if (status === 400) {
+            return ide.showGenericMessageModal('Error renaming project', data)
+          } else {
+            return ide.showGenericMessageModal(
+              'Error renaming project',
+              'Please try again in a moment'
+            )
+          }
+        })
+    }
 
-      ide.socket.on('projectNameUpdated', name =>
-        $scope.$apply(() => ($scope.project.name = name))
-      )
+    ide.socket.on('projectNameUpdated', name =>
+      $scope.$apply(() => ($scope.project.name = name))
+    )
 
-      return $scope.$watch('project.name', function(name) {
-        if (name != null) {
-          window.document.title =
-            name + ` - Online LaTeX Editor ${ExposedSettings.appName}`
-          return $scope.$applyAsync(
-            () =>
-              // This ensures that the element is measured *after* the binding is done (i.e. project name is rendered).
-              ($scope.state.overflowed =
-                projectNameReadOnlyEl.scrollWidth >
-                projectNameReadOnlyEl.clientWidth)
-          )
-        }
-      })
-    }
-  ])
+    return $scope.$watch('project.name', function(name) {
+      if (name != null) {
+        window.document.title =
+          name + ` - Online LaTeX Editor ${ExposedSettings.appName}`
+        return $scope.$applyAsync(
+          () =>
+            // This ensures that the element is measured *after* the binding is done (i.e. project name is rendered).
+            ($scope.state.overflowed =
+              projectNameReadOnlyEl.scrollWidth >
+              projectNameReadOnlyEl.clientWidth)
+        )
+      }
+    })
+  })
 })

+ 162 - 174
services/web/public/src/ide/settings/controllers/SettingsController.js

@@ -13,206 +13,194 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.controller('SettingsController', [
-    '$scope',
-    'settings',
-    'ide',
-    '_',
-    function($scope, settings, ide, _) {
-      $scope.overallThemesList = window.overallThemes
-      $scope.ui = { loadingStyleSheet: false }
-
-      const _updateCSSFile = function(theme) {
-        $scope.ui.loadingStyleSheet = true
-        const docHeadEl = document.querySelector('head')
-        const oldStyleSheetEl = document.getElementById('main-stylesheet')
-        const newStyleSheetEl = document.createElement('link')
-        newStyleSheetEl.addEventListener('load', e => {
-          return $scope.$applyAsync(() => {
-            $scope.ui.loadingStyleSheet = false
-            return docHeadEl.removeChild(oldStyleSheetEl)
-          })
+  App.controller('SettingsController', function($scope, settings, ide, _) {
+    $scope.overallThemesList = window.overallThemes
+    $scope.ui = { loadingStyleSheet: false }
+
+    const _updateCSSFile = function(theme) {
+      $scope.ui.loadingStyleSheet = true
+      const docHeadEl = document.querySelector('head')
+      const oldStyleSheetEl = document.getElementById('main-stylesheet')
+      const newStyleSheetEl = document.createElement('link')
+      newStyleSheetEl.addEventListener('load', e => {
+        return $scope.$applyAsync(() => {
+          $scope.ui.loadingStyleSheet = false
+          return docHeadEl.removeChild(oldStyleSheetEl)
         })
-        newStyleSheetEl.setAttribute('rel', 'stylesheet')
-        newStyleSheetEl.setAttribute('id', 'main-stylesheet')
-        newStyleSheetEl.setAttribute('href', theme.path)
-        return docHeadEl.appendChild(newStyleSheetEl)
-      }
+      })
+      newStyleSheetEl.setAttribute('rel', 'stylesheet')
+      newStyleSheetEl.setAttribute('id', 'main-stylesheet')
+      newStyleSheetEl.setAttribute('href', theme.path)
+      return docHeadEl.appendChild(newStyleSheetEl)
+    }
 
-      if (!['default', 'vim', 'emacs'].includes($scope.settings.mode)) {
-        $scope.settings.mode = 'default'
-      }
+    if (!['default', 'vim', 'emacs'].includes($scope.settings.mode)) {
+      $scope.settings.mode = 'default'
+    }
 
-      if (!['pdfjs', 'native'].includes($scope.settings.pdfViewer)) {
-        $scope.settings.pdfViewer = 'pdfjs'
-      }
+    if (!['pdfjs', 'native'].includes($scope.settings.pdfViewer)) {
+      $scope.settings.pdfViewer = 'pdfjs'
+    }
 
-      if (
-        $scope.settings.fontFamily != null &&
-        !['monaco', 'lucida'].includes($scope.settings.fontFamily)
-      ) {
-        delete $scope.settings.fontFamily
-      }
+    if (
+      $scope.settings.fontFamily != null &&
+      !['monaco', 'lucida'].includes($scope.settings.fontFamily)
+    ) {
+      delete $scope.settings.fontFamily
+    }
 
-      if (
-        $scope.settings.lineHeight != null &&
-        !['compact', 'normal', 'wide'].includes($scope.settings.lineHeight)
-      ) {
-        delete $scope.settings.lineHeight
-      }
+    if (
+      $scope.settings.lineHeight != null &&
+      !['compact', 'normal', 'wide'].includes($scope.settings.lineHeight)
+    ) {
+      delete $scope.settings.lineHeight
+    }
 
-      $scope.fontSizeAsStr = function(newVal) {
-        if (newVal != null) {
-          $scope.settings.fontSize = newVal
-        }
-        return $scope.settings.fontSize.toString()
+    $scope.fontSizeAsStr = function(newVal) {
+      if (newVal != null) {
+        $scope.settings.fontSize = newVal
       }
+      return $scope.settings.fontSize.toString()
+    }
 
-      $scope.$watch('settings.editorTheme', (editorTheme, oldEditorTheme) => {
-        if (editorTheme !== oldEditorTheme) {
-          return settings.saveSettings({ editorTheme })
+    $scope.$watch('settings.editorTheme', (editorTheme, oldEditorTheme) => {
+      if (editorTheme !== oldEditorTheme) {
+        return settings.saveSettings({ editorTheme })
+      }
+    })
+
+    $scope.$watch('settings.overallTheme', (overallTheme, oldOverallTheme) => {
+      if (overallTheme !== oldOverallTheme) {
+        const chosenTheme = _.find(
+          $scope.overallThemesList,
+          theme => theme.val === overallTheme
+        )
+        if (chosenTheme != null) {
+          _updateCSSFile(chosenTheme)
+          return settings.saveSettings({ overallTheme })
         }
-      })
+      }
+    })
 
-      $scope.$watch(
-        'settings.overallTheme',
-        (overallTheme, oldOverallTheme) => {
-          if (overallTheme !== oldOverallTheme) {
-            const chosenTheme = _.find(
-              $scope.overallThemesList,
-              theme => theme.val === overallTheme
-            )
-            if (chosenTheme != null) {
-              _updateCSSFile(chosenTheme)
-              return settings.saveSettings({ overallTheme })
-            }
-          }
-        }
-      )
+    $scope.$watch('settings.fontSize', (fontSize, oldFontSize) => {
+      if (fontSize !== oldFontSize) {
+        return settings.saveSettings({ fontSize: parseInt(fontSize, 10) })
+      }
+    })
 
-      $scope.$watch('settings.fontSize', (fontSize, oldFontSize) => {
-        if (fontSize !== oldFontSize) {
-          return settings.saveSettings({ fontSize: parseInt(fontSize, 10) })
-        }
-      })
+    $scope.$watch('settings.mode', (mode, oldMode) => {
+      if (mode !== oldMode) {
+        return settings.saveSettings({ mode })
+      }
+    })
 
-      $scope.$watch('settings.mode', (mode, oldMode) => {
-        if (mode !== oldMode) {
-          return settings.saveSettings({ mode })
-        }
-      })
+    $scope.$watch('settings.autoComplete', (autoComplete, oldAutoComplete) => {
+      if (autoComplete !== oldAutoComplete) {
+        return settings.saveSettings({ autoComplete })
+      }
+    })
 
-      $scope.$watch(
-        'settings.autoComplete',
-        (autoComplete, oldAutoComplete) => {
-          if (autoComplete !== oldAutoComplete) {
-            return settings.saveSettings({ autoComplete })
-          }
+    $scope.$watch(
+      'settings.autoPairDelimiters',
+      (autoPairDelimiters, oldAutoPairDelimiters) => {
+        if (autoPairDelimiters !== oldAutoPairDelimiters) {
+          return settings.saveSettings({ autoPairDelimiters })
         }
-      )
-
-      $scope.$watch(
-        'settings.autoPairDelimiters',
-        (autoPairDelimiters, oldAutoPairDelimiters) => {
-          if (autoPairDelimiters !== oldAutoPairDelimiters) {
-            return settings.saveSettings({ autoPairDelimiters })
-          }
-        }
-      )
+      }
+    )
 
-      $scope.$watch('settings.pdfViewer', (pdfViewer, oldPdfViewer) => {
-        if (pdfViewer !== oldPdfViewer) {
-          return settings.saveSettings({ pdfViewer })
-        }
-      })
+    $scope.$watch('settings.pdfViewer', (pdfViewer, oldPdfViewer) => {
+      if (pdfViewer !== oldPdfViewer) {
+        return settings.saveSettings({ pdfViewer })
+      }
+    })
 
-      $scope.$watch(
-        'settings.syntaxValidation',
-        (syntaxValidation, oldSyntaxValidation) => {
-          if (syntaxValidation !== oldSyntaxValidation) {
-            return settings.saveSettings({ syntaxValidation })
-          }
+    $scope.$watch(
+      'settings.syntaxValidation',
+      (syntaxValidation, oldSyntaxValidation) => {
+        if (syntaxValidation !== oldSyntaxValidation) {
+          return settings.saveSettings({ syntaxValidation })
         }
-      )
+      }
+    )
 
-      $scope.$watch('settings.fontFamily', (fontFamily, oldFontFamily) => {
-        if (fontFamily !== oldFontFamily) {
-          return settings.saveSettings({ fontFamily })
-        }
-      })
+    $scope.$watch('settings.fontFamily', (fontFamily, oldFontFamily) => {
+      if (fontFamily !== oldFontFamily) {
+        return settings.saveSettings({ fontFamily })
+      }
+    })
 
-      $scope.$watch('settings.lineHeight', (lineHeight, oldLineHeight) => {
-        if (lineHeight !== oldLineHeight) {
-          return settings.saveSettings({ lineHeight })
-        }
-      })
+    $scope.$watch('settings.lineHeight', (lineHeight, oldLineHeight) => {
+      if (lineHeight !== oldLineHeight) {
+        return settings.saveSettings({ lineHeight })
+      }
+    })
 
-      $scope.$watch('project.spellCheckLanguage', (language, oldLanguage) => {
-        if (this.ignoreUpdates) {
-          return
-        }
-        if (oldLanguage != null && language !== oldLanguage) {
-          settings.saveProjectSettings({ spellCheckLanguage: language })
-          // Also set it as the default for the user
-          return settings.saveSettings({ spellCheckLanguage: language })
-        }
-      })
+    $scope.$watch('project.spellCheckLanguage', (language, oldLanguage) => {
+      if (this.ignoreUpdates) {
+        return
+      }
+      if (oldLanguage != null && language !== oldLanguage) {
+        settings.saveProjectSettings({ spellCheckLanguage: language })
+        // Also set it as the default for the user
+        return settings.saveSettings({ spellCheckLanguage: language })
+      }
+    })
 
-      $scope.$watch('project.compiler', (compiler, oldCompiler) => {
-        if (this.ignoreUpdates) {
-          return
-        }
-        if (oldCompiler != null && compiler !== oldCompiler) {
-          return settings.saveProjectSettings({ compiler })
-        }
-      })
+    $scope.$watch('project.compiler', (compiler, oldCompiler) => {
+      if (this.ignoreUpdates) {
+        return
+      }
+      if (oldCompiler != null && compiler !== oldCompiler) {
+        return settings.saveProjectSettings({ compiler })
+      }
+    })
 
-      $scope.$watch('project.imageName', (imageName, oldImageName) => {
-        if (this.ignoreUpdates) {
-          return
-        }
-        if (oldImageName != null && imageName !== oldImageName) {
-          return settings.saveProjectSettings({ imageName })
-        }
-      })
+    $scope.$watch('project.imageName', (imageName, oldImageName) => {
+      if (this.ignoreUpdates) {
+        return
+      }
+      if (oldImageName != null && imageName !== oldImageName) {
+        return settings.saveProjectSettings({ imageName })
+      }
+    })
 
-      $scope.$watch('project.rootDoc_id', (rootDoc_id, oldRootDoc_id) => {
-        if (this.ignoreUpdates) {
-          return
-        }
-        // don't save on initialisation, Angular passes oldRootDoc_id as
-        // undefined in this case.
-        if (typeof oldRootDoc_id === 'undefined') {
-          return
-        }
-        // otherwise only save changes, null values are allowed
-        if (rootDoc_id !== oldRootDoc_id) {
-          return settings.saveProjectSettings({ rootDocId: rootDoc_id })
-        }
-      })
+    $scope.$watch('project.rootDoc_id', (rootDoc_id, oldRootDoc_id) => {
+      if (this.ignoreUpdates) {
+        return
+      }
+      // don't save on initialisation, Angular passes oldRootDoc_id as
+      // undefined in this case.
+      if (typeof oldRootDoc_id === 'undefined') {
+        return
+      }
+      // otherwise only save changes, null values are allowed
+      if (rootDoc_id !== oldRootDoc_id) {
+        return settings.saveProjectSettings({ rootDocId: rootDoc_id })
+      }
+    })
 
-      ide.socket.on('compilerUpdated', compiler => {
-        this.ignoreUpdates = true
-        $scope.$apply(() => {
-          return ($scope.project.compiler = compiler)
-        })
-        return delete this.ignoreUpdates
+    ide.socket.on('compilerUpdated', compiler => {
+      this.ignoreUpdates = true
+      $scope.$apply(() => {
+        return ($scope.project.compiler = compiler)
       })
+      return delete this.ignoreUpdates
+    })
 
-      ide.socket.on('imageNameUpdated', imageName => {
-        this.ignoreUpdates = true
-        $scope.$apply(() => {
-          return ($scope.project.imageName = imageName)
-        })
-        return delete this.ignoreUpdates
+    ide.socket.on('imageNameUpdated', imageName => {
+      this.ignoreUpdates = true
+      $scope.$apply(() => {
+        return ($scope.project.imageName = imageName)
       })
+      return delete this.ignoreUpdates
+    })
 
-      return ide.socket.on('spellCheckLanguageUpdated', languageCode => {
-        this.ignoreUpdates = true
-        $scope.$apply(() => {
-          return ($scope.project.spellCheckLanguage = languageCode)
-        })
-        return delete this.ignoreUpdates
+    return ide.socket.on('spellCheckLanguageUpdated', languageCode => {
+      this.ignoreUpdates = true
+      $scope.$apply(() => {
+        return ($scope.project.spellCheckLanguage = languageCode)
       })
-    }
-  ]))
+      return delete this.ignoreUpdates
+    })
+  }))

+ 44 - 48
services/web/public/src/ide/settings/services/settings.js

@@ -12,56 +12,52 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.factory('settings', [
-    'ide',
-    'event_tracking',
-    (ide, event_tracking) => ({
-      saveSettings(data) {
-        // Tracking code.
-        for (let key of Array.from(Object.keys(data))) {
-          const changedSetting = key
-          const changedSettingVal = data[key]
-          event_tracking.sendMB('setting-changed', {
-            changedSetting,
-            changedSettingVal
-          })
-        }
-        // End of tracking code.
-
-        data._csrf = window.csrfToken
-        return ide.$http.post('/user/settings', data)
-      },
+  App.factory('settings', (ide, event_tracking) => ({
+    saveSettings(data) {
+      // Tracking code.
+      for (let key of Array.from(Object.keys(data))) {
+        const changedSetting = key
+        const changedSettingVal = data[key]
+        event_tracking.sendMB('setting-changed', {
+          changedSetting,
+          changedSettingVal
+        })
+      }
+      // End of tracking code.
 
-      saveProjectSettings(data) {
-        // Tracking code.
-        for (let key of Array.from(Object.keys(data))) {
-          const changedSetting = key
-          const changedSettingVal = data[key]
-          event_tracking.sendMB('project-setting-changed', {
-            changedSetting,
-            changedSettingVal
-          })
-        }
-        // End of tracking code.
+      data._csrf = window.csrfToken
+      return ide.$http.post('/user/settings', data)
+    },
 
-        data._csrf = window.csrfToken
-        return ide.$http.post(`/project/${ide.project_id}/settings`, data)
-      },
+    saveProjectSettings(data) {
+      // Tracking code.
+      for (let key of Array.from(Object.keys(data))) {
+        const changedSetting = key
+        const changedSettingVal = data[key]
+        event_tracking.sendMB('project-setting-changed', {
+          changedSetting,
+          changedSettingVal
+        })
+      }
+      // End of tracking code.
 
-      saveProjectAdminSettings(data) {
-        // Tracking code.
-        for (let key of Array.from(Object.keys(data))) {
-          const changedSetting = key
-          const changedSettingVal = data[key]
-          event_tracking.sendMB('project-admin-setting-changed', {
-            changedSetting,
-            changedSettingVal
-          })
-        }
-        // End of tracking code.
+      data._csrf = window.csrfToken
+      return ide.$http.post(`/project/${ide.project_id}/settings`, data)
+    },
 
-        data._csrf = window.csrfToken
-        return ide.$http.post(`/project/${ide.project_id}/settings/admin`, data)
+    saveProjectAdminSettings(data) {
+      // Tracking code.
+      for (let key of Array.from(Object.keys(data))) {
+        const changedSetting = key
+        const changedSettingVal = data[key]
+        event_tracking.sendMB('project-admin-setting-changed', {
+          changedSetting,
+          changedSettingVal
+        })
       }
-    })
-  ]))
+      // End of tracking code.
+
+      data._csrf = window.csrfToken
+      return ide.$http.post(`/project/${ide.project_id}/settings/admin`, data)
+    }
+  })))

+ 52 - 60
services/web/public/src/ide/share/controllers/ShareController.js

@@ -13,66 +13,58 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.controller('ShareController', [
-    '$scope',
-    '$modal',
-    'ide',
-    'projectInvites',
-    'projectMembers',
-    'event_tracking',
-    function(
-      $scope,
-      $modal,
-      ide,
-      projectInvites,
-      projectMembers,
-      event_tracking
-    ) {
-      $scope.openShareProjectModal = function(isAdmin) {
-        $scope.isAdmin = isAdmin
-        event_tracking.sendMBOnce('ide-open-share-modal-once')
+  App.controller('ShareController', function(
+    $scope,
+    $modal,
+    ide,
+    projectInvites,
+    projectMembers,
+    event_tracking
+  ) {
+    $scope.openShareProjectModal = function(isAdmin) {
+      $scope.isAdmin = isAdmin
+      event_tracking.sendMBOnce('ide-open-share-modal-once')
 
-        return $modal.open({
-          templateUrl: 'shareProjectModalTemplate',
-          controller: 'ShareProjectModalController',
-          scope: $scope
-        })
-      }
-
-      ide.socket.on('project:tokens:changed', data => {
-        if (data.tokens != null) {
-          ide.$scope.project.tokens = data.tokens
-          return $scope.$digest()
-        }
-      })
-
-      return ide.socket.on('project:membership:changed', data => {
-        if (data.members) {
-          projectMembers
-            .getMembers()
-            .then(response => {
-              ;({ data } = response)
-              if (data.members) {
-                return ($scope.project.members = data.members)
-              }
-            })
-            .catch(() => {
-              return console.error('Error fetching members for project')
-            })
-        }
-        if (data.invites) {
-          return projectInvites
-            .getInvites()
-            .then(response => {
-              ;({ data } = response)
-              if (data.invites) {
-                return ($scope.project.invites = data.invites)
-              }
-            })
-            .catch(() => {
-              return console.error('Error fetching invites for project')
-            })
-        }
+      return $modal.open({
+        templateUrl: 'shareProjectModalTemplate',
+        controller: 'ShareProjectModalController',
+        scope: $scope
       })
     }
-  ]))
+
+    ide.socket.on('project:tokens:changed', data => {
+      if (data.tokens != null) {
+        ide.$scope.project.tokens = data.tokens
+        return $scope.$digest()
+      }
+    })
+
+    return ide.socket.on('project:membership:changed', data => {
+      if (data.members) {
+        projectMembers
+          .getMembers()
+          .then(response => {
+            ;({ data } = response)
+            if (data.members) {
+              return ($scope.project.members = data.members)
+            }
+          })
+          .catch(() => {
+            return console.error('Error fetching members for project')
+          })
+      }
+      if (data.invites) {
+        return projectInvites
+          .getInvites()
+          .then(response => {
+            ;({ data } = response)
+            if (data.invites) {
+              return ($scope.project.invites = data.invites)
+            }
+          })
+          .catch(() => {
+            return console.error('Error fetching invites for project')
+          })
+      }
+    })
+  }))

+ 35 - 39
services/web/public/src/ide/share/services/projectInvites.js

@@ -10,45 +10,41 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.factory('projectInvites', [
-    'ide',
-    '$http',
-    (ide, $http) => ({
-      sendInvite(email, privileges, grecaptchaResponse) {
-        return $http.post(`/project/${ide.project_id}/invite`, {
-          email,
-          privileges,
-          _csrf: window.csrfToken,
-          'g-recaptcha-response': grecaptchaResponse
-        })
-      },
+  App.factory('projectInvites', (ide, $http) => ({
+    sendInvite(email, privileges, grecaptchaResponse) {
+      return $http.post(`/project/${ide.project_id}/invite`, {
+        email,
+        privileges,
+        _csrf: window.csrfToken,
+        'g-recaptcha-response': grecaptchaResponse
+      })
+    },
 
-      revokeInvite(inviteId) {
-        return $http({
-          url: `/project/${ide.project_id}/invite/${inviteId}`,
-          method: 'DELETE',
-          headers: {
-            'X-Csrf-Token': window.csrfToken
-          }
-        })
-      },
+    revokeInvite(inviteId) {
+      return $http({
+        url: `/project/${ide.project_id}/invite/${inviteId}`,
+        method: 'DELETE',
+        headers: {
+          'X-Csrf-Token': window.csrfToken
+        }
+      })
+    },
 
-      resendInvite(inviteId, privileges) {
-        return $http.post(
-          `/project/${ide.project_id}/invite/${inviteId}/resend`,
-          {
-            _csrf: window.csrfToken
-          }
-        )
-      },
+    resendInvite(inviteId, privileges) {
+      return $http.post(
+        `/project/${ide.project_id}/invite/${inviteId}/resend`,
+        {
+          _csrf: window.csrfToken
+        }
+      )
+    },
 
-      getInvites() {
-        return $http.get(`/project/${ide.project_id}/invites`, {
-          json: true,
-          headers: {
-            'X-Csrf-Token': window.csrfToken
-          }
-        })
-      }
-    })
-  ]))
+    getInvites() {
+      return $http.get(`/project/${ide.project_id}/invites`, {
+        json: true,
+        headers: {
+          'X-Csrf-Token': window.csrfToken
+        }
+      })
+    }
+  })))

+ 26 - 30
services/web/public/src/ide/share/services/projectMembers.js

@@ -10,35 +10,31 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.factory('projectMembers', [
-    'ide',
-    '$http',
-    (ide, $http) => ({
-      removeMember(member) {
-        return $http({
-          url: `/project/${ide.project_id}/users/${member._id}`,
-          method: 'DELETE',
-          headers: {
-            'X-Csrf-Token': window.csrfToken
-          }
-        })
-      },
+  App.factory('projectMembers', (ide, $http) => ({
+    removeMember(member) {
+      return $http({
+        url: `/project/${ide.project_id}/users/${member._id}`,
+        method: 'DELETE',
+        headers: {
+          'X-Csrf-Token': window.csrfToken
+        }
+      })
+    },
 
-      addGroup(group_id, privileges) {
-        return $http.post(`/project/${ide.project_id}/group`, {
-          group_id,
-          privileges,
-          _csrf: window.csrfToken
-        })
-      },
+    addGroup(group_id, privileges) {
+      return $http.post(`/project/${ide.project_id}/group`, {
+        group_id,
+        privileges,
+        _csrf: window.csrfToken
+      })
+    },
 
-      getMembers() {
-        return $http.get(`/project/${ide.project_id}/members`, {
-          json: true,
-          headers: {
-            'X-Csrf-Token': window.csrfToken
-          }
-        })
-      }
-    })
-  ]))
+    getMembers() {
+      return $http.get(`/project/${ide.project_id}/members`, {
+        json: true,
+        headers: {
+          'X-Csrf-Token': window.csrfToken
+        }
+      })
+    }
+  })))

+ 104 - 113
services/web/public/src/main/account-settings.js

@@ -14,129 +14,120 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], function(App) {
-  App.controller('AccountSettingsController', [
-    '$scope',
-    '$http',
-    '$modal',
-    'event_tracking',
-    'UserAffiliationsDataService',
-    'UserOauthDataService',
-    function(
-      $scope,
-      $http,
-      $modal,
-      event_tracking,
-      UserAffiliationsDataService,
-      UserOauthDataService
-    ) {
-      $scope.subscribed = true
+  App.controller('AccountSettingsController', function(
+    $scope,
+    $http,
+    $modal,
+    event_tracking,
+    UserAffiliationsDataService,
+    UserOauthDataService
+  ) {
+    $scope.subscribed = true
 
-      $scope.unsubscribe = function() {
-        $scope.unsubscribing = true
-        return $http({
-          method: 'DELETE',
-          url: '/user/newsletter/unsubscribe',
-          headers: {
-            'X-CSRF-Token': window.csrfToken
-          }
+    $scope.unsubscribe = function() {
+      $scope.unsubscribing = true
+      return $http({
+        method: 'DELETE',
+        url: '/user/newsletter/unsubscribe',
+        headers: {
+          'X-CSRF-Token': window.csrfToken
+        }
+      })
+        .then(function() {
+          $scope.unsubscribing = false
+          return ($scope.subscribed = false)
         })
-          .then(function() {
-            $scope.unsubscribing = false
-            return ($scope.subscribed = false)
-          })
-          .catch(() => ($scope.unsubscribing = true))
-      }
+        .catch(() => ($scope.unsubscribing = true))
+    }
 
-      $scope.deleteAccount = function() {
-        let modalInstance
-        return (modalInstance = $modal.open({
-          templateUrl: 'deleteAccountModalTemplate',
-          controller: 'DeleteAccountModalController',
-          resolve: {
-            userDefaultEmail() {
-              return UserAffiliationsDataService.getUserDefaultEmail()
-                .then(
-                  defaultEmailDetails =>
-                    (defaultEmailDetails != null
-                      ? defaultEmailDetails.email
-                      : undefined) || null
-                )
-                .catch(() => null)
-            }
+    $scope.deleteAccount = function() {
+      let modalInstance
+      return (modalInstance = $modal.open({
+        templateUrl: 'deleteAccountModalTemplate',
+        controller: 'DeleteAccountModalController',
+        resolve: {
+          userDefaultEmail() {
+            return UserAffiliationsDataService.getUserDefaultEmail()
+              .then(
+                defaultEmailDetails =>
+                  (defaultEmailDetails != null
+                    ? defaultEmailDetails.email
+                    : undefined) || null
+              )
+              .catch(() => null)
           }
-        }))
-      }
-
-      return ($scope.upgradeIntegration = service =>
-        event_tracking.send('subscription-funnel', 'settings-page', service))
+        }
+      }))
     }
-  ])
 
-  return App.controller('DeleteAccountModalController', [
-    '$scope',
-    '$modalInstance',
-    '$timeout',
-    '$http',
-    'userDefaultEmail',
-    function($scope, $modalInstance, $timeout, $http, userDefaultEmail) {
-      $scope.state = {
-        isValid: false,
-        deleteText: '',
-        password: '',
-        confirmV1Purge: false,
-        confirmSharelatexDelete: false,
-        inflight: false,
-        error: null
-      }
+    return ($scope.upgradeIntegration = service =>
+      event_tracking.send('subscription-funnel', 'settings-page', service))
+  })
 
-      $scope.userDefaultEmail = userDefaultEmail
+  return App.controller('DeleteAccountModalController', function(
+    $scope,
+    $modalInstance,
+    $timeout,
+    $http,
+    userDefaultEmail
+  ) {
+    $scope.state = {
+      isValid: false,
+      deleteText: '',
+      password: '',
+      confirmV1Purge: false,
+      confirmSharelatexDelete: false,
+      inflight: false,
+      error: null
+    }
 
-      $modalInstance.opened.then(() =>
-        $timeout(() => $scope.$broadcast('open'), 700)
-      )
+    $scope.userDefaultEmail = userDefaultEmail
 
-      $scope.checkValidation = () =>
-        ($scope.state.isValid =
-          userDefaultEmail != null &&
-          $scope.state.deleteText.toLowerCase() ===
-            userDefaultEmail.toLowerCase() &&
-          $scope.state.password.length > 0 &&
-          $scope.state.confirmV1Purge &&
-          $scope.state.confirmSharelatexDelete)
+    $modalInstance.opened.then(() =>
+      $timeout(() => $scope.$broadcast('open'), 700)
+    )
 
-      $scope.delete = function() {
-        $scope.state.inflight = true
-        $scope.state.error = null
-        return $http({
-          method: 'POST',
-          url: '/user/delete',
-          headers: {
-            'X-CSRF-Token': window.csrfToken,
-            'Content-Type': 'application/json'
-          },
-          data: {
-            password: $scope.state.password
-          },
-          disableAutoLoginRedirect: true // we want to handle errors ourselves
-        })
-          .then(function() {
-            $modalInstance.close()
-            $scope.state.inflight = false
-            $scope.state.error = null
-            return setTimeout(() => (window.location = '/login'), 1000)
-          })
-          .catch(function(response) {
-            const { data, status } = response
-            $scope.state.inflight = false
-            if (status === 403) {
-              $scope.state.error = { code: 'InvalidCredentialsError' }
-            } else {
-              $scope.state.error = { code: data.error }
-            }
-          })
-      }
+    $scope.checkValidation = () =>
+      ($scope.state.isValid =
+        userDefaultEmail != null &&
+        $scope.state.deleteText.toLowerCase() ===
+          userDefaultEmail.toLowerCase() &&
+        $scope.state.password.length > 0 &&
+        $scope.state.confirmV1Purge &&
+        $scope.state.confirmSharelatexDelete)
 
-      return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
+    $scope.delete = function() {
+      $scope.state.inflight = true
+      $scope.state.error = null
+      return $http({
+        method: 'POST',
+        url: '/user/delete',
+        headers: {
+          'X-CSRF-Token': window.csrfToken,
+          'Content-Type': 'application/json'
+        },
+        data: {
+          password: $scope.state.password
+        },
+        disableAutoLoginRedirect: true // we want to handle errors ourselves
+      })
+        .then(function() {
+          $modalInstance.close()
+          $scope.state.inflight = false
+          $scope.state.error = null
+          return setTimeout(() => (window.location = '/login'), 1000)
+        })
+        .catch(function(response) {
+          const { data, status } = response
+          $scope.state.inflight = false
+          if (status === 403) {
+            $scope.state.error = { code: 'InvalidCredentialsError' }
+          } else {
+            $scope.state.error = { code: data.error }
+          }
+        })
     }
-  ])
+
+    return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
+  })
 })

+ 209 - 210
services/web/public/src/main/affiliations/controllers/UserAffiliationsController.js

@@ -15,250 +15,249 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.controller('UserAffiliationsController', [
-    '$scope',
-    'UserAffiliationsDataService',
-    '$q',
-    '_',
-    function($scope, UserAffiliationsDataService, $q, _) {
-      $scope.userEmails = []
+  App.controller('UserAffiliationsController', function(
+    $scope,
+    UserAffiliationsDataService,
+    $q,
+    _
+  ) {
+    $scope.userEmails = []
 
-      const LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/
-      const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+    const LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/
+    const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
 
-      const _matchLocalAndDomain = function(userEmailInput) {
-        const match =
-          userEmailInput != null
-            ? userEmailInput.match(LOCAL_AND_DOMAIN_REGEX)
-            : undefined
-        if (match != null) {
-          return { local: match[1], domain: match[2] }
-        } else {
-          return { local: null, domain: null }
-        }
+    const _matchLocalAndDomain = function(userEmailInput) {
+      const match =
+        userEmailInput != null
+          ? userEmailInput.match(LOCAL_AND_DOMAIN_REGEX)
+          : undefined
+      if (match != null) {
+        return { local: match[1], domain: match[2] }
+      } else {
+        return { local: null, domain: null }
       }
+    }
 
-      $scope.getEmailSuggestion = function(userInput) {
-        const userInputLocalAndDomain = _matchLocalAndDomain(userInput)
-        $scope.ui.isValidEmail = EMAIL_REGEX.test(userInput)
-        $scope.ui.isBlacklistedEmail = false
-        $scope.ui.showManualUniversitySelectionUI = false
-        if (userInputLocalAndDomain.domain != null) {
-          $scope.ui.isBlacklistedEmail = UserAffiliationsDataService.isDomainBlacklisted(
-            userInputLocalAndDomain.domain
-          )
-          return UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(
-            userInputLocalAndDomain.domain
-          )
-            .then(function(universityDomain) {
-              const currentUserInputLocalAndDomain = _matchLocalAndDomain(
-                $scope.newAffiliation.email
-              )
-              if (
-                currentUserInputLocalAndDomain.domain ===
-                universityDomain.hostname
-              ) {
-                $scope.newAffiliation.university = universityDomain.university
-                $scope.newAffiliation.department = universityDomain.department
-              } else {
-                $scope.newAffiliation.university = null
-                $scope.newAffiliation.department = null
-              }
-              return $q.resolve(
-                `${userInputLocalAndDomain.local}@${universityDomain.hostname}`
-              )
-            })
-            .catch(function() {
+    $scope.getEmailSuggestion = function(userInput) {
+      const userInputLocalAndDomain = _matchLocalAndDomain(userInput)
+      $scope.ui.isValidEmail = EMAIL_REGEX.test(userInput)
+      $scope.ui.isBlacklistedEmail = false
+      $scope.ui.showManualUniversitySelectionUI = false
+      if (userInputLocalAndDomain.domain != null) {
+        $scope.ui.isBlacklistedEmail = UserAffiliationsDataService.isDomainBlacklisted(
+          userInputLocalAndDomain.domain
+        )
+        return UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(
+          userInputLocalAndDomain.domain
+        )
+          .then(function(universityDomain) {
+            const currentUserInputLocalAndDomain = _matchLocalAndDomain(
+              $scope.newAffiliation.email
+            )
+            if (
+              currentUserInputLocalAndDomain.domain ===
+              universityDomain.hostname
+            ) {
+              $scope.newAffiliation.university = universityDomain.university
+              $scope.newAffiliation.department = universityDomain.department
+            } else {
               $scope.newAffiliation.university = null
               $scope.newAffiliation.department = null
-              return $q.reject(null)
-            })
-        } else {
-          $scope.newAffiliation.university = null
-          $scope.newAffiliation.department = null
-          return $q.reject(null)
-        }
-      }
-
-      $scope.selectUniversityManually = function() {
+            }
+            return $q.resolve(
+              `${userInputLocalAndDomain.local}@${universityDomain.hostname}`
+            )
+          })
+          .catch(function() {
+            $scope.newAffiliation.university = null
+            $scope.newAffiliation.department = null
+            return $q.reject(null)
+          })
+      } else {
         $scope.newAffiliation.university = null
         $scope.newAffiliation.department = null
-        return ($scope.ui.showManualUniversitySelectionUI = true)
+        return $q.reject(null)
       }
+    }
 
-      $scope.changeAffiliation = function(userEmail) {
-        if (
-          __guard__(
-            userEmail.affiliation != null
-              ? userEmail.affiliation.institution
-              : undefined,
-            x => x.id
-          ) != null
-        ) {
-          UserAffiliationsDataService.getUniversityDetails(
-            userEmail.affiliation.institution.id
-          ).then(
-            universityDetails =>
-              ($scope.affiliationToChange.university = universityDetails)
-          )
-        }
+    $scope.selectUniversityManually = function() {
+      $scope.newAffiliation.university = null
+      $scope.newAffiliation.department = null
+      return ($scope.ui.showManualUniversitySelectionUI = true)
+    }
 
-        $scope.affiliationToChange.email = userEmail.email
-        $scope.affiliationToChange.role = userEmail.affiliation.role
-        return ($scope.affiliationToChange.department =
-          userEmail.affiliation.department)
+    $scope.changeAffiliation = function(userEmail) {
+      if (
+        __guard__(
+          userEmail.affiliation != null
+            ? userEmail.affiliation.institution
+            : undefined,
+          x => x.id
+        ) != null
+      ) {
+        UserAffiliationsDataService.getUniversityDetails(
+          userEmail.affiliation.institution.id
+        ).then(
+          universityDetails =>
+            ($scope.affiliationToChange.university = universityDetails)
+        )
       }
 
-      $scope.saveAffiliationChange = function(userEmail) {
-        userEmail.affiliation.role = $scope.affiliationToChange.role
-        userEmail.affiliation.department = $scope.affiliationToChange.department
-        _resetAffiliationToChange()
-        return _monitorRequest(
-          UserAffiliationsDataService.addRoleAndDepartment(
-            userEmail.email,
-            userEmail.affiliation.role,
-            userEmail.affiliation.department
-          )
-        ).then(() => setTimeout(() => _getUserEmails()))
-      }
+      $scope.affiliationToChange.email = userEmail.email
+      $scope.affiliationToChange.role = userEmail.affiliation.role
+      return ($scope.affiliationToChange.department =
+        userEmail.affiliation.department)
+    }
+
+    $scope.saveAffiliationChange = function(userEmail) {
+      userEmail.affiliation.role = $scope.affiliationToChange.role
+      userEmail.affiliation.department = $scope.affiliationToChange.department
+      _resetAffiliationToChange()
+      return _monitorRequest(
+        UserAffiliationsDataService.addRoleAndDepartment(
+          userEmail.email,
+          userEmail.affiliation.role,
+          userEmail.affiliation.department
+        )
+      ).then(() => setTimeout(() => _getUserEmails()))
+    }
 
-      $scope.cancelAffiliationChange = email => _resetAffiliationToChange()
+    $scope.cancelAffiliationChange = email => _resetAffiliationToChange()
 
-      $scope.isChangingAffiliation = email =>
-        $scope.affiliationToChange.email === email
+    $scope.isChangingAffiliation = email =>
+      $scope.affiliationToChange.email === email
 
-      $scope.showAddEmailForm = () => ($scope.ui.showAddEmailUI = true)
+    $scope.showAddEmailForm = () => ($scope.ui.showAddEmailUI = true)
 
-      $scope.addNewEmail = function() {
-        let addEmailPromise
-        if ($scope.newAffiliation.university == null) {
-          addEmailPromise = UserAffiliationsDataService.addUserEmail(
-            $scope.newAffiliation.email
+    $scope.addNewEmail = function() {
+      let addEmailPromise
+      if ($scope.newAffiliation.university == null) {
+        addEmailPromise = UserAffiliationsDataService.addUserEmail(
+          $scope.newAffiliation.email
+        )
+      } else {
+        if ($scope.newAffiliation.university.isUserSuggested) {
+          addEmailPromise = UserAffiliationsDataService.addUserAffiliationWithUnknownUniversity(
+            $scope.newAffiliation.email,
+            $scope.newAffiliation.university.name,
+            $scope.newAffiliation.country.code,
+            $scope.newAffiliation.role,
+            $scope.newAffiliation.department
           )
         } else {
-          if ($scope.newAffiliation.university.isUserSuggested) {
-            addEmailPromise = UserAffiliationsDataService.addUserAffiliationWithUnknownUniversity(
-              $scope.newAffiliation.email,
-              $scope.newAffiliation.university.name,
-              $scope.newAffiliation.country.code,
-              $scope.newAffiliation.role,
-              $scope.newAffiliation.department
-            )
-          } else {
-            addEmailPromise = UserAffiliationsDataService.addUserAffiliation(
-              $scope.newAffiliation.email,
-              $scope.newAffiliation.university.id,
-              $scope.newAffiliation.role,
-              $scope.newAffiliation.department
-            )
-          }
+          addEmailPromise = UserAffiliationsDataService.addUserAffiliation(
+            $scope.newAffiliation.email,
+            $scope.newAffiliation.university.id,
+            $scope.newAffiliation.role,
+            $scope.newAffiliation.department
+          )
         }
-
-        $scope.ui.isAddingNewEmail = true
-        $scope.ui.showAddEmailUI = false
-        return _monitorRequest(addEmailPromise)
-          .then(function() {
-            _resetNewAffiliation()
-            _resetAddingEmail()
-            return setTimeout(() => _getUserEmails())
-          })
-          .finally(() => ($scope.ui.isAddingNewEmail = false))
       }
 
-      $scope.setDefaultUserEmail = userEmail =>
-        _monitorRequest(
-          UserAffiliationsDataService.setDefaultUserEmail(userEmail.email)
-        ).then(function() {
-          for (let email of Array.from($scope.userEmails || [])) {
-            email.default = false
-          }
-          return (userEmail.default = true)
+      $scope.ui.isAddingNewEmail = true
+      $scope.ui.showAddEmailUI = false
+      return _monitorRequest(addEmailPromise)
+        .then(function() {
+          _resetNewAffiliation()
+          _resetAddingEmail()
+          return setTimeout(() => _getUserEmails())
         })
+        .finally(() => ($scope.ui.isAddingNewEmail = false))
+    }
 
-      $scope.removeUserEmail = function(userEmail) {
-        $scope.userEmails = $scope.userEmails.filter(ue => ue !== userEmail)
-        return _monitorRequest(
-          UserAffiliationsDataService.removeUserEmail(userEmail.email)
-        )
-      }
+    $scope.setDefaultUserEmail = userEmail =>
+      _monitorRequest(
+        UserAffiliationsDataService.setDefaultUserEmail(userEmail.email)
+      ).then(function() {
+        for (let email of Array.from($scope.userEmails || [])) {
+          email.default = false
+        }
+        return (userEmail.default = true)
+      })
 
-      $scope.resendConfirmationEmail = function(userEmail) {
-        $scope.ui.isResendingConfirmation = true
-        return _monitorRequest(
-          UserAffiliationsDataService.resendConfirmationEmail(userEmail.email)
-        ).finally(() => ($scope.ui.isResendingConfirmation = false))
-      }
+    $scope.removeUserEmail = function(userEmail) {
+      $scope.userEmails = $scope.userEmails.filter(ue => ue !== userEmail)
+      return _monitorRequest(
+        UserAffiliationsDataService.removeUserEmail(userEmail.email)
+      )
+    }
 
-      $scope.acknowledgeError = function() {
-        _reset()
-        return _getUserEmails()
-      }
+    $scope.resendConfirmationEmail = function(userEmail) {
+      $scope.ui.isResendingConfirmation = true
+      return _monitorRequest(
+        UserAffiliationsDataService.resendConfirmationEmail(userEmail.email)
+      ).finally(() => ($scope.ui.isResendingConfirmation = false))
+    }
 
-      var _resetAffiliationToChange = () =>
-        ($scope.affiliationToChange = {
-          email: '',
-          university: null,
-          role: null,
-          department: null
-        })
+    $scope.acknowledgeError = function() {
+      _reset()
+      return _getUserEmails()
+    }
 
-      var _resetNewAffiliation = () =>
-        ($scope.newAffiliation = {
-          email: '',
-          country: null,
-          university: null,
-          role: null,
-          department: null
-        })
+    var _resetAffiliationToChange = () =>
+      ($scope.affiliationToChange = {
+        email: '',
+        university: null,
+        role: null,
+        department: null
+      })
 
-      var _resetAddingEmail = function() {
-        $scope.ui.showAddEmailUI = false
-        $scope.ui.isValidEmail = false
-        $scope.ui.isBlacklistedEmail = false
-        return ($scope.ui.showManualUniversitySelectionUI = false)
-      }
+    var _resetNewAffiliation = () =>
+      ($scope.newAffiliation = {
+        email: '',
+        country: null,
+        university: null,
+        role: null,
+        department: null
+      })
 
-      var _reset = function() {
-        $scope.ui = {
-          hasError: false,
-          errorMessage: '',
-          showChangeAffiliationUI: false,
-          isMakingRequest: false,
-          isLoadingEmails: false,
-          isAddingNewEmail: false,
-          isResendingConfirmation: false
-        }
-        _resetAffiliationToChange()
-        _resetNewAffiliation()
-        return _resetAddingEmail()
-      }
-      _reset()
+    var _resetAddingEmail = function() {
+      $scope.ui.showAddEmailUI = false
+      $scope.ui.isValidEmail = false
+      $scope.ui.isBlacklistedEmail = false
+      return ($scope.ui.showManualUniversitySelectionUI = false)
+    }
 
-      var _monitorRequest = function(promise) {
-        $scope.ui.hasError = false
-        $scope.ui.isMakingRequest = true
-        promise
-          .catch(function(response) {
-            $scope.ui.hasError = true
-            return ($scope.ui.errorMessage = __guard__(
-              response != null ? response.data : undefined,
-              x => x.message
-            ))
-          })
-          .finally(() => ($scope.ui.isMakingRequest = false))
-        return promise
+    var _reset = function() {
+      $scope.ui = {
+        hasError: false,
+        errorMessage: '',
+        showChangeAffiliationUI: false,
+        isMakingRequest: false,
+        isLoadingEmails: false,
+        isAddingNewEmail: false,
+        isResendingConfirmation: false
       }
+      _resetAffiliationToChange()
+      _resetNewAffiliation()
+      return _resetAddingEmail()
+    }
+    _reset()
 
-      // Populates the emails table
-      var _getUserEmails = function() {
-        $scope.ui.isLoadingEmails = true
-        return _monitorRequest(UserAffiliationsDataService.getUserEmails())
-          .then(emails => ($scope.userEmails = emails))
-          .finally(() => ($scope.ui.isLoadingEmails = false))
-      }
-      return _getUserEmails()
+    var _monitorRequest = function(promise) {
+      $scope.ui.hasError = false
+      $scope.ui.isMakingRequest = true
+      promise
+        .catch(function(response) {
+          $scope.ui.hasError = true
+          return ($scope.ui.errorMessage = __guard__(
+            response != null ? response.data : undefined,
+            x => x.message
+          ))
+        })
+        .finally(() => ($scope.ui.isMakingRequest = false))
+      return promise
+    }
+
+    // Populates the emails table
+    var _getUserEmails = function() {
+      $scope.ui.isLoadingEmails = true
+      return _monitorRequest(UserAffiliationsDataService.getUserEmails())
+        .then(emails => ($scope.userEmails = emails))
+        .finally(() => ($scope.ui.isLoadingEmails = false))
     }
-  ]))
+    return _getUserEmails()
+  }))
 function __guard__(value, transform) {
   return typeof value !== 'undefined' && value !== null
     ? transform(value)

+ 120 - 126
services/web/public/src/main/affiliations/factories/UserAffiliationsDataService.js

@@ -414,151 +414,145 @@ define(['base'], function(App) {
     }
   }
 
-  return App.factory('UserAffiliationsDataService', [
-    '$http',
-    '$q',
-    '_',
-    function($http, $q, _) {
-      const getCountries = () => $q.resolve(countriesList)
+  return App.factory('UserAffiliationsDataService', function($http, $q, _) {
+    const getCountries = () => $q.resolve(countriesList)
 
-      const getDefaultRoleHints = () => $q.resolve(defaultRoleHints)
+    const getDefaultRoleHints = () => $q.resolve(defaultRoleHints)
 
-      const getDefaultDepartmentHints = () => $q.resolve(defaultDepartmentHints)
+    const getDefaultDepartmentHints = () => $q.resolve(defaultDepartmentHints)
 
-      const getUserEmails = () =>
-        $http.get('/user/emails').then(response => response.data)
+    const getUserEmails = () =>
+      $http.get('/user/emails').then(response => response.data)
 
-      const getUserDefaultEmail = () =>
-        getUserEmails().then(userEmails =>
-          _.find(userEmails, userEmail => userEmail.default)
-        )
+    const getUserDefaultEmail = () =>
+      getUserEmails().then(userEmails =>
+        _.find(userEmails, userEmail => userEmail.default)
+      )
 
-      const getUniversitiesFromCountry = function(country) {
-        let universitiesFromCountry
-        if (universities[country.code] != null) {
-          universitiesFromCountry = universities[country.code]
-        } else {
-          universitiesFromCountry = $http
-            .get('/institutions/list', {
-              params: { country_code: country.code }
-            })
-            .then(response => (universities[country.code] = response.data))
-        }
-        return $q.resolve(universitiesFromCountry)
+    const getUniversitiesFromCountry = function(country) {
+      let universitiesFromCountry
+      if (universities[country.code] != null) {
+        universitiesFromCountry = universities[country.code]
+      } else {
+        universitiesFromCountry = $http
+          .get('/institutions/list', {
+            params: { country_code: country.code }
+          })
+          .then(response => (universities[country.code] = response.data))
       }
+      return $q.resolve(universitiesFromCountry)
+    }
 
-      const getUniversityDomainFromPartialDomainInput = function(
-        partialDomainInput
-      ) {
-        if (universitiesByDomain[partialDomainInput] != null) {
-          return $q.resolve(universitiesByDomain[partialDomainInput])
-        } else {
-          return $http
-            .get('/institutions/domains', {
-              params: { hostname: partialDomainInput, limit: 1 }
-            })
-            .then(function(response) {
-              const university = response.data[0]
-              if (
-                university != null &&
-                !isDomainBlacklisted(university.hostname)
-              ) {
-                universitiesByDomain[university.hostname] = university
-                return $q.resolve(university)
-              } else {
-                return $q.reject(null)
-              }
-            })
-        }
+    const getUniversityDomainFromPartialDomainInput = function(
+      partialDomainInput
+    ) {
+      if (universitiesByDomain[partialDomainInput] != null) {
+        return $q.resolve(universitiesByDomain[partialDomainInput])
+      } else {
+        return $http
+          .get('/institutions/domains', {
+            params: { hostname: partialDomainInput, limit: 1 }
+          })
+          .then(function(response) {
+            const university = response.data[0]
+            if (
+              university != null &&
+              !isDomainBlacklisted(university.hostname)
+            ) {
+              universitiesByDomain[university.hostname] = university
+              return $q.resolve(university)
+            } else {
+              return $q.reject(null)
+            }
+          })
       }
+    }
 
-      const getUniversityDetails = universityId =>
-        $http
-          .get(`/institutions/list/${universityId}`)
-          .then(response => response.data)
+    const getUniversityDetails = universityId =>
+      $http
+        .get(`/institutions/list/${universityId}`)
+        .then(response => response.data)
 
-      const addUserEmail = email =>
-        $http.post('/user/emails', {
-          email,
-          _csrf: window.csrfToken
-        })
+    const addUserEmail = email =>
+      $http.post('/user/emails', {
+        email,
+        _csrf: window.csrfToken
+      })
 
-      const addUserAffiliationWithUnknownUniversity = (
+    const addUserAffiliationWithUnknownUniversity = (
+      email,
+      unknownUniversityName,
+      unknownUniversityCountryCode,
+      role,
+      department
+    ) =>
+      $http.post('/user/emails', {
         email,
-        unknownUniversityName,
-        unknownUniversityCountryCode,
+        university: {
+          name: unknownUniversityName,
+          country_code: unknownUniversityCountryCode
+        },
         role,
-        department
-      ) =>
-        $http.post('/user/emails', {
-          email,
-          university: {
-            name: unknownUniversityName,
-            country_code: unknownUniversityCountryCode
-          },
-          role,
-          department,
-          _csrf: window.csrfToken
-        })
+        department,
+        _csrf: window.csrfToken
+      })
 
-      const addUserAffiliation = (email, universityId, role, department) =>
-        $http.post('/user/emails', {
-          email,
-          university: {
-            id: universityId
-          },
-          role,
-          department,
-          _csrf: window.csrfToken
-        })
+    const addUserAffiliation = (email, universityId, role, department) =>
+      $http.post('/user/emails', {
+        email,
+        university: {
+          id: universityId
+        },
+        role,
+        department,
+        _csrf: window.csrfToken
+      })
 
-      const addRoleAndDepartment = (email, role, department) =>
-        $http.post('/user/emails/endorse', {
-          email,
-          role,
-          department,
-          _csrf: window.csrfToken
-        })
+    const addRoleAndDepartment = (email, role, department) =>
+      $http.post('/user/emails/endorse', {
+        email,
+        role,
+        department,
+        _csrf: window.csrfToken
+      })
 
-      const setDefaultUserEmail = email =>
-        $http.post('/user/emails/default', {
-          email,
-          _csrf: window.csrfToken
-        })
+    const setDefaultUserEmail = email =>
+      $http.post('/user/emails/default', {
+        email,
+        _csrf: window.csrfToken
+      })
 
-      const removeUserEmail = email =>
-        $http.post('/user/emails/delete', {
-          email,
-          _csrf: window.csrfToken
-        })
+    const removeUserEmail = email =>
+      $http.post('/user/emails/delete', {
+        email,
+        _csrf: window.csrfToken
+      })
 
-      const resendConfirmationEmail = email =>
-        $http.post('/user/emails/resend_confirmation', {
-          email,
-          _csrf: window.csrfToken
-        })
+    const resendConfirmationEmail = email =>
+      $http.post('/user/emails/resend_confirmation', {
+        email,
+        _csrf: window.csrfToken
+      })
 
-      var isDomainBlacklisted = domain =>
-        domain.toLowerCase() in domainsBlackList
+    var isDomainBlacklisted = domain => domain.toLowerCase() in domainsBlackList
 
-      return {
-        getCountries,
-        getDefaultRoleHints,
-        getDefaultDepartmentHints,
-        getUserEmails,
-        getUserDefaultEmail,
-        getUniversitiesFromCountry,
-        getUniversityDomainFromPartialDomainInput,
-        getUniversityDetails,
-        addUserEmail,
-        addUserAffiliationWithUnknownUniversity,
-        addUserAffiliation,
-        addRoleAndDepartment,
-        setDefaultUserEmail,
-        removeUserEmail,
-        resendConfirmationEmail,
-        isDomainBlacklisted
-      }
+    return {
+      getCountries,
+      getDefaultRoleHints,
+      getDefaultDepartmentHints,
+      getUserEmails,
+      getUserDefaultEmail,
+      getUniversitiesFromCountry,
+      getUniversityDomainFromPartialDomainInput,
+      getUniversityDetails,
+      addUserEmail,
+      addUserAffiliationWithUnknownUniversity,
+      addUserAffiliation,
+      addRoleAndDepartment,
+      setDefaultUserEmail,
+      removeUserEmail,
+      resendConfirmationEmail,
+      isDomainBlacklisted
     }
-  ])
+  })
 })

+ 20 - 24
services/web/public/src/main/clear-sessions.js

@@ -11,29 +11,25 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 define(['base'], App =>
-  App.controller('ClearSessionsController', [
-    '$scope',
-    '$http',
-    function($scope, $http) {
-      $scope.state = {
-        otherSessions: window.otherSessions,
-        error: false,
-        success: false
-      }
+  App.controller('ClearSessionsController', function($scope, $http) {
+    $scope.state = {
+      otherSessions: window.otherSessions,
+      error: false,
+      success: false
+    }
 
-      return ($scope.clearSessions = function() {
-        console.log('>> clearing all sessions')
-        return $http({
-          method: 'POST',
-          url: '/user/sessions/clear',
-          headers: { 'X-CSRF-Token': window.csrfToken }
-        })
-          .then(function() {
-            $scope.state.otherSessions = []
-            $scope.state.error = false
-            return ($scope.state.success = true)
-          })
-          .catch(() => ($scope.state.error = true))
+    return ($scope.clearSessions = function() {
+      console.log('>> clearing all sessions')
+      return $http({
+        method: 'POST',
+        url: '/user/sessions/clear',
+        headers: { 'X-CSRF-Token': window.csrfToken }
       })
-    }
-  ]))
+        .then(function() {
+          $scope.state.otherSessions = []
+          $scope.state.error = false
+          return ($scope.state.success = true)
+        })
+        .catch(() => ($scope.state.error = true))
+    })
+  }))

+ 83 - 84
services/web/public/src/main/oauth/controllers/UserOauthController.js

@@ -1,92 +1,91 @@
 define(['base'], App =>
-  App.controller('UserOauthController', [
-    '$http',
-    '$scope',
-    '$q',
-    '_',
-    'UserOauthDataService',
-    function($http, $scope, $q, _, UserOauthDataService) {
-      const _monitorRequest = function(promise) {
-        $scope.ui.hasError = false
-        $scope.ui.isLoadingV1Ids = true
-        promise
-          .catch(response => {
-            $scope.ui.hasError = true
-            $scope.ui.errorMessage =
-              response && response.data && response.data.message
-                ? response.data.message
-                : 'error'
-          })
-          .finally(() => {
-            $scope.ui.isLoadingV1Ids = false
-          })
-        return promise
+  App.controller('UserOauthController', function(
+    $http,
+    $scope,
+    $q,
+    _,
+    UserOauthDataService
+  ) {
+    const _monitorRequest = function(promise) {
+      $scope.ui.hasError = false
+      $scope.ui.isLoadingV1Ids = true
+      promise
+        .catch(response => {
+          $scope.ui.hasError = true
+          $scope.ui.errorMessage =
+            response && response.data && response.data.message
+              ? response.data.message
+              : 'error'
+        })
+        .finally(() => {
+          $scope.ui.isLoadingV1Ids = false
+        })
+      return promise
+    }
+    const _reset = function() {
+      $scope.ui = {
+        hasError: false,
+        errorMessage: '',
+        isLoadingV1Ids: false
       }
-      const _reset = function() {
-        $scope.ui = {
-          hasError: false,
-          errorMessage: '',
-          isLoadingV1Ids: false
+      $scope.providers = window.oauthProviders
+      $scope.thirdPartyIds = window.thirdPartyIds
+      // until oauthUseV2=true, we will use OAuth data via v1 DB,
+      // except for Collabratec, which is only writing to the v2 DB.
+      // $scope.v2ThirdPartyIds is required for Collabratec,
+      // and only until v2 is authoritative. Though, we should leave this
+      // until we stop double writes, in case we need to flip.
+      // Double writes for OAuth will stop when oauthFallback=false
+      $scope.v2ThirdPartyIds = window.thirdPartyIds
+    }
+    const _getUserV1OauthProviders = () => {
+      $scope.ui.isLoadingV1Ids = true
+      return _monitorRequest(UserOauthDataService.getUserOauthV1()).then(
+        thirdPartyIds => {
+          $scope.thirdPartyIds = thirdPartyIds
         }
-        $scope.providers = window.oauthProviders
-        $scope.thirdPartyIds = window.thirdPartyIds
-        // until oauthUseV2=true, we will use OAuth data via v1 DB,
-        // except for Collabratec, which is only writing to the v2 DB.
-        // $scope.v2ThirdPartyIds is required for Collabratec,
-        // and only until v2 is authoritative. Though, we should leave this
-        // until we stop double writes, in case we need to flip.
-        // Double writes for OAuth will stop when oauthFallback=false
-        $scope.v2ThirdPartyIds = window.thirdPartyIds
-      }
-      const _getUserV1OauthProviders = () => {
-        $scope.ui.isLoadingV1Ids = true
-        return _monitorRequest(UserOauthDataService.getUserOauthV1()).then(
-          thirdPartyIds => {
-            $scope.thirdPartyIds = thirdPartyIds
-          }
-        )
-      }
-      const _unlinkError = (providerId, err) => {
-        $scope.providers[providerId].ui.hasError = true
-        $scope.providers[providerId].ui.errorMessage =
-          err && err.data && err.data.message ? err.data.message : 'error'
-      }
+      )
+    }
+    const _unlinkError = (providerId, err) => {
+      $scope.providers[providerId].ui.hasError = true
+      $scope.providers[providerId].ui.errorMessage =
+        err && err.data && err.data.message ? err.data.message : 'error'
+    }
 
-      $scope.unlink = providerId => {
-        if (window.ExposedSettings.isOverleaf) {
-          // UI
-          $scope.providers[providerId].ui = {
-            hasError: false,
-            isProcessing: true
-          }
-          // Data for update
-          const data = {
-            _csrf: window.csrfToken,
-            link: false,
-            providerId
-          }
-          $http
-            .post('/user/oauth-unlink', data)
-            .catch(error => {
-              $scope.providers[providerId].ui.isProcessing = false
-              _unlinkError(providerId, error)
-            })
-            .then(response => {
-              $scope.providers[providerId].ui.isProcessing = false
-              if (response.status === 200) {
-                $scope.thirdPartyIds[providerId] = null
-                // v2thirdPartyIds below can be removed post user c11n
-                $scope.v2ThirdPartyIds[providerId] = null
-              } else {
-                _unlinkError(providerId, response)
-              }
-            })
+    $scope.unlink = providerId => {
+      if (window.ExposedSettings.isOverleaf) {
+        // UI
+        $scope.providers[providerId].ui = {
+          hasError: false,
+          isProcessing: true
+        }
+        // Data for update
+        const data = {
+          _csrf: window.csrfToken,
+          link: false,
+          providerId
         }
+        $http
+          .post('/user/oauth-unlink', data)
+          .catch(error => {
+            $scope.providers[providerId].ui.isProcessing = false
+            _unlinkError(providerId, error)
+          })
+          .then(response => {
+            $scope.providers[providerId].ui.isProcessing = false
+            if (response.status === 200) {
+              $scope.thirdPartyIds[providerId] = null
+              // v2thirdPartyIds below can be removed post user c11n
+              $scope.v2ThirdPartyIds[providerId] = null
+            } else {
+              _unlinkError(providerId, response)
+            }
+          })
       }
+    }
 
-      _reset()
-      if (!window.oauthUseV2) {
-        _getUserV1OauthProviders()
-      }
+    _reset()
+    if (!window.oauthUseV2) {
+      _getUserV1OauthProviders()
     }
-  ]))
+  }))

+ 12 - 15
services/web/public/src/main/oauth/factories/UserOauthDataService.js

@@ -1,20 +1,17 @@
 define(['base'], function(App) {
-  return App.factory('UserOauthDataService', [
-    '$http',
-    function($http) {
-      const getUserOauthV1 = () => {
-        if (window.ExposedSettings.isOverleaf) {
-          return $http.get('/user/v1-oauth-uids').then(response => {
-            return response.data
-          })
-        } else {
-          return {}
-        }
+  return App.factory('UserOauthDataService', function($http) {
+    const getUserOauthV1 = () => {
+      if (window.ExposedSettings.isOverleaf) {
+        return $http.get('/user/v1-oauth-uids').then(response => {
+          return response.data
+        })
+      } else {
+        return {}
       }
+    }
 
-      return {
-        getUserOauthV1
-      }
+    return {
+      getUserOauthV1
     }
-  ])
+  })
 })

+ 24 - 26
services/web/public/src/main/project-list/services/project-list.js

@@ -2,33 +2,31 @@
     camelcase
 */
 define(['base'], App =>
-  App.service('ProjectListService', [
-    () => ({
-      getOwnerName(project) {
-        if (project.accessLevel === 'owner') {
-          return 'You'
-        } else if (project.owner != null) {
-          return this.getUserName(project.owner)
-        } else {
-          return 'None'
-        }
-      },
+  App.service('ProjectListService', () => ({
+    getOwnerName(project) {
+      if (project.accessLevel === 'owner') {
+        return 'You'
+      } else if (project.owner != null) {
+        return this.getUserName(project.owner)
+      } else {
+        return 'None'
+      }
+    },
 
-      getUserName(user) {
-        if (user && user._id === window.user_id) {
-          return 'You'
-        } else if (user) {
-          const { first_name, last_name, email } = user
-          if (first_name || last_name) {
-            return [first_name, last_name].filter(n => n != null).join(' ')
-          } else if (email) {
-            return email
-          } else {
-            return 'An Overleaf v1 User'
-          }
+    getUserName(user) {
+      if (user && user._id === window.user_id) {
+        return 'You'
+      } else if (user) {
+        const { first_name, last_name, email } = user
+        if (first_name || last_name) {
+          return [first_name, last_name].filter(n => n != null).join(' ')
+        } else if (email) {
+          return email
         } else {
-          return 'None'
+          return 'An Overleaf v1 User'
         }
+      } else {
+        return 'None'
       }
-    })
-  ]))
+    }
+  })))

+ 22 - 28
services/web/public/src/modules/errorCatcher.js

@@ -42,36 +42,30 @@ app.config([
 // Interceptor to check auth failures in all $http requests
 // http://bahmutov.calepin.co/catch-all-errors-in-angular-app.html
 
-app.factory('unAuthHttpResponseInterceptor', [
-  '$q',
-  '$location',
-  ($q, $location) => ({
-    responseError(response) {
-      // redirect any unauthorised or forbidden responses back to /login
-      //
-      // set disableAutoLoginRedirect:true in the http request config
-      // to disable this behaviour
-      if (
-        [401, 403].includes(response.status) &&
-        !(response.config != null
-          ? response.config.disableAutoLoginRedirect
-          : undefined)
-      ) {
-        // for /project urls set the ?redir parameter to come back here
-        // otherwise just go to the login page
-        if (window.location.pathname.match(/^\/project/)) {
-          window.location = `/login?redir=${encodeURI(
-            window.location.pathname
-          )}`
-        } else {
-          window.location = '/login'
-        }
+app.factory('unAuthHttpResponseInterceptor', ($q, $location) => ({
+  responseError(response) {
+    // redirect any unauthorised or forbidden responses back to /login
+    //
+    // set disableAutoLoginRedirect:true in the http request config
+    // to disable this behaviour
+    if (
+      [401, 403].includes(response.status) &&
+      !(response.config != null
+        ? response.config.disableAutoLoginRedirect
+        : undefined)
+    ) {
+      // for /project urls set the ?redir parameter to come back here
+      // otherwise just go to the login page
+      if (window.location.pathname.match(/^\/project/)) {
+        window.location = `/login?redir=${encodeURI(window.location.pathname)}`
+      } else {
+        window.location = '/login'
       }
-      // pass the response back to the original requester
-      return $q.reject(response)
     }
-  })
-])
+    // pass the response back to the original requester
+    return $q.reject(response)
+  }
+}))
 
 app.config([
   '$httpProvider',

Некоторые файлы не были показаны из-за большого количества измененных файлов