WaniKani hide mnemonics

Adds a possiblity to hide meaning and reading mnemonics.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name WaniKani hide mnemonics
// @namespace wkhidem
// @description Adds a possiblity to hide meaning and reading mnemonics.
// @version 1.8
// @author Niklas Barsk
// @include https://www.wanikani.com/review/session*
// @include https://www.wanikani.com/lesson/session*
// @include https://www.wanikani.com/radicals/*
// @include https://www.wanikani.com/kanji/*
// @include https://www.wanikani.com/vocabulary/*
// @run-at document-end
// ==/UserScript==

/*
 * This script is licensed under the MIT licence.
 */

if (isReview() || isLesson())
{
    // review/lessons quiz
    var mo = new MutationObserver(initQuiz);
    mo.observe(document.getElementById("item-info-col2"), {'childList': true});
}

if (isLesson())
{
    // Call init whenever the main-info class attribute is updated.
    // This happens whenever the user switch to a new item on the
    // lesson/learning part.
    var mo = new MutationObserver(init);
    mo.observe(document.getElementById("main-info"), {'attributes': true});
}

if (isLookup())
{
    if (document.getElementById("progress").style.display != "none")
    {
        // only run the script on items that has been unlocked since it's
        // not possible to add user mnemonics on locked items.
        init();
    }
}

function initQuiz(allmutations)
{
    if (allmutations[0].addedNodes.length > 0)
    {
        // Ignore the mutation if no nodes are added.
        // When going from one question to the next all elements in
        // item-info-col2 is first removed as one mutation and then
        // the new content is added as a second mutation. So mutations
        // without any added nodes should be ignored
        init();
    }
}

function init()
{
    if (!sanityCheckPassed())
    {
        // Don't try to run the script if the HTML can't be parsed.
        console.warn("WaniKani hide mnemonics need to be updated to support the latest version of WaniKani.");
        return;
    }

    setCorrectText();
    setCorrectVisibility();

    if (isQuiz())
    {
        // Update visibility state when the "Show All Information" button is pressed.
        document.getElementById("all-info").addEventListener("click", setCorrectVisibility);
    }

    // Setup listeners for changes to the note-meaning/reading.
    var mo = new MutationObserver(onNoteChanged);
    var options = {'childList': true};
    mo.observe(getNoteElement("meaning"), options);

    if (!isRadical())
    {
        mo.observe(getNoteElement("reading"), options);
    }
}


/**
 * Called whenever the note-reading or note-meaning elements children
 * are updated.
 */
function onNoteChanged(allmutations)
{
    // 1 children for the edit note form and 0 children when the
    // note is being displayed. Update visibility when form is closed
    // and note is shown again.
    if (allmutations[0].target.children.length == 0)
    {
        // example id for quiz: 'meaning-note'
        // example id for learning: 'supplement-voc-meaning-notes'
        var index = isLesson() && !isQuiz() ? 2 : 1;
        var which = allmutations[0].target.parentNode.id.split('-')[index];
        setCorrectVisibilityFor(which);
    }
}

/**
 * Set the correct text for the meaning and reading headers depending on
 * the current state.
 */
function setCorrectText()
{
    setCorrectTextFor("meaning");
    if (!isRadical())
    {
        setCorrectTextFor("reading");
    }
}

/**
 * Returns true if the mnemonic is hidden for the current character
 * and the given type.
 */
function isHidden(which)
{
    return isManuallyHidden(which) || isAutomaticallyHidden(which);
}

/**
 * Returns true if the user has hidden the mnemonic with the hide link.
 */
function isManuallyHidden(which)
{
    return localStorage.getItem(getStorageKey(which)) == "0";
}

/**
 * Returns true if the mnemonic has been hidden because there
 * is a note present.
 */
function isAutomaticallyHidden(which)
{
    return hasNote(which) &&
           localStorage.getItem(getStorageKey(which)) != "1";
}

/**
 * Set hidden status for the current character in the localStorage
 * for the give type.
 * @param which "reading" or "meaning" depending on which key is desired.
 * @param what new value for the current character:
 *             0: user has manually hidden the mnemonic.
 *             1: user has shown an automatically hidden mnemonic.
 */
function setStorage(which, what)
{
    localStorage.setItem(getStorageKey(which), what);
}

/**
 * Remove the stored information about the current character from
 * the localStorage for the give type.
 * @param which "reading" or "meaning" depending on which key is desired.
 */
function clearStorage(which)
{
    localStorage.removeItem(getStorageKey(which));
}

/**
 * Get the key that the removed state for the current character is
 * stored under in the localStorage.
 * @param "reading" or "meaning" depending on which key is desired.
 */
function getStorageKey(which)
{
    return getCharacterType() + "_" + getCharacter() + "_" + which;
}

/**
 * Return a textual representation of the current character.
 * For vocabulary, kanji and normal radicals it is the vocabulary,
 * kanji, or radical itself. For radicals that are just an image
 * it is the file name of the image.
 */
function getCharacter()
{
    var element;
    if (isLookup())
    {
        element = document.getElementsByClassName("japanese-font-styling-correction")[0];
    }
    else
    {
        element = document.getElementById("character");
    }

    var character = element.textContent.trim();
    if (character == "") // Radical with image instead of text.
    {
        var img = element.children[0];
        // During quiz the image is inside a span, during lessons
        // the image is directly under the character div.
        if (isQuiz())
        {
            img = img.children[0]
        }
        character = img.getAttribute("src").split("/").pop()
    }
    return character
}

/**
 * Return the type of the character the page is for: a string containing
 * "vocabulary", "kanji" or "radical".
 */
function getCharacterType()
{
    if (isLesson())
    {
        return document.getElementById("main-info").className.trim();
    }
    else if (isReview())
    {
        return document.getElementById("character").className.trim();
    }
    else if (isLookup())
    {
        var character = document.getElementsByClassName("japanese-font-styling-correction")[0];
        var cn = character.parentElement.className;
        return cn.substr(0, cn.indexOf("-"));
    }
}

/**
 * Return true if the current page is for a radical.
 */
function isRadical()
{
    return getCharacterType() == "radical";
}

/**
 * Return true if the current page is for vocabulary.
 */
function isVocabulary()
{
    return getCharacterType() == "vocabulary";
}

function isLookup()
{
    return document.URL.indexOf("/radicals/") != -1 ||
           document.URL.indexOf("/kanji/") != -1 ||
           document.URL.indexOf("/vocabulary/") != -1;
}

/**
 * Returns true if the current page is a lesson.
 */
function isLesson()
{
    return document.URL.indexOf("lesson") != -1;
}

/**
 * Returns true if the current page is a review.
 */
function isReview()
{
    return document.URL.indexOf("review") != -1;
}

/**
 * Returns true if the user is currently doing a quiz.
 */
function isQuiz()
{
    if (isReview())
    {
        return true;
    }
    if (isLesson())
    {
        var mainInfo = document.getElementById("main-info");
        return mainInfo.parentElement.className == "quiz";
    }
    return false;
}

/**
 * Returns true if the current item has a note set.
 * @param which specifies if it's the reading or meaning
 *              note that is of interest.
 */
function hasNote(which)
{
    return getNoteElement(which).textContent.trim() != "Click to add note";
}

/**
 * Set the correct visibility of the reading and meaning sections.
 */
function setCorrectVisibility()
{
    setCorrectVisibilityFor("meaning");
    if (!isRadical())
    {
        setCorrectVisibilityFor("reading");
    }
}

/**
 * Set the correct visibility for the specified header depending on the current state.
 * @param which The header that should be updated, either "reading" or "meaning".
 */
function setCorrectVisibilityFor(which)
{
    if (hiddenByWaniKani(which))
    {
        // Don't touch visibility for things hidden by WaniKani.
        return;
    }

    if (isHidden(which)) // In this case, should be hidden
    {
        hide(which);
    }
    else
    {
        show(which);
    }
}

/**
 * When doing a quiz WaniKani only shows the info that was being asked for
 * to see all info the user need to press a button to display it.
 * This method returns true if the given reading/meaning is currently hidden.
 *
 * @param which "reading" or "meaning"
 */
function hiddenByWaniKani(which)
{
    if (!isQuiz())
    {
        return false;
    }
    var infoHidden = document.getElementById("all-info").style.display != "none";
    var questionType = document.getElementById("question-type").className;
    return which != questionType && infoHidden;
}

/**
 * Set the correct state in local storage for the current item
 * and mnenemonic based on the given action.
 * @param action "hide" or "show"
 * @param which "reading" or "meaning"
 */
 function setCorrectStorage(action, which)
{
    var note = hasNote(which);
    if (action == "show" && !note ||
        action == "hide" && note)
    {
        // Default state, cleare any storage
        clearStorage(which);
    }
    else if (action == "show" && note)
    {
        // Force section to be visible
        setStorage(which, 1);
    }
    else if (action == "hide" && !note)
    {
        // Force section to be hidden
        setStorage(which, 0);
    }
}

/**
 * Hide the specified section.
 * @param which The section that should be hidden, either "reading" or "meaning".
 */
function hide(which)
{
    setCorrectStorage("hide", which);
    setDisplayStyle(which, "none");
    setCorrectText();
}

/**
 * Show the specified section.
 * @param which The section that should be shown, either "reading" or "meaning".
 */
function show(which)
{
    setCorrectStorage("show", which);
    setDisplayStyle(which, "");
    setCorrectText();
}

/**
 * Set the display style of the hidable section.
 * @param which The section that should be updated, either "reading" or "meaning".
 * @param display The new value of the display css property.
 */
function setDisplayStyle(which, display)
{
    var children = getHidableElements(which);
    for (i = 0; i < children.length; ++i)
    {
        children[i].style.display = display;
    }
}

/**
 * Returns an array with all elements that should be hidden or
 * shown when the hide/show link is clicked.
 * @param Specifies if it's the "reading" or "meaning" that should be hidden
 */
function getHidableElements(which)
{
    // return an array of items to hide/show
    var ret = [];
    if (isQuiz())
    {
        ret.push(getMnemonicContainer(which));
    }
    else if (isLesson())
    {
        var children = getLearningContainer(which).children;
        for (i = 0; i < children.length - 2; ++i) // note section is last 2 elements.
        {
            ret.push(children[i]);
        }
    }
    else if (isLookup())
    {
        if (isRadical())
        {
            ret.push(getLookupMnemonicContainer(which));
        }
        else
        {
            var children = getLookupMnemonicContainer(which).children;
            for (i = 0; i < children.length - 1; ++i) // note section is the last element.
            {
                ret.push(children[i]);
            }
        }
    }
    return ret;
}

/**
 * Set the correct text for reading/meaning/note header with the apropriate
 * show/hide link depending on what the current state is.
 *
 * @param which Specifies which header should be updated, the "reading" or "meaning" header.
 * @param action Specifies what happens when the header is pressed, either "show" or "hide".
 * @param headerID The ID of the header that should be updated.
 * @param header The DOM element which should have its text updated.
 */
function textForHeader(which, action, header)
{
    // Add the show/hide link to the header.
    header.innerHTML = header.firstChild.textContent + getLinkHTML(which, action);

    // Set either hide(which) or show(which) as onclick handler for the new link.
    document.getElementById(getLinkId(which, action)).onclick = function() 
    {
        if (action == "show")
        {
            show(which);
        }
        else
        {
            hide(which);
        }
    };
}

/**
 * Get the HTML for the show/hide link.
 * @param which Specifies if the link is for "reading" or "meaning".
 * @param action Specifies if the link is "hide" or "show".
 */
function getLinkHTML(which, action)
{
    // Examples of what the html looks like:
    // <span id="show-reading">(show original meaning)</span>
    // <span id="hide-meaning">(hide)</span>

    var linkText = action;
    if (action == "show")
    {
        if (isVocabulary())
        {
            linkText += " original explanation";
        }
        else
        {
            linkText += " original mnemonic";
        }
    }

    return "<span id=\"" + getLinkId(which, action) + "\"> (" + linkText + ")</span>";
}

/**
 * Return the id of the show/hide link.
 */
function getLinkId(which, action)
{
    var quiz = isQuiz() ? "-q" : "";
    return action + "-" + which + "-" + getCharacterType() + quiz;
}

/**
 * Set the correct text for the specified header depending on the current state.
 * @param which The header that should be updated, either "reading" or "meaning".
 */
function setCorrectTextFor(which)
{
    if (isHidden(which))
    {
        // Display the "show" link in the note.
        textForHeader(which, "show", getNoteHeader(which));
    }
    else
    {
        // Display the "hide" link in the header.
        textForHeader(which, "hide", getMnemonicHeader(which));

        // Make sure the default version of the Note header is displayed.
        var nh = getNoteHeader(which);
        nh.innerHTML = nh.firstChild.textContent;
    }
}

/**
 * Get the DOM element that contains the mnemonic.
 * @param which Specifies if the header for the reading or meaning should
 *              be returned. The parameter is ignored for radicals since
 *              they only have one mnemonic.
 */
function getMnemonicContainer(which)
{
    if (isRadical())
    {
        return document.getElementById("item-info-col2").children[0];
    }
    else
    {
        return document.getElementById("item-info-" + which + "-mnemonic");
    }
}

/**
 * Return the element that contains the mnemonics for the lookup pages.
 */
function getLookupMnemonicContainer(which)
{
    if (isRadical())
    {
        return document.getElementById("note-" + which).previousElementSibling;
    }
    else
    {
        return document.getElementById("note-" + which).parentElement;
    }
}

/**
 * Get the DOM element for the mnemonic header.
 * @param which Specifies if the header for the reading or meaning should
 *              be returned. The parameter is ignored for radicals since
 *              they only have one mnemonic.
 */
function getMnemonicHeader(which)
{
    if (isQuiz())
    {
        return getMnemonicContainer(which).children[0];
    }
    else if (isLesson())
    {
        return getLearningContainer(which).children[0];
    }
    else if(isLookup())
    {
        return getLookupMnemonicContainer(which).children[0];
    }
}

/**
 * Get the DOM element for the user notes header.
 * @param which Specifies if the notes header for the reading or meaning
 *              should be returned.
 */
function getNoteHeader(which)
{
    if (isQuiz() || isLookup())
    {
        return document.getElementById("note-" + which).children[0];
    }
    else if (isLesson())
    {
        var container = getLearningContainer(which);
        return container.children[container.children.length - 2];
    }
}

/**
 * Returns the DOM element that holds the user note.
* @param which Specifies if the notes element for the reading or meaning
 *              should be returned.
 */
function getNoteElement(which)
{
    var element;
    if (isLesson() && !isQuiz())
    {
        var id;
        if (isRadical())
        {
            id = "supplement-rad-name-notes";
        }
        else
        {
            id = "supplement-" + getCharacterType().substring(0,3) + "-" + which + "-notes";
        }
        element = document.getElementById(id).children[0];
    }
    else
    {
        element = document.getElementById("note-" + which).children[1];
    }
    return element;
}

/**
 * Get the container element of the mnemonics and notes in the learning
 * part of lessons. There is no id available for the actual headers and
 * mnemonics like in the quiz so the container element is the closest
 * we get.
 *
 * @param which Specifies if it's the container for "reading" or "meaning"
 *              that is desired.
 */
function getLearningContainer(which)
{
    var id = "supplement-" + getCharacterType().substring(0,3) + "-";
    var className = "pure-u-3-4";
    if (isRadical())
    {
        id += "name";
        className = "pure-u-1"
    }
    else
    {
        id += which;
    }

    return document.getElementById(id).getElementsByClassName(className)[0];
}

/**
 * Return true if critical assumptions made about the HTML code holds.
 */
function sanityCheckPassed()
{
    try
    {
        if (isLookup())
        {
            sanityCheckLookup();
        }

        if (isQuiz())
        {
            sanityCheckQuiz();
        }

        if (isLesson())
        {
            sanityCheckLesson();
        }

        // Make sure we can get a correct character type.
        var ct = getCharacterType();
        if (ct != "radical" && ct != "vocabulary" && ct != "kanji")
        {
            throw new Error("Unknown character type: " + ct);
        }

        // Make sure we can get a correct storage key
        var parts = getStorageKey("meaning").split("_");
        if (parts.length != 3 || parts[0] == "" ||
            parts[1] == ""    || parts[2] == "")
        {
            throw new Error("Unable to generate a correct storage key: " + key);
        }
    }
    catch (e)
    {
        console.error(e.toString());
        return false;
    }
    return true;
}

/**
 * Throws an exception if the critical assumptions made about the
 * HTML code in the lookup related code are wrong.
 */
function sanityCheckLookup()
{
    if (document.getElementsByClassName("japanese-font-styling-correction").length == 0)
    {
        throw new Error("No element with class 'japanese-font-styling-correction' exists");
    }

    ensureElementExists("note-meaning");
    if (!isRadical())
    {
        ensureElementExists("note-reading");
    }
}

/**
 * Throws an exception if the critical assumptions made about the
 * HTML code in the quiz related code are wrong.
 */
function sanityCheckQuiz()
{
    ensureElementExists("character");
    ensureElementExists("all-info");
    ensureElementExists("item-info-col2");
    ensureElementExists("note-meaning");
    var questionType = ensureElementExists("question-type");
    questionType = questionType.className;

    if (questionType != "reading" && questionType != "meaning")
    {
        throw new Error("'question-type' is neither \"reading\" nor \"meaning\", it is \"" + questionType + "\"");
    }

    if (!isRadical())
    {
        ensureElementExists("item-info-reading-mnemonic");
        ensureElementExists("item-info-meaning-mnemonic");
        ensureElementExists("note-reading");
    }
}

/**
 * Throws an exception if the critical assumptions made about the
 * HTML code in the lesson related code are wrong.
 */
function sanityCheckLesson()
{
    ensureElementExists("character");
    var mainInfo = ensureElementExists("main-info");

    // Make sure assumptions for lessons in isQuiz() holds.
    var cn = mainInfo.parentElement.className;
    if (cn != "" && cn != "quiz")
    {
        throw new Error("Parent of 'main-info' is neither empty nor \"quiz\"");
    }

    if (!isQuiz())
    {
        ensureElementExists("supplement-rad-name-notes");
        ensureElementExists("supplement-kan-meaning-notes");
        ensureElementExists("supplement-voc-meaning-notes");
        ensureElementExists("supplement-kan-reading-notes");
        ensureElementExists("supplement-voc-reading-notes");

        ensureElementExistsAndHasClass("supplement-voc-reading", "pure-u-3-4");
        ensureElementExistsAndHasClass("supplement-voc-meaning", "pure-u-3-4");
        ensureElementExistsAndHasClass("supplement-kan-reading", "pure-u-3-4");
        ensureElementExistsAndHasClass("supplement-kan-meaning", "pure-u-3-4");
        ensureElementExistsAndHasClass("supplement-rad-name", "pure-u-1");
    }
}

/**
 * Throws an exception if the given id doesn't exist in the DOM tree.
 * @return the element if it exist
 */
function ensureElementExists(id)
{
    var element = document.getElementById(id);
    if (element == null)
    {
        throw new Error(id + " does not exist");
    }
    return element;
}

/**
 * Throws an exception if the given id doesn't exist in the DOM tree.
 */
function ensureElementExistsAndHasClass(id, className)
{
    var element = ensureElementExists(id);
    if (element.getElementsByClassName(className)[0] == null)
    {
        throw new Error(id + " does not contain any element with class: " + className);
    }
}