In this reading, we'll see the DOM and jQuery in action. We'll create an image gallery and then a drop-down menu.
Often, you want to display a set of pictures and allow the user to choose which ones to look at more closely, something like an art gallery. Therefore, we'll call this JavaScript application a gallery. We'll have an arrangement of small pictures, often called thumbnails, and, if the user clicks on any one of them, a larger version is displayed.
Note that one advantage of a gallery is that a set of thumbnails can load quicker than even one of the larger versions, if the file sizes are much smaller. Therefore, you should go to the extra effort to make thumbnail-size images, using an image editor (like PhotoShop or Pixlr or the Gimp), rather than just using the width and height attributes to make the squeeze the big files down to a small screen area.
This will just be a small gallery, with just four pictures. In this version of the gallery application, we'll allocate space on the page for the larger version. In this example, we'll put a boring pale-yellow image there. If you decide to build a gallery application, you could choose one of the large images, either randomly or deterministically, or something else entirely.
Please try this gallery example
An alternative is to have the large image take over the window, putting everything else behind a semi-transparent dim filter, so that the user's attention is necessarily on the large image. This is called a Lightbox, and has been implemented many times since its originator, Lokesh Dhakar. We've implemented a very simple version of it; there are many more sophisticated ones. The lightbox, of course, requires an additional bit of coding to make the image go away so that the user can look at another image or do anything else.
Please try this lightbox gallery
Creating a gallery involves first putting a set of thumbnails on the page. That can easily be done by copy/pasting a bunch of HTML, like this:
<figure data-bigsrc="../potterpics/harry-potter-big.jpeg" data-alt="Harry Potter"> <img src="../potterpics/harry-potter-thumb.jpeg" alt="Harry Potter"> <figcaption>Harry Potter</figcaption> </figure> <figure data-bigsrc="../potterpics/hermione-granger-big.jpeg" data-alt="Hermione Granger"> <img src="../potterpics/hermione-granger-thumb.jpeg" alt="Hermione Granger"> <figcaption>Hermione Granger</figcaption> </figure> ...
That's acceptable, but a bit tedious and error prone. When you copy/paste the code, you have to remember to change all the places where we see "Harry Potter" to "Hermione Granger." If you miss one, rabid fans will notice when the caption for Hermione says Harry.
An alternative is to define a function to create one of these gallery items:
function addToGallery(galleryID,thumbsrc,bigsrc,alt) { console.log("add figure "+thumbsrc); var img = $("<img>") .attr("src",thumbsrc) .attr("alt",alt); var cap = $("<figcaption>"+alt+"</figcaption>"); $("<figure>") .attr("data-bigsrc",bigsrc) .attr("data-alt",alt) .append(img) .append(cap) .appendTo(galleryID); }
You can then invoke it with the necessary info. You'll notice that there's still a lot of repetition, but because only the key info is shown, it's easy to be thorough. (We could also define a higher-level function that could create the proper URLs, if we used a simple naming scheme. We won't describe that here.)
addToGallery("#gallery1", "../potterpics/harry-potter-thumb.jpeg", "../potterpics/harry-potter-big.jpeg", "Harry Potter"); addToGallery("#gallery1", "../potterpics/hermione-granger-thumb.jpeg", "../potterpics/hermione-granger-big.jpeg", "Hermione Granger"); ...
You probably noticed the two attributes starting
with data-
. Here's the code again:
<figure data-bigsrc="../potterpics/harry-potter-big.jpeg" data-alt="Harry Potter"> <img src="../potterpics/harry-potter-thumb.jpeg" alt="Harry Potter"> <figcaption>Harry Potter</figcaption> </figure>
We've learned about lots of tag attributes in HTML, such as SRC for IMG
and HREF for A. Those are all built into the HTML
language. These data-
attributes are something new. They
aren't built into the language, but we (the authors of the web page)
get to make them up. More precisely, the
prefix data-
is built into the language, but we get to
make up the suffix.
When we click on the figure, which we'll turn to in a moment, we'll
need the SRC and ALT for the big version, so we stash the values
here. Is this legal? Yes. The design of HTML5 anticipated the need for
data sprinkled throughout the DOM tree, and explicitly allowed the user
to create any attributes they want, as long as they start
with data-
. So, we're putting that information there so
that it'll be there when the event handler needs it.
We also need a place to display the big version of the picture. (This is the main difference between the simple and lightbox version of the gallery.) For the simple version, we just need a figure:
<figure id="large_image"> <img src="../potterpics/pale-yellow-266x200.png" alt="filler image"> <figcaption>A pale yellow image filler image</figcaption> </figure>
We put the pale yellow box there just so that the IMG has a SRC when the page loads. We should probably put an image from the gallery in there, but this explanation is a little clearer if there's a different image there at the beginning. We would use CSS to make this figure larger.
Finally, all the pieces are in place for the image gallery. We'll
define a function that will be attached to each thumbnail, and it'll
cause the larger version to be displayed in
the figure#large_image
that we just saw. Here's the code:
function enlargeGalleryImage() { var bigsrc = $(this).attr("data-bigsrc"); var alt = $(this).attr("data-alt"); $("#large_image img") .attr("src",bigsrc) .attr("alt",alt); $("#large_image figcaption").html(alt); }
Next, we have to attach it to every thumbnail in the gallery:
$("#gallery1 figure").click(enlargeGalleryImage);
This function is hard to understand because it is abstract:
there's just the one function, but it's used for 4 or 400
thumbnails. For each of those thumbnails, a different enlargement has to
be loaded. Fortunately, we put the URL of the enlargement on the figure
that was clicked on, so we can find out the URL using the magic
variable this
. Here's the key part:
var bigsrc = $(this).attr("data-bigsrc");
Each thumbnail as a data-bigsrc
attribute, and the event
handler can grab that attribute out of the thumbnail. Which thumbnail?
The one that was clicked on, which is stored in this
.
All the rest of the code is stuff we've seen before. The key step is here:
$("#large_image img") .attr("src",bigsrc)
That sets the src
of the large img to the appropriate
value.
The other lines of code are doing similar things for
the alt
attribute, and we also use the alt attribute to
fill in the figcaption
.
The lightbox is pretty similar, except that the destination is large and takes over the screen, using CSS:
<div id="lightbox_display"> <div id="lightbox_inner"> <figure> <img src="../potterpics/pale-yellow-266x200.png" alt="filler image"> <figcaption>A pale yellow image filler image</figcaption> </figure> </div> </div>
Here's the CSS.
#lightbox_display { position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; background: black; background: rgba(0,0,0,0.8); display: none; } #lightbox_display figure { background-color: white; width: 75%; margin: auto; } #lightbox_display figure img { width: 100%; }
The key ideas are that the outer box, lightbox_display
, is
fixed and as big as the screen. Inside that is a figure that is slightly
smaller and centered, with a white background. Inside that is an image
where the big version is displayed.
The event handler is very similar to the simple version, except we add
the .show()
at the end to make the lightbox visible.
function openLightboxImage() { var bigsrc = $(this).attr("data-bigsrc"); var alt = $(this).attr("data-alt"); $("#lightbox_display img") .attr("src",bigsrc) .attr("alt",alt); $("#lightbox_display figcaption").html(alt); $("#lightbox_display").show(); }
Of course, since the big version takes over the display, we have to set up a click handler to hide it when the user wants to make it go away:
function closeLightboxImage() { $("#lightbox_display").hide(); } $("#lightbox_display").click(closeLightboxImage);
That's the essentials of how image galleries work.
We now turn to drop-down menus, which will use many of these same ideas:
this
variable
Drop-downs also involve some new concepts:
This will take us a few steps, so be patient.
When we first learned about .hide()
and .show()
, the page would have to be re-arranged based on
whether the target element was shown or hidden. Try the following
example, clicking on the header
list item:
Notice how the page (particularly this paragraph) jumps up and down based on whether the submenu is shown? We don't want that with a drop-down menu.
We solve this by using position:absolute
on the menu,
and positioning it relative to the header. Here it is in action:
The html code looks like this:
And the CSS like this:
The auto
value makes the browser calculate the value to
use, thereby positioning the menu in the same place as it was in the
first example, but using absolute positioning. We added
the height
just so the menu wouldn't overlap this paragraph
when it was shown.
Now let's turn to the JavaScript. Let's start with toggling whether something is open or closed, as we did above. Here's the JavaScript code for the previous example:
The event handler gets the menu, using the selector #ex2
ul
, and determines whether it is hidden. It's hidden when its
CSS is display:none
, which we can determine via an
alternate usage of the .css()
method. We're accustomed
to a 2-argument usage, like this:
$(selector).css("property","value");
Instead we have this:
var curr = $(selector).css("property");
This usage reads the current value of the property. In this case,
we're reading the display
property and putting it in a
variable of the same name. The next lines check the value and if it's
"none", the child is hidden, so we should show()
it,
otherwise, hide()
it.
If we replaced those list items (Harry, Ron, and Hermione) with hyperlinks, we actually have a working drop-down menu:
Presto! A drop-down menu!
Chances are, you will have multiple drop-down menus, like this:
We've done a little CSS magic to lay those out horizontally. Note that
we use several selectors of the form A < B
. That is a
child selector, which is a variant on the descendant selectors that we
already know, but instead of B being any descendant of A, B must be
a child of A.
Let's take a look at the JavaScript code to toggle these menus (at least the first one):
The code is the same as before, but we'd need two more copies of it (suitably modified), one for each sub-menu. Later in the course, if there's time, we'll discuss a better, more abstract way to do this. For now, if you'd like, we'll turn to an important but complicated improvement.
(The rest of this reading is optional.)
There's one small flaw with our drop-down menus, and that is the expectation that users have. If they open a menu and then decide that they don't want to go to any of those pages, they have to click again on the header to close the menu. Most people are used to being able to click anywhere else on the page to close an open menu.
Doing that is cool and worthwhile, but tricky and requires some additional concepts. The first concept is putting an event handler on the whole document (the top of the DOM tree). That event handler will close the open menu. Let's go back to a simpler menu, but with our first attempt at that behavior. Try the following, which doesn't work. We'll soon see why, but for now, let's try to understand this attempt.
Here's the source code for the additional behavior, which adds an event handler on the document that closes everything, so that we can close an open menu by clicking someplace else in the document.
Note that we named the function closeAll
, because it will
close any open menu on the screen. As we've seen, there might be
multiple menus, and this function won't necessarily know which one is
open, if any. We could go to a lot of effort to figure out which menu is
open, or we could just use the power of jQuery to close every one of
them, whether they are open or not. We prefer the simpler approach. That
is to put a CSS class on each header LI and then select every
UL child of those LI elements. We then invoke the .hide()
method on them.
You'll notice that the menu is initially hidden, which is what we
usually want to do with drop-down menus. We do this by invoking
the closeAll
function in our code, above.
We then attach the closeAll
function as a click handler to
the document.
But why doesn't clicking on the header work to open the sub-menu? Is the event handler even running? Let's look more closely at the code:
First, we defined a log5
function. That's just for
debugging: it prints a message to the JS console, along with a counter,
so we can tell one message from another. Next, the same event handler
code as before, only augmented with the log5
calls.
Now, open your JS console and try clicking on the header. You'll see
that the toggle5
function is indeed being invoked,
but the closeAll
function is also invoked!. So,
the menu is opened and instantaneously closed. Why?
The answer is that clicking on the header not only counts as clicking on the header but also counts as clicking on the document.
In retrospect, it makes sense that clicking on the element also counts as clicking on the document, but it's still surprising. This phenomenon is called event bubbling, because an event bubbles up the DOM tree from the leaves to the root, like champagne bubbles going from the bottom to the top of the glass. Every element on the path from the leaf (like the LI) to the root (the document) gets a chance to react to the event. It's also called event propagation.
In this case, the normal event bubbling gets in our way. Is there a way to cancel the event propagation? Yes, but it takes a few steps.
First, our event handler is, in fact, invoked with an argument; it's
just that we've been ignoring it up to now. jQuery ensures that our
event handlers is invoked with an object that represents the event,
including information about what event it was (click, mouse motion,
keyboard events) and stuff like that. That event object has a method
called stopPropagation()
, which does exactly what we want.
So, our new and improved toggle
function is going to
accept an argument that is an event object, and it will use that
argument to stop the propagation of this event up the tree:
Here's the source code for fancy new toggle6
function:
Try it!
You're now prepared to understand all the code in this drop-down example.