/* Copyright 2020 Carlos de Alfonso (https://github.com/dealfonso) 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. */ (function(exports, $) { 'use strict'; let defaults = { // Threshold to consider that a page is visible visibleThreshold: 0.5, // Number of extra pages to load (appart from the visible) extraPagesToLoad: 3, // The class used for each page (the div that wraps the content of the page) pageClass: "pdfpage", // The class used for the content of each page (the div that contains the page) contentClass: "content-wrapper", // Function called when a document has been loaded and its structure has been created onDocumentReady: () => {}, // Function called when a new page is created (it is binded to the object, and receives a jQuery object as parameter) onNewPage: (page, i) => {}, // Function called when a page is rendered onPageRender: (page, i) => {}, // Function called to obtain a page that shows an error when the document could not be loaded (returns a jQuery object) errorPage: () => { $(`
`).addClass(this.settings.pageClass).append($(``).text("could not load document")) }, // Posible zoom values to iterate over using "in" and "out" zoomValues: [ 0.25, 0.5, 0.75, 1, 1.25, 1.50, 2, 4, 8 ], // Function called when the zoom level changes (it receives the zoom level) onZoomChange: (zoomlevel) => {}, // Function called whenever the active page is changed (the active page is the one that is shown in the viewer) onActivePageChanged: (page, i) => {}, // Percentage of the container that will be filled with the page zoomFillArea: 0.95, // Function called to get the content of an empty page emptyContent: () => $(''), // The scale to which the pages are rendered (1.5 is the default value for the PDFjs viewer); a higher value will render the pages with a higher resolution // but it will consume more memory and CPU. A lower value will render the pages with a lower resolution, but they will be uglier. renderingScale: 1.5, } // Class used to help in zoom management; probably it can be moved to the main class, but it is used to group methods class Zoomer { /** * Construct the helper class * @param {PDFjsViewer} viewer - the viewer object * @param {*} options - the options object */ constructor(viewer, options = {}) { let defaults = { // The possible zoom values to iterate through using "in" and "out" zoomValues: [ 0.25, 0.5, 0.75, 1, 1.25, 1.50, 2, 4, 8 ], // The area to fill the container with the zoomed pages fillArea: 0.9, } // The current zooom value this.current = 1; // The viewer instance whose pages may be zoomed this.viewer = viewer; // The settings this.settings = $.extend(defaults, options); // Need having the zoom values in order this.settings.zoomValues = this.settings.zoomValues.sort(); } /** Translates a zoom value into a float value; possible values: * - a float value * - a string with a keyword (e.g. "width", "height", "fit", "in", "out") * @param {number} zoom - the zoom value to be translated * @return {number} The zoom value */ get(zoom = null) { // If no zoom is specified, return the current one if (zoom === null) { return this.current; } // If it is a number, return it if (parseFloat(zoom) == zoom) { return zoom; } let $activepage = this.viewer.getActivePage(); let zoomValues = []; // If it is a keyword, return the corresponding value switch(zoom) { case "in": zoom = this.current; zoomValues = this.settings.zoomValues.filter((x) => x > zoom); if (zoomValues.length > 0) { zoom = Math.min(...zoomValues); } break; case "out": zoom = this.current; zoomValues = this.settings.zoomValues.filter((x) => x < zoom); if (zoomValues.length > 0) { zoom = Math.max(...zoomValues); } break; case "fit": zoom = Math.min(this.get("width"), this.get("height")); break; case "width": zoom = this.settings.fillArea * this.viewer.$container.width() / $activepage.data("width"); break; case "height": zoom = this.settings.fillArea * this.viewer.$container.height() / $activepage.data("height"); break; default: zoom = this.current; break; } return zoom; } /** * Sets the zoom value to each page (changes both the page and the content div); relies on the data-values for the page * @param {number} zoom - the zoom value to be set */ zoomPages(zoom) { zoom = this.get(zoom); this.viewer.getPages().forEach(function(page) { let $page = page.$div; let c_width = $page.data("width"); let c_height = $page.data("height"); $page.width(c_width * zoom).height(c_height * zoom); $page.data('zoom', zoom); $page.find(`.${this.viewer.settings.contentClass}`).width(c_width * zoom).height(c_height * zoom); }.bind(this)); this.current = zoom; } } class PDFjsViewer { /** * Constructs the object, and initializes actions: * - add the scroll handler to the container * - set the first adjusting action when the page is loaded * - creates the zoom helper * @param {jQuery} $container the jQuery value that will hold the pages * @param {dictionary} options options for the viewer */ constructor($container, options = {}) { this.settings = $.extend(Object.assign({}, defaults), options); // Create the zoomer helper this._zoom = new Zoomer(this, { zoomValues: this.settings.zoomValues, fillArea: this.settings.zoomFillArea, }); // Store the container this.$container = $container; // Add a reference to this object to the container $container.get(0)._pdfjsViewer = this; // Add the event listeners this._setScrollListener(); // Initialize some variables this.pages = []; this.pdf = null; // Whether the document is ready or not this._documentReady = false; } /** * Sets the current zoom level and applies it to all the pages * @param {number} zoom the desired zoom level, which will be a value (1 equals to 100%), or the keywords 'in', 'out', 'width', 'height' or 'fit' */ setZoom(zoom) { let container = this.$container.get(0); // Get the previous zoom and scroll position let prevzoom = this._zoom.current; let prevScroll = { top: container.scrollTop, left: container.scrollLeft }; // Now zoom the pages this._zoom.zoomPages(zoom); // Update the scroll position (to match the previous one), according to the new relationship of zoom container.scrollLeft = prevScroll.left * this._zoom.current / prevzoom; container.scrollTop = prevScroll.top * this._zoom.current / prevzoom; // Force to redraw the visible pages to upgrade the resolution this._visiblePages(true); // Call the callback (if provided) if (this._documentReady) { if (typeof this.settings.onZoomChange === "function") this.settings.onZoomChange.call(this, this._zoom.current); this.$container.get(0).dispatchEvent(new CustomEvent("zoomchange", { detail: { zoom: this._zoom.current } })); } return this._zoom.current; } /** * Obtain the current zoom level * @returns {number} the current zoom level */ getZoom() { return this._zoom.current; } /** * Function that removes the content of a page and replaces it with the empty content (i.e. a content generated by function emptyContent) * such content will not be visible except for the time that the * @param {jQuery} $page the page to be emptied */ _cleanPage($page) { let $emptyContent = this.settings.emptyContent(); $page.find(`.${this.settings.contentClass}`).empty().append($emptyContent) } /** * Function that replaces the content with the empty class in a page with a new content * @param {*} $page the page to be modified * @param {*} $content the new content that will be set in the page */ _setPageContent($page, $content) { $page.find(`.${this.settings.contentClass}`).empty().append($content) } /** * Recalculates which pages are now visible and forces redrawing them (moreover it cleans those not visible) */ refreshAll() { this._visiblePages(true); } /** Function that creates a scroll handler to update the active page and to load more pages as the scroll position changes */ _setScrollListener() { // Create a scroll handler that prevents reentrance if called multiple times and the loading of pages is not finished let scrollLock = false; let scrollPos = { top:0 , left:0 }; this.__scrollHandler = function(e) { // Avoid re-entrance for the same event while loading pages if (scrollLock === true) { return; } scrollLock = true; let container = this.$container.get(0); if ((Math.abs(container.scrollTop - scrollPos.top) > (container.clientHeight * 0.2 * this._zoom.current)) || (Math.abs(container.scrollLeft - scrollPos.left) > (container.clientWidth * 0.2 * this._zoom.current))) { scrollPos = { top: container.scrollTop, left: container.scrollLeft } this._visiblePages(); } scrollLock = false; }.bind(this); // Set the scroll handler this.$container.off('scroll'); this.$container.on('scroll', this.__scrollHandler); } /** * Function that creates the pageinfo structure for one page, along with the skeleton to host the page (i.e.