SitePoint Sponsor

User Tag List

Page 1 of 2 12 LastLast
Results 1 to 25 of 30
  1. #1
    Unobtrusively zen silver trophybronze trophy
    paul_wilkins's Avatar
    Join Date
    Jan 2007
    Location
    Christchurch, New Zealand
    Posts
    14,526
    Mentioned
    83 Post(s)
    Tagged
    3 Thread(s)

    Lightbulb JavaScript Challenge: Convert JQuery to Plain JavaScript

    In recent years, jQuery has become the de-facto JavaScript library on the web.
    It irons out many cross-browser inconsistencies and adds a welcome layer of syntactic sugar to client-side scripting.
    Yet, all too often we see jQuery being used to do something that vanilla JavaScript could do just as well, if not better.

    That's why we've decided to set you a little challenge: convert our jQuery into plain old JavaScript.

    Successful entries will be those who get things working in any browser without a single jQueryism.
    Those looking to score extra points can do so by making their JavaScript backwards compatible, so that it doesn't break in older browsers.

    The jQuery we want you to convert powers a simple tab-based navigation. The content of the tabs resides in different files which are loaded into the main page using AJAX.

    You can see the code involved below, or download the files from here.
    You can see the tab-based navigation in action here.
    The code involved is listed below.

    Entries need only include the JavaScript, but please use spoiler tags to hide the answers that you post.
    The challenge ends June 16

    For example, this:
    [code][spoiler]Like this[/spoiler][/code]

    Renders this:
    Code:
    Like this
    Please note: this won't work locally in Chrome (owing to the local AJAX requests). To get around this, please either use a different browser, a local server stack (such as xampp or easyphp), or a remote server.

    Good Luck!

    index.html
    HTML Code:
    <!DOCTYPE HTML>
    <html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Tabbed panels</title>
        <link rel="stylesheet" type="text/css" href="css/style.css">
    </head>
    
    <body>
        <h1>JS Tabs Challenge</h1>
        <p>All too often jQuery is used to do something that vanilla JavaScript can do just as well, if not better.<p>
        <p>Your challenge is to convert our jQuery into plain old JavaScript.</p>
    
        <div class="tabs">
            <ul class="nav">
                <li><a href="tab1.html">Tab 1</a></li>
                <li><a href="tab2.html">Tab 2</a></li>
                <li><a href="tab3.html">Tab 3</a></li>
            </ul>
        </div>
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script type="text/javascript" src="js/tabs.js"></script>
    </body>
    </html>
    tab1.html
    HTML Code:
    <h2>Here is the heading for Tab#1</h2>
    
    <p>Liquor ipsum dolor sit amet glenburgie pappy van winkle. Amaretto sour absolut clynelish macu stinger singapore sling tanqueray vodka sundowner, ardmore wild turkey; colombia? Cactus jack alexander gin fizz kahlua royal bermuda cocktail the last word ballantine dalwhinnie aultmore smirnoff grand marnier! </p>
    
    <p>Fish house punch brandy daisy brass monkey moonwalk. Murphy's corn n' oil dufftown agent orange kensington court special drumguish kamikaze; chopin french 75. Dufftown my fair lady. Jacquin imperial moscow mule. Van winkle family reserve sazerac; sundowner; sake manhattan jack rose kamikaze loch lomond. Remy martin lemon split mai-tai bowmore polmos krakow colorado bulldog.</p>
    tab2.html
    HTML Code:
    <h2>Here is the heading for Tab#2</h2>
    
    <p>Negroni glenturret strega el presidente dalmore miltonduff, "tom and jerry." Singapore sling teacher's oban glogg jim beam whiskey sour edradour, glen elgin. Farnell seven and seven tormore, drambuie nikolaschka blue lagoon, ardmore chopin smirnoff charro negro sloe gin balmenach alexander. </p>
    
    <p>Tom and jerry, piscola pia colada blair athol sea breeze kensington court special manhattan metaxa captain morgan. Speyside wolfram springbank greyhound; montgomery lime rickey matador churchill, churchill, ramos gin fizz harvey wallbanger deanston bloody aztec brackla. Polmos krakow kalimotxo, highland park metaxa hurricane paradise loch lomond early times?</p>
    tab3.html
    HTML Code:
    <h2>Here is the heading for Tab#3</h2>
    
    <p>Polmos krakow madras dufftown cuba libre white horse, gimlet lejon v.o. tom collins. Ruby dutchess vodka sunrise, gin and tonic lagavulin buck's fizz: four score strathmill metaxas sake bomb tanqueray. Fettercairn salty dog kremlin colonel; spanish coffee, pegu white horse crown royal kahlua, "glen ord gin pahit; van winkle family reserve?" Mai-tai, salty dog. </p>
    
    <p>Cosmopolitan glogg allt--bhainne j & b stinger balblair, fish house punch. Drambuie usher's sidecar long island iced tea paradise jameson kirsch creamsicle, brandy sour mudslide gordon's black russian tomintoul vodka sunrise." Jameson coronet vso fleischmann's bearded lady, metaxa, tomatin strathisla fettercairn gilbey's courvoisier.</p>
    style.css
    Code CSS:
    /* small reset as needed for this page only*/
    html, body{
        margin: 0;
        padding: 0;  
    }
    h1, h2, p, ul { 
        margin: 0 0 15px;
        padding: 0;
    }
    ul {
        list-style: none;
    }
    h1, h2 {
        font-size: 140%;
    }
    /*  end reset */
     
    body {
        padding: 25px;
    }
    .tabs {
        width: 600px;
        background: #f8f8f8;
        border: 1px solid #cccccc;
    }
    .nav {
        background: #E8E8E8;
        border-bottom: 1px solid #cccccc;
        float: left;
        width: 100%;
    }
    .nav li {float: left}
    .nav a {
        float: left;
        padding: 8px 15px;
        text-decoration: none;
        font-weight: bold;
        border-right: 1px solid #cccccc;
    }
    .nav,.nav a,.nav a:visited{
        color: #484848;
        background: #E8E8E8;
    }
    .nav a:hover,
    .nav a.selected { 
        background: #f8f8f8;
        position: relative;
        margin-bottom: -1px;
        padding-bottom: 9px;
    }
    .nav a.selected {
        cursor: default;
    }
    .content {
        padding: 10px 25px;
        clear: both;
        width: 550px; /* ie6/7 haslayout trip */
    }

    tabs.js
    Code JavaScript:
    $(document).ready(function () {
        $(".tabs .nav a").on("click", function (e) {
            e.preventDefault();
     
            var $tab = $(this),
                fileToLoad = $tab.attr("href"),
                $nav = $tab.parents('.nav'),
                $content = $nav.siblings('.content');
     
            // Prevent the same tab from being reloaded
            if ($tab.hasClass("selected")) {
                return false;
            }
     
            // check if section for content should be added
            if (!$content.length) {
                $content = $('<div>').addClass('content');
                $content.insertAfter($tab.parents('.nav'));
            }
     
            // change selected tab and load new content
            $nav.find('a').removeClass("selected");
            $content.fadeOut('slow', function () {
                $tab.addClass("selected");
                $(this).load(fileToLoad).fadeIn();
            });
        });
     
        $('.tabs .nav a:first').trigger('click');
    });
    Last edited by Pullo; Oct 22, 2013 at 12:43. Reason: Removed defunct links to download code and see it in action
    Programming Group Advisor
    Reference: JavaScript, Quirksmode Validate: HTML Validation, JSLint
    Car is to Carpet as Java is to JavaScript

  2. #2
    SitePoint Mentor bronze trophy
    fretburner's Avatar
    Join Date
    Apr 2013
    Location
    Brazil
    Posts
    1,256
    Mentioned
    32 Post(s)
    Tagged
    4 Thread(s)
    I've tried about 5 times now to post my code, but I keep getting a 500 error when I hit submit

    Edit: OK, well it let me post this, so I'm guessing it's got something to do with the length of the code maybe? I've put my attempt at a solution up on pastebin here: http://pastebin.com/PYyjGw0e

    It doesn't work on IE6 or 7 (but works on IE10) for some reason I can't figure out - perhaps someone else can tell me? Also, just for the record, it's not all my own code - I had to borrow snippets from here and there, but it's a great challenge!

  3. #3
    Galactic Overlord gold trophysilver trophybronze trophy
    HAWK's Avatar
    Join Date
    Aug 2003
    Location
    New Zealand
    Posts
    12,314
    Mentioned
    881 Post(s)
    Tagged
    13 Thread(s)
    Quote Originally Posted by fretburner View Post
    I've tried about 5 times now to post my code, but I keep getting a 500 error when I hit submit
    Damn it. I remember someone mentioning a bug like that in the past. I'll have to get it looked at. Thanks for working around it.

  4. #4
    SitePoint Member
    Join Date
    May 2013
    Posts
    1
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    This works in IE7,8,9 both standard and quirks, firefox, chrome, opera, and safari - hadn't tested on anything else so not sure if it works or not on other browsers/versions
    Code:
    
    		<script>
    			DoAjax = function(id){
    				
    				if(id == undefined || id == '')
    					return false;
    				
    				var a, page = 'page' + id + '.php';
    				
    				try{
    					a = new XMLHttpRequest();
    				} catch (e){
    					try{
    						a = new ActiveXObject("Msxml2.XMLHTTP");
    					} catch (e) {
    						try{
    							a = new ActiveXObject("Microsoft.XMLHTTP");
    						} catch (e){
    							alert("Guess I lost lol");
    							return false;
    						}
    					}
    				}
    				// Create a function that will receive data sent from the server
    				a.onreadystatechange = function(){
    					if(a.readyState == 4){
    						document.getElementById('content').innerHTML = a.responseText;
    					}
    				}
    				a.open("GET", "page" + id + ".php", true);
    				a.send(null); 
    			}                                            
    			
    			ClickListener = function(e,id) {
    				if(id == null)
    					var id = e.target.id;
    				else
    					var id = id;
    				
    				DoAjax(id);
    			}
    			
    			var tab1 	= document.getElementById('1');
    			var tab2 	= document.getElementById('2');
    			var tab3 	= document.getElementById('3');
    			
    			try {
    				tab1.addEventListener('click', ClickListener);
    				tab2.addEventListener('click', ClickListener);
    				tab3.addEventListener('click', ClickListener);
    			} catch(e) {	}
    			
    			//If you want to init on 1st page 
    			//DoAjax(1);
    			
    		</script>
    

  5. #5
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,095
    Mentioned
    28 Post(s)
    Tagged
    1 Thread(s)
    Here's my shot in coffeescript, I've only included the basics for modern browsers and not implemented things like fading which I would implement in CSS with transitions.
    Code:
    
    content = null
    
    load = (el, url, callback)->
      xhr = new XMLHttpRequest()
      xhr.onreadystatechange = ->
        el.innerHTML = @.responseText if @readyState == 4
        callback()
      xhr.open "GET", url, true
      xhr.send null
    
    click = (event)->
      el = event.target
      if el.href
        event.preventDefault()
        li = event.target.parentNode()
        load content, event.target.href, ->
          li.className = 'fade-in'
          setTimeout ->
            li.className = ''
          , 1000
    
    document.onreadystatechange = ->
      if document.readyState == "interactive"
        content = document.querySelector '.content'
        document.addEventListener 'click', click
    
    converted to js.
    Code:
    
    (function() {
    
    var click, content, load;
    
    content = null;
    
    load = function(el, url, callback) {
      var xhr;
      xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState === 4) {
          el.innerHTML = this.responseText;
        }
        return callback();
      };
      xhr.open("GET", url, true);
      return xhr.send(null);
    };
    
    click = function(event) {
      var el, li;
      el = event.target;
      if (el.href) {
        event.preventDefault();
        li = event.target.parentNode();
        return load(content, event.target.href, function() {
          li.className = 'fade-in';
          return setTimeout(function() {
            return li.className = '';
          }, 1000);
        });
      }
    };
    
    document.onreadystatechange = function() {
      if (document.readyState === "interactive") {
        content = document.querySelector('.content');
        return document.addEventListener('click', click);
      }
    };
    
    })();
    

  6. #6
    SitePoint Mentor bronze trophy
    fretburner's Avatar
    Join Date
    Apr 2013
    Location
    Brazil
    Posts
    1,256
    Mentioned
    32 Post(s)
    Tagged
    4 Thread(s)
    OK, here's the revised version of my code - compatible with IE6+ now, and implements all the features of the original script.
    Code:
    var TimeToFade = 1000.0;
        
    function animateFade(lastTick, eid, callback)
    {  
      var curTick = new Date().getTime();
      var elapsedTicks = curTick - lastTick;
      
      var element = document.getElementById(eid);
     
      if(element.FadeTimeLeft <= elapsedTicks)
      {
        element.style.opacity = element.FadeState == 1 ? '1' : '0';
        element.style.filter = 'alpha(opacity = ' + (element.FadeState == 1 ? '100' : '0') + ')';
        element.FadeState = element.FadeState == 1 ? 2 : -2;
        if (typeof callback == 'function') {
            callback();
        }
        return;
      }
     
      element.FadeTimeLeft -= elapsedTicks;
      var newOpVal = element.FadeTimeLeft/TimeToFade;
      if(element.FadeState == 1)
        newOpVal = 1 - newOpVal;
    
      element.style.opacity = newOpVal;
      element.style.filter = 'alpha(opacity = ' + (newOpVal*100) + ')';
      
      //setTimeout("animateFade(" + curTick + ",'" + eid + "')", 33);
      setTimeout(function(){animateFade(curTick, eid, callback)}, 33);
    }
    
    function fade(eid, callback)
    {
      var element = document.getElementById(eid);
      if(element == null)
        return;
        
       
      if(element.FadeState == null)
      {
        if(element.style.opacity == null || element.style.opacity == '' 
           || element.style.opacity == '1')
          element.FadeState = 2;
        else
          element.FadeState = -2;
      }
        
      if(element.FadeState == 1 || element.FadeState == -1)
      {
        element.FadeState = element.FadeState == 1 ? -1 : 1;
        element.FadeTimeLeft = TimeToFade - element.FadeTimeLeft;
      }
      else
      {
        element.FadeState = element.FadeState == 2 ? -1 : 1;
        element.FadeTimeLeft = TimeToFade;
        //setTimeout("animateFade(" + new Date().getTime() + ",'" + eid + "')", 33);
        setTimeout(function(){animateFade(new Date().getTime(), eid, callback)}, 33);
      }  
    }
    
    (function(window, document, undefined){
        
        if(typeof window.XMLHttpRequest === 'undefined' &&
            typeof window.ActiveXObject === 'function') {
            window.XMLHttpRequest = function() {
                try { return new ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch(e) {}
                try { return new ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch(e) {}
                return new ActiveXObject('Microsoft.XMLHTTP');
            };
        }
        
        var addEvent = function(element, evType, fn, useCapture) { 
            if (element.addEventListener) { 
                element.addEventListener (evType, fn, useCapture); 
                return true; 
            } else if (element.attachEvent) { 
                var r = element.attachEvent('on' + evType, fn); 
                return r; 
            } else { 
                element['on' + evType] = fn; 
            } 
        };
        
        var addClass = function(element, classname) {
            var cn = element.className;
            //test for existance
            if( cn.indexOf( classname ) != -1 ) {
                return;
            }
            //add a space if the element already has class
            if( cn != '' ) {
                classname = ' '+classname;
            }
            element.className = cn+classname;
        }
        
        var removeClass = function(element, classname) {
            var cn = element.className;
            var rxp = new RegExp( "\\s?\\b"+classname+"\\b", "g" );
            cn = cn.replace( rxp, '' );
            element.className = cn;
        }
        
        var hasClass = function(element, className) {
            var cn = element.className;
            //test for existance
            if( cn.indexOf( className ) != -1 ) {
                return true;
            }
            return false;
        }
        
        var getParent = function(element, className) {
            if (!element) {
                return element;
            } else if (hasClass(element, className)) {
                return element;
            } else {
                return getParent(element.parentNode, className);
            }
        };
        
        var getSibling = function(element, className) {
            var siblings = element.parentNode.children,
                j = siblings.length;
                
            for (var i=0; i < j; i++) {
                if (hasClass(siblings[i], className)) {
                    var sibling = siblings[i];
                }
            }
            
            return sibling;
        };
        
        var loadTabContent = function(url, callback) {
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function() {
                if (xhr.readyState == 4) {
                    callback(xhr.responseText);
                }
            }
            xhr.open('GET', url, true);
            xhr.send(null);
        };
        
        var loadTab = function(event) {
            event.preventDefault ? event.preventDefault() : event.returnValue = false;
            
            var $tab = event.target || event.srcElement,
                fileToLoad = $tab.getAttribute("href"),
                $nav = getParent($tab, 'nav'),
                $content = getSibling($nav, 'content');
                
            if (hasClass($tab, 'selected')) {
                return false;
            }
            
            //check if section for content should be added
            if (typeof $content == 'undefined') {
                $content = document.createElement('div');
                $content.id = 'content';
                addClass($content, 'content');
                $nav.parentNode.appendChild($content);
            }
     
            //change selected tab and load new content
            for (var i=0; i < links.length; i++) {
                removeClass(links[i], 'selected');
            }
            
            fade('content', function(){
                loadTabContent(fileToLoad, function(responseText){
                    addClass($tab, 'selected');
                    $content.innerHTML = responseText;
                    fade('content');
                })
            });
            
            
        };
        
        var lists = document.getElementsByTagName('li');
        var links = [];
    
        for (i=0; i < lists.length; i++) { 
            var link = lists[i].getElementsByTagName('a')[0];
            links.push(link);
            addEvent(link, "click", loadTab, false);
        }
    
    })(window, document);

  7. #7
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    5,313
    Mentioned
    178 Post(s)
    Tagged
    8 Thread(s)
    Great entries, guys!
    Keep them coming!

  8. #8
    SitePoint Member
    Join Date
    Feb 2013
    Posts
    14
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Some helpers for those who may be stuck.

    $(document).ready(function () {
    https://developer.mozilla.org/en-US/...MContentLoaded

    $(".tabs .nav a")
    https://developer.mozilla.org/en-US/...erySelectorAll

    .on("click", function (e) {
    https://developer.mozilla.org/en-US/...dEventListener

    $tab.attr("href")
    https://developer.mozilla.org/en-US/...t.getAttribute

    $tab.parents('.nav')
    https://developer.mozilla.org/en-US/...ode.parentNode
    Needs some kind of looping to replicate .parents()

    $nav.siblings('.content')
    https://developer.mozilla.org/en-US/...ode.childNodes
    Use with parentNode to get siblings.

    $tab.hasClass("selected")
    https://developer.mozilla.org/en-US/...ntsByClassName

    $('<div>')
    https://developer.mozilla.org/en-US/....createElement

    .addClass('content')
    https://developer.mozilla.org/en-US/...ment.className
    https://developer.mozilla.org/en-US/...ment.classList
    see classList.add and classList.remove

    $nav.find('a')
    https://developer.mozilla.org/en-US/...mentsByTagName

    .load(fileToLoad)
    https://developer.mozilla.org/en-US/...XMLHttpRequest

    .fadeIn()
    http://bavotasan.com/2011/a-simple-fade-with-css3/
    Use CSS and add/remove a class to the element with javascript className

    .trigger('click')
    https://developer.mozilla.org/en-US/....dispatchEvent

    Always check the browser compatibility at the bottom of these pages. Some of these will not work in IE8 and below, you will need to check the non-standard method which matches - there's almost always some alternative (eg addEventListener > attachEvent).

    There may be better alternatives to those which I have suggested - if you know of a better alternative please post a link and some comment about why it's better.

    In my opinion, the main complexity here is dealing with the ajax - ie the .load function in jquery.

    I admit I do write a fair bit of native javascript for libraries where I can't assume jquery is available. But if I'm working on a web app, I almost always use jquery because a) it's well known so contributors can jump right in and b) it saves a lot of time and typing because it's generally less verbose than javascript.
    Last edited by ianc.sp; May 26, 2013 at 20:10.

  9. #9
    The CSS Clinic is open silver trophybronze trophy
    Paul O'B's Avatar
    Join Date
    Jan 2003
    Location
    Hampshire UK
    Posts
    39,797
    Mentioned
    158 Post(s)
    Tagged
    3 Thread(s)
    Some good tips there they may even tempt me to have a go with my limited JS knowledge

  10. #10
    Community Advisor silver trophybronze trophy
    dresden_phoenix's Avatar
    Join Date
    Jun 2008
    Location
    Madison, WI
    Posts
    2,732
    Mentioned
    31 Post(s)
    Tagged
    0 Thread(s)
    Although my solution may not be coded as elegantly as many of my fellow contestants, I couldn't resist having a go at this one.

    I wish I had seen Ians post before I started... I am definitely bookmarking those links for later refence!


    ~90 lines of code with comments/and .js fade ( hto, yeah, I might have used CSS3 for that, I felt the spirit of the contest was to emulate the as much jQuery functionality as i technically could). Also I opted 'functionalize' many things, the idea being that in the real world code that can be reused is more valuable, that just a dedicated script for say making tabs. Though I haven't done extensive testing yet (other than the current big 5) , intended my code to cater to legacy browsers that do not support things like querySelector.

    Code:
    
    
    function getElementsByClass(theClass,root,tag){
    	theClass=' '+theClass+' ';
    	var i, c, tl,checkClass;
     	var tag = ( tag === undefined) ? "*" : tag;
    	var root = (typeof root === 'string') ? document.getElementById(root) : root;
    	var root = (root === undefined) ? document : root;
    	var allTags=root.getElementsByTagName(tag);
    	var selectedClass = new Array();
    	for (i=0, c=0, tl=allTags.length; i<tl; i++) {
    		checkClass=' '+allTags[i].className+' ';
    		if (checkClass.indexOf(theClass)>-1) {
    			selectedClass[c]=allTags[i];
    			c++;
    		}
    	}
    	return selectedClass;
    }
    function AJAX(url){ // overly simplyfied AJAX foo for this challenge only!!!
      	if (window.XMLHttpRequest){ var req=new XMLHttpRequest();}
     	else if(window.ActiveXObject){ var req= new ActiveXObject("Microsoft.XMLHTTP")}
     	else {return false }
      	req.open('GET', url, true);
      	req.send(''); 
     	return req;		   
     }
    function removeClass(strn,clssToKill){
    			strn=' '+strn+' ';
    			strn=strn.replace(' '+clssToKill+' ','');
    			return trim(strn);
    }
    function trim(strn){return strn.replace(/^\s\s*/, '').replace(/\s\s*$/, '');} // simplyfied Trim foo for this challenge only!!!
    function fadeFoo(el,curr,int){ //simplified fade foo
    			var IE= (typeof el.contLnk.style.opacity ==='string');
    			curr=(curr<0) ?  0 :curr; 
    			if (IE) {el.contLnk.style.opacity=curr+'';}
    			else{ el.style.filter = 'alpha(opacity=' + curr*100 + ')';}
    			if (curr == 0 && int<0){
    				el.contLnk.innerHTML=el.sw;
    				el.sw='';
    				int=-1*int;
    			}
    			curr=curr+int;
    			if (curr < 1){var to=setTimeout(function(){fadeFoo(el,curr,int)},50);}
    			if (curr>=1) {
    				if (IE) {el.contLnk.style.opacity='1';}
    				else{el.style.filter = 'alpha(opacity=100)';}
    			}
     }
    function clickFoo(el){
    	if ((' '+el.className+' ').indexOf('selected') > -1) { return;} 
     	var iii,req, tempClss,parIts, parL, par=el.parentNode.parentNode.getElementsByTagName('a');
    	for (iii=0,parL=par.length ; iii<parL; iii++){
    			par[iii].className=removeClass(par[iii].className,'selected');
    	}
    	req=AJAX(el.href);
    	req.onreadystatechange = function(){
    		if(req.readyState == 4  ){
    	   		el.className=el.className+" selected";
    	   		el.t=setTimeout(function(){fadeFoo(el,1,-.05)},0);
    	   		el.sw=req.responseText;
    	   	}
    	}
    }
      	// preps dipslpay container 
    cont=document.createElement('div');
    cont.class='content'; 
    temp=Array();
    temp1=Array();
    	 // seaches for designated class ( tag.class also possible)
         tabS=getElementsByClass('tabs',undefined,'div'); 
       	  for (i=0, l=tabS.length; i<l; i++) {	
      	  temp=getElementsByClass('nav',tabS[i],'ul');
    	  // make display area
     	 daClone=cont.cloneNode(true);
     	 tabS[i].appendChild(daClone);
          //  attach event to links
     		for (j=0, sln=temp.length; j<sln; j++) {
    			temp1=temp[j].getElementsByTagName('a');
      			for (k=0, slk=temp1.length; k<slk; k++) {
     				temp1[k].contLnk=daClone;
    		 		temp1[k].onclick=function(e){
    		 				e.preventDefault();
    		 				var self=this;
    	 	 				clickFoo(self);
    	 	 		}
      			}
      			clickFoo(temp1[0]); 
      		  }
     	}
    

  11. #11
    SitePoint Member
    Join Date
    Jun 2006
    Posts
    9
    Mentioned
    1 Post(s)
    Tagged
    0 Thread(s)
    This should work in most browsers, although I did not do thorough testing as I don't have every browser installed on this machine.

    I know this code is lengthy, but I would rather have it legible for future revisions that compact and impossible to understand a year from now when I have to revisit it.

    I hope this helps someone. :-)


    There are two versions of many of these functions, one for modern standards-compliant browsers, and one for Internet Explorer versions 8 and earlier. If I were to use this code in my own page I would likely place all of the legacy code in a separate file and wrap it in an IE conditional comment like this:
    Code:
    <![if lt IE 9]>
    <script type="text/javascript" src="tabsQuirks.js"></script>
    <![endif]>

    tabs.js

    Code:
    
    // Copyright 2013, Kevin Douglas, All rights reserved
    // please learn from this but don't just copy it
    // Kevin Douglas: kdouglas@satarah.com
    //
    // Notes:
    // I have simplified error checking and fault tolerance for this challenge
    // I have used procedural vs object-oriented techniques for this challenge
    // both in an effort to minimize code and complexity
    
    
    // *** TAILORED FUNCTIONALITY TO THIS ENVIRONMENT ***
    
    nodeEvent(window, 'load', main, 'attach');
    
    function main()
    {
    	// get node references
    	var tabs = nodeClass(document, 'tabs', 'find')[0];
    	var nav = nodeClass(tabs, 'nav', 'find')[0];
    	var links = nav.getElementsByTagName('a');
    	
    	// create-append content node
    	var div = tabs.appendChild(document.createElement('div'));
    	nodeClass(div, 'content', 'attach');
    	
    	// set handlers on link nodes
    	for (var i = 0; i < links.length; i++)
    	{
    		nodeEvent(links.item(i), 'click', linkEvents, 'attach');
    	}
    	
    	// load default tab and content
    	nodeEventTrigger(links.item(0), 'click');
    }
    
    function linkEvents(event)
    {
    	// cancel default click event
    	nodeEventCancel(event);
    	
    	// get specific tab node
    	var link = (typeof event.target == 'object') ?
    		event.target : // IE 9+, Moz, etc.
    		event.srcElement; // IE 6 - 8
    	if (link.nodeType == 3) // Safari
    	{
    		link = link.parentNode;
    	}
    		
    	// if link is currently selected tab, exit
    	if (nodeClass(link, 'selected', 'test'))
    	{
    		return;
    	}
    	
    	// get node references
    	var div = nodeClass(document, 'content', 'find')[0];
    	var prev = nodeClass(document, 'selected', 'find')[0];
    	
    	// load new tab content via AJAX
    	getFile(link.href, loadContent);
    	
    	// remove class from prev tab
    	if (typeof prev == 'object') // else error on page load
    	{
    		nodeClass(prev, 'selected', 'remove');
    	}
    
    	// attach class to new tab
    	nodeClass(link, 'selected', 'attach');
    	
    	// hide prev content
    	nodeOpacity(div, 'hide', false);
    }
    
    function loadContent(data)
    {
    	// get node reference
    	var div = nodeClass(document, 'content', 'find')[0];
    	
    	// wait for fade animation to complete
    	var handler = window.setInterval(function() 
    	{
    		if (typeof nodeOpacity.handler == 'undefined' &&
    		typeof nodeOpacityQuirks.handler == 'undefined')
    		{
    			div.innerHTML = data;
    			window.clearInterval(handler);
    			nodeOpacity(div, 'show', false);
    		}
    	}, 250);
    }
    
    
    // *** REUSABLE FUNCTIONALITY IN OTHER ENVIRONMENTS ***
    
    // this is a trivial GET AJAX approach
    function getFile(target, callback)
    {
    	function handleResponse()
    	{
    		if (xhr.readyState == 4) // response recieved
    		{
    			if (xhr.status == 200) // 'OK'
    			{
    				callback(xhr.responseText)
    			}
    			else // response error
    			{
    				throw new Error(xhr.status);
    			}
    		}
    	}
    	var xhr = (typeof window.XMLHttpRequest == 'function') ?
    		new XMLHttpRequest() : // IE 9+, Moz, etc.
    		new ActiveXObject("Microsoft.XMLHTTP"); // IE 6 - 8
    	xhr.onreadystatechange = handleResponse;
    	xhr.open('GET', target, true);
    	xhr.send(null);
    }
    
    // action = 'remove'|'attach'|'find'|'test', node = document for 'find' action, or a specific node
    function nodeClass(node, value, action) 
    {
    	var classes = node.className;
    	var pattern = new RegExp('(^| )' + value + '( |$)', 'i'); // case-insensitive
    	switch(action)
    	{
    		case 'remove':
    		{
    			classes = classes.replace(pattern, "$1"); // remove pattern and any leading whitespace
    			classes = classes.replace(/ $/, ''); // remove any end-of-string whitespace
    			node.className = classes;
    			return node.className;
    		}
    		case 'attach':
    		{
    			node.className = (classes.length) ? // existing classes?
    				classes + ' ' + value : value; // prefix whitespace or not
    			return node.className;
    		}
    		case 'find':
    		{
    			var found = [];
    			var nodes = node.getElementsByTagName("*");
    			for (var i = 0; i < nodes.length; i++)
    			{
    				if (pattern.test(nodes[i].className))
    				{
    					found[found.length] = nodes[i];
    				}
    			}
    			return found;
    		}
    		case 'test':
    		{
    			// fallthrough
    		}
    		default:
    		{
    			return pattern.test(classes);
    		}
    	}
    }
    
    // IE 9+, Moz, etc.
    // action = 'attach'|'remove'
    function nodeEvent(node, type, handler, action)
    {
    	if (typeof node.addEventListener == 'undefined')
    	{
    		nodeEventQuirks(node, type, handler, action);
    		return;
    	}
    	switch(action)
    	{
    		case 'attach':
    		{
    			node.addEventListener(type, handler, false);
    			break;
    		}
    		case 'remove':
    		{
    			node.removeEventListener(type, handler, false);
    			break;
    		}
    	}
    }
    
    // IE 6 - 8 *** develop for standards, devolve for legacy support ***
    function nodeEventQuirks(node, type, handler, action)
    {
    	switch(action)
    	{
    		case 'attach':
    		{
    			node.attachEvent('on' + type, handler);
    			break;
    		}
    		case 'remove':
    		{
    			node.detachEvent ('on' + type, handler);
    			break;
    		}
    	}
    }
    
    // IE 9+, Moz, etc.
    function nodeEventCancel(event)
    {
    	if (typeof event.preventDefault == 'undefined')
    	{
    		nodeEventCancelQuirks(event);
    		return;
    	}
    	event.preventDefault();
    }
    
    // IE 6 - 8 *** develop for standards, devolve for legacy support ***
    function nodeEventCancelQuirks(event)
    {
    	if (typeof event == 'undefined')
    	{
    		event = window.event;
    	}
    	event.returnValue = false;
    	return false;
    }
    
    // IE 9+, Moz, etc.
    function nodeEventTrigger(node, type)
    {
    	if (typeof document.createEvent == 'undefined')
    	{
    		nodeEventTriggerQuirks(node, type);
    		return;
    	}
    	var event = document.createEvent('HTMLEvents');
    	event.initEvent(type, true, true); // event type, bubbling, cancelable
    	node.dispatchEvent(event);
    }
    
    // IE 6 - 8 *** develop for standards, devolve for legacy support ***
    function nodeEventTriggerQuirks(node, type)
    {
    	var event = document.createEventObject();
    	node.fireEvent('on' + type, event)
    }
    
    // IE 9+, Moz, etc.
    // action = 'show'|'hide', revert = bool
    nodeOpacity.handler;
    function nodeOpacity(node, action, revert) 
    {
    	if (typeof node.style.opacity == 'undefined')
    	{
    		nodeOpacityQuirks(node, action, revert);
    		return;
    	}
    	if (typeof nodeOpacity.handler == 'number')
    	{
    		window.clearInterval(nodeOpacity.handler)
    		nodeOpacity.handler = undefined;
    	}
    	var fps = 12; // frames or steps per second
    	var increment = 1.25; // change per step, Note: 10 is evenly divisible by this value (8 steps)
    	var interval = Math.round(1000 / fps); // timer microseconds
    	var disolve = (action.toLowerCase() == 'hide') ? true : false; // 'hide'|'show'
    	var opacity = (disolve) ? 10 : 0; // start value
    	var outcome = (disolve) ? 0 : 10; // end value
    	nodeOpacity.handler = window.setInterval(function ()
    	{
    		opacity = opacity + (disolve ? - increment : increment);
    		node.style.opacity = opacity / 10; // set node opacity
    		//console.log(node.style.opacity); // debugging
    		if (opacity == outcome) // NOT decimal tolerant
    		{
    			window.clearInterval(nodeOpacity.handler); // exit transformation
    			nodeOpacity.handler = undefined; // reset handler
    			if (revert) // fade out-in | in-out behavior
    			{
    				nodeOpacity(node, (disolve ? 'show' : 'hide'), false);
    			}
    		}
    	}, interval);
    }
    
    // IE 6 - 8, *** develop for standards, devolve for legacy support ***
    // action = 'show'|'hide', revert = bool
    nodeOpacityQuirks.handler;
    function nodeOpacityQuirks(node, action, revert)
    {
    	if (typeof nodeOpacityQuirks.handler == 'number')
    	{
    		window.clearInterval(nodeOpacityQuirks.handler);
    		nodeOpacityQuirks.handler = undefined;
    	}
    	var fps = 12; // frames or steps per second
    	var increment = 12.5; // change per step, Note: 100 is evenly divisible by this value (8 steps)
    	var interval = Math.round(1000 / fps); // timer microseconds
    	var disolve = (action.toLowerCase() == 'hide') ? true : false; // 'hide'|'show'
    	var opacity = (disolve) ? 100 : 0; // start value
    	var outcome = (disolve) ? 0 : 100; // end value
    	nodeOpacityQuirks.handler = window.setInterval(function ()
    	{
    		opacity = opacity + (disolve ? - increment : increment);
    		node.style.filter = 'alpha(Opacity=' + opacity + ')'; // set node opacity
    		//console.log(opacity); // debugging
    		if (opacity == outcome) // NOT decimal tolerant
    		{
    			window.clearInterval(nodeOpacityQuirks.handler); // exit transformation
    			nodeOpacityQuirks.handler = undefined;
    			if (revert) // fade out-in | in-out behavior
    			{
    				nodeOpacityQuirks(node, (disolve ? 'show' : 'hide'), false);
    			}
    		}
    	}, interval);
    }
    
    

  12. #12
    Under Construction silver trophybronze trophy AussieJohn's Avatar
    Join Date
    Sep 2005
    Location
    Sydney, Australia
    Posts
    776
    Mentioned
    11 Post(s)
    Tagged
    0 Thread(s)
    This was an interesting little challenge. I'm sure I'm known on the forums to jump to jQuery solutions quite quickly. One of my main reasons for this is because then you don't have to deal with browser quirks.

    Writing the code for this challenge has once again reminded me of how crappily the DOM has been implemented in various browsers

    I wanted to emulate a lot of the jQuery functionality, yet still keep the code relatively lean. So obviously I haven't implemented a whole Animation or DOM framework, but some rudimentary methods are there. And to be honest if I'd build this for a real project, I would abstract all the relative functionalities out in to appropriate classes.

    One of the important things I also wanted to achieve is a solution that could cater to multiple sets of tabs on a page - should that ever be required.

    The only limitation of this solution is that you can only pass in a singular selector to the constructor (i.e. only "#some-id" or ".some-class" but not ".some-class .tabs" or ".tabs, .some-other-tabs"). This limitation only really exists for IE7 and below though as the solution for modern browsers implements querySelectorAll.

    So, coming in at around 575 lines, here is my solution:

    Also on pastebin for those wanting to see pretty code: http://pastebin.com/GS8z7LEH
    Also also on http://afterlight.com.au/sitepoint/js-challenge/ if you want to see the solution in action

    Code:
    ;(function(window, document){
        "use strict";
    
    
        /**
         * Async abstractions
         *
         * This class adapted from http://www.quirksmode.org/js/xmlhttp.html
         *
         * @returns {AsyncRequestObject}
         * @constructor
         */
        function AsyncRequestObject() {
    
    
            var self = this;
    
    
            var XMLHttpFactories = [
                function () {return new XMLHttpRequest();},
                function () {return new ActiveXObject("Msxml2.XMLHTTP");},
                function () {return new ActiveXObject("Msxml3.XMLHTTP");},
                function () {return new ActiveXObject("Microsoft.XMLHTTP");}
            ];
    
    
            /**
             * Create an XMLHttpObject based on which of the factories
             * returns an object that can be used.
             *
             * @returns {XMLHttpRequest}
             */
            function createXMLHTTPObject() {
                var xmlhttp = false;
                for (var i=0;i<XMLHttpFactories.length;i++) {
                    try {
                        xmlhttp = XMLHttpFactories[i]();
                    }
                    catch (e) {
                        continue;
                    }
                    break;
                }
                return xmlhttp;
            }
    
    
            /**
             * Perform a request to a given URL
             *
             * Options can include:
             *  postData  - an object containing data to be posted
             *  error     - an error callback
             *  success   - a success callback
             *
             * @param {String} url
             * @param {Object} options
             */
            this.request = function request(url, options) {
                var method;
    
    
                if (!this.xhr) {
                    return;
                }
    
    
                method = (options.postData) ? "POST" : "GET";
    
    
                this.xhr.open(method,url,true);
    
    
                if (options.postData) {
                    this.xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
                }
    
    
                this.xhr.onreadystatechange = function () {
                    if (self.xhr.readyState !== 4) {
                        return;
                    }
    
    
                    if (self.xhr.status !== 200 && self.xhr.status !== 304) {
                        if (options.error) {
                            options.error(self.xhr);
                        }
                        return;
                    }
                    options.success(self.xhr);
                };
    
    
                if (self.xhr.readyState === 4) {
                    return;
                }
    
    
                self.xhr.send(options.postData);
            };
    
    
    
    
            this.xhr = createXMLHTTPObject();
        }
    
    
        window.AsyncRequestObject = AsyncRequestObject;
    
    
    
    
        /**
         * Tabs class - will facilitate a simple HTML pattern
         * to be turned in to AJAX tabs
         *
         * @param {String} selector
         * @returns {Tabs}
         * @constructor
         */
        function Tabs(selector) {
    
    
            var self = this;
    
    
            this.selector = selector;
    
    
            //
            // N.B.: We use this method signature to aid with debugging.
            //
            // this.methodName = function methodName() {}
            //
            // So instead of seeing calls to "anonymous function" in the stack
            // we will actually see the function name.
            //
    
    
    
    
            /**
             * Fade an element in from 0 opacity to 100% opacity over a
             * given duration and when finished fire an optional callback.
             *
             * @param {Node}     el
             * @param {Number}   duration
             * @param {Function} [callback]
             */
            this.fadeIn = function fadeIn(el, duration, callback) {
                this.fadeTo(el, 0, 100, duration, callback);
            };
    
    
    
    
            /**
             * Fade an element out from 100 to 0% opacity over a
             * given duration and when finished fire an optional callback.
             *
             * @param {Node}     el
             * @param {Number}   duration
             * @param {Function} [callback]
             */
            this.fadeOut = function fadeOut(el, duration, callback) {
                this.fadeTo(el, 100, 0, duration, callback);
            };
    
    
    
    
            /**
             * Fade an element from one opacity to another over a
             * given duration and when finished fire an optional callback
             *
             * @param {Node}     el
             * @param {Number}   from
             * @param {Number}   to
             * @param {Number}   duration
             * @param {Function} [callback]
             */
            this.fadeTo = function fadeTo(el, from, to, duration, callback) {
                var startOpacity = parseInt(from, 10),
                    finalOpacity = parseInt(to, 10),
                    difference = Math.abs(finalOpacity - startOpacity),
                    tickDuration = parseInt(duration, 10) / difference,
                    currentOpacity = startOpacity;
    
    
                // Increase or Decrease the currentOpacity based on
                // which direction we need to go.
                function setCurrentOpacity() {
                    currentOpacity = startOpacity < finalOpacity ? ++currentOpacity : --currentOpacity;
                }
    
    
                // Check if the currentOpacity is out side of
                // the final opacity we want to achieve.
                function currentOpacityOutOfBounds() {
                    return startOpacity < finalOpacity ? (currentOpacity > finalOpacity) : (currentOpacity < finalOpacity);
                }
    
    
                // Recurse through this function until we've met
                // the stop condition.
                function fade() {
    
    
                    // our stop condition is that the current
                    // opacity is out of bounds
                    if (currentOpacityOutOfBounds()) {
    
    
                        //if we have a callback - call it now.
                        if (typeof callback === "function") {
                            callback.call(el);
                        }
                        return;
                    }
    
    
                    // set all the styles
                    el.style.MsFilter = "progid:DXImageTransform.Microsoft.Alpha(Opacity="+ currentOpacity +")";
                    el.style.filter = "alpha(opacity="+ currentOpacity +")";
                    el.style.opacity = currentOpacity / 100;
    
    
                    setCurrentOpacity();
    
    
                    setTimeout(fade, tickDuration);
                }
    
    
                fade();
            };
    
    
    
    
            /**
             * Load a file and pass its contents along to the callback
             *
             * @param path
             * @param callback
             */
            this.load = function load(path, callback) {
    
    
                this.async.request(path, {
                    success: function( xhr ){
                        callback.call(self, xhr.responseText);
                    },
                    error: function() {
                        callback.call(self, "Sorry, could not retrieve the content for this tab");
                    }
                });
    
    
            };
    
    
    
    
            /**
             * Check if an element has a class
             *
             * @param {Node}   el
             * @param {String} className
             */
            this.hasClass = function hasClass(el, className) {
                var classArray, classListLength, i;
    
    
                if (!el) {
                    return;
                }
    
    
                classArray = el.className.split(" ");
                classListLength = classArray.length;
    
    
                // not the most efficient way of searching, but it works :)
                for ( i = 0; i < classListLength; i++ ) {
                    if (classArray[i] === className) {
                        return true;
                    }
                }
    
    
                return false;
            };
    
    
    
    
            /**
             * Add a class to an element.
             *
             * @param {Node}   el
             * @param {String} newClass
             */
            this.addClass = function addClass(el, newClass) {
    
    
                if (!el) {
                    return;
                }
    
    
                if ( ! this.hasClass(el, newClass)) {
                    // only append the class name if it's not already present.
                    el.className += newClass;
                }
    
    
                return el;
            };
    
    
    
    
            /**
             * Remove a class from an element
             *
             * @param {Node} el
             * @param {String} oldClass
             */
            this.removeClass = function removeClass(el, oldClass) {
                var classArray, classArrayLength, i, newClassList = [];
    
    
                if (!el) {
                    return;
                }
    
    
                if (this.hasClass(el, oldClass)) {
                    //only try to remove the class if its present
    
    
                    classArray = el.className.split(" ");
                    classArrayLength = classArray.length;
    
    
                    // not a terribly efficient way to do it - but it's
                    // clear what the code does
                    for( i = 0; i < classArrayLength; i++ ) {
                        if ( classArray[i] !== oldClass ) {
                            // we add the classes to an array to keep DOM manipulation to a minimum
                            newClassList.push(classArray[i]);
                        }
                    }
    
    
                    el.className = newClassList.join(" ");
                }
    
    
                return el;
            };
    
    
    
    
            /**
             * Add an event listener to a DOM node
             *
             * @param {Node}     el
             * @param {String}   eventName
             * @param {Function} callback
             */
            this.addEventListener = function addEventListener(el, eventName, callback) {
    
    
                if (window.addEventListener) {
                    el.addEventListener(eventName, function(e) {
                        callback.call(this, e);
                    });
                    return;
                }
    
    
                if (window.attachEvent) {
                    el.attachEvent("on"+eventName, function(e) {
                        e = e || window.event;
                        e.preventDefault  = e.preventDefault  || function(){e.returnValue = false;};
                        e.stopPropagation = e.stopPropagation || function(){e.cancelBubble = true;};
                        callback.call(el, e);
                    });
                }
    
    
            };
    
    
    
    
            /**
             * Dispatch an event to a node
             *
             * @param {Node}   el
             * @param {String} eventName
             */
            this.dispatchEvent = function dispatchEvent(el, eventName) {
                var e;
    
    
                if (document.createEvent) {
                    e = document.createEvent("HTMLEvents");
                    e.initEvent(eventName, true, true);
                    el.dispatchEvent(e);
                    return;
                }
    
    
                if (document.createEventObject) {
                    e = document.createEventObject();
                    el.fireEvent("on" + eventName, e);
                    return;
                }
    
    
            };
    
    
    
    
            /**
             * Create a new DOM el and append it to a container Node
             * and optionally give it a class. Return the newly added element.
             *
             * @param {Node}   container
             * @param {String} nodeName
             * @param {String} [nodeClassName]
             * @returns {HTMLElement}
             */
            this.appendNode = function appendNode(container, nodeName, nodeClassName) {
                var frag, newNode;
    
    
                // add a placeholder for the content
                frag = document.createDocumentFragment();
                newNode = document.createElement(nodeName);
                newNode.className = nodeClassName || "";
                frag.appendChild(newNode);
    
    
                container.appendChild(frag);
                return newNode;
            };
    
    
    
    
            /**
             * Return an array of elements based on a selector.
             *
             * @param selector
             * @returns {*} A NodeList or an Array
             */
            this.getElementsBySelector = function getElementsBySelector(selector) {
    
    
                // Modern browsers are cool - not much work to be done.
                if (document.querySelectorAll) {
                    return document.querySelectorAll(selector);
                }
    
    
                //check if the selector starts with a "#" so we can use getElementById
                if (selector.indexOf("#") === 0) {
                    return [document.getElementById(selector.replace("#", ""))];
                }
    
    
                // Check if the selector starts with a "." so we can use getElementsByClassName
                // We also know that we'll have document.getElementsByClassName as we've used a polyfill.
                if (selector.indexOf(".") === 0) {
                    return document.getElementsByClassName(selector.replace(".", ""));
                }
    
    
                return false;
    
    
            };
    
    
    
    
            /**
             * Callback to fire when the content for a tab has loaded.
             *
             * @param {String} content The HTML to be injected
             */
            this.tabContentLoaded = function tabContentLoaded(content) {
    
    
                self.fadeOut(self.tabContentEl, 300, function() {
                    // inject the content in to a new element.
                    self.tabContentEl.innerHTML = content;
                    self.fadeIn(self.tabContentEl, 800);
                });
    
    
            };
    
    
    
    
            /**
             * Handle the Click event on tabs
             * @param e
             */
            this.tabClickHandler = function tabClickHandler(e) {
                var target = e.srcElement || e.target;
    
    
                e.preventDefault();
                if ( target.nodeName === "A" ) {
                    e.stopPropagation();
    
    
                    // prevent reloading the same tab
                    if (self.hasClass(target, "selected")) {
                        return false;
                    }
    
    
                    self.removeClass(self.currentTabEl, "selected");
                    self.currentTabEl = target;
                    self.addClass(self.currentTabEl, "selected");
                    self.load(target.href, self.tabContentLoaded);
    
    
                    return true;
                }
    
    
            };
    
    
    
    
            /**
             * Bind UI related events here
             */
            this.bindEvents = function bindEvents() {
    
    
                this.addEventListener(this.el, "click", this.tabClickHandler);
    
    
            };
    
    
    
    
            /**
             * Initialise the class
             *
             * Here we'll set up some default values and references to DOM elements.
             *
             */
            this.init = function init() {
                var i;
    
    
                this.tabs = [];
    
    
                // We do some checking to see if we  need to instantiate ourselves and pass
                // along an element or if we can simply proceed if a single element is found.
                // Ideally this would be abstracted to a DOM helper class.
                if (typeof this.selector === "string") {
                    this.el = this.getElementsBySelector(this.selector);
                }
                else {
                    this.el = this.selector;
                }
    
    
                if (this.el && this.el.length > 1) {
                    // We have multiple elements - so we'll create new
                    // Tab instances for each set of tabs
                    for ( i = 0; i < this.el.length; i++) {
                        this.tabs.push(new Tabs(this.el[i]));
                    }
    
    
                    return;
                }
                else if (this.el && this.el.length === 1) {
                    // just a single element we can prodeed with that.
                    this.el = this.el[0];
                }
                else {
                    // we end up here when the selector is not a string,
                    // in which case it would be an element
                    this.el = this.selector;
                }
    
    
    
    
                // get a reference to the nav container
                this.tabsNavContainer = this.el.getElementsByTagName("ul")[0]; // we definitely only want the first element
    
    
                // find all the tabs
                this.tabLinks = this.tabsNavContainer.getElementsByTagName("a");
    
    
                //inject a container for the content
                this.tabContentEl = this.appendNode(this.el, "div", "content");
    
    
                // AJAX functionality is abstracted
                this.async = new AsyncRequestObject();
    
    
                // Bind all the UI events
                this.bindEvents();
    
    
                // trigger a click on the first tab.
                this.dispatchEvent(this.tabLinks[0], "click");
            };
    
    
    
    
            //////////////////////////////////////////////////////////////////////////
            //
            // Borrowed from https://gist.github.com/eikes/2299607
            //
            // Note that we only really need this for < IE8 as IE8 and
            // up supports document.querySelectorAll
            //
            //////////////////////////////////////////////////////////////////////////
            //
            // Add a getElementsByClassName function if the browser doesn't have one
            // Limitation: only works with one class name
            // Copyright: Eike Send http://eike.se/nd
            // License: MIT License
            //
    
    
            if ( !document.getElementsByClassName ) {
                document.getElementsByClassName = function (search) {
                    var d = document, elements, pattern, i, results = [];
                    if ( d.querySelectorAll ) { // IE8
                        return d.querySelectorAll("." + search);
                    }
                    if ( d.evaluate ) { // IE6, IE7
                        pattern = ".//*[contains(concat(' ', @class, ' '), ' " + search + " ')]";
                        elements = d.evaluate(pattern, d, null, 0, null);
                        while ( (i = elements.iterateNext()) ) {
                            results.push(i);
                        }
                    }
                    else {
                        elements = d.getElementsByTagName("*");
                        pattern = new RegExp("(^|\\s)" + search + "(\\s|$)");
                        for ( i = 0; i < elements.length; i++ ) {
                            if ( pattern.test(elements[i].className) ) {
                                results.push(elements[i]);
                            }
                        }
                    }
                    return results;
                };
            }
    
    
            this.init();
        }
    
    
        window.Tabs = Tabs;
    
    
        var tabs = new Tabs(".tabs");
    
    
    }(window, document));
    
    var details = {
    . . web: "afterlight.com.au",
    . . photos: "jvdl.id.au",
    . . psa: "usethelatestversion.com"
    }

  13. #13
    The CSS Clinic is open silver trophybronze trophy
    Paul O'B's Avatar
    Join Date
    Jan 2003
    Location
    Hampshire UK
    Posts
    39,797
    Mentioned
    158 Post(s)
    Tagged
    3 Thread(s)
    Hi,

    Finally got around to having a go at this as I have been away on holiday and my feeble attempt can be found here. It should work back to IE6 with any luck.

    It's basically a collection of snippets that I borrowed from around the web so can't really take much credit although it did help broaden my knowledge a little (and to remind me how little I know about js).

    There is a logic error in that it doesn't take into account that there may be other tab elements on the page due to the fact that a class name was used and not an id. I did try to make a version that would run for multiple tab blocks but failed miserably so gave up as I ran out of time

    Thanks for making me have a try anyway and I look forward to seeing how the experts have done it.

  14. #14
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    5,313
    Mentioned
    178 Post(s)
    Tagged
    8 Thread(s)
    So then, the deadline has passed and the competition is now officially closed.

    Thank you very much to @fretburner ;, @sleignnet ;, @markbrown4 ;, @langsor ;, @dresden_phoenix ;, @AussieJohn ; & @Paul O'B ; for some great entries.

    Paul and my good self will go through the entries in the next few days and offer some feedback on what was submitted, as well as an "official" solution.

    Watch this space, as they say ...

  15. #15
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    5,313
    Mentioned
    178 Post(s)
    Tagged
    8 Thread(s)
    Having now had time to look at the entries in more detail we've decided to first present our solution before announcing a winner.

    We'd like to do this in stages, so that we can examine the various ways in which jQuery makes our life easier, as well as ways in which we can replicate what it does for us. Ideally this will lead people to question the wisdom of including a large library in a project when only a minor subset of its functionality is required, as well as highlight some current best practises along the way.

    As ianc.sp points out, the main complexity of converting the tabbed-navigation into plain JavaScript is the AJAX request which loads the content into the page. Accordingly, this seems like a good place to start.

    As you all know, AJAX requests are made using XMLHttpRequest (XHR). This is a protocol that was originally designed by Microsoft, implemented in JavaScript by Mozilla, Apple, and Google and is currently being standardized by the W3C.

    In standards compliant browsers, you simply create an instance of the object, open a URL, and send the request. The HTTP status of the result, as well as the result's content, is available in the request object once the transaction is completed.

    So far, so good …

    However, in old versions of Internet Explorer (IE5 and IE6) XMLHttpRequest is not supported, with these browsers relying on the proprietary ActiveXObject to make Ajax requests. To make matters worse, there are reports of XMLHttpRequest being buggy in IE7, too.

    So what to do?

    Instead of rolling your own AJAX solution, it can be better to stand on the shoulders of giants and make good use of solutions that have already been created. At the microjs.com site you can easily see all of the common libraries that handle it well. Some are large and provide additional functionality, while others such as microAjax provide only the ajax functionality with no extraneous addons.

    For example, microAjax gets the request with:

    Code javascript:
    this.getRequest = function() {
      if (window.ActiveXObject)
        return new ActiveXObject('Microsoft.XMLHTTP');
      else if (window.XMLHttpRequest)
        return new XMLHttpRequest();
      return false;
    };

    The magic sauce that they have, is when it comes time to make the post, they check if you are posting any data with the request and supply the appropriate headers for that request.

    Code javascript:
    if(this.request) {
      var req = this.request;
      req.onreadystatechange = this.bindFunction(this.stateChange, this);
     
      if (this.postBody!=="") {
        req.open("POST", url, true);
        req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        req.setRequestHeader('Connection', 'close');
      } else {
        req.open("GET", url, true);
      }
     
      req.send(this.postBody);
    }

    So what we have with microAjax is a simple set of code that easily achieves cross-browser compatibility, while ensuring that the standard needs of someone making an ajax request are met. It's difficult to improve on that, thus this is what we have chosen to use.

    If you do happen to require any additional special requirements though, there are other ajax libraries that may provide just what you need.

    It's also possible for you to take inspiration from any of the code, and to modify things to your own particular needs. Take care with that though, because it can be difficult to determine if such changes won't work for others, even though they seem to work currently for you.

    Having said all of that, at this juncture I would also encourage you to look at what jQuery is doing under the hood:

    Code:
    // Functions to create xhrs
    function createStandardXHR() {
        try {
            return new window.XMLHttpRequest();
        } catch( e ) {}
    }
    
    function createActiveXHR() {
        try {
            return new window.ActiveXObject( "Microsoft.XMLHTTP" );
        } catch( e ) {}
    }
    
    // Create the request object
    // (This is still attached to ajaxSettings for backward compatibility)
    jQuery.ajaxSettings.xhr = window.ActiveXObject ?
        /* Microsoft failed to properly
         * implement the XMLHttpRequest in IE7 (can't request local files),
         * so we use the ActiveXObject when it is available
         * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
         * we need a fallback.
         */
        function() {
            return !this.isLocal && createStandardXHR() || createActiveXHR();
        } :
        // For all other browsers, use the standard XMLHttpRequest object
        createStandardXHR;
    One way of doing this is to use James Padolsey’s jQuery source viewer.

    So, that's part one. Part two will follow shortly, but in the meantime we would welcome any comments or observations on the above.
    Last edited by paul_wilkins; Jun 28, 2013 at 04:37. Reason: minor spelling

  16. #16
    The CSS Clinic is open silver trophybronze trophy
    Paul O'B's Avatar
    Join Date
    Jan 2003
    Location
    Hampshire UK
    Posts
    39,797
    Mentioned
    158 Post(s)
    Tagged
    3 Thread(s)
    Good explanation Pullo - look forward to part 2

  17. #17
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    5,313
    Mentioned
    178 Post(s)
    Tagged
    8 Thread(s)
    One of the main reasons behind jQuery's meteoric rise to popularity was its use of the CSS selector syntax to select DOM elements and the ease with which it could then traverse these and modify their content.

    In this post I'd like to focus on how jQuery handles DOM selection under the hood and what native JavaScript functions we can use in its place.

    A quick glance at the API docs, shows the syntax for jQuery DOM selection to be jQuery( selector [, context ] ).
    The fact that this method takes an optional context parameter might come as a surprise to some, as we normally see it used without. However, passing a valid context parameter thus: $('a', $nav), would be equivalent to calling $nav.find('a').

    When jQuery receives a valid selector as a first parameter it starts by identifying the string it was passed using a regular expression. The easiest scenario here is when this string is an id, in which case jQuery can call the traditional document.getElementById and wrap the returned element in a jQuery object.

    In most other cases, jQuery will leverage the following native JavaScript methods where they are available:
    • document.querySelectorAll(selector) — returns a non-live NodeList of all the matching element nodes
    • document.getElementsByTagName(tagname) — returns a live HTMLCollection of matching elements ('live'meaning that it updates itself automatically to stay in sync with the DOM tree)
    • document.getElementsByClassName(class) — returns a HTMLCollection of elements with a specific class name
    Support for these methods is varied:
    • getElementById was introduced in DOM Level 1 for HTML documents and works well in almost all browsers (with the odd exception)
    • getElementsByTagName was introduced in DOM Level 2 and also works well in almost all browsers (again with the odd exception).
    • document.getElementsByClassName is not as widely supported, most notably lacking support in IE8. Support overview.
    • querySelectorAll and the related querySelector (which returns the first element within the document that matches the specified group of selectors) were introduced with the HTML5 Selectors API. They have been around for some time now and are supported in all modern browsers and IE8 (although IE8 only supports up to CSS2.1 selectors). Support overview.
    For the case that native support is not available, jQuery falls back to using the Sizzle engine which was written by John Resig (author of jQuery).
    Sizzle is a pure-JavaScript CSS selector engine designed to be easily dropped in to a host library. It's lightweight (4kb when minified), easy to use and I encourage you to check it out.

    E.g. to select all <span> elements which are a direct child of a <p> elements, you would do:

    Code JavaScript:
    Sizzle('p > span');
    or to select all inputs with an attribute name that starts with 'news':

    Code JavaScript:
    Sizzle('input[name^="news"]')
    You can find the Sizzle documentation here, where you will also find details on additional selectors that are supported by sizzle too.

    You can also search the jQuery sourcecode for occurrences of the term "Sizzle" to get an idea of how it is used.

    So what did we use?
    Again, why reinvent the wheel? We opted to include the Sizzle library in our code, then mapped querySelector and querySelectorAll to Sizzle if native browser support wasn't detected.

    You can do that, like this:

    Code JavaScript:
    // If there's native support for querySelector, don't load Sizzle.
    if (undefined !== document.querySelector) {
    	return;
    }
     
    ...
     
    document.querySelectorAll = function querySelectorAll(selector){
    	return Sizzle(selector, this);
    };
    document.querySelector = function querySelector(selector){
    	return (document.querySelectorAll.call(this, selector)[0] || null);
    };
    There are a couple of caveats to this approach. For example, IE7 doesn't have an Elements object from which to inherit things like querySelector, so you can't do:

    HTML Code:
    <ul id="myList">
      <li>One</li>
      <li>Two</li>
      <li class="highlight">Three</li>
      <li>Four</li>
      <li class="highlight">Five</li>
    </ul>
    Code JavaScript:
    var myList = document.getElementById("myList");
    console.log(myList.querySelectorAll(".highlight").length);
    In such a situation the obvious solution for compatibility is to refrain from using querySelector / querySelectorAll on partial searches, and use things like getElementsByTagName instead.

    ---

    Another area I would like to cover are the jQuery helper functions hasClass(), addClass() and removeClass() which we had to convert to regular JavaScript in the course of this challenge.

    Here's our solution followed by a brief explination:

    Code JavaScript:
    function hasClass (ele, cls) {
      return ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'));
    };
    This uses a regular expression to match a class name which can be preceded or followed by optional whitespace (to allow for multiple class names on the same element).

    Code JavaScript:
    function addClass (ele, cls) {
      if (!hasClass(ele, cls)) {
        ele.className += ' ' + cls;
      }
    };
    Hopefully this is a no brainer. Here we are using the previous helper function to check if the element already has a particular class name. If not we are adding it using string concatenation.

    Code JavaScript:
    function removeClass (ele, cls) {
      if (hasClass(ele, cls)) {
        var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'),
            newClass = ele.className.replace(reg, ' ');
        ele.className = newClass.replace(/^\s+|\s+$/g, '');
      }
    };
    This is a little more complicated. We cannot simply subtract the class we wish to remove (as we could in a language like Ruby), as otherwise JavaScript will attempt to coerce the string to a number and this will result in your element getting a class of NaN (Not A number).
    Instead we check if the element we want to remove the class from, actually has that class.
    If it does we use a regular expression to replace that particular class in the element's className property with a space, and assign the result to a variable newClass.
    We then assign the value held in our newClass variable to the element's className propery.

    This technique (obviously) makes use of the recent classList interface that provides easy and fast manipulation of element class names, however Internet Explorer only supports it as of IE10. There is a polyfill that provides support back to IE8 and IE9, but if you're needing things to work on IE7 or IE6 then you're out of luck and when you attempt to use the .classList methods, the browser won't like the undefined object errors.

    So, much as we would love to recommend the normal use of classList, any attempts to use it currently still have to check if the property exists before using it, due to IE6 and IE7 issues, so some form of hasClass, addClass and removeClass function is still required.

    As an end result, we can use Eli Grey's classList shim that can be found on the classList documentation page to polyfill suport for IE8 and IE9, and we can access it via custom hasClass, addClass and removeClass functions to protect against classList not being supported. What we end up with is loading the polyfill followed by class handling functions:

    HTML Code:
    <script src="js/polyfill/classlist-polyfill.js"></script>
    <script src="js/class-handling.js"></script>
    Because we do want to use the classList techniques if they are at all supported, the class-handling.js script chooses which one to use:

    Code javascript:
    ...
    function nativeRemoveClass(el, className) {
        el.classList.remove(className);
    }
    var supportsClassList = !!document.body.classList;
    window.hasClass = supportsClassList ? nativeHasClass : hasClass;
    window.addClass = supportsClassList ? nativeAddClass : addClass;
    window.removeClass = supportsClassList ? nativeRemoveClass : removeClass;

    An example of the above can be seen in action at the bottom of the script, at http://jsfiddle.net/pmw57/B7SHE/

    So, that's it for now.
    Well done if you made your way through all that

    By way of extra-curricular reading, here are one or two more great articles you might want to check out:

    http://www.sitepoint.com/series/nati...query-methods/
    http://blog.bigbinary.com/2010/02/15...ng-sizzle.html
    http://tech.pro/tutorial/1385/out-growing-jquery

    Stay tuned for part three when Paul will present the nuts and bolts of our solution.
    Last edited by paul_wilkins; Jul 19, 2013 at 04:10. Reason: added example nativeRemoveClass function and jsfiddle to the example

  18. #18
    Under Construction silver trophybronze trophy AussieJohn's Avatar
    Join Date
    Sep 2005
    Location
    Sydney, Australia
    Posts
    776
    Mentioned
    11 Post(s)
    Tagged
    0 Thread(s)
    That's a nice write-up. Looking forward to seeing how you guys have built your solution
    var details = {
    . . web: "afterlight.com.au",
    . . photos: "jvdl.id.au",
    . . psa: "usethelatestversion.com"
    }

  19. #19
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    5,313
    Mentioned
    178 Post(s)
    Tagged
    8 Thread(s)
    Thanks John.
    I appreciate that!

  20. #20
    Unobtrusively zen silver trophybronze trophy
    paul_wilkins's Avatar
    Join Date
    Jan 2007
    Location
    Christchurch, New Zealand
    Posts
    14,526
    Mentioned
    83 Post(s)
    Tagged
    3 Thread(s)

    JavaScript Challenge: Convert JQuery to Plain JavaScript

    Quote Originally Posted by Pullo View Post
    Stay tuned for part three when Paul will present the nuts and bolts of our solution.
    Thanks Pullo - here's what we've done with our solution.

    Our approach

    When writing code there are a number of different factors competing for your attention. If you want the code to be clear and understandable, and you want the code to also work in a cross-browser fashion, then it has to be accepted that this comes at the cost of size, due to polyfills and other-such techniques that allow the modern to work with the old.

    If keeping the file size down was more important then the polyfills would have to be lost. This results in either a loss of cross-browser compatibility, or it means writing the code using older techniques that tend to be more difficult to understand.

    So we have a choice between clarity, compatibility, and size - pick any two.

    Because I'm not interested in writing unclear code, and because I try to maintain communication with older browsers, the code that we have come up with has been arrived at with an eye to the future by using modern techniques for clarity, and an eye to the past with polyfills for cross-browser compatibility.

    Modern Techniques

    We've chosen to use modern coding techniques where they can be still supported by older web browsers such as IE7, because for the most part such techniques help to make the code easier to understand. There are some common modern techniques though that we haven't used. So here's a rundown of them, and why.

    Commonly forEach is one of the first choices for a polyfill, but in our code there is no use for that due to it being a nodelist that we are iterating over. If we did have an array to use forEach on, the compatibility code to provide full and complete support for forEach is quite large, so we could instead use of a smaller set of compatibility code that does a good enough job, and has the potential to be updated later on if needed should we require the additional support.

    The classList object is used in our code, but only at arms length through functions such as addClass. Due to issues with IE7 we can't make calls to el.classList without causing errors in IE7, so even when using a polyfill for it, we still have to check first whether classList is supported before being able to use it. If we only cared about supporting back to IE8, then with the classList polyfill we can use el.classList directly in our code without any issues.

    While querySelector is used to retrieve elements that the document.getElementById can be use for, there are other forms of retrieval that we use querySelector with such as obtaining the tab anchors, so we gain a good benefit by consistently using querySelector throughout our code.

    Polyfills

    Polyfills are additional pieces of scripting code that check if a web browser is capable of supporting a certain feature, and if not then additional code is used to emulate that missing feature instead. This is usually effective for extending support of features to web browsers that are several versions earlier than those that natively support a feature.

    One of the added benefits of using polyfills is that they can at some later stage be removed without impacting your code. Because the polyfill allows you to use the modern technique, the only concern when removing a polyfill is in terms of the older web browsers that you don't care to continue to support any more.

    querySelector

    The can I use web site is very useful for determining native support of features, and older support when polyfills are used. For example, their page on querySelector support shows that Internet Explorer has supported that natively since IE8, and when you hover on the red indicators for IE6 and IE7 you're shown that it's not supported but polyfills are available. Sometimes the page will provide a reference link to a polyfill that can be used, but in this case one is not provided so a google search for javascript queryselector polyfill can be done to allow us to research potential solutions.

    In this case, there are a couple of good querySelector polyfills to pick from:
    1. GitHub of polyfills is where you can extract from polyfill.js the part for querySelector, which links to its original article Creating a querySelector for IE that runs at “native speed”
    2. https://gist.github.com/branneman/1200441 is quite a different querySelector polyfill that's much larger in size because it incorporates the Sizzle CSS selector engine.

    The first one with its small size would be preferable, but it doesn't seem to work on IE7 in the way that we are using it, so the second polyfill with the built-in sizzle engine is what we'll be using here.

    addEventListener

    It's tempting to go with the MDN compatibility code for addEventListener but as it's only good for IE8, further looking reveals an MIT student who has provided a polyfill good back to IE6, at https://gist.github.com/eirikbacker/2864711 - this is a very nice benefit, because we can now happily use addEventListener in our code without any further compatibility issues.

    classList

    Working with class names has always been tricky and commonly results in us using some class-handling functions that provide much of the functionality for us. Thanks to the classList interface though, we now have the native ability to work with class names. Because it's the newest feature of what we're using though, browser compatibility is a real issue. At this stage if we were to use it as designed, even with a polyfill, IE7 would throw an error. We could go back to those class-handling functions from before, but then we wouldn't gain any benefit from browsers that do support classList. So a hybrid solution is called for.
    Eli Grey has a nice classList polyfill that's good back to IE8. If we want to go back further than that though, we'll have to come up with a hybrid solution, which is where helper function come in to play.

    Helper functions

    Class-handling functions

    We are going to be using the classList method where it is supported, and when it's not we will be reverting back to using the class-handling functions. How we do that, is by having the functions defined separately, and then choosing which ones to use for hasClass, addClass, removeClass when things begin.

    Code javascript:
    if (supportsClassList) {
        window.hasClass = classListContains;
        window.addClass = classListAdd;
        window.removeClass = classListRemove;
    } else {
        window.hasClass = hasClass;
        window.addClass = addClass;
        window.removeClass = removeClass;
    }

    MicroAjax
    The ajax communication library that we are using is a tiny one called MicroAjax. How we found is is by searching the microjs web site (no relation to MicroAjax) for their smallest ajax library, which does all that we need to do here.

    Fade in/out

    The last of what could be called some helper functions are for fading the content in and out. In this case, we are just adding a class name of fade-out to fade things out, and after a small delay we replace that with fade-in to fade things in. The fading effect is achieved with CSS. For example:

    Code css:
    #content { /* vendor prefixes are thanks to [url]http://cssprefixer.appspot.com/[/url] */
        -moz-transition: opacity 0.5s ease-in;
        -o-transition: opacity 0.5s ease-in;
        -webkit-transition: opacity 0.5s ease-in;
        transition: opacity 0.5s ease-in;
    }
    #content.fade-out {
        zoom: 1;
        filter: alpha(opacity=0);
        opacity: 0;
    }
    #content.fade-in {
        zoom: 1;
        filter: alpha(opacity=100);
        opacity: 1;
    }

    Even though IE9 and below don't perform the fading effect, their content still goes away for a short while before being replaced with the new content, which helps to provide a visual-feedback that the content has changed.

    Conclusion

    So after having explained what has been done, the next post will provide all of the details for the code itself. In the meantime though if you're curious, you can download the vanilla version of the code from this dropbox location.
    Last edited by paul_wilkins; Aug 5, 2013 at 06:05.
    Programming Group Advisor
    Reference: JavaScript, Quirksmode Validate: HTML Validation, JSLint
    Car is to Carpet as Java is to JavaScript

  21. #21
    Unobtrusively zen silver trophybronze trophy
    paul_wilkins's Avatar
    Join Date
    Jan 2007
    Location
    Christchurch, New Zealand
    Posts
    14,526
    Mentioned
    83 Post(s)
    Tagged
    3 Thread(s)
    After the prequel above, we can now get in to the nuts and bolts of a solution. There are a wide variety of approaches that can be taken when doing this, but this approach is aimed at being easily understandable and widely usable.

    Further posts can cover other topics such as making things more easily configurable, and adjustments that can be made to allow several different tabbed sections to be on one page.

    But for now - let's get in to the code.

    Code Wrapper

    The development code that we're using is contained within the following wrapper:

    Code javascript:
    /*globals helper, microAjax */
    (function (window, document) {
        'use strict';
     
    }(this, this.document));

    JSLint or some other linter is useful to help weed out any significant issues in the code, so my code editor has been set up to automatically check for and and notify about any JavaScript issues. The global comment at the start is so that the automatic checking knows about them when it does its checks.

    The function wrapper helps to prevent any functions within it from appearing in the global namespace. It's best to keep the global namespace as clean as possible. Also, using the function wrapper gives us a later flexibility to develop that function as a single tabs object from which configuration can occur. Doing so can help to remove code smells such as magic numbers and strings, but that's something that can be delved in to later on as further part of the project.

    Helper functions

    The class-handling functions and microAjax have already been covered in the previous post, but other helper functions are wantred here too. Because there are several helper functions that we want to use, it can help to groups them together under a consistant global object which we in this case will call helper. Most of the external functions we'll be using can be placed in the helper object. It's technically possible to put microAjax in there too, but for now we'll keep that code as it comes from their website and we can put aside making changes to it for a later time.

    The class-handling functions though can definately all go to the global helper object. Here's how we do that.

    Check if the helper object exists and if it doesn't, create a new object for it.

    class-handling.js
    Code javascript:
    (function (window, document) {
     
        window.helper = window.helper || {};
     
        ...
     
    }(this, document));

    Then, when we are defining the class-handling function to be used, we can add them to the helper object.

    Code javascript:
    var helper = window.helper,
        supportsClassList = !!document.body.classList;
     
    if (supportsClassList) {
        helper.hasClass = classListContains;
        helper.addClass = classListAdd;
        helper.removeClass = classListRemove;
    } else {
        helper.hasClass = hasClass;
        helper.addClass = addClass;
        helper.removeClass = removeClass;
    }

    The other useful functions that we have are too small to belong in their own script file, and yet are too general to be placed solely in the project that we're working on. That in this case applies to the forEachElement and preventDefault functions.

    We can create utils.js and place them in there,
    utils.js
    Code javascript:
    (function (window) {
        'use strict';
     
        // forEachElement and preventDefault functions defined here
        // ...
     
        window.helper = window.helper || {};
        var helper = window.helper;
     
        helper.forEachElement = forEachElement;
        helper.preventDefault = preventDefault;
    }(this));


    forEachElement

    The forEachElement function allows us to easily work with a NodeList collection of elements. We are advised that the array forEach method is not available for nodeLists, and that extending the the DOM to add a custom method is dangerous with older versions of Web browsers. It is recommended instead to use a normal for loop to iterate over such collections of elements, so this forEachElement function can help us to do that with ease at the expense of being a global object.

    I've kept the arguments for the callback function consistant with the arguments that are used with the forEach callback too, to help maintain a sense of consistancy.

    Code javascript:
    // Due to there being no native forEach methods to handle
    // iterating over elements, this forEachElement function
    // makes it easy to do so using the recommended for loop
    function forEachElement(els, callback) {
        var thisArg,
            el,
            i;
        for (i = 0; i < els.length; i += 1) {
            el = els[i];
            thisArg = el;
            callback.call(thisArg, el, i, els);
        }
    }


    preventDefault

    Because preventing the default action on web browsers can be tricky when using advanced event regestration techniques in a cross-browser environment, this preventDefault function helps with some of the details that are involved.

    Code javascript:
    // The typical returning false from an event handler is not effective when using addEventListener.
    // Instead, control is achieved in W3C compatible browsers with evt.preventDefault() and evt.stopPropagation()
    // and in Internet Explorer by setting to evt.returnValue = false and evt.cancelBubble = false
    function preventDefault(evt) {
        if (evt.preventDefault) {
            evt.preventDefault();
        }
        evt.returnValue = false;
    }

    The tabs script

    Due to JSLint rightfully expecting functions to be defined before they are called, this tends to result in smaller detail-oriented functions being higher up, and big-picture more interesting functions lower down, so I'll go through the functions in reverse order starting from the bottom.

    Attach tab handler

    When someone clicks on a tab link we want our script to take action, so the first thing to do is to attach a click handler to each of those links. Another possible option could be to instead attach just one event handler to the #nav section which is useful to do when a large number of elements may be triggering events, but figuring out which element triggered what event can involve other complexities so we can stay away from that. Because there are just a few tab links to deal with and not 50 or 1000 tabs, attaching an event to each of them is in this case an effective and easy solution.

    Code javascript:
    function init() {
        var tabLinks = document.querySelectorAll('#tabs' + ' ' + '.nav a'),
            tabClickHandler = function tabClickHandler(evt) {
                evt = evt || window.event;
                helper.preventDefault(evt);
     
                handleTabClick(this);
            };
     
        helper.forEachElement(tabLinks, function (el) {
            el.addEventListener('click', tabClickHandler, false);
        });
     
        tabLinks[0].click();
    }
     
    init();

    Placing the code in an init function can be useful for us later on when it comes to extending the script, and the tabLinks[0].click(); line helps to provide a nice way to load first tab link.

    Tab click handler

    When someone clicks on one of the tab links, we don't want the web browser to navigate to that link, so we have to tell the web browser to prevent the default behaviour for that link. After we've done that we can then go ahead and do our tab click actions.

    Code javascript:
    var tabClickHandler = function tabClickHandler(evt) {
            evt = evt || window.event;
            preventDefault(evt);
     
            handleTabClick(this);
        };

    Tab click

    When someone clicks on a tab, we don't want to do anything if it is already the active tab. If though it is not currently active, we want to fade out the old content and fade in the new content. Because the fading is done by using a setInterval method to change the class name, the updateAndLoadTabHandler callback function can easily access the tabs and file info thanks to being defined from within the handleTabClick function.

    The updateAndLoadTabHandler is run after the content of a tab has faded out. It activates the new tab and load in the new tab content. It doesn't need to be passed any variables either because it already knows them thanks to a nice feature called closure, where when a function is defined it retains knowledge of the environment from its parent function.

    Code javascript:
    function handleTabClick(link) {
        var tabLinks = document.querySelectorAll('#tabs' + ' ' + '.nav a'),
            container = document.querySelector('#content'),
            updateAndLoadTabHandler = function () {
                var fileToLoad = link.href;
     
                updateActiveTab(tabLinks, link);
     
                microAjax(fileToLoad, function updateContent(response) {
                    container.innerHTML = response;
                });
            };
     
        if (helper.hasClass(link, 'active')) {
            return false;
        }
     
        fadeTransition(container, updateAndLoadTabHandler);
    }

    The updateAndLoadTabHandler calls two functions, updateActiveTab and microAjax, which has been recently covered in a previous post.

    Update active tab

    The updateActiveTab function loops through each tab removing the active class name from each of them, and then adds it back on to the new active tab.

    Code javascript:
    // because tabLinks is a node list, using an ordinary for loop
    // is best recommended for looping through a node list, so the
    // forEachElement function helps us to easily achieve that.
    function updateActiveTab(tabLinks, currentLink) {
        helper.forEachElement(tabLinks, function (el) {
            helper.removeClass(el, 'active');
        });
        helper.addClass(currentLink, 'active');
    }

    Fade tab

    The fadeTransition function helps to simplify the task of fading out one set of content and then fading in the next. The callback function contains all that is needed to be known to change the tab and load in the next lot of content, so it's just a matter of waiting for the fadeout to have finished before running the callback function and fading in the new content.

    Code javascript:
    // The fadeout effect is given time to occur before the new tab is loaded
    // thanks to a simple setTimeout delay
    function fadeTransition(el, callback) {
        var delay = 800;
     
        fadeOut(el);
        window.setTimeout(function () {
            callback();
            fadeIn(el);
        }, delay);
    }

    It's the CSS styling that determines how the fadeout occurs, and for how long, thanks to cross-browser transparancy support from CSS-Tricks and using CSS transitions.

    Code css:
    /* IE opacity compatibility code thanks to
       [url]http://css-tricks.com/css-transparency-settings-for-all-broswers/[/url]
    */
    #content.fade-out {
        zoom: 1;
        filter: alpha(opacity=0);
        opacity: 0;
    }
    #content.fade-in {
        zoom: 1;
        filter: alpha(opacity=100);
        opacity: 1;
    }
    /* vendor prefixes thanks to [url]http://cssprefixer.appspot.com/[/url] */
    #content { 
        -moz-transition: opacity 0.5s ease-in;
        -o-transition: opacity 0.5s ease-in;
        -webkit-transition: opacity 0.5s ease-in;
        transition: opacity 0.5s ease-in;
    }

    Fade tab

    Finally, we fade the content by updating the class name and allowing CSS to perform the fading for us.

    There are a few older web browsers that the fade transition won't be supported on, but as it's a non-vital feature, it's okay for older web browsers to go without. A large amount of extra code would be required to provide older browsers with support for fading, which in this case is not worth the additional cost.

    Code javascript:
    // The fading transition is handled by a CSS transition effect
    function fadeOut(el) {
        helper.removeClass(el, 'fade-in');
        helper.addClass(el, 'fade-out');
    }
     
    function fadeIn(el) {
        helper.removeClass(el, 'fade-out');
        helper.addClass(el, 'fade-in');
    }

    And that brings us to an end of the native JavaScript code to load the tabs content from an AJAX source.

    While there are a lot of script files involved here coming to a total of 7, it's easily maintainable code that works on a wide range of web browsers. When it comes time to putting things in to production for a live web site, all of the script files can be easily compressed, for example by using the Online JavaScript Compression Tool

    Download

    Please feel free to download the vanilla tabs code and have an explore.

    The ajax requests will work locally on all web browsers except for Google Chrome, for which you may want to instead use a local web server such as EasyPHP so that you can then place the code in its local web location, and get to it by loading up http://localhost/

    And with Internet Explorer you will need to give permission for the local script to work, by clicking Allow Blocked Content.

    Good luck, and have fun exploring!
    Programming Group Advisor
    Reference: JavaScript, Quirksmode Validate: HTML Validation, JSLint
    Car is to Carpet as Java is to JavaScript

  22. #22
    The CSS Clinic is open silver trophybronze trophy
    Paul O'B's Avatar
    Join Date
    Jan 2003
    Location
    Hampshire UK
    Posts
    39,797
    Mentioned
    158 Post(s)
    Tagged
    3 Thread(s)
    Thanks Paul (and Pullo) for an interesting quiz and good explanations. There's a lot to digest so I'll have to work my way through it slowly

    Quote Originally Posted by Paul
    et, all too often we see jQuery being used to do something that vanilla JavaScript could do just as well, if not better.
    In some ways, for amateurs in JS like me, I think you just proved the case for jquery rather than the opposite . I was expecting the vanilla js version to be just a few lines longer but it was actually quite involved when you get into the detail. However, I appreciate that the JS version is obviously a lot shorter when you compare it to the jquery library itself.

  23. #23
    Unobtrusively zen silver trophybronze trophy
    paul_wilkins's Avatar
    Join Date
    Jan 2007
    Location
    Christchurch, New Zealand
    Posts
    14,526
    Mentioned
    83 Post(s)
    Tagged
    3 Thread(s)
    Quote Originally Posted by Paul O'B View Post
    In some ways, for amateurs in JS like me, I think you just proved the case for jquery rather than the opposite . I was expecting the vanilla js version to be just a few lines longer but it was actually quite involved when you get into the detail. However, I appreciate that the JS version is obviously a lot shorter when you compare it to the jquery library itself.
    This sounds like a good jumping off point to demonstrate a much smaller vanilla solution that works across all modern web browsers.
    I'll see what I can come up with by way of demonstration.
    Programming Group Advisor
    Reference: JavaScript, Quirksmode Validate: HTML Validation, JSLint
    Car is to Carpet as Java is to JavaScript

  24. #24
    Unobtrusively zen silver trophybronze trophy
    paul_wilkins's Avatar
    Join Date
    Jan 2007
    Location
    Christchurch, New Zealand
    Posts
    14,526
    Mentioned
    83 Post(s)
    Tagged
    3 Thread(s)
    Quote Originally Posted by paul_wilkins View Post
    This sounds like a good jumping off point to demonstrate a much smaller vanilla solution that works across all modern web browsers.
    I'll see what I can come up with by way of demonstration.
    Here's a fairly condensed version of the above code in just 23 lines. All it needs is the microAjax library.

    Code javascript:
    Array.prototype.forEach.call(document.querySelectorAll('.tab a'), function (el) {
        el.onclick = function (evt) {
            var link = this, content = document.querySelector('#content');
            if (!link.classList.contains('active')) {
                content.classList.remove('fade-in');
                content.classList.add('fade-out');
     
                window.setTimeout(function () {
                    Array.prototype.forEach.call(document.querySelectorAll(".tab a"), function (el) {
                        el.classList.remove('active');
                    });
                    link.classList.add('active');
     
                    microAjax(link.href, function (response) {
                        content.innerHTML = response;
                    });
     
                    content.classList.remove('fade-out');
                    content.classList.add('fade-in');
                }, 800);
            }
            return false;
        };
    });
    document.querySelector('.tab a').click();

    It's even shorter than the jQuery code too, but just because jQuery code can be done with modern vanilla JavaScript doesn't I think mean that we should aim at writing code in the same manner as jQuery.

    I think that we can write code that is better and more understandable than jQuery.
    Programming Group Advisor
    Reference: JavaScript, Quirksmode Validate: HTML Validation, JSLint
    Car is to Carpet as Java is to JavaScript

  25. #25
    The CSS Clinic is open silver trophybronze trophy
    Paul O'B's Avatar
    Join Date
    Jan 2003
    Location
    Hampshire UK
    Posts
    39,797
    Mentioned
    158 Post(s)
    Tagged
    3 Thread(s)
    Quote Originally Posted by paul_wilkins View Post
    Here's a fairly condensed version of the above code in just 23 lines. All it needs is the microAjax library.
    Thanks Paul, that certainly looks a lot neater and more succinct.

    The classlist object certainly makes things easier ( although it seems was influenced by jquery a little). It would seem to be good if JS adopted some of these "shortcuts" that libraries produce to make life easier for the likes of me


Tags for this Thread

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •