Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to programmatically update Google Docs in real-time (via Chrome Extension or external JS)

I would like to programmatically edit my Google Doc externally via a Google Chrome extension or a simple JavaScript and see the changes live (in real-time) in the Google Doc. When this question came up, I was looking for Chrome Extensions that edit a Google Doc and save the changes programmatically. During my research, I came across Grammarly. I am impressed by how they manage to apply their spelling corrections to the Google Doc in near real-time. You can reproduce it like this:

  1. Install Grammarly Chrome Extension
  1. Open/create a Google Doc
  2. Let Grammarly check your text (words that contain errors are highlighted)
  3. Left click on a highlighted word
  4. Click on the suggested correction

Grammarly will then update the Google Doc. What I noticed thereby:

  • The Google Doc doesn't seem to be updated via the Google Docs API or Google AppScript
  • The saving process seems to behave the same as the official autosave mechanism of Google Docs itself when the user manually edits the doc. This can be explained as follows:
    • Autosave indicator is triggered
      enter image description here
    • Google Docs save HTTP request is executed
      The HTTP FormData also contains the appropriate parameter combination (an existing word within the index range si (startIndex) and ei (endIndex) defined in the 1st command of the array is replaced by the new word in the 2nd command)
[{"commands":[{"ty":"ds","si":229,"ei":232}, {"ty":"is","ibi":229,"s":"Test"}]}]

enter image description here

I have already tried the following solutions:

  1. Use the Google Docs API.
    Result: ✓ works but with a noticeable delay of up to 5 seconds
gapi.client.docs.documents.batchUpdate({
    documented: <docId>,
    requests: [
        {
            deleteContentRange: {
                range: {
                    startIndex: 1,
                    endIndex: 10,
                },
            },
        },
        {
            insertText: {
                location: {
                    index: 1,
                },
                text: 'Lorem ipsum',
            },
        },
    ],
})
  1. Use the Google Script API to execute AppScript function.
    Result: ✓ works but with a noticeable delay of up to 5 seconds
// API call
await gapi.client.script.scripts.run({
    scriptId: <scriptId>,
    resource: {
        function: 'myFunction'
    }
})

// AppScript function from Google Script Editor
function myFunction() {
    var body = DocumentApp.openById(<docId>).getBody()
    body.appendParagraph("Lorem ipsum")
}
  1. Manipulate the DOM directly in the Google Doc (here I tried to edit the text of the Google Doc with JavaScript and then save it).
    Result: ✗ I couldn't find a way to trigger the autosave mechanism
  2. Manual execution of the internal Google Docs (auto-) save method.
    Result: ✗ led to an Internal Server Error

Unfortunately, all attempts so far have been unsuccessful or have not delivered the desired result.

like image 823
Druux Avatar asked Jun 29 '20 10:06

Druux


2 Answers

(This methods works even with new canvas-based rendering)

Turns out it's super easy; we were all overthinking it. You can just simulate a keypress with an event, and use a for loop to instantly replace whatever text you need to modify. However, Google Docs listens to key events using an Iframe with the contents

<html>
    <head></head>
    <body spellcheck="false" role="textbox" aria-label="Document content" contenteditable="true" style="background-color: transparent;"></body>
</html>

so we have to trigger the event on the IFrame rather than the document. Also, it wasn't working for me from the content script, so I had to inject another script into the page and use a custom event to tell the injected script to simulate a keypress. You can inject a script as described here, then, from the injected script, you can simulate keypresses on the document like this:

const keyEvent = document.createEvent('Event')
keyEvent.initEvent('keypress', true, true)
keyEvent.key = KEY // A key like 'a' or 'B' or 'Backspace'
  // You will need to change this line if you want to use other special characters such as the left and right arrows
keyEvent.keyCode = KEY.charCodeAt(0) 
document.querySelector('.docs-texteventtarget-iframe')
  .contentDocument.activeElement
  .dispatchEvent(keyEvent)

(obtained from this answer).

You can wrap the above code in an event listener in your injected script and dispatch a custom event , with the detail as the key name, from your content script, to trigger a keypress like this.

For my use case I didn't care where the cursor was, but if you need to find our where the cursor is so you can know how many times to simulate the left/right keys, you can get the index easily by counting the length of all the text classes and finding where the cursor is relative to each character. I found a nice little library for doing this (although, it has caveats as Google Docs apparently only loads one page at a time) that's too long to include here but you can view it on GitHub.

like image 165
Benjamin Ashbaugh Avatar answered Oct 06 '22 00:10

Benjamin Ashbaugh


As already answered here - yes, you need send key events to special iframe, not current document.

I'm searched for same functionality recently. After finding it, i made a library. In further you can use this - google-docs-utils.

  • GitHub - https://github.com/Amaimersion/google-docs-utils
  • NPM - https://www.npmjs.com/package/google-docs-utils

You can use it with Node.js or directly in browser:

  • Node.js: npm install google-docs-utils
  • Browser: https://unpkg.com/google-docs-utils@latest/dist/iife/index.js

Here is the code which solves your task using google-docs-utils package:

GoogleDocsUtils.typeText('test text'); // types at current caret position
GoogleDocsUtils.pressOn.Enter(); // move to new line
GoogleDocsUtils.typeText('another test text');

I looked at the source code of Grammarly - it uses same approach as this lib.

like image 25
Amaimersion Avatar answered Oct 06 '22 00:10

Amaimersion