Applying google app script code in selection in google docs - javascript

I have the following code that puts bold style some keywords in a whole google document:
function boldKeywords() {
// Words that will be put in bold:
var keywords = ["end", "proc", "fun"];
var document = DocumentApp.getActiveDocument();
var body = document.getBody();
var Style = {};
Style[DocumentApp.Attribute.BOLD] = true;
for (j in keywords) {
var found = body.findText(keywords[j]);
while(found != null) {
var foundText = found.getElement().asText();
var start = found.getStartOffset();
var end = found.getEndOffsetInclusive();
foundText.setAttributes(start, end, Style)
found = body.findText(keywords[j], found);
}
}
}
But I would like the code to put the keywords in bold only in the selected area of the document, for doing that, I tried using the function getSelection(), but I have the problem that this function returns a Range, but for applying findText I need a Body, somebody knows what could I do?

Modified Script
function boldKeywordsInSelection() {
const keywords = ["end", "proc", "fun"];
const document = DocumentApp.getActiveDocument();
const selection = document.getSelection();
// get a list of all the different range elements
const rangeElements = selection.getRangeElements();
const Style = {};
Style[DocumentApp.Attribute.BOLD] = true;
// forEach used here because for in was giving me trouble...
rangeElements.forEach(rangeElement => {
// Each range element has a corresponding element (e.g. paragraph)
const parentElement = rangeElement.getElement();
// fixing the limits of the bold operations depending on the selection
const startLimit = rangeElement.getStartOffset();
const endLimit = rangeElement.getEndOffsetInclusive();
for (j in keywords) {
let found = parentElement.findText(keywords[j]);
// wrapping in try catch to escape the for loop from within the while loop
try {
while (found != null) {
const foundText = found.getElement().asText();
const start = found.getStartOffset();
// Checking if the start of the word is after the start of the selection
if (start < startLimit) {
// If so, then skip to next word
found = parentElement.findText(keywords[j], found);
continue;
}
// Checking if the start of the word is after the end of the selection
// if so, go to next element
if (start > endLimit) throw "out of selection";
const end = found.getEndOffsetInclusive();
foundText.setAttributes(start, end, Style)
found = parentElement.findText(keywords[j], found);
}
} catch (e) {
Logger.log(e)
continue;
}
}
})
}
See the comments in the code for more details.
A getSelection produces a Range object, which contains within it various instances of RangeElement. Each RangeElement makes reference to a parent element, and the index positions within that parent. The parent is the element that the range element is a part of. For example:
This selection spans 3 elements. Therefore the selection has 3 range elements. You can only use the findText method on the whole element, not the range element.
This means that the flow of the script is generally the same, except that you need to go through each element and find the text within each element. Since this will return elements that are outside the selection, you need to keep track of the index positions of the selection and the found element and make sure the found element is within the selection.
References
Range
RangeElement
getSelection()

Related

Insert a HTML link next to every word that matches in a sentence for multiple words

I am trying to do something that I thought would be simple but been stuck on this for a while I want to find all instances of a word in a paragraph and insert a link next to it.
I dont want to use innerHTML and destroy the events. I also dont want to use jQuery ideally pure js.
I am looking to take this paragraph.
<p>red this is a sentence I want to change red and I want to change it for all instances the word red</p>
Find all index positions of the word red and change it too.
<p>red Some link this is a sentence I want to change red Some link and I want to change it for all instances the word red Some link</p>
So find every instance of the word red grab the index and then insert html not sure it can even be done the way I am doing it, it always only inserts it one time.
I have this so far.
var ps = document.querySelectorAll("p");
[].forEach.call(ps, function(p) {
const indexes = [...p.innerText.matchAll(new RegExp("red", "gi"))].map(
(a) => a.index
);
var link = document.createElement("a");
link.href = "";
link.innerHTML = `Changed`;
indexes.forEach((pos) => {
insertAtStringPos(p, pos, link);
})
});
function insertAtStringPos(el, pos, insertable) {
if (!el.children.length) {
var text = el.outerText;
var beginning = document.createTextNode(text.substr(0, pos));
var end = document.createTextNode(text.substr(pos - text.length));
while (el.hasChildNodes()) {
el.removeChild(el.firstChild);
}
el.appendChild(beginning);
el.appendChild(insertable);
el.appendChild(end);
}
}
I grabbed the insertAtStringPos function from another stackoverflow post.
I have an example here: https://jsbin.com/watopeteki/edit?html,js,console,output
Why do it always only insert once?
It can be an easier, you need just a split text and by a keyword insert a link.
function links() {
const ps = document.querySelectorAll('p');
return Array.from(ps).reduce((acc, p) => {
const links = p.querySelectorAll('a');
const isUpdate = Boolean(links?.length);
const text = p.innerHTML;
let index = 0;
const splitted = text.split(/(red)/gi);
splitted.forEach((txt) => {
const el = document.createTextNode(txt);
acc.appendChild(el);
if (txt === 'red') {
let link;
if (isUpdate) {
link = links[index++];
link.href = '';
link.innerHTML = `Changed after update`;
} else {
link = document.createElement('a');
link.href = '';
link.innerHTML = `Changed`;
acc.appendChild(link);
}
}
});
return acc;
}, document.createElement('p'));
}
const element = links(); // creates links
document.body.appendChild(element);
links(); // updates current links
If I understood correct, you need a function which updates your existing links. I have update stackblitz and example, check this out.
Stackblitz
I see a few problems with your code.
Your insertAtStringPos() function mutates the paragraph element contents, invalidating the remaining indexes in the indexes array. Reversing the indexes array before looping, and inserting from the end toward the beginning of the text, overcomes this problem.
You're passing the link element to the insertAtStringPos(). This same element gets inserted then moved with each subsequent insertion. Passing a cloned link element with each indexes iteration solves this problem.
outerText, in var text = el.outerText;, returns undefined in my version of Firefox (78.15, October 5, 2021).
To search for a word in one or more paragraphs, and insert a link node after that string, loop over each paragraph, search for occurrences of the string, and build an index array. Then loop the index array but first reverse it to start insertion at the end of the string. Also copy the link node before passing to the insertion function, otherwise the link node will simply be moved from one position to the next.
const ps = document.querySelectorAll("p");
const word = "red";
const link = document.createElement("a");
link.href = "";
link.innerHTML = "Changed";
[].forEach.call(ps, function(p) {
const indexes = [...p.innerText.matchAll(new RegExp(word, "gi"))].map(
// add word length to position, making sure position is not beyond end of text
(a) => (p.innerText.length > a.index + word.length)
? a.index + word.length // add word length to position
: p.innerText.length // word is at end of text
);
// execute insertion function for each position
// first reverse index array to start at the end of the text and work towards the beginning
indexes.reverse().forEach((pos) => {
// clone node before passing to insertion, otherwise same node simply gets moved
insertAtStringPos(p, pos, link.cloneNode(true)); // <-- clone node
})
});
function insertAtStringPos(el, pos, insertable) {
const text = el.childNodes[0].textContent;
const beginning = document.createTextNode(text.substr(0, pos) + " "); // text before and including word, plus a space
const end = document.createTextNode(
// if position is at end of text, create empty text node
(text.length > pos)
? " " + text.substr(pos - text.length) // a space, and text after word
: "" // empty text node
);
el.removeChild(el.childNodes[0]);
el.insertBefore(end, el.childNodes[0]);
el.insertBefore(insertable, el.childNodes[0]);
el.insertBefore(beginning, el.childNodes[0]);
}
<p>red this is a sentence I want to change red and I want to change it for all instances the word red</p>

Google apps script - retain links when copying footnote content

Background
I have a Google Apps Script that we use to parse the footnote content, wrapped in double parenthesis, in place of the footnote number superscript. The intended result should be:
Before Script
This is my footie index.1 1This is my
footie content with a link and emphasis.
After Script
This is my footie index. (( This is my footie content with a
link and emphasis.)
Problem
Everything works fine, except when I parse the footnotes in double parenthesis, they are losing all the links and formatting:
This is my footie index. (( This is my footie content with a
link and emphasis.)
If anyone can assist me with fixing the code below I would really appreciate the help :)
SOLUTION:
function convertFootNotes () {
var doc = DocumentApp.getActiveDocument()
var copy = generateCopy(doc) // make a copy to avoid damaging the original
var openCopy = doc; //DocumentApp.openById(copy.getId()) // you have to use the App API to copy, but the Doc API to manipulate
performConversion(openCopy); // perform formatting on the copy
}
function performConversion (docu) {
var footnotes = docu.getFootnotes(); // get the footnotes
footnotes.forEach(function (note) {
// Traverse the child elements to get to the `Text` object
// and make a deep copy
var paragraph = note.getParent(); // get the paragraph
var noteIndex = paragraph.getChildIndex(note); // get the footnote's "child index"
insertFootnote(note.getFootnoteContents(),true, paragraph, noteIndex);
note.removeFromParent();
})
}
function insertFootnote(note, recurse, paragraph, noteIndex){
var numC = note.getNumChildren(); //find the # of children
paragraph.insertText(noteIndex," ((");
noteIndex++;
for (var i=0; i<numC; i++){
var C = note.getChild(i).getChild(0).copy();
if (i==0){
var temp = C.getText();
var char1 = temp[0];
var char2 = temp[1];
if (C.getText()[0]==" "){
C = C.deleteText(0,0);
}
}
if (i>0){
paragraph.insertText(noteIndex,"\n");
noteIndex++;
}
paragraph.insertText(noteIndex,C);
noteIndex++;
} //end of looping through children
paragraph.insertText(noteIndex,"))");
}
function generateCopy (doc) {
var name = doc.getName() + ' #PARSED_COPY' // rename copy for easy visibility in Drive
var id = doc.getId()
return DriveApp.getFileById(id).makeCopy(name)
}
Were there any changes to the code other than the added )) to make it not work? Removing the (( & )) still did not have the formatting applied when testing it; getText() returns the element contents as a String, not a rich text object/element which contains the formatting info.
To get to the Text object:
getFootnoteContents().getChild(0) returns the FootnoteSection Paragraph
getChild(0).getChild(0) returns the Text object of that paragraph
copy() returns a detached deep copy of the text object to work with
Note: If there are other child elements in the FootnoteSection or in it's Paragraph child, you'll want to add some kind of type/index checking to get the correct one. However, with basic footnotes - as the above example - this is the correct path.
function performConversion (docu) {
var footnotes = docu.getFootnotes() // get the footnotes
var noteText = footnotes.map(function (note) {
// Traverse the child elements to get to the `Text` object
// and make a deep copy
var note_text_obj = note.getFootnoteContents().getChild(0).getChild(0).copy();
// Add the `((` & `))` to the start and end of the text object
note_text_obj.insertText(0, " ((");
note_text_obj.appendText(")) ");
return note_text_obj // reformat text with parens and save in array
})
...
}

Highlight phone number and wrap with tag javascript

The following code checks if the selected tag has childnodes. If a child node is present , it loops till a child node is found. When there are no further child nodes found, it loops out i.e it reaches a text node causing the loop to end. The function is made recursive to run until no child node is found. The code runs as per above info, but when I try to match TEXT_NODE (console.log() outputs all text node), replace() is used to identify phone numbers using regex and replaced with hyperlink. The number gets detected and is enclosed with a hyperlink but it gets displayed twice i.e. number enclosed with hyperlink and only the number.Following is the code
function DOMwalker(obj){
var regex = /\+\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}/g;
var y = "$&";
if(obj.hasChildNodes()){
var child = obj.firstChild;
while(child){
if(child.nodeType!==3)
{
DOMwalker(child);
}
if (child.nodeType=== 3) {
var text = child.nodeValue;
console.log(typeof text);
var regs = regex.exec(text);
match = text.replace(regex,y);
if(match){
var item = document.createElement('a');
item.setAttribute('href','javascript:void(0);');
var detect = document.createTextNode(match);
var x=item.appendChild(detect);
console.log(x);
child.parentNode.insertBefore(x,child);
}
}
child=child.nextSibling;
}
}
};
$(window).load(function(){
var tag = document.querySelector(".gcdMainDiv div.contentDiv");
DOMwalker(tag);
});
Following are the screenshot of the output:
Here the number gets printed twice instead of one with hyperlink which is been displayed(expected highlighted number with hyperlink) and second widout tags
Following is console.log of x
I have already gone through this.
The solution provided below works well with FF. The problem arises when used in IE11. It throws Unknown runtime error and references the .innerHTML. I used the appenChild(),but the error couldn't be resolved.
You've got a couple of problems with what you posted. First, if a child is not node type 3 and not a SCRIPT node, you re-call recursivetree() but you do not pass the child in. The function will just start over at the first div element and again, infinitely loop.
Second, you're calling replace() on the node itself, and not the node's innerHTML. You're trying to replace a node with a string, which just won't work, and I think you mean to replace any matching numbers within that node, rather than the entire node.
If you have <div>My number is +111-555-9999</div>, you only want to replace the number and not lose everything else.
Try this as a solution:
function recursivetree(obj){
var regex = /\+\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}/g;
var y = "$&";
var obj = obj || document.getElementsByTagName('div')[0];
if(obj.hasChildNodes()){
var child = obj.firstChild;
while(child){
if(child.nodeType !== 3 && child.nodeName !== 'SCRIPT'){
//Recall recursivetree with the child
recursivetree(child);
}
//A nodeType of 3, text nodes, sometimes do not have innerHTML to replace
//Check if the child has innerHTML and replace with the regex
if (child.innerHTML !== undefined) {
child.innerHTML = child.innerHTML.replace(regex,y);
}
child=child.nextSibling;
}
}
}
recursivetree();
Fiddle: http://jsfiddle.net/q07n5mz7/
Honestly? If you're trying to loop through the entire page and replace all instances of numbers, just do a replace on the body.
var regex = /\+\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}/g;
var y = "$&";
var body = document.body;
body.innerHTML = body.innerHTML.replace(regex, y);
Fiddle: http://jsfiddle.net/hmdv7adu/
Finally, I got the solution of my question. I referred to this answer which helped me to solve my query.
Here goes the code:
function DOMwalker(obj){
if(obj.hasChildNodes()){
var child = obj.firstChild;
var children = obj.childNodes;
var length = children.length;
for(var i = 0;i<length;i++){
var nodes = children[i];
if(nodes.nodeType !==3){
DOMwalker(nodes);
}
if(nodes.nodeType===3){
//Pass the parameters nodes:current node being traversed;obj:selector being passed as parameter for DOMwalker function
highlight(nodes,obj);
}
}
child = child.nextSibling;
}
}
function highlight(node,parent){
var regex =/(\d{1}-\d{1,4}-\d{1,5})|(\+\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9})/g;
//Stores the value of current node which is passed through the if loop
var matchs = node.data.match(regex);
if matchs is true,add it to DOM
if(matchs){
var anchor = document.createElement("a");
var y = /[(]\d[)]|[.-\s]/g;//removes spaces periods or dash,also number within brackets
var remove = number.replace(y,'');
//tel uri,if you have an app like skype for click-to dial
anchor.setAttribute("href","tel:"+remove);
//the anchor tag should be inserted before in the current node in the DOM
parent.insertBefore(anchor,node);
//append it toh the DOM to be displaye don the web page
anchor.appendChild(node);
}
else
{
return false;
}
}
I hope this code helps others.

Load SVG from file to canvas and ungroup it

I upload an SVG file to a canvas using FabricJS with the function
fabric.loadSVGFromURL (url, function(objects, options){
group = fabric.util.groupSVGElements(objects, options);
canvas.add(group).centerObject(group).renderAll();
});
This works perfectly. However the next step I want do is to ungroup the recently added group. The reason why I need to ungroup is that I want to be able to select the group's child elements by clicking on them since there is no access to these elements if they are grouped.
I found a snippet to perform an ungroup however when I do it with the group created width groupSVGElements the elements lose their original position scrambling the whole svg that I loaded.
Does anyone knows how to ungroup a loaded SVG and still keep the original positions of the elements?
You can still access each of the element using perPixelTargetFind
When set to true, objects are "found" on canvas on per-pixel basis rather than according to
bounding box.
I'm looking for the same solution. Did you find an answer so far?
Looking at the structure of an SVG element, I would image it should be possible to write a recursive method, which gives the children, the properties of the group and places them one level up. If you keep doing this, you should end up with all groups exploded and all properties intact (which are inherited otherwise).
Looking at SVG-EDIT, there is a function which should do this:
Function: ungroupSelectedElement
// Unwraps all the elements in a selected group (g) element. This requires
// significant recalculations to apply group's transforms, etc to its children
this.ungroupSelectedElement = function() {
var g = selectedElements[0];
if (!g) {
return;
}
if ($(g).data('gsvg') || $(g).data('symbol')) {
// Is svg, so actually convert to group
convertToGroup(g);
return;
}
if (g.tagName === 'use') {
// Somehow doesn't have data set, so retrieve
var symbol = svgedit.utilities.getElem(getHref(g).substr(1));
$(g).data('symbol', symbol).data('ref', symbol);
convertToGroup(g);
return;
}
var parents_a = $(g).parents('a');
if (parents_a.length) {
g = parents_a[0];
}
// Look for parent "a"
if (g.tagName === 'g' || g.tagName === 'a') {
var batchCmd = new svgedit.history.BatchCommand('Ungroup Elements');
var cmd = pushGroupProperties(g, true);
if (cmd) {batchCmd.addSubCommand(cmd);}
var parent = g.parentNode;
var anchor = g.nextSibling;
var children = new Array(g.childNodes.length);
var i = 0;
while (g.firstChild) {
var elem = g.firstChild;
var oldNextSibling = elem.nextSibling;
var oldParent = elem.parentNode;
// Remove child title elements
if (elem.tagName === 'title') {
var nextSibling = elem.nextSibling;
batchCmd.addSubCommand(new svgedit.history.RemoveElementCommand(elem, nextSibling, oldParent));
oldParent.removeChild(elem);
continue;
}
children[i++] = elem = parent.insertBefore(elem, anchor);
batchCmd.addSubCommand(new svgedit.history.MoveElementCommand(elem, oldNextSibling, oldParent));
}
// remove the group from the selection
clearSelection();
// delete the group element (but make undo-able)
var gNextSibling = g.nextSibling;
g = parent.removeChild(g);
batchCmd.addSubCommand(new svgedit.history.RemoveElementCommand(g, gNextSibling, parent));
if (!batchCmd.isEmpty()) {addCommandToHistory(batchCmd);}
// update selection
addToSelection(children);
}
};
See also:
https://code.google.com/p/svg-edit/source/browse/trunk/editor/svgcanvas.js

Inserting Custom Tags on User Selection

I want to insert my own custom tags and scripts around the selected text. Something like this
var range = window.getSelection().getRangeAt(0);
var sel = window.getSelection();
range.setStart( sel.anchorNode, sel.anchorOffset );
range.setEnd(sel.focusNode,sel.focusOffset);
highlightSpan = document.createElement("abbr");
highlightSpan.setAttribute("style","background-color: yellow;");
highlightSpan.setAttribute("onmouseout","javascript:HideContentFade(\"deleteHighlight\");");
highlightSpan.setAttribute("onmouseover","javascript:ShowHighlighter(\"deleteHighlight\",\""+id_val+"\");");
highlightSpan.appendChild(range.extractContents());
range.insertNode(highlightSpan);
This works in normal scenarios but if I select some text in different paragraphs the extractContents API will validate the HTML returned and put additional tags to make it valid HTML. I want the exact HTML that was selected without the additional validating that javascript did.
Is there any way this can be done?
I have tried it the way mentioned in How can I highlight the text of the DOM Range object? but the thing is I want user specific highlights so if A has added some highlight B should not be able to see it. For this I have my backend code ready.
If you wrap with tags the selected text that belongs to different paragraphs, you create invalid HTML code.
This is an example of invalid HTML code that you would generate.
<p>notselected <span>selected</p><p>selected</span> notselected</p>
In order to accomplish your task, you need to wrap with tags each text in each paragraph of the selection resulting in a code like this.
<p>notselected <span>selected</span></p><p><span>selected</span> notselected</p>
To accomplish this you have to iterate over all nodes selected and wrap the selected text like this:
function wrapSelection() {
var range, start, end, nodes, children;
range = window.getSelection().getRangeAt(0);
start = range.startContainer;
end = range.endContainer;
children = function (parent) {
var child, nodes;
nodes = [];
child = parent.firstChild;
while (child) {
nodes.push(child);
nodes = nodes.concat(children(child));
child = child.nextSibling;
}
return nodes;
}
nodes = children(range.commonAncestorContainer);
nodes = nodes.filter(function (node) {
return node.nodeType === Node.TEXT_NODE;
});
nodes = nodes.slice(nodes.indexOf(start) + 1, nodes.indexOf(end));
nodes.forEach(function (node) {
wrap = window.document.createElement("span");
node.parentNode.insertBefore(wrap, node);
wrap.appendChild(node);
});
start = new Range();
start.setStart(range.startContainer, range.startOffset);
start.setEnd(range.startContainer, range.startContainer.length);
start.surroundContents(window.document.createElement("span"));
end = new Range();
end.setStart(range.endContainer, 0);
end.setEnd(range.endContainer, range.endOffset);
end.surroundContents(window.document.createElement("span"));
}

Categories

Resources