How to create Table of contents in O365 add-in

How we can create Table of contents [TOC] in O365/Word API JavaScript/Office JS

Word.run(async function (context) {
  • first we need to get paragraphs from Word document and find Heading1,2,3
const currentDoc = context.document;
            const body = currentDoc.body;
            const paragraphs = currentDoc.body.paragraphs.load('items');
            const headers = [];
            const uniqueStr = new Date().getTime();
            let insertion = '';
            await context.sync();
            for (let i = 0; i < paragraphs.items.length; i++) {
                // we can use outlineLevel 1,2,3
                if (
                    paragraphs.items[i].styleBuiltIn === 'Heading1' ||
                    paragraphs.items[i].styleBuiltIn === 'Heading2' ||
                    paragraphs.items[i].styleBuiltIn === 'Heading3'
                ) {
                    headers.push(paragraphs.items[i]);
                }
            }
  • one first step we found Heading1,2,3.Now we have to create xml for TOC and we will insert in start of body
body.insertOoxml(
                `<pkg:package xmlns:pkg='http://schemas.microsoft.com/office/2006/xmlPackage'>
            <pkg:part pkg:name='/_rels/.rels' pkg:contentType='application/vnd.openxmlformats-package.relationships+xml' pkg:padding='512'>
                <pkg:xmlData>
                    <Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'>
                        <Relationship Id='rId1' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument' Target='word/document.xml'/>
                    </Relationships>
                </pkg:xmlData>
            </pkg:part>
            <pkg:part pkg:name='/word/document.xml' pkg:contentType='application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'>
                <pkg:xmlData>
                    <w:document xmlns:w='http://schemas.openxmlformats.org/wordprocessingml/2006/main'>
                        <w:body>
                            <w:sdt>
                                <w:sdtPr>
                                    <w:lock w:val="contentLocked"/>
                                </w:sdtPr>
                                <w:sdtContent>
                                    <w:p>
                                        <w:pPr>
                                            <w:alias w:val="Table of Contents"/>
                                            <w:tag w:val="TOCWinplaybox"/>
                                            <w:spacing w:after="480"/>
                                            <w:jc w:val="center"/>
                                            <w:pStyle w:val='TOCHeading'/>
                                        </w:pPr>
                                        <w:r>
                                            <w:rPr>
                                                <w:keepNext/>
                                                <w:keepLines/>
                                                <w:rFonts w:asciiTheme="majorHAnsi" w:eastAsiaTheme="majorEastAsia" w:hAnsiTheme="majorHAnsi" w:cstheme="majorBidi"/>
                                                <w:sz w:val="32"/>
                                                <w:szCs w:val="32"/>
                                                <w:b/>
                                                <w:caps/>
                                            </w:rPr>
                                            <w:t>Table of Contents</w:t>
                                        </w:r>
                                    </w:p>  
                                    ${insertion}
                                </w:sdtContent>
                            </w:sdt>
                        </w:body>
                    </w:document>
                </pkg:xmlData>
            </pkg:part>
        </pkg:package>`,
                Word.InsertLocation.start
            );

// “Insertion” is dynamic heading 1,2,3 coming up

now we have xml to insert our heading 1,2,3 in xml where found out in our paragraphs items and we will loop our header for inserting dynamic Heading1,2,3 and whenever we are inserting heading on that time we will insert bookmark also because we need to navigate from TOC heading1 to particular Heading 1

headers.map(async function (item) {
                const bookmarkName = `_Toc${item.outlineLevel + item._Id + uniqueStr}`;
                insertion += `<w:p>
                    <w:pPr>
                        <w:pStyle w:val='TOC${item.outlineLevel}'/>
                        <w:tabs>
                            <w:tab w:val='right' w:leader='dot' w:pos='9000'/>
                            <w:tab w:val="left" w:pos="9000" w:leader='dot'/>
                        </w:tabs>
                    </w:pPr>
                    <w:pPr>
                        <w:spacing w:after="0" w:before="0" w:beforeAutospacing="0" w:afterAutospacing="0" w:lineRule="auto" w:line="200"/>
                        ${
                            item.outlineLevel === 2
                                ? `<w:ind w:left='220'/>`
                                : item.outlineLevel === 3
                                ? `<w:ind w:left='440'/>`
                                : `<w:ind w:left='0'/>`
                        }
                    </w:pPr>
                    <w:r>
                        <w:rPr>
                            <w:noProof/>
                            <w:webHidden/>
                        </w:rPr>
                    </w:r>
                    <w:p>
                        <w:r>
                            <w:fldChar w:fldCharType="begin"/>
                        </w:r>
                        <w:r>
                            <w:instrText xml:space="preserve"> REF ${bookmarkName} \\h </w:instrText>
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType="separate"/>
                        </w:r>
                        <w:r>
                            <w:t>${item.text.toString()}</w:t>
                        </w:r>
                        <w:r>
                            <w:tab/>
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType='separate'/>
                        </w:r>
                        <w:r>
                        <w:t xml:space="preserve">1</w:t> 
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType="end"/>
                        </w:r>
                    </w:p>
                </w:p>`;
                const insertRange: Word.Range = item.getRange();
                insertRange.insertBookmark(bookmarkName);
            });

if we do not have any content than we will add below code [i mean for not found content]

insertion = `<w:p>
                <w:r>
                    <w:fldChar w:fldCharType="begin"/>
                </w:r>
                <w:r>
                    <w:instrText xml:space="preserve"> TOC \\o "1-3" \\h \z \\u </w:instrText>
                </w:r>
                <w:r>
                    <w:fldChar w:fldCharType="separate"/>
                </w:r>
                <w:r>
                    <w:rPr>
                        <w:b/>
                        <w:bCs/>
                        <w:noProof/>
                    </w:rPr>
                    <w:t>No table of contents entries found.</w:t>
                </w:r>
                <w:r>
                    <w:rPr>
                        <w:b/>
                        <w:bCs/>
                        <w:noProof/>
                    </w:rPr>
                    <w:fldChar w:fldCharType="end"/>
                </w:r>
            </w:p>`;
await context.sync();
        }).catch((error)=>{
 console.log('Error: ' + error);
        if (error instanceof OfficeExtension.Error) {
            console.log('Debug info: ' + JSON.stringify(error.debugInfo));
            console.log('Trace info: ' + JSON.stringify(error.traceMessages));
        }
});

Now we have TOC in Office js with the help of XML [insertOoxml]

FULL CODE:

Word.run(async function (context) {
            const currentDoc = context.document;
            const body = currentDoc.body;
            const paragraphs = currentDoc.body.paragraphs.load('items');
            const headers = [];
            const uniqueStr = new Date().getTime();
            let insertion = '';
            await context.sync();
            for (let i = 0; i < paragraphs.items.length; i++) {
                // we can use outlineLevel 1,2,3
                if (
                    paragraphs.items[i].styleBuiltIn === 'Heading1' ||
                    paragraphs.items[i].styleBuiltIn === 'Heading2' ||
                    paragraphs.items[i].styleBuiltIn === 'Heading3'
                ) {
                    headers.push(paragraphs.items[i]);
                }
            }

if (headers.length > 0) {
                headers.map(async function (item) {
                    const bookmarkName = `_Toc${item.outlineLevel + item._Id + uniqueStr}`;
                    insertion += `<w:p>
                    <w:pPr>
                        <w:pStyle w:val='TOC${item.outlineLevel}'/>
                        <w:tabs>
                            <w:tab w:val='right' w:leader='dot' w:pos='9000'/>
                            <w:tab w:val="left" w:pos="9000" w:leader='dot'/>
                        </w:tabs>
                    </w:pPr>
                    <w:pPr>
                        <w:spacing w:after="0" w:before="0" w:beforeAutospacing="0" w:afterAutospacing="0" w:lineRule="auto" w:line="200"/>
                        ${
                            item.outlineLevel === 2
                                ? `<w:ind w:left='220'/>`
                                : item.outlineLevel === 3
                                ? `<w:ind w:left='440'/>`
                                : `<w:ind w:left='0'/>`
                        }
                    </w:pPr>
                    <w:r>
                        <w:rPr>
                            <w:noProof/>
                            <w:webHidden/>
                        </w:rPr>
                    </w:r>
                    <w:p>
                        <w:r>
                            <w:fldChar w:fldCharType="begin"/>
                        </w:r>
                        <w:r>
                            <w:instrText xml:space="preserve"> REF ${bookmarkName} \\h </w:instrText>
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType="separate"/>
                        </w:r>
                        <w:r>
                            <w:t>${item.text.toString()}</w:t>
                        </w:r>
                        <w:r>
                            <w:tab/>
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType='separate'/>
                        </w:r>
                        <w:r>
                        <w:t xml:space="preserve">1</w:t> 
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType="end"/>
                        </w:r>
                    </w:p>
                </w:p>`;
                    const insertRange: Word.Range = item.getRange();
                    insertRange.insertBookmark(bookmarkName);
                });
            } else {
                insertion = `<w:p>
                <w:r>
                    <w:fldChar w:fldCharType="begin"/>
                </w:r>
                <w:r>
                    <w:instrText xml:space="preserve"> TOC \\o "1-3" \\h \z \\u </w:instrText>
                </w:r>
                <w:r>
                    <w:fldChar w:fldCharType="separate"/>
                </w:r>
                <w:r>
                    <w:rPr>
                        <w:b/>
                        <w:bCs/>
                        <w:noProof/>
                    </w:rPr>
                    <w:t>No table of contents entries found.</w:t>
                </w:r>
                <w:r>
                    <w:rPr>
                        <w:b/>
                        <w:bCs/>
                        <w:noProof/>
                    </w:rPr>
                    <w:fldChar w:fldCharType="end"/>
                </w:r>
            </w:p>`;
            }
            
            body.insertOoxml(
                `<pkg:package xmlns:pkg='http://schemas.microsoft.com/office/2006/xmlPackage'>
            <pkg:part pkg:name='/_rels/.rels' pkg:contentType='application/vnd.openxmlformats-package.relationships+xml' pkg:padding='512'>
                <pkg:xmlData>
                    <Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'>
                        <Relationship Id='rId1' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument' Target='word/document.xml'/>
                    </Relationships>
                </pkg:xmlData>
            </pkg:part>
            <pkg:part pkg:name='/word/document.xml' pkg:contentType='application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'>
                <pkg:xmlData>
                    <w:document xmlns:w='http://schemas.openxmlformats.org/wordprocessingml/2006/main'>
                        <w:body>
                            <w:sdt>
                                <w:sdtPr>
                                    <w:lock w:val="contentLocked"/>
                                </w:sdtPr>
                                <w:sdtContent>
                                    <w:p>
                                        <w:pPr>
                                            <w:alias w:val="Table of Contents"/>
                                            <w:tag w:val="TOCWinplaybox"/>
                                            <w:spacing w:after="480"/>
                                            <w:jc w:val="center"/>
                                            <w:pStyle w:val='TOCHeading'/>
                                        </w:pPr>
                                        <w:r>
                                            <w:rPr>
                                                <w:keepNext/>
                                                <w:keepLines/>
                                                <w:rFonts w:asciiTheme="majorHAnsi" w:eastAsiaTheme="majorEastAsia" w:hAnsiTheme="majorHAnsi" w:cstheme="majorBidi"/>
                                                <w:sz w:val="32"/>
                                                <w:szCs w:val="32"/>
                                                <w:b/>
                                                <w:caps/>
                                            </w:rPr>
                                            <w:t>Table of Contents</w:t>
                                        </w:r>
                                    </w:p>  
                                    ${insertion}
                                </w:sdtContent>
                            </w:sdt>
                        </w:body>
                    </w:document>
                </pkg:xmlData>
            </pkg:part>
        </pkg:package>`,
                Word.InsertLocation.start
            );
            await context.sync();
        }).catch((error)=>{
 console.log('Error: ' + error);
        if (error instanceof OfficeExtension.Error) {
            console.log('Debug info: ' + JSON.stringify(error.debugInfo));
            console.log('Trace info: ' + JSON.stringify(error.traceMessages));
        }
});

One thought on “How to create Table of contents in O365 add-in

Leave a Reply

Your email address will not be published.