WaniKani Open Framework Search Filters

Additional Search filters for the WaniKani Open Framework

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name          WaniKani Open Framework Search Filters
// @namespace     https://www.wanikani.com
// @description   Additional Search filters for the WaniKani Open Framework
// @author        prouleau
// @version       1.4.1
// @match         https://www.wanikani.com/*
// @license       MIT; http://opensource.org/licenses/MIT
// @grant         none
// ==/UserScript==

(function(wkof) {
	'use strict';

	var wkofMinimumVersion = '1.0.52';

	if (!wkof) {
		var response = confirm('WaniKani Open Framework Date Filters requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');

		if (response) {
			window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
		}

		return;
	}
	var settingsDialog;
	var settingsScriptId = 'searchFilters';
	var settingsTitle = 'Search Filters';

	var needToRegisterFilters = true;

	var filterNamePrefix = 'searchFilters_';
	var globalSearchFilterName = filterNamePrefix + 'globalSearch';
	var exactSearchFilterName = filterNamePrefix + 'exactSearch';
	var kunSearchFilterName = filterNamePrefix + 'kunSearch';
	var onSearchFilterName = filterNamePrefix + 'onSearch';
	var componentsSearchFilterName = filterNamePrefix + 'componentsSearch';
	var usedInSearchFilterName = filterNamePrefix + 'usedInSearch';
	var visSimSearchFilterName = filterNamePrefix + 'visSimSearch';
	var allowSearchFilterName = filterNamePrefix + 'allowSearch';
	var blockSearchFilterName = filterNamePrefix + 'blockSearch';
	var ctxSenSearchFilterName = filterNamePrefix + 'ctxSenkSearch';
	var mnemonicsSearchFilterName = filterNamePrefix + 'mnemonicsSearch';

	var supportedFilters = [globalSearchFilterName, exactSearchFilterName, kunSearchFilterName, onSearchFilterName, componentsSearchFilterName,
                            usedInSearchFilterName, visSimSearchFilterName, allowSearchFilterName, blockSearchFilterName, ctxSenSearchFilterName,
                            mnemonicsSearchFilterName, ];

    function prepareSubjectIndexThenRegisterFilters(){
        wkof.set_state(settingsScriptId, 'loading');
        var config = {wk_items: {filters:{}, options:{'subjects': true,}}};
        wkof.include('ItemData');
        wkof.ready('ItemData')
            .then(function(){return wkof.ItemData.get_items(config)})
            .then(prepareIndex)
            .then(updateFiltersWhenReady)
    }

    var componentIndex = {};
    var usedInIndex = {};
    var visuallySimilarIndex = {};
    function prepareIndex(items){
        for (var item of items){
            if (item.object !== 'radical'){
                componentIndex[item.data.slug] = componentIndex[item.data.slug] || [];
                componentIndex[item.data.slug] = componentIndex[item.data.slug].concat(item.data.component_subject_ids);
            }
            if (item.object !== 'vocabulary'){
                usedInIndex[item.data.slug] = usedInIndex[item.data.slug] || [];
                usedInIndex[item.data.slug] = usedInIndex[item.data.slug].concat(item.data.amalgamation_subject_ids);
                if (item.object === 'radical' && item.data.characters !== null){
                    usedInIndex[item.data.characters] = usedInIndex[item.data.characters] || [];
                    usedInIndex[item.data.characters] = usedInIndex[item.data.characters].concat(item.data.amalgamation_subject_ids);
                }
            }
             if (item.object === 'kanji'){
                visuallySimilarIndex[item.data.slug] = visuallySimilarIndex[item.data.slug] || [];
                visuallySimilarIndex[item.data.slug] = visuallySimilarIndex[item.data.slug].concat(item.data.visually_similar_subject_ids);
            }
        }
    }

	function updateFiltersWhenReady() {
		needToRegisterFilters = true;
		return waitForItemDataRegistry().then(registerFilters);
	}

	function waitForItemDataRegistry() {
		return wkof.wait_state('wkof.ItemData.registry', 'ready');
	}

	function registerFilters() {
		if (!needToRegisterFilters) {
			return;
		}

		supportedFilters.forEach(function(filterName) {
			delete wkof.ItemData.registry.sources.wk_items.filters[filterName];
		});

		registerSearchFilter();
        registerExactSearchFilter();
        registerKunSearchFilter();
        registerOnSearchFilter();
        registerComponentsSearchFilter();
        registerUsedInSearchFilter();
        registerVisSimSearchFilter();
        registerAllowSearchFilter();
        registerBlockSearchFilter();
        registerCtxSenSearchFilter();
        registerMnemonicsSearchFilter();

		needToRegisterFilters = false;
        wkof.set_state(settingsScriptId, 'ready');
	}

    function split_list(str) {return str.replace(/、/g,',').replace(/[\s ]+/g,' ').trim().replace(/ *, */g, ',').toLowerCase().split(',').filter(function(name) {return (name.length > 0);});}

    function makeComponentPairs(str){
        return split_list(str).reduce((acc, item) =>{return acc.concat(componentIndex[item] || [] )}, []);
    }

    function makeUsedInPairs(str){
        return split_list(str).reduce((acc, item) =>{return acc.concat(usedInIndex[item.replace(' ', '-')]) || [] }, []);
    }

    function makeVisuallySimilarPairs(str){
        return split_list(str).reduce((acc, item) =>{return acc.concat(visuallySimilarIndex[item] || [] )}, []);
    }

	// BEGIN Global Search
    let searchHover_tip = 'Enter a search term for meaning, reading or kanji.\nAll approximate matches will be found.\nYou may use latin, kana and kanji.\nMultiple terms separated by commas are allowed.';

	function registerSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[globalSearchFilterName] = {
			type: 'text',
			label: 'Global Search',
            placeholder: 'gambler, 力士',
			default: '',
			filter_func: searchFilter,
            filter_value_map: split_list,
			set_options: function(options) { options.subjects = true; },
			hover_tip: searchHover_tip,
		};
	}

	function searchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        for (var searchTerm of filterValue){
            if (item.data.characters !== null) if (item.data.characters.indexOf(searchTerm) >= 0){return true};
            if (item.data.slug.indexOf(searchTerm.replace(' ', '-')) >= 0){return true};
            for (var meaning of item.data.meanings){
                if (meaning.accepted_answer && meaning.meaning.toLowerCase().indexOf(searchTerm) >= 0){return true};
            };
            if (item.object !== 'radical' && item.object !== 'kana_vocabulary'){
                for (let reading of item.data.readings){
                    if (reading.reading.indexOf(searchTerm) >= 0) {return true;};
                };
            };
        };
		return false;
	}

	// END Global Search

	// BEGIN Exact Search
    let exactSearchHover_tip = 'Enter a search term for meaning, reading or kanji.\nReturns only exact matches.\nYou may use latin, kana and kanji.\nMultiple terms separated by commas are allowed.';

	function registerExactSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[exactSearchFilterName] = {
			type: 'text',
			label: 'Exact Search',
			default: '',
            placeholder: 'gambler, 力士',
			filter_func: exactSearchFilter,
            filter_value_map: split_list,
			set_options: function(options) { options.subjects = true; },
			hover_tip: exactSearchHover_tip,
		};
	}

	function exactSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        for (var searchTerm of filterValue){
            if (item.data.characters !== null) if (searchTerm === item.data.characters) {return true};
            if (searchTerm.replace(' ', '-') === item.data.slug){return true};
            for (var meaning of item.data.meanings){
                if (!meaning.accepted_answer) break;
                let term = meaning.meaning.trim().toLowerCase();
                if (searchTerm === term){return true};
                let words = term.split(' ').filter(function(name) {return (name.length > 0);})
                for (var word of words) {
                    if (searchTerm === word){return true};
                }
            };
            if (item.object !== 'radical' && item.object !== 'kana_vocabulary'){
                for (let reading of item.data.readings){
                    if (searchTerm === reading.reading) {return true;};
                };
            };
        };
		return false;
	}

	// END Exact Search

	// BEGIN Kunyomi Search
    let kunSearchHover_tip = 'Enter a search term for kunyomi reading.\nReturns only exact matches.\nMultiple terms separated by commas are allowed.';

	function registerKunSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[kunSearchFilterName] = {
			type: 'text',
			label: 'Kunyomi Search',
			default: '',
            placeholder: 'し、のう',
			filter_func: kunSearchFilter,
            filter_value_map: split_list,
			set_options: function(options) { options.subjects = true; },
			hover_tip: kunSearchHover_tip,
		};
	}

	function kunSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        for (var searchTerm of filterValue){
            if (item.object === 'kanji'){
                for (var reading of item.data.readings){
                    if (reading.accepted_answer && reading.type === 'kunyomi' && searchTerm === reading.reading){return true};
                };
            };
        };
		return false;
	}

    // END Kunyomi Search

	// BEGIN Onyomi Search
    let onSearchHover_tip = 'Enter a search term for onyomi reading.\nReturns only exact matches.\nMultiple terms separated by commas are allowed.';

	function registerOnSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[onSearchFilterName] = {
			type: 'text',
			label: 'Onyomi Search',
			default: '',
            placeholder: 'し、のう',
			filter_func: onSearchFilter,
            filter_value_map: split_list,
			set_options: function(options) { options.subjects = true; },
			hover_tip: onSearchHover_tip,
		};
	}

	function onSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        for (var searchTerm of filterValue){
            if (item.object === 'kanji'){
                for (var reading of item.data.readings){
                    if (reading.accepted_answer && reading.type === 'onyomi' && searchTerm === reading.reading){return true};
                };
            };
        };
		return false;
	}

    // END Onyomi Search

	// BEGIN Components Search
    let componentsSearchHover_tip = 'Looking for components on listed items.\nRadicals for kanji, Kanji for vocabulary.\nMultiple terms separated by commas are allowed.';

	function registerComponentsSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[componentsSearchFilterName] = {
			type: 'text',
			label: 'Components Search',
			default: '',
            placeholder: '能、入り口',
			filter_func: componentsSearchFilter,
            filter_value_map: makeComponentPairs,
			set_options: function(options) { options.subjects = true; },
			hover_tip: componentsSearchHover_tip,
		};
	}

	function componentsSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        return (item.object !== 'vocabulary' || item.object !== 'kana_vocabulary') && (filterValue.indexOf(item.id) >= 0);
	}

    // END Components Search

	// BEGIN Used In Search
    let usedInSearchHover_tip = 'Looking for where listed items are used.\nKanji for radicals, Vocabulary for kanji.\nMultiple terms separated by commas are allowed.';

	function registerUsedInSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[usedInSearchFilterName] = {
			type: 'text',
			label: 'Where Used Search',
			default: '',
            placeholder: '能、ground',
			filter_func: usedInSearchFilter,
            filter_value_map: makeUsedInPairs,
			set_options: function(options) { options.subjects = true; },
			hover_tip: usedInSearchHover_tip,
		};
	}

	function usedInSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        return (item.object !== 'radical' || item.object !== 'kana_vocabulary') && (filterValue.indexOf(item.id) >= 0)
	}

    // END Used In Search

	// BEGIN Visually Similar Search
    let visSimSearchHover_tip = 'Looking for kanji visually similar to listed kanji.\naccording to Wanikani.\nMultiple terms separated by commas are allowed.';

	function registerVisSimSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[visSimSearchFilterName] = {
			type: 'text',
			label: 'WK Visually Similar',
			default: '',
            placeholder: '能、人',
			filter_func: visSimSearchFilter,
            filter_value_map: makeVisuallySimilarPairs,
			set_options: function(options) { options.subjects = true; },
			hover_tip: visSimSearchHover_tip,
		};
	}

	function visSimSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        return (item.object === 'kanji') && (filterValue.indexOf(item.id) >= 0);
	}

    // END Visually Similar Search

    // BEGIN Allow List Search
    let allowSearchHover_tip = 'Enter a search term for the allow list.\nAll approximate matches will be found.\n# will match empty allow lists.\n* will match non empty allow lists.\nMultiple terms separated by commas are allowed.';

	function registerAllowSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[allowSearchFilterName] = {
			type: 'text',
			label: 'Allow List Search',
			default: '',
            placeholder: 'pure, conflict',
			filter_func: allowSearchFilter,
            filter_value_map: split_list,
			set_options: function(options) { options.subjects = true; },
			hover_tip: allowSearchHover_tip,
		};
	}

	function allowSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        var list = item.data.auxiliary_meanings.filter((entry)=> entry.type === 'whitelist');
        for (var searchTerm of filterValue){
            if (searchTerm === '#' && list.length === 0) return true;
            if (searchTerm === '*' && list.length !== 0) return true;
            for (var allowTerm of list){
                if (allowTerm.meaning.indexOf(searchTerm) >= 0) return true;
            };
        };
		return false;
	}

	// END Allow List Search

    // BEGIN Block List Search
    let blockSearchHover_tip = 'Enter a search term for the block list.\nAll approximate matches will be found.\n# will match empty block lists.\n* will match non empty block lists.\nMultiple terms separated by commas are allowed.';

	function registerBlockSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[blockSearchFilterName] = {
			type: 'text',
			label: 'Block List Search',
			default: '',
            placeholder: 'pure, conflict',
			filter_func: blockSearchFilter,
            filter_value_map: split_list,
			set_options: function(options) { options.subjects = true; },
			hover_tip: blockSearchHover_tip,
		};
	}

	function blockSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        var list = item.data.auxiliary_meanings.filter((entry)=> entry.type === 'blacklist');
        for (var searchTerm of filterValue){
            if (searchTerm === '#' && list.length === 0) return true;
            if (searchTerm === '*' && list.length !== 0) return true;
            for (var allowTerm of list){
                if (allowTerm.meaning.indexOf(searchTerm) >= 0) return true;
            };
        };
		return false;
	}

	// END Block List Search

    // BEGIN Context Sentences Search
    let ctxSenSearchHover_tip = 'Enter a search term for the context sentences.\nAll approximate matches will be found.\n# will match empty empty context sentences lists.\n* will match non empty empty context sentences lists.\nMultiple terms separated by commas are allowed.';

	function registerCtxSenSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[ctxSenSearchFilterName] = {
			type: 'text',
			label: 'Context Sentences Search',
			default: '',
            placeholder: 'pure, conflict',
			filter_func: ctxSenSearchFilter,
            filter_value_map: split_list,
			set_options: function(options) { options.subjects = true; },
			hover_tip: ctxSenSearchHover_tip,
		};
	}

	function ctxSenSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
		if (item.object !== 'vocabulary' && item.object !== 'kana_vocabulary') {
			return false;
		};
        var list = item.data.context_sentences;
        for (var searchTerm of filterValue){
            if (searchTerm === '#' && list.length === 0) return true;
            if (searchTerm === '*' && list.length !== 0) return true;
            for (var sentences of list){
                if (sentences.ja.indexOf(searchTerm) >= 0) return true;
                if (sentences.en.indexOf(searchTerm) >= 0) return true;
            };
        };
		return false;
	}

	// END  Context Sentences Search

    // BEGIN Mnemonics and Notes Search
    let mnemonicsSearchHover_tip = 'Enter a search term for the mnemonics, hints or user notes.\nAll approximate matches will be found.\nMultiple terms separated by commas are allowed.';

	function registerMnemonicsSearchFilter() {
		wkof.ItemData.registry.sources.wk_items.filters[mnemonicsSearchFilterName] = {
			type: 'text',
			label: 'Mnemonics/Notes Search',
			default: '',
            placeholder: 'pure, conflict',
			filter_func: mnemonicsSearchFilter,
            filter_value_map: split_list,
			set_options: function(options) { options.subjects = true; options.study_materials = true; },
			hover_tip: mnemonicsSearchHover_tip,
		};
	}

	function mnemonicsSearchFilter(filterValue, item) {
		if (item.data === undefined) {
			return false;
		};
        for (var searchTerm of filterValue){
            if (item.data.meaning_mnemonic.indexOf(searchTerm) >= 0) return true;
            if (item.data.reading_mnemonic && item.data.reading_mnemonic.indexOf(searchTerm) >= 0) return true;
            if (item.data.meaning_hint && item.data.meaning_hint.indexOf(searchTerm) >= 0) return true;
            if (item.data.reading_hint && item.data.reading_hint.indexOf(searchTerm) >= 0) return true;
            if (item.study_materials && item.study_materials.meaning_note && item.study_materials.meaning_note.indexOf(searchTerm) >= 0) return true;
            if (item.study_materials && item.study_materials.reading_note && item.study_materials.reading_note.indexOf(searchTerm) >= 0) return true;
        };
		return false;
	}

	// END  Mnemonics and Notes Search


    prepareSubjectIndexThenRegisterFilters();
})(window.wkof);