> 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-edit-table-of-contents-in-a-pdf.md).

# Edit Table of Contents in a PDF Showcase Demo Code Sample

Create accessible navigation with document outlines or table of contents. Add new outlines or edit them.

{% 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://apryse.com/capabilities#PageManipulation" class="button primary">Package: Page Manipulation</a><a href="/web/full-api/full-api-overview.md" class="button primary">Full API</a><a href="https://showcase.apryse.com/edit-table-of-contents-in-a-pdf" class="button primary">Live demo</a>
{% endhint %}

Create accessible navigation with document outlines or table of contents. Add new outlines or edit them.

This demo allows you to:

* Upload your own PDF file.
* Add or Edit Outlines or Table of Contents.
* Save edits and download an updated PDF.

**Implementation steps** To add Table of Contents or Outlines capability in WebViewer:

Step 1: Choose your [preferred web stack](/web/get-started/readme.md). 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-10-23
// File: showcase-demos/edit-table-of-contents/index.js

import WebViewer from '@pdftron/webviewer';

const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';

function initializeWebViewer() {
  WebViewer(
    {
      path: '/lib',
      initialDoc: 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/Report_2011.pdf',
      enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
      fullAPI: true, // Enable the full PDFNet API
      licenseKey: licenseKey, // Replace with your license key
    },
    document.getElementById('viewer')
  ).then((instance) => {

    // Set the toolbar group to the Annotate tools
    instance.UI.setToolbarGroup('toolbarGroup-Annotate');

    instance.Core.documentViewer.addEventListener('documentLoaded', () => {
      UIElements.pageNumber = 1;
      
      // Set default coordinates
      setDefaultCoordinates(instance);

      // Customize the webviewer left panel
      UIElements.customizeUI(instance);
    });

    console.log('WebViewer loaded successfully.');
  }).catch((error) => {
    console.error('Failed to initialize WebViewer:', error);
  });
}

// Function to get the iframe context of WebViewer
const getIframeContext = () => {
  return (
    document.getElementById('viewer')?.getElementsByTagName('iframe')?.[0]?.contentWindow
      .instance || window.WebViewer.getInstance()
  );
};

// Function to add a new outline to the PDF document
window.addOutline = (instance) => {
  const iframe = getIframeContext();
  const PDFNet = iframe.Core.PDFNet;

  const documentViewer = iframe.Core.documentViewer;

  return PDFNet.runWithCleanup(async () => {
    const doc = await documentViewer.getDocument().getPDFDoc();
    const newOutline = await PDFNet.Bookmark.create(doc, UIElements.outlineName);

    const page = await doc.getPage(UIElements.pageNumber);

    const zoom = 1;

    const dest = await PDFNet.Destination.createXYZ(page, UIElements.xCoordinate, UIElements.yCoordinate, zoom);

    newOutline.setAction(await PDFNet.Action.createGoto(dest));

    await doc.addRootBookmark(newOutline);

    instance.UI.reloadOutline();

    instance.UI.setActiveTabInPanel({ tabPanel: UIElements.tabPanel.dataElement, tabName: 'outlinesPanel' });
  });
};

// Helper function to set default coordinates based on the page size
window.setDefaultCoordinates = (instance) => {
  const doc = instance.Core.documentViewer.getDocument();
  const pageInfo = doc.getPageInfo(UIElements.pageNumber);
  UIElements.xCoordinate = 0;
  UIElements.yCoordinate = pageInfo.height;
}

//helper function to load the ui-elements.js script
function loadUIElementsScript() {
  return new Promise((resolve, reject) => {
    if (window.UIElements) {
      console.log('UIElements already loaded');
      resolve();
      return;
    }
    const script = document.createElement('script');
    script.src = '/showcase-demos/edit-table-of-contents/ui-elements.js';
    script.onload = function () {
      console.log('✅ UIElements script loaded successfully');
      resolve();
    };
    script.onerror = function () {
      console.error('Failed to load UIElements script');
      reject(new Error('Failed to load ui-elements.js'));
    };
    document.head.appendChild(script);
  });
}

// Load UIElements script first, then initialize WebViewer
loadUIElementsScript().then(() => {
  initializeWebViewer();
}).catch((error) => {
  console.error('Failed to load UIElements:', error);
});
</code></pre>

{% endtab %}

{% tab title="ui-elements.js" %}
{% code title="ui-elements.js" lineNumbers="true" %}

```js
// ES6 Compliant Syntax
// Copilot name: GitHub Copilot, version: 1.0.0, model: GPT-4, version: 2024-06, date: 2025-10-23
// File: showcase-demos/edit-table-of-contents/ui-elements.js

// Class with static UI elements and related functions for the table of contents demo

class UIElements {

    // The list of registered panels in the webviewer
    static viewerPanels = null;

    // The tab panel, representing the webviewer left panel
    static tabPanel = {
        handle: null,
        dataElement: 'tabPanel'
    };

    // The table of contents sub-panel to be registered
    static tableOfContentsPanel = {
        handle: null,
        dataElement: 'tableOfContentsPanel',
        render: null,
    };

    static outlineName = 'My First Outline';
    static pageNumber = 1;
    static xCoordinate = 0;
    static yCoordinate = 0;

    // Customize the webviewer left panel
    static customizeUI = (instance) => {
        const { UI } = instance;

        // Close the tab panel (if it's open) for refreshment.
        UI.closeElements([UIElements.tabPanel.dataElement]);

        // Get the list of registered panels in the webviewer
        UIElements.viewerPanels = UI.getPanels();

        // Find the Tab Panel to modify. The table of contents sub-panel will be added to this Tab panel.
        UIElements.tabPanel.handle = UIElements.viewerPanels.find((panel) => panel.dataElement === UIElements.tabPanel.dataElement);

        // Register the table of contents sub-panel
        UIElements.RegisterTableOfContentsPanel(instance);

        // Add the new table of contents sub-panel to list of sub-panels under the Tab Panel
        UIElements.tableOfContentsPanel.handle = { render: UIElements.tableOfContentsPanel.dataElement };
        UIElements.tabPanel.handle.panelsList = [UIElements.tableOfContentsPanel.handle, ...UIElements.tabPanel.handle.panelsList];

        UI.openElements([UIElements.tabPanel.dataElement]);
    };

    // Register the table of contents sub-panel
    static RegisterTableOfContentsPanel = (instance) => {
        UIElements.tableOfContentsPanel.render = UIElements.createTableOfContentsPanelElements(instance);
        instance.UI.addPanel({
            dataElement: UIElements.tableOfContentsPanel.dataElement,
            location: 'left',
            icon: '<svg xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 103.19 122.88"><path d="M17.16 0h82.72a3.32 3.32 0 013.31 3.31v92.32c-.15 2.58-3.48 2.64-7.08 2.48H15.94c-4.98 0-9.05 4.07-9.05 9.05s4.07 9.05 9.05 9.05h80.17v-9.63h7.08v12.24c0 2.23-1.82 4.05-4.05 4.05H16.29C7.33 122.88 0 115.55 0 106.59V17.16C0 7.72 7.72 0 17.16 0zm3.19 13.4h2.86c1.46 0 2.66.97 2.66 2.15v67.47c0 1.18-1.2 2.15-2.66 2.15h-2.86c-1.46 0-2.66-.97-2.66-2.15V15.55c.01-1.19 1.2-2.15 2.66-2.15z" fill-rule="evenodd" clip-rule="evenodd"/></svg>',
            title: 'Table of Contents',
            render: () => UIElements.tableOfContentsPanel.render,
        });
    };

    // Create the table of contents panel elements.
    static createTableOfContentsPanelElements = (instance) => {
        let panelDiv = document.createElement('div');
        panelDiv.id = 'tableOfContentsPanel';

        let paragraph = document.createTextNode('Add or edit the existing document outline or table of contents to create accessible navigation of your documents.');
        panelDiv.appendChild(paragraph);

        let dividerDiv = document.createElement('div');
        dividerDiv.style.borderTop = '1px solid #ccc';
        dividerDiv.style.margin = '10px 0';
        panelDiv.appendChild(dividerDiv);

        // Outline details
        let outlineDetails = document.createElement("h3");
        outlineDetails.textContent = "Outline Details";
        panelDiv.appendChild(outlineDetails);

        // Outline name label
        let outlineTitle = document.createTextNode('Name:');
        panelDiv.appendChild(outlineTitle);

        // Outline name input field
        const inputName = document.createElement('input');
        inputName.id = 'inputName';
        inputName.type = 'text';
        inputName.value = UIElements.outlineName;
        inputName.addEventListener("input", () => UIElements.outlineName = inputName.value);
        inputName.addEventListener("keydown", () => UIElements.outlineName = inputName.value);
        panelDiv.appendChild(document.createElement('p'));
        panelDiv.appendChild(inputName);

        // Outline destination page label
        let outlineDestination = document.createTextNode('Destination page:');
        panelDiv.appendChild(document.createElement('p'));
        panelDiv.appendChild(outlineDestination);

        // Outline destination page input field
        const pageCount = instance.Core.documentViewer.getPageCount();
        const inputDestination = document.createElement('input');
        inputDestination.id = 'inputDestination';
        inputDestination.type = 'number';
        inputDestination.min = '1';
        inputDestination.max = pageCount.toString();
        inputDestination.step = '1';
        inputDestination.value = UIElements.pageNumber.toString();
        inputDestination.addEventListener("input", () => {
            UIElements.inputFieldLimitations(inputDestination);
            UIElements.pageNumber = parseInt(inputDestination.value);
        });
        inputDestination.addEventListener("keydown", () => {
            UIElements.inputFieldLimitations(inputDestination);
            UIElements.pageNumber = parseInt(inputDestination.value);
        });
        panelDiv.appendChild(document.createElement('p'));
        panelDiv.appendChild(inputDestination);

        // Full page checkbox
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.checked = true;
        checkbox.onchange = () => {
            const inputX = UIElements.tableOfContentsPanel.render.querySelector('#inputX');
            const inputY = UIElements.tableOfContentsPanel.render.querySelector('#inputY');
            if (checkbox.checked) {
                setDefaultCoordinates(instance);
                inputX.value = UIElements.xCoordinate.toString();
                inputY.value = UIElements.yCoordinate.toString();
            }

            inputX.disabled = checkbox.checked;
            inputY.disabled = checkbox.checked;
        };

        // Full page checkbox label
        const label = document.createElement('label');
        label.textContent = 'Full page';

        panelDiv.appendChild(document.createElement('p'));
        panelDiv.appendChild(checkbox);
        panelDiv.appendChild(label);

        // X and Y coordinate input fields
        UIElements.createCoordinateInput(panelDiv, 'X');
        UIElements.createCoordinateInput(panelDiv, 'Y');

        // Add Outline button
        let button = document.createElement("button");
        button.textContent = 'Add New Outline';
        button.style.backgroundColor = 'blue';
        button.style.color = 'white';
        button.style.border = 'none';
        button.style.padding = '10px 15px';
        button.style.borderRadius = '12px';
        button.onmouseover = () => button.style.opacity = '0.8';
        button.onmouseout = () => button.style.opacity = '1.0';
        button.style.cursor = 'pointer';
        button.onclick = () => {
            addOutline(instance);
            button.style.opacity = '1.0';
        };
        panelDiv.appendChild(document.createElement('p'));
        panelDiv.appendChild(button);

        return panelDiv;
    };

    // Create coordinate input fields
    static createCoordinateInput(panelDiv, axis) {

        // Coordinate title field
        let title = document.createTextNode(`${axis} Coordinate:`);
        title.id = `title${axis.toUpperCase()}`;
        panelDiv.appendChild(document.createElement('p'));
        panelDiv.appendChild(title);

        // Coordinate input field
        const input = document.createElement('input');
        input.id = `input${axis.toUpperCase()}`;
        input.type = 'number';
        input.min = '0';
        input.disabled = true;
        input.value = axis === 'X' ? UIElements.xCoordinate.toString() : UIElements.yCoordinate.toString();
        input.addEventListener("input", () => {
            UIElements.inputFieldLimitations(input);
            axis === 'X' ? UIElements.xCoordinate = parseInt(input.value) : UIElements.yCoordinate = parseInt(input.value);
        });
        input.addEventListener("keydown", () => {
            UIElements.inputFieldLimitations(input);
            axis === 'X' ? UIElements.xCoordinate = parseInt(input.value) : UIElements.yCoordinate = parseInt(input.value);
        });
        panelDiv.appendChild(document.createElement('p'));
        panelDiv.appendChild(input);
    }

    // Limitations for input fields.
    // Ensures values are non-negative and within min/max range.
    // Used for page number and coordinate input fields.
    static inputFieldLimitations = (element) => {
        element.value = Math.abs(parseInt(element.value));
        const min = parseInt(element.min);
        const max = parseInt(element.max);
        if (element.value > max)
            element.value = max;
        else if (element.value < min)
            element.value = min;
    };
}
```

{% 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-edit-table-of-contents-in-a-pdf.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.
