Get all bookmark IDs from Word Document - ms-word

I want to scan an entire document containing text with the styles Heading 1, Heading 2, normal text and several bullet points / other text (which is basically a tech report). After scanning I want to extract bookmarks assigned to "Heading 2" elements, which also act as sub heading titles within the report.
getBookmarks() is defined in the Preview / Beta API which works if my cursor is placed on the "Heading 2" element, as seen below:
async function getBookmarks() {
Word.run(function(context) {
var range = context.document.getSelection();
var bkmrk = range.getBookmarks(true, true);
return context.sync().then(function() {
console.log("The bookmarks read from the document was: " + bkmrk.value);
});
}).catch(function(error) {
console.log("Error: " + JSON.stringify(error));
if (error instanceof OfficeExtension.Error) {
console.log("Debug info: " + JSON.stringify(error.debugInfo));
}
});
}
I've managed to scan the entire document and obtain the "style" attribute as well, as seen from the example code on API documentation:
async function getParagraphAll() { await Word.run(async (context) => {
// Gets the complete sentence (as range) associated with the insertion point.
let paragraphs = context.document.body.paragraphs
paragraphs.load("text, style");
await context.sync();
// Expands the range to the end of the paragraph to get all the complete sentences.
let completeParagraph = paragraphs.items[0]
.getRange()
.expandTo(
context.document
.getSelection()
.paragraphs.getFirst()
.getRange("End")
)
paragraphs.load("text, style, hyperlink");
await context.sync();
for (let i = 0; i < paragraphs.items.length; i++) {
console.log(paragraphs.items[i].style);
//let range = paragraphs.items[i].getRange() - Why is this not working ?
//let bkmrk = range.getBookmarks(true, false) - This doesnt get me the bookmark while its in
//the loop scanning the entire document. Is it because it fails on "Normal" style?
// Should I filter out "Normal" and only run "getBookmarks" on "Heading" style ?
console.log(paragraphs.items[i].style);
} }); }
I've made the reference to Libraries available in the preview API link: https://appsforoffice.microsoft.com/lib/beta/hosted/office.js
I'm struggling to understand why I can get the bookmark at cursor level but when I want to get it for the entire document, it just displays
do context.sync() before loading any property. There is no need for
load.

Related

How to get and replace the word on the left of the cursor

For a word add-ins in javascript, a simple use case is to get the word on the left of the cursor and to replace it in upper case.
For example, if | is the cursor:
Hello world| will become Hello WORLD|
Hello| world will become HELLO| world
Is it possible to perform this example with the Word.Range class? For example, to expand the range until a space like this fictive code:
Word.run(function (context) {
var selection = context.document.getSelection();
var cursor = selection.getRange('Start');
// Fictive: how to expand the range to the left until a space?
var range = cursor.expandToLeftUntil(' ');
range.load("text");
var html = range.getHtml();
await context.sync();
var textToReplace = html.value.toUpperCase();
// Replace the text
range.insertText(textToReplace, 'Replace');
await context.sync();
});
Or is there any other solution?
A possible strategy is to use the search method to get a RangeCollection of all the words in the document (or body or paragraph, etc.). Then get a reference to the current selected range (where the cursor is). Then loop through the collection and call the Range.compareLocationWith method to find the range that is "AdjacentBefore" the currently selected range.
I was trying to do a similar thing. At least, when a selection is empty get the word nearby the cursor. I hope their would be some API function, but that's not the case.
I started out with the answer/idea of Rick Kirkham (thanks!). I couldn't get to search method to work to get a list of words. Using split on a space worked fine though.
Instead of select like I do you could modify the the text.
If you don't want to get close-by but only after you should alter the function to check 'InsideStart' (in that scenario you would like to go to the previous word, so i-1).
Word.run(async (context) => {
let cursorOrSelection = context.document.getSelection();
cursorOrSelection.load();
await context.sync();
// if the cursor is empty we make a selection of the Word close-by
// this behaviour is done automatically when you insert a comment in Word
if (cursorOrSelection.isEmpty) {
console.log("Empty selection, cursor.");
// get the paragraph closest to the cursor.
const paragraph = cursorOrSelection.paragraphs.getFirst();
const allWordsInParagraph = paragraph.split([" "], true /* trimDelimiters*/, true /* trimSpaces */);
allWordsInParagraph.load();
await context.sync();
// compare the cursorRange with the ranges of individual words in the paragraph.
let compareRanges = [];
allWordsInParagraph.items.forEach( item => {
compareRanges.push({
compare: cursorOrSelection.compareLocationWith(item),
range: item
});
});
await context.sync();
// walk through all the words and compare the location relation with the cursor
// were the location relation changes, the word is near the cursor.
let previousLocationRelation = null;
let wordClosestToCursorRange = null;
for (let i = 0; i < compareRanges.length; i++) {
const locationRelation = compareRanges[i].compare.value;
console.log(locationRelation);
// if first entry is Before, we are at the beginning
if(i==0 && locationRelation === 'Before') {
wordClosestToCursorRange = compareRanges[i].range;
// jump out
break;
}
else {
if(previousLocationRelation && locationRelation != previousLocationRelation) {
// first "edge" we find.
// console.log('-- edge');
// if first edge we encounter is Before
// we need the previous one (could be after)
if(locationRelation === 'Before') {
wordClosestToCursorRange = compareRanges[i-1].range;
}
else {
// we are inside the word or end of the word
// Inside, InsideStart, InsideEnd
wordClosestToCursorRange = compareRanges[i].range;
}
// jump out we are only interested in the first edge
break;
}
}
previousLocationRelation = locationRelation;
}
wordClosestToCursorRange.select();
}
return context.sync();
})
.catch(function (error) {
console.log(error.message)
})

codecpetjs: Helper or Function that can find CSS attribute of a pseudo element (::after)

I have a codeceptjs/playwright test checking the +/- (opened / closed) indicator on an accordion. The +/- is a background-image applied to the button::after pseudo element.
I have it working as an I.executeScript() function as I have access to window, document, and full DOM elements.
I need to reuse it in a lot of locations and tests so I've been fighting trying to get it working as a helper or even a test specific function.
I can get the helper/function to fire and pass the props in. I just can't get to the ::after pseudo element to get the applied style
I.executeScript(() => {
let cssProp = window
.getComputedStyle(
document.querySelector(
"ul.accordion div#state1 h3.panel-title button"
),
"::after"
)
.getPropertyValue("background-image");
if (cssProp.indexOf("viewBox='0 0 12 12'") > -1) { //plus or minus svg shape
return true;
} else {
return false;
}
});
works flawlessly, but need to be repeated dozens of times, and I'd rather keep it a bit DRY.
helper function checkAccordionPlusMinus works with button element:
returns:
cssProp:: [ 'rgb(244, 244, 244) none repeat scroll 0% 0% / auto
padding-box border-box' ]
let selector = `ul.accordion div#${TestSelector} h3.panel-title button`;
let cssProp = await this.helpers["Playwright"].grabCssPropertyFrom(
selector,
"background"
);
console.log("cssProp:: ", cssProp);
helper function checkAccordionPlusMinus doesn't work with button:after or button::after element:
returns:
cssProp:: undefined
let selector = `ul.accordion div#${TestSelector} h3.panel-title button:after`;
let cssProp = await this.helpers["Playwright"].grabCssPropertyFrom(
selector,
"background-image"
);
console.log("cssProp:: ", cssProp);

retriving data from Content control using Word Api

I'm developing a Word Add-in (Word API + Office.js) where i am working with content controls, i am try to check whether control is blank
i am using the below code for this functionality
function callPromise() {
return new Promise(function (resolve, reject) {
var MadatoryFieldsList = ["Control1", "Control2", "Control3"];
$.each(MadatoryFieldsList, function (index, element) {
Word.run(function (context) {
var contentControls = context.document.contentControls.getByTag(element).getFirst();
contentControls.load('text');
return context.sync().then(function () {
var text = contentControls.text;
if (text == "") {
//document.getElementById('lblMandatory').innerText += element + " is Mandatory" + " ";
mandatoryflag = "False";
}
if (index === MadatoryFieldsList.length - 1) resolve();
})
});
});
});
}
this works fine when i create the content control manually from developer tab in word document ... but if i copy the same from different document or load it from database in the form of OOXML it is not able to fetch the controls.
please let me know if i am missing something

TinyMCE: wordcount plugin is not counting special characters as word

I have added the plugin wordcount to count the number of words entered in my TinyMCE texteditor.
plugins: "wordcount",
wordcount_cleanregex: /[.(),;:!?%#$?\x27\x22_+=\\/\-]*/g
It is counting letters and numbers but when I am giving a special character , it is not counting them.
for e.g ----
Hi I am 18 year old (for this it is giving me count 6)
Hi I am ## year old (for this it is giving me count 5)
Any idea what I need to do. I tried to remove:
%#$ from wordcount_cleanregex , but it didn't work.
Your issue is not with the wordcount_cleanregex setting but rather with the wordcount_countregex setting:
https://www.tinymce.com/docs/plugins/wordcount/#wordcount_countregex
If you look at the default one you will see why its skipping the characters that it is. Here is the exact regex:
https://regex101.com/r/wL4fL1/1
If you tweak that regex you can get it to count your ## as a word.
Note: There is some core cleaning that is done within the wordcount plugin that is done regardless of your configuration settings. In TinyMCE 4.4.1 it looks like this:
if (tx) {
tx = tx.replace(/\.\.\./g, ' '); // convert ellipses to spaces
tx = tx.replace(/<.[^<>]*?>/g, ' ').replace(/ | /gi, ' '); // remove html tags and space chars
// deal with html entities
tx = tx.replace(/(\w+)(&#?[a-z0-9]+;)+(\w+)/i, "$1$3").replace(/&.+?;/g, ' ');
tx = tx.replace(cleanre, ''); // remove numbers and punctuation
var wordArray = tx.match(countre);
if (wordArray) {
tc = wordArray.length;
}
}
...so some core things are still stripped from your content regardless of what you put in the wordcount_cleanregex and wordcount_countregex. If you want to change this core behavior you will need to modify the plugin's source code.
here is my wordcount plugin which I am adding externally , The function getCount returns the number of words perfectly when I run this function seprately on my .aspx page as javascript function, but when I am running under this plugin it is only counting letters (it is not counting any number/special characters)
tinymce.PluginManager.add('wordcount', function(editor) {
function update() {
editor.theme.panel.find('#wordcount').text(['Words: {0}', getCount()]);
}
editor.on('init', function() {
var statusbar = editor.theme.panel && editor.theme.panel.find('#statusbar')[0];
if (statusbar) {
tinymce.util.Delay.setEditorTimeout(editor, function() {
statusbar.insert({
type: 'label',
name: 'wordcount',
text: ['Words: {0}', getCount()],
classes: 'wordcount',
disabled: editor.settings.readonly
}, 0);
editor.on('setcontent beforeaddundo', update);
editor.on('keyup', function(e) {
if (e.keyCode == 32) {
update();
}
});
}, 0);
}
});
getCount = function () {
var body = editor.getBody().innerHTML;
text1 = body.replace(/<[^>]+>/g, '');
s = text1.replace(/ /g, ' ');
s = s.replace(/(^\s*)|(\s*$)/gi, "");//exclude start and end white-space
s = s.replace(/[ ]{2,}/gi, " ");//2 or more space to 1
s = s.replace(/\n /, "\n"); // exclude newline with a start spacing
return s.split(' ').length;
};
});

Handle selected event in autocomplete textbox using bootstrap Typeahead?

I want to run JavaScript function just after user select a value using autocomplete textbox bootstrap Typeahead.
I'm searching for something like selected event.
$('.typeahead').on('typeahead:selected', function(evt, item) {
// do what you want with the item here
})
$('.typeahead').typeahead({
updater: function(item) {
// do what you want with the item here
return item;
}
})
For an explanation of the way typeahead works for what you want to do here, taking the following code example:
HTML input field:
<input type="text" id="my-input-field" value="" />
JavaScript code block:
$('#my-input-field').typeahead({
source: function (query, process) {
return $.get('json-page.json', { query: query }, function (data) {
return process(data.options);
});
},
updater: function(item) {
myOwnFunction(item);
var $fld = $('#my-input-field');
return item;
}
})
Explanation:
Your input field is set as a typeahead field with the first line: $('#my-input-field').typeahead(
When text is entered, it fires the source: option to fetch the JSON list and display it to the user.
If a user clicks an item (or selects it with the cursor keys and enter), it then runs the updater: option. Note that it hasn't yet updated the text field with the selected value.
You can grab the selected item using the item variable and do what you want with it, e.g. myOwnFunction(item).
I've included an example of creating a reference to the input field itself $fld, in case you want to do something with it. Note that you can't reference the field using $(this).
You must then include the line return item; within the updater: option so the input field is actually updated with the item variable.
first time i've posted an answer on here (plenty of times I've found an answer here though), so here's my contribution, hope it helps. You should be able to detect a change - try this:
function bob(result) {
alert('hi bob, you typed: '+ result);
}
$('#myTypeAhead').change(function(){
var result = $(this).val()
//call your function here
bob(result);
});
According to their documentation, the proper way of handling selected event is by using this event handler:
$('#selector').on('typeahead:select', function(evt, item) {
console.log(evt)
console.log(item)
// Your Code Here
})
What worked for me is below:
$('#someinput').typeahead({
source: ['test1', 'test2'],
afterSelect: function (item) {
// do what is needed with item
//and then, for example ,focus on some other control
$("#someelementID").focus();
}
});
I created an extension that includes that feature.
https://github.com/tcrosen/twitter-bootstrap-typeahead
source: function (query, process) {
return $.get(
url,
{ query: query },
function (data) {
limit: 10,
data = $.parseJSON(data);
return process(data);
}
);
},
afterSelect: function(item) {
$("#divId").val(item.id);
$("#divId").val(item.name);
}
Fully working example with some tricks. Assuming you are searching for trademarks and you want to get the selected trademark Id.
In your view MVC,
#Html.TextBoxFor(model => model.TrademarkName, new { id = "txtTrademarkName", #class = "form-control",
autocomplete = "off", dataprovide = "typeahead" })
#Html.HiddenFor(model => model.TrademarkId, new { id = "hdnTrademarkId" })
Html
<input type="text" id="txtTrademarkName" autocomplete="off" dataprovide="typeahead" class="form-control" value="" maxlength="100" />
<input type="hidden" id="hdnTrademarkId" />
In your JQuery,
$(document).ready(function () {
var trademarksHashMap = {};
var lastTrademarkNameChosen = "";
$("#txtTrademarkName").typeahead({
source: function (queryValue, process) {
// Although you receive queryValue,
// but the value is not accurate in case of cutting (Ctrl + X) the text from the text box.
// So, get the value from the input itself.
queryValue = $("#txtTrademarkName").val();
queryValue = queryValue.trim();// Trim to ignore spaces.
// If no text is entered, set the hidden value of TrademarkId to null and return.
if (queryValue.length === 0) {
$("#hdnTrademarkId").val(null);
return 0;
}
// If the entered text is the last chosen text, no need to search again.
if (lastTrademarkNameChosen === queryValue) {
return 0;
}
// Set the trademarkId to null as the entered text, doesn't match anything.
$("#hdnTrademarkId").val(null);
var url = "/areaname/controllername/SearchTrademarks";
var params = { trademarkName: queryValue };
// Your get method should return a limited set (for example: 10 records) that starts with {{queryValue}}.
// Return a list (of length 10) of object {id, text}.
return $.get(url, params, function (data) {
// Keeps the current displayed items in popup.
var trademarks = [];
// Loop through and push to the array.
$.each(data, function (i, item) {
var itemToDisplay = item.text;
trademarksHashMap[itemToDisplay] = item;
trademarks.push(itemToDisplay);
});
// Process the details and the popup will be shown with the limited set of data returned.
process(trademarks);
});
},
updater: function (itemToDisplay) {
// The user selectes a value using the mouse, now get the trademark id by the selected text.
var selectedTrademarkId = parseInt(trademarksHashMap[itemToDisplay].value);
$("#hdnTrademarkId").val(selectedTrademarkId);
// Save the last chosen text to prevent searching if the text not changed.
lastTrademarkNameChosen = itemToDisplay;
// return the text to be displayed inside the textbox.
return itemToDisplay;
}
});
});