Wanikani Double-Check

Allows retyping typo'd answers, or marking wrong when WK's typo tolerance is too lax.

// ==UserScript==
// @name        Wanikani Double-Check
// @namespace   wkdoublecheck
// @description Allows retyping typo'd answers, or marking wrong when WK's typo tolerance is too lax.
// @match       https://www.wanikani.com/*
// @version     3.2.2
// @author      Robin Findley
// @copyright   2017-2024, Robin Findley
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// ==/UserScript==

// HOTKEYS:
//   "+"      - Marks answer as 'correct'.
//   "-"      - Marks answer as 'incorrect'.
//   "Escape" or "Backspace" - Resets question, allowing you to retype.

// SEE SETTINGS BELOW.

window.doublecheck = {};

(async function(gobj) {

    /* global wkof, Stimulus, WaniKani, importShim */

    let script_name = 'Double-Check';
    let wkof_version_needed = '1.2.6';

    let wkof_check_result = promise();
    let wkof_check_retries = 3;
    async function check_wkof() {
        if (!window.wkof) {
            if (--wkof_check_retries >= 0) {
                setTimeout(check_wkof, 1000);
                return wkof_check_result;
            }
            if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?')) {
                window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
            }
            return wkof_check_result;
        }
        if (wkof.version.compare_to(wkof_version_needed) === 'older') {
            if (confirm(script_name+' requires Wanikani Open Framework version '+wkof_version_needed+'.\nDo you want to be forwarded to the update page?')) {
                window.location.href = 'https://greasyfork.org/en/scripts/38582-wanikani-open-framework';
            }
            return wkof_check_result;
        }
        wkof_check_result.resolve();
        return wkof_check_result;
    }
    await check_wkof();

    const delay_before_installing = 500; // milliseconds
    wkof.on_pageload([
        '/subjects/extra_study',
        '/subjects/review',
        '/recent-mistakes/*/quiz'
    ], () => setTimeout(load_script, delay_before_installing));

    function load_script() {
        wkof.include('Menu,Settings');
        wkof.ready('Menu,Settings').then(setup);
    }

    let settings;
    let quiz_input, quiz_queue, additional_content, item_info, quiz_audio, quiz_stats, quiz_progress, quiz_header, response_helpers, wanakana;
    let answer_checker, answer_check, subject_stats, subject_stats_cache, session_stats;
    let old_submit_handler, ignore_submit, state, delay_timer, end_of_session_delay;
    let subject, synonyms, accepted_meanings, accepted_readings, srs_mgr;
    let qtype, new_answer_check, first_answer_check;

    function promise(){let a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}

    //------------------------------------------------------------------------
    // setup() - Set up the menu link and default settings.
    //------------------------------------------------------------------------
    let fresh_load = true;
    function setup() {
        fresh_load = true;
        wkof.Menu.insert_script_link({name:'doublecheck',submenu:'Settings',title:'Double-Check',on_click:open_settings});

        let defaults = {
            allow_retyping: true,
            allow_change_correct: false,
            show_corrected_answer: false,
            allow_change_incorrect: false,
            typo_action: 'ignore',
            wrong_answer_type_action: 'warn',
            wrong_number_n_action: 'warn',
            small_kana_action: 'warn',
            kanji_reading_for_vocab_action: 'warn',
            kanji_meaning_for_vocab_action: 'warn',
            delay_wrong: true,
            delay_multi_meaning: false,
            delay_slightly_off: false,
            delay_period: 1.5,
            warn_burn: 'never',
            burn_delay_period: 1.5,
            show_lightning_button: true,
            lightning_enabled: false,
            srs_msg_period: 1.2,
            autoinfo_correct: false,
            autoinfo_incorrect: false,
            autoinfo_multi_meaning: false,
            autoinfo_slightly_off: false,
            show_retype_button: true,
            show_change_button: true
        }
        return wkof.Settings.load('doublecheck', defaults)
            .then(init_ui);
    }

    //------------------------------------------------------------------------
    // open_settings() - Open the Settings dialog.
    //------------------------------------------------------------------------
    function open_settings() {
        let dialog = new wkof.Settings({
            script_id: 'doublecheck',
            title: 'Double-Check Settings',
            on_save: init_ui,
            pre_open: settings_preopen,
            content: {
                tabAnswers: {type:'page',label:'Answers',content:{
                    grpChangeAnswers: {type:'group',label:'Change Answer',content:{
                        allow_retyping: {type:'checkbox',label:'Allow retyping answer',default:true,hover_tip:'When enabled, you can retype your answer by pressing Escape or Backspace.',on_change:retype_setting_changed},
                        allow_change_incorrect: {type:'checkbox',label:'Allow changing to "incorrect"',default:true,hover_tip:'When enabled, you can change your answer\nto "incorrect" by pressing the "-" key.',on_change:change_setting_changed},
                        allow_change_correct: {type:'checkbox',label:'Allow changing to "correct"',default:true,hover_tip:'When enabled, you can change your answer\nto "correct" by pressing the "+" key.',on_change:change_setting_changed},
                        show_corrected_answer: {type:'checkbox',label:'Show corrected answer',default:false,hover_tip:'When enabled, pressing \'+\' to correct your answer puts the\ncorrected answer in the input field. Pressing \'+\' multiple\ntimes cycles through all acceptable answers.'},
                    }},
                    grpAnswerButtons: {type:'group',label:'Button Visibility',content:{
                        show_retype_button: {type:'checkbox',label:'Show "Retype" button',default:true,hover_tip:'When enabled, the Retype button is visible (when retyping is allowed).'},
                        show_change_button: {type:'checkbox',label:'Show "Mark Right/Wrong"',default:true,hover_tip:'When enabled, the Mark Right / Mark Wrong button is visible (when changing answer is allowed).'},
                    }},
                }},
                tabMistakeDelay: {type:'page',label:'Mistakes',content:{
                    grpCarelessMistakes: {type:'group',label:'Mistake Handling',content:{
                        typo_action: {type:'dropdown',label:'Typos in meaning',default:'ignore',content:{ignore:'Ignore',warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when meaning contains typos.'},
                        wrong_answer_type_action: {type:'dropdown',label:'Wrong answer type',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when reading was entered instead of meaning, or vice versa.'},
                        wrong_number_n_action: {type:'dropdown',label:'Wrong number of n\'s',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when you type the wrong number of n\'s in certain reading questions.'},
                        small_kana_action: {type:'dropdown',label:'Big kana instead of small',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when you type a big kana instead of small (e.g. ゆ instead of ゅ).'},
                        kanji_reading_for_vocab_action: {type:'dropdown',label:'Kanji reading instead of vocab',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when the reading of a kanji is entered for a single character vocab word instead of the correct vocab reading.'},
                        kanji_meaning_for_vocab_action: {type:'dropdown',label:'Kanji meaning instead of vocab',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when the meaning of a kanji is entered for a single character vocab word instead of the correct vocab meaning.'},
                    }},
                    grpDelay: {type:'group',label:'Mistake Delay',content:{
                        delay_wrong: {type:'checkbox',label:'Delay when wrong',default:true,refresh_on_change:true,hover_tip:'If your answer is wrong, you cannot advance\nto the next question for at least N seconds.'},
                        delay_multi_meaning: {type:'checkbox',label:'Delay when multiple meanings',default:false,hover_tip:'If the item has multiple meanings, you cannot advance\nto the next question for at least N seconds.'},
                        delay_slightly_off: {type:'checkbox',label:'Delay when answer has typos',default:false,hover_tip:'If your answer contains typos, you cannot advance\nto the next question for at least N seconds.'},
                        delay_period: {type:'number',label:'Delay period (in seconds)',default:1.5,hover_tip:'Number of seconds to delay before allowing\nyou to advance to the next question.'},
                    }},
                }},
                tabBurnReviews: {type:'page',label:'Burn Reviews',content:{
                    grpBurnReviews: {type:'group',label:'Burn Reviews',content:{
                        warn_burn: {type:'dropdown',label:'Warn before burning',default:'never',content:{never:'Never',cheated:'If you changed answer',always:'Always'},hover_tip:'Choose when to warn before burning an item.'},
                        burn_delay_period: {type:'number',label:'Delay after warning (in seconds)',default:1.5,hover_tip:'Number of seconds to delay before allowing\nyou to advance to the next question after seeing a burn warning.'},
                    }},
                }},
                tabLightning: {type:'page',label:'Lightning',content:{
                    grpLightning: {type:'group',label:'Lightning Mode',content:{
                        show_lightning_button: {type:'checkbox',label:'Show "Lightning Mode" button',default:true,hover_tip:'Show the "Lightning Mode" toggle\nbutton on the review screen.'},
                        lightning_enabled: {type:'checkbox',label:'Enable "Lightning Mode"',default:true,refresh_on_change:true,hover_tip:'Enable "Lightning Mode", which automatically advances to\nthe next question if you answer correctly.'},
                        srs_msg_period: {type:'number',label:'SRS popup time (in seconds)',default:1.2,min:0,hover_tip:'How long to show SRS up/down popup when in lightning mode.  (0 = don\'t show)'},
                    }},
                }},
                tabAutoInfo: {type:'page',label:'Item Info',content:{
                    grpAutoInfo: {type:'group',label:'Show Item Info',content:{
                        autoinfo_correct: {type:'checkbox',label:'After correct answer',default:false,hover_tip:'Automatically show the Item Info after correct answers.', validate:validate_autoinfo_correct},
                        autoinfo_incorrect: {type:'checkbox',label:'After incorrect answer',default:false,hover_tip:'Automatically show the Item Info after incorrect answers.', validate:validate_autoinfo_incorrect},
                        autoinfo_multi_meaning: {type:'checkbox',label:'When multiple meanings',default:false,hover_tip:'Automatically show the Item Info when an item has multiple meanings.', validate:validate_autoinfo_correct},
                        autoinfo_slightly_off: {type:'checkbox',label:'When answer has typos',default:false,hover_tip:'Automatically show the Item Info when your answer has typos.', validate:validate_autoinfo_correct},
                    }},
                }},
            }
        });
        dialog.open();
    }

    //------------------------------------------------------------------------
    // retype_setting_changed() - Enable/disable "show retype button" based on retype setting.
    //------------------------------------------------------------------------
    function retype_setting_changed(elem, name, value, item) {
        document.querySelector('#doublecheck_show_retype_button').toggleAttribute('disabled', !settings.allow_retyping);
    }

    //------------------------------------------------------------------------
    // change_setting_changed() - Enable/disable "show mark right/wrong" based on change setting.
    //------------------------------------------------------------------------
    function change_setting_changed() {
        document.querySelector('#doublecheck_show_change_button').toggleAttribute('disabled', !(settings.allow_change_correct || settings.allow_change_incorrect));
    }

    //------------------------------------------------------------------------
    // validate_autoinfo_correct() - Notify user if iteminfo and lightning are both enabled.
    //------------------------------------------------------------------------
    function validate_autoinfo_correct(enabled) {
        if (enabled && settings.lightning_enabled) {
            return 'Disable "Lightning Mode"!';
        }
    }

    //------------------------------------------------------------------------
    // validate_autoinfo_incorrect() - Notify user if iteminfo and lightning are both enabled, and wrong_delay disabled.
    //------------------------------------------------------------------------
    function validate_autoinfo_incorrect(enabled) {
        if (enabled && settings.lightning_enabled && !settings.delay_wrong) {
            return 'Disable "Lightning Mode", or<br>enable "Delay when wrong"!';
        }
    }

    //------------------------------------------------------------------------
    // settings_preopen() - Notify user if iteminfo and lightning are both enabled.
    //------------------------------------------------------------------------
    function settings_preopen(dialog) {
        dialog.dialog({width:525});
        dialog.find('#doublecheck_show_retype_button').prop('disabled', !settings.allow_retyping);
        dialog.find('#doublecheck_show_change_button').prop('disabled', !(settings.allow_change_incorrect || settings.allow_change_incorrect));
    }

    function insert_icons() {
		if (!document.getElementById('wk-icon__lightning')) {
			let svg = document.querySelector('svg symbol[id^="wk-icon"]').closest('svg');
			svg.insertAdjacentHTML('beforeend','<symbol id="wk-icon__lightning" viewport="0 0 500 500"><path d="M160,12L126,265L272,265L230,488L415,170L270,170L320,12Z"></path></symbol>');
		}
	}

    //------------------------------------------------------------------------
    // init_ui() - Initialize the user interface.
    //------------------------------------------------------------------------
    async function init_ui() {
        settings = wkof.settings.doublecheck;

        if (fresh_load) {
            fresh_load = false;
            await startup();
        }

        // Migrate 'lightning' setting from localStorage.
        let lightning = localStorage.getItem('lightning');
        if (lightning === 'false' || lightning === 'true') {
            localStorage.removeItem('lightning');
            settings.lightning_enabled = lightning;
            wkof.Settings.save('doublecheck');
        }

        insert_icons();

        // Initialize the Lightning Mode button.
        let lightning_icon = document.querySelector('#lightning-mode');
        if (lightning_icon) {
            lightning_icon.classList.toggle('doublecheck-active', settings.lightning_enabled);
            lightning_icon.hidden = !settings.show_lightning_button;
        }

        let rightwrong_btn = document.querySelector('#option-toggle-rightwrong');
        if (rightwrong_btn) rightwrong_btn.classList.toggle('hidden', !((settings.allow_change_correct || settings.allow_change_incorrect) && settings.show_change_button));
        let retype_btn = document.querySelector('#option-retype');
        if (retype_btn) retype_btn.classList.toggle('hidden', !(settings.allow_retyping && settings.show_retype_button));
        resize_buttons();

        additional_content = get_controller('additional-content');
        if (state === 'second_submit') {
            if (rightwrong_btn) {
                rightwrong_btn.querySelector('a').classList.toggle(additional_content.toggleDisabledClass, !(
                    (new_answer_check.passed && (settings.allow_change_incorrect || !first_answer_check.passed)) ||
                    (!new_answer_check.passed && (settings.allow_change_correct || first_answer_check.passed))
                ));
            }
            if (retype_btn) {
                retype_btn.querySelector('a').classList.toggle(additional_content.toggleDisabledClass, !settings.allow_retyping);
            }
        } else {
            if (rightwrong_btn) {
                rightwrong_btn.querySelector('a').classList.add(additional_content.toggleDisabledClass);
            }
        }
    }

    //------------------------------------------------------------------------
    // lightning_clicked() - Lightning button handler.
    //------------------------------------------------------------------------
    function lightning_clicked(e) {
        e.preventDefault();
        settings.lightning_enabled = !settings.lightning_enabled;
        wkof.Settings.save('doublecheck');
        document.querySelector('#lightning-mode').classList.toggle('doublecheck-active', settings.lightning_enabled);
        return false;
    }

    //------------------------------------------------------------------------
    // get_correct_answers() - Returns an array of acceptable answers.
    //------------------------------------------------------------------------
    function get_correct_answers() {
        if (qtype === 'reading') {
            if (subject.type === 'Kanji') {
                return subject[subject.primary_reading_type];
            } else {
                return [].concat(
                    subject.readings.map((r) => r.reading),
                    subject.auxiliary_readings.filter((r) => r.type === 'whitelist').map((r) => r.reading)
                ).filter((r) => typeof r === 'string');
            }
        } else {
            return [].concat(
                synonyms,
                subject.meanings,
                subject.auxiliary_meanings.filter((m) => m.type === 'whitelist').map((m) => m.meaning),
            );
        }
    }

    //------------------------------------------------------------------------
    // get_next_correct_answer() - Returns the next acceptable answer from the
    //    array returned by get_correct_answers().
    //------------------------------------------------------------------------
    function get_next_correct_answer() {
        let result = first_answer_check.correct_answers[first_answer_check.correct_answer_index];
        first_answer_check.correct_answer_index = (first_answer_check.correct_answer_index + 1) % first_answer_check.correct_answers.length;
        return result;
    }

    //------------------------------------------------------------------------
    // toggle_result() - Toggle an answer from right->wrong or wrong->right.
    //------------------------------------------------------------------------
    function toggle_result(new_state) {
        if (new_state === 'toggle') new_state = (new_answer_check.passed ? 'incorrect' : 'correct');
        if (state !== 'second_submit') return false;

        let input = quiz_input.inputTarget;
        let current_state = (quiz_input.inputContainerTarget.getAttribute('correct') === 'true' ? 'correct' : 'incorrect');
        let answer_to_show, answer_to_grade;
        clear_delay();
        switch (new_state) {
            case 'correct':
                if (!settings.allow_change_correct) {
                    if (!first_answer_check.passed) return;
                    answer_to_grade = first_answer_check.answer;
                    answer_to_show = answer_to_grade;
                } else if (current_state === 'correct') {
                    answer_to_grade = get_next_correct_answer();
                    answer_to_show = answer_to_grade;
                } else {
                    first_answer_check.correct_answer_index = 0;
                    answer_to_grade = get_next_correct_answer();
                    answer_to_show = (settings.show_corrected_answer ? answer_to_grade : first_answer_check.answer);
                }
                input.value = answer_to_grade;
                new_answer_check = {
                    action:'pass',
                    message:null,
                    passed:true,
                    accurate:true,
                    multipleAnswers:false,
                    exception:false,
                    answer:answer_to_grade
                };
                set_answer_state(new_answer_check);
                input.value = answer_to_show;
                break;
            case 'incorrect':
                if (!settings.allow_change_incorrect) {
                    if (first_answer_check.passed) return;
                    answer_to_show = first_answer_check.answer;
                } else {
                    answer_to_show = (settings.show_corrected_answer ? 'xxxxxx' : first_answer_check.answer);
                }
                answer_to_grade = 'xxxxxx';
                input.value = answer_to_grade;
                new_answer_check = {
                    action:'fail',
                    message:{
                        type:'itemInfoException',
                        text:`Need help? View the correct ${qtype} and mnemonic.`
                    },
                    passed:false,
                    accurate:false,
                    multipleAnswers:false,
                    exception:false,
                    answer:answer_to_grade
                };
                set_answer_state(new_answer_check);
                input.value = answer_to_show;
                break;
            case 'retype':
                if (!settings.allow_retyping) return false;
                set_answer_state({reset:true, retype:true, unanswer:true});
                break;
        }
    }

    //------------------------------------------------------------------------
    // do_delay() - Disable the submit button briefly to prevent clicking past wrong answers.
    //------------------------------------------------------------------------
    function do_delay(period) {
        if (period === undefined) period = settings.delay_period;
        ignore_submit = true;
        delay_timer = setTimeout(function() {
            delay_timer = -1;
            ignore_submit = false;
        }, period*1000);
    }

    //------------------------------------------------------------------------
    // clear_delay() - Clear the delay timer.
    //------------------------------------------------------------------------
    function clear_delay() {
        if (delay_timer) {
            ignore_submit = false;
            clearTimeout(delay_timer);
            delay_timer = undefined;
        }
    }

    //------------------------------------------------------------------------
    function show_exception(message) {
        if (typeof message !== 'string') return;
        quiz_input.exceptionTarget.textContent = message;
        quiz_input.exceptionContainerTarget.hidden = false;
    }

    //------------------------------------------------------------------------
    function hide_exception() {
        quiz_input.exceptionContainerTarget.hidden = true;
        quiz_input.exceptionTarget.textContent = '';
    }

    //------------------------------------------------------------------------
    function set_answer_state(results, final_submit) {
        quiz_stats = get_controller('quiz-statistics');
        quiz_queue = get_controller('quiz-queue');
        additional_content = get_controller('additional-content');
        item_info = get_controller('item-info');
        quiz_progress = get_controller('quiz-progress');
        quiz_audio = get_controller('quiz-audio');
        quiz_header = get_controller('quiz-header');
        if (!final_submit) {
            if (results.exception) {
                quiz_input.shakeForm();
                show_exception(answer_check.exception);
                quiz_input.inputEnabled = true;
                quiz_input.inputTarget.focus();
                return;
            }
            let rightwrong = document.querySelector('#option-toggle-rightwrong a');
            let rightwrong_text = rightwrong.querySelector('.additional-content__item-text');
            let rightwrong_icon = rightwrong.querySelector('svg');
            let retype = document.querySelector('#option-retype a');
            if (!results.passed || (results.reset === true)) {
                rightwrong.classList.toggle(additional_content.toggleDisabledClass, (results.reset === true) || !(settings.allow_change_correct || first_answer_check.passed));
                rightwrong_text.innerText = 'Mark Right';
                rightwrong_icon.classList.remove('dblchk--invert');
            } else {
                rightwrong.classList.toggle(additional_content.toggleDisabledClass, (results.reset === true) || !(settings.allow_change_incorrect || !first_answer_check.passed));
                rightwrong_text.innerText = 'Mark Wrong';
                rightwrong_icon.classList.add('dblchk--invert');
            }
            retype.classList.toggle(additional_content.toggleDisabledClass, (results.reset === true));

            if (results.reset) {
                additional_content.close();
                item_info.disable();
                quiz_audio.playButtonTarget.classList.add(quiz_audio.disabledClass)
                quiz_input.inputContainerTarget.removeAttribute('correct');
                quiz_input.inputTarget.value = '';
                quiz_input.inputChars = '';
                if (results.unanswer) window.dispatchEvent(new CustomEvent('didUnanswerQuestion'));
                quiz_input.inputEnabled = true;
                quiz_input.inputTarget.focus();

                quiz_stats.completeCountTarget.innerText = session_stats.complete.toString();
                quiz_stats.remainingCountTarget.innerText = session_stats.remaining.toString();
                let percent_complete = Math.round(100*session_stats.complete/(session_stats.complete + session_stats.remaining));
                quiz_progress.updateProgress({detail:{percentComplete:percent_complete}});
                quiz_stats.percentCorrectTarget.innerText = (session_stats.answered ? Math.round(100 * session_stats.correct / session_stats.answered).toString() + '%' : '100%');
                if (quiz_header.hasSrsContainerTarget) quiz_header.srsContainerTarget.dataset.hidden = true;
                state = 'first_submit';
                return;
            }
            quiz_input.inputEnabled = false;
            quiz_input.inputContainerTarget.setAttribute('correct', results.passed);
        }

        subject_stats = JSON.parse(subject_stats_cache.get(subject.id) || JSON.stringify({
            meaning:{
                incorrect:0,
                complete:false
            },
            reading:{
                incorrect:0,
                complete:(['Radical','KanaVocabulary'].indexOf(quiz_input.currentSubject.type) >= 0)
            }
        }));
        if (results.passed) {
            subject_stats[quiz_input.currentQuestionType].complete = true;
        } else {
            subject_stats[quiz_input.currentQuestionType].incorrect++;
        }
        if (final_submit) {
            subject_stats_cache.set(subject.id, JSON.stringify(subject_stats));
        }

        if (session_stats.remaining == null) {
            session_stats = {
                complete: 0,
                remaining: Number(quiz_stats.remainingCountTarget.innerText),
                correct: 0,
                answered: 0
            }
        }
        let temp_session_stats = Object.assign({}, session_stats);
        temp_session_stats.answered++;
        if (results.passed) temp_session_stats.correct++;
        if (subject_stats.meaning.complete && subject_stats.reading.complete) {
            temp_session_stats.complete++;
            temp_session_stats.remaining--;
        }
        end_of_session_delay = false;
        if (final_submit) {
            Object.assign(session_stats, temp_session_stats);
            if (session_stats.remaining === 0) end_of_session_delay = true;
        } else {
            quiz_stats.completeCountTarget.innerText = temp_session_stats.complete.toString();
            quiz_stats.remainingCountTarget.innerText = temp_session_stats.remaining.toString();
            let percent_complete = Math.round(100*temp_session_stats.complete/(temp_session_stats.complete + temp_session_stats.remaining));
            quiz_progress.updateProgress({detail:{percentComplete:percent_complete}});
            quiz_stats.percentCorrectTarget.innerText = Math.round(100 * temp_session_stats.correct / temp_session_stats.answered).toString() + '%';

            quiz_stats.disconnect();
            let event = {detail:{
                subjectWithStats:{subject:subject,stats:subject_stats},
                questionType:quiz_input.currentQuestionType,
                answer:quiz_input.inputTarget.value,
                results:results
            }};
            window.dispatchEvent(new CustomEvent('didAnswerQuestion',event));
            quiz_stats.connect();

            if (subject_stats.meaning.complete && subject_stats.reading.complete) {
                if (srs_mgr && !(settings.lightning_enabled && answer_check.passed)) {
                    srs_mgr.updateSRS({subject:subject,stats:subject_stats});
                }
            } else {
                if (quiz_header.hasSrsContainerTarget) quiz_header.srsContainerTarget.dataset.hidden = true;
            }

            if ((results.passed && settings.autoinfo_correct && !settings.lightning_enabled) ||
                (!results.passed && settings.autoinfo_incorrect) ||
                (results.passed && results.multipleAnswers && settings.autoinfo_multi_meaning && !settings.lightning_enabled) ||
                (results.passed && !results.accurate && settings.autoinfo_slightly_off && !settings.lightning_enabled))
            {
                item_info.toggleTarget.click();
                if (results.passed) item_info.showException(qtype,results)
            }
        }
    }

    //------------------------------------------------------------------------
    // new_submit_handler() - Intercept handler for 'submit' button.  Overrides default behavior as needed.
    //------------------------------------------------------------------------
    function new_submit_handler(e) {
        // Don't process 'submit' if we are ignoring temporarily (to prevent double-tapping past important info)
        if (ignore_submit) return;

        hide_exception();

        let input = quiz_input.inputTarget;
        qtype = quiz_input.currentQuestionType;
        subject = quiz_input.currentSubject;

        let submitted_immediately = false;
        switch (state) {
            case 'first_submit': {
                // We intercept the first 'submit' click, and simulate normal Wanikani screen behavior.

                // Do WK's standard checks for shake.
                let answer = quiz_input.inputTarget.value.trim();
                if (qtype === 'reading') {
                    answer = response_helpers.normalizeReadingResponse(answer);
                    input.value = answer;
                }
                if (!response_helpers.questionTypeAndResponseMatch(qtype, answer) || (answer.length === 0)) {
                    quiz_input.shakeForm();
                    quiz_input.inputEnabled = true;
                    quiz_input.inputTarget.focus();
                    return;
                }

                quiz_input.inputEnabled = false;
                quiz_input.lastAnswer = answer;

                // Do WK's standard answer evaluation.
                synonyms = quiz_input.quizUserSynonymsOutlet.synonymsForSubjectId(subject.id);
                answer_check = answer_checker.evaluate({questionType:qtype, response:answer, item:subject, userSynonyms:synonyms, inputChars:quiz_input.inputChars});
                if (answer_check.hasOwnProperty('action')) {
                    if (answer_check.action === 'retry') {
                        answer_check.passed = false;
                        answer_check.accurate = false;
                        answer_check.multipleAnswers = false;
                        answer_check.exception = answer_check.message.text;
                    } else {
                        answer_check.passed = (answer_check.action === 'pass');
                        if (answer_check.message === null) {
                            answer_check.accurate = true;
                            answer_check.multipleAnswers = false;
                            answer_check.exception = false;
                        } else if (/has multiple/.test(answer_check.message.text)) {
                            answer_check.accurate = true;
                            answer_check.multipleAnswers = true;
                            answer_check.exception = false;
                        } else if (/one of your synonyms/.test(answer_check.message.text)) {
                            answer_check.accurate = false;
                            answer_check.multipleAnswers = false;
                            answer_check.exception = answer_check.message.text;
                        } else if (/a bit off/.test(answer_check.message.text)) {
                            answer_check.accurate = false;
                            answer_check.multipleAnswers = false;
                            answer_check.exception = false;
                        }
                    }
                }

                // Process typos according to settings.
                if (answer_check.passed && !answer_check.accurate) {
                    switch (settings.typo_action) {
                        case 'warn': answer_check.exception = 'Your answer was close, but not exact'; break;
                        case 'wrong': answer_check.passed = false; answer_check.custom_msg = 'Your answer was not exact, as required by your settings.'; break;
                    }
                }

                // Process answer-type errors according to settings.
                if (!answer_check.passed) {
                    if (qtype === 'meaning') {
                        // Although Wanikani checks for readings entered as meanings, it only
                        // checks the 'preferred' reading.  Here, we check all readings.
                        if (subject.type === 'KanaVocabulary') {
                            accepted_readings = [subject.characters];
                        } else {
                            accepted_readings = [].concat(
                                subject.readings?.map((r)=>r.reading),
                                subject.auxiliary_readings?.filter((r)=>r.type==='whitelist').map((r)=>r.reading),
                                subject.onyomi,
                                subject.kunyomi,
                                subject.nanori
                            );
                        }
                        let answer_as_kana = to_kana(answer);
                        if (accepted_readings.indexOf(answer_as_kana) >= 0) {
                            if (settings.wrong_answer_type_action === 'warn') {
                                answer_check.exception = answer_check.exception || 'Oops, we want the meaning, not the reading.';
                            } else {
                                answer_check.exception = false;
                            }
                        }
                    } else {
                        accepted_meanings = [].concat(
                            subject.meanings,
                            subject.auxiliary_meanings?.filter((r)=>r.type==='whitelist').map((r)=>r.meaning),
                            synonyms
                        ).filter((s) => typeof s === 'string').map((s) => s.trim().toLowerCase().replace(/\s\s+/g,' '));
                        let meanings_as_hiragana = accepted_meanings.map(m => to_kana(m));
                        let answer_as_hiragana = Array.from(answer.toLowerCase()).map(c => wanakana.toHiragana(c)).join('');
                        if (meanings_as_hiragana.indexOf(answer_as_hiragana) >= 0) {
                            if (settings.wrong_answer_type_action === 'warn') {
                                answer_check.exception = 'Oops, we want the reading, not the meaning.';
                            } else {
                                answer_check.exception = false;
                            }
                        }
                    }
                }

                // Process all other exceptions according to settings.
                if (typeof answer_check.exception === 'string') {
                    if (((settings.kanji_meaning_for_vocab_action === 'wrong') && answer_check.exception.toLowerCase().includes('want the vocabulary meaning, not the kanji meaning')) ||
                        ((settings.kanji_reading_for_vocab_action === 'wrong') && answer_check.exception.toLowerCase().includes('want the vocabulary reading, not the kanji reading')) ||
                        ((settings.wrong_number_n_action === 'wrong') && answer_check.exception.toLowerCase().includes('forget that ん')) ||
                        ((settings.small_kana_action === 'wrong') && answer_check.exception.toLowerCase().includes('watch out for the small')))
                    {
                        answer_check.exception = false;
                        answer_check.passed = false;
                    }
                }

                // Remain in 'first_submit' if there was an exceptions.
                if (answer_check.exception) {
                    set_answer_state(answer_check);
                    return false;
                }
                state = 'second_submit';

                new_answer_check = Object.assign({answer:answer}, answer_check);
                first_answer_check = Object.assign({
                    answer:answer,
                    correct_answers:get_correct_answers(),
                    correct_answer_index: 0,
                }, answer_check);

                // Process "Mistake Delay" according to settings.
                if ((!answer_check.passed && settings.delay_wrong) ||
                    (answer_check.passed &&
                     ((!answer_check.accurate && settings.delay_slightly_off) ||
                      (answer_check.multipleAnswers && settings.delay_multi_meaning))
                    )
                   )
                {
                    set_answer_state(new_answer_check);
                    do_delay();
                    return false;
                }

                set_answer_state(answer_check);

                // Process lightning mode according to settings.
                if (settings.lightning_enabled && answer_check.passed) {
                    new_submit_handler(e);
                    return false;
                }

                return false;
            }
            case 'second_submit': {
                // We intercepted the first submit, allowing the user to optionally modify their answer.
                // Now, either the user has clicked submit again, or lightning is enabled and we are automatically clicking submit again.

                let answer = new_answer_check.answer;
                input.value = answer;
                set_answer_state(new_answer_check, true /* final_submit */);
                delete new_answer_check.answer;

                // Nasty hack to prevent audio from playing twice or stopping upon next question.
                let audio = quiz_audio.audioTarget;
                audio.setAttribute('data-quiz-audio-target', 'noplay');
                audio.insertAdjacentHTML('afterend', '<audio class="quiz-audio__audio dblchk" data-quiz-audio-target="audio"></audio>');
                let tmp_audio = document.querySelector('audio.dblchk');
                quiz_audio.disconnect();

                function dispatch_didFinalAnswer(e) {
                    window.dispatchEvent(new CustomEvent('didFinalAnswer',{detail:e.detail}));
                    window.removeEventListener('didAnswerQuestion', dispatch_didFinalAnswer);
                }
                window.addEventListener('didAnswerQuestion', dispatch_didFinalAnswer);
                quiz_queue.submitAnswer(answer, new_answer_check);

                // Nasty audio hack, continued.
                setTimeout(() => {
                    tmp_audio.remove();
                    audio.setAttribute('data-quiz-audio-target', 'audio');
                    quiz_audio.connect();
                }, 1);

                if (end_of_session_delay) {
                    setTimeout(next_item, 500);
                } else {
                    next_item();
                }

                function next_item() {
                    quiz_queue.nextItem();
                    set_answer_state({reset:true, unanswer:false});

                    quiz_header = get_controller('quiz-header');
                    if (quiz_header.hasSrsContainerTarget && settings.lightning_enabled && new_answer_check.passed &&
                        subject_stats.meaning.complete && subject_stats.reading.complete && srs_mgr) {
                        setTimeout(() => {
                            srs_mgr.updateSRS({subject:subject,stats:subject_stats});
                            setTimeout(()=>{
                                quiz_header.srsContainerTarget.dataset.hidden = true;
                            }, 1000 * settings.srs_msg_period);
                        }, 1);
                    }

                    state = 'first_submit';
                }
                return false;
            }
            default:
                return false;
        }

        return false;
    }

    //------------------------------------------------------------------------
    // Simulate input character by character and convert with WanaKana to kana
    //  -- Contributed by user @Sinyaven
    //------------------------------------------------------------------------
    function to_kana(text) {
        return Array.from(text).reduce((total, c) => wanakana.toKana(total + c, {IMEMode: true}), "").replace(/n$/, String.fromCharCode(12435));
    }

    //------------------------------------------------------------------------
    // Resize the buttons according to how many are visible.
    //------------------------------------------------------------------------
    function resize_buttons() {
        let buttons = Array.from(document.querySelectorAll('#additional-content .additional-content__menu-item'));
        let visible_buttons = buttons.filter((elem)=>!elem.matches('.hidden,[hidden]'));
        let btn_count = visible_buttons.length;
        for (let btn of visible_buttons) {
            let percent = Math.floor(10000/btn_count)/100 + '%';
            btn.style.width = `calc(${percent} - 10px)`;
            btn.style.flex = `0 0 calc(${percent} - 10px)`;
            btn.style.marginRight = '10px';
        }
        visible_buttons.slice(-1)[0].style.marginRight = '0px';
    }

    //------------------------------------------------------------------------
    // External hook for @polv's script, "WaniKani Disable Default Answers"
    //------------------------------------------------------------------------
    gobj.set_state = function(_state) {
        state = _state;
    };

    function get_controller(name) {
        return Stimulus.getControllerForElementAndIdentifier(document.querySelector(`[data-controller~="${name}"]`),name);
    }

    //------------------------------------------------------------------------
    // startup() - Install our intercept handlers, and add our Double-Check button and hotkey
    //------------------------------------------------------------------------
    async function startup() {
        // Intercept the submit button handler.
        let p = promise();
        quiz_input = undefined;
        quiz_queue = undefined;
        additional_content = undefined;
        item_info = undefined;
        quiz_audio = undefined;
        quiz_stats = undefined;
        quiz_progress = undefined;
        quiz_header = undefined;
        answer_checker = undefined;

        async function get_controllers() {
            try {
                // Check if all of our hooks into WK are valid, just in case something changed.
                if (!quiz_input) {
                    quiz_input = get_controller('quiz-input');
                    if (!quiz_input) throw 'Controller "quiz-input" not found.';
                }
                if (!quiz_queue) {
                    quiz_queue = get_controller('quiz-queue');
                    if (!quiz_queue) throw 'Controller "quiz-queue" not found.';
                }
                if (!additional_content) {
                    additional_content = get_controller('additional-content');
                    if (!additional_content) throw 'Controller "additional-content" not found.';
                }
                if (!item_info) {
                    item_info = get_controller('item-info');
                    if (!item_info) throw 'Controller "item-info" not found.';
                }
                if (!quiz_audio) {
                    quiz_audio = get_controller('quiz-audio');
                    if (!quiz_audio) throw 'Controller "quiz-audio" not found.';
                }
                if (!quiz_stats) {
                    quiz_stats = get_controller('quiz-statistics');
                    if (!quiz_stats) throw 'Controller "quiz-statistics" not found.';
                }
                if (!quiz_progress) {
                    quiz_progress = get_controller('quiz-progress');
                    if (!quiz_progress) throw 'Controller "quiz-progress" not found.';
                }
                if (!quiz_header) {
                    quiz_header = get_controller('quiz-header');
                    if (!quiz_header) throw 'Controller "quiz-header" not found.';
                }
                if (!response_helpers) {
                    response_helpers = await importShim('lib/answer_checker/utils/response_helpers');
                    if (!response_helpers) throw 'Import "lib/answer_checker/utils/response_helpers" failed.';
                }
                if (!wanakana) {
                    wanakana = await importShim('wanakana');
                    if (!wanakana) throw 'Import "wanakana" failed.';
                }
                if (!answer_checker) answer_checker = Stimulus.controllers.find((c)=>c.answerChecker)?.answerChecker;
                if (!answer_checker) {
                    let AnswerChecker = (await importShim('lib/answer_checker/answer_checker')).default;
                    if (!AnswerChecker) throw 'Import "lib/answer_checker/answer_checker" failed.';
                    answer_checker = new AnswerChecker;
                }
                if (quiz_queue.hasSubjectIdsWithSRSTarget) {
                    srs_mgr = quiz_queue.quizQueue.srsManager;
                } else {
                    srs_mgr = undefined;
                }

                if (quiz_input.submitAnswer !== new_submit_handler) {
                    old_submit_handler = quiz_input.submitAnswer;
                    quiz_input.submitAnswer = new_submit_handler;
                }

                p.resolve();
            } catch(err) {
                console.log('Double-Check:', err, ' Retrying...');
                setTimeout(get_controllers, 250);
            }
            return p;
        }

        await get_controllers();

        subject_stats_cache = new Map();
        session_stats = {};
        state = 'first_submit';
        ignore_submit = false;

        // Install the Lightning Mode button.
        let scripts_menu = document.getElementById('scripts-menu');

        // Insert CSS
        document.head.insertAdjacentHTML('beforeend',
            `<style name="doublecheck">
            #lightning-mode.doublecheck-active svg {fill:#ff0; opacity:1.0;}
            .wk-icon--thumbs-up.dblchk--invert {transform:scaleY(-1);}
            </style>`
        );

        // Insert lightning button
        scripts_menu.insertAdjacentHTML('afterend',
            `<div id="lightning-mode" class="character-header__menu-navigation-link" hidden>
                <a class="lightning-mode summary-button" href="#" title="Lightning Mode - When enabled, auto-\nadvance after answering correctly.">
                    <svg class="wk-icon wk-icon--lightning" title="Mark Right" viewBox="0 0 500 500" aria-hidden="true">
                        <use href="#wk-icon__lightning"></use>
                    </svg>
                </a>
            </div>`
        );
        document.querySelector('.lightning-mode').addEventListener('click', lightning_clicked);

        // Install the Double-Check features.
        document.querySelector('#additional-content ul').style.textAlign = 'center';
        document.querySelector('#additional-content ul').insertAdjacentHTML('beforeend',
            `<li id="option-toggle-rightwrong" class="additional-content__menu-item additional-content__menu-item--5">
                <a title="Mark Right" class="additional-content__item ${additional_content.toggleDisabledClass}">
                    <div class="additional-content__item-text">Mark Right</div>
                    <div class="additional-content__item-icon-container">
                        <svg class="wk-icon wk-icon--thumbs-up" title="Mark Right" viewBox="0 0 512 512" aria-hidden="true">
                            <use href="#wk-icon__thumbs-up"></use>
                        </svg>
                    </div>
                </a>
            </li>
            <li id="option-retype" class="additional-content__menu-item additional-content__menu-item--5">
                <a title="Retype" class="additional-content__item ${additional_content.toggleDisabledClass}">
                    <div class="additional-content__item-text">Re-type</div>
                    <div class="additional-content__item-icon-container">
                        <svg class="wk-icon wk-icon--reload" title="Re-type Answer" viewBox="0 0 512 512" aria-hidden="true">
                            <use href="#wk-icon__reload"></use>
                        </svg>
                    </div>
                </a>
            </li>`
        );
        document.querySelector('#option-toggle-rightwrong').addEventListener('click', toggle_result.bind(null,'toggle'));
        document.querySelector('#option-retype').addEventListener('click', toggle_result.bind(null,'retype'));
        let input = quiz_input.inputTarget;
        document.body.addEventListener('keypress', handle_rightwrong_hotkey);
        function handle_rightwrong_hotkey(event){
            if (state !== 'first_submit') {
                if (!document.querySelector('#wkofs_doublecheck') && (event.target === input || event.target === document.body)) {
                    if (event.which === 43) {
                        toggle_result('correct');
                        event.preventDefault();
                        event.stopPropagation();
                    }
                    if (event.which === 45) {
                        toggle_result('incorrect');
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
            }
        };
        document.body.addEventListener('keydown', handle_retype_hotkey);
        function handle_retype_hotkey(event){
            if (state !== 'first_submit') {
                if (!document.querySelector('#wkofs_doublecheck') && (event.target === input || event.target === document.body)) {
                    if ((event.which === 27 || event.which === 8)) {
                        toggle_result('retype');
                        event.preventDefault();
                        event.stopPropagation();
                    } else if (event.ctrlKey && event.key === 'l') {
                        event.preventDefault();
                        event.stopPropagation();
                        lightning_clicked();
                    }
                }
            }
        };

        document.head.insertAdjacentHTML('beforeend',
            `<style>
            #additional-content>ul>li.hidden {display:none;}
            #answer-form fieldset.confburn button, #answer-form fieldset.confburn input[type=text], #answer-form fieldset.confburn input[type=text]:disabled {
              background-color: #000 !important;
              color: #fff;
              text-shadow: 2px 2px 0 rgba(0,0,0,0.2);
              transition: background-color 0.1s ease-in;
              opacity: 1 !important;
            }
            </style>`
        );
    }

})(window.doublecheck);