Attaching paragraph to the top of nested list highlights level change - ms-word

I'm working on a simple office-js Word app, built with the Yeoman Generator. I have a very basic document with the following structure:
1. This is heading one
1.1 This is subheading 1
1.2 This is subheading 2
Some random text here...
Paragraph I want to attach to the list from the beginning of my doc
When I try to attach this last paragraph to my list though, I'm getting the following output:
Word document's screenshot:
It looks as though it's first trying to insert it to the level=1 of that list (thus the initial 3) and then actually inserts it to the root list's position (thus the secondary 2).
The code I'm running looks as follows:
const insertHeading = async (event: Office.AddinCommands.Event) => {
try {
await Word.run(async (context) => {
const selection = context.document.getSelection();
selection.load("paragraphs");
await context.sync();
const lists = context.document.body.lists;
const list = lists.getFirst();
const paragraph = selection.paragraphs.getFirst();
list.load("id");
await context.sync();
paragraph.attachToList(list.id, 0);
await context.sync();
})
} catch (error) {
console.info("Error occurred", error.debugInfo);
} finally {
event.completed();
}
}
I'd expect this paragraph to be added as 2nd item of the root level (with 2.) and without this turquoise strike, shown in the image above.
Any help would be much appreciated!

Related

How to loop through a RangeCollection in Word JS API?

I want to add a Find function in my Word Add-in. By clicking a button, the Word will scroll to the first place of the word need to be searched, and by clicking again, the Word will scroll to the next place. I figured out how to find the first one, but I can't loop through the RangeCollection of the search results of the word. I am looking for some functions similar to getNext()
Below is the code to find first "and". It works:
async function scroll() {
await Word.run(async (context) => {
context.document.body.search("and", { matchWholeWord: true, matchCase: false }).getFirst().select();
await context.sync()
}
)}
However, if I want to find the second or more, I can't do this:
async function scroll() {
await Word.run(async (context) => {
context.document.body.search("and", { matchWholeWord: true, matchCase: false }).getFirst().getNext().select();
await context.sync()
} )}
I read the document and there is no getNext() for RangeCollection. Hope somebody knows how to work around it. Thank you!
There are a few workarounds.
Solution 1
Load RangeCollection.items and record the index. Example:
var index = 0;
async function selectNext(...) {
...
var ranges = context.document.body.search(...);
ranges.load('items');
await context.sync();
if (index >= ranges.items.length) {
index = 0;
}
ranges.items[index++].select();
await context.sync();
...
}
This solution respects the index, but not the current selection position. So if you prefer to search downward instead of searching the next, here's solution 2.
Solution 2
Load RangeCollection.items and call Range.compareLocationWith. Example:
var ranges = context.document.body.search(...);
ranges.load('items');
await context.sync();
var selection = context.document.getSelection();
var compareResults = ranges.items.map(range => range.compareLocationWith(selection));
await context.sync();
for(var index in compareResults) {
if (compareResults[index].value == 'After') {
ranges.items[index].select();
await context.sync();
break;
}
}
Of course you would need additional code and polish it to make it a real product, but these are the rough ideas. Please see if they work for you.

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);
}

Showing multiple search results on leaflet map [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
https://www.website.ro/harta this is the map that uses leaflet.js and https://github.com/stefanocudini/leaflet-search, but i can search only single locations.
https://www.website.ro/public/ajax?q=electri
If i search for "electri" it has 3 locations, i want to show them when i hit enter, not to show "Not found".
Already searched on google, stackoverflow, didnt found similar answer/problem.
This can be done with careful use of the options that leaflet-search provides. First, let's create an array that will hold the potential results, and a featureLayer to render any results that show up:
const results = [];
var resultsLayer = L.featureGroup();
Now we can overwrite the buildTip option as a function which does pretty much what it does already by default, but pushes the results to an array as well:
var controlSearch = new L.Control.Search({
...options,
// hijack buildtip function, push results to array
buildTip: (text, loc) => {
results.push(loc); // <---- crucial line here
// the rest of this is lifted from the source code almost exactly
// so as to keep the same behavior when clicking on an option
const tip = L.DomUtil.create("div");
tip.innerHTML = text;
L.DomEvent.disableClickPropagation(tip)
.on(tip, "click", L.DomEvent.stop, controlSearch)
.on(
tip,
"click",
function (e) {
controlSearch._input.value = text;
controlSearch._handleAutoresize();
controlSearch._input.focus();
controlSearch._hideTooltip();
controlSearch._handleSubmit();
},
controlSearch
);
return tip;
},
// only move to the location if there are not multiple results
moveToLocation: results.length
? () => {}
: L.Control.Search._defaultMoveToLocation
});
Now we add an event listener to the input of the search, and if the user presses enter, and there are multiple results, the results that were pushed into the results array will be added to the resultsLayer as markers, and added to the map:
inputEl.addEventListener("keypress", function (e) {
if (e.key === "Enter" && results.length) {
markersLayer.remove();
results.forEach((result) => {
const marker = L.marker(result);
resultsLayer.addLayer(marker);
});
map.fitBounds(resultsLayer.getBounds());
}
});
Working codesandbox
Note this will likely require some cleanup work (i.e. emptying the array on new or empty searches), or readding the full data set if the search is empty, etc., but this should be enough to get you started.
Edit - Full item info
You asked in a comment how we can get the full details of an item and put that in a popup. Reading through leaflet-search's docs and source code, there doesn't seem to be any place that their code 'catches' the entire data object. The buildTip function really only needs 2 pieces of data from an item - the text to show in the tooltip, and the location it refers to. There's a bunch of TODOs regarding keeping the source data in a cache, but they're still todos.
What I would do is use the title and loc that is returned in a result to filter the original data and find its corresponding item in the original data:
const getFullItem = (title, loc) => {
return data.find((item) => item.title === title && loc.equals(item.loc));
};
We can also create a generic function to build the popup text for all the makers, and the results, so the popups are all consistent:
const buildPopupText = (item) => {
return `
<h4>Title: ${item.title}</h4>
<p>Phone: ${item.telefon}</p>
<p>more stuff from ${item.whatever}</p>
`;
};
When we hit enter and we map through the results, we'll use the result to get the original item:
inputEl.addEventListener("keypress", function (e) {
if (e.key === "Enter" && results.length) {
results.forEach((result) => {
const originalItem = getFullItem(result.text, result.loc);
const marker = L.marker(result.loc);
marker.bindPopup(buildPopupText(originalItem));
resultsLayer.addLayer(marker);
});
map.fitBounds(resultsLayer.getBounds());
}
});
So now the results popups build a popup from the originalItem, which has all the properties you'll need.
Working codesandbox

Replace text of current paragraph in MS word Office.js add-in

In a Word add-in, I'm trying to:
receive documentSelectionChanged events,
get the text of the current paragraph, and
replace the string foo with the string bar in the current paragraph.
Everything is working except the last part. The text of the Word document isn't changing.
This is my code:
function updateText() {
var range, foo_range, par;
Word.run(function (context) {
range = context.document.getSelection();
range.paragraphs.load('items');
return context.sync()
.then(function() {
par = range.paragraphs.items[0];
console.log(par.text); // THIS WORKS!
foo_range = par.search('foo');
foo_range.load('items');
})
.then(context.sync)
.then(function() {
console.log(foo_range.items[0].text); // THIS WORKS!
foo_range.items[0].insertText('bar', 'Replace');
// Here, I am trying all the load options I can think of
foo_range.load('items');
foo_range.items[0].load('text');
foo_range.load('text');
range.paragraphs.load('items');
range.paragraphs.load('text');
return context.sync();
});
});
}
Any idea why foo doesn't get replaced by bar in the Word document?
I can't reproduce. Your code works for me on desktop Office 365.
BTW, none of those load calls before the last context.sync do anything, and you should delete them. You only need to load a property (and then sync) when you are going to read the property after the sync. Since you are only writing to the document, you don't need to load anything.

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.