Wednesday, May 4, 2011

Adventures in Tristate Checkboxes, Closure, and GWT

For the last week I have been trying to find or create an HTML widget similar to the GMail ‘labels’ menu button, e.g., like this:

gmail-tristate-menu-button(Hey, I found something useful for the contents of my spam folder… to make this screenshot!) 

 

Today I have succeeded with a simple drop-down menu button containing a list of tristate checkboxes. I used (and hacked) the closure library to achieve this. Below is a screen shot of my drop-down menu button from my working demo.

tri-state-menu-button

I compiled these button widgets into a standalone javascript file, which gives some very rich functionality with a pretty small “surface area” API. You just pass in the id of a div tag where you want the menu button, a list of group names, and a callback function to receive changes. The source for the demo menu button is very straightforward. You could copy that buttonlib.x.js file and those css files, and be off and running.

<html>
  <head>
    <script src="buttonlib.x.js"></script>
    <link rel="stylesheet" href="../closure-library/closure/goog/css/common.css"> 
    <link rel="stylesheet" href="../closure-library/closure/goog/css/custombutton.css"> 
    <link rel="stylesheet" href="../closure-library/closure/goog/css/menu.css"> 
    <link rel="stylesheet" href="../closure-library/closure/goog/css/menubutton.css">
    <link rel="stylesheet" href="../closure-library/closure/goog/css/tristatemenuitem.css">
  </head>
  <body>
    <div style="font-size: 11px;"> 
          <div id="btn1" class="goog-custom-button goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">delete Freds</div>
        </div><div id="btn2" class="goog-custom-button goog-custom-button-collapse-left goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">set Freds to mixed</div>
        </div><div id="btn3" class="goog-custom-button goog-custom-button-collapse-left goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">set Pennys to checked</div>
        </div><div id="btn4" class="goog-custom-button goog-custom-button-collapse-left">
            <div style="padding: 0px 5px;">add Daves</div>
        </div>
    </div>
    <br/>
    <div id="btnGroups" style="font-size: 11px; font-family: Arial;"></div>
    <div style="margin-top:500px;"></div>
    <a href="http://www.youtube.com/watch?v=_LrlMoIzSjw" style="font-size: 11px;">the Daves I know</a>
    <script type="text/javascript">
        var mb1 = buttonlib.groupsMenuButtonCreate('btnGroups', 'Add to Groups...', function(groupKey, groupName, isChecked) {
            alert("groupKey="+groupKey+", groupName=" + groupName + ", isChecked=" + isChecked);
        });
        buttonlib.groupsMenuButtonAddItem(mb1, '14', 'Freds', true);
        buttonlib.groupsMenuButtonAddItem(mb1, '21', 'Gails', false);
        buttonlib.groupsMenuButtonAddItem(mb1, '32', 'Harrys', null);
        buttonlib.groupsMenuButtonAddItem(mb1, '47', 'Alices', false);
        buttonlib.groupsMenuButtonAddItem(mb1, '23', 'Pennys', null);
        buttonlib.groupsMenuButtonAddItem(mb1, '12', 'Toms', false);
        buttonlib.buttonBarDecorate('btn1', function() { buttonlib.groupsMenuButtonRemoveItem(mb1, '14'); }); /*freds*/
        buttonlib.buttonBarDecorate('btn2', function() { buttonlib.groupsMenuButtonUpdateItem(mb1, '14', null); }); /*freds*/
        buttonlib.buttonBarDecorate('btn3', function() { buttonlib.groupsMenuButtonUpdateItem(mb1, '23', true); }); /*pennys*/
        buttonlib.buttonBarDecorate('btn4', function() { buttonlib.groupsMenuButtonAddItem(mb1, '41', 'Daves', false); });
    </script>
  </body>
</html>

 

If you want to have a look at the original source code, load this non-compiled version of the demo and view its sources – here you’ll find the javascript is “uncompiled” and still readable. The drop-down menu button code utilizes the goog.ui.TriStateMenuItem class, which did NOT behave as desired. I ended up modifying goog/ui/tristatemenuitem.js and goog/ui/component.js to get the behavior I wanted. (I also edited goog/css/tristatemenuitem.css just to make the menu highlight color consistent with the normal menu ui.)

If I were more experienced with javascript, I would have made my own version of TriStateMenuItem and not touched google’s code. But my initial attempt at this failed, and I don’t have infinite time, so a-hacking I went. For the record, here’s the patch file for the changes I made to three source files in closure.  (If you dig in there, you’ll find the HALFCHECK state I added to component.js – this topic deserves a post of its own, if anyone wishes to discuss it.)

With respect to my earlier post seeking a “native” tri-state checkbox, I am happy to compromise with this solution. The images used to depict the checked and half-checked states do not appear to “belong” to any particular OS/platform, and I think the user is willing to accept their appearance as being “oh, I *am* in a browser…”. To nitpick, I wish these images had an actual “box” for the checkbox. When nothing is checked in the list, everything is blank and there is no visual cue that you might want to click on something. Clearly the screenshot of the GMail interface agrees with this, since they put actual boxes around their checkmarks.

I will now apply this closure widget in my GWT app using JSNI glue per my previous post.