pdf.js/web/toolbar.js
Jonas Jenwald ca244d9bca Introduce a viewer constant for document.documentElement.style
Over time, as we've been introducing JavaScript code to modify CSS variables, we've been adding shorthand properties to various classes to reduce unnecessary repetition when accessing the document-styles.
Rather than repeating this in multiple places, it seems overall simpler to just introduce a constant and re-use that throughout the viewer instead.
2022-05-25 09:55:54 +02:00

288 lines
9.2 KiB
JavaScript

/* Copyright 2016 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 {
animationStarted,
DEFAULT_SCALE,
DEFAULT_SCALE_VALUE,
docStyle,
MAX_SCALE,
MIN_SCALE,
noContextMenuHandler,
} from "./ui_utils.js";
const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
/**
* @typedef {Object} ToolbarOptions
* @property {HTMLDivElement} container - Container for the secondary toolbar.
* @property {HTMLSpanElement} numPages - Label that contains number of pages.
* @property {HTMLInputElement} pageNumber - Control for display and user input
* of the current page number.
* @property {HTMLSelectElement} scaleSelect - Scale selection control.
* Its width is adjusted, when necessary, on UI localization.
* @property {HTMLOptionElement} customScaleOption - The item used to display
* a non-predefined scale.
* @property {HTMLButtonElement} previous - Button to go to the previous page.
* @property {HTMLButtonElement} next - Button to go to the next page.
* @property {HTMLButtonElement} zoomIn - Button to zoom in the pages.
* @property {HTMLButtonElement} zoomOut - Button to zoom out the pages.
* @property {HTMLButtonElement} viewFind - Button to open find bar.
* @property {HTMLButtonElement} openFile - Button to open a new document.
* @property {HTMLButtonElement} presentationModeButton - Button to switch to
* presentation mode.
* @property {HTMLButtonElement} download - Button to download the document.
* @property {HTMLAnchorElement} viewBookmark - Button to obtain a bookmark link
* to the current location in the document.
*/
class Toolbar {
/**
* @param {ToolbarOptions} options
* @param {EventBus} eventBus
* @param {IL10n} l10n - Localization service.
*/
constructor(options, eventBus, l10n) {
this.toolbar = options.container;
this.eventBus = eventBus;
this.l10n = l10n;
this.buttons = [
{ element: options.previous, eventName: "previouspage" },
{ element: options.next, eventName: "nextpage" },
{ element: options.zoomIn, eventName: "zoomin" },
{ element: options.zoomOut, eventName: "zoomout" },
{ element: options.print, eventName: "print" },
{
element: options.presentationModeButton,
eventName: "presentationmode",
},
{ element: options.download, eventName: "download" },
{ element: options.viewBookmark, eventName: null },
];
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
this.buttons.push({ element: options.openFile, eventName: "openfile" });
}
this.items = {
numPages: options.numPages,
pageNumber: options.pageNumber,
scaleSelect: options.scaleSelect,
customScaleOption: options.customScaleOption,
previous: options.previous,
next: options.next,
zoomIn: options.zoomIn,
zoomOut: options.zoomOut,
};
this._wasLocalized = false;
this.reset();
// Bind the event listeners for click and various other actions.
this._bindListeners();
}
setPageNumber(pageNumber, pageLabel) {
this.pageNumber = pageNumber;
this.pageLabel = pageLabel;
this._updateUIState(false);
}
setPagesCount(pagesCount, hasPageLabels) {
this.pagesCount = pagesCount;
this.hasPageLabels = hasPageLabels;
this._updateUIState(true);
}
setPageScale(pageScaleValue, pageScale) {
this.pageScaleValue = (pageScaleValue || pageScale).toString();
this.pageScale = pageScale;
this._updateUIState(false);
}
reset() {
this.pageNumber = 0;
this.pageLabel = null;
this.hasPageLabels = false;
this.pagesCount = 0;
this.pageScaleValue = DEFAULT_SCALE_VALUE;
this.pageScale = DEFAULT_SCALE;
this._updateUIState(true);
this.updateLoadingIndicatorState();
}
_bindListeners() {
const { pageNumber, scaleSelect } = this.items;
const self = this;
// The buttons within the toolbar.
for (const { element, eventName } of this.buttons) {
element.addEventListener("click", evt => {
if (eventName !== null) {
this.eventBus.dispatch(eventName, { source: this });
}
});
}
// The non-button elements within the toolbar.
pageNumber.addEventListener("click", function () {
this.select();
});
pageNumber.addEventListener("change", function () {
self.eventBus.dispatch("pagenumberchanged", {
source: self,
value: this.value,
});
});
scaleSelect.addEventListener("change", function () {
if (this.value === "custom") {
return;
}
self.eventBus.dispatch("scalechanged", {
source: self,
value: this.value,
});
});
// Here we depend on browsers dispatching the "click" event *after* the
// "change" event, when the <select>-element changes.
scaleSelect.addEventListener("click", function (evt) {
const target = evt.target;
// Remove focus when an <option>-element was *clicked*, to improve the UX
// for mouse users (fixes bug 1300525 and issue 4923).
if (
this.value === self.pageScaleValue &&
target.tagName.toUpperCase() === "OPTION"
) {
this.blur();
}
});
// Suppress context menus for some controls.
scaleSelect.oncontextmenu = noContextMenuHandler;
this.eventBus._on("localized", () => {
this._wasLocalized = true;
this.#adjustScaleWidth();
this._updateUIState(true);
});
}
_updateUIState(resetNumPages = false) {
if (!this._wasLocalized) {
// Don't update the UI state until we localize the toolbar.
return;
}
const { pageNumber, pagesCount, pageScaleValue, pageScale, items } = this;
if (resetNumPages) {
if (this.hasPageLabels) {
items.pageNumber.type = "text";
} else {
items.pageNumber.type = "number";
this.l10n.get("of_pages", { pagesCount }).then(msg => {
items.numPages.textContent = msg;
});
}
items.pageNumber.max = pagesCount;
}
if (this.hasPageLabels) {
items.pageNumber.value = this.pageLabel;
this.l10n.get("page_of_pages", { pageNumber, pagesCount }).then(msg => {
items.numPages.textContent = msg;
});
} else {
items.pageNumber.value = pageNumber;
}
items.previous.disabled = pageNumber <= 1;
items.next.disabled = pageNumber >= pagesCount;
items.zoomOut.disabled = pageScale <= MIN_SCALE;
items.zoomIn.disabled = pageScale >= MAX_SCALE;
this.l10n
.get("page_scale_percent", { scale: Math.round(pageScale * 10000) / 100 })
.then(msg => {
let predefinedValueFound = false;
for (const option of items.scaleSelect.options) {
if (option.value !== pageScaleValue) {
option.selected = false;
continue;
}
option.selected = true;
predefinedValueFound = true;
}
if (!predefinedValueFound) {
items.customScaleOption.textContent = msg;
items.customScaleOption.selected = true;
}
});
}
updateLoadingIndicatorState(loading = false) {
const pageNumberInput = this.items.pageNumber;
pageNumberInput.classList.toggle(PAGE_NUMBER_LOADING_INDICATOR, loading);
}
/**
* Increase the width of the zoom dropdown DOM element if, and only if, it's
* too narrow to fit the *longest* of the localized strings.
*/
async #adjustScaleWidth() {
const { items, l10n } = this;
const predefinedValuesPromise = Promise.all([
l10n.get("page_scale_auto"),
l10n.get("page_scale_actual"),
l10n.get("page_scale_fit"),
l10n.get("page_scale_width"),
]);
await animationStarted;
const style = getComputedStyle(items.scaleSelect),
scaleSelectContainerWidth = parseInt(
style.getPropertyValue("--scale-select-container-width"),
10
),
scaleSelectOverflow = parseInt(
style.getPropertyValue("--scale-select-overflow"),
10
);
// The temporary canvas is used to measure text length in the DOM.
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d", { alpha: false });
ctx.font = `${style.fontSize} ${style.fontFamily}`;
let maxWidth = 0;
for (const predefinedValue of await predefinedValuesPromise) {
const { width } = ctx.measureText(predefinedValue);
if (width > maxWidth) {
maxWidth = width;
}
}
maxWidth += 2 * scaleSelectOverflow;
if (maxWidth > scaleSelectContainerWidth) {
docStyle.setProperty("--scale-select-container-width", `${maxWidth}px`);
}
// Zeroing the width and height cause Firefox to release graphics resources
// immediately, which can greatly reduce memory consumption.
canvas.width = 0;
canvas.height = 0;
}
}
export { Toolbar };