TL;DR: use the code snippet below.
It seems that what Google Docs does when you select Insert > Table, is to break up the current paragraph into two paragraphs, and then inserts a new table in between.
The difficult part is to split the paragraph in two. I've tried to achieve this in several ways. I couldn't find any API call from Google Apps Script documentation that does this (the Spreadsheet API has a moveTo()
method on a Range
object, but that can't help us here). I was hoping I could copy elements from a the given position in the paragraph onward to another paragraph and remove the originals. However, since the Document Service does not allow explicit insertion of several element types but can only manipulate them in-place, such as Equation
elements, copying these one-by-one is impossible.
Luckily, Paragraph
has a copy()
method which performs a deep copy. So my take on this is to copy the entire paragraph, remove everything from the cursor position onward in the original paragraph, and remove everything up to where the cursor was in the copy of the paragraph. This way, you have the paragraph split in the middle and you can insert a table at the required position. It works the same way for a ListItem
.
Here is the code for the function splitParagraphAt()
that returns the newly created paragraph (or list item; which is the next sibling of the original one). I threw in some extra checks in the code to make sure what it's doing is what you think it's doing. Following that, I added a short code excerpt on how this may be used to insert a table at the current cursor position. One might use splitParagraphAt()
similarly to insert any element at the cursor position.I haven't tested this thoroughly, so any input is welcome.
/**
* Splits the contents of the paragraph (or list item) at the given position,
* producing two adjacent paragraphs (or list items). This function may be used
* to insert any kind of element at an arbitrary document position, but placing
* it immediately before the second paragraph (or list item).
*
* @param {Position} pos The position where the paragraph (or list item) should
* be split. `pos.getElement()` should be either a Text, Paragraph or
* ListItem object.
*
* @returns {ContainerElement} The second (newly created) Paragraph or ListItem
* object.
*
*/
function splitParagraphAt(pos) {
var el = pos.getElement(), offset = pos.getOffset();
var inParagraph = (el.getType() == DocumentApp.ElementType.PARAGRAPH || el.getType() == DocumentApp.ElementType.LIST_ITEM);
if (!inParagraph && (el.getType() != DocumentApp.ElementType.TEXT)) {
throw new Error("Position must be inside text or paragraph.");
}
var par;
if (inParagraph) {
// in this case, `offset` is the number of child elements before this
// Position within the same container element
par = el;
if (offset == par.getNumChildren()) {
// we're at the end of the paragraph
return par.getParent().insertParagraph(
par.getParent().getChildIndex(par) + 1, "");
}
el = par.getChild(offset);
}
else {
par = el.getParent();
if (par == null || (par.getType() != DocumentApp.ElementType.PARAGRAPH && par.getType() != DocumentApp.ElementType.LIST_ITEM)) {
throw new Error("Parent of text is not a paragraph or a list item.");
}
}
var parContainer = par.getParent();
if (!("insertParagraph" in parContainer)) {
throw new Error("Cannot insert another paragraph in this container.");
}
// This assumes the given position is in the current document.
// alternatively, one may traverse through parents of par until document
// root is reached.
var doc = DocumentApp.getActiveDocument();
var elIndex = par.getChildIndex(el);
var newPar = par.copy();
var newEl = newPar.getChild(elIndex);
// remove everything up to position from the new element
if (!inParagraph && (offset != 0)) {
newEl.deleteText(0, offset-1);
}
newEl = newEl.getPreviousSibling();
while (newEl != null) {
// get the previous sibling before we remove the element.
var prevEl = newEl.getPreviousSibling();
newEl.removeFromParent();
newEl = prevEl;
}
// since we might remove el itself, we get the next sibling here already
var nextEl = el.getNextSibling();
// remove everything from position onwards in the original element
if (!inParagraph && (offset != 0)) {
el.deleteText(offset, el.getText().length-1);
}
else {
// we're at the beginning of the text (or just before a paragraph
// subelement) and need to remove the entire text/subelement.
el.removeFromParent();
}
el = nextEl;
while (el != null) {
// get the next sibling before we remove the element.
nextEl = el.getNextSibling();
el.removeFromParent();
el = nextEl;
}
// actually insert the newly created paragraph into the document tree.
switch (par.getType()) {
case DocumentApp.ElementType.PARAGRAPH:
parContainer.insertParagraph(parContainer.getChildIndex(par)+1, newPar);
break;
case DocumentApp.ElementType.LIST_ITEM:
parContainer.insertListItem(parContainer.getChildIndex(par)+1, newPar);
break;
}
return newPar;
}
Here's the code excerpt for inserting a table at the cursor position and setting the cursor position in the first cell of the table:
var doc = DocumentApp.getActiveDocument();
var cursor = doc.getCursor();
var el = (cursor.getOffset() == 0? cursor.getElement() : splitParagraphAt(cursor));
var parentEl = el.getParent();
var table = parentEl.insertTable(parentEl.getChildIndex(el), [['ಠ‿ಠ']]);
doc.setCursor(doc.newPosition(table.getCell(0, 0), 0));
Note that there still need to be some extra checks to see if there is a selection or not, etc. Specifically, this assumed cursor
will not be null
.