views:

447

answers:

2

How to make Keyboard navigation left/up/right/down (like for photo gallery) feature for jQury Tabs with History? Demo without Keyboard feature in http://dl.dropbox.com/u/6594481/tabs/index.html

Needed functions:
1. on keyboardtop/down> make select and CSS showactivenested ajax tabs from 1-st to last level
2. on keyboardleft/right> changeback/forwardcontent ofactivenested ajax tabs tab
3. an extra option, makeactivenested ajax tab on 'cursor-on' on concrete nested ajax tabs level

Read more detailed question with example pictures in http://stackoverflow.com/questions/2975003/jquery-tools-to-make-keyboard-and-cookies-feature-for-ajaxed-tabs-with-history

/**
 * @license 
 * jQuery Tools @VERSION Tabs- The basics of UI design.
 * 
 * NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
 * 
 * http://flowplayer.org/tools/tabs/
 *
 * Since: November 2008
 * Date: @DATE 
 */  
(function($) {

    // static constructs
    $.tools = $.tools || {version: '@VERSION'};

    $.tools.tabs = {

        conf: {
            tabs: 'a',
            current: 'current',
            onBeforeClick: null,
            onClick: null, 
            effect: 'default',
            initialIndex: 0,            
            event: 'click',
            rotate: false,

            // 1.2
            history: false
        },

        addEffect: function(name, fn) {
            effects[name] = fn;
        }

    };

    var effects = {

        // simple "toggle" effect
        'default': function(i, done) { 
            this.getPanes().hide().eq(i).show();
            done.call();
        }, 

        /*
            configuration:
                - fadeOutSpeed (positive value does "crossfading")
                - fadeInSpeed
        */
        fade: function(i, done) {        

            var conf = this.getConf(),            
                 speed = conf.fadeOutSpeed,
                 panes = this.getPanes();

            if (speed) {
                panes.fadeOut(speed);    
            } else {
                panes.hide();    
            }

            panes.eq(i).fadeIn(conf.fadeInSpeed, done);    
        },

        // for basic accordions
        slide: function(i, done) {
            this.getPanes().slideUp(200);
            this.getPanes().eq(i).slideDown(400, done);             
        }, 

        /**
         * AJAX effect
         */
        ajax: function(i, done)  {            
            this.getPanes().eq(0).load(this.getTabs().eq(i).attr("href"), done);    
        }        
    };       

    var w;

    /**
     * Horizontal accordion
     * 
     * @deprecated will be replaced with a more robust implementation
     */
    $.tools.tabs.addEffect("horizontal", function(i, done) {

        // store original width of a pane into memory
        if (!w) { w = this.getPanes().eq(0).width(); }

        // set current pane's width to zero
        this.getCurrentPane().animate({width: 0}, function() { $(this).hide(); });

        // grow opened pane to it's original width
        this.getPanes().eq(i).animate({width: w}, function() { 
            $(this).show();
            done.call();
        });

    });    


    function Tabs(root, paneSelector, conf) {

        var self = this, 
             trigger = root.add(this),
             tabs = root.find(conf.tabs),
             panes = paneSelector.jquery ? paneSelector : root.children(paneSelector),             
             current;


        // make sure tabs and panes are found
        if (!tabs.length)  { tabs = root.children(); }
        if (!panes.length) { panes = root.parent().find(paneSelector); }
        if (!panes.length) { panes = $(paneSelector); }


        // public methods
        $.extend(this, {                
            click: function(i, e) {

                var tab = tabs.eq(i);                                                 

                if (typeof i == 'string' && i.replace("#", "")) {
                    tab = tabs.filter("[href*=" + i.replace("#", "") + "]");
                    i = Math.max(tabs.index(tab), 0);
                }

                if (conf.rotate) {
                    var last = tabs.length -1; 
                    if (i < 0) { return self.click(last, e); }
                    if (i > last) { return self.click(0, e); }                        
                }

                if (!tab.length) {
                    if (current >= 0) { return self; }
                    i = conf.initialIndex;
                    tab = tabs.eq(i);
                }                

                // current tab is being clicked
                if (i === current) { return self; }

                // possibility to cancel click action                
                e = e || $.Event();
                e.type = "onBeforeClick";
                trigger.trigger(e, [i]);                
                if (e.isDefaultPrevented()) { return; }

                // call the effect
                effects[conf.effect].call(self, i, function() {

                    // onClick callback
                    e.type = "onClick";
                    trigger.trigger(e, [i]);                    
                });            

                // default behaviour
                current = i;
                tabs.removeClass(conf.current);    
                tab.addClass(conf.current);                

                return self;
            },

            getConf: function() {
                return conf;    
            },

            getTabs: function() {
                return tabs;    
            },

            getPanes: function() {
                return panes;    
            },

            getCurrentPane: function() {
                return panes.eq(current);    
            },

            getCurrentTab: function() {
                return tabs.eq(current);    
            },

            getIndex: function() {
                return current;    
            }, 

            next: function() {
                return self.click(current + 1);
            },

            prev: function() {
                return self.click(current - 1);    
            }        

        });

        // callbacks    
        $.each("onBeforeClick,onClick".split(","), function(i, name) {

            // configuration
            if ($.isFunction(conf[name])) {
                $(self).bind(name, conf[name]); 
            }

            // API
            self[name] = function(fn) {
                $(self).bind(name, fn);
                return self;    
            };
        });


        if (conf.history && $.fn.history) {
            $.tools.history.init(tabs);
            conf.event = 'history';
        }    

        // setup click actions for each tab
        tabs.each(function(i) {                 
            $(this).bind(conf.event, function(e) {
                self.click(i, e);
                return e.preventDefault();
            });            
        });

        // cross tab anchor link
        panes.find("a[href^=#]").click(function(e) {
            self.click($(this).attr("href"), e);        
        }); 

        // open initial tab
        if (location.hash) {
            self.click(location.hash);
        } else {
            if (conf.initialIndex === 0 || conf.initialIndex > 0) {
                self.click(conf.initialIndex);
            }
        }                

    }


    // jQuery plugin implementation
    $.fn.tabs = function(paneSelector, conf) {

        // return existing instance
        var el = this.data("tabs");
        if (el) { return el; }

        if ($.isFunction(conf)) {
            conf = {onBeforeClick: conf};
        }

        // setup conf
        conf = $.extend({}, $.tools.tabs.conf, conf);        

        this.each(function() {                
            el = new Tabs($(this), paneSelector, conf);
            $(this).data("tabs", el); 
        });        

        return conf.api ? el: this;        
    };        

}) (jQuery); 

/**
 * @license 
 * jQuery Tools @VERSION History "Back button for AJAX apps"
 * 
 * NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
 * 
 * http://flowplayer.org/tools/toolbox/history.html
 * 
 * Since: Mar 2010
 * Date: @DATE 
 */
(function($) {

    var hash, iframe, links, inited;        

    $.tools = $.tools || {version: '@VERSION'};

    $.tools.history = {

        init: function(els) {

            if (inited) { return; }

            // IE
            if ($.browser.msie && $.browser.version < '8') {

                // create iframe that is constantly checked for hash changes
                if (!iframe) {
                    iframe = $("<iframe/>").attr("src", "javascript:false;").hide().get(0);
                    $("body").append(iframe);

                    setInterval(function() {
                        var idoc = iframe.contentWindow.document, 
                             h = idoc.location.hash;

                        if (hash !== h) {                        
                            $.event.trigger("hash", h);
                        }
                    }, 100);

                    setIframeLocation(location.hash || '#');
                }


            // other browsers scans for location.hash changes directly without iframe hack
            } else { 
                setInterval(function() {
                    var h = location.hash;
                    if (h !== hash) {
                        $.event.trigger("hash", h);
                    }                        
                }, 100);
            }

            links = !links ? els : links.add(els);

            els.click(function(e) {
                var href = $(this).attr("href");
                if (iframe) { setIframeLocation(href); }

                // handle non-anchor links
                if (href.slice(0, 1) != "#") {
                    location.href = "#" + href;
                    return e.preventDefault();        
                }

            }); 

            inited = true;
        }    
    };  


    function setIframeLocation(h) {
        if (h) {
            var doc = iframe.contentWindow.document;
            doc.open().close();    
            doc.location.hash = h;
        }
    } 

    // global histroy change listener
    $(window).bind("hash", function(e, h)  { 
        if (h) {
            links.filter(function() {
              var href = $(this).attr("href");
              return href == h || href == h.replace("#", ""); 
            }).trigger("history", [h]);    
        } else {
            links.eq(0).trigger("history", [h]);    
        }

        hash = h;
    window.location.hash = hash;
    });


    // jQuery plugin implementation
    $.fn.history = function(fn) {

        $.tools.history.init(this);

        // return jQuery
        return this.bind("history", fn);        
    };    

})(jQuery);
$(function() {
$("#list").tabs("#content > div", {effect: 'ajax', history: true});
});​
A: 

You can bind actions to key presses by using

$.keyup(function(e) {
    key = e.which;
    if (key == 37) //code for left arrow key
    {
        ...
    }
});

A quick google search got me this list of key codes: http://www.cambiaresearch.com/c4/702b8cd1-e5b0-42e6-83ac-25f0306e3e25/Javascript-Char-Codes-Key-Codes.aspx

colinmarc
+2  A: 

Here is how I would add the keyboard functionality to your code.

But, I want to add that after looking at pretty much the same question you posted two days prior to this one, that this site is set up to help you find problems in your code, not to write it all for you. If you want someone to write all the code, hire someone to do it or you'll probably have to learn how to do it yourself (with troubleshooting help from us as needed).

Anyway, I set up this demo to help you get started. Hopefully there are enough comments that you can understand what I was doing and you can take it from there.

$(function() {
 var list = $('#list'),
     imgPerRow = -1,
     loop = true;
 // find first image y-offset to find the number of images per row
 var topOffset = list.find('img:eq(0)').offset().top,
     numTabs = list.find('a').length - 1,
     current, newCurrent;

 function changeTab(diff){
  current = list.find('a.current').index();
  newCurrent = (loop) ? (current + diff + numTabs + 1) % (numTabs + 1) : current + diff;
  if (loop) {
   if (newCurrent > numTabs) { newCurrent = 0; }
   if (newCurrent < 0) { newCurrent = numTabs; }
  } else {
   if (newCurrent > numTabs) { newCurrent = numTabs; }
   if (newCurrent < 0) { newCurrent = 0; }
  }
  // don't trigger change if tab hasn't changed (for non-looping mode)
  if (current != newCurrent) {
   list.find('a').eq(newCurrent).trigger('click'); // trigger click on tab
  }
 }

 list
  // set up tabs
  .tabs("#content > div", {effect: 'ajax', history: true})

  // find number of images on first row
  .find('img').each(function(i){
   if (imgPerRow < 0 && $(this).offset().top > topOffset ) {
    imgPerRow = i;
   }
  });

  // Set up arrow keys
  // Set to document for demo, probably better to use #list in the final version.
  $(document).bind('keyup', function(e){
   var key = e.which;
   if (key > 36 && key < 41) {
    if (key == 37) { changeTab(-1); }         // Left
    if (key == 38) { changeTab(-imgPerRow); } // Up
    if (key == 39) { changeTab(+1); }         // Right
    if (key == 40) { changeTab(+imgPerRow); } // Down
    e.preventDefault();
   }
  });

 // toggle looping through tabs
 $(':button').click(function(){
  loop = !loop;
  $('#loopStatus').text(loop);
 });

});
fudgey
@fudgey: I asked other developers to make this feature but sadly they where to busy. I very appreciate that you give me the answer. Thank you a lot! I change `keyup focus` to `keypress focus` otherwise Firebug shows 'The 'charCode' property of a keyup event should not be used. The value is meaningless.' Unfortunately it still shows warning **Unknown pseudo-class or pseudo-element 'button'.** Do you know to fix it? Again thanks!
Binyamin
Looks like with `keypress focus` it fully works just on Firefox, not on IE, Safari, Chrome.
Binyamin
You shouldn't need focus in the bind... I was trying to see if I could get the tab key to work and I accidentally left that in. As for using keypress and keycode in Firefox, check out this Quirksmode entry (http://www.quirksmode.org/js/keys.html) - so `keyup` is a better choice than `keypress`
fudgey
@fudgey: O.K. So to ignore Firebug warning "The `charCode` property of a keyup event should not be used." and to keep to use `keyup`!?
Binyamin
I don't know why you are getting that warning, the code above isn't using `charCode` at all. So you might want to check your other code.
fudgey
It is an warning that appears to `keyup` and `keydown`, and not on `keypress`. I have read that on changing `event.keyCode` and `event.charCode` to `event.which` must fix that warning, but it does not work for me.
Binyamin
Hmmm, I think you are right... try this new demo (http://jsfiddle.net/zTevK/2/) and see if you get the error.
fudgey
Anyway Firebug v1.5.4 shows JS warning : "The 'charCode' property of a `keyup` event should not be used. The value is meaningless.", http://jsfiddle.net/zTevK/2.
Binyamin
Looks like there is no solution for Firebug warning http://stackoverflow.com/questions/2996986/firebug-js-warning-to-jquery-1-4-2-the-charcode-property-of-a-keyup-event-shou
Binyamin
Ahhh ok, I was never seeing the error because I have those warnings turned off. LOL thanks for finding out :)
fudgey