Create accessible navigation with document outlines or table of contents. Add new outlines or edit them.
This demo allows you to:
Implementation steps
To add Table of Contents or Outlines capability in WebViewer:
Step 1: Choose 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 collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
1// ES6 Compliant Syntax
2// Copilot name: GitHub Copilot, version: 1.0.0, model: GPT-4, version: 2024-06, date: 2025-10-23
3// File: showcase-demos/edit-table-of-contents/index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7function initializeWebViewer() {
8 WebViewer(
9 {
10 path: '/lib',
11 initialDoc: 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/Report_2011.pdf',
12 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
13 fullAPI: true, // Enable the full PDFNet API
14 licenseKey: 'YOUR_LICENSE_KEY', // Replace with your license key
15 },
16 document.getElementById('viewer')
17 ).then((instance) => {
18
19 // Set the toolbar group to the Annotate tools
20 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
21
22 instance.Core.documentViewer.addEventListener('documentLoaded', () => {
23 UIElements.pageNumber = 1;
24
25 // Set default coordinates
26 setDefaultCoordinates(instance);
27
28 // Customize the webviewer left panel
29 UIElements.customizeUI(instance);
30 });
31
32 console.log('WebViewer loaded successfully.');
33 }).catch((error) => {
34 console.error('Failed to initialize WebViewer:', error);
35 });
36}
37
38// Function to get the iframe context of WebViewer
39const getIframeContext = () => {
40 return (
41 document.getElementById('viewer')?.getElementsByTagName('iframe')?.[0]?.contentWindow
42 .instance || window.WebViewer.getInstance()
43 );
44};
45
46// Function to add a new outline to the PDF document
47window.addOutline = (instance) => {
48 const iframe = getIframeContext();
49 const PDFNet = iframe.Core.PDFNet;
50
51 const documentViewer = iframe.Core.documentViewer;
52
53 return PDFNet.runWithCleanup(async () => {
54 const doc = await documentViewer.getDocument().getPDFDoc();
55 const newOutline = await PDFNet.Bookmark.create(doc, UIElements.outlineName);
56
57 const page = await doc.getPage(UIElements.pageNumber);
58
59 const zoom = 1;
60
61 const dest = await PDFNet.Destination.createXYZ(page, UIElements.xCoordinate, UIElements.yCoordinate, zoom);
62
63 newOutline.setAction(await PDFNet.Action.createGoto(dest));
64
65 await doc.addRootBookmark(newOutline);
66
67 instance.UI.reloadOutline();
68
69 instance.UI.setActiveTabInPanel({ tabPanel: UIElements.tabPanel.dataElement, tabName: 'outlinesPanel' });
70 });
71};
72
73// Helper function to set default coordinates based on the page size
74window.setDefaultCoordinates = (instance) => {
75 const doc = instance.Core.documentViewer.getDocument();
76 const pageInfo = doc.getPageInfo(UIElements.pageNumber);
77 UIElements.xCoordinate = 0;
78 UIElements.yCoordinate = pageInfo.height;
79}
80
81//helper function to load the ui-elements.js script
82function loadUIElementsScript() {
83 return new Promise((resolve, reject) => {
84 if (window.UIElements) {
85 console.log('UIElements already loaded');
86 resolve();
87 return;
88 }
89 const script = document.createElement('script');
90 script.src = '/showcase-demos/edit-table-of-contents-in-a-pdf/ui-elements.js';
91 script.onload = function () {
92 console.log('✅ UIElements script loaded successfully');
93 resolve();
94 };
95 script.onerror = function () {
96 console.error('Failed to load UIElements script');
97 reject(new Error('Failed to load ui-elements.js'));
98 };
99 document.head.appendChild(script);
100 });
101}
102
103// Load UIElements script first, then initialize WebViewer
104loadUIElementsScript().then(() => {
105 initializeWebViewer();
106}).catch((error) => {
107 console.error('Failed to load UIElements:', error);
108});
1// ES6 Compliant Syntax
2// Copilot name: GitHub Copilot, version: 1.0.0, model: GPT-4, version: 2024-06, date: 2025-10-23
3// File: showcase-demos/edit-table-of-contents/ui-elements.js
4
5// Class with static UI elements and related functions for the table of contents demo
6
7class UIElements {
8
9 // The list of registered panels in the webviewer
10 static viewerPanels = null;
11
12 // The tab panel, representing the webviewer left panel
13 static tabPanel = {
14 handle: null,
15 dataElement: 'tabPanel'
16 };
17
18 // The table of contents sub-panel to be registered
19 static tableOfContentsPanel = {
20 handle: null,
21 dataElement: 'tableOfContentsPanel',
22 render: null,
23 };
24
25 static outlineName = 'My First Outline';
26 static pageNumber = 1;
27 static xCoordinate = 0;
28 static yCoordinate = 0;
29
30 // Customize the webviewer left panel
31 static customizeUI = (instance) => {
32 const { UI } = instance;
33
34 // Close the tab panel (if it's open) for refreshment.
35 UI.closeElements([UIElements.tabPanel.dataElement]);
36
37 // Get the list of registered panels in the webviewer
38 UIElements.viewerPanels = UI.getPanels();
39
40 // Find the Tab Panel to modify. The table of contents sub-panel will be added to this Tab panel.
41 UIElements.tabPanel.handle = UIElements.viewerPanels.find((panel) => panel.dataElement === UIElements.tabPanel.dataElement);
42
43 // Register the table of contents sub-panel
44 UIElements.RegisterTableOfContentsPanel(instance);
45
46 // Add the new table of contents sub-panel to list of sub-panels under the Tab Panel
47 UIElements.tableOfContentsPanel.handle = { render: UIElements.tableOfContentsPanel.dataElement };
48 UIElements.tabPanel.handle.panelsList = [UIElements.tableOfContentsPanel.handle, ...UIElements.tabPanel.handle.panelsList];
49
50 UI.openElements([UIElements.tabPanel.dataElement]);
51 };
52
53 // Register the table of contents sub-panel
54 static RegisterTableOfContentsPanel = (instance) => {
55 UIElements.tableOfContentsPanel.render = UIElements.createTableOfContentsPanelElements(instance);
56 instance.UI.addPanel({
57 dataElement: UIElements.tableOfContentsPanel.dataElement,
58 location: 'left',
59 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>',
60 title: 'Table of Contents',
61 render: () => UIElements.tableOfContentsPanel.render,
62 });
63 };
64
65 // Create the table of contents panel elements.
66 static createTableOfContentsPanelElements = (instance) => {
67 let panelDiv = document.createElement('div');
68 panelDiv.id = 'tableOfContentsPanel';
69
70 let paragraph = document.createTextNode('Add or edit the existing document outline or table of contents to create accessible navigation of your documents.');
71 panelDiv.appendChild(paragraph);
72
73 let dividerDiv = document.createElement('div');
74 dividerDiv.style.borderTop = '1px solid #ccc';
75 dividerDiv.style.margin = '10px 0';
76 panelDiv.appendChild(dividerDiv);
77
78 // Outline details
79 let outlineDetails = document.createElement("h3");
80 outlineDetails.textContent = "Outline Details";
81 panelDiv.appendChild(outlineDetails);
82
83 // Outline name label
84 let outlineTitle = document.createTextNode('Name:');
85 panelDiv.appendChild(outlineTitle);
86
87 // Outline name input field
88 const inputName = document.createElement('input');
89 inputName.id = 'inputName';
90 inputName.type = 'text';
91 inputName.value = UIElements.outlineName;
92 inputName.addEventListener("input", () => UIElements.outlineName = inputName.value);
93 inputName.addEventListener("keydown", () => UIElements.outlineName = inputName.value);
94 panelDiv.appendChild(document.createElement('p'));
95 panelDiv.appendChild(inputName);
96
97 // Outline destination page label
98 let outlineDestination = document.createTextNode('Destination page:');
99 panelDiv.appendChild(document.createElement('p'));
100 panelDiv.appendChild(outlineDestination);
101
102 // Outline destination page input field
103 const pageCount = instance.Core.documentViewer.getPageCount();
104 const inputDestination = document.createElement('input');
105 inputDestination.id = 'inputDestination';
106 inputDestination.type = 'number';
107 inputDestination.min = '1';
108 inputDestination.max = pageCount.toString();
109 inputDestination.step = '1';
110 inputDestination.value = UIElements.pageNumber.toString();
111 inputDestination.addEventListener("input", () => {
112 UIElements.inputFieldLimitations(inputDestination);
113 UIElements.pageNumber = parseInt(inputDestination.value);
114 });
115 inputDestination.addEventListener("keydown", () => {
116 UIElements.inputFieldLimitations(inputDestination);
117 UIElements.pageNumber = parseInt(inputDestination.value);
118 });
119 panelDiv.appendChild(document.createElement('p'));
120 panelDiv.appendChild(inputDestination);
121
122 // Full page checkbox
123 const checkbox = document.createElement('input');
124 checkbox.type = 'checkbox';
125 checkbox.checked = true;
126 checkbox.onchange = () => {
127 const inputX = UIElements.tableOfContentsPanel.render.querySelector('#inputX');
128 const inputY = UIElements.tableOfContentsPanel.render.querySelector('#inputY');
129 if (checkbox.checked) {
130 setDefaultCoordinates(instance);
131 inputX.value = UIElements.xCoordinate.toString();
132 inputY.value = UIElements.yCoordinate.toString();
133 }
134
135 inputX.disabled = checkbox.checked;
136 inputY.disabled = checkbox.checked;
137 };
138
139 // Full page checkbox label
140 const label = document.createElement('label');
141 label.textContent = 'Full page';
142
143 panelDiv.appendChild(document.createElement('p'));
144 panelDiv.appendChild(checkbox);
145 panelDiv.appendChild(label);
146
147 // X and Y coordinate input fields
148 UIElements.createCoordinateInput(panelDiv, 'X');
149 UIElements.createCoordinateInput(panelDiv, 'Y');
150
151 // Add Outline button
152 let button = document.createElement("button");
153 button.textContent = 'Add New Outline';
154 button.style.backgroundColor = 'blue';
155 button.style.color = 'white';
156 button.style.border = 'none';
157 button.style.padding = '10px 15px';
158 button.style.borderRadius = '12px';
159 button.onmouseover = () => button.style.opacity = '0.8';
160 button.onmouseout = () => button.style.opacity = '1.0';
161 button.style.cursor = 'pointer';
162 button.onclick = () => {
163 addOutline(instance);
164 button.style.opacity = '1.0';
165 };
166 panelDiv.appendChild(document.createElement('p'));
167 panelDiv.appendChild(button);
168
169 return panelDiv;
170 };
171
172 // Create coordinate input fields
173 static createCoordinateInput(panelDiv, axis) {
174
175 // Coordinate title field
176 let title = document.createTextNode(`${axis} Coordinate:`);
177 title.id = `title${axis.toUpperCase()}`;
178 panelDiv.appendChild(document.createElement('p'));
179 panelDiv.appendChild(title);
180
181 // Coordinate input field
182 const input = document.createElement('input');
183 input.id = `input${axis.toUpperCase()}`;
184 input.type = 'number';
185 input.min = '0';
186 input.disabled = true;
187 input.value = axis === 'X' ? UIElements.xCoordinate.toString() : UIElements.yCoordinate.toString();
188 input.addEventListener("input", () => {
189 UIElements.inputFieldLimitations(input);
190 axis === 'X' ? UIElements.xCoordinate = parseInt(input.value) : UIElements.yCoordinate = parseInt(input.value);
191 });
192 input.addEventListener("keydown", () => {
193 UIElements.inputFieldLimitations(input);
194 axis === 'X' ? UIElements.xCoordinate = parseInt(input.value) : UIElements.yCoordinate = parseInt(input.value);
195 });
196 panelDiv.appendChild(document.createElement('p'));
197 panelDiv.appendChild(input);
198 }
199
200 // Limitations for input fields.
201 // Ensures values are non-negative and within min/max range.
202 // Used for page number and coordinate input fields.
203 static inputFieldLimitations = (element) => {
204 element.value = Math.abs(parseInt(element.value));
205 const min = parseInt(element.min);
206 const max = parseInt(element.max);
207 if (element.value > max)
208 element.value = max;
209 else if (element.value < min)
210 element.value = min;
211 };
212}
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales