[Userscript] Multiple Answer Input revamped

Hello everyone!
After deciding today, on a whim, that I wanted to be able to answer a given question with all possible meanings and readings, I looked for a script to turn my wishes into reality and found this older solution by Mempo.

Sadly, this didn’t work for me, so I spent a few hours today debugging Mempo’s script and writing my own version of it.

What does it do?
It works on the same premise as Mempo’s original version: When answering, you can now input multiple answers, separated either by a semicolon ( ; ) or more than one white space. It should be fine to mix and match separators. If you’re a psychopath.

I figured out a way to direct this towards Wanikani’s own spellchecker, so Wanikani should act like each answer was submitted normally and isolated.

If any answers are wrong, the entire thing gets marked as red and the input field shows only the answers you got wrong, seperated by semicolons.
If you get it right, the answer field should hopefully display what you yourself entered (since everything is correct).

As a further improvement upon Mempo’s version, mine also works on lessons, not just reviews, and is fine with all registered readings/meanings of any kanji, vocabulary and radical (since it just uses Wanikani’s default checker)

Before you install

  1. Now, I am a coder by trade, but I had never even touched Tampermonkey, Wanikani userscripts, greasy fork OR Javascript before today. I imagine there are some egregious convention and js noob mistakes in there, considering I built this thing using Mempo’s existing script, my coding experience, Google and lots of digging into how Wanikani’s frontend works via inspect element. I have written a list of problems and unresolved bugs down below (not just with javascript, with Wanikani userscripts as a whole), so if you have any solutions for any of them or just general feedback, I would be very grateful! (Incidentally, this is also my first forum post, so feel free to correct me on that front, as well.)

  2. Now in case you skipped reading the first point once I started crying about how hard javascript is (lol): There are known bugs. They’re just minor annoyances, but please make sure to be aware of them before you install. EDIT: There are no longer any known bugs! Install away and please tell me about any problems you do find!

  3. I both developed and tested this thing on Firefox & Tampermonkey. It’s untested on any other browser / script sandbox. EDIT: It is now tested on both Firefox and Chrome.

Now in case that was unsuccessful in scaring you off…
How to install

  1. Install Tampermonkey
  2. Install Wanikani Open Framework
  1. Install my Script (The Wanikani Open Framework is no longer a requirement)
  2. Hope that it works! (o゜▽゜)o☆

Known issues and cries for help

Javascript

  • In case of an exception, the page forces a reload and the console gets cleared. This writes the last user input into a URL as a parameter, causing the @match tag to no longer… well, match. This deactivates the script and forces the user to completely re-open the tab in question, cutting the study session short.
    The reason that this confuses me is that I was pretty sure Tampermonkey surrounds user scripts in a try/catch, so why does this happen?

  • Similarly, mashing the enter key causes the same problem, so I’m assuming that it, too, produces an exception. My code handles an empty input just fine, only mashing the enter key (-> spamming the code with empty inputs) causes such a strange behavior. Of course, I can’t debug it, because then I can’t spam Enter anymore…
    => Both solved by @Sinyaven - thank you!

  • At the very end, my code calls the original button.click again to re-route execution to the original Wanikani stuff. In case of the answer being correct, however, I then afterwards re-apply the original input, AFTER the button.click call. Is this event code synchronous, or do I have to worry about causing weird race conditions?

  • The script kind of spams the dev console with info. Is that fine or bad form?

  • Mempo’s original script called event.stopPropagation to, I assume, stop the event from reaching default Wanikani code. I just kind of copied this without question, but should I be worried? This particular function seems to be slightly controversial, judging by my cursory Googling.
    => All answered by @Kumirei ! Thank you!

  • …General javascript coding style / userscript / greasy fork / wanikani conventions I am ignorant about (of which there are many, I imagine, since I usually write in C#)

Greasy Fork

  • Since this script requires it and greasy fork told me to, I added
    // @require https://greasyfork.org/scripts/38582-wanikani-open-framework/code/Wanikani%20Open%20Framework.user.js
    To its header. Doing this doesn’t appear to be the norm, however, judging by other userscripts that claim to use the Framework. Is this tag wrong or alright?

    => It was wrong!

  • This script is kind of a derivative work of Mempo’s. Greasy Fork has a tag for this, but I don’t know how to set it. I would very much like to credit them for their work, but don’t know how.

  • Similarly, their original script doesn’t have a license tag. I would like to imagine that I didn’t just infringe copyright… I didn’t, did I?!

Wanikani Forums

  • Like I said, first post. This post is kind of a mix between asking for feedback and API/Third party, but I only saw one field for tags, so I went with the latter. If there is a way to apply more tags (or if it’s in the wrong place here), please tell me.
    EDIT: There isn’t, but if you see any other inconsistencies or problems with this post, the same still applies!

-------------------------------
-------------------------------

Again, sorry for being such a noob, but, well, I am. The astoundingly frustrating nature of writing javascript without understanding it notwithstanding, this was pretty fun and I’d like to write more scripts in the future, if I can think of interesting tasks to automate or some of you send me a few ideas :slight_smile:

General feedback on this one is of course also always welcome! I wouldn’t mind adding new separators for instance, or allowing you to configure your own! (provided that I can figure out how to store a config :wink:)
I am far from done with improving this script! (but out of ideas!)

Changelog
Version 1.2.1:

  • Oops. Typos.

Version 1.2.0:

  • Guess what?! WKOF is required again! (For real this time, I tested it)
  • Added settings support
  • Added setting to not immediately fail on a wrong answer if at least one answer is correct.
    Instead, it will now cause a shake effect and tell you which answers were wrong

Version 1.1.0:

  • Removed the @require tag again, as WKOF wasn’t actually required.
  • Cleaned up logging and moved it to respective Debug and Info levels.
  • Fixed known bugs
  • Tested on both Firefox and Chrome this time

Version 1.0.1: Added @require tag for the Wanikani Open Framework
Version 1.0: initial

6 Likes

Your script does not use WaniKani Open Framework, so you don’t have to @require it. And it is also not intended to be included with the @require tag.

Exceptions don’t cause a page reload. I think the problem is that you are suppressing some WaniKani code from executing with event.stopPropagation();. The WaniKani code probably contains event.preventDefault();. The default behavior of browsers for when you hit enter within a form input field is to submit the form (which in this case means reloading the webpage with ?user-response=... appended). So I think you should be able to fix this by suppressing this default behavior by adding event.preventDefault(); yourself.

Javascript is generally single-threaded, so in most cases you don’t have to worry about race conditions.

I haven’t heard of that yet – is this true?

2 Likes

I see! I left it in because the original script wouldn’t run without it (the $ wouldn’t get resolved to document.getElementById - I imagine that that’s what it is shorthand for). I’m actually mildly confused about that, now that I think about it - when googling about it, it ended up being a jQuery thing, but installing the WOF fixed the problem for me anyway. If the framework wasn’t the reason my errors were resolved, was it switching from Greasemonkey to Tampermonkey?
If I uninstall the Framework, what will $(Id) resolve to?

Oh, that exact call was commented out in the original script. I just figured it was a different way of achieving the same thing, but maybe I should have put some more thought into what exactly it meant. Thank you, I never would have figured that out ^^’

Ah, so going async is always an explicit choice?

Well, I thought it was, but in hindsight I might have been thinking about edge.
Uh-oh, I think I’m gonna have to catch up on some testing tomorrow.

Thank you very much for your advice! I’ll get an update ready tomorrow addressing your points :slight_smile:

2 Likes

You can use RegEx for the URL matching. Might want to use something like this instead of @match

// @include      /^https://(www|preview).wanikani.com/(lesson|review)/session$/

I’m just a hobby developer, so I am not sure if it’s bad form or not, however, you should consider that a lot of users have a number of userscripts installed, so it might be good to keep the logging to a minimum lest the console gets clogged. That’s just my opinion, though.

I think it’s fine for userscripts since they have to work around the existing code. It’s probably best not to make a habit of using it when you don’t have to, but userscripts do what they have to do to work. They can’t be and don’t need to be, examples of best practices

I think your code looks fine. Userscripts don’t need to be pristine, as long as they work you’re good

TBH I don’t know how to do that either. Our scripts spread here on the forums anyway, so a link to the original or a mention in the OP would be enough IMO

I guess technically you did? Lol. I think Mempo is fine with it, though. I haven’t seen a script without a standard open source license

The feedback category is for WK feedback, so you put it in the right place. There’s only one category per thread

Yes! I had practically no programming experience before I started making userscripts for WK. It’s been a fun couple of years

Yes, $ is shorthand for jQuery. However, $(selector) works much like document.querySelectorAll(selector)

Wanikani (not WKOF) provides jQuery in the global scope so nothing should change. If you want to get rid of linter warnings of $ not being defined you can just define it in the local scope const $ = window.jQuery

Yes. Well, unless you unknowingly use an async function. You can also make async calls look synchronous using the async/await statements.

2 Likes

Yes, I’m pretty sure that this problem was solved by switching from Greasemonkey to Tampermonkey. Greasemonkey has some really restrictive security features. $ is defined by vanilla WK in the global scope, but I think Greasemonkey provides your userscript with a separate global scope. Maybe you could have worked around this by using const $ = unsafeWindow.jQuery;, but I don’t think anybody uses Greasemonkey anyway, as the Open Framework does not work in it.

And even when using Promises / async & await, it does not result in parallel execution of Javascript code. If the browser has a promise result ready or wants to execute a callback or event handler, it has to wait until the currently executing Javascript code has finished.

It’s true that unless you know the code in button.click(), you can’t be sure that after button.click() finished it will not cause any changes to the input field anymore. For example, button.click() might contain a line

setTimeout(() => $('input#user-response')[0].value = "You didn't expect that, did you?", 1);

This would cause the given callback function to be queued for execution after 1ms. In such a case, button.click() would return, you change the input value, and after your Javascript code finished, the queued callback would execute and overwrite your change to the input field.

However, since Javascript is single-threaded, the callback of setTimeout() would always execute afterwards, so I’m not sure if this can be called a race condition.

2 Likes

Thank you, I took the liberty of implementing your suggestions in today’s new update.

Is it the same thing or does it simply fulfill the same functionality?

Thank you for your tips and feedback!

2 Likes

Same functionality, different implementation. I might be wrong, but I think the jQuery implementation came first, and it was later implemented in native JS because it was so popular

2 Likes

…That could have saved me a lot of time hahaha

Wait, then why use it? In conventional programming at least, the whole point of making something async is that it can run on a background thread to finish its operation without freezing e.g. the UI thread, but if everything is on the same thread, doesn’t this effectively make it synchronous, anyway?

In the same vein I’m struggling a little with this - if js only has a single thread to use, shouldn’t the timeout occupy that thread? How does it resume execution in the caller and react to the callback afterwards? The only thing I can think of is setTimeou() not occupying the thread in the way the classic Thread.Sleep() is liable to.
This would still, however, limit the usefulness of js await to awaiting operations that don’t occupy the thread, reducing the potential applications to pretty much only waiting without an attached operation…

Is there more to this and I’m overthinking it?

1 Like

I believe it’s primarily (though not exclusively) used when you make HTTP requests and it will take some time before you get a response. Since JS is single-threaded the alternative to promises is halting all execution until you get a response.

2 Likes

Ooooooh I see, that makes perfect sense. Of course waiting for a single-threaded application as a single-threaded application has only little benefit, but waiting for a different server to execute a long-running process, or your OS to complete a download etc. does make a lot of sense.

Thanks, I am enlightened

I’m also not that knowledgeable in Javascript, so take what I’m going to say with a grain of salt:

While all the Javascript code that you write only runs on a single thread, some native code might execute in different threads. As @Kumirei said, if you for example call fetch(), it will perform an HTTP request in a background thread while your single-threaded code continues to run. As soon as fetch() finished, it waits until the Javascript thread is “free” and then delivers the result.

Some other potentially time-consuming native functionality might also execute in parallel, for example database queries (I’m thinking of IndexedDB) or reading a file from your hard drive.

Expensive computations implemented in Javascript (and not native code) will indeed freeze the UI. And setTimeout() does not occupy the thread like Thread.Sleep():

setTimeout(() => console.log("timeout"), 1);
console.time("1000000 × sin");
Array.from({length: 1000000}, (v, i) => Math.sin(i));
console.timeEnd("1000000 × sin");
console.log("end");

The result of this code is:

1000000 × sin: 129.632080078125 ms
end
timeout

setTimeout() only schedules the execution of the callback function for one millisecond in the future but immediately returns and your code continues to run. After 1ms, the callback function gets queued for execution, but the execution has to wait until the Javascript thread is “free”, which takes another 128ms.

With all that said, you can nowadays run Javascript code in a separate thread by using Web Workers.

2 Likes

I see, so setTimeout itself only performs the scheduling operation, which is presumably very cheap and therefore returns execution to the main program pretty much immediately, with the scheduler coming in to call the awaited function once its own queue is empty. Meaning: Instead of the thread actively waiting, it resumes execution per an outside push. I think that was the main point I’d been missing.
I suppose this explains why javascript is, well, a scripting language - I will keep in mind to not run any expensive ops in js, but pass it off to native code wherever possible.

Thank you very much, both you and @Kumirei have been amazingly helpful! (^^ゞ

(As an aside: Thank you as well for the ConfusionGuesser, it’s very cool)

2 Likes

I’m going to try this out (haven’t yet), but I’m looking for a script that would do the same but instead mark the answer green if you get at least one part right, but tell you which you got wrong if any are off.

I feel like that would create more incentive to stretch my memory to remember more than just the easiest meaning or reading to recall… Otherwise… if I’m just going to get the item marked red because I went to the extra effort, what’s the point (besides ultimately greater learning… obvs, but you know… my mind works on labrat level, so where’s the reward?)??

1 Like

I see, so “if any answer is correct → correct” instead of the current behavior, which goes “any answer incorrect → incorrect” + telling you which exact inputs were false.

Basically: A flipped version?

Sure, I could do that, it shouldn’t be very difficult. I’ll probably update my original post with a link to the alternative version tomorrow.

2 Likes

Why not make it a setting?

1 Like

I can’t deny that the thought did cross my mind. However, I feel that incorporating a whole settings save system plus GUI would take an enormous amount of effort and therefore time compared to just making a second version, for little return because that’s not something people would change often…

I don’t know, I don’t really see the benefit for now (plus, I don’t know how that system is typically realized - are there any specific guidelines to follow? Heck, where do I put the config menu in the first place lol)

All of that notwithstanding, it does sound like a fun thing to try my hand at, I’ll admit :slight_smile:

1 Like

It seems you need to learn how to use wkof. With wkof implementing a setting is easy and fast.

1 Like

Seconding WKOF. Super easy to add settings once you get a hang of it

2 Likes

You could read the Double-Check script to see how it is done in a script showing during reviews.

1 Like

Alrighty then, looks like I’ll be using WKOF after all. Thanks for the suggestion, @prouleau and @Kumirei!

@ganbareniichan sorry, it’s gonna be a bit longer then, though only a few days at most ^^

2 Likes