diff --git a/src/lib/Controls.js b/src/lib/Controls.js index f84cf238a..9b6915473 100644 --- a/src/lib/Controls.js +++ b/src/lib/Controls.js @@ -1,6 +1,7 @@ import throttle from 'lodash.throttle'; import Browser from './Browser'; import { CLASS_HIDDEN } from './constants'; +import fullscreen from './Fullscreen'; const SHOW_PREVIEW_CONTROLS_CLASS = 'box-show-preview-controls'; const CONTROLS_BUTTON_CLASS = 'bp-controls-btn'; @@ -24,6 +25,18 @@ class Controls { /** @property {boolean} - Whether browser supports touch */ hasTouch = Browser.hasTouch(); + /** @property {HTMLElement} - Page num input element */ + pageNumInputEl; + + /** @property {String} - HTML template for page num element */ + pageNumTemplate = ` +
+ 1 + +  /  + 1 +
`.replace(/>\s*<'); + /** * [constructor] * @@ -239,6 +252,69 @@ class Controls { isPageNumFocused() { return document.activeElement.classList.contains(CONTROLS_PAGE_NUM_INPUT_CLASS); } + + /** + * Initializes page number selector. + * + * @private + * @param {number} pagesCount - Total number of page + * @return {void} + */ + initPageNumEl(pagesCount) { + const pageNumEl = this.controlsEl.querySelector('.bp-page-num'); + + // Update total page number + const totalPageEl = pageNumEl.querySelector('.bp-total-pages'); + totalPageEl.textContent = pagesCount; + + // Keep reference to page number input and current page elements + this.pageNumInputEl = pageNumEl.querySelector('.bp-page-num-input'); + this.pageNumInputEl.setAttribute('max', pagesCount); + + this.currentPageEl = pageNumEl.querySelector('.bp-current-page'); + } + + /** + * Disables or enables previous/next pagination buttons depending on + * current page number. + * + * @return {void} + */ + checkPaginationButtons(currentPageNum, pagesCount) { + const pageNumButtonEl = this.containerEl.querySelector('.bp-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 === pagesCount) { + nextPageButtonEl.disabled = true; + } else { + nextPageButtonEl.disabled = false; + } + } + } } export default 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/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js index 17c5cc274..06dea4622 100644 --- a/src/lib/viewers/doc/DocBaseViewer.js +++ b/src/lib/viewers/doc/DocBaseViewer.js @@ -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. * @@ -761,7 +697,7 @@ class DocBaseViewer extends BaseViewer { loadUI() { this.controls = new Controls(this.containerEl); this.bindControlListeners(); - this.initPageNumEl(); + this.controls.initPageNumEl(this.pdfViewer.pagesCount); } /** @@ -774,13 +710,13 @@ class DocBaseViewer extends BaseViewer { // 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(); + this.controls.pageNumInputEl.value = this.controls.currentPageEl.textContent; + this.controls.pageNumInputEl.focus(); + this.controls.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); + this.controls.pageNumInputEl.addEventListener('blur', this.pageNumInputBlurHandler); + this.controls.pageNumInputEl.addEventListener('keydown', this.pageNumInputKeydownHandler); } /** @@ -791,8 +727,8 @@ class DocBaseViewer extends BaseViewer { */ hidePageNumInput() { this.controls.controlsEl.classList.remove(SHOW_PAGE_NUM_INPUT_CLASS); - this.pageNumInputEl.removeEventListener('blur', this.pageNumInputBlurHandler); - this.pageNumInputEl.removeEventListener('keydown', this.pageNumInputKeydownHandler); + this.controls.pageNumInputEl.removeEventListener('blur', this.pageNumInputBlurHandler); + this.controls.pageNumInputEl.removeEventListener('keydown', this.pageNumInputKeydownHandler); } /** @@ -813,15 +749,15 @@ class DocBaseViewer extends BaseViewer { truePageNum = 1; } - if (this.pageNumInputEl) { - this.pageNumInputEl.value = truePageNum; + if (this.controls.pageNumInputEl) { + this.controls.pageNumInputEl.value = truePageNum; } - if (this.currentPageEl) { - this.currentPageEl.textContent = truePageNum; + if (this.controls.currentPageEl) { + this.controls.currentPageEl.textContent = truePageNum; } - this.checkPaginationButtons(); + this.controls.checkPaginationButtons(this.pdfViewer.currentPageNumber, this.pdfViewer.pagesCount); } //-------------------------------------------------------------------------- @@ -969,7 +905,7 @@ class DocBaseViewer extends BaseViewer { this.pdfViewer.currentScaleValue = 'auto'; this.loadUI(); - this.checkPaginationButtons(); + this.controls.checkPaginationButtons(this.pdfViewer.currentPageNumber, this.pdfViewer.pagesCount); // Set current page to previously opened page or first page this.setPage(this.getCachedPage()); diff --git a/src/lib/viewers/doc/DocumentViewer.js b/src/lib/viewers/doc/DocumentViewer.js index c2cce2bcb..382831005 100644 --- a/src/lib/viewers/doc/DocumentViewer.js +++ b/src/lib/viewers/doc/DocumentViewer.js @@ -1,5 +1,4 @@ import autobind from 'autobind-decorator'; -import pageNumTemplate from './pageNumButtonContent.html'; import DocBaseViewer from './DocBaseViewer'; import DocPreloader from './DocPreloader'; import fullscreen from '../../Fullscreen'; @@ -109,9 +108,7 @@ class DocumentViewer extends DocBaseViewer { '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(__('enter_page_num'), this.showPageNumInput, 'bp-page-num', this.controls.pageNumTemplate); this.controls.add(__('next_page'), this.nextPage, 'bp-doc-next-page-icon bp-next-page', ICON_DROP_DOWN); this.controls.add( diff --git a/src/lib/viewers/doc/PresentationViewer.js b/src/lib/viewers/doc/PresentationViewer.js index 2e014d479..98a4d59c8 100644 --- a/src/lib/viewers/doc/PresentationViewer.js +++ b/src/lib/viewers/doc/PresentationViewer.js @@ -1,6 +1,5 @@ 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'; @@ -199,9 +198,7 @@ class PresentationViewer extends DocBaseViewer { '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(__('enter_page_num'), this.showPageNumInput, 'bp-page-num', this.controls.pageNumTemplate); this.controls.add( __('next_page'), 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..6cf0ffac3 100644 --- a/src/lib/viewers/image/ImageBaseViewer.js +++ b/src/lib/viewers/image/ImageBaseViewer.js @@ -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 004c8230d..c9867b8ed 100644 --- a/src/lib/viewers/image/MultiImageViewer.js +++ b/src/lib/viewers/image/MultiImageViewer.js @@ -1,13 +1,21 @@ import autobind from 'autobind-decorator'; import ImageBaseViewer from './ImageBaseViewer'; +import Browser from '../../Browser'; import './MultiImage.scss'; - -import { ICON_FILE_IMAGE, ICON_FULLSCREEN_IN, ICON_FULLSCREEN_OUT } from '../../icons/icons'; +import { + ICON_FILE_IMAGE, + ICON_FULLSCREEN_IN, + ICON_FULLSCREEN_OUT, + ICON_DROP_DOWN, + ICON_DROP_UP +} from '../../icons/icons'; import { CLASS_INVISIBLE } from '../../constants'; +import { decodeKeydown } from '../../util'; const PADDING_BUFFER = 100; const CSS_CLASS_IMAGE = 'bp-images'; const CSS_CLASS_IMAGE_WRAPPER = 'bp-images-wrapper'; +const SHOW_PAGE_NUM_INPUT_CLASS = 'show-page-number-input'; const ZOOM_UPDATE_PAN_DELAY = 50; @autobind @@ -85,11 +93,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)); } @@ -192,6 +200,28 @@ class MultiImageViewer extends ImageBaseViewer { */ loadUI() { super.loadUI(); + this.controls.initPageNumEl(this.pagesCount); + this.controls.checkPaginationButtons(this.currentPageNumber, this.pagesCount); + } + + /** + * Binds listeners for document controls. Overridden. + * + * @protected + * @return {void} + */ + bindControlListeners() { + super.bindControlListeners(); + + this.controls.add( + __('previous_page'), + this.previousPage, + 'bp-image-previous-page-icon bp-previous-page', + ICON_DROP_UP + ); + this.controls.add(__('enter_page_num'), this.showPageNumInput, 'bp-page-num', this.controls.pageNumTemplate); + this.controls.add(__('next_page'), this.nextPage, 'bp-image-next-page-icon bp-next-page', ICON_DROP_DOWN); + this.controls.add( __('enter_fullscreen'), this.toggleFullscreen, @@ -236,12 +266,155 @@ class MultiImageViewer extends ImageBaseViewer { * @return {void} */ setPage(pageNum) { - if (pageNum <= 0 || pageNum > this.singleImageEls.length) { + if (pageNum <= 0 || pageNum > this.pagesCount) { return; } this.currentPageNumber = pageNum; this.singleImageEls[pageNum].scrollIntoView(); + this.updateCurrentPage(pageNum); + + this.emit('pagefocus', pageNum); + } + + /** + * 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.pagesCount; + + // refine the page number to fall within bounds + if (pageNum > pagesCount) { + truePageNum = pagesCount; + } else if (pageNum < 1) { + truePageNum = 1; + } + + if (!this.controls) { + return; + } + + if (this.controls.pageNumInputEl) { + this.controls.pageNumInputEl.value = truePageNum; + } + + if (this.controls.currentPageEl) { + this.controls.currentPageEl.textContent = truePageNum; + } + + this.controls.checkPaginationButtons(this.currentPageNumber, this.pagesCount); + } + + /** + * 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.controls.pageNumInputEl.value = this.controls.currentPageEl.textContent; + this.controls.pageNumInputEl.focus(); + this.controls.pageNumInputEl.select(); + + // finish input when input is blurred or enter key is pressed + this.controls.pageNumInputEl.addEventListener('blur', this.pageNumInputBlurHandler); + this.controls.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.controls.pageNumInputEl.removeEventListener('blur', this.pageNumInputBlurHandler); + this.controls.pageNumInputEl.removeEventListener('keydown', this.pageNumInputKeydownHandler); + } + + /** + * 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.singleImageEls[this.currentPageNumber].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.singleImageEls[this.currentPageNumber].focus(); + + event.stopPropagation(); + event.preventDefault(); + break; + + default: + break; + } + } + + /** + * Go to previous page + * + * @return {void} + */ + previousPage() { + this.setPage(this.currentPageNumber - 1); + } + + /** + * Go to next page + * + * @return {void} + */ + nextPage() { + this.setPage(this.currentPageNumber + 1); } }