This is a response to Cloze one by one uncovering - #14 by shallash
I’ll briefly explain how basic user input works in JS, so you can add buttons and keyboard shortcuts yourself whenever you need them in the future:
Foundation: The HTML element
The foundation for everything JS-related is the HTML element. Any content that isn’t plain text ( → textNode
) is contained inside an element:
- various content containers (
<div>
,<span>
,<p>
, …) - links (
<a>
) - images (
<img>
) - etc.
Elements can be accessed in lots of different ways, but the most universally usable function is
element.querySelector
and
element.querySelectorAll
where element
can be document
(global interface for the whole page) or any other HTML element, and the argument is a CSS selector for the element(s) you want to get.
Examples:
var element = document.querySelector("#elementID")
var chilrenWithSomeClass = element.querySelector(".class")
var allLinks = document.querySelectorAll("a")
You’re probably already familiar with
document.getElementById("id")
- which is a bit faster thanquerySelector("#id")
because it’s more specific ( → less overhead), but that’s irrelevant unless you’re making hundreds of thousands of calls.
Elements are stored as objects and as such can have lots of different attributes/properties, which can either be accessed with dot-notation or bracket notation.
element.property // standard
element["property"] // useful in specific cases
I recommend analyzing the attributes of different elements with the AnkiWebView Inspector to get a sense for what can be accessed.
User Interaction 101
1: Choose your target Element
Unless its height/width is set to 0, an element occupies a box-shaped area which you can interact with (you can extend that area beyond its actual bounds with CSS pseudo elements):
Pick an existing element or wrap the content you want to interact with inside an HTML tag with an
id
orclass
. In this sample image, a<div>
element with theid
“container” is highlighted using AnkiWebView Inspector.
2: Find the right Event
Elements constantly fire events: when you hover them, click/activate them, press keys while they’re in focus etc. You can use these events as triggers for your functions - and there’s tons of them. We only need to find the right one. Google is usually the fastest way - look for Mozilla Dev Network (MDN) pages, as they are of the highest quality. Here’s an overview: Event reference | MDN
I will use the click
event for this example, which can - perhaps unintuitive at first - be used on all elements, not just the <button>
element.
3: Attach an EventListener to your target
EventListener vs. GlobalEventHandler
To make a function execute when an element is clicked, you got two popular options:
element.addEventListener("click", function)
and the GlobalEventHandler method (via attribute, often used inline)
element.onclick = function
Syntax: oneventname
Only one
onclick
handler can be assigned to an object at a time. You may prefer to use the EventTarget.addEventListener() method instead, since it’s more flexible.
Beware: Don’t use function()
with parentheses as an argument.
We just want to assign the function to call when the event triggers, not execute it right away.
4: Create event handler
Once you assigned an EventListener to your target element, you just need to write a function that executes once the event is triggered.
Depending on the type of function we assigned to our EventListener, there are different options for how to access the element that triggered the event:
this
If we pass a traditional function, we can use this
to access the triggering element:
element.addEventListener("click", convertToUpperCase)
function convertToUpperCase() {
this.innerText = this.innerText.toUpperCase()
}
Notice how this function doesn’t have a return value? The element’s
innerText
attribute is altered as a side-effect.
event.target
If we got an arrow function, we cannot use this
, but we can use the target attribute of the event interface, which is implicitly passed to our arrow function:
var convertToLowerCase = (event) => {
let element = event.target
element.innerText = element.innerTex.toLowerCase()
}
element.addEventListener("click", convertToLowerCase)
Notice how I add the
eventListener
after the initialization of our variable convertToLowerCase? That is something you have to be aware of when using this form of functions. In most cases, you’ll probably be better off with traditional functions, because they are available anywhere in the current scope.
The event
interface can also be used in traditional functions of course (target
is just one of many useful properties).
Most useful events
- click (works with mouse and touch input)
– there’s also a dblclick event - keydown (used for keyboard shortcuts)
- focus & blur (i.e. when tabbing through things)
- mouseover (for hover effects)
Troubleshooting, common pitfalls
Most of the time, the reason why your interactive button isn’t working lies within the function that handles the event. Because the possibilites for errors are endless, I can’t give you a miracle cure for that sort of issue. But I can give you some common pitfalls I have encountered in the past that do not have anything to do with the event handling itself.
Passing function() instead of function
As mentioned above, when you pass a function with parentheses into an EventListener, JS will invoke the function immediately, not when the event fires.
The element you're trying to attach an EventListener to has not been loaded into the DOM yet
A web page is built up step by step. Sometimes you’ll encounter a situation where you think you successfully attaching an EventListener to an element, but it still doesn’t work, because at that time it wasn’t mounted into the DOM tree yet. This can be incredibly frustrating as it tends to be the last thing you’re looking at when trying to debug your template.
Solution: add the eventListener asynchronously (i.e. with a setTimeout, MutationObserver or Promise)
Cross-client differences (e.g. Anki Desktop and AnkiMobile)
The webview of each Anki client differs a bit. The documents have slightly different structures, classes and most importantly, run on different operating systems. That means some things that work on one client do not work on the other (e.g. lookahead and lookbehind assertions for regular expressions do not work on AnkiMobile, as it uses Safari for mobile, which kinda sucks).
AnkiDroid is not persistent
The most important difference you have to be aware of when striving for cross-platform compatibility is between AnkiDroid and the rest of the ecosystem: AnkiDroid rebuilds the whole document on each card flip, the other clients don’t. That means that elements/variables that remain persistent on those clients are not persistent on AnkiDroid.
I recommend you build your code for the persistent paradigm of Anki Desktop, AnkiMobile and AnkiWeb, as a reliance on the periodic destruction of the document (AnkiDroid) makes your code unusable on the other platforms, but it’s way easier to adjust the other way around.