Getting empty array on contentControls officejs - javascript

Word.run( async (context) => {
var searchResults = context.document.body.search('moslim');
context.load(searchResults);
return context.sync().then(async function () {
if (searchResults.items.length > 0) {
// Get the first search result
var range = searchResults.items[0].getRange();
// range.clear();
// Insert a content control around the search result
var contentControl = range.insertContentControl();
// Set the tag of the content control
contentControl.tag = 'your tag';
contentControl.insertText(searchResults.items[0].text.toString(), 'Replace');
// Load the content control
context.load(contentControl);
await context.sync()
var getcontentControl = range.contentControls
context.load(getcontentControl)
await context.sync()
console.log(getcontentControl.items)
}
});
});
last console.log gives empty array but contentcontrol has been added the ms word document.
Can anyone help on this what I did wrong?

I don't think you did anything "wrong" It's just that, despite the fact that you are calling a method called insertContentControl on your Range object, office-js surrounds the range with the control (at least in the case you mention) in such a way that it is not actually added to the range.
The question is what you are actually trying to achieve and how to do that. (It wasn't obvious from the Question as it stands).
If you did the equivalent thing in VBA to your found text, it might be
myrange.ContentControls.Add wdContentControlRichText
In that case, in VBA the content control is in myrange, i.e.
myrange.ContentControls.Count
returns 1. But it wouldn't be in myrange if myrange did not "cover" any characters (i.e. myrange.Start=myrange.End )
I made a simple modification to your office-js code to select the range it found. I also commented out the code that puts text in the control
var contentControl = range.insertContentControl();
range.select();
// Set the tag of the content control
contentControl.tag = 'your tag';
//contentControl.insertText(searchResults.items[0].text.toString(), 'Replace');
If I run your example code with those changes, it's quite easy to find out where the VBA WOrd object model thinks the control and the range are:
Sub rangeandccinfo()
Debug.Print ActiveDocument.ContentControls(1).Range.Start, ActiveDocument.ContentControls(1).Range.End
'In office-js we set by using the Range. So they should really be the same (except maybe we shouldn't
'expect office-js to behave the same as the traditional object model.
Debug.Print Selection.Start, Selection.End
Debug.Print Selection.Range.ContentControls.count
Debug.Print Selection.Range.Information(wdInContentControl)
End Sub
In my experiment, the results are like this
3 9
3 9
0
True
But if I just select the same word manually and insert the CC using VBA like this:
Sub ccrange()
Dim r As Range
Debug.Print Selection.Start, Selection.End
Set r = Selection.Range
'should be the same
Debug.Print r.Start, r.End
r.ContentControls.Add wdContentControlRichText
Debug.Print r.Start, r.End
r.Select
Set r = Nothing
End Sub
then run the rangeandccinfo() Sub, I see this:
3 9
2 9
1
True
i.e. in office-js, it looks as if the range and the range of the Content Control have the same Start and End, whereas in VBA, the Range starts one character before the Range of the Content Control, and that seems to be what make the VBA count of COntent COntrols in the Range 1 rather than 0.
If you actually click at the beginning of the text in a Content Control and click the left arrow button (in a document with left-to-right text) you will see that the insertion point does not appear to move. And yet its Range does change.
So it's at least partly an implementation choice how to do this. Personally, I have found it quite difficult to work with the way VBA does it (its difficult to work out what's inside what). It's quite possible that the office-js team encountered a similar problem and just decided to do it a way that made more sense to them.

Related

Create a Range spanning the character before cursor in Word Javascript API / Office.js

I'm trying to create an Office Javascript Add-in which will examine the character before the cursor and replace that character depending on what it is. So I need to create a Range of the character before the cursor. I can do this easily with a VBA macro, but unfortunately, I can't find a way to do this with the new javascript api. Is this possible?
If this is possible, it would also be helpful if I could look at the 5 characters before and after the cursor for added context.
Thanks.
A couple months ago I tried something similar. In short there is no good way to do it. You could try what I will specify below, but I would advice against it. The example is not thought through and most likely will contain a number of bugs. Additionally I find this an incredibly inefficient way to do something so simple.
Limitations in the API that prevent an easy solution:
There is no cursor, only selections. This means that you need to make an assumption that the cursor is always at the beginning of a selection.
Selections cannot be directly modified through the Office.js API. So it is not possible to expand the selection to include the previous character.
The 'Range' object does allow to be extended into both directions, but it requires another range as input. This means an earlier range needs to created/found (i.e. a range object before the current selection).
You can only navigate outside of the selection through the property 'parentBody' which will give you the entire body of the document. This needs to be processed in order to isolate a range before the cursor that could help us replace the character.
From what I can tell it is not possible to create a range for a single character. So a bigger range needs to be taken before the cursor and needs to replaced entirely.
Example
// WARNING: Incredibly inefficient and poor code. Do not use directly!
// WARNING: Edge cases are not tackled in this example.
function replaceCharacterBeforeCursor() {
Word.run(function (context) {
var selection = context.document.getSelection();
// Assumption: Cursor always starts at the beginning of a selection.
var cursor = selection.getRange('Start');
// Create a new range that covers everything before the cursor (or the beginning of the selection).
var startDocument = selection.parentBody.getRange("Start");
var rangeBeforeSelection = startDocument.expandTo(startDocument);
// Capture parent paragraph.
var parentParagraph = rangeBeforeSelection.paragraphs.getLast();
context.load(parentParagraph);
context
.sync()
.then(function () {
// Create range that captures everything from the beginning of the parent
// paragraph until the cursor.
var paragraphStart = parentParagraph.getRange('Start');
var wordRangeBeforeCursor = paragraphStart.expandTo(cursor);
context.load(wordRangeBeforeCursor);
context
.sync()
.then(function () {
// Replace last character.
var oldText = wordRangeBeforeCursor.text;
var wordLength = oldText.length;
var lastCharacter = oldText.substring(wordLength - 1);
if (lastCharacter !== " ") {
var newText = oldText.substring(0, wordLength - 1) + "test";
wordRangeBeforeCursor.insertText(newText, 'Replace');
context.sync();
}
});
});
});
}
Another way to do it is through text ranges. This would be substantially more inefficient. Either way I hope this will help you in finding a solution that fits your needs.

For loop keeps breaking out after 1 instance - Apps Script

I have looked everywhere I can think of for anything that can provide an answer to this. First time posting a question here - I can usually find my answers. I have a for loop that is pulling information from a range of data that is formatted in one cell like this: 09/01/2016 - Status changed to active.
The loop is supposed to first see how many values are in that column then go one by one and split the data into a simple array, post it into two columns on a separate sheet, then move onto the next one. The problem is that it stops after the first entry.
var numEntries = dataSheet.getRange(1,i+1,1000).getValues();
var lastEntry = numEntries.filter(String).length;
if (lastEntry == 7) {
// no change data to date
sheet.getRange(18,3).setValue("No changes yet");
} else {
var changeData = dataSheet.getRange(8,i+1,lastEntry-7).getValues();
for (var y = 0; y < changeData.length; y++) {
var changeHistory = changeData[y][y].split(" - ");
sheet.getRange(nextRow+1,2).setValue(changeHistory[0]);
sheet.getRange(nextRow+1,3).setValue(changeHistory[1]);
nextRow++;
Logger.log(nextRow);
Logger.log(changeData.length);
Logger.log(y);
}
}
I know that it is executing properly because it is properly setting the "No changes yet" value when there are no entries. Variable nextRow starts at a value of 17, and the log properly shows changeData.length with the number of entries and y being equal to 0. But then it stops. It doesn't follow the loop that y is still less than changeData.length. Any help is very much appreciated!
[edit] - I also want to point out that it does properly split and populate the first value into the two cells I want it to, so the whole for statement does work, it just doesn't loop. [edit]
[16-09-29 15:37:48:514 CDT] 18.0 [16-09-29 15:37:48:515 CDT]
11.0 [16-09-29 15:37:48:515 CDT] 0.0
changeData is an n*1 Array.
You are increasing y and and you are also trying to get the y-th column from changeData.
After the first iteration this is undefined because there's only one column.
undefined does not have a split method throwing an exception and terminating the script. (You may not see this exception, these kinds of exceptions aren't always shown to the user for some reason)
Try
var changeHistory = changeData[y][0].split(" - ");
instead.

For loop index undefined in IE9 for initial pass

On this page: http://koncordia.marketingassociates.com/19892arc/
I have a slideshow that I created custom prev/next links for. Each selection you make on the page advances it one slide forward. The progress bar at the top allows you to click a previous slide, and jump more than one back if you want (you can go from step 4 or step 1 for example).
This multi-step jump works fine in all the current major browsers, but the client uses IE9, and this is where I do not understand the source of the issue.
The following are the relevant methods in this issue. To mimic a user jumping back one or more slides I have a for loop iterate over simulatePrevClick() as many times as necessary; it's not sexy but it works.
The issue arises on the initial pass in IE9. The console spits out "undefined" for the first pass, but it says 0 for all other browsers (including IE 10 and 11) which is correct. If I remove the method call within the loop the iteration works perfectly, so it has something to do with the .click() event or way the method is called, but I don't know what.
No matter what, IE9 will show the immediate previous slide no matter how many they click back; the progress bar be out of sync if they click back more than one in this instance. The undefined result is not showing as an error, either.
//Highlight the right number of progress buttons
highlightProgressBar: function( slideNumber ) {
$(".btn-progress").attr('disabled', 'disabled').removeClass('active'); //Disabled all
$("#progress-wrapper a:lt(" + slideNumber + ")").removeAttr('disabled'); //Disable select number
$("#progress-wrapper a:eq(" + (slideNumber - 1) + ")").addClass('active'); //Add active to the specified button clicked
},
simulateNextClick: function () {
//The value of this must match what the responsiveslides function creates for the prev/next buttons (seen when you inspect element)
$(".transparent-btns_nav.transparent-btns1_nav.next").click();
},
simulatePrevClick: function () {
//The value of this must match what the responsiveslides function creates for the prev/next buttons (seen when you inspect element)
$(".transparent-btns_nav.transparent-btns1_nav.prev").click();
},
toggleProgressBar: function( clickedSlideNumber, activeSlideNumber ) {
var numSlides = activeSlideNumber - clickedSlideNumber;
for (var i=0; i < numSlides; i++) { //Anticipate user may click more than one step back
this.simulatePrevClick();
console.log(i); // **shows "undefined" on first pass in IE9 only**
}
this.highlightProgressBar(clickedSlideNumber);
}
Try to move the var i = 0 declaration out of the loop.
var i = 0;
for (; i < numSlides; i++) {}
It's really strange that that should happen.
This is just a guess, but I looked through the rest of your source code, and its possible that the root of your problem could be due to whenever you actually implement your toggleProgressBar function, in this area:
$(".btn-progress").click(function() {
var currentSlideID = $("#progress-wrapper").find('a.active').attr('id').split("-");
var clickedSlideID = $(this).attr('id').split("-");
slideFn.toggleProgressBar( clickedSlideID[1], currentSlideID[1] );
});
If I see right, your toggleProgressBar wants to accepts two numbers. However, what you're passing in are string literals:
slideFn.toggleProgressBar( "2", "1" );
ID attributes are output as strings, not numbers. I just tested the following in Chrome, and it worked:
"2" - "1" === 1 //true
This is because I guess V8 (Chrome's JS engine) coerces the two string literals into numbers. However, (while I have not tested it), this tells me that it's possible that IE might not be coercing the two strings into numbers (like I said, I don't know this for a fact, but this is something you might try debugging). Try this and see if it has any effect:
//first option
slideFn.toggleProgressBar( +clickedSlideID[1], +currentSlideID[1] );
//the + sign will typecast your strings into numbers
//second option
slideFn.toggleProgressBar( parseInt(clickedSlideID[1]), parseInt(currentSlideID[1]) );
However, in my experience, parseInt runs a little bit slower than using + to typecast the strings into numbers.
IE uses the Chakra JS engine, which I believe follows the standards of ECMAScript 3, which is from 1999. I haven't read through the standard, but it's worth considering the possibility that it has something to do with the issue.
Edit
Here's your problem:
$("#progress-wrapper").find('a.active') ==> []
The first time, there are no a.active elements. Thus, whenever you try to call split on an empty array, it throws a TypeError.
You need to give your first .btn-progress the class active, because the first time around, your first .btn-progress looks like this:
1
There's no active class. Only subsequent .btn-progress elements receive the class active whenever you click the .btn-continue. Your first one never does. Therefore, clickedSlideID[1] and currentSlideID[1] are undefined the first go around. It probably breaks in IE9 because IE9 doesn't understand i < undefined, but it's possible that other more modern browsers go ahead and execute anyway.
Somewhere in the beginning of your code, then, you need to do something like this:
$('.btn-progress').eq(0).addClass('active');
I just tried this in the console on your page, and it worked just fine. After I added the class active to the fist .btn-progress, currentSlideID[1] was now 1, and not undefined.

Quotes Required in Google Script Function Parameters?

Running into a bit of a weird issue here. I'm still learning the ins and outs of writing functions for Google Apps, and my first real project is a "shipping dashboard" - basically, a spreadsheet where I can take a tracking number from UPS, FedEx, etc., and from it parse the carrier, generate a tracking link, that type of thing. I'm trying to set it up as a function where I can set the type of data being requested (say, carrier), a tracking number, and have it return said information. Here's where I'm at right now:
function trackingData(infoType,trackingNumber) {
//Regex for various carrier's tracking numbers.
var upsValue = /\b(1Z ?[0-9A-Z]{3} ?[0-9A-Z]{3} ?[0-9A-Z]{2} ?[0-9A-Z]{4} ?[0-9A-Z]{3} ?[0-9A-Z]|[\dT]\d\d\d ?\d\d\d\d ?\d\d\d)\b/i;
var fedexValue = /(\b96\d{20}\b)|(\b\d{15}\b)|(\b\d{12}\b)/;
var uspsValue = /\b(91\d\d ?\d\d\d\d ?\d\d\d\d ?\d\d\d\d ?\d\d\d\d ?\d\d|91\d\d ?\d\d\d\d ?\d\d\d\d ?\d\d\d\d ?\d\d\d\d)\b/i;
//First option: we want to know the carrier we're shipping with.
if (infoType == 'carrier') {
if (upsValue.test(trackingNumber)) {
return 'UPS';
}
else if (fedexValue.test(trackingNumber)) {
return 'FedEx';
}
else if (uspsValue.test(trackingNumber)) {
return 'USPS';
}
else return null;
}
The issue comes when passing a value for infoType - if I reference a cell, or set each infoType as a variable and put in a value directly when calling the formula it works just fine. However, if I call it by putting in a cell:
=infoType(carrier,trackingNumber)
I get:
error: Unknown range name carrier
The weird thing is it DOES work if I call it with:
=infoType("carrier",trackingNumber)
(note the quotes around "carrier").
I've looked all over the place to find a solution to keep from having to put the quotes around the formula when it is called but so far haven't had any luck. Anyone have ideas?
error: Unknown range name carrier
That error message is due to the way that the Spreadsheet is interpreting the function arguments.
A function parameter is either a reference or a data element (number, date, string). References can be to a single cell (A1) or range (A1..A23), to a range on a different sheet (Sheet2!A1), or a named range. When you entered carrier without quotes, it was interpreted as a named range. Read Named and protected ranges if you're interested.
Here's a little hint from the preamble text with the Google spreadsheets function list:
... don't forget to add quotation marks around all function components made of alphabetic characters that aren't referring to cells or columns.
Summary - if you want an argument to be passed as a string, put quotes on it.

Range.compareBoundaryPoints() generates strange results

I am using a Range object as a limiter on another Range object representing the current selection, such that anything that is inside the selected range but is outside the boundaries of the limiting range is trimmed off. To compare the two Ranges' positions, I'm using the Range.compareBoundaryPoints() function, but the results make no sense.
The element I'm working with looks like this:
<p id="myBlock" contenteditable="true">
Hello hello hello, this is a great big block of test text.
</p>
The ranges are defined and their positional relationship retrieved as follows:
var limitingRange = document.createRange();
limitingRange.selectNodeContents($('#myBlock')[0]);
var selectedRange = window.getSelection().getRangeAt(0).cloneRange(); //Use whatever method is supported by the browser to get the Range
var endToStart = limitingRange.compareBoundaryPoints(Range.END_TO_START, selectedRange);
var startToEnd = limitingRange.compareBoundaryPoints(Range.START_TO_END, selectedRange);
var startToStart = limitingRange.compareBoundaryPoints(Range.START_TO_START, selectedRange);
var endToEnd = limitingRange.compareBoundaryPoints(Range.END_TO_END, selectedRange);
I then select the word "this" in the text and run the function. My ranges look like this (I'm inventing the .start and .end notation to try and make my description clearer):
(limitingRange.start)Hello hello hello, (selectedRange.start)this(selectedRange.end) is a great big block of test text.(limitingRange.end)
The resulting values are:
endToStart = -1
startToEnd = 1
startToStart = -1
endToEnd = 1
I am baffled as to how to interpret the results of the function. Based on the specification it seems like, for example, endToStart should contain -1 if the end of selectedRange comes before the start of limitingRange, but that's obviously not the case. What's going on here?
I'm answering my own question: Typing out what was going on led me to actually understand the function's behavior, which really is strange.
The result of a.compareBoundaryPoints(X_TO_Y, b) is the comparison of a.Y to b.X. -1 if a.Y comes before b.X, 0 if they are at the same position, and 1 if it comes after.
So in endToStart in the above example, limitingRange.start is being compared to selectedRange.end, with -1 indicating that limitingRange.start comes first.
startToEnd: limitingRange.end comes after selectedRange.start, so the result is 1.
startToStart: limitingRange.start comes before selectedRange.start, so the result is -1.
endToEnd: limitingRange.end comes after selectedRange.end, so the result is 1
Leaving me with only the question: Why does this function behave this way? The names of the constants are the opposite of what one would intuitively expect.

Categories

Resources