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

Enable Angular "annotations" via Babel plugin

GitOrigin-RevId: 343610025e3ba791fb284a0b696a1ea33e125a80
This commit is contained in:
Eric Mc Sween 2019-07-16 10:13:18 +01:00 committed by sharelatex
parent d7549544d6
commit 65849456d0
43 changed files with 5168 additions and 5810 deletions

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -35,72 +35,67 @@ const isInViewport = function(element) {
} }
define(['base'], App => define(['base'], App =>
App.directive('eventTracking', [ App.directive('eventTracking', event_tracking => ({
'event_tracking', scope: {
event_tracking => ({ eventTracking: '@',
scope: { eventSegmentation: '=?'
eventTracking: '@', },
eventSegmentation: '=?' link(scope, element, attrs) {
}, const sendGA = attrs.eventTrackingGa || false
link(scope, element, attrs) { const sendMB = attrs.eventTrackingMb || false
const sendGA = attrs.eventTrackingGa || false const sendMBFunction = attrs.eventTrackingSendOnce
const sendMB = attrs.eventTrackingMb || false ? 'sendMBOnce'
const sendMBFunction = attrs.eventTrackingSendOnce : 'sendMB'
? 'sendMBOnce' const sendGAFunction = attrs.eventTrackingSendOnce ? 'sendGAOnce' : 'send'
: 'sendMB' const segmentation = scope.eventSegmentation || {}
const sendGAFunction = attrs.eventTrackingSendOnce segmentation.page = window.location.pathname
? '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 @param {boolean} scrollEvent Use to unbind scroll event
*/ */
if (sendMB) { if (sendMB) {
event_tracking[sendMBFunction](scope.eventTracking, segmentation) event_tracking[sendMBFunction](scope.eventTracking, segmentation)
}
if (sendGA) {
event_tracking[sendGAFunction](
attrs.eventTrackingGa,
attrs.eventTrackingAction || scope.eventTracking,
attrs.eventTrackingLabel || ''
)
}
if (scrollEvent) {
return $(window).unbind('resize scroll')
}
} }
if (sendGA) {
if (attrs.eventTrackingTrigger === 'load') { event_tracking[sendGAFunction](
return sendEvent() attrs.eventTrackingGa,
} else if (attrs.eventTrackingTrigger === 'click') { attrs.eventTrackingAction || scope.eventTracking,
return element.on('click', e => sendEvent()) attrs.eventTrackingLabel || ''
} else if (attrs.eventTrackingTrigger === 'hover') { )
let timer = null }
let timeoutAmt = 500 if (scrollEvent) {
if (attrs.eventHoverAmt) { return $(window).unbind('resize scroll')
timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
}
return element
.on('mouseenter', function() {
timer = setTimeout(() => sendEvent(), timeoutAmt)
})
.on('mouseleave', () => clearTimeout(timer))
} else if (attrs.eventTrackingTrigger === 'scroll') {
if (!event_tracking.eventInCache(scope.eventTracking)) {
return $(window).on('resize scroll', () =>
_.throttle(
isInViewport(element) &&
!event_tracking.eventInCache(scope.eventTracking)
? sendEvent(true)
: undefined,
500
)
)
}
} }
} }
})
])) if (attrs.eventTrackingTrigger === 'load') {
return sendEvent()
} else if (attrs.eventTrackingTrigger === 'click') {
return element.on('click', e => sendEvent())
} else if (attrs.eventTrackingTrigger === 'hover') {
let timer = null
let timeoutAmt = 500
if (attrs.eventHoverAmt) {
timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
}
return element
.on('mouseenter', function() {
timer = setTimeout(() => sendEvent(), timeoutAmt)
})
.on('mouseleave', () => clearTimeout(timer))
} else if (attrs.eventTrackingTrigger === 'scroll') {
if (!event_tracking.eventInCache(scope.eventTracking)) {
return $(window).on('resize scroll', () =>
_.throttle(
isInViewport(element) &&
!event_tracking.eventInCache(scope.eventTracking)
? sendEvent(true)
: undefined,
500
)
)
}
}
}
})))

View file

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

View file

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

View file

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

View file

@ -13,81 +13,76 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
define(['base', 'ide/editor/Document'], (App, Document) => define(['base', 'ide/editor/Document'], (App, Document) =>
App.controller('SavingNotificationController', [ App.controller('SavingNotificationController', function(
'$scope', $scope,
'$interval', $interval,
'ide', ide
function($scope, $interval, ide) { ) {
let warnAboutUnsavedChanges let warnAboutUnsavedChanges
setInterval(() => pollSavedStatus(), 1000) setInterval(() => pollSavedStatus(), 1000)
$(window).bind('beforeunload', () => { $(window).bind('beforeunload', () => {
return warnAboutUnsavedChanges() return warnAboutUnsavedChanges()
}) })
let lockEditorModal = null // modal showing "connection lost" let lockEditorModal = null // modal showing "connection lost"
const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved
$scope.docSavingStatus = {} $scope.docSavingStatus = {}
var pollSavedStatus = function() { var pollSavedStatus = function() {
let t let t
const oldStatus = $scope.docSavingStatus const oldStatus = $scope.docSavingStatus
const oldUnsavedCount = $scope.docSavingStatusCount const oldUnsavedCount = $scope.docSavingStatusCount
const newStatus = {} const newStatus = {}
let newUnsavedCount = 0 let newUnsavedCount = 0
let maxUnsavedSeconds = 0 let maxUnsavedSeconds = 0
for (let doc_id in Document.openDocs) { for (let doc_id in Document.openDocs) {
const doc = Document.openDocs[doc_id] const doc = Document.openDocs[doc_id]
const saving = doc.pollSavedStatus() const saving = doc.pollSavedStatus()
if (!saving) { if (!saving) {
newUnsavedCount++ newUnsavedCount++
if (oldStatus[doc_id] != null) { if (oldStatus[doc_id] != null) {
newStatus[doc_id] = oldStatus[doc_id] newStatus[doc_id] = oldStatus[doc_id]
t = newStatus[doc_id].unsavedSeconds += 1 t = newStatus[doc_id].unsavedSeconds += 1
if (t > maxUnsavedSeconds) { if (t > maxUnsavedSeconds) {
maxUnsavedSeconds = t maxUnsavedSeconds = t
} }
} else { } else {
newStatus[doc_id] = { newStatus[doc_id] = {
unsavedSeconds: 0, unsavedSeconds: 0,
doc: ide.fileTreeManager.findEntityById(doc_id) doc: ide.fileTreeManager.findEntityById(doc_id)
}
} }
} }
} }
if (
newUnsavedCount > 0 &&
t > MAX_UNSAVED_SECONDS &&
!lockEditorModal
) {
lockEditorModal = ide.showLockEditorMessageModal(
'Connection lost',
'Sorry, the connection to the server is down.'
)
lockEditorModal.result.finally(() => (lockEditorModal = null)) // unset the modal if connection comes back
}
if (lockEditorModal && newUnsavedCount === 0) {
lockEditorModal.dismiss('connection back up')
}
// for performance, only update the display if the old or new
// counts of unsaved files are nonzeror. If both old and new
// unsaved counts are zero then we know we are in a good state
// and don't need to do anything to the UI.
if (newUnsavedCount || oldUnsavedCount) {
$scope.docSavingStatus = newStatus
$scope.docSavingStatusCount = newUnsavedCount
return $scope.$apply()
}
} }
return (warnAboutUnsavedChanges = function() { if (newUnsavedCount > 0 && t > MAX_UNSAVED_SECONDS && !lockEditorModal) {
if (Document.hasUnsavedChanges()) { lockEditorModal = ide.showLockEditorMessageModal(
return 'You have unsaved changes. If you leave now they will not be saved.' 'Connection lost',
} 'Sorry, the connection to the server is down.'
}) )
lockEditorModal.result.finally(() => (lockEditorModal = null)) // unset the modal if connection comes back
}
if (lockEditorModal && newUnsavedCount === 0) {
lockEditorModal.dismiss('connection back up')
}
// for performance, only update the display if the old or new
// counts of unsaved files are nonzeror. If both old and new
// unsaved counts are zero then we know we are in a good state
// and don't need to do anything to the UI.
if (newUnsavedCount || oldUnsavedCount) {
$scope.docSavingStatus = newStatus
$scope.docSavingStatusCount = newUnsavedCount
return $scope.$apply()
}
} }
]))
return (warnAboutUnsavedChanges = function() {
if (Document.hasUnsavedChanges()) {
return 'You have unsaved changes. If you leave now they will not be saved.'
}
})
}))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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