The new version below fixes the issue about synonyms staying the same for each word. For the issue about "the words I typed are immediately forgeted " could you try the new script and see if the problem is still there? If it is, could you provide very detailed instructions for how to reproduce the issue? (Every button you clicked and key you typed in order, and what happened compared to what should have happened?)
I don’t use dashboard progress plus, so I don’t know about the separate problems you are having with that. Maybe try asking in the thread on it?
// ==UserScript==
// @name WaniKani Lesson User Synonyms 2 Fork
// @namespace est_fills_cando
// @version 0.3.4
// @description Allows adding synonyms during lesson and lesson quiz.
// @author est_fills_cando
// @match https://www.wanikani.com/lesson/session
// @grant none
// ==/UserScript==/
/*global $, wkof */
// based heavily on the WaniKani Lesson User Synonyms 2 userscript
(function () {
// Make sure WKOF is installed
if (!wkof) {
let response = confirm(script_name+' 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;
}
wkof.include('Apiv2');
let UserSynonyms2 = {};
let keyTTL = 180 * 24 * 3600 * 1000; // 180 days should be enough for anyone
// subject ids are unique even across different subject types
// the subject type is not required when updating the synonym list via the api
// therefore, we can ignore subject types
function getSynKeyItem(item) {
return getSynKeyID(item.id);
}
function getSynKeyID(id) {
return 'l/syn/' + id;
}
function getSynListItem(item) {
return getSynListID(item.id);
}
function getSynListID(id) {
let synKey = getSynKeyID(id);
$.jStorage.setTTL(synKey, keyTTL); // extend TTL on read. setting TTL on non-existant keys seems fine.
let retVal = $.jStorage.get(synKey) || [];
return retVal;
}
function setSynListID(id, synList) {
let synKey = getSynKeyID(id);
if (synList.length > 0) {
$.jStorage.set(synKey, synList);
$.jStorage.setTTL(synKey, keyTTL);
} else {
$.jStorage.deleteKey(synKey);
}
}
// Calls callback once on each element matching selector that is added
// to the DOM, including existing elements at the time the function is first called.
// options is options for the MutationObserver (optional, is merged with default options {childList: true, subtree: true})
function onEachElementReady(selector, options, callback) {
if (!options)
options = {};
// optimizations for certain commonly used selector patterns
let els_found = new Set()
function check_available() {
for (const el of document.querySelectorAll(selector)) {
if (!els_found.has(el)) {
els_found.add(el);
callback(el);
}
}
}
queueMicrotask(check_available);
let mo = new MutationObserver(check_available);
mo.observe(document.documentElement, {
childList: true,
subtree: true,
...options
});
}
// runs the callback every time attributes of elt change
// including once initially
// this function only works if elt exists at the time it is called
function onEachAttrChange(elt, options, callback) {
if (!options)
options = {}
options = {attributes: true, ...options};
queueMicrotask(callback);
let mo = new MutationObserver(callback);
mo.observe(elt, options);
return mo;
}
// calls callback each time jStorage value stored at key,
// is updated with the updated value,
// including cases where the value is overwritten with itself,
// including once initially
// callOnNull is whether to call on null values (default:false)
function onEachJStorageChange(key, callback, callOnNull) {
let val = $.jStorage.get(key);
if (val !== null || callOnNull)
setTimeout(() => callback(val), 0); //setTimeout here because local storage events are macro tasks
$.jStorage.listenKeyChange(key, function (key, action) {
if (action === 'updated') {
val = $.jStorage.get(key);
if (val !== null || callOnNull)
callback(val);
}
});
}
// loads any user synonyms stored on WaniKani's servers into the locally stored lists of synonyms,
// clobbering any existing values
// also stores subject ids of each subject
// input is array of subject_ids to retrieve
UserSynonyms2.loadSynonymsFromAPI = async function (ids) {
if (ids.length == 0)
return new Promise(resolve => resolve([]));
let options = {
force_update: true,
filters: {
subject_ids: ids
}
};
await wkof.ready('Apiv2');
let mats = await wkof.Apiv2.fetch_endpoint('study_materials', options);
let syns = {};
for (const mat of mats.data) {
syns[mat.data.subject_id] = mat.data.meaning_synonyms;
}
for (const id of ids) {
let syns_for_id = [];
if (id in syns) {
syns_for_id = syns[id];
}
setSynListID(id, syns_for_id);
}
}
// removes the given synonym, if it exists, from the given item in jStorage
// and saves the synonym list
UserSynonyms2.removeAndSave = function (itemID, syn) {
let li = getSynListID(itemID).filter(entry => entry != syn);
return UserSynonyms2.saveSynonymList(itemID, li);
}
// adds the given synonym to the given item in jStorage
// and saves the synonym list
UserSynonyms2.addAndSave = function (itemID, syn) {
let li = getSynListID(itemID);
li.push(syn);
return UserSynonyms2.saveSynonymList(itemID, li);
}
// prevent enter key up, which happens after textbox disappears
// and therefore triggers on body, from advancing lesson/quiz
let enterKeypressStartedInInput = false;
function setKeyInterceptor(selector) {
$(selector).keydown(function (event) {
if (event.key == "Enter" && enterKeypressStartedInInput) {
event.stopPropagation();
}
});
$(selector).keyup(function (event) {
if (event.key == "Enter" && enterKeypressStartedInInput) {
event.stopPropagation();
enterKeypressStartedInInput = false;
}
});
}
UserSynonyms2.load = function (userSynStr, itemID, element) {
if (!element || element.length == 0)
return
UserSynonyms2.generateList(element, userSynStr);
UserSynonyms2.addOption(itemID);
UserSynonyms2.removeOption(itemID);
// recalculate positioning of navigation bar at bottom of lesson page
// only seems to be needed on initial page load when information section
// is long enough that it nearly reaches the bottom of the page
// do it in request animation frame instead of setttimeout
// to prevent the nav bar visually "jumping" from wrong spot to right spot
// don't use microtasks because the values don't seem to have perfectly
// stabilized at that point
window.requestAnimationFrame(function() {
var e= $("#supplement-info");
var t=$("#batch-items");
if (t.length > 0) {
if($(window).height() - e.offset().top - e.outerHeight(!0) > t.height() + 25) {
t.get(0).classList.add("bottom");
} else {
t.get(0).classList.remove("bottom");
}
}
});
};
UserSynonyms2.addOption = function (itemID) {
let btnAdd, wrapper;
btnAdd = $('.user-synonyms-add-btn');
wrapper = UserSynonyms2.wrapper();
btnAdd.off('click');
btnAdd.on('click', function () {
let inputBtnRemove, inputBtnAdd, inputLi, inputForm, inputInput;
$(this).hide();
inputLi = $('<li></li>', {
'class': 'user-synonyms-add-form'
}).appendTo(wrapper);
inputForm = $('<form></form>').appendTo(inputLi);
inputInput = $('<input></input>', {
type: 'text',
autocapitalize: 'off',
autocomplete: 'off',
autocorrect: 'off'
}).appendTo(inputForm).focus();
inputInput.keyup(function (event) {
event.stopPropagation();
if (event.key == "Enter") {
enterKeypressStartedInInput = false;
}
}); // prevent hotkeys while typing
inputInput.keydown(function (event) {
event.stopPropagation();
if (event.key == "Enter") {
event.preventDefault(); // not sure if necessary
enterKeypressStartedInInput = true;
inputBtnAdd.click();
}
}); // prevent hotkeys while typing
inputBtnAdd = $('<button></button>', {
type: 'submit',
text: 'Add'
}).appendTo(inputForm);
inputBtnRemove = $('<button></button>', {
type: 'button',
html: '<i class="icon-remove"></i>'
}).appendTo(inputForm);
inputBtnAdd.off('click');
inputBtnAdd.on('click', function (event) {
event.preventDefault();
let newSynText, synLiElems, newLen;
newSynText = inputInput.val();
// fake accept duplicate, but don't send to server (server doesn't check?)
if (getSynListID(itemID).indexOf(newSynText.toLowerCase()) >= 0) {
// delay for key events to be caught
setTimeout(function () {
inputLi.remove();
btnAdd.show();
}, 100);
return;
}
synLiElems = wrapper.find('li');
newLen = newSynText.length + synLiElems.slice(0, synLiElems.size() - 1).text().length;
if (newLen > 255) {
inputBtnAdd.attr('disabled', 'disabled').text('Exceeded Synonym Limit');
} else if (newSynText.trim().length !== 0) {
UserSynonyms2.addAndSave(itemID, newSynText).then(function() {
let newSynElem;
newSynElem = $('<li class="user-synonym">' + newSynText + '</li>');
wrapper.find('li:last').prev().before(newSynElem);
inputLi.remove();
btnAdd.show();
UserSynonyms2.removeOption(itemID);
}).catch(function() {
alert('adding synonym failed');
});
}
});
inputBtnRemove.off('click');
inputBtnRemove.on('click', function (event) {
inputLi.remove();
btnAdd.show();
});
});
};
/**
* Save the list of user synonms currently in local storage
* Only works if subject with itemID is in the activeQueue
* @param itemID
*/
UserSynonyms2.saveSynonymList = async function (itemID, synList) {
function getType(item) {
var type = 'x';
if (item.rad) {
type = 'radical';
} else if (item.kan) {
type = 'kanji';
} else if (item.voc) {
type = 'vocabulary';
}
return type;
}
let its = $.jStorage.get('l/activeQueue').filter(it => it.id == itemID);
let it;
if (its.length > 0)
it = its[0];
else
it = $.jStorage.get('l/currentQuizItem')
let type = getType(it);
//let url = "https://api.wanikani.com/v2/study_materials/" + itemID;
let url = "https://www.wanikani.com/study_materials/" + itemID;
await $.ajax({
type: 'PUT',
url: url,
contentType: 'application/json; charset=utf-8',
data: JSON.stringify({
study_material: {
subject_type: type,
subject_id: itemID,
meaning_synonyms: synList
}
})
});
setSynListID(itemID, synList);
}
UserSynonyms2.generateList = function (element, userSyn) {
let wrapper, i;
$('.user-synonyms ul').remove();
element.append($('<ul></ul>'));
wrapper = UserSynonyms2.wrapper();
for (i = 0; i < userSyn.length; i++) {
$('<li class="user-synonym">' + userSyn[i] + '</li>', {
title: 'Click to remove synonym'
}).appendTo(wrapper);
}
$('<li></li>', {
html: ' ',
title: 'Add your own synonym',
'class': 'user-synonyms-add-btn'
}).appendTo(wrapper);
};
UserSynonyms2.removeOption = function (itemID) {
let synElems = UserSynonyms2.wrapper().find('li:not(.user-synonyms-add-btn):not(.user-synonyms-add-form)');
synElems.off('click');
synElems.on('click', function () {
let clickedSynElem = $(this);
UserSynonyms2.removeAndSave(itemID, clickedSynElem.text()).then(clickedSynElem.remove.bind(clickedSynElem));
});
};
UserSynonyms2.wrapper = function () {
return $('.user-synonyms ul');
};
// from: https://gist.githubusercontent.com/arantius/3123124/raw/grant-none-shim.js
function addStyle(aCss) {
let head, style;
head = document.getElementsByTagName('head')[0];
if (head) {
style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.textContent = aCss;
head.appendChild(style);
return style;
}
return null;
}
// Init / UI Display
let initializedSkeletons = new Map();
UserSynonyms2.synonymSection = function(child_or_sibling, target) {
if (initializedSkeletons.has(target.get(0) ))
return initializedSkeletons.get(target.get(0));
let cur;
if (child_or_sibling =='sibling')
cur =$('<h2>User Synonyms</h2>').insertAfter(target);
else if (child_or_sibling == 'child')
cur =$('<h2>User Synonyms</h2>').appendTo(target);
cur = $('<section class="user-synonyms"></section>').insertAfter(cur);
initializedSkeletons.set(target.get(0), cur);
return cur;
}
let quizVisibilityObserver = null;
UserSynonyms2.showCurrentSynonyms = function() {
if (quizVisibilityObserver) {
quizVisibilityObserver.disconnect();
quizVisibilityObserver = null;
}
if ($.jStorage.get('l/quizActive')) {
let el, synSec;
let currentQuizItem = $.jStorage.get('l/currentQuizItem');
if (!currentQuizItem || !initialized_subject_ids.has(currentQuizItem.id)) {return}
if (currentQuizItem.rad) {
el = $('#item-info-col1 section:first-child');
if (el.length==0) {return}
synSec = UserSynonyms2.synonymSection('child', el);
} else {
el = $('#item-info-meaning');
if (el.length==0) {return}
synSec = UserSynonyms2.synonymSection('sibling', el);
quizVisibilityObserver = onEachAttrChange(el.get(0), null, function() {
if (window.getComputedStyle(el.get(0)).display != 'none') {
synSec.css('display','');
synSec.prev().css('display','');
} else {
synSec.css('display', 'none');
synSec.prev().css('display', 'none');
}
});
}
el.contents().filter(function() {return this.nodeType==Node.TEXT_NODE}).get(0).textContent = currentQuizItem.original_answers_en.join(', ');
UserSynonyms2.load(getSynListItem(currentQuizItem), currentQuizItem.id, synSec);
} else {
let currentLesson = $.jStorage.get('l/currentLesson');
if (!currentLesson || !initialized_subject_ids.has(currentLesson.id)) {return}
if (currentLesson.rad) {
let synSec = UserSynonyms2.synonymSection('sibling', $('#supplement-rad-name-mne'));
UserSynonyms2.load(getSynListItem(currentLesson), currentLesson.id, synSec);
} else if (currentLesson.kan) {
let synSec = UserSynonyms2.synonymSection('sibling', $('#supplement-kan-meaning .col1 div'));
UserSynonyms2.load(getSynListItem(currentLesson), currentLesson.id, synSec);
} else if (currentLesson.voc) {
let synSec = UserSynonyms2.synonymSection('sibling', $('#supplement-voc-synonyms'));
UserSynonyms2.load(getSynListItem(currentLesson), currentLesson.id, synSec);
}
}
}
// during lesson
// prevent enter events during synonym entry from triggering unrelated things
setKeyInterceptor('body');
// at start of each lesson set (detected via activeQueue change)
// load synonym data from api
let initialized_subject_ids = new Set();
onEachJStorageChange('l/activeQueue', function(activeQueue) {
let subject_ids = activeQueue.map(it => it.id).filter(id => !initialized_subject_ids.has(id));
subject_ids.forEach(id => initialized_subject_ids.add(id));
if (subject_ids.length != 0) {
UserSynonyms2.loadSynonymsFromAPI(subject_ids).then(function() {
UserSynonyms2.showCurrentSynonyms();
});
}
});
// show user synonyms when looking at each lesson
onEachJStorageChange('l/currentLesson', function (currentLesson) {
UserSynonyms2.showCurrentSynonyms();
});
// during lesson quiz
// copy user synonyms into the answers list for quiz
let in_call = false;
onEachJStorageChange('l/currentQuizItem', function (currentQuizItem) {
if (!in_call) {
in_call = true;
if (!currentQuizItem.orignal_answers_en)
currentQuizItem.original_answers_en = [...currentQuizItem.en];
currentQuizItem.en = currentQuizItem.original_answers_en.concat(getSynListItem(currentQuizItem));
$.jStorage.set('l/currentQuizItem', currentQuizItem); // this call would cause infinite recursion without using in_call
in_call = false;
}
});
// show user synonyms in information box in lesson quiz for radicals
onEachElementReady('#item-info-col1 section:first-child', null, function(el) {
if ($(el).find('h2').text() == 'Name')
UserSynonyms2.showCurrentSynonyms();
});
// show user synonyms in information box in lesson quiz for kanji and vocab
onEachElementReady('#item-info-meaning', null, function(el) {
UserSynonyms2.showCurrentSynonyms();
});
// watch for completed items. completed items should be safe to delete local storage...
(function () {
let lastCompleted = 0;
onEachJStorageChange('l/count/completed', function (completed) {
let currentQuizItem, synKey;
if (completed > lastCompleted) {
currentQuizItem = $.jStorage.get('l/currentQuizItem');
synKey = getSynKeyItem(currentQuizItem);
$.jStorage.deleteKey(synKey); // harmless to delete non-existant keys
}
lastCompleted = completed;
});
}());
// this is style extracted from the wanikani review page
addStyle('\n' +
'.user-synonyms ul {\n' +
' margin: 0;\n' +
' padding: 0;\n' +
'}\n' +
'.user-synonyms ul li {\n' +
' display: inline-block;\n' +
' line-height: 1.5em;\n' +
'}\n' +
'.user-synonyms ul li:not(.user-synonyms-add-btn):not(.user-synonyms-add-form) {\n' +
' cursor: pointer;\n' +
' vertical-align: middle;\n' +
'}\n' +
'.user-synonyms ul li:not(.user-synonyms-add-btn):not(.user-synonyms-add-form):after {\n' +
' background-color: #EEEEEE;\n' +
' border-radius: 3px;\n' +
' color: #A2A2A2;\n' +
' content: "\\f00d";\n' +
' font-family: FontAwesome;\n' +
' font-size: 0.5em;\n' +
' margin-left: 0.5em;\n' +
' margin-right: 1.5em;\n' +
' padding: 0.15em 0.3em;\n' +
' transition: background-color 0.3s linear 0s, color 0.3s linear 0s;\n' +
' vertical-align: middle;\n' +
'}\n' +
'.user-synonyms ul li:hover:not(.user-synonyms-add-btn):not(.user-synonyms-add-form):after {\n' +
' background-color: #FF0033;\n' +
' color: #FFFFFF;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-btn {\n' +
' cursor: pointer;\n' +
' display: block;\n' +
' font-size: 0.75em;\n' +
' margin-top: 0.25em;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-btn:after {\n' +
' content: "";\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-btn:before {\n' +
' background-color: #EEEEEE;\n' +
' border-radius: 3px;\n' +
' color: #A2A2A2;\n' +
' content: "+ ADD SYNONYM";\n' +
' margin-right: 0.5em;\n' +
' padding: 0.15em 0.3em;\n' +
' transition: background-color 0.3s linear 0s, color 0.3s linear 0s;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-btn:hover:before {\n' +
' background-color: #A2A2A2;\n' +
' color: #FFFFFF;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-form {\n' +
' display: block;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-form form {\n' +
' display: block;\n' +
' margin: 0;\n' +
' padding: 0;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-form form input, .user-synonyms ul li.user-synonyms-add-form form button {\n' +
' line-height: 1em;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-form form input {\n' +
' -moz-border-bottom-colors: none;\n' +
' -moz-border-left-colors: none;\n' +
' -moz-border-right-colors: none;\n' +
' -moz-border-top-colors: none;\n' +
' border-color: -moz-use-text-color -moz-use-text-color #A2A2A2;\n' +
' border-image: none;\n' +
' border-style: none none solid;\n' +
' border-width: 0 0 1px;\n' +
' display: block;\n' +
' margin: 0;\n' +
' outline: medium none;\n' +
' padding: 0;\n' +
' width: 100%;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-form form button {\n' +
' background-color: #EEEEEE;\n' +
' border: medium none;\n' +
' border-radius: 3px;\n' +
' color: #A2A2A2;\n' +
' font-size: 0.75em;\n' +
' outline: medium none;\n' +
' transition: background-color 0.3s linear 0s, color 0.3s linear 0s;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-form form button:hover {\n' +
' background-color: #A2A2A2;\n' +
' color: #FFFFFF;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-form form button:disabled {\n' +
' background-color: #FF0000;\n' +
' color: #FFFFFF;\n' +
' cursor: default;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-form form button[type="button"] {\n' +
' margin-left: 0.25em;\n' +
' padding-left: 0.3em;\n' +
' padding-right: 0.3em;\n' +
'}\n' +
'.user-synonyms ul li.user-synonyms-add-form form button[type="button"]:hover {\n' +
' background-color: #FF0000;\n' +
' color: #FFFFFF;\n' +
'}\n');
}());