// ES6 Compliant Syntax
// Copilot name: GitHub Copilot, version: 1.0.0, model: GPT-4, version: 2024-06, date: 2025-09-23
// File: pdf-search/index.js
import WebViewer from '@pdftron/webviewer';
const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
WebViewer(
{
path: '/lib',
initialDoc: 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/section-508.pdf',
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) => {
// Customize the main webviewer left panel after the load completion
instance.Core.documentViewer.addEventListener('documentLoaded', () => {
customizeUI(instance);
});
// Define the search listener to capture search results
// This listener is called when search results are returned
const searchListener = (searchPattern, options, results) => {
// Update the checkbox states on the custom search panel based on the search options used
checkBoxControl(checkBoxData[0]).checked = options.caseSensitive;
checkBoxControl(checkBoxData[1]).checked = options.wholeWord;
searchResults = formattedSearchResults(results);
updateResultsViewer();
};
// Add search listener to capture search results
instance.UI.addSearchListener(searchListener);
console.log('✅ WebViewer loaded successfully.');
}).catch((error) => {
console.error('❌ Failed to initialize WebViewer:', error);
});
// Store the search results
let searchResults = null;
// 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 search sub-panel to be registered
const leftSearchPanel = {
handle: null,
dataElement: 'leftSearchPanel',
render: null,
};
const checkBoxData = [
{ label: "Case sensitive" },
{ label: "Whole word" },
{ label: "Wild card" },
{ label: "Regular expression" }
];
// Customize the main webviewer left panel
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 search sub-panel will be added to this Tab panel.
tabPanel.handle = viewerPanels.find((panel) => panel.dataElement === tabPanel.dataElement);
// Register the custom search sub-panel
RegisterSearchPanel(instance);
// Add the new custom search sub-panel to list of sub-panels under the Tab Panel
leftSearchPanel.handle = { render: leftSearchPanel.dataElement };
tabPanel.handle.panelsList = [leftSearchPanel.handle, ...tabPanel.handle.panelsList];
UI.openElements([tabPanel.dataElement]);
UI.setPanelWidth(tabPanel.dataElement, 400);
};
// Register the custom search sub-panel
const RegisterSearchPanel = (instance) => {
leftSearchPanel.render = createSearchPanelElements(instance);
instance.UI.addPanel({
dataElement: leftSearchPanel.dataElement,
location: 'left',
icon: '<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="18" height="18" viewBox="0 0 24 24"><path d="M 10 2 C 5.5935644 2 2 5.5935677 2 10 C 2 14.406432 5.5935644 18 10 18 C 11.844022 18 13.540969 17.365427 14.896484 16.310547 L 20.292969 21.707031 A 1.0001 1.0001 0 1 0 21.707031 20.292969 L 16.310547 14.896484 C 17.365427 13.540969 18 11.844021 18 10 C 18 5.5935677 14.406436 2 10 2 z M 10 4 C 13.325556 4 16 6.674446 16 10 C 16 13.325554 13.325556 16 10 16 C 6.6744439 16 4 13.325554 4 10 C 4 6.674446 6.6744439 4 10 4 z"></path></svg>',
title: 'Search',
render: () => leftSearchPanel.render,
});
};
// Create the search panel elements.
const createSearchPanelElements = (instance) => {
let panelDiv = document.createElement('div');
panelDiv.id = 'search';
let paragraph = document.createTextNode('A demo of the PDF search and indexing capabilities in WebViewer, a JavaScript-based PDF SDK for web apps. Found words are highlighted throughout the PDF document.');
panelDiv.appendChild(paragraph);
let dividerDiv = document.createElement('div');
dividerDiv.style.borderTop = '1px solid #ccc';
dividerDiv.style.margin = '10px 0';
panelDiv.appendChild(dividerDiv);
// Search input field
const inputSearch = document.createElement('input');
inputSearch.id = 'inputSearch';
inputSearch.type = 'text';
inputSearch.placeholder = 'Search here ...';
inputSearch.addEventListener("input", () => {
enableButton(searchButton, inputSearch.value.trim() !== '');
});
inputSearch.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
searchPDF(instance);
}
});
panelDiv.appendChild(inputSearch);
// Search button
const searchButton = document.createElement('button');
searchButton.textContent = 'Search';
enableButton(searchButton, false); // Initially disable the search button
searchButton.style.marginLeft = '10px';
searchButton.onclick = () => searchPDF(instance); // Search the PDF document
panelDiv.appendChild(searchButton);
// Create checkboxes for items in the checkBoxData array
checkBoxData.forEach(item => {
// Checkbox input
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = labelToId(item.label);
// Checkbox label
const label = document.createElement('label');
label.textContent = item.label;
panelDiv.appendChild(document.createElement('p'));
panelDiv.appendChild(checkbox);
panelDiv.appendChild(label);
});
// Display search results
let jsonDiv = document.createElement('div');
jsonDiv.id = 'json';
const jsonTitle = document.createElement("h3");
jsonTitle.textContent = "Search Results";
jsonDiv.appendChild(jsonTitle);
jsonDiv.appendChild(document.createElement('p'));
const span = document.createElement("span");
span.id = 'resultsText';
let resultsText = document.createTextNode('');
span.appendChild(resultsText);
jsonDiv.appendChild(span);
jsonDiv.appendChild(document.createElement('p'));
const scrollBox = document.createElement("div");
scrollBox.style.width = "350px";
scrollBox.style.height = "350px";
scrollBox.style.border = "2px solid #444";
scrollBox.style.overflow = "scroll"; // Enables both vertical and horizontal scroll
scrollBox.style.whiteSpace = "nowrap"; // Prevents wrapping for horizontal scroll
scrollBox.style.padding = "10px";
scrollBox.style.fontFamily = "monospace";
scrollBox.style.backgroundColor = "black";
scrollBox.style.color = "white";
// Format and insert results JSON data
const jsonContent = document.createElement("pre");
scrollBox.appendChild(jsonContent);
jsonDiv.appendChild(scrollBox);
// Open JSON data dialog button
let jsonDataDialogButton = document.createElement('button');
jsonDataDialogButton.textContent = 'Open in Dialog';
jsonDataDialogButton.id = 'jsonDataDialogButton';
enableButton(jsonDataDialogButton, false); // Initially disable the button
jsonDataDialogButton.onclick = () => openJsonDataDialog();
jsonDiv.appendChild(jsonDataDialogButton);
jsonDiv.appendChild(document.createElement('p'));
panelDiv.appendChild(dividerDiv.cloneNode());
panelDiv.appendChild(jsonDiv);
return panelDiv;
};
// Format the search results for better readability
const formattedSearchResults = (results) => {
if (results === null || results.length === 0) {
return [];
}
const formatted = [];
results.forEach((result) => {
const element = {
ambient_str: result.ambient_str,
result_str: result.result_str,
result_str_start: result.result_str_start,
result_str_end: result.result_str_end,
page_num: result.page_num,
resultCode: result.resultCode,
quads: result.quads,
};
formatted.push(element);
});
return formatted;
};
// Update the results viewer with the latest search results
const updateResultsViewer = () => {
const resultsText = leftSearchPanel.render.querySelector('#resultsText');
resultsText.textContent = (searchResults && searchResults.length > 0) ? `${searchResults.length} results found. (Data has been formatted for readability)` : 'No results found';
const jsonContent = leftSearchPanel.render.querySelector('pre');
jsonContent.textContent = (searchResults && searchResults.length > 0) ? JSON.stringify(searchResults, null, 1) : '';
const jsonDataDialogButton = leftSearchPanel.render.querySelector('#jsonDataDialogButton');
enableButton(jsonDataDialogButton, (searchResults && searchResults.length > 0));
};
// Open search results in a viewer with zoom in/out and close buttons
const openJsonDataDialog = () => {
let fontSize = 14;
// Create overlay
const overlay = document.createElement("div");
overlay.className = "modal-overlay";
overlay.onclick = (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
}
};
// Modal box
const modal = document.createElement("div");
modal.className = "modal-box";
// Controls
const controls = document.createElement("div");
controls.className = "modal-controls";
const zoomInBtn = document.createElement("button");
zoomInBtn.textContent = "+";
zoomInBtn.onclick = () => {
fontSize += 2;
content.style.fontSize = fontSize + "px";
};
const zoomOutBtn = document.createElement("button");
zoomOutBtn.textContent = "-";
zoomOutBtn.onclick = () => {
fontSize = Math.max(10, fontSize - 2);
content.style.fontSize = fontSize + "px";
};
const closeBtn = document.createElement("button");
closeBtn.textContent = "Close";
closeBtn.className = "modal-close";
closeBtn.onclick = () => {
document.body.removeChild(overlay);
};
controls.appendChild(zoomInBtn);
controls.appendChild(zoomOutBtn);
controls.appendChild(closeBtn);
// Content
const content = document.createElement("pre");
content.className = "modal-content";
content.style.fontSize = fontSize + "px";
content.innerHTML = JSON.stringify(searchResults, null, 1);
modal.appendChild(controls);
modal.appendChild(content);
overlay.appendChild(modal);
document.body.appendChild(overlay);
}
// Convert a label to a valid ID by removing spaces
const labelToId = (label) => label.replace(/\s+/g, '');
// Get the checkbox control based on the label
const checkBoxControl = (item) => {
const checkbox = leftSearchPanel.render.querySelector(`#${labelToId(item.label)}`);
return checkbox;
};
// Search the PDF document based on the search pattern and options
const searchPDF = (instance) => {
instance.Core.documentViewer.clearSearchResults();
// Get the search pattern from the search input field value in the search panel
// searchPattern can be something like "search*m" with "wildcard" option set to true
// searchPattern can be something like "search1|search2" with "regex" option set to true
const searchPattern = leftSearchPanel.render.querySelector('#inputSearch').value.trim();
// Set search options values based on the checkbox states
const searchOptions = {
caseSensitive: checkBoxControl(checkBoxData[0]).checked, // match case
wholeWord: checkBoxControl(checkBoxData[1]).checked, // match whole words only
wildcard: checkBoxControl(checkBoxData[2]).checked, // allow using '*' as a wildcard value
regex: checkBoxControl(checkBoxData[3]).checked, // string is treated as a regular expression
ambientString: true, // return ambient string as part of the result
};
instance.UI.searchTextFull(searchPattern, searchOptions);
};
// Enable or disable a button based on the state
const enableButton = (button, state) => {
button.disabled = !state;
button.style.backgroundColor = (state) ? 'blue' : 'gray';
button.style.color = (state) ? 'white' : 'darkgray';
};