// ES6 Compliant Syntax
// Copilot name: GitHub Copilot, version: N/A, model: GPT-4, version: N/A, date: August 13, 2025
// File: index.js
import WebViewer from '@pdftron/webviewer';
const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
const initialDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/letter_template.docx';
// The list of registered panels in the main webviewer
let viewerPanels = null;
// The tab panel, representing the webviewer left panel
const tabPanel = {
handle: null,
dataElement: 'tabPanel'
};
// The custom template sub-panel to be registered
const templatePanel = {
handle: null,
dataElement: 'templatePanel',
templateKeys: null,
render: null,
};
// The logos to choose from
const logos = [
{ title: 'Angular', file: '/assets/angular.png' },
{ title: 'React', file: '/assets/react.png' },
{ title: 'Vue', file: '/assets/vue.png' },
];
// The rows to insert into the table
let insertRows = [
['Title', 'Web Developer'],
['Start Date', '01/10/2022'],
['Expected Salary', '$100,000'],
];
// The logo to insert into the document
const logoJSON = {
val: { image_url: logos[0].file, width: 64, height: 64 },
type: 'image',
};
// The table to insert into the document
const tableJSON = {
val: { insert_rows: insertRows },
type: 'table',
};
// A sample JSON data
const sampleJSON = {
dest_street_address: { val: '187 Duizelstraat', type: 'text' },
applicant_first_name: { val: 'Janice', type: 'text' },
applicant_surname: { val: 'Symonds', type: 'text' },
Date: { val: '01/06/2022', type: 'text' },
sender_name: { val: 'Arnold Smith', type: 'text' },
company_name: { val: 'React-Faux', type: 'text' },
logo: logoJSON,
table: tableJSON,
};
// The initial selected logo. By default it is Angular
let selectedButton = logos[0].title;
WebViewer(
{
path: '/lib',
initialDoc: initialDoc,
enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
enableMeasurement: true,
licenseKey: licenseKey, // Replace with your license key
},
document.getElementById('viewer')
).then((instance) => {
const { documentViewer } = instance.Core;
// Customize the main webviewer left panel after the load completion.
// The left panel will display the template sub-panel with input fields.
// The input fields are representing the template keys were extracted from the loaded document.
documentViewer.addEventListener('documentLoaded', async () => {
const doc = documentViewer.getDocument();
templatePanel.templateKeys = await doc.getTemplateKeys();
customizeUI(instance);
});
console.log('✅ WebViewer loaded successfully.');
}).catch((error) => {
console.error('❌ Failed to initialize WebViewer:', error);
});
// Customize the main webviewer left panel after the load completion
const customizeUI = (instance) => {
const { UI } = instance;
// Close the tab panel (if it's open) for refreshment.
UI.closeElements([tabPanel.dataElement]);
// Get the list of registered panels in the main webviewer
viewerPanels = UI.getPanels();
// Find the Tab Panel to modify. The template sub-panel will be added to this Tab panel.
tabPanel.handle = viewerPanels.find((panel) => panel.dataElement === tabPanel.dataElement);
// Register the custom template sub-panel
RegisterTemplatePanel(instance);
// Add the new custom template sub-panel to list of sub-panels under the Tab Panel
templatePanel.handle = { render: templatePanel.dataElement };
tabPanel.handle.panelsList = [templatePanel.handle, ...tabPanel.handle.panelsList];
UI.openElements([tabPanel.dataElement]);
UI.setPanelWidth(tabPanel.dataElement, 400);
};
// Register the custom template sub-panel
const RegisterTemplatePanel = (instance) => {
templatePanel.render = createTemplatePanelElements(instance);
instance.UI.addPanel({
dataElement: templatePanel.dataElement,
location: 'left',
icon: '<svg width="18px" height="18px" viewBox="0 0 24 24" id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#080808;}</style></defs><title>template</title><path class="cls-1" d="M21,.5H3a2,2,0,0,0-2,2V22a2,2,0,0,0,2,2H21a2,2,0,0,0,2-2V2.5A2,2,0,0,0,21,.5Zm0,2v2H3v-2ZM3,22V6.5H21V22Z"/><path class="cls-1" d="M12.5,4H20a.5.5,0,0,0,0-1H12.5a.5.5,0,0,0,0,1Z"/><path class="cls-1" d="M4.5,4a.43.43,0,0,0,.19,0,.35.35,0,0,0,.16-.11A.47.47,0,0,0,5,3.5a.43.43,0,0,0,0-.19.36.36,0,0,0-.11-.16.5.5,0,0,0-.7,0A.35.35,0,0,0,4,3.31.43.43,0,0,0,4,3.5a.51.51,0,0,0,.5.5Z"/><path class="cls-1" d="M5.65,3.85A.36.36,0,0,0,5.81,4,.44.44,0,0,0,6,4a.47.47,0,0,0,.35-.15.36.36,0,0,0,.11-.16.6.6,0,0,0,0-.19.51.51,0,0,0-.15-.35A.49.49,0,0,0,5.81,3a.36.36,0,0,0-.16.11.47.47,0,0,0-.15.35.4.4,0,0,0,0,.19A.35.35,0,0,0,5.65,3.85Z"/><path class="cls-1" d="M8,8H4.5a1,1,0,0,0,0,2H8A1,1,0,0,0,8,8Z"/><path class="cls-1" d="M8,11.67H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,15.33H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,19H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,8H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,11.67H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,15.33H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,19H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,8h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,11.67h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,15.33h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,19h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/></svg>',
title: 'Template',
render: () => templatePanel.render,
});
};
// Create the template panel elements.
// The elements will be created dynamically,
// based on the template keys extracted from
// the loaded document.
const createTemplatePanelElements = (instance) => {
let panelDiv = document.createElement('div');
panelDiv.id = 'template';
let paragraph = document.createTextNode('Securely merge JSON data with MS Word, PowerPoint and Excel templates directly in the browser - no server required! Easily generate documents using text/image templates with JavaScript.');
panelDiv.appendChild(paragraph);
let dividerDiv = document.createElement('div');
dividerDiv.style.borderTop = '1px solid #ccc';
dividerDiv.style.margin = '10px 0';
panelDiv.appendChild(dividerDiv);
if (templatePanel.templateKeys.length === 0) {
panelDiv.appendChild(document.createTextNode('Sorry, there are no template keys in this document.'));
return panelDiv;
}
let labelField = null;
let inputField = null;
let logoField = null;
let tableField = null;
templatePanel.templateKeys.forEach(element => {
switch (element) {
// if logo key is found, then create its div.
case 'logo':
logoField = document.createElement('div');
logoField.appendChild(document.createTextNode(`${element}:`));
logoField.appendChild(document.createElement('p'));
logoField.id = 'logo';
if (sampleJSON[element].type === 'image') {
logos.forEach(image => {
const img = document.createElement('img');
img.src = image.file;
img.alt = image.title;
img.style.width = '20px';
img.style.height = '20px';
let frameworkBtn = document.createElement('button');
frameworkBtn.title = image.title;
frameworkBtn.id = image.title;
frameworkBtn.style.marginRight = '5px';
if (image.title === logos[0].title)
frameworkBtn.style.backgroundColor = 'blue';
else
frameworkBtn.style.backgroundColor = 'white';
frameworkBtn.appendChild(img);
frameworkBtn.onclick = () => {
selectedButton = image.title;
frameworkBtn.style.backgroundColor = 'blue';
templatePanel.render.querySelectorAll('button').forEach(btn => {
if (btn !== frameworkBtn)
btn.style.backgroundColor = 'white';
});
}
logoField.appendChild(frameworkBtn);
});
let heightInput = document.createElement('input');
heightInput.id = 'height';
heightInput.title = 'H (pts)';
heightInput.type = typeof (sampleJSON[element].val.height);
heightInput.value = sampleJSON[element].val.height;
heightInput.style.height = '40px';
heightInput.style.width = '40px';
heightInput.disabled = true;
heightInput.style.marginRight = '5px';
logoField.appendChild(heightInput);
let widthInput = document.createElement('input');
widthInput.id = 'width';
widthInput.title = 'W (pts)';
widthInput.type = typeof (sampleJSON[element].val.width);
widthInput.value = sampleJSON[element].val.width;
widthInput.style.height = '40px';
widthInput.style.width = '40px';
widthInput.disabled = true;
widthInput.style.marginRight = '5px';
logoField.appendChild(widthInput);
}
break;
// if table key is found, then create its div.
case 'table':
tableField = document.createElement('div');
tableField.appendChild(document.createTextNode(`${element}:`));
tableField.appendChild(document.createElement('p'));
tableField.id = 'table';
sampleJSON[element].val.insert_rows.forEach((row) => {
let rowLabel = document.createTextNode(`${row[0]}:`);
let rowInput = document.createElement('input');
rowInput.id = `${row[0]}`;
rowInput.type = 'text';
rowInput.value = row[1];
tableField.appendChild(document.createElement('p'));
tableField.appendChild(rowLabel);
tableField.appendChild(document.createElement('p'));
tableField.appendChild(rowInput);
});
break;
// input fields for text
default:
labelField = document.createTextNode(`${element}:`);
inputField = document.createElement('input');
inputField.id = element;
if (sampleJSON[element] === undefined) {
inputField.type = 'text';
inputField.placeholder = 'Text To Fill Template';
}
else {
inputField.type = sampleJSON[element].type;
inputField.value = sampleJSON[element].val;
}
}
panelDiv.appendChild(labelField);
panelDiv.appendChild(document.createElement('p'));
if (inputField)
panelDiv.appendChild(inputField);
panelDiv.appendChild(document.createElement('p'));
});
if (logoField) {
panelDiv.appendChild(dividerDiv.cloneNode());
panelDiv.appendChild(logoField);
}
if (tableField) {
panelDiv.appendChild(dividerDiv.cloneNode());
panelDiv.appendChild(tableField);
}
// Fill template and Reset document buttons
let fillTemplateButton = document.createElement('button');
fillTemplateButton.textContent = 'Fill Template';
fillTemplateButton.style.backgroundColor = 'white';
fillTemplateButton.style.color = 'blue';
fillTemplateButton.onclick = () => fillTemplate(instance); // Fill the template with data
let resetButton = document.createElement('button');
resetButton.textContent = 'Reset Document';
resetButton.style.backgroundColor = 'white';
resetButton.style.color = 'blue';
resetButton.onclick = () => instance.UI.loadDocument(initialDoc);
panelDiv.appendChild(dividerDiv.cloneNode());
panelDiv.appendChild(fillTemplateButton);
panelDiv.appendChild(document.createElement('p'));
panelDiv.appendChild(resetButton);
return panelDiv;
};
// Collect data from the custom template sub-panel fields,
// then format it for the template
// and apply it to the loaded document.
const fillTemplate = async (instance) => {
const formattedValues = {};
// Collect data from input fields
templatePanel.render.querySelectorAll('input').forEach((input) => {
if (templatePanel.templateKeys.includes(input.id)) {
formattedValues[input.id] = input.value;
}
});
// Collect data from table fields
let tableInputs = templatePanel.render.querySelectorAll('#table input');
if (tableInputs.length > 0) {
insertRows = [];
tableInputs.forEach((input) =>
insertRows.push([input.id, input.value])
);
sampleJSON.table.val.insert_rows = insertRows;
formattedValues['table'] = sampleJSON.table.val;
}
// Collect data from logo field
sampleJSON['logo'].val.image_url = window.location.origin + logos.find(img => img.title === selectedButton).file;
formattedValues['logo'] = sampleJSON['logo'].val;
// Apply the formatted template values
await instance.Core.documentViewer.getDocument().applyTemplateValues(formattedValues);
};