@digital-blueprint/lunchlottery-app
Version:
[GitHub Repository](https://github.com/digital-blueprint/lunchlottery-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/lunchlottery-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/lunchlottery-app/)
537 lines (471 loc) • 20.5 kB
JavaScript
import {createInstance} from './i18n.js';
import {css, html} from 'lit';
import {classMap} from 'lit/directives/class-map.js';
import {live} from 'lit/directives/live.js';
import {ScopedElementsMixin, LangMixin} from '@dbp-toolkit/common';
import DBPLitElement from '@dbp-toolkit/common/dbp-lit-element';
import {MiniSpinner, Icon} from '@dbp-toolkit/common';
import * as commonUtils from '@dbp-toolkit/common/utils';
import * as commonStyles from '@dbp-toolkit/common/styles';
import {name as pkgName} from './../package.json';
import {readBinaryFileContent} from './utils.js';
let pdfjsPromise = null;
/**
* Dynamically imports the PDF.js library and sets up the worker source.
* This can be used separately from the PdfViewer component to ensure that PDF.js
* is loaded and set up.
*
* @async
* @returns {Promise<object>} A promise that resolves to the imported PDF.js module.
*/
export async function importPdfJs() {
if (!pdfjsPromise) {
pdfjsPromise = (async () => {
const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
pdfjs.GlobalWorkerOptions.workerSrc = commonUtils.getAssetURL(
pkgName,
'pdfjs/pdf.worker.mjs',
);
return pdfjs;
})();
}
return pdfjsPromise;
}
/**
* Loads a PDF document using PDF.js with default configuration options.
*
* This sets various default options for loading the PDF document, such as the
* cmap URL and cmap packed setting, as well as disabling eval for security
* reasons unless explicitly enabled.
*
* @param {import('pdfjs-dist/legacy/build/pdf.mjs')} pdfjs - The PDF.js library instance.
* @param {object} [src] - The source configuration object for the PDF document.
* @returns {import('pdfjs-dist/legacy/build/pdf.mjs').PDFDocumentLoadingTask}
*/
export function getPdfJsDocument(pdfjs, src = {}) {
let cmaps = commonUtils.getAssetURL(pkgName, 'pdfjs/cmaps/');
// Set for cmaps we ship by default
if (src.cMapUrl === undefined) src.cMapUrl = cmaps;
if (src.cMapPacked === undefined) src.cMapPacked = true;
// Disable eval by default for security reasons, unless explicitly enabled
if (src.isEvalSupported === undefined) src.isEvalSupported = false;
return pdfjs.getDocument(src);
}
/**
* PdfViewer web component
*/
export class PdfViewer extends LangMixin(ScopedElementsMixin(DBPLitElement), createInstance) {
constructor() {
super();
this.pdfDoc = null;
this.currentPage = 0;
this.totalPages = 0;
this.isShowPage = false;
this.isPageLoaded = false;
this.showErrorMessage = false;
this.isPageRenderingInProgress = false;
this.isShowPlacement = true;
this.canvas = null;
this.canvasToPdfScale = 1.0;
this.currentPageOriginalHeight = 0;
this.currentPageOriginalWidth = 0;
this.pdfMainContainer = null;
this.initialClientWidth = 0;
this.initialClientHeight = 0;
this.isFirstRendering = true;
this.autoResize = 'cover';
this._onWindowResize = this._onWindowResize.bind(this);
}
static get scopedElements() {
return {
'dbp-mini-spinner': MiniSpinner,
'dbp-icon': Icon,
};
}
/**
* See: https://lit-element.polymer-project.org/guide/properties#initialize
*/
static get properties() {
return {
...super.properties,
autoResize: {type: String, attribute: 'auto-resize'},
currentPage: {type: Number, attribute: false},
totalPages: {type: Number, attribute: false},
isShowPage: {type: Boolean, attribute: false},
isPageRenderingInProgress: {type: Boolean, attribute: false},
isPageLoaded: {type: Boolean, attribute: false},
showErrorMessage: {type: Boolean, attribute: false},
};
}
update(changedProperties) {
super.update(changedProperties);
changedProperties.forEach((oldValue, propName) => {
switch (propName) {
case 'autoResize':
this._onWindowResize();
break;
}
});
}
_onWindowResize() {
this.isFirstRendering = true;
this.showPage(this.currentPage);
}
disconnectedCallback() {
window.removeEventListener('resize', this._onWindowResize);
super.disconnectedCallback();
}
connectedCallback() {
super.connectedCallback();
const that = this;
window.addEventListener('resize', this._onWindowResize);
this.updateComplete.then(async () => {
that.canvas = that._('#pdf-canvas');
that.pdfMainContainer = that._('#pdf-main-container');
// this._('#upload-pdf-input').addEventListener('change', function() {
// that.showPDF(this.files[0]);
// });
});
}
enforceCanvasBoundaries(obj) {
// top-left corner
if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top);
obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left);
}
// bottom-right corner
if (
obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height ||
obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
) {
obj.top = Math.min(
obj.top,
obj.canvas.height -
obj.getBoundingRect().height +
obj.top -
obj.getBoundingRect().top,
);
obj.left = Math.min(
obj.left,
obj.canvas.width -
obj.getBoundingRect().width +
obj.left -
obj.getBoundingRect().left,
);
}
}
async onPageNumberChanged(e) {
let obj = e.target;
const page_no = parseInt(obj.value);
console.log('page_no = ', page_no);
if (page_no > 0 && page_no <= this.totalPages) {
await this.showPage(page_no);
}
}
/**
* Initialize and load the PDF
*
* @param file
* @param placementData
* @param fixWidthAdaption
*/
async showPDF(file, placementData = {}, fixWidthAdaption = false) {
this.isPageLoaded = false; // prevent redisplay of previous pdf
this.showErrorMessage = false;
this.isShowPage = true;
const data = await readBinaryFileContent(file);
// get handle of pdf document
try {
let pdfjs = await importPdfJs();
this.pdfDoc = await getPdfJsDocument(pdfjs, {data: data}).promise;
} catch (error) {
console.error(error);
this.showErrorMessage = true;
return;
}
// total pages in pdf
this.totalPages = this.pdfDoc.numPages;
const page = placementData.currentPage || 1;
// show the first page
// if the placementData has no values we want to initialize the signature position
await this.showPage(page);
this.isPageLoaded = true;
if (fixWidthAdaption) {
// fix width adaption after "this.isPageLoaded = true"
setTimeout(() => {
this._onWindowResize();
}, 100);
}
}
/**
* Load and render specific page of the PDF
*
* @param pageNumber
*/
async showPage(pageNumber) {
// we need to wait until the last rendering is finished
if (this.isPageRenderingInProgress || this.pdfDoc === null) {
return;
}
const that = this;
this.isPageRenderingInProgress = true;
this.currentPage = pageNumber;
try {
// get handle of page
await this.pdfDoc.getPage(pageNumber).then(async (page) => {
if (that.isFirstRendering) {
that.isFirstRendering = false;
// we weren't able to get the initial width and height of the container in the connectedCallback (this.updateComplete)
// clientWidth and clientHeight were also not set correctly, getBoundingClientRect() was the only way to get the correct values
that.initialClientWidth = that.getBoundingClientRect().width - 2;
that.initialClientHeight = that.getBoundingClientRect().height - 2;
// console.log("that._('#pdf-meta').clientHeight", that._('#pdf-meta').clientHeight);
// console.log("that._('#pdf-meta')", that._('#pdf-meta'));
// console.log("that._('#pdf-meta').getBoundingClientRect().height", that._('#pdf-meta').getBoundingClientRect().height);
// console.log("this.initialClientWidth", that.initialClientWidth);
// console.log("this.initialClientHeight", that.initialClientHeight);
}
// original width of the pdf page at scale 1
const originalViewport = page.getViewport({scale: 1});
this.currentPageOriginalHeight = originalViewport.height;
this.currentPageOriginalWidth = originalViewport.width;
// const proportion = this.currentPageOriginalWidth / this.currentPageOriginalHeight;
// set the canvas width to the width of the container (minus the borders)
// let clientWidth = this.pdfMainContainer.clientWidth - 2;
// let clientHeight = this.pdfMainContainer.clientHeight - 2;
let clientWidth = that.initialClientWidth;
let clientHeight = that.initialClientHeight - that._('#pdf-meta').clientHeight;
console.log('clientWidth', clientWidth);
console.log('clientHeight', clientHeight);
// let proposedCanvasWidth = clientWidth;
// let proposedCanvasHeight = clientHeight;
this.canvas.width = clientWidth;
// as the canvas is of a fixed width we need to adjust the scale of the viewport where page is rendered
this.canvasToPdfScale = clientWidth / originalViewport.width;
console.log('this.canvasToPdfScale: ' + this.canvasToPdfScale);
// get viewport to render the page at required scale
let viewport = page.getViewport({scale: this.canvasToPdfScale});
// if the height of the viewport is higher than the height of the container and the autoResize is set
// to 'contain', then we need to adjust the scale again
if (this.autoResize === 'contain' && viewport.height > clientHeight) {
// this.canvasToPdfScale = this.canvasToPdfScale * (clientHeight / viewport.height);
this.canvasToPdfScale = clientHeight / originalViewport.height;
console.log(
'viewport.height to high, new this.canvasToPdfScale',
this.canvasToPdfScale,
);
// get viewport to render the page at required scale
viewport = page.getViewport({scale: this.canvasToPdfScale});
}
// set canvas height same as viewport height
this.canvas.height = viewport.height;
this.canvas.width = viewport.width;
// setting page loader height for smooth experience
this._('#page-loader').style.height = this.canvas.height + 'px';
this._('#page-loader').style.lineHeight = this.canvas.height + 'px';
// setting wrapper height, so that the absolute positions of the pdf-canvas and
// the annotation-layer don't disturb the page layout
this._('#canvas-wrapper').style.height = this.canvas.height + 'px';
// page is rendered on <canvas> element
const render_context = {
canvasContext: this.canvas.getContext('2d'),
viewport: viewport,
};
// render the page contents in the canvas
try {
await page.render(render_context).promise;
} catch (error) {
console.error(error.message);
} finally {
that.isPageRenderingInProgress = false;
}
});
} catch (error) {
console.error(error.message);
that.isPageRenderingInProgress = false;
}
}
sendCancelEvent() {
const event = new CustomEvent('dbp-pdf-viewer-cancel', {
detail: {},
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
static get styles() {
// language=css
return css`
${commonStyles.getGeneralCSS()}
${commonStyles.getButtonCSS()}
#pdf-meta input[type=number] {
max-width: 50px;
}
#page-loader {
display: flex;
align-items: center;
justify-content: center;
}
/* it's too risky to adapt the height */
/*
#pdf-meta button, #pdf-meta input {
max-height: 15px;
}
*/
#canvas-wrapper {
position: relative;
/*align-items: center;*/
/*justify-content: center;*/
/*display: flex;*/
border: var(--dbp-border);
}
#canvas-wrapper-inner {
display: block;
position: relative;
}
#canvas-wrapper canvas {
position: absolute;
top: 0;
left: 0;
}
.buttons {
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: center;
align-items: center;
}
.nav-buttons {
display: flex;
justify-content: center;
flex-grow: 1;
flex-wrap: wrap;
}
.buttons .page-info {
align-self: center;
white-space: nowrap;
}
.nav-buttons > * {
margin: 2px;
}
input[type='number'] {
background-color: var(--dbp-background);
border: var(--dbp-border);
color: var(--dbp-content);
padding: 0 0.3em;
}
#pdf-meta {
border: var(--dbp-border);
padding: 0.54em;
border-bottom-width: 0;
}
.button.is-cancel {
color: var(--dbp-danger);
}
.error-message {
text-align: center;
border: 1px solid black;
border-top: 0px;
padding: 15px;
}
dbp-mini-spinner {
margin: auto;
display: block;
width: 17px;
}
`;
}
render() {
const i18n = this._i18n;
return html`
<!--
<form>
<input type="file" name="pdf" id="upload-pdf-input">
</form>
-->
<div id="pdf-main-container" class="${classMap({hidden: !this.isShowPage})}">
<dbp-mini-spinner
class="${classMap({
hidden: this.isPageLoaded || this.showErrorMessage,
})}"></dbp-mini-spinner>
<div
class="error-message ${classMap({
hidden: !this.showErrorMessage || this.isPageLoaded,
})}">
${i18n.t('pdf-viewer.error-message')}
</div>
<div class="${classMap({hidden: !this.isPageLoaded})}">
<div id="pdf-meta">
<div class="buttons ${classMap({hidden: !this.isPageLoaded})}">
<div class="nav-buttons">
<button
class="button is-icon"
title="${i18n.t('pdf-viewer.first-page')}"
@click="${async () => {
await this.showPage(1);
}}"
?disabled="${this.isPageRenderingInProgress ||
this.currentPage === 1}">
<dbp-icon name="angle-double-left"></dbp-icon>
</button>
<button
class="button is-icon"
title="${i18n.t('pdf-viewer.previous-page')}"
@click="${async () => {
if (this.currentPage > 1)
await this.showPage(--this.currentPage);
}}"
?disabled="${this.isPageRenderingInProgress ||
this.currentPage === 1}">
<dbp-icon name="chevron-left"></dbp-icon>
</button>
<input
type="number"
min="1"
max="${this.totalPages}"
@input="${this.onPageNumberChanged}"
.value="${live(this.currentPage)}" />
<div class="page-info">
${i18n.t('pdf-viewer.page-count', {
totalPages: this.totalPages,
})}
</div>
<button
class="button is-icon"
title="${i18n.t('pdf-viewer.next-page')}"
@click="${async () => {
if (this.currentPage < this.totalPages)
await this.showPage(++this.currentPage);
}}"
?disabled="${this.isPageRenderingInProgress ||
this.currentPage === this.totalPages}">
<dbp-icon name="chevron-right"></dbp-icon>
</button>
<button
class="button is-icon"
title="${i18n.t('pdf-viewer.last-page')}"
@click="${async () => {
await this.showPage(this.totalPages);
}}"
?disabled="${this.isPageRenderingInProgress ||
this.currentPage === this.totalPages}">
<dbp-icon name="angle-double-right"></dbp-icon>
</button>
</div>
</div>
</div>
<div
id="canvas-wrapper"
class="${classMap({hidden: this.isPageRenderingInProgress})}">
<div id="canvas-wrapper-inner">
<canvas id="pdf-canvas"></canvas>
</div>
</div>
<div class="${classMap({hidden: !this.isPageRenderingInProgress})}">
<dbp-mini-spinner id="page-loader"></dbp-mini-spinner>
</div>
</div>
</div>
`;
}
}