Galleries and Drop-Downs

In this reading, we'll see the DOM and jQuery in action. We'll create an image gallery and then a drop-down menu.

Image Gallery

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.

Gallery Example

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

Lightbox Gallery

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

How It's done

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");
...

DATA attributes

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.

Where the Big Picture Goes

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.

Adding an Event Handler

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.

How Lightbox Works

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.

Drop-Downs

We now turn to drop-down menus, which will use many of these same ideas:

  • The this variable
  • Things that appear and disappear when clicked on, or
  • make other elements appear and disappear.

Drop-downs also involve some new concepts:

  • finding a child element
  • determining whether an element is shown or hidden
  • event propagation

This will take us a few steps, so be patient.

Positioning

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:

  • header
    • Harry
    • Ron
    • Hermione

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:

  • header
    • Harry
    • Ron
    • Hermione

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.

Toggling

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!

Multiple Menus

Chances are, you will have multiple drop-down menus, like this:

  • Characters
    • Harry
    • Ron
    • Herminone
  • Houses
    • Gryffindor
    • Hufflepuff
    • Ravenclaw
    • Slytherin
  • Teachers
    • Dumbledore
    • Flitwick
    • McGonagal
    • Snape
    • Sprout

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.

Click Anywhere to Close

(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.

  • header
    • Harry
    • Ron
    • Hermione

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.

Event Bubbling

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:

  • header
    • Harry
    • Ron
    • Hermione

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.

© Wellesley College Computer Science Staff. This work is licensed under a Creative Commons License. Date Modified: Sunday, 28-Aug-2016 13:06:35 EDT