kevinwebber.ca
28th of December, 2010
A personal project I’m working on uses the jQuery Fancybox library to display images. It’s a great lightbox, except for one small problem: AJAX refreshes break it.
A page I’m working on accepts multiple image uploads. Once the images are successfully processed asynchronously on the server, an image panel on the same page is automatically refreshed. The AJAX callback works fine and updates the panel view with the newly uploaded photos, but the net result is that Fancybox stops working completely.
The solution is pretty simple: Fancybox needs to be re-initialized after the view is refreshed. This involves invoking a JavaScript function after the AJAX callback completes. Fortunately, I’m using Wicket, which makes invoking JavaScript after the callback a breeze.
Here’s the (condensed version of) code for my behavior class that handles the repainting, callback, and post-callback JS invocation:
1final IBehavior repaintBehavior = new AbstractDefaultAjaxBehavior() {
2 @Override
3 protected void respond(AjaxRequestTarget target)
4 {
5 ThingModel thingModel = new ThingModel(thingId);
6 UnprocessedPhotosModel photosModel = new UnprocessedPhotosModel(getCurrentUser().getId(), tokenId);
7 List photos = photosModel.getObject();
8
9 for (Photo photo : photos)
10 {
11 photo.setThing(thingModel.getObject());
12 ServiceFactory.getPhotoService().savePhoto(photo);
13 }
14
15 target.addComponent(photosPanel);
16 target.appendJavascript("$.fancyboxResetBusy(); reinitFancybox();");
17 }
18
19 @Override
20 public void renderHead(IHeaderResponse response)
21 {
22 super.renderHead(response);
23 CharSequence callback = getCallbackScript();
24 response.renderJavascript("function uploadCompleted() { " + callback + "}", "customUploadCompleted");
25 }
26};
For those who aren’t familiar with Wicket, the renderHead method is rendering a custom JavaScript function in the view. The multiple upload Flash component I use invokes the rendered JS function after it finishes processing the images. When the Flash uploader invokes uploadCompleted, Wicket handles all the AJAX plumbing and executes the Java code in the respond method above.
The two most important lines of code are the calls to target. target.addComponent instructs Wicket to refresh the photo panel, and target.appendJavascript instructs Wicket to invoke the functions in the String parameter after the panel is refreshed.
Let’s take a look at the reinitFancybox function which gets invoked after the panel is refreshed.
1function reinitFancybox() {
2 // CSS
3 $(".gallery img, portfolio.list img, .portfolio.grid-2 div img, a.fancy, ul.screens img").css("opacity", "1");
4
5 // ON MOUSE OVER
6 $(".gallery img, .portfolio-list img, .portfolio.grid-2 div img, a.fancy, ul.screens img").hover(function () {
7 // SET OPACITY TO 100%
8 $(this).stop().animate({opacity: 0.5}, "fast");
9 },
10 // ON MOUSE OUT
11 function () {
12 // SET OPACITY BACK TO 100%
13 $(this).stop().animate({opacity: 1}, "fast");
14 });
15 // INIT FANCYBOX
16 $("li.image a, a.fancy, .portfolio.grid li a.folio-zoom, .portfolio-list.image a").fancybox({
17 'titlePosition' : 'over'
18 });
19}
Most of the code above re-initializes the CSS hover effects I use to pretty up my image panel. The last piece is responsible for re-initializing Fancybox itself. At this point, we should be done, Fancybox should work perfectly again. But there’s a small catch, the Fancybox script includes a global variable named busy which is set to true at the beginning of almost every single function. If for any reason the script is interrupted before busy is set back to false, re-initialization will always abort. Here’s a snippet of the Fancybox code below:
1$.fancybox = function(obj) {
2 if (busy) {
3 return;
4 }
5
6 busy = true;
7
8 // ...
9};
It’s pretty obvious that if busy is true Fancybox won’t initialize. We need to make sure busy is set to false before attempting to re-initialize. Unfortunately, this requires a small kludge addition to the Fancybox script itself. I’ve added the following public function:
1$.fancyboxResetBusy = function() {
2 busy = false;
3}
Invoking this before the initialize function ensures the initialization doesn’t return prematurely.