> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/showcase-demo-pdf-search.md).

# PDF Search Showcase Demo Code Sample

Add search capability to PDFs using configurable string searches

{% hint style="info" %}
**Requirements**

*These packages are required to use these features in production. Trial keys have unlimited access to all features*

<a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="https://showcase.apryse.com/pdf-search" class="button primary">Live demo</a>
{% endhint %}

Add PDF search functionality with configurable search methods that allow users to refine or broaden string match criteria.

This demo allows you to:

* Choose your own PDF file
* Type a search string
* Configure the search options:
  * Case Sensitive
  * Whole Word
  * Wild Card
  * Regular Expression
* Highlight matching string in the document

### **Implementation steps**

To add PDF Search capability with WebViewer: Step 1: [Get started with WebViewer](/web/get-started/readme.md) in your preferred web stack Step 2: Add the ES6 JavaScript sample code provided in this guide

Once you generate your license key, it will automatically be included in your sample code below.

{% @apryse-license-key/apryse-license-key platform="WEB\_VIEWER" variant="compact" %}

{% tabs %}
{% tab title="index.js" %}

<pre class="language-js" data-line-numbers><code class="lang-js">// 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 = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';

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: '&#x3C;svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="18" height="18" viewBox="0 0 24 24">&#x3C;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">&#x3C;/path>&#x3C;/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 &#x26;&#x26; 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 &#x26;&#x26; searchResults.length > 0) ? JSON.stringify(searchResults, null, 1) : '';

  const jsonDataDialogButton = leftSearchPanel.render.querySelector('#jsonDataDialogButton');
  enableButton(jsonDataDialogButton, (searchResults &#x26;&#x26; 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';
};
</code></pre>

{% endtab %}

{% tab title="modal.css" %}
{% code title="modal.css" lineNumbers="true" %}

```css
/* Modal styles for pdf-search demo */

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 1001;
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-box {
  background: white;
  padding: 20px;
  border-radius: 8px;
  width: 80%;
  max-width: 800px;
  height: 80%;
  max-height: 600px;
  display: flex;
  flex-direction: column;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  position: relative;
}

.modal-controls {
  margin-bottom: 15px;
  display: flex;
  gap: 10px;
  align-items: center;
}

.modal-controls button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s ease;
}

.modal-controls button:not(.modal-close) {
  background: #007cba;
  color: white;
}

.modal-controls button:not(.modal-close):hover {
  background: #005a8b;
}

.modal-close {
  background: #dc3545 !important;
  color: white !important;
  margin-left: auto;
}

.modal-close:hover {
  background: #b02a37 !important;
}

.modal-content {
  background: #f8f9fa;
  padding: 15px;
  border-radius: 4px;
  overflow: auto;
  flex: 1;
  font-family: 'Courier New', monospace;
  white-space: pre-wrap;
  word-wrap: break-word;
  border: 1px solid #dee2e6;
  margin: 0;
  color: #000000;
}

```

{% endcode %}
{% endtab %}
{% endtabs %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/showcase-demo-pdf-search.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
