/* 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) => { if (evt.button !== 0) { return; } // 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, };