diff --git a/src/lib/Controls.js b/src/lib/Controls.js
index f84cf238a..e4aa84f45 100644
--- a/src/lib/Controls.js
+++ b/src/lib/Controls.js
@@ -4,8 +4,8 @@ import { CLASS_HIDDEN } from './constants';
const SHOW_PREVIEW_CONTROLS_CLASS = 'box-show-preview-controls';
const CONTROLS_BUTTON_CLASS = 'bp-controls-btn';
-const CONTROLS_PAGE_NUM_INPUT_CLASS = 'bp-doc-page-num-input';
-const CONTROLS_PAGE_NUM_WRAPPER_CLASS = 'bp-doc-page-num-wrapper';
+const CONTROLS_PAGE_NUM_INPUT_CLASS = 'bp-page-num-input';
+const CONTROLS_PAGE_NUM_WRAPPER_CLASS = 'bp-page-num-wrapper';
const CONTROLS_AUTO_HIDE_TIMEOUT_IN_MILLIS = 2000;
class Controls {
diff --git a/src/lib/Controls.scss b/src/lib/Controls.scss
index 2ea3831c0..5216ce2cc 100644
--- a/src/lib/Controls.scss
+++ b/src/lib/Controls.scss
@@ -16,6 +16,71 @@
position: relative;
table-layout: fixed;
transition: opacity .5s;
+
+ // Page num input CSS
+ .bp-page-num {
+ min-width: 48px;
+ width: auto; // Let page num expand as needed
+
+ span {
+ display: inline;
+ font-size: 14px;
+ }
+ }
+
+ .bp-page-num-wrapper {
+ background-color: #444;
+ border-radius: 3px;
+ margin: 5px;
+ padding: 7px 5px;
+ }
+
+ /* stylelint-disable property-no-vendor-prefix */
+ // Removes the spinner for number type inputs in Webkit browsers
+ input::-webkit-outer-spin-button,
+ input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ }
+
+ // Removes the spinner for number type inputs in Firefox
+ input[type=number] {
+ -moz-appearance: textfield;
+ }
+
+ /* stylelint-enable property-no-vendor-prefix */
+
+ .bp-page-num-input {
+ font-size: 14px;
+ margin: 0 auto;
+ position: absolute;
+ text-align: center;
+ visibility: hidden;
+ width: 44px; // hard-coded to solve layout issues
+ }
+
+ &.show-page-number-input {
+ .bp-page-num-wrapper {
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ }
+
+ .bp-page-num {
+ opacity: 1;
+ }
+
+ .bp-current-page,
+ .bp-page-num-divider,
+ .bp-total-pages {
+ display: none;
+ }
+
+ .bp-page-num-input {
+ display: inline-block;
+ position: static;
+ visibility: visible;
+ }
+ }
}
.box-show-preview-controls .bp-controls {
diff --git a/src/lib/PageControls.js b/src/lib/PageControls.js
new file mode 100644
index 000000000..812e58edc
--- /dev/null
+++ b/src/lib/PageControls.js
@@ -0,0 +1,231 @@
+import EventEmitter from 'events';
+import fullscreen from './Fullscreen';
+import Browser from './Browser';
+import { decodeKeydown } from './util';
+import { ICON_DROP_DOWN, ICON_DROP_UP } from './icons/icons';
+
+const SHOW_PAGE_NUM_INPUT_CLASS = 'show-page-number-input';
+const CONTROLS_PAGE_NUM_WRAPPER_CLASS = 'bp-page-num-wrapper';
+const CONTROLS_CURRENT_PAGE = 'bp-current-page';
+const CONTROLS_PAGE_NUM_INPUT_CLASS = 'bp-page-num-input';
+const CONTROLS_TOTAL_PAGES = 'bp-total-pages';
+const PAGE_NUM = 'bp-page-num';
+const PREV_PAGE = 'bp-previous-page';
+const NEXT_PAGE = 'bp-next-page';
+
+const pageNumTemplate = `
+
+ 1
+
+ /
+ 1
+
`.replace(/>\s*<');
+
+class PageControls extends EventEmitter {
+ /**
+ * [constructor]
+ *
+ * @param {HTMLElement} controls - Viewer controls
+ * @param {Function} previousPage - Previous page handler
+ * @param {Function} nextPage - Next page handler
+ * @return {Controls} Instance of controls
+ */
+ constructor(controls, previousPage, nextPage) {
+ super();
+
+ this.controls = controls;
+ this.controlsEl = controls.controlsEl;
+ this.currentPageEl = controls.currentPageEl;
+ this.pageNumInputEl = controls.pageNumInputEl;
+
+ this.controls.add(__('previous_page'), previousPage, `bp-previous-page-icon ${PREV_PAGE}`, ICON_DROP_UP);
+ this.controls.add(__('enter_page_num'), this.showPageNumInput.bind(this), PAGE_NUM, pageNumTemplate);
+ this.controls.add(__('next_page'), nextPage, `bp-next-page-icon ${NEXT_PAGE}`, ICON_DROP_DOWN);
+ }
+
+ /**
+ * Initializes page number selector.
+ *
+ * @private
+ * @param {number} pagesCount - Total number of page
+ * @return {void}
+ */
+ init(pagesCount) {
+ const pageNumEl = this.controlsEl.querySelector(`.${PAGE_NUM}`);
+ this.pagesCount = pagesCount;
+
+ // Update total page number
+ const totalPageEl = pageNumEl.querySelector(`.${CONTROLS_TOTAL_PAGES}`);
+ totalPageEl.textContent = pagesCount;
+
+ // Keep reference to page number input and current page elements
+ this.pageNumInputEl = pageNumEl.querySelector(`.${CONTROLS_PAGE_NUM_INPUT_CLASS}`);
+ this.pageNumInputEl.setAttribute('max', pagesCount);
+
+ this.currentPageEl = pageNumEl.querySelector(`.${CONTROLS_CURRENT_PAGE}`);
+ }
+
+ /**
+ * Replaces the page number display with an input box that allows the user to type in a page number
+ *
+ * @private
+ * @return {void}
+ */
+ showPageNumInput() {
+ // show the input box with the current page number selected within it
+ this.controlsEl.classList.add(SHOW_PAGE_NUM_INPUT_CLASS);
+
+ this.pageNumInputEl.value = this.currentPageEl.textContent;
+ this.pageNumInputEl.focus();
+ this.pageNumInputEl.select();
+
+ // finish input when input is blurred or enter key is pressed
+ this.pageNumInputEl.addEventListener('blur', this.pageNumInputBlurHandler.bind(this));
+ this.pageNumInputEl.addEventListener('keydown', this.pageNumInputKeydownHandler.bind(this));
+ }
+
+ /**
+ * Hide the page number input
+ *
+ * @private
+ * @return {void}
+ */
+ hidePageNumInput() {
+ this.controlsEl.classList.remove(SHOW_PAGE_NUM_INPUT_CLASS);
+ this.pageNumInputEl.removeEventListener('blur', this.pageNumInputBlurHandler);
+ this.pageNumInputEl.removeEventListener('keydown', this.pageNumInputKeydownHandler);
+ }
+
+ /**
+ * Disables or enables previous/next pagination buttons depending on
+ * current page number.
+ *
+ * @param {number} currentPageNum - Current page number
+ * @param {number} pagesCount - Total number of page
+ * @return {void}
+ */
+ checkPaginationButtons(currentPageNum, pagesCount) {
+ const pageNumButtonEl = this.controlsEl.querySelector(`.${PAGE_NUM}`);
+ const previousPageButtonEl = this.controlsEl.querySelector(`.${PREV_PAGE}`);
+ const nextPageButtonEl = this.controlsEl.querySelector(`.${NEXT_PAGE}`);
+
+ // Safari disables keyboard input in fullscreen before Safari 10.1
+ const isSafariFullscreen = Browser.getName() === 'Safari' && fullscreen.isFullscreen(this.controlsEl);
+
+ // Disable page number selector if there is only one page or less
+ if (pageNumButtonEl) {
+ if (pagesCount <= 1 || isSafariFullscreen) {
+ pageNumButtonEl.disabled = true;
+ } else {
+ pageNumButtonEl.disabled = false;
+ }
+ }
+
+ // Disable previous page if on first page, otherwise enable
+ if (previousPageButtonEl) {
+ if (currentPageNum === 1) {
+ previousPageButtonEl.disabled = true;
+ } else {
+ previousPageButtonEl.disabled = false;
+ }
+ }
+
+ // Disable next page if on last page, otherwise enable
+ if (nextPageButtonEl) {
+ if (currentPageNum === pagesCount) {
+ nextPageButtonEl.disabled = true;
+ } else {
+ nextPageButtonEl.disabled = false;
+ }
+ }
+ }
+
+ /**
+ * Update page number in page control widget.
+ *
+ * @private
+ * @param {number} pageNum - Number of page to update to
+ * @return {void}
+ */
+ updateCurrentPage(pageNum) {
+ let truePageNum = pageNum;
+
+ // refine the page number to fall within bounds
+ if (pageNum > this.pagesCount) {
+ truePageNum = this.pagesCount;
+ } else if (pageNum < 1) {
+ truePageNum = 1;
+ }
+
+ if (this.pageNumInputEl) {
+ this.pageNumInputEl.value = truePageNum;
+ }
+
+ if (this.currentPageEl) {
+ this.currentPageEl.textContent = truePageNum;
+ }
+
+ this.currentPageNumber = truePageNum;
+ this.checkPaginationButtons(this.currentPageNumber, this.pagesCount);
+ }
+
+ /**
+ * Blur handler for page number input.
+ *
+ * @private
+ * @param {Event} event Blur event
+ * @return {void}
+ */
+ pageNumInputBlurHandler(event) {
+ const target = event.target;
+ const pageNum = parseInt(target.value, 10);
+
+ if (!isNaN(pageNum)) {
+ this.emit('setpage', pageNum);
+ }
+
+ this.hidePageNumInput();
+ }
+
+ /**
+ * Keydown handler for page number input.
+ *
+ * @private
+ * @param {Event} event - Keydown event
+ * @return {void}
+ */
+ pageNumInputKeydownHandler(event) {
+ const key = decodeKeydown(event);
+
+ switch (key) {
+ case 'Enter':
+ case 'Tab':
+ // The keycode of the 'next' key on Android Chrome is 9, which maps to 'Tab'.
+ // this.docEl.focus();
+ // We normally trigger the blur handler by blurring the input
+ // field, but this doesn't work for IE in fullscreen. For IE,
+ // we blur the page behind the controls - this unfortunately
+ // is an IE-only solution that doesn't work with other browsers
+ if (Browser.getName() !== 'Explorer') {
+ event.target.blur();
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+
+ case 'Escape':
+ this.hidePageNumInput();
+ // this.docEl.focus();
+
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+export default PageControls;
diff --git a/src/lib/__tests__/Controls-test.js b/src/lib/__tests__/Controls-test.js
index 5f3bd9799..dbead76fd 100644
--- a/src/lib/__tests__/Controls-test.js
+++ b/src/lib/__tests__/Controls-test.js
@@ -93,7 +93,7 @@ describe('lib/Controls', () => {
element.className = '';
expect(controls.isPreviewControlButton(element)).to.be.false;
- parent.className = 'bp-doc-page-num-wrapper';
+ parent.className = 'bp-page-num-wrapper';
expect(controls.isPreviewControlButton(element)).to.be.true;
});
});
diff --git a/src/lib/__tests__/PageControls-test.html b/src/lib/__tests__/PageControls-test.html
new file mode 100644
index 000000000..3d412d36e
--- /dev/null
+++ b/src/lib/__tests__/PageControls-test.html
@@ -0,0 +1 @@
+
diff --git a/src/lib/__tests__/PageControls-test.js b/src/lib/__tests__/PageControls-test.js
new file mode 100644
index 000000000..cba7bf7f1
--- /dev/null
+++ b/src/lib/__tests__/PageControls-test.js
@@ -0,0 +1,232 @@
+/* eslint-disable no-unused-expressions */
+import PageControls from '../PageControls';
+import Controls from '../Controls';
+import { CLASS_HIDDEN } from './../constants';
+import fullscreen from '../Fullscreen';
+import Browser from '../Browser';
+import { decodeKeydown } from '../util';
+import { ICON_DROP_DOWN, ICON_DROP_UP } from '../icons/icons';
+
+let pageControls;
+let clock;
+let stubs = {};
+
+const sandbox = sinon.sandbox.create();
+
+const SHOW_PREVIEW_CONTROLS_CLASS = 'box-show-preview-controls';
+const RESET_TIMEOUT_CLOCK_TICK = 2001;
+
+const SHOW_PAGE_NUM_INPUT_CLASS = 'show-page-number-input';
+const CONTROLS_PAGE_NUM_WRAPPER_CLASS = 'bp-page-num-wrapper';
+const CONTROLS_CURRENT_PAGE = 'bp-current-page';
+const CONTROLS_PAGE_NUM_INPUT_CLASS = 'bp-page-num-input';
+const CONTROLS_TOTAL_PAGES = 'bp-total-pages';
+const PAGE_NUM = 'bp-page-num';
+const PREV_PAGE = 'bp-previous-page';
+const NEXT_PAGE = 'bp-next-page';
+
+describe('lib/PageControls', () => {
+ before(() => {
+ fixture.setBase('src/lib');
+ });
+
+ beforeEach(() => {
+ fixture.load('__tests__/PageControls-test.html');
+ const controls = new Controls(document.getElementById('test-page-controls-container'));
+ pageControls = new PageControls(controls, sandbox.stub, sandbox.stub);
+ });
+
+ afterEach(() => {
+ fixture.cleanup();
+ sandbox.verifyAndRestore();
+
+ if (pageControls && typeof pageControls.destroy === 'function') {
+ pageControls.destroy();
+ }
+
+ pageControls = null;
+ stubs = {};
+ });
+
+ describe('constructor()', () => {
+ it('should create the correct DOM structure', () => {
+ expect(pageControls.controlsEl).to.not.be.undefined;
+ expect(pageControls.controls.buttonRefs.length).equals(3);
+ });
+ });
+
+ describe('init()', () => {
+ it('should initialize the page number selector', () => {
+ const pagesCount = '5';
+ pageControls.init(pagesCount);
+ const totalPageEl = pageControls.controlsEl.querySelector(`.${CONTROLS_TOTAL_PAGES}`);
+ const pageNumInputEl = pageControls.controlsEl.querySelector(`.${CONTROLS_PAGE_NUM_INPUT_CLASS}`);
+ expect(pageControls.pagesCount).equals(pagesCount);
+ expect(totalPageEl).to.have.text(pagesCount);
+ expect(pageNumInputEl).to.have.attr('max', pagesCount);
+ expect(pageControls.currentPageEl).to.not.be.undefined;
+ });
+ });
+
+ describe('showPageNumInput()', () => {
+ it('should set the page number input value, focus, select, and add listeners', () => {
+ pageControls.currentPageEl = 0;
+ pageControls.pageNumInputEl = {
+ value: 0,
+ focus: sandbox.stub(),
+ select: sandbox.stub(),
+ addEventListener: sandbox.stub()
+ };
+
+ pageControls.showPageNumInput();
+ expect(pageControls.controlsEl).to.have.class(SHOW_PAGE_NUM_INPUT_CLASS);
+ expect(pageControls.pageNumInputEl.focus).to.be.called;
+ expect(pageControls.pageNumInputEl.select).to.be.called;
+ expect(pageControls.pageNumInputEl.addEventListener).to.be.calledWith('blur', sinon.match.func);
+ expect(pageControls.pageNumInputEl.addEventListener).to.be.calledWith('keydown', sinon.match.func);
+ });
+ });
+
+ describe('hidePageNumInput()', () => {
+ it('should hide the input class and remove event listeners', () => {
+ pageControls.pageNumInputEl = {
+ removeEventListener: sandbox.stub()
+ };
+
+ pageControls.hidePageNumInput();
+ expect(pageControls.controlsEl).to.not.have.class(SHOW_PAGE_NUM_INPUT_CLASS);
+ expect(pageControls.pageNumInputEl.removeEventListener).to.be.calledWith('blur', sinon.match.func);
+ expect(pageControls.pageNumInputEl.removeEventListener).to.be.calledWith('keydown', sinon.match.func);
+ });
+ });
+
+ describe('checkPaginationButtons()', () => {
+ beforeEach(() => {
+ stubs.pageNumButtonEl = pageControls.controlsEl.querySelector(`.${PAGE_NUM}`);
+ stubs.previousPageButtonEl = pageControls.controlsEl.querySelector(`.${PREV_PAGE}`);
+ stubs.nextPageButtonEl = pageControls.controlsEl.querySelector(`.${NEXT_PAGE}`);
+
+ stubs.browser = sandbox.stub(Browser, 'getName').returns('Safari');
+ stubs.fullscreen = sandbox.stub(fullscreen, 'isFullscreen').returns(true);
+ });
+
+ it('should disable/enable page number button el based on current page and browser type', () => {
+ pageControls.checkPaginationButtons();
+ expect(stubs.pageNumButtonEl.disabled).to.equal(true);
+
+ pageControls.checkPaginationButtons(1, 6);
+ expect(stubs.pageNumButtonEl.disabled).to.equal(true);
+
+ stubs.fullscreen.returns('false');
+ stubs.browser.returns('Chrome');
+ pageControls.checkPaginationButtons(1, 6);
+ expect(stubs.pageNumButtonEl.disabled).to.equal(false);
+ });
+
+ it('should disable/enable previous page button el based on current page', () => {
+ pageControls.checkPaginationButtons(1, 5);
+ expect(stubs.previousPageButtonEl.disabled).to.equal(true);
+
+ pageControls.checkPaginationButtons(20, 20);
+ expect(stubs.previousPageButtonEl.disabled).to.equal(false);
+ });
+
+ it('should disable/enable next page button el based on current page', () => {
+ pageControls.checkPaginationButtons(20, 20);
+ expect(stubs.nextPageButtonEl.disabled).to.equal(true);
+
+ pageControls.checkPaginationButtons(1, 20);
+ expect(stubs.nextPageButtonEl.disabled).to.equal(false);
+ });
+ });
+
+ describe('updateCurrentPage()', () => {
+ it('should only update the page to a valid value', () => {
+ pageControls.pagesCount = 10;
+ pageControls.pageNumInputEl = {
+ value: 1,
+ textContent: 1
+ };
+ const checkPaginationButtonsStub = sandbox.stub(pageControls, 'checkPaginationButtons');
+
+ pageControls.updateCurrentPage(-5);
+ expect(checkPaginationButtonsStub).to.be.called;
+ expect(pageControls.pageNumInputEl.value).to.equal(1);
+
+ pageControls.updateCurrentPage(25);
+ expect(checkPaginationButtonsStub).to.be.called;
+ expect(pageControls.pageNumInputEl.value).to.equal(10);
+
+ pageControls.updateCurrentPage(7);
+ expect(checkPaginationButtonsStub).to.be.called;
+ expect(pageControls.pageNumInputEl.value).to.equal(7);
+ });
+ });
+
+ describe('pageNumInputBlurHandler()', () => {
+ beforeEach(() => {
+ stubs.event = {
+ target: {
+ value: 5
+ }
+ };
+ stubs.emit = sandbox.stub(pageControls, 'emit');
+ stubs.hidePageNumInputStub = sandbox.stub(pageControls, 'hidePageNumInput');
+ });
+
+ it('should hide the page number input and set the page if given valid input', () => {
+ pageControls.pageNumInputBlurHandler(stubs.event);
+ expect(stubs.emit).to.be.calledWith('setpage', stubs.event.target.value);
+ expect(stubs.hidePageNumInputStub).to.be.called;
+ });
+
+ it('should hide the page number input but not set the page if given invalid input', () => {
+ stubs.event.target.value = 'not a number';
+
+ pageControls.pageNumInputBlurHandler(stubs.event);
+ expect(stubs.emit).to.be.not.be.called;
+ expect(stubs.hidePageNumInputStub).to.be.called;
+ });
+ });
+
+ describe('pageNumInputKeydownHandler()', () => {
+ beforeEach(() => {
+ stubs.event = {
+ key: 'Enter',
+ stopPropagation: sandbox.stub(),
+ preventDefault: sandbox.stub(),
+ target: {
+ blur: sandbox.stub()
+ }
+ };
+ stubs.browser = sandbox.stub(Browser, 'getName').returns('Explorer');
+ stubs.hidePageNumInput = sandbox.stub(pageControls, 'hidePageNumInput');
+ });
+
+ it('should focus the doc element if IE and stop default actions on \'enter\'', () => {
+ pageControls.pageNumInputKeydownHandler(stubs.event);
+ expect(stubs.browser).to.be.called;
+ expect(stubs.event.stopPropagation).to.be.called;
+ expect(stubs.event.preventDefault).to.be.called;
+ });
+
+ it('should blur if not IE and stop default actions on \'enter\'', () => {
+ stubs.browser.returns('Chrome');
+
+ pageControls.pageNumInputKeydownHandler(stubs.event);
+ expect(stubs.browser).to.be.called;
+ expect(stubs.event.target.blur).to.be.called;
+ expect(stubs.event.stopPropagation).to.be.called;
+ expect(stubs.event.preventDefault).to.be.called;
+ });
+
+ it('should hide the page number input, focus the document, and stop default actions on \'Esc\'', () => {
+ stubs.event.key = 'Esc';
+
+ pageControls.pageNumInputKeydownHandler(stubs.event);
+ expect(stubs.hidePageNumInput).to.be.called;
+ expect(stubs.event.stopPropagation).to.be.called;
+ expect(stubs.event.preventDefault).to.be.called;
+ });
+ });
+});
diff --git a/src/lib/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js
index 17c5cc274..49afda508 100644
--- a/src/lib/viewers/doc/DocBaseViewer.js
+++ b/src/lib/viewers/doc/DocBaseViewer.js
@@ -3,6 +3,7 @@ import throttle from 'lodash.throttle';
import BaseViewer from '../BaseViewer';
import Browser from '../../Browser';
import Controls from '../../Controls';
+import PageControls from '../../PageControls';
import DocFindBar from './DocFindBar';
import fullscreen from '../../Fullscreen';
import Popup from '../../Popup';
@@ -17,7 +18,7 @@ import {
STATUS_ERROR
} from '../../constants';
import { checkPermission, getRepresentation } from '../../file';
-import { get, createAssetUrlCreator, decodeKeydown } from '../../util';
+import { get, createAssetUrlCreator } from '../../util';
import { ICON_PRINT_CHECKMARK, ICON_FILE_DOCUMENT } from '../../icons/icons';
import { JS, CSS } from './docAssets';
@@ -28,7 +29,6 @@ const SAFARI_PRINT_TIMEOUT_MS = 1000; // Wait 1s before trying to print
const PRINT_DIALOG_TIMEOUT_MS = 500;
const MAX_SCALE = 10.0;
const MIN_SCALE = 0.1;
-const SHOW_PAGE_NUM_INPUT_CLASS = 'show-page-number-input';
const IS_SAFARI_CLASS = 'is-safari';
const SCROLL_EVENT_THROTTLE_INTERVAL = 200;
const SCROLL_END_TIMEOUT = this.isMobile ? 500 : 250;
@@ -306,7 +306,7 @@ class DocBaseViewer extends BaseViewer {
* @return {void}
*/
setPage(pageNum) {
- if (pageNum <= 0 || pageNum > this.pdfViewer.pagesCount) {
+ if (pageNum < 1 || pageNum > this.pdfViewer.pagesCount) {
return;
}
@@ -347,50 +347,6 @@ class DocBaseViewer extends BaseViewer {
this.cache.set(CURRENT_PAGE_MAP_KEY, currentPageMap, true /* useLocalStorage */);
}
- /**
- * Disables or enables previous/next pagination buttons depending on
- * current page number.
- *
- * @return {void}
- */
- checkPaginationButtons() {
- const pagesCount = this.pdfViewer.pagesCount;
- const currentPageNum = this.pdfViewer.currentPageNumber;
- const pageNumButtonEl = this.containerEl.querySelector('.bp-doc-page-num');
- const previousPageButtonEl = this.containerEl.querySelector('.bp-previous-page');
- const nextPageButtonEl = this.containerEl.querySelector('.bp-next-page');
-
- // Safari disables keyboard input in fullscreen before Safari 10.1
- const isSafariFullscreen = Browser.getName() === 'Safari' && fullscreen.isFullscreen(this.containerEl);
-
- // Disable page number selector if there is only one page or less
- if (pageNumButtonEl) {
- if (pagesCount <= 1 || isSafariFullscreen) {
- pageNumButtonEl.disabled = true;
- } else {
- pageNumButtonEl.disabled = false;
- }
- }
-
- // Disable previous page if on first page, otherwise enable
- if (previousPageButtonEl) {
- if (currentPageNum === 1) {
- previousPageButtonEl.disabled = true;
- } else {
- previousPageButtonEl.disabled = false;
- }
- }
-
- // Disable next page if on last page, otherwise enable
- if (nextPageButtonEl) {
- if (currentPageNum === this.pdfViewer.pagesCount) {
- nextPageButtonEl.disabled = true;
- } else {
- nextPageButtonEl.disabled = false;
- }
- }
- }
-
/**
* Zoom into document.
*
@@ -663,26 +619,6 @@ class DocBaseViewer extends BaseViewer {
});
}
- /**
- * Initializes page number selector.
- *
- * @private
- * @return {void}
- */
- initPageNumEl() {
- const pageNumEl = this.controls.controlsEl.querySelector('.bp-doc-page-num');
-
- // Update total page number
- const totalPageEl = pageNumEl.querySelector('.bp-doc-total-pages');
- totalPageEl.textContent = this.pdfViewer.pagesCount;
-
- // Keep reference to page number input and current page elements
- this.pageNumInputEl = pageNumEl.querySelector('.bp-doc-page-num-input');
- this.pageNumInputEl.setAttribute('max', this.pdfViewer.pagesCount);
-
- this.currentPageEl = pageNumEl.querySelector('.bp-doc-current-page');
- }
-
/**
* Fetches PDF and converts to blob for printing.
*
@@ -760,68 +696,8 @@ class DocBaseViewer extends BaseViewer {
*/
loadUI() {
this.controls = new Controls(this.containerEl);
+ this.pageControls = new PageControls(this.controls, this.prevPage, this.nextPage);
this.bindControlListeners();
- this.initPageNumEl();
- }
-
- /**
- * Replaces the page number display with an input box that allows the user to type in a page number
- *
- * @private
- * @return {void}
- */
- showPageNumInput() {
- // show the input box with the current page number selected within it
- this.controls.controlsEl.classList.add(SHOW_PAGE_NUM_INPUT_CLASS);
-
- this.pageNumInputEl.value = this.currentPageEl.textContent;
- this.pageNumInputEl.focus();
- this.pageNumInputEl.select();
-
- // finish input when input is blurred or enter key is pressed
- this.pageNumInputEl.addEventListener('blur', this.pageNumInputBlurHandler);
- this.pageNumInputEl.addEventListener('keydown', this.pageNumInputKeydownHandler);
- }
-
- /**
- * Hide the page number input
- *
- * @private
- * @return {void}
- */
- hidePageNumInput() {
- this.controls.controlsEl.classList.remove(SHOW_PAGE_NUM_INPUT_CLASS);
- this.pageNumInputEl.removeEventListener('blur', this.pageNumInputBlurHandler);
- this.pageNumInputEl.removeEventListener('keydown', this.pageNumInputKeydownHandler);
- }
-
- /**
- * Update page number in page control widget.
- *
- * @private
- * @param {number} pageNum - Number of page to update to
- * @return {void}
- */
- updateCurrentPage(pageNum) {
- let truePageNum = pageNum;
- const pagesCount = this.pdfViewer.pagesCount;
-
- // refine the page number to fall within bounds
- if (pageNum > pagesCount) {
- truePageNum = pagesCount;
- } else if (pageNum < 1) {
- truePageNum = 1;
- }
-
- if (this.pageNumInputEl) {
- this.pageNumInputEl.value = truePageNum;
- }
-
- if (this.currentPageEl) {
- this.currentPageEl.textContent = truePageNum;
- }
-
- this.checkPaginationButtons();
}
//--------------------------------------------------------------------------
@@ -901,64 +777,6 @@ class DocBaseViewer extends BaseViewer {
*/
bindControlListeners() {}
- /**
- * Blur handler for page number input.
- *
- * @param {Event} event Blur event
- * @return {void}
- * @private
- */
- pageNumInputBlurHandler(event) {
- const target = event.target;
- const pageNum = parseInt(target.value, 10);
-
- if (!isNaN(pageNum)) {
- this.setPage(pageNum);
- }
-
- this.hidePageNumInput();
- }
-
- /**
- * Keydown handler for page number input.
- *
- * @private
- * @param {Event} event - Keydown event
- * @return {void}
- */
- pageNumInputKeydownHandler(event) {
- const key = decodeKeydown(event);
-
- switch (key) {
- case 'Enter':
- case 'Tab':
- // The keycode of the 'next' key on Android Chrome is 9, which maps to 'Tab'.
- this.docEl.focus();
- // We normally trigger the blur handler by blurring the input
- // field, but this doesn't work for IE in fullscreen. For IE,
- // we blur the page behind the controls - this unfortunately
- // is an IE-only solution that doesn't work with other browsers
- if (Browser.getName() !== 'Explorer') {
- event.target.blur();
- }
-
- event.stopPropagation();
- event.preventDefault();
- break;
-
- case 'Escape':
- this.hidePageNumInput();
- this.docEl.focus();
-
- event.stopPropagation();
- event.preventDefault();
- break;
-
- default:
- break;
- }
- }
-
/**
* Handler for 'pagesinit' event.
*
@@ -969,7 +787,7 @@ class DocBaseViewer extends BaseViewer {
this.pdfViewer.currentScaleValue = 'auto';
this.loadUI();
- this.checkPaginationButtons();
+ this.pageControls.checkPaginationButtons(this.pdfViewer.currentPageNumber, this.pdfViewer.pagesCount);
// Set current page to previously opened page or first page
this.setPage(this.getCachedPage());
@@ -1029,7 +847,7 @@ class DocBaseViewer extends BaseViewer {
*/
pagechangeHandler(event) {
const pageNum = event.pageNumber;
- this.updateCurrentPage(pageNum);
+ this.pageControls.updateCurrentPage(pageNum);
// We only set cache the current page if 'pagechange' was fired after
// preview is loaded - this filters out pagechange events fired by
diff --git a/src/lib/viewers/doc/DocumentViewer.js b/src/lib/viewers/doc/DocumentViewer.js
index c2cce2bcb..21537f4bb 100644
--- a/src/lib/viewers/doc/DocumentViewer.js
+++ b/src/lib/viewers/doc/DocumentViewer.js
@@ -1,11 +1,8 @@
import autobind from 'autobind-decorator';
-import pageNumTemplate from './pageNumButtonContent.html';
import DocBaseViewer from './DocBaseViewer';
import DocPreloader from './DocPreloader';
import fullscreen from '../../Fullscreen';
import {
- ICON_DROP_DOWN,
- ICON_DROP_UP,
ICON_FILE_DOCUMENT,
ICON_FILE_PDF,
ICON_FILE_SPREADSHEET,
@@ -103,16 +100,8 @@ class DocumentViewer extends DocBaseViewer {
this.controls.add(__('zoom_out'), this.zoomOut, 'bp-doc-zoom-out-icon', ICON_ZOOM_OUT);
this.controls.add(__('zoom_in'), this.zoomIn, 'bp-doc-zoom-in-icon', ICON_ZOOM_IN);
- this.controls.add(
- __('previous_page'),
- this.previousPage,
- 'bp-doc-previous-page-icon bp-previous-page',
- ICON_DROP_UP
- );
-
- const buttonContent = pageNumTemplate.replace(/>\s*<'); // removing new lines
- this.controls.add(__('enter_page_num'), this.showPageNumInput, 'bp-doc-page-num', buttonContent);
- this.controls.add(__('next_page'), this.nextPage, 'bp-doc-next-page-icon bp-next-page', ICON_DROP_DOWN);
+ this.pageControls.init(this.pdfViewer.pagesCount);
+ this.pageControls.addListener('setpage', this.setPage);
this.controls.add(
__('enter_fullscreen'),
diff --git a/src/lib/viewers/doc/PresentationViewer.js b/src/lib/viewers/doc/PresentationViewer.js
index 2e014d479..0f6bdfa2c 100644
--- a/src/lib/viewers/doc/PresentationViewer.js
+++ b/src/lib/viewers/doc/PresentationViewer.js
@@ -1,12 +1,9 @@
import autobind from 'autobind-decorator';
import throttle from 'lodash.throttle';
-import pageNumTemplate from './pageNumButtonContent.html';
import DocBaseViewer from './DocBaseViewer';
import PresentationPreloader from './PresentationPreloader';
import { CLASS_INVISIBLE } from '../../constants';
import {
- ICON_DROP_DOWN,
- ICON_DROP_UP,
ICON_FILE_PRESENTATION,
ICON_FULLSCREEN_IN,
ICON_FULLSCREEN_OUT,
@@ -193,22 +190,8 @@ class PresentationViewer extends DocBaseViewer {
this.controls.add(__('zoom_out'), this.zoomOut, 'bp-exit-zoom-out-icon', ICON_ZOOM_OUT);
this.controls.add(__('zoom_in'), this.zoomIn, 'bp-enter-zoom-in-icon', ICON_ZOOM_IN);
- this.controls.add(
- __('previous_page'),
- this.previousPage,
- 'bp-presentation-previous-page-icon bp-previous-page',
- ICON_DROP_UP
- );
-
- const buttonContent = pageNumTemplate.replace(/>\s*<'); // removing new lines
- this.controls.add(__('enter_page_num'), this.showPageNumInput, 'bp-doc-page-num', buttonContent);
-
- this.controls.add(
- __('next_page'),
- this.nextPage,
- 'bp-presentation-next-page-icon bp-next-page',
- ICON_DROP_DOWN
- );
+ this.pageControls.init(this.pdfViewer.pagesCount);
+ this.pageControls.addListener('setpage', this.setPage);
this.controls.add(
__('enter_fullscreen'),
diff --git a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js
index e0646732f..3bd2a7cb5 100644
--- a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js
+++ b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js
@@ -3,6 +3,7 @@ import DocBaseViewer from '../DocBaseViewer';
import Browser from '../../../Browser';
import BaseViewer from '../../BaseViewer';
import Controls from '../../../Controls';
+import PageControls from '../../../PageControls';
import fullscreen from '../../../Fullscreen';
import DocPreloader from '../DocPreloader';
import * as file from '../../../file';
@@ -568,76 +569,6 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {
});
});
- describe('checkPaginationButtons()', () => {
- beforeEach(() => {
- const pageNumButtonEl = document.createElement('div');
- pageNumButtonEl.className = 'bp-doc-page-num';
- pageNumButtonEl.disabled = undefined;
- docBase.containerEl.appendChild(pageNumButtonEl);
-
- const previousPageButtonEl = document.createElement('div');
- previousPageButtonEl.className = 'bp-previous-page';
- previousPageButtonEl.disabled = undefined;
- docBase.containerEl.appendChild(previousPageButtonEl);
-
- const nextPageButtonEl = document.createElement('div');
- nextPageButtonEl.className = 'bp-next-page';
- nextPageButtonEl.disabled = undefined;
- docBase.containerEl.appendChild(nextPageButtonEl);
-
- docBase.pdfViewer = {
- pagesCount: 0,
- currentPageNumber: 1
- };
-
- stubs.pageNumButtonEl = pageNumButtonEl;
- stubs.previousPageButtonEl = previousPageButtonEl;
- stubs.nextPageButtonEl = nextPageButtonEl;
- stubs.browser = sandbox.stub(Browser, 'getName').returns('Safari');
- stubs.fullscreen = sandbox.stub(fullscreen, 'isFullscreen').returns(true);
- });
-
- afterEach(() => {
- docBase.containerEl.innerHTML = '';
- docBase.pdfViewer = undefined;
- });
-
- it('should disable/enable page number button el based on current page and browser type', () => {
- docBase.checkPaginationButtons();
- expect(stubs.pageNumButtonEl.disabled).to.equal(true);
-
- docBase.pdfViewer.pagesCount = 6;
- docBase.checkPaginationButtons();
- expect(stubs.pageNumButtonEl.disabled).to.equal(true);
-
- stubs.fullscreen.returns('false');
- stubs.browser.returns('Chrome');
- docBase.checkPaginationButtons();
- expect(stubs.pageNumButtonEl.disabled).to.equal(false);
- });
-
- it('should disable/enable previous page button el based on current page', () => {
- docBase.checkPaginationButtons();
- expect(stubs.previousPageButtonEl.disabled).to.equal(true);
-
- docBase.pdfViewer.currentPageNumber = 20;
- docBase.checkPaginationButtons();
- expect(stubs.previousPageButtonEl.disabled).to.equal(false);
- });
-
- it('should disable/enable next page button el based on current page', () => {
- docBase.pdfViewer.currentPageNumber = 20;
- docBase.pdfViewer.pagesCount = 20;
-
- docBase.checkPaginationButtons();
- expect(stubs.nextPageButtonEl.disabled).to.equal(true);
-
- docBase.pdfViewer.currentPageNumber = 1;
- docBase.checkPaginationButtons();
- expect(stubs.nextPageButtonEl.disabled).to.equal(false);
- });
- });
-
describe('zoom methods', () => {
beforeEach(() => {
docBase.pdfViewer = {
@@ -1089,41 +1020,6 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {
});
});
- describe('initPageNumEl()', () => {
- beforeEach(() => {
- docBase.pdfViewer = {
- pagesCount: 5
- };
- stubs.totalPageEl = {
- textContent: 0,
- setAttribute: sandbox.stub()
- };
- stubs.querySelector = {
- querySelector: sandbox.stub().returns(stubs.totalPageEl)
- };
- docBase.controls = {
- controlsEl: {
- querySelector: sandbox.stub().returns(stubs.querySelector)
- }
- };
- });
-
- it('should set the text content on the total page element', () => {
- docBase.initPageNumEl();
-
- expect(docBase.controls.controlsEl.querySelector).to.be.called;
- expect(stubs.querySelector.querySelector).to.be.called;
- expect(stubs.totalPageEl.textContent).to.equal(5);
- });
-
- it('should keep track of the page number input and current page elements', () => {
- docBase.initPageNumEl();
-
- expect(docBase.pageNumInputEl).to.equal(stubs.totalPageEl);
- expect(docBase.currentPageEl).to.equal(stubs.totalPageEl);
- });
- });
-
describe('fetchPrintBlob()', () => {
beforeEach(() => {
stubs.get = sandbox.stub(util, 'get').returns(Promise.resolve('blob'));
@@ -1139,83 +1035,13 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {
describe('loadUI()', () => {
it('should set controls, bind listeners, and init the page number element', () => {
const bindControlListenersStub = sandbox.stub(docBase, 'bindControlListeners');
- const initPageNumElStub = sandbox.stub(docBase, 'initPageNumEl');
docBase.loadUI();
expect(bindControlListenersStub).to.be.called;
- expect(initPageNumElStub).to.be.called;
expect(docBase.controls instanceof Controls).to.be.true;
});
});
- describe('showPageNumInput()', () => {
- it('should set the page number input value, focus, select, and add listeners', () => {
- docBase.controls = {
- controlsEl: {
- classList: {
- add: sandbox.stub()
- }
- }
- };
- docBase.currentPageEl = 0;
- docBase.pageNumInputEl = {
- value: 0,
- focus: sandbox.stub(),
- select: sandbox.stub(),
- addEventListener: sandbox.stub()
- };
-
- docBase.showPageNumInput();
- expect(docBase.pageNumInputEl.focus).to.be.called;
- expect(docBase.pageNumInputEl.select).to.be.called;
- expect(docBase.pageNumInputEl.addEventListener).to.be.called.twice;
- });
- });
-
- describe('hidePageNumInput()', () => {
- it('should hide the input class and remove event listeners', () => {
- docBase.controls = {
- controlsEl: {
- classList: {
- remove: sandbox.stub()
- }
- }
- };
- docBase.pageNumInputEl = {
- removeEventListener: sandbox.stub()
- };
-
- docBase.hidePageNumInput();
- expect(docBase.controls.controlsEl.classList.remove).to.be.called;
- expect(docBase.pageNumInputEl.removeEventListener).to.be.called;
- });
- });
-
- describe('updateCurrentPage()', () => {
- it('should only update the page to a valid value', () => {
- docBase.pdfViewer = {
- pagesCount: 10
- };
- docBase.pageNumInputEl = {
- value: 1,
- textContent: 1
- };
- const checkPaginationButtonsStub = sandbox.stub(docBase, 'checkPaginationButtons');
-
- docBase.updateCurrentPage(-5);
- expect(checkPaginationButtonsStub).to.be.called;
- expect(docBase.pageNumInputEl.value).to.equal(1);
-
- docBase.updateCurrentPage(25);
- expect(checkPaginationButtonsStub).to.be.called;
- expect(docBase.pageNumInputEl.value).to.equal(10);
-
- docBase.updateCurrentPage(7);
- expect(checkPaginationButtonsStub).to.be.called;
- expect(docBase.pageNumInputEl.value).to.equal(7);
- });
- });
-
describe('bindDOMListeners()', () => {
beforeEach(() => {
stubs.addEventListener = sandbox.stub(docBase.docEl, 'addEventListener');
@@ -1309,84 +1135,18 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {
});
});
- describe('pageNumInputBlurHandler()', () => {
- beforeEach(() => {
- docBase.event = {
- target: {
- value: 5
- }
- };
- stubs.setPageStub = sandbox.stub(docBase, 'setPage');
- stubs.hidePageNumInputStub = sandbox.stub(docBase, 'hidePageNumInput');
- });
-
- it('should hide the page number input and set the page if given valid input', () => {
- docBase.pageNumInputBlurHandler(docBase.event);
- expect(stubs.setPageStub).to.be.calledWith(docBase.event.target.value);
- expect(stubs.hidePageNumInputStub).to.be.called;
- });
-
- it('should hide the page number input but not set the page if given invalid input', () => {
- docBase.event.target.value = 'not a number';
-
- docBase.pageNumInputBlurHandler(docBase.event);
- expect(stubs.setPageStub).to.not.be.called;
- expect(stubs.hidePageNumInputStub).to.be.called;
- });
- });
-
- describe('pageNumInputKeydownHandler()', () => {
- beforeEach(() => {
- docBase.event = {
- key: 'Enter',
- stopPropagation: sandbox.stub(),
- preventDefault: sandbox.stub(),
- target: {
- blur: sandbox.stub()
- }
- };
- stubs.browser = sandbox.stub(Browser, 'getName').returns('Explorer');
- stubs.focus = sandbox.stub(docBase.docEl, 'focus');
- stubs.hidePageNumInput = sandbox.stub(docBase, 'hidePageNumInput');
- });
-
- it('should focus the doc element if IE and stop default actions on \'enter\'', () => {
- docBase.pageNumInputKeydownHandler(docBase.event);
- expect(stubs.browser).to.be.called;
- expect(stubs.focus).to.be.called;
- expect(docBase.event.stopPropagation).to.be.called;
- expect(docBase.event.preventDefault).to.be.called;
- });
-
- it('should blur if not IE and stop default actions on \'enter\'', () => {
- stubs.browser.returns('Chrome');
-
- docBase.pageNumInputKeydownHandler(docBase.event);
- expect(stubs.browser).to.be.called;
- expect(docBase.event.target.blur).to.be.called;
- expect(docBase.event.stopPropagation).to.be.called;
- expect(docBase.event.preventDefault).to.be.called;
- });
-
- it('should hide the page number input, focus the document, and stop default actions on \'Esc\'', () => {
- docBase.event.key = 'Esc';
-
- docBase.pageNumInputKeydownHandler(docBase.event);
- expect(stubs.hidePageNumInput).to.be.called;
- expect(stubs.focus).to.be.called;
- expect(docBase.event.stopPropagation).to.be.called;
- expect(docBase.event.preventDefault).to.be.called;
- });
- });
-
describe('pagesinitHandler()', () => {
beforeEach(() => {
stubs.loadUI = sandbox.stub(docBase, 'loadUI');
- stubs.checkPaginationButtons = sandbox.stub(docBase, 'checkPaginationButtons');
stubs.setPage = sandbox.stub(docBase, 'setPage');
stubs.getCachedPage = sandbox.stub(docBase, 'getCachedPage');
stubs.emit = sandbox.stub(docBase, 'emit');
stubs.setupPages = sandbox.stub(docBase, 'setupPageIds');
+
+ docBase.pageControls = {
+ checkPaginationButtons: sandbox.stub()
+ };
+ stubs.checkPaginationButtons = docBase.pageControls.checkPaginationButtons;
});
it('should load UI, check the pagination buttons, set the page, and make document scrollable', () => {
@@ -1400,6 +1160,7 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {
expect(stubs.setPage).to.be.called;
expect(docBase.docEl).to.have.class('bp-is-scrollable');
expect(stubs.setupPages).to.be.called;
+ expect(stubs.checkPaginationButtons).to.be.called;
});
it('should broadcast that the preview is loaded if it hasn\'t already', () => {
@@ -1447,7 +1208,6 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {
describe('pagechangeHandler()', () => {
beforeEach(() => {
- stubs.updateCurrentPage = sandbox.stub(docBase, 'updateCurrentPage');
stubs.cachePage = sandbox.stub(docBase, 'cachePage');
stubs.emit = sandbox.stub(docBase, 'emit');
docBase.event = {
@@ -1456,6 +1216,10 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {
docBase.pdfViewer = {
pageCount: 1
};
+ docBase.pageControls = {
+ updateCurrentPage: sandbox.stub()
+ };
+ stubs.updateCurrentPage = docBase.pageControls.updateCurrentPage;
});
it('should emit the pagefocus event', () => {
diff --git a/src/lib/viewers/doc/__tests__/DocumentViewer-test.js b/src/lib/viewers/doc/__tests__/DocumentViewer-test.js
index d49bb2df3..a457a7ae0 100644
--- a/src/lib/viewers/doc/__tests__/DocumentViewer-test.js
+++ b/src/lib/viewers/doc/__tests__/DocumentViewer-test.js
@@ -153,6 +153,18 @@ describe('lib/viewers/doc/DocumentViewer', () => {
});
describe('bindControlListeners()', () => {
+ beforeEach(() => {
+ doc.pdfViewer = {
+ pagesCount: 4,
+ cleanup: sandbox.stub()
+ };
+
+ doc.pageControls = {
+ init: sandbox.stub(),
+ addListener: sandbox.stub()
+ };
+ });
+
it('should add the correct controls', () => {
doc.bindControlListeners();
expect(doc.controls.add).to.be.calledWith(
@@ -162,20 +174,10 @@ describe('lib/viewers/doc/DocumentViewer', () => {
ICON_ZOOM_OUT
);
expect(doc.controls.add).to.be.calledWith(__('zoom_in'), doc.zoomIn, 'bp-doc-zoom-in-icon', ICON_ZOOM_IN);
- expect(doc.controls.add).to.be.calledWith(
- __('previous_page'),
- doc.previousPage,
- 'bp-doc-previous-page-icon bp-previous-page',
- ICON_DROP_UP
- );
- expect(doc.controls.add).to.be.calledWith(__('enter_page_num'), doc.showPageNumInput, 'bp-doc-page-num');
- expect(doc.controls.add).to.be.calledWith(
- __('next_page'),
- doc.nextPage,
- 'bp-doc-next-page-icon bp-next-page',
- ICON_DROP_DOWN
- );
+ expect(doc.pageControls.init).to.be.called;
+ expect(doc.pageControls.addListener).to.be.calledWith('setpage', sinon.match.func);
+
expect(doc.controls.add).to.be.calledWith(
__('enter_fullscreen'),
doc.toggleFullscreen,
diff --git a/src/lib/viewers/doc/__tests__/PresentationViewer-test.js b/src/lib/viewers/doc/__tests__/PresentationViewer-test.js
index e90ad2e7f..8a572a2d8 100644
--- a/src/lib/viewers/doc/__tests__/PresentationViewer-test.js
+++ b/src/lib/viewers/doc/__tests__/PresentationViewer-test.js
@@ -299,7 +299,19 @@ describe('lib/viewers/doc/PresentationViewer', () => {
});
describe('bindControlListeners()', () => {
- it('should ', () => {
+ beforeEach(() => {
+ presentation.pdfViewer = {
+ pagesCount: 4,
+ cleanup: sandbox.stub()
+ };
+
+ presentation.pageControls = {
+ init: sandbox.stub(),
+ addListener: sandbox.stub()
+ };
+ });
+
+ it('should add the correct controls', () => {
presentation.bindControlListeners();
expect(presentation.controls.add).to.be.calledWith(
__('zoom_out'),
@@ -313,23 +325,10 @@ describe('lib/viewers/doc/PresentationViewer', () => {
'bp-enter-zoom-in-icon',
ICON_ZOOM_IN
);
- expect(presentation.controls.add).to.be.calledWith(
- __('previous_page'),
- presentation.previousPage,
- 'bp-presentation-previous-page-icon bp-previous-page',
- ICON_DROP_UP
- );
- expect(presentation.controls.add).to.be.calledWith(
- __('enter_page_num'),
- presentation.showPageNumInput,
- 'bp-doc-page-num'
- );
- expect(presentation.controls.add).to.be.calledWith(
- __('next_page'),
- presentation.nextPage,
- 'bp-presentation-next-page-icon bp-next-page',
- ICON_DROP_DOWN
- );
+
+ expect(presentation.pageControls.init).to.be.called;
+ expect(presentation.pageControls.addListener).to.be.calledWith('setpage', sinon.match.func);
+
expect(presentation.controls.add).to.be.calledWith(
__('enter_fullscreen'),
presentation.toggleFullscreen,
diff --git a/src/lib/viewers/doc/_docBase.scss b/src/lib/viewers/doc/_docBase.scss
index 563fda7e5..54c092cdc 100644
--- a/src/lib/viewers/doc/_docBase.scss
+++ b/src/lib/viewers/doc/_docBase.scss
@@ -80,73 +80,6 @@
}
}
-.bp-controls {
- // Page num input CSS
- .bp-doc-page-num {
- min-width: 48px;
- width: auto; // Let page num expand as needed
-
- span {
- display: inline;
- font-size: 14px;
- }
- }
-
- .bp-doc-page-num-wrapper {
- background-color: #444;
- border-radius: 3px;
- margin: 5px;
- padding: 7px 5px;
- }
-
- /* stylelint-disable property-no-vendor-prefix */
- // Removes the spinner for number type inputs in Webkit browsers
- input::-webkit-outer-spin-button,
- input::-webkit-inner-spin-button {
- -webkit-appearance: none;
- }
-
- // Removes the spinner for number type inputs in Firefox
- input[type=number] {
- -moz-appearance: textfield;
- }
-
- /* stylelint-enable property-no-vendor-prefix */
-
- .bp-doc-page-num-input {
- font-size: 14px;
- margin: 0 auto;
- position: absolute;
- text-align: center;
- visibility: hidden;
- width: 44px; // hard-coded to solve layout issues
- }
-
- &.show-page-number-input {
- .bp-doc-page-num-wrapper {
- background-color: transparent;
- border: none;
- padding: 0;
- }
-
- .bp-doc-page-num {
- opacity: 1;
- }
-
- .bp-doc-current-page,
- .bp-doc-page-num-divider,
- .bp-doc-total-pages {
- display: none;
- }
-
- .bp-doc-page-num-input {
- display: inline-block;
- position: static;
- visibility: visible;
- }
- }
-}
-
.bp-print-notification {
display: none;
font-size: 24px;
diff --git a/src/lib/viewers/doc/pageNumButtonContent.html b/src/lib/viewers/doc/pageNumButtonContent.html
deleted file mode 100644
index d6ae11d11..000000000
--- a/src/lib/viewers/doc/pageNumButtonContent.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
- 1
-
- /
- 1
-
diff --git a/src/lib/viewers/image/ImageBaseViewer.js b/src/lib/viewers/image/ImageBaseViewer.js
index bd411cd3f..13632bb54 100644
--- a/src/lib/viewers/image/ImageBaseViewer.js
+++ b/src/lib/viewers/image/ImageBaseViewer.js
@@ -43,8 +43,8 @@ class ImageBaseViewer extends BaseViewer {
return;
}
- this.zoom();
this.loadUI();
+ this.zoom();
this.imageEl.classList.remove(CLASS_INVISIBLE);
this.loaded = true;
@@ -164,6 +164,20 @@ class ImageBaseViewer extends BaseViewer {
*/
loadUI() {
this.controls = new Controls(this.containerEl);
+ this.bindControlListeners();
+ }
+
+ //--------------------------------------------------------------------------
+ // Event Listeners
+ //--------------------------------------------------------------------------
+
+ /**
+ * Bind event listeners for document controls
+ *
+ * @private
+ * @return {void}
+ */
+ bindControlListeners() {
this.controls.add(__('zoom_out'), this.zoomOut, 'bp-image-zoom-out-icon', ICON_ZOOM_OUT);
this.controls.add(__('zoom_in'), this.zoomIn, 'bp-image-zoom-in-icon', ICON_ZOOM_IN);
}
diff --git a/src/lib/viewers/image/MultiImageViewer.js b/src/lib/viewers/image/MultiImageViewer.js
index 33a916d26..741619a09 100644
--- a/src/lib/viewers/image/MultiImageViewer.js
+++ b/src/lib/viewers/image/MultiImageViewer.js
@@ -1,7 +1,7 @@
import autobind from 'autobind-decorator';
import ImageBaseViewer from './ImageBaseViewer';
+import PageControls from '../../PageControls';
import './MultiImage.scss';
-
import { ICON_FILE_IMAGE, ICON_FULLSCREEN_IN, ICON_FULLSCREEN_OUT } from '../../icons/icons';
import { CLASS_INVISIBLE } from '../../constants';
@@ -29,6 +29,9 @@ class MultiImageViewer extends ImageBaseViewer {
this.singleImageEls = [this.imageEl.appendChild(document.createElement('img'))];
this.loadTimeout = 60000;
+
+ // Defaults the current page number to 1
+ this.currentPageNumber = 1;
}
/**
@@ -68,6 +71,8 @@ class MultiImageViewer extends ImageBaseViewer {
this.imageUrls = this.constructImageUrls(template);
this.imageUrls.forEach((imageUrl, index) => this.setupImageEls(imageUrl, index));
+
+ this.wrapperEl.addEventListener('scroll', this.scrollHandler, true);
})
.catch(this.handleAssetError);
}
@@ -82,11 +87,11 @@ class MultiImageViewer extends ImageBaseViewer {
const { viewer, representation } = this.options;
const metadata = representation.metadata;
const asset = viewer.ASSET;
+ this.pagesCount = metadata.pages;
const urlBase = this.createContentUrlWithAuthParams(template, asset);
-
const urls = [];
- for (let pageNum = 1; pageNum <= metadata.pages; pageNum++) {
+ for (let pageNum = 1; pageNum <= this.pagesCount; pageNum++) {
urls.push(urlBase.replace('{page}', pageNum));
}
@@ -166,6 +171,9 @@ class MultiImageViewer extends ImageBaseViewer {
// Give the browser some time to render before updating pannability
setTimeout(this.updatePannability, ZOOM_UPDATE_PAN_DELAY);
+
+ // Set current page to previously opened page or first page
+ this.setPage(this.currentPageNumber);
}
/**
@@ -186,6 +194,22 @@ class MultiImageViewer extends ImageBaseViewer {
*/
loadUI() {
super.loadUI();
+ this.pageControls.checkPaginationButtons(this.currentPageNumber, this.pagesCount);
+ }
+
+ /**
+ * Binds listeners for document controls. Overridden.
+ *
+ * @protected
+ * @return {void}
+ */
+ bindControlListeners() {
+ super.bindControlListeners();
+
+ this.pageControls = new PageControls(this.controls, this.previousPage, this.nextPage);
+ this.pageControls.init(this.pagesCount);
+ this.pageControls.addListener('setpage', this.setPage);
+
this.controls.add(
__('enter_fullscreen'),
this.toggleFullscreen,
@@ -222,6 +246,130 @@ class MultiImageViewer extends ImageBaseViewer {
this.singleImageEls[index].removeEventListener('error', this.errorHandler);
}
+
+ /**
+ * Go to previous page
+ *
+ * @return {void}
+ */
+ previousPage() {
+ this.setPage(this.currentPageNumber - 1);
+ }
+
+ /**
+ * Go to next page
+ *
+ * @return {void}
+ */
+ nextPage() {
+ this.setPage(this.currentPageNumber + 1);
+ }
+
+ /**
+ * Go to specified page
+ *
+ * @param {number} pageNum - Page to navigate to
+ * @return {void}
+ */
+ setPage(pageNum) {
+ if (pageNum < 1 || pageNum > this.pagesCount) {
+ return;
+ }
+
+ this.currentPageNumber = pageNum;
+ this.singleImageEls[pageNum - 1].scrollIntoView();
+ }
+
+ /**
+ * Handles scroll event in the wrapper element
+ *
+ * @private
+ * @return {void}
+ */
+ scrollHandler() {
+ if (this.scrollCheckHandler) {
+ return;
+ }
+
+ if (!this.scrollState) {
+ const currentPageEl = this.singleImageEls[this.currentPageNumber - 1];
+ this.scrollState = {
+ down: false,
+ lastY: currentPageEl.scrollTop
+ };
+ }
+
+ const imageScrollHandler = this.isSingleImageElScrolled.bind(this);
+ this.scrollCheckHandler = window.requestAnimationFrame(imageScrollHandler);
+ }
+
+ /**
+ * Updates page number if the single image has been scrolled past
+ *
+ * @private
+ * @return {void}
+ */
+ isSingleImageElScrolled() {
+ this.scrollCheckHandler = null;
+ const currentY = this.wrapperEl.scrollTop;
+ const lastY = this.scrollState.lastY;
+
+ if (currentY !== lastY) {
+ this.scrollState.isScrollingDown = currentY > lastY;
+ }
+ this.scrollState.lastY = currentY;
+ this.updatePageChange();
+ }
+
+ /**
+ * Updates page number in the page controls
+ *
+ * @private
+ * @param {number} pageNum - Page just navigated to
+ * @return {void}
+ */
+ pagechangeHandler(pageNum) {
+ this.currentPageNumber = pageNum;
+ this.pageControls.updateCurrentPage(pageNum);
+ this.emit('pagefocus', this.currentPageNumber);
+ }
+
+ /**
+ * Update the page number based on scroll direction. Only increment if
+ * wrapper is scrolled down past at least half of the current page element.
+ * Only decrement page if wrapper is scrolled up past at least half of the
+ * previous page element
+ *
+ * @private
+ * @return {void}
+ */
+ updatePageChange() {
+ let pageNum = this.currentPageNumber;
+ const currentPageEl = this.singleImageEls[this.currentPageNumber - 1];
+ const wrapperScrollOffset = this.scrollState.lastY;
+ const currentPageMiddleY = currentPageEl.offsetTop + currentPageEl.clientHeight / 2;
+ const isScrolledToBottom = wrapperScrollOffset + this.wrapperEl.clientHeight >= this.wrapperEl.scrollHeight;
+
+ if (
+ this.scrollState.isScrollingDown &&
+ currentPageEl.nextSibling &&
+ (wrapperScrollOffset > currentPageMiddleY || isScrolledToBottom)
+ ) {
+ // Increment page
+ const nextPage = currentPageEl.nextSibling;
+ pageNum = parseInt(nextPage.dataset.pageNumber, 10);
+ } else if (!this.scrollState.isScrollingDown && currentPageEl.previousSibling) {
+ const prevPage = currentPageEl.previousSibling;
+ const prevPageMiddleY = prevPage.offsetTop + prevPage.clientHeight / 2;
+
+ // Decrement page
+ if (prevPageMiddleY > wrapperScrollOffset) {
+ pageNum = parseInt(prevPage.dataset.pageNumber, 10);
+ }
+ }
+
+ this.pagechangeHandler(pageNum);
+ }
}
export default MultiImageViewer;
diff --git a/src/lib/viewers/image/README.md b/src/lib/viewers/image/README.md
index d6547dedd..b2190b777 100755
--- a/src/lib/viewers/image/README.md
+++ b/src/lib/viewers/image/README.md
@@ -67,6 +67,7 @@ At the default zoom level, clicking on the image will zoom in once. When zoomed
### Controls:
* Zoom In
* Zoom Out
+* Set Page: either with the up and down arrows, or by clicking the page number and entering text
* Fullscreen: can be exited with the escape key
## Supported File Extensions
@@ -85,6 +86,7 @@ The image viewer fires the following events
| reload | The preview reloads ||
| resize | The preview resizes | 1. {number} **height**: window height 2. {number} **width**: window width |
| zoom | The preview zooms in or out | 1. {number} **zoom**: new zoom value 2. {boolean} **canZoomIn**: true if the viewer can zoom in more 3. {boolean} **canZoomOut**: true if the viewer can zoom out more |
+| pagefocus | A page is visible | {number} page number of focused page |
| pan | The preview is panning ||
| panstart | Panning starts ||
| panend | Panning ends ||
@@ -97,5 +99,8 @@ The following methods are available for the multi-page image viewer.
| Method Name | Explanation | Method Parameters |
| --- | --- | --- |
| zoom | Zooms the image | {string} 'in', 'out', or 'reset' |
+| previousPage | Navigates to the previous page ||
+| nextPage | Navigates to the next page ||
+| setPage | Navigates to a given page | {number} page number |
| print | Prints the image ||
| toggleFullscreen | Toggles fullscreen mode ||
diff --git a/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js b/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js
index e0954091e..65968a11e 100644
--- a/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js
+++ b/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js
@@ -3,6 +3,7 @@ import ImageBaseViewer from '../ImageBaseViewer';
import BaseViewer from '../../BaseViewer';
import Browser from '../../../Browser';
import fullscreen from '../../../Fullscreen';
+import { ICON_ZOOM_IN, ICON_ZOOM_OUT } from '../../../icons/icons';
const CSS_CLASS_PANNING = 'panning';
const CSS_CLASS_ZOOMABLE = 'zoomable';
@@ -209,10 +210,28 @@ describe('lib/viewers/image/ImageBaseViewer', () => {
describe('loadUI()', () => {
it('should create controls and add control buttons for zoom', () => {
+ sandbox.stub(imageBase, 'bindControlListeners');
imageBase.loadUI();
expect(imageBase.controls).to.not.be.undefined;
- expect(imageBase.controls.buttonRefs.length).to.equal(2);
+ expect(imageBase.bindControlListeners).to.be.called;
+ });
+ });
+
+ describe('bindControlListeners()', () => {
+ it('should add the correct controls', () => {
+ imageBase.controls = {
+ add: sandbox.stub()
+ };
+
+ imageBase.bindControlListeners();
+ expect(imageBase.controls.add).to.be.calledWith(
+ __('zoom_out'),
+ imageBase.zoomOut,
+ 'bp-image-zoom-out-icon',
+ ICON_ZOOM_OUT
+ );
+ expect(imageBase.controls.add).to.be.calledWith(__('zoom_in'), imageBase.zoomIn, 'bp-image-zoom-in-icon', ICON_ZOOM_IN);
});
});
diff --git a/src/lib/viewers/image/__tests__/MultiImageViewer-test.js b/src/lib/viewers/image/__tests__/MultiImageViewer-test.js
index ba0d3dfd8..3889e756c 100644
--- a/src/lib/viewers/image/__tests__/MultiImageViewer-test.js
+++ b/src/lib/viewers/image/__tests__/MultiImageViewer-test.js
@@ -46,6 +46,15 @@ describe('lib/viewers/image/MultiImageViewer', () => {
}
};
+ stubs.singleImageEl = {
+ src: undefined,
+ setAttribute: sandbox.stub(),
+ classList: {
+ add: sandbox.stub()
+ },
+ scrollIntoView: sandbox.stub()
+ };
+
multiImage = new MultiImageViewer(options);
Object.defineProperty(BaseViewer.prototype, 'setup', { value: sandbox.stub() });
@@ -102,6 +111,10 @@ describe('lib/viewers/image/MultiImageViewer', () => {
stubs.bindDOMListeners = sandbox.stub(multiImage, 'bindDOMListeners');
stubs.bindImageListeners = sandbox.stub(multiImage, 'bindImageListeners');
stubs.setupImageEls = sandbox.stub(multiImage, 'setupImageEls');
+ multiImage.wrapperEl = {
+ addEventListener: sandbox.stub()
+ };
+ stubs.addWrapperListener = multiImage.wrapperEl.addEventListener;
});
it('should create the image urls', () => {
@@ -120,6 +133,7 @@ describe('lib/viewers/image/MultiImageViewer', () => {
expect(stubs.bindImageListeners).to.be.called;
expect(stubs.bindDOMListeners).to.be.called;
expect(stubs.constructImageUrls).to.be.called;
+ expect(stubs.addWrapperListener).to.be.calledWith('scroll', sinon.match.func, 'true');
})
.catch(() => {});
});
@@ -138,13 +152,6 @@ describe('lib/viewers/image/MultiImageViewer', () => {
beforeEach(() => {
multiImage.setup();
stubs.bindImageListeners = sandbox.stub(multiImage, 'bindImageListeners');
- stubs.singleImageEl = {
- src: undefined,
- setAttribute: sandbox.stub(),
- classList: {
- add: sandbox.stub()
- }
- };
});
it('should set the single image el and error handler if it is not the first image', () => {
@@ -252,6 +259,7 @@ describe('lib/viewers/image/MultiImageViewer', () => {
beforeEach(() => {
stubs.zoomEmit = sandbox.stub(multiImage, 'emit');
stubs.setScale = sandbox.stub(multiImage, 'setScale');
+ stubs.scroll = sandbox.stub(multiImage, 'setPage');
stubs.updatePannability = sandbox.stub(multiImage, 'updatePannability');
multiImage.setup();
});
@@ -289,7 +297,7 @@ describe('lib/viewers/image/MultiImageViewer', () => {
multiImage.loadUI();
expect(multiImage.controls).to.not.be.undefined;
- expect(multiImage.controls.buttonRefs.length).to.equal(4);
+ expect(multiImage.controls.buttonRefs.length).to.equal(7);
});
});
@@ -356,4 +364,23 @@ describe('lib/viewers/image/MultiImageViewer', () => {
expect(multiImage.emit).to.be.calledWith('scale', { scale: 0.5 });
});
});
+
+ describe('setPage()', () => {
+ it('should scroll to the current page', () => {
+ multiImage.singleImageEls = {
+ 1: stubs.singleImageEl,
+ 2: stubs.singleImageEl,
+ 3: stubs.singleImageEl
+ };
+ sandbox.stub(multiImage, 'emit');
+
+ multiImage.setPage(2);
+ expect(multiImage.currentPageNumber).equals(2);
+ });
+
+ it('should not do anything if setting an invalid page', () => {
+ multiImage.setPage(-1);
+ expect(multiImage.currentPageNumber).to.be.undefined;
+ });
+ });
});