Wednesday, July 13, 2011

11:15 AM
Hey guys, have you noticed that pretty box on WordPress codex that gives us a preview about what we can see on a page? So, I haven’t seen too many blogs use this kind of feature and it is really useful for our readers, since they can just skip to the content that they are interested in and avoid wasting time. Wikipedia has a table of contents that makes it easier for readers to skip around, right?


I’m not the very first to do something like this with jQuery. But our goal in here is to develop a complete jQuery plugin, from start to finish, with options, and that is easy to customize. And, of course, something that I hope is useful to you

So, let’s rock!

STOC – Smooth Table of Contents jQuery plugin


Since there are a lot of “tocs” around the web, our plugin will be called STOC and the main features are:

  • Automatically adds the table of contents to target element
  • You can select to search just a part of you page
  • You can select what is the first heading we will have to search (h1, h2…)
  • You can select the “depth” of the search
  • SubItems are made of sublists inside parent item
  • You can select which text will display before the table (title)
  • You can select whether ol or ul to you listing
  • You can enable / disable smooth scrolling




Planning and planning – Before code, let’s think about it

The main idea is to have a jQuery plugin that generates a table of content inside the target element. To have this working we need some basic customization with these options:

  • Where to search – If the table is generated based on entire page, just a section content
  • Depth of H’s – How many “levels” of titles we will have in our search
  • Start tag – Which level of heading will be the first on set (h1, h2, h3)
  • Title if the box – What to display as box’s title
  • List type – whether to have ordered or unordered list

The hardest thing when we are doing something just for fun is to define the scope. Actually it is hard too when we have a “real” project, but when it comes to pet projects it is harder because you just can’t measure accurately what will bring you the expected revenue (fun).

So, what do I do in these cases is list anything that I could do on it, and just cut down what will take too much time and will not be so good to do.

In this case, for example I listed these features:

  • Customizable via options – I think this one was essential, so I just kept it
  • Smooth scrolling - This one I didn’t see in any other plugin / snippet. It would be good to have, so I kept it.
  • Accordion for hierarchy - I found this idea really cool, but useless, I drop it.
  • Preview of the text on hover - I’ve stolen this idea of one site but actually didn’t find it useful also, so I drop it.

So what I’ve done here is to define which of the cool features had big potential to be a waste of time. Even if I had more time to code I would never use them, just because they haven’t the expected benefit (fun x time).

Now that we now what we want to do, let’s start to code.

Basic plugin structure with options

First we need to create our file. The standard for jQuery plugins files names is jQuery.PLUGINNAME.js so, our file will be jquery.stoc.js.

We also have all our options defined above, so we need now to save a variable for each one of them, so our user can send his own parameters.

Here is our commented code:


/*
This line creates our function "wrapped" by jQuery container, so we won't have any problem with others libraries
*/
(function($){
/*
Here the standard is $.fn.PLUGINNAME. so when we call $(element).stoc() jquery will run this code
Pay attention that we pass options to our funcion, so when user defines it we can extend our plugin
*/
  $.fn.stoc = function(options) {
  //Our default options
  var defaults = {
      search: "body", //where we will search for titles
      depth: 6, //how many hN should we search
      start: 1, //which hN will be the first (and after it we go just deeper)
      stocTitle: "<h2>Contents</h2>", //what to display before our box
      listType: "ul", //could be ul or ol
      smoothScroll: 1
  };
  //let's extend our plugin with default or user options when defined
  var options = $.extend(defaults, options);
  return this.each(function() {
      //our functions here
      alert("I'm a beta tester alert box!");
  });
};
})(jQuery);

Let’s try a simple demo to see it working. Create this HTML in same folder as our plugin:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Smooth Table Of Contents jQuery plugin - DEMO</title>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
  <script type="text/javascript" src="jquery.stoc.js"></script>
  <script type="text/javascript">
      $(function(){
          $("#items").stoc();
      });
  </script>
  <style type="text/css">
      body {
          background: #fafafa url(handmadepaper.png); //via subtlepatterns
      }
          #container {
              position: relative;
              top: 50px;
              width: 960px;
             margin: 0 auto;
              padding-bottom: 20px;
          }
          #container p, #container h1, #container h2, #container h3, #container h4, #container h5 {
              font-family: "arial";
              padding: 10px 20px 0;
              margin: 0
          }
          #items {
              float: right;
              width: 260px;
              padding-bottom: 10px;
              margin:0 0 10px 20px;
              /* rgba with ie compatibility */
              background-color: transparent;
              background-color: rgba(255,255,255,0.4);
              filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#20ffffff,endColorstr=#20ffffff);
              -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#20ffffff,endColorstr=#20ffffff)";
          }
              #items ul {
                  margin: 0 0 0 20px;
                  padding: 0 0 5px;
                  list-style-type: none;
              }
                  #items ul ul {
                      font-size: 90%;
                  }
              #items ul a {
                  font-family: "arial";
                  text-decoration: none;
                  color: #c10000;
              }
                  #items ul a:hover { color: #ff0000 }
  </style>
</head>
<body id="page-1">
  <div id="container">
      <div id="items">
      </div>
      <h1>1 - Phasellus vulputate</h1>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas metus est, egestas vel aliquet at, pellentesque nec lorem. Pellentesque molestie bibendum eros, eu suscipit nisi volutpat fringilla. Vivamus fringilla nisl ut ante commodo porta. </p>
      [... lot of more lipsum text with h's here]
  </div>
</body>
</html>


If you create this file, when you load it you should see our pretty beta tester alert. So our plugin is being called (make sure that you add jquery before it, as I’ve done including api.googleapis…). if you want to overwrite any option defined before, you just have to pass it as .stoc({ OPTIONNAME: VALUE }) instead of just .stoc(). For example, to define our search just in #container, add .stoc({ search: "#container" }).

How should we select our headings?

Now we have to prepare our plugin to get all h’s that we have to (based in our options). What we can do is get all the headings we have to, and when we loop through each one of them we will discover which level it is. I think it is easier than trying to get the whole hierarchy for each “tree” of headings.

Since our current object can change as we run our code, we have also to “cache” our current object so we will always now which object we are modifying. Our code now will be:


return this.each(function() {
   //"cache" our target and search objects
   obj = $(this); //target
   src = $(options.search); //search
   //let's declare some variables. We need this var declaration to create them as local variables (not global)
   var appHTML = "", tagNumber = 0, txt = "", id = "", before = "", after = "", previous = options.start, start = options.start, depth = options.depth, i = 0, srcTags = "h" + options.start, cacheHN = "";
   //which tags we will search
   while ( depth > 1) {
       start++; //we will just get our start level and numbers higher than it
       srcTags = srcTags + ", h" + start;
       depth--; //since went one level up, our depth will go one level down
   }
 });


If you alert you srcTags you will see something like this “h1, h2, h3, h4, h5, h6?. This is what we will pass to jQuery as the elements that we want to search for.

Building our table

We have all our elements, what we have to do is run a function on each one of them with the wonderful each() jQuery function.

  • Inside each element, we need to:
  • Know which level the current element is (tagNumber)
  • Set one id to this element, if it doesn’t have one
  • Get the elements text
  • Test if is its level is lower, higher or equal than previous element and open / close ul’s based on this
  • If element number is higher than previous means that we went down one level (e.g. from h2 to h3)
  • If element number is equals to previous means that we stay on same level (e.g. h4)If element number is lower than previous means that we went up, but we don’t know how many levels (e.g. from h4 to h1)





  • Append element HTML to our target item.
We also have to correct the last item because if it is not top-level it will let some uls open, and we don’t want it :D

Our commented code now will be:

/* our setup stuff here */
/*inside our return function */
 //which tags we will search
     while ( depth > 1) {
         start++; //we will just get our start level and numbers higher than it
         srcTags = srcTags + ", h" + start;
         depth--; //since went one level up, our depth will go one level down
     }
     src.find(srcTags).each(function() {
         //we will cache our current H element
         cacheHN = $(this);
         //if we are on h1, 2, 3...
         tagNumber = ( cacheHN.get(0).tagName ).substr(1);
         //sets the needed id to the element
         id = cacheHN.attr('id');
         if (id == "") { //if it doesn't have only, of course
             id = "h" + tagNumber + "_" + i;
             cacheHN.attr('id', id);
         }
         //our current text
         txt = cacheHN.text();
         switch(true) { //with switch(true) we can do comparisons in each case
             case (tagNumber > previous) : //it means that we went down one level (e.g. from h2 to h3)
                     appHTML = appHTML + "<" + options.listType +"><li>"+ before +"<a href=\"#"+ id + "\">" + txt + "</a>";
                     previous = tagNumber;
                 break;
             case (tagNumber == previous) : //it means that stay on the same level (e.g. h3 and stay on it)
                     appHTML = appHTML + "</li><li>"+ before +"<a href=\"#"+ id + "\">" + txt +  "</a>";
                 break;
             case (tagNumber < previous) : //it means that we went up but we don't know how much levels  (e.g. from h3 to h2)
                     while(tagNumber != previous) {
                         appHTML = appHTML + "</" + options.listType +"></li>";
                         previous--;
                     }
                     appHTML = appHTML + "<li>"+ before +"<a href=\"#"+ id + "\">" + txt + "</a></li>";
                 break;
         }
         i++;
     });
     //corrects our last item, because it may have some opened ul's
     while(tagNumber != options.start) {
         appHTML = appHTML + "</" + options.listType +">";
         tagNumber--;
     }
     //append our html to our object
     appHTML = options.stocTitle + "<"+ options.listType + ">" + appHTML + "</" + options.listType + ">";
       obj.append(appHTML);

How to Make our STOC smoother

I’ve stolen CSS trick’s smooth scroll code, but I hope they don’t mind :D

What we have to do here is just put this (compressed) function to load when our smooth scroll in on (if the user doesn’t set it as 0).

/*all code above in here*/
  //append our html to our object
      appHTML = options.stocTitle + "<"+ options.listType + ">" + appHTML + "</" + options.listType + ">";
      obj.append(appHTML);
      //our pretty smooth scrolling here
      // acctually I've just compressed the code so you guys will think that I'm the man . Source: http://css-tricks.com/snippets/jquery/smooth-scrolling/
      if (options.smoothScroll == 1) {
          $(window).load(function(){
              function filterPath(string){return string.replace(/^\//,'').replace(/(index|default).[a-zA-Z]{3,4}$/,'').replace(/\/$/,'')}var locationPath=filterPath(location.pathname);var scrollElem=scrollableElement('html','body');obj.find('a[href*=#]').each(function(){var thisPath=filterPath(this.pathname)||locationPath;if(locationPath==thisPath&&(location.hostname==this.hostname||!this.hostname)&&this.hash.replace(/#/,'')){var $target=$(this.hash),target=this.hash;if(target){var targetOffset=$target.offset().top;$(this).click(function(event){event.preventDefault();$(scrollElem).animate({scrollTop:targetOffset},400,function(){location.hash=target})})}}});function scrollableElement(els){for(var i=0,argLength=arguments.length;i<argLength;i++){var el=arguments[i],$scrollElement=$(el);if($scrollElement.scrollTop()>0){return el}else{$scrollElement.scrollTop(1);var isScrollable=$scrollElement.scrollTop()>0;$scrollElement.scrollTop(0);if(isScrollable){return el}}}return[]}
          });
        }

Our final result is this:

(function($){
 $.fn.stoc = function(options) {
  //Our default options
  var defaults = {
      search: "body", //where we will search for titles
      depth: 6, //how many hN should we search
      start: 1, //which hN will be the first (and after it we go just deeper)
      stocTitle: "<h2>Contents</h2>", //what to display before our box
      listType: "ul", //could be ul or ol
      smoothScroll: 1
  };
  //let's extend our plugin with default or user options when defined
  var options = $.extend(defaults, options);
  return this.each(function() {
      //"cache" our target and search objects
      obj = $(this); //target
      src = $(options.search); //search
      //let's declare some variables. We need this var declaration to create them as local variables (not global)
      var appHTML = "", tagNumber = 0, txt = "", id = "", before = "", after = "", previous = options.start, start = options.start, depth = options.depth, i = 0, srcTags = "h" + options.start, cacheHN = "";
      //which tags we will search
      while ( depth > 1) {
          start++; //we will just get our start level and numbers higher than it
          srcTags = srcTags + ", h" + start;
          depth--; //since went one level up, our depth will go one level down
      }
      src.find(srcTags).each(function() {
          //we will cache our current H element
          cacheHN = $(this);
          //if we are on h1, 2, 3...
          tagNumber = ( cacheHN.get(0).tagName ).substr(1);
          //sets the needed id to the element
          id = cacheHN.attr('id');
          if (id == "") { //if it doesn't have only, of course
              id = "h" + tagNumber + "_" + i;
              cacheHN.attr('id', id);
          }
          //our current text
          txt = cacheHN.text();
          switch(true) { //with switch(true) we can do comparisons in each case
              case (tagNumber > previous) : //it means that we went down one level (e.g. from h2 to h3)
                      appHTML = appHTML + "<" + options.listType +"><li>"+ before +"<a href=\"#"+ id + "\">" + txt + "</a>";
                      previous = tagNumber;
                  break;
              case (tagNumber == previous) : //it means that stay on the same level (e.g. h3 and stay on it)
                      appHTML = appHTML + "</li><li>"+ before +"<a href=\"#"+ id + "\">" + txt +  "</a>";
                  break;
              case (tagNumber < previous) : //it means that we went up but we don't know how much levels  (e.g. from h3 to h2)
                      while(tagNumber != previous) {
                          appHTML = appHTML + "</" + options.listType +"></li>";
                          previous--;
                      }
                      appHTML = appHTML + "<li>"+ before +"<a href=\"#"+ id + "\">" + txt + "</a></li>";
                  break;
          }
          i++;
      });
      //corrects our last item, because it may have some opened ul's
      while(tagNumber != options.start) {
          appHTML = appHTML + "</" + options.listType +">";
          tagNumber--;
      }
      //append our html to our object
      appHTML = options.stocTitle + "<"+ options.listType + ">" + appHTML + "</" + options.listType + ">";
      obj.append(appHTML);
      //our pretty smooth scrolling here
      // acctually I've just compressed the code so you guys will think that I'm the man . Source: http://css-tricks.com/snippets/jquery/smooth-scrolling/
      if (options.smoothScroll == 1) {
          $(window).load(function(){
              function filterPath(string){return string.replace(/^\//,'').replace(/(index|default).[a-zA-Z]{3,4}$/,'').replace(/\/$/,'')}var locationPath=filterPath(location.pathname);var scrollElem=scrollableElement('html','body');obj.find('a[href*=#]').each(function(){var thisPath=filterPath(this.pathname)||locationPath;if(locationPath==thisPath&&(location.hostname==this.hostname||!this.hostname)&&this.hash.replace(/#/,'')){var $target=$(this.hash),target=this.hash;if(target){var targetOffset=$target.offset().top;$(this).click(function(event){event.preventDefault();$(scrollElem).animate({scrollTop:targetOffset},400,function(){location.hash=target})})}}});function scrollableElement(els){for(var i=0,argLength=arguments.length;i<argLength;i++){var el=arguments[i],$scrollElement=$(el);if($scrollElement.scrollTop()>0){return el}else{$scrollElement.scrollTop(1);var isScrollable=$scrollElement.scrollTop()>0;$scrollElement.scrollTop(0);if(isScrollable){return el}}}return[]}
          });
      }
  });
 };
})(jQuery);

Are you hungry yet?

What about you help me with some improvements on this plugin? Do you have any tip? Anything that you can think of a better way to do?

Do you have any other features in mind? Or even another plugin idea that you’d like to see a tutorial on?

Share your thoughts with us! 

0 comments: