// ==UserScript==
// @name Wanikani Double-Check
// @namespace wkdoublecheck
// @description Allows retyping typo'd answers, or marking wrong when WK's typo tolerance is too lax.
// @match https://www.wanikani.com/extra_study/session*
// @match https://www.wanikani.com/review/session*
// @match https://preview.wanikani.com/extra_study/session*
// @match https://preview.wanikani.com/review/session*
// @version 2.3.3
// @author Robin Findley
// @copyright 2017+, Robin Findley
// @license MIT; http://opensource.org/licenses/MIT
// @run-at document-end
// @grant none
// ==/UserScript==
// CREDITS: This is a replacement for an original script by Wanikani user @Ethan.
// Ethan's script stopped working due to some Wanikani changes. The code below is
// 100% my own, but it closely replicates the functionality of Ethan's original script.
// HOTKEYS:
// "+" - Marks answer as 'correct'.
// "-" - Marks answer as 'incorrect'.
// "Escape" or "Backspace" - Resets question, allowing you to retype.
// SEE SETTINGS BELOW.
window.doublecheck = {};
(function(gobj) {
/* global wkof, additionalContent, lastItems, Srs, wanakana, WaniKani */
var settings;
wkof.include('Menu,Settings');
wkof.ready('document,Menu,Settings').then(setup);
//------------------------------------------------------------------------
// setup() - Set up the menu link and default settings.
//------------------------------------------------------------------------
function setup() {
wkof.Menu.insert_script_link({name:'doublecheck',submenu:'Settings',title:'Double-Check',on_click:open_settings});
var defaults = {
allow_retyping: true,
allow_change_correct: false,
show_corrected_answer: false,
allow_change_incorrect: false,
typo_action: 'ignore',
wrong_answer_type_action: 'warn',
wrong_number_n_action: 'warn',
small_kana_action: 'warn',
kanji_reading_for_vocab_action: 'warn',
kanji_meaning_for_vocab_action: 'warn',
delay_wrong: true,
delay_multi_meaning: false,
delay_slightly_off: false,
delay_period: 1.5,
warn_burn: 'never',
burn_delay_period: 1.5,
show_lightning_button: true,
lightning_enabled: false,
srs_msg_period: 1.2,
autoinfo_correct: false,
autoinfo_incorrect: false,
autoinfo_multi_meaning: false,
autoinfo_slightly_off: false
}
return wkof.Settings.load('doublecheck', defaults)
.then(init_ui.bind(null, true /* first_time */));
}
//------------------------------------------------------------------------
// open_settings() - Open the Settings dialog.
//------------------------------------------------------------------------
function open_settings() {
var dialog = new wkof.Settings({
script_id: 'doublecheck',
title: 'Double-Check Settings',
on_save: init_ui,
pre_open: settings_preopen,
content: {
tabAnswers: {type:'page',label:'Answers',content:{
grpChangeAnswers: {type:'group',label:'Change Answer',content:{
allow_retyping: {type:'checkbox',label:'Allow retyping answer',default:true,hover_tip:'When enabled, you can retype your answer by pressing Escape or Backspace.'},
allow_change_incorrect: {type:'checkbox',label:'Allow changing to "incorrect"',default:true,hover_tip:'When enabled, you can change your answer\nto "incorrect" by pressing the "-" key.'},
allow_change_correct: {type:'checkbox',label:'Allow changing to "correct"',default:true,hover_tip:'When enabled, you can change your answer\nto "correct" by pressing the "+" key.'},
show_corrected_answer: {type:'checkbox',label:'Show corrected answer',default:false,hover_tip:'When enabled, pressing \'+\' to correct your answer puts the\ncorrected answer in the input field. Pressing \'+\' multiple\ntimes cycles through all acceptable answers.'},
}},
grpCarelessMistakes: {type:'group',label:'Careless Mistakes',content:{
typo_action: {type:'dropdown',label:'Typos in meaning',default:'ignore',content:{ignore:'Ignore',warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when meaning contains typos.'},
wrong_answer_type_action: {type:'dropdown',label:'Wrong answer type',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when reading was entered instead of meaning, or vice versa.'},
wrong_number_n_action: {type:'dropdown',label:'Wrong number of n\'s',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when you type the wrong number of n\'s in certain reading questions.'},
small_kana_action: {type:'dropdown',label:'Big kana instead of small',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when you type a big kana instead of small (e.g. ゆ instead of ゅ).'},
kanji_reading_for_vocab_action: {type:'dropdown',label:'Kanji reading instead of vocab',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when the reading of a kanji is entered for a single character vocab word instead of the correct vocab reading.'},
kanji_meaning_for_vocab_action: {type:'dropdown',label:'Kanji meaning instead of vocab',default:'warn',content:{warn:'Warn/shake',wrong:'Mark wrong'},hover_tip:'Choose an action to take when the meaning of a kanji is entered for a single character vocab word instead of the correct vocab meaning.'},
}},
}},
tabMistakeDelay: {type:'page',label:'Mistake Delay',content:{
grpDelay: {type:'group',label:'Delay Next Question',content:{
delay_wrong: {type:'checkbox',label:'Delay when wrong',default:true,refresh_on_change:true,hover_tip:'If your answer is wrong, you cannot advance\nto the next question for at least N seconds.'},
delay_multi_meaning: {type:'checkbox',label:'Delay when multiple meanings',default:false,hover_tip:'If the item has multiple meanings, you cannot advance\nto the next question for at least N seconds.'},
delay_slightly_off: {type:'checkbox',label:'Delay when answer has typos',default:false,hover_tip:'If your answer contains typos, you cannot advance\nto the next question for at least N seconds.'},
delay_period: {type:'number',label:'Delay period (in seconds)',default:1.5,hover_tip:'Number of seconds to delay before allowing\nyou to advance to the next question.'},
}},
}},
tabBurnReviews: {type:'page',label:'Burn Reviews',content:{
grpBurnReviews: {type:'group',label:'Burn Reviews',content:{
warn_burn: {type:'dropdown',label:'Warn before burning',default:'never',content:{never:'Never',cheated:'If you changed answer',always:'Always'},hover_tip:'Choose when to warn before burning an item.'},
burn_delay_period: {type:'number',label:'Delay after warning (in seconds)',default:1.5,hover_tip:'Number of seconds to delay before allowing\nyou to advance to the next question after seeing a burn warning.'},
}},
}},
tabLightning: {type:'page',label:'Lightning',content:{
grpLightning: {type:'group',label:'Lightning Mode',content:{
show_lightning_button: {type:'checkbox',label:'Show "Lightning Mode" button',default:true,hover_tip:'Show the "Lightning Mode" toggle\nbutton on the review screen.'},
lightning_enabled: {type:'checkbox',label:'Enable "Lightning Mode"',default:true,refresh_on_change:true,hover_tip:'Enable "Lightning Mode", which automatically advances to\nthe next question if you answer correctly.'},
srs_msg_period: {type:'number',label:'SRS popup time (in seconds)',default:1.2,min:0,hover_tip:'How long to show SRS up/down popup when in lightning mode. (0 = don\'t show)'},
}},
}},
tabAutoInfo: {type:'page',label:'Item Info',content:{
grpAutoInfo: {type:'group',label:'Show Item Info',content:{
autoinfo_correct: {type:'checkbox',label:'After correct answer',default:false,hover_tip:'Automatically show the Item Info after correct answers.', validate:validate_autoinfo_correct},
autoinfo_incorrect: {type:'checkbox',label:'After incorrect answer',default:false,hover_tip:'Automatically show the Item Info after incorrect answers.', validate:validate_autoinfo_incorrect},
autoinfo_multi_meaning: {type:'checkbox',label:'When multiple meanings',default:false,hover_tip:'Automatically show the Item Info when an item has multiple meanings.', validate:validate_autoinfo_correct},
autoinfo_slightly_off: {type:'checkbox',label:'When answer has typos',default:false,hover_tip:'Automatically show the Item Info when your answer has typos.', validate:validate_autoinfo_correct},
}},
}},
}
});
dialog.open();
}
//------------------------------------------------------------------------
// validate_autoinfo_correct() - Notify user if iteminfo and lightning are both enabled.
//------------------------------------------------------------------------
function validate_autoinfo_correct(enabled) {
if (enabled && settings.lightning_enabled) {
return 'Disable "Lightning Mode"!';
}
}
//------------------------------------------------------------------------
// validate_autoinfo_incorrect() - Notify user if iteminfo and lightning are both enabled, and wrong_delay disabled.
//------------------------------------------------------------------------
function validate_autoinfo_incorrect(enabled) {
if (enabled && settings.lightning_enabled && !settings.delay_wrong) {
return 'Disable "Lightning Mode", or<br>enable "Delay when wrong"!';
}
}
//------------------------------------------------------------------------
// settings_preopen() - Notify user if iteminfo and lightning are both enabled.
//------------------------------------------------------------------------
function settings_preopen(dialog) {
dialog.dialog({width:525});
}
//------------------------------------------------------------------------
// init_ui() - Initialize the user interface.
//------------------------------------------------------------------------
var first_time = true;
function init_ui() {
settings = wkof.settings.doublecheck;
if (first_time) {
first_time = false;
startup();
}
// Migrate 'lightning' setting from localStorage.
var lightning = localStorage.getItem('lightning');
if (lightning === 'false' || lightning === 'true') {
localStorage.removeItem('lightning');
settings.lightning_enabled = lightning;
wkof.Settings.save('doublecheck');
}
// Initialize the Lightning Mode button.
document.querySelector('#lightning-mode').classList.toggle('doublecheck-active', settings.lightning_enabled);
document.querySelector('#lightning-mode').hidden = !settings.show_lightning_button;
document.querySelector('#option-double-check').classList.toggle('hidden', !(settings.allow_change_correct || settings.allow_change_incorrect));
document.querySelector('#option-retype').classList.toggle('hidden', !settings.allow_retyping);
resize_buttons();
if (state === 'second_submit') {
document.querySelector('#option-double-check').classList.toggle('disabled', !(
(new_answer.passed && (settings.allow_change_incorrect || !first_answer.passed)) ||
(!new_answer.passed && (settings.allow_change_correct || first_answer.passed))
));
document.querySelector('#option-retype').classList.toggle('disabled', !settings.allow_retyping);
} else {
document.querySelector('#option-double-check').classList.add('disabled');
}
}
var old_submit_handler, old_answer_checker, ignore_submit = false, state = 'first_submit', show_srs, srs_load, delay_timer;
var item, itype, item_id, item_status, qtype, valid_answers, wrong_cnt, question_cnt, completed_cnt, answer, new_answer, active_queue;
var last_item_id, last_qtype, first_answer;
function promise(){var a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}
//------------------------------------------------------------------------
// lightning_clicked() - Lightning button handler.
//------------------------------------------------------------------------
function lightning_clicked() {
settings.lightning_enabled = !settings.lightning_enabled;
wkof.Settings.save('doublecheck');
document.querySelector('#lightning-mode').classList.toggle('doublecheck-active', settings.lightning_enabled);
return false;
}
//------------------------------------------------------------------------
// get_correct_answers() - Returns an array of acceptable answers.
//------------------------------------------------------------------------
function get_correct_answers() {
if (qtype === 'reading') {
if (itype === 'k') {
switch (item.emph) {
case "onyomi": return item.on;
case "kunyomi": return item.kun;
case "nanori": return item.nanori;
}
} else {
return item.kana;
}
} else {
return [].concat(item.syn,item.en);
}
}
//------------------------------------------------------------------------
// get_next_correct_answer() - Returns the next acceptable answer from the
// array returned by get_correct_answers().
//------------------------------------------------------------------------
function get_next_correct_answer() {
var result = first_answer.correct_answers[first_answer.correct_answer_index];
first_answer.correct_answer_index = (first_answer.correct_answer_index + 1) % first_answer.correct_answers.length;
return result;
}
//------------------------------------------------------------------------
// toggle_result() - Toggle an answer from right->wrong or wrong->right.
//------------------------------------------------------------------------
function toggle_result(new_state) {
if (new_state === 'toggle') new_state = (new_answer.passed ? 'incorrect' : 'correct');
if (state !== 'second_submit') return false;
var input = document.querySelector('#answer-form fieldset input');
var current_response = input.value;
clear_delay();
switch (new_state) {
case 'correct':
if (!(settings.allow_change_correct || first_answer.passed)) return false;
if (first_answer.passed) {
input.value = first_answer.response;
} else {
input.value = get_next_correct_answer();
}
new_answer = {passed:true, accurate:true, multipleAnswers:false, exception:false};
set_answer_state(new_answer, false /* show_msgs */);
if (!settings.show_corrected_answer) input.value = current_response;
break;
case 'incorrect':
if (!(new_answer.passed && (settings.allow_change_incorrect || !first_answer.passed))) return false;
if (first_answer.passed) {
input.value = 'xxxxxx';
} else {
input.value = first_answer.response;
}
new_answer = {passed:false, accurate:false, multipleAnswers:false, exception:false};
set_answer_state(new_answer, false /* show_msgs */);
if (!settings.show_corrected_answer) input.value = current_response;
break;
case 'retype':
if (!settings.allow_retyping) return false;
set_answer_state({reset:true, due_to_retype:true});
break;
}
}
//------------------------------------------------------------------------
// do_delay() - Disable the submit button briefly to prevent clicking past wrong answers.
//------------------------------------------------------------------------
function do_delay(period) {
if (period === undefined) period = settings.delay_period;
ignore_submit = true;
delay_timer = setTimeout(function() {
delay_timer = -1;
ignore_submit = false;
}, period*1000);
}
//------------------------------------------------------------------------
// clear_delay() - Clear the delay timer.
//------------------------------------------------------------------------
function clear_delay() {
if (delay_timer) {
ignore_submit = false;
clearTimeout(delay_timer);
delay_timer = undefined;
}
}
//------------------------------------------------------------------------
// return_new_answer() - Alternate answer checker that overrides our results.
//------------------------------------------------------------------------
function return_new_answer() {
return new_answer;
}
//------------------------------------------------------------------------
// set_answer_state() - Update the screen to show results of answer-check.
//------------------------------------------------------------------------
function set_answer_state(answer, show_msgs) {
// If user requested to retype answer, reset the question.
var dblchk = document.querySelector('#option-double-check');
if (answer.reset) {
clear_delay();
if (state === 'second_submit') {
$.jStorage.set('wrongCount', wrong_cnt);
$.jStorage.set('questionCount', question_cnt);
$.jStorage.set('completedCount', completed_cnt);
$.jStorage.set('activeQueue', active_queue);
}
state = 'first_submit';
// If we are resetting due to the user clicking 'retype', then we need to trigger
// a refresh the input field and stats by updating 'currentItem' in jStorage.
if (answer.due_to_retype) {
$.jStorage.set('currentItem', $.jStorage.get('currentItem'));
return
}
window.wkRefreshAudio();
try {document.querySelector("#answer-exception").remove();} catch(e) {}
dblchk.classList.add('disabled');
dblchk.querySelector('span').setAttribute('title','Mark Right');
dblchk.querySelector('span i').className = 'fa fa-thumbs-up';
document.querySelector('#option-retype').classList.add('disabled');
if (typeof Srs === 'object') Srs.remove();
return;
}
// If answer is invalid for some reason, do the shake thing.
var input = document.querySelector('#user-response');
var fieldset = document.querySelector('#answer-form fieldset');
if (answer.exception) {
try {document.querySelector('#answer-exception').remove();} catch(e) {}
if (answer.confirming_burn) {
// NOTE: We can only reach this branch if the current answer is correct, otherwise we wouldn't be burning it.
dblchk.querySelector('span').setAttribute('title','Mark Wrong')
dblchk.querySelector('span i').className = 'fa fa-thumbs-down';
dblchk.classList.toggle('disabled', !(settings.allow_change_incorrect || !first_answer.passed));
fieldset.classList.remove('incorrect','correct');
fieldset.classList.add('confburn');
document.querySelector('#additional-content').insertAdjacentHTML('beforeend','<div id="answer-exception"><span>'+answer.exception+'</span></div>');
document.querySelector('#answer-exception').classList.add('animated','fadeInUp');
return;
}
if (!$("#answer-form form").is(":animated")) {
document.querySelector('#reviews').style.overflowX = 'hidden';
$('#answer-form form').effect('shake', {}, 300, function() {
document.querySelector('#reviews').style.overflowX = 'visible';
if (!answer.accurate && input.value !== '') {
if (typeof answer.exception === 'string') {
document.querySelector('#answer-form form').insertAdjacentHTML('beforeend','<div id="answer-exception" class="answer-exception-form"><span>' + answer.exception + '</span></div>');
document.querySelector('#answer-exception').classList.add('animated','fadeInUp');
}
}
}).find("input").focus();
}
return;
}
document.querySelector('#answer-form form input').blur();
// Draw 'correct' or 'incorrect' results, enable Double-Check button, and calculate updated statistics.
try {document.querySelector('#answer-exception').classList.add('animated','fadeInUp');} catch(e) {}
var new_status = Object.assign({},item_status);
var retype = document.querySelector('#option-retype');
retype.classList.toggle('disabled', !settings.allow_retyping);
if (answer.passed) {
fieldset.classList.remove('incorrect','confburn');
fieldset.classList.add('correct');
dblchk.querySelector('span').setAttribute('title','Mark Wrong');
dblchk.querySelector('span i').className = 'fa fa-thumbs-down';
dblchk.classList.toggle('disabled', !(settings.allow_change_incorrect || !first_answer.passed));
if (qtype === 'meaning') {
new_status.mc = (new_status.mc || 0) + 1;
} else {
new_status.rc = (new_status.rc || 0) + 1;
if (input.value.slice(-1) === 'n') input.value = input.value.slice(0,-1)+'ん';
}
$.jStorage.set('wrongCount', wrong_cnt);
} else {
fieldset.classList.remove('correct','confburn');
fieldset.classList.add('incorrect');
dblchk.querySelector('span').setAttribute('title','Mark Right');
dblchk.querySelector('span i').className = 'fa fa-thumbs-up';
dblchk.classList.toggle('disabled', !(settings.allow_change_correct || first_answer.passed));
$.jStorage.set('wrongCount', wrong_cnt + 1);
}
$.jStorage.set('questionCount', question_cnt + 1);
if (((itype === 'r') || ((new_status.rc || 0) >= 1)) && ((new_status.mc || 0) >= 1)) {
if (show_srs) {
if (settings.lightning_enabled) {
if (settings.srs_msg_period > 0) {
var status = Object.assign({},new_status);
var srs = item.srs;
if (typeof Srs === 'object') {
setTimeout(Srs.load.bind(Srs, status, srs), 100);
setTimeout(Srs.remove, settings.srs_msg_period * 1000);
}
}
} else {
if (typeof Srs === 'object') {
Srs.remove();
Srs.load(new_status,item.srs);
}
}
}
$.jStorage.set('completedCount', completed_cnt + 1);
$.jStorage.set('activeQueue', active_queue.slice(1));
} else {
$.jStorage.set('completedCount', completed_cnt);
$.jStorage.set('activeQueue', active_queue);
}
document.querySelector("#user-response").disabled = true;
window.wkRefreshAudio();
additionalContent.enableButtons();
if (typeof lastItems === 'object') lastItems.disableSessionStats();
try {document.querySelector("#answer-exception").remove();} catch(e) {}
// Open item info, depending on settings.
var showing_info = false;
if (answer.passed && !settings.lightning_enabled &&
(settings.autoinfo_correct ||
(settings.autoinfo_slightly_off && !answer.accurate) ||
(settings.autoinfo_multi_meaning && answer.multipleAnswers)
)) {
showing_info = true;
document.querySelector('#option-item-info').click();
} else if (!answer.passed && !(settings.lightning_enabled && !settings.delay_wrong) && settings.autoinfo_incorrect) {
showing_info = true;
document.querySelector('#option-item-info').click();
}
// When user is submitting an answer, display the on-screen message that Wanikani normally shows.
if (show_msgs) {
var msg;
if (answer.passed) {
if (!answer.accurate) {
msg = 'Your answer was a bit off. Check the '+qtype+' to make sure you are correct';
} else if (answer.multipleAnswers) {
msg = 'Did you know this item has multiple possible '+qtype+'s?';
}
} else if (answer.custom_msg) {
msg = answer.custom_msg;
} else {
msg = 'Need help? View the correct '+qtype+' and mnemonic';
}
if (msg) {
if (showing_info) {
document.querySelector('#information').insertAdjacentHTML('afterbegin','<div id="answer-exception" style="top:0;"><span>'+msg+'</span></div>');
document.querySelector('#answer-exception').classList.add('animated','fadeInUp');
} else {
document.querySelector('#additional-content').insertAdjacentHTML('beforeend','<div id="answer-exception"><span>'+msg+'</span></div>');
document.querySelector('#answer-exception').classList.add('animated','fadeInUp');
}
let item_info_btn = document.querySelector('#option-item-info');
let iipos = item_info_btn.offsetLeft + item_info_btn.offsetWidth/2;
let answer_exception = document.querySelector('#answer-exception>span');
answer_exception.style.transform = '';
let aepos = answer_exception.offsetLeft + answer_exception.offsetWidth/2;
answer_exception.style.transform = 'translateX('+(iipos-aepos)+'px)';
}
}
}
//------------------------------------------------------------------------
// new_submit_handler() - Intercept handler for 'submit' button. Overrides default behavior as needed.
//------------------------------------------------------------------------
function new_submit_handler(e) {
// Don't process 'submit' if we are ignoring temporarily (to prevent double-tapping past important info)
if (ignore_submit) {
// If the user presses <enter> during delay period,
// WK enables the user input field, which makes Item Info not work.
// Let's make sure the input field is disabled.
setTimeout(function(){
document.querySelector("#user-response").disabled = true;
},1);
return false;
}
var submitted_immediately = false;
switch(state) {
case 'first_submit':
// We intercept the first 'submit' click, and simulate normal Wanikani screen behavior.
state = 'second_submit';
// Capture the state of the system before submitting the answer.
item = $.jStorage.get('currentItem');
itype = (item.rad ? 'r' : (item.kan ? 'k' : 'v'));
item_id = itype + item.id;
item_status = $.jStorage.get(item_id) || {};
qtype = $.jStorage.get('questionType');
wrong_cnt = $.jStorage.get('wrongCount') || 0;
question_cnt = $.jStorage.get('questionCount') || 0;
completed_cnt = $.jStorage.get('completedCount') || 0;
active_queue = $.jStorage.get('activeQueue') || [];
show_srs = $.jStorage.get('r/srsIndicator');
// Ask Wanikani if the answer is right (but we don't actually submit the answer).
answer = old_answer_checker(qtype, document.querySelector("#user-response").value);
// Update the screen to reflect the results of our checked answer.
$("html, body").animate({scrollTop: 0}, 200);
// Check if [meaning has kana] or [reading has latin]
var text = document.querySelector('#user-response').value;
if ((qtype === 'reading' && window.answerChecker.isNonKanaPresent(text)) ||
(qtype === 'meaning' && window.answerChecker.isKanaPresent(text)) ||
(text === '')) {
answer.exception = answer.exception || true;
}
// Non-exact answer (i.e. "Close but no cigar" script)
if (answer.passed && !answer.accurate) {
switch (settings.typo_action) {
case 'warn': answer.exception = 'Your answer was close, but not exact'; break;
case 'wrong': answer.passed = false; answer.custom_msg = 'Your answer was not exact, as required by your settings.'; break;
}
}
// Check for reading/meaning mixups
if (!answer.passed) {
if (qtype === 'meaning') {
var accepted_readings = [].concat(item.kana, item.on, item.kun, item.nanori);
var answer_as_kana = to_kana(document.querySelector('#user-response').value);
if (accepted_readings.indexOf(answer_as_kana) >= 0) {
if (settings.wrong_answer_type_action === 'warn') {
answer.exception = 'Oops, we want the meaning, not the reading.';
} else {
answer.exception = false;
}
}
} else {
// Although Wanikani now checks for readings entered as meanings, it only
// checks the 'preferred' reading. Here, we check all readings.
var accepted_meanings = item.en;
try {
accepted_meanings = accepted_meanings.concat(item.syn, item.auxiliary_meanings
.filter((meaning) => meaning.type === 'whitelist')
.map((meaning) => meaning.meaning));
} catch(e) {}
var meanings_as_hiragana = accepted_meanings.map(m => to_kana(m.toLowerCase()).replace(/\s/g,''));
var answer_as_hiragana = Array.from(document.querySelector('#user-response').value.toLowerCase()).map(c => wanakana.toHiragana(c)).join('');
if (meanings_as_hiragana.indexOf(answer_as_hiragana) >= 0) {
if (settings.wrong_answer_type_action === 'warn') {
answer.exception = 'Oops, we want the reading, not the meaning.';
} else {
answer.exception = false;
}
}
}
}
// Check for Wanikani warnings that should be changed to 'wrong', based on settings.
if (typeof answer.exception === 'string') {
if (((settings.kanji_meaning_for_vocab_action === 'wrong') && answer.exception.toLowerCase().includes('want the vocabulary meaning, not the kanji meaning')) ||
((settings.kanji_reading_for_vocab_action === 'wrong') && answer.exception.toLowerCase().includes('want the vocabulary reading, not the kanji reading')) ||
((settings.wrong_number_n_action === 'wrong') && answer.exception.toLowerCase().includes('forget that ん')) ||
((settings.small_kana_action === 'wrong') && answer.exception.toLowerCase().includes('watch out for the small')))
{
answer.exception = false;
answer.passed = false;
}
}
// Copy the modified answer to new_answer, which is what will be submitted to Wanikani.
new_answer = Object.assign({}, answer);
// Check for exceptions that are preventing the answer from being submitted.
if (answer.exception) {
set_answer_state(answer, true /* show_msgs */);
state = 'first_submit';
return false;
}
// At this point, the answer is ready for submission (i.e. no exceptions).
// If this is the user's first attempt at this question, remember the result so
// we can determine whether they altered their answer later.
if (!((item_id === last_item_id) && (qtype === last_qtype))) {
first_answer = Object.assign({
response:document.querySelector("#user-response").value,
correct_answers:get_correct_answers(),
correct_answer_index: 0,
}, answer);
}
last_item_id = item_id;
last_qtype = qtype;
// Optionally (according to settings), temporarily ignore any additional clicks on the
// 'submit' button to prevent the user from clicking past important info about the answer.
if ((!answer.passed && settings.delay_wrong) ||
(answer.passed &&
((!answer.accurate && settings.delay_slightly_off) ||
(answer.multipleAnswers && settings.delay_multi_meaning))
)
)
{
set_answer_state(answer, true /* show_msgs */);
do_delay();
return false;
}
set_answer_state(answer, true /* show_msgs */);
if (settings.lightning_enabled) {
new_submit_handler(e);
}
return false;
case 'second_submit':
// If the user changed their answer to 'correct', mark the item
// in storage, so we can warn the user if it comes up for burn.
// The mark is kept for 10 days in case the user doesn't complete
// the item (reading and meaning) within one session.
if (!first_answer.passed && new_answer.passed) {
$.jStorage.set('confburn/' + item.id, true, {TTL:1000*3600*24*10});
}
// Before accepting a final submit, notify the user if item will burn (depending on settings).
new_answer.exception = false;
if (!new_answer.confirming_burn) {
// Check if we need to warn the user that this is a 'burn' review.
// NOTE: "item_status.ni" seems to be used by other scripts.
var will_burn = (item.srs === 8) && new_answer.passed &&
!(item_status.mi || item_status.ri || item_status.ni) &&
((itype === 'r') ||
(((item_status.rc || 0) + (qtype === 'reading' ? 1 : 0) > 0) &&
((item_status.mc || 0) + (qtype === 'meaning' ? 1 : 0) > 0)));
var cheated = $.jStorage.get('confburn/' + item.id) ? true : false;
if (will_burn && (settings.warn_burn !== 'never')) {
// Prompt before burning, and suppress proceeding for a moment.
if (cheated) {
new_answer.exception = 'You modified an answer on this item. It will be burned if you continue.';
} else if (settings.warn_burn === 'always') {
new_answer.exception = 'This item will be burned if you continue.'
}
if (new_answer.exception) {
new_answer.confirming_burn = true;
set_answer_state(new_answer, true /* show_msgs */);
// Not sure what's causing the input field to be re-enabled, but we have to disable it:
setTimeout(function () {
document.querySelector("#user-response").disabled = true;
}, 1);
if (settings.burn_delay_period > 0) {
do_delay(settings.burn_delay_period);
}
return false;
}
}
} else {
// We are burning the item now, so we can remove the marker.
$.jStorage.deleteKey('confburn/' + item.id);
delete new_answer.confirming_burn;
}
// We intercepted the first submit, allowing the user to optionally modify their answer.
// Now, either the user has clicked submit again, or lightning is enabled and we are automatically clicking submit again.
// Since Wanikani didn't see the first submit (because we intercepted it), now we need to simulate two submits for Wanikani:
// 1. One for Wanikani to check the (possibly corrected) result, and
// 2. One for Wanikani to move on to the next question.
// Reset the screen to pre-submitted state, so Wanikani won't get confused when it tries to process the answer.
// Wanikani code will then update the screen according to our forced answer-check result.
document.querySelector('#option-double-check').classList.add('disabled');
document.querySelector('#option-double-check span').setAttribute('title','Double-Check')
document.querySelector('#option-double-check span i').className = 'fa fa-thumbs-up';
document.querySelector('#option-retype').classList.add('disabled');
document.querySelector('#user-response').disabled = false;
$.jStorage.set('wrongCount', wrong_cnt);
$.jStorage.set('questionCount', question_cnt);
$.jStorage.set('completedCount', completed_cnt);
$.jStorage.set('activeQueue', active_queue);
// Prevent WK from posting a second SRS notice.
if (typeof Srs === 'object') {
srs_load = Srs.load;
Srs.load = function(){};
}
// This is the first submit actually forwarded to Wanikani.
// It will check our (possibly corrected) answer.
var old_audioAutoplay = window.audioAutoplay;
window.audioAutoplay = false;
click_submit.apply(this, arguments)
.then(() => {
// This is hidden third click from above, which Wanikani thinks is the second click.
// Wanikani will move to the next question.
state = 'first_submit';
// We need to disable the input field, so Wanikani will see this as the second click.
document.querySelector('#user-response').disabled = true;
// Restore the SRS message function, which we disabled in second_submit above.
if (typeof Srs === 'object') Srs.load = srs_load;
// This is the second submit actually forwarded to Wanikani.
// It will move on to the next question.
click_submit.apply(this, arguments)
.then(() => {
window.audioAutoplay = old_audioAutoplay;
window.wkRefreshAudio();
});
});
return false;
default:
return false;
}
return false;
}
//------------------------------------------------------------------------
// Simulate input character by character and convert with WanaKana to kana
// -- Contributed by user @Sinyaven
//------------------------------------------------------------------------
function to_kana(text) {
return Array.from(text).reduce((total, c) => wanakana.toKana(total + c, {IMEMode: true}), "").replace(/n$/, String.fromCharCode(12435));
}
//------------------------------------------------------------------------
// Resize the buttons according to how many are visible.
//------------------------------------------------------------------------
function resize_buttons() {
var buttons = Array.from(document.querySelectorAll('#additional-content ul>li'));
var btn_count = buttons.length - buttons.filter((elem)=>elem.matches('.hidden,[hidden]')).length;
for (let btn of document.querySelectorAll('#additional-content ul > li')) {
btn.style.width = Math.floor(9900/btn_count)/100 + '%';
}
}
//------------------------------------------------------------------------
// External hook for @polv's script, "WaniKani Disable Default Answers"
//------------------------------------------------------------------------
gobj.set_state = function(_state) {
state = _state;
};
//------------------------------------------------------------------------
// startup() - Install our intercept handlers, and add our Double-Check button and hotkey
//------------------------------------------------------------------------
function startup() {
// Intercept the submit button handler.
try {
var intercepted = false;
try {
old_submit_handler = $._data( $('#answer-form form')[0], 'events').submit[0].handler;
$._data( $('#answer-form form')[0], 'events').submit[0].handler = new_submit_handler;
intercepted = true;
} catch(err) {}
if (!intercepted) {
try {
old_submit_handler = $._data( $('#answer-form button')[0], 'events').click[0].handler;
$._data( $('#answer-form button')[0], 'events').click[0].handler = new_submit_handler;
intercepted = true;
} catch(err) {}
}
if (intercepted) {
old_answer_checker = window.enhanceAnswerChecker({evaluate:window.answerChecker.evaluate}).evaluate;
}
} catch(err) {}
if (typeof old_submit_handler !== 'function' || typeof old_answer_checker !== 'function') {
alert('Wanikani Double-Check script is not working.');
return;
}
// Clear warning popups if question changes due to reasons outside of this script
$.jStorage.listenKeyChange("currentItem", function(key, action){
set_answer_state({reset:true});
});
// Install the Lightning Mode button.
document.head.insertAdjacentHTML('beforeend','<style>#lightning-mode.doublecheck-active {color:#ff0; opacity:1.0;}</style>');
document.querySelector('#summary-button').insertAdjacentHTML('beforeend','<a id="lightning-mode" href="#" hidden ><i class="fa fa-bolt" title="Lightning Mode - When enabled, auto-\nadvance after answering correctly."></i></a>');
document.querySelector('#lightning-mode').addEventListener('click', lightning_clicked);
// Install the Double-Check features.
document.querySelector('#additional-content ul').style.textAlign = 'center';
document.querySelector('#additional-content ul').insertAdjacentHTML('beforeend',
`<li id="option-double-check" class="disabled"><span title="Double Check"><i class="fa fa-thumbs-up"></i></span></li>
<li id="option-retype" class="disabled"><span title="Retype"><i class="fa fa-undo"></i></span></li></ul>`
);
document.querySelector('#option-double-check').addEventListener('click', toggle_result.bind(null,'toggle'));
document.querySelector('#option-retype').addEventListener('click', toggle_result.bind(null,'retype'));
document.body.addEventListener('keypress', function(event){
if (event.which === 43) toggle_result('correct');
if (event.which === 45) toggle_result('incorrect');
return true;
});
document.body.addEventListener('keydown', function(event){
if ((event.which === 27 || event.which === 8) &&
(state !== 'first_submit') &&
(event.target.nodeName === 'BODY') &&
(!document.querySelector('#wkofs_doublecheck')))
{
toggle_result('retype');
return false;
} else if (event.ctrlKey && event.key === 'l') {
lightning_clicked();
return false;
}
return true;
});
document.head.insertAdjacentHTML('beforeend',
`<style>
#additional-content>ul>li.hidden {display:none;}
#answer-form fieldset.confburn button, #answer-form fieldset.confburn input[type=text], #answer-form fieldset.confburn input[type=text]:disabled {
background-color: #000 !important;
color: #fff;
text-shadow: 2px 2px 0 rgba(0,0,0,0.2);
transition: background-color 0.1s ease-in;
opacity: 1 !important;
}
</style>`
);
// Override the answer checker.
window.answerChecker.evaluate = return_new_answer;
window.enhanceAnswerChecker = function(answerChecker) {return answerChecker;};
// To prevent Wanikani from cutting the audio off in lightning mode,
// We instruct any currently playing audio to unload when it's done,
// rather than unloading it immediately.
window.Howler.unload = function(){
for (var i = window.Howler._howls.length-1; i >= 0; i--) {
var howl = window.Howler._howls[i];
if (howl.playing() || howl._queue.length > 0) {
howl.on('end', howl.unload.bind(howl));
} else {
howl.unload();
}
}
};
}
function click_submit() {
var p = promise();
old_submit_handler.apply(this, arguments);
if (document.querySelector('#answer-form button').disabled) {
// Set up callback for when 'submit' button is re-enabled after being clicked.
var mo = new MutationObserver((mutation) => {
if (mutation.pop().target.disabled) return;
mo.disconnect();
mo = undefined;
if (window.location.pathname === '/extra_study/session') {
// The Extra Study page needs a tiny pause before advancing.
setTimeout(() => {
p.resolve();
}, 1);
} else {
p.resolve();
}
});
mo.observe(document.querySelector('#answer-form button'), {attributeFilter: ['disabled']});
} else {
if (window.location.pathname === '/extra_study/session') {
// The Extra Study page needs a tiny pause before advancing.
setTimeout(() => {
p.resolve();
}, 1);
} else {
p.resolve();
}
}
return p;
}
})(window.doublecheck);