Wanikani Self-Study Quiz Edition

Self-study your items via the Wanikani level pages

当前为 2016-08-29 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Wanikani Self-Study Quiz Edition
// @namespace   rfindley
// @description Self-study your items via the Wanikani level pages
// @version     2.0.18
// @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) {

    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, audio_name = '';
        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,
            request_play: false,
            loaded: false,

            clear: function() {
                audio.name = '';
                audio.level = 0;
                audio.request_load = false;
                audio.request_play = 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.load();
                });
            },

            load: function() {
                if (audio.loaded) return;
                dialog.find('audio:not(.old)').each(function(i,tag){
                    $(tag).attr('src', audio.urls[audio.level][audio.name]);
                    tag.load();
                    tag.oncanplaythrough = audio.onloaded;
                });
            },

            onloaded: function(event) {
                event.target.oncanplaythrough = null;
                audio.request_load = false;
                audio.loaded = true;
                if (audio.request_play) {
                    audio.play();
                }
            },

            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 type="audio/mpeg"></audio>');
                if (audio.request_load) audio.load();
            },

            play: function() {
                if (!audio.loaded) {
                    audio.request_play = true;
                    audio.load();
                    return;
                }
                dialog.find('audio:not(.old)').each(function(i,tag){
                    audio.request_play = false;
                    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('/account')
            .done(function(page, status, xhr){
                if (status !== 'success') return show_error();
                apikey = $(page).find('#api-button').closest('.controls').find('input[type="text"]').val();
                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;

            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+'/'+level)
            .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 = items_cache[itype][level].slice(0);

            // Remove any items that aren't included in the current selection.
            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, 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);