Office template filling

Fill MS Office templates using this JavaScript sample (no servers or other external dependencies required). This sample identifies and shows users the exact area where text has changed in documents.This sample works on all browsers (including IE11) and mobile devices without using plug-ins. Learn more about our Web SDK.

1let editor;
2let viewedDocSchema = {};
3let annotations = [];
4let annotationsByTag = {};
5
6const sampleFilePath = {
7 'SYH-letter': '../../samples/files/template-SYH-letter.docx',
8 'invoice-simple': '../../samples/files/template-invoice-simple.docx',
9 'invoice-complex': '../../samples/files/template-invoice-complex.docx',
10};
11const queryDoc = new URLSearchParams(window.location.search).get('doc');
12if (queryDoc in sampleFilePath) {
13 document.getElementById('samples-file-picker').value = queryDoc;
14}
15let viewingFile = document.getElementById('samples-file-picker').value || 'SYH-letter';
16
17WebViewer(
18 {
19 path: '../../../lib',
20 preloadWorker: 'office',
21 fullAPI: false,
22 css: 'webviewer.css',
23 },
24 document.getElementById('viewer')
25).then(instance => {
26 const { UI, Core } = instance;
27 const { documentViewer } = instance.Core;
28
29 UI.disableFeatures(UI.Feature.Annotations);
30
31 loadDoc();
32
33 async function loadDoc() {
34 updateFileStatus();
35 // Loading the template document with doTemplatePrep, so that we can access the schema and bounding boxes:
36 await instance.UI.loadDocument(sampleFilePath[viewingFile] || viewingFile, {
37 officeOptions: {
38 doTemplatePrep: true,
39 },
40 onError: pageModificationsAfterLoadError,
41 });
42 }
43
44 async function generateDocument() {
45 const templateValues = editor.getValue();
46 convertLinks(templateValues);
47 instance.UI.closeElements('errorModal');
48 // Fill the template document with the data from templateValues:
49 await documentViewer
50 .getDocument()
51 .applyTemplateValues(templateValues)
52 .then(updateAnnotations)
53 .catch(e => UI.displayErrorMessage(e.message));
54 }
55
56 documentViewer.addEventListener('documentLoaded', async () => {
57 // Get the schema of the template keys used in the document:
58 const schema = await documentViewer
59 .getDocument()
60 .getTemplateKeys('schema')
61 .catch(e => UI.displayErrorMessage(e.message));
62
63 if (Object.keys(schema['keys']).length === 0) {
64 // WebViewer can handle templates that don't contain any tags, but it is not useful in a demo so we report an error.
65 UI.showWarningMessage({
66 title: 'No tags',
67 message: 'The selected document does not contain any template tags. Please choose another document.',
68 });
69 pageModificationsAfterLoadError();
70 return;
71 }
72
73 const jsonSchema = templateSchemaToJsonSchema(schema);
74 await updateAnnotations(instance);
75
76 if (!editor || JSON.stringify(schema) !== JSON.stringify(viewedDocSchema)) {
77 viewedDocSchema = schema;
78 const options = {
79 theme: 'pdftron',
80 iconlib: 'pdftron',
81 schema: jsonSchema,
82 prompt_before_delete: false,
83 disable_properties: true,
84 disable_array_reorder: true,
85 disable_array_delete_last_row: true,
86 disable_array_delete_all_rows: true,
87 expand_height: true,
88 keep_oneof_values: false,
89 };
90 if (viewingFile in prePopulateData) {
91 options.startval = prePopulateData[viewingFile];
92 }
93 if (editor) {
94 editor.destroy();
95 }
96 editor = new JSONEditor(document.getElementById('autofill-form'), options);
97 viewedDocSchema = schema;
98 editor.on('ready', pageModificationsAfterLoad);
99 editor.on('ready', addEventHandlersToJsonEditor);
100 editor.on('addRow', addEventHandlersToJsonEditor);
101 } else {
102 // We already have an editor with the correct schema.
103 pageModificationsAfterLoad();
104 }
105 });
106
107 class UnselectableSelectionModel extends Core.Annotations.SelectionModel {
108 constructor(annotation) {
109 super(annotation, false);
110 }
111 drawSelectionOutline() {}
112 testSelection() {
113 return false;
114 }
115 }
116
117 async function updateAnnotations() {
118 Core.annotationManager.deleteAnnotations(annotations, true);
119 annotations = [];
120 annotationsByTag = {};
121 const fillColor = new Core.Annotations.Color(255, 255, 0, 0.2);
122 const strokeColor = new Core.Annotations.Color(150, 150, 0, 1);
123 // Get the bounding boxes of the template keys in the document:
124 const boundingBoxes = await documentViewer
125 .getDocument()
126 .getTemplateKeys('locations')
127 .catch(e => UI.displayErrorMessage(e.message));
128 if (boundingBoxes) {
129 for (const tag in boundingBoxes) {
130 for (const boundingBox of boundingBoxes[tag]) {
131 const pageNum = boundingBox[0];
132 const rect = boundingBox[1];
133 const annotationRect = new Core.Math.Rect(rect.x1 - 2, rect.y1 - 2, rect.x2 + 2, rect.y2 + 2);
134 const annotation = new Core.Annotations.RectangleAnnotation();
135 annotation.setRect(annotationRect);
136 annotation.setPageNumber(pageNum);
137 annotation.FillColor = fillColor;
138 annotation.StrokeColor = strokeColor;
139 annotation.StrokeThickness = 1;
140 annotation.selectionModel = UnselectableSelectionModel;
141 annotation.Hidden = true;
142 annotations.push(annotation);
143 if (!annotationsByTag.hasOwnProperty(tag)) {
144 annotationsByTag[tag] = [];
145 }
146 annotationsByTag[tag].push(annotation);
147 }
148 }
149 }
150 Core.annotationManager.addAnnotations(annotations, true);
151 Core.annotationManager.drawAnnotationsFromList(annotations);
152 }
153
154 function showAnnotationsForTemplateTag(templateTag) {
155 const annotations = annotationsByTag[templateTag];
156 if (annotations && documentViewer.getDocument()) {
157 Core.annotationManager.showAnnotations(annotations);
158 const visiblePages = documentViewer
159 .getDisplayModeManager()
160 .getDisplayMode()
161 .getVisiblePages(0.0);
162 for (const annotation of annotations) {
163 if (visiblePages.includes(annotation.getPageNumber())) {
164 return;
165 }
166 }
167 Core.annotationManager.jumpToAnnotation(annotations[0]);
168 }
169 }
170
171 function hideAnnotationsForTemplateTag(templateTag) {
172 const annotations = annotationsByTag[templateTag];
173 if (annotations && documentViewer.getDocument()) {
174 Core.annotationManager.hideAnnotations(annotations);
175 }
176 }
177
178 function addEventHandlersToJsonEditor() {
179 for (const el of document.querySelectorAll('[data-template-path]')) {
180 if (!el || el.getAttribute('data-has-annotation-listeners') === 'true') {
181 continue;
182 }
183 el.setAttribute('data-has-annotation-listeners', 'true');
184 const templatePath = el.getAttribute('data-template-path');
185 const showAnnotationsFunc = showAnnotationsForTemplateTag.bind(null, templatePath);
186 const hideAnnotationsFunc = hideAnnotationsForTemplateTag.bind(null, templatePath);
187 const mouseEl = el.getAttribute('data-schematype') === 'array' ? el.firstChild : el;
188 mouseEl.addEventListener('mouseenter', showAnnotationsFunc);
189 mouseEl.addEventListener('mouseleave', hideAnnotationsFunc);
190 }
191 }
192
193 document.getElementById('file-picker').onchange = e => {
194 const file = e.target.files[0];
195 if (file) {
196 document.getElementById('samples-file-picker').selectedIndex = 0;
197 viewingFile = file;
198 loadDoc();
199 }
200 };
201 document.getElementById('samples-file-picker').onchange = e => {
202 viewingFile = e.target.value;
203 loadDoc();
204 };
205 document.getElementById('reset-document-button').onclick = loadDoc;
206 document.getElementById('generate-document-button').onclick = async () => {
207 await generateDocument();
208 await generateDocument();
209 };
210});
211
212function templateSchemaKeyValuesToJsonSchema(templateKV) {
213 const ret = {};
214 for (const key in templateKV) {
215 const valTemplateSchema = templateKV[key];
216 const valJsonSchema = {};
217 ret[key] = valJsonSchema;
218 valJsonSchema['propertyOrder'] = valTemplateSchema['docOrder'];
219 switch (valTemplateSchema['typeId']) {
220 case 'TemplateSchemaBool':
221 valJsonSchema['$ref'] = '#/definitions/template-bool';
222 break;
223 case 'TemplateSchemaContent':
224 valJsonSchema['$ref'] = '#/definitions/template-content';
225 break;
226 case 'TemplateSchemaString':
227 valJsonSchema['$ref'] = '#/definitions/template-text';
228 break;
229 case 'TemplateSchemaObject':
230 valJsonSchema['$ref'] = '#/definitions/template-object';
231 valJsonSchema['properties'] = templateSchemaKeyValuesToJsonSchema(valTemplateSchema['properties']);
232 break;
233 case 'TemplateSchemaLoop':
234 const loopTypeSet = new Set(valTemplateSchema['loopType']);
235 valJsonSchema['$ref'] = loopTypeSet.has('tableRow') && loopTypeSet.size === 1 ? '#/definitions/template-row-loop' : '#/definitions/template-loop';
236 valJsonSchema['items'] = {
237 title: key,
238 properties: templateSchemaKeyValuesToJsonSchema(valTemplateSchema['itemSchema']),
239 };
240 }
241 }
242 return ret;
243}
244
245function templateSchemaToJsonSchema(templateSchema) {
246 return {
247 $ref: '#/definitions/template-schema',
248 properties: templateSchemaKeyValuesToJsonSchema(templateSchema['keys']),
249 definitions: schemaDefinitions,
250 };
251}
252
253function pageModificationsAfterLoadError() {
254 document.getElementById('autofill-form-and-footer').className = 'autofill-form-error';
255}
256
257function pageModificationsAfterLoad() {
258 document.getElementById('autofill-form-and-footer').className = '';
259 document.getElementById('prep-message').style.display = 'none';
260}
261
262function updateFileStatus() {
263 document.getElementById('file-status').innerText = viewingFile.name || viewingFile;
264}
265
266function convertLinks(json) {
267 const referenceLinkConverter = document.getElementById('reference-link-converter');
268 if (!json || typeof json !== 'object') {
269 return;
270 }
271 if (Array.isArray(json)) {
272 for (const item of json) {
273 convertLinks(item);
274 }
275 return;
276 }
277 for (const entry in json) {
278 if (entry === 'image_url') {
279 referenceLinkConverter.href = json[entry];
280 json[entry] = referenceLinkConverter.href;
281 } else {
282 convertLinks(json[entry]);
283 }
284 }
285}
286
287const prePopulateData = {
288 'SYH-letter': {
289 date: '07/16/21',
290 land_location: '225 Parc St., Rochelle, QC ',
291 lease_problem: 'According to the city records, the lease was initiated in September 2010 and never terminated',
292 client: {
293 full_name: 'Mrs. Eric Tragar',
294 gender_possesive: 'her',
295 },
296 dest: {
297 address: '187 Duizelstraat\n5043 EC Tilburg, Netherlands',
298 given_name: 'Janice N.',
299 surname: 'Symonds',
300 title: 'Ms.',
301 },
302 sender: {
303 name: 'Arnold Smith',
304 },
305 logo: {
306 image_url: '../../files/logo_red.png',
307 width: '64',
308 height: '64',
309 },
310 },
311 'invoice-simple': {
312 invoice_number: 3467821,
313 bill_to_name: 'Victoria Guti\u00e9rrez',
314 bill_to_address: '218 Spruce Ave.\nAnna Maria, FL\n34216',
315 ship_to_name: 'Mar\u00eda Rosales',
316 ship_to_address: '216 E. Kennedy Blvd.\nTampa, FL\n34202',
317 total_due: '430.50',
318 total_paid: '150.00',
319 total_owing: '280.50',
320 items: [
321 {
322 description: 'Item 1',
323 qty: 1,
324 price: '10.00',
325 total: '10.00',
326 },
327 {
328 description: 'Item 2',
329 qty: 20,
330 price: '20.00',
331 total: '400.00',
332 },
333 {
334 description: 'Item 3',
335 qty: 1,
336 price: '0.00',
337 total: '0.00',
338 },
339 ],
340 subtotal: '410.00',
341 sales_tax_rate: '5.0%',
342 sales_tax: '20.50',
343 },
344 'invoice-complex': {
345 invoice_number: 3467821,
346 bill_to_name: 'Victoria Guti\u00e9rrez',
347 bill_to_address: '218 Spruce Ave.\nAnna Maria, FL\n34216',
348 ship_to_name: 'Mar\u00eda Rosales',
349 ship_to_address: '216 E. Kennedy Blvd.\nTampa, FL\n34202',
350 total_due: '880.50',
351 total_paid: '150.00',
352 total_owing: '730.50',
353 pay_by_date: 'Dec 31 2021',
354 pay_by_date_elapsed: false,
355 vendors: [
356 {
357 vendor: 'OEM Corp.',
358 items: [
359 {
360 description: 'Item 1',
361 qty: 1,
362 price: '10.00',
363 total: '10.00',
364 },
365 {
366 description: 'Item 2',
367 qty: 20,
368 price: '20.00',
369 total: '400.00',
370 },
371 ],
372 subtotal: '410.00',
373 sales_tax_rate: '5.0%',
374 sales_tax: '20.50',
375 amount_due: '430.50',
376 },
377 {
378 vendor: 'ABC Logistics',
379 items: [
380 {
381 description: 'Freight, mile',
382 qty: 84,
383 price: '5.00',
384 total: '420.00',
385 },
386 {
387 description: 'Pickup',
388 qty: 1,
389 price: '30.00',
390 total: '30.00',
391 },
392 ],
393 subtotal: '450.00',
394 sales_tax_rate: '5.0%',
395 sales_tax: '22.50',
396 discount: '-22.50',
397 amount_due: '450.00',
398 },
399 ],
400 },
401};
402
403const schemaDefinitions = {
404 'template-schema': {
405 type: 'object',
406 title: 'Template data',
407 },
408 'template-bool': {
409 type: 'boolean',
410 format: 'checkbox',
411 },
412 'template-text': {
413 type: 'string',
414 format: 'textarea',
415 },
416 'template-object': {
417 type: 'object',
418 },
419 'template-loop': {
420 type: 'array',
421 items: {
422 type: 'object',
423 },
424 },
425 'template-row-loop': {
426 $ref: '#/definitions/template-loop',
427 format: 'table',
428 },
429 'template-image': {
430 type: 'object',
431 properties: {
432 image_url: {
433 $ref: '#/definitions/template-text',
434 propertyOrder: 1,
435 format: 'url/file-download',
436 },
437 width: {
438 type: 'string',
439 title: 'width',
440 propertyOrder: 2,
441 },
442 height: {
443 type: 'string',
444 title: 'height',
445 propertyOrder: 3,
446 },
447 },
448 },
449 'template-markdown': {
450 type: 'object',
451 properties: {
452 markdown: {
453 $ref: '#/definitions/template-text',
454 },
455 },
456 },
457 'template-html': {
458 type: 'object',
459 properties: {
460 html: {
461 $ref: '#/definitions/template-text',
462 },
463 },
464 },
465 'template-content': {
466 anyOf: [
467 {
468 title: 'text',
469 $ref: '#/definitions/template-text',
470 },
471 {
472 title: 'image',
473 $ref: '#/definitions/template-image',
474 },
475 {
476 title: 'markdown',
477 $ref: '#/definitions/template-markdown',
478 },
479 {
480 title: 'html',
481 $ref: '#/definitions/template-html',
482 },
483 ],
484 },
485};

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales