diff --git a/web/app.js b/web/app.js index f37093218..bf2226b87 100644 --- a/web/app.js +++ b/web/app.js @@ -38,6 +38,7 @@ import { PDFHistory } from './pdf_history'; import { PDFLinkService } from './pdf_link_service'; import { PDFOutlineViewer } from './pdf_outline_viewer'; import { PDFPresentationMode } from './pdf_presentation_mode'; +import { PDFSidebarResizer } from './pdf_sidebar_resizer'; import { PDFThumbnailViewer } from './pdf_thumbnail_viewer'; import { PDFViewer } from './pdf_viewer'; import { SecondaryToolbar } from './secondary_toolbar'; @@ -109,6 +110,8 @@ let PDFViewerApplication = { pdfHistory: null, /** @type {PDFSidebar} */ pdfSidebar: null, + /** @type {PDFSidebarResizer} */ + pdfSidebarResizer: null, /** @type {PDFOutlineViewer} */ pdfOutlineViewer: null, /** @type {PDFAttachmentViewer} */ @@ -419,6 +422,8 @@ let PDFViewerApplication = { this.pdfSidebar = new PDFSidebar(sidebarConfig, this.l10n); this.pdfSidebar.onToggled = this.forceRendering.bind(this); + this.pdfSidebarResizer = new PDFSidebarResizer(appConfig.sidebarResizer, + eventBus, this.l10n); resolve(undefined); }); }, @@ -1305,7 +1310,7 @@ let PDFViewerApplication = { let { eventBus, _boundEvents, } = this; _boundEvents.windowResize = () => { - eventBus.dispatch('resize'); + eventBus.dispatch('resize', { source: window, }); }; _boundEvents.windowHashChange = () => { eventBus.dispatch('hashchange', { @@ -1578,7 +1583,7 @@ function webViewerInitialized() { appConfig.mainContainer.addEventListener('transitionend', function(evt) { if (evt.target === /* mainContainer */ this) { - PDFViewerApplication.eventBus.dispatch('resize'); + PDFViewerApplication.eventBus.dispatch('resize', { source: this, }); } }, true); diff --git a/web/pdf_sidebar_resizer.js b/web/pdf_sidebar_resizer.js new file mode 100644 index 000000000..4f59fa20b --- /dev/null +++ b/web/pdf_sidebar_resizer.js @@ -0,0 +1,184 @@ +/* Copyright 2017 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NullL10n } from './ui_utils'; + +const SIDEBAR_WIDTH_VAR = '--sidebar-width'; +const SIDEBAR_MIN_WIDTH = 200; // pixels +const SIDEBAR_RESIZING_CLASS = 'sidebarResizing'; + +/** + * @typedef {Object} PDFSidebarResizerOptions + * @property {HTMLDivElement} outerContainer - The outer container + * (encasing both the viewer and sidebar elements). + * @property {HTMLDivElement} resizer - The DOM element that can be dragged in + * order to adjust the width of the sidebar. + */ + +class PDFSidebarResizer { + /** + * @param {PDFSidebarResizerOptions} options + * @param {EventBus} eventBus - The application event bus. + * @param {IL10n} l10n - Localization service. + */ + constructor(options, eventBus, l10n = NullL10n) { + this.enabled = false; + this.isRTL = false; + this.sidebarOpen = false; + this.doc = document.documentElement; + this._width = null; + this._outerContainerWidth = null; + this._boundEvents = Object.create(null); + + this.outerContainer = options.outerContainer; + this.resizer = options.resizer; + this.eventBus = eventBus; + this.l10n = l10n; + + if (typeof CSS === 'undefined' || typeof CSS.supports !== 'function' || + !CSS.supports(SIDEBAR_WIDTH_VAR, `calc(-1 * ${SIDEBAR_MIN_WIDTH}px)`)) { + console.warn('PDFSidebarResizer: ' + + 'The browser does not support resizing of the sidebar.'); + return; + } + this.enabled = true; + this.resizer.classList.remove('hidden'); // Show the resizer DOM element. + + this.l10n.getDirection().then((dir) => { + this.isRTL = (dir === 'rtl'); + }); + this._addEventListeners(); + } + + /** + * returns {number} + */ + get outerContainerWidth() { + if (!this._outerContainerWidth) { + this._outerContainerWidth = this.outerContainer.clientWidth; + } + return this._outerContainerWidth; + } + + /** + * @private + * returns {boolean} Indicating if the sidebar width was updated. + */ + _updateWidth(width = 0) { + if (!this.enabled) { + return false; + } + // Prevent the sidebar from becoming too narrow, or from occupying more + // than half of the available viewer width. + const maxWidth = Math.floor(this.outerContainerWidth / 2); + if (width > maxWidth) { + width = maxWidth; + } + if (width < SIDEBAR_MIN_WIDTH) { + width = SIDEBAR_MIN_WIDTH; + } + // Only update the UI when the sidebar width did in fact change. + if (width === this._width) { + return false; + } + this._width = width; + this.doc.style.setProperty(SIDEBAR_WIDTH_VAR, `${width}px`); + return true; + } + + /** + * @private + */ + _mouseMove(evt) { + let width = evt.clientX; + // For sidebar resizing to work correctly in RTL mode, invert the width. + if (this.isRTL) { + width = this.outerContainerWidth - width; + } + this._updateWidth(width); + } + + /** + * @private + */ + _mouseUp(evt) { + // Re-enable the `transition-duration` rules when sidebar resizing ends... + this.outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS); + // ... and ensure that rendering will always be triggered. + this.eventBus.dispatch('resize', { source: this, }); + + let _boundEvents = this._boundEvents; + window.removeEventListener('mousemove', _boundEvents.mouseMove); + window.removeEventListener('mouseup', _boundEvents.mouseUp); + } + + /** + * @private + */ + _addEventListeners() { + if (!this.enabled) { + return; + } + let _boundEvents = this._boundEvents; + _boundEvents.mouseMove = this._mouseMove.bind(this); + _boundEvents.mouseUp = this._mouseUp.bind(this); + + this.resizer.addEventListener('mousedown', (evt) => { + // Disable the `transition-duration` rules when sidebar resizing begins, + // in order to improve responsiveness and to avoid visual glitches. + this.outerContainer.classList.add(SIDEBAR_RESIZING_CLASS); + + window.addEventListener('mousemove', _boundEvents.mouseMove); + window.addEventListener('mouseup', _boundEvents.mouseUp); + }); + + this.eventBus.on('sidebarviewchanged', (evt) => { + this.sidebarOpen = !!(evt && evt.view); + }); + + this.eventBus.on('resize', (evt) => { + // When the *entire* viewer is resized, such that it becomes narrower, + // ensure that the sidebar doesn't end up being too wide. + if (evt && evt.source === window) { + // Always reset the cached width when the viewer is resized. + this._outerContainerWidth = null; + + if (this._width) { + // NOTE: If the sidebar is closed, we don't need to worry about + // visual glitches nor ensure that rendering is triggered. + if (this.sidebarOpen) { + this.outerContainer.classList.add(SIDEBAR_RESIZING_CLASS); + let updated = this._updateWidth(this._width); + + Promise.resolve().then(() => { + this.outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS); + // Trigger rendering if the sidebar width changed, to avoid + // depending on the order in which 'resize' events are handled. + if (updated) { + this.eventBus.dispatch('resize', { source: this, }); + } + }); + } else { + this._updateWidth(this._width); + } + } + } + }); + } +} + +export { + PDFSidebarResizer, +}; diff --git a/web/ui_utils.js b/web/ui_utils.js index 394e1e625..dc762dfeb 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -52,6 +52,10 @@ function formatL10nValue(text, args) { * @implements {IL10n} */ let NullL10n = { + getDirection() { + return Promise.resolve('ltr'); + }, + get(property, args, fallback) { return Promise.resolve(formatL10nValue(fallback, args)); }, diff --git a/web/viewer.css b/web/viewer.css index 83e4ff0c6..1d2f4f8d7 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -15,6 +15,10 @@ @import url(pdf_viewer.css); +:root { + --sidebar-width: 200px; +} + * { padding: 0; margin: 0; @@ -145,7 +149,9 @@ select { position: absolute; top: 32px; bottom: 0; - width: 200px; + width: 200px; /* Here, and elsewhere below, keep the constant value for compatibility + with older browsers that lack support for CSS variables. */ + width: var(--sidebar-width); visibility: hidden; z-index: 100; border-top: 1px solid #333; @@ -159,17 +165,28 @@ html[dir='ltr'] #sidebarContainer { -webkit-transition-property: left; transition-property: left; left: -200px; + left: calc(-1 * var(--sidebar-width)); } html[dir='rtl'] #sidebarContainer { -webkit-transition-property: right; transition-property: right; right: -200px; + right: calc(-1 * var(--sidebar-width)); } .loadingInProgress #sidebarContainer { top: 36px; } +#outerContainer.sidebarResizing #sidebarContainer { + /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */ + -webkit-transition-duration: 0s; + transition-duration: 0s; + /* Prevent e.g. the thumbnails being selected when the sidebar is resized. */ + -webkit-user-select: none; + -moz-user-select: none; +} + #outerContainer.sidebarMoving #sidebarContainer, #outerContainer.sidebarOpen #sidebarContainer { visibility: visible; @@ -230,15 +247,23 @@ html[dir='rtl'] #viewerContainer { box-shadow: inset -1px 0 0 hsla(0,0%,100%,.05); } +#outerContainer.sidebarResizing #viewerContainer { + /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */ + -webkit-transition-duration: 0s; + transition-duration: 0s; +} + html[dir='ltr'] #outerContainer.sidebarOpen #viewerContainer { -webkit-transition-property: left; transition-property: left; left: 200px; + left: var(--sidebar-width); } html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer { -webkit-transition-property: right; transition-property: right; right: 200px; + right: var(--sidebar-width); } .toolbar { @@ -273,6 +298,21 @@ html[dir='rtl'] #toolbarSidebar { 0 0 1px hsla(0,0%,0%,.1); } +#sidebarResizer { + position: absolute; + top: 0; + bottom: 0; + width: 6px; + z-index: 200; + cursor: ew-resize; +} +html[dir='ltr'] #sidebarResizer { + right: -6px; +} +html[dir='rtl'] #sidebarResizer { + left: -6px; +} + #toolbarContainer, .findbar, .secondaryToolbar { position: relative; height: 32px; @@ -1133,9 +1173,14 @@ html[dir='rtl'] .verticalToolbarSeparator { } .thumbnail { - float: left; margin: 0 10px 5px 10px; } +html[dir='ltr'] .thumbnail { + float: left; +} +html[dir='rtl'] .thumbnail { + float: right; +} #thumbnailView > a:last-of-type > .thumbnail { margin-bottom: 10px; diff --git a/web/viewer.html b/web/viewer.html index 762de689c..b8d99d59b 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -86,6 +86,7 @@ See https://github.com/adobe-type-tools/cmap-resources +
diff --git a/web/viewer.js b/web/viewer.js index 4c2453b13..ba680ec65 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -117,6 +117,10 @@ function getViewerConfiguration() { outlineView: document.getElementById('outlineView'), attachmentsView: document.getElementById('attachmentsView'), }, + sidebarResizer: { + outerContainer: document.getElementById('outerContainer'), + resizer: document.getElementById('sidebarResizer'), + }, findBar: { bar: document.getElementById('findbar'), toggleButton: document.getElementById('viewFind'),