您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Library script that other userscripts can use to inject additional item information into WaniKani.
// ==UserScript== // @name WaniKani Item Info Injector // @namespace waniKaniItemInfoInjector // @version 3.13 // @description Library script that other userscripts can use to inject additional item information into WaniKani. // @author Sinyaven // @license MIT-0 // @match https://www.wanikani.com/* // @match https://preview.wanikani.com/* // @homepageURL https://community.wanikani.com/t/53823 // @run-at document-start // @grant none // ==/UserScript== // nomenclature: // stateSelector: {on, type, under, spoiler}; all of them are arrays // state : {on, type, under, hiddenSpoiler, id, meaning, typeDependentItemInfo...}; only under, meaning, and hiddenSpoiler are arrays; hiddenSpoiler contains a list of "under" sections that are currently hidden but might be added to the UI later (currently only during review/lessonQuiz while the item info is not fully expanded) // injectorState: {on, type, under, hiddenSpoiler, id, meaning, typeDependentItemInfo..., injector}; state + injector // callback : function getting as argument an injectorState // callbackEntry: {stateSelector, callback, appendedElements, injectorDeactivators, alreadyHandled, entryId} // typeDependentItemInfo [Text Radical]: {characters}; the property "characters" is only added if the radical is not an image // typeDependentItemInfo [ Kanji]: {characters, reading, composition, emphasis, onyomi, kunyomi, nanori}; reading, composition, onyomi, kunyomi, and nanori are arrays; composition is an array of radicals {characters, meaning} where meaning is an array // typeDependentItemInfo [ Vocabulary]: {characters, reading, composition, partOfSpeech}; reading, composition, and partOfSpeech are arrays; composition is an array of kanji {characters, meaning, reading} where meaning and reading are arrays // typeDependentItemInfo [ Kana Vocab]: {characters, partOfSpeech}; partOfSpeech is an array ((global, unsafeGlobal) => { "use strict"; /* eslint no-multi-spaces: off */ // private variables and functions let _currentState = {}; let _injectAt = {}; let _callbacks = []; let _appendedElements = []; let _scheduledCallbackElements = []; let _maxEntryId = 0; let _itemChanged = true; let _newRootElement = document.body; let _newUrl = document.URL; const _VERSION = `3.13`; const _SCRIPT_NAME = `WaniKani Item Info Injector`; const _CSS_NAMESPACE = `item-info-injector`; const _TAB_NAME_MAPPING = {}; _TAB_NAME_MAPPING.radical = { [`name` ]: `meaning`, [`examples` ]: `examples` }; _TAB_NAME_MAPPING.kanji = { [`radicals` ]: `composition`, [`meaning` ]: `meaning`, [`readings` ]: `reading`, [`examples` ]: `examples` }; _TAB_NAME_MAPPING.vocabulary = { [`kanji composition`]: `composition`, [`meaning` ]: `meaning`, [`reading` ]: `reading`, [`context` ]: `examples` }; _TAB_NAME_MAPPING.kanaVocabulary = { [`meaning` ]: `meaning`, [`context` ]: `examples` }; const _URL_HASH_MAPPING = {}; _URL_HASH_MAPPING.radical = { [`` ]: `meaning`, [`#meaning` ]: `meaning`, [`#amalgamations`]: `examples`, }; _URL_HASH_MAPPING.kanji = { [`` ]: `composition`, [`#composition` ]: `composition`, [`#meaning` ]: `meaning`, [`#reading` ]: `reading`, [`#amalgamations`]: `examples`, }; _URL_HASH_MAPPING.vocabulary = { [`` ]: `composition`, [`#composition` ]: `composition`, [`#meaning` ]: `meaning`, [`#reading` ]: `reading`, [`#context` ]: `examples`, }; _URL_HASH_MAPPING.kanaVocabulary = { [`` ]: `meaning`, [`#meaning` ]: `meaning`, [`#context` ]: `examples`, }; const _ACCORDION_NAME_MAPPING = {}; _ACCORDION_NAME_MAPPING.radical = { [`Name` ]: `meaning`, [`Found In Kanji` ]: `examples` }; _ACCORDION_NAME_MAPPING.kanji = { [`Radical Combination`]: `composition`, [`Meaning` ]: `meaning`, [`Reading` ]: `reading` }; _ACCORDION_NAME_MAPPING.vocabulary = { [`Related Kanji` ]: `composition`, [`Kanji Composition` ]: `composition`, [`Meaning` ]: `meaning`, [`Reading` ]: `reading`, [`Context` ]: `examples` }; _ACCORDION_NAME_MAPPING.kanaVocabulary = { [`Meaning` ]: `meaning`, [`Context` ]: `examples` }; const _PROPERTIES_TO_REMOVE_FROM_CURRENT_STATE = {}; _PROPERTIES_TO_REMOVE_FROM_CURRENT_STATE.radical = [`reading`, `composition`, `partOfSpeech`, `onyomi`, `kunyomi`, `nanori`, `emphasis`]; _PROPERTIES_TO_REMOVE_FROM_CURRENT_STATE.kanji = [`partOfSpeech`]; _PROPERTIES_TO_REMOVE_FROM_CURRENT_STATE.vocabulary = [`onyomi`, `kunyomi`, `nanori`, `emphasis`]; _PROPERTIES_TO_REMOVE_FROM_CURRENT_STATE.kanaVocabulary = [`reading`, `composition`, `onyomi`, `kunyomi`, `nanori`, `emphasis`]; // REGION 0: helper functions for interacting with page and other utils // get the root element that should contain all item info elements; can be the document body, but also the item info Turbo frame function _getRootElement() { if (!_relevantRootElementChildren(_newRootElement).length) _newRootElement = document.body; return _newRootElement; } // it seems like Turbo does not move the SVG and the .wk-modal element into document.body, so let's ignore it function _relevantRootElementChildren(rootElement) { return [...rootElement?.children ?? []].filter(c => c.tagName !== `svg` && !c.classList.contains(`wk-modal`)); } // from @rfindley function getController(name) { return unsafeGlobal.Stimulus.getControllerForElementAndIdentifier(document.querySelector(`[data-controller~="${name}"]`), name); } function _isFullLessonQuizUrl(url) { return /wanikani.com\/subject-lessons\/[\d-]+\/quiz/.test(url); } function _isFullLessonUrl(url) { return /wanikani.com\/subject-lessons\/[\d-]+\/\d+/.test(url) || /wanikani.com\/recent-mistakes\/[\d-]+\/subjects\/\d+\/lesson/.test(url); } function _currentIdFromLessonUrl(url) { let id = url.match(/wanikani.com\/subject-lessons\/[\d-]+\/(\d+)/)?.[1] ?? url.match(/wanikani.com\/recent-mistakes\/[\d-]+\/subjects\/(\d+)\/lesson/)?.[1]; return id == null ? null : parseInt(id); } function _rootElementContentInMainPage() { if (document.body.contains(_relevantRootElementChildren(_getRootElement())[0] ?? document.body)) return true; return new Promise(resolve => { let observer = new MutationObserver(m => { if (_relevantRootElementChildren(m[0].target).length > 0) return; observer.disconnect(); observer = null; resolve(true); }); observer.observe(_newRootElement, {childList: true}); }); } function _firstCharToLower(s) { return (s[0]?.toLowerCase() ?? ``) + s.substring(1); } // REGION 1: detect current state and set up listeners for keeping track of state changes let debug = true; function _init() { document.addEventListener(`turbo:before-render` , handleTurboBeforeRender); document.addEventListener(`turbo:before-frame-render`, handleTurboBeforeFrameRender); document.addEventListener(`turbo:click` , handleTurboClick); document.addEventListener(`turbo:load` , handleTurboLoad); window .addEventListener(`hashchange` , handleHashChange); // none of the Turbo events seem to fire when navigating the history during lessons document.addEventListener(`keydown` , handleKeyDown); _initOnNewPage(); } function handleTurboBeforeRender(e) { if (e.detail.newBody.lastElementChild?.tagName === `IFRAME`) return; // sometimes in lessons, turbo:before-render gets called twice -- ignore the first time _newUrl = document.URL; if (_init.ignoreNextLessonBeforeRender && _isFullLessonUrl(document.URL)) { _init.ignoreNextLessonBeforeRender = false; return; } // when loading wanikani/subjects/lesson, turbo:before-render is fired twice, but the second time seems to be rejected => ignore the second time if (document.location.pathname === `/subject-lessons/start`) _init.ignoreNextLessonBeforeRender = true; _newRootElement = e.detail.newBody; _initOnNewPage(); } function handleTurboBeforeFrameRender(e) { if (e.detail.newFrame.id !== `subject-info`) return; // only listen to changes to the subject info frame _newRootElement = e.detail.newFrame; _newUrl = document.URL; _initOnNewPage(); } function handleTurboClick(e) { let newId = _currentIdFromLessonUrl(e.detail.url); if (newId === null || newId !== _currentIdFromLessonUrl(document.URL)) return; // only listen to tab changes which stay on the same subject _newRootElement = document.body; _newUrl = e.detail.url; _initOnNewLessonTab(); } function handleTurboLoad() { // handle history navigation not caught by other listeners if (_newUrl === document.URL) return; _newUrl = document.URL; setTimeout(() => { _newRootElement = document.body; _initOnNewPage(); }); } function handleHashChange(e) { if (!e.isTrusted) return; handleTurboLoad(); } function handleKeyDown(e) { if (e.key !== `e`) return; let collapsedNative = document.querySelectorAll(`.subject-section__toggle[aria-expanded=false]`); let collapsed = [...document.querySelectorAll(`.subject-info section.${_CSS_NAMESPACE}-accordion-closed > button`)]; let expanded = [...document.querySelectorAll(`.subject-info section.${_CSS_NAMESPACE}:not(.${_CSS_NAMESPACE}-accordion-closed) > button`)]; (collapsedNative.length ? collapsed : expanded).forEach(e => e.click()); } function removeAllInjectedElements() { _getRootElement().querySelectorAll(`.${_CSS_NAMESPACE}, .${_CSS_NAMESPACE}-empty`).forEach(e => e.remove()); } function _initOnNewPage() { removeAllInjectedElements(); _currentState = {}; if (document.URL.includes(`wanikani.com/radicals/` )) { _currentState = {on: `itemPage`, type: `radical`}; _initItemPage(); } else if (document.URL.includes(`wanikani.com/kanji/` )) { _currentState = {on: `itemPage`, type: `kanji` }; _initItemPage(); } else if (document.URL.includes(`wanikani.com/vocabulary/`)) { _currentState = {on: `itemPage`, type: _getRootElement().querySelector(`#components`) === null ? `kanaVocabulary` : `vocabulary`}; _initItemPage(); } else if (document.URL.includes(`wanikani.com/subjects/review` )) { _currentState.on = `review`; _initReviewPage(); } else if (document.URL.includes(`wanikani.com/subjects/extra_study` )) { _currentState.on = `extraStudy`; _initReviewPage(); } else if (/wanikani.com\/recent-mistakes\/.*quiz/ .test(document.URL)) { _currentState.on = `extraStudy`; _initReviewPage(); } else if ( _isFullLessonQuizUrl(document.URL)) { _currentState.on = `lessonQuiz`; _initReviewPage(); } else if (document.location.pathname === `/subject-lessons/start` || _isFullLessonUrl(document.URL)) { _currentState.on = `lesson`; _initLessonPage(); } } function _initOnNewLessonTab() { _updateCurrentStateUnder(); _initInjectorFunctions(); _handleStateChange(); } function _initItemPage() { _updateCurrentStateItemPage(); _initInjectorFunctions(); _handleStateChange(); } function _initReviewPage() { if (_getRootElement().id !== `subject-info`) return; _updateCurrentStateReview(); _initInjectorFunctions(); _handleStateChange(); } function _initLessonPage() { _updateCurrentStateLesson(); _initInjectorFunctions(); _handleStateChange(); } function _updateCurrentState() { if (_currentState.on === `itemPage`) return; // no need to update after the initial call of _updateCurrentStateItemPage() on page load since there cannot be any changes _isFullLessonUrl(document.URL) ? _updateCurrentStateLesson() : _updateCurrentStateReview(); } function _updateCurrentStateItemPage() { _currentState.id = parseInt(document.head.querySelector(`meta[name=subject_id]`).content); _currentState.characters = _getRootElement().querySelector(`span.subject-character__characters-text`).textContent.trim(); _currentState.emphasis = _getRootElement().querySelector(`.subject-readings__reading--primary h3`)?.textContent.replace(`’`, ``).toLowerCase(); _currentState.partOfSpeech = [..._getRootElement().querySelectorAll(`.subject-section__meanings h2`)].find(h => h.textContent === `Word Type`)?.nextElementSibling.textContent.split(`,`).map(p => p.trim().replace(/\b\w/g, c => c.toUpperCase())); _currentState.meaning = [..._getRootElement().querySelectorAll(`.subject-section__meanings h2`)].filter(h => [`Primary`, `Alternative`, `Alternatives`].includes(h.textContent)).flatMap(h => h.nextElementSibling.textContent.split(`,`)).map(m => m.trim()); _currentState.onyomi = [..._getRootElement().querySelectorAll(`.subject-readings__reading-title`)].find(s => s.textContent === `On’yomi`)?.nextElementSibling.textContent.split(`,`).map(r => r.trim()).filter(r => r !== `None`); _currentState.kunyomi = [..._getRootElement().querySelectorAll(`.subject-readings__reading-title`)].find(s => s.textContent === `Kun’yomi`)?.nextElementSibling.textContent.split(`,`).map(r => r.trim()).filter(r => r !== `None`); _currentState.nanori = [..._getRootElement().querySelectorAll(`.subject-readings__reading-title`)].find(s => s.textContent === `Nanori`)?.nextElementSibling.textContent.split(`,`).map(r => r.trim()).filter(r => r !== `None`); _currentState.composition = [..._getRootElement().querySelectorAll(`.subject-section--components .subject-character__characters`)].map(s => { let result = {meaning: [...s.nextElementSibling.querySelectorAll(`.subject-character__meaning`)].map(m => m.textContent.trim()), reading: [...s.nextElementSibling.querySelectorAll(`.subject-character__reading`)].map(r => r.textContent.trim()), characters: s.textContent.trim()}; if (result.reading.length === 0) delete result.reading; if (result.characters === ``) delete result.characters; return result; }); _currentState.reading = [..._getRootElement().querySelectorAll(`.reading-with-audio__reading`)].map(p => p.textContent).concat(_currentState[_currentState.emphasis] || []); _PROPERTIES_TO_REMOVE_FROM_CURRENT_STATE[_currentState.type].forEach(prop => delete _currentState[prop]); _updateCurrentStateUnder(); } function _updateCurrentStateReview() { let subjects = JSON.parse((_getRootElement().querySelector(`[data-quiz-queue-target="subjects"]`) ?? document.querySelector(`[data-quiz-queue-target="subjects"]`)).textContent); let currentId = parseInt(document.querySelector(`[data-subject-id]`)?.dataset.subjectId ?? subjects[0]?.id); let currentItem = subjects.find(s => s.id === currentId) ?? getController(`quiz-queue`).quizQueue.activeQueue.find(b => b.id == currentId); _currentState.id = currentItem.id; _currentState.type = _firstCharToLower(currentItem.type); _currentState.characters = currentItem.characters; _currentState.emphasis = currentItem.readings?.find(r => r.kind === `primary`)?.type; _currentState.partOfSpeech = [..._getRootElement().querySelectorAll(`.subject-section__meanings h2`)].find(h => h.textContent === `Word Type`)?.nextElementSibling.textContent.split(`,`).map(p => p.trim().replace(/\b\w/g, c => c.toUpperCase())); _currentState.meaning = currentItem.meanings?.filter(r => [`primary`, `alternative`].includes(r.kind)).map(m => m.text); _currentState.onyomi = currentItem.readings?.filter(r => r.type === `onyomi`).map(r => r.text); _currentState.kunyomi = currentItem.readings?.filter(r => r.type === `kunyomi`).map(r => r.text); _currentState.nanori = currentItem.readings?.filter(r => r.type === `nanori`).map(r => r.text); _currentState.composition = [..._getRootElement().querySelectorAll(`.subject-section--components .subject-character__characters`)].map(s => { let result = {meaning: [...s.nextElementSibling.querySelectorAll(`.subject-character__meaning`)].map(m => m.textContent.trim()), reading: [...s.nextElementSibling.querySelectorAll(`.subject-character__reading`)].map(r => r.textContent.trim()), characters: s.textContent.trim()}; if (result.reading.length === 0) delete result.reading; if (result.characters === ``) delete result.characters; return result; }); _currentState.reading = _currentState.type === `kanji` ? _currentState[_currentState.emphasis] : currentItem.readings?.filter(r => [`primary`, `alternative`].includes(r.kind)).map(r => r.text); Object.entries(_currentState).filter(e => !e[1]).forEach(e => delete _currentState[e[0]]); _PROPERTIES_TO_REMOVE_FROM_CURRENT_STATE[_currentState.type].forEach(prop => delete _currentState[prop]); _updateCurrentStateUnder(); } function _updateCurrentStateLesson() { _currentState.id = _currentIdFromLessonUrl(document.URL) ?? parseInt(_getRootElement().querySelector(`[id=user_synonyms]`).getAttribute(`src`).match(/subject_id=(\d+)/)?.[1]); _currentState.type = [`radical`, `kanji`, `vocabulary`].find(t => _getRootElement().querySelector(`.character-header--${t}`) !== null); _currentState.type = _currentState.type === `vocabulary` && _getRootElement().querySelector(`#composition`) === null ? `kanaVocabulary` : _currentState.type; _currentState.characters = _getRootElement().querySelector(`.character-header__characters`).textContent; _currentState.partOfSpeech = [`vocabulary`, `kanaVocabulary`].includes(_currentState.type) ? [..._querySelector(`.subject-section__title-text {Word Type} ^+`)?.children ?? []].flatMap(c => c.textContent.split(`, `)) : null; _currentState.meaning = [_getRootElement().querySelector(`.character-header__meaning`).textContent, ...[..._querySelector(`.subject-section__title-text {Other Meanings} ^+`)?.children ?? []].flatMap(c => c.textContent.split(`, `))]; _currentState.onyomi = _currentState.type === `kanji` ? [..._querySelector(`.subject-section__title-text {Readings (on’yomi) } ^+`)?.children ?? []].map(c => c.textContent) : null; _currentState.kunyomi = _currentState.type === `kanji` ? [..._querySelector(`.subject-section__title-text {Readings (kun’yomi)} ^+`)?.children ?? []].map(c => c.textContent) : null; _currentState.nanori = _currentState.type === `kanji` ? [..._querySelector(`.subject-section__title-text {Readings (nanori) } ^+`)?.children ?? []].map(c => c.textContent) : null; _currentState.emphasis = _currentState.type === `kanji` ? [`onyomi`, `kunyomi`, `nanori`].find(r => _currentState[r].length) : null; _currentState.composition = _currentState.type === `kanji` ? [..._querySelector(`.subject-section__title-text {Radical Composition} ^+`)?.querySelectorAll(`.subject-character__characters`) ?? []].map(s => { let result = {meaning: [...s.nextElementSibling.querySelectorAll(`.subject-character__meaning`)].map(m => m.textContent.trim()), reading: [...s.nextElementSibling.querySelectorAll(`.subject-character__reading`)].map(r => r.textContent.trim()), characters: s.textContent.trim()}; if (result.reading.length === 0) delete result.reading; if (result.characters === ``) delete result.characters; return result; }) : _currentState.type === `vocabulary` ? [..._querySelector(`.subject-section__title-text { Kanji Composition} ^+`)?.querySelectorAll(`.subject-character__characters`) ?? []].map(s => { let result = {meaning: [...s.nextElementSibling.querySelectorAll(`.subject-character__meaning`)].map(m => m.textContent.trim()), reading: [...s.nextElementSibling.querySelectorAll(`.subject-character__reading`)].map(r => r.textContent.trim()), characters: s.textContent.trim()}; if (result.reading.length === 0) delete result.reading; if (result.characters === ``) delete result.characters; return result; }) : null; _currentState.reading = _currentState.type === `radical` ? null : [..._getRootElement().querySelectorAll(`.reading-with-audio__reading`)].map(r => r.textContent).concat(_currentState[_currentState.emphasis] ?? []); _PROPERTIES_TO_REMOVE_FROM_CURRENT_STATE[_currentState.type].forEach(prop => delete _currentState[prop]); _updateCurrentStateUnder(); } function _updateCurrentStateUnder() { // update _currentState.under and _currentState.hiddenSpoiler _currentState.hiddenSpoiler = []; if (_currentState.on === `lesson`) { _currentState.under = [_URL_HASH_MAPPING[_currentState.type][new URL(_newUrl).hash]]; } else { switch (_currentState.type) { case `kanaVocabulary`: case `radical` : _currentState.under = [ `meaning`, `examples`]; break; case `kanji` : case `vocabulary` : _currentState.under = [`composition`, `meaning`, `reading`, `examples`]; break; } if ([`review`, `lessonQuiz`, `extraStudy`].includes(_currentState.on)) { let spoiler = []; switch (document.querySelector(`[for=user-response]`).dataset.questionType) { case `meaning`: spoiler = [`reading`, `composition`]; break; case `reading`: spoiler = [`meaning`, `composition`, `examples`]; break; } _currentState.hiddenSpoiler = _currentState.under.filter(u => spoiler.includes(u)); _currentState.under = _currentState.under.filter(u => !spoiler.includes(u)); } } } // REGION 2: code that provides the item info injection functionality function _skipInjectedSections(location) { while (location?.nextElementSibling?.classList.contains(_CSS_NAMESPACE)) location = location.nextElementSibling; return location; } function _querySelector(selector) { selector = selector.replace(`%current-tab`, () => Object.entries(_URL_HASH_MAPPING[_currentState.type]).reverse().find(e => e[1] === _currentState.under[0])[0]); selector = selector.replace(/%(\w+)/, (_, under) => { _injectAt[under](); return `#${_CSS_NAMESPACE}-loc-${under}`; }); let [, sel, contentFilter, indexSelector, customSuffix] = selector.match(/^(.+?)(?:{(.+)})?(\$?)([\+\-\^\~\s]*)$/); let result = (contentFilter || indexSelector) ? [..._getRootElement().querySelectorAll(sel)] : _getRootElement().querySelector(sel); if (contentFilter) { contentFilter = contentFilter.split(`,`).map(c => c.trim()); result = contentFilter.reduce((element, filter) => element || result.find(r => r.textContent === filter && !r.classList.contains(_CSS_NAMESPACE)), null); } else if (indexSelector) { result = result.pop(); // for now, the only available indexSelector is "$" with the meaning "last match" } [...customSuffix].forEach(c => { if (c === `-`) result = result?.previousElementSibling; if (c === `+`) result = result?.nextElementSibling; if (c === `^`) result = result?.parentElement; if (c === `~`) result = _skipInjectedSections(result); }); return result; } function _xPathSelector(selector) { // selector = selector.replace(/%(\w+)/, (_, under) => { _injectAt[under](); return `*[@id="${_CSS_NAMESPACE}-loc-${under}"]`; }); return document.evaluate(selector, _getRootElement(), null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } function _injectLocatorAt(id, ...selector) { let locator = document.createElement(`div`); locator.classList.add(`${_CSS_NAMESPACE}-empty`); locator.id = id; let inject = selector.reduce((injectFunc, sel) => { if (injectFunc) return injectFunc; let [, xpath, s, customSuffix] = sel.match(/^(XPATH |)(.+?)([<>\-])?\s*$/); let location = xpath ? _xPathSelector(s) : _querySelector(s); if (!location) return null; if (customSuffix === `<`) return location.prepend.bind(location); if (customSuffix === `>`) return location.append .bind(location); if (customSuffix === `-`) return location.previousElementSibling?.after.bind(location.previousElementSibling) || location.before.bind(location); // workaround for inserting before space in pageList else return location.after .bind(location); }, null); if (!inject) return null; inject(locator); _appendedElements.push(locator); return locator; } function _setInjectorFunc(under, ...selector) { let id = `${_CSS_NAMESPACE}-loc-${under}`; _injectAt[under] = (...elements) => { let locator = _getRootElement().ownerDocument?.getElementById(id) ?? _getRootElement().querySelector(`[id="${id}"]`) ?? _injectLocatorAt(id, ...selector); if (!locator) { console.warn(`${_SCRIPT_NAME}: Could not find location under ${under}`); return; } locator = _skipInjectedSections(locator); locator.after(...elements); }; } // custom CSS: // XPATH (at start): main part is xpath selector, not css selector // current-tab: the currently open lesson tab // %top etc.: the specified anchor // {text content} (at end): element with given textContent // $ (at end): last match // - (at end): previousElementSibling | // + (at end): nextElementSibling | any order // ^ (at end): parentElement | // ~ (at end): skip injected sections | // < (at end): prepend (first child) // > (at end): append (last child) // - (at end): before function _initInjectorFunctions() { switch(_currentState.on) { case `itemPage`: _setInjectorFunc(`top` , `.page-nav` ); _setInjectorFunc(`topSide` , `%top ~` ); _setInjectorFunc(`bottom` , `.subject-section--progress -`, `.page-nav ^>` ); _setInjectorFunc(`bottomSide` , `%bottom ~` ); _setInjectorFunc(`composition` , `.subject-section--components` ); _setInjectorFunc(`meaning` , `.subject-section--meaning` ); _setInjectorFunc(`reading` , `.subject-section--reading` ); _setInjectorFunc(`examples` , `.subject-section--amalgamations, .subject-section--context` ); _setInjectorFunc(`meaningSide` , `.subject-section__meanings $` ); _setInjectorFunc(`readingSide` , `.subject-readings, .subject-readings-with-audio` ); _setInjectorFunc(`compositionSubsection`, `.subject-section--components >` ); _setInjectorFunc(`meaningSubsection` , `#user_meaning_note ^-`, `.subject-section--meaning >` ); _setInjectorFunc(`readingSubsection` , `#user_reading_note ^-`, `.subject-section--reading >` ); _setInjectorFunc(`examplesSubsection` , `.subject-section--amalgamations, .subject-section--context >`); _setInjectorFunc(`compositionPageList` , `.wk-nav__item-link[href='#components'] ^`, `%topSidePageList ~`); _setInjectorFunc(`meaningPageList` , `.wk-nav__item-link[href='#meaning'], .wk-nav__item-link[href='#information'] ^`); _setInjectorFunc(`readingPageList` , `.wk-nav__item-link[href='#reading'] ^` ); _setInjectorFunc(`examplesPageList` , `.wk-nav__item-link[href='#amalgamations'], .wk-nav__item-link[href='#context'] ^`); _setInjectorFunc(`topPageList` , `.wk-nav__items <` ); _setInjectorFunc(`topSidePageList` , `%topPageList ~` ); _setInjectorFunc(`bottomPageList` , `.wk-nav__item-link[href='#progress'] ^-`, `.wk-nav__items >`); _setInjectorFunc(`bottomSidePageList` , `%bottomPageList ~` ); break; case `lesson`: _setInjectorFunc(`customSideInfo` , `%current-tab .subject-slide__sections -`); _setInjectorFunc(`top` , `%current-tab .subject-slide__sections <`); _setInjectorFunc(`bottom` , `%current-tab .subject-slide__sections >`); _setInjectorFunc(`topSide` , `%current-tab .subject-slide__aside <`, `%customSideInfo <`); _setInjectorFunc(`bottomSide` , `%current-tab .subject-slide__aside >`, `%customSideInfo >`); _setInjectorFunc(`composition` , `%bottom -`); _setInjectorFunc(`meaning` , `%bottom -`); _setInjectorFunc(`reading` , `%bottom -`); _setInjectorFunc(`examples` , `%bottom -`); _setInjectorFunc(`meaningSide` , `%bottomSide -`); _setInjectorFunc(`readingSide` , `%bottomSide -`); _setInjectorFunc(`compositionSubsection`, `%composition -`); _setInjectorFunc(`meaningSubsection` , `#meaning .subject-section__title-text {Meaning Notes, Name Notes} ^^-`); _setInjectorFunc(`readingSubsection` , `#reading .subject-section__title-text {Reading Notes} ^^-`); _setInjectorFunc(`examplesSubsection` , `%examples -`); break; case `lessonQuiz`: case `review`: case `extraStudy`: _setInjectorFunc(`top` , `.container <`); _setInjectorFunc(`bottom` , `.container >`); _setInjectorFunc(`topSide` , `%top ~` ); _setInjectorFunc(`bottomSide` , `%bottom ~` ); _setInjectorFunc(`composition` , `.subject-section--components` ); _setInjectorFunc(`meaning` , `.subject-section--meaning` ); _setInjectorFunc(`reading` , `.subject-section--reading` ); _setInjectorFunc(`examples` , `.subject-section--context, .subject-section--amalgamations`); _setInjectorFunc(`meaningSide` , `.subject-section__meanings $` ); _setInjectorFunc(`readingSide` , `.subject-readings-with-audio, .subject-readings`); _setInjectorFunc(`compositionSubsection`, `.subject-section--components .subject-section__content >`); _setInjectorFunc(`meaningSubsection` , `#user_meaning_note ^-` ); _setInjectorFunc(`readingSubsection` , `#user_reading_note ^-` ); _setInjectorFunc(`examplesSubsection` , `.subject-section--context .subject-section__content >`, `.subject-section--amalgamations .subject-section__content >`); break; default: break; } } function _handleStateChange() { _handleCallbacks(_callbacks); } function _handleCallbacks(callbacks) { callbacks.forEach(c => { c.injectorDeactivators.forEach(d => d()); c.injectorDeactivators = []; }); _appendedElements.push(...callbacks.map(c => Object.values(c.appendedElements)).flat(2)); _appendedElements.forEach(e => e.remove()); _appendedElements = []; callbacks.forEach(c => { c.appendedElements = {}; c.alreadyHandled = false; _handleCallbackEntry(c); }); _appendElementsGeneratedByCallbacks(_callbacks); // reappend ALL callbacks to guarantee consistent order } function _appendElementsGeneratedByCallbacks(callbacks) { let locations = [...new Set(callbacks.flatMap(c => Object.keys(c.appendedElements)))].filter(l => l !== `others`); let entries = locations.map(l => [l, callbacks.flatMap(c => c.appendedElements[l] || [])]); entries.forEach(([location, elements]) => _injectAt[location](...elements)); _updateCustomSideInfoVisibility(); } function _scheduleAppendElementsGeneratedByCallbacks(callbacks) { if (_scheduledCallbackElements.length === 0) { global.requestAnimationFrame(() => { _appendElementsGeneratedByCallbacks(_scheduledCallbackElements); _scheduledCallbackElements = []; }); } _scheduledCallbackElements.push(...callbacks); } function _addCallbackEntry(stateSelector, callback) { let entryId = ++_maxEntryId; let callbackEntry = {stateSelector, callback, appendedElements: {}, injectorDeactivators: [], alreadyHandled: false, entryId}; _callbacks.push(callbackEntry); if (_currentState.under) { // only handle if current state is already known _handleCallbackEntry(callbackEntry); _scheduleAppendElementsGeneratedByCallbacks([callbackEntry]); } return entryId; } async function _handleCallbackEntry(callbackEntry) { if (callbackEntry.alreadyHandled) return; let injectUnder = _matchesCurrentStateUnder(callbackEntry.stateSelector); if (!injectUnder) return; let injectorState = _currentStateDeepCopy(); injectorState.injector = _createInjector(injectUnder, callbackEntry.stateSelector.spoiler, callbackEntry.appendedElements, callbackEntry.injectorDeactivators); //try { callbackEntry.callback.call(null, injectorState); } catch(e) { console.error(e); } // error in console pointed to this line as error source => workaround: use async so that caller of _handleCallbackEntry() continues to run even after error callbackEntry.callback.call(null, injectorState); callbackEntry.alreadyHandled = true; } function _matchesCurrentStateUnder(stateSelector) { if (!stateSelector.on .includes(_currentState.on )) return null; if (!stateSelector.type.includes(_currentState.type)) return null; let allUnder = [..._currentState.under, ..._currentState.hiddenSpoiler]; let result = [...stateSelector.under].reverse().find(s => allUnder.includes(s)); return result; } function _createInjector(injectUnder, spoiler, appendedElements, injectorDeactivators) { let sideInfo = true; let injActive = true; let injector = {get active() { return injActive; }}; injector.registerAppendedElement = (element ) => _injectorRegisterAppendedElement(element, injActive, appendedElements); injector.append = (heading, body, additionalSettings) => _injectorAppend(heading, body, additionalSettings, injectUnder, injActive, `` , spoiler, appendedElements); injector.appendSubsection = (heading, body, additionalSettings) => _injectorAppend(heading, body, additionalSettings, injectUnder, injActive, `Subsection`, spoiler, appendedElements); if (sideInfo) injector.appendSideInfo = (heading, body, additionalSettings) => _injectorAppend(heading, body, additionalSettings, injectUnder, injActive, `Side` , spoiler, appendedElements); injector.appendAtTop = (heading, body, additionalSettings) => _injectorAppend(heading, body, additionalSettings, injectUnder, injActive, `Top` , spoiler, appendedElements); injector.appendAtBottom = (heading, body, additionalSettings) => _injectorAppend(heading, body, additionalSettings, injectUnder, injActive, `Bottom` , spoiler, appendedElements); if (sideInfo) injector.appendSideInfoAtTop = (heading, body, additionalSettings) => _injectorAppend(heading, body, additionalSettings, injectUnder, injActive, `TopSide` , spoiler, appendedElements); if (sideInfo) injector.appendSideInfoAtBottom = (heading, body, additionalSettings) => _injectorAppend(heading, body, additionalSettings, injectUnder, injActive, `BottomSide`, spoiler, appendedElements); injectorDeactivators.push(() => { injActive = false; }); return injector; } function _injectorRegisterAppendedElement(element, injActive, appendedElements) { if (!injActive ) throw `${_SCRIPT_NAME}: Injector is inactive`; if (![1, 3].includes(element.nodeType)) throw `${_SCRIPT_NAME}: Can only register elements or text nodes`; if (appendedElements.others) appendedElements.others.push(element); else appendedElements.others = [element]; } function _injectorAppend(heading, body, additionalSettings, injectUnder, injActive, special, spoiler, appendedElements) { if (!body) return null; let under = additionalSettings?.under || injectUnder; if (!injActive ) throw `${_SCRIPT_NAME}: Injector is inactive`; if (!_currentState.under.concat(_currentState.hiddenSpoiler).includes(under)) throw `${_SCRIPT_NAME}: Under ${under} not available`; if (![`meaning`, `reading`].includes(under) && special === `Side` ) throw `${_SCRIPT_NAME}: Cannot append side info under ${under}`; if ([`Top`, `TopSide`, `Bottom`, `BottomSide`].includes(special)) { under = _firstCharToLower(special); special = ``; } let pageListEntry = null; if (_currentState.on === `itemPage` && !special) { let u = `${under}PageList`; let space = document.createTextNode(` `); if (appendedElements[u]) appendedElements[u].push(space); else appendedElements[u] = [space]; pageListEntry = _createSection(under, `PageList`, appendedElements); } let section = _createSection(under, special, appendedElements); _insertContent(heading, body, special, spoiler, section, pageListEntry, additionalSettings?.sectionName); if (additionalSettings?.injectImmediately && section) _injectAt[under + special](section); return section; } function _createSection(under, special, appendedElements) { under += special; let itemPageSideInfo = _currentState.on !== `lesson` && special === `Side`; let itemPagePageList = special === `PageList`; let section = document.createElement(itemPageSideInfo ? `div` : itemPagePageList ? `li` : `section`); section.classList.add(_CSS_NAMESPACE); section.classList.add(`${_CSS_NAMESPACE}-empty`); if (itemPagePageList) section.classList.add(`wk-nav__item`); if (itemPageSideInfo) section.classList.add(`subject-section__meanings`); if (appendedElements[under]) appendedElements[under].push(section); else appendedElements[under] = [section]; return section; } async function _insertContent(heading, body, special, spoiler, section, pageListEntry, sectionName) { if (!section) return; let onItemPageOrReview = _currentState.on !== `lesson`; let isSideInfo = special === `Side`; let collapsibleSection = ![`itemPage`, `lesson`].includes(_currentState.on) && special !== `Subsection` && special !== `Side`; if ([heading, body].some(p => p && typeof p === `object` && typeof p.then === `function`)) { heading = await heading; body = await body; } if (!body) return; let elements = _toElements(body, false); if (elements.length === 0) return; let hHeading = collapsibleSection ? _toAccordionHeadingElement(heading) : _toHeadingElement(heading, onItemPageOrReview, special); if (hHeading) { if (pageListEntry && sectionName !== ``) { let link = document.createElement(`a`); link.href = `#`; link.textContent = sectionName || hHeading.textContent; link.classList.add(`wk-nav__item-link`); link.addEventListener(`click`, e => { section.scrollIntoView({behavior: `smooth`}); e.preventDefault(); }); pageListEntry.append(link); pageListEntry.classList.remove(`${_CSS_NAMESPACE}-empty`); } section.appendChild(hHeading); } if (collapsibleSection) { section.classList.toggle(`${_CSS_NAMESPACE}-accordion-closed`, spoiler.some(s => _currentState.hiddenSpoiler.includes(s))); section.classList.add(`${_CSS_NAMESPACE}-accordion`); let div = document.createElement(`div`); div.append(...elements); elements = [div]; } if (onItemPageOrReview && isSideInfo) elements.forEach(e => e.classList.add(`text-gray-700`, `subject-section__meanings-items`)); section.append(...elements); section.classList.remove(`${_CSS_NAMESPACE}-empty`); _updateCustomSideInfoVisibility(section.ownerDocument); } function _toElements(strOrElements, justArray) { if (!strOrElements) return []; if (!Array.isArray(strOrElements)) strOrElements = [strOrElements]; if (justArray || strOrElements.every(e => e.nodeType === 1)) return strOrElements; let p = document.createElement(`p`); p.append(...strOrElements); return [p]; } function _toHeadingElement(heading, onItemPageOrReview, special) { let isSubsection = special === `Subsection`; let elements = _toElements(heading, true); if (elements.length === 0) return null; let result = document.createElement((onItemPageOrReview && isSubsection) ? `H3` : `H2`); result.append(...elements); if (onItemPageOrReview) result.classList.add(isSubsection ? `subject-section__subtitle` : special === `Side` ? `subject-section__meanings-title` : `subject-section__title`); return result; } function _toAccordionHeadingElement(heading) { let svgns = `http://www.w3.org/2000/svg`; let result = document.createElement(`BUTTON`); let arrow = document.createElement(`span`); let svg = document.createElementNS(svgns, `svg`); let use = document.createElementNS(svgns, `use`); arrow.classList.add(`subject-section__toggle-icon`); svg.classList.add(`wk-icon`, `wk-icon--chevron_right`); svg.setAttributeNS(svgns, `viewBox`, `0 0 320 512`); svg.setAttribute(`aria-hidden`, `true`); use.setAttribute(`href`, `#wk-icon__chevron-right`); result.addEventListener(`click`, _handleAccordionHeadingClick); svg.append(use); arrow.append(svg); result.append(arrow, ..._toElements(heading, true)); return result; } function _handleAccordionHeadingClick(e) { e.currentTarget.parentElement.classList.toggle(`${_CSS_NAMESPACE}-accordion-closed`); } function _updateCustomSideInfoVisibility() { if (_currentState.on !== `lesson` || !_currentState.under.includes(`meaning`)) return; let side = _getRootElement().querySelector(`#${_CSS_NAMESPACE}-loc-customSideInfo`); side?.classList.add(`pure-u-1-4`, `col1`, `subject-slide__aside`); side?.classList.toggle(`${_CSS_NAMESPACE}-empty`, !side.querySelector(`.${_CSS_NAMESPACE}:not(.${_CSS_NAMESPACE}-empty)`)); } // REGION 3: CSS injection function _addCss() { let style = document.createElement(`style`); style.textContent = ` .${_CSS_NAMESPACE}-empty { display: none; } .${_CSS_NAMESPACE}:not(.wk-nav__item):not(.subject-section__meanings) { scroll-margin: 80px; margin: 0 0 30px; line-height: 1.6; font-size: 16px; text-shadow: 0 1px 0 #fff; } .${_CSS_NAMESPACE} > h2:not(.subject-section__meanings-title) { line-height: 1.6; font-family: var(--font-family-title); line-height: 1.4; font-size: 28px; text-shadow: 0 1px 0 #fff; border-bottom: 1px solid #d5d5d5; margin-bottom: 10px; } .${_CSS_NAMESPACE} > h3 { margin-top: 30px; } .${_CSS_NAMESPACE}.${_CSS_NAMESPACE}-accordion { margin: 0; } .${_CSS_NAMESPACE}-accordion-closed > div { display: none; } .${_CSS_NAMESPACE}-accordion > button { align-items: center; display: flex; width: 100%; padding: 0 0 7px; background: none; text-align: left; cursor: pointer; font-family: var(--font-family-title); line-height: 1.4; font-size: 28px; text-shadow: 0 1px 0 #fff; border: none; border-bottom: 1px solid #d5d5d5; margin-bottom: 10px; color: inherit; } .${_CSS_NAMESPACE}-accordion .subject-section__toggle-icon { display: block; transform: rotate(90deg); margin-right: var(--spacing-tight); } .${_CSS_NAMESPACE}-accordion-closed .subject-section__toggle-icon { transform: rotate(0); } .${_CSS_NAMESPACE}-accordion > button > span > svg { width: 11.25px } .subject-readings { margin-bottom: var(--spacing-normal); }`; document.head.appendChild(style); } // REGION 4: Helper functions for providing the interface that allows to register callbacks function _currentStateDeepCopy() { let copy = {..._currentState}; let arrayProperties = [`under`, `hiddenSpoiler`, `partOfSpeech`, `meaning`, `onyomi`, `kunyomi`, `nanori`, `reading`]; arrayProperties.forEach(prop => { if (_currentState[prop]) copy[prop] = [..._currentState[prop]]; }); let composition = _currentState.composition?.map(c => { let result = {characters: c.characters, meaning: [...c.meaning]}; if (c.reading) result.reading = [...c.reading]; return result; }); if (composition) copy.composition = composition; return copy; } function _argumentsToArray(args) { return args?.flatMap(a => a.split(`,`)).map(a => a.trim()) || []; } function _removeDuplicates(array) { return array.filter((a, i) => array.indexOf(a) === i); } function _checkAgainst(array, keywords) { let duplicateKeywords = array.filter((a, i) => array.includes(a, i + 1) && array.indexOf(a) === i); let unknownKeywords = _removeDuplicates(array.filter(a => !keywords.includes(a))); if (unknownKeywords .length > 0) throw `${_SCRIPT_NAME}: Unknown keywords [${unknownKeywords.join(`, `)}]`; if (duplicateKeywords.length > 0) throw `${_SCRIPT_NAME}: Duplicate keywords [${duplicateKeywords.join(`, `)}]`; return array; } function _fillStateSelector(stateSelector) { if (!stateSelector.on ?.length) stateSelector.on = [`itemPage`, `lesson`, `lessonQuiz`, `review`, `extraStudy`]; if (!stateSelector.type ?.length) stateSelector.type = [`radical`, `kanji`, `vocabulary`, `kanaVocabulary`]; if (!stateSelector.under?.length) stateSelector.under = [`composition`, `meaning`, `reading`, `examples`]; stateSelector.spoiler = stateSelector.spoiler || stateSelector.under; return stateSelector; } function _executeCallback(strOrCallback) { if (typeof strOrCallback !== `function`) return strOrCallback; return strOrCallback(_currentStateDeepCopy()); } function _removeElementsOfEntry(callbackEntry) { callbackEntry.injectorDeactivators.forEach(d => d()); callbackEntry.injectorDeactivators = []; Object.values(callbackEntry.appendedElements).flat().forEach(e => e.remove()); callbackEntry.appendedElements = {}; } function _remove(entryId) { let callbackEntry = _callbacks.find(c => c.entryId === entryId); _removeElementsOfEntry(callbackEntry); _callbacks = _callbacks.filter(c => c.entryId !== entryId); _updateCustomSideInfoVisibility(); } function _renew(entryId) { let callbackEntry = _callbacks.find(c => c.entryId === entryId); callbackEntry.alreadyHandled = false; _removeElementsOfEntry(callbackEntry); _handleCallbackEntry(callbackEntry); _appendElementsGeneratedByCallbacks(_callbacks); _updateCustomSideInfoVisibility(); } function _isNewerThan(otherVersion) { let v1 = _VERSION.split(`.`).map(v => parseInt(v)); let v2 = otherVersion.split(`.`).map(v => parseInt(v)); return v1.reduce((r, v, i) => r ?? (v === v2[i] ? null : (v > (v2[i] || 0))), null) || false; } function _selectorChain(currentChainLink, stateSelector) { let result = {}; switch(currentChainLink) { case `on` : result.forType = (...types ) => _forType (stateSelector, types ); // fall through case `forType` : result.under = (...tabs ) => _under (stateSelector, tabs ); // fall through case `under` : result.spoiling = (...spoilers ) => _spoiling (stateSelector, spoilers); // fall through case `spoiling`: result.notify = (callback ) => _notify (stateSelector, callback); result.notifyWhenVisible = (callback ) => _notifyWhenVisible (stateSelector, callback); result.append = (heading, body) => _append (stateSelector, heading, body); result.appendSideInfo = (heading, body) => _appendSideInfo (stateSelector, heading, body); result.appendSubsection = (heading, body) => _appendSubsection (stateSelector, heading, body); result.appendAtTop = (heading, body) => _appendAtTop (stateSelector, heading, body); result.appendAtBottom = (heading, body) => _appendAtBottom (stateSelector, heading, body); result.appendSideInfoAtTop = (heading, body) => _appendSideInfoAtTop (stateSelector, heading, body); result.appendSideInfoAtBottom = (heading, body) => _appendSideInfoAtBottom(stateSelector, heading, body); } if (stateSelector.on?.length && stateSelector.on .every(o => ![`review`, `lessonQuiz`, `extraStudy`].includes(o))) delete result.spoiling; if ( stateSelector.under?.some (u => ![`meaning`, `reading` ].includes(u))) { delete result.appendSideInfo; delete result.appendSideInfoAtTop; delete result.appendSideInfoAtBottom; } return result; } function _actionHandle(entryId) { return entryId ? { remove: () => _remove(entryId), renew : () => _renew (entryId) } : null; } function _on(stateSelector, pages) { stateSelector.on = _checkAgainst(_argumentsToArray(pages), [`itemPage`, `lesson`, `lessonQuiz`, `review`, `extraStudy`]); return _selectorChain(`on`, stateSelector); } function _forType(stateSelector, types) { stateSelector.type = _checkAgainst(_argumentsToArray(types), [`radical`, `kanji`, `vocabulary`, `kanaVocabulary`]); return _selectorChain(`forType`, stateSelector); } function _under(stateSelector, tabs) { stateSelector.under = _checkAgainst(_argumentsToArray(tabs), [`composition`, `meaning`, `reading`, `examples`]); return _selectorChain(`under`, stateSelector); } function _spoiling(stateSelector, spoilers) { stateSelector.spoiler = (spoilers.length === 1 && spoilers[0].trim() === `nothing`) ? [] : _checkAgainst(_argumentsToArray(spoilers), [`composition`, `meaning`, `reading`, `examples`]); return _selectorChain(`spoiling`, stateSelector); } function _notifyWhenVisible(stateSelector, callback) { stateSelector.content = true; return _notify(stateSelector, async (...args) => { await _rootElementContentInMainPage(); callback(...args); }); } function _notify(stateSelector, callback) { let entryId = null; if (callback) entryId = _addCallbackEntry(_fillStateSelector(stateSelector), callback); return _actionHandle(entryId); } function _append(stateSelector, heading, body) { let entryId = null; if (body) entryId = _addCallbackEntry(_fillStateSelector(stateSelector), injectorState => injectorState.injector.append(_executeCallback(heading), _executeCallback(body))); return _actionHandle(entryId); } function _appendSideInfo(stateSelector, heading, body) { stateSelector.content = true; if (!stateSelector.under?.length) stateSelector.under = [`meaning`, `reading`]; let entryId = null; if (body) entryId = _addCallbackEntry(_fillStateSelector(stateSelector), injectorState => injectorState.injector.appendSideInfo(_executeCallback(heading), _executeCallback(body))); return _actionHandle(entryId); } function _appendSubsection(stateSelector, heading, body) { stateSelector.content = true; let entryId = null; if (body) entryId = _addCallbackEntry(_fillStateSelector(stateSelector), injectorState => injectorState.injector.appendSubsection(_executeCallback(heading), _executeCallback(body))); return _actionHandle(entryId); } function _appendAtTop(stateSelector, heading, body) { let entryId = null; if (body) entryId = _addCallbackEntry(_fillStateSelector(stateSelector), injectorState => injectorState.injector.appendAtTop(_executeCallback(heading), _executeCallback(body))); return _actionHandle(entryId); } function _appendAtBottom(stateSelector, heading, body) { let entryId = null; if (body) entryId = _addCallbackEntry(_fillStateSelector(stateSelector), injectorState => injectorState.injector.appendAtBottom(_executeCallback(heading), _executeCallback(body))); return _actionHandle(entryId); } function _appendSideInfoAtTop(stateSelector, heading, body) { if (!stateSelector.under?.length) stateSelector.under = [`meaning`, `reading`]; let entryId = null; if (body) entryId = _addCallbackEntry(_fillStateSelector(stateSelector), injectorState => injectorState.injector.appendSideInfoAtTop(_executeCallback(heading), _executeCallback(body))); return _actionHandle(entryId); } function _appendSideInfoAtBottom(stateSelector, heading, body) { if (!stateSelector.under?.length) stateSelector.under = [`meaning`, `reading`]; let entryId = null; if (body) entryId = _addCallbackEntry(_fillStateSelector(stateSelector), injectorState => injectorState.injector.appendSideInfoAtBottom(_executeCallback(heading), _executeCallback(body))); return _actionHandle(entryId); } function _domReady() { return document.readyState === `interactive` || document.readyState === `complete`; } async function _publishInterface() { if (unsafeGlobal.wkItemInfo && !_isNewerThan(unsafeGlobal.wkItemInfo.version)) return; // if newer, register this version instead // the older version will also continue to run, creating a bit of an overhead // but this should be negligible; there probably won't be that many versions // of WaniKani Item Info Injector anyway, and ideally, all scripts @require // the newest version; as a last resort, users can also install the newest // version of this script manually and set it as the first executed script // in their script manager unsafeGlobal.wkItemInfo = Object.freeze({ // public functions on : (...pages ) => _on ({}, pages), forType : (...types ) => _forType ({}, types), under : (...tabs ) => _under ({}, tabs), spoiling : (...spoilers ) => _spoiling ({}, spoilers), notify : (callback ) => _notify ({}, callback), notifyWhenVisible : (callback ) => _notifyWhenVisible ({}, callback), append : (heading, body) => _append ({}, heading, body), appendSideInfo : (heading, body) => _appendSideInfo ({}, heading, body), appendSubsection : (heading, body) => _appendSubsection ({}, heading, body), appendAtTop : (heading, body) => _appendAtTop ({}, heading, body), appendAtBottom : (heading, body) => _appendAtBottom ({}, heading, body), appendSideInfoAtTop : (heading, body) => _appendSideInfoAtTop ({}, heading, body), appendSideInfoAtBottom: (heading, body) => _appendSideInfoAtBottom({}, heading, body), version : _VERSION, get currentState() { _updateCurrentState(); return _currentStateDeepCopy(); } }); if (!_domReady()) { await new Promise(resolve => document.addEventListener(`readystatechange`, resolve, {once: true})); } _init(); _addCss(); } _publishInterface(); })(window, window.unsafeWindow || window);