How we can update Table of contents [TOC] in O365/Word API JavaScript/Office JS
To creating TOC we had done in pervious post click here.
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]);
}
}
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>`;
}
- one first step we found Heading1,2,3.Now we need to find tag in content controls [TOCWinplaybox]
context.load(body, ['items', 'contentControls']);
await context.sync();
const contentControlsWithTag = context.document.contentControls.getByTag('TOCWinplaybox');
// Queue a command to load the tag property for all of content controls.
context.load(contentControlsWithTag, 'tag');
await context.sync();
- now we will find content controls is exists or not
if (contentControlsWithTag.items.length === 0) {
// create content controls[contentControlsWithTag] not found
insertXML(context.document.body, insertion, Word.InsertLocation.start);
context.document.body.paragraphs.getFirst().select();
} else {
return context.sync().then(function () {
//the contentControlsWithTag is always returned as an array of items
contentControlsWithTag.items[0].cannotEdit = false;
insertXML(contentControlsWithTag.items[0], insertion, Word.InsertLocation.replace);
contentControlsWithTag.items[0].getRange('Start').select();
});
}
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));
}
});
- InsertXML function rather than writing multiple times
const insertXML = (property: Word.Body | Word.ContentControl, insertion: string, location: Word.InsertLocation) => {
return property.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>`,
location
);
};
Now we have inserting + updating feature of 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>`;
}
context.load(body, ['items', 'contentControls']);
await context.sync();
const contentControlsWithTag = context.document.contentControls.getByTag('TOCWinplaybox');
// Queue a command to load the tag property for all of content controls.
context.load(contentControlsWithTag, 'tag');
await context.sync();
if (contentControlsWithTag.items.length === 0) {
// create content controls[contentControlsWithTag] not found
insertXML(context.document.body, insertion, Word.InsertLocation.start);
context.document.body.paragraphs.getFirst().select();
} else {
return context.sync().then(function () {
//the contentControlsWithTag is always returned as an array of items
contentControlsWithTag.items[0].cannotEdit = false;
insertXML(contentControlsWithTag.items[0], insertion, Word.InsertLocation.replace);
contentControlsWithTag.items[0].getRange('Start').select();
});
}
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));
}
});
const insertXML = (property: Word.Body | Word.ContentControl, insertion: string, location: Word.InsertLocation) => {
return property.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>`,
location
);
};