Forchuse

WK Self-Study Quiz w 'show_kanji' button

Oct 25th, 2019
446
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name        Wanikani Self-Study Quiz
  3. // @namespace   rfindley
  4. // @description Quiz yourself on Wanikani items
  5. // @version     3.0.25
  6. // @include     https://www.wanikani.com/*
  7. // @exclude     https://www.wanikani.com/review*
  8. // @exclude     https://www.wanikani.com/lesson*
  9. // @require     https://unpkg.com/[email protected]/umd/wanakana.min.js
  10. // @copyright   2018+, Robin Findley
  11. // @license     MIT; http://opensource.org/licenses/MIT
  12. // @run-at      document-end
  13. // @grant       none
  14. // ==/UserScript==
  15.  
  16. window.ss_quiz = {};
  17.  
  18. (function(gobj) {
  19.  
  20.     /* global $, wkof, wanakana */
  21.     /* eslint no-multi-spaces: "off" */
  22.  
  23.     //===================================================================
  24.     // Initialization of the Wanikani Open Framework.
  25.     //-------------------------------------------------------------------
  26.     var script_name =  'Self-Study Quiz';
  27.     var wkof_version_needed = '1.0.17';
  28.     if (!window.wkof) {
  29.         if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?')) {
  30.             window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
  31.         }
  32.         return;
  33.     }
  34.     if (wkof.version.compare_to(wkof_version_needed) === 'older') {
  35.         if (confirm(script_name+' requires Wanikani Open Framework version '+wkof_version_needed+'.\nDo you want to be forwarded to the update page?')) {
  36.             window.location.href = 'https://greasyfork.org/en/scripts/38582-wanikani-open-framework';
  37.         }
  38.         return;
  39.     }
  40.  
  41.     wkof.include('Menu');
  42.     wkof.ready('Menu').then(install_menu);
  43.  
  44.     function install_menu() {
  45.         wkof.Menu.insert_script_link({
  46.             name: 'selfstudyquiz',
  47.             submenu: 'Open',
  48.             title: 'Self-Study Quiz',
  49.             on_click: open_quiz
  50.         });
  51.     }
  52.  
  53.     //########################################################################
  54.     // QUIZ SETTINGS DIALOG
  55.     //########################################################################
  56.  
  57.     //========================================================================
  58.     // setup_quiz_settings()
  59.     //------------------------------------------------------------------------
  60.     var quiz_settings_state = 'init';
  61.     function setup_quiz_settings() {
  62.         if (quiz_settings_state === 'init') {
  63.             quiz_settings_state = 'loading';
  64.             return wkof.ready('Settings')
  65.             .then(function(){
  66.                 quiz_settings_state = 'setup';
  67.                 setup_quiz_settings();
  68.             });
  69.         }
  70.         if (quiz_settings_state !== 'setup') return;
  71.  
  72.         var config = {
  73.             script_id: 'ss_quiz',
  74.             title: 'Self-Study Quiz',
  75.             pre_open: preopen_quiz_settings,
  76.             on_save: save_quiz_settings,
  77.             on_close: close_quiz_settings,
  78.             on_refresh: refresh_quiz_settings,
  79.             no_bkgd: true,
  80.             settings: {
  81.                 pg_questions: {type:'page',label:'Questions',hover_tip:'Choose what quiz questions you want to be asked',content:{
  82.                     grp_qpre_list: {type:'group',label:'Presets List',content:{
  83.                         active_qpreset: {type:'list',refresh_on_change:true,hover_tip:'Question Presets',content:{}},
  84.                     }},
  85.                     grp_qpre: {type:'group',label:'Selected Preset',content:{
  86.                         sect_qpre_name: {type:'section',label:'Preset Name'},
  87.                         qpre_name: {type:'text',label:'Edit Preset Name',on_change:refresh_qpresets,path:'@qpresets[@active_qpreset].name',hover_tip:'Enter a name for the selected preset'},
  88.  
  89.                         sect_qpre_question: {type:'section',label:'Questions <span class="icon-circle-arrow-right"></span> Answers'},
  90.                         char2mean: {type:'checkbox',label:'Rad/Kan/Voc <span class="icon-circle-arrow-right"></span> Meaning',path:'@qpresets[@active_qpreset].content.char2mean',hover_tip:'Question: A radical or kanji character, or vocab word drawn with kanji\nAnswer: The meaning in English'},
  91.                         char2read: {type:'checkbox',label:'Kan/Voc <span class="icon-circle-arrow-right"></span> Reading',path:'@qpresets[@active_qpreset].content.char2read',hover_tip:'Question: A kanji character, or vocab word drawn with kanji\nAnswer: The Japanese reading, in hiragana or katakana'},
  92.                         read2mean: {type:'checkbox',label:'Voc Reading <span class="icon-circle-arrow-right"></span> Meaning',path:'@qpresets[@active_qpreset].content.read2mean',hover_tip:'Question: A kanji or vocab reading, in hiragana or katakana\nAnswer: The meaning in English'},
  93.                         mean2read: {type:'checkbox',label:'Voc Meaning <span class="icon-circle-arrow-right"></span> Reading',path:'@qpresets[@active_qpreset].content.mean2read',hover_tip:'Question: A vocab word in English\nAnswer: The Japanese reading, in hiragana or katakana'},
  94.                         aud2mean: {type:'checkbox',label:'Voc Audio <span class="icon-circle-arrow-right"></span> Meaning',path:'@qpresets[@active_qpreset].content.aud2mean',hover_tip:'Question: A vocab word, in spoken audio\nAnswer: The meaning in English'},
  95.                         aud2read: {type:'checkbox',label:'Voc Audio <span class="icon-circle-arrow-right"></span> Reading',path:'@qpresets[@active_qpreset].content.aud2read',hover_tip:'Question: A vocab word, in spoken audio\nAnswer: The Japanese reading, in hiragana or katakana'},
  96.                     }},
  97.                 }},
  98.                 pg_items: {type:'page',label:'Items',hover_tip:'Choose what items you want to be quizzed on',content:{
  99.                     grp_ipre_list: {type:'group',label:'Presets List',content:{
  100.                         active_ipreset: {type:'list',refresh_on_change:true,hover_tip:'Item Presets',content:{}},
  101.                     }},
  102.                     grp_ipre: {type:'group',label:'Selected Preset',content:{
  103.                         sect_ipre_name: {type:'section',label:'Preset Name'},
  104.                         ipre_name: {type:'text',label:'Edit Preset Name',on_change:refresh_ipresets,path:'@ipresets[@active_ipreset].name',hover_tip:'Enter a name for the selected preset'},
  105.  
  106.                         sect_ipre_srcs: {type:'section',label:'Item Sources'},
  107.                         ipre_srcs: {type:'tabset',content:{}},
  108.                     }},
  109.                 }},
  110.                 pg_opts: {type:'page',label:'Settings',hover_tip:'Configure the user interface settings',content:{
  111.                     grp_quiz_size: {type:'group',label:'Quiz Size',content:{
  112.                         max_quiz_size: {type:'number',label:'Maximum Quiz Size',hover_tip:'Set the approximate maximum quiz size. (0 for unlimited)',default:0},
  113.                     }},
  114.                     grp_synonyms: {type:'group',label:'Synonyms',content:{
  115.                         synonyms_order: {type:'dropdown',label:'Synonym order in Help',hover_tip:'Set the order that synonyms appear in Help hints. (default: First)',default:'first',content:{first:'First',last:'Last'}},
  116.                     }},
  117.                     grp_typos: {type:'group',label:'Typo Tolerance',content:{
  118.                         allow_typos: {type:'checkbox',label:'Allow typos',hover_tip:'When enabled, English answers with minor typos will be accepted.',default:true},
  119.                     }},
  120.                     grp_help: {type:'group',label:'Wrong Answers',content:{
  121.                         autoshow_correct: {type:'checkbox',label:'Auto-show Correct Answer',hover_tip:'Automatically show the correct answer\nwhen you answer incorrectly.',default:false},
  122.                     }},
  123.                     grp_msgs: {type:'group',label:'Warning Messages',content:{
  124.                         show_slightly_off: {type:'checkbox',label:'Answer is slightly off',path:'@messages.show_slightly_off',hover_tip:'Tells you when your answer is slightly off',default:true},
  125.                         show_multi_reading: {type:'checkbox',label:'Has multiple readings',path:'@messages.show_multi_reading',hover_tip:'Tells you when an item has multiple readings',default:false},
  126.                     }},
  127.                     grp_halt: {type:'group',label:'Override Lightning',content:{
  128.                         halt_slightly_off: {type:'checkbox',label:'Halt if slightly off',path:'@messages.halt_slightly_off',hover_tip:'Override lightning mode when your answer is slightly off',default:true},
  129.                         halt_multi_reading: {type:'checkbox',label:'Halt if multiple readings',path:'@messages.halt_multi_reading',hover_tip:'Override lightning mode when an item has multiple readings',default:false},
  130.                     }},
  131.                     grp_audio: {type:'group',label:'Audio',content:{
  132.                         audio_type: {type:'dropdown',label:'Audio file type',hover_tip:'Audio file type (default=mp3)',default:'mp3',content:{mp3:'mp3',ogg:'ogg'}},
  133.                         audio_gender: {type:'dropdown',label:'Speaker',hover_tip:'',default:'rotate',content:{rotate:'Rotate',random:'Random',male:'Male',female:'Female'}},
  134.                     }},
  135.                 }},
  136.             },
  137.         };
  138.  
  139.         populate_items_config(config);
  140.  
  141.         quiz.settings_dialog = new wkof.Settings(config);
  142.         quiz_settings_state = 'ready';
  143.         open_quiz_settings();
  144.     }
  145.  
  146.     //========================================================================
  147.     // preopen_quiz_settings()
  148.     //------------------------------------------------------------------------
  149.     function preopen_quiz_settings(dialog) {
  150.         var btn_grp =
  151.             '<div class="pre_list_btn_grp">'+
  152.             '<button type="button" ref="###" action="new" class="ui-button ui-corner-all ui-widget" title="Create a new preset">New</button>'+
  153.             '<button type="button" ref="###" action="up" class="ui-button ui-corner-all ui-widget" title="Move the selected preset up in the list"><span class="icon-arrow-up"></span></button>'+
  154.             '<button type="button" ref="###" action="down" class="ui-button ui-corner-all ui-widget" title="Move the selected preset down in the list"><span class="icon-arrow-down"></span></button>'+
  155.             '<button type="button" ref="###" action="delete" class="ui-button ui-corner-all ui-widget" title="Delete the selected preset">Delete</button>'+
  156.             '</div>';
  157.  
  158.         var wrap = dialog.find('#ss_quiz_active_qpreset').closest('.row');
  159.         wrap.addClass('pre_list_wrap');
  160.         wrap.prepend(btn_grp.replace(/###/g, 'qpreset'));
  161.         wrap.find('.pre_list_btn_grp').on('click', 'button', preset_button_pressed);
  162.  
  163.         wrap = dialog.find('#ss_quiz_active_ipreset').closest('.row');
  164.         wrap.addClass('pre_list_wrap');
  165.         wrap.prepend(btn_grp.replace(/###/g, 'ipreset'));
  166.         wrap.find('.pre_list_btn_grp').on('click', 'button', preset_button_pressed);
  167.  
  168.         $('#ss_quiz_ipre_srcs .row:first-child').each(function(i,e){
  169.             var row = $(e);
  170.             var right = row.find('>.right');
  171.             row.prepend(right);
  172.             row.addClass('src_enable');
  173.         });
  174.  
  175.         // Customize the item source filters.
  176.         var srcs = $('#ss_quiz_ipre_srcs');
  177.         var flt_grps = srcs.find('.wkof_group');
  178.         flt_grps.addClass('filters');
  179.         var filters = flt_grps.find('.row');
  180.         filters.prepend('<div class="enable"><input type="checkbox"></div>');
  181.         filters.on('change', '.enable input[type="checkbox"]', toggle_filter);
  182.  
  183.         init_settings();
  184.         refresh_qpresets();
  185.         refresh_ipresets();
  186.     }
  187.  
  188.     //========================================================================
  189.     // open_quiz_settings()
  190.     //------------------------------------------------------------------------
  191.     function open_quiz_settings() {
  192.         if (quiz_settings_state !== 'ready') return setup_quiz_settings();
  193.         quiz_settings_state = 'open';
  194.         var backup = {};
  195.         quiz.backup = backup;
  196.         backup.max_quiz_size = quiz.settings.max_quiz_size;
  197.         backup.qpre = JSON.stringify(quiz.settings.qpresets[quiz.settings.active_qpreset].content);
  198.         backup.ipre = JSON.stringify(quiz.settings.ipresets[quiz.settings.active_ipreset].content);
  199.         quiz.settings_dialog.open();
  200.     }
  201.  
  202.     //========================================================================
  203.     // save_quiz_settings()
  204.     //------------------------------------------------------------------------
  205.     function save_quiz_settings(settings) {
  206.         quiz.settings = settings;
  207.         populate_presets($('#ss_quiz_qna'), settings.qpresets, settings.active_qpreset);
  208.         populate_presets($('#ss_quiz_source'), settings.ipresets, settings.active_ipreset);
  209.         var qpre = JSON.stringify(quiz.settings.qpresets[quiz.settings.active_qpreset].content);
  210.         var ipre = JSON.stringify(quiz.settings.ipresets[quiz.settings.active_ipreset].content);
  211.         var reshuffle = (qpre !== quiz.backup.qpre) || (quiz.settings.max_quiz_size !== quiz.backup.max_quiz_size);
  212.         var refetch = (ipre !== quiz.backup.ipre);
  213.         var redraw = (quiz.settings.synonyms_order !== quiz.backup.synonyms_order);
  214.         delete quiz.backup;
  215.         if (refetch) {
  216.             fetch_items().then(quiz.start);
  217.         } else if (reshuffle) {
  218.             quiz.start();
  219.         } else if (redraw) {
  220.             quiz.qinfo.cache = {};
  221.             quiz.ask();
  222.         }
  223.     }
  224.  
  225.     //========================================================================
  226.     // close_quiz_settings()
  227.     //------------------------------------------------------------------------
  228.     function close_quiz_settings(settings) {
  229.         quiz_settings_state = 'setup';
  230.     }
  231.  
  232.     //========================================================================
  233.     // refresh_quiz_settings()
  234.     //------------------------------------------------------------------------
  235.     function refresh_quiz_settings(settings) {
  236.         $('#ss_quiz_ipre_srcs .wkof_group .row').each(function(i,e){
  237.             var row = $(e);
  238.             var panel = row.closest('[role="tabpanel"]');
  239.             var source = panel.attr('id').match(/^ss_quiz_pg_(.*)$/)[1];
  240.             var filter_name = row.find('.setting').attr('name').slice((source+'_flt_').length);
  241.             var preset = quiz.settings.ipresets[quiz.settings.active_ipreset].content;
  242.             var enabled = false;
  243.             try {
  244.                 enabled = preset[source].filters[filter_name].enabled;
  245.             } catch(e) {}
  246.  
  247.             if (enabled) {
  248.                 row.addClass('checked');
  249.             } else {
  250.                 row.removeClass('checked');
  251.             }
  252.             row.find('.enable input[type="checkbox"]').prop('checked', enabled);
  253.         });
  254.     }
  255.  
  256.     //========================================================================
  257.     // refresh_qpresets()
  258.     //------------------------------------------------------------------------
  259.     function refresh_qpresets() {
  260.         var settings = quiz.settings;
  261.         populate_presets($('#ss_quiz_active_qpreset'), settings.qpresets, settings.active_qpreset);
  262.     }
  263.  
  264.     //========================================================================
  265.     // refresh_ipresets()
  266.     //------------------------------------------------------------------------
  267.     function refresh_ipresets() {
  268.         var settings = quiz.settings;
  269.         populate_presets($('#ss_quiz_active_ipreset'), settings.ipresets, settings.active_ipreset);
  270.     }
  271.  
  272.     //========================================================================
  273.     // preset_button_pressed()
  274.     //------------------------------------------------------------------------
  275.     function preset_button_pressed(e) {
  276.         var settings = quiz.settings;
  277.         var ref = e.currentTarget.attributes.ref.value;
  278.         var action = e.currentTarget.attributes.action.value;
  279.         var selected = Number(settings['active_'+ref]);
  280.         var presets = settings[ref+'s'];
  281.         var elem = $('#ss_quiz_active_'+ref);
  282.  
  283.         var dflt;
  284.         if (ref === 'qpreset') {
  285.             dflt = {name:'<untitled>', content:$.extend(true, {}, qpre_defaults)};
  286.         } else {
  287.             dflt = {name:'<untitled>', content:$.extend(true, {}, ipre_defaults)};
  288.         }
  289.  
  290.         switch (action) {
  291.             case 'new':
  292.                 presets.push(dflt);
  293.                 selected = presets.length - 1;
  294.                 settings[ref+'s'] = presets;
  295.                 settings['active_'+ref] = selected;
  296.                 populate_presets(elem, presets, selected);
  297.                 quiz.settings_dialog.refresh();
  298.                 $('#ss_quiz_'+ref.slice(0,4)+'_name').focus().select();
  299.                 break;
  300.  
  301.             case 'up':
  302.                 if (selected <= 0) break;
  303.                 presets = [].concat(presets.slice(0, selected-1), presets[selected], presets[selected-1], presets.slice(selected+1));
  304.                 selected--;
  305.                 settings[ref+'s'] = presets;
  306.                 settings['active_'+ref] = selected;
  307.                 populate_presets(elem, presets, selected);
  308.                 break;
  309.  
  310.             case 'down':
  311.                 if (selected >= presets.length-1) break;
  312.                 presets = [].concat(presets.slice(0, selected), presets[selected+1], presets[selected], presets.slice(selected+2));
  313.                 selected++;
  314.                 settings[ref+'s'] = presets;
  315.                 settings['active_'+ref] = selected;
  316.                 populate_presets(elem, presets, selected);
  317.                 break;
  318.  
  319.             case 'delete':
  320.                 presets = presets.slice(0, selected).concat(presets.slice(selected+1));
  321.                 selected = Math.max(0, selected-1);
  322.                 settings[ref+'s'] = presets;
  323.                 settings['active_'+ref] = selected;
  324.                 populate_presets(elem, presets, selected);
  325.                 quiz.settings_dialog.refresh();
  326.                 break;
  327.         }
  328.     }
  329.  
  330.     //========================================================================
  331.     // init_settings()
  332.     //------------------------------------------------------------------------
  333.     var qpre_defaults = {char2mean:false, char2read:false, read2mean:false, mean2read:false, aud2mean:false, aud2read:false};
  334.     function init_settings() {
  335.         var idx;
  336.         // Merge some defaults
  337.         var defaults = {
  338.             pairing: 'reading_first',
  339.             allow_typos: true,
  340.             play_audio: true,
  341.             mute_audio: false,
  342.             autoshow_correct: false,
  343.             max_quiz_size: 0, // 0 = unlimited
  344.             messages: {
  345.                 show_slightly_off: true,
  346.                 show_multi_reading: false,
  347.                 halt_slightly_off: true,
  348.                 halt_multi_reading: false,
  349.             }
  350.         };
  351.         var settings = $.extend(true, {}, defaults, wkof.settings.ss_quiz);
  352.         wkof.settings.ss_quiz = quiz.settings = settings;
  353.         if (settings.qpresets === undefined) {
  354.             settings.qpresets = [
  355.                 {name:'All Questions', content:{char2mean:true, char2read:true, read2mean:true, mean2read:true, aud2mean:true, aud2read:true}},
  356.                 {name:'Japanese to English', content:{char2mean:true, char2read:true, read2mean:false, mean2read:false, aud2mean:false, aud2read:false}},
  357.                 {name:'English to Japanese', content:{char2mean:false, char2read:false, read2mean:false, mean2read:true, aud2mean:false, aud2read:false}},
  358.                 {name:'Audio Quiz', content:{char2mean:false, char2read:false, read2mean:false, mean2read:false, aud2mean:true, aud2read:true}},
  359.             ];
  360.             settings.active_qpreset = 0;
  361.         }
  362.         for (idx in settings.qpresets) {
  363.             settings.qpresets[idx].content = $.extend(true, {}, qpre_defaults, settings.qpresets[idx].content);
  364.         }
  365.         if (settings.messages === undefined) {
  366.             settings.messages = {show_slightly_off:true, show_multi_reading:false, halt_slightly_off:true, halt_multi_reading:false}
  367.         }
  368.         if (settings.ipresets === undefined) {
  369.             settings.ipresets = [
  370.                 {name:'All Learned Items', content:{wk_items:{enabled:true,filters:{srs:{enabled:true,value:{appr1:true,appr2:true,appr3:true,appr4:true,guru1:true,guru2:true,mast:true,enli:true,burn:true}}}}}},
  371.                 {name:'Apprentice Items', content:{wk_items:{enabled:true,filters:{srs:{enabled:true,value:{appr1:true,appr2:true,appr3:true,appr4:true}}}}}},
  372.                 {name:'Burned Items', content:{wk_items:{enabled:true,filters:{srs:{enabled:true,value:{burn:true}}}}}},
  373.                 {name:'Resurrected Items', content:{wk_items:{enabled:true,filters:{have_burned:{enabled:true,value:true},srs:{enabled:true,value:{appr1:true,appr2:true,appr3:true,appr4:true,guru1:true,guru2:true,mast:true,enli:true}}}}}},
  374.             ];
  375.             settings.active_ipreset = 0;
  376.         }
  377.         if (ipre_defaults) {
  378.             for (idx in settings.ipresets) {
  379.                 settings.ipresets[idx].content = $.extend(true, {}, ipre_defaults, settings.ipresets[idx].content);
  380.             }
  381.         }
  382.     }
  383.  
  384.     //========================================================================
  385.     // populate_items_config()
  386.     //------------------------------------------------------------------------
  387.     var ipre_defaults;
  388.     function populate_items_config(config) {
  389.         var ipre_srcs = config.settings.pg_items.content.grp_ipre.content.ipre_srcs.content;
  390.         var srcs = wkof.ItemData.registry.sources;
  391.         ipre_defaults = {};
  392.         for (var src_name in srcs) {
  393.             var src = srcs[src_name];
  394.             var pg_content = {};
  395.             ipre_srcs['pg_'+src_name] = {type:'page',label:src.description,content:pg_content};
  396.             var settings = {};
  397.             ipre_defaults[src_name] = settings;
  398.             pg_content[src_name+'_enable'] = {
  399.                 type:'checkbox',
  400.                 label:'Include this source',
  401.                 path:'@ipresets[@active_ipreset].content["'+src_name+'"].enabled',
  402.                 hover_tip:'Check to include this data source in the quiz'
  403.             };
  404.             // Enable Wanikani source by default.
  405.             settings.enabled = (src_name === 'wk_items');
  406.  
  407.             // Add 'Options' section.  'wk_items' is handled automatically.
  408.             if (src_name !== 'wk_items') {
  409.                 if (src.options && Object.keys(src.options).length > 0) {
  410.                     settings.options = {};
  411.                     var opt_content = {};
  412.                     pg_content['grp_'+src_name+'_options'] = {type:'group',label:'Options',content:opt_content};
  413.                     for (var opt_name in src.options) {
  414.                         var opt = src.options[opt_name];
  415.                         switch (opt.type) {
  416.                             case 'checkbox':
  417.                                 opt_content[src_name+'_opt_'+opt_name] = {
  418.                                     type:'checkbox',
  419.                                     label:opt.label,
  420.                                     default:opt.default,
  421.                                     hover_tip:opt.hover_tip
  422.                                 }
  423.                                 break;
  424.                         }
  425.                     }
  426.                 }
  427.             }
  428.  
  429.             // Add 'Filters' section.
  430.             if (src.filters && Object.keys(src.filters).length > 0) {
  431.                 settings.filters = {};
  432.                 var flt_content = {};
  433.                 pg_content['grp_'+src_name+'_filters'] = {type:'group',label:'Filters',content:flt_content};
  434.                 for (var flt_name in src.filters) {
  435.                     var flt = src.filters[flt_name];
  436.                     settings.filters[flt_name] = {enabled:false, value:flt.default};
  437.                     switch (flt.type) {
  438.                         case 'checkbox':
  439.                             flt_content[src_name+'_flt_'+flt_name] = {
  440.                                 type:'checkbox',
  441.                                 label:flt.label,
  442.                                 default:flt.default,
  443.                                 path:'@ipresets[@active_ipreset].content["'+src_name+'"].filters["'+flt_name+'"].value',
  444.                                 hover_tip:flt.hover_tip
  445.                             }
  446.                             break;
  447.                         case 'multi':
  448.                             var dflt = flt.default;
  449.                             if (typeof flt.filter_value_map === 'function') dflt = flt.filter_value_map(dflt);
  450.                             flt_content[src_name+'_flt_'+flt_name] = {
  451.                                 type:'list',
  452.                                 multi:true,
  453.                                 size:Math.min(4,Object.keys(flt.content).length),
  454.                                 label:flt.label,
  455.                                 content:flt.content,
  456.                                 default:dflt,
  457.                                 path:'@ipresets[@active_ipreset].content["'+src_name+'"].filters["'+flt_name+'"].value',
  458.                                 hover_tip:flt.hover_tip
  459.                             }
  460.                             settings.filters[flt_name].value = dflt;
  461.                             break;
  462.                         case 'text':
  463.                         case 'number':
  464.                         case 'input':
  465.                             flt_content[src_name+'_flt_'+flt_name] = {
  466.                                 type:flt.type,
  467.                                 label:flt.label,
  468.                                 placeholder:flt.placeholder,
  469.                                 default:flt.default,
  470.                                 path:'@ipresets[@active_ipreset].content["'+src_name+'"].filters["'+flt_name+'"].value',
  471.                                 hover_tip:flt.hover_tip
  472.                             }
  473.                             break;
  474.                         case 'button':
  475.                             flt_content[src_name+'_flt_'+flt_name] = {
  476.                                 type:flt.type,
  477.                                 label:flt.label,
  478.                                 on_click:flt.on_click,
  479.                                 hover_tip:flt.hover_tip
  480.                             }
  481.                             break;
  482.                     }
  483.                 }
  484.             }
  485.         }
  486.     }
  487.  
  488.     //========================================================================
  489.     // toggle_filter()
  490.     //------------------------------------------------------------------------
  491.     function toggle_filter(e) {
  492.         var row = $(e.delegateTarget);
  493.         var panel = row.closest('[role="tabpanel"]');
  494.         var source = panel.attr('id').match(/^ss_quiz_pg_(.*)$/)[1];
  495.         var enabled = row.find('.enable input[type="checkbox"]').prop('checked');
  496.         var preset = quiz.settings.ipresets[quiz.settings.active_ipreset].content;
  497.         var filter_name = row.find('.setting').attr('name').slice((source+'_flt_').length);
  498.  
  499.         if (enabled) {
  500.             row.addClass('checked');
  501.         } else {
  502.             row.removeClass('checked');
  503.         }
  504.         try {
  505.             preset[source].filters[filter_name].enabled = enabled;
  506.         } catch(e) {}
  507.     }
  508.  
  509.     //########################################################################
  510.     // QUIZ DIALOG
  511.     //########################################################################
  512.  
  513.     //========================================================================
  514.     // install_css()
  515.     //------------------------------------------------------------------------
  516.     function install_css() {
  517.         $('head').append(
  518.             '<style id="ss_quiz_css" type="text/css">'+
  519.             '.noselect {-webkit-touch-callout:none; -webkit-user-select:none; -khtml-user-select:none; -moz-user-select: none; -ms-user-select:none; user-select: none;}'+
  520.  
  521.             '#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;}'+
  522.             '#ss_quiz {position:fixed; z-index:12001; width:573px; background-color:#000; border-radius:8px; border:8px solid rgba(0,0,0,0.85); font-size:16px; line-height:16px; font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;}'+
  523.             '#ss_quiz * {text-align:center;}'+
  524.  
  525.             '#ss_quiz .titlebar {cursor:move; text-align:left; padding-bottom:4px; font-size:1.125em; font-weight:bold; line-height:1.125em; background-color:rgba(0,0,0,0.85); color:#ddd;}'+
  526.             '#ss_quiz .titlebar .button {display:inline-block; float:right; height:20px; width:20px; line-height:1em; cursor:pointer; border:1px solid rgba(255,255,255,0.2); border-radius:4px;}'+
  527.  
  528.             '#ss_quiz .prev, #ss_quiz .next {display:inline-block; width:80px; color:#fff; line-height:8em; cursor:pointer;}'+
  529.             '#ss_quiz .prev:hover {background-image:linear-gradient(to left, rgba(0,0,0,0), rgba(0,0,0,0.2));}'+
  530.             '#ss_quiz .next:hover {background-image:linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.2));}'+
  531.             '#ss_quiz .prev {float:left;}'+
  532.             '#ss_quiz .next {float:right;}'+
  533.  
  534.             '#ss_quiz .cfgbar {background-color:rgba(32,32,32,0.85); padding:4px 0; border-bottom:1px solid #444;}'+
  535.             '#ss_quiz .cfgbar select {margin:0; background:transparent; color:rgba(255,255,255,0.5); border:1px solid #777; width:248px; height:2em; text-align:left; font-size:0.875em; border-radius:4px; padding:4px 6px;}'+
  536.             '#ss_quiz .cfgbar option {color:#000;}'+
  537.             '#ss_quiz .cfgbar .button {display:inline-block; width:24px; height:24px; cursor:pointer; color:#777; font-size:24px; vertical-align:middle;}'+
  538.             '#ss_quiz .cfgbar .button:hover {color:#ccc;}'+
  539.  
  540.             '#ss_quiz .statusbar {line-height:1em; color:rgba(255,255,255,0.5); background-color:rgba(32,32,32,0.85);}'+
  541.  
  542.             '#ss_quiz .settings {float:left; padding:6px 8px; text-align:left; line-height:1.5em; font-size:0.875em;}'+
  543.             '#ss_quiz .settings span[class*="icon-"] {font-size:1.3em; padding:0 2px;}'+
  544.             '#ss_quiz .settings .ss_audio {padding-left:0; padding-right:4px;}'+
  545.             '#ss_quiz .settings .ss_done {font-size:1.25em;}'+
  546.             '#ss_quiz .settings .ss_pair {font-weight:bold;}'+
  547.             '#ss_quiz .settings .ss_kanji {font-weight:bold; padding:0 1px 0 4px}'+
  548.             '#ss_quiz .settings span {cursor:pointer;}'+
  549.             '#ss_quiz .settings > span:hover {color:rgba(255,255,204,0.8);}'+
  550.             '#ss_quiz .settings span.active {color:#ffc;}'+
  551.  
  552.             '#ss_quiz .stats_labels {text-align:right; font-family:monospace; font-size:14px; line-height:14px; white-space:pre;}'+
  553.             '#ss_quiz .stats {float:right; text-align:right; color:rgba(255,255,255,0.8); font-family:monospace; padding:0 5px;}'+
  554.  
  555.             '#ss_quiz .icon-audio:before {content:"\\f028";}'+
  556.  
  557.             '#ss_quiz .ss_audio {display:inline-block;box-sizing:border-box;width:22px;text-align:left}'+
  558.             '#ss_quiz .ss_audio:before {content:"\\f026";}'+
  559.             '#ss_quiz .ss_audio.active:before {content:"\\f028";}'+
  560.             '#ss_quiz .ss_audio.mute {color:rgba(255,0,0,0.8);}'+
  561.             '#ss_quiz .ss_audio.mute:hover {color:rgba(255,127,127,0.8);}'+
  562.  
  563.             '#ss_quiz[data-qtype="characters"] .question {font-size:2em;}'+
  564.             '#ss_quiz .question svg.radical {width:1em;height:1em;stroke:#fff;stroke-width:68;stroke-linecap:square;stroke-miterlimit:2;fill:none;}'+
  565.  
  566.             '#ss_quiz .atype {font-size:1.75em; line-height:2em; cursor:default; color:#fff; border-top:1px solid #000; border-bottom:1px solid #000;}'+
  567.             '#ss_quiz[data-atype="reading"] .atype {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;}'+
  568.             '#ss_quiz[data-atype="meaning"] .atype {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;}'+
  569.  
  570.             '#ss_quiz .help, #ss_quiz .show_kanji {display:none;'+
  571.             '  position:absolute; top:3%; left:13%; width:74%; box-sizing:border-box; border:2px solid #000; border-radius:15px; padding:4px;'+
  572.             '  color:#555; text-shadow:2px 2px 0 rgba(0,0,0,0.13); background-color:rgba(255,255,255,0.9); font-size:0.8em; line-height:1.2em;'+
  573.             '}'+
  574.             '#ss_quiz.help .help, #ss_quiz.show_kanji .show_kanji {display:inherit;}'+
  575.  
  576.             '#ss_quiz .message {visibility:hidden;'+
  577.             '  position:absolute; bottom:3%; left:13%; width:74%; box-sizing:border-box; border:2px solid #000; border-radius:15px; padding:4px;'+
  578.             '  color:#555; text-shadow:2px 2px 0 rgba(0,0,0,0.13); background-color:rgba(255,255,255,0.9); font-size:0.6em; line-height:1.2em; opacity:0; transition:visibility 0.25s, opacity 0.25s linear;'+
  579.             '}'+
  580.             '#ss_quiz.message .message {visibility:visible; opacity:1; transition:visibility 0s, opacity 0.25s linear;}'+
  581.  
  582.             '#ss_quiz .answer {font-size:1.75em; background-color:#ddd; padding:8px;}'+
  583.             '#ss_quiz .answer input {'+
  584.             '  width:100%; background-color:#fff; height:2em; margin:0; border:2px solid #000; padding:0;'+
  585.             '  box-sizing:border-box; border-radius:0; font-size:1em;'+
  586.             '}'+
  587.             '#ss_quiz[data-result="correct"] .answer input {color:#fff; background-color:#8c8; text-shadow:2px 2px 0 rgba(0,0,0,0.2);}'+
  588.             '#ss_quiz[data-result="incorrect"] .answer input {color:#fff; background-color:#f03; text-shadow:2px 2px 0 rgba(0,0,0,0.2);}'+
  589.  
  590.             '#ss_quiz .btn.requiz {position:absolute; top:6px; right:6px; padding-left:6px; padding-right:6px;}'+
  591.  
  592.             '#ss_quiz .qwrap {height:8em; position:relative; clear:both; font-size:1.75em}'+
  593.             '#ss_quiz[data-itype="radical"] .qwrap, #ss_quiz[data-itype="radical"] .summary .que {background-color:#0af;}'+
  594.             '#ss_quiz[data-itype="kanji"] .qwrap, #ss_quiz[data-itype="kanji"] .summary .que {background-color:#f0a;}'+
  595.             '#ss_quiz[data-itype="vocabulary"] .qwrap, #ss_quiz[data-itype="vocabulary"] .summary .que {background-color:#a0f;}'+
  596.  
  597.             '#ss_quiz .qwrap > .center {display:none; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);}'+
  598.  
  599.             '#ss_quiz[data-mode="loading"] .qwrap {background-color:#ccc; opacity:0.5;}'+
  600.             '#ss_quiz[data-mode="loading"] .answer {opacity:0.5;}'+
  601.  
  602.             '#ss_quiz[data-mode="question"] .question {display:block;}'+
  603.             '#ss_quiz .question {overflow-x:auto; overflow-y:hidden; color:#fff; text-align:center; line-height:1.1em; font-size:1em; cursor:default;}'+
  604.             '#ss_quiz .question .icon-audio {font-size:2.5em; cursor:pointer;}'+
  605.  
  606.             '#ss_quiz[data-mode="summary"] .summary {display:block;}'+
  607.             '#ss_quiz .summary {display:none; position:absolute; width:74%; height:100%; background-color:rgba(0,0,0,0.7); color:#fff; font-weight:bold;}'+
  608.             '#ss_quiz .summary h3 {'+
  609.             '  background-image:linear-gradient(to bottom, #3c3c3c, #1a1a1a); background-repeat:repeat-x;'+
  610.             '  border-top:1px solid #777; border-bottom:1px solid #000; margin:0; box-sizing:border-box;'+
  611.             '  text-shadow:2px 2px 0 rgba(0,0,0,0.5); color:#fff; font-size:0.8em; font-weight:bold; line-height:40px;'+
  612.             '}'+
  613.             '#ss_quiz .summary .errors {position:absolute; top:40px; bottom:0px; width:100%; margin:0; overflow-y:auto; list-style-type:none;}'+
  614.             '#ss_quiz .summary li {margin:4px 0 0 0; font-size:0.6em; font-weight:bold; line-height:1.4em;}'+
  615.  
  616.             '#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;}'+
  617.             '#ss_quiz .summary .ans {background-color:#fff; color:#000;}'+
  618.             '#ss_quiz .summary .wrong {color:#f22;}'+
  619.  
  620.  
  621.             //--[ Settings dialog ]-------------------------------------------
  622.             '#wkof_ds div[role="dialog"][aria-describedby="wkofs_ss_quiz"] {z-index:12002;}'+
  623.  
  624.             '#wkofs_ss_quiz.wkof_settings .pre_list_btn_grp {width:60px;float:left;margin-right:2px;}'+
  625.             '#wkofs_ss_quiz.wkof_settings .pre_list_btn_grp button {width:100%; padding:2px 0;}'+
  626.             '#wkofs_ss_quiz.wkof_settings .pre_list_btn_grp button:not(:last-child) {margin-bottom:2px;}'+
  627.             '#wkofs_ss_quiz.wkof_settings .pre_list_wrap {display:flex;}'+
  628.             '#wkofs_ss_quiz.wkof_settings .pre_list_wrap .right {flex:1;}'+
  629.             '#wkofs_ss_quiz.wkof_settings .pre_list_wrap .list {overflow:auto;height:100%;}'+
  630.  
  631.             '#wkofs_ss_quiz.wkof_settings .filters .row {border-top:1px solid #ccc; padding:6px 4px; margin-bottom:0;}'+
  632.             '#wkofs_ss_quiz.wkof_settings .filters .row:not(.checked) {padding-top:0px;padding-bottom:0px;}'+
  633.             '#wkofs_ss_quiz .filters .row .enable input[type="checkbox"] {margin:0;}'+
  634.             '#wkofs_ss_quiz.narrow .filters .row.checked .right input[type="checkbox"]:after {content:"⇐yes?";margin-left:28px;line-height:30px;}'+
  635.             '#wkofs_ss_quiz .filters .row.checked {background-color:#f7f7f7;}'+
  636.             '#wkofs_ss_quiz .filters .row:not(.checked) {opacity:0.5;}'+
  637.             '#wkofs_ss_quiz .filters .row .enable {display:inline; margin:0; float:left;}'+
  638.             '#wkofs_ss_quiz:not(.narrow) .filters .left {width:170px;}'+
  639.  
  640.             '#wkofs_ss_quiz .filters .row .enable input[type="checkbox"] {margin:0 4px 0 0;}'+
  641.             '#wkofs_ss_quiz .filters .row:not(.checked) .right {display:none;}'+
  642.             '#wkofs_ss_quiz .filters .row:not(.checked) .left label {text-align:left;}'+
  643.             '#wkofs_ss_quiz.narrow .filters .row .left {width:initial;}'+
  644.             '#wkofs_ss_quiz.narrow .filters .row .left label {line-height:30px;}'+
  645.             '#wkofs_ss_quiz #ss_quiz_ipre_srcs .src_enable .left {width:initial;}'+
  646.             '#wkofs_ss_quiz #ss_quiz_ipre_srcs .src_enable .left label {text-align:left;width:initial;line-height:30px;}'+
  647.             '#wkofs_ss_quiz #ss_quiz_ipre_srcs .src_enable .right {float:left; margin:0 4px;width:initial;}'+
  648.             //----------------------------------------------------------------
  649.  
  650.             '</style>'
  651.         );
  652.     }
  653.  
  654.     //========================================================================
  655.     // open_quiz()
  656.     //------------------------------------------------------------------------
  657.     var quiz_setup_state = 'init';
  658.     function open_quiz(custom_options) {
  659.         if (quiz_setup_state === 'init') {
  660.             quiz_setup_state = 'loading';
  661.             install_css();
  662.             wkof.include('ItemData, Settings');
  663.             wkof.ready('ItemData, Settings').then(function(){
  664.                 return wkof.Settings.load('ss_quiz');
  665.             }).then(function(){
  666.                 quiz_setup_state = 'ready';
  667.                 init_settings();
  668.                 open_quiz(custom_options);
  669.             });
  670.         }
  671.         if (quiz_setup_state !== 'ready') return;
  672.  
  673.         var quiz_html =
  674.             '<div id="ss_quiz" class="dialog" data-itype="loading" data-atype="meaning" data-mode="question">'+
  675.             '  <div class="titlebar noselect">Self-Study Quiz<span class="button" title="Close the quiz.\nHotkey: Rapid triple-tap [Esc]">x</span></div>'+
  676.             '  <div class="cfgbar">'+
  677.             '    <select id="ss_quiz_qna" title="Choose what quiz questions you want to be asked"></select>'+
  678.             '    <select id="ss_quiz_source" title="Choose what items you want to be quizzed on"></select>'+
  679.             '    <span class="icon-repeat shuffle button" title="Shuffle Quiz (Ctrl-S)\nDouble-click to reset Round counter"></span>'+
  680.             '    <span class="icon-cog config button" title="Configure presets"></span>'+
  681.             '  </div>'+
  682.             '  <div class="statusbar">'+
  683.             '    <div class="settings noselect">'+
  684.             '      <span class="icon-bolt ss_lightning" title="Lightning Mode: Skip <enter> on correct answers (Ctrl-L)"></span>'+
  685.             '      <span class="icon-audio ss_audio" title="Toggle when to play audio (Ctrl-Shift-A)\n* Red = Never play audio\n* Gray = Audio questions only\n* Yellow = Audio questions, After correct reading, Opening help for reading\n\nTo play audio immediately, press (Ctrl-A)"></span>'+
  686.             '      <span class="icon-question-sign ss_help" title="Help: Peek at item info (F1, Ctrl-H, or ?)"></span>'+
  687.             '      <span class="icon-show-kanji ss_kanji" title="Show kanji (Ctrl-K)">字</span>'+
  688.             '      <span class="icon-step-forward ss_done" title="End the quiz and show summary (Esc or Ctrl-E)"></span><br />'+
  689.             '      <span class="ss_pair" title="Pairing mode: Group reading and meaning together (Ctrl-P)">Pairing: <span class="data">Disabled</span></span>'+
  690.             '    </div>'+
  691.             '    <div class="stats"></div>'+
  692.             '    <div class="stats_labels">Round:<br>Remaining:<br>Correct:<br>Incorrect:</div>'+
  693.             '  </div>'+
  694.             '  <div class="qwrap">'+
  695.             '    <div class="prev" title="Previous question (Ctrl-Left)"><i class="icon-chevron-left"></i></div>'+
  696.             '    <div class="next" title="Next question (Ctrl-Right)"><i class="icon-chevron-right"></i></div>'+
  697.             '    <div class="question center"></div>'+
  698.             '    <div class="help"></div>'+
  699.             '    <div class="show_kanji"></div>'+
  700.             '    <div class="message"></div>'+
  701.             '    <div class="summary center">'+
  702.             '      <h3>Summary - <span class="percent">100%</span> Correct <button class="btn requiz" title="Re-quiz wrong items">Re-quiz</button></h3>'+
  703.             '      <ul class="errors"></ul>'+
  704.             '    </div>'+
  705.             '  </div>'+
  706.             '  <div class="atype">Loading...</div>'+
  707.             '  <div class="answer"><input type="text" lang="en" value=""></div>'+
  708.             '</div>';
  709.  
  710.         if (quiz.dialog) quiz.close();
  711.         var dialog = (quiz.dialog = $(quiz_html));
  712.  
  713.         var settings = quiz.settings;
  714.         init_custom_options(custom_options);
  715.         populate_presets(dialog.find('#ss_quiz_qna'), settings.qpresets, settings.active_qpreset);
  716.         populate_presets(dialog.find('#ss_quiz_source'), settings.ipresets, settings.active_ipreset);
  717.  
  718.         wkof.Settings.background.open();
  719.         $('#wkof_ds').append(dialog);
  720.  
  721.         dialog.css('top', Math.max(0,Math.floor((window.innerHeight - dialog.outerHeight()) / 2)));
  722.         dialog.css('left', Math.floor((window.innerWidth - dialog.outerWidth()) / 2));
  723.  
  724.         // Initialize settings
  725.         var settings_bar = dialog.find('.statusbar .settings');
  726.         if (settings.lightning_mode === true) settings_bar.find('.ss_lightning').addClass('active');
  727.         if (settings.repeat_quiz === true) settings_bar.find('.ss_repeat').addClass('active');
  728.         if (settings.shuffle_on_repeat === true) settings_bar.find('.ss_shuffle').addClass('active');
  729.         if (settings.play_audio === true) settings_bar.find('.ss_audio').addClass('active');
  730.         if (settings.mute_audio === true) settings_bar.find('.ss_audio').addClass('mute');
  731.         toggle_pairing(null, true /* initialize */);
  732.  
  733.         // Events
  734.         dialog.find('.settings .ss_lightning').on('click', toggle_lightning);
  735.         dialog.find('.settings .ss_audio').on('click', toggle_audio);
  736.         dialog.find('.settings .ss_help').on('click', toggle_help);
  737.         dialog.find('.settings .ss_kanji').on('click', toggle_kanji);
  738.         dialog.find('.settings .ss_pair').on('click', toggle_pairing);
  739.         dialog.find('.settings .ss_done').on('click', process_escape);
  740.         dialog.find('.prev').on('click', quiz.prev);
  741.         dialog.find('.next').on('click', quiz.next);
  742.         dialog.find('.titlebar').on('mousedown touchstart', drag);
  743.         dialog.find('.cfgbar .button.shuffle').on('click', manual_shuffle);
  744.         dialog.find('.cfgbar .button.config').on('click', open_quiz_settings);
  745.         dialog.find('.titlebar .button').on('click', close_quiz);
  746.         dialog.find('.summary .requiz').on('click', quiz.requiz);
  747.         dialog.find('.question').on('click', '.icon-audio', play_audio.bind(null,true,null));
  748.         $('#ss_quiz_qna').on('change', qpreset_changed);
  749.         $('#ss_quiz_source').on('change', ipreset_changed);
  750.         $('body').on('keydown.ss_quiz_key keypress.ss_quiz_key', quiz_key_handler);
  751.         freeze_body();
  752.  
  753.         set_mode('loading');
  754.         fetch_items().then(quiz.start);
  755.     }
  756.  
  757.     //========================================================================
  758.     // init_custom_options()
  759.     //------------------------------------------------------------------------
  760.     function init_custom_options(custom) {
  761.         if (!custom) {
  762.             quiz.custom = {
  763.                 has_ipreset: false,
  764.                 using_ipreset: false,
  765.                 has_qpreset: false,
  766.                 using_qpreset: false,
  767.             }
  768.             return;
  769.         }
  770.         quiz.custom = custom;
  771.         if (custom.qpreset) {
  772.             quiz.custom.has_qpreset = true;
  773.             quiz.custom.using_qpreset = true;
  774.         }
  775.         if (custom.ipreset) {
  776.             quiz.custom.has_ipreset = true;
  777.             quiz.custom.using_ipreset = true;
  778.         }
  779.     }
  780.  
  781.     //========================================================================
  782.     // close_quiz()
  783.     //------------------------------------------------------------------------
  784.     function close_quiz(e) {
  785.         unfreeze_body();
  786.         $('body').off('.ss_quiz_key');
  787.         quiz.dialog.remove();
  788.         wkof.Settings.background.close();
  789.     }
  790.  
  791.     var body_scroll_y;
  792.     function freeze_body() {
  793.         body_scroll_y = window.scrollY;
  794.         $('body').css('overflow', 'hidden').scrollTop(body_scroll_y);
  795.     }
  796.     function unfreeze_body() {
  797.         $('body').css('overflow','unset');
  798.         window.scroll({top:body_scroll_y});
  799.     }
  800.  
  801.     //========================================================================
  802.     // qpreset_changed()
  803.     //------------------------------------------------------------------------
  804.     function qpreset_changed(e) {
  805.         var settings = quiz.settings;
  806.         var selected = e.target.selectedOptions[0].attributes.name.value;
  807.         if (selected === 'custom') {
  808.             quiz.custom.using_qpreset = true;
  809.         } else {
  810.             quiz.custom.using_qpreset = false;
  811.             settings.active_qpreset = selected;
  812.             wkof.Settings.save('ss_quiz');
  813.         }
  814.         quiz.start();
  815.     }
  816.  
  817.     //========================================================================
  818.     // ipreset_changed()
  819.     //------------------------------------------------------------------------
  820.     function ipreset_changed(e) {
  821.         var settings = quiz.settings;
  822.         var selected = e.target.selectedOptions[0].attributes.name.value;
  823.         if (selected === 'custom') {
  824.             quiz.custom.using_ipreset = true;
  825.         } else {
  826.             quiz.custom.using_ipreset = false;
  827.             settings.active_ipreset = selected;
  828.             wkof.Settings.save('ss_quiz');
  829.         }
  830.         fetch_items().then(quiz.start);
  831.     }
  832.  
  833.     //========================================================================
  834.     // populate_presets()
  835.     //------------------------------------------------------------------------
  836.     function populate_presets(elem, presets, active_preset) {
  837.         var html = '';
  838.         for (var idx in presets) {
  839.             var preset = presets[idx];
  840.             var name = preset.name.replace(/</g,'&lt;').replace(/>/g,'&gt;');
  841.             html += '<option name="'+idx+'">'+name+'</option>';
  842.         }
  843.         var elem_name = elem.attr('id')
  844.         if (elem_name === 'ss_quiz_qna' && quiz.custom.has_qpreset) {
  845.             html += '<option name="custom">('+quiz.custom.qpreset.name+')</option>';
  846.             if (quiz.custom.using_qpreset) active_preset = presets.length;
  847.         } else if (elem_name === 'ss_quiz_source' && quiz.custom.has_ipreset) {
  848.             html += '<option name="custom">('+quiz.custom.ipreset.name+')</option>';
  849.             if (quiz.custom.using_ipreset) active_preset = presets.length;
  850.         }
  851.         elem.html(html);
  852.         elem.children().eq(active_preset).prop('selected', true);
  853.     }
  854.  
  855.     //########################################################################
  856.     // QUIZ DATA
  857.     //########################################################################
  858.  
  859.     var quiz = {
  860.         // Dialogs
  861.         dialog: null,
  862.         settings_dialog: null,
  863.  
  864.         // Item Lists
  865.         items: [],
  866.         group_list: [],
  867.         serial_list: [],
  868.         index: null,
  869.  
  870.         // Status
  871.         showing_help: false,
  872.         mode: 'loading',
  873.  
  874.         // Question Info
  875.         qinfo: {
  876.             load: load_qinfo,
  877.             prep: prep_qinfo,
  878.             cache: {},
  879.         },
  880.  
  881.         // Stats
  882.         stats: {
  883.             round: 1,
  884.             total: 0,
  885.             correct: 0,
  886.             incorrect: 0,
  887.         },
  888.  
  889.         // Functions
  890.         start: start_quiz,
  891.         shuffle: shuffle_quiz,
  892.         requiz: requiz,
  893.         ask: ask_question,
  894.         submit: submit_answer,
  895.         prev: prev_question,
  896.         next: next_question,
  897.         close: close_quiz,
  898.     };
  899.     gobj.open = open_quiz;
  900.  
  901.     //========================================================================
  902.     // fetch_items()
  903.     //------------------------------------------------------------------------
  904.     function fetch_items() {
  905.         var settings = quiz.settings;
  906.         var ipreset = (quiz.custom.using_ipreset ? quiz.custom.ipreset.content : settings.ipresets[settings.active_ipreset].content);
  907.  
  908.         set_mode('loading');
  909.         var config = {};
  910.         for (var src_name in ipreset) {
  911.             var src_preset = ipreset[src_name];
  912.             if (!src_preset.enabled) continue;
  913.             if (!wkof.ItemData.registry.sources[src_name]) continue;
  914.             var src_cfg = {};
  915.             config[src_name] = src_cfg;
  916.             src_cfg.filters = {};
  917.             if (src_name === 'wk_items') src_cfg.options = {study_materials: true};
  918.             var ipre_filters = src_preset.filters;
  919.             for (var flt_name in ipre_filters) {
  920.                 var ipre_flt = ipre_filters[flt_name];
  921.                 if (!ipre_flt.enabled) continue;
  922.                 if (!wkof.ItemData.registry.sources[src_name].filters[flt_name]) continue;
  923.                 src_cfg.filters[flt_name] = {value: ipre_flt.value};
  924.                 if (ipre_flt.invert === true) src_cfg.filters[flt_name].invert = true;
  925.             }
  926.         }
  927.         return wkof.ItemData.get_items(config)
  928.         .then(function(items){
  929.             quiz.items = items;
  930.         });
  931.     }
  932.  
  933.     //========================================================================
  934.     // shuffle_quiz()
  935.     //------------------------------------------------------------------------
  936.     function shuffle_quiz() {
  937.         var settings = quiz.settings;
  938.         var qpreset = (quiz.custom.using_qpreset ? quiz.custom.qpreset.content : settings.qpresets[settings.active_qpreset].content);
  939.         var pairing = settings.pairing || 'disabled';
  940.  
  941.         var valid_question_types = {
  942.             char2read: {radical:false, kanji:true,  vocabulary:true},
  943.             char2mean: {radical:true,  kanji:true,  vocabulary:true},
  944.             read2mean: {radical:false, kanji:false, vocabulary:true},
  945.             mean2read: {radical:false, kanji:false, vocabulary:true},
  946.             aud2read:  {radical:false, kanji:false, vocabulary:true},
  947.             aud2mean:  {radical:false, kanji:false, vocabulary:true},
  948.         };
  949.  
  950.         var id, idx, item, qset;
  951.         var grp_list = quiz.group_list = [];
  952.         quiz.stats.total = 0;
  953.         switch (pairing) {
  954.             case 'disabled':
  955.                 var qna = ['char2mean','char2read','read2mean','mean2read','aud2mean','aud2read'];
  956.                 for (id in quiz.items) {
  957.                     item = quiz.items[id];
  958.                     for (idx in qna) {
  959.                         var qtype = qna[idx];
  960.                         if (valid(qtype)) {
  961.                             grp_list.push({item:item, qna:[qtype], order:Math.random()});
  962.                         }
  963.                     }
  964.                 }
  965.                 break;
  966.  
  967.             case 'reading_first':
  968.                 for (id in quiz.items) {
  969.                     item = quiz.items[id];
  970.                     qset = [];
  971.                     if (valid('char2read')) qset.push('char2read');
  972.                     if (valid('char2mean')) qset.push('char2mean');
  973.                     if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  974.                     if (valid('mean2read')) grp_list.push({item:item, qna:['mean2read'], order:Math.random()});
  975.                     if (valid('read2mean')) grp_list.push({item:item, qna:['read2mean'], order:Math.random()});
  976.                     qset = [];
  977.                     if (valid('aud2read')) qset.push('aud2read');
  978.                     if (valid('aud2mean')) qset.push('aud2mean');
  979.                     if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  980.                 }
  981.                 break;
  982.  
  983.             case 'meaning_first':
  984.                 for (id in quiz.items) {
  985.                     item = quiz.items[id];
  986.                     qset = [];
  987.                     if (valid('char2mean')) qset.push('char2mean');
  988.                     if (valid('char2read')) qset.push('char2read');
  989.                     if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  990.                     if (valid('read2mean')) grp_list.push({item:item, qna:['read2mean'], order:Math.random()});
  991.                     if (valid('mean2read')) grp_list.push({item:item, qna:['mean2read'], order:Math.random()});
  992.                     qset = [];
  993.                     if (valid('aud2mean')) qset.push('aud2mean');
  994.                     if (valid('aud2read')) qset.push('aud2read');
  995.                     if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  996.                 }
  997.                 break;
  998.  
  999.             case 'random_order':
  1000.                 for (id in quiz.items) {
  1001.                     item = quiz.items[id];
  1002.                     qset = [];
  1003.                     if (Math.random() < 0.5) {
  1004.                         if (valid('char2read')) qset.push('char2read');
  1005.                         if (valid('char2mean')) qset.push('char2mean');
  1006.                     } else {
  1007.                         if (valid('char2mean')) qset.push('char2mean');
  1008.                         if (valid('char2read')) qset.push('char2read');
  1009.                     }
  1010.                     if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  1011.                     if (valid('mean2read')) grp_list.push({item:item, qna:['mean2read'], order:Math.random()});
  1012.                     if (valid('read2mean')) grp_list.push({item:item, qna:['read2mean'], order:Math.random()});
  1013.                     qset = [];
  1014.                     if (Math.random() < 0.5) {
  1015.                         if (valid('aud2read')) qset.push('aud2read');
  1016.                         if (valid('aud2mean')) qset.push('aud2mean');
  1017.                     } else {
  1018.                         if (valid('aud2mean')) qset.push('aud2mean');
  1019.                         if (valid('aud2read')) qset.push('aud2read');
  1020.                     }
  1021.                     if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  1022.                 }
  1023.                 break;
  1024.         }
  1025.  
  1026.         grp_list.sort(function(a,b){return a.order - b.order;});
  1027.         var serial_list = quiz.serial_list = [];
  1028.         for (var idx1 in grp_list) {
  1029.             for (var idx2 in grp_list[idx1].qna) {
  1030.                 serial_list.push([idx1, idx2]);
  1031.             }
  1032.         }
  1033.         quiz.qinfo.cache = {};
  1034.         quiz.stats.real_total = quiz.stats.total;
  1035.         if (settings.max_quiz_size > 0) quiz.stats.total = Math.min(quiz.stats.total, settings.max_quiz_size);
  1036.  
  1037.         function valid(qtype) {
  1038.             var valid = ((qpreset[qtype] === true) && (valid_question_types[qtype][item.object] === true));
  1039.             if (valid) quiz.stats.total++;
  1040.             return valid;
  1041.         }
  1042.     }
  1043.  
  1044.     //########################################################################
  1045.     // QUIZ
  1046.     //########################################################################
  1047.  
  1048.     //========================================================================
  1049.     // jw_distance() - Jaro-Winkler Distance
  1050.     //------------------------------------------------------------------------
  1051.     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){h=a[b];if(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;}
  1052.  
  1053.     //========================================================================
  1054.     // to_title_case() - Make first letter of each word upper-case.
  1055.     //------------------------------------------------------------------------
  1056.     function to_title_case(str) {return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});}
  1057.  
  1058.     //========================================================================
  1059.     // start_quiz()
  1060.     //------------------------------------------------------------------------
  1061.     function start_quiz(options) {
  1062.         if (!options) options = {};
  1063.         if (options.keep_round_count !== true) options.keep_round_count = false; // Default 'false'
  1064.         if (!options.keep_round_count) quiz.stats.round = 1;
  1065.         quiz.stats.correct = 0;
  1066.         quiz.stats.incorrect = 0;
  1067.         quiz.index = 0;
  1068.         quiz.shuffle();
  1069.         if (quiz.stats.total === 0) return set_mode('no_items');
  1070.         set_mode('question');
  1071.     }
  1072.  
  1073.     //========================================================================
  1074.     // requiz()
  1075.     //------------------------------------------------------------------------
  1076.     function requiz() {
  1077.         quiz.do_requiz = true;
  1078.         quiz.next();
  1079.     }
  1080.  
  1081.     //========================================================================
  1082.     // set_mode()
  1083.     //------------------------------------------------------------------------
  1084.     function set_mode(mode) {
  1085.         var dialog = quiz.dialog;
  1086.         if (mode === 'previous') mode = quiz.last_mode;
  1087.         dialog.attr('data-mode', mode);
  1088.         switch (mode) {
  1089.             case 'loading':
  1090.                 dialog.attr('data-itype', 'loading');
  1091.                 dialog.attr('data-atype', 'loading');
  1092.                 dialog.find('.atype').html('Loading...');
  1093.                 break;
  1094.  
  1095.             case 'no_items':
  1096.                 dialog.attr('data-itype', 'loading');
  1097.                 dialog.attr('data-atype', 'reading');
  1098.                 dialog.find('.atype').html('No questions found!');
  1099.                 break;
  1100.  
  1101.             case 'question':
  1102.                 ask_question();
  1103.                 break;
  1104.  
  1105.             case 'summary':
  1106.                 dialog.attr('data-atype', 'reading');
  1107.                 dialog.find('.atype').html('[Enter] for new quiz, [Esc] to return');
  1108.                 dialog.find('.answer input').val('').prop('readonly', true);
  1109.                 dialog.attr('data-result', '');
  1110.                 populate_errors();
  1111.                 break;
  1112.         }
  1113.         if (quiz.mode !== quiz.last_mode) quiz.last_mode = quiz.mode;
  1114.         quiz.mode = mode;
  1115.     }
  1116.  
  1117.     function is_svg(img) {return (img.content_type === 'image/svg+xml') && (img.metadata.inline_styles === false);}
  1118.  
  1119.     //========================================================================
  1120.     // ask_question()
  1121.     //------------------------------------------------------------------------
  1122.     function ask_question(erase_old_answer) {
  1123.         var dialog = quiz.dialog;
  1124.         var qinfo = quiz.qinfo.load(quiz.index);
  1125.  
  1126.         toggle_help('off');
  1127.         toggle_kanji('off');
  1128.         dialog.attr('data-itype', qinfo.item.type);
  1129.         dialog.attr('data-qtype', qinfo.question.type);
  1130.         dialog.attr('data-atype', qinfo.answer.type);
  1131.         if (quiz.message_timer) {
  1132.             clearTimeout(quiz.message_timer);
  1133.             delete quiz.message_timer;
  1134.         }
  1135.         dialog.removeClass('message');
  1136.         dialog.attr('class', 'dialog');
  1137.         dialog.addClass(qinfo.item.type);
  1138.  
  1139.         // Draw the question
  1140.         var question = dialog.find('.question');
  1141.         question.attr('lang', qinfo.question.lang);
  1142.         if (qinfo.question.html || qinfo.item.type !== 'radical') {
  1143.             question.html(qinfo.question.html);
  1144.         } else {
  1145.             qinfo.question.svg_promise.then(function(qinfo){
  1146.                 if (quiz.index === qinfo.index) question.html(qinfo.question.html);
  1147.             });
  1148.         }
  1149.  
  1150.         // Initialize the answer
  1151.         var input = $('#ss_quiz .answer input');
  1152.         var old_answer = get_user_answer(quiz.index);
  1153.         if (erase_old_answer) {
  1154.             if (old_answer[0] !== '') quiz.stats[old_answer[0]]--;
  1155.             update_quiz_stats();
  1156.             set_user_answer(quiz.index, '', '');
  1157.             old_answer = ['', ''];
  1158.         }
  1159.         if (old_answer[0] !== '') {
  1160.             dialog.attr('data-result', old_answer[0]);
  1161.             input.val(old_answer[1]).prop('readonly', true);
  1162.         } else {
  1163.             dialog.attr('data-result', '');
  1164.             input.val('').prop('readonly', false);
  1165.         }
  1166.  
  1167.         if (qinfo.answer.lang === 'ja') {
  1168.             if (input.attr('lang') !== 'ja') {
  1169.                 input.attr('lang', 'ja');
  1170.                 wanakana.bind(input[0], {IMEMode:true});
  1171.             }
  1172.         } else {
  1173.             if (input.attr('lang') === 'ja') {
  1174.                 input.attr('lang', 'en');
  1175.                 wanakana.unbind(input[0]);
  1176.             }
  1177.         }
  1178.  
  1179.         // Populate the help window
  1180.         dialog.find('.help').attr('lang',qinfo.answer.lang).html(to_title_case(qinfo.answer.good.join(', '))+(qinfo.answer.help_suffix || ''));
  1181.         dialog.find('.atype').html(qinfo.answer.html);
  1182.  
  1183.         // Populate the kanji window
  1184.         dialog.find('.show_kanji').attr('lang','ja').html(qinfo.item.object.data.characters);
  1185.  
  1186.         // Update progress stats
  1187.         update_quiz_stats();
  1188.  
  1189.         // If question is audio, play audio now.
  1190.         if (!erase_old_answer && qinfo.question.type === 'audio') {
  1191.             play_audio(true /* force_play */, qinfo);
  1192.         }
  1193.  
  1194.         input.focus();
  1195.  
  1196.         quiz.qinfo.prep(quiz.index);
  1197.     }
  1198.  
  1199.     //========================================================================
  1200.     // play_audio()
  1201.     //------------------------------------------------------------------------
  1202.     function play_audio(force_play, qinfo) {
  1203.         if (quiz.settings.mute_audio) return;
  1204.         if (!force_play && !quiz.settings.play_audio) return;
  1205.         if (!qinfo) qinfo = quiz.qinfo.load(quiz.index);
  1206.         if (!qinfo) return;
  1207.         if (!qinfo.question.audio_promise) return;
  1208.         var nextInfo = quiz.qinfo.load(quiz.index+1);
  1209.         qinfo.question.audio_promise.then(function(qinfo){
  1210.             if (!((quiz.index === qinfo.index) ||
  1211.                   (quiz.settings.lightning_mode && nextInfo.question.type !== 'audio'))) return;
  1212.             qinfo.question.audio.currentTime = 0;
  1213.             qinfo.question.audio.play();
  1214.         });
  1215.     }
  1216.  
  1217.     //========================================================================
  1218.     // load_qinfo()
  1219.     //------------------------------------------------------------------------
  1220.     function load_qinfo(index) {
  1221.         if (index < 0 || index >= quiz.stats.total) return null;
  1222.         if (!quiz.qinfo.cache[index]) populate_qinfo(index);
  1223.         return quiz.qinfo.cache[index];
  1224.     }
  1225.  
  1226.     //========================================================================
  1227.     // prep_qinfo()
  1228.     //------------------------------------------------------------------------
  1229.     function prep_qinfo(index) {
  1230.         Object.keys(quiz.qinfo.cache).forEach(function(cache_idx){
  1231.             if (cache_idx < index-2 || cache_idx > index+2) {
  1232.                 delete quiz.qinfo.cache[cache_idx];
  1233.             }
  1234.         });
  1235.         for (var ofs = 1; ofs <= 2; ofs++) {
  1236.             populate_qinfo(index+ofs);
  1237.         }
  1238.     }
  1239.  
  1240.     //========================================================================
  1241.     // populate_qinfo()
  1242.     //------------------------------------------------------------------------
  1243.     function populate_qinfo(index) {
  1244.         if (index < 0 || index >= quiz.stats.total) return;
  1245.         if (quiz.qinfo.cache[index]) return;
  1246.         var qinfo = {index:index, item:{}, question:{}, answer:{}};
  1247.         quiz.qinfo.cache[index] = qinfo;
  1248.  
  1249.         var grp_idx = quiz.serial_list[index];
  1250.         var group = quiz.group_list[grp_idx[0]];
  1251.         var item = group.item;
  1252.         var qnatype = group.qna[grp_idx[1]];
  1253.         qinfo.first_in_group = (grp_idx[1] == 0);
  1254.         qinfo.item.type = item.object;
  1255.         qinfo.item.object = item;
  1256.         qinfo.question.type = {
  1257.             char2read:'characters', char2mean:'characters', mean2read:'meaning',
  1258.             read2mean:'reading', aud2read:'audio', aud2mean:'audio'
  1259.         }[qnatype];
  1260.         qinfo.answer.type = {
  1261.             char2read:'reading', char2mean:'meaning', mean2read:'reading',
  1262.             read2mean:'meaning', aud2read:'reading', aud2mean:'meaning'
  1263.         }[qnatype];
  1264.         qinfo.answer.html = to_title_case(qinfo.item.type+' '+qinfo.answer.type);
  1265.  
  1266.         var synonyms = [];
  1267.         try {synonyms = item.study_materials.meaning_synonyms || [];} catch(e) {}
  1268.         var meanings = item.data.meanings.map(meaning => meaning.meaning);
  1269.         if (quiz.settings.synonyms_order === 'first') {
  1270.             meanings = synonyms.concat(meanings).map(meaning => meaning.toLowerCase());
  1271.         } else {
  1272.             meanings = meanings.concat(synonyms).map(meaning => meaning.toLowerCase());
  1273.         }
  1274.  
  1275.         if (qinfo.item.type === 'vocabulary') {
  1276.             qinfo.question.audio_promise = new Promise(function(resolve, reject){
  1277.                 qinfo.question.audio = new Audio();
  1278.                 qinfo.question.audio.oncanplaythrough = function(){
  1279.                     resolve(qinfo);
  1280.                 }
  1281.                 if (item.id !== undefined) {
  1282.                     var audio_sources = item.data.pronunciation_audios;
  1283.                     var filtered_sources;
  1284.                     switch (quiz.settings.audio_type) {
  1285.                         case 'mp3': filtered_sources = audio_sources.filter(a => a.content_type == 'audio/mpeg'); break;
  1286.                         case 'ogg': filtered_sources = audio_sources.filter(a => a.content_type == 'audio/ogg'); break;
  1287.                         default: filtered_sources = audio_sources;
  1288.                     }
  1289.                     if (filtered_sources.length !== 0) audio_sources = filtered_sources;
  1290.                     switch (quiz.settings.audio_gender) {
  1291.                         case 'male': filtered_sources = audio_sources.filter(a => a.metadata.gender == 'male'); break;
  1292.                         case 'female': filtered_sources = audio_sources.filter(a => a.metadata.gender == 'female'); break;
  1293.                         case 'rotate':
  1294.                             quiz.gender = quiz.gender || 'female';
  1295.                             quiz.gender = (quiz.gender === 'female' ? 'male' : 'female');
  1296.                             filtered_sources = audio_sources.filter(a => a.metadata.gender == quiz.gender);
  1297.                             break;
  1298.                         default: filtered_sources = audio_sources;
  1299.                     }
  1300.                     if (filtered_sources.length !== 0) audio_sources = filtered_sources;
  1301.                     if (audio_sources.length === 0) {
  1302.                         qinfo.question.audio.src = null;
  1303.                     } else {
  1304.                         qinfo.question.audio.src = audio_sources[Math.floor(Math.random() * audio_sources.length)].url;
  1305.                     }
  1306.                 }
  1307.             });
  1308.         }
  1309.  
  1310.         switch (qinfo.question.type) {
  1311.             case 'characters':
  1312.                 qinfo.question.lang = 'ja';
  1313.                 if (qinfo.item.type === 'radical' && !item.data.characters) {
  1314.                     var svg_url = item.data.character_images.filter(is_svg)[0].url;
  1315.                     qinfo.question.svg_promise = wkof.load_file(svg_url).then(function(svg){
  1316.                         qinfo.question.html = svg;
  1317.                         return qinfo;
  1318.                     });
  1319.                 } else {
  1320.                     qinfo.question.html = item.data.characters;
  1321.                 }
  1322.                 break;
  1323.  
  1324.             case 'reading':
  1325.                 qinfo.question.lang = 'ja';
  1326.                 qinfo.question.html = item.data.readings.map(reading => reading.reading).join(', ');
  1327.                 break;
  1328.  
  1329.             case 'meaning':
  1330.                 qinfo.question.lang = 'en';
  1331.                 qinfo.question.html = to_title_case(meanings.join(', '));
  1332.                 break;
  1333.  
  1334.             case 'audio':
  1335.                 qinfo.question.lang = 'ja';
  1336.                 qinfo.question.html = '<span class="icon-audio"></span>';
  1337.                 qinfo.answer.help_suffix = '<br><span lang="ja">('+item.data.characters+')</span>';
  1338.                 break;
  1339.         }
  1340.  
  1341.         var idx, idx2, reading;
  1342.         qinfo.answer.other = [];
  1343.         qinfo.answer.bad = [];
  1344.         qinfo.answer.reading_type = '';
  1345.         switch (qinfo.answer.type) {
  1346.             case 'reading':
  1347.                 qinfo.answer.good = [];
  1348.                 qinfo.answer.lang = 'ja';
  1349.                 for (idx in item.data.readings) {
  1350.                     reading = item.data.readings[idx];
  1351.                     if (qinfo.item.type === 'vocabulary' || reading.accepted_answer) {
  1352.                         qinfo.answer.good.push(reading.reading);
  1353.                         if (qinfo.item.type === 'kanji') {
  1354.                             qinfo.answer.reading_type = reading.type.replace('yomi','\'yomi');
  1355.                         }
  1356.                     } else {
  1357.                         qinfo.answer.other.push(reading.reading);
  1358.                     }
  1359.                     qinfo.answer.bad = meanings;
  1360.                 }
  1361.                 break;
  1362.  
  1363.             case 'meaning':
  1364.                 qinfo.answer.good = meanings;
  1365.                 qinfo.answer.lang = 'en';
  1366.                 if (!item.data.readings) break;
  1367.                 for (idx in item.data.readings) {
  1368.                     reading = item.data.readings[idx];
  1369.  
  1370.                     if (qinfo.item.type === 'vocabulary' || reading.accepted_answer) {
  1371.                         qinfo.answer.bad.push(reading.reading);
  1372.                     }
  1373.                 }
  1374.                 break;
  1375.         }
  1376.     }
  1377.  
  1378.     //========================================================================
  1379.     // get_user_answer()
  1380.     //------------------------------------------------------------------------
  1381.     function get_user_answer(index) {
  1382.         var grp_idx = quiz.serial_list[index];
  1383.         var group = quiz.group_list[grp_idx[0]];
  1384.         if (!group.answer || !group.answer[grp_idx[1]]) return ['', ''];
  1385.         return group.answer[grp_idx[1]];
  1386.     }
  1387.  
  1388.     //========================================================================
  1389.     // set_user_answer()
  1390.     //------------------------------------------------------------------------
  1391.     function set_user_answer(index, status, answer) {
  1392.         var grp_idx = quiz.serial_list[index];
  1393.         var group = quiz.group_list[grp_idx[0]];
  1394.         if (!group.answer) group.answer = [];
  1395.         group.answer[grp_idx[1]] = [status, answer];
  1396.     }
  1397.  
  1398.     //========================================================================
  1399.     // submit_answer()
  1400.     //------------------------------------------------------------------------
  1401.     function submit_answer() {
  1402.         var dialog = quiz.dialog;
  1403.         var input = $('#ss_quiz .answer input');
  1404.  
  1405.         var qinfo = quiz.qinfo.load(quiz.index);
  1406.         var item = qinfo.item.object;
  1407.         var itype = qinfo.item.type;
  1408.         var atype = qinfo.answer.type;
  1409.         var raw_answer = input.val();
  1410.         var answer = raw_answer;
  1411.         var action = 'fail';
  1412.         var msgcfg = quiz.settings.messages;
  1413.         var is_exact = true;
  1414.         var is_multi = false;
  1415.         var message;
  1416.  
  1417.         if (answer === '') {
  1418.             atype = 'ignore';
  1419.             action = 'shake';
  1420.         }
  1421.  
  1422.         switch (atype) {
  1423.             case 'reading':
  1424.                 answer = wanakana.toHiragana(answer);
  1425.                 if (qinfo.answer.good.indexOf(answer) >= 0 || qinfo.answer.good.indexOf(raw_answer) >= 0) {
  1426.                     action = 'correct';
  1427.                     if (qinfo.answer.good.length > 1) is_multi = true;
  1428.                     if (is_multi && msgcfg.show_multi_reading) message = 'This item has multiple readings';
  1429.                 } else if (itype === 'kanji' && qinfo.answer.other.indexOf(answer) >= 0) {
  1430.                     action = 'shake';
  1431.                     message = 'We\'re looking for the '+to_title_case(qinfo.answer.reading_type)+' reading';
  1432.                 } else {
  1433.                     var bad = qinfo.answer.bad.map(function(english){
  1434.                         return wanakana.toHiragana(english.toLowerCase());
  1435.                     });
  1436.                     if (bad.indexOf(answer) >= 0) {
  1437.                         action = 'shake';
  1438.                         message = 'We\'re looking for the reading, not the meaning';
  1439.                     } else if (!wanakana.isKana(answer)) {
  1440.                         action = 'shake';
  1441.                         message = 'Your answer contains invalid characters';
  1442.                     } else {
  1443.                         action = 'incorrect';
  1444.                     }
  1445.                 }
  1446.                 break;
  1447.  
  1448.             case 'meaning':
  1449.                 var is_correct = false;
  1450.                 is_exact = false;
  1451.                 answer = answer.toLowerCase();
  1452.                 var allow_typos = (quiz.settings.allow_typos === true);
  1453.                 for (var idx in qinfo.answer.good) {
  1454.                     var good_answer = qinfo.answer.good[idx];
  1455.                     if (answer === good_answer) {
  1456.                         is_correct = true;
  1457.                         is_exact = true;
  1458.                         break;
  1459.                     } else if (allow_typos && jw_distance(good_answer, answer) > 0.9) {
  1460.                         is_correct = true;
  1461.                     }
  1462.                 }
  1463.                 if (is_correct) {
  1464.                     action = 'correct';
  1465.                     if (!is_exact && msgcfg.show_slightly_off === true) message = "Your answer was slightly off";
  1466.                 } else {
  1467.                     var alt_answer = wanakana.toHiragana(answer,{IMEMode:true});
  1468.                     if (qinfo.answer.bad.indexOf(alt_answer) >= 0) {
  1469.                         action = 'shake';
  1470.                         message = 'We\'re looking for the meaning, not the reading';
  1471.                     } else {
  1472.                         action = 'incorrect';
  1473.                     }
  1474.                 }
  1475.                 break;
  1476.         }
  1477.  
  1478.         if (action !== 'shake') set_user_answer(quiz.index, action, answer);
  1479.         switch (action) {
  1480.             case 'correct':
  1481.                 quiz.stats.correct++;
  1482.  
  1483.                 // If question is reading, play audio now.
  1484.                 if (qinfo.answer.type === 'reading' && qinfo.question.type !== 'audio') {
  1485.                     play_audio(false /* force_play */, qinfo);
  1486.                 }
  1487.  
  1488.                 if ((quiz.settings.lightning_mode === true) &&
  1489.                     (!is_multi || !msgcfg.show_multi_reading || !msgcfg.halt_multi_reading) &&
  1490.                     (is_exact || !msgcfg.show_slightly_off || !msgcfg.halt_slightly_off )) {
  1491.                     return quiz.next();
  1492.                 } else {
  1493.                     update_quiz_stats();
  1494.                     input.prop('readonly', true);
  1495.                 }
  1496.                 dialog.attr('data-result', 'correct');
  1497.                 break;
  1498.  
  1499.             case 'shake':
  1500.                 shake(input);
  1501.                 input.focus();
  1502.                 break;
  1503.  
  1504.             case 'incorrect':
  1505.                 quiz.stats.incorrect++;
  1506.                 update_quiz_stats();
  1507.                 input.prop('readonly', true);
  1508.                 dialog.attr('data-result', 'incorrect');
  1509.  
  1510.                 if (quiz.settings.autoshow_correct && !quiz.showing_help) {
  1511.                     toggle_help('on');
  1512.                 }
  1513.                 break;
  1514.         }
  1515.  
  1516.         if (message) {
  1517.             dialog.find('.message').text(message);
  1518.             dialog.addClass('message');
  1519.             if (quiz.message_timer) {
  1520.                 clearTimeout(quiz.message_timer);
  1521.                 delete quiz.message_timer;
  1522.             }
  1523.             quiz.message_timer = setTimeout(function(){
  1524.                 dialog.removeClass('message');
  1525.                 quiz.message_timer = undefined;
  1526.             },2750);
  1527.         }
  1528.     }
  1529.  
  1530.     //========================================================================
  1531.     // shake()
  1532.     //------------------------------------------------------------------------
  1533.     function shake(elem) {
  1534.         var dist = '15px';
  1535.         var speed = 75;
  1536.         var right = {padding:'0 '+dist+' 0 0'}, left = {padding:'0 0 0 '+dist}, center = {padding:"0 0 0 0"};
  1537.  
  1538.         elem.animate(left,speed/2).animate(right,speed)
  1539.         .animate(left,speed).animate(right,speed)
  1540.         .animate(left,speed).animate(center,speed/2);
  1541.     }
  1542.  
  1543.     //========================================================================
  1544.     // prev_question()
  1545.     //------------------------------------------------------------------------
  1546.     function prev_question() {
  1547.         switch (quiz.mode) {
  1548.             case 'question':
  1549.                 if (quiz.index > 0) quiz.index--;
  1550.                 quiz.ask();
  1551.                 break;
  1552.  
  1553.             case 'summary':
  1554.                 if (quiz.index === quiz.stats.total) {
  1555.                     quiz.index = quiz.stats.total - 1;
  1556.                     update_quiz_stats();
  1557.                 }
  1558.                 set_mode('question');
  1559.                 break;
  1560.         }
  1561.         quiz.ask();
  1562.     }
  1563.  
  1564.     //========================================================================
  1565.     // next_question()
  1566.     //------------------------------------------------------------------------
  1567.     function next_question() {
  1568.         switch (quiz.mode) {
  1569.             case 'question':
  1570.                 if (quiz.index < quiz.stats.total-1) {
  1571.                     quiz.index++;
  1572.                     quiz.ask();
  1573.                 } else {
  1574.                     quiz.index = quiz.stats.total;
  1575.                     update_quiz_stats();
  1576.                     set_mode('summary');
  1577.                 }
  1578.                 break;
  1579.  
  1580.             case 'summary':
  1581.                 if (quiz.do_requiz) {
  1582.                     delete quiz.do_requiz;
  1583.                     if (!quiz.original_items) {
  1584.                         quiz.original_items = quiz.items;
  1585.                     }
  1586.                     quiz.items = quiz.requiz_items;
  1587.                     delete quiz.requiz_items;
  1588.                 } else {
  1589.                     delete quiz.requiz_items;
  1590.                     if (quiz.original_items) {
  1591.                         quiz.items = quiz.original_items;
  1592.                         delete quiz.original_items;
  1593.                     }
  1594.                     quiz.stats.round++;
  1595.                 }
  1596.                 quiz.start({keep_round_count:true});
  1597.                 break;
  1598.         }
  1599.     }
  1600.  
  1601.     //========================================================================
  1602.     // populate_errors()
  1603.     //------------------------------------------------------------------------
  1604.     function populate_errors() {
  1605.         var dialog = quiz.dialog;
  1606.         var percent_elem = dialog.find('.summary .percent');
  1607.         var errors_elem = dialog.find('.summary .errors');
  1608.  
  1609.         var total = quiz.stats.correct + quiz.stats.incorrect;
  1610.         var percent = (total === 0 ? 100 : 100 * quiz.stats.correct / total);
  1611.         percent_elem.text((Math.round(percent*100)/100).toString()+'%');
  1612.         if (total === quiz.stats.correct) {
  1613.             $('#ss_quiz .summary .requiz').addClass('hidden');
  1614.         } else {
  1615.             $('#ss_quiz .summary .requiz').removeClass('hidden');
  1616.         }
  1617.  
  1618.         var idx;
  1619.         var err_list = dialog.find('.summary .errors');
  1620.         err_list.html('');
  1621.         var requiz_items = {};
  1622.         quiz.requiz_items = [];
  1623.         for (idx = 0; idx < quiz.stats.total; idx++) {
  1624.             var grp_idx = quiz.serial_list[idx];
  1625.             var group = quiz.group_list[grp_idx[0]];
  1626.             if (!group.answer) continue;
  1627.             var answer = group.answer[grp_idx[1]];
  1628.             if (!answer || answer[0] !== 'incorrect') continue;
  1629.             var item = group.item;
  1630.             if (!requiz_items[item.id]) {
  1631.                 requiz_items[item.id] = 1;
  1632.                 quiz.requiz_items.push(item);
  1633.             }
  1634.             var itype = item.object;
  1635.             var qnatype = group.qna[grp_idx[1]];
  1636.             answer = answer[1];
  1637.             var qtype = {
  1638.                 char2read:'characters', char2mean:'characters', mean2read:'meaning',
  1639.                 read2mean:'reading', aud2read:'audio', aud2mean:'audio'
  1640.             }[qnatype];
  1641.             var atype = {
  1642.                 char2read:'reading', char2mean:'meaning', mean2read:'reading',
  1643.                 read2mean:'meaning', aud2read:'reading', aud2mean:'meaning'
  1644.             }[qnatype];
  1645.             var qlang = (qtype === 'meaning' ? 'en' : 'ja');
  1646.             var alang = (atype === 'meaning' ? 'en' : 'ja');
  1647.             var qtitle = to_title_case(itype+' '+atype);
  1648.             var atitle;
  1649.             switch (atype) {
  1650.                 case 'meaning':
  1651.                     var synonyms = [];
  1652.                     try {synonyms = item.study_materials.meaning_synonyms || [];} catch(e) {}
  1653.                     var meanings = item.data.meanings.map(meaning => meaning.meaning);
  1654.                     if (quiz.settings.synonyms_order === 'first') {
  1655.                         meanings = synonyms.concat(meanings).map(meaning => meaning.toLowerCase());
  1656.                     } else {
  1657.                         meanings = meanings.concat(synonyms).map(meaning => meaning.toLowerCase());
  1658.                     }
  1659.                     atitle = meanings.join(', ');
  1660.                     break;
  1661.                 case 'reading':
  1662.                     atitle = to_title_case(item.data.readings.map(reading => reading.reading).join(', '));
  1663.                     break;
  1664.             }
  1665.             var qtext = item.data.slug;
  1666.             if (qtype === 'audio') qtext += ' <i class="icon-audio"></i>';
  1667.             var atext = answer + ' <i class="icon-remove-sign wrong"></i>';
  1668.             err_list.append(
  1669.                 '<li><span class="que" lang="'+qlang+'" title="'+qtitle+'">'+qtext+'</span>'+
  1670.                 '<i class="icon-long-arrow-right"></i>'+
  1671.                 '<span class="ans" lang="'+alang+'" title="'+atitle+'">'+atext+'</span></li>'
  1672.             );
  1673.         }
  1674.     }
  1675.  
  1676.     //========================================================================
  1677.     // update_quiz_stats()
  1678.     //------------------------------------------------------------------------
  1679.     function update_quiz_stats() {
  1680.         var stats = $('#ss_quiz .stats_labels');
  1681.         var stats_width = quiz.stats.total.toString().length; // Number of digits in quiz counter
  1682.         var remaining = quiz.stats.total - quiz.index;
  1683.         stats.html(
  1684.             'Round: '+('       '+quiz.stats.round).slice(-1*stats_width)+'<br>'+
  1685.             'Remaining: '+('       '+remaining).slice(-1*stats_width)+'<br>'+
  1686.             'Correct: '+('       '+quiz.stats.correct).slice(-1*stats_width)+'<br>'+
  1687.             'Incorrect: '+('       '+quiz.stats.incorrect).slice(-1*stats_width)
  1688.         );
  1689.     }
  1690.  
  1691.     //========================================================================
  1692.     // quiz_key_handler()
  1693.     //------------------------------------------------------------------------
  1694.     var keycode_xlat = {
  1695.         '8':'Backspace', '13':'Enter', '27':'Escape', '37':'ArrowLeft', '39':'ArrowRight', '65':'KeyA',
  1696.         '69':'KeyE', '72':'KeyH', '75':'KeyK', '76':'KeyL', '80':'KeyP', '82':'KeyR', '83':'KeyS', '112':'F1',
  1697.     };
  1698.     function quiz_key_handler(e) {
  1699.         if (quiz_settings_state === 'open') return true;
  1700.         var input = quiz.dialog.find('.answer input');
  1701.         var input_readonly = input.prop('readonly');
  1702.         var code;
  1703.         if (e.type === 'keydown') {
  1704.             if (e.originalEvent.keyCode) {
  1705.                 code = keycode_xlat[e.originalEvent.keyCode] || 'Unknown';
  1706.             } else {
  1707.                 code = e.originalEvent.code;
  1708.             }
  1709.         } else {
  1710.             code = String.fromCharCode(e.charCode);
  1711.         }
  1712.  
  1713.         if (code === 'Enter') {
  1714.             if (quiz.mode === 'question' && !input_readonly) {
  1715.                 quiz.submit(e);
  1716.             } else {
  1717.                 quiz.next();
  1718.             }
  1719.         } else if (code === 'Escape') {
  1720.             process_escape();
  1721.         } else if (code === 'F1' || code === '?') {
  1722.             toggle_help();
  1723.         } else if (code === 'Backspace') {
  1724.             // Prevent backspace from navigating away from the page.
  1725.             if (quiz.mode !== 'question') return false;
  1726.             if (input_readonly) quiz.ask(true /* erase_old_answer */);
  1727.             return true;
  1728.         } else if (e.ctrlKey || e.metaKey) {
  1729.             switch (code) {
  1730.                 case 'KeyA':
  1731.                     if (e.shiftKey) {
  1732.                         toggle_audio();
  1733.                     } else {
  1734.                         play_audio(true);
  1735.                     }
  1736.                     break;
  1737.                 case 'KeyE': process_escape(); break;   // End
  1738.                 case 'KeyH': toggle_help(); break;      // Help
  1739.                 case 'KeyK': toggle_kanji(); break;     // Kanji
  1740.                 case 'KeyL': toggle_lightning(); break; // Lightning
  1741.                 case 'KeyP': toggle_pairing(); break;   // Pairing
  1742.                 case 'KeyR': // Re-quiz
  1743.                     if (quiz.mode !== 'summary' || quiz.dialog.find('.summary .requiz').hasClass('hidden')) break;
  1744.                     quiz.requiz();
  1745.                     break;
  1746.                 case 'KeyS': manual_shuffle(); break;
  1747.                 case 'ArrowLeft': quiz.prev(); break;
  1748.                 case 'ArrowRight': quiz.next(); break;
  1749.                 default: return true;
  1750.             }
  1751.         } else {
  1752.             var is_special = (e.key.length !== 1);
  1753.             if (is_special) return true;
  1754.  
  1755.             // Let the browser handle regular keys in the input box
  1756.             if (e.target === input[0]) return true;
  1757.  
  1758.             // Let the browser handle all other keys while not in question mode.
  1759.             if (quiz.mode !== 'question') return true;
  1760.         }
  1761.         return false;
  1762.     }
  1763.  
  1764.     //========================================================================
  1765.     // manual_shuffle()
  1766.     //------------------------------------------------------------------------
  1767.     function manual_shuffle() {
  1768.         var keep_round_count = true;
  1769.         if (quiz.shuffle_timer === undefined) {
  1770.             quiz.shuffle_timer = setTimeout(function(){
  1771.                 delete quiz.shuffle_timer;
  1772.             }, 1000);
  1773.         } else {
  1774.             clearTimeout(quiz.shuffle_timer);
  1775.             delete quiz.shuffle_timer;
  1776.             keep_round_count = false;
  1777.         }
  1778.         quiz.start({keep_round_count:keep_round_count});
  1779.     }
  1780.  
  1781.     //========================================================================
  1782.     // process_escape()
  1783.     //------------------------------------------------------------------------
  1784.     function process_escape() {
  1785.         if (quiz.escape_timer === undefined) {
  1786.             quiz.escape_counter = 1;
  1787.             quiz.escape_timer = setTimeout(function(){
  1788.                 delete quiz.escape_counter;
  1789.                 delete quiz.escape_timer;
  1790.             }, 750);
  1791.         } else {
  1792.             quiz.escape_counter++;
  1793.             if (quiz.escape_counter === 3) {
  1794.                 clearTimeout(quiz.escape_timer);
  1795.                 delete quiz.escape_timer;
  1796.                 quiz.close();
  1797.                 return;
  1798.             }
  1799.         }
  1800.         switch (quiz.mode) {
  1801.             case 'question':
  1802.                 set_mode('summary');
  1803.                 break;
  1804.  
  1805.             case 'summary':
  1806.                 if (quiz.index === quiz.stats.total) quiz.index = quiz.stats.total-1;
  1807.                 set_mode('previous');
  1808.                 break;
  1809.         }
  1810.     }
  1811.  
  1812.     //========================================================================
  1813.     // toggle_audio()
  1814.     //------------------------------------------------------------------------
  1815.     function toggle_audio() {
  1816.         var elem = $('#ss_quiz .settings .ss_audio');
  1817.         if (quiz.settings.mute_audio) {
  1818.             quiz.settings.mute_audio = false;
  1819.             quiz.settings.play_audio = false;
  1820.             elem.removeClass('mute');
  1821.             elem.removeClass('active');
  1822.         } else if (quiz.settings.play_audio) {
  1823.             quiz.settings.mute_audio = true;
  1824.             quiz.settings.play_audio = false;
  1825.             elem.addClass('mute');
  1826.             elem.removeClass('active');
  1827.         } else {
  1828.             quiz.settings.mute_audio = false;
  1829.             quiz.settings.play_audio = true;
  1830.             elem.removeClass('mute');
  1831.             elem.addClass('active');
  1832.         }
  1833.         wkof.Settings.save('ss_quiz');
  1834.     }
  1835.  
  1836.     //========================================================================
  1837.     // toggle_help()
  1838.     //------------------------------------------------------------------------
  1839.     function toggle_help(value) {
  1840.         if (quiz.mode !== 'question') return;
  1841.         var elem = $('#ss_quiz .settings .ss_help');
  1842.         switch (value) {
  1843.             case 'on':
  1844.                 elem.addClass('active');
  1845.                 quiz.dialog.addClass('help');
  1846.                 quiz.showing_help = true;
  1847.                 toggle_kanji('off');
  1848.                 break;
  1849.             case 'off':
  1850.                 elem.removeClass('active');
  1851.                 quiz.dialog.removeClass('help');
  1852.                 quiz.showing_help = false;
  1853.                 break;
  1854.             default:
  1855.                 elem.toggleClass('active');
  1856.                 quiz.dialog.toggleClass('help');
  1857.                 quiz.showing_help = !quiz.showing_help;
  1858.                 if (quiz.showing_help) toggle_kanji('off');
  1859.                 break;
  1860.         }
  1861.         var qinfo = quiz.qinfo.load(quiz.index);
  1862.         if (quiz.showing_help && qinfo.answer.type === 'reading') play_audio(false /* force_play */);
  1863.     }
  1864.  
  1865.     //========================================================================
  1866.     // toggle_kanji()
  1867.     //------------------------------------------------------------------------
  1868.     function toggle_kanji(value) {
  1869.  
  1870.         var qinfo = quiz.qinfo.load(quiz.index);
  1871.         if (quiz.mode === 'question' && qinfo.question.type !== 'characters') {
  1872.             var elem = $('#ss_quiz .settings .ss_kanji');
  1873.             switch (value) {
  1874.                 case 'on':
  1875.                     elem.addClass('active');
  1876.                     quiz.dialog.addClass('show_kanji');
  1877.                     quiz.showing_kanji = true;
  1878.                     toggle_help('off');
  1879.                     break;
  1880.                 case 'off':
  1881.                     elem.removeClass('active');
  1882.                     quiz.dialog.removeClass('show_kanji');
  1883.                     quiz.showing_kanji = false;
  1884.                     break;
  1885.                 default:
  1886.                     elem.toggleClass('active');
  1887.                     quiz.dialog.toggleClass('show_kanji');
  1888.                     quiz.showing_kanji = !quiz.showing_kanji;
  1889.                     if (quiz.showing_kanji) toggle_help('off');
  1890.                     break;
  1891.             }
  1892.         } else {
  1893.             return;
  1894.         }
  1895.  
  1896.     }
  1897.  
  1898.     //========================================================================
  1899.     // toggle_lightning()
  1900.     //------------------------------------------------------------------------
  1901.     function toggle_lightning() {
  1902.         var elem = $('#ss_quiz .settings .ss_lightning');
  1903.         elem.toggleClass('active');
  1904.         quiz.settings.lightning_mode = elem.hasClass('active');
  1905.         wkof.Settings.save('ss_quiz');
  1906.     }
  1907.  
  1908.     //========================================================================
  1909.     // toggle_pairing()
  1910.     //------------------------------------------------------------------------
  1911.     function toggle_pairing(e, initialize) {
  1912.         var elem_pair = $('#ss_quiz .settings .ss_pair');
  1913.         var elem_data = elem_pair.find('.data');
  1914.         var values = ['disabled', 'reading_first', 'meaning_first', 'random_order'];
  1915.         var value = Math.max(0, values.indexOf(quiz.settings.pairing));
  1916.  
  1917.         if (!initialize) value = (value + 1) % values.length;
  1918.         quiz.settings.pairing = value = values[value];
  1919.         wkof.Settings.save('ss_quiz')
  1920.  
  1921.         switch (value) {
  1922.             case 'disabled': elem_data.text('Disabled'); elem_pair.removeClass('active'); break;
  1923.             case 'reading_first': elem_data.text('Reading First'); elem_pair.addClass('active'); break;
  1924.             case 'meaning_first': elem_data.text('Meaning First'); elem_pair.addClass('active'); break;
  1925.             case 'random_order': elem_data.text('Random Order'); elem_pair.addClass('active'); break;
  1926.         }
  1927.         if (!initialize) quiz.start({keep_round_count:true});
  1928.     }
  1929.  
  1930.     //========================================================================
  1931.     // drag()
  1932.     //------------------------------------------------------------------------
  1933.     function drag(e) {
  1934.         var dlg = $(e.currentTarget).closest('.dialog');
  1935.         var pos = dlg.position();
  1936.         var ofs = {x: e.pageX-pos.left, y: e.pageY-pos.top};
  1937.         $('body')
  1938.         .on('mousemove.ss_quiz_drag touchmove.ss_quiz_drag', function(e){
  1939.             dlg.css({left: Math.max(0,e.pageX-ofs.x), top: Math.max(0,e.pageY-ofs.y)});
  1940.         })
  1941.         .on('mouseup.ss_quiz_drag touchend.ss_quiz_drag', function(e){
  1942.             $('body').off('.ss_quiz_drag');
  1943.         });
  1944.     }
  1945.  
  1946.     wkof.set_state('ss_quiz', 'ready');
  1947.  
  1948. })(window.ss_quiz);
Add Comment
Please, Sign In to add comment