I had occasion the other day to mess around with a particular web page. The page was designed to provide an overview of a particular topic (essentially a list of high-level headers) and that had detail sections that were hidden. The user had to click on a “more” button on a particular header to show its individual detail section. Not only that, but should a detail section be shown, the “more” button was changed to a “less” button, so that the user got a hint that he could close that particular detail section (at which point, the “less” button became a “more” button once more).
That basic scenario was relatively simple to implement. Essentially, using jQuery, I identified the buttons, attached handlers to their click events, and in these handlers, dropped down the details section and changed the button to a “less” image. Once the button was a “less” button, the event handler did the opposite: hide the details section and replace the image once again to a “more” image. As I said, not too difficult and the page worked pretty well.
(function ($) {
"use strict";
var handleSingleItemClick = function () {
var $this = $(this);
if (this.src.indexOf("more") >= 0) {
$this.attr("src", "less.png");
$this.parent().next().stop().show();
}
else {
$this.attr("src", "more.png");
$this.parent().next().stop().hide();
}
},
bindEvents = function () {
$(".moreless")
.click(handleSingleItemClick);
};
bindEvents();
}(jQuery));
The next step, in and of itself, wasn’t too difficult either: have special mouse hover images for the more/less buttons. So if the user moves the mouse over a button, the image changes in some way to show that it is “active” (I used a pink version of the original button). If the user moves the mouse off, the image changes back. So in essence, I had to have four separate images: normal “more” button, hover “more” button, normal “less” button, and hover “less” button. Sounds simple enough: using jQuery again, I could attach handlers to the mouseenter and mouseleave events and switch the images then.
(function ($) {
"use strict";
var handleSingleItemClick = function () {
var $this = $(this);
if (this.src.indexOf("more") >= 0) {
$this.attr("src", "lessHover.png");
$this.parent().next().stop().show();
}
else {
$this.attr("src", "moreHover.png");
$this.parent().next().stop().hide();
}
},
makeImageHandler = function (moreImg, lessImg) {
return function () {
if (this.src.indexOf("more") >= 0) {
this.src = moreImg;
}
else {
this.src = lessImg;
}
};
},
bindEvents = function () {
$(".moreless")
.mouseenter(makeImageHandler("morehover.png", "lesshover.png"))
.mouseleave(makeImageHandler("more.png", "less.png"))
.click(handleSingleItemClick);
};
bindEvents();
}(jQuery));
Case solved? Almost…
The problem was that the first time the user moused over a button there was a small, yet noticeable delay. Why? Well, the browser had to go fetch the hover image from the server in order to display it. The normal button image was pre-fetched (as it were) since it was explicitly named in the HTML. The hover image was only needed when the mouse cursor entered the image and that triggered the handler which then set the hover image, causing the browser to go fetch the image from the server. A small but noticeable delay, as I said. So how could I get the page to have all its images pre-loaded?
It seemed a little silly to define some special markup that referenced the image files: I’d have to make sure that those elements were hidden or off the page in some way. Instead I decided to write a little function that did the pre-load of the images required.
(function ($) {
"use strict";
var
// code as before //
imageCache = [],
preloadImages = function () {
var imageList = ["more.png", "morehover.png",
"less.png", "lesshover.png"],
image;
for (var i = imageList.length - 1; i >= 0; i--) {
image = new Image();
image.src = imageList[i];
imageCache.push(image);
}
};
preloadImages();
bindEvents();
}(jQuery));
The reason this works is that the Image
class is a DOM class, not just a simple JavaScript class. Setting the src
property on a newly instantiated Image object will cause the DOM to signal that the file is needed and the browser to go ahead and fetch it. Once created, the image object can’t then be thrown away: the garbage collector may free it and the browser may decide that the cached image is no longer needed. No problem: I just created a cache array of these images that would hang around for the life of the page. You can play around with the finished page here.
And that solved the problem of the slight image load delay.
2 Responses
#1 Chee Meng said...
21-Oct-13 4:44 AMJulian, why not use CSS instead?
With CSS you could make the button swap to a different image with the :hover selector.
While you're at it, you could also make use of CSS sprites. That way, all 4 scenarios would use the same image (it will be automatically 'preloaded', in a sense).
This also helps you better separate your UI and code logic, where the Javascript need not know exactly what image is being used, the path, etc.
Just my 2 cents :)
#2 julian m bucknall said...
21-Oct-13 7:38 AMChee Meng: Ah ha! I was wondering who'd point that out :). You are correct, with the
:hover
pseudo-class I could have changed the image via CSS to show a special "hover" version. Since the image file would have been "declared" in the CSS, the browser would have downloaded it automatically. And, yes, at that point, it would make sense to use sprites.Still doesn't help with the "click then change image" problem, though. That has to be done via JavaScript.
Then again, this post is not particularly about changing images via hover; it's more about how to pre-load images. (It also -- if the reader reads the code, that is -- shows an example of using a function that returns a function and the requirement for closures, although I don't point that out.)
Cheers, Julian
Leave a response
Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.
_emphasis_
**strong**
[text](url)
`IEnumerable`
* an item
1. an item
> Now is the time...
Preview of response