diff --git a/services/web/frontend/js/base.js b/services/web/frontend/js/base.js index b327f3bcdb..1327569685 100644 --- a/services/web/frontend/js/base.js +++ b/services/web/frontend/js/base.js @@ -37,23 +37,31 @@ const App = angular 'sessionStorage', 'ui.select', ]) - .config(function ($qProvider, $httpProvider, uiSelectConfig) { - $qProvider.errorOnUnhandledRejections(false) - uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin' + .config([ + '$qProvider', + 'uiSelectConfig', + function ($qProvider, uiSelectConfig) { + $qProvider.errorOnUnhandledRejections(false) + uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin' - configureMathJax() - }) + configureMathJax() + }, + ]) -App.run(($rootScope, $templateCache) => { - $rootScope.usersEmail = getMeta('ol-usersEmail') +App.run([ + '$rootScope', + '$templateCache', + function ($rootScope, $templateCache) { + $rootScope.usersEmail = getMeta('ol-usersEmail') - // UI Select templates are hard-coded and use Glyphicon icons (which we don't import). - // The line below simply overrides the hard-coded template with our own, which is - // basically the same but using Font Awesome icons. - $templateCache.put( - 'bootstrap/match.tpl.html', - '
' - ) -}) + // UI Select templates are hard-coded and use Glyphicon icons (which we don't import). + // The line below simply overrides the hard-coded template with our own, which is + // basically the same but using Font Awesome icons. + $templateCache.put( + 'bootstrap/match.tpl.html', + '' + ) + }, +]) export default App diff --git a/services/web/frontend/js/directives/asyncForm.js b/services/web/frontend/js/directives/asyncForm.js index 970b186a76..626e63a5be 100644 --- a/services/web/frontend/js/directives/asyncForm.js +++ b/services/web/frontend/js/directives/asyncForm.js @@ -1,171 +1,176 @@ import App from '../base' import '../vendor/libs/passfield' -App.directive('asyncForm', ($http, validateCaptcha, validateCaptchaV3) => ({ - controller: [ - '$scope', - '$location', - function ($scope, $location) { - this.getEmail = () => $scope.email - this.getEmailFromQuery = () => - $location.search().email || $location.search().new_email - return this - }, - ], - link(scope, element, attrs, ctrl) { - let response - const formName = attrs.asyncForm +App.directive('asyncForm', [ + '$http', + 'validateCaptcha', + 'validateCaptchaV3', + ($http, validateCaptcha, validateCaptchaV3) => ({ + controller: [ + '$scope', + '$location', + function ($scope, $location) { + this.getEmail = () => $scope.email + this.getEmailFromQuery = () => + $location.search().email || $location.search().new_email + return this + }, + ], + link(scope, element, attrs, ctrl) { + let response + const formName = attrs.asyncForm - scope[attrs.name].response = response = {} - scope[attrs.name].inflight = false - scope.email = - scope.email || - scope.usersEmail || - ctrl.getEmailFromQuery() || - attrs.newEmail + scope[attrs.name].response = response = {} + scope[attrs.name].inflight = false + scope.email = + scope.email || + scope.usersEmail || + ctrl.getEmailFromQuery() || + attrs.newEmail - const validateCaptchaIfEnabled = function (callback) { - scope.$applyAsync(() => { - scope[attrs.name].inflight = true - }) + const validateCaptchaIfEnabled = function (callback) { + scope.$applyAsync(() => { + scope[attrs.name].inflight = true + }) - if (attrs.captchaActionName) { - validateCaptchaV3(attrs.captchaActionName) - } - if (attrs.captcha != null) { - validateCaptcha(callback) - } else { - callback() - } - } - - const _submitRequest = function (grecaptchaResponse) { - const formData = {} - for (const data of Array.from(element.serializeArray())) { - formData[data.name] = data.value + if (attrs.captchaActionName) { + validateCaptchaV3(attrs.captchaActionName) + } + if (attrs.captcha != null) { + validateCaptcha(callback) + } else { + callback() + } } - if (grecaptchaResponse) { - formData['g-recaptcha-response'] = grecaptchaResponse - } + const _submitRequest = function (grecaptchaResponse) { + const formData = {} + for (const data of Array.from(element.serializeArray())) { + formData[data.name] = data.value + } - // clear the response object which may be referenced downstream - Object.keys(response).forEach(field => delete response[field]) + if (grecaptchaResponse) { + formData['g-recaptcha-response'] = grecaptchaResponse + } - // for asyncForm prevent automatic redirect to /login if - // authentication fails, we will handle it ourselves - const httpRequestFn = _httpRequestFn(element.attr('method')) - return httpRequestFn(element.attr('action'), formData, { - disableAutoLoginRedirect: true, - }) - .then(function (httpResponse) { - const { data, headers } = httpResponse - scope[attrs.name].inflight = false - response.success = true - response.error = false + // clear the response object which may be referenced downstream + Object.keys(response).forEach(field => delete response[field]) - const onSuccessHandler = scope[attrs.onSuccess] - if (onSuccessHandler) { - onSuccessHandler(httpResponse) - return - } + // for asyncForm prevent automatic redirect to /login if + // authentication fails, we will handle it ourselves + const httpRequestFn = _httpRequestFn(element.attr('method')) + return httpRequestFn(element.attr('action'), formData, { + disableAutoLoginRedirect: true, + }) + .then(function (httpResponse) { + const { data, headers } = httpResponse + scope[attrs.name].inflight = false + response.success = true + response.error = false - if (data.redir) { - ga('send', 'event', formName, 'success') - return (window.location = data.redir) - } else if (data.message) { - response.message = data.message + const onSuccessHandler = scope[attrs.onSuccess] + if (onSuccessHandler) { + onSuccessHandler(httpResponse) + return + } - if (data.message.type === 'error') { - response.success = false - response.error = true - return ga('send', 'event', formName, 'failure', data.message) + if (data.redir) { + ga('send', 'event', formName, 'success') + return (window.location = data.redir) + } else if (data.message) { + response.message = data.message + + if (data.message.type === 'error') { + response.success = false + response.error = true + return ga('send', 'event', formName, 'failure', data.message) + } else { + return ga('send', 'event', formName, 'success') + } + } else if (scope.$eval(attrs.asyncFormDownloadResponse)) { + const blob = new Blob([data], { + type: headers('Content-Type'), + }) + location.href = URL.createObjectURL(blob) // Trigger file save + } + }) + .catch(function (httpResponse) { + const { data, status } = httpResponse + scope[attrs.name].inflight = false + response.success = false + response.error = true + response.status = status + response.data = data + + const onErrorHandler = scope[attrs.onError] + if (onErrorHandler) { + onErrorHandler(httpResponse) + return + } + + let responseMessage + if (data.message && data.message.text) { + responseMessage = data.message.text } else { - return ga('send', 'event', formName, 'success') + responseMessage = data.message } - } else if (scope.$eval(attrs.asyncFormDownloadResponse)) { - const blob = new Blob([data], { - type: headers('Content-Type'), - }) - location.href = URL.createObjectURL(blob) // Trigger file save - } - }) - .catch(function (httpResponse) { - const { data, status } = httpResponse - scope[attrs.name].inflight = false - response.success = false - response.error = true - response.status = status - response.data = data - const onErrorHandler = scope[attrs.onError] - if (onErrorHandler) { - onErrorHandler(httpResponse) - return - } - - let responseMessage - if (data.message && data.message.text) { - responseMessage = data.message.text - } else { - responseMessage = data.message - } - - if (status === 400) { - // Bad Request - response.message = { - text: - responseMessage || - 'Invalid Request. Please correct the data and try again.', - type: 'error', + if (status === 400) { + // Bad Request + response.message = { + text: + responseMessage || + 'Invalid Request. Please correct the data and try again.', + type: 'error', + } + } else if (status === 403) { + // Forbidden + response.message = { + text: + responseMessage || + 'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.', + type: 'error', + } + } else if (status === 429) { + response.message = { + text: + responseMessage || + 'Too many attempts. Please wait for a while and try again.', + type: 'error', + } + } else { + response.message = { + text: + responseMessage || + 'Something went wrong talking to the server :(. Please try again.', + type: 'error', + } } - } else if (status === 403) { - // Forbidden - response.message = { - text: - responseMessage || - 'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.', - type: 'error', - } - } else if (status === 429) { - response.message = { - text: - responseMessage || - 'Too many attempts. Please wait for a while and try again.', - type: 'error', - } - } else { - response.message = { - text: - responseMessage || - 'Something went wrong talking to the server :(. Please try again.', - type: 'error', - } - } - ga('send', 'event', formName, 'failure', data.message) - }) - } - - const submit = () => - validateCaptchaIfEnabled(response => _submitRequest(response)) - - const _httpRequestFn = (method = 'post') => { - const $HTTP_FNS = { - post: $http.post, - get: $http.get, + ga('send', 'event', formName, 'failure', data.message) + }) } - return $HTTP_FNS[method.toLowerCase()] - } - element.on('submit', function (e) { - e.preventDefault() - submit() - }) + const submit = () => + validateCaptchaIfEnabled(response => _submitRequest(response)) - if (attrs.autoSubmit) { - submit() - } - }, -})) + const _httpRequestFn = (method = 'post') => { + const $HTTP_FNS = { + post: $http.post, + get: $http.get, + } + return $HTTP_FNS[method.toLowerCase()] + } + + element.on('submit', function (e) { + e.preventDefault() + submit() + }) + + if (attrs.autoSubmit) { + submit() + } + }, + }), +]) App.directive('formMessages', () => ({ restrict: 'E', diff --git a/services/web/frontend/js/directives/bookmarkableTabset.js b/services/web/frontend/js/directives/bookmarkableTabset.js index 1293f8ba16..c9f530c954 100644 --- a/services/web/frontend/js/directives/bookmarkableTabset.js +++ b/services/web/frontend/js/directives/bookmarkableTabset.js @@ -1,58 +1,64 @@ import _ from 'lodash' import App from '../base' -App.directive('bookmarkableTabset', $location => ({ - restrict: 'A', - require: 'tabset', - link(scope, el, attrs, tabset) { - const _makeActive = function (hash) { - if (hash && hash !== '') { - const matchingTab = _.find( - tabset.tabs, - tab => tab.bookmarkableTabId === hash - ) - if (matchingTab) { - matchingTab.select() - return el.children()[0].scrollIntoView({ behavior: 'smooth' }) +App.directive('bookmarkableTabset', [ + '$location', + $location => ({ + restrict: 'A', + require: 'tabset', + link(scope, el, attrs, tabset) { + const _makeActive = function (hash) { + if (hash && hash !== '') { + const matchingTab = _.find( + tabset.tabs, + tab => tab.bookmarkableTabId === hash + ) + if (matchingTab) { + matchingTab.select() + return el.children()[0].scrollIntoView({ behavior: 'smooth' }) + } } } - } - scope.$applyAsync(function () { - // for page load - const hash = $location.hash() - _makeActive(hash) - - // for links within page to a tab - // this needs to be within applyAsync because there could be a link - // within a tab to another tab - const linksToTabs = document.querySelectorAll('.link-to-tab') - const _clickLinkToTab = event => { - const hash = event.currentTarget.getAttribute('href').split('#').pop() + scope.$applyAsync(function () { + // for page load + const hash = $location.hash() _makeActive(hash) - } - if (linksToTabs) { - Array.from(linksToTabs).map(link => - link.addEventListener('click', _clickLinkToTab) - ) - } - }) - }, -})) + // for links within page to a tab + // this needs to be within applyAsync because there could be a link + // within a tab to another tab + const linksToTabs = document.querySelectorAll('.link-to-tab') + const _clickLinkToTab = event => { + const hash = event.currentTarget.getAttribute('href').split('#').pop() + _makeActive(hash) + } -App.directive('bookmarkableTab', $location => ({ - restrict: 'A', - require: 'tab', - link(scope, el, attrs, tab) { - const tabScope = el.isolateScope() - const tabId = attrs.bookmarkableTab - if (tabScope && tabId && tabId !== '') { - tabScope.bookmarkableTabId = tabId - tabScope.$watch('active', function (isActive, wasActive) { - if (isActive && !wasActive && $location.hash() !== tabId) { - return $location.hash(tabId) + if (linksToTabs) { + Array.from(linksToTabs).map(link => + link.addEventListener('click', _clickLinkToTab) + ) } }) - } - }, -})) + }, + }), +]) + +App.directive('bookmarkableTab', [ + '$location', + $location => ({ + restrict: 'A', + require: 'tab', + link(scope, el, attrs, tab) { + const tabScope = el.isolateScope() + const tabId = attrs.bookmarkableTab + if (tabScope && tabId && tabId !== '') { + tabScope.bookmarkableTabId = tabId + tabScope.$watch('active', function (isActive, wasActive) { + if (isActive && !wasActive && $location.hash() !== tabId) { + return $location.hash(tabId) + } + }) + } + }, + }), +]) diff --git a/services/web/frontend/js/directives/eventTracking.js b/services/web/frontend/js/directives/eventTracking.js index 2836b9fb3e..a1db53d987 100644 --- a/services/web/frontend/js/directives/eventTracking.js +++ b/services/web/frontend/js/directives/eventTracking.js @@ -61,65 +61,70 @@ const isInViewport = function (element) { return elBtm > viewportTop && elTop < viewportBtm } -export default App.directive('eventTracking', eventTracking => ({ - 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 +export default App.directive('eventTracking', [ + 'eventTracking', + eventTracking => ({ + 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) { - eventTracking[sendMBFunction](scope.eventTracking, segmentation) + if (sendMB) { + eventTracking[sendMBFunction](scope.eventTracking, segmentation) + } + if (sendGA) { + eventTracking[sendGAFunction]( + attrs.eventTrackingGa, + attrs.eventTrackingAction || scope.eventTracking, + attrs.eventTrackingLabel || '' + ) + } + if (scrollEvent) { + return $(window).unbind('resize scroll') + } } - if (sendGA) { - eventTracking[sendGAFunction]( - attrs.eventTrackingGa, - attrs.eventTrackingAction || scope.eventTracking, - attrs.eventTrackingLabel || '' + + if (attrs.eventTrackingTrigger === 'load') { + return sendEvent() + } else if (attrs.eventTrackingTrigger === 'click') { + return element.on('click', e => sendEvent()) + } else if (attrs.eventTrackingTrigger === 'hover') { + let timer = null + let timeoutAmt = 500 + if (attrs.eventHoverAmt) { + timeoutAmt = parseInt(attrs.eventHoverAmt, 10) + } + return element + .on('mouseenter', function () { + timer = setTimeout(() => sendEvent(), timeoutAmt) + }) + .on('mouseleave', () => clearTimeout(timer)) + } else if ( + attrs.eventTrackingTrigger === 'scroll' && + !eventTracking.eventInCache(scope.eventTracking) + ) { + $(window).on( + 'resize scroll', + _.throttle(() => { + if (isInViewport(element)) { + sendEvent(true) + } + }, 500) ) } - if (scrollEvent) { - return $(window).unbind('resize scroll') - } - } - - if (attrs.eventTrackingTrigger === 'load') { - return sendEvent() - } else if (attrs.eventTrackingTrigger === 'click') { - return element.on('click', e => sendEvent()) - } else if (attrs.eventTrackingTrigger === 'hover') { - let timer = null - let timeoutAmt = 500 - if (attrs.eventHoverAmt) { - timeoutAmt = parseInt(attrs.eventHoverAmt, 10) - } - return element - .on('mouseenter', function () { - timer = setTimeout(() => sendEvent(), timeoutAmt) - }) - .on('mouseleave', () => clearTimeout(timer)) - } else if ( - attrs.eventTrackingTrigger === 'scroll' && - !eventTracking.eventInCache(scope.eventTracking) - ) { - $(window).on( - 'resize scroll', - _.throttle(() => { - if (isInViewport(element)) { - sendEvent(true) - } - }, 500) - ) - } - }, -})) + }, + }), +]) diff --git a/services/web/frontend/js/directives/focus.js b/services/web/frontend/js/directives/focus.js index 17ce160e7b..7d10bc9998 100644 --- a/services/web/frontend/js/directives/focus.js +++ b/services/web/frontend/js/directives/focus.js @@ -10,52 +10,61 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import App from '../base' -App.directive('focusWhen', $timeout => ({ - restrict: 'A', - link(scope, element, attr) { - return scope.$watch(attr.focusWhen, function (value) { - if (value) { - return $timeout(() => element.focus()) - } - }) - }, -})) +App.directive('focusWhen', [ + '$timeout', + $timeout => ({ + restrict: 'A', + link(scope, element, attr) { + return scope.$watch(attr.focusWhen, function (value) { + if (value) { + return $timeout(() => element.focus()) + } + }) + }, + }), +]) -App.directive('focusOn', $timeout => ({ +App.directive('focusOn', () => ({ restrict: 'A', link(scope, element, attrs) { return scope.$on(attrs.focusOn, () => element.focus()) }, })) -App.directive('selectWhen', $timeout => ({ - restrict: 'A', - link(scope, element, attr) { - return scope.$watch(attr.selectWhen, function (value) { - if (value) { - return $timeout(() => element.select()) - } - }) - }, -})) +App.directive('selectWhen', [ + '$timeout', + $timeout => ({ + restrict: 'A', + link(scope, element, attr) { + return scope.$watch(attr.selectWhen, function (value) { + if (value) { + return $timeout(() => element.select()) + } + }) + }, + }), +]) -App.directive('selectOn', $timeout => ({ +App.directive('selectOn', () => ({ restrict: 'A', link(scope, element, attrs) { return scope.$on(attrs.selectOn, () => element.select()) }, })) -App.directive('selectNameWhen', $timeout => ({ - restrict: 'A', - link(scope, element, attrs) { - return scope.$watch(attrs.selectNameWhen, function (value) { - if (value) { - return $timeout(() => selectName(element)) - } - }) - }, -})) +App.directive('selectNameWhen', [ + '$timeout', + $timeout => ({ + restrict: 'A', + link(scope, element, attrs) { + return scope.$watch(attrs.selectNameWhen, function (value) { + if (value) { + return $timeout(() => selectName(element)) + } + }) + }, + }), +]) App.directive('selectNameOn', () => ({ restrict: 'A', @@ -64,19 +73,22 @@ App.directive('selectNameOn', () => ({ }, })) -App.directive('focus', $timeout => ({ - scope: { - trigger: '@focus', - }, +App.directive('focus', [ + '$timeout', + $timeout => ({ + scope: { + trigger: '@focus', + }, - link(scope, element) { - return scope.$watch('trigger', function (value) { - if (value === 'true') { - return $timeout(() => element[0].focus()) - } - }) - }, -})) + link(scope, element) { + return scope.$watch('trigger', function (value) { + if (value === 'true') { + return $timeout(() => element[0].focus()) + } + }) + }, + }), +]) function selectName(element) { // Select up to last '.'. I.e. everything except the file extension diff --git a/services/web/frontend/js/directives/mathjax.js b/services/web/frontend/js/directives/mathjax.js index 25627035ad..f391045b68 100644 --- a/services/web/frontend/js/directives/mathjax.js +++ b/services/web/frontend/js/directives/mathjax.js @@ -2,7 +2,7 @@ import App from '../base' -export default App.directive('mathjax', function ($compile, $parse) { +export default App.directive('mathjax', function () { return { link(scope, element, attrs) { if (!(MathJax && MathJax.Hub)) return diff --git a/services/web/frontend/js/directives/scroll.js b/services/web/frontend/js/directives/scroll.js index 82abe22d7e..7fc7f1e634 100644 --- a/services/web/frontend/js/directives/scroll.js +++ b/services/web/frontend/js/directives/scroll.js @@ -11,46 +11,49 @@ */ import App from '../base' -export default App.directive('updateScrollBottomOn', $timeout => ({ - restrict: 'A', - link(scope, element, attrs, ctrls) { - // We keep the offset from the bottom fixed whenever the event fires - // - // ^ | ^ - // | | | scrollTop - // | | v - // | |----------- - // | | ^ - // | | | - // | | | clientHeight (viewable area) - // | | | - // | | | - // | | v - // | |----------- - // | | ^ - // | | | scrollBottom - // v | v - // \ - // scrollHeight +export default App.directive('updateScrollBottomOn', [ + '$timeout', + $timeout => ({ + restrict: 'A', + link(scope, element, attrs, ctrls) { + // We keep the offset from the bottom fixed whenever the event fires + // + // ^ | ^ + // | | | scrollTop + // | | v + // | |----------- + // | | ^ + // | | | + // | | | clientHeight (viewable area) + // | | | + // | | | + // | | v + // | |----------- + // | | ^ + // | | | scrollBottom + // v | v + // \ + // scrollHeight - let scrollBottom = 0 - element.on( - 'scroll', - e => - (scrollBottom = - element[0].scrollHeight - - element[0].scrollTop - - element[0].clientHeight) - ) - - return scope.$on(attrs.updateScrollBottomOn, () => - $timeout( - () => - element.scrollTop( - element[0].scrollHeight - element[0].clientHeight - scrollBottom - ), - 0 + let scrollBottom = 0 + element.on( + 'scroll', + e => + (scrollBottom = + element[0].scrollHeight - + element[0].scrollTop - + element[0].clientHeight) ) - ) - }, -})) + + return scope.$on(attrs.updateScrollBottomOn, () => + $timeout( + () => + element.scrollTop( + element[0].scrollHeight - element[0].clientHeight - scrollBottom + ), + 0 + ) + ) + }, + }), +]) diff --git a/services/web/frontend/js/directives/stopPropagation.js b/services/web/frontend/js/directives/stopPropagation.js index f310f7808a..0682e128ef 100644 --- a/services/web/frontend/js/directives/stopPropagation.js +++ b/services/web/frontend/js/directives/stopPropagation.js @@ -9,14 +9,14 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import App from '../base' -App.directive('stopPropagation', $http => ({ +App.directive('stopPropagation', () => ({ restrict: 'A', link(scope, element, attrs) { return element.bind(attrs.stopPropagation, e => e.stopPropagation()) }, })) -export default App.directive('preventDefault', $http => ({ +export default App.directive('preventDefault', () => ({ restrict: 'A', link(scope, element, attrs) { return element.bind(attrs.preventDefault, e => e.preventDefault()) diff --git a/services/web/frontend/js/directives/videoPlayState.js b/services/web/frontend/js/directives/videoPlayState.js index c99b5131a2..580a464f73 100644 --- a/services/web/frontend/js/directives/videoPlayState.js +++ b/services/web/frontend/js/directives/videoPlayState.js @@ -10,20 +10,23 @@ */ import App from '../base' -export default App.directive('videoPlayState', $parse => ({ - restrict: 'A', - link(scope, element, attrs) { - const videoDOMEl = element[0] - return scope.$watch( - () => $parse(attrs.videoPlayState)(scope), - function (shouldPlay) { - if (shouldPlay) { - videoDOMEl.currentTime = 0 - return videoDOMEl.play() - } else { - return videoDOMEl.pause() +export default App.directive('videoPlayState', [ + '$parse', + $parse => ({ + restrict: 'A', + link(scope, element, attrs) { + const videoDOMEl = element[0] + return scope.$watch( + () => $parse(attrs.videoPlayState)(scope), + function (shouldPlay) { + if (shouldPlay) { + videoDOMEl.currentTime = 0 + return videoDOMEl.play() + } else { + return videoDOMEl.pause() + } } - } - ) - }, -})) + ) + }, + }), +]) diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js b/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js index 54f5c2580c..c538c7efca 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js +++ b/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js @@ -3,10 +3,14 @@ import { react2angular } from 'react2angular' import EditorNavigationToolbarRoot from '../components/editor-navigation-toolbar-root' import { rootContext } from '../../../shared/context/root-context' -App.controller('EditorNavigationToolbarController', function ($scope, ide) { - // wrapper is required to avoid scope problems with `this` inside `EditorManager` - $scope.openDoc = (doc, args) => ide.editorManager.openDoc(doc, args) -}) +App.controller('EditorNavigationToolbarController', [ + '$scope', + 'ide', + function ($scope, ide) { + // wrapper is required to avoid scope problems with `this` inside `EditorManager` + $scope.openDoc = (doc, args) => ide.editorManager.openDoc(doc, args) + }, +]) App.component( 'editorNavigationToolbarRoot', diff --git a/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js b/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js index 769fb84998..61a9666664 100644 --- a/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js +++ b/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js @@ -5,14 +5,11 @@ import { cloneDeep } from 'lodash' import FileTreeRoot from '../components/file-tree-root' import { rootContext } from '../../../shared/context/root-context' -App.controller( - 'ReactFileTreeController', - function ( - $scope, - $timeout, - ide - // eventTracking - ) { +App.controller('ReactFileTreeController', [ + '$scope', + '$timeout', + 'ide', + function ($scope, $timeout, ide) { $scope.isConnected = true $scope.$on('project:joined', () => { @@ -108,8 +105,8 @@ App.controller( $scope.reindexReferences = () => { ide.$scope.$emit('references:should-reindex', {}) } - } -) + }, +]) App.component( 'fileTreeRoot', diff --git a/services/web/frontend/js/features/file-view/controllers/file-view-controller.js b/services/web/frontend/js/features/file-view/controllers/file-view-controller.js index 63223a7dbd..301fab5bd3 100644 --- a/services/web/frontend/js/features/file-view/controllers/file-view-controller.js +++ b/services/web/frontend/js/features/file-view/controllers/file-view-controller.js @@ -5,8 +5,9 @@ import _ from 'lodash' import { rootContext } from '../../../shared/context/root-context' import FileView from '../components/file-view' -export default App.controller( - 'FileViewController', +export default App.controller('FileViewController', [ + '$scope', + '$rootScope', function ($scope, $rootScope) { $scope.file = $scope.openFile @@ -14,8 +15,8 @@ export default App.controller( const oldKeys = $rootScope._references.keys return ($rootScope._references.keys = _.union(oldKeys, newKeys)) } - } -) + }, +]) App.component( 'fileView', diff --git a/services/web/frontend/js/features/outline/controllers/outline-controller.js b/services/web/frontend/js/features/outline/controllers/outline-controller.js index 4f012cc5df..62728f3a35 100644 --- a/services/web/frontend/js/features/outline/controllers/outline-controller.js +++ b/services/web/frontend/js/features/outline/controllers/outline-controller.js @@ -3,42 +3,47 @@ import OutlinePane from '../components/outline-pane' import { react2angular } from 'react2angular' import { rootContext } from '../../../shared/context/root-context' -App.controller('OutlineController', function ($scope, ide, eventTracking) { - $scope.isTexFile = false - $scope.outline = [] - $scope.eventTracking = eventTracking +App.controller('OutlineController', [ + '$scope', + 'ide', + 'eventTracking', + function ($scope, ide, eventTracking) { + $scope.isTexFile = false + $scope.outline = [] + $scope.eventTracking = eventTracking - function shouldShowOutline() { - return !$scope.editor.newSourceEditor - } + function shouldShowOutline() { + return !$scope.editor.newSourceEditor + } - $scope.show = shouldShowOutline() - - $scope.$watch('editor.newSourceEditor', function () { $scope.show = shouldShowOutline() - }) - $scope.$on('outline-manager:outline-changed', onOutlineChange) - - function onOutlineChange(e, outlineInfo) { - $scope.$applyAsync(() => { - $scope.isTexFile = outlineInfo.isTexFile - $scope.outline = outlineInfo.outline - $scope.highlightedLine = outlineInfo.highlightedLine + $scope.$watch('editor.newSourceEditor', function () { + $scope.show = shouldShowOutline() }) - } - $scope.jumpToLine = (lineNo, syncToPdf) => { - ide.outlineManager.jumpToLine(lineNo, syncToPdf) - eventTracking.sendMB('outline-jump-to-line') - } + $scope.$on('outline-manager:outline-changed', onOutlineChange) - $scope.onToggle = isOpen => { - $scope.$applyAsync(() => { - $scope.$emit('outline-toggled', isOpen) - }) - } -}) + function onOutlineChange(e, outlineInfo) { + $scope.$applyAsync(() => { + $scope.isTexFile = outlineInfo.isTexFile + $scope.outline = outlineInfo.outline + $scope.highlightedLine = outlineInfo.highlightedLine + }) + } + + $scope.jumpToLine = (lineNo, syncToPdf) => { + ide.outlineManager.jumpToLine(lineNo, syncToPdf) + eventTracking.sendMB('outline-jump-to-line') + } + + $scope.onToggle = isOpen => { + $scope.$applyAsync(() => { + $scope.$emit('outline-toggled', isOpen) + }) + } + }, +]) // Wrap React component as Angular component. Only needed for "top-level" component App.component( diff --git a/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js b/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js index ef69cd248b..c546c6169d 100644 --- a/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js +++ b/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js @@ -14,8 +14,10 @@ App.component( ) ) -export default App.controller( - 'ReactShareProjectModalController', +export default App.controller('ReactShareProjectModalController', [ + '$scope', + 'eventTracking', + 'ide', function ($scope, eventTracking, ide) { $scope.show = false @@ -61,5 +63,5 @@ export default App.controller( }) } }) - } -) + }, +]) diff --git a/services/web/frontend/js/ide.js b/services/web/frontend/js/ide.js index bea5d3af1d..ef5020641b 100644 --- a/services/web/frontend/js/ide.js +++ b/services/web/frontend/js/ide.js @@ -70,8 +70,15 @@ import { reportCM6Perf } from './infrastructure/cm6-performance' import { reportAcePerf } from './ide/editor/ace-performance' import { debugConsole } from '@/utils/debugging' -App.controller( - 'IdeController', +App.controller('IdeController', [ + '$scope', + '$timeout', + 'ide', + 'localStorage', + 'eventTracking', + 'metadata', + 'CobrandingDataService', + '$window', function ( $scope, $timeout, @@ -79,7 +86,6 @@ App.controller( localStorage, eventTracking, metadata, - $q, CobrandingDataService, $window ) { @@ -478,23 +484,26 @@ If the project has been renamed please look in your project list for a new proje return $scope.$digest() } }) - } -) + }, +]) cleanupServiceWorker() -angular.module('SharelatexApp').config(function ($provide) { - $provide.decorator('$browser', [ - '$delegate', - function ($delegate) { - $delegate.onUrlChange = function () {} - $delegate.url = function () { - return '' - } - return $delegate - }, - ]) -}) +angular.module('SharelatexApp').config([ + '$provide', + function ($provide) { + $provide.decorator('$browser', [ + '$delegate', + function ($delegate) { + $delegate.onUrlChange = function () {} + $delegate.url = function () { + return '' + } + return $delegate + }, + ]) + }, +]) export default angular.bootstrap(document.body, ['SharelatexApp']) diff --git a/services/web/frontend/js/ide/directives/layout.js b/services/web/frontend/js/ide/directives/layout.js index 4216eebbfb..e0c0efe79d 100644 --- a/services/web/frontend/js/ide/directives/layout.js +++ b/services/web/frontend/js/ide/directives/layout.js @@ -17,196 +17,201 @@ import _ from 'lodash' import '../../vendor/libs/jquery-layout' import '../../vendor/libs/jquery.ui.touch-punch' -export default App.directive('layout', ($parse, $compile, ide) => ({ - compile() { - return { - pre(scope, element, attrs) { - let customTogglerEl, spacingClosed, spacingOpen, state - const name = attrs.layout +export default App.directive('layout', [ + '$parse', + '$compile', + 'ide', + ($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 = 7 - } - - if (attrs.spacingClosed != null) { - spacingClosed = parseInt(attrs.spacingClosed, 10) - } else { - spacingClosed = 7 - } - - const options = { - spacing_open: spacingOpen, - spacing_closed: spacingClosed, - slidable: false, - enableCursorHotkey: false, - onopen: pane => { - return onPaneOpen(pane) - }, - onclose: pane => { - return onPaneClose(pane) - }, - onresize: () => { - return onInternalResize() - }, - maskIframesOnResize: scope.$eval( - attrs.maskIframesOnResize || 'false' - ), - east: { - size: scope.$eval(attrs.initialSizeEast), - initClosed: scope.$eval(attrs.initClosedEast), - }, - west: { - size: scope.$eval(attrs.initialSizeWest), - initClosed: scope.$eval(attrs.initClosedWest), - }, - } - - // Restore previously recorded state - if ((state = ide.localStorage(`layout.${name}`)) != null) { - if (state.east != null) { - if ( - attrs.minimumRestoreSizeEast == null || - (state.east.size >= attrs.minimumRestoreSizeEast && - !state.east.initClosed) - ) { - options.east = state.east - } - options.east.initClosed = state.east.initClosed + if (attrs.spacingOpen != null) { + spacingOpen = parseInt(attrs.spacingOpen, 10) + } else { + spacingOpen = 7 } - if (state.west != null) { - if ( - attrs.minimumRestoreSizeWest == null || - (state.west.size >= attrs.minimumRestoreSizeWest && - !state.west.initClosed) - ) { - options.west = state.west - } - // NOTE: disabled so that the file tree re-opens on page load - // options.west.initClosed = state.west.initClosed - } - } - options.east.resizerCursor = 'ew-resize' - options.west.resizerCursor = 'ew-resize' - - function repositionControls() { - state = layout.readState() - if (state.east != null) { - const controls = element.find('> .ui-layout-resizer-controls') - if (state.east.initClosed) { - return controls.hide() - } else { - controls.show() - return controls.css({ - right: state.east.size, - }) - } + if (attrs.spacingClosed != null) { + spacingClosed = parseInt(attrs.spacingClosed, 10) + } else { + spacingClosed = 7 } - } - function repositionCustomToggler() { - if (customTogglerEl == null) { - return + const options = { + spacing_open: spacingOpen, + spacing_closed: spacingClosed, + slidable: false, + enableCursorHotkey: false, + onopen: pane => { + return onPaneOpen(pane) + }, + onclose: pane => { + return onPaneClose(pane) + }, + onresize: () => { + return onInternalResize() + }, + maskIframesOnResize: scope.$eval( + attrs.maskIframesOnResize || 'false' + ), + east: { + size: scope.$eval(attrs.initialSizeEast), + initClosed: scope.$eval(attrs.initClosedEast), + }, + west: { + size: scope.$eval(attrs.initialSizeWest), + initClosed: scope.$eval(attrs.initClosedWest), + }, } - state = layout.readState() - const positionAnchor = customTogglerPane === 'east' ? 'right' : 'left' - const paneState = state[customTogglerPane] - if (paneState != null) { - return customTogglerEl.css( - positionAnchor, - paneState.initClosed ? 0 : paneState.size - ) - } - } - function resetOpenStates() { - state = layout.readState() - if (attrs.openEast != null && state.east != null) { - const openEast = $parse(attrs.openEast) - return openEast.assign(scope, !state.east.initClosed) - } - } - - // Someone moved the resizer - function onInternalResize() { - state = layout.readState() - scope.$broadcast(`layout:${name}:resize`, state) - repositionControls() - if (hasCustomToggler) { - repositionCustomToggler() - } - return resetOpenStates() - } - - let oldWidth = element.width() - // Something resized our parent element - const onExternalResize = function () { - if ( - attrs.resizeProportionally != null && - scope.$eval(attrs.resizeProportionally) - ) { - const eastState = layout.readState().east - if (eastState != null) { - const currentWidth = element.width() - if (currentWidth > 0) { - const newInternalWidth = - (eastState.size / oldWidth) * currentWidth - oldWidth = currentWidth - layout.sizePane('east', newInternalWidth) + // Restore previously recorded state + if ((state = ide.localStorage(`layout.${name}`)) != null) { + if (state.east != null) { + if ( + attrs.minimumRestoreSizeEast == null || + (state.east.size >= attrs.minimumRestoreSizeEast && + !state.east.initClosed) + ) { + options.east = state.east } + options.east.initClosed = state.east.initClosed + } + if (state.west != null) { + if ( + attrs.minimumRestoreSizeWest == null || + (state.west.size >= attrs.minimumRestoreSizeWest && + !state.west.initClosed) + ) { + options.west = state.west + } + // NOTE: disabled so that the file tree re-opens on page load + // options.west.initClosed = state.west.initClosed + } + } + + options.east.resizerCursor = 'ew-resize' + options.west.resizerCursor = 'ew-resize' + + function repositionControls() { + state = layout.readState() + if (state.east != null) { + const controls = element.find('> .ui-layout-resizer-controls') + if (state.east.initClosed) { + return controls.hide() + } else { + controls.show() + return controls.css({ + right: state.east.size, + }) + } + } + } + + function repositionCustomToggler() { + if (customTogglerEl == null) { return } + state = layout.readState() + const positionAnchor = + customTogglerPane === 'east' ? 'right' : 'left' + const paneState = state[customTogglerPane] + if (paneState != null) { + return customTogglerEl.css( + positionAnchor, + paneState.initClosed ? 0 : paneState.size + ) + } } - ide.$timeout(() => { - layout.resizeAll() - }) - } - - const layout = element.layout(options) - layout.resizeAll() - - if (attrs.resizeOn != null) { - for (const event of Array.from(attrs.resizeOn.split(','))) { - scope.$on(event, () => onExternalResize()) - } - } - - if (hasCustomToggler) { - state = layout.readState() - const customTogglerScope = scope.$new() - - customTogglerScope.isOpen = true - customTogglerScope.isVisible = true - - if ( - (state[customTogglerPane] != null - ? state[customTogglerPane].initClosed - : undefined) === true - ) { - customTogglerScope.isOpen = false + function resetOpenStates() { + state = layout.readState() + if (attrs.openEast != null && state.east != null) { + const openEast = $parse(attrs.openEast) + return openEast.assign(scope, !state.east.initClosed) + } } - customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen - customTogglerScope.tooltipMsgWhenClosed = customTogglerMsgWhenClosed - - customTogglerScope.tooltipPlacement = - customTogglerPane === 'east' ? 'left' : 'right' - customTogglerScope.handleClick = function () { - layout.toggle(customTogglerPane) - return repositionCustomToggler() + // Someone moved the resizer + function onInternalResize() { + state = layout.readState() + scope.$broadcast(`layout:${name}:resize`, state) + repositionControls() + if (hasCustomToggler) { + repositionCustomToggler() + } + return resetOpenStates() } - customTogglerEl = $compile(`\ + + let oldWidth = element.width() + // Something resized our parent element + const onExternalResize = function () { + if ( + attrs.resizeProportionally != null && + scope.$eval(attrs.resizeProportionally) + ) { + const eastState = layout.readState().east + if (eastState != null) { + const currentWidth = element.width() + if (currentWidth > 0) { + const newInternalWidth = + (eastState.size / oldWidth) * currentWidth + oldWidth = currentWidth + layout.sizePane('east', newInternalWidth) + } + return + } + } + + ide.$timeout(() => { + layout.resizeAll() + }) + } + + const layout = element.layout(options) + layout.resizeAll() + + if (attrs.resizeOn != null) { + for (const event of Array.from(attrs.resizeOn.split(','))) { + scope.$on(event, () => onExternalResize()) + } + } + + if (hasCustomToggler) { + state = layout.readState() + const customTogglerScope = scope.$new() + + customTogglerScope.isOpen = true + customTogglerScope.isVisible = true + + if ( + (state[customTogglerPane] != null + ? state[customTogglerPane].initClosed + : undefined) === true + ) { + customTogglerScope.isOpen = false + } + + customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen + customTogglerScope.tooltipMsgWhenClosed = customTogglerMsgWhenClosed + + customTogglerScope.tooltipPlacement = + customTogglerPane === 'east' ? 'left' : 'right' + customTogglerScope.handleClick = function () { + layout.toggle(customTogglerPane) + return repositionCustomToggler() + } + customTogglerEl = $compile(`\ \ `)(customTogglerScope) - element.append(customTogglerEl) - } - - function onPaneOpen(pane) { - if (!hasCustomToggler || pane !== customTogglerPane) { - return + element.append(customTogglerEl) } - return customTogglerEl - .scope() - .$applyAsync(() => (customTogglerEl.scope().isOpen = true)) - } - function onPaneClose(pane) { - if (!hasCustomToggler || pane !== customTogglerPane) { - return + function onPaneOpen(pane) { + if (!hasCustomToggler || pane !== customTogglerPane) { + return + } + return customTogglerEl + .scope() + .$applyAsync(() => (customTogglerEl.scope().isOpen = true)) } - return customTogglerEl - .scope() - .$applyAsync(() => (customTogglerEl.scope().isOpen = false)) - } - // Ensure editor resizes after loading. This is to handle the case where - // the window has been resized while the editor is loading - scope.$on('editor:loaded', () => { - ide.$timeout(() => layout.resizeAll()) - }) + function onPaneClose(pane) { + if (!hasCustomToggler || pane !== customTogglerPane) { + return + } + return customTogglerEl + .scope() + .$applyAsync(() => (customTogglerEl.scope().isOpen = false)) + } - // Save state when exiting - $(window).unload(() => { - // Save only the state properties for the current layout, ignoring sublayouts inside it. - // If we save sublayouts state (`children`), the layout library will use it when - // initializing. This raises errors when the sublayout elements aren't available (due to - // being loaded at init or just not existing for the current project/user). - const stateToSave = _.mapValues(layout.readState(), pane => - _.omit(pane, 'children') - ) - ide.localStorage(`layout.${name}`, stateToSave) - }) + // Ensure editor resizes after loading. This is to handle the case where + // the window has been resized while the editor is loading + scope.$on('editor:loaded', () => { + ide.$timeout(() => layout.resizeAll()) + }) - if (attrs.openEast != null) { - scope.$watch(attrs.openEast, function (value, oldValue) { - if (value != null && value !== oldValue) { - if (value) { - layout.open('east') - } else { - layout.close('east') + // Save state when exiting + $(window).unload(() => { + // Save only the state properties for the current layout, ignoring sublayouts inside it. + // If we save sublayouts state (`children`), the layout library will use it when + // initializing. This raises errors when the sublayout elements aren't available (due to + // being loaded at init or just not existing for the current project/user). + const stateToSave = _.mapValues(layout.readState(), pane => + _.omit(pane, 'children') + ) + ide.localStorage(`layout.${name}`, stateToSave) + }) + + if (attrs.openEast != null) { + scope.$watch(attrs.openEast, function (value, oldValue) { + if (value != null && value !== oldValue) { + if (value) { + layout.open('east') + } else { + layout.close('east') + } + if (hasCustomToggler && customTogglerPane === 'east') { + repositionCustomToggler() + customTogglerEl.scope().$applyAsync(function () { + customTogglerEl.scope().isOpen = value + }) + } } - if (hasCustomToggler && customTogglerPane === 'east') { - repositionCustomToggler() - customTogglerEl.scope().$applyAsync(function () { - customTogglerEl.scope().isOpen = value + return setTimeout(() => scope.$digest(), 0) + }) + } + + if (attrs.allowOverflowOn != null) { + const overflowPane = scope.$eval(attrs.allowOverflowOn) + const overflowPaneEl = layout.panes[overflowPane] + // Set the panel as overflowing (gives it higher z-index and sets overflow rules) + layout.allowOverflow(overflowPane) + // Read the given z-index value and increment it, so that it's higher than synctex controls. + const overflowPaneZVal = overflowPaneEl.zIndex() + overflowPaneEl.css('z-index', overflowPaneZVal + 1) + } + + resetOpenStates() + onInternalResize() + + if (attrs.layoutDisabled != null) { + return scope.$watch(attrs.layoutDisabled, function (value) { + if (value) { + layout.hide('east') + } else { + layout.show('east') + } + if (hasCustomToggler) { + return customTogglerEl.scope().$applyAsync(function () { + customTogglerEl.scope().isOpen = !value + return (customTogglerEl.scope().isVisible = !value) }) } - } - return setTimeout(() => scope.$digest(), 0) - }) - } + }) + } + }, - if (attrs.allowOverflowOn != null) { - const overflowPane = scope.$eval(attrs.allowOverflowOn) - const overflowPaneEl = layout.panes[overflowPane] - // Set the panel as overflowing (gives it higher z-index and sets overflow rules) - layout.allowOverflow(overflowPane) - // Read the given z-index value and increment it, so that it's higher than synctex controls. - const overflowPaneZVal = overflowPaneEl.zIndex() - overflowPaneEl.css('z-index', overflowPaneZVal + 1) - } - - resetOpenStates() - onInternalResize() - - if (attrs.layoutDisabled != null) { - return scope.$watch(attrs.layoutDisabled, function (value) { - if (value) { - layout.hide('east') - } else { - layout.show('east') - } - if (hasCustomToggler) { - return customTogglerEl.scope().$applyAsync(function () { - customTogglerEl.scope().isOpen = !value - return (customTogglerEl.scope().isVisible = !value) - }) - } - }) - } - }, - - post(scope, element, attrs) { - const name = attrs.layout - const state = element.layout().readState() - return scope.$broadcast(`layout:${name}:linked`, state) - }, - } - }, -})) + post(scope, element, attrs) { + const name = attrs.layout + const state = element.layout().readState() + return scope.$broadcast(`layout:${name}:linked`, state) + }, + } + }, + }), +]) diff --git a/services/web/frontend/js/ide/directives/verticalResizablePanes.js b/services/web/frontend/js/ide/directives/verticalResizablePanes.js index e3aee964f3..cc91746a20 100644 --- a/services/web/frontend/js/ide/directives/verticalResizablePanes.js +++ b/services/web/frontend/js/ide/directives/verticalResizablePanes.js @@ -1,129 +1,133 @@ import App from '../../base' -export default App.directive('verticalResizablePanes', (localStorage, ide) => ({ - restrict: 'A', - link(scope, element, attrs) { - const name = attrs.verticalResizablePanes - const minSize = scope.$eval(attrs.verticalResizablePanesMinSize) - const maxSize = scope.$eval(attrs.verticalResizablePanesMaxSize) - const defaultSize = scope.$eval(attrs.verticalResizablePanesDefaultSize) - let storedSize = null - let manualResizeIncoming = false +export default App.directive('verticalResizablePanes', [ + 'localStorage', + 'ide', + (localStorage, ide) => ({ + restrict: 'A', + link(scope, element, attrs) { + const name = attrs.verticalResizablePanes + const minSize = scope.$eval(attrs.verticalResizablePanesMinSize) + const maxSize = scope.$eval(attrs.verticalResizablePanesMaxSize) + const defaultSize = scope.$eval(attrs.verticalResizablePanesDefaultSize) + let storedSize = null + let manualResizeIncoming = false - if (name) { - const storageKey = `vertical-resizable:${name}:south-size` - storedSize = localStorage(storageKey) - $(window).unload(() => { - if (storedSize) { - localStorage(storageKey, storedSize) - } - }) - } - - const layoutOptions = { - center: { - paneSelector: '[vertical-resizable-top]', - paneClass: 'vertical-resizable-top', - size: 'auto', - }, - south: { - paneSelector: '[vertical-resizable-bottom]', - paneClass: 'vertical-resizable-bottom', - resizerClass: 'vertical-resizable-resizer', - resizerCursor: 'ns-resize', - size: 'auto', - resizable: true, - closable: false, - slidable: false, - spacing_open: 6, - spacing_closed: 6, - maxSize: '75%', - }, - } - - const toggledExternally = attrs.verticalResizablePanesToggledExternallyOn - const hiddenExternally = attrs.verticalResizablePanesHiddenExternallyOn - const hiddenInitially = attrs.verticalResizablePanesHiddenInitially - const resizeOn = attrs.verticalResizablePanesResizeOn - const resizerDisabledClass = `${layoutOptions.south.resizerClass}-disabled` - - function enableResizer() { - if (layoutHandle.resizers && layoutHandle.resizers.south) { - layoutHandle.resizers.south.removeClass(resizerDisabledClass) - } - } - - function disableResizer() { - if (layoutHandle.resizers && layoutHandle.resizers.south) { - layoutHandle.resizers.south.addClass(resizerDisabledClass) - } - } - - function handleDragEnd() { - manualResizeIncoming = true - } - - function handleResize(paneName, paneEl, paneState) { - if (manualResizeIncoming) { - storedSize = paneState.size - } - manualResizeIncoming = false - } - - if (toggledExternally) { - scope.$on(toggledExternally, (e, open) => { - if (open) { - enableResizer() - layoutHandle.sizePane('south', storedSize ?? defaultSize ?? 'auto') - } else { - disableResizer() - layoutHandle.sizePane('south', 'auto') - } - }) - } - - if (hiddenExternally) { - ide.$scope.$on(hiddenExternally, (e, open) => { - if (open) { - layoutHandle.show('south') - } else { - layoutHandle.hide('south') - } - }) - } - - if (resizeOn) { - ide.$scope.$on(resizeOn, () => { - ide.$timeout(() => { - layoutHandle.resizeAll() + if (name) { + const storageKey = `vertical-resizable:${name}:south-size` + storedSize = localStorage(storageKey) + $(window).unload(() => { + if (storedSize) { + localStorage(storageKey, storedSize) + } }) - }) - } + } - if (maxSize) { - layoutOptions.south.maxSize = maxSize - } + const layoutOptions = { + center: { + paneSelector: '[vertical-resizable-top]', + paneClass: 'vertical-resizable-top', + size: 'auto', + }, + south: { + paneSelector: '[vertical-resizable-bottom]', + paneClass: 'vertical-resizable-bottom', + resizerClass: 'vertical-resizable-resizer', + resizerCursor: 'ns-resize', + size: 'auto', + resizable: true, + closable: false, + slidable: false, + spacing_open: 6, + spacing_closed: 6, + maxSize: '75%', + }, + } - if (minSize) { - layoutOptions.south.minSize = minSize - } + const toggledExternally = attrs.verticalResizablePanesToggledExternallyOn + const hiddenExternally = attrs.verticalResizablePanesHiddenExternallyOn + const hiddenInitially = attrs.verticalResizablePanesHiddenInitially + const resizeOn = attrs.verticalResizablePanesResizeOn + const resizerDisabledClass = `${layoutOptions.south.resizerClass}-disabled` - if (defaultSize) { - layoutOptions.south.size = defaultSize - } + function enableResizer() { + if (layoutHandle.resizers && layoutHandle.resizers.south) { + layoutHandle.resizers.south.removeClass(resizerDisabledClass) + } + } - // The `drag` event fires only when the user manually resizes the panes; the `resize` event fires even when - // the layout library internally resizes itself. In order to get explicit user-initiated resizes, we need to - // listen to `drag` events. However, when the `drag` event fires, the panes aren't yet finished sizing so we - // get the pane size *before* the resize happens. We do get the correct size in the next `resize` event. - // The solution to work around this is to set up a flag in `drag` events which tells the next `resize` event - // that it was user-initiated (therefore, storing the value). - layoutOptions.south.ondrag_end = handleDragEnd - layoutOptions.south.onresize = handleResize + function disableResizer() { + if (layoutHandle.resizers && layoutHandle.resizers.south) { + layoutHandle.resizers.south.addClass(resizerDisabledClass) + } + } - const layoutHandle = element.layout(layoutOptions) - if (hiddenInitially === 'true') { - layoutHandle.hide('south') - } - }, -})) + function handleDragEnd() { + manualResizeIncoming = true + } + + function handleResize(paneName, paneEl, paneState) { + if (manualResizeIncoming) { + storedSize = paneState.size + } + manualResizeIncoming = false + } + + if (toggledExternally) { + scope.$on(toggledExternally, (e, open) => { + if (open) { + enableResizer() + layoutHandle.sizePane('south', storedSize ?? defaultSize ?? 'auto') + } else { + disableResizer() + layoutHandle.sizePane('south', 'auto') + } + }) + } + + if (hiddenExternally) { + ide.$scope.$on(hiddenExternally, (e, open) => { + if (open) { + layoutHandle.show('south') + } else { + layoutHandle.hide('south') + } + }) + } + + if (resizeOn) { + ide.$scope.$on(resizeOn, () => { + ide.$timeout(() => { + layoutHandle.resizeAll() + }) + }) + } + + if (maxSize) { + layoutOptions.south.maxSize = maxSize + } + + if (minSize) { + layoutOptions.south.minSize = minSize + } + + if (defaultSize) { + layoutOptions.south.size = defaultSize + } + + // The `drag` event fires only when the user manually resizes the panes; the `resize` event fires even when + // the layout library internally resizes itself. In order to get explicit user-initiated resizes, we need to + // listen to `drag` events. However, when the `drag` event fires, the panes aren't yet finished sizing so we + // get the pane size *before* the resize happens. We do get the correct size in the next `resize` event. + // The solution to work around this is to set up a flag in `drag` events which tells the next `resize` event + // that it was user-initiated (therefore, storing the value). + layoutOptions.south.ondrag_end = handleDragEnd + layoutOptions.south.onresize = handleResize + + const layoutHandle = element.layout(layoutOptions) + if (hiddenInitially === 'true') { + layoutHandle.hide('south') + } + }, + }), +]) diff --git a/services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js b/services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js index 4e5b9ee548..c1b03b4224 100644 --- a/services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js +++ b/services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js @@ -14,9 +14,10 @@ import App from '../../../base' import Document from '../Document' -export default App.controller( - 'SavingNotificationController', - function ($scope, $interval, ide) { +export default App.controller('SavingNotificationController', [ + '$scope', + 'ide', + function ($scope, ide) { let warnAboutUnsavedChanges setInterval(() => pollSavedStatus(), 1000) @@ -98,5 +99,5 @@ export default App.controller( return 'You have unsaved changes. If you leave now they will not be saved.' } }) - } -) + }, +]) diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor.js b/services/web/frontend/js/ide/editor/directives/aceEditor.js index edc7a9ab97..32503697ad 100644 --- a/services/web/frontend/js/ide/editor/directives/aceEditor.js +++ b/services/web/frontend/js/ide/editor/directives/aceEditor.js @@ -54,11 +54,21 @@ if (ace.config._moduleUrl == null) { } } -App.directive( - 'aceEditor', +App.directive('aceEditor', [ + 'ide', + '$compile', + '$rootScope', + 'eventTracking', + 'localStorage', + '$cacheFactory', + 'metadata', + 'graphics', + 'preamble', + 'files', + '$http', + '$q', function ( ide, - $timeout, $compile, $rootScope, eventTracking, @@ -69,8 +79,7 @@ App.directive( preamble, files, $http, - $q, - $window + $q ) { monkeyPatchSearch($rootScope, $compile) @@ -951,8 +960,8 @@ App.directive( \ `, } - } -) + }, +]) function monkeyPatchSearch($rootScope, $compile) { const searchHtml = `\ diff --git a/services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js b/services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js index 5bc46bca48..51c588d2c2 100644 --- a/services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js +++ b/services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js @@ -1,15 +1,18 @@ import App from '../../../base' -App.controller('FileTreeController', function ($scope) { - $scope.openNewDocModal = () => { - window.dispatchEvent( - new CustomEvent('file-tree.start-creating', { detail: { mode: 'doc' } }) - ) - } - - $scope.orderByFoldersFirst = function (entity) { - if ((entity != null ? entity.type : undefined) === 'folder') { - return '0' +App.controller('FileTreeController', [ + '$scope', + function ($scope) { + $scope.openNewDocModal = () => { + window.dispatchEvent( + new CustomEvent('file-tree.start-creating', { detail: { mode: 'doc' } }) + ) } - return '1' - } -}) + + $scope.orderByFoldersFirst = function (entity) { + if ((entity != null ? entity.type : undefined) === 'folder') { + return '0' + } + return '1' + } + }, +]) diff --git a/services/web/frontend/js/ide/files/services/files.js b/services/web/frontend/js/ide/files/services/files.js index c881f60e0a..98cf1745d2 100644 --- a/services/web/frontend/js/ide/files/services/files.js +++ b/services/web/frontend/js/ide/files/services/files.js @@ -1,22 +1,25 @@ import _ from 'lodash' import App from '../../../base' -export default App.factory('files', function (ide) { - const Files = { - getTeXFiles() { - const texFiles = [] - ide.fileTreeManager.forEachEntity(function (entity, _folder, path) { - if ( - entity.type === 'doc' && - /.*\.(tex|md|txt|tikz)/.test(entity.name) - ) { - const cloned = _.clone(entity) - cloned.path = path - texFiles.push(cloned) - } - }) - return texFiles - }, - } - return Files -}) +export default App.factory('files', [ + 'ide', + function (ide) { + const Files = { + getTeXFiles() { + const texFiles = [] + ide.fileTreeManager.forEachEntity(function (entity, _folder, path) { + if ( + entity.type === 'doc' && + /.*\.(tex|md|txt|tikz)/.test(entity.name) + ) { + const cloned = _.clone(entity) + cloned.path = path + texFiles.push(cloned) + } + }) + return texFiles + }, + } + return Files + }, +]) diff --git a/services/web/frontend/js/ide/graphics/services/graphics.js b/services/web/frontend/js/ide/graphics/services/graphics.js index c89fece87e..9d42597059 100644 --- a/services/web/frontend/js/ide/graphics/services/graphics.js +++ b/services/web/frontend/js/ide/graphics/services/graphics.js @@ -13,30 +13,33 @@ import _ from 'lodash' */ import App from '../../../base' -export default App.factory('graphics', function (ide) { - const Graphics = { - getGraphicsFiles() { - const graphicsFiles = [] - ide.fileTreeManager.forEachEntity(function (entity, folder, path) { - if ( - entity.type === 'file' && - __guardMethod__( - entity != null ? entity.name : undefined, - 'match', - o => o.match(/.*\.(png|jpg|jpeg|pdf|eps)/i) - ) - ) { - const cloned = _.clone(entity) - cloned.path = path - return graphicsFiles.push(cloned) - } - }) - return graphicsFiles - }, - } +export default App.factory('graphics', [ + 'ide', + function (ide) { + const Graphics = { + getGraphicsFiles() { + const graphicsFiles = [] + ide.fileTreeManager.forEachEntity(function (entity, folder, path) { + if ( + entity.type === 'file' && + __guardMethod__( + entity != null ? entity.name : undefined, + 'match', + o => o.match(/.*\.(png|jpg|jpeg|pdf|eps)/i) + ) + ) { + const cloned = _.clone(entity) + cloned.path = path + return graphicsFiles.push(cloned) + } + }) + return graphicsFiles + }, + } - return Graphics -}) + return Graphics + }, +]) function __guardMethod__(obj, methodName, transform) { if ( diff --git a/services/web/frontend/js/ide/history/components/historyEntriesList.js b/services/web/frontend/js/ide/history/components/historyEntriesList.js index 657c06dbc6..ab158ff4d3 100644 --- a/services/web/frontend/js/ide/history/components/historyEntriesList.js +++ b/services/web/frontend/js/ide/history/components/historyEntriesList.js @@ -10,7 +10,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import App from '../../../base' -const historyEntriesListController = function ($scope, $element, $attrs) { +const historyEntriesListController = function ($scope, $element) { const ctrl = this ctrl.$entryListViewportEl = null ctrl.isDragging = false @@ -133,6 +133,6 @@ export default App.component('historyEntriesList', { onRangeSelect: '&', onLabelDelete: '&', }, - controller: historyEntriesListController, + controller: ['$scope', '$element', historyEntriesListController], templateUrl: 'historyEntriesListTpl', }) diff --git a/services/web/frontend/js/ide/history/components/historyEntry.js b/services/web/frontend/js/ide/history/components/historyEntry.js index 93e52e592a..645868f290 100644 --- a/services/web/frontend/js/ide/history/components/historyEntry.js +++ b/services/web/frontend/js/ide/history/components/historyEntry.js @@ -13,7 +13,7 @@ import _ from 'lodash' import App from '../../../base' import ColorManager from '../../colors/ColorManager' import displayNameForUser from '../util/displayNameForUser' -const historyEntryController = function ($scope, $element, $attrs) { +const historyEntryController = function ($scope, $element) { const ctrl = this // 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 @@ -120,6 +120,6 @@ export default App.component('historyEntry', { require: { historyEntriesList: '^historyEntriesList', }, - controller: historyEntryController, + controller: ['$scope', '$element', historyEntryController], templateUrl: 'historyEntryTpl', }) diff --git a/services/web/frontend/js/ide/history/components/historyFileEntity.js b/services/web/frontend/js/ide/history/components/historyFileEntity.js index 8ce52c1d87..b1d44ac975 100644 --- a/services/web/frontend/js/ide/history/components/historyFileEntity.js +++ b/services/web/frontend/js/ide/history/components/historyFileEntity.js @@ -12,7 +12,7 @@ import App from '../../../base' import iconTypeFromName from '../../file-tree/util/iconTypeFromName' import fileOperationI18nNames from '../../file-tree/util/fileOperationI18nNames' -const historyFileEntityController = function ($scope, $element, $attrs) { +const historyFileEntityController = function ($scope) { const ctrl = this ctrl.hasOperation = false ctrl.getRenameTooltip = i18nRenamedStr => { @@ -99,6 +99,6 @@ export default App.component('historyFileEntity', { bindings: { fileEntity: '<', }, - controller: historyFileEntityController, + controller: ['$scope', historyFileEntityController], templateUrl: 'historyFileEntityTpl', }) diff --git a/services/web/frontend/js/ide/history/components/historyFileTree.js b/services/web/frontend/js/ide/history/components/historyFileTree.js index bcc1b38203..39ff9dfd9e 100644 --- a/services/web/frontend/js/ide/history/components/historyFileTree.js +++ b/services/web/frontend/js/ide/history/components/historyFileTree.js @@ -8,7 +8,7 @@ import _ from 'lodash' * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import App from '../../../base' -const historyFileTreeController = function ($scope, $element, $attrs) { +const historyFileTreeController = function ($scope) { const ctrl = this ctrl.handleEntityClick = file => ctrl.onSelectedFileChange({ file }) ctrl._fileTree = [] @@ -58,6 +58,6 @@ export default App.component('historyFileTree', { onSelectedFileChange: '&', isLoading: '<', }, - controller: historyFileTreeController, + controller: ['$scope', historyFileTreeController], templateUrl: 'historyFileTreeTpl', }) diff --git a/services/web/frontend/js/ide/history/components/historyLabel.js b/services/web/frontend/js/ide/history/components/historyLabel.js index e2d148a298..3dd2333861 100644 --- a/services/web/frontend/js/ide/history/components/historyLabel.js +++ b/services/web/frontend/js/ide/history/components/historyLabel.js @@ -10,7 +10,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import App from '../../../base' -const historyLabelController = function ($scope, $element, $attrs, $filter) { +const historyLabelController = function () { const ctrl = this ctrl.$onInit = () => { if (ctrl.showTooltip == null) { diff --git a/services/web/frontend/js/ide/history/components/historyLabelsList.js b/services/web/frontend/js/ide/history/components/historyLabelsList.js index 5c9e72e5e0..b94ed0af06 100644 --- a/services/web/frontend/js/ide/history/components/historyLabelsList.js +++ b/services/web/frontend/js/ide/history/components/historyLabelsList.js @@ -13,7 +13,7 @@ import _ from 'lodash' import App from '../../../base' import ColorManager from '../../colors/ColorManager' import displayNameForUser from '../util/displayNameForUser' -const historyLabelsListController = function ($scope, $element, $attrs) { +const historyLabelsListController = function ($scope) { const ctrl = this ctrl.isDragging = false ctrl.versionsWithLabels = [] @@ -170,6 +170,6 @@ export default App.component('historyLabelsList', { onRangeSelect: '&', onLabelDelete: '&', }, - controller: historyLabelsListController, + controller: ['$scope', historyLabelsListController], templateUrl: 'historyLabelsListTpl', }) diff --git a/services/web/frontend/js/ide/history/controllers/HistoryV2AddLabelModalController.js b/services/web/frontend/js/ide/history/controllers/HistoryV2AddLabelModalController.js index 3887dc2b4b..d8f0a2d593 100644 --- a/services/web/frontend/js/ide/history/controllers/HistoryV2AddLabelModalController.js +++ b/services/web/frontend/js/ide/history/controllers/HistoryV2AddLabelModalController.js @@ -11,8 +11,11 @@ */ import App from '../../../base' -export default App.controller( - 'HistoryV2AddLabelModalController', +export default App.controller('HistoryV2AddLabelModalController', [ + '$scope', + '$modalInstance', + 'ide', + 'update', function ($scope, $modalInstance, ide, update) { $scope.update = update $scope.inputs = { labelName: null } @@ -43,5 +46,5 @@ export default App.controller( } }) }) - } -) + }, +]) diff --git a/services/web/frontend/js/ide/history/controllers/HistoryV2DeleteLabelModalController.js b/services/web/frontend/js/ide/history/controllers/HistoryV2DeleteLabelModalController.js index a399de199a..4db555e4e9 100644 --- a/services/web/frontend/js/ide/history/controllers/HistoryV2DeleteLabelModalController.js +++ b/services/web/frontend/js/ide/history/controllers/HistoryV2DeleteLabelModalController.js @@ -11,8 +11,11 @@ */ import App from '../../../base' -export default App.controller( - 'HistoryV2DeleteLabelModalController', +export default App.controller('HistoryV2DeleteLabelModalController', [ + '$scope', + '$modalInstance', + 'ide', + 'labelDetails', function ($scope, $modalInstance, ide, labelDetails) { $scope.labelDetails = labelDetails $scope.state = { @@ -38,5 +41,5 @@ export default App.controller( } }) }) - } -) + }, +]) diff --git a/services/web/frontend/js/ide/history/controllers/HistoryV2FileTreeController.js b/services/web/frontend/js/ide/history/controllers/HistoryV2FileTreeController.js index dbf3b49403..fb97dec47f 100644 --- a/services/web/frontend/js/ide/history/controllers/HistoryV2FileTreeController.js +++ b/services/web/frontend/js/ide/history/controllers/HistoryV2FileTreeController.js @@ -12,11 +12,12 @@ */ import App from '../../../base' -export default App.controller( - 'HistoryV2FileTreeController', +export default App.controller('HistoryV2FileTreeController', [ + '$scope', + 'ide', function ($scope, ide) { $scope.handleFileSelection = file => { ide.historyManager.selectFile(file) } - } -) + }, +]) diff --git a/services/web/frontend/js/ide/history/controllers/HistoryV2ListController.js b/services/web/frontend/js/ide/history/controllers/HistoryV2ListController.js index cbfc7e5be8..3394cb2aa7 100644 --- a/services/web/frontend/js/ide/history/controllers/HistoryV2ListController.js +++ b/services/web/frontend/js/ide/history/controllers/HistoryV2ListController.js @@ -12,8 +12,10 @@ */ import App from '../../../base' -export default App.controller( - 'HistoryV2ListController', +export default App.controller('HistoryV2ListController', [ + '$scope', + '$modal', + 'ide', function ($scope, $modal, ide) { $scope.hoveringOverListSelectors = false $scope.listConfig = { showOnlyLabelled: false } @@ -50,5 +52,5 @@ export default App.controller( }, }, })) - } -) + }, +]) diff --git a/services/web/frontend/js/ide/history/controllers/HistoryV2ToolbarController.js b/services/web/frontend/js/ide/history/controllers/HistoryV2ToolbarController.js index 87d31c7bcc..d15c09202c 100644 --- a/services/web/frontend/js/ide/history/controllers/HistoryV2ToolbarController.js +++ b/services/web/frontend/js/ide/history/controllers/HistoryV2ToolbarController.js @@ -13,9 +13,13 @@ import App from '../../../base' import { debugConsole } from '@/utils/debugging' -export default App.controller( - 'HistoryV2ToolbarController', - ($scope, $modal, ide, eventTracking, waitFor) => { +export default App.controller('HistoryV2ToolbarController', [ + '$scope', + '$modal', + 'ide', + 'eventTracking', + 'waitFor', + function ($scope, $modal, ide, eventTracking, waitFor) { $scope.currentUpdate = null $scope.currentLabel = null @@ -120,5 +124,5 @@ export default App.controller( }) .catch(debugConsole.error) } - } -) + }, +]) diff --git a/services/web/frontend/js/ide/metadata/services/metadata.js b/services/web/frontend/js/ide/metadata/services/metadata.js index 1230333916..4cf05fa007 100644 --- a/services/web/frontend/js/ide/metadata/services/metadata.js +++ b/services/web/frontend/js/ide/metadata/services/metadata.js @@ -13,111 +13,117 @@ import _ from 'lodash' */ import App from '../../../base' -export default App.factory('metadata', function ($http, ide) { - const debouncer = {} // DocId => Timeout +export default App.factory('metadata', [ + '$http', + 'ide', + function ($http, ide) { + const debouncer = {} // DocId => Timeout - const state = { documents: {} } + const state = { documents: {} } - const metadata = { state } + const metadata = { state } - metadata.onBroadcastDocMeta = function (data) { - if (data.docId != null && data.meta != null) { - state.documents[data.docId] = data.meta - window.dispatchEvent( - new CustomEvent('project:metadata', { detail: state.documents }) - ) - } - } - - metadata.onEntityDeleted = function (e, entity) { - if (entity.type === 'doc') { - return delete state.documents[entity.id] - } - } - - metadata.getAllLabels = () => - _.flattenDeep( - (() => { - const result = [] - for (const docId in state.documents) { - const meta = state.documents[docId] - result.push(meta.labels) - } - return result - })() - ) - - metadata.getAllPackages = function () { - const packageCommandMapping = {} - for (const _docId in state.documents) { - const meta = state.documents[_docId] - for (const packageName in meta.packages) { - const commandSnippets = meta.packages[packageName] - packageCommandMapping[packageName] = commandSnippets + metadata.onBroadcastDocMeta = function (data) { + if (data.docId != null && data.meta != null) { + state.documents[data.docId] = data.meta + window.dispatchEvent( + new CustomEvent('project:metadata', { detail: state.documents }) + ) } } - return packageCommandMapping - } - metadata.loadProjectMetaFromServer = () => - $http - .get(`/project/${window.project_id}/metadata`) - .then(function (response) { - const { data } = response - if (data.projectMeta) { - return (() => { - const result = [] - for (const docId in data.projectMeta) { - const docMeta = data.projectMeta[docId] - result.push((state.documents[docId] = docMeta)) - } - window.dispatchEvent( - new CustomEvent('project:metadata', { detail: state.documents }) - ) - return result - })() + metadata.onEntityDeleted = function (e, entity) { + if (entity.type === 'doc') { + return delete state.documents[entity.id] + } + } + + metadata.getAllLabels = () => + _.flattenDeep( + (() => { + const result = [] + for (const docId in state.documents) { + const meta = state.documents[docId] + result.push(meta.labels) + } + return result + })() + ) + + metadata.getAllPackages = function () { + const packageCommandMapping = {} + for (const _docId in state.documents) { + const meta = state.documents[_docId] + for (const packageName in meta.packages) { + const commandSnippets = meta.packages[packageName] + packageCommandMapping[packageName] = commandSnippets } - }) - - metadata.loadDocMetaFromServer = docId => - $http - .post(`/project/${window.project_id}/doc/${docId}/metadata`, { - // Don't broadcast metadata when there are no other users in the - // project. - broadcast: ide.$scope.onlineUsersCount > 0, - _csrf: window.csrfToken, - }) - .then(function (response) { - const { data } = response - // handle the POST response like a broadcast event when there are no - // other users in the project. - metadata.onBroadcastDocMeta(data) - }) - - metadata.scheduleLoadDocMetaFromServer = function (docId) { - if (ide.$scope.permissionsLevel === 'readOnly') { - // The POST request is blocked for users without write permission. - // The user will not be able to consume the meta data for edits anyways. - return - } - // De-bounce loading labels with a timeout - const existingTimeout = debouncer[docId] - - if (existingTimeout != null) { - clearTimeout(existingTimeout) - delete debouncer[docId] + } + return packageCommandMapping } - return (debouncer[docId] = setTimeout(() => { - // TODO: wait for the document to be saved? - metadata.loadDocMetaFromServer(docId) - return delete debouncer[docId] - }, 2000)) - } + metadata.loadProjectMetaFromServer = () => + $http + .get(`/project/${window.project_id}/metadata`) + .then(function (response) { + const { data } = response + if (data.projectMeta) { + return (() => { + const result = [] + for (const docId in data.projectMeta) { + const docMeta = data.projectMeta[docId] + result.push((state.documents[docId] = docMeta)) + } + window.dispatchEvent( + new CustomEvent('project:metadata', { detail: state.documents }) + ) + return result + })() + } + }) - window.addEventListener('editor:metadata-outdated', () => { - metadata.scheduleLoadDocMetaFromServer(ide.$scope.editor.sharejs_doc.doc_id) - }) + metadata.loadDocMetaFromServer = docId => + $http + .post(`/project/${window.project_id}/doc/${docId}/metadata`, { + // Don't broadcast metadata when there are no other users in the + // project. + broadcast: ide.$scope.onlineUsersCount > 0, + _csrf: window.csrfToken, + }) + .then(function (response) { + const { data } = response + // handle the POST response like a broadcast event when there are no + // other users in the project. + metadata.onBroadcastDocMeta(data) + }) - return metadata -}) + metadata.scheduleLoadDocMetaFromServer = function (docId) { + if (ide.$scope.permissionsLevel === 'readOnly') { + // The POST request is blocked for users without write permission. + // The user will not be able to consume the meta data for edits anyways. + return + } + // De-bounce loading labels with a timeout + const existingTimeout = debouncer[docId] + + if (existingTimeout != null) { + clearTimeout(existingTimeout) + delete debouncer[docId] + } + + return (debouncer[docId] = setTimeout(() => { + // TODO: wait for the document to be saved? + metadata.loadDocMetaFromServer(docId) + return delete debouncer[docId] + }, 2000)) + } + + window.addEventListener('editor:metadata-outdated', () => { + metadata.scheduleLoadDocMetaFromServer( + ide.$scope.editor.sharejs_doc.doc_id + ) + }) + + return metadata + }, +]) diff --git a/services/web/frontend/js/ide/review-panel/controllers/BulkActionsModalController.js b/services/web/frontend/js/ide/review-panel/controllers/BulkActionsModalController.js index ab96ab81dd..fc972c85d7 100644 --- a/services/web/frontend/js/ide/review-panel/controllers/BulkActionsModalController.js +++ b/services/web/frontend/js/ide/review-panel/controllers/BulkActionsModalController.js @@ -11,12 +11,15 @@ */ import App from '../../../base' -export default App.controller( - 'BulkActionsModalController', +export default App.controller('BulkActionsModalController', [ + '$scope', + '$modalInstance', + 'isAccept', + 'nChanges', function ($scope, $modalInstance, isAccept, nChanges) { $scope.isAccept = isAccept $scope.nChanges = nChanges $scope.cancel = () => $modalInstance.dismiss() return ($scope.confirm = () => $modalInstance.close(isAccept)) - } -) + }, +]) diff --git a/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js b/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js index bcc794344a..c4bd22dd7d 100644 --- a/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js +++ b/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js @@ -20,8 +20,15 @@ import EventEmitter from '../../../utils/EventEmitter' import ColorManager from '../../colors/ColorManager' import getMeta from '../../../utils/meta' -export default App.controller( - 'ReviewPanelController', +export default App.controller('ReviewPanelController', [ + '$scope', + '$element', + 'ide', + '$timeout', + '$http', + '$modal', + 'eventTracking', + 'localStorage', function ( $scope, $element, @@ -1412,8 +1419,8 @@ export default App.controller( // Add methods somewhere that React can see them $scope.reviewPanel.saveEdit = $scope.saveEdit - } -) + }, +]) // send events to the CodeMirror 6 track changes extension const dispatchReviewPanelEvent = (type, payload) => { diff --git a/services/web/frontend/js/ide/review-panel/controllers/TrackChangesUpgradeModalController.js b/services/web/frontend/js/ide/review-panel/controllers/TrackChangesUpgradeModalController.js index 9d1e619841..c814d843d4 100644 --- a/services/web/frontend/js/ide/review-panel/controllers/TrackChangesUpgradeModalController.js +++ b/services/web/frontend/js/ide/review-panel/controllers/TrackChangesUpgradeModalController.js @@ -11,7 +11,10 @@ */ import App from '../../../base' -export default App.controller( - 'TrackChangesUpgradeModalController', - ($scope, $modalInstance) => ($scope.cancel = () => $modalInstance.dismiss()) -) +export default App.controller('TrackChangesUpgradeModalController', [ + '$scope', + '$modalInstance', + function ($scope, $modalInstance) { + $scope.cancel = () => $modalInstance.dismiss() + }, +]) diff --git a/services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js b/services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js index 4fd844ff3b..f2595b7d1b 100644 --- a/services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js +++ b/services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js @@ -11,59 +11,62 @@ */ import App from '../../../base' -export default App.directive('aggregateChangeEntry', $timeout => ({ - restrict: 'E', - templateUrl: 'aggregateChangeEntryTemplate', - scope: { - entry: '=', - user: '=', - permissions: '=', - onAccept: '&', - onReject: '&', - onIndicatorClick: '&', - onMouseEnter: '&', - onMouseLeave: '&', - onBodyClick: '&', - }, - link(scope, element, attrs) { - scope.contentLimit = 17 - scope.isDeletionCollapsed = true - scope.isInsertionCollapsed = true - scope.deletionNeedsCollapsing = false - scope.insertionNeedsCollapsing = false +export default App.directive('aggregateChangeEntry', [ + '$timeout', + $timeout => ({ + restrict: 'E', + templateUrl: 'aggregateChangeEntryTemplate', + scope: { + entry: '=', + user: '=', + permissions: '=', + onAccept: '&', + onReject: '&', + onIndicatorClick: '&', + onMouseEnter: '&', + onMouseLeave: '&', + onBodyClick: '&', + }, + link(scope, element, attrs) { + scope.contentLimit = 17 + scope.isDeletionCollapsed = true + scope.isInsertionCollapsed = true + scope.deletionNeedsCollapsing = false + scope.insertionNeedsCollapsing = false - element.on('click', function (e) { - if ( - $(e.target).is( - '.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i' - ) - ) { - return scope.onBodyClick() + element.on('click', function (e) { + if ( + $(e.target).is( + '.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i' + ) + ) { + return scope.onBodyClick() + } + }) + + scope.toggleDeletionCollapse = function () { + scope.isDeletionCollapsed = !scope.isDeletionCollapsed + return $timeout(() => scope.$emit('review-panel:layout')) } - }) - scope.toggleDeletionCollapse = function () { - scope.isDeletionCollapsed = !scope.isDeletionCollapsed - return $timeout(() => scope.$emit('review-panel:layout')) - } + scope.toggleInsertionCollapse = function () { + scope.isInsertionCollapsed = !scope.isInsertionCollapsed + return $timeout(() => scope.$emit('review-panel:layout')) + } - scope.toggleInsertionCollapse = function () { - scope.isInsertionCollapsed = !scope.isInsertionCollapsed - return $timeout(() => scope.$emit('review-panel:layout')) - } + scope.$watch( + 'entry.metadata.replaced_content.length', + deletionContentLength => + (scope.deletionNeedsCollapsing = + deletionContentLength > scope.contentLimit) + ) - scope.$watch( - 'entry.metadata.replaced_content.length', - deletionContentLength => - (scope.deletionNeedsCollapsing = - deletionContentLength > scope.contentLimit) - ) - - return scope.$watch( - 'entry.content.length', - insertionContentLength => - (scope.insertionNeedsCollapsing = - insertionContentLength > scope.contentLimit) - ) - }, -})) + return scope.$watch( + 'entry.content.length', + insertionContentLength => + (scope.insertionNeedsCollapsing = + insertionContentLength > scope.contentLimit) + ) + }, + }), +]) diff --git a/services/web/frontend/js/ide/review-panel/directives/changeEntry.js b/services/web/frontend/js/ide/review-panel/directives/changeEntry.js index 42054a1021..ee52ede57a 100644 --- a/services/web/frontend/js/ide/review-panel/directives/changeEntry.js +++ b/services/web/frontend/js/ide/review-panel/directives/changeEntry.js @@ -11,44 +11,47 @@ */ import App from '../../../base' -export default App.directive('changeEntry', $timeout => ({ - restrict: 'E', - templateUrl: 'changeEntryTemplate', - scope: { - entry: '=', - user: '=', - permissions: '=', - onAccept: '&', - onReject: '&', - onIndicatorClick: '&', - onMouseEnter: '&', - onMouseLeave: '&', - onBodyClick: '&', - }, - link(scope, element, attrs) { - scope.contentLimit = 40 - scope.isCollapsed = true - scope.needsCollapsing = false +export default App.directive('changeEntry', [ + '$timeout', + $timeout => ({ + restrict: 'E', + templateUrl: 'changeEntryTemplate', + scope: { + entry: '=', + user: '=', + permissions: '=', + onAccept: '&', + onReject: '&', + onIndicatorClick: '&', + onMouseEnter: '&', + onMouseLeave: '&', + onBodyClick: '&', + }, + link(scope, element, attrs) { + scope.contentLimit = 40 + scope.isCollapsed = true + scope.needsCollapsing = false - element.on('click', function (e) { - if ( - $(e.target).is( - '.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i' - ) - ) { - return scope.onBodyClick() + element.on('click', function (e) { + if ( + $(e.target).is( + '.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i' + ) + ) { + return scope.onBodyClick() + } + }) + + scope.toggleCollapse = function () { + scope.isCollapsed = !scope.isCollapsed + return $timeout(() => scope.$emit('review-panel:layout')) } - }) - scope.toggleCollapse = function () { - scope.isCollapsed = !scope.isCollapsed - return $timeout(() => scope.$emit('review-panel:layout')) - } - - return scope.$watch( - 'entry.content.length', - contentLength => - (scope.needsCollapsing = contentLength > scope.contentLimit) - ) - }, -})) + return scope.$watch( + 'entry.content.length', + contentLength => + (scope.needsCollapsing = contentLength > scope.contentLimit) + ) + }, + }), +]) diff --git a/services/web/frontend/js/ide/review-panel/directives/commentEntry.js b/services/web/frontend/js/ide/review-panel/directives/commentEntry.js index e05f9e8050..6d091da9d9 100644 --- a/services/web/frontend/js/ide/review-panel/directives/commentEntry.js +++ b/services/web/frontend/js/ide/review-panel/directives/commentEntry.js @@ -11,82 +11,85 @@ */ import App from '../../../base' -export default App.directive('commentEntry', $timeout => ({ - restrict: 'E', - templateUrl: 'commentEntryTemplate', - scope: { - entry: '=', - threads: '=', - permissions: '=', - onResolve: '&', - onReply: '&', - onIndicatorClick: '&', - onMouseEnter: '&', - onMouseLeave: '&', - onSaveEdit: '&', - onDelete: '&', - onBodyClick: '&', - }, - link(scope, element, attrs) { - scope.state = { animating: false } +export default App.directive('commentEntry', [ + '$timeout', + $timeout => ({ + restrict: 'E', + templateUrl: 'commentEntryTemplate', + scope: { + entry: '=', + threads: '=', + permissions: '=', + onResolve: '&', + onReply: '&', + onIndicatorClick: '&', + onMouseEnter: '&', + onMouseLeave: '&', + onSaveEdit: '&', + onDelete: '&', + onBodyClick: '&', + }, + link(scope, element, attrs) { + scope.state = { animating: false } - element.on('click', function (e) { - if ( - $(e.target).is( - '.rp-entry, .rp-comment-loaded, .rp-comment-content, .rp-comment-reply, .rp-entry-metadata' - ) - ) { - return scope.onBodyClick() - } - }) + element.on('click', function (e) { + if ( + $(e.target).is( + '.rp-entry, .rp-comment-loaded, .rp-comment-content, .rp-comment-reply, .rp-entry-metadata' + ) + ) { + return scope.onBodyClick() + } + }) - scope.handleCommentReplyKeyPress = function (ev) { - if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { - ev.preventDefault() - if (scope.entry.replyContent.length > 0) { - ev.target.blur() - return scope.onReply() + scope.handleCommentReplyKeyPress = function (ev) { + if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { + ev.preventDefault() + if (scope.entry.replyContent.length > 0) { + ev.target.blur() + return scope.onReply() + } } } - } - scope.animateAndCallOnResolve = function () { - scope.state.animating = true - element.find('.rp-entry').css('top', 0) - $timeout(() => scope.onResolve(), 350) - return true - } - - scope.startEditing = function (comment) { - comment.editing = true - return setTimeout(() => scope.$emit('review-panel:layout')) - } - - scope.saveEdit = function (comment) { - comment.editing = false - return scope.onSaveEdit({ comment }) - } - - scope.confirmDelete = function (comment) { - comment.deleting = true - return setTimeout(() => scope.$emit('review-panel:layout')) - } - - scope.cancelDelete = function (comment) { - comment.deleting = false - return setTimeout(() => scope.$emit('review-panel:layout')) - } - - scope.doDelete = function (comment) { - comment.deleting = false - return scope.onDelete({ comment }) - } - - return (scope.saveEditOnEnter = function (ev, comment) { - if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { - ev.preventDefault() - return scope.saveEdit(comment) + scope.animateAndCallOnResolve = function () { + scope.state.animating = true + element.find('.rp-entry').css('top', 0) + $timeout(() => scope.onResolve(), 350) + return true } - }) - }, -})) + + scope.startEditing = function (comment) { + comment.editing = true + return setTimeout(() => scope.$emit('review-panel:layout')) + } + + scope.saveEdit = function (comment) { + comment.editing = false + return scope.onSaveEdit({ comment }) + } + + scope.confirmDelete = function (comment) { + comment.deleting = true + return setTimeout(() => scope.$emit('review-panel:layout')) + } + + scope.cancelDelete = function (comment) { + comment.deleting = false + return setTimeout(() => scope.$emit('review-panel:layout')) + } + + scope.doDelete = function (comment) { + comment.deleting = false + return scope.onDelete({ comment }) + } + + return (scope.saveEditOnEnter = function (ev, comment) { + if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { + ev.preventDefault() + return scope.saveEdit(comment) + } + }) + }, + }), +]) diff --git a/services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js b/services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js index 1b39a3408f..835c9f5d58 100644 --- a/services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js +++ b/services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js @@ -10,25 +10,28 @@ */ import App from '../../../base' -export default App.directive('reviewPanelCollapseHeight', $parse => ({ - restrict: 'A', - link(scope, element, attrs) { - return scope.$watch( - () => $parse(attrs.reviewPanelCollapseHeight)(scope), - function (shouldCollapse) { - const neededHeight = element.prop('scrollHeight') - if (neededHeight > 0) { - if (shouldCollapse) { - return element.animate({ height: 0 }, 150) +export default App.directive('reviewPanelCollapseHeight', [ + '$parse', + $parse => ({ + restrict: 'A', + link(scope, element, attrs) { + return scope.$watch( + () => $parse(attrs.reviewPanelCollapseHeight)(scope), + function (shouldCollapse) { + const neededHeight = element.prop('scrollHeight') + if (neededHeight > 0) { + if (shouldCollapse) { + return element.animate({ height: 0 }, 150) + } else { + return element.animate({ height: neededHeight }, 150) + } } else { - return element.animate({ height: neededHeight }, 150) - } - } else { - if (shouldCollapse) { - return element.height(0) + if (shouldCollapse) { + return element.height(0) + } } } - } - ) - }, -})) + ) + }, + }), +]) diff --git a/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js b/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js index 9d5a4cce99..6c69f26562 100644 --- a/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js +++ b/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js @@ -16,7 +16,7 @@ import App from '../../../base' import { debugConsole } from '@/utils/debugging' -export default App.directive('reviewPanelSorted', $timeout => ({ +export default App.directive('reviewPanelSorted', () => ({ link(scope, element, attrs) { let previous_focused_entry_index = 0 diff --git a/services/web/frontend/js/ide/services/ide.js b/services/web/frontend/js/ide/services/ide.js index c79ccd9d72..d0d3ee1f01 100644 --- a/services/web/frontend/js/ide/services/ide.js +++ b/services/web/frontend/js/ide/services/ide.js @@ -17,8 +17,14 @@ import EditorWatchdogManager from '../connection/EditorWatchdogManager' import { debugConsole } from '@/utils/debugging' // We create and provide this as service so that we can access the global ide // from within other parts of the angular app. -App.factory( - 'ide', +App.factory('ide', [ + '$http', + 'queuedHttp', + '$modal', + '$q', + '$filter', + '$timeout', + 'eventTracking', function ($http, queuedHttp, $modal, $q, $filter, $timeout, eventTracking) { const ide = {} ide.$http = $http @@ -132,21 +138,28 @@ App.factory( }) return ide - } -) + }, +]) -App.controller( - 'GenericMessageModalController', +App.controller('GenericMessageModalController', [ + '$scope', + '$modalInstance', + 'title', + 'message', function ($scope, $modalInstance, title, message) { $scope.title = title $scope.message = message return ($scope.done = () => $modalInstance.close()) - } -) + }, +]) -App.controller( - 'OutOfSyncModalController', +App.controller('OutOfSyncModalController', [ + '$scope', + '$window', + 'title', + 'message', + 'editorContent', function ($scope, $window, title, message, editorContent) { $scope.title = title $scope.message = message @@ -158,8 +171,8 @@ App.controller( // https://github.com/overleaf/issues/issues/3694 $window.location.reload() } - } -) + }, +]) function __guard__(value, transform) { return typeof value !== 'undefined' && value !== null diff --git a/services/web/frontend/js/ide/toolbar/EditorLoaderController.js b/services/web/frontend/js/ide/toolbar/EditorLoaderController.js index 0bad5f847a..59c233b6b0 100644 --- a/services/web/frontend/js/ide/toolbar/EditorLoaderController.js +++ b/services/web/frontend/js/ide/toolbar/EditorLoaderController.js @@ -1,17 +1,21 @@ import App from '../../base' -App.controller('EditorLoaderController', function ($scope, localStorage) { - $scope.$watch('editor.showVisual', function (val) { - localStorage( - `editor.mode.${$scope.project_id}`, - val === true ? 'rich-text' : 'source' - ) - }) +App.controller('EditorLoaderController', [ + '$scope', + 'localStorage', + function ($scope, localStorage) { + $scope.$watch('editor.showVisual', function (val) { + localStorage( + `editor.mode.${$scope.project_id}`, + val === true ? 'rich-text' : 'source' + ) + }) - $scope.$watch('editor.newSourceEditor', function (val) { - localStorage( - `editor.source_editor.${$scope.project_id}`, - val === true ? 'cm6' : 'ace' - ) - }) -}) + $scope.$watch('editor.newSourceEditor', function (val) { + localStorage( + `editor.source_editor.${$scope.project_id}`, + val === true ? 'cm6' : 'ace' + ) + }) + }, +]) diff --git a/services/web/frontend/js/ide/toolbar/EditorToolbarController.js b/services/web/frontend/js/ide/toolbar/EditorToolbarController.js index 892d6ffd5f..f68fb70526 100644 --- a/services/web/frontend/js/ide/toolbar/EditorToolbarController.js +++ b/services/web/frontend/js/ide/toolbar/EditorToolbarController.js @@ -4,16 +4,20 @@ import importOverleafModules from '../../../macros/import-overleaf-module.macro' const eModules = importOverleafModules('editorToolbarButtons') const editorToolbarButtons = eModules.map(item => item.import.default) -export default App.controller('EditorToolbarController', ($scope, ide) => { - const editorButtons = [] +export default App.controller('EditorToolbarController', [ + '$scope', + 'ide', + function ($scope, ide) { + const editorButtons = [] - for (const editorToolbarButton of editorToolbarButtons) { - const button = editorToolbarButton.button($scope, ide) + for (const editorToolbarButton of editorToolbarButtons) { + const button = editorToolbarButton.button($scope, ide) - if (editorToolbarButton.source) { - editorButtons.push(button) + if (editorToolbarButton.source) { + editorButtons.push(button) + } } - } - $scope.editorButtons = editorButtons -}) + $scope.editorButtons = editorButtons + }, +]) diff --git a/services/web/frontend/js/main.js b/services/web/frontend/js/main.js index a00080543f..f133265ada 100644 --- a/services/web/frontend/js/main.js +++ b/services/web/frontend/js/main.js @@ -41,15 +41,18 @@ import './features/cookie-banner' import '../../modules/modules-main' import './cdn-load-test' import { debugConsole } from '@/utils/debugging' -angular.module('SharelatexApp').config(function ($locationProvider) { - try { - return $locationProvider.html5Mode({ - enabled: true, - requireBase: false, - rewriteLinks: false, - }) - } catch (e) { - debugConsole.error("Error while trying to fix '#' links: ", e) - } -}) +angular.module('SharelatexApp').config([ + '$locationProvider', + function ($locationProvider) { + try { + return $locationProvider.html5Mode({ + enabled: true, + requireBase: false, + rewriteLinks: false, + }) + } catch (e) { + debugConsole.error("Error while trying to fix '#' links: ", e) + } + }, +]) export default angular.bootstrap(document.body, ['SharelatexApp']) diff --git a/services/web/frontend/js/main/account-upgrade-angular.js b/services/web/frontend/js/main/account-upgrade-angular.js index c8e4f85b2d..90fed142f7 100644 --- a/services/web/frontend/js/main/account-upgrade-angular.js +++ b/services/web/frontend/js/main/account-upgrade-angular.js @@ -1,14 +1,20 @@ import App from '../base' import { startFreeTrial, upgradePlan, paywallPrompt } from './account-upgrade' -App.controller('FreeTrialModalController', function ($scope) { - $scope.buttonClass = 'btn-primary' - $scope.startFreeTrial = (source, version) => - startFreeTrial(source, version, $scope) - $scope.paywallPrompt = source => paywallPrompt(source) -}) +App.controller('FreeTrialModalController', [ + '$scope', + function ($scope) { + $scope.buttonClass = 'btn-primary' + $scope.startFreeTrial = (source, version) => + startFreeTrial(source, version, $scope) + $scope.paywallPrompt = source => paywallPrompt(source) + }, +]) -App.controller('UpgradeModalController', function ($scope) { - $scope.buttonClass = 'btn-primary' - $scope.upgradePlan = source => upgradePlan(source, $scope) -}) +App.controller('UpgradeModalController', [ + '$scope', + function ($scope) { + $scope.buttonClass = 'btn-primary' + $scope.upgradePlan = source => upgradePlan(source, $scope) + }, +]) diff --git a/services/web/frontend/js/main/annual-upgrade.js b/services/web/frontend/js/main/annual-upgrade.js index 7c5a474a1f..bac294e779 100644 --- a/services/web/frontend/js/main/annual-upgrade.js +++ b/services/web/frontend/js/main/annual-upgrade.js @@ -12,9 +12,10 @@ import App from '../base' import { debugConsole } from '@/utils/debugging' -export default App.controller( - 'AnnualUpgradeController', - function ($scope, $http, $modal) { +export default App.controller('AnnualUpgradeController', [ + '$scope', + '$http', + function ($scope, $http) { const MESSAGES_URL = '/user/subscription/upgrade-annual' $scope.upgradeComplete = false @@ -43,5 +44,5 @@ export default App.controller( debugConsole.error('something went wrong changing plan', err) ) }) - } -) + }, +]) diff --git a/services/web/frontend/js/main/bonus.js b/services/web/frontend/js/main/bonus.js index 4a44a7c5a8..6f1f4d9dc3 100644 --- a/services/web/frontend/js/main/bonus.js +++ b/services/web/frontend/js/main/bonus.js @@ -9,17 +9,22 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import App from '../base' -App.controller( - 'BonusLinksController', - ($scope, $modal) => - ($scope.openLinkToUsModal = () => +App.controller('BonusLinksController', [ + '$scope', + '$modal', + function ($scope, $modal) { + $scope.openLinkToUsModal = () => $modal.open({ templateUrl: 'BonusLinkToUsModal', controller: 'BonusModalController', - })) -) + }) + }, +]) -export default App.controller( - 'BonusModalController', - ($scope, $modalInstance) => ($scope.cancel = () => $modalInstance.dismiss()) -) +export default App.controller('BonusModalController', [ + '$scope', + '$modalInstance', + function ($scope, $modalInstance) { + $scope.cancel = () => $modalInstance.dismiss() + }, +]) diff --git a/services/web/frontend/js/main/clear-sessions.js b/services/web/frontend/js/main/clear-sessions.js index ad73fba0ee..6ecf09d8d8 100644 --- a/services/web/frontend/js/main/clear-sessions.js +++ b/services/web/frontend/js/main/clear-sessions.js @@ -12,8 +12,9 @@ import App from '../base' import getMeta from '../utils/meta' -export default App.controller( - 'ClearSessionsController', +export default App.controller('ClearSessionsController', [ + '$scope', + '$http', function ($scope, $http) { $scope.state = { otherSessions: getMeta('ol-otherSessions'), @@ -34,5 +35,5 @@ export default App.controller( }) .catch(() => ($scope.state.error = true)) }) - } -) + }, +]) diff --git a/services/web/frontend/js/main/event.js b/services/web/frontend/js/main/event.js index 90d883252a..d85999b175 100644 --- a/services/web/frontend/js/main/event.js +++ b/services/web/frontend/js/main/event.js @@ -24,98 +24,102 @@ const CACHE_KEY = 'mbEvents' let heartbeatsSent = 0 let nextHeartbeat = new Date() -App.factory('eventTracking', function ($http, localStorage) { - const _getEventCache = function () { - let eventCache = localStorage(CACHE_KEY) +App.factory('eventTracking', [ + '$http', + 'localStorage', + function ($http, localStorage) { + const _getEventCache = function () { + let eventCache = localStorage(CACHE_KEY) - // Initialize as an empy object if the event cache is still empty. - if (eventCache == null) { - eventCache = {} - localStorage(CACHE_KEY, eventCache) + // Initialize as an empy object if the event cache is still empty. + if (eventCache == null) { + eventCache = {} + localStorage(CACHE_KEY, eventCache) + } + + return eventCache } - return eventCache - } + const _eventInCache = function (key) { + const curCache = _getEventCache() + return curCache[key] || false + } - const _eventInCache = function (key) { - const curCache = _getEventCache() - return curCache[key] || false - } + const _addEventToCache = function (key) { + const curCache = _getEventCache() + curCache[key] = true - const _addEventToCache = function (key) { - const curCache = _getEventCache() - curCache[key] = true + return localStorage(CACHE_KEY, curCache) + } - return localStorage(CACHE_KEY, curCache) - } + const _sendEditingSessionHeartbeat = segmentation => + $http({ + url: `/editingSession/${window.project_id}`, + method: 'PUT', + data: { segmentation }, + headers: { + 'X-CSRF-Token': window.csrfToken, + }, + }) - const _sendEditingSessionHeartbeat = segmentation => - $http({ - url: `/editingSession/${window.project_id}`, - method: 'PUT', - data: { segmentation }, - headers: { - 'X-CSRF-Token': window.csrfToken, + return { + send(category, action, label, value) { + return ga('send', 'event', category, action, label, value) }, - }) - return { - send(category, action, label, value) { - return ga('send', 'event', category, action, label, value) - }, + sendGAOnce(category, action, label, value) { + if (!_eventInCache(action)) { + _addEventToCache(action) + return this.send(category, action, label, value) + } + }, - sendGAOnce(category, action, label, value) { - if (!_eventInCache(action)) { - _addEventToCache(action) - return this.send(category, action, label, value) - } - }, + editingSessionHeartbeat(segmentationCb = () => {}) { + debugConsole.log('[Event] heartbeat trigger') - editingSessionHeartbeat(segmentationCb = () => {}) { - debugConsole.log('[Event] heartbeat trigger') + // If the next heartbeat is in the future, stop + if (nextHeartbeat > new Date()) return - // If the next heartbeat is in the future, stop - if (nextHeartbeat > new Date()) return + const segmentation = segmentationCb() - const segmentation = segmentationCb() + debugConsole.log('[Event] send heartbeat request', segmentation) + _sendEditingSessionHeartbeat(segmentation) - debugConsole.log('[Event] send heartbeat request', segmentation) - _sendEditingSessionHeartbeat(segmentation) + heartbeatsSent++ - heartbeatsSent++ + // send two first heartbeats at 0 and 30s then increase the backoff time + // 1min per call until we reach 5 min + const backoffSecs = + heartbeatsSent <= 2 + ? 30 + : heartbeatsSent <= 6 + ? (heartbeatsSent - 2) * 60 + : 300 - // send two first heartbeats at 0 and 30s then increase the backoff time - // 1min per call until we reach 5 min - const backoffSecs = - heartbeatsSent <= 2 - ? 30 - : heartbeatsSent <= 6 - ? (heartbeatsSent - 2) * 60 - : 300 + nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate() + }, - nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate() - }, + sendMB, - sendMB, + sendMBSampled(key, segmentation, rate = 0.01) { + if (Math.random() < rate) { + this.sendMB(key, segmentation) + } + }, - sendMBSampled(key, segmentation, rate = 0.01) { - if (Math.random() < rate) { - this.sendMB(key, segmentation) - } - }, + sendMBOnce(key, segmentation) { + if (!_eventInCache(key)) { + _addEventToCache(key) + this.sendMB(key, segmentation) + } + }, - sendMBOnce(key, segmentation) { - if (!_eventInCache(key)) { - _addEventToCache(key) - this.sendMB(key, segmentation) - } - }, - - eventInCache(key) { - return _eventInCache(key) - }, - } -}) + eventInCache(key) { + return _eventInCache(key) + }, + } + }, +]) export default $('.navbar a').on('click', function (e) { const href = $(e.target).attr('href') diff --git a/services/web/frontend/js/main/importing.js b/services/web/frontend/js/main/importing.js index 2976808d02..6469321709 100644 --- a/services/web/frontend/js/main/importing.js +++ b/services/web/frontend/js/main/importing.js @@ -1,6 +1,9 @@ import App from '../base' -App.controller( - 'ImportingController', +App.controller('ImportingController', [ + '$interval', + '$scope', + '$timeout', + '$window', function ($interval, $scope, $timeout, $window) { $interval(function () { $scope.state.load_progress += 5 @@ -14,5 +17,5 @@ App.controller( $scope.state = { load_progress: 20, } - } -) + }, +]) diff --git a/services/web/frontend/js/main/learn.js b/services/web/frontend/js/main/learn.js index b2c1c731ba..f6bce77255 100644 --- a/services/web/frontend/js/main/learn.js +++ b/services/web/frontend/js/main/learn.js @@ -2,9 +2,10 @@ import _ from 'lodash' import App from '../base' import '../directives/mathjax' import '../services/algolia-search' -App.controller( - 'SearchWikiController', - function ($scope, algoliaSearch, $modal) { +App.controller('SearchWikiController', [ + '$scope', + 'algoliaSearch', + function ($scope, algoliaSearch) { $scope.hits = [] $scope.hits_total = 0 $scope.config_hits_per_page = 20 @@ -95,7 +96,7 @@ App.controller( } ) } - } -) + }, +]) export default App.controller('LearnController', function () {}) diff --git a/services/web/frontend/js/main/scribtex-popup.js b/services/web/frontend/js/main/scribtex-popup.js index 52e4c4aa70..2f29cd22b6 100644 --- a/services/web/frontend/js/main/scribtex-popup.js +++ b/services/web/frontend/js/main/scribtex-popup.js @@ -7,8 +7,11 @@ */ import App from '../base' -export default App.controller('ScribtexPopupController', ($scope, $modal) => - $modal.open({ - templateUrl: 'scribtexModalTemplate', - }) -) +export default App.controller('ScribtexPopupController', [ + '$modal', + function ($modal) { + $modal.open({ + templateUrl: 'scribtexModalTemplate', + }) + }, +]) diff --git a/services/web/frontend/js/main/subscription/team-invite-controller.js b/services/web/frontend/js/main/subscription/team-invite-controller.js index 7a13803357..6a45904e96 100644 --- a/services/web/frontend/js/main/subscription/team-invite-controller.js +++ b/services/web/frontend/js/main/subscription/team-invite-controller.js @@ -13,55 +13,59 @@ import App from '../../base' import getMeta from '../../utils/meta' import { debugConsole } from '@/utils/debugging' -export default App.controller('TeamInviteController', function ($scope, $http) { - $scope.inflight = false +export default App.controller('TeamInviteController', [ + '$scope', + '$http', + function ($scope, $http) { + $scope.inflight = false - const hideJoinSubscription = getMeta('ol-cannot-join-subscription') - const hasIndividualRecurlySubscription = getMeta( - 'ol-hasIndividualRecurlySubscription' - ) + const hideJoinSubscription = getMeta('ol-cannot-join-subscription') + const hasIndividualRecurlySubscription = getMeta( + 'ol-hasIndividualRecurlySubscription' + ) - if (hideJoinSubscription) { - $scope.view = 'restrictedByManagedGroup' - } else if (hasIndividualRecurlySubscription) { - $scope.view = 'hasIndividualRecurlySubscription' - } else { - $scope.view = 'teamInvite' - } + if (hideJoinSubscription) { + $scope.view = 'restrictedByManagedGroup' + } else if (hasIndividualRecurlySubscription) { + $scope.view = 'hasIndividualRecurlySubscription' + } else { + $scope.view = 'teamInvite' + } - $scope.keepPersonalSubscription = () => ($scope.view = 'teamInvite') + $scope.keepPersonalSubscription = () => ($scope.view = 'teamInvite') - $scope.cancelPersonalSubscription = function () { - $scope.inflight = true - const request = $http.post('/user/subscription/cancel', { - _csrf: window.csrfToken, - }) - request.then(function () { - $scope.inflight = false - return ($scope.view = 'teamInvite') - }) - return request.catch(() => { - $scope.inflight = false - $scope.cancel_error = true - debugConsole.error('the request failed') - }) - } + $scope.cancelPersonalSubscription = function () { + $scope.inflight = true + const request = $http.post('/user/subscription/cancel', { + _csrf: window.csrfToken, + }) + request.then(function () { + $scope.inflight = false + return ($scope.view = 'teamInvite') + }) + return request.catch(() => { + $scope.inflight = false + $scope.cancel_error = true + debugConsole.error('the request failed') + }) + } - return ($scope.joinTeam = function () { - $scope.inflight = true - const inviteToken = getMeta('ol-inviteToken') - const request = $http.put(`/subscription/invites/${inviteToken}/`, { - _csrf: window.csrfToken, + return ($scope.joinTeam = function () { + $scope.inflight = true + const inviteToken = getMeta('ol-inviteToken') + const request = $http.put(`/subscription/invites/${inviteToken}/`, { + _csrf: window.csrfToken, + }) + request.then(function (response) { + const { status } = response + $scope.inflight = false + $scope.view = 'inviteAccepted' + if (status !== 200) { + // assume request worked + return ($scope.requestSent = false) + } + }) + return request.catch(() => debugConsole.error('the request failed')) }) - request.then(function (response) { - const { status } = response - $scope.inflight = false - $scope.view = 'inviteAccepted' - if (status !== 200) { - // assume request worked - return ($scope.requestSent = false) - } - }) - return request.catch(() => debugConsole.error('the request failed')) - }) -}) + }, +]) diff --git a/services/web/frontend/js/main/system-messages.js b/services/web/frontend/js/main/system-messages.js index 3284b66210..9fe2e25d94 100644 --- a/services/web/frontend/js/main/system-messages.js +++ b/services/web/frontend/js/main/system-messages.js @@ -12,38 +12,42 @@ import App from '../base' const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000 // Controller for messages (array) -App.controller('SystemMessagesController', ($http, $scope) => { - $scope.messages = [] - function pollSystemMessages() { - // Ignore polling if tab is hidden or browser is offline - if (document.hidden || !navigator.onLine) { - return +App.controller('SystemMessagesController', [ + '$http', + '$scope', + function ($http, $scope) { + $scope.messages = [] + function pollSystemMessages() { + // Ignore polling if tab is hidden or browser is offline + if (document.hidden || !navigator.onLine) { + return + } + + $http + .get('/system/messages') + .then(response => { + // Ignore if content-type is anything but JSON, prevents a bug where + // the user logs out in another tab, then a 302 redirect was returned, + // which is transparently resolved by the browser to the login (HTML) + // page. + // This then caused an Angular error where it was attempting to loop + // through the HTML as a string + if (response.headers('content-type').includes('json')) { + $scope.messages = response.data + } + }) + .catch(() => { + // ignore errors + }) } + pollSystemMessages() + setInterval(pollSystemMessages, MESSAGE_POLL_INTERVAL) + }, +]) - $http - .get('/system/messages') - .then(response => { - // Ignore if content-type is anything but JSON, prevents a bug where - // the user logs out in another tab, then a 302 redirect was returned, - // which is transparently resolved by the browser to the login (HTML) - // page. - // This then caused an Angular error where it was attempting to loop - // through the HTML as a string - if (response.headers('content-type').includes('json')) { - $scope.messages = response.data - } - }) - .catch(() => { - // ignore errors - }) - } - pollSystemMessages() - setInterval(pollSystemMessages, MESSAGE_POLL_INTERVAL) -}) - -export default App.controller( - 'SystemMessageController', - function ($scope, $sce) { +export default App.controller('SystemMessageController', [ + '$scope', + function ($scope) { $scope.hidden = $.localStorage(`systemMessage.hide.${$scope.message._id}`) $scope.protected = $scope.message._id === 'protected' $scope.htmlContent = $scope.message.content @@ -55,5 +59,5 @@ export default App.controller( return $.localStorage(`systemMessage.hide.${$scope.message._id}`, true) } }) - } -) + }, +]) diff --git a/services/web/frontend/js/main/token-access.js b/services/web/frontend/js/main/token-access.js index 234ff8f918..64ffb7b7ff 100644 --- a/services/web/frontend/js/main/token-access.js +++ b/services/web/frontend/js/main/token-access.js @@ -1,8 +1,10 @@ import App from '../base' import { debugConsole } from '@/utils/debugging' -App.controller( - 'TokenAccessPageController', - ($scope, $http, $location, localStorage) => { +App.controller('TokenAccessPageController', [ + '$scope', + '$http', + '$location', + function ($scope, $http, $location) { window.S = $scope $scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import' | 'requireAccept' @@ -86,5 +88,5 @@ App.controller( } ) } - } -) + }, +]) diff --git a/services/web/frontend/js/main/translations.js b/services/web/frontend/js/main/translations.js index d790f890d0..88780d3fed 100644 --- a/services/web/frontend/js/main/translations.js +++ b/services/web/frontend/js/main/translations.js @@ -1,7 +1,9 @@ import App from '../base' -App.controller( - 'TranslationsPopupController', +App.controller('TranslationsPopupController', [ + '$scope', + 'ipCookie', + 'localStorage', function ($scope, ipCookie, localStorage) { function getStoredDismissal() { const localStore = localStorage('hide-i18n-notification') @@ -28,5 +30,5 @@ App.controller( localStorage('hide-i18n-notification', true) $scope.hidei18nNotification = true } - } -) + }, +]) diff --git a/services/web/frontend/js/modules/errorCatcher.js b/services/web/frontend/js/modules/errorCatcher.js index 5994dcfc29..0a7b388770 100644 --- a/services/web/frontend/js/modules/errorCatcher.js +++ b/services/web/frontend/js/modules/errorCatcher.js @@ -16,8 +16,8 @@ const UNHANDLED_REJECTION_ERR_MSG = 'Possibly unhandled rejection: canceled' app.config([ '$provide', - $provide => - $provide.decorator('$exceptionHandler', [ + function ($provide) { + return $provide.decorator('$exceptionHandler', [ '$log', '$delegate', ($log, $delegate) => @@ -37,39 +37,48 @@ app.config([ return $delegate(exception, cause) }, - ]), + ]) + }, ]) // 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) => ({ - 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', + function ($q) { + return { + 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', - $httpProvider => - $httpProvider.interceptors.push('unAuthHttpResponseInterceptor'), + function ($httpProvider) { + return $httpProvider.interceptors.push('unAuthHttpResponseInterceptor') + }, ]) diff --git a/services/web/frontend/js/services/queued-http.js b/services/web/frontend/js/services/queued-http.js index 6d5c41bbfe..5bc3172870 100644 --- a/services/web/frontend/js/services/queued-http.js +++ b/services/web/frontend/js/services/queued-http.js @@ -12,66 +12,71 @@ */ import App from '../base' -export default App.factory('queuedHttp', function ($http, $q) { - const pendingRequests = [] - let inflight = false +export default App.factory('queuedHttp', [ + '$http', + function ($http) { + const pendingRequests = [] + let inflight = false - function processPendingRequests() { - if (inflight) { - return - } - const doRequest = pendingRequests.shift() - if (doRequest != null) { - inflight = true - return doRequest() - .then(function () { - inflight = false - return processPendingRequests() - }) - .catch(function () { - inflight = false - return processPendingRequests() - }) - } - } - - const queuedHttp = function (...args) { - // We can't use Angular's $q.defer promises, because it only passes - // a single argument on error, and $http passes multiple. - const promise = {} - const successCallbacks = [] - const errorCallbacks = [] - - // Adhere to the $http promise conventions - promise.then = function (callback, errCallback) { - successCallbacks.push(callback) - if (errCallback != null) { - errorCallbacks.push(errCallback) + function processPendingRequests() { + if (inflight) { + return } + const doRequest = pendingRequests.shift() + if (doRequest != null) { + inflight = true + return doRequest() + .then(function () { + inflight = false + return processPendingRequests() + }) + .catch(function () { + inflight = false + return processPendingRequests() + }) + } + } + + const queuedHttp = function (...args) { + // We can't use Angular's $q.defer promises, because it only passes + // a single argument on error, and $http passes multiple. + const promise = {} + const successCallbacks = [] + const errorCallbacks = [] + + // Adhere to the $http promise conventions + promise.then = function (callback, errCallback) { + successCallbacks.push(callback) + if (errCallback != null) { + errorCallbacks.push(errCallback) + } + return promise + } + + promise.catch = function (callback) { + errorCallbacks.push(callback) + return promise + } + + const doRequest = () => + $http(...Array.from(args || [])) + .then((...args) => + Array.from(successCallbacks).map(fn => + fn(...Array.from(args || [])) + ) + ) + .catch((...args) => + Array.from(errorCallbacks).map(fn => fn(...Array.from(args || []))) + ) + + pendingRequests.push(doRequest) + processPendingRequests() + return promise } - promise.catch = function (callback) { - errorCallbacks.push(callback) - return promise - } + queuedHttp.post = (url, data) => queuedHttp({ method: 'POST', url, data }) - const doRequest = () => - $http(...Array.from(args || [])) - .then((...args) => - Array.from(successCallbacks).map(fn => fn(...Array.from(args || []))) - ) - .catch((...args) => - Array.from(errorCallbacks).map(fn => fn(...Array.from(args || []))) - ) - - pendingRequests.push(doRequest) - processPendingRequests() - - return promise - } - - queuedHttp.post = (url, data) => queuedHttp({ method: 'POST', url, data }) - - return queuedHttp -}) + return queuedHttp + }, +]) diff --git a/services/web/frontend/js/services/wait-for.js b/services/web/frontend/js/services/wait-for.js index 0b3b26d4e4..5a177f2887 100644 --- a/services/web/frontend/js/services/wait-for.js +++ b/services/web/frontend/js/services/wait-for.js @@ -11,32 +11,35 @@ */ import App from '../base' -export default App.factory('waitFor', function ($q) { - const waitFor = function (testFunction, timeout, pollInterval) { - if (pollInterval == null) { - pollInterval = 500 - } - const iterationLimit = Math.floor(timeout / pollInterval) - let iterations = 0 - return $q(function (resolve, reject) { - let tryIteration - return (tryIteration = function () { - if (iterations > iterationLimit) { - return reject( - new Error( - `waiting too long, ${JSON.stringify({ timeout, pollInterval })}` +export default App.factory('waitFor', [ + '$q', + function ($q) { + const waitFor = function (testFunction, timeout, pollInterval) { + if (pollInterval == null) { + pollInterval = 500 + } + const iterationLimit = Math.floor(timeout / pollInterval) + let iterations = 0 + return $q(function (resolve, reject) { + let tryIteration + return (tryIteration = function () { + if (iterations > iterationLimit) { + return reject( + new Error( + `waiting too long, ${JSON.stringify({ timeout, pollInterval })}` + ) ) - ) - } - iterations += 1 - const result = testFunction() - if (result != null) { - return resolve(result) - } else { - return setTimeout(tryIteration, pollInterval) - } - })() - }) - } - return waitFor -}) + } + iterations += 1 + const result = testFunction() + if (result != null) { + return resolve(result) + } else { + return setTimeout(tryIteration, pollInterval) + } + })() + }) + } + return waitFor + }, +])