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