NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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(); })();