diff --git a/src/lib/viewers/doc/PresentationViewer.js b/src/lib/viewers/doc/PresentationViewer.js index e74bcc73e..b6033ef88 100644 --- a/src/lib/viewers/doc/PresentationViewer.js +++ b/src/lib/viewers/doc/PresentationViewer.js @@ -1,6 +1,7 @@ import throttle from 'lodash/throttle'; import DocBaseViewer from './DocBaseViewer'; import PresentationPreloader from './PresentationPreloader'; +import { CLASS_INVISIBLE } from '../../constants'; import './Presentation.scss'; const WHEEL_THROTTLE = 200; @@ -21,6 +22,7 @@ class PresentationViewer extends DocBaseViewer { // Bind context for callbacks this.mobileScrollHandler = this.mobileScrollHandler.bind(this); this.pagesinitHandler = this.pagesinitHandler.bind(this); + this.pagechangingHandler = this.pagechangingHandler.bind(this); this.throttledWheelHandler = this.getWheelHandler().bind(this); } @@ -49,6 +51,33 @@ class PresentationViewer extends DocBaseViewer { this.preloader.removeAllListeners('preload'); } + /** + * Go to specified page. We implement presentation mode by hiding all pages + * except for the page we are going to. + * + * @param {number} pageNum Page to navigate to + * @return {void} + */ + setPage(pageNum) { + this.checkOverflow(); + + // Hide all pages + const pages = this.docEl.querySelectorAll('.page'); + [].forEach.call(pages, pageEl => { + pageEl.classList.add(CLASS_INVISIBLE); + }); + + super.setPage(pageNum); + + // Show page we are navigating to + const pageEl = this.docEl.querySelector(`[data-page-number="${this.pdfViewer.currentPageNumber}"]`); + pageEl.classList.remove(CLASS_INVISIBLE); + + // Force page to be rendered - this is needed because the presentation + // DOM layout can trick pdf.js into thinking that this page is not visible + this.pdfViewer.update(); + } + /** * Handles keyboard events for presentation viewer. * @@ -100,14 +129,16 @@ class PresentationViewer extends DocBaseViewer { //-------------------------------------------------------------------------- /** - * Initialize pdf.js viewer. + * Loads PDF.js with provided PDF. * - * @protected * @override - * @return {pdfjsViewer.PDFViewer} PDF viewer type + * @param {string} pdfUrl The URL of the PDF to load + * @return {void} + * @protected */ - initPdfViewer() { - return this.initPdfViewerClass(this.pdfjsViewer.PDFSinglePageViewer); + initViewer(pdfUrl) { + super.initViewer(pdfUrl); + this.overwritePdfViewerBehavior(); } //-------------------------------------------------------------------------- @@ -125,7 +156,6 @@ class PresentationViewer extends DocBaseViewer { super.bindDOMListeners(); this.docEl.addEventListener('wheel', this.throttledWheelHandler); - if (this.hasTouch) { this.docEl.addEventListener('touchstart', this.mobileScrollHandler); this.docEl.addEventListener('touchmove', this.mobileScrollHandler); @@ -144,7 +174,6 @@ class PresentationViewer extends DocBaseViewer { super.unbindDOMListeners(); this.docEl.removeEventListener('wheel', this.throttledWheelHandler); - if (this.hasTouch) { this.docEl.removeEventListener('touchstart', this.mobileScrollHandler); this.docEl.removeEventListener('touchmove', this.mobileScrollHandler); @@ -185,6 +214,41 @@ class PresentationViewer extends DocBaseViewer { } } + /** + * Handler for 'pagesinit' event. + * + * @private + * @return {void} + */ + pagesinitHandler() { + // We implement presentation mode by hiding other pages except for the first page + const pageEls = [].slice.call(this.docEl.querySelectorAll('.pdfViewer .page'), 0); + pageEls.forEach(pageEl => { + if (pageEl.getAttribute('data-page-number') === '1') { + return; + } + + pageEl.classList.add(CLASS_INVISIBLE); + }); + + super.pagesinitHandler(); + + // Initially scale the page to fit. This will change to auto on resize events. + this.pdfViewer.currentScaleValue = 'page-fit'; + } + + /** + * Page change handler. + * + * @private + * @param {event} e - Page change event + * @return {void} + */ + pagechangingHandler(e) { + this.setPage(e.pageNumber); + super.pagechangingHandler(e); + } + /** * Returns throttled mousewheel handler * @@ -204,6 +268,51 @@ class PresentationViewer extends DocBaseViewer { } }, WHEEL_THROTTLE); } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Overwrite some pdf_viewer.js behavior for presentations. + * + * @private + * @return {void} + */ + overwritePdfViewerBehavior() { + // Overwrite scrollPageIntoView for presentations since we have custom pagination behavior + // This override is needed to allow PDF.js to change pages when clicking on links in a presentation that + // navigate to other pages + this.pdfViewer.scrollPageIntoView = pageObj => { + if (!this.loaded) { + return; + } + + let pageNum = pageObj; + if (typeof pageNum !== 'number') { + pageNum = pageObj.pageNumber || 1; + } + + this.setPage(pageNum); + }; + // Overwrite _getVisiblePages for presentations to always calculate instead of fetching visible + // elements since we lay out presentations differently + this.pdfViewer._getVisiblePages = () => { + const currentPageObj = this.pdfViewer._pages[this.pdfViewer._currentPageNumber - 1]; + const visible = [ + { + id: currentPageObj.id, + view: currentPageObj, + }, + ]; + + return { + first: currentPageObj, + last: currentPageObj, + views: visible, + }; + }; + } } export default PresentationViewer; diff --git a/src/lib/viewers/doc/__tests__/PresentationViewer-test.js b/src/lib/viewers/doc/__tests__/PresentationViewer-test.js index 787c9f250..02e80bc10 100644 --- a/src/lib/viewers/doc/__tests__/PresentationViewer-test.js +++ b/src/lib/viewers/doc/__tests__/PresentationViewer-test.js @@ -1,8 +1,9 @@ /* eslint-disable no-unused-expressions */ +import PresentationViewer from '../PresentationViewer'; import BaseViewer from '../../BaseViewer'; import DocBaseViewer from '../DocBaseViewer'; import PresentationPreloader from '../PresentationPreloader'; -import PresentationViewer from '../PresentationViewer'; +import { CLASS_INVISIBLE } from '../../../constants'; const sandbox = sinon.sandbox.create(); @@ -98,6 +99,58 @@ describe('lib/viewers/doc/PresentationViewer', () => { }); }); + describe('setPage()', () => { + let page1; + let page2; + let page3; + + beforeEach(() => { + page1 = document.createElement('div'); + page1.setAttribute('data-page-number', '1'); + page1.classList.add('page'); + + page2 = document.createElement('div'); + page2.setAttribute('data-page-number', '2'); + page2.classList.add(CLASS_INVISIBLE, 'page'); + + page3 = document.createElement('div'); + page3.setAttribute('data-page-number', '3'); + page3.classList.add('page'); + + presentation.docEl.appendChild(page1); + presentation.docEl.appendChild(page2); + presentation.docEl.appendChild(page3); + }); + + afterEach(() => { + presentation.docEl.removeChild(page1); + presentation.docEl.removeChild(page2); + presentation.docEl.removeChild(page3); + }); + + it('should check to see if overflow is present', () => { + const checkOverflowStub = sandbox.stub(presentation, 'checkOverflow'); + + presentation.setPage(2); + expect(checkOverflowStub).to.be.called; + }); + + it('should all other pages', () => { + sandbox.stub(presentation, 'checkOverflow'); + presentation.setPage(2); + + expect(page1).to.have.class(CLASS_INVISIBLE); + expect(page3).to.have.class(CLASS_INVISIBLE); + }); + + it('should show the page being set', () => { + sandbox.stub(presentation, 'checkOverflow'); + presentation.setPage(2); + + expect(page2).to.not.have.class(CLASS_INVISIBLE); + }); + }); + describe('onKeydown()', () => { beforeEach(() => { stubs.previousPage = sandbox.stub(presentation, 'previousPage'); @@ -181,6 +234,23 @@ describe('lib/viewers/doc/PresentationViewer', () => { }); }); + describe('initViewer()', () => { + const initViewerFunc = DocBaseViewer.prototype.initViewer; + + afterEach(() => { + Object.defineProperty(DocBaseViewer.prototype, 'initViewer', { value: initViewerFunc }); + }); + + it('should overwrite the scrollPageIntoView method', () => { + const stub = sandbox.stub(presentation, 'overwritePdfViewerBehavior'); + Object.defineProperty(DocBaseViewer.prototype, 'initViewer', { value: sandbox.stub() }); + + presentation.initViewer('url'); + + expect(stub).to.be.called; + }); + }); + describe('bindDOMListeners()', () => { beforeEach(() => { stubs.addEventListener = sandbox.stub(presentation.docEl, 'addEventListener'); @@ -297,6 +367,41 @@ describe('lib/viewers/doc/PresentationViewer', () => { }); }); + describe('pagesInitHandler()', () => { + beforeEach(() => { + stubs.setPage = sandbox.stub(presentation, 'setPage'); + stubs.page1 = document.createElement('div'); + stubs.page1.setAttribute('data-page-number', '1'); + stubs.page1.className = 'page'; + + stubs.page2 = document.createElement('div'); + stubs.page2.setAttribute('data-page-number', '2'); + stubs.page2.className = 'page'; + + stubs.page3 = document.createElement('div'); + stubs.page3.setAttribute('data-page-number', '3'); + stubs.page3.className = 'page'; + + document.querySelector('.pdfViewer').appendChild(stubs.page1); + document.querySelector('.pdfViewer').appendChild(stubs.page2); + document.querySelector('.pdfViewer').appendChild(stubs.page3); + }); + + it('should hide all pages except for the first one', () => { + presentation.pagesinitHandler(); + + expect(stubs.page1).to.not.have.class(CLASS_INVISIBLE); + expect(stubs.page2).to.have.class(CLASS_INVISIBLE); + expect(stubs.page3).to.have.class(CLASS_INVISIBLE); + }); + + it('should set the pdf viewer scale to page-fit', () => { + presentation.pagesinitHandler(); + + expect(presentation.pdfViewer.currentScaleValue).to.equal('page-fit'); + }); + }); + describe('getWheelHandler()', () => { let wheelHandler; @@ -336,4 +441,52 @@ describe('lib/viewers/doc/PresentationViewer', () => { expect(stubs.nextPage).to.not.be.called; }); }); + + describe('overwritePdfViewerBehavior()', () => { + describe('should overwrite the scrollPageIntoView method', () => { + it('should do nothing if the viewer is not loaded', () => { + const setPageStub = sandbox.stub(presentation, 'setPage'); + const page = { + pageNumber: 3, + }; + + presentation.loaded = false; + presentation.overwritePdfViewerBehavior(); + presentation.pdfViewer.scrollPageIntoView(page); + + expect(setPageStub).to.not.be.called; + }); + + it('should change the page if the viewer is loaded', () => { + const setPageStub = sandbox.stub(presentation, 'setPage'); + const page = { + pageNumber: 3, + }; + + presentation.loaded = true; + presentation.overwritePdfViewerBehavior(); + presentation.pdfViewer.scrollPageIntoView(page); + + expect(setPageStub).to.be.calledWith(3); + }); + }); + + it('should overwrite the _getVisiblePages method', () => { + presentation.pdfViewer = { + _pages: { + 0: { + id: 1, + view: 'pageObj', + }, + }, + _currentPageNumber: 1, + }; + + presentation.overwritePdfViewerBehavior(); + const result = presentation.pdfViewer._getVisiblePages(); + + expect(result.first.id).to.equal(1); + expect(result.last.id).to.equal(1); + }); + }); });