Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(viewer): Annotations render correctly for presentation files #1114

Merged
merged 2 commits into from
Dec 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 116 additions & 7 deletions src/lib/viewers/doc/PresentationViewer.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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();
}

//--------------------------------------------------------------------------
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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
*
Expand All @@ -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;
155 changes: 154 additions & 1 deletion src/lib/viewers/doc/__tests__/PresentationViewer-test.js
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
});
});
});