// ==UserScript== // @name Wanikani Self-Study Quiz // @namespace rfindley // @description Quiz yourself on Wanikani items // @version 3.0.22 // @include https://www.wanikani.com/* // @exclude https://www.wanikani.com/review* // @exclude https://www.wanikani.com/lesson* // @require https://unpkg.com/wanakana // @copyright 2018+, Robin Findley // @license MIT; http://opensource.org/licenses/MIT // @run-at document-end // @grant none // @downloadURL none // ==/UserScript== window.ss_quiz = {}; (function(gobj) { /* global $, wkof, wanakana */ /* eslint no-multi-spaces: "off" */ //=================================================================== // Initialization of the Wanikani Open Framework. //------------------------------------------------------------------- var script_name = 'Self-Study Quiz'; var wkof_version_needed = '1.0.17'; if (!window.wkof) { if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?')) { window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549'; } return; } if (wkof.version.compare_to(wkof_version_needed) === 'older') { if (confirm(script_name+' requires Wanikani Open Framework version '+wkof_version_needed+'.\nDo you want to be forwarded to the update page?')) { window.location.href = 'https://greasyfork.org/en/scripts/38582-wanikani-open-framework'; } return; } wkof.include('Menu'); wkof.ready('Menu').then(install_menu); function install_menu() { wkof.Menu.insert_script_link({ name: 'selfstudyquiz', submenu: 'Open', title: 'Self-Study Quiz', on_click: open_quiz }); } //######################################################################## // QUIZ SETTINGS DIALOG //######################################################################## //======================================================================== // setup_quiz_settings() //------------------------------------------------------------------------ var quiz_settings_state = 'init'; function setup_quiz_settings() { if (quiz_settings_state === 'init') { quiz_settings_state = 'loading'; return wkof.ready('Settings') .then(function(){ quiz_settings_state = 'setup'; setup_quiz_settings(); }); } if (quiz_settings_state !== 'setup') return; var config = { script_id: 'ss_quiz', title: 'Self-Study Quiz', pre_open: preopen_quiz_settings, on_save: save_quiz_settings, on_close: close_quiz_settings, on_refresh: refresh_quiz_settings, no_bkgd: true, settings: { pg_questions: {type:'page',label:'Questions',hover_tip:'Choose what quiz questions you want to be asked',content:{ grp_qpre_list: {type:'group',label:'Presets List',content:{ active_qpreset: {type:'list',refresh_on_change:true,hover_tip:'Question Presets',content:{}}, }}, grp_qpre: {type:'group',label:'Selected Preset',content:{ sect_qpre_name: {type:'section',label:'Preset Name'}, 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'}, sect_qpre_question: {type:'section',label:'Questions Answers'}, char2mean: {type:'checkbox',label:'Rad/Kan/Voc 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'}, char2read: {type:'checkbox',label:'Kan/Voc 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'}, read2mean: {type:'checkbox',label:'Voc Reading Meaning',path:'@qpresets[@active_qpreset].content.read2mean',hover_tip:'Question: A kanji or vocab reading, in hiragana or katakana\nAnswer: The meaning in English'}, mean2read: {type:'checkbox',label:'Voc Meaning Reading',path:'@qpresets[@active_qpreset].content.mean2read',hover_tip:'Question: A vocab word in English\nAnswer: The Japanese reading, in hiragana or katakana'}, aud2mean: {type:'checkbox',label:'Voc Audio Meaning',path:'@qpresets[@active_qpreset].content.aud2mean',hover_tip:'Question: A vocab word, in spoken audio\nAnswer: The meaning in English'}, aud2read: {type:'checkbox',label:'Voc Audio Reading',path:'@qpresets[@active_qpreset].content.aud2read',hover_tip:'Question: A vocab word, in spoken audio\nAnswer: The Japanese reading, in hiragana or katakana'}, }}, }}, pg_items: {type:'page',label:'Items',hover_tip:'Choose what items you want to be quizzed on',content:{ grp_ipre_list: {type:'group',label:'Presets List',content:{ active_ipreset: {type:'list',refresh_on_change:true,hover_tip:'Item Presets',content:{}}, }}, grp_ipre: {type:'group',label:'Selected Preset',content:{ sect_ipre_name: {type:'section',label:'Preset Name'}, 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'}, sect_ipre_srcs: {type:'section',label:'Item Sources'}, ipre_srcs: {type:'tabset',content:{}}, }}, }}, pg_opts: {type:'page',label:'Settings',hover_tip:'Configure the user interface settings',content:{ grp_quiz_size: {type:'group',label:'Quiz Size',content:{ max_quiz_size: {type:'number',label:'Maximum Quiz Size',hover_tip:'Set the approximate maximum quiz size. (0 for unlimited)',default:0}, }}, grp_synonyms: {type:'group',label:'Synonyms',content:{ 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'}}, }}, grp_typos: {type:'group',label:'Typo Tolerance',content:{ allow_typos: {type:'checkbox',label:'Allow typos',hover_tip:'When enabled, English answers with minor typos will be accepted.',default:true}, }}, grp_help: {type:'group',label:'Wrong Answers',content:{ autoshow_correct: {type:'checkbox',label:'Auto-show Correct Answer',hover_tip:'Automatically show the correct answer\nwhen you answer incorrectly.',default:false}, }}, grp_msgs: {type:'group',label:'Warning Messages',content:{ 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}, 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}, }}, grp_halt: {type:'group',label:'Override Lightning',content:{ 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}, 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}, }}, }}, }, }; populate_items_config(config); quiz.settings_dialog = new wkof.Settings(config); quiz_settings_state = 'ready'; open_quiz_settings(); } //======================================================================== // preopen_quiz_settings() //------------------------------------------------------------------------ function preopen_quiz_settings(dialog) { var btn_grp = '
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
('+item.data.characters+')';
break;
}
var idx, idx2, reading;
qinfo.answer.other = [];
qinfo.answer.bad = [];
qinfo.answer.reading_type = '';
switch (qinfo.answer.type) {
case 'reading':
qinfo.answer.good = [];
qinfo.answer.lang = 'ja';
for (idx in item.data.readings) {
reading = item.data.readings[idx];
if (qinfo.item.type === 'vocabulary' || reading.accepted_answer) {
qinfo.answer.good.push(reading.reading);
if (qinfo.item.type === 'kanji') {
qinfo.answer.reading_type = reading.type.replace('yomi','\'yomi');
}
} else {
qinfo.answer.other.push(reading.reading);
}
qinfo.answer.bad = meanings;
}
break;
case 'meaning':
qinfo.answer.good = meanings;
qinfo.answer.lang = 'en';
if (!item.data.readings) break;
for (idx in item.data.readings) {
reading = item.data.readings[idx];
if (qinfo.item.type === 'vocabulary' || reading.accepted_answer) {
qinfo.answer.bad.push(reading.reading);
}
}
break;
}
}
//========================================================================
// get_user_answer()
//------------------------------------------------------------------------
function get_user_answer(index) {
var grp_idx = quiz.serial_list[index];
var group = quiz.group_list[grp_idx[0]];
if (!group.answer || !group.answer[grp_idx[1]]) return ['', ''];
return group.answer[grp_idx[1]];
}
//========================================================================
// set_user_answer()
//------------------------------------------------------------------------
function set_user_answer(index, status, answer) {
var grp_idx = quiz.serial_list[index];
var group = quiz.group_list[grp_idx[0]];
if (!group.answer) group.answer = [];
group.answer[grp_idx[1]] = [status, answer];
}
//========================================================================
// submit_answer()
//------------------------------------------------------------------------
function submit_answer() {
var dialog = quiz.dialog;
var input = $('#ss_quiz .answer input');
var qinfo = quiz.qinfo.load(quiz.index);
var item = qinfo.item.object;
var itype = qinfo.item.type;
var atype = qinfo.answer.type;
var raw_answer = input.val();
var answer = raw_answer;
var action = 'fail';
var msgcfg = quiz.settings.messages;
var is_exact = true;
var is_multi = false;
var message;
if (answer === '') {
atype = 'ignore';
action = 'shake';
}
switch (atype) {
case 'reading':
answer = wanakana.toHiragana(answer);
if (qinfo.answer.good.indexOf(answer) >= 0 || qinfo.answer.good.indexOf(raw_answer) >= 0) {
action = 'correct';
if (qinfo.answer.good.length > 1) is_multi = true;
if (is_multi && msgcfg.show_multi_reading) message = 'This item has multiple readings';
} else if (itype === 'kanji' && qinfo.answer.other.indexOf(answer) >= 0) {
action = 'shake';
message = 'We\'re looking for the '+to_title_case(qinfo.answer.reading_type)+' reading';
} else {
var bad = qinfo.answer.bad.map(function(english){
return wanakana.toHiragana(english.toLowerCase());
});
if (bad.indexOf(answer) >= 0) {
action = 'shake';
message = 'We\'re looking for the reading, not the meaning';
} else if (!wanakana.isKana(answer)) {
action = 'shake';
message = 'Your answer contains invalid characters';
} else {
action = 'incorrect';
}
}
break;
case 'meaning':
var is_correct = false;
is_exact = false;
answer = answer.toLowerCase();
var allow_typos = (quiz.settings.allow_typos === true);
for (var idx in qinfo.answer.good) {
var good_answer = qinfo.answer.good[idx];
if (answer === good_answer) {
is_correct = true;
is_exact = true;
break;
} else if (allow_typos && jw_distance(good_answer, answer) > 0.9) {
is_correct = true;
}
}
if (is_correct) {
action = 'correct';
if (!is_exact && msgcfg.show_slightly_off === true) message = "Your answer was slightly off";
} else {
var alt_answer = wanakana.toHiragana(answer,{IMEMode:true});
if (qinfo.answer.bad.indexOf(alt_answer) >= 0) {
action = 'shake';
message = 'We\'re looking for the meaning, not the reading';
} else {
action = 'incorrect';
}
}
break;
}
if (action !== 'shake') set_user_answer(quiz.index, action, answer);
switch (action) {
case 'correct':
quiz.stats.correct++;
// If question is reading, play audio now.
if (qinfo.answer.type === 'reading' && qinfo.question.type !== 'audio') {
play_audio(false /* force_play */, qinfo);
}
if ((quiz.settings.lightning_mode === true) &&
(!is_multi || !msgcfg.show_multi_reading || !msgcfg.halt_multi_reading) &&
(is_exact || !msgcfg.show_slightly_off || !msgcfg.halt_slightly_off )) {
return quiz.next();
} else {
update_quiz_stats();
input.prop('readonly', true);
}
dialog.attr('data-result', 'correct');
break;
case 'shake':
shake(input);
input.focus();
break;
case 'incorrect':
quiz.stats.incorrect++;
update_quiz_stats();
input.prop('readonly', true);
dialog.attr('data-result', 'incorrect');
if (quiz.settings.autoshow_correct && !quiz.showing_help) {
toggle_help('on');
}
break;
}
if (message) {
dialog.find('.message').text(message);
dialog.addClass('message');
if (quiz.message_timer) {
clearTimeout(quiz.message_timer);
delete quiz.message_timer;
}
quiz.message_timer = setTimeout(function(){
dialog.removeClass('message');
quiz.message_timer = undefined;
},2750);
}
}
//========================================================================
// shake()
//------------------------------------------------------------------------
function shake(elem) {
var dist = '15px';
var speed = 75;
var right = {padding:'0 '+dist+' 0 0'}, left = {padding:'0 0 0 '+dist}, center = {padding:"0 0 0 0"};
elem.animate(left,speed/2).animate(right,speed)
.animate(left,speed).animate(right,speed)
.animate(left,speed).animate(center,speed/2);
}
//========================================================================
// prev_question()
//------------------------------------------------------------------------
function prev_question() {
switch (quiz.mode) {
case 'question':
if (quiz.index > 0) quiz.index--;
quiz.ask();
break;
case 'summary':
if (quiz.index === quiz.stats.total) {
quiz.index = quiz.stats.total - 1;
update_quiz_stats();
}
set_mode('question');
break;
}
quiz.ask();
}
//========================================================================
// next_question()
//------------------------------------------------------------------------
function next_question() {
switch (quiz.mode) {
case 'question':
if (quiz.index < quiz.stats.total-1) {
quiz.index++;
quiz.ask();
} else {
quiz.index = quiz.stats.total;
update_quiz_stats();
set_mode('summary');
}
break;
case 'summary':
if (quiz.do_requiz) {
delete quiz.do_requiz;
if (!quiz.original_items) {
quiz.original_items = quiz.items;
}
quiz.items = quiz.requiz_items;
delete quiz.requiz_items;
} else {
delete quiz.requiz_items;
if (quiz.original_items) {
quiz.items = quiz.original_items;
delete quiz.original_items;
}
quiz.stats.round++;
}
quiz.start({keep_round_count:true});
break;
}
}
//========================================================================
// populate_errors()
//------------------------------------------------------------------------
function populate_errors() {
var dialog = quiz.dialog;
var percent_elem = dialog.find('.summary .percent');
var errors_elem = dialog.find('.summary .errors');
var total = quiz.stats.correct + quiz.stats.incorrect;
var percent = (total === 0 ? 100 : 100 * quiz.stats.correct / total);
percent_elem.text((Math.round(percent*100)/100).toString()+'%');
if (total === quiz.stats.correct) {
$('#ss_quiz .summary .requiz').addClass('hidden');
} else {
$('#ss_quiz .summary .requiz').removeClass('hidden');
}
var idx;
var err_list = dialog.find('.summary .errors');
err_list.html('');
var requiz_items = {};
quiz.requiz_items = [];
for (idx = 0; idx < quiz.stats.total; idx++) {
var grp_idx = quiz.serial_list[idx];
var group = quiz.group_list[grp_idx[0]];
if (!group.answer) continue;
var answer = group.answer[grp_idx[1]];
if (!answer || answer[0] !== 'incorrect') continue;
var item = group.item;
if (!requiz_items[item.id]) {
requiz_items[item.id] = 1;
quiz.requiz_items.push(item);
}
var itype = item.object;
var qnatype = group.qna[grp_idx[1]];
answer = answer[1];
var qtype = {
char2read:'characters', char2mean:'characters', mean2read:'meaning',
read2mean:'reading', aud2read:'audio', aud2mean:'audio'
}[qnatype];
var atype = {
char2read:'reading', char2mean:'meaning', mean2read:'reading',
read2mean:'meaning', aud2read:'reading', aud2mean:'meaning'
}[qnatype];
var qlang = (qtype === 'meaning' ? 'en' : 'ja');
var alang = (atype === 'meaning' ? 'en' : 'ja');
var qtitle = to_title_case(itype+' '+atype);
var atitle;
switch (atype) {
case 'meaning':
var synonyms = [];
try {synonyms = item.study_materials.meaning_synonyms || [];} catch(e) {}
var meanings = item.data.meanings.map(meaning => meaning.meaning);
if (quiz.settings.synonyms_order === 'first') {
meanings = synonyms.concat(meanings).map(meaning => meaning.toLowerCase());
} else {
meanings = meanings.concat(synonyms).map(meaning => meaning.toLowerCase());
}
atitle = meanings.join(', ');
break;
case 'reading':
atitle = to_title_case(item.data.readings.map(reading => reading.reading).join(', '));
break;
}
var qtext = item.data.slug;
if (qtype === 'audio') qtext += ' ';
var atext = answer + ' ';
err_list.append(
'
'+
'Remaining: '+(' '+remaining).slice(-1*stats_width)+'
'+
'Correct: '+(' '+quiz.stats.correct).slice(-1*stats_width)+'
'+
'Incorrect: '+(' '+quiz.stats.incorrect).slice(-1*stats_width)
);
}
//========================================================================
// quiz_key_handler()
//------------------------------------------------------------------------
var keycode_xlat = {
'8':'Backspace', '13':'Enter', '27':'Escape', '37':'ArrowLeft', '39':'ArrowRight', '65':'KeyA',
'69':'KeyE', '72':'KeyH', '76':'KeyL', '80':'KeyP', '82':'KeyR', '83':'KeyS', '112':'F1',
};
function quiz_key_handler(e) {
if (quiz_settings_state === 'open') return true;
var input = quiz.dialog.find('.answer input');
var input_readonly = input.prop('readonly');
var code;
if (e.type === 'keydown') {
if (e.originalEvent.keyCode) {
code = keycode_xlat[e.originalEvent.keyCode] || 'Unknown';
} else {
code = e.originalEvent.code;
}
} else {
code = String.fromCharCode(e.charCode);
}
if (code === 'Enter') {
if (quiz.mode === 'question' && !input_readonly) {
quiz.submit(e);
} else {
quiz.next();
}
} else if (code === 'Escape') {
process_escape();
} else if (code === 'F1' || code === '?') {
toggle_help();
} else if (code === 'Backspace') {
// Prevent backspace from navigating away from the page.
if (quiz.mode !== 'question') return false;
if (input_readonly) quiz.ask(true /* erase_old_answer */);
return true;
} else if (e.ctrlKey || e.metaKey) {
switch (code) {
case 'KeyA':
if (e.shiftKey) {
toggle_audio();
} else {
play_audio(true);
}
break;
case 'KeyE': process_escape(); break; // End
case 'KeyH': toggle_help(); break; // Help
case 'KeyL': toggle_lightning(); break; // Lightning
case 'KeyP': toggle_pairing(); break; // Pairing
case 'KeyR': // Re-quiz
if (quiz.mode !== 'summary' || quiz.dialog.find('.summary .requiz').hasClass('hidden')) break;
quiz.requiz();
break;
case 'KeyS': manual_shuffle(); break;
case 'ArrowLeft': quiz.prev(); break;
case 'ArrowRight': quiz.next(); break;
default: return true;
}
} else {
var is_special = (e.key.length !== 1);
if (is_special) return true;
// Let the browser handle regular keys in the input box
if (e.target === input[0]) return true;
// Let the browser handle all other keys while not in question mode.
if (quiz.mode !== 'question') return true;
}
return false;
}
//========================================================================
// manual_shuffle()
//------------------------------------------------------------------------
function manual_shuffle() {
var keep_round_count = true;
if (quiz.shuffle_timer === undefined) {
quiz.shuffle_timer = setTimeout(function(){
delete quiz.shuffle_timer;
}, 1000);
} else {
clearTimeout(quiz.shuffle_timer);
delete quiz.shuffle_timer;
keep_round_count = false;
}
quiz.start({keep_round_count:keep_round_count});
}
//========================================================================
// process_escape()
//------------------------------------------------------------------------
function process_escape() {
if (quiz.escape_timer === undefined) {
quiz.escape_counter = 1;
quiz.escape_timer = setTimeout(function(){
delete quiz.escape_counter;
delete quiz.escape_timer;
}, 750);
} else {
quiz.escape_counter++;
if (quiz.escape_counter === 3) {
clearTimeout(quiz.escape_timer);
delete quiz.escape_timer;
quiz.close();
return;
}
}
switch (quiz.mode) {
case 'question':
set_mode('summary');
break;
case 'summary':
if (quiz.index === quiz.stats.total) quiz.index = quiz.stats.total-1;
set_mode('previous');
break;
}
}
//========================================================================
// toggle_audio()
//------------------------------------------------------------------------
function toggle_audio() {
var elem = $('#ss_quiz .settings .ss_audio');
if (quiz.settings.mute_audio) {
quiz.settings.mute_audio = false;
quiz.settings.play_audio = false;
elem.removeClass('mute');
elem.removeClass('active');
} else if (quiz.settings.play_audio) {
quiz.settings.mute_audio = true;
quiz.settings.play_audio = false;
elem.addClass('mute');
elem.removeClass('active');
} else {
quiz.settings.mute_audio = false;
quiz.settings.play_audio = true;
elem.removeClass('mute');
elem.addClass('active');
}
wkof.Settings.save('ss_quiz');
}
//========================================================================
// toggle_help()
//------------------------------------------------------------------------
function toggle_help(value) {
if (quiz.mode !== 'question') return;
var elem = $('#ss_quiz .settings .ss_help');
switch (value) {
case 'on':
elem.addClass('active');
quiz.dialog.addClass('help');
quiz.showing_help = true;
break;
case 'off':
elem.removeClass('active');
quiz.dialog.removeClass('help');
quiz.showing_help = false;
break;
default:
elem.toggleClass('active');
quiz.dialog.toggleClass('help');
quiz.showing_help = !quiz.showing_help;
break;
}
var qinfo = quiz.qinfo.load(quiz.index);
if (quiz.showing_help && qinfo.answer.type === 'reading') play_audio(false /* force_play */);
}
//========================================================================
// toggle_lightning()
//------------------------------------------------------------------------
function toggle_lightning() {
var elem = $('#ss_quiz .settings .ss_lightning');
elem.toggleClass('active');
quiz.settings.lightning_mode = elem.hasClass('active');
wkof.Settings.save('ss_quiz');
}
//========================================================================
// toggle_pairing()
//------------------------------------------------------------------------
function toggle_pairing(e, initialize) {
var elem_pair = $('#ss_quiz .settings .ss_pair');
var elem_data = elem_pair.find('.data');
var values = ['disabled', 'reading_first', 'meaning_first', 'random_order'];
var value = Math.max(0, values.indexOf(quiz.settings.pairing));
if (!initialize) value = (value + 1) % values.length;
quiz.settings.pairing = value = values[value];
wkof.Settings.save('ss_quiz')
switch (value) {
case 'disabled': elem_data.text('Disabled'); elem_pair.removeClass('active'); break;
case 'reading_first': elem_data.text('Reading First'); elem_pair.addClass('active'); break;
case 'meaning_first': elem_data.text('Meaning First'); elem_pair.addClass('active'); break;
case 'random_order': elem_data.text('Random Order'); elem_pair.addClass('active'); break;
}
if (!initialize) quiz.start({keep_round_count:true});
}
//========================================================================
// drag()
//------------------------------------------------------------------------
function drag(e) {
var dlg = $(e.currentTarget).closest('.dialog');
var pos = dlg.position();
var ofs = {x: e.pageX-pos.left, y: e.pageY-pos.top};
$('body')
.on('mousemove.ss_quiz_drag touchmove.ss_quiz_drag', function(e){
dlg.css({left: Math.max(0,e.pageX-ofs.x), top: Math.max(0,e.pageY-ofs.y)});
})
.on('mouseup.ss_quiz_drag touchend.ss_quiz_drag', function(e){
$('body').off('.ss_quiz_drag');
});
}
wkof.set_state('ss_quiz', 'ready');
})(window.ss_quiz);