Annotations Tutorial: Adding user interactivity with jQuery

In this part of the per-paragraph commenting tutorial series, we are going to add the first bits of interactivity using Javascript to what we have acheived so far using purely CSS3 effects on HTML. The annotations must become visible when the user clicks on their bubbles, and not before.


Now that the basic layout and how things should look like in collapsed state and expanded states have been visualized, we now move on to adding interactivity to them. In this section, we will add some user interactivity to the annotations - By default, the annotations must not be visible, but their speech buttons must be visible near the paragraph they are associated with, so that we have something we could click on. When the user clicks on the annotation button, the annotations become visible. Only the annotations corresponding to the content being clicked on should be visible, while safely hiding the rest.

Okay? Okay!

User interactivity in per-paragraph commenting app

We could start off from readymade jQuery solutions, like the accordion, but spending few moments fiddling with it, I surmised that it would be better if we wrote one for ourselves which could handle both horizontal and vertical rolling out of our annotations.

When I had originally started to develop this part of the per-paragraph annotations, I was a toddler with vanilla javascript and knew much too little of jQuery. Various people on discussion forums advices practicing vanilla JS and to get comfortable with it before getting to use jQuery for jQuery made things… a little too easy. So, I did make the entire thing in vanilla JS as much as possible, but couldn't help cheating when it came to Ajax calls. I did manage to do it (and the first version of annotations on PirateLearner runs on that code as of April 2015, so in case you are interested, you can look into that), but while doing so, I realized that I was reinventing the wheel, a lot. So, now I bring in the knowledge of the entire wheel, jQuery. We'll start to work on where we left off last time, using the same code pen.

We're going to rely more on CSS3 transitions rather than Jquery DOM Manipulation and animation effects to achieve this. This is primarily because CSS3 transitions are GPU friendly while jQuery is CPU intensive. Also, GPU acceleration is cool! So, our devices handle the previous one better. (Also that I haven't quite explored jQuery animations. There I said it!)

So, here is what we will do. When the page loads, we will bind the click event on each of the annotation buttons for the function which must unroll or rollback the annotations for that respective content. Right now, we don't need to worry about which annotation belongs to which content, since they are cleanly laid out in our markup as siblings to a unique parent (which has a unique data-section-id).

In the registered function, we need to find the parent, and then unhide the sibling with class name ' comment '. The CSS transition property will be used to animate the effect. Simple? But first, let us hide the annotations by default.

Add class ' hidden ' to comments-container . We add this class rather than adding a display:none style because toggling class is much simpler and does not interfere with the behaviour of the other classes.

Now, let's get scripting:

$(document).ready(function(){
/* Find all annotation toggle buttons and bind the click event to them. */
$('.comments--toggle').on('click', toggleAnnotations );
});

Define the toggleAnnotations method:

var toggleAnnotations = function(e){ /* find the parent with classname 'comments' */ $(this).parent('.comments').children('.comments-container').toggleClass('hidden'); };

Click on the buttons to see. See, simple, ain't it?

Okay, onward! On medium width desktop screens, the article shifts even before we've clicked the annotations bubble. For this one too, it makes more sense to keep that effect in a different stylesheet class rather than manipulating the style element of the commentable-container . Why? Because the effect is supposed to come into play only for a range of screen widths (for medium screens that is), and is not universal.

So, to the overall container with id 'commentable-container', we'll want a class which acts as a flag whether the annotations are visible or not. Lets name it annotations-active. It applies on the article section.

&.annotations-active{
@media(min-width: $screen-md) and (max-width: $screen-lg){
transform: translate(-25rem,0);
transition: all 1s;        
}
}

And update our script function toggleAnnotations() by adding the following:

/* Add class annotations-active to the parent with ID commentable-container */
$(this).parents("#commentable-container").toggleClass('annotations-active');

Note the use of parent() and parents() methods. While parent would return only the immediate parent, parents()(will traverse back the entire DOM tree).

Everything toggles just fine, unless we move from one annotation to another. In that case, the previous annotation stays put, and the next one also becomes visible. This is not the desired behaviour. The toggle class is to blame. Now, let us introspect a little. What do we want?

When a user clicks on an annotation box, its respective annotations must unhide, AND the rest of them must hide. On clicking on it again, the annotations must hide again. Also, on clicking on any other annotation box, the current annotations must hide (which is essentially the same as our first line). As a cherry on top, we would want that when a particular annotation is active, its bubble has an active color.

At this point, I'd rather use a state variable than any other logic. So, I'll introduce a variable which will contain the ID of the content for which annotations are currently active. It would also be a good time to enclose all our methods and variables into a single object to avoid writing into someone else's global variables.

var annotations = {};
annotations.currentAnnotation = 0;
annotations.toggleAnnotations = function(e){...};

So anytime an annotation goes active, we update this variable with the ID of its parent container, deriving it from the data-section-id .

Here's our new function:

annotations.toggleAnnotations = function(e){
/* 
* If the clicked annotation is the same is the one that was active, we need to close its annotations only,
* otherwise we must close it and open the currently active's annotations.
*/
activeID = parseInt($(this).parents('.annotation--container').attr('data-section-id'));
/* Hide all other annotations first */
$('.comments').children('.comments-container').addClass('hidden');
/* If we clicked on the same bubble, it must collapse, and we return to initial state */
if(annotations.currentAnnotation === activeID ){
/* Remove the higlight class from the bubble: .annotation-highlight */
$(this).removeClass('annotation-highlight');
$(this).parents("#commentable-container").removeClass('annotations-active');
annotations.currentAnnotation = 0;
return;
}
/* Else, we've selected a new bubble (and have already hidden everything.)*/
/* Update state variable */
annotations.currentAnnotation = parseInt($(this).parents('.annotation--container').attr('data-section-id'));
console.log('Current Annotation is: ' + annotations.currentAnnotation);
/* find the parent with classname 'comments' */
$(this).addClass('annotation-highlight');
$(this).parent('.comments').children('.comments-container').removeClass('hidden');
/* Add class annotations-active to the parent with ID commentable-container */
if(!$(this).parents("#commentable-container").hasClass('annotations-active')){
$(this).parents("#commentable-container").addClass('annotations-active');
}
};

We'll explain it tomorrow (tomorrow never comes, read it once; if something is unclear, let me know. I'll redo it.). But there is one issue. When we click on a bubble, we desire that it must stay permanently ON until we click it off. This doesn't seem to be happening. Umm, hmm. A little look at the Element Inspector reveals that the style is being overridden by other styles. Wait, but why?

The theory says, that other rules are more specific to the element. We'll get to it in a moment. But we could always quick fix it first by the sleight of hand? We'll just make the rule more focused on what we are trying to do, by moving the ' annotation-highlight ' class into ' comments-toggle '. This also needs another poor change. Now every time we choose another annotation bubble, the first one must fade away. Since we're not storing the previous object in our state, but just its ID, it would be at least 3 lines of code to find the parent with that ID, and then removing the class from its sibling which has the class ' comments-toggle '. Rather, we've resorted to a quick shortcut - remove class from all of the objects. Since we know, as a matter of design that at an instant, only one object will have that class, the effect will apply to only one (though jQuery is supposedly doing it for all, which is much wasted effort). Here's what I think is the bad code which does the job:

$('.comments--toggle').removeClass('annotation-highlight');

Let's also write that other option by stopping being lazy for a moment:

$('*[data-section-id="'+annotations.currentAnnotation+'"]').find('.comments--toggle').removeClass('annotation-highlight');

I said it was three lines and it is just one (though they actually are three lines, we've chained them together. :) ) The completed page is shown below:

See the Pen Annotations Tutorial by Anshul Thakur (@anshulthakur) on CodePen.

That does it! We're finished with this leg of the tutorial series. One step closer towards our per-paragraph commenting app. In the next tutorial, we will look into how annotations can be dynamically loaded on the page and associated with their parent content blocks, paving our way to loading the annotatons through Ajax calls.