[Userscript] Self-Study Quiz

To anyone interested, I threw together a modified version of the self study script that allows for studying more than level at once. Additionally, you can specify a “time until review” for each SRS level so that you don’t accidentally study too close to a review. Detailed explanation and variable settings toward the top of the file.

I will not actively support this version of the script, but I figured I’d share.

// ==UserScript==
// @name        Wanikani Self-Study Quiz Edition
// @namespace   rfindley
// @description Self-study your items via the Wanikani level pages
// @version     2.0.20
// @include     https://www.wanikani.com/level/*
// @exclude     https://www.wanikani.com/level/*/*
// @include     https://www.wanikani.com/radicals*
// @exclude     https://www.wanikani.com/radicals/*
// @include     https://www.wanikani.com/kanji*
// @exclude     https://www.wanikani.com/kanji/*
// @include     https://www.wanikani.com/vocabulary*
// @exclude     https://www.wanikani.com/vocabulary/*
// @require     https://greasyfork.org/scripts/19781-wanakana/code/WanaKana.js?version=126349
// @copyright   2016+, Robin Findley
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// ==/UserScript==

window.wkselfstudy = {};

(function(gobj) {
	// HOW IT WORKS //

	// No longer filters quiz using "Remove Unlocked", "Remove Burned", or "Remove Unburned". These only impact the UI.
	// Still filters by "Remove Locked". Keep this unchchecked to do pre-studying.
	// Also filters by "time left until review", as specified by the variable 'daysUntilReviewAllowedToStudy' below.

	// Will do a quiz for more than one level based on the level selected and the value of the variable 'levelsToQuizAtOnce'.
	// For example, if 'levelsToQuizAtOnce' is set to 5, clicking the "Quiz" button for any level from 11 to 15 will quiz all levels from 11 to 15 at once.

	// END HOW IT WORKS //

	// SETTINGS //

	// How many levels to self study at once.
	var levelsToQuizAtOnce = 5;

	// Specify number of days left until a review to allow for self study.
	// Setting the number to the full interval means that SRS level will never show up in self study.
	var daysUntilReviewAllowedToStudy = {
		1: 0, // Full interval is 4 hours
		2: 0, // Full interval is 8 hours
		3: 0.75, // Full interval is 1 day
		4: 1.5, // Full interval is 2 days
		5: 5, // Full interval is 7 days
		6: 10, // Full interval is 14 days
		7: 25, // Full interval is 30 days
		8: 120 // Full interval is 120 days
	};

	// END SETTINGS //

    var settings = {
        compatible: 2,
        // ss_hidelocked   - Hide locked items
        // ss_hideunlocked - Hide unlocked items (includes ss_hideburn)
        // ss_hideburned   - Hide burned items
        // ss_hideunburned - Hide unburned items (includes ss_hidelock)
        // ss_hidechar     - Hide the radical/kanji/vocab characters
        // ss_hideread     - Hide the reading
        // ss_hidemean     - Hide the meaning
        // ss_quizctom     - Character -> Meaning
        // ss_quizctor     - Character -> Reading
        // ss_quizrtom     - Reading -> Meaning
        // ss_quizmtor     - Meaning -> Reading
        // ss_quizator     - Audio -> Reading
        // ss_quizatom     - Audio -> Meaning
        configs: [
            ['Japanese to English',      'ss_hidelocked ss_hideread ss_hidemean ss_quizctom ss_quizctor'],
            ['English to Japanese',      'ss_hidelocked ss_hideread ss_hidechar ss_quizmtor'],
            ['[BURNED] Japanese to English', 'ss_hideunburned ss_hideread ss_hidemean ss_quizctom ss_quizctor'],
            ['[BURNED] English to Japanese', 'ss_hideunburned ss_hideread ss_hidechar ss_quizmtor'],
            ['Listening Quiz',           'ss_hidelocked ss_hideread ss_hidechar ss_hidemean ss_quizator ss_quizator'],
        ],
        selected_config: 0,
        enabled: true,
        randomize_on_load: true,
        lightning_mode: false, // Skip 'correct', jump to next item.
        audio_mode: false, // Auto-play audio files (i.e. readings).
        quiz_pairing: 1, // 0=none, 1=Reading first, 2=Meaning first
        quiz_repeat: true, // Repeat after finishing quiz.
        quiz_shuffle: true, // Shuffle before repeating quiz.
        quiz_typo: true // Allow typos in English answers
    };
    gobj.settings = settings;

    var html =
        '<div class="selfstudy">'+
        '  <label>Self-study:</label>'+
        '  <div class="btn-group">'+
        '    <button class="btn enable" title="Enable/Disable self-study plugin">OFF</button>'+
        '    <button class="btn quiz" title="Open the quiz window">Quiz</button>'+
        '    <button class="btn shuffle" title="Shuffle the list of items below">Shuffle</button>'+
        '    <select class="btn config" title="Select a self-study preset"></select>'+
        '    <button class="btn config" title="Configure self-study presets"><i class="icon-gear"></i></button>'+
        '  </div>'+
        '</div>';

    var config_html =
        '<div id="ss_config" class="hidden">'+
        '  <div class="section"><label>Presets</label>'+
        '    <div class="btns">'+
        '      <button class="btn new">New</button>'+
        '      <button class="btn up">Up</button>'+
        '      <button class="btn dn">Down</button>'+
        '      <button class="btn del">Delete</button>'+
        '    </div>'+
        '    <div class="list">'+
        '      <select class="configs" size="7"></select>'+
        '    </div>'+
        '    <div class="hide_cfg">'+
        '      <div class="txtline">'+
        '        <label>Edit name:</label>'+
        '        <div class="expand"><input type="text" class="preset"></div>'+
        '      </div>'+
        '    </div>'+
        '  </div>'+
        '  <div class="section"><label>Items</label>'+
        '    <div class="cbbox">'+
        '      <div><label>Remove Locked:</label><input type="checkbox" name="ss_hidelocked"></div>'+
        '      <div><label>Remove Unlocked:</label><input type="checkbox" name="ss_hideunlocked"></div>'+
        '    </div>'+
        '    <div class="cbbox">'+
        '      <div><label>Remove Burned:</label><input type="checkbox" name="ss_hideburned"></div>'+
        '      <div><label>Remove Unburned:</label><input type="checkbox" name="ss_hideunburned"></div>'+
        '    </div>'+
        '  </div>'+
        '  <div class="section"><label>Information</label>'+
        '    <div class="cbbox">'+
        '      <div><label>Hide Rad/Kan/Voc:</label><input type="checkbox" name="ss_hidechar"></div>'+
        '    </div>'+
        '    <div class="cbbox">'+
        '      <div><label>Hide Reading:</label><input type="checkbox" name="ss_hideread"></div>'+
        '      <div><label>Hide Meaning:</label><input type="checkbox" name="ss_hidemean"></div>'+
        '    </div>'+
        '  </div>'+
        '  <div class="section"><label>Quiz</label>'+
        '    <div class="cbbox">'+
        '      <div><label>Rad/Kan/Voc <i class="icon-circle-arrow-right"></i> Meaning:</label><input type="checkbox" name="ss_quizctom"></div>'+
        '      <div><label>Kan/Voc <i class="icon-circle-arrow-right"></i> Reading:</label><input type="checkbox" name="ss_quizctor"></div>'+
        '      <div><label>Reading <i class="icon-circle-arrow-right"></i> Meaning:</label><input type="checkbox" name="ss_quizrtom"></div>'+
        '      <div><label>Meaning <i class="icon-circle-arrow-right"></i> Reading:</label><input type="checkbox" name="ss_quizmtor"></div>'+
        '    </div>'+
        '    <div class="cbbox">'+
        '      <div><label>Voc Audio <i class="icon-circle-arrow-right"></i> Reading:</label><input type="checkbox" name="ss_quizator"></div>'+
        '      <div><label>Voc Audio <i class="icon-circle-arrow-right"></i> Meaning:</label><input type="checkbox" name="ss_quizatom"></div>'+
        '    </div>'+
        '  </div>'+
        '  <div class="dlg_close">'+
        '    <div class="btn-group">'+
        '      <button class="btn save">Save</button>'+
        '      <button class="btn cancel">Cancel</button>'+
        '    </div>'+
        '  </div>'+
        '</div>';

    var quiz_html =
        '<div id="ss_quiz" class="hidden kanji meaning">'+
        '  <div class="topbar">'+
        '    <div class="settings noselect">'+
        '      <span class="icon-bolt ss_lightning" title="Lightning Mode: Skip <enter> on correct answers (Ctrl-L)"></span>'+
        '      <span class="icon-retweet ss_repeat" title="Repeat after finishing quiz (Ctrl-R)"></span>'+
        '      <span class="icon-random ss_shuffle" title="Shuffle before repeating quiz (Ctrl-S)"></span>'+
        '      <span class="icon-audio ss_audio" title="Auto-play audio (Ctrl-Shift-A; Ctrl-A to play)"></span>'+
        '      <span class="icon-warning-sign ss_typo" title="Allow typos (oops) in English answers (Ctrl-O)" style="padding-left: 0px;"></span>'+
        '      <span class="icon-question-sign ss_help" title="Help: Peek at item info (F1, Ctrl-H, or ?)"></span>'+
        '      <span class="ss_done" title="End the quiz and show summary (Esc or Ctrl-E)"><strong>%</strong></span><br />'+
        '      <span class="ss_pair" data-value="0" title="Pairing mode: Group reading and meaning together (Ctrl-P)">Pairing: <span class="data">Disabled</span></span>'+
        '    </div>'+
        '    <div class="stats"></div>'+
        '    <div class="stats_labels">Round:<br>Remaining:<br>Correct:<br>Incorrect:</div>'+
        '  </div>'+
        '  <div class="qwrap">'+
        '    <div class="prev" title="Previous question (Ctrl-Left)"><i class="icon-chevron-left"></i></div>'+
        '    <div class="next" title="Next question (Ctrl-Right)"><i class="icon-chevron-right"></i></div>'+
        '    <div class="question"></div>'+
        '    <div class="help"></div>'+
        '    <div class="summary center">'+
        '      <h3>Summary - <span class="percent">100%</span> Correct <button class="btn requiz" title="Re-quiz wrong items">Re-quiz</button></h3>'+
        '      <ul class="errors"></ul>'+
        '    </div>'+
        '    <div class="round center"><span class="center">Round 1</span></div>'+
        '  </div>'+
        '  <div class="qtype"></div>'+
        '  <div class="answer"><input type="text" value=""></div>'+
        '</div>';

    var css =
        '.noselect {-webkit-touch-callout:none; -webkit-user-select:none; -khtml-user-select:none; -moz-user-select: none;'+
        '-ms-user-select:none; user-select: none;}'+

        '.selfstudy {margin-left:20px; margin-bottom:10px; position:relative;}'+
        '.selfstudy label {display:inline; vertical-align:middle; padding-right:4px; color:#999; font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif; text-shadow:0 1px 0 #fff;}'+
        '.selfstudy button.enable {width:55px;}'+
        '.ss_active .selfstudy button.enable.on {background-color:#b3e6b3; background-image:linear-gradient(to bottom, #ecf9ec, #b3e6b3);}'+
        '.selfstudy select.config {width:300px;}'+

        '.selfstudy .center {display:block; position:relative; top:50%; left:50%; transform:translate(-50%,-50%);}'+

        'section[id^="level-"].ss_active.ss_hidechar .character-item a span:not(.dummy) {opacity:0; transition:opacity ease-in-out 0.15s}'+
        'section[id^="level-"].ss_active.ss_hideread .character-item a li[lang="ja"] {opacity:0; transition:opacity ease-in-out 0.15s}'+
        'section[id^="level-"].ss_active.ss_hidemean .character-item a li:not([lang="ja"]) {opacity:0; transition:opacity ease-in-out 0.15s}'+
        'section[id^="level-"].ss_active.ss_hideburned .character-item.burned {display:none;}'+
        'section[id^="level-"].ss_active.ss_hidelocked .character-item.locked {display:none;}'+
        'section[id^="level-"].ss_active.ss_hideunburned .character-item:not(.burned) {display:none;}'+
        'section[id^="level-"].ss_active.ss_hideunlocked .character-item:not(.locked) {display:none;}'+

        'section.ss_active .character-item:hover a span {opacity: initial !important; transition:opacity ease-in-out 0.05s !important;}'+
        'section.ss_active .character-item:hover a li {opacity: initial !important; transition:opacity ease-in-out 0.05s !important;}'+

        '#ss_config {position:absolute; z-index:1029; width:573px; background-color:rgba(0,0,0,0.9); border-radius:8px; padding:8px;}'+

        '#ss_config select.configs {width:475px;}'+
        '#ss_config label {color:#ccc; text-shadow:initial; text-align:right; vertical-align:baseline;}'+
        '#ss_config .btns {display:inline-block; float:left; vertical-align:top; margin-right:8px;}'+
        '#ss_config .btns .btn {display:block; margin-bottom:5px;}'+
        '#ss_config .btn {width:70px;}'+

        '#ss_config .list {overflow-x:auto;}'+
        '#ss_config .list select.configs {width:100%; height:135px;}'+

        '#ss_config .section {border-top:1px solid #ccc; padding:0 0 8px 0;}'+
        '#ss_config .section > label {display:block; text-align:left; color:#ffc; font-size:1.2em; font-weight:bold; padding-left:4px; margin-bottom:4px; background-color:#2e2e2e; background-image:linear-gradient(to bottom, #3c3c3c, #1a1a1a); background-repeat:repeat-x;}'+

        '#ss_config .txtline label {display:inline-block; float:left; margin-right:8px; width:100px; line-height:30px; clear:both;}'+
        '#ss_config .txtline .expand {overflow-x:auto;}'+
        '#ss_config .txtline input {box-sizing:border-box; width:100%; height:30px;}'+

        '#ss_config .cbbox {display:inline-block; width:49%; vertical-align:top;}'+
        '#ss_config .cbbox label {display:inline-block; float:left; margin:0 8px 0 0; width:190px; line-height:20px;}'+
        '#ss_config .cbbox input {position:relative; overflow-x:auto; height:20px; margin:0; top:1px;}'+

        '#ss_config [class*="icon-"] {color:#fff;}'+

        '#ss_config .dlg_close {text-align:center; margin-top:16px; margin-bottom:8px;}'+

        '#ss_quiz [lang="ja"] {font-family: "Meiryo","Yu Gothic","Hiragino Kaku Gothic Pro","TakaoPGothic","Yu Gothic","ヒラギノ角ゴ Pro W3","メイリオ","Osaka","MS PGothic","MS Pゴシック",sans-serif;}'+
        '#ss_quiz {position:absolute; z-index:1028; width:573px; background-color:rgba(0,0,0,0.85); border-radius:8px; border:8px solid rgba(0,0,0,0.85); font-size:2em;}'+
        '#ss_quiz * {text-align:center;}'+
        '#ss_quiz .qwrap {height:8em; position:relative; clear:both;}'+

        '#ss_quiz.radicals .qwrap, #ss_quiz.radicals .summary .que {background-color:#0af;}'+
        '#ss_quiz.kanji .qwrap, #ss_quiz.kanji .summary .que {background-color:#f0a;}'+
        '#ss_quiz.vocabulary .qwrap, #ss_quiz.vocabulary .summary .que {background-color:#a0f;}'+

        '#ss_quiz .prev, #ss_quiz .next {display:inline-block; width:80px; color:#fff; line-height:8em; cursor:pointer;}'+
        '#ss_quiz .prev:hover {background-image:linear-gradient(to left, rgba(0,0,0,0), rgba(0,0,0,0.2));}'+
        '#ss_quiz .next:hover {background-image:linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.2));}'+
        '#ss_quiz .prev {float:left;}'+
        '#ss_quiz .next {float:right;}'+

        '#ss_quiz .topbar {font-size:0.5em; line-height:1em; color: rgba(255,255,255,0.5);}'+

        '#ss_quiz .settings {float:left; padding:6px 8px; text-align:left; line-height:1.5em;}'+
        '#ss_quiz .settings span[class*="icon-"] {font-size:1.3em; padding:0 2px;}'+
        '#ss_quiz .settings .ss_audio {padding-left:0; padding-right:4px;}'+
        '#ss_quiz .settings .ss_typo {padding-left:0px;}'+
        '#ss_quiz .settings .ss_done {font-size:1.25em;}'+
        '#ss_quiz .settings .ss_pair {font-weight:bold;}'+
        '#ss_quiz .settings span {cursor:pointer;}'+
        '#ss_quiz .settings span:hover {color:rgba(255,255,204,0.8);}'+
        '#ss_quiz .settings span.active {color:#ffc;}'+
        '#ss_quiz.help .settings .ss_help {color:#ffc;}'+

        '#ss_quiz .stats_labels {text-align:right; font-family:monospace;}'+
        '#ss_quiz .stats {float:right; text-align:right; color:rgba(255,255,255,0.8); font-family:monospace; padding:0 5px;}'+

        '#ss_quiz .round {display:none; font-weight:bold; position:absolute; box-sizing:border-box; width:60%; height:75%; border-radius:24px; border:2px solid #000; background-color:#fff;}'+
        '#ss_quiz.round .round {display:block;}'+

        '#ss_quiz .question {'+
        '  overflow-x:auto; overflow-y:hidden; position:relative; top:50%; transform:translateY(-50%);'+
        '  color:#fff; text-align:center; line-height:1.1em; font-size:1em; font-weight:bold; cursor:default;'+
        '}'+
        '#ss_quiz .question[data-type="char"] {font-size:2em;}'+
        '#ss_quiz .icon-audio:before {content:"\\f028";}'+
        '#ss_quiz .question .icon-audio {font-size:2.5em; cursor:pointer;}'+
        '#ss_quiz.summary .question {display:none;}'+

        '#ss_quiz .qtype {line-height:2em; cursor:default; text-transform:capitalize;}'+
        '#ss_quiz .qtype.reading {color:#fff; text-shadow:-1px -1px 0 #000; border-top:1px solid #555; border-bottom:1px solid #000; background-color:#2e2e2e; background-image:linear-gradient(to bottom, #3c3c3c, #1a1a1a); background-repeat:repeat-x;}'+
        '#ss_quiz .qtype.meaning {color:#555; text-shadow:-1px -1px 0 rgba(255,255,255,0.1); border-top:1px solid #d5d5d5; border-bottom:1px solid #c8c8c8; background-color:#e9e9e9; background-image:linear-gradient(to bottom, #eee, #e1e1e1); background-repeat:repeat-x;}'+

        '#ss_quiz .help {display:none;'+
        '  position:absolute; top:3%; left:13%; width:74%; box-sizing:border-box; border:2px solid #000; border-radius:15px; padding:4px;'+
        '  color:#555; text-shadow:2px 2px 0 rgba(0,0,0,0.2); background-color:rgba(255,255,255,0.9); font-size:0.8em; line-height:1.2em;'+
        '}'+
        '#ss_quiz.help .help {display:inherit;}'+

        '#ss_quiz .answer {background-color:#ddd; padding:8px;}'+
        '#ss_quiz .answer input {'+
        '  width:100%; background-color:#fff; height:2em; margin:0; border:2px solid #000; padding:0;'+
        '  box-sizing:border-box; border-radius:0; font-size:1em;'+
        '}'+
        '#ss_quiz .answer input.correct {color:#fff; background-color:#8c8; text-shadow:2px 2px 0 rgba(0,0,0,0.2);}'+
        '#ss_quiz .answer input.incorrect {color:#fff; background-color:#f03; text-shadow:2px 2px 0 rgba(0,0,0,0.2);}'+

        '#ss_quiz.loading .qwrap, #ss_quiz.loading .answer {display:none;}'+

        '#ss_quiz .summary {display:none; position:absolute; width:74%; height:100%; background-color:rgba(0,0,0,0.7); color:#fff; font-weight:bold;}'+
        '#ss_quiz.summary .summary {display:block;}'+
        '#ss_quiz .summary h3 {'+
        '  background-image:linear-gradient(to bottom, #3c3c3c, #1a1a1a); background-repeat:repeat-x;'+
        '  border-top:1px solid #777; border-bottom:1px solid #000; margin:0; box-sizing:border-box;'+
        '  text-shadow:2px 2px 0 rgba(0,0,0,0.5); color:#fff; font-size:0.8em; font-weight:bold; line-height:40px;'+
        '}'+
        '#ss_quiz .summary .errors {position:absolute; top:40px; bottom:0px; width:100%; margin:0; overflow-y:auto; list-style-type:none;}'+
        '#ss_quiz .summary li {margin:4px 0 0 0; font-size:0.6em; font-weight:bold; line-height:1.4em;}'+

        '#ss_quiz .summary .errors span {display:inline-block; padding:2px 4px 0px 4px; border-radius:4px; line-height:1.1em; max-width:50%; vertical-align:middle; cursor:pointer;}'+
        '#ss_quiz .summary .ans {background-color:#fff; color:#000;}'+
        '#ss_quiz .summary .wrong {color:#f22;}'+

        '#ss_quiz .btn.requiz {position:absolute; top:6px; right:6px; padding-left:6px; padding-right:6px;}'+

        '';

    var cfg_tmp;

    // Jaro-Winkler Distance
    function jw_distance(a, c) {
        var h, b, d, k, e, g, f, l, n, m, p;
        if (a.length > c.length) {
            c = [c, a];
            a = c[0];
            c = c[1];
        }
        k = ~~Math.max(0, c.length / 2 - 1);
        e = [];
        g = [];
        b = n = 0;
        for (p = a.length; n < p; b = ++n) {
            for (h = a[b], l = Math.max(0, b - k), f = Math.min(b + k + 1, c.length), d = m = l; l <= f ? m < f : m > f; d = l <= f ? ++m : --m) {
                if (g[d] === undefined && h === c[d]) {
                    e[b] = h;
                    g[d] = c[d];
                    break;
                }
            }
        }
        e = e.join("");
        g = g.join("");
        d = e.length;
        if (d) {
            b = f = k = 0;
            for (l = e.length; f < l; b = ++f) {
                h = e[b];
                if (h !== g[b]) k++;
            }
            b = g = e = 0;
            for (f = a.length; g < f; b = ++g) {
                if (h = a[b], h === c[b])
                    e++;
                else
                    break;
            }
            a = (d/a.length + d/c.length + (d - ~~(k/2))/d)/3;
            a += 0.1 * Math.min(e, 4) * (1 - a);
        } else {
            a = 0;
        }
        return a;
    }

    //-------------------------------------------------------------------
    // Open the configuration dialog.
    //-------------------------------------------------------------------
    function configure(e) {

        var sel, ssgrp, dialog;

        function setup() {
            dialog = $(config_html).appendTo(ssgrp);
            sel = $('#ss_config select.configs');

            // "New" handler
            dialog.find('button.new').on('click', function() {
                cfg_tmp.push(['<new>','']);
                sel.append('<option value="'+(cfg_tmp.length-1)+'">&lt;new&gt;</option>');
                select_config(sel.children().length-1);
                $('#ss_config .preset').focus().select();
            });

            // "Delete" handler
            dialog.find('button.del').on('click', function() {
                var opt = sel.find(':selected');
                var idx = opt.index();
                opt.remove();
                var len = sel.children().length;
                if (idx >= len) idx = len-1;
                select_config(idx);
            });

            // "Up" handler
            dialog.find('button.up').on('click', function() {
                var opt = sel.find(':selected');
                if (opt.index() > 0) opt.insertBefore(opt.prev());
            });

            // "Down" handler
            dialog.find('button.dn').on('click', function() {
                var opt = sel.find(':selected');
                if (opt.index() < sel.children().length-1) opt.insertAfter(opt.next());
            });

            // "Configs" selection changed
            sel.on('change', function() {
                select_config(sel.find(':selected').index());
            });

            // "Preset" name changed
            dialog.find('.preset').on('change', function(e) {
                var opt = sel.find(':selected');
                var text = e.currentTarget.value;
                opt.text(text);
                var idx = opt.val();
                cfg_tmp[idx][0] = text;
            });

            // "Checkbox" changed
            dialog.find('input[type="checkbox"]').on('change', function() {
                var opt = sel.find(':selected');
                var idx = opt.val();
                var props = [];
                dialog.find('input[type="checkbox"]:checked').each(function(i,e){props.push(e.name);});
                cfg_tmp[idx][1] = props.join(' ');
            });

            // "Save" handler
            dialog.find('button.save').on('click', save_config);

            // "Cancel" handler
            dialog.find('button.cancel').on('click', cancel_config);
        }

        function save_config() {
            settings.configs = [];
            sel.children().each(function(i,v){
                var idx = $(v).val();
                settings.configs.push(cfg_tmp[idx].slice(0));
            });
            settings.selected_config = sel.find(':selected').index();
            save_settings();
            dialog.addClass('hidden');
            populate_presets();
            set_config(settings.selected_config);
        }

        function cancel_config() {
            cfg_tmp = undefined;
            dialog.addClass('hidden');
        }

        function select_config(idx) {
            var opt = sel.children().eq(idx);
            opt.prop('selected',true);
            $('#ss_config input.preset').val(opt.text());

            var props = cfg_tmp[opt.val()][1];
            $('#ss_config .cbbox input').prop('checked', false);
            props.split(' ').forEach(function(prop,i){
                $('#ss_config .cbbox input[name="'+prop+'"]').prop('checked', true);
            });
        }

        ssgrp = $(e.currentTarget).closest('.selfstudy');
        dialog = $('#ss_config');
        if (dialog.length === 0) {
            setup();
        } else if (dialog.is(':visible')) {
            return cancel_config();
        } else {
            ssgrp.append(dialog);
            sel = $('#ss_config select.configs');
        }

        // Clone the existing settings.
        var options = [];
        cfg_tmp = settings.configs.map(function(e,i){
            options.push('<option value="'+i+'">'+e[0]+'</option>');
            return e.slice(0);
        });

        // Populate configs.
        sel.html(options.join(''));
        select_config(settings.selected_config);

        // Unhide the config dialog.
        var top = ssgrp.find('.btn-group').height() + 4;
        dialog.css('top',top).removeClass('hidden');
    }

    //-------------------------------------------------------------------
    // Save settings.
    //-------------------------------------------------------------------
    function save_settings() {
        localStorage.setItem('selfstudy_settings', JSON.stringify(settings));
    }

    //-------------------------------------------------------------------
    // Button event handler.
    //-------------------------------------------------------------------
    function toggle_enable() {
        settings.enabled = !settings.enabled;
        save_settings();
        set_enable();
    }

    //-------------------------------------------------------------------
    // Button event handler.
    //-------------------------------------------------------------------
    function config_change_event(e) {
        set_config(Number(e.currentTarget.value));
    }

    //-------------------------------------------------------------------
    // Add a shuffle function to Array and jQuery.
    //-------------------------------------------------------------------
    function fisher_yates_shuffle() {
        var i = this.length, j, temp;
        if (i===0) return this;
        while (--i) {
            j = Math.floor(Math.random()*(i+1));
            temp = this[i]; this[i] = this[j]; this[j] = temp;
        }
        return this;
    }
    if (typeof Array.prototype.shuffle !== 'function') Array.prototype.shuffle = fisher_yates_shuffle;
    $.fn.shuffle = fisher_yates_shuffle;

    //-------------------------------------------------------------------
    // Shuffle items.
    //-------------------------------------------------------------------
    function shuffle(e) {
        if (e === undefined) {
            // Shuffle all
            $('section[id^="level-"]').each(function(){
                var sec = $(this);
                sec.find('[class$="-character-grid"]').append(sec.find('.character-item').detach().shuffle());
            });
        } else {
            // Shuffle specific group
            var btn = $(e.currentTarget);
            var sec = btn.closest('section[id^="level-"]');
            sec.find('[class$="-character-grid"]').append(sec.find('.character-item').detach().shuffle());
            quiz.refresh();
        }
    }

    //-------------------------------------------------------------------
    // Enable or disable the plugin.
    //-------------------------------------------------------------------
    function set_enable() {
        var btns = $('.selfstudy button.enable');
        var secs = $('section[id^="level-"]');

        if (settings.enabled) {
            secs.addClass('ss_active');
            btns.addClass('on').text('ON');
        } else {
            secs.removeClass('ss_active');
            btns.removeClass('on').text('OFF');
        }
    }

    //-------------------------------------------------------------------
    // Select a configuration.
    //-------------------------------------------------------------------
    function set_config(val) {
        var secs = $('section[id^="level-"]');

        // Remove all ss_* classes except ss_alive
        secs.each(function(i,e){
            e.className = e.className.split(' ').filter(function(v){return (v.match(/^ss_(?!active)/) === null);}).join(' ');
        });

        settings.selected_config = val;
        save_settings();
        $('.selfstudy select.config').val(val);

        settings.configs[settings.selected_config][1].split(' ').forEach(function(cfgopt,idx){
            secs.addClass(cfgopt);
        });
        quiz.refresh();
    }

    //-------------------------------------------------------------------
    // Populate the presets into the drop-down box.
    //-------------------------------------------------------------------
    function populate_presets() {
        var options = [];
        settings.configs.forEach(function(config,idx){
            var cfgname = config[0];
            var cfgopts = config[1];
            options.push('<option value="'+idx+'">'+cfgname+'</option>');
        });
        $('.selfstudy select.config').html(options.join(''));
    }

    //-------------------------------------------------------------------
    // Make first letter of each word upper-case.
    //-------------------------------------------------------------------
    function toTitleCase(str) {
        return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
    }

    //-------------------------------------------------------------------
    // Trim surrounding whitespace
    //-------------------------------------------------------------------
    function trim(str) {
        return str.replace(/^\s*|\s*$/g,'');
    }

    var quiz = {};
    gobj.quiz = quiz;

    (function (quiz) {
        var ssgrp, sec, dialog, apikey, itype, ichar, level, items, order, requiz_order, wanakana_isbound = false;
        var round, correct, incorrect, quiz_idx, quiz_max, answered;
        var items_cache = {radicals:[],kanji:[],vocabulary:[]};
        var good_answers, all_answers, alang, qlang, atype, qtype, force_summary;

        // Make the items cache accessible from the console (via wkselfstudy.items_cache)
        gobj.items_cache = items_cache;

        //-------------------------------------------------------------------
        // Quiz on all the items in the selected section.
        //-------------------------------------------------------------------
        quiz.open = function(e) {
            if (e !== undefined && setup(e) === 'closed') return;
            fetch_data();
        };
        quiz.close = function() {
            dialog.addClass('hidden');
            $('body').off('.ss_quiz');
        };
        quiz.is_open = function() {
            var dlg = $('#ss_quiz');
            if (dlg.length===0 || dlg.hasClass('hidden')) return false;
            return true;
        };
        quiz.refresh = function() {
            if (!quiz.is_open()) return;
            quiz.open();
        };

        function reset_answer() {
            $('#ss_quiz .answer input').prop('readonly',false).removeClass('correct incorrect').val('').focus();
        }

        function set_pairing(value) {
            var text = [
                'Disabled',
                'Reading first',
                'Meaning first'
            ][settings.quiz_pairing];
            $('#ss_quiz .ss_pair').attr('data-value',settings.quiz_pairing).children('.data').text(text);
            quiz.refresh();
        }

        function set_help(value) {
            if (value) {
                dialog.addClass('help');
            } else {
                dialog.removeClass('help');
            }
        }

        function toggle_pair() {
            var elem = $('#ss_quiz .settings .ss_pair');
            var pairing = (Number(elem.attr('data-value'))+1)%3;
            settings.quiz_pairing = pairing;
            set_pairing(pairing);
            save_settings();
        }

        function toggle_lightning() {
            var elem = $('#ss_quiz .settings .ss_lightning');
            elem.toggleClass('active');
            settings.lightning_mode = elem.hasClass('active');
            save_settings();
        }

        function toggle_audio() {
            var elem = $('#ss_quiz .settings .ss_audio');
            elem.toggleClass('active');
            settings.audio_mode = elem.hasClass('active');
            if (settings.audio_mode && itype==='vocabulary' && atype==='reading') audio.load();
            save_settings();
        }

        function toggle_repeat() {
            var elem = $('#ss_quiz .settings .ss_repeat');
            elem.toggleClass('active');
            settings.quiz_repeat = elem.hasClass('active');
            save_settings();
        }

        function toggle_shuffle() {
            var elem = $('#ss_quiz .settings .ss_shuffle');
            elem.toggleClass('active');
            settings.quiz_shuffle = elem.hasClass('active');
            save_settings();
        }

        function toggle_typo() {
            var elem = $('#ss_quiz .settings .ss_typo');
            elem.toggleClass('active');
            settings.quiz_typo = elem.hasClass('active');
            save_settings();
        }

        function toggle_help() {
            if (quiz_idx < 0 || quiz_idx > quiz_max) return;
            $('#ss_quiz').toggleClass('help');
            if (settings.audio_mode && $('#ss_quiz').hasClass('help') && itype==='vocabulary' && atype==='reading') {
                audio.play();
            }
        }

        var audio = {
            urls: [],
            name: '',
            level: 0,
            request_load: false,
            loaded: false,

            clear: function() {
                audio.name = '';
                audio.level = 0;
                audio.request_load = false;
                audio.loaded = false;

                dialog.find('audio.old').remove();
                dialog.find('audio').each(function(i,tag){
                    if (tag.paused) {
                        $(tag).remove();
                    } else {
                        $(tag).addClass('old');
                        tag.onended = function(event) {
                            $(event.target).remove();
                        }
                    }
                });
            },

            load_urls: function(level) {
                if (audio.urls[level] !== undefined) return;
                $.getJSON('https://www.idigtech.com/wanikani/json/audio_urls/'+level+'.json', function(json, status, xhr){
                    if (status !== 'success') return;
                    audio.urls[level] = json;
                    if (audio.request_load) {
                        audio.set(audio.name, audio.level, audio.request_load);
                    }
                });
            },

            load: function() {
                if (audio.loaded) return;
                dialog.find('audio:not(.old)').each(function(i,tag){
                    tag.load();
                    audio.request_load = false;
                    audio.loaded=true;
                });
            },

            set: function(name, level, preload) {
                if (name !== audio.name) audio.loaded = false;
                if (audio.loaded) return;
                audio.clear();
                audio.name = name;
                audio.level = level;
                audio.request_load = preload;
                if (audio.urls[level] === undefined) return audio.load_urls(level);
                dialog.append('<audio><source src="'+audio.urls[level][name]+'" type="audio/mpeg"></audio>');
                if (audio.request_load) audio.load();
            },

            play: function() {
                if (!audio.loaded) audio.load();
                dialog.find('audio:not(.old)').each(function(i,tag){
                    if (!tag.paused) {
                        tag.currentTime = 0;
                    } else {
                        tag.play();
                    }
                });
            }
        };
        quiz.audio = audio;

        function goto_summary() {
            quiz_idx = quiz_max;
            force_summary = true;
            next_question();
        }

        function settings_handler(e) {
            var cname = $(e.currentTarget).attr('class').match(/\bss_\S*\b/)[0];

            switch (cname) {
                case 'ss_pair': toggle_pair(); break;
                case 'ss_lightning': toggle_lightning(); break;
                case 'ss_audio': toggle_audio(); break;
                case 'ss_repeat': toggle_repeat(); break;
                case 'ss_shuffle': toggle_shuffle(); break;
                case 'ss_typo': toggle_typo(); break;
                case 'ss_help': toggle_help(); break;
                case 'ss_done': goto_summary(); break;
            }
        }

        function scroll_errors(e) {
            var t = e.currentTarget;

            // If scrollbar is visible...
            if (t.scrollHeight > t.clientHeight) {
                var delta = e.originalEvent.deltaY;

                // ...and we are scrolling beyond the limit...
                if ((delta < 0 && (t.scrollTop <= 0)) ||
                    (delta > 0 && (t.scrollTop+t.clientHeight >= t.scrollHeight))) {

                    // ...prevent scroll from bubbling to window.
                    e.preventDefault();
                    e.stopPropagation();
                }
            }
        }

        function requiz(e) {
            order = requiz_order.shuffle().slice(0);
            requiz_order = [];
            round = 1;
            correct = 0;
            incorrect = 0
            answered = false;
            quiz_idx = 0;
            quiz_max = order.length-1;
            force_summary = false;
            dialog.find('.errors').html('');
            do_quiz();
        }

        function setup(e) {
            ssgrp = $(e.currentTarget).closest('.selfstudy');
            sec = $(e.currentTarget).closest('section[id^="level-"]');
            var id = sec.attr('id').split('-');
            level = id[1];
            itype = window.location.pathname.split('/')[1];
            if (itype === 'level') itype = id[2];

            dialog = $('#ss_quiz');
            if (dialog.length === 0) {
                dialog = $(quiz_html).appendTo(ssgrp);

                // "Prev" button handler
                dialog.find('.prev').on('click', prev_question);

                // "Next" button handler
                dialog.find('.next').on('click', next_question);

                // "Enter" handler for answer
                dialog.find('.answer input').on('keydown keypress', quiz_key);

                // "Enter" handler for answer
                dialog.find('.settings').on('click', '>span', settings_handler);

                // Handle scrollbar inside errors window.
                dialog.find('.errors').on('wheel', scroll_errors);

                // "Re-quiz" handler
                dialog.find('.summary .requiz').on('click', requiz);

                // Audio-click handler
                dialog.find('.question').on('click', '.icon-audio', audio.play);

            } else if (dialog.is(':visible')) {
                if (ssgrp.find('#ss_quiz').length === 0) {
                    ssgrp.append(dialog);
                } else {
                    dialog.addClass('hidden');
                    return 'closed';
                }
            } else {
                ssgrp.append(dialog);
            }
            if (settings.lightning_mode) dialog.find('.ss_lightning').addClass('active');
            if (settings.audio_mode) dialog.find('.ss_audio').addClass('active');
            if (settings.quiz_repeat) dialog.find('.ss_repeat').addClass('active');
            if (settings.quiz_shuffle) dialog.find('.ss_shuffle').addClass('active');
            if (settings.quiz_typo) dialog.find('.ss_typo').addClass('active');
            set_pairing(settings.quiz_pairing);
        }

        function show_error() {
            console.log('wkselfstudy: Failed to get API key!');
        }

        function is_apikey_valid(apikey) {
            return (apikey !== null && apikey.match(/^[0-9a-f]{32}$/) !== null);
        }

        function get_apikey() {
            apikey = localStorage.getItem('apiKey');

            if (is_apikey_valid(apikey)) return true;

            $.get('/settings/account')
            .done(function(page, status, xhr){
                if (status !== 'success') return show_error();
                apikey = $(page).find('#user_api_key').attr('value');
                if (apikey === undefined || apikey.match(/^[0-9a-f]{32}$/) === null) return show_error();
                localStorage.setItem('apiKey', apikey);
                fetch_data();
            })
            .fail(show_error);

            return false;
        }

        function fetch_data() {
            round = 1;
            correct = 0;
            incorrect = 0;
            answered = false;
            quiz_idx = 0;
            force_summary = false;
            dialog.find('.summary .errors').html('');
            if (!get_apikey()) return;

			let levelStart = Math.floor((level - 1) / levelsToQuizAtOnce) * levelsToQuizAtOnce + 1;
			let promises = [];

			for (let i = 0; i < levelsToQuizAtOnce; i++) {
				let loopLevel = levelStart + i;
				if (itype==='vocabulary') {
					audio.load_urls(loopLevel);
				}

            	if (items_cache[itype][loopLevel] === undefined) {
					let promise = $.getJSON('/api/user/' + apikey + '/' + itype + '/' + loopLevel);
					promises[i] = promise;

					promise.done(function(json, status, xhr) {
						if (status !== 'success')
							return show_error();
						items_cache[itype][loopLevel] = json.requested_information
					});
				}
			}


			$.when.apply($, promises).done(function() {
				create_quiz();
			}).fail(show_error);

			/*
			if (itype==='vocabulary') audio.load_urls(level);
            if (items_cache[itype][level] !== undefined) return create_quiz();

            dialog.attr('class','loading');
            dialog.find('.qtype').removeClass('meaning').addClass('reading').html('<strong>Loading...</strong>');

            $.getJSON('/api/user/' + apikey + '/' + itype + '/' + loopLevel)
            .done(function(json, status, xhr){
                if (status !== 'success') return show_error();
                items_cache[itype][level] = json.requested_information;
                create_quiz();
            })
            .fail(show_error);
			*/
		}

        function is_unlocked(item) {
            return (item.user_specific !== null) && (item.user_specific.unlocked_date !== null) && (item.user_specific.unlocked_date > 0);
        }
        function is_locked(item) {
            return !is_unlocked(item);
        }
        function is_burned(item) {
            return is_unlocked(item) && (item.user_specific.burned === true || item.user_specific.burned_date > 0);
        }
        function is_unburned(item) {
            return !is_burned(item);
        }

        function create_quiz() {
            var char_to_mean, char_to_read, read_to_mean, mean_to_read, aud_to_read, aud_to_mean;

            items = [].concat(...items_cache[itype].filter(function(n){ return n != undefined }));

            // Remove any items that aren't included in the current selection.

			var now = Date.now();
            items = items.filter(function(item) {
				if (is_locked(item))
					return true;

				var nextReviewDate = new Date(item.user_specific.available_date * 1000);
				var daysUntilNextReview = (nextReviewDate - now) / 86400000;

				return daysUntilNextReview > daysUntilReviewAllowedToStudy[item.user_specific.srs_numeric];
			});

            if (sec.hasClass('ss_hidelocked')) items = items.filter(is_unlocked);
            //if (sec.hasClass('ss_hideunlocked')) items = items.filter(is_locked);
            //if (sec.hasClass('ss_hideburned')) items = items.filter(is_unburned);
            //if (sec.hasClass('ss_hideunburned')) items = items.filter(is_burned);

            if (itype==='radicals') {
                char_to_mean = true;
                char_to_read = false;
                read_to_mean = false;
                mean_to_read = false;
                aud_to_mean = false;
            } else {
                char_to_mean = sec.hasClass('ss_quizctom');
                char_to_read = (sec.hasClass('ss_quizctor') && (itype!=='radicals'));
                read_to_mean = sec.hasClass('ss_quizrtom');
                mean_to_read = sec.hasClass('ss_quizmtor');
                aud_to_read = (sec.hasClass('ss_quizator') && (itype==='vocabulary'));
                aud_to_mean = (sec.hasClass('ss_quizatom') && (itype==='vocabulary'));
            }

            var idx, idx2, max = items.length;
            order = []; requiz_order=[];
            var tmp_order = [];
            switch (settings.quiz_pairing) {
                case 0: // No pairing
                    for (idx=0; idx<max; idx++) {
                        if (aud_to_read) order.push([idx,3,1]);
                        if (aud_to_mean) order.push([idx,3,2]);
                        if (char_to_mean) order.push([idx,0,2]);
                        if (char_to_read) order.push([idx,0,1]);
                        if (read_to_mean) order.push([idx,1,2]);
                        if (mean_to_read) order.push([idx,2,1]);
                    }
                    order.shuffle();
                    do_quiz(true /* requeue when wrong */);
                    break;

                case 1: // Reading first
                    for (idx=0; idx<max; idx++) tmp_order.push(idx);
                    tmp_order.shuffle();
                    for (idx2=0; idx2<max; idx2++) {
                        idx = tmp_order[idx2];
                        if (aud_to_read) order.push([idx,3,1]);
                        if (aud_to_mean) order.push([idx,3,2]);
                        if (char_to_read) order.push([idx,0,1]);
                        if (mean_to_read) order.push([idx,2,1]);
                        if (char_to_mean) order.push([idx,0,2]);
                        if (read_to_mean) order.push([idx,1,2]);
                    }
                    do_quiz(false /* repeat when wrong */);
                    break;

                case 2: // Meaning first
                    for (idx=0; idx<max; idx++) tmp_order.push(idx);
                    tmp_order.shuffle();
                    for (idx2=0; idx2<max; idx2++) {
                        idx = tmp_order[idx2];
                        if (aud_to_mean) order.push([idx,3,2]);
                        if (char_to_mean) order.push([idx,0,2]);
                        if (read_to_mean) order.push([idx,1,2]);
                        if (aud_to_read) order.push([idx,3,1]);
                        if (char_to_read) order.push([idx,0,1]);
                        if (mean_to_read) order.push([idx,2,1]);
                    }
                    do_quiz(false /* repeat when wrong */);
                    break;
            }
        }

        function do_quiz(requeue_when_wrong) {
            quiz_idx = (round > 1 ? -1 : 0);

            // Unhide the config dialog.
            var top = ssgrp.find('.btn-group').height() + 4;
            dialog.css('top',top).attr('class', itype);

            quiz_max = order.length-1;
            update_stats();
            update_question();
        }

        function item_meaning(item) {
            var arr = [];
            if (item.user_specific && item.user_specific.user_synonyms !== null)
                arr = item.user_specific.user_synonyms.map(function(v){return trim(v.replace('-',' '));});
            arr = arr.concat(item.meaning.split(',').map(function(v){return trim(v.replace('-',' '));}));
            return arr;
        }
        function item_reading(item, good_only) {
            var arr = [];
            if (item.kana) { // vocab
                arr = arr.concat(item.kana.split(',').map(function(v){return v.replace(/^\s*|\..*\s*$/g,'');}));
            } else if (item.important_reading) { // kanji
                if (good_only) {
                    arr = arr.concat(item[item.important_reading].split(',').map(function(v){return v.replace(/^\s*|\..*\s*$/g,'');}));
                } else {
                    if (item.onyomi) arr = arr.concat(item.onyomi.split(',').map(function(v){return v.replace(/^\s*|\..*\s*$/g,'');}));
                    if (item.kunyomi) arr = arr.concat(item.kunyomi.split(',').map(function(v){return v.replace(/^\s*|\..*\s*$/g,'');}));
                    if (item.nanori) arr = arr.concat(item.nanori.split(',').map(function(v){return v.replace(/^\s*|\..*\s*$/g,'');}));
                }
            }
            return arr;
        }

        function update_stats() {
            var remaining = quiz_max;
            if (quiz_idx > 0) remaining -= quiz_idx;
            if (!answered) remaining++;
            $('#ss_quiz .stats').html([round, remaining, correct, incorrect].join('<br>'));
        }

        function update_question() {
            if (quiz_idx === -1) {
                dialog.find('.round span').text('Round '+round);
                dialog.find('.qtype').removeClass('meaning').addClass('reading').html('Press [enter] to continue');
                dialog.addClass('round');
                $('body').on('keydown.ss_quiz keypress.ss_quiz', quiz_key);
                return;
            } else if (quiz_idx > quiz_max) {
                var percent = Math.floor(100*correct/Math.max(1,(correct+incorrect)));
                dialog.find('.summary .percent').text(percent+'%');
                if ((percent === 100) || (correct+incorrect === 0))
                    dialog.find('.summary .requiz').addClass('hidden');
                else
                    dialog.find('.summary .requiz').removeClass('hidden');
                dialog.find('.qtype').removeClass('meaning').addClass('reading').html('Press [enter] to continue');
                dialog.removeClass('round').addClass('summary');
                $('body').on('keydown.ss_quiz keypress.ss_quiz', quiz_key);
                return;
            }
            if (quiz_idx === 0) dialog.removeClass('round');

            var qinfo = order[quiz_idx];
            qtype = ['char','read','mean', 'aud'][qinfo[1]];
            atype = ['','reading','meaning'][qinfo[2]];
            var qtext;
            var answer = $('#ss_quiz .answer input');
            qlang = ''; alang = '';
            reset_answer();
            answered = false;

            all_answers = [];
            answer.val('');
            if (atype === 'reading') {
                if (!wanakana_isbound) {
                    wanakana.bind(answer[0]);
                    wanakana_isbound = true;
                }
            } else {
                if (wanakana_isbound) {
                    wanakana.unbind(answer[0]);
                    wanakana_isbound = false;
                }
            }
            item = items[qinfo[0]];
            if (itype === 'radicals') {
                qlang = 'ja';
                if (item.character !== null)
                    qtext = item.character;
                else
                    qtext = '<i class="radical-'+item.meaning+'"></i>';

                good_answers = item_meaning(item);
                all_answers = good_answers;
            } else {
                var mean_arr = item_meaning(item);
                var imp_read_arr = item_reading(item,true);
                var all_read_arr = item_reading(item);
                switch (qtype) {
                    case 'char':
                        qlang = 'ja';
                        qtext = item.character;
                        break;
                    case 'read':
                        qlang = 'ja';
                        qtext = toTitleCase(all_read_arr.join(', '));
                        break;
                    case 'mean':
                        qtext = toTitleCase(item_meaning(item).join(', '));
                        break;
                    case 'aud':
                        qtext = '<i class="icon-audio"></i>';
                        ichar = item.character;
                        break;
                }
                if (atype === 'reading') {
                    alang = 'ja';
                    good_answers = imp_read_arr;
                    all_answers = all_read_arr.concat(mean_arr);
                } else {
                    good_answers = mean_arr;
                    all_answers = mean_arr.concat(all_read_arr);
                }
            }

            var help_text = toTitleCase(good_answers.join(', '));
            if (qtype !== 'char') help_text += '<br>(<span lang="ja">'+item.character+'</span>)';
            dialog.find('.question').attr('data-type', qtype).attr('lang',qlang).html(qtext);
            dialog.find('.help').html(help_text).attr('lang',alang);
            var type_text = (itype==='radicals' ? 'radical' : itype) + ' <strong>'+atype+'</strong>';
            dialog.find('.qtype').removeClass('reading meaning').addClass(atype).html(type_text);
            if (itype==='vocabulary') {
                var play_audio_now = (qtype==='aud');
                var preload_audio = (play_audio_now || (settings.audio_mode && atype==='reading'));
                audio.set(item.character, item.level, preload_audio);
                if (play_audio_now) {
                    audio.play();
                }
            } else {
                audio.clear();
            }

            $('#ss_quiz .answer input').attr('lang',alang).focus().select();
        }

        function prev_question(e) {
            if (quiz_idx === 0) return;
            quiz_idx--;
            if (e !== undefined) update_stats();
            if (quiz_idx === quiz_max) dialog.removeClass('round summary');
            update_question();
        }

        function next_question(e, prevent_exit) {
            quiz_idx++;
            if (quiz_idx > quiz_max) {
                if (!settings.quiz_repeat || force_summary) {
                    if (quiz_idx > quiz_max+1) {
                        if (!prevent_exit)
                            quiz.close();
                        else
                            quiz_idx--;
                        return;
                    }
                    if (e !== undefined) update_stats();
                } else if (settings.quiz_shuffle) {
                    dialog.removeClass('round summary');
                    round++;
                    answered = false;
                    update_stats();
                    create_quiz();
                }
            } else {
                if (e !== undefined) update_stats();
            }
            update_question();
        }

        function shake(elem) {
            var dist = '25px';
            var speed = 100;
            var right = {padding:'0 '+dist+' 0 0'}, left = {padding:'0 0 0 '+dist}, center = {padding:"0 0 0 0"};

            elem.animate(left,speed/2).animate(right,speed)
                .animate(left,speed).animate(right,speed)
                .animate(left,speed).animate(center,speed/2);
        }

        function quiz_submit(e) {
            var input = $('#ss_quiz .answer input');

            // Handle keys for 'Round X' and 'Summary'
            if (quiz_idx === -1 || quiz_idx > quiz_max) {
                next_question();
                return;
            }

            if (input.hasClass('correct')) {
                set_help(false);
                reset_answer();
                next_question();
                return;
            } else if (input.hasClass('incorrect')) {
                set_help(false);
                reset_answer();
                return;
            }

            var answer = trim(input.val().toLowerCase());
            if (answer === '') return;

            var is_correct;
            if (settings.quiz_typo && alang != 'ja') {
                is_correct = (good_answers.filter(function(a){return (jw_distance(a,answer)>0.9);}).length > 0);
            } else {
                is_correct = (good_answers.indexOf(answer) >= 0);
            }
            if (is_correct) {
                if (!answered) {
                    correct++;
                    answered = true;
                    update_stats();
                }
                if (itype==='vocabulary' && settings.audio_mode && qtype!=='aud' && atype==='reading') {
                    audio.play();
                }
                if (settings.lightning_mode) return next_question();
                input.addClass('correct').prop('readonly',true);
                $('body').on('keydown.ss_quiz keypress.ss_quiz', quiz_key);
            } else if (all_answers.indexOf(answer) >= 0 ||
                       (alang === 'ja' ?
                        (!wanakana.isKana(answer) || all_answers.indexOf(wanakana.toRomaji(answer)) >= 0) :
                        (all_answers.indexOf(wanakana.toHiragana(answer)) >= 0)
                       )
                      ) {
                shake(input);
            } else {
                if (!answered) {
                    incorrect++;
                    answered = true;
                    update_stats();
                    requiz_order.push(order[quiz_idx]);
                }
                input.addClass('incorrect').prop('readonly',true);
                $('body').on('keydown.ss_quiz keypress.ss_quiz', quiz_key);
                dialog.find('.summary .errors').append(
                    '<li><span class="que"'+(qlang==='ja'?' lang="ja"':'')+' title="'+toTitleCase(dialog.find('.qtype').text())+'">'+
                    (qtype==='aud' ? ichar+' ' : '')+dialog.find('.question').html()+'</span> '+
                    '<i class="icon-long-arrow-right"></i> '+
                    '<span class="ans"'+(alang==='ja'?' lang="ja"':'')+' title="'+good_answers.join(', ')+'">'+
                    answer+' <i class="icon-remove-sign wrong"></i></span></li>'
                );
            }
        }

        function quiz_key(e) {
            $('body').off('.ss_quiz');
            var code;
            if (e.type==='keydown') {
                if (e.originalEvent.code === undefined) {
                    // Shim for Safari's lack of support for KeyboardEvent.code
                    switch (e.originalEvent.keyCode) {
                        case 8: code = 'Backspace'; break;
                        case 13: code = 'Enter'; break;
                        case 27: code = 'Escape'; break;
                        case 37: code = 'ArrowLeft'; break;
                        case 39: code = 'ArrowRight'; break;
                        case 65: code = 'KeyA'; break;
                        case 69: code = 'KeyE'; break;
                        case 72: code = 'KeyH'; break;
                        case 76: code = 'KeyL'; break;
                        case 79: code = 'KeyO'; break;
                        case 82: code = 'KeyR'; break;
                        case 83: code = 'KeyS'; break;
                        case 112: code = 'F1'; break;
                        default: code = 'Unknown'; break;
                    }
                } else {
                    code = e.originalEvent.code;
                }
            } else {
                code = String.fromCharCode(e.charCode);
            }

            if (code==='Enter') {
                quiz_submit(e);
            } else if (code==='Escape') { // Esc
                if (quiz_idx > quiz_max)
                    quiz_submit(e);
                else
                    goto_summary();
            } else if (code==='F1' || code==='?') { // F1 or ?
                toggle_help();
            } else if (code==='Backspace' && $('.answer input').prop('readonly')) { // Prevent backspace from navigating away from the page
                e.preventDefault();
                e.stopPropagation();
            } else if (e.ctrlKey || e.metaKey) {
                switch(code) {
                    case 'KeyA':
                        if (e.shiftKey) {
                            toggle_audio();
                        } else {
                            if (itype==='vocabulary') {
                                audio.play();
                            }
                        }
                        break;     // Audio
                    case 'KeyE': goto_summary(); break;     // End
                    case 'KeyH': toggle_help(); break;      // Help
                    case 'KeyL': toggle_lightning(); break; // Lightning
                    case 'KeyO': toggle_typo(); break;      // Typo ("oops")
                    case 'KeyR': toggle_repeat(); break;    // Repeat
                    case 'KeyS': if (e.shiftKey) quiz.refresh(); else toggle_shuffle(); break;   // Shuffle
                    case 'ArrowLeft' : prev_question(true /* update stats */, true /* prevent_exit */); break; // Previous question
                    case 'ArrowRight': next_question(true /* update stats */, true /* prevent_exit */); break; // Next question
                }
            } else {
                return;
            }
            e.preventDefault();
            e.stopPropagation();
        }

    })(quiz);

    //-------------------------------------------------------------------
    // Startup. Runs at document 'load' event.
    //-------------------------------------------------------------------
    function startup() {
        // Load settings.
        var s = localStorage.getItem('selfstudy_settings');
        if (s) {
            s = JSON.parse(s);
            if (s.compatible !== undefined && s.compatible == settings.compatible) {
                delete settings.configs;
                $.extend(true, settings, s);
            }
        }

        // Insert CSS
        $('head').append('<style type="text/css">'+css+'</style>');

        // Insert HTML
        $('section[id^="level-"]').prepend(html);

        populate_presets();

        // Install handlers
        $('.selfstudy button.enable').on('click', toggle_enable);
        $('.selfstudy button.quiz').on('click', quiz.open);
        $('.selfstudy button.shuffle').on('click', shuffle);
        $('.selfstudy select.config').on('change', config_change_event);
        $('.selfstudy button.config').on('click', configure);

        set_config(settings.selected_config);
        if (settings.enabled) {
            set_enable();
            shuffle();
        }
    }

    // Run startup() after window.onload event.
    if (document.readyState === 'complete')
        startup();
    else
        window.addEventListener("load", startup, false);

})(window.wkselfstudy);
2 Likes