Merge pull request #15129 from overleaf/mj-jpa-angular-parameters

[web] Explicitly name angular parameters

GitOrigin-RevId: 91beae68989d6c8122132b531a4338b116d87424
This commit is contained in:
Jakob Ackermann 2023-10-13 11:13:49 +02:00 committed by Copybot
parent 6b3dac803d
commit e959529828
61 changed files with 1780 additions and 1570 deletions

View file

@ -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',
'<div class="ui-select-match" ng-hide="$select.open && $select.searchEnabled" ng-disabled="$select.disabled" ng-class="{\'btn-default-focus\':$select.focus}"><span tabindex="-1" class="btn btn-default form-control ui-select-toggle" aria-label="{{ $select.baseTitle }} activate" ng-disabled="$select.disabled" ng-click="$select.activate()" style="outline: 0;"><span ng-show="$select.isEmpty()" class="ui-select-placeholder text-muted">{{$select.placeholder}}</span> <span ng-hide="$select.isEmpty()" class="ui-select-match-text pull-left" ng-class="{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}" ng-transclude=""></span> <i class="caret pull-right" ng-click="$select.toggle($event)"></i> <a ng-show="$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)" aria-label="{{ $select.baseTitle }} clear" style="margin-right: 10px" ng-click="$select.clear($event)" class="btn btn-xs btn-link pull-right"><i class="fa fa-times" aria-hidden="true"></i></a></span></div>'
)
})
// UI Select templates are hard-coded and use Glyphicon icons (which we don't import).
// The line below simply overrides the hard-coded template with our own, which is
// basically the same but using Font Awesome icons.
$templateCache.put(
'bootstrap/match.tpl.html',
'<div class="ui-select-match" ng-hide="$select.open && $select.searchEnabled" ng-disabled="$select.disabled" ng-class="{\'btn-default-focus\':$select.focus}"><span tabindex="-1" class="btn btn-default form-control ui-select-toggle" aria-label="{{ $select.baseTitle }} activate" ng-disabled="$select.disabled" ng-click="$select.activate()" style="outline: 0;"><span ng-show="$select.isEmpty()" class="ui-select-placeholder text-muted">{{$select.placeholder}}</span> <span ng-hide="$select.isEmpty()" class="ui-select-match-text pull-left" ng-class="{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}" ng-transclude=""></span> <i class="caret pull-right" ng-click="$select.toggle($event)"></i> <a ng-show="$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)" aria-label="{{ $select.baseTitle }} clear" style="margin-right: 10px" ng-click="$select.clear($event)" class="btn btn-xs btn-link pull-right"><i class="fa fa-times" aria-hidden="true"></i></a></span></div>'
)
},
])
export default App

View file

@ -1,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',

View file

@ -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)
}
})
}
},
}),
])

View file

@ -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)
)
}
},
}))
},
}),
])

View file

@ -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

View file

@ -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

View file

@ -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
)
)
},
}),
])

View file

@ -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())

View file

@ -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()
}
}
}
)
},
}))
)
},
}),
])

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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(

View file

@ -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(
})
}
})
}
)
},
])

View file

@ -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'])

View file

@ -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(`\
<a href \
ng-show=\"isVisible\" \
class=\"custom-toggler ${`custom-toggler-${customTogglerPane}`}\" \
@ -216,99 +221,100 @@ tooltip-placement=\"{{ tooltipPlacement }}\" \
ng-click=\"handleClick()\" \
aria-label=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\">\
`)(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)
},
}
},
}),
])

View file

@ -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')
}
},
}),
])

View file

@ -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.'
}
})
}
)
},
])

View file

@ -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(
</div>\
`,
}
}
)
},
])
function monkeyPatchSearch($rootScope, $compile) {
const searchHtml = `\

View file

@ -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'
}
},
])

View file

@ -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
},
])

View file

@ -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 (

View file

@ -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',
})

View file

@ -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',
})

View file

@ -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',
})

View file

@ -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',
})

View file

@ -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) {

View file

@ -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',
})

View file

@ -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(
}
})
})
}
)
},
])

View file

@ -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(
}
})
})
}
)
},
])

View file

@ -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)
}
}
)
},
])

View file

@ -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(
},
},
}))
}
)
},
])

View file

@ -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)
}
}
)
},
])

View file

@ -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
},
])

View file

@ -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))
}
)
},
])

View file

@ -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) => {

View file

@ -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()
},
])

View file

@ -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)
)
},
}),
])

View file

@ -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)
)
},
}),
])

View file

@ -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)
}
})
},
}),
])

View file

@ -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)
}
}
}
}
)
},
}))
)
},
}),
])

View file

@ -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

View file

@ -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

View file

@ -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'
)
})
},
])

View file

@ -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
},
])

View file

@ -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'])

View file

@ -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)
},
])

View file

@ -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)
)
})
}
)
},
])

View file

@ -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()
},
])

View file

@ -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))
})
}
)
},
])

View file

@ -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')

View file

@ -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,
}
}
)
},
])

View file

@ -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 () {})

View file

@ -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',
})
},
])

View file

@ -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'))
})
})
},
])

View file

@ -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)
}
})
}
)
},
])

View file

@ -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(
}
)
}
}
)
},
])

View file

@ -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
}
}
)
},
])

View file

@ -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')
},
])

View file

@ -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
},
])

View file

@ -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
},
])