Add support for submenus to contextmenu JS code.
authorMichael M Slusarz <slusarz@curecanti.org>
Tue, 3 Feb 2009 04:26:22 +0000 (21:26 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Mon, 9 Feb 2009 08:13:19 +0000 (01:13 -0700)
imp/js/ContextSensitive.js
imp/js/src/ContextSensitive.js
imp/js/src/DimpBase.js
imp/templates/index/index-dimp.inc
imp/themes/graphics/popright.png [new file with mode: 0644]
imp/themes/screen-dimp.css
imp/themes/silver/graphics/popright.png [new file with mode: 0644]
imp/themes/silver/screen-dimp.css

index 5735c54..e20bec4 100644 (file)
@@ -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
index 290222c..910e453 100644 (file)
@@ -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');
+        }
     }
 
 });
index 114fcfa..db2d34e 100644 (file)
@@ -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);
index 2776fca..2b75d32 100644 (file)
@@ -416,10 +416,8 @@ function _simpleButton($id, $text, $image, $imagedir = null)
 <div class="context" id="ctx_otheractions" style="display:none">
  <a id="previewtoggle"><span class="contextImg previewImg"></span><?php echo ($prefs->getValue('dimp_show_preview') ? _("Hide Preview") : _("Show Preview")); ?></a>
  <div class="sep"></div>
- <a id="oa_seen"><span class="contextImg mailseenImg"></span><?php echo _("Mark as Read") ?></a>
- <a id="oa_unseen"><span class="contextImg mailunseenImg"></span><?php echo _("Mark as New") ?></a>
- <a id="oa_flagged"><span class="contextImg mailflaggedImg"></span><?php echo _("Flag Message") ?></a>
- <a id="oa_clear"><span class="contextImg clearflagImg"></span><?php echo _("Clear Flag") ?></a>
+ <a id="oa_setflag"><span class="contextImg mailflaggedImg"></span><?php echo _("Set Flag") ?></a>
+ <a id="oa_clearflag"><span class="contextImg clearflagImg"></span><?php echo _("Clear Flag") ?></a>
  <div class="sep" id="oa_sep1"></div>
 <?php if ($has_blacklist || $has_whitelist): ?>
 <?php if ($has_blacklist): ?>
@@ -438,6 +436,16 @@ function _simpleButton($id, $text, $image, $imagedir = null)
 <?php endif; ?>
 </div>
 
+<div class="context" id="ctx_otheractions2" style="display:none">
+ <a id="oa_unseen"><span class="contextImg mailunseenImg"></span><?php echo _("Mark as New") ?></a>
+ <a id="oa_flagged"><span class="contextImg mailflaggedImg"></span><?php echo _("Flag Message") ?></a>
+</div>
+
+<div class="context" id="ctx_otheractions3" style="display:none">
+ <a id="oa_seen"><span class="contextImg mailseenImg"></span><?php echo _("Mark as Read") ?></a>
+ <a id="oa_clear"><span class="contextImg clearflagImg"></span><?php echo _("Clear Flag") ?></a>
+</div>
+
 <div class="context" id="ctx_contacts" style="display:none">
  <div><?php _createDA(_("New Message"), 'compose.png', 'ctx_contacts_new') ?></div>
  <div><?php _createDA(_("Add to Address Book"), 'add_contact.png', 'ctx_contacts_add') ?></div>
diff --git a/imp/themes/graphics/popright.png b/imp/themes/graphics/popright.png
new file mode 100644 (file)
index 0000000..9aac6c1
Binary files /dev/null and b/imp/themes/graphics/popright.png differ
index 3f6d041..c3c0411 100644 (file)
@@ -811,9 +811,12 @@ span.attachName:hover {
     color: #fff;
     cursor: pointer;
 }
-.context a img {
-    vertical-align: middle;
-    padding-right: 4px;
+.context a span.contextExpand {
+    float: right;
+    margin-top: 2px;
+    width: 4px;
+    height: 9px;
+    background: url("graphics/popright.png") no-repeat;
 }
 .context div.sep {
     border-bottom: 1px #d4d4d4 solid;
diff --git a/imp/themes/silver/graphics/popright.png b/imp/themes/silver/graphics/popright.png
new file mode 100644 (file)
index 0000000..1dec43e
Binary files /dev/null and b/imp/themes/silver/graphics/popright.png differ
index c27d5f4..4e9ad2d 100644 (file)
     border-top: none;
 }
 
+.context a span.contextExpand {
+    width: 16px;
+    height: 16px;
+    background-image: url("graphics/popright.png");
+    margin-top: -1px;
+}
+
 span.mailseenImg {
     background-image: url("graphics/mail_seen.png");
 }