In a document similar to the above, I can get all the paragraphs with the following code:
var paras = body.getParagraphs();
Notice that the code above not only returns the top level paragraphs but also returns all the sub-level paragraphs inside ListItem
s, Table
s etc.
How can I do the same thing within a selected range? Following code only returns top level elements.
const selection = DocumentApp.getActiveDocument().getSelection();
var rangeElements = selection.getRangeElements();
For example, the table above contains 9 non-empty paragraphs and I'd like to process them one by one if they are in selection.
What I'm trying to achieve is similar to translating the text in a selection by preserving the formatting, tables, list items etc. as much as possible.
.getRangeElements()
returns an array of RangeElements. A range element is a wrapper object that is used to help us deal with partial selections. We can call.getElement()
on each item in this array to get the Element object which is a very generic object that can represent almost any piece of a Google Doc.Elements
have a.getType()
method that return an ElementType enum; and there are a lot of them!Let's use what we know so far to see what the possible types are in a Google Doc (I've created one similar to yours (img) as an example):
Ah Ha! It looks like we only have to deal with PARAGRAPH, LIST_ITEM, and TABLE ElementTypes for now, but let's keep their children in mind too (We will find out that these are 3 of 5 that can have children). This sounds like a job for a recursive function that will continually dig down into child elements until we've found and dealt with them all.
So let's try that. This next part may look confusing but essentially it is finding an element, checking if it has children, then looking at those to see if they have children, and so on. We also want to check if we are getting new ElementTypes to deal with as well...
Alright, so each line of the log is a chain of Elements and their children. We have some new ElementTypes (HORIZONTAL_RULE, TABLE_ROW, and TEXT). If a chain is only a
Paragraph
and has no children, indicated by 'PARAGRAPH.' we can ignore it as it is a blank line. We can also ignoreHORIZONTAL_RULE
as this obviously won't contain text.If we have gotten to a TEXT Element it means we can perform our function (ie. for OP it would be a translation) like we have done with LIST_ITEMs and PARAGRAPHs. However, we still have to deal with TableRow Objects (which logs like this:
TABLE.TABLE_ROW
). This is similar to our main 3 elements and can be used with ourif(elemType == "TABLE" || elemType == "LIST_ITEM" || elemType == "PARAGRAPH")
which changes toif(elemType == "TABLE" || elemType == "LIST_ITEM" || elemType == "PARAGRAPH" || elemType == "TABLE_ROW")
.This gives us another new Element in our chain; TableCell (logs like:
TABLE.TABLE_ROW.TABLE_CELL
), which we can again add to our if statement making it:if(elemType == "TABLE" || elemType == "LIST_ITEM" || elemType == "PARAGRAPH" || elemType == "TABLE_ROW" || elemType == "TABLE_CELL")
Time to see what happens when we've dealt with Table ElementTypes.
This is great! We've reached into the depths of every parent element and reached either a Text Element or a blank paragraph! From here we can slightly modify our code to add the functions that we want to perform while maintaining the structure of the document:
BOOM! Done. I know this is a really long post, but I've broken down each section of the solution into parts to help new Apps Script coders understand the structure of a Selection (and Document Body, I guess) and how to modify it when the structure is very complicated (many nested Elements). I really hope this was helpful. If anybody sees a piece that can be improved, let me know.
As a note to OP: Be warned that this doesn't necessarily deal with partial selections of an Element, but that can easily be dealt with by modifying the first function a little to check for
isPartial()
on the RangeElement.