Is there a way to undo changes made by a google apps script? - google-apps-script

So I wonder what it takes to make changes made by google apps script to a document reversible.
In particular I am working on a script that applies custom styles to selected elements from a document in Google Docs. It's not a hard thing to do. The problem is that the changes made by the script are not reflected in the history of the document and thus cannot be undone. There is no notion of a reversible editing session either as far as I can tell.
So is there a way to undo the changes made by a script?
function onOpen() {
DocumentApp.getUi()
.createMenu('Extras')
.addItem('Apply code style', 'applyCodeStyle')
.addToUi();
}
function applyCodeStyle() {
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection) {
var elements = selection.getSelectedElements();
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
// Only modify elements that can be edited as text; skip images and other non-text elements.
if (element.getElement().editAsText) {
var text = element.getElement().editAsText();
// Bold the selected part of the element, or the full element if it's completely selected.
if (element.isPartial()) {
text.setBold(element.getStartOffset(), element.getEndOffsetInclusive(), true);
} else {
text.setBold(true);
}
}
}
}
}

The closest I can imagine it to create a backup copy of your file in a specific folder every 5 minutes or so when you are modifying it so you have at least a copy of this doc version. Not ideal but it works...
Here is a piece of code that does it, starting from your code I just added the timer/copy stuff, you can try it by changing the folder ID.
EDIT : added a try/catch for first execution without error.
function applyCodeStyle() {
var selection = DocumentApp.getActiveDocument().getSelection();
try{
var x = new Date().getTime()/60000-new Date(Utilities.jsonParse(ScriptProperties.getProperty('lastBKP'))).getTime()/60000 ;
}catch(e){
ScriptProperties.setProperty('lastBKP', Utilities.jsonStringify(new Date()));
var x = 0
}
Logger.log(x+' minutes')
if (selection) {
if(x > 5){
var docId = DocumentApp.getActiveDocument().getId();
DriveApp.getFileById(docId).makeCopy(DriveApp.getFolderById('0B3qSFd3iikE3NWd5TmRZdjdmMEk')).setName('backup_of_'+DocumentApp.getActiveDocument().getName()+'_on_'+Utilities.formatDate(new Date(),'GMT','yyyy-MMM-dd-HH-mm'));
Logger.log("file copied because new Date().getTime()/3600-new Date(Utilities.jsonParse(ScriptProperties.getProperty('lastBKP'))).getTime()/3600 ="+x);
ScriptProperties.setProperty('lastBKP', Utilities.jsonStringify(new Date()));
}
var elements = selection.getSelectedElements();
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (element.getElement().editAsText) {
var text = element.getElement().editAsText();
if (element.isPartial()) {
text.setBold(element.getStartOffset(), element.getEndOffsetInclusive(), true);
} else {
text.setBold(true);
}
}
}
}
}

Related

Google Docs API - complete documentation (hyperlink issue)

I hope everyone is in good health. This post is my continue of my previous post
My main goal
So main goal was to get the hyperlink and change it the text linked with it. I initially used code from this post and modified it to change the text of first hyperlink. Here is my modified code to change the text of first hyperlink.
function onOpen() {
const ui = DocumentApp.getUi();
ui.createMenu('What to do?')
.addItem('HyperLink Modifier', 'findAndReplacetext')
.addToUi();
}
/**
* Get an array of all LinkUrls in the document. The function is
* recursive, and if no element is provided, it will default to
* the active document's Body element.
*
* #param element The document element to operate on.
* .
* #returns {Array} Array of objects, vis
* {element,
* startOffset,
* endOffsetInclusive,
* url}
*/
function getAllLinks(element) {
var links = [];
element = element || DocumentApp.getActiveDocument().getBody();
if (element.getType() === DocumentApp.ElementType.TEXT) {
var textObj = element.editAsText();
var text = element.getText();
var inUrl = false;
for (var ch=0; ch < text.length; ch++) {
var url = textObj.getLinkUrl(ch);
if (url != null) {
if (!inUrl) {
// We are now!
inUrl = true;
var curUrl = {};
curUrl.element = element;
curUrl.url = String( url ); // grab a copy
curUrl.startOffset = ch;
}
else {
curUrl.endOffsetInclusive = ch;
}
}
else {
if (inUrl) {
// Not any more, we're not.
inUrl = false;
links.push(curUrl); // add to links
curUrl = {};
}
}
}
if (inUrl) {
// in case the link ends on the same char that the element does
links.push(curUrl);
}
}
else {
var numChildren = element.getNumChildren();
for (var i=0; i<numChildren; i++) {
links = links.concat(getAllLinks(element.getChild(i)));
}
}
return links;
}
/**
* Replace all or part of UrlLinks in the document.
*
* #param {String} searchPattern the regex pattern to search for
* #param {String} replacement the text to use as replacement
*
* #returns {Number} number of Urls changed
*/
function findAndReplacetext() {
var links = getAllLinks();
while(links.length > 0){
var link = links[0];
var paragraph = link.element.getText();
var linkText = paragraph.substring(link.startOffset, link.endOffsetInclusive+1);
var newlinkText = `(${linkText})[${link.url}]`
link.element.deleteText(link.startOffset, link.endOffsetInclusive);
link.element.insertText(link.startOffset, newlinkText);
links = getAllLinks();
}
}
String.prototype.betterReplace = function(search, replace, position) {
if (this.length > position) {
return this.slice(0, position) + this.slice(position).replace(search, replace);
}
return this;
}
Note: I used insertText and deleteText functions to update the text value of hyperlink.
My problem with above code
Now the problem was that this code was running too slow. I thought may be it was because I was running the script every-time I needed to search for next hyperlink, So maybe I can break the loop and only get the first hyperlink each time. Then from my previous post the guy gave me a solution to break loop and only get the first hyperlink but when I tried the new code unfortunately it was still slow. In that post he also proposed me a new method by using Google Docs API, I tried using that it was was super fast. Here is the code using Google Docs API
function myFunction() {
const doc = DocumentApp.getActiveDocument();
const res = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
if (paragraph && paragraph.elements) {
paragraph.elements.forEach(({textRun}) => {
if (textRun && textRun.textStyle && textRun.textStyle.link) {
ar.push({text: textRun.content, url: textRun.textStyle.link.url});
}
});
}
return ar;
}, []);
console.log(res) // You can retrieve 1st link and test by console.log(res[0]).
}
My new problem
I liked the new code but I am stuck again at this point as I am unable to find how can I change the text associated with the hyperlink. I tried using the functions setContent and setUrl but they don't seem to work. Also I am unable to find the documentation for these functions on main documentation of this API. I did find I reference for previously mentioned functions here but they are not available for appscript. Here is the sample document I am working on
https://docs.google.com/document/d/1eRvnR2NCdsO94C5nqly4nRXCttNziGhwgR99jElcJ_I/edit?usp=sharing
End note:
I hope I was able to completly convey my message and all the details assosiated with it. If not kindly don't be mad at me, I am still in learning process and my English skills are pretty weak. Anyway if you want any other data let me know in the comments and Thanks for giving your time I really appreciate that.
In order to remove all the hyperlink from your document, you can do the following:
First, retrieve the start and end indexes of these hyperlinks. This can be done by calling documents.get, iterate through all elements in the body content, checking which ones are paragraphs, iterating through the corresponding TextRun, and checking which TextRuns contain a TextStyle with a link property. All this is already done in the code you provided in your question.
Next, for all TextRuns that include a link, retrieve their startIndex and endIndex.
Using these retrieved indexes, call batchUpdate to make an UpdateTextStyleRequest. You want to remove the link property between each pair of indexes, and for that you would just need to set fields to link (in order to specify which properties you want to update) and don't set a link property in the textStyle property you provide in the request since, as the docs for TextStyle say:
link: If unset, there is no link.
Code sample:
function removeHyperlinks() {
const doc = DocumentApp.getActiveDocument();
const hyperlinkIndexes = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
if (paragraph && paragraph.elements) {
paragraph.elements.forEach(element => {
const textRun = element.textRun;
if (textRun && textRun.textStyle && textRun.textStyle.link) {
ar.push({startIndex: element.startIndex, endIndex: element.endIndex });
}
});
}
return ar;
}, []);
hyperlinkIndexes.forEach(hyperlinkIndex => {
const resourceUpdateStyle = {
requests: [
{
updateTextStyle: {
textStyle: {},
fields: "link",
range: {
startIndex: hyperlinkIndex.startIndex,
endIndex: hyperlinkIndex.endIndex
}
}
}
]
}
Docs.Documents.batchUpdate(resourceUpdateStyle, doc.getId());
});
}

How to adding item menus with the java script of an loop's sub menus inside like this example ".addItem("Go to This Sheet", "S"+i+"GoToS")"?

Hy, Everyone .. I have a question about creating the menu in GAS (Google Apps Scripts) to implement to a Google Spreadsheet without a three of the scripts which is has been take long long way and take my energy too and many many so many lines of the scripts like this I have created. This is the script. Here's the code :
function Menu1() {
var ui = s.getUi(),
s = SpreadsheetApp,
ss = s.getAcgtiveSpreadsheet(),
sss = ss.getSheets(),
madeMenu = ui.createMenu('Sheet Tools Just For an Example Menus');
for (var i=0; i < sss.length; i++){
madeMenu.addSubMenu(ui.createMenu(sss[i].getName())
.addItem('Go to ...', 'S'+i+'GoToS')
.addItem('Rename ...', 'S'+i+'RenameS')
.addItem('Move ...', 'S'+i+'MoveS'))
madeMenu.addToUi();
}
}
function GoToS(getSheetNumber) {
var sheet = sss[getSheetNumber];
ss.setActiveSheet(sheet);
}
This of the main of my problems cames to !!! Because these structures of the scripts and then so to make me had to create this lines. See at the below :
function S0GoToS() {
GoToS(0)
}
function S1GoToS() {
GoToS(1)
}
function S2GoToS() {
GoToS(2)
}
function S3GoToS() {
GoToS(3)
}
function S4GoToS() {
GoToS(4)
}
function S5GoToS() {
GoToS(5)
}
The question is How to create them without the third-sub of the scripts ??? I thought and I hope there is the another way to create these for sure yes I believe there is but that just the because I don't know how about that way. Please someone chould be can help me to solve this case. Any achieves will be appreciated. Thanks in advance has taken in your time and I appologies for my poor english.
You can, in fact, generate those functions dynamically. The idea is to keep a for-loop outside of any of your functions, in the "global" scope, which will generate all these functions. Afterwards, they can be called by a menu action. Your could would look like the following:
// function "constructors"
function createGoToFunction(sheetIndex) {
return function() {
var sheet = SpreadsheetApp.getActive().getSheets()[sheetIndex];
sheet.activate();
}
}
function createRenameFunction(sheetIndex) {
return function() {
// Your rename logic
}
}
function createMoveFunction(sheetIndex) {
return function() {
// Your move logic
}
}
// functions definition
this['ALL_SHEETS'] = SpreadsheetApp.getActive().getSheets();
for (i=0; i<this['ALL_SHEETS'].length; i++) {
this['S'+i+'GoToS'] = createGoToFunction(i);
this['S'+i+'RenameS'] = createRenameFunction(i);
this['S'+i+'MoveS'] = createMoveFunction(i);
}
delete this['ALL_SHEETS'];
delete this['i'];
function Menu1() {
var ui = SpreadsheetApp.getUi();
var sheets = SpreadsheetApp.getActive().getSheets();
var madeMenu = ui.createMenu('Sheet Tools Just For an Example Menus');
for (var i=0; i < sheets.length; i++){
var subMenu = ui.createMenu(sheets[i].getName())
.addItem('Go to ...', 'S'+i+'GoToS')
.addItem('Rename ...', 'S'+i+'RenameS')
.addItem('Move ...', 'S'+i+'MoveS');
madeMenu.addSubMenu(subMenu);
}
madeMenu.addToUi();
}
function onOpen() {
Menu1();
}
In order to implement your own functionality for the functions, you just have to change the body of them defined on top (see createGoToFunction as an example).

Appending template to Google Docs header/footer not working when testing as Add-on

I have built a tool in google scripts that is currently set up as a bound script to a google docs file. The goal of the tool is to pull in templates from other created docs files from a custom form input. The code works as is as a bound script, but I am trying to port that code over to create an Add-on for users within my organization. The issue right now is that when trying to insert the template elements into the header or footer of the clean test documents, the code throws an error when attempting to append the elements to the saying Cannot call method "appendParagraph" of null.
I have tried appending as a header and footer with DocumentApp.getActiveDocument().getHeader().appendParagraph. This method works when I am using the script as a bound script and it works as expected. When I try to append the paragraph (or other element) to the body instead of the header or footer while testing as an Add-on the elements are appended without issue. The only time I am getting this error is when I try to append the elements to either the header or footer.
function examplefunction_( docLink, locPaste ) {
var srcBody = DocumentApp.openById( docLink )
.getBody();
var srcNumChild = srcBody.getNumChildren();
var activeBody = DocumentApp.getActiveDocument()
.getBody();
var activeHead = DocumentApp.getActiveDocument()
.getHeader();
var activeFoot = DocumentApp.getActiveDocument()
.getFooter();
var a =0;
if ( locPaste == 'Body') {
var activeLoc = activeBody;
}
else if (locPaste == 'Header') {
var activeLoc = activeHead;
}
else if (locPaste == 'Footer') {
var activeLoc = activeFoot;
}
for (var i = 0; i<srcNumChild; i++) {
if (srcBody.getChild(i).getType() == DocumentApp.ElementType.PARAGRAPH) {
var srcText = srcBody.getChild(i).copy();
activeLoc.appendParagraph(srcText);
}
else if (srcBody.getChild(i).getType() == DocumentApp.ElementType.TABLE) {
var srcTable = srcBody.getTables();
var copiedTable = srcTable[a].copy()
a = a + 1;
activeLoc.appendTable(copiedTable);
}
else if (srcBody.getChild(i).getType() == DocumentApp.ElementType.LIST_ITEM) {
var srcList = srcBody.getChild(i).getText();
var listAtt = srcBody.getChild(i).getAttributes();
var nestLvl = srcBody.getChild(i).getNestingLevel();
activeLoc.appendListItem(srcList).setAttributes(listAtt).setNestingLevel(nestLvl);
}
else {
Logger.log("Could not get element: " + i);
}
}
}
I expected the elements to be appended to the headers and footers from the template without error like when run as a bounded script. The actual result while being run kills the process with error "Cannot call method appendParagraph of null." at the line: activeLoc.appendParagraph(srcText);
You cannot append a paragraph to a header or footer, if your document does not have a header or a footer.
Add to the beginning of your function:
if(!DocumentApp.getActiveDocument().getHeader()){
DocumentApp.openById(docLink).addHeader();
}
Same for the footer.

Can drawings be selected via apps script in Google Docs?

I have a document with Google drawings that for whatever reason are not selectable within the UI. I am not sure how they were ever placed.
I was hoping to write a script to delete them, but I'm not finding a function that applies to drawings specifically.
I'm wondering if anyone knows a trick to accomplish this..
The closest thing I found was their sample for deleting images:
function myFunction() {
var body = DocumentApp.getActiveDocument().getBody();
// Remove all images in the document body.
var imgs = body.getImages();
for (var i = 0; i < imgs.length; i++) {
// Retrieve the paragraph's attributes.
var atts = imgs[i].getAttributes();
// Log the paragraph attributes.
for (var att in atts) {
Logger.log(att + ":" + atts[att]);
}
imgs[i].removeFromParent();
}
}
Never too late (I hope). The trick here is that inline drawings (InlineDrawing) are children of Paragraph or ListItem (source).
If you want to remove some inline drawings, the code below works for me. If you want to find all drawings, please see the TODO comment. It is a simple code, please enhance it if you intend to use it. Just for reference.
Unfortunately, to this time, I didn't find out how to remove drawings that are not inline (drawings that are above or below text). Please forgive my limitation.
function eraseSomeDrawingsFromDoc() {
var body = DocumentApp.getActiveDocument().getBody();
const paragraphs = body.getParagraphs()
paragraphs.forEach(paragraph => {
const childIfAny = paragraph.getNumChildren() > 0 && paragraph.getChild(0) //TODO: analyze all children
const childType = childIfAny && childIfAny.getType()
const iAmADrawing = childType === DocumentApp.ElementType.INLINE_DRAWING
if(iAmADrawing) childIfAny.removeFromParent()
})
}

How to make a closed search in Google Docs?

I have a document where I need to find a text or word, each time i run a function the selection has to go to next if a word or text is found. If it is at the end it should take me to top in a circular way just like find option in notepad.
Is there a way to do it?
I know about findText(searchPattern, from) but I do not understand how to use it.
There are several wrappers and classes in the DocumentApp. They help to work with the contents of the file.
Class Range
Class RangeElement
Class RangeBuilder
It is necessary to understand carefully what they are responsible. In your case the code below should be work fine:
function myFunctionDoc() {
// sets the search pattern
var searchPattern = '29';
// works with current document
var document = DocumentApp.getActiveDocument();
// detects selection
var selection = document.getSelection();
if (!selection) {
if (!document.getCursor()) return;
selection = document.setSelection(document.newRange().addElement(document.getCursor().getElement()).build()).getSelection();
}
selection = selection.getRangeElements()[0];
// searches
var currentDocument = findNext(document, searchPattern, selection, function(rangeElement) {
// This is the callback body
var doc = this;
var rangeBuilder = doc.newRange();
if (rangeElement) {
rangeBuilder.addElement(rangeElement.getElement());
} else {
rangeBuilder.addElement(doc.getBody().asText(), 0, 0);
}
return doc.setSelection(rangeBuilder.build());
}.bind(document));
}
// the search engine is implemented on body.findText
function findNext(document, searchPattern, from, callback) {
var body = document.getBody();
var rangeElement = body.findText(searchPattern, from);
return callback(rangeElement);
}
It looks for the pattern. If body.findText returns undefined then it sets on top of the document.
I have a gist about the subject https://gist.github.com/oshliaer/d468759b3587cfb424348fa722765187