Advent of JavaScript, Day 4

Advent of JS Homepage

Challenge #4 is creating a computer keyboard that jiggles the key it wants you to press:

Screenshot of keyboard

With the project files downloaded and codesandbox‘d into a live CodeSandbox, I’m ready to get going!


User Requirements

Again, let’s start with the User Requirements and speculate how I can solve these:

  • See the computer keyboard centered on the page

Done already via styles.css.

  • A random letter will start to jiggle.

The animation already exists via styles.cssjiggle class.

As for choosing a random letter, each <button> has a data-key that indicates what should be pressed (e.g. TAB). I can randomly pick one of these (ideally, from a dwindling list so repeats don’t happen) and toggle jiggle.

  • The user should type the same key that’s jiggling and it will stop.

I’ll need to double-check the KeyboardEvent properties to support SHIFT & possibly others.

Otherwise, string matching against event.key should be enough!

  • A new, random key will start jiggling

I can have a randomizeKey function that will randomly pick a key from the <button>s and toggle jiggle.

Wiring it Up

So, this was actually a little more fun than I expected!

  1. Getting a list of keys is straightforward:

    let keys = Array.from(document.querySelectorAll('[data-key]'))
  2. So is toggling the jiggle class:

    if (currentKey) currentKey.classList.remove('jiggle')
    
    currentKey = keys[i]
    currentKey.classList.add('jiggle')
  3. And listening to events:

    window.addEventListener('keyup', (event) => {
      if (event.key.toUpperCase() === currentKey.getAttribute('data-key')) {
        randomizeKey()
      }
    })

    I also listen to click events so I can click the Capslock button (since I have it mapped to Escape on my keyboard):

    window.addEventListener('click', (event) => {
      if (event.target.hasAttribute('data-key')) {
        randomizeKey()
      }
    })

Algorithms

But, what I haven’t done before was swapping variables without an intermediate value and an optimal, simple shuffling algorithm.

  1. Swapping variables without an intermediate value is easy with ES6:

    ;[a, b] = [b, a]

    For this use-case, I swapped the random i key with key at the end of the array:

    ;[keys[i], keys[end]] = [keys[end], keys[i]]
  2. Next, I wanted to see if there’s an official algorithm to what I proposed above, and there is: Fisher-Yates Shuffle

    There’s a fantastic visualization from Mike Bostocks:

    const randomizeKey = () => {
      // If there are no available keys left, restart the shuffle
      if (end === 0) end = keys.length
    
      // Pick a random index from 0 to the end of the array,
      // then shorten the array
      i = Math.floor(Math.random() * end--)
    
      // Stop jiggling the current key
      if (currentKey) currentKey.classList.remove('jiggle')
    
      currentKey = keys[i]
      currentKey.classList.add('jiggle')
    
      // Swap random key with the key at the end.
      // This ensures it won't get picked again since it's been "shuffled" out to the end
      ;[keys[i], keys[end]] = [keys[end], keys[i]]
    }
    1. Initially, the end index is the length of the array.
    2. We randomly pick i from 0 to the end of the array.
    3. We decrement end by 1 so that we can move the random keys to the end of the array.
    4. Using the previous swap algorithm, we swap the randomly selected key with the unshuffled key at the end.
    5. After we finish shuffling and the end is 0, we reset it back to keys.length & reshuffle the shuffled array all over again.

Demo

I spent the most time actually writing this blog post and porting over Mike Bostock’s visualization :)