Wanikani Self-Study

Self-study your items via the Wanikani level pages

As of 2016-05-12. See the latest version.

// ==UserScript==
// @name        Wanikani Self-Study
// @namespace   rfindley
// @description Self-study your items via the Wanikani level pages
// @version     1.2.0
// @include     https://www.wanikani.com/level/*
// @exclude     https://www.wanikani.com/level/*/*
// @include     https://www.wanikani.com/radicals*
// @exclude     https://www.wanikani.com/radicals/*
// @include     https://www.wanikani.com/kanji*
// @exclude     https://www.wanikani.com/kanji/*
// @include     https://www.wanikani.com/vocabulary*
// @exclude     https://www.wanikani.com/vocabulary/*
// @copyright   2016+, Robin Findley
// @license     MIT; http://opensource.org/licenses/MIT
// @run-at      document-end
// @grant       none
// ==/UserScript==

// Although this script is a 100% my own code, general credits go
// to Axel Örn Sigurðsson (WK user @absalon) for original concept:
// https://www.wanikani.com/chat/api-and-third-party-apps/8575

wkselfstudy = {};

(function(gobj) {

    var settings = {
        compatible: 2,
        // ss_hidelocked   - Hide locked items
        // ss_hideunlocked - Hide unlocked items (includes ss_hideburn)
        // ss_hideburned   - Hide burned items
        // ss_hideunburned - Hide unburned items (includes ss_hidelock)
        // ss_hidechar     - Hide the radical/kanji/vocab characters
        // ss_hideread     - Hide the reading
        // ss_hidemean     - Hide the meaning
        configs: [
            ['Japanese to English',      'ss_hidelocked ss_hideread ss_hidemean'],
            ['English to Japanese',      'ss_hidelocked ss_hideread ss_hidechar'],
            ['[BURNED] Japanese to English', 'ss_hideunburned ss_hideread ss_hidemean'],
            ['[BURNED] English to Japanese', 'ss_hideunburned ss_hideread ss_hidechar'],
        ],
        selected_config: 0,
        enabled: true,
        randomize_on_load: true
    };

    var html =
        '<div class="selfstudy">'+
        '  <label>Self-study:</label>'+
        '  <div class="btn-group">'+
        '    <button class="btn enable">OFF</button>'+
        '    <button class="btn shuffle">Shuffle</button>'+
        '    <select class="btn config"></select>'+
        '    <button class="btn config">Config</button>'+
        '  </div>'+
        '</div>';

    var config =
        '<div id="ss_config" class="hidden">'+
        '  <div class="btns">'+
        '    <button class="btn new">New</button>'+
        '    <button class="btn up">Up</button>'+
        '    <button class="btn dn">Down</button>'+
        '    <button class="btn del">Delete</button>'+
        '  </div>'+
        '  <div class="list">'+
        '    <select class="configs" size="7"></select>'+
        '  </div>'+
        '  <div class="txtline">'+
        '    <label>Preset name:</label>'+
        '    <div class="expand"><input type="text" class="preset"></div>'+
        '  </div>'+
        '  <div class="cbbox">'+
        '    <div><label>Hide Rad/Kan/Voc:</label><input type="checkbox" name="ss_hidechar"></div>'+
        '    <div><label>Hide Reading:</label><input type="checkbox" name="ss_hideread"></div>'+
        '    <div><label>Hide Meaning:</label><input type="checkbox" name="ss_hidemean"></div>'+
        '  </div>'+
        '  <div class="cbbox">'+
        '    <div><label>Hide Locked:</label><input type="checkbox" name="ss_hidelocked"></div>'+
        '    <div><label>Hide Unlocked:</label><input type="checkbox" name="ss_hideunlocked"></div>'+
        '    <div><label>Hide Unburned:</label><input type="checkbox" name="ss_hideunburned"></div>'+
        '    <div><label>Hide Burned:</label><input type="checkbox" name="ss_hideburned"></div>'+
        '  </div>'+
        '  <div class="dlg_close">'+
        '    <div class="btn-group">'+
        '      <button class="btn save">Save</button>'+
        '      <button class="btn cancel">Cancel</button>'+
        '    </div>'+
        '  </div>'+
        '</div>';

    var css =
        '.selfstudy {margin-left:20px; margin-bottom:10px; position:relative;}'+
        '.selfstudy label {display:inline; vertical-align:middle; padding-right:4px; color:#999; font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif; text-shadow:0 1px 0 #fff;}'+
        '.selfstudy button.enable {width:55px;}'+
        '.ss_active .selfstudy button.enable.on {background-color:#b3e6b3; background-image:linear-gradient(to bottom, #ecf9ec, #b3e6b3);}'+
        '.selfstudy select.config {width:300px;}'+

        'section[id^="level-"].ss_active.ss_hidechar .character-item a span:not(.dummy) {opacity:0; transition:opacity ease-in-out 0.15s}'+
        'section[id^="level-"].ss_active.ss_hideread .character-item a li[lang="ja"] {opacity:0; transition:opacity ease-in-out 0.15s}'+
        'section[id^="level-"].ss_active.ss_hidemean .character-item a li:not([lang="ja"]) {opacity:0; transition:opacity ease-in-out 0.15s}'+
        'section[id^="level-"].ss_active.ss_hideburned .character-item.burned {display:none;}'+
        'section[id^="level-"].ss_active.ss_hidelocked .character-item.locked {display:none;}'+
        'section[id^="level-"].ss_active.ss_hideunburned .character-item:not(.burned) {display:none;}'+
        'section[id^="level-"].ss_active.ss_hideunlocked .character-item:not(.locked) {display:none;}'+

        'section.ss_active .character-item:hover a span {opacity: initial !important; transition:opacity ease-in-out 0.05s !important;}'+
        'section.ss_active .character-item:hover a li {opacity: initial !important; transition:opacity ease-in-out 0.05s !important;}'+

        '#ss_config {position:absolute; z-index:3; width:550px; background-color:rgba(0,0,0,0.8); border-radius:8px; padding:8px;}'+

        '#ss_config select.configs {width:475px;}'+
        '#ss_config label {color:#ccc; text-shadow:initial; text-align:right; vertical-align:baseline;}'+
        '#ss_config .btns {display:inline-block; float:left; vertical-align:top; margin-right:8px;}'+
        '#ss_config .btns .btn {display:block; margin-bottom:5px;}'+
        '#ss_config .btn {width:70px;}'+

        '#ss_config .list {overflow-x:auto;}'+
        '#ss_config .list select.configs {width:100%; height:135px;}'+

        '#ss_config .txtline label {display:inline-block; float:left; margin-right:8px; width:100px; line-height:30px; clear:both;}'+
        '#ss_config .txtline .expand {overflow-x:auto;}'+
        '#ss_config .txtline input {box-sizing:border-box; width:100%; height:30px;}'+

        '#ss_config .cbbox {display:inline-block; width:49%; vertical-align:top;}'+
        '#ss_config .cbbox label {display:inline-block; float:left; margin:0 8px 0 0; width:135px; line-height:30px;}'+
        '#ss_config .cbbox input {position:relative; overflow-x:auto; height:30px; margin:0; top:1px;}'+

        '#ss_config .dlg_close {text-align:center; margin-top:16px;}'+

        '';

    var cfg_tmp;

    //-------------------------------------------------------------------
    // Open the configuration dialog.
    //-------------------------------------------------------------------
    function configure(e) {

        var sel, ssgrp, dialog;

        function setup() {
            dialog = $(config).appendTo(ssgrp);
            sel = $('#ss_config select.configs');

            // "New" handler
            dialog.find('button.new').on('click', function() {
                cfg_tmp.push(['<new>','']);
                sel.append('<option value="'+(cfg_tmp.length-1)+'">&lt;new&gt;</option>');
                select_config(sel.children().length-1);
                $('#ss_config .preset').focus().select();
            });

            // "Delete" handler
            dialog.find('button.del').on('click', function() {
                var opt = sel.find(':selected');
                var idx = opt.index();
                opt.remove();
                var len = sel.children().length;
                if (idx >= len) idx = len-1;
                select_config(idx);
            });

            // "Up" handler
            dialog.find('button.up').on('click', function() {
                var opt = sel.find(':selected');
                if (opt.index() > 0) opt.insertBefore(opt.prev());
            });

            // "Down" handler
            dialog.find('button.dn').on('click', function() {
                var opt = sel.find(':selected');
                if (opt.index() < sel.children().length-1) opt.insertAfter(opt.next());
            });

            // "Configs" selection changed
            sel.on('change', function() {
                select_config(sel.find(':selected').index());
            });

            // "Preset" name changed
            dialog.find('.preset').on('change', function(e) {
                var opt = sel.find(':selected');
                var text = e.currentTarget.value;
                opt.text(text);
                var idx = opt.val();
                cfg_tmp[idx][0] = text;
            });

            // "Checkbox" changed
            dialog.find('input[type="checkbox"]').on('change', function() {
                var opt = sel.find(':selected');
                var idx = opt.val();
                var props = [];
                dialog.find('input[type="checkbox"]:checked').each(function(i,e){props.push(e.name);});
                cfg_tmp[idx][1] = props.join(' ');
            });

            // "Save" handler
            dialog.find('button.save').on('click', save_config);

            // "Cancel" handler
            dialog.find('button.cancel').on('click', cancel_config);
        }

        function save_config() {
            settings.configs = [];
            sel.children().each(function(i,v){
                var idx = $(v).val();
                settings.configs.push(cfg_tmp[idx].slice(0));
            });
            settings.selected_config = sel.find(':selected').index();
            save_settings();
            dialog.addClass('hidden');
            populate_presets();
            set_config(settings.selected_config);
        }

        function cancel_config() {
            cfg_tmp = undefined;
            dialog.addClass('hidden');
        }

        function select_config(idx) {
            var opt = sel.children().eq(idx);
            opt.prop('selected',true);
            $('#ss_config input.preset').val(opt.text());

            var props = cfg_tmp[opt.val()][1];
            $('#ss_config .cbbox input').prop('checked', false);
            props.split(' ').forEach(function(prop,i){
                $('#ss_config .cbbox input[name="'+prop+'"]').prop('checked', true);
            });
        }

        ssgrp = $(e.currentTarget).closest('.selfstudy');
        window.ssgrp = ssgrp;
        dialog = $('#ss_config');
        if (dialog.length === 0) {
            setup();
        } else if (dialog.is(':visible')) {
            return cancel_config();
        } else {
            ssgrp.append(dialog);
            sel = $('#ss_config select.configs');
        }

        // Clone the existing settings.
        var options = [];
        cfg_tmp = settings.configs.map(function(e,i){
            options.push('<option value="'+i+'">'+e[0]+'</option>');
            return e.slice(0);
        });
        window.cfg_tmp = cfg_tmp;

        // Populate configs.
        sel.html(options.join(''));
        select_config(settings.selected_config);

        // Unhide the config dialog.
        var top = ssgrp.find('.btn-group').height() + 4;
        dialog.css('top',top).removeClass('hidden');
    }

    //-------------------------------------------------------------------
    // Save settings.
    //-------------------------------------------------------------------
    function save_settings() {
        localStorage.setItem('selfstudy_settings', JSON.stringify(settings));
    }

    //-------------------------------------------------------------------
    // Button event handler.
    //-------------------------------------------------------------------
    function toggle_enable() {
        settings.enabled = !settings.enabled;
        save_settings();
        set_enable();
    }

    //-------------------------------------------------------------------
    // Button event handler.
    //-------------------------------------------------------------------
    function config_change_event(e) {
        set_config(Number(e.currentTarget.value));
    }

    //-------------------------------------------------------------------
    // Shuffle items.
    //-------------------------------------------------------------------
    function shuffle(e) {
        if (e === undefined) {
            // Shuffle all
            $('section[id^="level-"]').each(function(){
                var sec = $(this);
                sec.find('[class$="-character-grid"]').append(sec.find('.character-item').detach().sort(function(){return Math.round(Math.random())*2-1;}));
            });
        } else {
            // Shuffle specific group
            var btn = $(e.currentTarget);
            var sec = btn.closest('section[id^="level-"]');
            sec.find('[class$="-character-grid"]').append(sec.find('.character-item').detach().sort(function(){return Math.round(Math.random())*2-1;}));
        }
    }

    //-------------------------------------------------------------------
    // Enable or disable the plugin.
    //-------------------------------------------------------------------
    function set_enable() {
        var btns = $('.selfstudy button.enable');
        var secs = $('section[id^="level-"]');

        if (settings.enabled) {
            secs.addClass('ss_active');
            btns.addClass('on').text('ON');
        } else {
            secs.removeClass('ss_active');
            btns.removeClass('on').text('OFF');
        }
    }

    //-------------------------------------------------------------------
    // Select a configuration.
    //-------------------------------------------------------------------
    function set_config(val) {
        var secs = $('section[id^="level-"]');

        // Remove all current ss_hide classes
        secs.each(function(i,e){
            e.className = e.className.split(' ').filter(function(v){return v.match(/^ss_hide/) === null;}).join(' ');
        });

        settings.selected_config = val;
        save_settings();
        $('.selfstudy select.config').val(val);

        settings.configs[settings.selected_config][1].split(' ').forEach(function(cfgopt,idx){
            secs.addClass(cfgopt);
        });
    }

    //-------------------------------------------------------------------
    // Populate the presets into the drop-down box.
    //-------------------------------------------------------------------
    function populate_presets() {
        var options = [];
        settings.configs.forEach(function(config,idx){
            var cfgname = config[0];
            var cfgopts = config[1];
            options.push('<option value="'+idx+'">'+cfgname+'</option>');
        });
        $('.selfstudy select.config').html(options.join(''));
    }

    //-------------------------------------------------------------------
    // Startup. Runs at document 'load' event.
    //-------------------------------------------------------------------
    function startup() {
        // Load settings.
        var s = localStorage.getItem('selfstudy_settings');
        if (s) {
            s = JSON.parse(s);
            if (s.compatible !== undefined && s.compatible == settings.compatible) {
                delete settings.configs;
                $.extend(true, settings, s);
            }
        }

        // Insert CSS
        $('head').append('<style type="text/css">'+css+'</style>');

        // Insert HTML
        $('section[id^="level-"]').prepend(html);

        populate_presets();

        // Install handlers
        $('.selfstudy button.enable').on('click', toggle_enable);
        $('.selfstudy button.shuffle').on('click', shuffle);
        $('.selfstudy select.config').on('change', config_change_event);
        $('.selfstudy button.config').on('click', configure);

        set_config(settings.selected_config);
        if (settings.enabled) {
            set_enable();
            shuffle();
        }
    }

    // Run startup() after window.onload event.
    if (document.readyState === 'complete')
        startup();
    else
        window.addEventListener("load", startup, false);

})(wkselfstudy);