WaniKani Lesson User Synonyms 3

Adds the section "User Synonyms" to WaniKani lessons.

// ==UserScript==
// @name         WaniKani Lesson User Synonyms 3
// @namespace    https://greasyfork.org/users/317813
// @version      1.9
// @description  Adds the section "User Synonyms" to WaniKani lessons.
// @author       Sinyaven
// @license      MIT-0
// @match        https://www.wanikani.com/lesson/session
// @match        https://preview.wanikani.com/lesson/session
// @homepageURL  https://community.wanikani.com/t/54399
// @require      https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1111117
// @grant        none
// ==/UserScript==

(function() {
    "use strict";
	/* global WaniKani, wkof, wkItemInfo, $ */

	if (!window.wkof) {
		if (confirm(`WaniKani Lesson User Synonyms 3 script requires Wanikani Open Framework.\nShow installation instructions?`)) {
			location.href = `https://community.wanikani.com/t/28549`;
		}
		return;
	}

	addCss();
	wkof.include(`Apiv2`);
	wkItemInfo.on(`lesson,lessonQuiz`).under(`meaning`).notifyWhenVisible(addUserSynonymSection);

	async function downloadUserSynonyms(itemId) {
		await wkof.ready(`Apiv2`);
		let response = await wkof.Apiv2.fetch_endpoint(`study_materials`, {filters: {subject_ids: [itemId]}, disable_progress_dialog: true});
		return response.data[0]?.data.meaning_synonyms || [];
	}

	function uploadUserSynonyms(itemId, itemType, synonyms) {
		let data = {study_material: {subject_type: itemType, subject_id: itemId, meaning_synonyms: synonyms}};
		fetch(`/study_materials/${itemId}`, {
			method: `PUT`,
			headers: {
				"Content-Type": `application/json; charset=utf-8`,
				"X-CSRF-Token": document.querySelector(`meta[name=csrf-token]`).content
			},
			body: JSON.stringify(data)
		});
	}

	function updateUserSynonymsInLessonQueue(itemId, synonyms) {
		let activeQueue = $.jStorage.get(`l/activeQueue`);
		let item = activeQueue.find(a => a.id === itemId);
		if (!item) return;
		item.syn = synonyms;
		$.jStorage.set(`l/activeQueue`, activeQueue);
	}

	function addUserSynonymSection(injectorState) {
		injectorState.injector.appendSideInfo(`User Synonyms`, userSynonymSection(injectorState)).classList.add(`user-synonyms`);
	}

	function userSynonymSection(injectorState) {
		let blacklist = $.jStorage.get(injectorState.on === `lesson` ? `l/currentLesson` : `l/currentQuizItem`).auxiliary_meanings.filter(m => m.type === `blacklist`).map(m => m.meaning.toLowerCase());
		let ul = document.createElement(`ul`);
		insertUserSynonyms(ul, injectorState, blacklist);
		let addSynonym = document.createElement(`li`);
		addSynonym.title = `Add your own synonym`;
		addSynonym.classList.add(`user-synonyms-add-btn`);
		addSynonym.addEventListener(`click`, e => clickOnAddSynonym(e, injectorState, blacklist));
		ul.append(addSynonym);
		return ul;
	}

	async function insertUserSynonyms(ul, injectorState, blacklist) {
		let userSynonyms = await downloadUserSynonyms(injectorState.id);
		ul.prepend(...userSynonyms.map(u => createSynonymElement(u, injectorState, blacklist)));
		updateUserSynonymsInLessonQueue(injectorState.id, userSynonyms);
	}

	function createSynonymElement(synonym, injectorState, blacklist) {
		let li = document.createElement(`li`);
		li.title = `Click to remove synonym`;
		li.textContent = synonym;
		li.style.maxWidth = `100%`;
		li.style.overflowWrap = `break-word`;
		li.addEventListener(`click`, e => clickOnSynonym(e, injectorState));
		if (blacklist.includes(synonym.toLowerCase())) li.classList.add(`on-blacklist`);
		return li;
	}

	function clickOnSynonym(event, injectorState) {
		let parent = event.currentTarget.parentElement;
		event.currentTarget.remove();
		readUserSynonymsFromHtmlAndUploadThem(parent, injectorState);
	}

	function clickOnAddSynonym(event, injectorState, blacklist) {
		let li = document.createElement(`li`);
		let form = document.createElement(`form`);
		let input = document.createElement(`input`);
		let bSubmit = document.createElement(`button`);
		let bCancel = document.createElement(`button`);
		let icon = document.createElement(`i`);
		li.classList.add(`user-synonyms-add-form`);
		input.type = `text`;
		input.autocapitalize = `none`;
		input.autocomplete = `off`;
		input.autocorrect = `off`;
		input.spellcheck = `false`;
		input.maxLength = 64;
		bCancel.type = `button`;
		bSubmit.type = `submit`;
		bSubmit.textContent = `Add`;
		icon.classList.add(`fa`, `fa-times`);
		compatibilityModeFix(form, input, injectorState, blacklist);
		form.addEventListener(`submit`, e => submitSynonym(e, injectorState, blacklist));
		input.addEventListener(`keydown`, e => e.stopPropagation());
		bCancel.addEventListener(`click`, clickOnCancel);
		bCancel.append(icon);
		form.append(input, bSubmit, bCancel);
		li.append(form);
		event.currentTarget.before(li);
		input.focus();
	}

	function compatibilityModeFix(form, input, injectorState, blacklist) {
		if (!WaniKani.wanikani_compatibility_mode) return;
		let delaySubmit = false;
		form.addEventListener(`submit`, e => { if (delaySubmit) e.stopImmediatePropagation(); e.preventDefault(); delaySubmit = false; });
		input.addEventListener(`keydown`, e => { if (e.code === `Enter`) delaySubmit = true; });
		input.addEventListener(`keyup`, e => { if (e.code === `Enter`) addSynonym(e.currentTarget.parentElement, injectorState, blacklist); e.stopPropagation(); });
	}

	function clickOnCancel(event) {
		event.currentTarget.parentElement.parentElement.remove();
	}

	function submitSynonym(event, injectorState, blacklist) {
		addSynonym(event.currentTarget, injectorState, blacklist);
		event.preventDefault();
	}

	function addSynonym(form, injectorState, blacklist) {
		let synonym = form[0].value.trim();
		let ul = form.parentElement.parentElement;
		if (!synonym || readUserSynonymsFromHtml(ul).includes(synonym)) return;
		let synonymElement = createSynonymElement(synonym, injectorState, blacklist);
		form.parentElement.before(synonymElement);
		form.parentElement.remove();
		readUserSynonymsFromHtmlAndUploadThem(ul, injectorState);
	}

	function readUserSynonymsFromHtmlAndUploadThem(ul, injectorState) {
		let userSynonyms = readUserSynonymsFromHtml(ul);
		uploadUserSynonyms(injectorState.id, injectorState.type, userSynonyms);
		updateUserSynonymsInLessonQueue(injectorState.id, userSynonyms);
	}

	function readUserSynonymsFromHtml(ul) {
		return [...ul.children].filter(c => !c.classList.contains(`user-synonyms-add-btn`) && !c.classList.contains(`user-synonyms-add-form`)).map(c => c.textContent);
	}

	function addCss() {
		let style = document.createElement(`style`);
		style.textContent = `
			.user-synonyms ul {
				margin: 0;
				padding: 0
			}

			.user-synonyms ul li {
				display: inline-block;
				line-height: 1.5em
			}

			.user-synonyms ul li:not(.user-synonyms-add-btn):not(.user-synonyms-add-form) {
				cursor: pointer;
				vertical-align: middle
			}

			.user-synonyms ul li:not(.user-synonyms-add-btn):not(.user-synonyms-add-form):after {
				content: '\\f00d';
				margin-left: 0.5em;
				margin-right: 1.5em;
				padding: 0.15em 0.3em;
				color: #a2a2a2;
				background-color: #eee;
				font-size: 0.5em;
				font-family: FontAwesome;
				-webkit-border-radius: 3px;
				-moz-border-radius: 3px;
				border-radius: 3px;
				-webkit-transition: background-color 0.3s linear, color 0.3s linear;
				-moz-transition: background-color 0.3s linear, color 0.3s linear;
				-o-transition: background-color 0.3s linear, color 0.3s linear;
				transition: background-color 0.3s linear, color 0.3s linear;
				vertical-align: middle
			}

			.user-synonyms ul li:not(.user-synonyms-add-btn):not(.user-synonyms-add-form):hover:after {
				background-color: #f03;
				color: #fff
			}

			.user-synonyms ul li.user-synonyms-add-btn {
				cursor: pointer;
				display: block;
				font-size: 0.75em;
				margin-top: 0.25em
			}

			.user-synonyms ul li.user-synonyms-add-btn:after {
				content: ''
			}

			.user-synonyms ul li.user-synonyms-add-btn:before {
				content: '+ ADD SYNONYM';
				margin-right: 0.5em;
				padding: 0.15em 0.3em;
				background-color: #eee;
				color: #a2a2a2;
				-webkit-transition: background-color 0.3s linear, color 0.3s linear;
				-moz-transition: background-color 0.3s linear, color 0.3s linear;
				-o-transition: background-color 0.3s linear, color 0.3s linear;
				transition: background-color 0.3s linear, color 0.3s linear;
				-webkit-border-radius: 3px;
				-moz-border-radius: 3px;
				border-radius: 3px
			}

			.user-synonyms ul li.user-synonyms-add-btn:hover:before {
				background-color: #a2a2a2;
				color: #fff
			}

			.user-synonyms ul li.user-synonyms-add-form {
				display: block
			}

			.user-synonyms ul li.user-synonyms-add-form form {
				display: block;
				margin: 0;
				padding: 0
			}

			.user-synonyms ul li.user-synonyms-add-form form input,.user-synonyms ul li.user-synonyms-add-form form button {
				line-height: 1em
			}

			.user-synonyms ul li.user-synonyms-add-form form input {
				outline: none;
				display: block;
				width: 100%;
				margin: 0;
				padding: 0;
				border: 0;
				border-bottom: 1px solid #a2a2a2
			}

			.user-synonyms ul li.user-synonyms-add-form form button {
				outline: none;
				background-color: #eee;
				color: #a2a2a2;
				font-size: 0.75em;
				border: none;
				-webkit-transition: background-color 0.3s linear, color 0.3s linear;
				-moz-transition: background-color 0.3s linear, color 0.3s linear;
				-o-transition: background-color 0.3s linear, color 0.3s linear;
				transition: background-color 0.3s linear, color 0.3s linear;
				-webkit-border-radius: 3px;
				-moz-border-radius: 3px;
				border-radius: 3px
			}

			.user-synonyms ul li.user-synonyms-add-form form button:hover {
				background-color: #a2a2a2;
				color: #fff
			}

			.user-synonyms ul li.user-synonyms-add-form form button:disabled {
				cursor: default;
				background-color: red;
				color: #fff
			}

			.user-synonyms ul li.user-synonyms-add-form form button[type=button] {
				margin-left: 0.25em;
				padding-left: 0.3em;
				padding-right: 0.3em
			}

			.user-synonyms ul li.user-synonyms-add-form form button[type=button]:hover {
				background-color: red;
				color: #fff
			}

			.user-synonyms .user-synonyms-add-form + .user-synonyms-add-btn {
				display: none
			}

			.item-info-injector.user-synonyms li + li + li + li + li + li + li + li + * {
				display: none
			}

			.user-synonyms .on-blacklist {
				color: red
			}
		`; // except for the last three rules, the CSS is copied from the WK review page.
		document.head.appendChild(style);
	}
})();