Tuesday, March 11, 2008

 

Creating Custom Dojo Widget using Cross-Domain Build

Dojo comes with a great set of widgets, but is it often necessary to create custom widgets for application specific needs. In this tutorial, I will walk through an example of such a custom widget. The custom widget resides on my own server, while rest of the Dojo (and dijit) is used from AOL CDN hosted xdomain build. Live demo of the widget is also available. This custom widget is built on Dojo 1.0.2 and works great on Firefox 2, Konqueror 3.5.1, IE 6 and IE 7.

We will create a new widget named blogscope.TestWidget which extends the dijit.TitlePane. Looking at the TitlePane code, one can see the empty function onDownloadEnd which is called when the widget finishes downloading content from an external url. For the custom widget, we will extend the TitlePane by filling in this function with a simple alert() statement.

First, we will use dojo.declare to extend the dijit widget to create the new custom widget. I will omit the details of this part, but you can find examples elsewhere.


dojo.provide("blogscope.TestWidget");
dojo.require("dijit.TitlePane");
dojo.declare(
"blogscope.TestWidget",
[dijit.TitlePane],
{
 onDownloadEnd: function(currentConent){
  // summary:
  //  called when download is finished
  alert('Contents Loaded from '+this.href);
 }
}

If you are not using AOL CDN, then simply put the code above in file TestWidget.js. Place this file in directory blogscope such that the dojo path is /js/dojo/ and this widget file is in /js/blogscope/TestWidget.js. Then you can register the widget as dojo.registerModulePath("blogscope", "../blogscope") (the directory name containing the custom widget is relative to the Dojo directory) and use dojo.require("blogscope.TestWidget") in your code. The name of the widget has to be the same as the name of the file containing the code.

Since we wish the xdomain build of Dojo, we need to do some more work. First, the custom widget source code has to be in file TestWidget.xd.js and not TestWidget.js. For the cross-domain build we need to add some more information as to what this module is providing and what all modules it depends on so that Dojo loading system can make sure that the dependencies are met. The contents of this file are as shown below.


dojo._xdResourceLoaded({
depends: [["provide", "blogscope.TestWidget"],
["require", "dijit.TitlePane"]],
defineResource: function(dojo){
 if(!dojo._hasResource["blogscope.TestWidget"]){ 
  dojo._hasResource["blogscope.TestWidget"] = true;
  dojo.provide("blogscope.TestWidget");
  dojo.require("dijit.TitlePane");
  dojo.declare(
  "blogscope.TestWidget",
  [dijit.TitlePane],
  {
   onDownloadEnd: function(currentConent){
    // summary:
    //  called when download is finished
    alert('Contents Loaded from '+this.href);
   }
  }
 );
 }
}});

Note: Ideally, one should use the dojo build system to generate .xd.js files from .js code. But I don't know how to use the build system, so I created the file manually. The main drawback of manual authoring is that the format of .xd.js files may change with version (it is already different for Dojo version 1.1).

Copy the file TestWidget.xd.js on the server such that it is available at say http://www.blogscope.net/js/dojo/blogscope/TestWidget.xd.js. Modify the djConfig variable to set module path as shown below.


<script type="text/javascript"
  djConfig="isDebug: true, parseOnLoad: true, useXDomain: true, modulePaths: {'blogscope': 'http://www.blogscope.net/js/dojo/blogscope'}"
  src="http://o.aolcdn.com/dojo/1.0.2/dojo/dojo.xd.js"></script>
 <script type='text/javascript'>
 dojo.require("blogscope.TestWidget");
 dojo.require("dojo.parser");
 </script>

Since, the module name blogscope is registered to path http://www.blogscope.net/js/dojo/blogscope, dojo.require() adds the a link to http://www.blogscope.net/js/dojo/blogscope/TestWidget.xd.js which can be seen using Firebug.

The widget is now ready to use using the following HTML.


<div dojoType="blogscope.TestWidget" open="false" title="Click to Expand the Widget" href="ajaxContents.html">
</div>

Also See

Labels: , , , ,


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: , , ,


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