Return to Snippet

Revision: 31906
at September 15, 2010 19:48 by dom111


Initial Code
(function($) {
  /**
   * toParent
   * 
   * Returns the number of nodes (or steps) to the specified elements
   * 
   * @param string dest The CSS selector of the target element
   * @param string step The CSS selector of each 'step' (optional)
   * @return integer The number of steps from the current element to dest, or null on error
   */
  $.fn.toParent = function(dest, step) {
    var cur = $(this), i = 0, max = 200;
    
    while (i < max) {
      if ($(cur).is(dest)) {
        return i;
      }
      
      if ($(cur).length == 0) {
        return null;
      }
      
      if (step) {
        while (true) {
          cur = $(cur).parent();

          if ($(cur).is(step) || $(cur).is(dest) || !$(cur).length) {
            break;
          }
        }
        
      } else {
        cur = $(cur).parent();
      }
      
      i++;
    }
    
    return null;
  }
  
  /**
   * dropdownify
   * 
   * Creates a multi-level drop-down menu using the current element as the base
   * 
   * @param object options (optional)
   * @return object The jQuery object for chaining
   */
  $.fn.dropdownify = function(options) {
    options = $.extend({
      'delay': 100,
      'items': 'li',
      'menus': 'ul',
      'onhide': null,
      'onshow': null,
      'position': {
        // level (number: 1 being top level, all others being the respective level deep): position (string: top, bottom, left, right)
        1: 'bottom',
        'default': 'right'
      },
      'timeout': 750,
      'zIndex': 200
    }, options || {});
    
    // store the current selector, just in case
    options.selector = this.selector;
    
    // add an identifier to the root element
    $(this).addClass('dropdownify-root');
    $(this).find(options.items).each(function(i, el) {
      if ($(el).find(options.menus).length) {
        $(el).addClass('submenu dropdownify-submenu');
      }
    });
    
    // returns an object to be passed to $.css() for positioning
    var getPos = function(el) {
      // get the deptch of the current element
      var level = $(el).toParent('.dropdownify-root', options.menus);
      
      // check the positioning options
      if (typeof options.position == 'object') {
        // if it's an object, check for the current depth
        if (level in options.position) {
          var or = options.position[level];
          
        // or use the default
        } else {
          var or = options.position['default'];
        }
      
      // just use it if it's not an object
      } else {
        var or = options.position;
      }
      
      // if the orientation item is also an object
      if (typeof or == 'object') {
        // use the .v .h and .p values
        var offsetV = ('v' in or) ? or.v : 0;
        var offsetH = ('h' in or) ? or.h : 0;
        or = ('p' in or) ? or.p : 'bottom';
      
      // use the defaults
      } else {
        var offsetV = 0;
        var offsetH = 0;
      }
      
      // return the object
      return (function(or) {
        switch (or) {
          case 'bottom':
            return {
              'top': ($(el).height() + offsetV) + 'px',
              'left': (0 + offsetH) + 'px'
            };
            break;
            
          case 'top':
            return {
              'bottom': ($(el).height() + offsetV) + 'px',
              'left': (0 + offsetH) + 'px'
            };
            break;
            
          case 'left':
            return {
              'top': (0 + offsetV) + 'px',
              'right': ($(el).width() + offsetH) + 'px'
            };
            break;
            
          case 'right':
            return {
              'top': (0 + offsetV) + 'px',
              'left': ($(el).width() + offsetH) + 'px'
            };
            break;
        }
      })(or);
    }
    
    // find all 'items' in the current node
    $(this).find(options.items).each(function(i, el) {
      // hide all the submenus
      $(el).css({
        'position': 'relative',
        'overflow': 'visible'
      }).children(options.menus).css({
        'display': 'none',
        'position': 'absolute'
      });
      
      // store the options locally
      $(el).data('dropdownify', options);
      
      // hover in function
      $(el).mouseenter(function() {
        // hide any existing ones if we're top-level
        if ($(this).toParent('.dropdownify-root', options.menus) == 1) {
          $('.dropdownify-root').find(options.menus).css({
            'display': 'none',
            'visibility': 'hidden'
          });
        }
        
        // clear the close timeout
        window.clearTimeout($(this).data('dropdownify').timeoutClose);
        
        // store the timeout function for possible clearing
        $(this).data('dropdownify').timeoutOpen = window.setTimeout(function() {
          var object = this;
          
          return function() {
            return function() {
              $(this).children($(this).data('dropdownify').menus).addClass('dropdownify-current').css({
                'z-index': (options.zIndex + $(this).toParent('.dropdownify-root', options.menus)),
                'display': 'block',
                'visibility': 'visible',
                'width': $(this).width()
              }).css(getPos(this));
              
              if (options.onshow) {
                try {
                  options.onshow.apply(this);
                } catch (err) {}
              }
            }.apply(object);
          }
        }.apply(this), options.delay);
      });
      
      // hover out function
      $(el).mouseleave(function() {
        // clear the open timeout
        window.clearTimeout($(this).data('dropdownify').timeoutOpen);
        
        // store the timeout function for possible clearing
        $(this).data('dropdownify').timeoutClose = window.setTimeout(function() {
          var object = this;
          
          return function() {
            return function() {
              $(this).children($(this).data('dropdownify').menus).css({
                'visibility': 'hidden',
                'display': 'none'
              });
              
              if (options.onhide) {
                try {
                  options.onhide.apply(this);
                } catch (err) {}
              }
            }.apply(object);
          }
        }.apply(this), $(this).data('dropdownify').timeout);
      });
    });
    
    // return the object for chaining
    return this;
  }
})(jQuery);

Initial URL
http://www.dom111.co.uk/blog/coding/dropdownify-minimal-effort-dropdown/218

Initial Description
So recently I was asked to change a navigation style of an existing site to drop-down menus.

Simple, I thought, just use one of the many existing drop-down plugins. I tried many, but most seemed to use hardcoded styles and I had a few problems (some of which I encountered again, writing this).

So I’ve made this, I think it’s fairly robust, but I’m sure there’ll be problems with embedded objects (flash) and select boxes (in <= IE6), but for my needs, it sufficed, so I thought I'd share, in case anyone else needs a simple script to manage drop-downs.

Initial Title
jQuery - Dropdownify

Initial Tags
javascript, dropdown, jquery

Initial Language
jQuery