mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-15 01:55:29 +00:00
Merge pull request #15129 from overleaf/mj-jpa-angular-parameters
[web] Explicitly name angular parameters GitOrigin-RevId: 91beae68989d6c8122132b531a4338b116d87424
This commit is contained in:
parent
6b3dac803d
commit
e959529828
61 changed files with 1780 additions and 1570 deletions
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
},
|
||||
}))
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
}))
|
||||
)
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
|||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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.'
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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 = `\
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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(
|
|||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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(
|
|||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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(
|
|||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
}))
|
||||
)
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
})
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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 () {})
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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'))
|
||||
})
|
||||
})
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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(
|
|||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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')
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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
|
||||
},
|
||||
])
|
||||
|
|
Loading…
Add table
Reference in a new issue