define [ "base" "ide/pdfng/directives/pdfTextLayer" "ide/pdfng/directives/pdfAnnotations" "ide/pdfng/directives/pdfHighlights" "ide/pdfng/directives/pdfRenderer" "ide/pdfng/directives/pdfPage" "ide/pdfng/directives/pdfSpinner" "libs/pdf" ], ( App pdfTextLayer pdfAnnotations pdfHighlights pdfRenderer pdfPage pdfSpinner pdf ) -> # App = angular.module 'pdfViewerApp', ['pdfPage', 'PDFRenderer', 'pdfHighlights'] App.controller 'pdfViewerController', ['$scope', '$q', '$timeout', 'PDFRenderer', '$element', 'pdfHighlights', 'pdfSpinner', ($scope, $q, $timeout, PDFRenderer, $element, pdfHighlights, pdfSpinner) -> @load = () -> # $scope.pages = [] $scope.document.destroy() if $scope.document? $scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1 # TODO need a proper url manipulation library to add to query string url = $scope.pdfSrc # add 'pdfng=true' to show that we are using the angular pdfjs viewer queryStringExists = url.match(/\?/) url = url + (if not queryStringExists then '?' else '&') + 'pdfng=true' # for isolated compiles, load the pdf on-demand because nobody will overwrite it onDemandLoading = window.location?.search?.match(/isolated=true/)? $scope.document = new PDFRenderer(url, { scale: 1, disableAutoFetch: if onDemandLoading then true else undefined navigateFn: (ref) -> # this function captures clicks on the annotation links $scope.navigateTo = ref $scope.$apply() progressCallback: (progress) -> return if onDemandLoading is true # don't show progress for on-demand page loading $scope.$emit 'progress', progress loadedCallback: () -> $scope.$emit 'loaded' errorCallback: (error) -> Raven?.captureMessage?('pdfng error ' + error + ' (1% sample)') if Math.random() < 0.01 $scope.$emit 'pdf:error', error pageSizeChangeCallback: (pageNum, deltaH) -> $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 (result) -> $scope.pdfViewport = result.pdfViewport $scope.pdfPageSize = [ result.pdfViewport.height, result.pdfViewport.width ] # console.log 'resolved q.all, page size is', result $scope.numPages = result.numPages .catch (error) -> $scope.$emit 'pdf:error', error return $q.reject(error) @setScale = (scale, containerHeight, containerWidth) -> $scope.loaded.then () -> scale = {} if not scale? if containerHeight == 0 or 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) $scope.defaultPageSize = [ numScale * $scope.pdfPageSize[0], numScale * $scope.pdfPageSize[1] ] # console.log 'in setScale result', $scope.scale.scale, $scope.defaultPageSize .catch (error) -> $scope.$emit 'pdf:error', error return $q.reject(error) @redraw = (position) -> # console.log 'in redraw' # console.log 'reseting pages array for', $scope.numPages $scope.pages = ({ pageNum: i + 1 } for i in [0 .. $scope.numPages-1]) if position? && position.page? # console.log 'position is', position.page, position.offset # console.log 'setting current page', position.page pagenum = position.page if pagenum > $scope.numPages - 1 pagenum = $scope.numPages - 1 $scope.pages[pagenum].current = true $scope.pages[pagenum].position = position @zoomIn = () -> # console.log 'zoom in' newScale = $scope.scale.scale * 1.2 $scope.forceScale = { scaleMode: 'scale_mode_value', scale: newScale } @zoomOut = () -> # console.log 'zoom out' newScale = $scope.scale.scale / 1.2 $scope.forceScale = { scaleMode: 'scale_mode_value', scale: newScale } @fitWidth = () -> # console.log 'fit width' $scope.forceScale = { scaleMode: 'scale_mode_fit_width' } @fitHeight = () -> # console.log 'fit height' $scope.forceScale = { scaleMode: 'scale_mode_fit_height' } @checkPosition = () -> # console.log 'check position' $scope.forceCheck = ($scope.forceCheck || 0) + 1 @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) @getPdfPosition = () -> # console.log 'in getPdfPosition' topPageIdx = 0 topPage = $scope.pages[0] # find first visible page visible = $scope.pages.some (page, i) -> [topPageIdx, topPage] = [i, page] if page.visible if visible && topPage.element? # 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 top = topPage.element.offset().top bottom = top + topPage.element.innerHeight() viewportTop = $element.offset().top viewportBottom = viewportTop + $element.height() topVisible = (top >= viewportTop && top < viewportBottom) 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.viewport pdfOffset = viewport.convertToPdfPoint(0, canvasOffset); else # console.log 'WARNING: had to default to global page size' viewport = $scope.pdfViewport scaledOffset = canvasOffset / $scope.scale.scale pdfOffset = viewport.convertToPdfPoint(0, scaledOffset); # console.log 'converted to offset = ', pdfOffset newPosition = { "page": topPageIdx, "offset" : { "top" : pdfOffset[1], "left": 0} "pageSize": { "height": viewport.viewBox[3], "width": viewport.viewBox[2] } } return newPosition @computeOffset = (page, position) -> # console.log 'computing offset for', page, position element = page.element #console.log 'element =', $(element), 'parent =', $(element).parent() t1 = $(element).offset()?.top t2 = $(element).parent().offset()?.top if not (t1? and t2?) return $q((resolve, reject) -> reject('elements destroyed')) 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()) currentScroll = $(element).parent().scrollTop() offset = position.offset # convert offset to pixels return $scope.document.getPdfViewport(page.pageNum).then (viewport) -> page.viewport = viewport pageOffset = viewport.convertToViewportPoint(offset.left, offset.top) # if the passed-in position doesn't have the page height/width add them now position.pageSize ?= {"height": viewport.viewBox[3], "width": viewport.viewBox[2]} # console.log 'addition offset =', pageOffset # console.log 'total', pageTop + pageOffset[1] Math.round(pageTop + pageOffset[1] + currentScroll) ## 10 is margin @setPdfPosition = (page, position) -> # console.log 'required pdf Position is', position @computeOffset(page, position).then (offset) -> $scope.pleaseScrollTo = offset $scope.position = position return this ] App.directive 'pdfViewer', ['$q', '$timeout', 'pdfSpinner', ($q, $timeout, pdfSpinner) -> { controller: 'pdfViewerController' controllerAs: 'ctrl' scope: { "pdfSrc": "=" "highlights": "=" "position": "=" "scale": "=" "pleaseJumpTo": "=" } template: """
""" link: (scope, element, attrs, ctrl) -> # console.log 'in pdfViewer element is', element # console.log 'attrs', attrs spinner = new pdfSpinner layoutReady = $q.defer() layoutReady.notify 'waiting for layout' layoutReady.promise.then () -> # console.log 'layoutReady was resolved' renderVisiblePages = () -> visiblePages = getVisiblePages() pages = getExtraPages visiblePages scope.document.renderPages(pages) getVisiblePages = () -> top = element[0].scrollTop; bottom = top + element[0].clientHeight; visiblePages = _.filter scope.pages, (page) -> return false if not page.element? pageElement = page.element[0] pageTop = pageElement.offsetTop pageBottom = pageTop + pageElement.clientHeight page.visible = pageTop < bottom and pageBottom > top return page.visible return visiblePages getExtraPages = (visiblePages) -> extra = [] firstVisiblePage = visiblePages[0].pageNum firstVisiblePageIdx = firstVisiblePage - 1 len = visiblePages.length lastVisiblePage = visiblePages[len-1].pageNum 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 rescaleTimer = null queueRescale = (scale) -> # console.log 'call to queueRescale' return if rescaleTimer? or layoutTimer? or elementTimer? # console.log 'adding to rescale queue' rescaleTimer = setTimeout () -> doRescale scale rescaleTimer = null , 0 spinnerTimer = null doRescale = (scale) -> # console.log 'doRescale', scale return unless scale? origposition = angular.copy scope.position # console.log 'origposition', origposition if not spinnerTimer? spinnerTimer = setTimeout () -> spinner.add(element) spinnerTimer = null , 100 layoutReady.promise.then (parentSize) -> [h, w] = parentSize # console.log 'in promise', h, w ctrl.setScale(scale, h, w).then () -> # console.log 'in setscale then', scale, h, w scope.$evalAsync () -> if spinnerTimer clearTimeout spinnerTimer else spinner.remove(element) ctrl.redraw(origposition) $timeout renderVisiblePages scope.loadSuccess = true .catch (error) -> scope.$emit 'pdf:error', error elementTimer = null 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 or element.width() == 0 return if elementTimer? elementTimer = setTimeout () -> elementTimer = null updateLayout() , 1000 else scope.parentSize = [ element.innerHeight(), element.innerWidth() ] # console.log 'resolving layoutReady with', scope.parentSize $timeout () -> layoutReady.resolve scope.parentSize scope.$emit 'flash-controls' layoutTimer = null queueLayout = () -> # console.log 'call to queue layout' return if layoutTimer? # console.log 'added to queue layoyt' layoutReady = $q.defer() layoutTimer = setTimeout () -> # console.log 'calling update layout' updateLayout() # console.log 'setting layout timer to null' 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', (event, error) -> return if error == 'cancelled' # check if too many retries or file is missing message = error?.message or error if scope.loadCount > 3 || message.match(/^Missing PDF/i) || message.match(/^loading/i) scope.$emit 'pdf:error:display' return if scope.loadSuccess 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' return scope.$on 'pdf:page:size-change', (event, pageNum, delta) -> #console.log 'page size change event', pageNum, delta origposition = angular.copy scope.position #console.log 'orig position', JSON.stringify(origposition) if origposition? && pageNum - 1 < origposition.page && delta != 0 currentScrollTop = element.scrollTop() #console.log 'adjusting scroll from', currentScrollTop, 'by', delta scope.adjustingScroll = true element.scrollTop(currentScrollTop + delta) element.on 'scroll', () -> #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) scope.scrollHandlerTimeout = setTimeout scrollHandler, 25 scrollHandler = () -> renderVisiblePages() newPosition = ctrl.getPdfPosition() if newPosition? scope.position = newPosition scope.scrollHandlerTimeout = null scope.$watch 'pdfSrc', (newVal, oldVal) -> # console.log 'loading pdf', newVal, oldVal return unless newVal? scope.loadCount = 0; # new pdf, so reset load count scope.loadSuccess = false ctrl.load().then () -> # trigger a redraw scope.scale = angular.copy (scope.scale) .catch (error) -> scope.$emit 'pdf:error', error scope.$watch 'scale', (newVal, oldVal) -> # no need to set scale when initialising, done in pdfSrc return if newVal == oldVal # console.log 'XXX calling Setscale in scale watch' queueRescale newVal scope.$watch 'forceScale', (newVal, oldVal) -> # console.log 'got change in numscale watcher', newVal, oldVal return unless newVal? queueRescale newVal # scope.$watch 'position', (newVal, oldVal) -> # console.log 'got change in position watcher', newVal, oldVal scope.$watch 'forceCheck', (newVal, oldVal) -> # console.log 'forceCheck', newVal, oldVal return unless newVal? scope.adjustingScroll = true # temporarily disable scroll queueRescale scope.scale scope.$watch('parentSize', (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' return unless newVal? queueRescale scope.scale , true) # scope.$watch 'elementWidth', (newVal, oldVal) -> # console.log '*** watch INTERVAL element width is', newVal, oldVal scope.$watch 'pleaseScrollTo', (newVal, oldVal) -> # console.log 'got request to ScrollTo', newVal, 'oldVal', oldVal return unless newVal? scope.adjustingScroll = true # temporarily disable scroll # handler while we reposition $(element).scrollTop(newVal) scope.pleaseScrollTo = undefined scope.$watch 'pleaseJumpTo', (newPosition, oldPosition) -> # console.log 'in pleaseJumpTo', newPosition, oldPosition return unless newPosition? ctrl.setPdfPosition scope.pages[newPosition.page-1], newPosition scope.$watch 'navigateTo', (newVal, oldVal) -> return unless newVal? # console.log 'got request to navigate to', newVal, 'oldVal', oldVal scope.navigateTo = undefined # console.log 'navigate to', newVal # console.log 'look up page num' 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 (pidx) -> # console.log 'page num is', pidx page = scope.pages[pidx] scope.document.getPdfViewport(page.pageNum).then (viewport) -> #console.log 'got viewport', viewport 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] newPosition = {page: pidx, offset: {top: r[3], left: r[2]}} ctrl.setPdfPosition scope.pages[pidx], newPosition # XXX? scope.$watch "highlights", (areas) -> # console.log 'got HIGHLIGHTS in pdfViewer', areas return if !areas? #console.log 'areas are', areas highlights = for area in areas or [] { page: area.page - 1 highlight: left: area.h top: area.v height: area.height width: area.width } #console.log 'highlights', highlights return if !highlights.length scope.$broadcast 'pdf:highlights', areas 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 pageNum = scope.pages[first.page]?.pageNum if !pageNum? return # ignore highlight scroll if page not found # use a visual offset of 72pt to match the offset in PdfController syncToCode scope.document.getPdfViewport(pageNum).then (viewport) -> position = { page: first.page offset: left: first.highlight.left top: viewport.viewBox[3] - first.highlight.top + first.highlight.height + 72 } ctrl.setPdfPosition(scope.pages[first.page], position) scope.$on '$destroy', () -> # console.log 'handle pdfng directive destroy' clearTimeout elementTimer if elementTimer? clearTimeout layoutTimer if layoutTimer? clearTimeout rescaleTimer if rescaleTimer? clearTimeout spinnerTimer if spinnerTimer? } ]