Team:Brown-Stanford/Outreach/menu.js
From 2011.igem.org
/*!
* jQuery Radmenu (Radial Menu) Plugin * version: 1.0.0 (14-MAY-2011) * @requires v1.4.2 or later * * Author: Nirvana Tikku - ntikku@gmail.com - @ntikku * Documentation: * http://www.tikku.com/jquery-radmenu-plugin * * Dual licensed under the MIT and GPL licenses: * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * */
- (function($){
// radmenu namespace
var RADMENU = ".radmenu", // events are radmenu.{event} - guarantee no NS collision
OPTS = "options"+RADMENU,
PREVOPTS = "prevoptions"+RADMENU,
RADMENU_CLASS = "ui-radmenu-parent";
// private defaults var defaults = {
// radial menu container properties
listClass: "list",
itemClass: "item",
activeItemClass: "active",
// active item selection properties
selectEvent: null, // click, mouseenter etc
onSelect: function($selected){},
// initial setup properties
radius: 10, // in pixels
angleOffset: 0, // in degrees
centerX: 0,
centerY: 0,
// animation properties
animSpeed: 500,
animEasing: "swing",
// scaling properties and method
initialScale: 1,
scaleAnimSpeed: 0,
scaleAnimEasing: "swing",
scaleAnimOpts: {},
// example onScaleItem: $item.css("font-size", factor+"em"); onScaleItem: function($item, factor, coords){},
// public events
afterAnimation: function($m){},
onShow: function($items){$items.show();},
onHide: function($items){$items.hide();},
onNext: function($items){return true;},
onPrev: function($items){return true;},
// rotation property and fine-tuning event
rotate: false,
getRotation: function(degrees, index, numItems){return degrees;}
};
// radial menu container setup defaults $.radmenu = {
container: {
html: "",css: { "position": "relative" },
clz: "radial_div",
itemClz: "radial_div_item"
}
};
/** * jQuery Radmenu Plugin * @params * > input, dealt with by type * if empty - assumes initialization * if object - assumes initialization * if string - assumes trigger method * if number - select a particular menu item */ $.fn.radmenu = function(input, param){
try {
var $this = $(this);
var type = typeof input;
if(arguments.length==0 || type=="object")
return init($this, input);
else if(type=="string")
return (input=="items" || input=="opts") ? $this.triggerHandler(input+RADMENU) : $this.trigger(input+RADMENU, param || null);
else if(type=="number")
return $this.trigger("select"+RADMENU,input);
} catch (e) {
return "error : "+e;
}
};
/** * private :: init fn * @params * $menu - the jQuery obj / array w/ menu target * opts - options object, to be merged with defaults */ function init($menu, opts){
var o = $.extend({}, defaults, opts);
return $menu.each(function(m){
var $this = $(this);
if(!$this.hasClass(RADMENU_CLASS)) {
var $list = $this.find("."+o.listClass);
$list.find("."+o.itemClass).hide(); // ensure its hidden
// set the options within the data for the elem & bind evts $this.data(OPTS, updateRadius(o, o.initialScale, o.radius));
for(e in MENU) $this.bind(e+RADMENU, $this, MENU[e]);
$this.addClass(RADMENU_CLASS);
}
});
};
/** * selects a menu item - this method provides * functionality for nested radial menus * @param * evt - the event object * triggers select event on radmenu container * using the index of the 'target object' */ function selectMenuitem(evt){
var $this = $(this);
var $element = $(evt.target);
var container = $.radmenu.container;
if(!$element.hasClass(container.itemClz)) $element = $element.closest("."+container.itemClz);
var isInNested = $element.parents("."+container.itemClz).length>0;
var index = $element.index();
if(!isInNested) $this.parents("."+container.clz).radmenu(index);
else $this.radmenu(index);
cancelBubble(evt);
};
/** * cancel event bubbling - x-browser friendly * @param * evt - the event object */ function cancelBubble(evt){
if(!$.support.opacity) window.event.cancelBubble = true;
else evt.stopPropagation();
};
/** * All the events bound to the radial menu instance */ var MENU = { opts: function(evt) {
return getMenu(evt).opts;
}, show: function(evt, fn){ // fn = user input onshow
var $m = getMenu(evt);
var container = $.radmenu.container;
// clear any existing radial menus within the menu $m.menu.find("."+container.clz).remove();
// grab the desired menu items to be used in building the radmenu var $menuitems = $m.menu.find("."+$m.opts.itemClass);
// create a div that will be the radmenu & create the HTML for the items var $radialMenu = $(container.html) .addClass(container.clz) .css(container.css) .html(buildMenuHTML($menuitems, $m.opts));
// assign a selection event if the user has specified something var $menuitems = $radialMenu.find("."+container.itemClz);
if($m.opts.selectEvent!=null) $menuitems.bind($m.opts.selectEvent,selectMenuitem);
// append the radmenu items inside the menu $radialMenu.appendTo($m.menu);
if(typeof(fn) == "function") fn($menuitems); // allow passing in a method
else $m.opts.onShow($menuitems); // user can do what they want
cancelBubble(evt);
}, hide: function(evt){
var $m = getMenu(evt);
// remove the radmenu that was built and appended inside the menu var $menu = $m.menu.find("."+$.radmenu.container.clz);
$m.opts.onHide($menu.find("."+$.radmenu.container.itemClz));
$menu.remove();
cancelBubble(evt);
}, select: function(evt, selectIndex){
var $m = getMenu(evt);
// with a specific index specified, grab the item var $selected = $($m.raditems().get(selectIndex));
// remove the active class on the elements siblings $selected.siblings().removeClass($m.opts.activeItemClass);
// add the active class on the selected item $selected.addClass($m.opts.activeItemClass);
// pass the selected item to a customizable function $m.opts.onSelect($selected);
cancelBubble(evt);
}, next: function(evt){ // clockwise
var $m = getMenu(evt);
if( !$m.opts.onNext($m) ) return;
// switch the first and last items and then animate switchItems($m, $m.raditems().length-1, 0);
}, prev: function(evt){ // anticlockwise
var $m = getMenu(evt);
if( !$m.opts.onPrev($m) ) return;
// switch the last and first items and then animate switchItems($m, 0, $m.raditems().length-1);
}, shuffle: function(evt){
var $m = getMenu(evt);
var len = $m.raditems().length;
// swap some random item with another random item, and add some shuffling effects switchItems($m, rnd(len), rnd(len));
}, destroy: function(evt){
var $m = getMenu(evt);
$m.menu.data(OPTS, null) .data(PREVOPTS, null) .removeClass(RADMENU_CLASS) .unbind(RADMENU);
return $m.menu;
}, items: function(evt){
return getMenu(evt).raditems();
}, scale: function(evt, factor){
var $m = getMenu(evt);
if(factor){
var o = $m.opts;
var container = $.radmenu.container;
var prevOpts = $m.menu.data(PREVOPTS);
if(!prevOpts) $m.menu.data(PREVOPTS, prevOpts=o);
// get the radial menu items var $items = $m.menu.find("."+container.itemClz);
var updatedRadiusOpts = updateRadius(o, factor, prevOpts.radius);
$m.menu.data(OPTS, updatedRadiusOpts); // save the radius for anim purposes
$items.each(function(i){ // for each item update the x,y + css
var $this = $(this);
var coords = getCoords(i, $items.length, updatedRadiusOpts);
var animOpts = {
top: coords.top,
left: coords.left
};
if(typeof(o.scaleAnimOpts) == "object") {
animOpts = $.extend({}, o.scaleAnimOpts, animOpts);
}
$this.animate(animOpts, o.scaleAnimSpeed, o.scaleAnimEasing);
$m.opts.onScaleItem($this, factor, coords);
});
}
return $m.menu; } };
// simply multiples the radius by a factor function updateRadius(opts, radius, factor){
return $.extend({},opts,{radius:(factor*radius)});
};
// random int offset function rnd(i){
return parseInt( Math.random() * i );
};
/** * getMenu - this is a method that returns all * the required objects within a given method * that is subscribed to the radmenu object. * * @params * evt - the event object * @return * Object * > menu - jQueryfied menu * > opts - the options * > raditems - the radial menu items */ function getMenu(evt){
var $menu = evt.data;
return {
menu: $menu,
opts: $menu.data(OPTS),
raditems: function(){
// you will want to trigger raditems() if the contents get modified return $menu.find("."+$.radmenu.container.itemClz);
}
}; };
/** * Switch Items -- this method is used in re-evaluating the (x,y) coords * as a result of swapping the position of items. The intent with this * is that the plugin will reoganize the items (in the container, * as opposed to the original dom elements) such that the elements * positions, when selected, remains fixed around the circle. * Based on given indexes the items are swapped and then animated. The * most typical case involves removing the first and swapping the last * and vice versa. * * @params * $m - the menu package * remove - the index of the menuitem to replace in the swap * add - the index of the menuitem to use in the swap (a placeholder) */ function switchItems($m, remove, add){
if(remove==add) add = remove - 1; // ensure that we don't lose any items
var $remove = $($m.raditems()[remove]); // grab the replacement item
var toAddto = $m.raditems()[add]; // grab the placeholder
// insertion is dependent on index of items if(remove>add) $remove.insertBefore(toAddto);
else $remove.insertAfter(toAddto);
animateWheel($m, (remove<add)); // posOffset = 5:neat, 10:fireworksesque, 15:subtleish
};
/** * buildMenuHTML - returns string instead of objects * for performance * * @params * $menuitems - the jQueryified menu items * opts - the radial menu's options * @return * String * > each item is wrapped with an * absolute positioned div at an * offset determined by it's location * on a circle */ function buildMenuHTML($menuitems, opts){
var ret = [];
$menuitems.each(function(i){ // for each item we will want to build the HTML
var $this = $(this);
var coords = getCoords(i, $menuitems.length, opts); // each item has a position
var rotationHTML = "transform:rotate("+coords.angle+"deg); ";
ret.push(" ");});
return ret.join("");
};
/** * Get the radians value of an angle given a * particular slice of the selection * * @params * iIdx - the instance index * iNum - the number of menu items */ function getAngleAtIndex(iIdx, iNum){
return 2 * Math.PI * parseFloat(iIdx/iNum); // radians
};
/** * getCoords - returns coordinates for menuitems, as * well as an animation object with the appropriate rotation * as per config * * @params * iIdx - the instance index (1st, 2nd, 3rd, etc..) * iNum - the number of menuitems to distribute * oOpts - the options provided by the user customizations * bClockwise - a flag for the animation object * @return * Object - (x, y) coords & the angle in degrees */ function getCoords(iIdx, iNum, oOpts, bClockwise){
var radius = oOpts.radius; // user specified radius
var angle = getAngleAtIndex(iIdx, iNum);
angle += toRadians(oOpts.angleOffset); // provide flexibility of angle
// assuming: hypotenuse (hyp) = radius // // opposite |\ hypotenuse // | \ // 90deg |__\ (*theta* - angle) // adjacent // // x-axis offset: cos(theta) = adjacent / hypotenuse // ==> adjacent = left = cos(theta) * radius // y-axis offset: sin(theta) = opposite / hypotenuse // ==> opposite = top = sin(theta) * radius
var l = oOpts.centerX + ( Math.cos( angle ) * radius ), // "left" // angle is rounded to 2dp to fix a bug t = oOpts.centerY + ( Math.sin( parseInt(angle*100)/100 ) * radius ); // "top"
var degrees = oOpts.rotate ? oOpts.getRotation( angle * 180 / Math.PI, iIdx, iNum ) : 0;
// NOTE: why not just simply rotate to the angle? buggy // the element that gets shifted cycles through an unnecessary revolution // i.e. >360deg -> >0 deg var slice = oOpts.rotate ? ( getAngleAtIndex(1, iNum) * 180 / Math.PI ) : 0;
var rotation = ( bClockwise==true ? "-=" : "+=" ) + slice;
return {
left: l,
top: t,
angle: degrees,
animObj:{
left: l,
top: t,
radrotate: rotation
}
}; // return the x,y coords
};
/** * simple method to convert degrees to radians */ function toRadians(degrees){
return degrees * Math.PI / 180;
};
/** * animateWheel - performs animation on menu items within * the container elements * * @params * $m - object holding menu & options * iPosOffset - the position offset for the initial menuitem * bClockwise - for the animation, clockwise or counter */ function animateWheel($m, bClockwise){
// get the menu from the $m menu package var $menuitems = $m.raditems();
// get a handle on the number of items var len = $menuitems.length;
// for each item, we're going to animate left/top attributes $menuitems.each(function(i){
var $this = $(this);
// establish the new coordinates with a customizable offset var coords = getCoords(i, len, $m.opts, bClockwise);
// playing with this is fun - this basically just // performs the animation with new coordinates
$this.animate( coords.animObj, $m.opts.animSpeed, $m.opts.animEasing, function(){ if(i==(len-1) ){ // allow the user to do something after completing an animation $m.opts.afterAnimation($m); } } ); }); };
/** * Transform Utils */ var XForm = {};
// local cache of the appropriate transform to use XForm.attr = undefined;
// Safari, Chrome, FF 3.5+, IE 9+, and Opera 11+ XForm.opts = ["","-webkit-","-moz-","-ms-","-o-"]; XForm.cssattrs = ["","Webkit","Moz","ms","O"];
/** * Get the relevant CSS attr and cache it */ XForm.getCSSAttr = function($elm){
if( this.attr ) return this.attr;
return this.attr = (function(){
for(var ii=0; ii<XForm.cssattrs.length; ii++){
var opt = XForm.cssattrs[ii]+"Transform";
if( $elm[0].style[opt] ) return opt;
}
return "transform";
})();
};
/** * Deduce which transform is applicable * and extract the attr's value * @params * $elm - jQuerified element which the * css attribute is evaluated against */ XForm.getTransformValue = function($elm){
return jQuery.style( $elm[0], XForm.getCSSAttr($elm) );
};
/** * jQuery Proxy object */ var _ = {};
_.cur = $.fx.prototype.cur;
/** * jQuery Proxy Method: * We need to override this method in order for the "rotate([x]deg)" * css value to be parsed and passed through to the step fn numerically * * Credit due: Zachary Johnson www.zachstronaut.com * from https://github.com/zachstronaut/jquery-animate-css-rotate-scale */
$.fx.prototype.cur = function() {
if ( this.prop == "radrotate" ) {
var $elm = $(this.elem);
var style = XForm.getTransformValue($elm) || 'none';
if (style) {
var m = style.match(/rotate\(([^)]+)\)/);
if (m && m[1]) {
return parseFloat( m[1] );
}
}
return 0;
}
return _.cur.apply(this, arguments);
};
// // use a custom animation property - radrotate // $.fx.step.radrotate = function(fx) {
var $elm = $(fx.elem);
$elm.css(XForm.getCSSAttr($elm), "rotate("+ fx.now +"deg)");
};
})(jQuery);