WKCM2

Community Mnemonics for WaniKani. Submit your own mnemonics and view other submissions.

// ==UserScript==
// @name        WKCM2
// @description Community Mnemonics for WaniKani. Submit your own mnemonics and view other submissions.
// @namespace   wkcm2
// @match       https://*.wanikani.com/subject-lessons/*
// @match       https://*.wanikani.com/level/*
// @match       https://*.wanikani.com/kanji*
// @match       https://*.wanikani.com/vocabulary*
// @match       https://*.wanikani.com/radicals*
// @require     https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1166918
// @homepage    https://github.com/Dakes/WaniKaniCommunityMnemonics2/
// @version     0.3.2.1
// @author      Daniel Ostertag (Dakes)
// @license     GPL-3.0
// @grant       none
// ==/UserScript==

/*
Copyright (C) 2022  Dakes (Daniel Ostertag) https://github.com/Dakes

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

var rollupUserScript = (function (exports) {
    'use strict';

    /**
     * Global constant values
     */
    const WKCM2_version = "0.3.2";
    const scriptName = 'WKCM2';
    // Google sheet: https://docs.google.com/spreadsheets/d/13oZkp8eS059nxsYc6fOJNC3PjXVnFvUC8ntRt8fdoCs/edit?usp=sharing
    // google sheets apps script url, for sheet access
    const sheetApiUrl = "https://script.google.com/macros/s/AKfycby_Kqff92G40TGXr0PSulvQ2gqx6bkHVEl6LplZ-zc5ZIHhJGwe7AA8I4nDErKMiu2GEw/exec";
    // "https://script.google.com/macros/s/AKfycbxCxmHz_5ibnHn0un5HxaCLeJTRHxwdrS5fW4nmXBYXyA-Jw6aDPPrrHWrieir3B8kDFQ/exec";
    // Maximum number, how many mnemonics one user can submit for one item.
    const mnemMaxCount = 5;
    // If date of cached item is older than this number of days, refetch.
    // NOTE: If too many people use WKCM2, it might be necessary to turn this up, so the API doesn't get spammed with requests.
    const cacheDayMaxAge = 7;
    let isList = false;
    let isItem = false;
    // @ts-ignore;  A wrapper for the window, because unsafeWindow doesn't work in Firefox 
    // @ts-ignore;  and window does not have access to wkof in some browsers?? (How even? idk, it worked before)
    let win = typeof unsafeWindow != 'undefined' ? unsafeWindow : window;
    function setPageVars() {
        isList = (
        // true if on a level page
        /level\/[0-9]{1,3}/gi.test(window.location.pathname.slice(window.location.pathname.indexOf("com/") + 2)) ||
            // true if on a /kanji?difficulty=pleasant site
            /(kanji|vocabulary|radicals)\?(difficulty=[A-Za-z].*)/gi
                .test(window.location.pathname.slice(window.location.pathname.indexOf("com/") + 2) + window.location.search));
        isItem = /(kanji|vocabulary|radicals)\/.*/gi
            .test(window.location.pathname.slice(window.location.pathname.indexOf("com/") + 2));
    }
    setPageVars();
    const cacheFillIdent = "wkcm2-fillCache";
    // getData refetch timeout. How long to wait with new execution of updateCM after previous getData fetch.
    // Especially, if the apps script is overloaded it can take a while (~5s). So it has to be enough time,
    // to allow for the data to arrive and prevent spamming of the apps script. 
    const refetchTimeout = 10000; // in ms

    /**
     * Functions to get information about the currently loaded page/item
     */
    /**
     * @returns The current item. (説得, stick, etc.)
     */
    function getItem() {
        let item = null;
        item = win.wkItemInfo.currentState.characters;
        if (item == undefined)
            item = win.wkItemInfo.currentState.meaning[0].toLowerCase();
        if (item == null) {
            let msg = "Error: getItem, item is null. ";
            console.log("WKCM2: " + msg);
            // TODO: maybe add flag, that marks the iframe for this item "unupdatable", after an error display
            updateIframe(null, msg, null);
        }
        return item;
    }
    /**
     * Returns radical, kanji or vocabulary
     * */
    function getItemType() {
        let itemType = win.wkItemInfo.currentState.type;
        if (isList)
            itemType = window.location.pathname.slice(1);
        if (itemType == null)
            console.log("WKCM2: getItemType, itemType null");
        if (itemType === "radicals")
            itemType = "radical";
        return itemType;
    }
    /**
     * When URL changes calls right init function
     * // callback with delay of "delay" ms
     * @param delay delay after URL change, to call functions.
     * @param callback Optional callback, extra function to execute.
     */
    function detectUrlChange(delay = 250, callback = function () { }) {
        const observer = new MutationObserver((mutations) => {
            if (window.location.href !== observerUrl.previousUrl) {
                setPageVars();
                observerUrl.previousUrl = window.location.href;
                setTimeout(function () {
                    if (isList)
                        initList();
                    else if (isItem) {
                        infoInjectorInit("meaning");
                        if (getItemType() != "radical")
                            infoInjectorInit("reading");
                    }
                    callback();
                }, delay);
            }
        });
        const config = { subtree: true, childList: true };
        // start listening to changes
        observer.observe(document, config);
    }
    var observerUrl;
    (function (observerUrl) {
        observerUrl.previousUrl = "";
    })(observerUrl || (observerUrl = {}));
    /**
     * Reexecutes callback function every "timeout" ms until classname exists.
     * @param selector selector to get element by id or classname
     * @param callback Callback function, that would create element found by selector
     * @param interval
     */
    function waitForClass(selector, callback, interval = 250, firstTimeout = 0) {
        if (timer.iter[selector] == undefined)
            timer.iter[selector] = 0;
        // other timer is still running
        if (timer.timer[selector])
            return;
        let callbackWrapper = async function () {
            let timeout = 0;
            let ele = document.querySelector(selector);
            timer.iter[selector]++;
            if (timer.iter[selector] <= 1)
                timeout = firstTimeout;
            if (ele || timer.iter[selector] >= timer.maxIter) {
                timer.iter[selector] = 0;
                timer.timer[selector] = clearInterval(timer.timer[selector]);
                return;
            }
            else
                setTimeout(async () => {
                    await callback();
                }, timeout);
        };
        timer.timer[selector] = setInterval(callbackWrapper, interval);
    }
    var timer;
    (function (timer_1) {
        // Array of timers with selector as key
        timer_1.timer = {};
        timer_1.iter = {};
        timer_1.maxIter = 25;
    })(timer || (timer = {}));

    /**
     * Miscellaneous utility function used by various functions.
     */
    /**
     * converts kanji -> k etc.
     * */
    function getShortItemType(type) {
        return getItemTypeLen(type, 1);
    }
    function getItemTypeLen(type, len = 99) {
        if (type === "kanji" || type === "k" || type === "kan") // @ts-ignore
            return "kanji".substring(0, len);
        else if (type === "vocabulary" || type === "v" || type === "voc") // @ts-ignore
            return "vocabulary".substring(0, len);
        else if (type === "radical" || type === "r" || type === "rad") // @ts-ignore
            return "radical".substring(0, len);
        else
            throw new Error("WKCM2: getShortItemType got wrong ItemType: " + type);
    }
    /**
     * converts meaning -> m, reading -> r
     * */
    function getShortMnemType(type) {
        if (type === "reading" || type === "r")
            return "r";
        else if (type === "meaning" || type === "m")
            return "m";
        else
            throw new Error("WKCM2: getShortMnemType got wrong ItemType: " + type);
    }
    function addClass(id, className = "disabled") {
        let ele = document.getElementById(id);
        if (ele == null)
            return false;
        ele.classList.add(className);
        return true;
    }
    function removeClass(id, className = "disabled") {
        let ele = document.getElementById(id);
        if (!ele)
            return false;
        ele.classList.remove(className);
        return true;
    }
    const memoize = (fn) => {
        const cache = new Map();
        const cached = function (val) {
            return cache.has(val)
                ? cache.get(val)
                : cache.set(val, fn.call(this, val)) && cache.get(val);
        };
        cached.cache = cache;
        return cached;
    };
    /**
     * Adds a Event Listener for a click event to the element with id id.
     * */
    function addClickEvent(id, func, params) {
        let div = document.getElementById(id);
        if (div)
            div.addEventListener("click", function () { func(...params); }, false);
    }
    /**
     * Adds the given HTML to an element searched by the querySelector search query. Checks, if the element exists.
     * @param eleOrSel Selector of element to add code to, or element directly.
     * @param html HTML to add
     * @param position InsertPosition. default: beforeend (Inside at end)
     */
    function addHTMLinEle(eleOrSel, html, position = "beforeend") {
        let element;
        if (typeof eleOrSel == "string") {
            if (eleOrSel[0] != "." && eleOrSel[0] != "#" && eleOrSel[1] != "#")
                eleOrSel = "#" + eleOrSel;
            element = document.querySelector(eleOrSel);
        }
        else {
            element = eleOrSel;
        }
        if (element)
            element.insertAdjacentHTML(position, html);
    }
    function waitForEle(id) {
        return new Promise(resolve => {
            if (document.getElementById(id))
                return resolve(document.getElementById(id));
            const observer = new MutationObserver(mutations => {
                if (document.getElementById(id)) {
                    resolve(document.getElementById(id));
                    observer.disconnect();
                }
            });
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }
    /**
     * Adds css in the head
     * */
    function addGlobalStyle(css) {
        let head = document.getElementsByTagName('head')[0];
        if (!head)
            return;
        let style = document.createElement('style');
        style.innerHTML = css; //css.replace(/;/g, ' !important;');
        head.appendChild(style);
    }
    /**
     * Handle the API response after inserting or modifying data
     * @param response
     * @param callback optional callback function to execute on success. Default: dataUpdateAfterInsert
     */
    function handleApiPutResponse(response, callback = dataUpdateAfterInsert) {
        if (response.status < 300) // < 200 Informational Response
         {
            callback();
            // do something to celebrate the successfull insertion of the request
        }
        else if (response.status >= 300) // includes error not ==
         {
            console.log("WKCM2: API access error: ", response.text());
            // do something to handle the failure
        }
    }

    var stylesheet$7=".cm-radical {\n  background-color: #0af;\n  background-image: linear-gradient(to bottom, #0af, #0093dd);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#FF00AAFF\", endColorstr=\"#FF0093DD\", GradientType=0);\n}\n\n.cm-kanji {\n  background-color: #f0a;\n  background-image: linear-gradient(to bottom, #f0a, #dd0093);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#FFFF00AA\", endColorstr=\"#FFDD0093\", GradientType=0);\n}\n\n.cm-vocabulary {\n  background-color: #a0f;\n  background-image: linear-gradient(to bottom, #a0f, #9300dd);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#FFAA00FF\", endColorstr=\"#FF9300DD\", GradientType=0);\n}\n\n.cm-reading {\n  background-color: #555;\n  background-image: linear-gradient(to bottom, #555, #333);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#FF555555\", endColorstr=\"#FF333333\", GradientType=0);\n  box-shadow: 0 -2px 0 rgba(0, 0, 0, 0.8) inset;\n}\n\n.cm-request {\n  background-color: #e1aa00;\n  color: black !important;\n  background-image: linear-gradient(to bottom, #e1aa00, #e76000);\n  background-repeat: repeat-x;\n}\n\n.cm-kanji, .cm-radical, .cm-reading, .cm-vocabulary, .cm-request {\n  padding: 1px 4px;\n  color: #fff;\n  font-weight: normal;\n  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);\n  white-space: nowrap;\n  border-radius: 3px;\n  box-shadow: 0 -2px 0 rgba(0, 0, 0, 0.2) inset;\n}\n\nbody {\n  font-size: 100% !important;\n  font-weight: 300 !important;\n  line-height: 1.5 !important;\n  /*Item Page has different background color. Item: #eee. Other: #fff*/\n  /*background-color: ${(isItem ? '#eee' : '#fff')} !important;*/\n  background-color: #fff !important;\n  font-family: \"Ubuntu\", Helvetica, Arial, sans-serif;\n}\n\n/* The scrollbar is ugly af. At least on Chrom*. Hide scrollbar in iframe, but it is still scrolable, if mnem is long.\n   TODO: display scrollbar again, only when mnem is long. (Maybe determine by line count. )\n */\n::-webkit-scrollbar {\n  display: none;\n}\n\n* {\n  -ms-overflow-style: none !important;\n  scrollbar-width: none !important;\n}\n\n/*\n.highlight-kanji.highlight-kanji { ${kanHighlight } }\n.highlight-vocabulary.highlight-vocabulary { ${vocHighlight} }\n.highlight-radical.highlight-radical { ${radHighlight} }\n.highlight-reading.highlight-reading { ${readHighlight} }\n*/";

    // Makes iframe (Mnemonics) pretty. background, hide scrollbar and most importantly highlighting, copied from list page
    // NOTE: fix for different background color on item page
    function iframeCSS() {
        return /*css*/ `<style>
${isItem ?
        stylesheet$7.replaceAll("background-color: #fff", "background-color: #eee") :
        stylesheet$7}
</style>`;
    }
    /**
     * Creates emty Iframe for CM user content later on
     * @param mnemType m, r or meaning, reading
     * */
    function getInitialIframe(mnemType) {
        let iframeId = "cm-iframe-" + mnemType;
        let iframeClass = "cm-mnem-text";
        let initialSrcdoc = getIframeSrcdoc("Loading Community Mnemonic ...");
        let userContentIframe = `<iframe sandbox referrerpolicy='no-referrer' scrolling='auto' frameBorder='0' class='${iframeClass}' id='${iframeId}' srcdoc="${initialSrcdoc}"></iframe>`;
        return userContentIframe;
    }
    /**
     * wraps iframe update, to not update content, if it is the same as the currently displayed.
     * This reduces these annoying flashes, where the whole iframe content disappears for a moment.
     * @param text NOT the whole content, just the message, that will be visible.
     * */
    function updateIframe(mnemType, text, user = null) {
        if (mnemType == null) {
            updateIframe("meaning", text, user);
            updateIframe("reading", text, user);
            return;
        }
        let iframe = document.getElementById(`cm-iframe-${mnemType}`);
        if (iframe == null)
            return;
        let newIframeHtml = getIframeSrcdoc(text, user);
        let newIframeContent = /<body.*?>([\s\S]*)<\/body>/.exec(newIframeHtml)[1];
        let oldIframeContent = /<body.*?>([\s\S]*)<\/body>/.exec(iframe.srcdoc)[1];
        if (newIframeContent == oldIframeContent)
            return;
        iframe.srcdoc = newIframeHtml;
    }
    /**
     * Generates the content of the iframe, that will be set as it's srcdoc property.
     * Needs the WaniKani CSS an the actual body content.
     * */
    function getIframeSrcdoc(text, user = null) {
        if (typeof text != "string") {
            console.log("WKCM2 Error: getIframeSrcdoc, did not get text, but: ", typeof text, text);
            text = "";
        }
        let cssLinks = getWKcss();
        let cssString = "";
        for (const l of cssLinks)
            cssString = cssString + l.outerHTML;
        // override style to fix some oddities
        cssString = cssString + iframeCSS();
        cssString = cssString.replaceAll('"', "'");
        // just to be sure replace those signs here again. But those shouldn't be in the sheet to begin with.
        text = text.replaceAll('<', '&lt;').replaceAll('>', '&gt;')
            .replaceAll('"', '&quot;').replaceAll("'", '&#039;');
        text = Escaping.replaceMarkup(text);
        // text = escape(text);
        let userMsg = "";
        // user can be null, if it is a system message
        if (user != null && typeof user === "string" && user != "") {
            user = user.replaceAll('<', '&lt;').replaceAll('>', '&gt;')
                .replaceAll('"', '&quot;').replaceAll("'", '&#039;');
            userMsg = "by " + Escaping.getUserProfileLink(user);
        }
        if (user == "!")
            userMsg = "This is a request. It should have been deleted after submission of a mnemonic. If you are seeing this, please post in the forum, open an issue on GitHub, or just downvote it. ";
        let srcdoc = `<html><head>${cssString}</head><body><div class='col2'>${text}</div><div id='user-link'>${userMsg}</div></body></html>`;
        return srcdoc;
    }
    // getIframeSrcdoc ▲
    // getIframeSrcdoc helpers ▼
    /**
     * gets all stylesheets in link tags WaniKani uses, for use in iframes.
     * Memoizes result.
     * */
    function getWKcssUncached() {
        let css = [];
        let allLinks = Array.from(document.querySelectorAll("head link"));
        for (const link of allLinks) {
            // @ts-ignore
            if (link?.rel !== "stylesheet")
                continue;
            css.push(link);
        }
        return css;
    }
    const getWKcss = memoize(getWKcssUncached);

    /**
     * Functions to generate Messages, that are displayed.
     * And to process text, like escape deascape, etc.
     */
    // updateCMelements helpers ▼
    function getNoMnemMsg() {
        let msg = `No Community Mnemonic for this item exists yet. [br]Be the first to submit one.`;
        return msg;
    }
    function getRadicalReadingMessage() {
        let msg = `Radicals have no reading. `;
        return msg;
    }
    function getMnemRequestedMsg(users) {
        // TODO: make request color darker red, the more users requested
        let len = users.length;
        let msg = `A Mnemonic was [request]requested[/request] for this item. [br][request]Help the community by being the first to submit one![/request]`;
        if (len === 1)
            msg = `A Mnemonic was [request]requested[/request] by the user [request]${users[0]}[/request]. [br]Help them by being the first to submit one! `;
        else if (len > 1)
            msg = `A Mnemonic was [request]requested[/request] by the users [request]${users.slice(0, -1).join(', ') + ' and ' + users.slice(-1)}[/request]. [br]Help them by being the first to submit one! `;
        return msg;
    }
    /**
     * Replaces HTML encoded characters with their real counterpart.
     * Only used before editing, so that the user does not see the confusing HTML entities.
     * So this only lands in the textbox, not in the HTML, or iframe. It is used for comparisons as well.
     * */
    function decodeHTMLEntities(text) {
        if (text === "" || text == null)
            return "";
        if (!text || typeof text != "string") {
            return;
        }
        let entities = [
            ['amp', '&'], ['#x26', '&'], ['#38', '&'],
            ['apos', '\''], ['#x27', '\''], ['#39', '\''],
            ['#x2F', '/'], ['#47', '/'],
            ['lt', '<'], ['#60', '<'], ['#x3C', '<'],
            ['gt', '>'], ['#62', '>'], ['#x3E', '>'],
            ['nbsp', ' '],
            ['quot', '"'], ['#34', '"'], ['#x22', '"'],
            ['#39', "'"], ['#x27', "'"],
            ['#92', '\\'], ['#x5C', '\\'],
            ['#96', '`'], ['#x60', '`'],
            ['#35', '#'], ['#x23', '#'],
            ['#37', '%'], ['#x25', '%']
        ];
        for (let i = 0, max = entities.length; i < max; ++i)
            text = text.replace(new RegExp('&' + entities[i][0] + ';', 'g'), entities[i][1]);
        return text;
    }

    /**
     * Functions related to the initialization and usage of WKOF
     * https://community.wanikani.com/t/wanikani-open-framework-developer-thread/22231
     */
    // @ts-ignore
    const { wkof } = win;
    function checkWKOF_old() {
        var wkof_version_needed = '1.0.58';
        if (wkof && wkof.version.compare_to(wkof_version_needed) === 'older') {
            if (confirm(scriptName + ' 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 false;
        }
        else if (!wkof) {
            if (confirm(scriptName + ' 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 false;
        }
        else
            return true;
    }
    async function waitForWKOF() {
        // https://codepen.io/eanbowman/pen/jxqKjJ
        let timeout = 2000;
        let start = Date.now();
        return new Promise(waitForFoo); // set the promise object within the ensureFooIsSet object
        // waitForFoo makes the decision whether the condition is met
        // or not met or the timeout has been exceeded which means
        // this promise will be rejected
        function waitForFoo(resolve, reject) {
            if (wkof)
                return resolve(true);
            else if ((Date.now() - start) >= timeout)
                return reject(false);
            else
                setTimeout(waitForFoo.bind(this, resolve, reject), 50);
        }
    }
    /**
     * checks, if script version saved is the same. If it is not, deletes cache.
     * */
    function resetWKOFcache(versionCheck = true) {
        if (versionCheck === false) {
            wkof.file_cache.delete(/^wkcm2-/);
            wkof.file_cache.save("wkcm2-version", WKCM2_version);
            return;
        }
        wkof.file_cache.load("wkcm2-version").then(value => {
            // found
            if (WKCM2_version != value) {
                // regex delete of all wkcm2 saves
                wkof.file_cache.delete(/^wkcm2-/);
                wkof.file_cache.save("wkcm2-version", WKCM2_version);
            }
            return value;
        }, reason => {
            // version not saved, save current version
            wkof.file_cache.save("wkcm2-version", WKCM2_version);
        });
    }

    let WKUser = null;
    let userApiKey = null;
    function setUsername() {
        try {
            if (wkof) {
                try {
                    WKUser = wkof.Apiv2.user;
                    return WKUser;
                }
                catch (err) {
                    console.log("WKCM2: setUsername, ", err);
                    WKUser = wkof.user["username"];
                    return WKUser;
                }
            }
        }
        catch (err) {
            console.log("WKCM2: setUsername, wkof.user  ", err);
        }
        // backup method
        const userClass = "user-summary__username";
        // not working in Lesson & Review
        try {
            WKUser = document.getElementsByClassName(userClass)[0].innerHTML;
        }
        catch (err) {
            throw new Error("WKCM2 Warning: CMUser not set. \n" + err);
        }
        if (WKUser == null || typeof WKUser != "string" || WKUser == "")
            throw new Error("WKCM2 Error: WKUser not set: " + WKUser);
        return WKUser;
    }
    function setApiKey() {
        try {
            userApiKey = wkof.Apiv2.key;
        }
        catch (err) {
            throw new Error("WKCM2 Error: API key not set.");
        }
        return userApiKey;
    }

    /**
     * Create the textbox and all of its buttons for writing mnemonics
     * */
    function getCMForm(mnemType) {
        let CMForm = /*HTML*/ `
<form id="cm-${mnemType}-form" class="cm-form cm-mnem-text" onsubmit="return false">
<div id="cm-${mnemType}-format" class="cm-format">
<div id="cm-format-${mnemType}-bold"      class="cm-btn cm-format-btn cm-format-bold"      title="bold"><b>b</b></div>
<div id="cm-format-${mnemType}-italic"    class="cm-btn cm-format-btn cm-format-italic"    title="italic"><i>i</i></div>
<div id="cm-format-${mnemType}-underline" class="cm-btn cm-format-btn cm-format-underline" title="underline"><u>u</u></div>
<div id="cm-format-${mnemType}-strike"    class="cm-btn cm-format-btn cm-format-strike"    title="strikethrough"><s>s</s></div>
<div id="cm-format-${mnemType}-newline"   class="cm-btn cm-format-btn cm-format-newline"   title="newline"><div>&#92;n</div></div>
<div id="cm-format-${mnemType}-qmark"     class="cm-btn cm-format-btn cm-format-qmark"     title="Question Mark"><div>?</div></div>
<div id="cm-format-${mnemType}-reading"   class="cm-btn cm-format-btn cm-reading"          title="reading">読</div>
<div id="cm-format-${mnemType}-rad"       class="cm-btn cm-format-btn cm-radical"          title="radical">部</div>
<div id="cm-format-${mnemType}-kan"       class="cm-btn cm-format-btn cm-kanji"            title="kanji">漢</div>
<div id="cm-format-${mnemType}-voc"       class="cm-btn cm-format-btn cm-vocabulary"       title="vocabulary">語</div></div>
<fieldset class="note-${mnemType} noSwipe">
<!-- Textarea (Textbox) -->
<textarea id="cm-${mnemType}-text" class="cm-text" maxlength="5000" placeholder="Submit a community mnemonic"></textarea>
<div class="flex items-center"><span id="cm-${mnemType}-chars-remaining" class="block" title="Characters Remaining">5000<i class="fa fa-pencil ml-2"></i></span>
<!-- Save and Cancel Buttons -->
<button type="submit" id="cm-${mnemType}-save" class="cm-btn cm-save-highlight disabled:cursor-not-allowed disabled:opacity-50">Save</button>
<button type="button" id="cm-${mnemType}-cancel" class="cm-btn cm-cancel-highlight disabled:cursor-not-allowed disabled:opacity-50">Cancel</button></div>

</fieldset>
</form>`;
        return CMForm;
    }

    /**
     * Functions to generate the mnemonic div
     * but also to modify it, like toggle buttons
     */
    function getHeader(mnemType) {
        return `Community ${mnemType.charAt(0).toUpperCase() + mnemType.slice(1)} Mnemonic`;
    }
    /**
     * Creates the initial HTML code for the individual Mnemonic types, including Iframes. But also all Buttons.
     * Does not include content
     */
    function getCMdivContent(mnemType) {
        const userContentIframe = getInitialIframe(mnemType);
        let header = getHeader(mnemType);
        // ◄►
        let content = 
        /*HTML*/ `
<div id="cm-${mnemType}" class="cm-content">
    <!--  <h2 class="subject-section__subtitle">${header}</h2>  -->
    <div id="cm-${mnemType}-prev"        class="fa-solid fa-angle-left cm-btn cm-prev disabled"><span></span></div>
    ${userContentIframe}
    <div id="cm-${mnemType}-next"         class="fa-solid fa-angle-right cm-btn cm-next disabled"><span></span></div>
    <div id="cm-${mnemType}-info"         class="cm-info">

    <div id="cm-${mnemType}-user-buttons" class="cm-user-buttons">
        <div id="cm-${mnemType}-edit"         class="cm-btn cm-edit-highlight cm-small-btn disabled" >Edit</div>
        <div id="cm-${mnemType}-delete"       class="cm-btn cm-delete-highlight cm-small-btn disabled">Delete</div>
        <div id="cm-${mnemType}-request"      class="cm-btn cm-request-highlight cm-small-btn disabled">Request</div>
    </div>

    <div class="cm-score">Score: <span id="cm-${mnemType}-score-num" class="cm-score-num">0</span></div>
    <div id="cm-${mnemType}-upvote"       class="cm-btn cm-upvote-highlight disabled">Upvote <i class="fa-solid fa-chevrons-up"></i></div>
    <div id="cm-${mnemType}-downvote"     class="cm-btn cm-downvote-highlight disabled">Downvote <i class="fa-solid fa-chevrons-down"></i></div>
    <div id="cm-${mnemType}-submit"       class="cm-btn cm-submit-highlight disabled">Submit Yours</div></div>
</div>
`;
        return content;
    }
    function setScore(mnemType, score) {
        let scoreEle = document.getElementById(`cm-${mnemType}-score-num`);
        if (scoreEle != null) {
            // make sure score is number and not (potentially harmful) string
            if (!Number.isNaN(Number(score)))
                scoreEle.innerText = String(score);
            else
                scoreEle.innerText = "0";
        }
    }
    class Buttons {
        /**
         * Enable/Disable all buttons that depend on the Mnemonic being by the user, or not.
         * @param owner boolean. Owner of mnem: True, else False
         * */
        static toggleUserButtons(mnemType, owner) {
            if (owner == true) {
                removeClass(`cm-${mnemType}-edit`);
                removeClass(`cm-${mnemType}-delete`);
                addClass(`cm-${mnemType}-request`);
                addClass(`cm-${mnemType}-upvote`);
                addClass(`cm-${mnemType}-downvote`);
            }
            else if (owner == false) {
                addClass(`cm-${mnemType}-edit`);
                addClass(`cm-${mnemType}-delete`);
                addClass(`cm-${mnemType}-request`);
                removeClass(`cm-${mnemType}-upvote`);
                removeClass(`cm-${mnemType}-downvote`);
            }
        }
        /**
         * Disables or enables the arrows for prev and next mnem. Depending on amount of mnems available and active one.
         * */
        static toggleArrows(mnemType, length, index) {
            let left = `cm-${mnemType}-prev`;
            let right = `cm-${mnemType}-next`;
            // make array length match index, now both start at 0
            addClass(left);
            addClass(right);
            if (length > 0 && length != null)
                length = length - 1;
            else
                return;
            if (length > index)
                removeClass(right);
            if (length > 0 && index > 0)
                removeClass(left);
        }
        /**
         * Enables/Disables voring buttons depending on users vote
         * votesJson["mnemUser"][mnemIndex]{WKuser} <-- contains vote
         * */
        static toggleVotes(mnemType, votesJson, mnemUser, mnemIndex) {
            if (votesJson == null || mnemUser == WKUser)
                return;
            const downv = `cm-${mnemType}-downvote`;
            const upv = `cm-${mnemType}-upvote`;
            try {
                const userVote = Number(votesJson[mnemUser][mnemIndex][WKUser]);
                if (userVote >= 1)
                    addClass(upv);
                else if (userVote <= -1)
                    addClass(downv);
            }
            catch (err) {
                // catch votesJson access in case mnemUser or WKUser do not have and entries.
                //// console.log("WKCM2 Error in toggleVotes, mnem_div.ts:", err);
            }
        }
        static disableButtons(mnemType) {
            addClass(`cm-${mnemType}-edit`);
            addClass(`cm-${mnemType}-delete`);
            addClass(`cm-${mnemType}-request`);
            addClass(`cm-${mnemType}-upvote`);
            addClass(`cm-${mnemType}-downvote`);
            addClass(`cm-${mnemType}-submit`);
            addClass(`cm-${mnemType}-prev`);
            addClass(`cm-${mnemType}-next`);
        }
        static editCM(mnemType) {
            if (currentMnem.mnem[mnemType] == undefined)
                return;
            if (currentMnem.currentUser[mnemType] == undefined)
                return;
            if (currentMnem.currentUser[mnemType] !== WKUser)
                return;
            Textarea.submitting = false;
            let iframe = document.getElementById(`cm-iframe-${mnemType}`);
            if (!iframe)
                return;
            Buttons.disableButtons(mnemType);
            iframe.outerHTML = getCMForm(mnemType);
            Textarea.initEditButtons(mnemType);
            let textarea = document.getElementById(`cm-${mnemType}-text`);
            if (textarea) {
                // replace HTML entities, so user actually sees the sign, they used before. Like < instead of &#60;
                textarea.value = decodeHTMLEntities(currentMnem.mnem[mnemType]);
            }
        }
        static deleteCM(mnemType) {
            if (!confirm("Your mnemonic will be deleted. This can not be undone! Are you sure?"))
                return;
            addClass(`cm-${mnemType}-delete`);
            addClass(`cm-${mnemType}-edit`);
            if (currentMnem.mnem[mnemType] == undefined)
                return;
            if (currentMnem.currentUser[mnemType] !== WKUser)
                return;
            let item = getItem();
            let shortType = getShortItemType(getItemType());
            deleteMnemonic(mnemType, item, shortType).then(response => {
                handleApiPutResponse(response);
            }).catch(reason => console.log("WKCM2: requestCM failed: ", reason));
        }
        static requestCM(mnemType) {
            addClass(`cm-${mnemType}-request`);
            let shortType = getShortItemType(getItemType());
            requestMnemonic(mnemType, getItem(), shortType).then(response => {
                handleApiPutResponse(response);
            }).catch(reason => console.log("WKCM2: requestCM failed: ", reason));
        }
        static voteCM(mnemType, vote) {
            if (!currentMnem.currentUser)
                return;
            if (typeof currentMnem.currentUser[mnemType] != "string")
                return;
            if (!currentMnem.mnemIndex)
                return;
            if (Number.isNaN(Number(currentMnem.mnemIndex[mnemType])))
                return;
            let item = getItem();
            let shortType = getShortItemType(getItemType());
            if (Number(vote) >= 1)
                addClass(`cm-${mnemType}-upvote`);
            else if (Number(vote) <= -1)
                addClass(`cm-${mnemType}-downvote`);
            voteMnemonic(mnemType, item, shortType, vote).then(response => {
                handleApiPutResponse(response, function () {
                    return dataUpdateAfterInsert(undefined, undefined, undefined, undefined, undefined, currentMnem.mnemIndex[mnemType], mnemType);
                });
            }).catch(reason => console.log("WKCM2: requestCM failed:\n", reason));
        }
        static submitCM(mnemType) {
            // "Submit Yours" Button
            let iframe = document.getElementById("cm-iframe-" + mnemType);
            if (!iframe)
                return;
            // save edit mode (whether editing or submitting new)
            Textarea.submitting = true;
            iframe.outerHTML = getCMForm(mnemType);
            // Buttons.disableButtons(mnemType);
            Buttons.disableButtons(mnemType);
            Textarea.initEditButtons(mnemType);
        }
        static initInteractionButtons(mnemType) {
            addClickEvent(`cm-${mnemType}-edit`, Buttons.editCM, [mnemType]);
            addClickEvent(`cm-${mnemType}-delete`, Buttons.deleteCM, [mnemType]);
            addClickEvent(`cm-${mnemType}-request`, Buttons.requestCM, [mnemType]);
            addClickEvent(`cm-${mnemType}-upvote`, Buttons.voteCM, [mnemType, "1"]);
            addClickEvent(`cm-${mnemType}-downvote`, Buttons.voteCM, [mnemType, "-1"]);
            addClickEvent(`cm-${mnemType}-submit`, Buttons.submitCM, [mnemType]);
            addClickEvent(`cm-${mnemType}-prev`, switchCM, [mnemType, -1]);
            addClickEvent(`cm-${mnemType}-next`, switchCM, [mnemType, 1]);
        }
    }

    /**
     * Functions related to the update of displayed mnemonics
     * and the fetch of data belonging to displayed mnemonics
     */
    // Namespaces for global variables
    var currentMnem;
    (function (currentMnem) {
        // currentMnem.mnem saves the last refreshed mnem globally for edit & save functions
        // Reading from HTML doesn't really work, because characters have been unescaped.
        currentMnem.mnem = {};
        // Index of active mnem, of all mnems. (update & vote) {meaning: 0, reading: 0}
        currentMnem.mnemIndex = {};
        // user of currently displayed mnem. (edit & vote)
        currentMnem.currentUser = {};
        // Index of active mnem, of the (author) users mnems. (editSave) {meaning: 0, reading: 0}
        currentMnem.userIndex = {};
    })(currentMnem || (currentMnem = {}));
    /**
     * fetches Data, if not given. Will update at index given. updates both given mnemTypes, or just one, if string.
     * Then calls updateCMelements, which does the visual update of the content and buttons and stuff.
     * @param dataJson needed to bypass recursive getMnemonic call, once data got loaded.
     * False because it can be null, when no mnem is available. False: refetch from API
     * @param mnemType array by default to make calling the function more convenient. Will be executed for both values in array.
     * @param index index of Mnem to use
     * */
    function updateCM(dataJson = false, mnemType = ["meaning", "reading"], index = 0) {
        // display loading message
        /*
        if (typeof mnemType == "object")
            for (let ele of mnemType)
                updateIframe(ele, "Loading Community Mnemonic ...")
        */
        let type = getItemType();
        if (dataJson || dataJson === null) {
            if (typeof mnemType === "string")
                mnemType = [mnemType];
            else {
                // reset global mnem storage for save&editing when updating both types
                // use mnemType as key
                // mnemonics, for edit, save & cancel
                currentMnem.mnem = {};
                // user of currently displayed mnem. (edit & vote)
                currentMnem.currentUser = {};
                // Index of active mnem, of all mnems. (No matter user)
                currentMnem.mnemIndex = {};
                // Index of active mnem, of the users mnems. (Also other users; mnemUser)
                currentMnem.userIndex = {};
            }
            for (let ele of mnemType) // @ts-ignore
                updateCMelements(ele, type, dataJson, index);
        }
        else {
            let item = getItem();
            getData(item, getShortItemType(type)).then((dataJson) => {
                if (dataJson !== undefined)
                    updateCM(dataJson, mnemType, index);
            }).catch((reason) => {
                console.log("WKCM2: updateCM error: ", reason);
                setTimeout(function () { updateCM(false, mnemType, index); }, refetchTimeout);
            });
        }
    }
    /**
     * function that is doing the updating of the iframe contents.
     * Getting called in updateCM from data promise to reduce clutter in nested .then()
     * @param mnemType reading or meaning
     * @param type kanji, vocabulary or radical
     * @param dataJson json containing data from the DB:
     * {Type: 'k', Item: '活', Meaning_Mnem: {...}, Reading_Mnem: '!', Meaning_Score: {...}, ...}
     * @param index Global Index of mnemonic.
     * */
    function updateCMelements(mnemType, type, dataJson, index = 0) {
        // check if cm type exists in HTML
        if (!document.querySelector("#cm-" + mnemType))
            return;
        // Radicals only have meaning, no reading. Disable Reading buttons and update Reading message
        if (mnemType == "reading" && type == "radical") {
            Buttons.disableButtons(mnemType);
            updateIframe(mnemType, getRadicalReadingMessage());
            return;
        }
        // initialize, set and/or reset index
        currentMnem.mnemIndex[mnemType] = index;
        // if mnemJson is undefined or null, no mnemonic exists for this item/type combo.
        //reset score display
        setScore(mnemType, 0);
        Buttons.disableButtons(mnemType);
        removeClass(`cm-${mnemType}-submit`);
        currentMnem.currentUser[mnemType] = null;
        currentMnem.mnem[mnemType] = null;
        if (dataJson != null) {
            // sanity check if Mnems are filled, or just contain empty jsons ("" keys length is 0)
            if ((Object.keys(dataJson["Meaning_Mnem"]).length == 0 || dataJson["Meaning_Mnem"] == "{}") &&
                (Object.keys(dataJson["Reading_Mnem"]).length == 0 || dataJson["Reading_Mnem"] == "{}")) {
                updateIframe(mnemType, getNoMnemMsg());
                removeClass(`cm-${mnemType}-request`);
                return;
            }
            let mnemSelector = mnemType.charAt(0).toUpperCase() + mnemType.slice(1) + "_Mnem";
            let scoreSelector = mnemType.charAt(0).toUpperCase() + mnemType.slice(1) + "_Score";
            let votesSelector = mnemType.charAt(0).toUpperCase() + mnemType.slice(1) + "_Votes";
            let mnemJson = jsonParse(dataJson[mnemSelector]);
            let scoreJson = jsonParse(dataJson[scoreSelector]); // Score != Votes
            let votesJson = jsonParse(dataJson[votesSelector]);
            // no mnem available for current item
            if (mnemJson == null) {
                updateIframe(mnemType, getNoMnemMsg());
                removeClass(`cm-${mnemType}-request`);
            }
            // request JSON: {"!": ["Anonymous", "Dakes"]}
            else if (Object.keys(mnemJson)[0] == "!" && Object.keys(mnemJson).length == 1) {
                updateIframe(mnemType, getMnemRequestedMsg(mnemJson["!"]));
                if (mnemJson["!"].includes(WKUser))
                    addClass(`cm-${mnemType}-request`);
                else
                    removeClass(`cm-${mnemType}-request`);
                // disable request button, if user already requested
            }
            // default case. Mnem available
            else {
                Buttons.toggleArrows(mnemType, getMnemCount(mnemJson), index);
                // save dataJson to pseodo global, to prevent reloading from cache. (is faster [only a bit])
                switchCM.dataJson = dataJson;
                let currentJsonUser = getNthDataUser(mnemJson, index);
                updateIframe(mnemType, ...currentJsonUser); // (mnemType, mnem, user)
                // to know which mnem to edit.
                currentMnem.currentUser[mnemType] = currentJsonUser[1];
                currentMnem.userIndex[mnemType] = getUserIndex(mnemJson, index, currentMnem.currentUser[mnemType]);
                let score = 0;
                try {
                    score = scoreJson[currentMnem.currentUser[mnemType]][currentMnem.userIndex[mnemType]];
                }
                catch (err) {
                    // ignore in cases: ScoreJson is null (empty). And user entry does not exist.
                }
                setScore(mnemType, score);
                Buttons.toggleUserButtons(mnemType, currentJsonUser[1] == WKUser);
                currentMnem.userIndex[mnemType] = getUserIndex(mnemJson, index, currentMnem.currentUser[mnemType]);
                Buttons.toggleVotes(mnemType, votesJson, currentJsonUser[1], currentMnem.userIndex[mnemType]);
                // save for editing only if the currently displayed mnem is by user
                if (currentJsonUser[1] == WKUser)
                    currentMnem.mnem[mnemType] = currentJsonUser[0];
                // disable submit button if user submitted too many mnems
                if (getUserMnemCount(mnemJson, WKUser) >= mnemMaxCount)
                    addClass(`cm-${mnemType}-submit`);
            }
        }
        // no mnem available for both items
        else {
            updateIframe(mnemType, getNoMnemMsg()); // (mnem, user)
            removeClass(`cm-${mnemType}-request`);
            currentMnem.mnem[mnemType] = null;
        }
    }
    // updateCMelements ▲
    /**
     * Switch displayed mnemonic to next or previous
     * @param {*} mnemType reading/meaning
     * @param {*} summand to add to index (usually -1/+1)
     */
    function switchCM(mnemType, summand) {
        let idx = 0;
        if (!Number.isNaN(Number(currentMnem.mnemIndex[mnemType])))
            idx = Number(currentMnem.mnemIndex[mnemType]);
        let dataJson = false;
        if (Object.keys(switchCM.dataJson).length != 0)
            dataJson = switchCM.dataJson;
        let newIdx = idx + summand;
        if (newIdx < 0) {
            console.log("WKCM2 Error: switchCM; new Index is < 0: ", newIdx, idx, summand);
            newIdx = 0;
        }
        updateCM(dataJson, mnemType, newIdx);
        switchCM.dataJson = {};
    }
    (function (switchCM) {
        switchCM.dataJson = {};
    })(switchCM || (switchCM = {}));
    /**
     * @param mnemJson json of either Meaning or Reading mnemonic. NOT whole data json
     * @return total number of mnemonics
     * */
    function getMnemCount(mnemJson) {
        if (mnemJson == null)
            return 0;
        let mnemCount = 0;
        for (let user in mnemJson) {
            mnemCount = mnemCount + mnemJson[user].length;
        }
        return mnemCount;
    }
    /**
     * @param mnemJson json of either Meaning or Reading mnemonic. NOT whole data json
     * @param user user whose mnems to count
     * @return number of mnemonics user submitted
     * */
    function getUserMnemCount(mnemJson, user) {
        if (mnemJson == null)
            return 0;
        if (!mnemJson[user])
            return 0;
        return mnemJson[user].length;
    }
    /**
     * Get data point at position n and return in array with user (owner of data) in second element.
     * @param innerJson inner json of data. either Meaning or Reading mnemonic. Or Votes. NOT whole data json.
     * MUST be in the form: {"user": [1, 2, 3], "user2": [4, 5, 6]}
     * @param n number of mnem to get. (Global index)
     * @return Array of nth data point in json and user: [data, user]
     * */
    function getNthDataUser(innerJson, n) {
        if (n < 0) {
            console.log("WKCM2 Error: getNthDataUser got index < 0: ", n);
            n = 0;
        }
        if (innerJson == null)
            return [null, null];
        let count = 0;
        for (let user in innerJson) {
            for (let data of innerJson[user]) {
                if (count == n)
                    return [data, user];
                ++count;
            }
        }
        return [null, null];
    }
    /**
     * Get the index of the users individual mnem from the global mnem index.
     * Relevant for editing mnem, to overwrite the correct one in the sheet.
     * */
    function getUserIndex(mnemJson, n, user) {
        if (mnemJson == null)
            return 0;
        if (mnemJson[user] == null)
            return 0;
        let count = 0;
        for (let currentUser in mnemJson) {
            let userCount = 0;
            for (let data of mnemJson[currentUser]) {
                if (count == n && currentUser == user)
                    return userCount;
                ++userCount;
                ++count;
            }
        }
        return 0;
    }
    /**
     * Initializes Button functionality with EventListener click
     * */
    function initButtons(mnemType) {
        //// mnemType = getFullMnemType(mnemType);
        Buttons.initInteractionButtons(mnemType);
        //? Textarea.initEditButtons(mnemType);
    }
    /**
     * Textarea for writing Mnemonics
     */
    class Textarea {
        /**
         * Save button during Mnemonic writing. Submitting and edit.
         * Submit Mnemonic to Database Sheet.
         * */
        static editSaveCM(mnemType) {
            let textarea = Textarea.getTextArea(mnemType);
            if (!textarea)
                return;
            let newMnem = Escaping.replaceInNewMnem(textarea.value);
            // if newMnem empty "", nothing to save
            if (!newMnem)
                return;
            // if currentMnem.mnem[mnemType] wasn't set, no mnem exists for this, then set it to empty string.
            if (!currentMnem.mnem[mnemType])
                currentMnem.mnem[mnemType] = "";
            // nothing to save
            if (newMnem == decodeHTMLEntities(currentMnem.mnem[mnemType]))
                return;
            addClass(`cm-${mnemType}-save`);
            let type = getItemType();
            let item = getItem();
            // index of the mnemonic for this user in the DB. Needed to update the correct one
            let mnemUserIndexDB = -1;
            mnemUserIndexDB = currentMnem.userIndex[mnemType];
            // append new mnem if mode is submit
            if (Textarea.submitting)
                mnemUserIndexDB = -1;
            // restore iframe. needed by dataUpdate after insert.
            let editForm = document.getElementById(`cm-${mnemType}-form`);
            if (editForm) {
                editForm.outerHTML = getInitialIframe(mnemType);
                Buttons.disableButtons(mnemType);
            }
            // api call to put data
            submitMnemonic(mnemType, item, getShortItemType(type), mnemUserIndexDB, newMnem)
                .then(a => {
                addClass(`cm-${mnemType}-cancel`);
                // with undefined, uses default parameter.
                dataUpdateAfterInsert(undefined, undefined, undefined, undefined, undefined, currentMnem.mnemIndex[mnemType], mnemType);
            })
                .catch(reason => console.log("WKCM2: editSaveCM failed: ", reason));
            Textarea.submitting = false;
            currentMnem.userIndex[mnemType] = 0;
            currentMnem.mnem[mnemType] = null;
        }
        /**
         * Cancel button during Mnemonic writing. Submitting and edit.
         * Prompts for confirmation, if content is edited or not empty.
         * */
        static editCancelCM(mnemType) {
            let textarea = Textarea.getTextArea(mnemType);
            let cancelConfirm = true;
            // only open dialog if it has content and it was edited
            if (textarea) // && currentMnem.mnem[mnemType])
                if (textarea.value && decodeHTMLEntities(currentMnem.mnem[mnemType]) !== textarea.value)
                    cancelConfirm = confirm("Your changes will be lost. ");
            if (cancelConfirm) {
                let editForm = document.getElementById(`cm-${mnemType}-form`);
                if (!editForm)
                    return;
                Textarea.submitting = false;
                editForm.outerHTML = getInitialIframe(mnemType);
                updateCM(false, mnemType, currentMnem.mnemIndex[mnemType]);
            }
            currentMnem.mnem[mnemType] = {};
        }
        /**
         * Insert the tag "tag" in mnem writing field, at current cursor position, or around highlighted text.
         * */
        static insertTag(mnemType, tag) {
            let textarea = Textarea.getTextArea(mnemType);
            if (!textarea)
                return;
            let selectedText = Textarea.getSelectedText(textarea);
            let insertText = "[" + tag + "]" + selectedText + "[/" + tag + "]";
            if (textarea.setRangeText) {
                //if setRangeText function is supported by current browser
                textarea.setRangeText(insertText);
            }
            else {
                textarea.focus();
                document.execCommand('insertText', false /*no UI*/, insertText);
            }
            textarea.focus();
        }
        /**
         * Insert the text in mnem writing field, at current cursor position.
         * */
        static insertText(mnemType, text) {
            let textarea = Textarea.getTextArea(mnemType);
            if (!textarea)
                return;
            if (textarea.setRangeText) {
                //if setRangeText function is supported by current browser
                textarea.setRangeText(text);
            }
            else {
                textarea.focus();
                document.execCommand('insertText', false /*no UI*/, text);
            }
            textarea.focus();
        }
        static initEditButtons(mnemType) {
            mnemType = mnemType;
            addClickEvent(`cm-${mnemType}-save`, Textarea.editSaveCM, [mnemType]);
            addClickEvent(`cm-${mnemType}-cancel`, Textarea.editCancelCM, [mnemType]);
            addClickEvent(`cm-format-${mnemType}-bold`, Textarea.insertTag, [mnemType, "b"]);
            addClickEvent(`cm-format-${mnemType}-italic`, Textarea.insertTag, [mnemType, "i"]);
            addClickEvent(`cm-format-${mnemType}-underline`, Textarea.insertTag, [mnemType, "u"]);
            addClickEvent(`cm-format-${mnemType}-strike`, Textarea.insertTag, [mnemType, "s"]);
            addClickEvent(`cm-format-${mnemType}-newline`, Textarea.insertText, [mnemType, "[n]"]);
            addClickEvent(`cm-format-${mnemType}-qmark`, Textarea.insertText, [mnemType, "?"]);
            addClickEvent(`cm-format-${mnemType}-reading`, Textarea.insertTag, [mnemType, "read"]);
            addClickEvent(`cm-format-${mnemType}-rad`, Textarea.insertTag, [mnemType, "rad"]);
            addClickEvent(`cm-format-${mnemType}-kan`, Textarea.insertTag, [mnemType, "kan"]);
            addClickEvent(`cm-format-${mnemType}-voc`, Textarea.insertTag, [mnemType, "voc"]);
        }
        // Button functionality ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
        static getTextArea(mnemType) {
            return document.getElementById(`cm-${mnemType}-text`);
        }
        static getSelectedText(textArea) {
            let text = textArea.value;
            let indexStart = textArea.selectionStart;
            let indexEnd = textArea.selectionEnd;
            return text.substring(indexStart, indexEnd);
        }
    }
    // TODO: check if needed
    // ? @deprecated
    // true, if mnem is currently being written. (Textarea active)
    Textarea.submitting = false;

    /**
     * Functions related to local data update and processing
     */
    /**
     * Update the displayed Mnemonic & cache in the background.
     * If a new one is available. If no new one is available does noting.
     * @param item item to update (星). Will be set if null.
     * @param item type of item (kanji) Will be set if null.
     * @param cachedData old data json (currently in cache) will be updated, if new version is different.
     * @param wait number of ms to wait with execution, or false. (Because after insertion into sheet it takes a moment for the updated version to be returned. Annoyingly even when using promises. )
     * */
    async function dataBackgroundUpdate(item = null, type = null, cachedData = null, wait = false) {
        if (wait && typeof wait == "number") {
            setTimeout(function () {
                dataBackgroundUpdate(item, type, cachedData, wait = false);
            }, wait);
            return;
        }
        if (item == null)
            item = getItem();
        if (type == null)
            type = getItemType();
        let identifier = getCacheId(item, type);
        if (cacheExpired(identifier)) {
            fetchData(item, type).then(responseJson => {
                // fetch worked
                // wkof.file_cache.save(identifier, responseJson);
                let reponseJsonCopy = JSON.parse(JSON.stringify(responseJson));
                // updateCM(reponseJsonCopy);
                if (!isEqualsJson(cachedData, responseJson)) {
                    wkof.file_cache.save(identifier, responseJson);
                    updateCM(reponseJsonCopy);
                }
                return responseJson;
            }).catch(reason => {
                // fetch failed
                // TODO: handle failed fetch
                console.log("WKCM2: Error, dataBackgroundUpdate, Fetch of data from spreadsheet failed: " + reason);
            });
        }
    }
    // dataBackgroundUpdate ▲
    /**
     * Update the displayed Mnemonic & cache. It will be called after an submission to the sheet.
     * So compared to dataBackgroundUpdate it expects an update, and will repeat the fetch a few times, until it gives up.
     * After the insertion into the sheet it takes a few moments (~1-2s) until the new data is returned.
     * @param item item to update (星). Will be set if null.
     * @param type of item (kanji) Will be set if null.
     * @param cachedData old data json (currently in cache) will be updated, if new version is different. Will be set if false.
     * @param tries number of times to retry before giving up, waits "wait"ms between executions.
     * @param wait number of ms to wait with execution, or false. (Because after insertion into sheet it takes a moment for the updated version to be returned. Annoyingly even when using promises. )
     * @param index Index to use for displayed mnemonic. So user sees their changed mnem directly after submission. Should only be used togetcher with mnemType.
     * @param mnemType just as index, mnemType to pass through.
     * */
    function dataUpdateAfterInsert(item = null, type = null, cachedData = false, tries = 10, wait = 1000, index = 0, mnemType = undefined) {
        if (tries < 0) {
            console.log("WKCM2: dataUpdateAfterInsert, Maximum number of tries reached, giving up. Currently displayed Mnemonic will not be updated. ");
            updateCM(undefined, mnemType, index);
            return Promise.resolve();
        }
        if (item == null)
            item = getItem();
        if (type == null)
            type = getItemType();
        let identifier = getCacheId(item, type);
        if (cachedData === false) {
            wkof.file_cache.load(identifier).then(cachedData => dataUpdateAfterInsert(item, type, cachedData, tries, wait, index, mnemType))
                .catch(err => {
                dataUpdateAfterInsert(item, type, null, tries, wait, index, mnemType);
            });
            return Promise.resolve();
        }
        else if (typeof cachedData != "boolean") {
            fetchData(item, type).then(responseJson => {
                // fetch worked
                let reponseJsonCopy = JSON.parse(JSON.stringify(responseJson));
                // @ts-ignore
                if (!isEqualsJson(cachedData, responseJson)) {
                    wkof.file_cache.save(identifier, responseJson);
                    updateCM(reponseJsonCopy, mnemType, index);
                }
                else {
                    // retry after "wait" ms
                    setTimeout(function () {
                        dataUpdateAfterInsert(item, type, cachedData, --tries, wait + 250, index, mnemType);
                    }, wait);
                }
            }).catch(reason => {
                // fetch failed
                // TODO: handle failed fetch
                console.log("WKCM2: Error, dataUpdateAfterInsert, Fetch of data from spreadsheet failed: " + reason);
            });
        }
    }
    // dataUpdateAfterInsert ▲
    /**
     * wraps JSON.parse
     * @return JSON, null if invalid
     * */
    function jsonParse(jsonString) {
        let newJson = null;
        if (jsonString != "" && typeof jsonString == "string") {
            try {
                newJson = JSON.parse(jsonString);
                if (jsonParse.refetchCounter > 0)
                    jsonParse.refetchCounter = 0;
            }
            catch (err) {
                console.log("WKCM2: jsonParse, got invalid json string: ", jsonString);
                // sometimes fetch was faster then score calculation => #ERROR!
                // if found retry. But only a few times. (There may really be #ERROR! in DB)
                if (jsonString.includes("#ERROR!") || jsonString.includes("#NAME?")) {
                    if (jsonParse.refetchCounter < 5)
                        deleteCacheItem().then(r => {
                            getData();
                            jsonParse.refetchCounter++;
                        });
                }
            }
        }
        // for consistency if empty json, convert to null
        if (newJson != null)
            if (typeof newJson == "object")
                if (Object.keys(newJson).length == 0)
                    newJson = null;
        return newJson;
    }
    (function (jsonParse) {
        jsonParse.refetchCounter = 0;
    })(jsonParse || (jsonParse = {}));
    function isEqualsJson(obj1, obj2) {
        if (obj1 == null && obj2 == null)
            return true;
        else if (obj1 == null || obj2 == null)
            return false;
        let keys1 = Object.keys(obj1);
        let keys2 = Object.keys(obj2);
        //return true when the two json has same length and all the properties has same value key by key
        return keys1.length === keys2.length && Object.keys(obj1).every(key => obj1[key] == obj2[key]);
    }
    function hasRequest(dataJson) {
        if (dataJson == null)
            return false;
        if (dataJson["Meaning_Mnem"][2] == "!")
            return true;
        if (dataJson["Reading_Mnem"][2] == "!")
            return true;
        return false;
    }
    function mnemAvailable(dataJson) {
        if (dataJson == null)
            return false;
        if (dataJson["Meaning_Mnem"][2] && dataJson["Meaning_Mnem"][2] != "!")
            return true;
        if (dataJson["Reading_Mnem"][2] && dataJson["Reading_Mnem"][2] != "!")
            return true;
        return false;
    }
    /**
     * Functions for Escaping/Unescaping User content.
     * Or generating Strings with User content.
     */
    class Escaping {
        /**
         * Replace stuff, that should not land in DB. Or maybe unintended input by user.
         * Technically redundant, since this is handled better by apps script.
         * */
        static replaceInNewMnem(text) {
            // is handled by insertion apps script as well. 
            // replace newlines with markup
            text = text.replace(/\n/g, '[n]').replace(/\r/g, '[n]');
            return text;
        }
        /**
         * Replace custom markup with actual HTML tags for highlighting.
         * Those are the only HTML tags, that should land in the iframe.
         * */
        static replaceMarkup(text) {
            const list = ["b", "i", "u", "s", "br"];
            for (const ele of list) {
                text = text.replaceAll("[" + ele + "]", "<" + ele + ">");
                text = text.replaceAll("[/" + ele + "]", "</" + ele + ">");
            }
            // [/span] used as closing tag for legacy data in db.
            text = text.replaceAll("[/span]", `</span>`);
            text = text.replaceAll("[kan]", `<span class="cm-kanji">`);
            text = text.replaceAll("[/kan]", `</span>`);
            text = text.replaceAll("[voc]", `<span class="cm-vocabulary">`);
            text = text.replaceAll("[/voc]", `</span>`);
            text = text.replaceAll("[rad]", `<span class="cm-radical">`);
            text = text.replaceAll("[/rad]", `</span>`);
            text = text.replaceAll("[read]", `<span class="cm-reading">`);
            text = text.replaceAll("[/read]", `</span>`);
            text = text.replaceAll("[request]", `<span class="cm-request">`);
            text = text.replaceAll("[/request]", `</span>`);
            text = text.replaceAll("[n]", `<br>`);
            text = text.replaceAll("[br]", `<br>`);
            // legacy replace \n, that are already in the DB. (saved literally as \\n)
            text = text.replaceAll("\n", `<br>`);
            text = text.replaceAll("\\n", `<br>`);
            return text;
        }
        static getUserProfileLink(user) {
            // Don't give Anonymous a profile link
            if (typeof user != "string" || user == "")
                return "";
            if (user == "Anonymous")
                return `<a>Anonymous</a>`;
            else if (user == "!")
                return "";
            else
                return `<a href="https://www.wanikani.com/users/${user}" target="_blank" >${user}</a>`;
        }
    }

    /**
     * Functions related to fetching and pulling data to and from the Google Sheets API.
     */
    /**
     * Abstraction layer from direct data fetch,
     * to make use of caches to make the script more responsive.
     * @param item Current Item. Optional, gets it if not given.
     * @param type Current Item Type (short), optional. gets it if not given.
     * @param fetchOnMiss False: default. Refetch from API on cache miss.
     * If false, interprets cache miss as not in DB and fills cache with null.
     * @returns Promise resolving to DataJson or null.
     */
    async function getData(item, type, fetchOnMiss = false) {
        if (type == undefined)
            type = getShortItemType(getItemType());
        if (item == undefined || item == "")
            item = getItem();
        if (item == null || type == null) {
            throw new Error("WKCM2: getData, item or type is null. " + item + type);
        }
        let identifier = getCacheId(item, type);
        // get from wkof cache
        let data = wkof.file_cache.load(identifier).then((value) => {
            getData.misses = 0;
            if (cacheExpired(identifier, cacheDayMaxAge))
                dataBackgroundUpdate(item, type, value);
            return value;
        }, (reason) => {
            // cache miss
            if (!fetchOnMiss) {
                wkof.file_cache.save(identifier, null);
                return null;
            }
            // fetch data from db, put in cache and return
            // ? maybe remove? is not used anyway
            getData.misses++;
            // protection against deadlock "just in case" something somewhere else at some point breaks.
            if (getData.misses > 1) {
                if (getData.misses > 10)
                    throw new Error("WKCM2: There was a problem with fetching the Mnemonic Data.: " + reason);
                return null;
            }
            return fetchData(item, type).then(responseJson => {
                // fetch worked
                wkof.file_cache.save(identifier, responseJson);
                let reponseJsonCopy = JSON.parse(JSON.stringify(responseJson));
                // only toggle visual update if the original item is still displayed.
                let curTyIt = getShortItemType(getItemType()) + getItem();
                let prevTyIt = getShortItemType(type) + item;
                if (curTyIt == prevTyIt)
                    updateCM(reponseJsonCopy);
                return responseJson;
            }).catch(reason => {
                // fetch failed
                // TODO: handle failed fetch
                console.log("WKCM2: Error, getData, Fetch of data from spreadsheet failed: " + reason);
                // create and return "Error" object, to signale failed fetch and display that.
                return null;
            });
        });
        return data;
    }
    (function (getData) {
        // static miss counter, to protect from infinite cache miss loop (only triggered when an error with the apps script exists)
        getData.misses = 0;
    })(getData || (getData = {}));
    /**
     * Fetch data from Sheet. Returned as json.
     * @param item required. kanji, vocabluary or radical string
     * @param type k, v, r or empty string to fetch all for that item
     * */
    async function fetchData(item, type) {
        // TODO: sleep between failed fetches???
        let shortType = getShortItemType(type);
        let url = sheetApiUrl + `?item=${item}&type=${shortType}&exec=get`;
        url = encodeURI(url);
        // TODO: handle case of malformed URL
        return fetch(url)
            .then(response => response.json()).catch(reason => { console.log("WKCM2: fetchData failed: " + reason); return null; })
            .then((responseJson) => {
            if (responseJson == null)
                return null;
            else {
                // Object.keys... .length on "" is 0. neat
                if (Object.keys(responseJson["Meaning_Mnem"]).length == 0 || responseJson["Meaning_Mnem"] == "{}")
                    if (Object.keys(responseJson["Reading_Mnem"]).length == 0 || responseJson["Reading_Mnem"] == "{}")
                        return null;
                return responseJson;
            }
        });
    }
    async function getAll() {
        let url = sheetApiUrl + `?exec=getall`;
        url = encodeURI(url);
        return fetch(url, { method: "GET" }).then(response => response.json())
            .catch(reason => {
            console.log("WKCM2: fillCache failed: ", reason);
            return null;
        });
    }
    async function submitMnemonic(mnemType, item, shortType, mnemIndexDB, newMnem) {
        let shortMnemType = getShortMnemType(mnemType);
        newMnem = encodeURIComponent(newMnem);
        let url = sheetApiUrl +
            `?exec=put&item=${item}&type=${shortType}&apiKey=${encodeURIComponent(userApiKey)}&mnemType=${shortMnemType}&mnemIndex=${mnemIndexDB}&mnem=${newMnem}`;
        return fetch(url, { method: "POST" });
    }
    async function voteMnemonic(mnemType, item, shortType, vote) {
        let shortMnemType = getShortMnemType(mnemType);
        let url = sheetApiUrl +
            `?exec=vote&item=${item}&type=${shortType}&mnemType=${shortMnemType}&apiKey=${userApiKey}&mnemUser=${currentMnem.currentUser[mnemType]}&mnemIndex=${currentMnem.userIndex[mnemType]}&vote=${vote}`;
        url = encodeURI(url);
        return fetch(url, { method: "POST" });
    }
    async function requestMnemonic(mnemType, item, shortType) {
        let shortMnemType = getShortMnemType(mnemType);
        let url = sheetApiUrl + `?exec=request&item=${item}&type=${shortType}&apiKey=${userApiKey}&mnemType=${shortMnemType}`;
        url = encodeURI(url);
        return fetch(url, { method: "POST" });
    }
    async function deleteMnemonic(mnemType, item, shortType) {
        if (currentMnem.currentUser[mnemType] != WKUser)
            return;
        let shortMnemType = getShortMnemType(mnemType);
        let url = sheetApiUrl +
            `?exec=del&item=${item}&type=${shortType}&mnemType=${shortMnemType}&apiKey=${userApiKey}&mnemIndex=${currentMnem.userIndex[mnemType]}`;
        url = encodeURI(url);
        return fetch(url, { method: "POST" });
    }

    /**
     * Functions related to cache access update etc.
     */
    // caching happens in getData using WaniKani Open Framework's wkof.file_cache
    function getCacheId(item, type) {
        type = getShortItemType(type);
        return "wkcm2-" + type + item;
    }
    /**
     * @param identifier wkof.file_cache identifier
     * @param maxAge Age of cache to compare against in days.
     * @return true if older than daydiff, else false
     * */
    function cacheExpired(identifier, maxAge = cacheDayMaxAge) {
        // 86400000ms == 1d
        let cachedDate = 0;
        try {
            if (wkof.file_cache.dir[identifier] === undefined)
                return true;
            cachedDate = Date.parse(wkof.file_cache.dir[identifier]["added"]);
        }
        catch (err) {
            console.log("WKCM2: cacheAgeOlder, ", err);
            return true;
        }
        let cacheAge = Math.floor((Date.now() - cachedDate) / 86400000);
        if (cacheAge > maxAge)
            return true;
        else
            return false;
    }
    /**
     * Only fills cache, if cache is expired.
     * */
    function fillCacheIfExpired() {
        wkof.file_cache.load(cacheFillIdent).then(value => {
            // found
            if (cacheExpired(cacheFillIdent, cacheDayMaxAge)) {
                // regex; delete whole wkcm2 cache
                wkof.file_cache.delete(/^wkcm2-/);
                fillCache();
                wkof.file_cache.save("wkcm2-version", WKCM2_version);
            }
        }, reason => {
            fillCache();
        });
    }
    /**
     * Fills the cache with all available items.
     * Deletes the current wkcm cache
     * runs async. in the background.
     * NOTE: Items, that are not in the DB are not fetched by getall. So they still are uncached.
     * But the No mnem available message is displayed prematurely, so it should be fine.
     * */
    async function fillCache() {
        getAll().then((responseJson) => {
            if (responseJson == null)
                return null;
            else {
                resetWKOFcache(false);
                for (let typeItem in responseJson) {
                    let identifier = getCacheId(responseJson[typeItem]["Item"], responseJson[typeItem]["Type"]);
                    wkof.file_cache.save(identifier, responseJson[typeItem]);
                }
                wkof.file_cache.save(cacheFillIdent, "Cache Filled");
            }
        }).catch(err => console.log("WKCM2: fillCache, ", err));
    }
    async function deleteCacheItem(item, type) {
        if (type == undefined)
            type = getShortItemType(getItemType());
        if (item == undefined || item == "")
            item = getItem();
        let identifier = getCacheId(item, type);
        return wkof.file_cache.delete(identifier);
    }

    /**
     * Returns new elements for the legend on item list pages (.../kanji/, .../level/)
     * */
    function getLegendLi() {
        return `
<li class="subject-legend__item" title="A Community Mnemonic was Requested.">
    ${getBadge(true, true)}
    <div class="subject-legend__item-title">CM Requested</div>
</li>
<li class="subject-legend__item" title="A Community Mnemonic is available.">
    ${getBadge(false, true)}
    <div class="subject-legend__item-title">CM Available</div>
</li>`;
    }
    /**
     * Returns a badge for items in lists, whether a Mnemonic is available or requested
     * */
    function getBadge(request = false, legend = false) {
        if (!request)
            return `<span lang="ja" class="${getBadgeClassAvail(legend)}">有</span>`;
        else
            return `<span lang="ja" class="${getBadgeClassReq(legend)}">求</span>`;
    }
    function getBadgeClass(type = "available", legend = false) {
        if (legend)
            return "subject-legend__item-badge--cm-" + type;
        else
            return `character-item__badge ${getBadgeBaseClass(type)}`;
    }
    function getBadgeBaseClass(type = "") {
        return `character-item__badge__cm-${type}`;
    }
    function getBadgeClassReq(legend = false) {
        return getBadgeClass("request", legend);
    }
    function getBadgeClassAvail(legend = false) {
        return getBadgeClass("available", legend);
    }

    var stylesheet$6=".cm-content {\n  height: 100%;\n  text-align: left;\n  display: inline-block;\n}\n\n#turbo-body .container #wkcm2 .cm-content {\n  padding-bottom: 50px;\n}\n\n.cm {\n  font-family: \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  overflow: auto;\n}";

    var stylesheet$5=".subject-legend__item {\n  flex: 0 0 17%;\n}\n\n.subject-legend__item-badge--cm-request {\n  background-color: #e1aa00;\n}\n\n.subject-legend__item-badge--cm-available {\n  background-color: #71aa00;\n}\n\n.subject-legend__item-badge--cm-request, .subject-legend__item-badge--cm-available {\n  width: 2em;\n  height: 2em;\n  line-height: 2.1;\n  color: #fff;\n  font-size: 16px;\n  border-radius: 50%;\n  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);\n  box-shadow: 0 -2px 0px rgba(0, 0, 0, 0.2) inset, 0 0 10px rgba(255, 255, 255, 0.5);\n  margin-bottom: 14px;\n  text-align: center;\n}\n\n.character-item__badge__cm-request {\n  background-color: #e1aa00;\n  left: 30px;\n}\n@media screen and (max-width: 767px) {\n  .character-item__badge__cm-request {\n    left: 0px;\n    transform: translate(45%, 0%);\n  }\n}\n\n.character-item__badge__cm-available {\n  background-color: #71aa00;\n  left: 60px;\n}\n@media screen and (max-width: 767px) {\n  .character-item__badge__cm-available {\n    left: 0px;\n    transform: translate(45%, -112%);\n  }\n}\n\n.character-grid__item--vocabulary .character-item__badge__cm-request {\n  left: 0px;\n  transform: translate(45%, 0%);\n}\n.character-grid__item--vocabulary .character-item__badge__cm-available {\n  left: 0px;\n  transform: translate(45%, -112%);\n}\n.character-grid__item--vocabulary .character-item {\n  padding-left: 40px;\n}\n\n@media screen and (max-width: 767px) {\n  .character-item {\n    padding-left: 40px;\n  }\n}";

    var stylesheet$4=".cm-btn {\n  color: white;\n  font-size: 14px;\n  cursor: pointer;\n  filter: contrast(0.9);\n  border-radius: 3px;\n  box-shadow: 0 -2px 0 rgba(0, 0, 0, 0.2) inset;\n  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3);\n  transition: text-shadow 0.15s linear;\n  text-align: center;\n  font-weight: normal;\n}\n\n#item-info .cm-submit-highlight, #item-info .cm-upvote-highlight, #item-info .cm-downvote-highlight, #supplement-info .cm-submit-highlight, #supplement-info .cm-upvote-highlight, #supplement-info .cm-downvote-highlight {\n  height: 15px;\n  padding: 1px 0px 4px 0px;\n}\n\n.cm-btn:hover {\n  filter: contrast(1.15) !important;\n}\n\n.cm-btn:active {\n  filter: contrast(1.2) !important;\n  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.2) inset;\n}\n\n.cm-btn.disabled.cm-btn.disabled {\n  opacity: 0.3;\n  pointer-events: none;\n}\n\n.cm-prev, .cm-next {\n  color: #333333;\n  font-size: 50px;\n  margin: 0px 0px 0px 0px;\n  padding: 15px 10px 0px 0px;\n  box-shadow: none !important;\n}\n\n.cm-prev:not(.disabled), .cm-next:not(.disabled) {\n  text-shadow: 0 4px 0 rgba(0, 0, 0, 0.3);\n}\n\n.cm-prev:hover, .cm-next:hover {\n  text-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);\n}\n\n.cm-prev:active, .cm-next:active {\n  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3);\n}\n\n.cm-prev {\n  float: left;\n}\n\n.cm-next {\n  float: right;\n}\n\n.cm-prev.disabled, .cm-next.disabled {\n  opacity: 0.25;\n}\n\n.cm-small-btn, .cm-submit-highlight, .cm-form-submit, .cm-form-cancel {\n  text-align: center;\n  font-size: 14px;\n  width: 75px;\n  margin-right: 10px;\n  float: left;\n  padding: 0px 4px;\n}\n\n.cm-upvote-highlight, .cm-downvote-highlight {\n  width: 95px;\n  margin-right: 10px;\n  float: left;\n}\n\n.cm-upvote-highlight {\n  background-image: linear-gradient(to bottom, #5c5, #46ad46);\n}\n\n.cm-downvote-highlight {\n  background-image: linear-gradient(to bottom, #c55, #ad4646);\n}\n\n.cm-delete-highlight {\n  background-image: linear-gradient(to bottom, #811, #6d0606);\n  margin-right: 10px;\n}\n\n.cm-edit-highlight {\n  background-image: linear-gradient(to bottom, #ccc, #adadad);\n}\n\n.cm-request-highlight {\n  background-image: linear-gradient(to bottom, #e1aa00, #d57602);\n}\n\n.cm-submit-highlight {\n  width: 125px;\n  margin-left: 75px;\n  float: right;\n  background-image: linear-gradient(to bottom, #616161, #393939);\n}\n\n.cm-cancel-highlight, .cm-save-highlight {\n  width: 75px;\n  background-image: linear-gradient(to bottom, #616161, #393939);\n  padding: 0px 0px 0px 0px;\n}\n\n/*Edit, delete, request are small buttons*/\n.cm-small-btn {\n  font-size: 12px;\n  width: 50px;\n  height: 13px;\n  line-height: 1;\n}\n\n.cm-submit-highlight.disabled, .cm-form-submit.disabled {\n  color: #8b8b8b !important;\n}\n\n/*.cm-request-highlight { margin-top: 10px; width: 100px; background-image: linear-gradient(to bottom, #ea5, #d69646)}*/";

    var stylesheet$3=".cm-format-btn.cm-format-btn {\n  filter: contrast(0.8);\n  text-align: center;\n  width: 35px;\n  height: 30px;\n  font-size: 20px;\n  line-height: 30px;\n  margin-left: 5px;\n  float: left;\n  box-shadow: 0 -4px 0 rgba(0, 0, 0, 0.2) inset;\n}\n\n.cm-format-btn:active {\n  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.2) inset !important;\n}\n\n.cm-format .cm-kanji, .cm-format .cm-radical, .cm-format .cm-vocabulary, .cm-format .cm-reading {\n  font-weight: bold;\n  display: inline-block;\n  color: #fff;\n  text-align: center;\n  box-sizing: border-box;\n  line-height: 1;\n}\n\n.cm-format-btn.cm-format-bold, .cm-format-btn.cm-format-italic, .cm-format-btn.cm-format-underline, .cm-format-btn.cm-format-newline, .cm-format-btn.cm-format-qmark, .cm-format-btn.cm-format-strike {\n  background-color: #f5f5f5;\n  background-image: linear-gradient(to bottom, #7a7a7a, #4a4a4a);\n  background-repeat: repeat-x;\n}";

    var stylesheet$2=".cm-form form {\n  min-height: 300px;\n}\n\n.cm-form fieldset {\n  padding: 1px;\n  height: 110px;\n}\n\n.cm-text {\n  overflow: auto;\n  word-wrap: break-word;\n  resize: none;\n  height: calc(100% - 30px);\n  width: 98%;\n}\n\n.counter-note {\n  padding: 0px;\n  margin: 0px;\n  margin-right: 10px;\n  margin-top: 2px;\n}\n\n.cm-mnem-text {\n  float: left;\n  width: calc(100% - 120px);\n  height: 100%;\n  min-height: 125px;\n}";

    var stylesheet$1=".cm-user-buttons {\n  position: absolute;\n  margin-top: -20px;\n}\n\n.cm-info {\n  display: inline-block;\n  margin-top: 20px;\n  margin-left: 65px;\n}\n\n.cm-info div {\n  margin-bottom: 0px;\n}\n\n.cm-score {\n  float: left;\n  width: 80px;\n}\n\n.cm-score-num {\n  color: #555;\n}\n\n.cm-score-num.pos {\n  color: #5c5;\n}\n\n.cm-score-num.neg {\n  color: #c55;\n}\n\n.cm-nomnem {\n  margin-top: -10px !important;\n}\n\n.cm-form fieldset {\n  clear: left;\n}\n\n.cm-format {\n  margin: 0 !important;\n}\n\n.cm-delete-text {\n  position: absolute;\n  opacity: 0;\n  text-align: center;\n}\n\n.cm-delete-text h3 {\n  margin: 0;\n}";

    var stylesheet=".cm-radical {\n  background-color: #0af;\n  background-image: linear-gradient(to bottom, #0af, #0093dd);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#FF00AAFF\", endColorstr=\"#FF0093DD\", GradientType=0);\n}\n\n.cm-kanji {\n  background-color: #f0a;\n  background-image: linear-gradient(to bottom, #f0a, #dd0093);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#FFFF00AA\", endColorstr=\"#FFDD0093\", GradientType=0);\n}\n\n.cm-vocabulary {\n  background-color: #a0f;\n  background-image: linear-gradient(to bottom, #a0f, #9300dd);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#FFAA00FF\", endColorstr=\"#FF9300DD\", GradientType=0);\n}\n\n.cm-reading {\n  background-color: #555;\n  background-image: linear-gradient(to bottom, #555, #333);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#FF555555\", endColorstr=\"#FF333333\", GradientType=0);\n  box-shadow: 0 -2px 0 rgba(0, 0, 0, 0.8) inset;\n}\n\n.cm-request {\n  background-color: #e1aa00;\n  color: black !important;\n  background-image: linear-gradient(to bottom, #e1aa00, #e76000);\n  background-repeat: repeat-x;\n}\n\n.cm-kanji, .cm-radical, .cm-reading, .cm-vocabulary, .cm-request {\n  padding: 1px 4px;\n  color: #fff;\n  font-weight: normal;\n  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);\n  white-space: nowrap;\n  border-radius: 3px;\n  box-shadow: 0 -2px 0 rgba(0, 0, 0, 0.2) inset;\n}";

    /**
     * Functions for the item lists
     * (wanikani.com/vocabulary)
     */
    function initHeader() {
        addHTMLinEle(".subject-legend__items", getLegendLi(), "beforeend");
    }
    async function addBadgeToItems() {
        // cancel if they were already added
        if (document.querySelector(`[class*='${getBadgeBaseClass()}']`))
            return;
        let types = ["radical", "kanji", "vocabulary"];
        //let typeShort = getShortItemType(getItemType());
        // needed for "levels" Overview, where all three are present
        for (let type of types) {
            let itemList = document.querySelectorAll(`.character-item--${type}`);
            for (let i = 0; i < itemList.length; i++) {
                if (typeof itemList[i] != "object" || itemList[i] == null) {
                    console.log(type, itemList[i]);
                    console.log(typeof itemList[i]);
                    continue;
                }
                let spanItem = itemList[i].querySelector(".character-item__characters");
                let item = "";
                if (spanItem.innerText) {
                    item = spanItem.innerText;
                }
                else if (type == "radical") // Image Radical
                 {
                    let radImg = spanItem.querySelector("img.radical-image");
                    item = radImg.alt;
                }
                else {
                    continue;
                }
                await getData(item, getShortItemType(type), false).then((res) => {
                    if (hasRequest(res))
                        addBadge(itemList[i], getBadge(true), getBadgeBaseClass("request"));
                    if (mnemAvailable(res))
                        addBadge(itemList[i], getBadge(false), getBadgeBaseClass("available"));
                });
            }
        }
    }
    /**
     * Only add Badge if not already present.
     * @param node
     * @param badgeHTML
     * @param selector
     */
    function addBadge(node, badgeHTML, selector) {
        if (!node.parentNode.querySelector(`.${selector}`)) {
            let range = document.createRange();
            range.selectNode(document.body);
            let newElement = range.createContextualFragment(badgeHTML);
            node.parentNode.insertBefore(newElement, node);
        }
    }

    run();
    // all code runs from here
    function run() {
        // Runs checks if elements exist before running init and waits for them. Then calls init.
        waitForWKOF().then(exists => {
            if (exists) {
                wkof.include('Apiv2');
                wkof.ready('Apiv2').then(init);
            }
            else
                console.log("WKCM2: there was a problem with checking for wkof. Please check if it is installed correctly and running. ");
        }).catch(exists => {
            console.log("WKCM2: ERROR. WKOF not found.");
            checkWKOF_old();
        });
    }
    // CSS
    const generalCSS = stylesheet$6;
    const listCSS = stylesheet$5;
    const buttonCSS = stylesheet$4;
    const formatButtonCSS = stylesheet$3;
    const textareaCSS = stylesheet$2;
    const contentCSS = stylesheet$1;
    const highlightCSS = stylesheet;
    // Init ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
    /**
     * Runs the right code depending if the current page is Lesson, Review or List
     * */
    function init() {
        // resets cache on new version of WKCM2
        resetWKOFcache();
        // refills whole cache, if not already filled or old.
        fillCacheIfExpired();
        setUsername();
        setApiKey();
        if (isInitialized())
            return;
        addGlobalStyle(generalCSS);
        addGlobalStyle(buttonCSS);
        addGlobalStyle(formatButtonCSS);
        addGlobalStyle(contentCSS);
        addGlobalStyle(textareaCSS);
        addGlobalStyle(highlightCSS);
        if (isList) {
            fillCacheIfExpired();
            initList();
        }
        else {
            infoInjectorInit("meaning");
            infoInjectorInit("reading");
        }
        if (isList || isItem)
            detectUrlChange(500);
    }
    /**
     * Usese WKItemInfoInjector to inject HTML into page and call init
     * @param mnemType
     */
    function infoInjectorInit(mnemType) {
        if (isInitialized())
            return;
        let cm_div = document.createElement("div");
        cm_div.innerHTML = getCMdivContent(mnemType);
        // insert HTML Elements
        win.wkItemInfo
            //.on("lesson,lessonQuiz,review,extraStudy,itemPage")
            //.forType("radical,kanji,vocabulary")
            .under(mnemType).spoiling(mnemType)
            .appendSubsection(getHeader(mnemType), cm_div); //, { injectImmediately: true });
        // callback to initialize HTML Elements inserted above
        const wkItemInfoSelector = win.wkItemInfo.on("lesson,lessonQuiz,review,extraStudy,itemPage")
            .forType("radical,kanji,vocabulary").under(mnemType).spoiling(mnemType);
        let notify = wkItemInfoSelector.notifyWhenVisible || wkItemInfoSelector.notify;
        // wkItemInfoSelector.notifyWhenVisible(o => {console.log("here")})
        notify(o => {
            // console.log(o);
            waitForEle(`cm-${mnemType}`).then(() => {
                initCM(mnemType);
            });
        });
    }
    /**
     * initializes Buttons and starts first content update.
     */
    function initCM(mnemType) {
        initButtons(mnemType);
        updateCM(undefined, mnemType);
    }
    function initList() {
        if (isInitialized())
            return;
        addGlobalStyle(listCSS);
        waitForClass("." + getBadgeClassAvail(true), initHeader, 250);
        waitForClass(`[class*='${getBadgeBaseClass()}']`, addBadgeToItems, 100, 25);
    }
    /**
     * return true if initialized. False else
     * @param mnemType can be null. If null uses both.
     * @returns
     */
    function isInitialized(mnemType = null) {
        if (!isList) {
            if (mnemType == null)
                if (getItemType() == "radical")
                    return isInitialized("meaning");
                else
                    return isInitialized("reading") && isInitialized("meaning");
            if (document.querySelector("#wkcm2"))
                return true;
            if (document.querySelector(`#cm-${mnemType}`))
                return true;
        }
        else // For list
         {
            if (document.querySelector(".character-item__badge__cm-request"))
                return true;
            if (document.querySelector(".character-item__badge__cm-available"))
                return true;
        }
        return false;
    }

    exports.infoInjectorInit = infoInjectorInit;
    exports.initList = initList;

    Object.defineProperty(exports, '__esModule', { value: true });

    return exports;

})({});