Save contents of unsubmitted Google Classroom question responses

Prevents unsubmitted work in question assignments from being lost after closing the tab or browser

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name        Save contents of unsubmitted Google Classroom question responses
// @namespace   Violentmonkey Scripts
// @match       *://classroom.google.com/*
// @grant GM_getValue
// @grant GM_setValue
// @version     1.3
// @author      CyrilSLi
// @description Prevents unsubmitted work in question assignments from being lost after closing the tab or browser
// @license     MIT
// ==/UserScript==

const spanText = "Your answer";
var textarea, span;
const retries = 100;
var retry = 0, retryTimeout = 0;
function findEls() {
    textarea = [...document.getElementsByTagName("textarea")].filter(
        t => t.placeholder === "Type your answer" && window.getComputedStyle(t).visibility !== "hidden"
    )[0];
    span = [...document.getElementsByTagName("span")].filter(
        s => s.textContent.startsWith(spanText) && window.getComputedStyle(s).visibility !== "hidden"
    )[0];

    if (textarea == null || span == null) {
        retryTimeout = setTimeout(findEls, 200);
        retry++;
        if (retry === retries) {
            console.error("Question assignment not found on page.");
            clearTimeout(retryTimeout);
            throw new Error();
        }
    } else {
        console.log("Found question elements.");
        saveQuestion();
        clearTimeout(retryTimeout);
    }
}

var pageURL = "", pageID = "";
const pageRegEx = new RegExp("\/c\/[A-Z0-9a-z]+?\/sa\/[A-Z0-9a-z]+");
const observer = new MutationObserver((muts) => {
    if (document.location.pathname !== pageURL) {
        pageURL = document.location.pathname;
        if (pageRegEx.test(document.location)) {
            removeEventListener("input", areaInput);
            removeEventListener("keydown", typeTabs);
            clearTimeout(retryTimeout);
            console.log("Found question assignment.");
            retry = 0;
            pageID = document.location.pathname.match(pageRegEx)[0];
            setTimeout(findEls, 0);
        }
    }
});
observer.observe(document.body, {
    attributes: true
});

function setSpanTime() {
    span.textContent = `${spanText} (last updated ${new Date().toLocaleTimeString("en-US", { hour12: false })})`;
}
function areaInput(ev) {
    GM_setValue(pageID, textarea.value);
    setSpanTime();
}
function typeTabs(ev) {
    // https://stackoverflow.com/a/6637396
    if (ev.key == "Tab") {
        ev.preventDefault();
        const start = this.selectionStart;
        const end = this.selectionEnd;
        this.value = this.value.substring(0, start) + "\t" + this.value.substring(end);
        this.selectionStart = this.selectionEnd = start + 1;
    }
}
function saveQuestion(ev) {
    textarea.addEventListener("input", areaInput);
    textarea.addEventListener("keydown", typeTabs);
    textarea.value = GM_getValue(pageID, "");
    refreshEls();
}
function refreshEls() {
    rows = 1;
    if (!(textarea.scrollHeight || textarea.clientHeight)) {
        setTimeout(refreshEls, 200);
        return;
    }
    while (textarea.scrollHeight > textarea.clientHeight) {
        textarea.setAttribute("rows", rows++);
    }
    setTimeout(setSpanTime, 1000);
}
window.addEventListener("beforeunload", () => {
    removeEventListener("input", areaInput);
    textarea.value = "";
});