// Use keyboard to navigate through DOM elements marked with a specific class name
// Requires Prototype, http://prototypejs.org/

// Steffen Tiedemann Christensen
// steffen@23company.com
// http://www.23company.com

// Please note: The approach the detection the nearest focus neighbour here is by no means fool-proof, it is merely simple.
// If you find yourself in a situation with many overlapping or non-symmetric objects, you will probably need to
// ammend the code to be based non only on direction, but also on distance.

// Usage
// new KeyboardFocus();
// new KeyboardFocus('focusable', 'focused', 'hovered');
var KeyboardFocus = Class.create({
        initialize: function(focusableClassName, focusedClassName, hoveredClassName) {
            // Set up standard classes
            this.focusableClassName = (typeof(focusableClassName)!='undefined' ? focusableClassName : 'focusable'),
            this.focusedClassName = (typeof(focusedClassName)!='undefined' ? focusedClassName : 'focused'),
            this.hoveredClassName = (typeof(hoveredClassName)!='undefined' ? hoveredClassName : 'hovered'),
            // Listen to up, down, left and right key presses
            Event.observe(document, 'keydown', function(e){
                    switch (e.keyCode){
                    case 38: this.search('up'); Event.stop(e); break;
                    case 40: this.search('down'); Event.stop(e); break;
                    case 37: this.search('left'); Event.stop(e); break;
                    case 39: this.search('right'); Event.stop(e); break;
                    case 13: $$('.'+this.focusedClassName).each(function(el){el.click();}); Event.stop(e); break;
                    }
                }.bind(this));
            // Do the work
            this.update();
            document.observe('dom:loaded', function(){this.update();}.bind(this));
            // And return the object
            return(this);
        },
        update: function(){
            // Make focusable elements both clickable and hovereable
            $$('.'+this.focusableClassName).each(function(el){
                    el.observe('click', function(){this.setFocus(el);}.bind(this));
                    el.observe('mouseover', function(){el.addClassName('hovered')}.bind(this));
                    el.observe('mouseout', function(){el.removeClassName('hovered')}.bind(this));
                }.bind(this));
            // Focus somewhere by default
            if($$('.'+this.focusedClassName).length==0) {this.search('down'); this.search('up');}
        },
        setFocus: function(block){
            // Remove current focus
            $$('.'+this.focusedClassName).each(function(el){
                    el.removeClassName(this.focusedClassName);
                }.bind(this));
            // Add new focus to specified object
            $(block).addClassName(this.focusedClassName);
            $(block).focus();
        },
        search: function(direction, element, padding) {
            // Default padding to 0
            if(typeof(padding)=='undefined') padding = 0;
            // Default element is the currently selected one
            if(typeof(element)=='undefined') {
                var focused = $$('.'+this.focusedClassName);
                if(focused.length>0) element=focused[0];
            }
            if(typeof(element)!='undefined') {
                // There an element to begin from; use to to set up scope/range for the search
                var dim = $(element).getDimensions();
                var pos = $(element).positionedOffset();
                switch (direction) {
                case 'up':
                case 'down':
                    var range = [pos.left-padding, pos.left+dim.width+padding];
                    var offset = pos.top;
                    break;
                case 'left':
                case 'right':
                    var range = [pos.top-padding, pos.top+dim.height+padding];
                    var offset = pos.left;
                    break;
                }
            } else {
                // There's no currently selected element; begin from the upper left-hand corner
                var range = [0,50+padding];
                var offset = 0;
            }

            // Do the search by looping through all focusable elements, checking if they're in range of the search, and then selecting the closest item
            var match = null;
            var matchOffset = null;
            $$('.'+this.focusableClassName).each(function(el){
                    var d = el.getDimensions();
                    var p = el.positionedOffset();
                    switch (direction) {
                    case 'up':
                        if(p.left<=range[1] && p.left+d.width>=range[0] && p.top<offset) {
                            if (matchOffset==null || p.top>matchOffset) {
                                matchOffset = p.top;
                                match = el;
                            }
                        }
                        break;
                    case 'down':
                        if(p.left<=range[1] && p.left+d.width>=range[0] && p.top>offset) {
                            if (matchOffset==null || p.top<matchOffset) {
                                matchOffset = p.top;
                                match = el;
                            }
                        }
                        break;
                    case 'left':
                        if(p.top<=range[1] && p.top+d.height>=range[0] && p.left<offset) {
                            if (matchOffset==null || p.left>matchOffset) {
                                matchOffset = p.left;
                                match = el;
                            }
                        }
                        break;
                    case 'right':
                        if(p.top<=range[1] && p.top+d.height>=range[0] && p.left>offset) {
                            if (matchOffset==null || p.left<matchOffset) {
                                matchOffset = p.left;
                                match = el;
                            }
                        }
                        break;
                    }
                });

            if(match!=null) {
                // We have a match! Select it and return success
                if(match!=null) this.setFocus(match);
                return(true);
            } else {
                // There are no elements matching our search. Widen the scapo/range and try again for a while.
                if(padding>=1000) return(false);
                padding += 60;
                return(this.search(direction, element, padding));
            }
        }
    });