WaniKani Later Crabigator

Adds a button for pushing the current review item back in the review queue.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         WaniKani Later Crabigator
// @namespace    latercrabigator
// @version      1.23
// @description  Adds a button for pushing the current review item back in the review queue.
// @author       Sinyaven
// @license      MIT-0
// @match        https://www.wanikani.com/*
// @match        https://preview.wanikani.com/*
// @require      https://greasyfork.org/scripts/462049-wanikani-queue-manipulator/code/WaniKani%20Queue%20Manipulator.user.js?version=1426722
// @grant        none
// ==/UserScript==

(function() {
    "use strict";

	/* global wkof, wkQueue */
	/* eslint no-multi-spaces: "off" */

	let bSkip = null;
	let iUserResponse = null;
	let keyDownHandlerWithAnkiCheck    = null;
	let keyDownHandlerWithoutAnkiCheck = null;

	addCss();
	init();

	// it seems like Turbo does not move the SVG element into document.body, so let's ignore it
	function relevantRootElementChildren(rootElement) {
		return [...rootElement?.children ?? []].filter(c => c.tagName !== `svg`);
	}

	document.addEventListener("turbo:before-render", async e => {
		let observer = new MutationObserver(m => {
			if (relevantRootElementChildren(m[0].target).length > 0) return;
			observer.disconnect();
			observer = null;
			init();
		});
		observer.observe(e.detail.newBody, {childList: true});
	});

	window.addEventListener(`willShowNextQuestion`, () => {
		if (!bSkip) return;
		bSkip.disabled = true;
		setTimeout(() => { bSkip.disabled = false; });
	});

	function init() {
		if (!document.URL.includes(`wanikani.com/subjects/review`) && !document.URL.includes(`wanikani.com/subjects/extra_study`) && !document.URL.includes(`wanikani.com/subjects/lesson/quiz`) && !/wanikani.com\/recent-mistakes\/.*quiz/.test(document.URL)) return;

		iUserResponse = document.querySelector(`input.quiz-input__input`);
		keyDownHandlerWithAnkiCheck    = keyDownHandler.bind(null, iUserResponse, true);
		keyDownHandlerWithoutAnkiCheck = keyDownHandler.bind(null, iUserResponse, false);

		addSettingsMenu();
		addButton();
	}

	function toggleKeyListener(enabled) {
		const funcName = enabled ? `addEventListener` : `removeEventListener`;
		document     ?.[funcName](`keydown`, keyDownHandlerWithAnkiCheck);
		iUserResponse?.[funcName](`keydown`, keyDownHandlerWithoutAnkiCheck);
	}

	function keyDownHandler(iUserResponse, ankiModeCheck, ev) {
		if (ev.key !== `Enter` || iUserResponse.value !== `` || ev.ctrlKey || ev.altKey || ev.metaKey || (ankiModeCheck && !isInAnkiMode())) return;
		bSkip.click();
		ev.stopPropagation();
		ev.preventDefault();
	}

	function addButton() {
		bSkip = document.createElement(`button`);
		bSkip.textContent = `Later`;
		bSkip.classList.add(`later-crabigator`);
		bSkip.addEventListener(`click`, e => { e.preventDefault(); e.stopPropagation(); pushBack(); });
		document.querySelector(`[data-quiz-input-target='form']`).appendChild(bSkip);
	}

	function addCss() {
		let style = document.createElement(`style`);
		style.textContent = `
			button.later-crabigator { right: calc(3em - 10px); position: absolute; border: none; background: none; font-size: 22px; top: 10px; bottom: 10px; cursor: pointer; }
			button.later-crabigator.left { left: 18px; right: auto; }
			.quiz-input__input-container[correct] button.later-crabigator { display: none; }`;
		document.head.appendChild(style);
	}

	async function pushBack() {
		if (isInAnkiMode() && iUserResponse.value !== ``) return;

		wkQueue.applyManipulation(q => q.concat(q.shift()));
	}

	// WK Anki Mode compatibility:

	function isInAnkiMode() {
		return !(document.getElementById(`WKANKIMODE_anki`)?.textContent.endsWith(`Off`) ?? true) ||
			document.getElementById(`anki-mode`)?.classList.contains(`anki-active`);
	}

	// Optional wkof settings menu

	async function addSettingsMenu() {
		if (typeof wkof !== `object`) return;

		const defaultSettings = {
			skipWhenAnswerEmpty: false,
			buttonLeft: false
		};

		wkof.include(`Menu,Settings`);
		await wkof.ready(`Menu,Settings`);
		wkof.Menu.insert_script_link({name: `later_crabigator`, submenu: `Settings`, title: `Later Crabigator`, on_click: openSettings});
		await wkof.Settings.load(`later_crabigator`, defaultSettings);
		applySettings();
	}

	function openSettings() {
		let dialog = new wkof.Settings({
			script_id: `later_crabigator`,
			title: `Later Crabigator Settings`,
			on_save: applySettings,
			content: {
				skipWhenAnswerEmpty: {type: `checkbox`, label: `Skip when answer empty`, hover_tip: `If enabled, submitting an empty answer automatically clicks the "Later" button.`},
				buttonLeft         : {type: `checkbox`, label: `Left-aligned button`   , hover_tip: `Place the "Later" button at the left side of the input box.`}
			}
		});
		dialog.open();
	}

	function applySettings() {
		if (typeof wkof !== `object`) return;

		toggleKeyListener(wkof?.settings?.later_crabigator?.skipWhenAnswerEmpty);
		bSkip?.classList.toggle(`left`, wkof?.settings?.later_crabigator?.buttonLeft);
	}
})();