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()
})
}
Related
Goal: I have a very long document with many unique sections that each have bookmarks. Any time a section is mentioned in the document, I want that mention to become a link to the corresponding bookmark. It doesn't have to be event-driven, I intend to do it from a menu.
I have the below code written to get a list of the names of each bookmarked line so I can match it to the words in the doc. I'm trying to figure out what line of code to use to link specific text to that bookmark. I've tried to use the setLinkUrl("beginningofurl" + id[i]) code, but the ID of the bookmarks doesn't tell me if it's a header or regular text, and sometimes it is just regular text. I'm wondering if there's a better way of doing this?
var DOC = DocumentApp.getActiveDocument();
function Setlink() {
var bookmarks = DOC.getBookmarks();
var names = [];
for (var i = 0; i < bookmarks.length; i++){
names.push(bookmarks[i].getPosition().getSurroundingText().getText());
}
Logger.log(names);
}
Headings are a property of Paragraph elements. To check a Bookmark to see if it is in a paragraph of a certain Paragraph Heading, we need to get the Position, then the Element, and then check if the Element is indeed a Paragraph before we can check the Paragraph Heading.
We can put our test for if an Element is a heading in a predicate function named isElementInHeading that will return true or false when given an Element.
function isElementInHeading(element) {
if (element.getType() !== DocumentApp.ElementType.PARAGRAPH) {
return false;
}
const {ParagraphHeading} = DocumentApp;
switch (element.getHeading()) {
case ParagraphHeading.HEADING1:
case ParagraphHeading.HEADING2:
case ParagraphHeading.HEADING3:
case ParagraphHeading.HEADING4:
case ParagraphHeading.HEADING5:
case ParagraphHeading.HEADING6:
return true;
}
return false;
}
This can be used to both filter the bookmarks to include only those that mark headings, and to skip over the same headings when using setLinkUrl.
The strategy in this example is to collect both the bookmark's ID and the desired text in one go using a reducer function, then search through the document for each bit of text, check that we didn't just find the header again, and then apply the link.
I am not quite sure how you are getting the URL, but I found just copying and pasting the URL into the script as const url = "https://docs.google.com/.../edit#bookmark="; worked for me.
// for Array.prototype.reduce
function getHeadingBookmarksInfo(bookmarks, bookmark) {
const element = bookmark.getPosition().getElement();
if (isElementInHeading(element)) {
return [
...bookmarks,
{ id: bookmark.getId(), text: element.getText() }
];
}
return bookmarks;
}
function updateLinks() {
const doc = DocumentApp.getActiveDocument();
const bookmarks = doc.getBookmarks();
const headingBookmarksInfo = bookmarks.reduce(getHeadingBookmarksInfo, []);
const body = doc.getBody();
headingBookmarksInfo.forEach(function(info) {
const {id, text} = info;
let foundRef = body.findText(text);
while (foundRef !== null) {
const element = foundRef.getElement();
if (!isElementInHeading(element.getParent())) {
element.asText()
.setLinkUrl(
foundRef.getStartOffset(),
foundRef.getEndOffsetInclusive(),
url + id // assumes url is hardcoded in global scope
);
}
foundRef = body.findText(text, foundRef);
}
});
}
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
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
Edit: Adding more to the code
I'm learning how to create functions that traverse and act on elements in the DOM, but I'm running into an issue where the bellow variable "child" won't hold the value of children[i] so I can't act on it. I can't even act just on children[i] so when I check for the classList, there isn't anything there. What is it that I don't understand?
var newFunc = function(className){
var current = document.body;
var children = current.childNodes;
var results = [];
if( current.classList.contains(className) ){
results.push(current);
}
for (var i=0; i<children.length; i++){
var child = children[i];
if (child.classList.contains(className)){
results.push(child);
}
}
return results;
};
Thanks! I've been looking through MDN for awhile and done some googling but I think I may be asking the wrong question.
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);
}