From 16ffc77998c273a5e74ed7e56fe0e929e30b67e4 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Mon, 2 Feb 2009 21:26:22 -0700 Subject: [PATCH] Add support for submenus to contextmenu JS code. --- imp/js/ContextSensitive.js | 2 +- imp/js/src/ContextSensitive.js | 146 +++++++++++++++++++++++++------- imp/js/src/DimpBase.js | 12 +-- imp/templates/index/index-dimp.inc | 16 +++- imp/themes/graphics/popright.png | Bin 0 -> 107 bytes imp/themes/screen-dimp.css | 9 +- imp/themes/silver/graphics/popright.png | Bin 0 -> 205 bytes imp/themes/silver/screen-dimp.css | 7 ++ 8 files changed, 149 insertions(+), 43 deletions(-) create mode 100644 imp/themes/graphics/popright.png create mode 100644 imp/themes/silver/graphics/popright.png diff --git a/imp/js/ContextSensitive.js b/imp/js/ContextSensitive.js index 5735c5451..e20bec4e2 100644 --- a/imp/js/ContextSensitive.js +++ b/imp/js/ContextSensitive.js @@ -1 +1 @@ -var ContextSensitive=Class.create({initialize:function(){this.current=this.lasttarget=this.target=null;this.elements=$H();document.observe("contextmenu",this.rightClickHandler.bindAsEventListener(this));document.observe("click",this.leftClickHandler.bindAsEventListener(this));document.observe(Prototype.Browser.Gecko?"DOMMouseScroll":"mousescroll",this.close.bind(this))},addElement:function(d,c,a){var b=Boolean(a.left);if(d&&!this.validElement(d,b)){this.elements.set(d+Number(b),new ContextSensitive.Element(d,c,a))}},removeElement:function(a){this.elements.unset(a+"0");this.elements.unset(a+"1")},close:function(a){if(this.current){if(a){this.current.hide()}else{Effect.Fade(this.current,{duration:0.2})}this.current=this.target=null}},element:function(a){return a?this.target:this.lasttarget},currentmenu:function(){if(this.current&&this.current.visible()){return this.current}},validElement:function(b,a){return this.elements.get(b+Number(Boolean(a)))},disable:function(d,c,a){var b=this.validElement(d,c);if(b){b.disable=a}},leftClickHandler:function(a){if(a.isRightClick()){return}this.rightClickHandler(a,true)},rightClickHandler:function(b,a){if(this.trigger(b.element(),a,b.pointerX(),b.pointerY())){b.stop()}},trigger:function(f,e,h,g){var j,b,d,c,k,i,a;[f].concat(f.ancestors()).find(function(l){j=this.validElement(l.id,e);return j},this);if(!j||j.disable){this.close();return false}b=$(j.ctx);if(!b){this.close();return false}else{if(e&&b==this.current){return false}}this.current=b;this.lasttarget=this.target=$(j.id);d=j.opts.offset;if(!d&&(Object.isUndefined(h)||Object.isUndefined(g))){d=f.id}d=$(d);if(d){c=d.viewportOffset();a=document.viewport.getScrollOffsets();h=c[0]+a.left;g=c[1]+d.getHeight()+a.top}i=document.viewport.getDimensions();k=b.getDimensions();if((g+k.height)>i.height){g=i.height-k.height-10}if((h+k.width)>i.width){h=i.width-k.width-10}if(j.opts.onShow){j.opts.onShow(j)}Effect.Appear(b.setStyle({left:h+"px",top:g+"px"}),{duration:0.2});return true}});ContextSensitive.Element=Class.create({initialize:function(c,b,a){this.id=c;this.ctx=b;this.opts=a;this.opts.left=Boolean(a.left);this.disable=false}}); \ No newline at end of file +var ContextSensitive=Class.create({initialize:function(){this.lasttarget=this.target=null;this.elements=$H();this.submenus=$H();this.current=[];this.onShow=null;document.observe("contextmenu",this._rightClickHandler.bindAsEventListener(this));document.observe("click",this._leftClickHandler.bindAsEventListener(this));document.observe(Prototype.Browser.Gecko?"DOMMouseScroll":"mousescroll",this.close.bind(this))},setOnShow:function(a){this.onShow=a},addElement:function(d,c,a){var b=Boolean(a.left);if(d&&!this.validElement(d,b)){this.elements.set(d+Number(b),new ContextSensitive.Element(d,c,a))}},removeElement:function(a){this.elements.unset(a+"0");this.elements.unset(a+"1")},close:function(a){this._closeSubmenu(0,a)},_closeSubmenu:function(a,b){this.current.splice(a,this.current.size()-a).each(function(c){if(b){c.hide()}else{Effect.Fade(c,{duration:0.2})}});this.target=this.current[a]},element:function(a){return a?this.target:this.lasttarget},currentmenu:function(){return this.current.last()},validElement:function(b,a){return this.elements.get(b+Number(Boolean(a)))},disable:function(d,c,a){var b=this.validElement(d,c);if(b){b.disable=a}},_leftClickHandler:function(a){if(a.isRightClick()){return}this._rightClickHandler(a,true)},_rightClickHandler:function(b,a){if(this.trigger(b.element(),a,b.pointerX(),b.pointerY())){b.stop()}},trigger:function(f,e,h,g){var j,b,i,d,c,a;[f].concat(f.ancestors()).find(function(k){j=this.validElement(k.id,e);return j},this);if(!j||j.disable){if(!j||!j.hasClassName("contextSubmenu")){this.close()}return false}b=$(j.ctx);if(!b){this.close();return false}i=b.readAttribute("id");if(e&&i==this.currentmenu()){return false}this.close();this.lasttarget=this.target=$(j.id);d=j.opts.offset;if(!d&&(Object.isUndefined(h)||Object.isUndefined(g))){d=f.readAttribute("id")}d=$(d);if(d){c=d.viewportOffset();a=document.viewport.getScrollOffsets();h=c[0]+a.left;g=c[1]+d.getHeight()+a.top}this._displayMenu(b,h,g);return true},_displayMenu:function(c,a,f){var e=c.readAttribute("id"),d=c.getDimensions(),b=document.viewport.getDimensions();if((f+d.height)>b.height){f=b.height-d.height-10}if((a+d.width)>b.width){a=b.width-d.width-10}if(this.onShow){this.onShow(e)}Effect.Appear(c.setStyle({left:a+"px",top:f+"px"}),{duration:0.2});this.current.push(e)},addSubMenu:function(b,a){if(!this.submenus.get(b)){if(!this.submenus.size()){document.observe("mouseover",this._mouseoverHandler.bindAsEventListener(this))}this.submenus.set(b,a);$(b).addClassName("contextSubmenu").insert({top:new Element("SPAN",{className:"contextExpand"})})}},_mouseoverHandler:function(h){var f=h.element(),b=f.readAttribute("id"),k=this.currentmenu(),g,d,a,c,j,i;if(f.hasClassName("contextSubmenu")){a=this.submenus.get(b);if(a!=k){g=f.up().readAttribute("id");if(g!=k){this._closeSubmenu(this.current.indexOf(g)+1)}d=f.viewportOffset();c=document.viewport.getScrollOffsets();j=d[0]+c.left+f.getWidth();i=d[1]+c.top;this._displayMenu($(a),j,i)}}else{if((this.current.size()>1)&&f.hasClassName("contextElt")&&f.up().readAttribute("id")!=k){this._closeSubmenu(this.current.indexOf(b))}}}});ContextSensitive.Element=Class.create({initialize:function(c,b,a){this.id=c;this.ctx=b;this.opts=a;this.opts.left=Boolean(a.left);this.disable=false;b=$(b);if(b){b.select("A").invoke("addClassName","contextElt")}}}); \ No newline at end of file diff --git a/imp/js/src/ContextSensitive.js b/imp/js/src/ContextSensitive.js index 290222c6b..910e4536d 100644 --- a/imp/js/src/ContextSensitive.js +++ b/imp/js/src/ContextSensitive.js @@ -32,15 +32,26 @@ var ContextSensitive = Class.create({ initialize: function() { - this.current = this.lasttarget = this.target = null; + this.lasttarget = this.target = null; this.elements = $H(); + this.submenus = $H(); + this.current = []; + this.onShow = null; - document.observe('contextmenu', this.rightClickHandler.bindAsEventListener(this)); - document.observe('click', this.leftClickHandler.bindAsEventListener(this)); + document.observe('contextmenu', this._rightClickHandler.bindAsEventListener(this)); + document.observe('click', this._leftClickHandler.bindAsEventListener(this)); document.observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousescroll', this.close.bind(this)); }, /** + * Set an onshow function. + */ + setOnShow: function(func) + { + this.onShow = func; + }, + + /** * Elements are of type ContextSensitive.Element. */ addElement: function(id, target, opts) @@ -61,18 +72,26 @@ var ContextSensitive = Class.create({ }, /** - * Hide the current element. + * Hide the currently displayed element(s). */ close: function(immediate) { - if (this.current) { + this._closeSubmenu(0, immediate); + }, + + /** + * Close all submenus below a specified level. + */ + _closeSubmenu: function(idx, immediate) + { + this.current.splice(idx, this.current.size() - idx).each(function(s) { if (immediate) { - this.current.hide(); + s.hide(); } else { - Effect.Fade(this.current, { duration: 0.2 }); + Effect.Fade(s, { duration: 0.2 }); } - this.current = this.target = null; - } + }); + this.target = this.current[idx]; }, /** @@ -84,13 +103,12 @@ var ContextSensitive = Class.create({ }, /** - * Returns the current displayed menu element, if any. + * Returns the current displayed menu element ID, if any. If more than one + * submenu is open, returns the last ID opened. */ currentmenu: function() { - if (this.current && this.current.visible()) { - return this.current; - } + return this.current.last(); }, /** @@ -117,7 +135,7 @@ var ContextSensitive = Class.create({ * Called when a left click event occurs. Will return before the * element is closed if we click on an element inside of it. */ - leftClickHandler: function(e) + _leftClickHandler: function(e) { // Check for a right click. FF on Linux triggers an onclick event even // w/a right click, so disregard. @@ -126,13 +144,13 @@ var ContextSensitive = Class.create({ } // Check if the mouseclick is registered to an element now. - this.rightClickHandler(e, true); + this._rightClickHandler(e, true); }, /** * Called when a right click event occurs. */ - rightClickHandler: function(e, left) + _rightClickHandler: function(e, left) { if (this.trigger(e.element(), left, e.pointerX(), e.pointerY())) { e.stop(); @@ -144,7 +162,7 @@ var ContextSensitive = Class.create({ */ trigger: function(target, leftclick, x, y) { - var ctx, el, offset, offsets, size, v, voffsets; + var ctx, el, el_id, offset, offsets, voffsets; [ target ].concat(target.ancestors()).find(function(n) { ctx = this.validElement(n.id, leftclick); @@ -153,7 +171,10 @@ var ContextSensitive = Class.create({ // Return if event not found or event is disabled. if (!ctx || ctx.disable) { - this.close(); + // Return if this is a click on a submenu item. + if (!ctx || !ctx.hasClassName('contextSubmenu')) { + this.close(); + } return false; } @@ -163,19 +184,21 @@ var ContextSensitive = Class.create({ if (!el) { this.close(); return false; - } else if (leftclick && el == this.current) { + } + + el_id = el.readAttribute('id'); + if (leftclick && el_id == this.currentmenu()) { return false; } - // Register the current element that will be shown and the - // element that was clicked on. - this.current = el; + // Register the current element that will be shown and the element + // that was clicked on. + this.close(); this.lasttarget = this.target = $(ctx.id); - // Get the base element positions. offset = ctx.opts.offset; if (!offset && (Object.isUndefined(x) || Object.isUndefined(y))) { - offset = target.id; + offset = target.readAttribute('id'); } offset = $(offset); @@ -186,9 +209,20 @@ var ContextSensitive = Class.create({ y = offsets[1] + offset.getHeight() + voffsets.top; } + this._displayMenu(el, x, y); + + return true; + }, + + /** + * Display the [sub]menu on the screen. + */ + _displayMenu: function(elt, x, y) + { // Get window/element dimensions - v = document.viewport.getDimensions(); - size = el.getDimensions(); + var id = elt.readAttribute('id'), + size = elt.getDimensions(), + v = document.viewport.getDimensions(); // Make sure context window is entirely on screen if ((y + size.height) > v.height) { @@ -198,14 +232,60 @@ var ContextSensitive = Class.create({ x = v.width - size.width - 10; } - if (ctx.opts.onShow) { - ctx.opts.onShow(ctx); + if (this.onShow) { + this.onShow(id); } - Effect.Appear(el.setStyle({ left: x + 'px', top: y + 'px' }), { duration: 0.2 }); + Effect.Appear(elt.setStyle({ left: x + 'px', top: y + 'px' }), { duration: 0.2 }); - return true; + this.current.push(id); + }, + + /** + * Add a submenu to an existing menu. + */ + addSubMenu: function(id, submenu) + { + if (!this.submenus.get(id)) { + if (!this.submenus.size()) { + document.observe('mouseover', this._mouseoverHandler.bindAsEventListener(this)); + } + this.submenus.set(id, submenu); + $(id).addClassName('contextSubmenu').insert({ top: new Element('SPAN', { className: 'contextExpand' }) }); + } + }, + + /** + * Mouseover DOM Event handler. + */ + _mouseoverHandler: function(e) + { + var elt = e.element(), + id = elt.readAttribute('id'), + cm = this.currentmenu(), + div_id, offsets, sub, voffsets, x, y; + + if (elt.hasClassName('contextSubmenu')) { + sub = this.submenus.get(id); + if (sub != cm) { + div_id = elt.up().readAttribute('id'); + if (div_id != cm) { + this._closeSubmenu(this.current.indexOf(div_id) + 1); + } + + offsets = elt.viewportOffset(); + voffsets = document.viewport.getScrollOffsets(); + x = offsets[0] + voffsets.left + elt.getWidth(); + y = offsets[1] + voffsets.top; + this._displayMenu($(sub), x, y); + } + } else if ((this.current.size() > 1) && + elt.hasClassName('contextElt') && + elt.up().readAttribute('id') != cm) { + this._closeSubmenu(this.current.indexOf(id)); + } } + }); ContextSensitive.Element = Class.create({ @@ -219,6 +299,12 @@ ContextSensitive.Element = Class.create({ this.opts = opts; this.opts.left = Boolean(opts.left); this.disable = false; + + /* Add 'contextElt' class to all context children. */ + target = $(target); + if (target) { + target.select('A').invoke('addClassName', 'contextElt'); + } } }); diff --git a/imp/js/src/DimpBase.js b/imp/js/src/DimpBase.js index 114fcfa57..db2d34ed5 100644 --- a/imp/js/src/DimpBase.js +++ b/imp/js/src/DimpBase.js @@ -581,7 +581,6 @@ var DimpBase = { p.left = true; } - p.onShow = this.bcache.get('onMS') || this.bcache.set('onMS', this._onMenuShow.bind(this)); DimpCore.DMenu.addElement(p.id, 'ctx_' + p.type, p); }, @@ -595,11 +594,11 @@ var DimpBase = { DimpCore.DMenu.removeElement($(elt).identify()); }, - _onMenuShow: function(ctx) + _onMenuShow: function(ctx_id) { var elts, folder, ob, sel; - switch (ctx.ctx) { + switch (ctx_id) { case 'ctx_folder': elts = $('ctx_folder_create', 'ctx_folder_rename', 'ctx_folder_delete'); folder = DimpCore.DMenu.element(); @@ -622,7 +621,7 @@ var DimpBase = { break; case 'ctx_message': - [ $('ctx_message_reply_list') ].invoke(this.viewport.createSelection('domid', ctx.id).get('dataob').first().listmsg ? 'show' : 'hide'); + [ $('ctx_message_reply_list') ].invoke(this.viewport.createSelection('domid', ctx_id).get('dataob').first().listmsg ? 'show' : 'hide'); break; case 'ctx_reply': @@ -634,7 +633,9 @@ var DimpBase = { break; case 'ctx_otheractions': - $('oa_seen', 'oa_unseen', 'oa_flagged', 'oa_clear', 'oa_sep1', 'oa_blacklist', 'oa_whitelist', 'oa_sep2', 'oa_undeleted').compact().invoke(this.viewport.getSelected().size() ? 'show' : 'hide'); + $('oa_setflag', 'oa_clearflag', 'oa_sep1', 'oa_blacklist', 'oa_whitelist', 'oa_sep2', 'oa_undeleted').compact().invoke(this.viewport.getSelected().size() ? 'show' : 'hide'); + DimpCore.DMenu.addSubMenu('oa_setflag', 'ctx_otheractions2'); + DimpCore.DMenu.addSubMenu('oa_clearflag', 'ctx_otheractions3'); break; } return true; @@ -2229,6 +2230,7 @@ var DimpBase = { this._setFilterText(true); /* Add popdown menus. */ + DimpCore.DMenu.setOnShow(this._onMenuShow.bind(this)); this._addMouseEvents({ id: 'button_reply', type: 'reply' }, true); DimpCore.DMenu.disable('button_reply_img', true, true); this._addMouseEvents({ id: 'button_forward', type: 'forward' }, true); diff --git a/imp/templates/index/index-dimp.inc b/imp/templates/index/index-dimp.inc index 2776fca7b..2b75d327d 100644 --- a/imp/templates/index/index-dimp.inc +++ b/imp/templates/index/index-dimp.inc @@ -416,10 +416,8 @@ function _simpleButton($id, $text, $image, $imagedir = null) + + + +