Saturday, September 15, 2007

 

Enhanced AJAXified Tooltips (using Dojo 0.9)

Tooltips can be very useful widgets for displaying some information quickly. Dojo 0.3 had a nice tooltip widget, which I was able to extend further to use in BlogScope. But in Dojo 0.9, the tooltip widget was re-written with lot of functionality removed. I have managed to create a custom tooltip widget that can be used for displaying large amounts text (that can include images and videos too) and that can fetch the contents by performing an asynchronous HTTP request.

A demo for the new widget is available. The new javascript and CSS files can be downloaded. Explanation for how to use the code and how it works is below.

The file application.js defines two new widgets, blogscope.MasterTooltip and blogscope.Tooltip based on dijit.Tooltip and dijit._MasterTooltip. As an end user you only need to create a blogscope.Tooltip object. Each Tooltip contains a MatserTooltip which stores the actual contents. When the tooltip is opened for the first time, an asynchronous I/O request is dispatched to fetch the contents, and in the meantime a loading icon is displayed. When the contents of the tooltip are displayed, the height and width is clipped to a maximum, which makes the tooltip suitable for displaying large amounts of contents. The tooltip is created right next to the mouse cursor, and the user can move/select/copy from the tooltip. When the mouse moves out of the tooltip area, the MasterTooltip is hidden.

To create the tooltip, following HTML code is required


<span style="color: red" id="tooltipConnection1"> your mouse here (tooltip1)</span>
<div id="tooltipContent1" style="display: none;"></div>

This HTML creates a connection target with id tooltipConnection1, mouse over which will show the tooltip. The DIV tooltipContent1, although never really visible, is required as a placeholder for the actual widget. Following Javascript creates the actual tooltip


blogscopeAddOnLoad( function() {
   var tooltip1 = new blogscope.Tooltip({connectId: "tooltipConnection1", 
       href: "./tooltip.php"}, dojo.byId("tooltipContent1"));
    }
    );

Above code is creating a new object of type blogscope.Tooltip. Note that the javascript is wrapped in blogscopeAddOnLoad, which is a function similar to dojo.addOnLoad, to ensure that the tooltip widget declaration is loaded in advance. When the tooltip widget is created, the node with id tooltipContent1 is replaced by the widget. Two parameters, href and connectId are required for every tooltip. Additional parameters that can be specified include maxWidth, maxHeight and label.

Those who simply wish to use the new Tooltip widget can copy the js and css file, and include it in the HTML (along with dojo 0.9). For those interested in details, the javascript code for the two new widgets is as follows (see application.js for complete code). Note that we want this code to be executed after the Dojo libs finish loading, and therefore everything is inside dojo.addOnLoad.


dojo.addOnLoad( function() {
    dojo.provide("blogscope.Tooltip");
    dojo.provide("blogscope.MasterTooltip");
    // A lot of code here was originaly copied from Tooltip.js in dojo source code, and then modified.
    // Refer to dijit/Tooltip.js for more documentation and explanation
    dojo.declare(
        // Each tooltip has a master tooltip which contains the actual content
        "blogscope.MasterTooltip",
        dijit._MasterTooltip,
        {
            // the fade in fade out duration
            duration: 50,
            parentTooltip: null,
            templateString: "<div id=\"dijitTooltip\" class=\"dijitTooltip dijitTooltipBelow\">\n\t<div class=\"dijitTooltipContainer dijitTooltipContents blogscopeTooltipContainer\" id=\"dijitTooltipContainer\" dojoAttachPoint=\"containerNode\" waiRole='alert'></div>\n\t<div class=\"dijitTooltipConnector blogscopeTooltipConnector\"></div>\n</div>\n",
            show: function(/*DomNode*/ aroundNode){
                if(this.fadeOut.status() == "playing"){
                    // previous tooltip is being hidden; 
                    // wait until the hide completes then show new one
                    this._onDeck=arguments;
                    return;
                }
                this.containerNode.innerHTML=this.parentTooltip.label;
                // Firefox bug. when innerHTML changes to be shorter than previous
                // one, the node size will not be updated until it moves.
                this.domNode.style.top = (this.domNode.offsetTop + 1) + "px";
    
                var align = {'BL' : 'TL'};
                var pos = dijit.placeOnScreen(this.domNode, {x: this.parentTooltip.mouseX, y: this.parentTooltip.mouseY}, ["TL"]);
                this.domNode.className="dijitTooltip dijitTooltipBelow";
    
                // show it
                dojo.style(this.domNode, "opacity", 0);
                this.fadeIn.play();
                this.isShowingNow = true;
                // In our template string, we have 3 div nodes nested in one another
                // for asthetic reasons, we chose to adjust dimension of the innermost DIV
                var adjustNode = getFirstDivChild(getFirstDivChild(this.domNode));
                adjustDimensions(adjustNode, this.parentTooltip.maxWidth, this.parentTooltip.maxHeight);
            },
            // refresh the contents of the tooltip
            refresh: function() {
                this.containerNode.innerHTML=this.parentTooltip.label;
                if (this.isShowingNow == true) {
                    var adjustNode = getFirstDivChild(getFirstDivChild(this.domNode));
                    adjustDimensions(adjustNode, this.parentTooltip.maxWidth, this.parentTooltip.maxHeight);
                }
            },
            // called once after creation of the widget
            postCreate: function(){
                dojo.body().appendChild(this.domNode);
                this.bgIframe = new dijit.BackgroundIframe(this.domNode);
                // I wanted to set a unique id for the domNode, but getUniqueId does not work with IE6
                // If the line below is uncommented, it prints error in IE6
                // this.domNode.id = dijit.getUniqueId(this.declaredClass.replace(/\./g,"_"));
                // Setup fade-in and fade-out functions.
                this.fadeIn = dojo.fadeIn({ node: this.domNode, duration: this.duration, onEnd: dojo.hitch(this, "_onShow") });
                this.fadeOut = dojo.fadeOut({ node: this.domNode, duration: this.duration, onEnd: dojo.hitch(this, "_onHide") });
                // connect the event of mouse moving out
                this.connect(this.domNode, "onmouseout", "_onMouseOut");
            },
            // when mouse moves out
            _onMouseOut: function(/*Event*/ e){
                this.parentTooltip._onMouseOut(e);
            }
        }
    );
    // blogscope.Tooltip is the main widget that we will use
    dojo.declare(
        "blogscope.Tooltip",
        dijit.Tooltip,
        {
            // false if the contents are yet to be loaded from the HTTP request
            hasLoaded: false,
            // location from where to fetch the contents
            href: "",
            // max height and width of the tooltip
            maxWidth: 400,
            maxHeight: 100,
            // the position of mouse when the tooltip is created
            mouseX: 0,
            mouseY: 0,
            // contents to diplay in the tooltip. Initialized to a loading icon.
            label: "<div><img src=\"loading.gif\"> Loading...</div>",
            masterTooltip: null,
            loadContent: function() { 
                if (this.hasLoaded == false) {
                    this.hasLoaded = true;
                    dojo.xhrGet({
                        url: this.href,
                        parentTooltip: this,
                        load: function(response, ioArgs){
                            console.log("data received from ", this.url);
                            console.log("parent is "+this.parentTooltip);
                            this.parentTooltip.label = response;
                            if(this.parentTooltip.isShowingNow){
                                this.parentTooltip.masterTooltip.refresh();
                            }
                        },
                        handleAs: "text"
                    }); 
                }
            },
            open: function(){
                if (this.masterTooltip == null) {
                    // initialized the master tooltip
                    this.masterTooltip = new blogscope.MasterTooltip({parentTooltip: this});
                }
                this.loadContent();
                if(this.isShowingNow){ return; }
                if(this._showTimer){
                    clearTimeout(this._showTimer);
                    delete this._showTimer;
                }
                this.masterTooltip.show(this._connectNode);
                this.isShowingNow = true;
            },
            close: function(){
                if(!this.isShowingNow){ return; }
                if (this.masterTooltip != null) {
                    this.masterTooltip.hide();
                }
                this.isShowingNow = false;
            },
            _onMouseOut: function(/*Event*/ e){
                // currXD and Y are current position of the mouse
                var currX = e.pageX;
                var currY = e.pageY;
                // this.mouseX and this.mouseY are the top-left corners of the tooltip
                var posOffset = Math.abs(this.mouseX - e.pageX) + Math.abs(this.mouseY - e.pageY);
                // console.log("Mouse out called with ",e);
                // we allow mouse movement of 6px
                if (posOffset < 6) {
                    return;
                }
                if(dojo.isDescendant(e.relatedTarget, this._connectNode)){
                     // false event; just moved from target to target child; ignore.
                    return;
                }
                if (this.masterTooltip != null) {
                    // get coordinates and dimensions of the actual tooltip contents
                    var c = dojo.coords(this.masterTooltip.domNode);
                    console.log("Tooltip coordinates", c, "Curr", currX, currY, "MouseXY", this.mouseX, this.mouseY);
                    if (this.mouseX < currX && this.mouseX + c.w > currX && this.mouseY < currY && this.mouseY + c.h > currY) {
                        // the mouse is still on the tooltip contents, no need to close it
                        return;
                    }
                }
                // close the tooltip
                this._onUnHover(e);
            },
            _onHover: function(/*Event*/ e){
                if(this._hover){ return; }
                this._hover=true;
                // find the current mouse postion, the top-left corner of the tooltip will be here
                this.mouseX = e.pageX;
                this.mouseY = e.pageY;
                console.log("Mouse X,Y is "+this.mouseX+", "+this.mouseY);
                // If tooltip not showing yet then set a timer to show it shortly
                if(!this.isShowingNow && !this._showTimer){
                    this._showTimer = setTimeout(dojo.hitch(this, "open"), this.showDelay);
                }
            },
            _onMouseOver: function(/*Event*/ e){
                this._onHover(e);
            }
        }
    );
    // we have finished declaration of the widget
    // now exectute all functions that were waiting for the custom widget to load
    blogscopeTooltipLoaded = true;
    dojo.forEach(blogscopeLoaders,
        function(f) {
            f();
        }
    )
}
);

Code for blogscopeAddOnLoad, which ensures that the declaration of the new widget is loaded before executing more javascript is


// This is a list of functions that need to be executed once the custom widget
// blogscope.Tooltip loads
var blogscopeLoaders = [];
// This variable is set to true after blogscope.Tooltip declaration finishes loading
var blogscopeTooltipLoaded = false;
// This function is similar to dojo.addOnLoad, but it ensures that the
// custom blogscope.Tooltip widget declaration is loaded before the function call is made
blogscopeAddOnLoad = function (/*Function*/ f) {
    if (blogscopeTooltipLoaded) {
        console.log("Calling the function", f);
        f();
    } else {
        console.log("Pushing the function to queue", f);
        blogscopeLoaders.push(f);
    }
}

The code to clip the tooltip to a max width and height is


// Adjusts the dimension of the supplied node so that it does not exceeds
// the max width and height
function adjustDimensions(/*DomNode*/node, /*int*/maxWidth, /*int*/maxHeight) {
    var sHeight = node.scrollHeight;
    var sWidth = node.scrollWidth;
    console.log("height and width are "+sHeight+" and "+sWidth+" for "+node+" with id "+node.id);
    // console.log("max height and width are "+maxHeight+" and "+maxWidth);
    if (sHeight > maxHeight) {
            console.log("resizing to max height "+maxHeight);
            node.style.height = maxHeight + "px";
            node.style.overflow = "auto";
    }
    if (sWidth > maxWidth) {
            console.log("resizing to max width "+maxWidth);
            node.style.width = maxWidth + "px";
            node.style.overflow = "auto";
    }
}

Enjoy!

Labels: , , ,


Comments:
hey nilesh, I know you somehow because you were there in bansals, Kota. I need a small info from you regarding dojo. Is there a workaround possible so that we can use DOJO 0.41 as well as d new features in 0.9 ? I cant spend much time on this problem and thought that you might have encountered it in the past. I tried contacting a few people who said it is impossible.. but m still not giving up hope.
 
Thanks! I had a problem with custom tooltip placement in dojo 0.9 too.
 
This is some very interesting code and sounds like it is exactly what I have been trying to do myself. But the big question is -- Will this work with Dojo 1.1.3. So far, I've been trying this code and it never seems to make the item I want enabled for hover over... I am suspicious that it is broken with versions of dojo outside of what you tested with... Any Ideas/comments???

Many thanks in any case as it is very educational even if it does not work.
 
@anon -- this code does not work with Dojo versions > 0.9.
 
This comment has been removed by a blog administrator.
 
This comment has been removed by a blog administrator.
 
This comment has been removed by a blog administrator.
 
Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?