Inline Drawings not Copying From One Google Doc to Another (Apps Script) - google-apps-script

I am trying to copy inline drawings from one document to another. I have been successful in copying text, tables, etc. but am having issues with inline drawings.
For additional context, the code is linked to a Google Form and copies certain pages to a new document based on the results of the submission.
The error message simply reads "Action not allowed" at the line "body.appendParagraph(drawing)". I own all of the source materials/code. The error message is emailed to me upon submission.
I have seen from other responses that the inline drawing element is a child of the paragraph element and have tried to use the suggested fix but for some reason, it is not working for me.
Relevant code is shown below. Please let me know if you need any additional information and I would be happy to provide it! I have little to no coding background so I am sure there is just a silly mistake somewhere.
if (question_two_answer != 'correct_answer') {
var totalElements = otherBody2.getNumChildren();
for( var k = 0; k < totalElements; ++k ) {
var element = otherBody2.getChild(k).copy();
var type = element.getType();
if( type == DocumentApp.ElementType.PARAGRAPH ) {
if(element.asParagraph().getNumChildren() !=0 && element.asParagraph().getChild(0).getType() == DocumentApp.ElementType.INLINE_DRAWING) {
var drawing = element.asParagraph().copy();
body.appendParagraph(drawing);
}
else {
body.appendParagraph(element);
}
}
}
}

Related

Is it possible to iterate on every single page of a Google Document in Apps Script? How can I do that?

I got a Google Docs made of several pages (not a fixed number..) and each of them is filled with an unspecified number of text paragraphs. I need to put a unique title on top of every page and I thought to do that using an array where all titles are listed..
titles = [title1, title2, title3 ...];
Unfortunately I can't figure out how to do that even reading documentation..
Is there a way to loop through document pages in apps script?
The script should also understand if a paragraph starts in a page and ends in another, just to avoid cutting it off in two parts.
I would really appreciate any help or suggestions here..Thank you!
Description
You would need to have a Page Break at the end of each page because there is no other way to know how many paragraphs are on a page. Also when adding your unique headers you have to be careful to not disturb the pagination.
But here is an example of how to find the page breaks for each page. I leave it to you to figure out how to insert the "title" paragraph.
Code.gs
function test() {
try {
let doc = DocumentApp.getActiveDocument();
let body = doc.getBody();
let paras = body.getParagraphs();
for( let i=0; i<paras.length; i++ ) {
let para = paras[i];
let nChild = para.getNumChildren();
if( nChild > 0 ) {
let child = para.getChild(nChild-1);
if( child.getType() == DocumentApp.ElementType.PAGE_BREAK ) {
console.log("page break");
}
}
}
}
catch(err) {
console.log(err);
}
}

How to convert elements to the same size as the first selected element?

I want to set the size of all elements as the size of the first selected element. but seems something wrong with my code , somehow first element size not working for others.
Please see attached script
/*
* Make all elements same size
*/
function sameSizeElements() {
var selection = SlidesApp.getActivePresentation().getSelection();
var selectionType = selection.getSelectionType();
var pageElements = selection.getPageElementRange().getPageElements();
//iterate the selected page elements to grab the values of each positiion
for (var i = 0; i < pageElements.length; i++) {
if(i != 0){
pageElements[i].setWidth(pageElements[0].getWidth());
pageElements[i].setHeight(pageElements[0].getHeight());
}
}
}
Here is the full code you can put in ScriptEditor > code.js default file and refresh slide. it will work as you want to debug.
Updated
We are in the discussion with Google App Script issue tracking team - https://issuetracker.google.com/issues/162545277
As the current workaround, how about this method?
Issue and workaround:
In the current stage, unfortunately, it seems that this bug is still not resolved. By this, in your case, the 1st selected image cannot be retrieved. I think that this is the current reason of your issue.
In order to achieve your goal, as the current workaround, I would like to propose the following flow.
Select an image which is used as the basic size and script is run.
The object ID of image is saved to the PropertiesService.
Select the images you want to resize and the script is run.
When your script is modified for this flow, it becomes as follows.
Modified script:
function allMenu(){
var slideUi = SlidesApp.getUi();
slideUi.createMenu('LAK')
.addSeparator()
.addSubMenu(slideUi.createMenu('Sizes')
.addItem('Select base image', 'selectBaseImage') // Added
.addItem('Same Size', 'sameSizeElements'))
.addToUi();
}
// 1. At first, it saves an image which is used as the base image.
function selectBaseImage() {
var selection = SlidesApp.getActivePresentation().getSelection();
var pageElements = selection.getPageElementRange().getPageElements();
if (pageElements.length == 1) {
PropertiesService.getScriptProperties().setProperty("baseImage", pageElements[0].getObjectId());
} else {
throw new Error("Select one image.");
}
}
// 2. As the next step, the selected images are resized using the saved image.
function sameSizeElements() {
var prop = PropertiesService.getScriptProperties();
var objectId = prop.getProperty("baseImage");
if (objectId != "") {
var selection = SlidesApp.getActivePresentation().getSelection();
var baseImage = selection.getCurrentPage().getPageElementById(objectId);
var pageElements = selection.getPageElementRange().getPageElements();
for (var i = 0; i < pageElements.length; i++) {
pageElements[i].setWidth(baseImage.getWidth());
pageElements[i].setHeight(baseImage.getHeight());
}
prop.deleteProperty("baseImage");
} else {
throw new Error("Base image was not found.");
}
}
Result:
References:
getSelection()
Properties Service
Additional context to the issue:
when a server-side function is invoked, a ctx object is sent to the /invoke endpoint that, among other info (session id ssid, document id docId, revision number rev, and application type app), contains data about currently selected PageElements in property value with key sel.
The value is an array of arrays of strings and numbers, of which only one of the elements contains strings - if you look closely, these strings are object Ids (you can confirm with getObjectId method) in order of selection.
At least this confirms that the order of selection can be respected, but the way how the abovementioned list is managed server-side changes that order. Unfortunately, I could not confirm any particular order of how the elements (apart from grouping of Ids of form g8edc625556_0_0) are returned by .getSelection().getPageElementRange().getPageElements() chain of methods:

Copying comments along with specific elements in Google Apps Script

I am writing a google apps script that places the paragraphs of one document into a dictionary, then inserts them into a set of other documents based on certain criteria. The paragraphs are extracted like so:
var totalElements = doc.getNumChildren();
for( var j = 0; j < totalElements; ++j ) {
var element = doc.getChild(j).copy();
var type = element.getType();
// logic to add element to dictionary
}
Later, once the location a paragraph should be inserted is found, it is inserted via the insertParagraph() function:
body.insertParagraph(paragraphIndex, element);
This works fine, except for the fact that some paragraphs have comments attached to them that need to be copied along with them, and this method does not keep comments attached to their elements. Is there any way to implement this? Thanks for any help!

How to copy ListItems from one Google Document to another while preserving numbering?

The accepted answer to How to copy content and formatting between Google Docs? indicates that we have to add conditional code just to copy elements. But I cannot get it to work for ListItem types, because the target document shows the list items without the original numbering.
var source_doc = DocumentApp.getActiveDocument();
var selection = source_doc.getSelection();
if (!selection) {
var ui = DocumentApp.getUi();
ui.alert('Please make a selection first.');
return;
}
var target_doc = DocumentApp.create('CopyOf'+DocumentApp.getActiveDocument().getName());
var target_body = target_doc.getBody();
var elements = selection.getRangeElements();
for (var i = 1; i < elements.length; i++) {
var source_element = elements[i].getElement();
var copy_element = source_element.copy();
if (copy_element.getType() == DocumentApp.ElementType.PARAGRAPH) {
target_body.appendParagraph(copy_element);
} else if (copy_element.getType() == DocumentApp.ElementType.LIST_ITEM) {
// This does not keep the numbering on the list item. Why?
target_body.appendListItem(copy_element);
// And playing games with setListId doesn't work either:
// copy_element.setListId(source_element);
// target_body.appendListItem(copy_element);
}
// TODO: Handle the other elements here.
}
The source document displays like this:
Target document renders like this:
How do I preserve ListItem formatting?
This seems much much harder than it should be: What I really want is to copy the users selection verbatim into a new document preserving all formatting, and from a google script.
It would seem that this could be done at a higher level. I can manually copy and paste and preserve the formatting, just not from the script.
I'm guessing that the cause of this is that there's a problem with using a Selection. Reading from a document directly seems to work fine.
Try appending the ListItem as text as a workaround.
target_body.appendListItem(copy_element.getText());
This will only copy the text though, not the formatting. You can also try to implement it by making a new list instead of copying the element directly. Here's a sample SO that might help.
I was having a similar problem (but not using a selection). It was being copied as a list but without any actual bullets. I just re-set the bullets manually like this:
target_body.appendListItem(copy_element).setGlyphType(DocumentApp.GlyphType.NUMBER)

A script for google spreadsheet to provide multiple hyperlink choice for one cell

I have a google spreadsheet. In some cells, it has multiple names(strings) that I would like to associate with individual hyperlinks.
E.g. if I have a cell such as "Charles Darwin", it's easy for me to create a hyperlink out of this name by doing something like
=Hyperlink(VLOOKUP("Charles Darwin", People!$A$1:$B$738, 2, false), "Charles Darwin")
(note that I have a "People" sheet from which I grab the hyperlink)
But if I happen to have multiple entries in that cell, say ";" or newline separated, e.g., "Charles Darwin; George Washington", I can't do that. I'd like to give the user an ability to click on the cell, have the contents of the cell be sent (as argument) to some kind of script, and for that script to find the hyperlinks in my "People" sheet for those strings, and then to present the user with a little "pop-up" right next to that cell, where the desired hyperlink could be clicked on.
I tried to find something along those lines on this site, but nothing similar seemed to come up. Might someone have a link or two for me (or basic example code) that I could start with to try to solve this? (I am assuming this is possible).
It's not possible to have two hyperlinks on the same cell.
It is possible to write scripts to Google Spreadsheets, but I'm not sure it's going to suit your use case well. The solution I see would be like this:
The user click on the desired cell, selecting it.
Then he clicks on a custom menu and picks an entry there, e.g. show links
A popup will show up (not besides the cell, but centered on the screen) with the links.
Do you think this is fine? The code would look like this (open the menu Tools > Script Editor)
function onOpen() {
SpreadsheetApp.getActive().
addMenu("Test", [{name: 'Show Links', functionName:'showLinks'}]);
}
function showLinks() {
var values = SpreadsheetApp.getActiveRange().getValue().split(';');
var app = UiApp.createApplication().setTitle('Links');
var grid = app.createGrid(values.length, 2);
for( var i = 0; i < values.length; ++i ) {
var url = findLink(values[i]);
grid.setWidget(
i, 0, app.createLabel(values[i])).setWidget(
i, 1, url ? app.createAnchor(url, url) : app.createLabel('Not Found'));
}
app.add(grid);
SpreadsheetApp.getActive().show(app);
}
var mapName2Url = null;
function findLink(name) {
if( mapName2Url == null ) { //lazy load
mapName2Url = {};
var data = SpreadsheetApp.getActive().getSheetByName('People').getDataRange().getValues();
for( var i = 1; i < data.length; ++i ) //skipping the header
mapName2Url[data[i][0]] = data[i][1];
}
return mapName2Url[name];
}
After you paste it on the script editor, run the onOpen function twice to authorize it and have the menu created for you. Next time you open the spreadsheet, the menu should be created automatically.
By the way, I have not tested this code, so it might contain dumb mistakes.