I'm using the Slides API in my appsScript to change a tables column size, when later I query the tables column width I still get the old values.
When the script ends I can see in slides the table width has changed & next time I'll run the script it will return the updated width value.
Is there a way to wait for the request to finish? Something else?
To reproduce, open an empty google slides and place a table in the first (and only slide).
/**
* #OnlyCurrentDoc Limits the script to only accessing the current presentation.
*/
function getColumnsWidth(table) {
colsWidth = []
for (var c=0; c<table.getNumColumns(); c++) {
col = table.getColumn(c)
colsWidth.push(col.getWidth());
}
return colsWidth
}
function changeColumnWidth(table, columnsInd, W){
Logger.log('Called changeColumnWidth with columnsInd=%s, W=%s', columnsInd, W)
var presentation = SlidesApp.getActivePresentation();
var tableObjectId = table.getObjectId();
var resource = {"requests": [
{"updateTableColumnProperties": {
"objectId": tableObjectId,
"columnIndices": columnsInd,
"tableColumnProperties": {"columnWidth": {"unit": "PT", "magnitude": W}}, "fields": "*"}
}
]};
Slides.Presentations.batchUpdate(resource, presentation.getId());
}
function getTableInSlide(selection) {
var currentPage = selection.getCurrentPage();
pageElements = currentPage.getPageElements();
for (var i=0; i<pageElements.length; i++){
var elementType = pageElements[i].getPageElementType()
if (elementType == 'TABLE'){
return pageElements[i].asTable()
}
}
return Null
}
function myFunction() {
var presentation = SlidesApp.getActivePresentation();
var selection = presentation.getSelection();
table = getTableInSlide(selection)
var colsW = getColumnsWidth(table)
Logger.log('columns width (before update): %s', colsW);
changeColumnWidth(table, [0], colsW[0]*2)
colsW = getColumnsWidth(table)
Logger.log('columns width (after update): %s', colsW);
}
Output:
columns width (before update): [43.303149606299215, 181.63385826771653, 149.73228346456693]
Called changeColumnWidth with columnsInd=[0.0], W=86.60629921259843
columns width (after update): [**43.303149606299215**, 181.63385826771653, 149.73228346456693]
Next run output:
columns width (before update): [**86.60629921259843**, 181.63385826771653, 149.73228346456693]
Called changeColumnWidth with columnsInd=[0.0], W=173.21259842519686
columns width (after update): [86.60629921259843, 181.63385826771653, 149.73228346456693]
P.S
I'm using the slides API as I didn't find any better way to change a tables column width (if there is - I'd love to learn that too :)
I recommend giving it some time before checking for the updated value. The function to try is Utilities.sleep(numberOfSeconds), see docs.
Basically, before you prepend it to this section as shown here:
Utilities.sleep(1000)
colsW = getColumnsWidth(table)
Logger.log('columns width (after update): %s', colsW);
If that does not work then I suggest you explicitly load the updated file again, e.g.
const originalTable = getTableInSlide()
changeColumnWidth(originalTable, 1, 2,)
const updatedTable = getTableInSlide()
const newWidth = getColumnsWidth(updatedTable)
Not sure I fully got the example, but I had a similar problem. I solved it by using the following command:
saveAndClose()
Then reopening the Presentation. It forces execution completion.
Related
I store in a Google Sheets table some data about quotes, which I inject in a Google Slides (duplicate of a template).
My script works replacing the placeholders with the cell content and injecting the images from the URLs. However, I get an error message that breaks the rest of the script.
function createSlide_(currentAppName) {
let presentation = SlidesApp.openByUrl('https://docs.google.com/presentation/d/###/edit');
let templateSlide = presentation.getSlideById('###');
let newSlide = templateSlide.duplicate();
newSlide.setSkipped(false);
newSlide.replaceAllText('{{AppName}}',currentAppName[0]);
newSlide.replaceAllText('{{Company}}',currentAppName[1]);
newSlide.replaceAllText('{{Founder}}',currentAppName[2]);
newSlide.replaceAllText('{{Designation}}',currentAppName[3]);
newSlide.replaceAllText('{{quote}}',currentAppName[4]);
newSlide.getShapes().forEach(s => {
if (s.getText().asString().trim() == "{{AppImage}}") s.replaceWithImage(DriveApp.getFileById(currentAppName[5].match(/[-\w]{25,}/)).getBlob());
if (s.getText().asString().trim() == "{{FounderImage}}") s.replaceWithImage(DriveApp.getFileById(currentAppName[6].match(/[-\w]{25,}/)).getBlob());
});
};
Exception: Page element is not of type shape.
I also tried to store var shapes = newSlide.getShapes(), check that it exists and then iterate if typeof shapes !== 'undefined' and shapes.length > 0.
I also tried the getPageElements method - the script does not work.
Any idea please?
Not know exactly what is failing in you script I created a simple example. Try this and see what you template consists of. I have a simple slide with just one text box.
function test() {
try {
let presentation = SlidesApp.getActivePresentation();
let slides = presentation.getSlides();
slides.forEach( slide => console.log(slide.getObjectId()) );
let template = presentation.getSlideById("p");
let slide = template.duplicate();
slide.replaceAllText("{{hello}}","goodbye");
let elements = slide.getPageElements();
elements.forEach( element => console.log(element.getPageElementType().toString()))
}
catch(err) {
console.log(err)
}
}
Execution log
3:52:46 PM Notice Execution started
3:52:47 PM Info p
3:52:47 PM Info SHAPE
3:52:47 PM Notice Execution completed
This question was cross-posted to Web Applications: Help me find my Element in a Google Doc so I can act on it in script?
Here is my Google Doc:
https://docs.google.com/document/d/1TgzOIq0g4DyDefmJIEnqycDITIx_2GNQkpdxMGwtqB8/edit?usp=sharing
You can see a button in it titled ENTER NEW NOTE.
I have been successful at rolling through the elements of the doc to find the table and to replace txt in those areas as needed. But the button here needs to have the URL changed, and I cannot figure out how to do it.
This URL seems to give an idea, but I cannot turn it into my answer since I don't quite understand. Mail merge: can't append images from template
Would someone help me with this to the point of showing the actual code, because I have tried to edit the many examples found about elements, setURL and looking to parent, etc. I just end up with a mess.
I am calling the script from a Google Sheet, to wok on a BUNCH of Google Docs. (I will be running through the spreadsheet to get URL's for the next doc to have it's URL replaced.
Here is as close as I believe I have gotten:
function getDocElements() {
var doc = DocumentApp.openByUrl("https://docs.google.com/document/d/1TgzOIq0g4DyDefmJIEnqycDITIx_2GNQkpdxMGwtqB8/edit?usp=sharing"),
body = doc.getBody(),
numElements = doc.getNumChildren(),
elements = [];
for (var i = 0; i < numElements; ++i){
var element = doc.getChild(i),
type = element.getType();
// daURL = element.getURL();
// Look for child elements within the paragraph. Inline Drawings are children.
// if(element.asParagraph().getNumChildren() !=0 && element.asParagraph().getChild(0).getType() == DocumentApp.ElementType.INLINE_DRAWING) {
var drawingRange = body.findElement(DocumentApp.ElementType.INLINE_DRAWING);
while (drawingRange != null) {
var element = drawingRange.getElement();
var drawingElement = element.asInlineDrawing();
//drawingElement.removeFromParent();
drawingElement.setURL("http://www.google.com");
drawingRange = body.findElement(DocumentApp.ElementType.INLINE_DRAWING);
}
// For whatever reason, drawings don't have their own methods in the InlineDrawing class. This bit copies and adds it to the bottom of the doc.
//var drawing = element.asParagraph().copy();
//body.appendParagraph(drawing);
}
Logger.log(i + " : "+type);
}
Here is my newest iteration that shows in the logs the elements, including the inLineDrawing I want to change...
===========
function getDocElement() {
var doc = DocumentApp.openByUrl("https://docs.google.com/document/d/1TgzOIq0g4DyDefmJIEnqycDITIx_2GNQkpdxMGwtqB8/edit?usp=sharing"),
body = doc.getBody(),
numElements = doc.getNumChildren(),
elements = [];
for (var i = 0; i < numElements; ++i){
var element = doc.getChild(i),
type = element.getType();
// daURL = element.getURL();
Logger.log(i + " : " + numElements + " : "+ type + " " + element);
// Search through the page elements. Paragraphs are top-level, which is why I start with those.
if( type == DocumentApp.ElementType.PARAGRAPH ){
// Look for child elements within the paragraph. Inline Drawings are children.
if(element.asParagraph().getNumChildren() !=0 && element.asParagraph().getChild(0).getType() == DocumentApp.ElementType.INLINE_DRAWING) {
//element.getParent().setLinkUrl("http://www.google.com");
Logger.log(element.asParagraph().getChild(0).getType() + " : " + element.getAttributes());
// For whatever reason, drawings don't have their own methods in the InlineDrawing class. This bit copies and adds it to the bottom of the doc.
var drawing = element.asParagraph().copy();
//body.appendParagraph(drawing);
// body.appendParagraph();
if(element.getParent() !=''){
//element.asParagraph().appendHorizontalRule();
//element.editAsText().appendText("text");
// element.getParent().insertHorizontalRule(0);
}
}
}
}
}
I'm not sure why the setLinkUrl() is not available for InlineDrawing 🤔
If you can replace your drawing with an image (You can download your drawing as png or svg and insert it), you will be able to use setLinkUrl
Here is an example:
function myFunction() {
var body = DocumentApp.getActiveDocument().getBody();
// All inline images as a RangeElement
var images = body.findElement(DocumentApp.ElementType.INLINE_IMAGE);
// select first image, in case your doc has more than one you'll need to loop
var element = images.getElement();
var image = element.asInlineImage();
image.setLinkUrl("www.google.com");
}
Unfortunately the Class InlineDrawing doesn't have methods to access the attached links nor any other to programmatically change it to a InlineImage1. It looks to me that you will have have to make the link changes manually.
Related Feature requests:
Issue 3367: Allow exporting InlineDrawing as an image
Issue 1054: Add ability to create and modify drawings
References
1: Answer by Henrique Abreu to Modifying a drawing using Google Apps Script
My goal is to parse a TableOfContents element in a Google Document and write it to another one. I want to do this for every document in a folder.
Having gone to the bother of converting each document to the type generated by DocsList just so I can use this method [ which a document generated by DocumentApp does not have. Why, I don't understand, because otherwise the two 'documents' are similar when it comes to finding parts. ], I find that what I get back is a SearchResult. How is this elusive construction used? I've tried converting it into a TableOfContents element [ ele = searchResult.asTableOfContents() ], which does not error out, but nothing I do allows me parse through its child elements to recover their text works. Interestingly enough, if you get a TableOfContents element by parsing through the document's paragraphs to get it, THAT let's you parse the TOC.
Would someone speak to this question. I sure would appreciate a code snippet because I'm getting nowhere, and I have put some hours into this.
The asTableOfContents() method is only there to help the editor's autocomplete function. It has no run-time impact, and cannot be used to cast to a different type. (See ContainerElement documentation.)
To parse the table of contents, start by retrieving the element from the SearchResult. Below is an example that goes through the items in a document's table of contents to produce an array of item information.
Example Document
Parsing results
On a simple document with a few headings and a table of contents, here's what it produced:
[13-08-20 16:31:56:415 EDT]
[
{text=Heading 1.0, linkUrl=#heading=h.50tkhklducwk, indentFirstLine=18.0, indentStart=18.0},
{text=Heading 1.1, linkUrl=#heading=h.ugj69zpoikat, indentFirstLine=36.0, indentStart=36.0},
{text=Heading 1.2, linkUrl=#heading=h.xb0y0mu59rag, indentFirstLine=36.0, indentStart=36.0},
{text=Heading 2.0, linkUrl=#heading=h.gebx44eft4kq, indentFirstLine=18.0, indentStart=18.0}
]
Code
function test_parseTOC() {
var fileId = '--Doc-ID--';
Logger.log( parseTOC( fileId ) );
}
function parseTOC( docId ) {
var contents = [];
var doc = DocumentApp.openById(docId);
// Define the search parameters.
var searchElement = doc.getBody();
var searchType = DocumentApp.ElementType.TABLE_OF_CONTENTS;
// Search for TOC. Assume there's only one.
var searchResult = searchElement.findElement(searchType);
if (searchResult) {
// TOC was found
var toc = searchResult.getElement().asTableOfContents();
// Parse all entries in TOC. The TOC contains child Paragraph elements,
// and each of those has a child Text element. The attributes of both
// the Paragraph and Text combine to make the TOC item functional.
var numChildren = toc.getNumChildren();
for (var i=0; i < numChildren; i++) {
var itemInfo = {}
var tocItem = toc.getChild(i).asParagraph();
var tocItemAttrs = tocItem.getAttributes();
var tocItemText = tocItem.getChild(0).asText();
// Set itemInfo attributes for this TOC item, first from Paragraph
itemInfo.text = tocItem.getText(); // Displayed text
itemInfo.indentStart = tocItem.getIndentStart(); // TOC Indentation
itemInfo.indentFirstLine = tocItem.getIndentFirstLine();
// ... then from child Text
itemInfo.linkUrl = tocItemText.getLinkUrl(); // URL Link in document
contents.push(itemInfo);
}
}
// Return array of objects containing TOC info
return contents;
}
Bad news
The bad news is that you are limited in what you can do to a table of contents from a script. You cannot insert a TOC or add new items to an existing one.
See Issue 2502 in the issue tracker, and star it for updates.
If you can post code or explain your issue with DocsList vs DocumentApp, it could be looked at. The elements of a Google Document can only be manipulated via DocumentApp.
I modified the above code to re-create the TOC in a table only with the desired levels(i.e. h1, h2). The only caveat is that TOC must be present & updated before running this.
function findToc(body, level = 2) {
const indent = 18;
let contents = [];
const tocType = TABLE_OF_CONTENTS;
const tocContainer = body.findElement(tocType);
if (tocContainer) {
// TOC was found
const toc = tocContainer.getElement().asTableOfContents();
const totalLines = toc.getNumChildren();
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
const tocItem = toc.getChild(lineIndex).asParagraph();
const { INDENT_START } = tocItem.getAttributes();
const isDesiredLevel = Number(INDENT_START) <= indent * (level - 1);
if (isDesiredLevel) {
contents.push(tocItem.copy());
}
}
}
return contents;
}
function addToTable(cellText) {
body = DocumentApp.openById(docId).getBody();
const table = body.appendTable();
const tr = table.insertTableRow(0);
const td = tr.insertTableCell(0);
cellText.forEach(text => {
td.appendParagraph(text);
})
}
function parseTOC(docId) {
body = DocumentApp.openById(docId).getBody();
const contents = findToc(body);
addToTable(contents);
}
Where can I read documentation concerning the execution order rules for GS files?
To dimension the problem I created two trivial objects, each in their own file.
1_File.gs
var ObjB = new Object();
ObjB.sayName = "[" + ObjA.sayName + "]";
0_File.gs
var ObjA = new Object();
ObjA.sayName = " I'm A ";
A call such as ...
Logger.log(ObjA.sayName + " : " + ObjB.sayName);
... gets the error ...
TypeError: Cannot read property "sayName" from undefined.
If I move the code from 1_File.gs into 0_File.gs, and vice versa, then there is no error and the log shows correctly ...
I'm A : [ I'm A ]
Renaming 0_File.gs to 2_File.gs doesn't affect execution order either, so I assume that order depends on which file gets created first.
Is there no concept of "include" or "import" that would allow me to make order of execution explicit?
Where can I read documentation concerning the execution order rules for GS files?
There is no such documentation and I think will not be any time published. In similar way, an initialization order of the static variables in C++ is also undefined and depends on compiler/linker.
Is there no concept of "include" or "import" that would allow me to make order of execution explicit?
Yes, there is no "includes", "imports" and even "modules", but there are libraries.
Also there is a workaround by using a closure. Bellow is a sample code. By executing the test function the log contains c.d. The idea is to have in all gs files a function started with init. In these functions all global variables are instanced. The anonymous closure is executed during the Code.gs file instancing and calls all "init" functions of all gs files.
Code.gs
var c;
function callAllInits_() {
var keys = Object.keys(this);
for (var i = 0; i < keys.length; i++) {
var funcName = keys[i];
if (funcName.indexOf("init") == 0) {
this[funcName].call(this);
}
}
}
(function() {
callAllInits_();
c = { value : 'c.' + d.value };
})();
function test() {
Logger.log(c.value);
}
d.gs
var d;
function initD() {
d = { value : 'd' };
};
I tackled this problem by creating a class in each file and making sure that each class is instantiated in the original Code.gs (which I renamed to _init.gs). Instantiating each class acts as a form of include and makes sure everything is in place before executing anything.
_init.gs:
// These instances can now be referred to in all other files
var Abc = new _Abc();
var Menu = new _Menu();
var Xyz = new _Xyz();
var Etc = new _Etc();
// We need the global context (this) in order to dynamically add functions to it
Menu.createGlobalFunctions(this);
function onInstall(e) {
onOpen(e);
}
function onOpen(e) {
Menu.build();
}
And classes usually look like this:
menu.gs:
function _Menu() {
this.build = function() {
...
}
...
}
If you have more than one level of inheritance, you need to give the init functions names like init000Foo, init010Bar, and init020Baz, and then sort the init functions by name before executing. This will ensure init000Foo gets evaluated first, then Bar, then Baz.
function callAllInits() {
var keys = Object.keys(this);
var inits = new Array();
for (var i = 0; i < keys.length; i += 1) {
var funcName = keys[i];
if (funcName.indexOf("init") == 0) {
inits.push(funcName);
}
}
inits.sort();
for (var i = 0; i < inits.length; i += 1) {
// To see init order:
// Logger.log("Initializing " + inits[i]);
this[inits[i]].call(this);
}
}
The other answers (i.e., don't write any top-level code which references objects in other files) describe the ideal way to avoid this problem. However, if you've already written a lot of code and rewriting it is not feasible, there is a workaround:
Google App Script appears to load code files in the order they were created. The oldest file first, followed by the next, and the most recently created file last. This is the order displayed in the editor when "Sort files alphabetically" is unchecked.
Thus, if you have the files in this order:
Code.gs
1_File.gs (depends on 0_File.gs)
0_File.gs
An easy fix is to make a copy of 1_File.gs and then delete the original, effectively moving it to the end of the list.
Click the triangle next to 1_File.gs and select "Make a copy"
Code.gs
1_File.gs
0_File.gs
1_File copy.gs
Click the triangle next to 1_File.gs and select "Delete"
Code.gs
0_File.gs
1_File copy.gs
Click the triangle next to 1_File copy.gs and select "Rename", then remove the " copy" from the end.
Code.gs
0_File.gs
1_File.gs
Now 0_File.gs is loaded before 1_File.gs.
This works for me as of December 2021. Quite likely, the other answers are outdated.
You can easily fix this. When you look at the scripts in the "Files" section of the web editor, you see they have an order. Files are evaluated in the order they appear there. Clicking on the three dots to the right of a file name brings up a menu that allows you to move a file up or down.
There is no such order in Google Apps Script. It purely depends on where you have these objects declared and how your function is invoked.
Can you explain a bit about how and when your Logger.log() code will be invoked.
Also, when do you declare your objects objA and objB ?
These will help us provide a better answer
here is how I would do this...
main
function include(filename) {
return ContentService.createTextOutput(filename);
}
function main() {
include('Obj A');
include('Obj B');
Logger.log(ObjA.sayName + " : " + ObjB.sayName);
}
Obj A
var ObjA = new Object();
ObjA.sayName = " I'm A ";
Obj B
var ObjB = new Object();
ObjB.sayName = "[" + ObjA.sayName + "]";
Can anyone confirm that HTML widgets accept ClickHandlers on the Server side ? I can't get my below code to work.
I create a serverHandler (and for good measure I have even added a useless callback element). Subsequently, I add it to a HTML.addClickHander (for good measure I have even added it to .addMouseUpHandler as well). The function is NOT executed.
var mouseclick = app.createServerHandler("handleTrainingClick_").addCallbackElement(lstFilter);
var params = [ "fromOrg", "trainingTitle", "dueDate", "medical", "status" ];
var resultSet = blSelectActiveTrainings_();
while (resultSet.hasNext()) {
var training = resultSet.next();
var html = TRAINING_ROW;
for (var pI in params) {
html = html.replace("$"+params[pI], training[params[pI]]);
}
pnlList.add(app.createHTML(html).setId(training.id).addClickHandler(mouseclick).addMouseUpHandler(mouseclick)
.addMouseMoveHandler(mousemove).addMouseOutHandler(mouseout).addMouseOverHandler(mouseover));
}
function handleTrainingClick_(e) {
Logger.log(e.source);
var app = UiApp.getActiveApplication();
return app;
}
HTML widgets server side handlers work just fine. It was an incorrect reference in my code. Thanks all.