Creating Flickr-Style Photo Tagging With jQuery

Posted February 2, 2010 at 2:38 PM

Tags: Javascript / DHTML

Lately, I've been experimenting with a lot mouse-move event-binding, which has led to some really cool internal stuff here at work. Building on top of some of that recent learning, this morning, I wanted to see if I could create a Flickr-style photo tagging effect using jQuery. I've never actually used the Flickr-photo tagging system, so the actually experience here is based on a lose assumption; but, I've seen the white boxes that it eventually creates, so that's where I went with this code.

 
 
 
 
 
 
 
 
 
 

In this Flickr-style effect, we start off with an image on the page. The user can then draw boxes on top of portions of that image and then leave a note as to the relevance of said box (ex. "The lighting here is amazing!"). In addition to tagging a photo the user can also mouse over existing tags to see the notes left by other users. To keep things simple for this demo, I have concentrated purely on the Javascript, leaving out all server-side code and persistence mechanisms - that can always be discussed in a follow-up post.

The HTML for this page is very basic as most of the magic happens in the Javascript files:

 Launch code in new window » Download code as text file »

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>Flickr-Style Photo Tagging With jQuery</title>
  • <style type="text/css">
  •  
  • #photo-container {
  • position: relative ;
  • }
  •  
  • #photo-container img {
  • display: block ;
  • }
  •  
  • a.tag {
  • border: 1px solid #FFFFFF ;
  • height: 1px ;
  • position: absolute ;
  • width: 1px ;
  • z-index: 100 ;
  • }
  •  
  • a.selected-tag {
  • border-color: #FFFFFF ;
  • z-index: 200 ;
  • }
  •  
  • div.hide-tags a.tag {
  • display: none ;
  • }
  •  
  • div.tag-message {
  • background-color: #212121 ;
  • border: 1px solid #000000 ;
  • color: #F0F0F0 ;
  • display: none ;
  • font-family: verdana ;
  • font-size: 12px ;
  • padding: 5px 10px 5px 10px ;
  • position: absolute ;
  • white-space: nowrap ;
  • z-index: 200 ;
  • }
  •  
  • </style>
  • <script type="text/javascript" src="../jquery-1.4.js"></script>
  • <script type="text/javascript" src="./tagging.js"></script>
  • </head>
  • <body>
  •  
  • <h1>
  • Flickr-Style Photo Tagging With jQuery
  • </h1>
  •  
  • <div id="photo-container" class="hide-tags">
  • <img src="./sexy.jpg" width="520" height="347" />
  • </div>
  •  
  • </body>
  • </html>

As you can see, there is not much more than a containing DIV and an IMG tag. The containing DIV is important because the user-created tags will need to be absolutely positioned above the image. By using a container DIV, we can create a locally-positioned element that can act as the offset parent to the tag elements.

With these elements in place, the Javascript has to then bind a few key event listeners. On the Container, we need to listen for:

HoverIn (MouseEnter): When the user mouses into the container, we want to show the existing tags.

HoverOut (MouseLeave): When the user mouses out of the container, we want to hide the existing tags (so that the user may view the unadulterated image).

MouseDown+CTRL: When the user presses the mouse button, we want to start drawing a tag. For convenience, I have mandated that the user must hold down the CTRL key when first pressing the mouse button; I felt that this would prevent the creation of unwanted tags.

MouseUp: When the user mouses up, any pending tag (currently being drawn) needs to be finalized.

MouseMove: When the user moves their mouse, any pending tag (currently being drawn) has to be resized or have its position translated.

Because mouse-move events are fired so often, I only bind the mouse-move event listener when the user has pressed the mouse button. In this way, we know that the mouse-move event listener is only doing the heavy lifting when its role is relevant (when a new tag is being created). Similarly, when the user releases the mouse button, I am unbinding the mouse-move event listener.

To get around having to keep a reference to the event handlers themselves, I am using event name spaces to bind and unbind event handlers. Doing this allows me to make sweeping unbind() calls without having to worry about breaking any additional event bindings created outside the scope of this effect (ie. bindings not created by this process).

In addition to the container element, event listeners need to be placed on the tags as well. In this particular approach, I have chosen to explicitly bind tag-based event listeners rather than using a "live"-bind approach. Due to the large number of relevant events, the explicit nature of direct event binding just helped me keep things clear in my head. If you wanted to, there's no reason a live-binding couldn't be used.

The tags only need to know about two events:

MouseOver: When the user mouses over a given tag, I need to show the associated note as well as fade out all other tags.

MouseOut: When the user mouses out of a given tag, I need to hide the current note and bring all the tags back to their default state.

Ok, now that you see where we're going with this code, let's take a look at the Javascript:

 Launch code in new window » Download code as text file »

  • // Create a self-executing function that will pass in a
  • // window object wrapper in a jquery container as well as
  • // the jQuery short-cut.
  • //
  • // NOTE: This will change all future references to the
  • // "window" object made in this scope.
  • ;(function( window, $ ){
  •  
  • // When the DOM is ready, initialize the page.
  • $(function(){
  •  
  • // Get references to our dom elements.
  • var container = $( "#photo-container" );
  •  
  • // Get a reference to our image being tagged.
  • var image = container.children( "img" );
  •  
  • // Get our message object (which we will add to the
  • // container).
  • var message = $( "<div class='tag-message'></div>" );
  •  
  • // I am the collection of tags added to this photo.
  • var tags = $( [] );
  •  
  • // I am the pending tag - I am the one currently being
  • // drawn by the user.
  • var pendingTag = null;
  •  
  •  
  • // Resize the container to be the dimensions of the
  • // image so that we don't have any mouse confusion.
  • container.width( image.width() );
  • container.height( image.height() );
  •  
  • // Add the message to the contianer.
  • container.append( message );
  •  
  •  
  • // I get the contianer-local top / left coordiantes
  • // of the current mouse position based on the given page-
  • // level X,Y coordinates.
  • var getLocalPosition = function( mouseX, mouseY ){
  • // Get the current position of the container.
  • var containerOffset = container.offset();
  •  
  • // Adjust the client coordiates to acocunt for
  • // the offset of the page and the position of the
  • // container.
  • var localPosition = {
  • left: Math.floor(
  • mouseX - containerOffset.left + window.scrollLeft()
  • ),
  • top: Math.floor(
  • mouseY - containerOffset.top + window.scrollTop()
  • )
  • };
  •  
  • // Return the local position of the mouse.
  • return( localPosition );
  • };
  •  
  •  
  • // I add a pending tag at the given position and store it
  • // as the global pending tag.
  • var addPendingTag = function( mouseX, mouseY ){
  • // Get the local position of the mouse.
  • var localPosition = getLocalPosition( mouseX, mouseY );
  •  
  • // Create the new tag.
  • var tag = $( "<a class='tag selected-tag'><br /></a>" );
  •  
  • // Set the absolute positon (within the container).
  • tag.css({
  • left: (localPosition.left + "px"),
  • top: (localPosition.top + "px")
  • });
  •  
  • // Set the anchor points for the tag. This is the
  • // point from which the drawing will be made
  • // (regardless of technical position).
  • tag.data({
  • anchorLeft: localPosition.left,
  • anchorTop: localPosition.top
  • });
  •  
  • // Set it as the pending tag.
  • pendingTag = tag;
  •  
  • // Add it to the container.
  • container.append( pendingTag );
  •  
  • // Return the new tag.
  • return( pendingTag );
  • };
  •  
  •  
  • // I resize the pending tag based on the given mouse
  • // position.
  • var resizePendingTag = function( mouseX, mouseY ){
  • // Get the local position of the mouse.
  • var localPosition = getLocalPosition( mouseX, mouseY );
  •  
  • // Get the current anchor position of the tag.
  • var anchorLeft = pendingTag.data( "anchorLeft" );
  • var anchorTop = pendingTag.data( "anchorTop" );
  •  
  • // Get the height and width of the pending tag based
  • // on its current position plus the position of the
  • // mouse.We're going to allow bi-directional drawing.
  • var width = Math.abs(
  • (localPosition.left - anchorLeft)
  • );
  •  
  • var height = Math.abs(
  • (localPosition.top - anchorTop)
  • );
  •  
  • // Set the dimensions of the tag.
  • pendingTag.width( Math.max( width, 1 ) );
  • pendingTag.height( Math.max( height, 1 ) );
  •  
  • // Check to see if the mouse position is greater
  • // than the original anchor position, the move the
  • // tag (this will give us the bi-directional re-size
  • // illusion).
  •  
  • // Check left.
  • if (localPosition.left < anchorLeft){
  •  
  • // Move left.
  • pendingTag.css( "left", (localPosition.left + "px") );
  •  
  • }
  •  
  • // Check top.
  • if (localPosition.top < anchorTop){
  •  
  • // Move up.
  • pendingTag.css( "top", (localPosition.top + "px") );
  •  
  • }
  • };
  •  
  •  
  • // I finalize the pending tag after the drawing has
  • // stopped.
  • var finalizePendingTag = function(){
  • // Get the tag information from the user.
  • var message = prompt( "Message:" );
  •  
  • // Check to see if the message was returned.
  • if (message){
  •  
  • // Associate the message with the tag.
  • pendingTag.data( "message", message );
  •  
  • // Remove the active tag status.
  • pendingTag.removeClass( "selected-tag" );
  •  
  • // Remove the anchor data as it will not be used
  • // again.
  • pendingTag.removeData( "anchorLeft" );
  • pendingTag.removeData( "anchorTop" );
  •  
  • // Bind the mouse over event on this tag.
  • pendingTag.bind(
  • "mouseover.tag",
  • onTagMouseOver
  • );
  •  
  • // Bind the mouse out event on this tag.
  • pendingTag.bind(
  • "mouseout.tag",
  • onTagMouseOut
  • );
  •  
  • // Add this as one of the tags.
  • tags = tags.add( pendingTag );
  •  
  • } else {
  •  
  • // No message was provided so remove the tag from
  • // the container as it is of no use.
  • pendingTag.remove();
  •  
  • }
  •  
  • // Clear the pending tag.
  • pendingTag = null;
  • };
  •  
  •  
  • // I handle the mouse over event on the tags. We are using
  • // this rather than a "live" style binding for performance.
  • var onTagMouseOver = function( event ){
  • // Check to see if there is a pending tag. If so, then
  • // return out - we don't want to mess with that.
  • if (pendingTag){
  • return;
  • }
  •  
  • // Get the current tag.
  • var tag = $( this );
  •  
  • // Get teh current position of the tag.
  • var tagPosition = tag.position();
  •  
  • // Set the tag message.
  • message.text( tag.data( "message" ) );
  •  
  • // Position and show the message.
  • message
  • .css({
  • left: (tagPosition.left + "px"),
  • top: ((tagPosition.top + tag.outerHeight() + 4) + "px")
  • })
  • .show()
  • ;
  •  
  • // Make this the selected tag.
  • tag.addClass( "selected-tag" );
  •  
  • // Dim the other tags' opacity.
  • tags.css( "opacity", .25 );
  •  
  • // Show the current tag.
  • tag.css( "opacity", 1 );
  • };
  •  
  •  
  • // I handle the mouse out event on the tags. We are
  • // using this rather than a "live" style binding for
  • // performance.
  • var onTagMouseOut = function( event ){
  • // Check to see if there is a pending tag. If so,
  • // then return out - we don't want to mess with that.
  • if (pendingTag){
  • return;
  • }
  •  
  • // Get the current tag.
  • var tag = $( this );
  •  
  • // Hide the message.
  • message.hide();
  •  
  • // Make sure to deselected tag.
  • tag.removeClass( "selected-tag" );
  •  
  • // Show all the tags.
  • tags.css( "opacity", 1 );
  • };
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Bind to the hover event on the container. When the user
  • // hovers over the container, we want to show the tags.
  • container.hover(
  • function(){
  • // Show the tags be removing the "hide" class.
  • container.removeClass( "hide-tags" );
  • },
  • function(){
  • // Hide the tags by adding the "hide" class.
  • container.addClass( "hide-tags" );
  • }
  • );
  •  
  •  
  • // Bind to the mouse down even on the container.
  • container.mousedown(
  • function( event ){
  • // Check to see if the user currently has the CTRL
  • // key held down. We only want to start drawing a
  • // tag IF the CTRL key is down so the user doesn't
  • // start tagging the photo accidentally.
  • if (event.ctrlKey){
  •  
  • // The user is going to start drawing. Cancel
  • // the default event to make sure the browser
  • // does not try to select the IMG object.
  • event.preventDefault();
  •  
  • // Add the pending tag to the container.
  • addPendingTag( event.clientX, event.clientY );
  •  
  • // Now that we are drawing a tag, let's bind
  • // the mousemove event to the container.
  • container.bind(
  • "mousemove.tag",
  • function( event ){
  • // Resize the pending tag.
  • resizePendingTag(
  • event.clientX,
  • event.clientY
  • );
  • }
  • );
  •  
  • // Now that we have started drawing, we're
  • // going to need a way to STOP drawing. If
  • // the user mouses-up, then finalize drawing.
  • container.bind(
  • "mouseup.tag",
  • function(){
  • // Unbinde any mouse up and mouse move
  • // events related to tagging.
  • container.unbind( "mouseup.tag" );
  • container.unbind( "mousemove.tag" );
  •  
  • // Finalize the pending tag.
  • finalizePendingTag();
  • }
  • );
  •  
  • }
  • }
  • );
  •  
  • });
  •  
  • })( jQuery( window ), jQuery );

While this might seem like a lot of code (I know my code-formatter sucks for Javascript), it's only about four functions and four event bindings. When I was writing this, the trickiest thing I came across was the translation of the mouse coordinates. When a mouse-related event fires, its coordinates are relative to the screen, not to the element with the current event binding. As such, before they could be used effectively, the mouse coordinates need to be translated from the global stage down to the local stage, taking the window offset and current element's position into account.

Explorations like this are really helping me to become more comfortable with mouse-base events. Obviously, there's a lot more that you can do with this effect, including, but not limited to, server-side interaction; this was only meant as an exploration, not a completed effect. Perhaps I'll beef it up for a follow-up post. All in all, this was just a lot of fun. jQuery is so freakin' empowering, it makes me feel tingly.

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Other Searches  |  Print Page




Learning ColdFusion 9 - ColdFusion 9 tutorials, samples, examples, demos

Reader Comments

Feb 2, 2010 at 3:55 PM // reply »
1 Comments

Very cool post, I look forward to the follow up post with server side interaction. Your videos really add to these posts as well, thanks for sharing so much with the community!


Feb 2, 2010 at 6:36 PM // reply »
7,486 Comments

@John,

Glad you like; I've been getting some great feedback about the videos lately, which is awesome. Reading code is good; but, I think it only works *after* you see the big picture and I think that's where the video comes into play.


Feb 2, 2010 at 7:56 PM // reply »
16 Comments

You need to package this up as a jQuery plugin and release it!


Feb 2, 2010 at 11:29 PM // reply »
7,486 Comments

@Adam,

Alright, let me see what I can do.


Feb 4, 2010 at 10:30 AM // reply »
7,486 Comments

@Adam,

I did my best to wrap this up into an actual plugin. I created a very simple ColdFusion layer (cached photo tags without a database) and turned it all into a project:

http://www.bennadel.com/blog/1839-jQuery-Photo-Tagger-Plugin-For-Flickr-Style-Photo-Tagging.htm


Feb 20, 2010 at 5:01 PM // reply »
2 Comments

Couldn't agree more about the video's!! They absolutely help put the code into proper context, and give a point of reference before leaping into a sea of scripting... makes all the difference in the world...

And this is exceptionally cool... not just in terms of developing it into a full fledged tagging plugin, but stuff like this really helps push me into seeing the possibilities with jQuery beyond just making stuff slide in and out and change color...

Gotta echo Adam's thanks for everything you do for the development community... makes all the difference in the world for some of us!


Feb 22, 2010 at 8:16 PM // reply »
7,486 Comments

@Ryan,

Thanks a lot my man :) I'm glad that you're liking the videos and the code. jQuery is really awesome as well, and if I can pass that on in any way, my job here is done!


Post Comment  |  Ask Ben

Recent Blog Comments
Mar 11, 2010 at 4:21 PM
Amazon's Kindle eBook And Wireless Reading Device
@John, Amazon's Kindle wireless reader had a huge hurtle to overcome in order to become more than just a gimmicky gadget that would be forgotten in a couple months after its glitzy launch.Look her ... read »
Mar 11, 2010 at 3:24 PM
Ask Ben: Using jQuery To Act On A Click Event Based On The Target Element
@TripeL, Awesome :) Glad it was helpful. ... read »
Mar 11, 2010 at 3:23 PM
Ask Ben: Using jQuery To Act On A Click Event Based On The Target Element
WOW...that's what I'm looking for. The code examples are very helpful. Thanks ... read »
Mar 11, 2010 at 1:20 PM
What Is The Best Time Of Day To Workout?
Well I am glad I stick to mid afternoon / evening work outs. Interesting find! ... read »
Mar 11, 2010 at 1:13 PM
CFHTTPSession.cfc For Multi-CFHttp Requests With Maintained Session
It worked for what I needed perfectly the first try... this is huge, you have made my week! ... read »
Mar 11, 2010 at 12:54 PM
Using Appropriate Status Codes With Each API Response
I forgot to mention that using this application stack allows me to separate as much of the core/business logic into the API Library which leaves the web applications just to handle presentation layer ... read »
Mar 11, 2010 at 12:47 PM
Using Appropriate Status Codes With Each API Response
@Ben Yep, we look a lot at the available http status codes to try and find the best match to what the error is. Between the status code, headers, and/or sending back what the error was via json or x ... read »
Mar 11, 2010 at 12:40 PM
Creating An Image Zoom And Clip Effect With jQuery And ColdFusion
@Ben Nadel, Hi, thanks for answering that fast, i did a little debug... Image data: ----------- Dibujo_uno_small.jpg : 474px × 570px 72dpi Dibujo_uno_big.jpg : 1947px × 2337px 72dpi Code source ... read »