Pieter van der Westhuizen

Building Google Docs add-on with Apps Scripts

Did you know that 5 million businesses use Google Apps? Were you also aware that users can create and edit both Word and Excel style documents and spreadsheets right inside their browsers?

Google Apps provides a whole new set of opportunities for developers by enabling them to build web apps and automate tasks inside Google Apps using the Google Apps Script. I’ve spend some time delving into the Google Apps Script library and found I, as a Microsoft Office developer, was remarkably comfortable with the object model the Google Apps Script provides.

Google Apps Script Document Service

In this article we’ll focus on how to create a Google Docs add-on and add your own functionality to it. It bore some resemblance to the Microsoft Office object model as some objects may seem very familiar to MS Office developers.

Opening an existing document

Before we can jump into exploring the Google Apps Script for Google Docs, let’s first create a new script. To do this, log into your Google Drive account and create a new document or open an already existing document. To open an existing document simply click on its name.

To open an existing document simply click on its name.

The document will open in a new window and you’ll notice the traditional Word editing icons and functions such as font name, size and color as well as text alignment and justification, to name but a few.

You'll see the traditional Word editing icons and functions in a Google Doc.

Creating a Google Doc script

Next, we’ll create a script with which the user can choose various options to shuffle elements in the document. Create a new script by selecting Script editor… from the Tools menu.

Creating a new Google Doc script

This will open a new script editor window, when prompted, select Document in the Create script for list.

In a new script editor window select Document in the Create script for list.

This will automatically create a file called Code.gs with a number of functions for you. For this example, remove all of them except for the onOpen function. It might be a good idea to save the example code if you want to explore other options later.

Our Script Editor window should resemble the following before we start working on it:

Script Editor window

Creating a user interface for our Google Doc add-on

You can use scripts to extend Google Docs add-ons by either adding your own custom menus, dialogs or sidebars. In this example, we’ll use a sidebar for our UI. Sidebars in Google Apps are similar to Task Panes in Microsoft Office. We’ll give our users the following options to perform in the open documents:

  • Shuffle all paragraphs
  • Shuffle selected sentences
  • Shuffle selected words

Google Apps Scripts provide the UI Service which developers can use to build user interfaces for their scripts. It allows you to add elements such as radio, push and toggle buttons as well as a variety of other UI elements.

In this example, we’ll need 3 radio buttons and a normal push button to perform the shuffling action based on the radio button the user selected. To do this, in the onOpen function, we first need to get a reference to the documents’ UI environment object, from there we’ll invoke its showSideBar function. The showSideBar function in turn accepts a parameter of either an HtmlOutput or UiInstance object. We’ll create a new UiInstance.

Add the following to your onOpen function:

function onOpen() {
    var myUI = DocumentApp.getUi();
    myUI.showSidebar(buildUI());
}

The buildUI function returns a UiInstance object. We first set the title of the sidebar by setting it in the setTitle function. We then create a hidden element called selectedRadioButton. This element will hold the name of the radio button the user selected. You create this element by calling the createHidden function of the UiInstance object.

We then create three radio buttons and a push button, these buttons are created via the createRadioButon and createButton functions. Notice you can set the Id and text by using the setId and setText functions, respectively. This is the case with both the createHidden and createRadioButton functions.

Since we want the elements to be shown underneath each other, we’ll add a Vertical Panel to the UI via the ceateVerticalPanel function and proceed to add the buttons to this panel.

The code for the buildUI function follows below:

function buildUI() {
    var app = UiApp.createApplication();
    app.setTitle('Randomizer Sidebar');
 
    var selectedRadioButton = app.createHidden().setName('selectedRadioButton').setId('selectedRadioButton');
    var radioShuffleParagraphs = app.createRadioButton('radioShuffle').setId('radioShuffleParagraphs').setText('Shuffle all paragraphs.');
    var radioShuffleSentences = app.createRadioButton('radioShuffle').setId('radioShuffleSentences').setText('Shuffle selected sentences.');
    var radioShuffleWords = app.createRadioButton('radioShuffle').setId('radioShuffleWords').setText('Shuffle selected words.');
    var buttonShuffle = app.createButton("Keep on Shufflin'");
    var panel = app.createVerticalPanel();
 
    panel.add(radioShuffleParagraphs);
    panel.add(radioShuffleSentences);
    panel.add(radioShuffleWords);
    panel.add(buttonShuffle);
    app.add(panel);
 
    var handler = app.createServerHandler('shuffleIt');
    buttonShuffle.addClickHandler(handler);
 
    var handlerRadioButtons = app.createServerHandler('radioButtonsChange')
    radioShuffleParagraphs.addValueChangeHandler(handlerRadioButtons);
    radioShuffleSentences.addValueChangeHandler(handlerRadioButtons);
    radioShuffleWords.addValueChangeHandler(handlerRadioButtons);
 
    return app;
}

Creating server handlers and responding to UI events

The last two things we need to do for our Google Docs add-on is to add server handlers in order to respond to when the button is clicked. The function that will be called when the user clicks on the button will be called shuffleIt. This is similar to events. All three radio buttons will call the same function when their values changed. This function will be called radioButtonsChange.

All the radioButtonsChange handler will do is save the name of the radio button that was selected. This value is saved to the hidden element we’ve created earlier. The code listing for the radioButtonsChange function is as follows:

function radioButtonsChange(e) {
    ScriptProperties.setProperty('selectedRadio', e.parameter.source);
}

The shuffleIt function in return will read the value inside the hidden element, called selectedRadio and based on this value call one of three methods.

function shuffleIt(e) {
    var selectedOption = ScriptProperties.getProperty('selectedRadio');
    if (selectedOption == 'radioShuffleParagraphs') {
        shuffleParagraphs();
    } else if (selectedOption == 'radioShuffleSentences') {
        shuffleSentences();
    } else if (selectedOption == 'radioShuffleWords') {
        shuffleWords();
    }
}

Shuffle paragraphs in a Google Document

When the user selects to shuffle all paragraphs in the document, the shuffleParagraphs function will be called.

function shuffleParagraphs() {
    var doc = DocumentApp.getActiveDocument();
    var allParagraphs = doc.getBody().getParagraphs();
    var shuffledParagraphs = shuffle(allParagraphs);
    var paragraphsToInsert = [];
 
    for (var i = 0; i < allParagraphs.length; i++) {
        if (allParagraphs[i] && !allParagraphs[i].isAtDocumentEnd()) {
            allParagraphs[i].removeFromParent();
        }
    }
 
    shuffledParagraphs.forEach(function (paragraph) {
        paragraphsToInsert.push(paragraph.copy());
    });
 
    paragraphsToInsert.forEach(function (paragraph) {
        doc.getBody().appendParagraph(paragraph);
    });
 
    doc.getBody().getParagraphs()[0].removeFromParent();
}

The code above will get an array of all the paragraphs in the document, using the getParagraphs function of the Body object. The Body object is accessible via the Document object by invoking getBody.

We then shuffle the array using the shuffle function and loop through all the paragraphs in the document and remove them. We then load copies of the shuffled paragraphs into a new array and append these copies to the document before removing the last original paragraph from the document.

Shuffle sentences and words in a Google Doc

Shuffling words and sentences in a Google document is a little more complex. Both the shuffleWords and shuffleSentences functions call a function called shuffleSelectecText. This function takes the selection object and the type of item that should be shuffled as a parameter, which could either be words or sentences. The code for all three functions follows below:

function shuffleWords() {
    var doc = DocumentApp.getActiveDocument();
    var selection = doc.getSelection();
    shuffleSelectedText(selection, 'words');
}
 
function shuffleSentences() {
    var doc = DocumentApp.getActiveDocument();
    var selection = doc.getSelection();
    shuffleSelectedText(selection, 'sentences');
}
 
function shuffleSelectedText(selection, splitType) {
    var words = '';
    var shuffledWords = '';
    var allWords = '';
    var selectedElements = selection.getRangeElements();
 
    for (var i = 0; i < selectedElements.length; i++) {
        var element = selectedElements[i];
        if (element.getElement().getType() == DocumentApp.ElementType.PARAGRAPH) {
            words += element.getElement().asText().getText();
        }
        if (element.getElement().getType() == DocumentApp.ElementType.TEXT) {
            if (element.isPartial()) {
                words += element.getElement().asText().getText().substring(element.getStartOffset(), element.getEndOffsetInclusive() + 1);
            }
        }
    }
 
    if (splitType == 'words') {
        shuffledWords = shuffle(words.split(' '));
    } else if (splitType == 'sentences') {
        shuffledWords = shuffle(words.match(/[^\.!\?]+[\.!\?]+/g));
    }
 
    for (var i = 0; i < shuffledWords.length; i++) {
        allWords += shuffledWords[i] + ' ';
    }
 
    for (var i = 0; i < selectedElements.length; i++) {
        var element = selectedElements[i];
        if (element.getElement().getType() == DocumentApp.ElementType.TEXT) {
            if (element.isPartial()) {
                var searchText = element.getElement().asText().getText().substring(element.getStartOffset(), element.getEndOffsetInclusive() + 1);
                if (searchText) {
                    element.getElement().asText().replaceText(searchText, allWords);
                }
            }
        }
        if (element.getElement().getType() == DocumentApp.ElementType.PARAGRAPH) {
            var searchText = element.getElement().asText().getText()
            if (searchText) {
                element.getElement().asText().replaceText(searchText, allWords);
            }
        }
    }
 
    return allWords;
}

The shuffleSelectedText function, loops through the current selected elements in the document and determines whether it is paragraphs or text that was selected. This is done by checking the elements’ type by invoking the GetType function and comparing it to the ElementType enum, e.g.

element.getElement().getType() == DocumentApp.ElementType.PARAGRAPH

The function also checks whether the element is a partial selection, which means the user did not select an entire paragraph but only a part of it. This is done by checking the isPartial property of the element object.

From there either the words or sentences are split based on a Regex match or simply a blank character, then loaded into an array and shuffled using the shuffle function. The original selection is then replaced with the newly shuffled text.

The complete code listing for the entire scripts will be as follows:

function onOpen() {
    var myUI = DocumentApp.getUi();
    myUI.showSidebar(buildUI());
}
 
function buildUI() {
    var app = UiApp.createApplication();
    app.setTitle('Randomizer Sidebar');
 
    var selectedRadioButton = app.createHidden().setName('selectedRadioButton').setId('selectedRadioButton');
    var radioShuffleParagraphs = app.createRadioButton('radioShuffle').setId('radioShuffleParagraphs').setText('Shuffle all paragraphs.');
    var radioShuffleSentences = app.createRadioButton('radioShuffle').setId('radioShuffleSentences').setText('Shuffle selected sentences.');
    var radioShuffleWords = app.createRadioButton('radioShuffle').setId('radioShuffleWords').setText('Shuffle selected words.');
    var buttonShuffle = app.createButton("Keep on Shufflin'");
    var panel = app.createVerticalPanel();
 
    panel.add(radioShuffleParagraphs);
    panel.add(radioShuffleSentences);
    panel.add(radioShuffleWords);
    panel.add(buttonShuffle);
    app.add(panel);
 
    var handler = app.createServerHandler('shuffleIt');
    buttonShuffle.addClickHandler(handler);
 
    var handlerRadioButtons = app.createServerHandler('radioButtonsChange')
    radioShuffleParagraphs.addValueChangeHandler(handlerRadioButtons);
    radioShuffleSentences.addValueChangeHandler(handlerRadioButtons);
    radioShuffleWords.addValueChangeHandler(handlerRadioButtons);
 
    return app;
}
 
function radioButtonsChange(e) {
    ScriptProperties.setProperty('selectedRadio', e.parameter.source);
}
 
function shuffleIt(e) {
    var selectedOption = ScriptProperties.getProperty('selectedRadio');
    if (selectedOption == 'radioShuffleParagraphs') {
        shuffleParagraphs();
    } else if (selectedOption == 'radioShuffleSentences') {
        shuffleSentences();
    } else if (selectedOption == 'radioShuffleWords') {
        shuffleWords();
    }
}
 
function shuffleParagraphs() {
    var doc = DocumentApp.getActiveDocument();
    var allParagraphs = doc.getBody().getParagraphs();
    var shuffledParagraphs = shuffle(allParagraphs);
    var paragraphsToInsert = [];
 
    for (var i = 0; i < allParagraphs.length; i++) {
        if (allParagraphs[i] && !allParagraphs[i].isAtDocumentEnd()) {
            allParagraphs[i].removeFromParent();
        }
    }
 
    shuffledParagraphs.forEach(function (paragraph) {
        paragraphsToInsert.push(paragraph.copy());
    });
 
    paragraphsToInsert.forEach(function (paragraph) {
        doc.getBody().appendParagraph(paragraph);
    });
 
    doc.getBody().getParagraphs()[0].removeFromParent();
}
 
function shuffleWords() {
    var doc = DocumentApp.getActiveDocument();
    var selection = doc.getSelection();
    shuffleSelectedText(selection, 'words');
}
 
function shuffleSentences() {
    var doc = DocumentApp.getActiveDocument();
    var selection = doc.getSelection();
    shuffleSelectedText(selection, 'sentences');
}
 
function shuffleSelectedText(selection, splitType) {
    var words = '';
    var shuffledWords = '';
    var allWords = '';
    var selectedElements = selection.getRangeElements();
 
    for (var i = 0; i < selectedElements.length; i++) {
        var element = selectedElements[i];
        if (element.getElement().getType() == DocumentApp.ElementType.PARAGRAPH) {
            words += element.getElement().asText().getText();
        }
        if (element.getElement().getType() == DocumentApp.ElementType.TEXT) {
            if (element.isPartial()) {
                words += element.getElement().asText().getText().substring(element.getStartOffset(), element.getEndOffsetInclusive() + 1);
            }
        }
    }
 
    if (splitType == 'words') {
        shuffledWords = shuffle(words.split(' '));
    } else if (splitType == 'sentences') {
        shuffledWords = shuffle(words.match(/[^\.!\?]+[\.!\?]+/g));
    }
 
    for (var i = 0; i < shuffledWords.length; i++) {
        allWords += shuffledWords[i] + ' ';
    }
 
    for (var i = 0; i < selectedElements.length; i++) {
        var element = selectedElements[i];
        if (element.getElement().getType() == DocumentApp.ElementType.TEXT) {
            if (element.isPartial()) {
                var searchText = element.getElement().asText().getText().substring(element.getStartOffset(), element.getEndOffsetInclusive() + 1);
                if (searchText) {
                    element.getElement().asText().replaceText(searchText, allWords);
                }
            }
        }
        if (element.getElement().getType() == DocumentApp.ElementType.PARAGRAPH) {
            var searchText = element.getElement().asText().getText()
            if (searchText) {
                element.getElement().asText().replaceText(searchText, allWords);
            }
        }
    }
 
    return allWords;
}
 
function shuffle(array) {
    var currentIndex = array.length
      , temporaryValue
      , randomIndex
    ;
    while (0 !== currentIndex) {
        randomIndex = Math.floor(Math.random() * currentIndex);
        currentIndex -= 1;
        temporaryValue = array[currentIndex];
        array[currentIndex] = array[randomIndex];
        array[randomIndex] = temporaryValue;
    }
 
    return array;
}

If all goes well you should see the sidebar when editing the document in Google Docs, as illustrated in the following image:

The custom sidebar appears when editing the document in Google Docs.

As you can see there are a few similarities between the Microsoft Office object model and the Google Docs object model, but the two still remain very different. It is, however, fairly straightforward to use the Google Docs object model using JavaScript, because their script editor does have decent intelli-sense and the Google Apps Script documentation is also very good.

Google Apps opens up a whole new world for MS Office developers to expand their product ranges to new customers across a wide range of devices.

Thank you for reading. Until next time, keep coding!

Available downloads:

Sample Google Doc script

You may also be interested in:

4 Comments

  • https://secure.gravatar.com/avatar/167474fb93a30f6e4652844c72f05861?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D32&r=G haris says:

    Hello,

    I am very interesting regarding view and editing Word document online. Platform like Google Doc are more fascinating on this are like most of developer trying to integrated with his/her app.
    I like to integrated my .Net web application to Google Doc but not fully understanding the terms and script to achieve this goal.
    My workflow is user click button to show document -> then server send this document to Google drive -> then, server open Google Doc with correct id and password-> if ok, Google doc will open that document recently send to google drive -> user can see the document -> user can edit and save it -> while close Google docs, server download back that document to server and delete that document at Google drive -> no document on Google drive ->that document has safely on server.
    Can you help me regarding matter, any help are promptly advance thanks you?

  • https://secure.gravatar.com/avatar/e1a4c2b21a5186e0b27c1c601f418b76?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D32&r=G Pieter van der Westhuizen says:

    Hi Harris,

    Google has some very nice quickstarts articles and training materials availabel at https://developers.google.com/apps-script/quickstart/docs

    From your .Net app you would probably have to use OAuth to allow the user to authenticate with their Google Drive account, from there you would be able to call the Google API’s.

    Good luck with the project!

  • https://secure.gravatar.com/avatar/c6aa27f089f66ced23f23a3a233a7b67?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D32&r=G Pamela M. says:

    Hi Peter,

    Thanks for the really useful demonstration of how to manipulate Google Document content. I’ve spent the last 2 weeks trying to solve a simple problem, and your above example provides some useful insight, but perhaps you can help me with one remaining key ingredient that still remains elusive to me: for Google Documents that have “Suggested Edits” in them, how might I programmatically (using Apps Scripts) accept all suggested edits (and remove all comments) and create a new google document with those accepted changes?

    Based on all the various approaches I have tried, I suspect that the above code manipulates the text as if all suggested edits were not incorporated. Am I correct in this thinking? Where are the suggested edits “buried” in Google Documents, and how can they be used to replace the test that they are supposed to replace, in a completely programmatic manner?

    One approach that I (only yesterday) discovered was that one can insert javascript into the browser’s URL text box after bringing up a Google Document into a Chrome browser window (see http://www.tcg.com/blog/accept-all-bookmark-for-google-docs-suggestions/ for this super-easy trick). The problem is, this approach is a manual one that requires human intervention (e.g., must load up the Google Document into Chrome browser, then apply the Javascript in the URL window). I’m looking for an approach in which one can “var doc = DocumentApp.getActiveDocument()”, then by code accept all the suggested changes and remove comments, then save the resulting changed content into a new Document.

    I’ve written an Apps script that reads in the document, makes a copy of the document (the copy is the one for all suggested changes to be accepted, and successfully then does the remaining desired functions with the copied version of the document. But I am utterly stuck with the final hurdle: how to get that copied file to have all of the “suggested changes” to be accepted, and then overwrite the copied file with those accepted changes? Sadly, there is very little “out there” on the topic of suggested changes in Google Documents and how to manipulate them. The fact that javascript is bewildering to me (I cut my teeth on Fortran and IDL) isn’t helping, because much of the discussion that I run across is far too advanced for me to even know if I am reading something that is relevant to my problem. (That’s why I am especially appreciative of your above post — the articulate description of what the code is doing is fantastic).

    thanks for any advice you might have in solving this (hopefully trivial) problem! (Sorry for the long novel I appear to have written here!)

    –Pam

  • https://secure.gravatar.com/avatar/e1a4c2b21a5186e0b27c1c601f418b76?s=32&d=https%3A%2F%2Fsecure.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D32&r=G Pieter van der Westhuizen says:

    Hi Pam,

    I’m glad to hear the article were able to help you. From what it sound like, you’re pretty much there.

    What I can suggest is that you use the code detailed on http://www.tcg.com/blog/accept-all-bookmark-for-google-docs-suggestions/ and create a function called acceptSuggestions. Full JavaScript code follows:

    function acceptSuggestions() {
    var d = document.getElementsByClassName(“docos-accept-suggestion”);
    d = Array.prototype.slice.call(d);
    d.forEach(
    function (n) { var e = document.createEvent(“MouseEvents”);
    e.initEvent(“click”, true, false); n.dispatchEvent(e, true);
    e = document.createEvent(“MouseEvents”); e.initEvent(“mousedown”, true, false);
    n.dispatchEvent(e, true); e = document.createEvent(“MouseEvents”);
    e.initEvent(“mouseup”, true, false); n.dispatchEvent(e, true); });
    };

    You mentioned that you already make a copy of the document, so all you need to do then is to call the acceptSuggestions method after you’ve copied the document and the copied document is open. Make sure the function is inside a JS file that you can access and simply call the function like so:

    acceptSuggestions();

    Hope this helps!

    Good luck.

Post a comment

Have any questions? Ask us right now!