4
\$\begingroup\$

I've started working on a script that turns sections with headings into a tabbed interface.

The repo is at https://github.com/derekjohnson/tabs and a demo at http://derekjohnson.github.io/tabs/

It works in IE8 up, although I haven't fully tested it on all the devices I have access to.

I'm particularly interested in feedback on the tabs.js file, and welcome all comments.

As I mentioned in the README I know it could do with a config object, but I've never done one so I'll be learning on this project. Any tips on that gratefully received too.

tabs.js:

(function(win, doc, undefined) {
'use strict';

// Quick feature test
if('querySelector' in doc) {

    var tabs = function() {

        /* Helper functions
           ========================================================================== */

        // Cross browser events
        var add_event = function(el, ev, fn) {
            'addEventListener' in win ? 
                el.addEventListener(ev, fn, false) : 
                el.attachEvent('on' + ev, fn);
        };

        // Faster class selectors
        // http://jsperf.com/queryselector-vs-getelementsbyclassname-0
        var get_single_by_class = function(className) {
            return 'getElementsByClassName' in doc ? 
                doc.getElementsByClassName(className)[0] : 
                doc.querySelector('.' + className);
        }

        //http://jsperf.com/byclassname-vs-queryselectorall
        var get_many_by_class = function(className) {
            return 'getElementsByClassName' in doc ? 
                doc.getElementsByClassName(className) : 
                doc.querySelectorAll('.' + className);
        }



        /* Feature detect for localStorage courtesy of 
           http://mathiasbynens.be/notes/localstorage-pattern
           ========================================================================== */
        var storage,
            fail,
            uid;

        try {
            uid = new Date;
            (storage = win.localStorage).setItem(uid, uid);
            fail = storage.getItem(uid) != uid;
            storage.removeItem(uid);
            fail && (storage = false);
        } catch(e) {}



        /* DOM nodes we'll need
           ========================================================================== */

        var wrapper = get_single_by_class('js-tab-ui'),
            panels = get_many_by_class('js-panel'),
            tab_names = get_many_by_class('js-panel__title'),
            i,
            ii = panels.length;



        /* Show hide the panels, update the tabs' attributes
           ========================================================================== */

        var show_hide = function(x_id) {
            for(i=0; i<ii; i++) {
                // display the correct panel, hide the others
                if(panels[i].getAttribute('aria-labelledby') === x_id) {
                    panels[i].style.display = 'block';
                } else {
                    panels[i].style.display = 'none';
                }

                // update the ARIA
                if(items[i].id === x_id) {
                    items[i].setAttribute('aria-selected', 'true');
                } else {
                    items[i].setAttribute('aria-selected', 'false');
                }
            }

            // put the tab id into localStorage
            if(storage) {
                localStorage['tab'] = x_id;
            }
        }



        /* When a tab has been clicked
           ========================================================================== */

        var clicked = function(event) {
            var x,
                x_id;

            typeof event.target !== 'undefined' ?
                x = event.target :
                x = event.srcElement;

            if(x.nodeName.toLowerCase() === 'li') {
                // get the id of the clicked tab
                x_id = x.id;
            } else {
                return; // stop clicks on the <ul> hiding everything
            }

            show_hide(x_id);
        };



        /* Keyboard interaction
           ========================================================================== */
        var kbd = function(event) {
            var x,
                x_id,
                key_code,
                next,
                prev;

            event = event || win.event;

            key_code = event.keyCode || event.which;

            typeof event.target !== 'undefined' ?
                x = event.target :
                x = event.srcElement;

            // up or right arrow key moves focus to the next tab
            if(key_code === 38 || key_code === 39) {
                next = x.nextSibling;

                // make sure we're on an element node
                if(next.nodeType !== 1) {
                    next = next.nextSibling;
                }

                next.setAttribute('tabindex', 0);
                next.focus();
            }

            // left or down arrow key moves focus to the previous tab
            if(key_code === 37 || key_code === 40) {
                prev = x.previousSibling;

                // make sure we're on an element node
                if(prev.nodeType !== 1) {
                    prev = prev.previousSibling;
                }

                prev.setAttribute('tabindex', 0);
                prev.focus();
            }

            // space bar
            if(key_code === 32) {
                show_hide(x.id);
            }

            // Prevent space bar moving the page down
            event.preventDefault ? event.preventDefault() : event.returnValue = false;
        }



        /* Create each tab item
           ========================================================================== */

        var build_tab = function(el, text, classification) {
            el.innerHTML = text;
            el.className = classification;
            el.setAttribute('role', 'tab');

            return el;
        };



        /* Make an empty list that will hold the tabs
           ========================================================================== */

        var frag = doc.createDocumentFragment(),
            tabs = doc.createElement('ul');

        // Basic attributes for the list
        tabs.className = 'product-tabs';
        tabs.setAttribute('role', 'tablist');



        /* Build each tab and add all required attributes to tabs & panels
           ========================================================================== */

        var items = [];

        for(i=0; i<ii; i++) {
            var li = build_tab(doc.createElement('li'), tab_names[i].innerHTML, 'product-tabs__item');

            // Add unique attributes to each list item
            li.id = 'tab' + (i + 1);
            li.setAttribute('aria-controls', panels[i].id);

            if(i === 0) {
                li.setAttribute('tabindex', 0);
                li.setAttribute('aria-selected', 'true');
            } else {
                li.setAttribute('aria-selected', 'false');
            }

            // Stick them into the document fragment
            frag.appendChild(li);

            // Stick them into the items array
            items[i] = li;

            // Panels
            panels[i].setAttribute('role', 'tabpanel');
            panels[i].setAttribute('aria-labelledby', 'tab' + (i + 1));
        }



        /* Insert the tabs into the DOM
           ========================================================================== */

        tabs.appendChild(frag);

        wrapper.insertBefore(tabs, get_single_by_class('js-panel'));



        /* Listen for clicks on the tab list
           ========================================================================== */
        add_event(tabs, 'click', clicked);



        /* Listen for key presses
           ========================================================================== */
        add_event(tabs, 'keydown', kbd);



        /* If a tab id is in localStorage open the corresponding panel
           ========================================================================== */

        if(storage && localStorage['tab']) {
            show_hide(localStorage['tab']);
        }
    };

    // Make all that happen
    tabs();

} else {
    return;
}
})(this, this.document);
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

From a once over:

  • In general, impressive code
  • The bottom part of your code is too stretched vertically, this:

        /* Insert the tabs into the DOM
           ========================================================================== */
    
        tabs.appendChild(frag);
    
        wrapper.insertBefore(tabs, get_single_by_class('js-panel'));
    
    
    
        /* Listen for clicks on the tab list
           ========================================================================== */
        add_event(tabs, 'click', clicked);
    
    
    
        /* Listen for key presses
           ========================================================================== */
        add_event(tabs, 'keydown', kbd);
    
    
    
        /* If a tab id is in localStorage open the corresponding panel
           ========================================================================== */
    
        if(storage && localStorage['tab']) {
            show_hide(localStorage['tab']);
        }
    };
    
    // Make all that happen
    tabs();
    

    could have been this:

        // Insert the tabs into the DOM
        tabs.appendChild(frag);
        wrapper.insertBefore(tabs, get_single_by_class('js-panel'));
        // Listen for clicks on the tab list
        add_event(tabs, 'click', clicked);
        // Listen for key presses
        add_event(tabs, 'keydown', kbd);
        // If a tab id is in localStorage open the corresponding panel
        if(storage && localStorage['tab']) {
            show_hide(localStorage['tab']);
        }
    };
    
    // Make all that happen
    tabs();
    
  • I am pretty sure this

        typeof event.target !== 'undefined' ?
            x = event.target :
            x = event.srcElement;
    

    could be

        x = event.target || event.srcElement;
    
  • The bigger picture is that this:

    /* When a tab has been clicked
       ========================================================================== */
    
    
    var clicked = function(event) {
        var x,
            x_id;
    
        typeof event.target !== 'undefined' ?
            x = event.target :
            x = event.srcElement;
    
        if(x.nodeName.toLowerCase() === 'li') {
            // get the id of the clicked tab
            x_id = x.id;
        } else {
            return; // stop clicks on the <ul> hiding everything
        }
    
        show_hide(x_id);
    };
    

    could be

    // When a tab has been clicked
    var onClicked = function(event) {
        var clickedElement = event.target || event.srcElement;
    
        if(clickedElement.nodeName.toLowerCase() === 'li') {
          show_hide(clickedElement.id);
        }
    };
    
  • You are not following lowerCamelCase
  • You should used named constants for the keycodes key_code === LEFT_ARROW reads better than key_code === 37

On the whole, I think you should review your code to reduce the line count. I am not saying that you should play CodeGolf, but this could be written in a fewer lines while keeping the same level of quality.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you, it looks like I need to think a bit smarter to trim the fat. I had no idea keycodes had named constants, I'd never seen that before. Re. the lowerCamelCase, my understanding is that it's a matter of taste? \$\endgroup\$ Apr 13, 2014 at 20:05
2
\$\begingroup\$
  • I'm not a big fan of the general code structure. All the DOM helper functions should be extracted into a separate module to avoid code duplication, since other scripts will need the same/similar functions. Is there any special reason you are not using an existing DOM library?

  • I know it's a bit due to the fact how JavaScript works, but there is a lot a scrolling involved when you want to follow the flow of the program: First scroll all the way down, until you find the start point (tabs();), then scroll all the way back up looking for the definition of tab and finally scroll almost all the way back down to find they place where it there is actually runable code. (Not having all the DOM helper functions inside the main function would help here a lot).

  • Your code can only handle a single set of tabs on a page, which is very limiting.

  • ii is a terrible variable name.

\$\endgroup\$
3
  • \$\begingroup\$ Thank you, some great tips there. I hadn't thought of more that one set of tabs, I'll look to fix that. I didn't use a lib to see if I could get away with it, it's a learning exercise too. I got a wee telling off on twitter for ii too, so that's going. Thanks again :) \$\endgroup\$ Apr 13, 2014 at 20:00
  • \$\begingroup\$ "DOM helper functions should be extracted into a separate module". Good choice of words, it led me to figuring out the module pattern. Thank you :) \$\endgroup\$ Apr 17, 2014 at 10:48
  • \$\begingroup\$ I though you already knew about module patterns, since your code uses one :) \$\endgroup\$
    – RoToRa
    Apr 17, 2014 at 14:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.