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

2

10

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.

Sophocles answered 29/6, 2020 at 10:21 Comment(15)
You have used the two options you have for updating google docs. The Google docs api and app script. As there is nothing else i dont think this question can be anwsered.Ironsmith
@DaImTo I'm afraid so. Even though I'm assuming that there must be a rational explanation for why it works faster at Grammarly? There may be a special agreement between Grammarly and Google...Sophocles
I think you would have to ask Grammarly. SO cant tell you thatIronsmith
Maybe api is not accessed client side in the browser. Detect errors>user clicked highlighted word>Ping to grammerly server> server accesses docs-apiKoslo
Yeah, I think your thinking is going in the right direction @TheMaster. I took a closer look at Grammarly's minified client script of the Chrome Extension and it seems that they communicate with their server via WebSockets. I can well imagine that this can improve performance. Nevertheless, I can't explain why Google Docs' save function is executed - exactly with the parameters that are responsible for replacing one word with another.Sophocles
Have just updated my question to show how the save request is executedSophocles
Have you confirmed requests via docs api doesn't trigger save?Koslo
@Koslo yes I didSophocles
@Sophocles I'm asking because I remember it used to trigger in sheets - when a edit is made by a script, it'll show "last edit made 5 minutes ago by anonymous" on the top.Koslo
I am not familiar with sheets. Just made another API request to my doc where I inserted a text - "last edit" didn't change. save didn't trigger either. History only consists of my changes I manually made in docs. So I doubt it triggers in docs. Btw. are we talking about AppScript or gapi?Sophocles
I was talking about Apps script(built-in triggers). There is also another method: where you publish a google-apps-script-web-application to act as a custom api receiver (though that would be rare and it's essentially the same as script:run)Koslo
@Koslo Yeah, I tried that as well. But I am still experiencing a delay of 3 - 5 seconds...Sophocles
Does it trigger "save" though?Koslo
If I run it directly in the script editor, it doesn't. Haven't tried the Script API. Do you have any example for me when exactly the save function should be triggered? Not sure if that really solves my problem, though.Sophocles
I've been trying this as well, I can't figure it out either! Did you ever figure it out?Blunk
B
9

(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.

Blunk answered 2/12, 2020 at 19:40 Comment(3)
worked for me too! thanks :) it's important to use 'keypress' instead of 'keydown' even though 'keypress' is deprecatedPanada
Does this still work after Google Docs has switched to canvas-based rendering? @scitronboyArtefact
@HudsonKim It still does!Blunk
C
3

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.

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

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.

Currier answered 28/12, 2020 at 21:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.