Canonical way to reference Elements - google-apps-script

Is there a canonical way to set an ID—or other searchable, persistent data attribute—on Elements in Google Docs, such that I can easily refer back to it later? I'm looking for something equivalent to getElementById in javascript. Almost all examples I've seen, including Google's own docs, seem to reference objects by searching for text strings or inserting new strings.
I've found one reference in the NamedRanges class to a getId function, but I can't find any place to set that ID. I do see the setAttributes function on Elements but that seems to apply only for pre-defined attribute types. I haven't tested that, though.
In case it's relevant: my interest is in automatically creating a document from a Google Sheet and populating based on the current values in the sheet. I'd like to assign specific Elements individual IDs so I can easily retrieve the Element and replace the text if the values in the sheet change later on.

Turns out that this is possible using NamedRanges, I just didn't read carefully enough.
Note: All the following examples are working off this Google doc. You can make a copy and select "Script Editor" from the Tools menu to see the code.
You can assign named ranges pretty easily using Apps Script. The below code looks through the doc for [[TITLE]] and [[ABSTRACT]] and assigns named ranges to those chunks. Note that in the aforelinked doc I put them in a table to avoid issues with partial ranges.
function assignNamedRanges() {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
const placeholders = ["title", "abstract"];
placeholders.forEach(p => {
const rangeBuilder = doc.newRange();
const text = body.findText("[[" + p.toUpperCase() + "]]");
rangeBuilder.addElement(text.getElement());
doc.addNamedRange(p, rangeBuilder.build());
});
}
Once you assigned them, you can update the range to something else in a separate function:
function updateNamedRanges() {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
const title = doc.getNamedRanges("title")[0];
const abstract = doc.getNamedRanges("abstract")[0];
title.getRange().getRangeElements()[0].getElement().asText().setText("Bob");
abstract.getRange().getRangeElements()[0].getElement().asText().setText("I like pancakes");
}
Note that NamedRanges are persistent, and the multiple NamedRange instances can have the same name. This means that if you run the first function four times, you'll have eight named ranges. You can make a convenience function to clear all those out pretty easily:
function clearNamedRanges() {
DocumentApp.getActiveDocument().getNamedRanges().forEach(r => {
r.remove();
})
}

So I been checking the documentation about elements for Google Docs in AppScript and it seems that some of them can be modified but not as freely as it looks as noted in the documentation:
Elements shown in bold can be inserted; non-bold elements can only be manipulated in place.
I tried checking with setAttributes as you mentioned however the attributes itself can only be from a document elements like: TEXT, PARAGRAPH, TABLE, ETC, this elements can't receive an ID as there is not method to insert an specific ID as you are requiring, most of the values that can be inserted are specific element attributes like: Font size, Font family, etc.

Related

Get A Google Doc Element By ID with Google App Script

Question:
I am trying to avoid looping through the document until I find the tag and then grabbing the parent. Does anyone know if there is a way to find a google doc element by an id or handle?
Context:
I have a script that opens a defined template doc; copies it into a new document element by element, and then replaces any mustache tags with the values from a passed in object using the replaceText function. This part works fine, but there is one section where I would like to repeat a table row based on the object value being an array. Unfortunately I can not seem to figure out how to locate the table in the template doc other than something like:
if('{{ some tag }}' in aElement){
var repeatableElement = aElement.getParent();
if(repeatableElement){
goRunRepeatFunction();
}
}
Just seems like there should be a way to jump directly to an element by id.
Your task can be accomplished by using this snippet:
function getElement() {
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
var numOfElements = body.getNumChildren();
var element = body.getChild(INDEX_OF_THE_CHILD).asText();
console.log(element.getText());
}
This gathers the body of the document and retrieves the number of children it has. This number represents the elements the document has. So, in order to access a certain element, you just have to make use of the getChild(index) method
Note
Please bear in mind that the getNumChildren() method will end up returning the number of all the elements in the document, including empty lines - these will be considered elements as well.
Usually, when working with a document in Apps Script, the elements are retrieved by their types in order to preserve them. So for example, if you have images in your document, it'd be best to retrieve those by using the getImages() and this will end up returning a list of containing all the images in the document, with each image being accessible by their index.
Another method would be to retrieve all the paragraphs in the body by using the getParagraphs() method, but if you know the exact structure of your document, you can retrieve all the elements by their type, and then access them as such.
Reference
Class ContainerElement - getNumChildren();
Class ContainerElement - getChild(Integer);
Class Body - getParagraphs().

How to copy and paste specific information in a same doc using Google App Script?

I'm very new using GAS what I'm trying to do is to copy some information that is already on the bottom of my doc to anywhere I want in my docs, this should work just by copying the information and paste it at the place I desire, but I want it to be done with Google App Script because it's a daily task and it's easier to do it with a function, instead of copying and pasting manually. Searching on how to do this, I found a lot of information about how to do it on Spreadsheets, but I needed it to be done on Google Docs. How can I do that?
If someone can guide me or send me a link to another similar question that would be very helpful, I don't know where to start.
This is what I have until now, I get all the data of the current doc and set it again to the page, the code gives me problems because it deletes my other information, also it selects all the doc's information. I want to select a piece of specific information and don't copy the content style.
function copyPasteInfo() {
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
var notesText = body.getText();
body.appendPageBreak();
body.setText(notesText);
}
Link to the doc document
https://docs.google.com/document/d/1s2TCspXbjvHVurwhIWSdwJ_hMcZIoLTKj4FAB82nmhM/edit
Video example of how what i want to do
https://www.screencast.com/t/UmEon8Fm0lPe
Picture of the information i'm trying to copy and paste to the bottom of my doc
If I correctly understood your question, this code will help you to achieve your goal.
let body = DocumentApp.getActiveDocument().getBody();
function moveTable() {
// Get the last table and the previous table found in your Doc
const [previousTable, bottomTable] = getDesireTables();
// Make a copy of your last table
const bottomTableCopy = bottomTable.copy();
// Get the previous table's index
const previousTableIndex = body.getChildIndex(previousTable);
// Insert the last table's copy under the previous table in your Doc
body.insertTable(previousTableIndex + 1, bottomTableCopy);
// Remove the original last table
body.removeChild(bottomTable);
}
function getDesireTables(){
const tablesArr = body.getTables().slice(-3);
// Get the parent element type to check if it's a cell
const parentELementType = tablesArr[tablesArr.length - 1].getParent().getType();
if(parentELementType === DocumentApp.ElementType.TABLE_CELL){
// If there's a table inside a table, return this
return tablesArr.slice(0, 2);
}
else{
return tablesArr.slice(-2);
}
}
What I did was to get the last two tables in the Doc, then I made a copy of the last one and with the index of the previous one, I inserted it under the previous one.
Edit
I noticed you had a table inside a table. therefore I added the getDesireTables function. Which it will check if your bottom table has a table inside.
Docs
These are the docs I used to help you:
getTables().
copy().
insertTable(childIndex, table).

Store and retrieve selection elements in Google Apps Script via PropertiesService

The goal is to store the selected text into PropertiesService for later insertion into a Google Doc. Kind of like cut and paste so that the user can collapse and expand elements.
The code is
ui = DocumentApp.getUi();
var selection = DocumentApp.getActiveDocument().getSelection();
ui.alert(JSON.stringify(selection))
The alert contents is
[{}]
I understand that selection returns a Range. So I tried to store individual elements:
if (selection) {
var elements = selection.getRangeElements();
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
ui.alert(JSON.stringify(element.getElement()))
}
}
The alert contents for individual elements is always
[{}]
where there is one {} for every element in the list.
The functions I plan to use to store and retrieve follow:
function save_value(elements) {
PropertiesService.getScriptProperties().setProperty('selection', JSON.stringify(elements));
}
function get_value() {
return JSON.parse(PropertiesService.getScriptProperties().getProperty('selection'));
}
I realize there is a workaround here by storing the element values.
element.getElement().asText().getText();
This comes with downsides.
Code complexity when elements are partially selected. If the selection is partially selecting an element, it creates complexity in the code. The code would have to leverage isPartial(), getStartOffset() and getEndOffsetInclusive().
Code complexity in multiple elements. Instead of one swift store, I have to iterate and chain each store, or have multiple stores, or something else.
Unsure about this one but code complexity in distinct elements. Based on the element, the value method varies. For example, storing an image vs text.
Note, I also realized that not all elements can be inserted, and am experimenting to see if there is a workaround by using elements.

How can I email a specific range of cells while KEEPING the displayed formatting?

I am trying to export a specific range of cells in the form of an email. The cells are formatted a certain way, and also have conditional formatting associated for color-coding.
Here is my demo sheet: https://docs.google.com/spreadsheets/d/1ibB87Vhz7wTjKrIrasYSRLoAiadQHtNqqmyl-xywtOI/edit?usp=sharing
Ive gotten the email to send successfully, however I cant figure out how to take the associated formatting, conditional formatting, fonts, borders...etc.
Currently, it's taking the displayValues, and listing them with comas. I'm also looking to get rid of the comas, and if possible, limit the range to only cells with data within the range.
Here is what I've got:
function sendReport(){
var incidents = SpreadsheetApp.getActiveSheet().getRange(2,1,4,1).getDisplayValues();
var subjectRange = SpreadsheetApp.getActiveSheet().getRange("A1");
var subjectCell = subjectRange.getValues();
for (i in subjectCell) {
var rowData = subjectCell[i];
var subject = rowData [0];
}
MailApp.sendEmail("emailaddress#domain.com",
subject,
incidents);
}
That's because you are literally sending an Array in your email (since getValues() returns a 2-dimensional Array). If you want to keep formatting, you should construct an html table from this array by iterating over it and append the result to email body via htmlBody property in options.
Take a look at the documentation for detailed info on how to get styles from Range. You will have to map each styling into style="" attribute for HtmlElements you want to be formatted, like so (to save resources, get styling in bulk and include into Array loop):
getFontColors() - color:{method result} (if one, apply to HtmlTableElement);
getFontFamilies() - font-family:{method result};
...etc...
or simply get all styles in bulk with getTextStyles() and access individual style property with appropriate methods (note that you will still need to access background colors with getBackgrounds() method, height with getHeight(), etc).

Get page selection including HTML?

I'm writing a Chrome Extension, and I was wondering if it was possible to get the selected text of a particular tab, including the underlying HTML? So if I select a link, it should also return the <a> tag.
I tried looking at the context menu event objects (yes, I'm using a context menu for this), and this is all that comes with the callback:
editable : false
menuItemId : 1
pageUrl : <the URL>
selectionText : <the selected text in plaintext formatting, not HTML>
It also returns a Tab object, but nothing in there was very useful, either.
So I'm kind of at a loss here. Is this even possible? If so, any ideas you might have would be great. Thanks! :)
Getting the selected text of a page is fairly easy, you can do something like
var text = window.getSelection().toString();
and you'll get a text representation of the currently selected text that you can pass from a content script to a background page or a popup.
Getting HTML content is a lot more difficult, mostly because the selection isn't always at a clean HTML boundary in the document (what if you only select a small part of a long link, or a few cells of a table for example). The most direct way to get all of the html associated with a selection is to reference commonAncestorContainer, which is a property on a selection range that corresponds with the deepest node which contains both the start and end of the selection. To get this, you'd do something like:
var selection = window.getSelection();
// Only works with a single range - add extra logic to
// iterate over more ranges if needed
var range = selection.getRangeAt(0);
var container = range.commonAncestorContainer;
var html = container.innerHTML
Of course, this will likely contain a lot of HTML that wasn't actually selected. It's possible that you could iterate through the children of the common ancestor and prune out anything that wasn't in the selection, but that's going to be a bit more involved and may not be necessary depending on what you're trying to do.
To show how to wrap this all up into an extension, I've written a short sample which you can reference:
http://github.com/kurrik/chrome-extensions/tree/master/contentscript-selection/
If you don't want all of the siblings, just the selected HTML, use range's other methods like .cloneContents() (to copy) or .extractContents() (to cut).
Here I use .cloneContents():
function getSelectedHTML() {
var range = window.getSelection().getRangeAt(0); // Get the selected range
var div = document.createElement("div");
div.appendChild(range.cloneContents()); // Get the document fragment from selected range
return div.innerHTML; // Return the actual HTML
}