How can i retrive and manipulate "chapters" and "subchapters" of a word document based on headings? - ms-word

A client would like me to build an app or add-in where they can search for a word and display every chapter the word is found in. Then they would like to be able to select chapters and have them exported to a separate document. Basically a document assembler/builder but working just with chapters/sections.
I've looked into the office.js documentation and there doesn't seem to be a way for me to interact with the table of contents or sections based on headings. I guess i could edit the document, remove all sections and add in new ones where a chapter begins, but that would require a lot of manual editing and id like to automate this as much as possible.
My second thought was to grab each heading and the paragraphs until the next heading is hit, but the documentation is a bit sparse and Im not sure what the best way to do that is.
So is there anyway with office.js to cleanly separate chapters/sections?

The following loops over paragraphs and looks for words with the style heading 1 or heading 2 and adds their ranges to headingRanges[].
headingRanges[] is later looped over and a new range, deltaRange, is created by expanding each heading range to the start of the following heading.
I then output the deltaRange's text to console.
This chops up chapters with sub-chapters in them, but if that functionality is necessary it shouldn't be too difficult to build on top of this.
let headings = [];
let headingRanges = [];
Word.run(async context => {
let paragraphs = context.document.body.paragraphs;
context.load(paragraphs, ["items"]);
await context.sync();
for (let i = 0; i < paragraphs.items.length; ++i) {
let item = paragraphs.items[i];
context.load(item, ["text", "style", "styleBuiltIn"]);
await context.sync();
if ( item.style === "heading 1" || item.style === "heading 2"){
//console.log("(" + item.style + ")\n" + item.text);
headings.push(item.text);
let itemRange = item.getRange("start");
itemRange.track();
headingRanges.push(itemRange);
}
}
for(let i = 0; i < headingRanges.length-1; i++){
let deltaRange = headingRanges[i].expandTo(headingRanges[i+1]);
let chapterText = deltaRange.text;
await context.sync();
console.log(chapterText);
}
});
I'm now having issues with keeping track() of ranges and having Word.run() use that ranges context, but ill make a separate post for that.

Related

How to add and edit a short answer in multiple google forms

So I have multiple google forms (around 20 forms), that I need to do 2 things to them:
1- These 20 forms are placed in a folder in my google drive. I need to add more like an "Access code" where users will have to insert in order to continue the solving the quiz.
The way I did that was to add a "short answer" question to "section 1" of the quiz asking "Enter your Access Code", add "response validation", "Regular expression" and "Pattern". Also making this a "required question". This should look something like the below picture
Example of google form
So is it possible to have a scriptto add this question to all 20 forms
2- The "access code" in these google forms will have to be updated frequently, so I don' want to be updating the "Pattern" manually for each form, is t possible to have a google script to edit the value of the pattern for each form
Thanks in advance guys :)
I managed to solve this issue that I was having, through looking for different codes and here are the codes that I used.
N.B. The codes might not be very clean as I was copying them from other parts/projects, but they have worked for me
1- Update the 20 forms with adding the access code question, I figured it was not possible to add a question at a certain position in the google form, however I can add a question at the end of the form and then move this item to the position I want:
function AddAccesscodeQ() {
var filess = DriveApp.getFolderById("Drive id>").getFiles();
while (filess.hasNext()) {
var file = filess.next();
var form = FormApp.openById(file.getId());
var sectionIndex= 0; // Please set the index you want to insert.
//I added a "sample item" to be moved and edited later
var newItemQ = form.addTextItem().setTitle("New sample item").getIndex(); // New sample item
// I added a Pagebreak that also should be moved after the questions "Enter Your Access Code"
var newItemPB = form.addPageBreakItem().getIndex();
var items = form.getItems(FormApp.ItemType.PAGE_BREAK);
var sections = [0];
for (var i = 0; i < items.length; i++) {
// I pushed the items in the google form twice downwards, to be able to move the "sample item" and "Page break" to the top of the form
sections.push(items[i].getIndex());
sections.push(items[i].getIndex());
}
var insertIndex = sections[sectionIndex + 1] || null;
if (insertIndex) {
// Here I moved the 2 new items to the desired positions
form.moveItem(newItemQ, 0);
form.moveItem(newItemPB, 1);
}
// Here I am going to edit the "Sample Question" to be as desired
var itemss = form.getItems();
var itemID = itemss[0].getId();
var itemse = form.getItemById(itemID).asTextItem()
.setTitle('Enter Your Access Code').setRequired(true);
//Create validation rule
var validation = FormApp.createTextValidation()
.setHelpText('Invalid Code')
.requireTextMatchesPattern("<Access Code>")
.build();
itemse.setValidation(validation);
}
}
2- The second problem was that I later might need to change this access code to a new one for the 20 forms
function UpdateAccessCode() {
var filesPhCH = DriveApp.getFolderById("<Drive ID>").getFiles();
while (filesPhCH.hasNext()) {
var file = filesPhCH.next();
var form = FormApp.openById(file.getId());
var items = form.getItems();
//Loop through the items and list them
for (var i = 0;i<items.length;i++){
var item = items[i];
var itemID = item.getId();
var itemtitle = item.getTitle();
var itemindex = item.getIndex();
// I found no need to continue the for loop since the items that need modification are at the top of the form
if (itemindex == 0){
break;
}
}
//Select the question you want to update
var itemse = form.getItemById(itemID).asTextItem()
.setTitle('Enter Your Access Code');
//Create validation rule
var validation = FormApp.createTextValidation()
//.setTitle('Enter Your Access Code');
.setHelpText('Invalid Code')
.requireTextMatchesPattern("<Enter the new Access Code>")
.build();
itemse.setValidation(validation);
}
}
I hope this might help someone as it has saved a lot of time for me ;)

Text style in google script TextItem title

I have written an app to automatically update an order form everytime an order is passed. Currently, the form consists in N Textitems, which titles are like:
Product (remains : [number of remaining products])
Product description
This is performed by the following lines :
var Products= wsStocks.getRange(1,1,wsStocks.getLastRow(),1).getValues();
var Description = wsStocks.getRange(1,2,wsStocks.getLastRow(),2).getValues();
var Qtys = wsStocks.getRange(1,3,wsStocks.getLastRow(),3).getValues();
for(j=0;j<Products.length;j++){
Items[j].setTitle( `${Products[j][0]} (remains: ${Qtys[j][0]})`+ "\n" +`${Description[j][0]}`;
};
I would like to set a text style for this title : I want the information on the number of remaining products to be in italic, and the description to be in small size. But while I have found how to set the style for a Text variable, I can't find how to do this for a string used in the setTitle method ?
You should get the Range of each item from the Items array first and then you should be able to change the TextStyle according to your needs by using the setTextStyle() method.
For customizing the text styles, you can create your own, something similar to this.
// italic text style
var style1 = SpreadsheetApp.newTextStyle()
.setFontSize(14)
.setItalic(true)
.build();
// small size text style
var style2 = var style = SpreadsheetApp.newTextStyle();
.setFontSize(11)
.build();
Afterwards, for each element of the Items array, you should do this
for(j = 0; j < Products.length; j++)
sheet.getRange("RANGE_OF_Items[j]_ELEMENT").setTextStyle(style1); //or style2
Reference
Apps Script Class TextStyle;
Apps Script Range Class - setTextStyle(style).

Insert a line break with inserted text in a Word document

Building a word add-in using javascript (office.js) to insert text. So far unformatted text with .insertText. If I would like to insert the below, which function should be used?
formatted text (for instance size, font, style)
Line break
Bullet point
Code:
results.items[i].insertText("Any text going here.", "replace");
How would I for instance insert a line break in the "Any text going here"?
Using JavaScript, add a "line-break" (I'm assuming you mean the same as pressing ENTER in the UI - this is technically a new paragraph) using the string "\n". So, for example:
results.items[i].insertText("Any text going here.\n", "replace");
Use insertBreak for inserting breaks of different types. It could be line break, paragraph break, section break etc.
insertBreak(breakType: Word.BreakType, insertLocation: Word.InsertLocation): void;
For adding lists like bullet points. Use startNewList
startNewList(): Word.List;
List example
//This example starts a new list stating with the second paragraph.
await Word.run(async (context) => {
let paragraphs = context.document.body.paragraphs;
paragraphs.load("$none"); //We need no properties.
await context.sync();
var list = paragraphs.items[1].startNewList(); //Indicates new list to be started in the second paragraph.
list.load("$none"); //We need no properties.
await context.sync();
//To add new items to the list use start/end on the insert location parameter.
list.insertParagraph('New list item on top of the list', 'Start');
let paragraph = list.insertParagraph('New list item at the end of the list (4th level)', 'End');
paragraph.listItem.level = 4; //Sets up list level for the lsit item.
//To add paragraphs outside the list use before/after:
list.insertParagraph('New paragraph goes after (not part of the list)', 'After');
await context.sync();
});
For formatting text, you can get hints by looking at examples here which set Font family and color of text.
//adding formatting like html style
var blankParagraph = context.document.body.paragraphs.getLast().insertParagraph("", "After");
blankParagraph.insertHtml('<p style="font-family: verdana;">Inserted HTML.</p><p>Another paragraph</p>', "End");
// another example using modern Change the font color
// Run a batch operation against the Word object model.
Word.run(function (context) {
// Create a range proxy object for the current selection.
var selection = context.document.getSelection();
// Queue a commmand to change the font color of the current selection.
selection.font.color = 'blue';
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
console.log('The font color of the selection has been changed.');
});
})
.catch(function (error) {
console.log('Error: ' + JSON.stringify(error));
if (error instanceof OfficeExtension.Error) {
console.log('Debug info: ' + JSON.stringify(error.debugInfo));
}
});
The Word addin tutorial has a lot of nifty tricks for common tasks with code samples.

How get parent Range of current Selection in word add-in using JavaScript API

I need to insert a paragraph with ContentControl after the current selection paragraph, suppose the current selection is in middle of any paragraph, table or CC, I need to insert a new paragraph with CC after that.
I have tried below code to get the current selection and set range to end of that then will insert the paragraph after it:
var range = context.document.getSelection().getRange("end");
range.insertParagraph("","After");
but it insert the Paragraph after the current Selection, not after current selection parent.
Please advice. Thanks.
What you are observing is by design. You are getting the range of the selection. What you need to do is to get the range of the paragraph and then add another after.
All ranges have a paragraphs collection, the first paragraph will the the paragraph containing the selection, so you can get tit by calling:
context.document.getSelection().paragraphs.getFirst().getRange().insertParagraph("",after");
the full code sample would look like this:
Word.run(async (context) => {
var myParagraph = context.document.getSelection().paragraphs.getFirst().getRange().insertParagraph("", "after")
myParagraph.insertContentControl();
return context.sync();
})
.catch(function (error) {
console.log(error.message)
})
Note: if the selection expands more than one paragraph, probably you would need to do a getLast() instead of a getFirst(), but i am not sure your exact scenario.
thanks!

jsTree Node Expand/Collapse

I ran into the excellent jstree jQuery UI plug in this morning. In a word - great! It is easy to use, easy to style & does what it says on the box. The one thing I have not yet been able to figure out is this - in my app I want to ensure that only one node is expanded at any given time. i.e. when the user clicks on the + button and expands a node, any previously expanded node should silently be collapsed. I need to do this in part to prevent the container div for a rather lengthy tree view from creating an ugly scrollbar on overflow and also to avoid "choice overload" for the user.
I imagine that there is some way of doing this but the good but rather terse jstree documentation has not helped me to identify the right way to do this. I would much appreciate any help.
jsTree is great but its documentation is rather dense. I eventually figured it out so here is the solution for anyone running into this thread.
Firstly, you need to bind the open_node event to the tree in question. Something along the lines of
$("tree").jstree({"themes":objTheme,"plugins":arrPlugins,"core":objCore}).
bind("open_node.jstree",function(event,data){closeOld(data)});
i.e. you configure the treeview instance and then bind the open_node event. Here I am calling the closeOld function to do the job I require - close any other node that might be open. The function goes like so
function closeOld(data)
{
var nn = data.rslt.obj;
var thisLvl = nn;
var levels = new Array();
var iex = 0;
while (-1 != thisLvl)
{
levels.push(thisLvl);
thisLvl = data.inst._get_parent(thisLvl);
iex++;
}
if (0 < ignoreExp)
{
ignoreExp--;
return;
}
$("#divElements").jstree("close_all");
ignoreExp = iex;
var len = levels.length - 1;
for (var i=len;i >=0;i--) $('#divElements').jstree('open_node',levels[i]);
}
This will correctly handle the folding of all other nodes irrespective of the nesting level of the node that has just been expanded.
A brief explanation of the steps involved
First we step back up the treeview until we reach a top level node (-1 in jstree speak) making sure that we record every ancestor node encountered in the process in the array levels
Next we collapse all the nodes in the treeview
We are now going to re-expand all of the nodees in the levels array. Whilst doing so we do not want this code to execute again. To stop that from happening we set the global ignoreEx variable to the number of nodes in levels
Finally, we step through the nodes in levels and expand each one of them
The above answer will construct tree again and again.
The below code will open the node and collapse which are already opened and it does not construct tree again.
.bind("open_node.jstree",function(event,data){
closeOld(data);
});
and closeOld function contains:
function closeOld(data)
{
if($.inArray(data.node.id, myArray)==-1){
myArray.push(data.node.id);
if(myArray.length!=1){
var arr =data.node.id+","+data.node.parents;
var res = arr.split(",");
var parentArray = new Array();
var len = myArray.length-1;
for (i = 0; i < res.length; i++) {
parentArray.push(res[i]);
}
for (var i=len;i >=0;i--){
var index = $.inArray(myArray[i], parentArray);
if(index==-1){
if(data.node.id!=myArray[i]){
$('#jstree').jstree('close_node',myArray[i]);
delete myArray[i];
}
}
}
}
}
Yet another example for jstree 3.3.2.
It uses underscore lib, feel free to adapt solution to jquery or vanillla js.
$(function () {
var tree = $('#tree');
tree.on('before_open.jstree', function (e, data) {
var remained_ids = _.union(data.node.id, data.node.parents);
var $tree = $(this);
_.each(
$tree
.jstree()
.get_json($tree, {flat: true}),
function (n) {
if (
n.state.opened &&
_.indexOf(remained_ids, n.id) == -1
) {
grid.jstree('close_node', n.id);
}
}
);
});
tree.jstree();
});
I achieved that by just using the event "before_open" and close all nodes, my tree had just one level tho, not sure if thats what you need.
$('#dtree').on('before_open.jstree', function(e, data){
$("#dtree").jstree("close_all");
});