Skip to content

Instantly share code, notes, and snippets.

@leandro
Created December 9, 2008 15:48
Show Gist options
  • Save leandro/33938 to your computer and use it in GitHub Desktop.
Save leandro/33938 to your computer and use it in GitHub Desktop.
/**
* @description DropDownAnything, drop down menu based on prototype
* @author Leandro N. Camargo; leandro-at-gmail-dot-com
* @version 0.1
* @date 08/09/2008
* @requires prototype.js 1.6
*/
var DDropDown = function(menu, targets, opt) {
var null_fn = function() {}, _this = this, ie = Prototype.Browser.IE, select_callback = false, menu_caller;
_this.menu = menu;
_this.rel_element = null;
_this.rel_position = 'top';
_this.last_collection = null;
_this.menu_caller = null;
_this.targets = typeof(targets) == 'string' ? $$(targets) : targets;
_this.opt = Object.extend({
context:'unique',
items_selector:'a',
before_select:null_fn,
on_select:null_fn,
after_select:null_fn,
before_show:null_fn,
before_hide:null_fn,
after_show:null_fn,
after_hide: null_fn,
cancel_click_default:true,
menu_relative_to:'target', // or 'click'
z_index: 100
}, opt || {});
_this.shim = ie ? new Element('iframe', {
style: 'position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);display:none;opacity:0',
src: 'javascript:void(0);',
frameborder: 0
}) : false;
var fn = function(ev) {
Event.stop(ev);
menu_caller = _this.menu_caller = this;
var before_return = _this.opt.before_show(ev, this, _this);
if(typeof before_return != 'undefined' && before_return === false)
return false;
_this.show(ev, this);
_this.opt.after_show(ev, this, _this);
DDropDown.current_instance = _this;
DDropDown.current_caller = this;
};
if(_this.targets instanceof Array) {
_this.targets.invoke('observe', 'click', fn).invoke('setStyle', {cursor: Prototype.Browser.IE ? 'hand' : 'pointer'});
} else {
_this.targets.observe('click', fn).setStyle({cursor: Prototype.Browser.IE ? 'hand' : 'pointer'});
}
// this event has attached a named function because it could be called outside such event (in _this.show)
// that's why this function has some quirky statements
$(document).observe('click', _this.hide = function(ev, _instance) {
if(!DDropDown.showing || _this != DDropDown.current_instance) return false;
var src = Event.element(ev), el = DDropDown.showing;
if(this != document || DDropDown.should_it_hide(src, el)) {
var instance = _instance || _this;
if(this != document) instance.menu_caller = DDropDown.current_caller;
var before_return = instance.opt.before_hide(ev, el, instance);
if(typeof before_return != 'undefined' && before_return === false)
return false;
instance._hide(ev, el);
instance.opt.after_hide(ev, el, instance);
DDropDown.current_caller = DDropDown.current_instance = DDropDown.showing = false;
instance.menu_caller = menu_caller; // putting back the real element caller to its place
}
});
_this.show = function(ev, element) {
//if showing a menu when another was already opened
DDropDown.showing && DDropDown.current_instance.hide(ev, DDropDown.current_instance);
if(_this.opt.menu_relative_to == 'click') {
var
mouseRef = Event.pointer(ev),
refX = mouseRef.x,
refY = mouseRef.y;
} else {
var
targetDim = element.getDimensions(),
targetOff = element.cumulativeOffset(),
refX = targetOff[0],
refY = targetOff[1],
targetW = targetDim.width,
targetH = targetDim.height;
}
var
menu = DDropDown.showing = DDropDown.get_menu(element, _this.menu, _this.opt.context),
x = refX,
y = refY,
vpDim = document.viewport.getDimensions(),
vpOff = document.viewport.getScrollOffsets(),
menuDim = menu.getDimensions(),
menuW = menuDim.width,
menuH = menuDim.height,
menuOff;
if(_this.opt.menu_relative_to == 'click') {
menuOff = {
left: (x + menuW > vpDim.width ? x - menuW : x) + 'px',
top: (y + menuH > (vpDim.height + vpOff.top) && (y - vpOff.top) > menuH ? y - menuH : y) + 'px'
};
} else {
menuOff = {
left: (x + menuW + targetW > vpDim.width ? x + targetW - menuW : x) + 'px',
top: (y + menuH + targetH > (vpDim.height + vpOff.top) && (y - vpOff.top) > menuH ? y - menuH : y + targetH) + 'px'
}
}
_this.rel_element = menu.next() || menu.previous() || menu.up() || document.body;
_this.rel_position = menu.next() ? 'before' : (menu.previous() ? 'after' : 'top');
$(document.body).insert({top: menu});
menu.setStyle(menuOff).setStyle({zIndex: _this.opt.z_index, position: 'absolute'}).show();
if(ie) {
$(document.body).insert(_this.shim);
_this.shim.setStyle({height: menuH + 'px', width: menuW + 'px'}).setStyle(menuOff).show();
}
var items = _this.last_collection = menu.select(_this.opt.items_selector);
if(_this.opt.on_select != null_fn || _this.opt.after_select != null_fn || _this.opt.before_select != null_fn) {
items.invoke('observe', 'click', select_callback || (select_callback = function(ev) {
var before_return = _this.opt.before_select(ev, this);
if(typeof before_return != 'undefined' && !before_return) return false;
_this.opt.cancel_click_default && Event.stop(ev);
_this.opt.on_select(ev, this, _this);
_this.opt.after_select(ev, this);
}));
}
};
_this._hide = function(ev, element) {
element.hide();
var o = {};
o[_this.rel_position] = element;
_this.rel_element.insert(o)
if(ie) _this.shim.hide();
_this.last_collection.invoke('stopObserving', 'click', select_callback || null_fn);
};
};
DDropDown.showing = false;
// these two 'current_*' static methods exist to control whenever happens a click in caller with an already opened menu
DDropDown.current_instance = false;
DDropDown.current_caller = false;
DDropDown.should_it_hide = function(el, menu) {
return !el.ancestors().include(menu);
};
DDropDown.get_menu = function(element, menu, context) {
// possible relations: parent, ancestor, self (when menu is inside the clicked element) and unique (when we talk about #id)
if(context.indexOf('parent') === 0) {
if(context == 'parent') return element.up().select(menu)[0];
return element.up(context.match(/parent\((\d+)\)/)[1]).select(menu)[0];
} else if(context == 'self') {
return element.select(menu)[0];
} else if(context.indexOf('ancestor') === 0) {
return element.up(context.match(/ancestor\(([^\)]+)\)/)[1]);
} else { // expcts 'unique' here
return $$(menu)[0];
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment