Categories
CSS HTML JavaScript

JavaScript Shakespeare Exercise Solution

Since posting the original exercise, I have made a few changes to the look of the page, so if you download the completed version, do not be surprised that some features are different from those described in the exercise.

First, download the slim minified version of jQuery and add a link to it just before the closing of the body tag. Then add a link to a new .js file after the jQuery link:

<script src="js/jquery-3.1.1.min.js"></script>
<script src="js/shakespeare.js"></script>

Planning the Script

Before coding anything we need to figure out what the page actually needs to do. Each component of the task can be coded as a function for invocation in the (document).ready() handler as well as in other functions.

Here are most of the planned functions:

  • Sliding instructions panel up when it is clicked
  • Sliding instructions panel down when button is clicked
  • Hiding quotes and titles
  • Selecting and displaying a random quote on click of genre panel
  • Displaying the titles in the clicked genre panel
  • Styling the active panel and titles
  • Comparing clicked title to title of random quote
  • Updating score
  • Generating random insult
  • Displaying random insult

The Instructions Panel

As noted above, the instructions panel will slide up when it is clicked, and slide down when the instructions button is clicked. When down (as at page load), it will take up the full width and height of the screen.

So first let’s style it.

/* INSTRUCTIONS SECTION ======================================= */
.instructions-text {
	color: white;
	background-color: rgba(0,0,0,.85);

	font-size: 1.5rem;
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;

	display: flex;
	flex-flow: column nowrap;
	justify-content: center;
	align-items: center;
}

.instructions-text:hover {
	cursor: pointer;
}

.instructions-text ul {
	display: flex;
	flex-flow: column nowrap;
}


By using four values on the positioned instructions, we can get it to cover the entire width and height of the browser window. The flex values are to center the text horizontally and vertically. By setting the cursor to pointer, we provide a hint that the panel is clickable (when the user wants to hide the instructions).

Now lets add the jQuery, beginning with the $(document).ready() handler: the function you will start most jQuery scripts with. I like to keep that handler clean by making functions of separate tasks and then calling them from the ready() handler, but there are other ways to do it.

$(document).ready(function(){	
	showHideInstructions();
});

function showHideInstructions(){
	$('.instructions-text, header button').on('click', function(){
		$('.instructions-text').toggleClass('instructions-toggled');
	});
}

The “on” method is the jQuery way of adding an EventListener (if you’re familiar with “vanilla” js). The first element passed to that method is the event to listen for (passed as a string), then the function that happens when that event is triggered.

Here we have added an EventListener to each of two items (the instructions panel and the button in the header). The toggleClass method does what you might expect: adds a class if the targeted element does not have it, and removes it if it does.

Obviously, though, we need here to add the instructions-toggled class to our stylesheet.

.instructions-toggled {
	transform: translateY(-100%);
}

This transformation will move the panel up the Y-axis 100% of its height, hiding it off screen.

Test it in the browser.

You will see that the menu disappears when clicked, and reappears when the button in the header is clicked. However, we need to make a transition so the change is not instaneous. Although jQuery has transition methods, it is typically more processor-friendly to apply transitions via CSS. So add the following to your .instructions-text style: transition: transform .5s;

Test again: the panel slides up and down when the panel or the button are clicked.

Hiding Titles and Quotations

If you examine the HTML I gave you, you will see that I have put the play titles and associated quotes in a Description List (DL), with the titles marked up as DT elements and the quotes as DD elements. Hiding them is again as easy as selecting them and toggling a class:

function hideAll(){
	// hide all quotations and titles
	$('dd, dt').addClass('hidden');

	// clear out quotation box
	$('.quotation').text('');
}

If you know JavaScript, you will no doubt note here that you do not need to loop through the returned node list to toggle the classses (if you don’t know JS, that comment won’t make sense, of course).

If you further examine the markup, you will see that I have put an empty quotation DIV in each panel, between the heading and the DL. Later when we generate the random quote, we will put the quote into that DIV, but in this hideAll() function, we will insert an empty string.

The reason we will do this is so that the box empties of text every time the function is invoked. That way, whenever we generate a new random quote for example, it won’t be added to the previous quote).

Now add this function to the (document).ready() handler:

$(document).ready(function(){	
	showHideInstructions();
        hideAll()
});

Test your page in the browser. The main panels should now only show the genres (HISTORIES, TRAGEDIES, COMEDIES, PROBLEM PLAYS).

Showing Titles and Random Quotation in One Panel

When a user clicks on a genre panel, it needs to “open” and show the titles and a random quote. As well, any open panels must close.

Each genre panel has a class of menu-section. We will attach an EventListener to each via the jQuery on method.

When a panel is clicked, the handler will first of all invoke the hideAll() function we wrote earlier. This will close the panels, which means that when we open the clicked one, we won’t have two open.

Then we save in a variable a reference to the panel that was clicked: the $(this) construction saves a reference to the target of the click event. By wrapping the JavaScript THIS in $(), we convert it into a jQuery object, which means that we can use a range of jQuery methods on it.

For example, in the next line, we add a class to the clicked panel, so that we can use styles that apply to the currently-selected panel. As well, with the $(this) object (saved here as genre), we can search within the active panel (using the <b>find</b> method) for DTs, which hold the play titles.

Having returned a jQuery list of DTs in this clicked panel, we can then toggle the HIDDEN class on these DTs only. This means, in short, that the DTs in the other panels will remain hidden, while those in the clicked panel will now be visible.

function onGenreClick(){
	$('.menu-section').on('click', function(){
		
		hideAll();

		var genre = $(this);

		// FOR STYLING PURPOSES, we can then a use descendent selector 
		// to style the titles.
		genre.addClass('active');
		
		// Make TITLES visible in this panel only
		genre.find('dt').toggleClass('hidden');

                // Check how many titles there are
		var titleNum = genre.find('dt').length;

                genre.find('.quotation').text("sample text");
		// genre.find('.quotation').text(randomQuote(genre, titleNum));

	});
}

Add the onGenreClick() function to the document ready handler.

Test the file.

Everything should work. If something does not work, go to the Console in the Inspector and see if you can work out what the error message is telling you.

Obviously, we will need to set our  hidden class to display: none.

As well, if you look in the QUOTATION div, you will see that we have just written the words sample text into the quotation box.  (Using the text() method, incidentally, we can set text and we can get text.)

Delete the line that is writing the text into the quotation DIV, and uncomment the line after it.

Now when you test the file, you will definitely get an error. The reason is in the last part of this function. Instead of writing sample text we will write a random quote by passing a function called randomQuote to the text() method.

The problem, however, is that we have not yet written that randomQuote function.

Generating the Random Quote

If you look at the reference to the randomQuote function in the code above, you will see that inside the brackets there are two variables being passed to the function: genre and titleNum.

The first, genre, is the saved reference to the clicked panel itself. The second is the number of DTs inside that panel, which we determined using the length property.

So here is the actual function:

function randomQuote(genre, titleNum){

	var randomNumber = Math.floor(Math.random() * titleNum);
	var theQuote = genre.find('dd').eq(randomNumber).text();
	
	return(theQuote);
}

In the first line inside the function, we generate a random number between 0 and .999999 (etc) which we then multiply by the number of titles passed in via the titleNum variable. Then we use Math.floor() to round the number down.

The result is that our random number will be between 0 and (the number of titles minus 1). This is useful, because the jQuery set of returned elements is indexed like an array (starting at 0).

The logic of the line before the return function is this

  • within this panel (genre), find all the DD elements
  • with this list of DD elements, chose the one whose index is the same as the random number. ( EQ(number) will return the element at the specified index (location) in a set.)
  • get the text from inside the randomly selected DD

By passing the theQuote to the return function, we pass the quote back to where the randomQuote was called. That was in the previous function.

Note here, also, that we do not invoke this randomQuote function inside the $(document).ready() handler. The reason is that this function does not need to run immediately on page load. Rather, it is invoked by another function when needed.

Making the Titles Clickable

We now need to make the titles (which reside inside DTs) clickable.

To do that we will add the following code to our script:

function onTitleClick(){
	$('dt').on('click', function(evt){

	// Prevent CLICK event from BUBBLING UP and triggering the section LINK
	evt.stopPropagation();
	
	var title = $(this);

	// Find the text of the next nearest sibling of the title DT.
	var associatedQuote = title.next().text();

	// Find what is in the QUOTATIONS box right now.
	var visibleQuote = title.parent().parent().find('.quotation').text();

	if ( associatedQuote == visibleQuote ){
		shakespeareScore++;
		shakespeareAttempts++;
	}
	else {
		shakespeareAttempts++;
	}
	
	updateScore();

	});

}

Add a reference to this function to the document ready function. It should now look like this:

$(document).ready(function(){
	
	showHideInstructions();
	hideAll();
	onGenreClick();
	onTitleClick();
});

Test the page. A number of errors will be flagged in the console.

Obviously, we have not yet written the updateScore function, so just comment it out for now.

As well, when we test whether the quotation has been identified correctly, we increment one or both of two variables: shakespeareScore and shakespeareAttempts. Those variables do not yet exist. To remedy that, add the following code above the document ready handler:

var shakespeareScore = shakespeareAttempts = 0;

Now the script should work.

The most important part of the above script is the evt.stopPropagation() code. What is happening here is that we are passing the event object (you can call it anything you like, like any variable, but it is usually called e or evt) and using stopPropagation with it.

This solves a frustrating problem: the DTs are clickable, but they are inside another element (the genre panel) which is also clickable. The problem is that a click event “bubbles up”: the click will trigger the event handler attached to the DT, but also the event handler attached to the .menu-section (genre panel). This means that clicking a title will trigger a new random quote.

stopPropagation, in other words, will prevent a click on a child element from also applying to any of the child’s ancestors.

Because of the structure of our HTML (DT + DD), the use of the next() method (along with the text() method, of course) will return the quote associated with the clicked title.

In a similar way, we will travel up the DOM from the clicked DT to the parent DL then the parent .instructions-text DIV. This will allow us to search the text of the QUOTATIONS DIV in order to compare title choice to actual title. ( We could have just stored that information in a variable, of course, which would be more efficent…)

Then we test to see if our two strings (guess title text vs actual title text) match. If so, the user gains a point.

Which means that it is time to write the updateScore() function.

Updating the Score

This is easy:

function updateScore(){
	$('.score span').text(shakespeareScore + '/' + shakespeareAttempts);
}

This function does not need to be added to the document ready handler, as it is invoked by another function (and we do not need it to run immediately when the page is fully loaded).

Test the page. Hopefully everything is working as expected.

The Insult Generator

To make the insult, we will write a function that when triggered does the following things:

  • takes a random adjective from the adjectives array
  • takes a second random adjective from the adjectives array
  • takes a random noun from the nouns array
  • joins these together in a sentence
  • displays that sentence on the page

Since we will need to generate random numbers multiple times, we will make a function that does this when invoked and passed the array length.

The generation itself is pretty easy: since items in an array can be accessed by number enclosed in square brackets, if we call our randomize function, passing it the length of the array itself, we will get a random word.


function generateInsult() {

var insult = "";

var adjectives = ["artless","bawdy","beslubbering","bootless","brutish","churlish","cockered","clouted","craven","currish","dankish","dissembling","droning","errant","fawning","fobbing","froward","frothy","gleeking","goatish","gorbellied","impertinent","infectious","jarring","loggerheaded","lumpish","mammering","mangled","mewling","paunchy","pribbling","puking","puny","quailing","rank","reeky","roguish","ruttish","saucy","spleeny","spongy","surly","tottering","unmuzzled","vain","venomed","villainous","warped","wayward","weedy","yeasty","base-court","bat-fowling","beef-witted","beetle-headed","boil-brained","clapper-clawe","clay-brained","common-kissing","crook-pated","dismal-dreaming","dizzy-eyed","doghearted","dread-bolted","earth-vexing","elf-skinned","fat-kidneyed","fen-sucked","flap-mouthed","fly-bitten","folly-fallen","fool-born","full-gorged","guts-griping","half-faced","hasty-witted","hedge-born","hell-hated","idle-headed","ill-breeding","ill-nurtured","knotty-pated","milk-livered","motley-minded","onion-eyed","plume-plucked","pottle-deep","pox-marked","reeling-ripe","rough-hewn","rude-growing","rump-fed","shard-borne","sheep-biting"];

var nouns = ["apple-john", "baggage", "barnacle", "bladder", "boar-pig", "bugbear", "bum-bailey", "canker-blossom", "clack-dish", "clotpole", "coxcomb", "codpiece", "death-token", "dewberry", "flap-dragon", "flax-wench", "flirt-gill", "foot-licker", "fustilarian", "giglet", "gudgeon", "haggard", "harpy", "hedge-pig", "horn-beast", "hugger-mugger", "jolthead", "lewdster", "lout", "maggot-pie", "malt-worm", "mammet", "measle", "minnow", "miscreant", "moldwarp", "mumble-news", "nut-hook", "pigeon-egg", "pignut", "puttock", "pumpion", "ratsbane", "scut", "skainsmate"];
var firstAdjective = adjectives[randomizeMe(adjectives.length)];
var secondAdjective = adjectives[randomizeMe(adjectives.length)];
var noun = nouns[randomizeMe(nouns.length)];

console.log(firstAdjective, secondAdjective, noun);

}

function randomizeMe(arg){
var randomNumber = Math.floor(Math.random() * arg);
return(randomNumber);
}

Test the page and go to the console to see if you are getting the three words output.

Modify the Function For Grammar

Add the to our generateInsult function, replacing the console.log operation.


var article = "a";

insult = "Thou art " + article + " " + firstAdjective + " " + secondAdjective + " " + noun + ".";
console.log(insult);

The reason I have turned the grammatical article into a variable will be become apparent if you go to the console and reload the page a number of times. Eventually, you will generate a firstAdjective that begins with a vowel. At that point, you will need to change the value of article to <b>an</b>.

The following code will do that.

The first line gets the first character of the firstAdjective variable. The next line tests if it is vowel. If so, it changes it.

var firstLetter = firstAdjective.charAt(0);
if (firstLetter == "a" || firstLetter =="e" || firstLetter == "i" || firstLetter == "o" || firstLetter == "u" ) {
article = "an";
}

(Make sure that you add the code before the line that creates the full sentence, so the sentence uses the correct article.)

Reload the page a number of times and check that the correct article is ending up in the sentence.

Wire Up the InsultMe Button

If we look in the footer of the document, you will see a button with an ID of <b>insult-trigger</b>and a DIV with an ID of <b>insult</b>. Let’s wire up that button so that it outputs the insult when clicked.

First change the generateInsult function to add a return(insult) in the last line.

Then add the following.


function onInsultMeOnClick() {
$('#insult-trigger').on('click', function(){
$('#insult').text(generateInsult());
});
}

Finally, now add the onInsultMeOnClick() function to the document ready handler.

Test your page.

Hopefully everything is working.

Download the solution file.