document-viewer-ts
Version:
PDF and MS Doc viewer written in TypeScript for React and vanilla JavaScript
262 lines • 11.7 kB
JavaScript
import { GlobalWorkerOptions, getDocument, renderTextLayer } from 'pdfjs-dist/legacy/build/pdf.mjs';
const chevronLeft = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>`;
const chevronRight = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>`;
const handleError = (containerDiv) => (err) => {
renderErrorMessage(containerDiv)(`There was an error fetching your document. Please try again later. Error: ${err}`);
};
const zoomValues = [50, 80, 100, 125, 150, 200, 300, 400];
const defaultWidth = 80;
const PDFtoCSSConvert = 96 / 72;
const defaultPageWidth = 700;
const scaleTextLayer = async (textLayerDiv, textContent, pdfPage, canvas, viewport, pdfScale) => {
textLayerDiv.innerHTML = '';
const textLayerFragment = document.createElement('div');
textLayerFragment.className = 'textLayer';
const scale = pdfScale * (canvas.offsetWidth / viewport.width);
const vs = pdfPage.getViewport({ scale });
await renderTextLayer({
textContentSource: textContent,
container: textLayerFragment,
viewport: vs
})?.promise;
textLayerDiv.appendChild(textLayerFragment);
};
const createLoadingIndicator = () => {
const container = document.createElement('div');
container.className = 'indicator-container';
const indicator = document.createElement('div');
indicator.className = 'lds-ring';
indicator.innerHTML = '<div></div><div></div><div></div><div></div></div>';
container.appendChild(indicator);
return container;
};
export const renderPDF = async (containerDiv, documentUrl) => {
const wrapperDiv = document.createElement('div');
wrapperDiv.className = 'wrapper';
containerDiv.appendChild(wrapperDiv);
const loadingIndicator = createLoadingIndicator();
wrapperDiv.appendChild(loadingIndicator);
const documentId = containerDiv.id;
const canvasContainer = document.createElement('div');
canvasContainer.className = 'canvas-container';
const controlsDiv = document.createElement('div');
controlsDiv.className = 'viewer-controls';
const fullPageNumberDiv = document.createElement('div');
fullPageNumberDiv.className = 'page-number';
const pageNumberInput = document.createElement('input');
pageNumberInput.className = 'page-number-input';
pageNumberInput.type = 'number';
const pageDiv = document.createElement('div');
pageDiv.className = 'page-label';
const outOfDiv = document.createElement('div');
const pageCountDiv = document.createElement('div');
const nextButton = document.createElement('button');
nextButton.className = 'next-button';
const prevButton = document.createElement('button');
prevButton.className = 'prev-button';
const loadingTask = getDocument({ url: documentUrl, isEvalSupported: false });
const zoomSelect = document.createElement('select');
zoomSelect.className = 'zoom-select';
zoomValues.forEach((v) => {
const zoomOption = document.createElement('option');
zoomOption.value = `${v}`;
zoomOption.textContent = `${v}%`;
zoomSelect.appendChild(zoomOption);
});
zoomSelect.value = `${defaultWidth}`;
const getZoomVal = (originalPageWidth) => {
const pageWidth = containerDiv.clientWidth * (parseInt(zoomSelect.value) / 100);
const scaledBy = pageWidth / originalPageWidth;
containerDiv.style.setProperty('--scale-factor', scaledBy.toString());
const scaleVal = scaledBy * 2.5 * PDFtoCSSConvert;
return scaleVal;
};
try {
const pdfDocument = await loadingTask.promise;
wrapperDiv.removeChild(loadingIndicator);
const isValidPage = (page) => page <= pdfDocument.numPages && page > 0;
// initial viewer setup
pageNumberInput.value = '1';
pageDiv.textContent = 'Page ';
outOfDiv.textContent = '/';
pageCountDiv.textContent = `${pdfDocument.numPages}`;
fullPageNumberDiv.appendChild(pageDiv);
fullPageNumberDiv.appendChild(pageNumberInput);
fullPageNumberDiv.appendChild(outOfDiv);
fullPageNumberDiv.appendChild(pageCountDiv);
// page container setup
const pageContainer = document.createElement('div');
pageContainer.className = 'page-container';
pageContainer.style.width = `${defaultWidth}%`;
canvasContainer.style.alignItems = 'center';
canvasContainer.appendChild(pageContainer);
const canvas = document.createElement('canvas');
canvas.className = 'pdf-viewer-canvas';
canvas.id = `${documentId}-canvas`;
pageContainer.appendChild(canvas);
// text layer setup
const textLayerDiv = document.createElement('div');
textLayerDiv.className = 'text-layer';
pageContainer.appendChild(textLayerDiv);
nextButton.innerHTML = chevronRight;
prevButton.innerHTML = chevronLeft;
wrapperDiv.appendChild(controlsDiv);
controlsDiv.appendChild(prevButton);
controlsDiv.appendChild(fullPageNumberDiv);
controlsDiv.appendChild(nextButton);
controlsDiv.appendChild(zoomSelect);
wrapperDiv.appendChild(canvasContainer);
const displayPage = async (page) => {
pageNumberInput.value = `${page}`;
const pdfPage = await pdfDocument.getPage(page);
const originalPageWidth = Number(pdfPage._pageInfo.view[2] || defaultPageWidth);
const viewport = pdfPage.getViewport({ scale: getZoomVal(originalPageWidth) });
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = '100%';
const ctx = canvas.getContext('2d');
await pdfPage.render({
canvasContext: ctx || new CanvasRenderingContext2D(),
viewport,
});
const textContent = await pdfPage.getTextContent();
scaleTextLayer(textLayerDiv, textContent, pdfPage, canvas, viewport, getZoomVal(originalPageWidth));
window.addEventListener('resize', () => {
scaleTextLayer(textLayerDiv, textContent, pdfPage, canvas, viewport, getZoomVal(originalPageWidth));
});
};
zoomSelect.onchange = async () => {
const pageNumber = parseInt(pageNumberInput.value || '1');
const zoomVal = parseInt(zoomSelect.value);
pageContainer.style.width = `${zoomSelect.value}%`;
if (zoomVal > 100) {
canvasContainer.style.alignItems = 'flex-start';
}
else {
canvasContainer.style.alignItems = 'center';
}
await displayPage(pageNumber);
};
const skipPage = (direction) => () => {
const pageNumber = parseInt(pageNumberInput.value || '1');
const nextPageNumber = pageNumber + direction;
if (isValidPage(nextPageNumber)) {
displayPage(nextPageNumber);
}
};
nextButton.onclick = skipPage(1);
prevButton.onclick = skipPage(-1);
pageNumberInput.onchange = () => {
const pageNumber = Math.max(Math.min(parseInt(pageNumberInput.value), pdfDocument.numPages), 1);
displayPage(pageNumber);
};
pageNumberInput.addEventListener('click', (e) => e.stopPropagation());
zoomSelect.addEventListener('click', (e) => e.stopPropagation());
containerDiv.addEventListener('keydown', (e) => {
e.stopPropagation();
switch (e.key) {
case ('ArrowLeft'):
skipPage(-1)();
return;
case ('ArrowRight'):
skipPage(1)();
return;
default:
return;
}
});
containerDiv.addEventListener('click', () => {
containerDiv.focus();
});
containerDiv.setAttribute('tabIndex', '1');
containerDiv.focus();
await displayPage(1);
return;
}
catch (err) {
handleError(containerDiv)(err);
return err;
}
};
const renderDocx = (containerDiv, documentUrl) => {
const microsoftViewer = document.createElement('iframe');
microsoftViewer.width = '100%';
microsoftViewer.height = '100%';
microsoftViewer.setAttribute('frameborder', '0');
const params = new URLSearchParams({ src: documentUrl });
microsoftViewer.setAttribute('src', `https://view.officeapps.live.com/op/embed.aspx?${params.toString()}`);
containerDiv.appendChild(microsoftViewer);
};
const renderTxt = (containerDiv, documentUrl) => {
const embed = document.createElement('embed');
embed.className = 'txt-embed';
embed.setAttribute('src', documentUrl);
containerDiv.appendChild(embed);
};
const renderErrorMessage = (containerDiv) => (errorMessage) => {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = errorMessage;
containerDiv.appendChild(errorDiv);
};
export const renderDocument = (workerSrc) => (containerDiv) => {
try {
containerDiv.innerHTML = '';
const documentUrl = containerDiv.getAttribute('data-document-url');
containerDiv.classList.add('document-viewer-ts');
if (!documentUrl)
throw new Error('No document url specified');
const splitOnPeriods = documentUrl.split('.');
const extension = splitOnPeriods[(splitOnPeriods.length - 1)]?.split('?')[0]?.toLowerCase();
switch (extension) {
case 'pdf':
try {
(() => globalThis)();
new File([], 'test.txt');
GlobalWorkerOptions.workerSrc = workerSrc;
renderPDF(containerDiv, documentUrl);
}
catch (err) {
renderErrorMessage(containerDiv)('Your browser does not support showing PDF previews. Click the download button to view this document.');
}
return;
case 'doc':
case 'docx':
case 'ppt':
case 'pptx':
case 'xls':
case 'xlsx':
case 'xlt':
case 'xlsm':
case 'xlw':
case 'pps':
case 'ppxs':
case 'ppsm':
case 'sldx':
case 'sldm':
renderDocx(containerDiv, documentUrl);
return;
case 'txt':
renderTxt(containerDiv, documentUrl);
return;
default:
throw new Error('This file type is not supported for viewing in a web browser. Please click the “Download” button to view this document.');
}
}
catch (err) {
handleError(containerDiv)(err);
}
};
const loadDocuments = (workerSrc) => () => {
const containerDivs = document.getElementsByClassName('viewer-container');
Array.from(containerDivs).forEach(renderDocument(workerSrc));
};
export const init = (workerSrc) => {
loadDocuments(workerSrc)();
window.addEventListener('load', loadDocuments(workerSrc));
};
//# sourceMappingURL=base.js.map