UCalgary Card Keyboard Shortcuts

Use keyboard on cards.ucalgary.ca: numbers to select, Enter to submit/next/review, ~ to clear, supports all question types

// ==UserScript==
// @name         UCalgary Card Keyboard Shortcuts
// @version      2.2
// @description  Use keyboard on cards.ucalgary.ca: numbers to select, Enter to submit/next/review, ~ to clear, supports all question types
// @match        https://cards.ucalgary.ca/card/*
// @grant        none
// @namespace https://greasyfork.org/users/1331386
// ==/UserScript==

(function () {
    'use strict';

    function addKeyboardHints() {
        const forms = document.querySelectorAll('form.question');
        forms.forEach(form => {
            const labels = form.querySelectorAll('.option label');
            labels.forEach((label, idx) => {
                if (!label.dataset.hinted) {
                    const numberHint = `<span style="font-weight: 600; margin-right: 4px;">${idx + 1}.</span>`;
                    label.innerHTML = numberHint + label.innerHTML;
                    label.dataset.hinted = 'true';
                }
            });
        });
    }

    function clearSelections() {
        document.querySelectorAll('form.question input[type="radio"], form.question input[type="checkbox"]')
            .forEach(input => input.checked = false);
    }

    function getOptionIndex(e) {
        const keyMap = {
            Digit1: 0,
            Digit2: 1,
            Digit3: 2,
            Digit4: 3,
            Digit5: 4,
            Digit6: 5,
            Digit7: 6,
            Digit8: 7,
            Digit9: 8,
            Digit0: 9,
        };

        if (e.code in keyMap) {
            const baseIndex = keyMap[e.code];
            return e.shiftKey ? baseIndex + 10 : baseIndex;
        }

        return null;
    }

    function handleEnterKey() {
        const submitBtn = document.querySelector('form.question .submit button');
        const nextBtn = document.querySelector('#next');
        const reviewBtn = document.querySelector('div.actions span.review-buttons a.save');

        if (submitBtn && submitBtn.offsetParent !== null) {
            submitBtn.click();
        } else if (nextBtn && nextBtn.offsetParent !== null) {
            nextBtn.click();
        } else if (reviewBtn && reviewBtn.offsetParent !== null) {
            reviewBtn.click();
        }
    }

    document.addEventListener('keydown', function (e) {
        const radios = document.querySelectorAll('form.question input[type="radio"]');
        const checkboxes = document.querySelectorAll('form.question input[type="checkbox"]');

        const index = getOptionIndex(e);
        if (index !== null) {
            if (radios[index]) {
                radios[index].checked = true;
                radios[index].scrollIntoView({ behavior: "smooth", block: "center" });
            }
            if (checkboxes[index]) {
                checkboxes[index].checked = !checkboxes[index].checked;
                checkboxes[index].scrollIntoView({ behavior: "smooth", block: "center" });
            }
            return;
        }

        if (e.key === 'Enter' || e.key === ' ') {
            handleEnterKey();
        }

        if (e.key === '~') {
            clearSelections();
        }
    });

    window.addEventListener('load', addKeyboardHints);
    document.addEventListener('DOMContentLoaded', addKeyboardHints);
    const observer = new MutationObserver(addKeyboardHints);
    observer.observe(document.body, { childList: true, subtree: true });
})();