/* Copyright (c) 2004-2006, The Dojo Foundation All Rights Reserved. Licensed under the Academic Free License version 2.1 or above OR the modified BSD license. For more information on Dojo licensing, see: http://dojotoolkit.org/community/licensing.shtml */ dojo.provide("dojo.widget.Menu2"); dojo.provide("dojo.widget.html.Menu2"); dojo.provide("dojo.widget.PopupMenu2"); dojo.provide("dojo.widget.MenuItem2"); dojo.provide("dojo.widget.MenuBar2"); dojo.require("dojo.html"); dojo.require("dojo.style"); dojo.require("dojo.event.*"); dojo.require("dojo.widget.*"); dojo.require("dojo.widget.HtmlWidget"); dojo.widget.PopupMenu2 = function(){ dojo.widget.HtmlWidget.call(this); this.items = []; // unused??? this.targetNodeIds = []; // fill this with nodeIds upon widget creation and it becomes context menu for those nodes this.queueOnAnimationFinish = []; this.eventNames = { open: "" }; } dojo.inherits(dojo.widget.PopupMenu2, dojo.widget.HtmlWidget); dojo.lang.extend(dojo.widget.PopupMenu2, { widgetType: "PopupMenu2", isContainer: true, snarfChildDomOutput: true, currentSubmenu: null, currentSubmenuTrigger: null, parentMenu: null, parentMenuBar: null, isShowingNow: false, menuX: 0, menuY: 0, menuWidth: 0, menuHeight: 0, menuIndex: 0, domNode: null, containerNode: null, eventNaming: "default", templateString: '', templateCssPath: dojo.uri.dojoUri("src/widget/templates/HtmlMenu2.css"), itemHeight: 18, iconGap: 1, accelGap: 10, submenuGap: 2, finalGap: 5, submenuIconSize: 4, separatorHeight: 9, submenuDelay: 500, submenuOverlap: 5, contextMenuForWindow: false, openEvent: null, submenuIconSrc: dojo.uri.dojoUri("src/widget/templates/images/submenu_off.gif").toString(), submenuIconOnSrc: dojo.uri.dojoUri("src/widget/templates/images/submenu_on.gif").toString(), initialize: function(args, frag) { if (this.eventNaming == "default") { for (var eventName in this.eventNames) { this.eventNames[eventName] = this.widgetId+"/"+eventName; } } }, postCreate: function(){ if (this.domNode.style.display=="none"){ this.domNode.style.display = ""; } this.domNode.style.left = '-9999px' this.domNode.style.top = '-9999px' // attach menu to document body if it's not already there if (this.domNode.parentNode != document.body){ document.body.appendChild(this.domNode); } if (this.contextMenuForWindow){ var doc = document.documentElement || document.body; dojo.widget.Menu2.OperaAndKonqFixer.fixNode(doc); dojo.event.connect(doc, "oncontextmenu", this, "onOpen"); } else if ( this.targetNodeIds.length > 0 ){ for(var i=0; i open // so close enables animation and next "open" is put to queue to occur at new location if(this.animationInProgress){ this.queueOnAnimationFinish.push(this.open, arguments); return; } var viewport = dojo.html.getViewportSize(); var scrolloffset = dojo.html.getScrollOffset(); var clientRect = { 'left' : scrolloffset[0], 'right' : scrolloffset[0] + viewport[0], 'top' : scrolloffset[1], 'bottom': scrolloffset[1] + viewport[1] }; if (parentMenu){ // submenu is opening if (x + this.menuWidth > clientRect.right){ x = x - (this.menuWidth + parentMenu.menuWidth - (2 * this.submenuOverlap)); } if (y + this.menuHeight > clientRect.bottom){ y = y - (this.menuHeight - (this.itemHeight + 5)); } // TODO: why 5? }else{ // top level menu is opening x+=scrolloffset[0]; y+=scrolloffset[1]; explodeSrc[0] += scrolloffset[0]; explodeSrc[1] += scrolloffset[1]; if (x < clientRect.left){ x = clientRect.left; } if (x + this.menuWidth > clientRect.right){ x = x - this.menuWidth; } if (y < clientRect.top){ y = clientRect.top; } if (y + this.menuHeight > clientRect.bottom){ y = y - this.menuHeight; } } this.parentMenu = parentMenu; this.explodeSrc = explodeSrc; this.menuIndex = parentMenu ? parentMenu.menuIndex + 1 : 1; this.menuX = x; this.menuY = y; // move the menu into position but make it invisible // (because when menus are initially constructed they are visible but off-screen) this.domNode.style.zIndex = 200 + this.menuIndex; this.domNode.style.left = x + 'px'; this.domNode.style.top = y + 'px'; this.domNode.style.display='none'; this.domNode.style.position='absolute'; // then use the user defined method to display it this.show(); this.isShowingNow = true; }, close: function(){ // If we are in the process of opening the menu and we are asked to close it, // we should really cancel the current animation, but for simplicity we will // just ignore the request if(this.animationInProgress){ this.queueOnAnimationFinish.push(this.close, []); return; } this.closeSubmenu(); this.hide(); this.isShowingNow = false; dojo.widget.html.Menu2Manager.closed(this); if (this.parentMenuBar){ this.parentMenuBar.closedMenu(this); } }, onShow: function() { dojo.widget.HtmlWidget.prototype.onShow.call(this); this.processQueue(); }, // do events from queue processQueue: function() { if (!this.queueOnAnimationFinish.length) return; var func = this.queueOnAnimationFinish.shift(); var args = this.queueOnAnimationFinish.shift(); func.apply(this, args); }, onHide: function() { dojo.widget.HtmlWidget.prototype.onHide.call(this); this.processQueue(); }, closeAll: function(){ if (this.parentMenu){ this.parentMenu.closeAll(); }else{ this.close(); } }, closeSubmenu: function(){ if (this.currentSubmenu == null){ return; } this.currentSubmenu.close(); this.currentSubmenu = null; this.currentSubmenuTrigger.is_open = false; this.currentSubmenuTrigger.closedSubmenu(); this.currentSubmenuTrigger = null; }, openSubmenu: function(submenu, from_item){ var our_x = dojo.style.getPixelValue(this.domNode, 'left'); var our_y = dojo.style.getPixelValue(this.domNode, 'top'); var our_w = dojo.style.getOuterWidth(this.domNode); var item_y = from_item.topPosition; var x = our_x + our_w - this.submenuOverlap; var y = our_y + item_y; this.currentSubmenu = submenu; this.currentSubmenu.open(x, y, this, from_item.domNode); this.currentSubmenuTrigger = from_item; this.currentSubmenuTrigger.is_open = true; }, onOpen: function(e){ this.openEvent = e; //dojo.debugShallow(e); this.open(e.clientX, e.clientY, null, [e.clientX, e.clientY]); if(e["preventDefault"]){ e.preventDefault(); } }, isPointInMenu: function(x, y){ if (x < this.menuX){ return false; } if (x > this.menuX + this.menuWidth){ return false; } if (y < this.menuY){ return false; } if (y > this.menuY + this.menuHeight){ return false; } return true; } }); dojo.widget.MenuItem2 = function(){ dojo.widget.HtmlWidget.call(this); this.eventNames = { engage: "" }; } dojo.inherits(dojo.widget.MenuItem2, dojo.widget.HtmlWidget); dojo.lang.extend(dojo.widget.MenuItem2, { widgetType: "MenuItem2", templateString: '
' +'
' +'' +'' +'
' +'
 
' +'
', // // nodes // domNode: null, iconNode: null, labelNode: null, accelNode: null, submenuNode: null, targetNode: null, // // internal settings // is_hovering: false, hover_timer: null, is_open: false, topPosition: 0, // // options // caption: 'Untitled', accelKey: '', iconSrc: '', submenuId: '', disabled: false, eventNaming: "default", postCreate: function(){ dojo.html.disableSelection(this.domNode); if (this.disabled){ this.setDisabled(true); } this.labelNode.childNodes[0].appendChild(document.createTextNode(this.caption)); this.accelNode.childNodes[0].appendChild(document.createTextNode(this.accelKey)); this.labelShadowNode = this.labelNode.childNodes[0].childNodes[0]; this.accelShadowNode = this.accelNode.childNodes[0].childNodes[0]; this.labelShadowNode.appendChild(document.createTextNode(this.caption)); this.accelShadowNode.appendChild(document.createTextNode(this.accelKey)); if (this.eventNaming == "default") { for (var eventName in this.eventNames) { this.eventNames[eventName] = this.widgetId+"/"+eventName; } } }, layoutItem: function(label_w, accel_w){ var x_label = this.parent.itemHeight + this.parent.iconGap; var x_accel = x_label + label_w + this.parent.accelGap; var x_submu = x_accel + accel_w + this.parent.submenuGap; var total_w = x_submu + this.parent.submenuIconSize + this.parent.finalGap; this.iconNode.style.left = '0px'; this.iconNode.style.top = '0px'; if (this.iconSrc){ if ((this.iconSrc.toLowerCase().substring(this.iconSrc.length-4) == ".png") && (dojo.render.html.ie)){ this.iconNode.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+this.iconSrc+"', sizingMethod='image')"; this.iconNode.style.backgroundImage = ''; }else{ this.iconNode.style.backgroundImage = 'url('+this.iconSrc+')'; } }else{ this.iconNode.style.backgroundImage = ''; } dojo.style.setOuterWidth(this.iconNode, this.parent.itemHeight); dojo.style.setOuterHeight(this.iconNode, this.parent.itemHeight); dojo.style.setOuterHeight(this.labelNode, this.parent.itemHeight); dojo.style.setOuterHeight(this.accelNode, this.parent.itemHeight); dojo.style.setContentWidth(this.domNode, total_w); dojo.style.setContentHeight(this.domNode, this.parent.itemHeight); this.labelNode.style.left = x_label + 'px'; this.accelNode.style.left = x_accel + 'px'; this.submenuNode.style.left = x_submu + 'px'; dojo.style.setOuterWidth(this.submenuNode, this.parent.submenuIconSize); dojo.style.setOuterHeight(this.submenuNode, this.parent.itemHeight); this.submenuNode.style.display = this.submenuId ? 'block' : 'none'; this.submenuNode.style.backgroundImage = 'url('+this.parent.submenuIconSrc+')'; dojo.style.setOuterWidth(this.targetNode, total_w); dojo.style.setOuterHeight(this.targetNode, this.parent.itemHeight); }, onHover: function(){ if (this.is_hovering){ return; } if (this.is_open){ return; } this.parent.closeSubmenu(); this.highlightItem(); if (this.is_hovering){ this.stopSubmenuTimer(); } this.is_hovering = true; this.startSubmenuTimer(); }, onUnhover: function(){ if (!this.is_open){ this.unhighlightItem(); } this.is_hovering = false; this.stopSubmenuTimer(); }, // Internal function for clicks _onClick: function(){ if (this.disabled){ return; } if (this.submenuId){ if (!this.is_open){ this.stopSubmenuTimer(); this.openSubmenu(); } }else{ this.parent.closeAll(); } // for some browsers the onMouseOut doesn't get called (?), so call it manually this.onUnhover(); // user defined handler for click this.onClick(); dojo.event.topic.publish(this.eventNames.engage, this); }, // User defined function to handle clicks onClick: function() { }, highlightItem: function(){ dojo.html.addClass(this.domNode, 'dojoMenuItem2Hover'); this.submenuNode.style.backgroundImage = 'url('+this.parent.submenuIconOnSrc+')'; }, unhighlightItem: function(){ dojo.html.removeClass(this.domNode, 'dojoMenuItem2Hover'); this.submenuNode.style.backgroundImage = 'url('+this.parent.submenuIconSrc+')'; }, startSubmenuTimer: function(){ this.stopSubmenuTimer(); if (this.disabled){ return; } var self = this; var closure = function(){ return function(){ self.openSubmenu(); } }(); this.hover_timer = window.setTimeout(closure, this.parent.submenuDelay); }, stopSubmenuTimer: function(){ if (this.hover_timer){ window.clearTimeout(this.hover_timer); this.hover_timer = null; } }, openSubmenu: function(){ // first close any other open submenu this.parent.closeSubmenu(); var submenu = dojo.widget.getWidgetById(this.submenuId); if (submenu){ this.parent.openSubmenu(submenu, this); } //dojo.debug('open submenu for item '+this.widgetId); }, closedSubmenu: function(){ this.onUnhover(); }, setDisabled: function(value){ this.disabled = value; if (this.disabled){ dojo.html.addClass(this.domNode, 'dojoMenuItem2Disabled'); }else{ dojo.html.removeClass(this.domNode, 'dojoMenuItem2Disabled'); } }, getLabelWidth: function(){ var node = this.labelNode.childNodes[0]; return dojo.style.getOuterWidth(node); }, getAccelWidth: function(){ var node = this.accelNode.childNodes[0]; return dojo.style.getOuterWidth(node); }, menuOpen: function(message) { } }); dojo.widget.MenuSeparator2 = function(){ dojo.widget.HtmlWidget.call(this); } dojo.inherits(dojo.widget.MenuSeparator2, dojo.widget.HtmlWidget); dojo.lang.extend(dojo.widget.MenuSeparator2, { widgetType: "MenuSeparator2", domNode: null, topNode: null, bottomNode: null, templateString: '
' +'
' +'
' +'
', postCreate: function(){ dojo.html.disableSelection(this.domNode); this.layoutItem(); }, layoutItem: function(label_w, accel_w){ var full_width = this.parent.itemHeight + this.parent.iconGap + label_w + this.parent.accelGap + accel_w + this.parent.submenuGap + this.parent.submenuIconSize + this.parent.finalGap; if (isNaN(full_width)){ return; } dojo.style.setContentHeight(this.domNode, this.parent.separatorHeight); dojo.style.setContentWidth(this.domNode, full_width); } }); // // the menu manager makes sure we don't have several menus // open at once. the root menu in an opening sequence calls // opened(). when a root menu closes it calls closed(). then // everything works. lovely. // dojo.widget.html.Menu2Manager = new function(){ this.currentMenu = null; this.currentButton = null; // button that opened current menu (if any) this.focusNode = null; dojo.event.connect(document, 'onmousedown', this, 'onClick'); dojo.event.connect(window, "onscroll", this, "onClick"); this.closed = function(menu){ if (this.currentMenu == menu){ this.currentMenu = null; this.currentButton = null; } }; this.opened = function(menu, button){ if (menu == this.currentMenu){ return; } if (this.currentMenu){ this.currentMenu.close(); } this.currentMenu = menu; this.currentButton = button; }; this.onClick = function(e){ if (!this.currentMenu){ return; } var scrolloffset = dojo.html.getScrollOffset(); var x = e.clientX + scrolloffset[0]; var y = e.clientY + scrolloffset[1]; var m = this.currentMenu; // starting from the base menu, perform a hit test // and exit when one succeeds while (m){ if (m.isPointInMenu(x, y)){ return; } m = m.currentSubmenu; } // Also, if user clicked the button that opened this menu, then // that button will send the menu a close() command, so this code // shouldn't try to close the menu. Closing twice messes up animation. if (this.currentButton && dojo.html.overElement(this.currentButton, e)){ return; } // the click didn't fall within the open menu tree // so close it this.currentMenu.close(); }; } // ************************** make contextmenu work in konqueror and opera ********************* dojo.widget.Menu2.OperaAndKonqFixer = new function(){ var implement = true; var delfunc = false; /** dom event check * * make a event and dispatch it and se if it calls function below, * if it does its supported and we dont need to implement our own */ // gets called if we have support for oncontextmenu if (!dojo.lang.isFunction(document.oncontextmenu)){ document.oncontextmenu = function(){ implement = false; delfunc = true; } } if (document.createEvent){ // moz, safari has contextmenu event, need to do livecheck on this env. try { var e = document.createEvent("MouseEvents"); e.initMouseEvent("contextmenu", 1, 1, window, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, null); document.dispatchEvent(e); } catch (e) {/* assume not supported */} } else { // IE no need to implement custom contextmenu implement = false; } // clear this one if it wasn't there before if (delfunc){ delete document.oncontextmenu; } /***** end dom event check *****/ /** * this fixes a dom node by attaching a custom oncontextmenu function that gets called when apropriate * @param node a dom node * * no returns */ this.fixNode = function(node){ if (implement){ // attach stub oncontextmenu function if (!dojo.lang.isFunction(node.oncontextmenu)){ node.oncontextmenu = function(e){/*stub*/} } // attach control function for oncontextmenu if (window.opera){ // opera // listen to ctrl-click events node._menufixer_opera = function(e){ if (e.ctrlKey){ this.oncontextmenu(e); } }; dojo.event.connect(node, "onclick", node, "_menufixer_opera"); } else { // konqueror // rightclick, listen to mousedown events node._menufixer_konq = function(e){ if (e.button==2 ){ e.preventDefault(); // need to prevent browsers menu this.oncontextmenu(e); } }; dojo.event.connect(node, "onmousedown", node, "_menufixer_konq"); } } } /** * this cleans up a fixed node, prevent memoryleak? * @param node node to clean * * no returns */ this.cleanNode = function(node){ if (implement){ // checks needed if we gets a non fixed node if (node._menufixer_opera){ dojo.event.disconnect(node, "onclick", node, "_menufixer_opera"); delete node._menufixer_opera; } else if(node._menufixer_konq){ dojo.event.disconnect(node, "onmousedown", node, "_menufixer_konq"); delete node._menufixer_konq; } if (node.oncontextmenu){ delete node.oncontextmenu; } } } }; dojo.widget.MenuBar2 = function(){ dojo.widget.HtmlWidget.call(this); } dojo.inherits(dojo.widget.MenuBar2, dojo.widget.HtmlWidget); dojo.lang.extend(dojo.widget.MenuBar2, { widgetType: "MenuBar2", isContainer: true, snarfChildDomOutput: true, currentItem: null, isExpanded: false, currentSubmenu: null, currentSubmenuTrigger: null, domNode: null, containerNode: null, templateString: '
', templateCssPath: dojo.uri.dojoUri("src/widget/templates/HtmlMenu2.css"), itemHeight: 18, openEvent: null, postCreate: function(){ // do something here this.layoutMenuSoon(); }, layoutMenuSoon: function(){ dojo.lang.setTimeout(this, "layoutMenu", 0); }, layoutMenu: function(){ // menu must be attached to DOM for size calculations to work var parent = this.domNode.parentNode; if (! parent || parent == undefined) { document.body.appendChild(this.domNode); } // determine menu height var max_label_h = 0; for(var i=0; i' +'' +'
 
' +'', // // nodes // domNode: null, labelNode: null, targetNode: null, // // internal settings // is_hovering: false, hover_timer: null, is_open: false, // // options // caption: 'Untitled', accelKey: '', iconSrc: '', submenuId: '', disabled: false, eventNaming: "default", postCreate: function(){ dojo.html.disableSelection(this.domNode); if (this.disabled){ this.setDisabled(true); } this.labelNode.childNodes[0].appendChild(document.createTextNode(this.caption)); this.labelShadowNode = this.labelNode.childNodes[0].childNodes[0]; this.labelShadowNode.appendChild(document.createTextNode(this.caption)); if (this.eventNaming == "default") { for (var eventName in this.eventNames) { this.eventNames[eventName] = this.widgetId+"/"+eventName; } } }, layoutItem: function(item_h){ var label_w = dojo.style.getOuterWidth(this.labelNode); var clientLeft = dojo.style.getPixelValue(this.domNode, "padding-left", true); var clientTop = dojo.style.getPixelValue(this.domNode, "padding-top", true); this.labelNode.style.left = clientLeft + 'px'; dojo.style.setOuterHeight(this.labelNode, item_h); dojo.style.setContentWidth(this.domNode, label_w); dojo.style.setContentHeight(this.domNode, item_h); this.labelNode.style.left = '0px'; dojo.style.setOuterWidth(this.targetNode, label_w); dojo.style.setOuterHeight(this.targetNode, item_h); }, getLabelHeight: function(){ return dojo.style.getOuterHeight(this.labelNode); }, onHover: function(){ this.parent.itemHover(this); }, onUnhover: function(){ this.parent.itemUnhover(this); }, _onClick: function(){ this.parent.itemClick(this); }, highlightItem: function(){ dojo.html.addClass(this.domNode, 'dojoMenuBarItem2Hover'); }, unhighlightItem: function(){ dojo.html.removeClass(this.domNode, 'dojoMenuBarItem2Hover'); }, expandMenu: function(){ var submenu = dojo.widget.getWidgetById(this.submenuId); if (submenu){ this.parent.openSubmenu(submenu, this); } }, setDisabled: function(value){ this.disabled = value; if (this.disabled){ dojo.html.addClass(this.domNode, 'dojoMenuBarItem2Disabled'); }else{ dojo.html.removeClass(this.domNode, 'dojoMenuBarItem2Disabled'); } } }); // make it a tag dojo.widget.tags.addParseTreeHandler("dojo:MenuBar2"); dojo.widget.tags.addParseTreeHandler("dojo:MenuBarItem2"); dojo.widget.tags.addParseTreeHandler("dojo:PopupMenu2"); dojo.widget.tags.addParseTreeHandler("dojo:MenuItem2"); dojo.widget.tags.addParseTreeHandler("dojo:MenuSeparator2");