Return to Snippet

Revision: 31907
at September 15, 2010 19:50 by dom111


Initial Code
Element.implement({
  /**
   * 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
   */
  toParent: function(dest, step) {
    var cur = $(this), i = 0, max = 200;

    while (i < max) {
      if (!cur) {
        return null;
      }

      if ($(cur).match(dest)) {
        return i;
      }

      if (step) {
        while (true) {
          cur = $(cur).getParent();

          if (!cur || $(cur).match(step) || $(cur).match(dest)) {
            break;
          }
        }

      } else {
        cur = $(cur).getParent();
      }

      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 object for chaining
   */
  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 || {});
    
    // add an identifier to the root element
    $(this).addClass('dropdownify-root');
    $(this).getElements(options.items).each(function(el, i) {
      if ($(el).getElements(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).getStyle('height').toInt() + offsetV) + 'px',
              'left': (0 + offsetH) + 'px'
            };
            break;
            
          case 'top':
            return {
              'bottom': ($(el).getStyle('height').toInt() + offsetV) + 'px',
              'left': (0 + offsetH) + 'px'
            };
            break;
            
          case 'left':
            return {
              'top': (0 + offsetV) + 'px',
              'right': ($(el).getStyle('width').toInt() + offsetH) + 'px'
            };
            break;
            
          case 'right':
            return {
              'top': (0 + offsetV) + 'px',
              'left': ($(el).getStyle('width').toInt() + offsetH) + 'px'
            };
            break;
        }
      })(or);
    }
    
    // find all 'items' in the current node
    $(this).getElements(options.items).each(function(el, i) {
      // hide all the submenus
      $(el).setStyles({
        'position': 'relative',
        'overflow': 'visible'
      }).getChildren(options.menus).setStyles({
        'display': 'none',
        'position': 'absolute'
      });
      
      // store the options locally
      $(el).store('dropdownify', options);
      
      // hover in function
      $(el).addEvent('mouseenter', function() {
        // hide any existing ones if we're top-level
        if ($(this).toParent('.dropdownify-root', options.menus) == 1) {
          $$('.dropdownify-root')[0].getElements(options.menus).setStyles({
            'display': 'none',
            'visibility': 'hidden'
          });
        }
        
        // clear the close timeout
        window.clearTimeout($(this).retrieve('dropdownify').timeoutClose);
        
        // store the timeout function for possible clearing
        $(this).retrieve('dropdownify').timeoutOpen = window.setTimeout(function() {
          var object = this;
          
          return function() {
            return function() {
              $(this).getChildren($(this).retrieve('dropdownify').menus).addClass('dropdownify-current').setStyles({
                'z-index': (options.zIndex + $(this).toParent('.dropdownify-root', options.menus)),
                'display': 'block',
                'visibility': 'visible',
                'width': $(this).getStyle('width')
              }).setStyles(getPos(this));
              
              if (options.onshow) {
                try {
                  options.onshow.apply(this);
                } catch (err) {}
              }
            }.apply(object);
          }
        }.apply(this), options.delay);
      });
      
      // hover out function
      $(el).addEvent('mouseleave', function() {
        // clear the open timeout
        window.clearTimeout($(this).retrieve('dropdownify').timeoutOpen);
        
        // store the timeout function for possible clearing
        $(this).retrieve('dropdownify').timeoutClose = window.setTimeout(function() {
          var object = this;
          
          return function() {
            return function() {
              $(this).getChildren($(this).retrieve('dropdownify').menus).setStyles({
                'visibility': 'hidden',
                'display': 'none'
              });
              
              if (options.onhide) {
                try {
                  options.onhide.apply(this);
                } catch (err) {}
              }
            }.apply(object);
          }
        }.apply(this), $(this).retrieve('dropdownify').timeout);
      });
    });
    
    // return the object for chaining
    return this;
  }
});

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
mootools dropdownify

Initial Tags
javascript, dropdown

Initial Language
JavaScript