The Day 4 challenge from Advent of CSS.

The challenge was to build a keyboard that met the following requirements:

  • Users see the computer keyboard centered on the page
  • A random letter will start to jiggle.
  • The user should type the same key that's jiggling and it will stop.
  • A new, random key will start jiggling

Link to the Webflow project.

Some of the key bits from this challenge:

Using the Webflow CMS

I used the Webflow CMS for the keys, to make it easier to (a) load in content and (b) make page-wide changes to the structure later on.

Each key item had a few fields in the CMS that defined: - the text to show - the style of button (regular or 'special') - the width of button - the order of appearance - each key's value - what we'd compare with the user's keystrokes

The free Webflow plan limits us to 50 items so I had to remove a few keys from the keyboard (hope nobody misses the forward and back slashes...).

Laying out the keyboard

The keys were added via a single collection list with flexbox, with fixed widths applied to each key size and to the overall keyboard element.

(I realised afterwards that if I'd used 4 collection lists (one for each row of keys), I could have used flexbox a little more effectively and let the larger 'special' keys grow to fill the remaining space in each row.)

Because I ended up hard-coding things by eye, the keyboard isn't responsive, and only works for ~900px screens and above. But I was more interested in the dynamic element of this task anyway.

I used an embed element for each CMS item, adding appropriate classes dynamically from each item to define the style and size of each button.

Making it interactive

As above, the challenge was to randomly start animating a key, then stop animating it if the user pressed the correct key on their keyboard.

Here's our overall plan. When the user clicks Start, we do the following:

  1. Generate a random integer between 1 and 48 (our number of keys)

  2. Find the relevant key.

    We add IDs to each key using the Order field from the CMS as a dynamic embed, so we can easily find the key matching this integer.

  3. From this key, get the value we need to match again

    Again, we get this from the CMS and store it as a data attribute on the key item

  4. Start animating this key

    We do this by simply adding a class .anim to the key. The CSS for this animation (a red outline and a basic rotation animation) is stored in an embed element on the page.

  5. Listen for keyboard presses

    We add an event listener to the Document to listen for keydown events.

  6. When a key is pressed, we pass the value of the keydown event to a checking function.

    Because I wasn't 100% sure about capitalisation of the 'real' keydown values compared to the values in the CMS (not forgetting the inclusion of the caps lock key in this minigame, which complicates matters!), we convert both values to lower case before comparing.

  7. A success animation plays if the key is correct.

    This is simply an extra child element in each key that originally has a 0% opacity. If a key is correctly pressed, we add an .anim class to this child element that causes it to quickly become semi visible and grow, giving the effect of sort of green flash around each key. Again, the CSS for this is in an embed element on the page.

    An important point here - before adding this .anim class, we first try and remove it - in case this key was successfully pressed before. But - this is not enough to cause the animation to fire again unfortunately. You have to give the page a bit of a kick to reset itself so you can again re-animate the child element. You can either do this a few ways, e.g. by removing and re-adding the element from/to the DOM, or, you can do what I've done here and trigger a reflow with the offsetWidth method. This CSS Tricks article (from 2011) goes into the problem and various methods.

  8. If the key is correct, we also need to stop the currently jiggling key from jiggling further.

    We do this by just removing the .anim class from the key element. (NB - I've used .anim to trigger both the parent key and child success elements - they're just trigger classes, they don't do anything unless they're also applied to another class)

  9. Then we need to pick a new key and start the process over again!

A few other bits:

  • I also added listeners to listen for clicks on the on-screen keyboard, as an alternative to pressing physical keys. This was in part due to the fact that my MacBook doesn't have a Delete key, so I couldn't actually press the "del" key on the onscreen keyboard.
  • Pressing Tab and Enter on your physical keyboard may do some other things (such as tabbing to other links on the page and activating them, like in my footer element). I'm sure there's a way of hijacking these and stopping these things from happening, but I didn't look into it.
  • I would have quite like to have added sound effects and some sort of scoring system, but having spent a couple of hours on this challenge I figured that could wait until another time ;)

The full JS code

    /*
- generate a random integer between 1 and 48
- find the key with this id
- start jiggling this key
- detect keyboard presses
- on key press, check if pressed key matches active key
- if it matches, provide some sort of success indication
- generate a new random integer and repeat
*/


(function() {

var gameActive = false;
var rand, activeKey, clickedKey, activeKeyCode;

function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive
}

function startJiggleKey(key) {
key.classList.add("anim");
}

function stopJiggleKey(key) {
key.classList.remove("anim");
}

function getKeyAndJiggleIt() {

/* if game is not active, do nothing */
if(gameActive === false) {
return;
}

/* get a random number */
rand = getRandomInt(1,48);

/* get the key and its code which we've loaded from the Webflow CMS */
activeKey = document.getElementById("key-" + rand);
activeKeyCode = activeKey.dataset.keyValue.toLowerCase();

/* get jiggling */
startJiggleKey(activeKey);
}

function highlightCorrectKey(key) {
var keySuccess = key.querySelector(".key__success");
keySuccess.classList.remove("anim");
keySuccess.offsetWidth;
keySuccess.classList.add("anim");
}

/* listen for start */
document.getElementById("start").onclick = function() {

/* set game to active */
gameActive = true;

getKeyAndJiggleIt()

document.getElementById("start").parentElement.style.display = 'none';
}

function testClickSuccess(inputKey) {

/* if we don't have an active key, do nothing */
if (!activeKeyCode) {
return;
}

/* if key matches */
if (inputKey === activeKeyCode) {

/* success action */
highlightCorrectKey(activeKey);

/* stop the jiggle */
stopJiggleKey(activeKey);

/* start again */
getKeyAndJiggleIt()

}

else {
}

}

/* listen for keypresses */
document.addEventListener('keydown', function (event) { /* from https://alligator.io/js/listening-to-keyboard/ */

/* check this click */
testClickSuccess(event.key.toLowerCase());

});

/* add listeners to virtual keys too */
var keys = document.querySelectorAll(".key");
for (var i = 0; i < keys.length; i++) {

if(keys[i].hasAttribute("data-key-value")) {
keys[i].onclick = function() {
testClickSuccess(this.dataset.keyValue.toLowerCase());
}
}
}
})()