/*****************************************************************************
  RightContext v0.2.4
  Author: Harel Malka  
  http://www.harelmalka.com 
  harel@harelmalka.com
  Bugs: rightcontext@googlemail.com
  
  February 2007
  ----------------------------------------------------------------------------
  
  RightContext is a supercharged context menu. I created it to 
  answer a few requirements I had of context menus which I could not find 
  anywhere else; mainly provide the *correct* contextualized links depending 
  on what was triggering the menu. Originaly this was a Right Click menu only, 
  though this was changed in 0.2.4 to allow menu triggering by Left/Right Click,
  or Mouse over actions. 
  RightContext generates unique context menus that are built based on the DOM 
  element clicked using special attributes embedded in the element's html. 
  The attributes are custom generated and can be anything. Menu items can 
  include [tags] referencing those attributes, which will cause them to be 
  transformed to contain the actual values when the menu is constructed 
  (i.e., when an element is left/right clicked or moused over, depending on 
  the menu's setting.)
  
  Some of the key features of RightClick:
      * Trigger items via Right click, Left click or Mouse over (new in 0.2.4) 
      * Menu items that link somewhere
      * Menu items that perform a custom javascript function
      * Menu items that display hardcoded text
      * Menu items that retrieve text via a remote 'ajax' call
      * Menu item separators
      * Supports multiple different menus that can be called depending on 
        the element clicked
      * All menu items can contain [tags] which are transformed at runtime to 
        the values embedded in the clicked element
      * Conditional evaluation of menu items. An item can show or not show depending
        on a specified condition in the menu template (new in v0.2.3)
      * Css based look and feel
      * Unobtrusive standalone javascript: no additional js framework required.

  RightContext is the menu object. It contains a collection of context menu 
  definitions which can be retrieved by unique names. A context menu will 
  receive a set of arguments from any calling Context that can be used to 
  construct the links or text in it. 
  This is a singleton object that should have one instance per page.

  A few of the object's properties that you can edit:
      menuTriggerEvent -> Can be either RIGHT (default), LEFT, or MOVE to 
                          determine what triggers the menus generated (left/right
                          click or a mouse over event). Value is in UPPER case.
      allowedContexts  -> A String array of tag names that can have a menu attached
                          to them. The default list covers A, DIV, SPAN, INPUT tags.

  A menu definition is an object with the following spec:

      menu1 = { attributes: "attr1,attr2,attr3" ,

                items: [ 
                       {type:RightContext.TYPE_MENU,
                        text:"Search for [attr2] on Google",
                        url:"http://www.goole.com?q=[attr1]",
                        image: "icon.gif", align:"right" },

                       {type:RightContext.TYPE_MENU,
                        text:"The second item in the menu. it will show if attr3=Y (and its: [attr3])",
                        requires: ["attr3", "Y"],
                        onclick:function() { alert('This is a custom javascript')} },
                      
                       {type: RightContext.TYPE_SEPERATOR },
                    
                       {type: RightContext.TYPE_TEXT,
                        text: "This is hardcoded, yet dynamic text: attr1=[attr1], attr2=[attr2], attr3=[attr3]"} ,
                    
                       {type: RightContext.TYPE_TEXT_EXT,
                        url: "external.html"}   ]
               };

 Each menu definition object contains two properties: 
 
     attributes : a comma delimited list of attributes that this menu will 
                  look for in the elements its bound to
     items      : an array of objects, each describes a menu item of a given 
                  type. The example above covers all 
                  currently supported types and their respective properties.   
 
 A couple of notes about item properties:
    * Use either url or onclick for TYPE_MENU items - url will take precedence 
      over onclick. 
    * For url type menu items, you can add an optional frame property to redirect
      the link to a specific frame, or use _blank to open a new window
    * The image attribute for TYPE_MENU are optional.    
    * For TYPE_MENU, you can use the optional 'requires' property. It accepts 
      an array with two elements; the first one is an attribute to evaluate and 
      the second is what it should evaluate to if this menu item is to be displayed.
 
 After creating some menu definitions, you need to add them to the menu 
 collection within RightContext by doing:
 
     RightContext.addMenu("menuOne", menu1); 
     
 This adds menu1 and tags it with unique name: menuOne. The unique name is how 
 we'll refer to this menu later.
 
 Not all html tags are used to allow context menus. By default, only 
 A, DIV, SPAN and INPUT tags are considered 'contextualizable' but you can add 
 additional tags by editing the allowedContexts property of the RightContext 
 object. 
 
 To generate a context menu for a particular html tag, first add the context 
 attribute to it, and then any additional attributes that you'd like it to 
 have. The context attribute holds which menu to call up.
 The additional ones are the attributes defined in the attributes property of 
 the menu definition. 
 For the menu definition above, here's an A tag example:
 
     <a href="http://www.freecrm.com" context="menuOne" attr1="freecrm" 
                        attr2="FreeCRM.com" attr3="Hi mum!">Right Click Me</a>
     
     The above link will use the menuOne (mapped to menu1) defintion. When 
     triggered, the first menu item will a link titled 
     "Search for freecrm on Google" ([attr1] is replaced by the tag's 
     attribute value - freecrm)
     The second item will pop a javascript alert and will be titled 
     "The second item in the menu. attr3=Hi Mum!".
     It will be followed by a separator and then the following text:
     "This is hardcoded, yet dynamic text: attr1=freecrm, attr2=FreeCRM.com,
     attr3=Hi mum!"
     The last item will retrieve the contents of the external.html file 
     provided. Obviously, you can also embed url parameters in the url as tags.
     Note that the link above will cause the second menu item to not display
     because the attr3 attribute is equal to "Hi mum!" while the menu template
     defines that the second item will only show if the attr3 is equal to Y.

 You can create as many menu definitions as needed for different elements on 
 your page. Simple define which menu to call in the tag's context attribute.
 
 Finally, to kick of the party and start some context menu action going, you 
 need to bind the tags that contain the context attribute to their respectful 
 menus by doing: 
 
     RightContext.initialize();
 
 Please see the example file provided (index.html). An example is worth 
 a thousand comments...

 CSS Notes:
 The RightContext object makes use of some CSS classes defined in the
 rightcontext.css file. You need to include that in your html, or make use
 of these classes yourself.
 
 Some credits: 
 I was always on the lookout for the right context menu but was too busy (or 
 lazy) to make my own. What finally broke the camel's back was a ContextMenu 
 script using prototype that i found here:
 http://www.ajaxline.com/node/338 (note - last i checked the link was not 
 active). But, it wasn't stand alone, and it wasn't really "contextualize" 
 to what's clicked. I wasn't a happy puppy, So i rolled up my sleeves. 
 I used the getDimensions method from prototype since it worked great and 
 saved me time doing my own. (http://prototype.conio.net)
 I also adapted the logic in Alessandro Fulciniti great bubble tooltip 
 script's Locate method for the locateMousePos (slightly modified)
 method for the same reason (http://pro.html.it  and  
 http://web-graphics.com )
 Also thanks to all the good people who bothered to comment about this script.
 Due to them I added the left click and mouse over trigger options.
 
 LICENSE:
 This script is FREE to use and modify for personal or commercial purposes. 
 I only ask that you either leave this commented message intact, OR leave 
 credit where credit is due, with all URLs provided (not just to myself, 
 but also to prototype and Alessandro).
 As well, if you make any modifications to this script that enhances its 
 functionality, please DO give them back to the world and please email them 
 to me so that I can consider including them into my own version here. 
 I'd also love to hear of any good use you've made with this script!
 
 WARRANTY: 
 This code comes with NO Warranty whatsoever. I cannot be help responsible
 for any damage of any kind to any thing resulting from using this code.
 Do NOT use this code for NASA shuttle launches! It won't work. 
 
 Please send bug reports to rightcontext@googlemail.com
 
 Have fun

 CHANGE LOG -----------------------------------------------------------------

 0.2   : 12 Feb 2007  Added support for tags in menu onclick event handlers.
                      Added a couple of fixes provided by JDG including a Safari
                      fix.
 0.2.1 : 17 Feb 2007  Bug fix: event handler parsing was overwriting the template
                      with the first parsed item.
 0.2.2 : 20 Feb 2007  Bug fix: IE6-7 issue with onlclick events containing tags 
                      and a menu positioning fix.
 0.2.3 : 20 Feb 2007  Added conditional display of menu items based on [tag] values
 0.2.4 : 26 Feb 2007  Added menu triggering via LEFT click and mouse over (MOVE)
                      events.
 ****************************************************************************/

var RightContext = {
    //some final vars:
    TYPE_MENU: 0,       // menu item
    TYPE_TEXT: 1,       // inline text (non-mutable hard coded)
    TYPE_TEXT_EXT: 2,   // external text (retrived via rpc call)
    TYPE_SEPERATOR:3,   // separator line
    TYPE_ATTRIBUTES:4,  // menu attributes.

    // set the event to trigger the menus: RIGHT,LEFT (right/left click) or mouse MOVE 
    menuTriggerEvent: "RIGHT",
    
    // object to hold temp mouse position
    mousePos: {x:0, y:0},

    // offset for menu from mouse pointer
    rightOffset: 5,

    // type of html tags that can have context menus. You can edit this to 
    // allow more tags into the party.
    allowedContexts: ["a","div","span","input"],

    // object to hold a collection of menus indexed by name
    menuCollection: new Object(),

    // the currently visible context menu DIV element
    contextMenu: null,
    
    // some state machine: is the menu showing (LEFT), and should killing it be aborted (MOVE)
    isShowing: false,
    abortKill: false,
    
    // image cache
    images: new Object(),
    
    // var to hold external requests
    req: null,
    
    // initialize RightContext object 
    initialize: function () {
        this.attachContextEvents();   
    },

    // adds a menu to the menuCollection
    addMenu: function (n, m) { 
        this.menuCollection[n] = m;
    },

    // return a menu from the menu collection
    getMenu: function (n) {
        return this.menuCollection[n];
    },

    // loop all context allowed tags in the document and attach menu events to 
    // those that contain the menu attribute
    attachContextEvents: function () {
        var tagContext, thisTag;
        for (var t=0; t<this.allowedContexts.length; t++) {
            tags = document.getElementsByTagName(this.allowedContexts[t]);

            for (e=0; e<tags.length; e++) {
                thisTag = tags[e];
                tagContext = thisTag.getAttribute("context");
                if (tagContext!=null && tagContext != "undefined") {
                	this.bindEvent('mousemove', tags[e], function(e) { return RightContext.locateMousePos(e); });
					if (this.menuTriggerEvent=="RIGHT") {
                    	tags[e].oncontextmenu = function() {   return RightContext.render(this);   }; 
                    } else if (this.menuTriggerEvent=="LEFT") {
						//this.bindEvent('click', tags[e],  function() {  return RightContext.render(this, tagContext);  });
                        tags[e].onclick = function(e) { 
                                                        RightContext.killBubble(e); 
                                                        return RightContext.render(this)
                                                       };
                    } else if (this.menuTriggerEvent=="MOVE") {
                        if (!document.all) {
                            this.bindEvent('mouseover', tags[e], function(e) { RightContext.locateMousePos(e); return RightContext.render(this); });
	    					this.bindEvent('mouseout',  tags[e], function(e) { setTimeout("RightContext.killMenu()", 10); });
                        } else {
                            tags[e].onmouseover =  function(e) { RightContext.locateMousePos(e); return RightContext.render(this); };
                            tags[e].onmouseout = function(e) { setTimeout("RightContext.killMenu()", 10); };
                        }
                    }
                }
            }
        }
    },
    
    killBubble: function(e) {
        if (!e) var e = window.event;
        e.cancelBubble = true;
        if (e.stopPropagation) e.stopPropagation();
    },
    
    // binds an event handler to an object
    bindEvent: function (evt, obj, act, bubble) {
        if (!bubble) bubble = false;
        if (obj.addEventListener) {
            obj.addEventListener(evt, act, bubble);
        } else if (obj.attachEvent) {
            obj.attachEvent('on'+evt, act);
        }
    },
    
 
    /*
     renders a given menu and attaches it to the caller object.
     The caller is responsible to contain a few extra attributes
     that will help construct the links for this menu (i.e., provide the Context)
    */
    render: function (caller, name) {
        var url, title;
        // if name was not specified, grab it from the caller
        // v0.2 - changed to getAttribute (used direct nodeValue access before my mistake). pointed out by JDG.
        var name = name || caller.getAttribute("context");

        // get the requested menu
        var thisMenu = this.getMenu(name);
         
        // extracts this menus attributes list and items
        var attributes = thisMenu["attributes"].split(',');   
        var items = thisMenu.items;

        // constructs a map from the callers attributes
        var objMap = this.buildAttributeMap(attributes, caller);

        // start building the menu itself, but first remove menu if visible
        this.killMenu();
        this.buildMenu(caller);
        
        // create a table to build the menu items in
        tbl = document.createElement("TABLE");
        tbl.id = "rcRightContextTable";
        
        // loop the menu items and render each according to its type
        for (var m=0; m<items.length; m++) {
            switch (items[m]["type"]) {
                case this.TYPE_MENU:
                    // add the menu item
                    if (this.isDisplayed(items[m], objMap)) {
                        this.addMenuItem(items[m], objMap, tbl);
                    }
                    break;

                case this.TYPE_TEXT:
                    // add fixed text
                    text = this.transform(items[m]["text"], objMap);
                    cell = this.addTableCell(tbl, "rcMenuItemText", text);
                    break;

                case this.TYPE_TEXT_EXT:
                    cell = this.addTableCell(tbl, "rcMenuItemTextExt");
                    url = this.transform(items[m]["url"], objMap);
                    this.request(url, function() { if (RightContext.req.readyState == 4 && RightContext.req.status == 200) { cell.innerHTML = RightContext.req.responseText } });
                    break;

                case this.TYPE_SEPERATOR:
                    cell = this.addTableCell(tbl);
                    cell.appendChild(this.getSeparator());
                    break;
                
                default:
                    // no default behaviour
                    break;
            }
            
        }
        // append the menu item table to the menu itself
        this.contextMenu.appendChild(tbl);
        // make sure we're not overflowed to the edge of the screen.
        this.repositionMenu();
        
        if (this.menuTriggerEvent=="MOVE") {
            this.bindEvent('mouseout',  this.contextMenu, function(e) { RightContext.abortKill = false; setTimeout("RightContext.killMenu()",10); });
			this.bindEvent('mouseover', this.contextMenu, function(e) { RightContext.abortKill = true;  });
        } else if (this.menuTriggerEvent=="LEFT" || this.menuTriggerEvent=="RIGHT") {
        	this.bindEvent('click', document.body, function(e) { setTimeout("RightContext.killMenu();", 10); }, false);
        } 
        this.isShowing = true;
        
        return false;
    },

    isDisplayed : function(item, objMap) {
        var reqVar, reqVal;
        var shown = true; // by default all items are shown, unless they require something 
        // lets make sure this item does not require any condition to be true in order to display
        if (item["requires"] != null && item["requires"] != "undefined") {
            // yep, this one has a requirement...
            reqVar = item["requires"][0];
            reqVal = item["requires"][1];
            if (objMap[reqVar] != null && objMap[reqVar] != "undefined") {
                // if the condition is not met, do not show this item.
                if (objMap[reqVar] != reqVal) {
                    shown = false;    
                }
            } else {
                // if the condition is not defined do not show the item
                shown = false;
            }
        }
        return shown;
    },
    
    // check if the menu goes outside the window boundries and adjust its 
    // location if so
    repositionMenu: function() {
        var mPos = this.findPosition(this.contextMenu);
        var mDim = this.getDimensions(this.contextMenu);
        var winHeight = window.innerHeight || document.body.clientHeight;   
        var winWidth = window.innerWidth || document.body.clientWidth;
        if (mPos.y + mDim.height > winHeight-30 ) {
            this.position(this.contextMenu, mPos.x, mPos.y - mDim.height);
            mPos = this.findPosition(this.contextMenu);
        } 
        if (mPos.x + mDim.width > winWidth - 30 ) {
            this.position(this.contextMenu, mPos.x-mDim.width, mPos.y);
        } 
    },
    
    // returns an HR sepearator which uses the rcMenuSeparator style
    getSeparator: function () {
        var sep = document.createElement("HR");
        sep.className = "rcMenuSeparator";
        return sep;
    },
    
    // adds a table cell to the provided table and returns it.
    // attached a class if provided and initializes the cell with some content 
    // where applicable
    addTableCell: function (table, className, content) {
        row = table.insertRow(-1);
        cell = row.insertCell(0);
        if (className) { 
            cell.className = className;
            if (content) {
                cell.innerHTML = content;
            }
        }
        return cell;
    },

    // adds a menu item to the provided table. transforms all data as defined 
    // in the objMap argument
    addMenuItem: function (item, objMap, tbl) {
        var title = this.transform(item["text"], objMap);
        var url, frame, img, imgAlign, itemSrc, tmp, itemAction; 
        var cell = this.addTableCell(tbl, "rcMenuItem", title); 
        cell.style.cursor = document.all?'hand':'pointer';
        this.bindEvent('mouseover', cell, function(e) { this.className="rcMenuItemHover";});
        this.bindEvent('mouseout',  cell, function(e) { this.className="rcMenuItem";     });
        
        // deal with image if applicable
        if (item["image"]!=null && item["image"]!="undefined") {
        	// get image alignment or default to absmiddle
        	imgAlign = (item["align"]!=null && item["align"]!="undefined") ? item["align"] : "absmiddle";
        	// load the image from the cache, or from disk (and then cache it)
        	if (this.images[item["image"]] != null && this.images[item["image"]] != "undefined") {
        		img = this.images[item["image"]];
        	} else {
        		img = this.loadImage(item["image"]);
        	}
        	// set image alignment
        	img.align=imgAlign;
        	// insert the image as first child of the cell
        	cell.insertBefore(this.images[item["image"]], cell.childNodes[0]);
        }
        
        if (item["url"]!=null && item["url"] != "undefined") {
            url   = this.transform(item["url"],  objMap);
            frame = false;
            if (item["frame"] != null && item["frame"] != "undefined") {
                frame = item["frame"];
            }
            cell.onclick = function () { RightContext.redirect(url, frame); }
        } else {
            // we first need to find out if the event handler contains a potential 
            // tag. if so, we grab its source, transform it and re-evaluate it. 
            // if this fails, the value reverts back to its original function
            itemAction = item["onclick"]; 
            try {

	    itemSrc = item["onclick"].toString();
	    if (itemSrc.indexOf('[')>-1) {
                itemSrc = this.transform(itemSrc, objMap);
                if (document.all) { // IE's eval won't return a parsed function.. we need to tell it to set it and then eval
                    eval('itemAction = ' + itemSrc);
                } else { // firefox allows this. 
                    itemAction = eval(itemSrc);
                }
            }
         
            } catch (e) {
               // nothing...
            }

            // set the cell onclick event handler.
            cell.onclick=itemAction;
        }

    },

    // transforms a string based on the provided map
    transform: function (str, map) {
        var tStr, tmp;
        tStr = str;
        for (p in map) {
            tmp = "[" + p + "]";
            tStr = tStr.replace(tmp, map[p]);
        }
        return tStr;
    },

    // returns the menu's attributes collection that will be used to construct 
    // the transformation map
    getMenuAttributeArray: function (menu) {
        for (var i=0; i<menu.length; i++) {
            if (menu[i].type == this.TYPE_ATTRIBUTES) {
                return menu[i]["attributes"].split(',');
            }
        }
        return new Array(0);
    },

    // construct the transformation map for a given object based on the tags in 
    // attribs
    buildAttributeMap: function (attribs, obj) {
        var thisAttr, thisValue;
        var attrMap = new Object();

        for (var a=0; a<attribs.length; a++) {
            thisAttr = attribs[a];
            thisValue = obj.getAttribute(attribs[a]);
            if (typeof thisValue != "undefined") {
                attrMap[thisAttr] = thisValue;
            }
        }
        return attrMap;
    },

    // find the position of an element on the screen and returns an array of [x,y]
    findPosition: function (obj) {
        var lft = 0;
        var top = 0;
        if (obj.offsetParent) {
            lft = obj.offsetLeft;
            top = obj.offsetTop;
            while (obj = obj.offsetParent) {
                lft += obj.offsetLeft;
                top += obj.offsetTop;
            }
        }
        return {x:lft,y:top};
    },

    // Returns the dimensions of an element on screen. Lifted from the wonderful 
    // prototype framework
    getDimensions: function(obj) {
        //var display = obj.getStyle('display');
        //if (display != 'none' && display != null) // Safari bug
        //  return {width: element.offsetWidth, height: element.offsetHeight};

        // All *Width and *Height properties give 0 on elements with display none,
        // so enable the element temporarily
        var objStyle = obj.style;
        var originalVisibility = objStyle.visibility;
        var originalPosition = objStyle.position;
        var originalDisplay = objStyle.display;
        objStyle.visibility = 'hidden';
        objStyle.position = 'absolute';
        objStyle.display = 'block';
        var originalWidth = obj.clientWidth;
        var originalHeight = obj.clientHeight;
        objStyle.display = originalDisplay;
        objStyle.position = originalPosition;
        objStyle.visibility = originalVisibility;
        return {width: originalWidth, height: originalHeight};
    },

    // positions object at x,y coordinates
    // v0.2 - added px to the position coordinate (provided by JDG)
    position: function (obj, x, y) {
        obj.style.left = x + 'px';
        obj.style.top  = y + 'px';
    },

    // builds a menu for parent object
    buildMenu: function (parent) {
        var pos, dim, tbl;
        //document.onmousemove  = RightContext.getMousePos;
        this.contextMenu = document.createElement("DIV");
        this.contextMenu.id = "rcRightContext";
        this.contextMenu.className = 'rcMenuContainer';

        // get the position and dimensions of the parent
        pos = this.findPosition(parent);
        dim = this.getDimensions(parent);

        // position the container to the bottom right of the element.
        this.position (this.contextMenu, this.mousePos.x + this.rightOffset, pos.y+dim.height);

        // set some event handlers
        // if the menu is triggered by a right click, disable the right click on the menu itself
        if (this.menuTriggerEvent == "RIGHT") {
	        this.contextMenu.oncontextmenu = function () { return false; };
	    }
  		
        // add the container to the body of the document
        document.body.appendChild(this.contextMenu);
         
    },


    // kills the currently visible context menu
    killMenu: function () {
        if (!this.abortKill && this.isShowing) {
        try {
            rc = this.contextMenu;
            document.body.removeChild(rc);
        } catch (e) {
            // already removed?
        }
        this.contextMenu = null;
        this.isShowing = false;
        this.abortKill = false;
        }
    },

    // locate the mouse cursor position 
    locateMousePos: function(e) {
        var posx = 0, posy =0;
        if(e==null) e=window.event;
        if(e.pageX || e.pageY) {
            posx=e.pageX; posy=e.pageY;
        } else if (e.clientX || e.clientY) {
            if(document.documentElement.scrollTop){
                posx=e.clientX+document.documentElement.scrollLeft;
                posy=e.clientY+document.documentElement.scrollTop;
            } else {
                posx=e.clientX+document.body.scrollLeft;
                posy=e.clientY+document.body.scrollTop;
            }
        }
        this.mousePos = {x:posx , y:posy};

    },
    
    // redirects the browser to given url
    // if frame!=false, it will open in provided frame (or new win if _blank)
    redirect: function (u, frame) {
        if (!frame) {
            document.location = u;  
        } else {
            if (frame=="_blank") {
                w = window.open(u, 'w');
            } else {
                window.frames[frame].document.location = u;
            }
        }
    },
    
    // performs a request - ajax style
    request: function (url, callBack) {
        if (window.XMLHttpRequest) { // native XMLHttpRequest
            this.req = new XMLHttpRequest();
            this.req.onreadystatechange =  callBack; 
            this.req.open("GET", url, true);
            this.req.send(null);
         } else if (window.ActiveXObject) { // The M$ 'standard'
            this.req = new ActiveXObject("Microsoft.XMLHTTP");
            if (this.req) { 
                this.req.onreadystatechange =   callBack;
                this.req.open("GET", url, true);
                this.req.send();
             }
         }
    },
    
    loadImage: function (url) {
    	var img = new Image();
    	img.src = url; 
    	img.className = "rcImage";
    	this.images[url] = img;
    	return img;
    }

};