rhitakorrr / 4thewords Remote Editor Support

// ==UserScript==
// @namespace    https://openuserjs.org/users/rhitakorrr
// @name         4thewords Remote Editor Support
// @version      0.1.0
// @description  Allows 4thewords to track writing progress, auto-save, and update the UI when using a remote editor (e.g. Vim with GhostText)
// @author       rhitakorrr
// @copyright    2021, rhitakorrr (https://openuserjs.org/users/rhitakorrr)
// @licence      MIT
// @match        *://4thewords.com/*
// @grant        none
// ==/UserScript==

// ==OpenUserJS==
// @author       rhitakorrr
// ==/OpenUserJS==

(function() {
    'use strict';

    const DELAY = 1000; // auto-save delay (in milliseconds)
    const DEBUG_LOGGING = false; // enable/disable debug logging

    function log(txt, always) {
        if(DEBUG_LOGGING || always === true) console.log("4TW/Auto-Save: " + txt);
    }

    function getCurrentPage() {
        return window.location.pathname.split("/").slice(1).join("/");
    }

    function currentPageIsEditor() {
        return getCurrentPage().split("/").slice(0,2).join("/") === "files/editor";
    }

    function createStagingTextArea() {
        const $textarea = document.createElement("div");
        $textarea.id = "staging-textarea";
        $textarea.contentEditable = "true";
        $textarea.style = "height: 82vh; overflow-y: auto;";
        return $textarea;
    }

    function getTextContainer() {
        return document.getElementById("working-file");
    }

    function whenTextContainerReady(action) {
        var intervalRef = null;
        const targetPage = getCurrentPage();

        function go() {
            const $textContainer = getTextContainer();
            if(!!$textContainer && $textContainer.textContent !== "" && getCurrentPage() === targetPage) {
                action($textContainer);
                clearInterval(intervalRef);
                intervalRef = null;
            }
        }

        intervalRef = setInterval(go, 50);
        return intervalRef;
    }

    function onPageChange(action) {
        var lastPage = null;
        function go() {
            if(getCurrentPage() !== lastPage) {
                lastPage = getCurrentPage();
                action();
            }
        }
        setInterval(go, 50);
    }

    function main() {
        const intervalRate = 250; // milliseconds

        var count = 0; // milliseconds since last change
        var lastWords = null; // words last time we checked
        var lastSaveWords = null; // our text last time we triggered a save

        var runningInterval = null; // id returned by setInterval
        var runningTextContainerInterval = null; // id returned by setInterval

        log("delay = " + DELAY + " (milliseconds)", true);

        function tryAutoSave($textarea, $textContainer) {
            const currentWords = $textarea.textContent;
            const currentHtml = $textarea.innerHTML;
            if(currentWords === lastWords) {
                count += intervalRate;
                if(count >= DELAY) {
                    if(lastSaveWords !== currentWords) {
                        log("Change detected: auto-saving", true);
                        $textContainer.dispatchEvent(new Event("input"));
                        lastSaveWords = currentWords;
                    } else {
                        log("Skipping auto-save: No Change.")
                    }
                    count = 0;
                }
            } else {
                count = 0;
                if(currentWords.trim() === "") {
                    $textContainer.innerHTML = " ";
                } else {
                    $textContainer.innerHTML = currentHtml;
                }
            }
            lastWords = currentWords;
            log("count = " + count + ", remaining = " + (DELAY - count));
        }

        onPageChange(function() {
            log("Page changed; clearing intervals and last words vars", true);

            clearInterval(runningInterval);
            runningInterval = null;

            clearInterval(runningTextContainerInterval);
            runningTextContainerInterval = null;

            count = 0;
            lastSaveWords = null;

            if(currentPageIsEditor()) {
                log("On editor page: starting up...", true);
                runningTextContainerInterval = whenTextContainerReady(function($textContainer) {
                    log("Text container now exists");
                    log("Setting up staging textarea");
                    const $textarea = createStagingTextArea();
                    $textContainer.parentNode.insertBefore($textarea, $textContainer);
                    $textarea.innerHTML = $textContainer.innerHTML;
                    log("Disabling 4TW text container in favor of staging area for input");
                    $textContainer.contentEditable = "false";
                    $textContainer.dispatchEvent(new Event("input"));
                    $textContainer.style = "display: none";
                    log("Text container ready: ready to auto-save", true);
                    lastWords = $textContainer.textContent;
                    lastSaveWords = $textContainer.textContent;
                    runningInterval = setInterval(function() {
                        if(!!$textContainer && currentPageIsEditor()) { tryAutoSave($textarea, $textContainer); }
                    }, intervalRate);
                });
            }
        });
    }

    main();
})();