WaniKani Wrong Answer Delay

Adds delay when you answered incorrectly so you have time to look at your answer again, and spot any typos

// ==UserScript==
// @name         WaniKani Wrong Answer Delay
// @namespace    http://tampermonkey.net/
// @version      0.91
// @description  Adds delay when you answered incorrectly so you have time to look at your answer again, and spot any typos
// @author       cometzero
// @include      https://www.wanikani.com/review/session
// @include      http://www.wanikani.com/review/session
// @grant        none
// ==/UserScript==

/**
  * How long does user have to wait when he answered wrong
  */
var waitTime = 1000;

/**
  * Speed of the animation in FPS
  */
var FPS = 100;
var interval = 1000 / FPS;

/**
  * Enables or disables log messages
  */
var DEBUG = false;

(function() {
    'use strict';

    // init observer
    onSetUserResponseDisabled();

    onIncorrectAnswerObserver(function(){
        log("Wrong answer!");
        disableNextOne();

        // animate user wait time
        var timesRun = 0;
        var intervalId = setInterval(function(){
            if(timesRun > waitTime / interval){
                clearInterval(intervalId);
                enableNextOne();
                setDelayProgress(1);
            }else{
                var progress = timesRun / waitTime * interval;
                setDelayProgress(progress);
            }
            timesRun += 1;
        }, interval);
    });

    // listens for option-item-info click and shortcut key 'F' and than calls showItemInfo
    // since it will not open by itself while in delay state, since the input is disabled
    $("#option-item-info").click(function() {
        showItemInfo();
    });
    $(document).on("keydown.reviewScreen", function(n) {
           if(n.keyCode == 70) { // letfter 'F'
                showItemInfo();
           }
    });

})();

/**
  * Shows item info while the user waits
  * The problem is that show item info wont open by itself since disabled attribute is not set.
  * So in order to open it we need to set disabled attribute and than trigger click so that WaniKani js click
  * function is called again.
  * isDisabled must be set to false so that "onSetUserResponseDisabled" will not automatically set disabled attribute.
  */
var openingItemInfo = false;
function showItemInfo(){
    if(isDisabled){
        // make sure that the cick was registerd before disabling the input again
        $("#option-item-info").click(function() {
           if(openingItemInfo){
               $("#user-response").prop("disabled", false);
               isDisabled = true;
           }
           openingItemInfo = false;
        });

        isDisabled = false;
        $("#user-response").prop("disabled", true);
        openingItemInfo = true;
        $("#option-item-info").trigger("click");
    }
}

/**
  * Create div overlay that will be displayed when user is waiting to be able to go to the next letter
  */
function createOverlay(){
    var overlay = $("<div></div>");
    overlay.css("height", "100%");
    overlay.css("width", "0%");
    overlay.css("position", "absolute");
    overlay.css("background-color", "rgba(255, 0, 51, 0.30)");
    overlay.css("top", "0");
    overlay.css("left", "0");

    $("#user-response").parent().append(overlay);
    return overlay;
}

// overlay is created only once and than saved in this variable
var overlay;

/**
  * Show user how much time it has to wait
  * @param {progress} progress between 0 .. 1. 1 means it is completed
  */
function setDelayProgress(progress){
    // if overlay doesn't exist creat it
    if(!overlay){
        overlay = createOverlay();
    }

    overlay.css("width", (100-progress*100) + "%");
    overlay.css("left", (progress*50) + "%");

    var trans = Math.min(0.40, 1-progress);
    overlay.css("background-color", "rgba(255, 0, 51, "+trans+")");
}

/**
  * Enables user to proceed to next leter
  */
function enableNextOne(){
    _enableNextOne(true);
}

/**
  * Disables user to proceed to next letter
  */
function disableNextOne(){
    _enableNextOne(false);
}

var isDisabled = false; // is user currently able to go to the next letter?
function _enableNextOne(enable){
    log("Enable next one: " + enable);
    isDisabled = !enable;

    // if "user-response <input>" is not disabled than it will not go to the next letter
    // this is just how the wanikani works.
    // Since input is not disabled we have to set pointer-events to none so that user cannot input anything
    $("#user-response").prop("enabled", !enable);
    $("#user-response").prop("disabled", enable);
    $("#user-response").css("pointer-events", enable? "" : "none");
}

/**
 * Set observer that is called every time user answered incorrectly
 * @param {function} function that is called user answered incorrectly
 */
function onIncorrectAnswerObserver(f){
    MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

    var observer = new MutationObserver(function(mutations, observer) {
          mutations.forEach(function(mutation) {
              // check if it has class name set as "incorrect"
              if(mutation.attributeName == "class" && mutation.target.className == "incorrect"){
                  f();
              }
          });
    });

    // configuration of the observer:
    var config = {attributes: true, attributeOldValue: true };

    // we have to select parent since it is the one that gets class changed
    var target = document.getElementById("user-response").parentElement;

    // pass in the target node, as well as the observer options
    observer.observe(target, config);
}

/**
 * When user response is not disabled and user presses enter wanikani will automatically disabled it
 * That means that user can proceed to the next letter. We don't want that... we want for user to wait.
 */
function onSetUserResponseDisabled(){
    MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

    var observer = new MutationObserver(function(mutations, observer) {
          mutations.forEach(function(mutation) {
              // check if it has class name set as "incorrect"
              if(isDisabled && mutation.attributeName == "disabled"){
                  $("#user-response").prop("disabled", false);
              }
          });
    });

    // configuration of the observer:
    var config = {attributes: true, attributeOldValue: true };

    // we have to select parent since it is the one that gets class changed
    var target = document.getElementById("user-response");

    // pass in the target node, as well as the observer options
    observer.observe(target, config);
}


function log(logMessage){
    if(DEBUG){
        console.log(logMessage);
    }
}