Annotations Tutorial: User Form and annotation sorting

In this part of our per-paragraph commenting annotations app, we create a dynamic form to post annotations on any paragraph and add some customization to the visibility of these annotations.


In the previous tutorial, we finally loaded the annotations (from fixtures albeit) and associated them with their respective content blocks. Thus, if data starts coming from the backend now, we can pretty much handle it provided the interface specifications are strictly adhered to. Now, we want to finish some unfinished tasks.

Bucket Lists(Image Credits)

So far, the user has been able to only see what has been made already. There has been no means to add one's own annotations to the page. In this post, we take care of that lack by creating an annotation form.

Also, so far, we've just seen that there are annotations. We haven't been able to see who made them (even though we put the user information in the fixtures in the last tutorial). So, we'd like to rectify that too.

As another personalization to annotations, we'll by default neatly tuck in the annotations from other people into a small tab. The reason for doing so lies in the ends we are trying to achieve. Annotations are like the scribblings done on the margins of a book. I wouldn't like it if someone else scribbled in my books. The sense of ownership is lost. Similarly, annotations are supposed to be a personal thing. So, by default we must not be seeing other's annotations even if they don't mind us seeing theirs, just like they won't mind us reading their books. So, if you deliberately want to see what others might have said or thought about a particular para, you can look by expanding the tab with a single click, but not before that.

  1. Add form to page and test posting.
  2. Add user information fetched in JSON.
  3. Hide annotations from other people

So, let's get started.

We would like to see the users image and their name before their annotations. So, for prototyping, we'll reuse the template from this previoustutorial.

See the Pen Annotations (Rolled out state) by Anshul Thakur (@anshulthakur) on CodePen.

Our markup of a comment would change. What was just a span element once now becomes the following:

  <div class="comments-container-item">
  <div class="comments-container--media">
  <img src="images/male.png"/>
  </div>
  <div class="comments-container--block">
  <a href="#"><span class="comments-author-name">John Doe</span></a>
  <span class="comments-container--text">Lorem Ipsum </span>
  </div>
  </div>

Also, the CSS (Sass in our case) will also need updation:

  .comments-container-item{
    padding-top: 10px;
    margin-bottom: 10px; 
    
    .comments-container--media{
      position: relative;
      float: left;
      width: 32px;
      height: 32px;
      margin: 0;
      border: 1px solid $image-borders;
      img{
        width:100%;
        height:100%;
        overflow: hidden;
        margin:0;
        padding: 0;
        border: none;
      }
    }
    .comments-container--block{
      padding-left: 40px;
      position: relative;
      padding-right: 5px;
      padding-top: 5px;
      a {
        color: $link-default;
        text-decoration: none;
      }
      .comments-author-name{
        position: relative;
        font-size: 1.3rem;
        font-style: none;
        text-decoration: none;
      }
      .comments-container--text{
        display: block;
        position: relative;
        font-size: 1.2rem;
      }
    }
    &:first-child{
    padding-top: 5px;
    }
  }

Note that we've kept a bottom margin of 10px. This is because we might want to add some additional controls at the bottom of each annotation later, for example, a 'reply' option, or 'flag as inappropriate'. So, better keep the space ready now.

Most other things are more or less obvious. We've used a fixed size image as avatar pic, and it is floated to the left. The content block makes way for it by leaving the necessary padding.

One issue that cropped up was in giving the avatar a hyperlink to the profile, like the user name had. It did not work. Laziness took the best of me there, and I did not prod further, I was just putting it there as an extra link. Most likely, that is being caused due to the z-index values conflicts. So, if need be, we'll look into it later.

Also, we would like to tuck in other people's annotations. So, why not make that container now itself?

  <div class="comments-container-bucket hidden">
  <div class="comments-container-item">
  <div class="comments-container--media">
  <img src="images/female.png"/>
  </div>
  <div class="comments-container--block">
  <a href="#"><span class="comments-author-name">John Doe</span></a>
  <span class="comments-container--text">Your documentation source should be written as regular Markdown files, 
  and placed in a directory somewhere in your project. 
  Normally this directory will be named docs and will exist at the top level of your project, 
  alongside the mkdocs.yml configuration file. </span>
  </div>
  </div>
  </div>

And its styles:

  .comments-bucket-toggle{
    display: block;
    position: relative;
    width: 100%;
    padding: 2px;
    background-color: $btn-default-color;
    border: 1px solid $button-border-color;
    @include border-radius(2px, 2px);
    font-size: 1rem;
    text-align: center;
    &.folded:before{
    	content:"Load";
    }
    &.unfolded:before{
    	content: "Hide";
    }
    }
    .comments-container-bucket{
    	position: relative;    
  }

Yep, there isn't much to style the bucket, but only its button.

Lastly, the form to post an annotation:

  <div class="comments-form-block">
  <form class="comments-form" id="annotation_form">
  <textarea id="comments-form-text" class="comments-form--user-input" 
  placeholder="Make a note" form="annotation_form" cols="40" rows="3" maxlength="500"></textarea>
  <button class="comments-submit" id="comments-form-submit" type="submit">Submit</button>
  </form>
  </div>

Styles:

  .comments-form-block{
    position: relative;
    display:block;
    .comments-form{
    position: relative;
    .comments-form--user-input{
        position: relative;
        display: block;
        @include border-radius(2px,2px);
        border: 1px solid $image-borders;
        font-size: 1.2rem;
      }
      .comments-submit{
        position: relative;
        display:inline-block;
        text-align: center;
        margin-top: 2px;
        vertical-align: middle;
        cursor: pointer;
        font-size: 1.2rem;
        line-height: 1.7;
        @include border-radius(4px, 4px);
        background-color: $btn-primary-color;
        border: 1px solid lighten($btn-primary-color, 10%);
      }
    }
  }

Now that our basic markup is done, we'd like to get to the scripting part. There are three things we'd want to do right now.

  1. If the user clicks on 'Load other annotations', other annotations must become visible.
  2. If the user writes something in the annotation and clicks 'Post', it must add as annotation.
  3. Each content where annotation can be created must get the form to post on when the user clicks there.

But first, we have to move our form out of the markup, and into our script, since we don't have these elements on our most recent page.

This will need little changes:

In the method renderAnnotations() , we now write:

  /* Find the annotations container inside that and append the comment */
  currentComment = $('<div class="comments-container-item">'+
  		'<div class="comments-container--media">'+
  		'<img class="comments-author-image" src=""/>'+
  		'</div>'+
  		'<div class="comments-container--block">'+
 		 '<a class="comments-author-link" href="#"><span class="comments-author-name"></span></a>'+
  		'<span class="comments-container--text"></span>'+
  		'</div>'+
  		'</div>');
  		/* Create the annotation */
	currentComment.find('.comments-author-image').attr('src', data[i]['user']['user_gravatar']);
  	currentComment.find('.comments-author-name').text(data[i]['user']['user_name']);
  	currentComment.find('.comments-author-link').attr('href',data[i]['user']['user_url']);
  	currentComment.find('.comments-container--text').text(data[i]['annotation_body']);
  if(parseInt(data[i]['user']['user_id']) === parseInt(annotations.currentUser['user_id'])){
  /* Append to main visible list*/
  currentObject.find('[id *="user_annotations_"]').append(currentComment);
  }
  else{
  /* Append to the folded list */
  currentObject.find('[id *="other_annotations_"]').append(currentComment);
  /* Now that we have other annotations, unhide the show button too */
  currentObject.find('.comments-bucket-toggle').show();
  /* Bind event to show fold or unfold other annotations*/
  currentObject.find('.comments-bucket-toggle').on('click', toggleOtherAnnotations);
  if(!currentObject.find('.comments-bucket-toggle').hasClass('unfolded')){
  currentObject.find('.comments-bucket-toggle').removeClass('unfolded');
  currentObject.find('.comments-bucket-toggle').addClass('folded');
  }
  }

instead of simply appending a span element. We've made one quick change in the html markup here. Now, even the user annotations are wrapped in a bucket, and both buckets have been given unique IDs. This is to ease our appending process. We wanted to do that personalization thing, remember?

Second, we're adding attributes to the 'Author' of the annotations now. If there are annotations created on that part by other users, we add their comments to the ' other_annotations ' bucket and also unhide our 'Show annotations' button. Otherwise, it stays hidden.

Okay. So, now that the user can make annotations, we will assume that the user information is present on the page right now as:

annotations.currentUser = {
user_id: "1",
user_name: "John Doe",
user_gravatar: "images/male.png",
user_url: "#",
};

Now, write the method to handle what happens on click as:

var postAnnotation = function(e){
/* Construct a JSON string of the data in annotation. */
/* First find the parent's para-id */
id = parseInt($(this).parents(".annotation--container").attr("data-section"));
console.log("Para ID: "+ id);
/* Current text must not be empty. Though this must be taken care of in HTML5 required flag*/
body = $("#comments-form-text").text();
console.log(body);
if(body === ''){
console.log('Error. Body has no content.')
}
/* create a fixture */
content = [
{
content_id: "1",
paragraph_id: id,
annotation_body: body,
user : annotations.currentUser,
}
];
/* Return object */
renderAnnotations(content);
}

Here, we're reusing our renderAnnotations method (that is what we had made it for, right?). The server is expected to return a list of annotation objects. When the user posts, only a single annotation would be returned. So, the content fixture is essentially that object.

Now, bind it to the click button:

$("#comments-form-submit").on('click', postAnnotation);

To show or hide other annotations for each block, add the following:

var toggleOtherAnnotations = function(e){
if($(this).hasClass('folded')){
/* Unfold the annotations */
$(this).removeClass('folded').addClass('unfolded')
$(this).next().removeClass('hidden');
}
else{
/* Fold the annotations */
$(this).removeClass('unfolded').addClass('folded')
$(this).next().addClass('hidden');                        
}
}

So far, we haven't yet shown the form to anyone, lets write the routines for that:

/**
* showForm
* 
* @brief Attaches the form to the current content block
*/
var showForm = function(id){
console.log('Show form in '+id);
/* Clear out form contents, if any. */
annotations.formElement.find($('#comments-form-text')).val('');
/* Find the current block, detach the form from previous and prepend here */
annotations.formElement.find("#comments-form-submit").unbind('click', postAnnotation);
annotations.formElement.detach();
$('*[data-section-id="'+id+'"]').find('.comments-container').prepend(annotations.formElement);        
/*
* bind the postAnnotation to the form button.
*/
annotations.formElement.find("#comments-form-submit").on('click', postAnnotation);
};
var hideForm = function(){
console.log('Hide Form');
annotations.formElement = $('.comments-form-block');
/*
* unbind event
*/
annotations.formElement.find("#comments-form-submit").unbind('click', postAnnotation);
annotations.formElement.detach();
};

Only a single instance of this form exists on the page. Once loaded, it will be shuttled between blocks.

We can invoke the show form when we click on the annotation bubble, and it will be hidden if we chose a different annotation, click elsewhere on the screen, or click on the annotation bubble again.

Something is not right. Our form disappears after making a few annotations. The reason is that once attached, the jQuery object has changed, and we are again and again referencing the same variable without updating it.

So, lets fix that.

var showForm = function(id){
console.log('Show form in '+id);
console.log($('.comments-form-block'));
if($('.comments-form-block').length != 0){
/* The form has been attached somewhere. */
/* Clear out form contents, if any. */
console.log('Attached elsewhere');
annotations.formElement = $('.comments-form-block'); 
annotations.formElement.find($('#comments-form-text')).val('');
/* Find the current block, detach the form from previous and prepend here */
annotations.formElement.find("#comments-form-submit").unbind('click', postAnnotation);
annotations.formElement.detach();
}
/* Else it is pointing to the object not yet appended */
$('*[data-section-id="'+id+'"]').find('.comments-container').prepend(annotations.formElement);        
/*
* bind the postAnnotation to the form button.
*/
annotations.formElement.find("#comments-form-submit").on('click', postAnnotation);
//                console.log('Printing');
//                console.log($('.comments-form-block'));
};
var hideForm = function(){
console.log('Hide Form');
if($('.comments-form-block').length >0){
annotations.formElement = $('.comments-form-block');
/*
* unbind event
*/
annotations.formElement.find("#comments-form-submit").unbind('click', postAnnotation);
annotations.formElement.detach();                        
}
};

Okay, so, we'll detach only once if the form is found on the document. Also, before rendering the form, we'll check if it exists on some other node before.

That does it. How does the page look now? Mine looks like the one in this pen below:

In the last leg of this frontend design, we will deal with orphaned annotations, in case the content block on which user made an annotation is deleted or thoroughly updated (that is replaced by another which would be given a different ID).