Check if text selection is in header/footer - ms-word

The Word Addin we created allows adding custom comments to text selections. Word does not allow adding comments in headers / footers. Because of that, users should get warned when text in a header/footer is selected.
The selection's OOXML structure for text in body and text in header is identical.
The Word UI itself disabled the review comments section when footer/header text is selected.
When dumping the text selection object to te console, none of the object fields point to the selection being in header/footer.
How can be found out programmatically that text is selected in the header/footer?
Issue: https://github.com/OfficeDev/office-js/issues/341

You can achieve this by looking at the parentBody property of the selection range. The type property on the parentBody will reveal whether the selection is in the 'Header' or elsewhere (see documentation).
Example
function determineSelectionInHeader() {
Word.run(function (context) {
const HEADER_TYPE = "Header";
// Retrieve and load 'type' of selection.
var selection = context.document.getSelection();
var parentBody = selection.parentBody;
parentBody.load("type");
context
.sync()
.then(function () {
if (parentBody.type === HEADER_TYPE) {
console.log("This is the header");
}
});
});
}

Related

Understanding binding and selection in Word Add-in

I'm trying to build an add-in with similar behaviour like the comment system.
I select a part of text.
Press a button in my add-in. A card is created that links to that text.
I do something else, like write text on a different position.
When I press the card in my add-in, I'd like to jump back to the selected text (in point 1).
I studied the API, documentation. And learned that I could do something like that with Bindings. A contentcontrol might also be an option, although I noticed that you can't connect and eventhandler (it's in beta). I might need an eventhandler to track changes later.
Create binding (step 2)
Office.context.document.bindings.addFromSelectionAsync(Office.BindingType.Text, { id: 'MyBinding' }, (asyncResult) => {
if (asyncResult.status == Office.AsyncResultStatus.Failed) {
console.log('Action failed. Error: ' + asyncResult.error.message);
} else {
console.log('Added new binding with id: ' + asyncResult.value.id);
}
});
Works. Then I click somewhere else in my document, to continue with step 4.
View binding (step 4).
So I click the card and what to jump back to that text binding, with the binding selected.
I figured there are multiple ways.
Method #1
Use the Office.select function below logs the text contents of the binding. However, it doesn't select that text in the document.
Office.select("bindings#MyBinding").getDataAsync(function (asyncResult) {
if (asyncResult.status == Office.AsyncResultStatus.Failed) {
}
else {
console.log(asyncResult.value);
}
});
Method #2
Use the GoToById function to jump to the binding.
Office.context.document.goToByIdAsync("MyBinding", Office.GoToType.Binding, function (asyncResult) {
let val = asyncResult.value;
console.log(val);
});
This shows like a blue like frame around the text that was previously selected and puts the cursor at the start.
I'd prefer that I don't see that frame (no idea if that's possible) and I would like to the text selected.
There is the Office.GoToByIdOptions interface that mentions:
In Word: Office.SelectionMode.Selected selects all content in the binding.
I don't understand how pass that option in the function call though and I can't find an example. Can I use this interface to get the selection?
https://learn.microsoft.com/en-us/javascript/api/office/office.document?view=common-js-preview#office-office-document-gotobyidasync-member(1)
goToByIdAsync(id, goToType, options, callback)
If there are other ways to do this, I'd like to know that as well.
With some help I could figure it out. I learned that an Interface is just an object.
So in this case:
const options = {
selectionMode: Office.SelectionMode.Selected
};
Office.context.document.goToByIdAsync("MyBinding", Office.GoToType.Binding, options, function (asyncResult) {
console.log(asyncResult);
});
This gives the selected result.
Sure someone can provide a better answer than this, as it's unfamiliar territory for me, but...
When you create a Binding from the Selection in Word, you're going to get a Content Control anyway. So to avoid having something that looks like a content control with the blue box, you either have to modify the control's display or you have to find some other way to reference a region of your document. In the traditional Word Object model, you could use a bookmark, for example. But the office-js APIs do not seem very interested in them.
However, when you create a Binding, which is an Office object, you don't get immediate access to the Content Control's properties (since that's a Word object). So instead of creating the Binding then trying to modify the Content Control, you may be better off creating the Content Control then Binding to it.
Something like this:
async function markTarget() {
Word.run(async (context) => {
const cc = context.document.getSelection().insertContentControl();
// "Hidden" means you don't get the "Bounding Box"
// (blue box with Title), or the Start/End tag view
cc.appearance = "Hidden";
// Provide a Title so we have a Name to bind to
cc.title = "myCC";
// If you don't want users changing the content, you
// could uncomment the following line
//cc.cannotDelete = true;
return context.sync()
.then(
() => {
console.log("Content control inserted");
// Now create a binding using the named item
Office.context.document.bindings.addFromNamedItemAsync("myCC",
Office.BindingType.Text,
{ id: 'MyBinding' });
},
() => console.log("Content control insertion failed")
).then(
() => console.log("Added new binding"),
() => console.log("Binding creation failed")
)
});
}
So why not just create the ContentControl, name it, and then you should be able to select it later using its Title, right? Well, getting the "data" from a control is one thing. Actually selecting it doesn't seem straightforward in the API, whereas Selecting a Binding seems to be.
So this code is pretty similar to your approach, but adds the parameter to select the whole text. The syntax for that is really the same syntax as { id: 'MyBinding' } in the code you already have.
function selectTarget() {
Office.context.document.goToByIdAsync(
"MyBinding",
Office.GoToType.Binding,
{ selectionMode: Office.SelectionMode.Selected },
function(asyncResult) {
let val = asyncResult.value;
console.log(val);
}
);
}
Both the Binding and the ContentControl (and its Title) are persisted when you save/reopen the document. In this case, the Binding is persisted as a piece of XML that stores the type ("text"), name ("MyBinding") and a reference to the internal ID of the content control, which is a 32-bit number, although that is not immediately obvious when you look at the XML - in an example here, the Id Word stores for the ContentControl is -122165626, but "Office" stores the ID for the Binding as 4172801670, but that's because they are using the two different two's complement representations of the same number.

How can I detect a content control without the user selecting the whole thing?

Background
I've inserted a bunch of content controls into my word document. Each content control is a bit of text e.g. "Hello world".
What I'm trying to do
When a user puts their cursor within the content control I want to access the details of the content control within my add-in.
What I've tried
I run this at startup.
Office.context.document.addHandlerAsync(Office.EventType.DocumentSelectionChanged, () => {
Word.run( async (context) => {
const range = context.document.getSelection();
console.log({range});
const contentControls = range.contentControls;
contentControls.load("items");
await context.sync();
console.log('contentControls.items', contentControls.items)
})
})
Problem
If a user pops their cursor in the content control no "items" are reported. However if a user highlights the whole content control the "items" are correctly reported.
Question
Is there a way to detect if a user is within a content control without them having to select the whole thing?
Try using Range.parentContentControl (or parentContentControlOrNullObject) to get a reference to the containing content control.
For example,
const parentContentControl = range.parentContentControlOrNullObject;
await context.sync();
if (parentContentControl.isNullObject) {
console.log("The cursor is not in a content control")
}
else {
console.log('parentContentControl', parentContentControl);
}

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 to remove selection after replace in vscode API

while creating an extension for vscode I got stuck in selection, now the problem is when I replace some range of textEditor through an api it replaces that range as well as make that range selected. For snippets this is a good idea but my extension requirement is not to select replaced text, I searched in api but did't find anything related to remove text selection (Selection occurs when the document is empty)
editor.edit((editBuilder)=>{ //editor is an object of active text editor
editBuilder.replace(textRange,text) // text = 'dummydummydummy'
}) //after this I got the following output
editor.edit(builder => {
builder.replace(selection, newStr);
})
// The edit call returns a promise. When that resolves you can set
// the selection otherwise you interfere with the edit itself.
// So use "then" to sure edit call is done;
.then(success => {
console.log("success:", success);
// Change the selection: start and end position of the new
// selection is same, so it is not to select replaced text;
var postion = editor.selection.end;
editor.selection = new vscode.Selection(postion, postion);
});
I believe that this is happening because the edit is being applied within the current selection. edit returns a promise that is resolved when the edit is applied, and you can use this to set the selection after the edit is successful:
editor.edit((editBuilder) => {
editBuilder.replace(textRange, text)
}).then(success => {
if (success) {
// make selection empty
editor.selection.active = editor.selection.anchor
}
})
let editor = vscode.window.activeTextEditor;
let selection = editor.selection;
editor.edit(builder => {
builder.replace(selection, newStr);
});
see: TextEditorEdit API Doc

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!