// ==UserScript==
// @name Wanikani Double-Check
// @namespace wkdoublecheck
// @description Adds a delay after wrong answers to prevent double-tapping <enter>
// @include https://www.wanikani.com/review/session*
// @version 2.0.0
// @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: "!" - Toggles an answer between 'correct' and 'incorrect'.
// SEE SETTINGS BELOW.
window.wkdoublecheck = {};
(function(gobj) {
//==[ Settings ]=====================================================
var settings = {
// Delay when answer is wrong.
delay_wrong: 1,
// Delay when answer has multiple meanings.
delay_multi_meaning: 0,
// Delay when answer is slightly off (e.g. minor typo).
delay_slightly_off: 1,
// Amount of time to delay (in milliseconds) before allowing the
// user to move on after an accepted typo or rejected answer.
delay_period: 1500,
};
// Make the settings accessible from the console via 'wkdoublecheck.settings'
gobj.settings = settings;
//===================================================================
// Theory of operation:
// ====================
// Wanikani's normal process:
// 1) User clicks 'submit'
// a) Wanikani checks answer and updates screen with the result.
// b) If both reading and meaning have been answered, Wanikani immediately sends the result to the server. (<-- BAD!!)
// 2) User clicks 'submit' (or enter) again to move to the next question.
// a) Wanikani updates the screen to show the next question.
//
// Our modified process:
// 1) User clicks 'submit'
// a) We intercept the click, check the answer ourself, and update the screen.
// Wanikani's code is unaware of what we're doing.
// b) User now has the opportunity to modify their answer.
// 2) User clicks 'submit' (or enter) again to move to the next question.
// a) We intercept the click again.
// b) We reset the display back to pre-submitted state, so Wanikani's code won't be confused.
// c) We call Wanikani's normal 'submit' function, but we intercept the answer-checker function,
// so Wanikani will see whatever result the user requested.
// Wanikani's updates the screen with the result.
// d) Keep in mind, the user has clicked the 'submit' button twice, but Wanikani has only
// seen one click. So, we have to send a third hidden click so Wanikani will catch up to
// where the user thinks we are (i.e. 'next question').
// 3) We intercept the hidden click, and forward it to Wanikani's code.
// a) Wanikani updates the screen to show the next question.
var old_submit_handler, old_answer_checker, ignore_submit = false, state = 'first_submit';
var item, itype, item_id, item_status, qtype, valid_answers, wrong_cnt, question_cnt, completed_cnt, answer, new_answer;
//------------------------------------------------------------------------
// toggle_result() - Toggle an answer from right->wrong or wrong->right.
//------------------------------------------------------------------------
function toggle_result() {
if ($('#option-double-check').hasClass('disabled')) return false;
if (new_answer.passed)
new_answer = {passed:false, accurate:false, multipleAnswers:false, exception:false};
else
new_answer = {passed:true, accurate:true, multipleAnswers:false, exception:false};
set_answer_state(new_answer, false /* show_msgs */);
}
//------------------------------------------------------------------------
// do_delay() - Disable the submit button briefly to prevent clicking past wrong answers.
//------------------------------------------------------------------------
function do_delay() {
ignore_submit = true;
setTimeout(function() {
ignore_submit = false;
}, settings.delay_period);
}
//------------------------------------------------------------------------
// 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 answer is invalid for some reason, do the shake thing.
if (answer.exception) {
if (!$("#answer-form form").is(":animated")) {
$("#reviews").css("overflow-x", "hidden");
var xlat = {onyomi:"on'yomi", kunyomi:"kun'yomi", nanori:"nanori"};
var emph = xlat[item.emph];
$("#answer-form form").effect("shake", {}, 400, function() {
$("#reviews").css("overflow-x", "visible");
$("#answer-form form").append($('<div id="answer-exception" class="answer-exception-form"><span>WaniKani is looking for the '+emph+" reading</span></div>").addClass("animated fadeInUp"));
}).find("input").focus();
}
return;
}
// Draw 'correct' or 'incorrect' results, enable Double-Check button, and calculate updated statistics.
$("#user-response").blur();
if (answer.passed) {
$("#answer-form fieldset").removeClass('incorrect').addClass("correct");
$('#option-double-check').removeClass('disabled').find('span').attr('title','Mark Wrong').find('i').attr('class','icon-thumbs-down');
new_wrong_cnt = wrong_cnt;
new_completed_cnt = completed_cnt + ((((itype==='r' || ((item_status.rc || 0) >= 1)) ? 1 : 0) + ((item_status.mc || 0) >= 1 ? 1 : 0) + 1) >= 2 ? 1 : 0);
} else {
$("#answer-form fieldset").removeClass('correct').addClass("incorrect");
$('#option-double-check').removeClass('disabled').find('span').attr('title','Mark Right').find('i').attr('class','icon-thumbs-up');
new_wrong_cnt = wrong_cnt + 1;
new_completed_cnt = completed_cnt + ((((itype==='r' || ((item_status.rc || 0) >= 1)) ? 1 : 0) + ((item_status.mc || 0) >= 1 ? 1 : 0)) >= 2 ? 1 : 0);
}
new_question_cnt = question_cnt + 1;
$.jStorage.set('wrongCount', new_wrong_cnt);
$.jStorage.set('questionCount', new_question_cnt);
$.jStorage.set('completedCount', new_completed_cnt);
$("#user-response").prop("disabled", !0);
additionalContent.enableButtons();
lastItems.disableSessionStats();
$("#answer-exception").remove();
// 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 {
msg = 'Need help? View the correct '+qtype+' and mnemonic';
}
if (msg)
$("#additional-content").append($('<div id="answer-exception"><span>'+msg+'</span></div>').addClass("animated fadeInUp"));
}
}
//------------------------------------------------------------------------
// 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) return false;
// For more information about the state machine below,
// see the "Theory of operation" info at the top of the script.
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');
question_cnt = $.jStorage.get('questionCount');
completed_cnt = $.jStorage.get('completedCount');
// Ask Wanikani if the answer is right (but we don't actually submit the answer).
answer = old_answer_checker(qtype, $("#user-response").val());
// Update the screen to reflect the results of our checked answer.
$('#option-double-check').removeClass('disabled');
$("html, body").animate({scrollTop: 0}, 200);
new_answer = Object.assign({},answer);
set_answer_state(answer, true);
if (answer.exception) return false;
// 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))
)
)
{
do_delay();
}
return false;
case 'second_submit':
// We take the user's second 'submit' click (after they've optionally toggled the answer result),
// and send it to Wanikani's code as if it were the first click.
// Then we send a hidden third 'submit', which Wanikani will see as the second 'submit', which moves us to the next question.
state = 'third_submit';
// 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.
$('#option-double-check').addClass('disabled').find('span').attr('title','Double-Check').find('i').attr('class','icon-thumbs-up');
$('#user-response').removeAttr('disabled');
$.jStorage.set('wrongCount', wrong_cnt);
$.jStorage.set('questionCount', question_cnt);
$.jStorage.set('completedCount', completed_cnt);
// Prepare a hidden third click, which tells Wanikani to move to the next question.
setTimeout(function(){
$("#answer-form button").trigger('click');
}, 1);
// We want Wanikani to see our forced answer-check result,
// so we set up to intercept the answer-checker here.
return old_submit_handler.apply(this, arguments);
case 'third_submit':
// 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.
$('#user-response').attr('disabled','disabled');
return old_submit_handler.apply(this, arguments);
default:
return false;
}
return false;
}
//------------------------------------------------------------------------
// startup() - Install our intercept handlers, and add our Double-Check button and hotkey ("!")
//------------------------------------------------------------------------
function startup() {
// Check if we can intercept the submit button handler.
try {
old_submit_handler = $._data( $('#answer-form button')[0], 'events').click[0].handler;
old_answer_checker = answerChecker.evaluate;
} catch(err) {}
if (typeof old_submit_handler !== 'function' || typeof old_answer_checker !== 'function') {
alert('Wanikani Mistake Delay script is not working.');
return;
}
// Replace the handler.
$._data( $('#answer-form button')[0], 'events').click[0].handler = new_submit_handler;
var btn_count = $('#additional-content ul').children().length + 1;
$('#additional-content ul').css('text-align','center').append('<li id="option-double-check" class="disabled"><span title="Double-Check"><i class="icon-thumbs-up"></i></span></li>');
$('#additional-content ul > li').css('width',Math.floor(9950/btn_count)/100 + '%');
$('#option-double-check').on('click', toggle_result);
$('body').on('keypress', function(event){
if (event.key === '!') toggle_result();
return true;
});
answerChecker.evaluate = return_new_answer;
}
// Run startup() after window.onload event.
if (document.readyState === 'complete')
startup();
else
window.addEventListener("load", startup, false);
})(window.wkdoublecheck);