Search
Categories
Monday
15Mar2010

Things that make you go... AH HAH!!

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" rendered="false">

    <xp:this.afterRenderResponse>
        <![CDATA[#{javascript:
            var con = facesContext.getExternalContext();
            var writer = facesContext.getResponseWriter();
            var response = con.getResponse();
           
            response.setContentType('application/x-gzip');
            response.setHeader('Cache-Control', 'no-cache');
            response.setHeader('Content-disposition', 'attachment, filename="' + database.getTitle().split(' ').join('') + '-' + new Date().toUTCString() + '.xml.gz"');
             
            writer.write(session.createDxlExporter().exportDxl(database.getAllDocuments()));
        }]]>
    </xp:this.afterRenderResponse>

</xp:view>

Make sure you have gzip compression turned on ....

Sunday
14Mar2010

Writing Client-Side Javascript for Re-Use, Part III


This is the third post in a series on reuseable javascript objects. If you have not read them yet, start with the first and second posts.

Now, lets discuss how we get dojo to load our new class from an NSF. Normally you would do this by including a dojo module through the XPage's resources tab, or you would use the javascript code:

dojo.require('com.ZetaOne.widget.listControl');

Here's where we have to perform a little "magic" to make dojo aware of our javascript repository that exists in an nsf.  You see, all dojo javascript is located in the domino\js\ folder of your data directory, and this folder is mapped to /domjs on the webserver.  If we were to ask dojo to load our class "com.ZetaOne.widget.listControl" it would look for the file called "listControl.js" in the /domjs/dojo-1.3.2/com/ZetaOne/widget folder of our webserver. Which it won't find.

So, I've made a slight modification to the dojo.require function, and named it dojo.requireFromNsf so we can easily require javascript objects located in any nsf.  You'll need to download this file, and place it in the folder:

[DOMINO DATA]/domino/js/dojo-1.3.2/dojo/_base/_loader

name the file loader_nsf.js

The file you are placing is called loader_nsf.js. This file is NOT obfuscated or compressed at this point, so you can review it. I'd recommend compressing it before rolling out into production.

The dojo.requireFromNsf() function takes an extra parameter that dojo.require does not. It's the base path to our reusable javascript library NSF. So, to load our listControl widget, we'd include the following line in a client side javascript library.

dojo.requireFromNsf('com.ZetaOne.widget.listControl', '/zojs.nsf/');

The '/zojs.nsf/' is the path the re-usable javascript component database you created from post #2. Make sure to include the full path, including the beginning and ending '/'. For more details about the dojo.requireFromNsf function, review the source code.

Now, we can load our javascript code from our repository. To do so, let's open the database where we are implementing the sample listControl widget, and create a client side javascript library.  Let's practice our namespacing here so create the library with a name that represents your reverse domain and put it in the app namespace, along with listControlSample.js as the actual class name. For example, my fully qualified javascript library name would be com/ZetaOne/app/listControlSample.js .

Let's include our basic class construct:

dojo.provide("com.ZetaOne.app.listControlSample");

(function(){
   
        dojo.declare("com.ZetaOne.app.listControlSample", null, {
            constructor: function() {

            },
            destroy: function() {

            }
        })
})();


Then, lets add the code to requireFromNsf our listControl widget, and a line to alert us that our class has been created so we can see it in action:

dojo.provide("com.ZetaOne.app.listControlSample");

dojo.require("dojo._base._loader.loader_nsf");
dojo.requireFromNsf('com.ZetaOne.widget.listControl', '/zojs.nsf/');

(function(){
   
        dojo.declare("com.ZetaOne.app.listControlSample", null, {
            constructor: function() {
                alert('com.ZetaOne.app.listControlSample Created!');
            },
            destroy: function() {

            }
        })
})();

And for now, lets add a line to end of this library to actually create an instance of our new class. Normally, we would seperate this out into a different controller script rather than in the class script itself, but we'll review that later.  So for now, and the following line to the end of the script:

window.listControlSample = new com.ZetaOne.app.listControlSample();

Now, save and include this client side javascript library in the xpage you created in Part II.  You should be able now preview your XPage in the browser, and watch it load the com.ZetaOne.widget.listControl object from your NSF javascript repository, AND you should be able to select different nodes in the list. In my next post I'll post a live sample of this in play and have a set of downloadable sample databases, as well as explain how to further this functionality to your individual XPages and custom controls to create a TRUE Model-View-Controller implementation of your XPage apps, with your own re-usable javascript component library.

Happ Coding!

Sunday
14Mar2010

Writing Client-Side Javascript for Re-Use, Part II

This is the second post in a series on reuseable javascript objects. If you have not read it yet, start at the first post here.

Now, let's continue on by creating a new, blank database somewhere on your Domino webserver to serve as your new re-usable javascript code repository. A simple URL denoting reusable javascript objects is probably best. For example, at ZO, I've created a database titled "ZetaOne Javascript Repository" in the root of the data directory called zojs.nsf.  Give anonymous reader access to the database so it can be referenced from any application. In that new database, lets create a new client side javascript library. with the name "com/ZetaOne/widget/listControl.js".  The name is important.  The script library name needs to match what we are going to define our new class as. I'll explain why later.

Lets get started by entering a few basic lines to define the actual class:


dojo.provide("com.ZetaOne.widget.listControl");

(function(){
        dojo.declare("com.ZetaOne.widget.listControl", dijit._Widget, {
            create: function(params, srcNodeRef) {

            },
            destroy: function() {

            }
        })
})();

This is the very basic definition if our new class. the first line "dojo.provide" plugs our new class into the dojo runtime, so dojo knows it has been loaded. This is important, as different objects can reference the same class multiple times, but it will only be loaded one time to save download time and bandwidth, and to prevent existing data from getting over-written by one module reloading an existing module.

The next line "(function(){" creates whats known as an anonymous function that allows us to define our new "class". Which is where the third line "dojo.declare" comes in. That block of code creates a new "class" called com.ZetaOne.widget.listControl that inherits from dijit._Widget (dijit._Widget is the base class in dojo from which most all UI controls are derived. We won't get into why we're inheriting from this class in this post, just know that we have basically now extended dijit._Widget).  Our new class also has two new members "create" and "destroy" which are just like standard constructors and destructors in languages that implement a real class.

We can add additional member objects, variables and functions by declaring them just like the create and destroy functions are defined.  For reference, here is the entire implementation of the com.ZetaOne.widget.listControl object as it has been implemented as a class object. We won't get into the nitty gritty of what is going on with this class, but it's here as an example. In a later post, we'll get deeper into this actual object to describe the mechanations at work.

dojo.provide("com.ZetaOne.widget.listControl");

(function(){
        // PUBLIC MEMBERS
        dojo.declare("com.ZetaOne.widget.listControl", dijit._Widget, {
            create: function(params, srcNodeRef) {
                    if (typeof srcNodeRef == 'undefined') {
                        console.error('com.ZetaOne.widget.listControl requires a DOM node!');
                        return false;
                    } else
                        this.domNode = srcNodeRef;
        
                    this.id = this.domNode.id;
                    this.inherited(arguments);
                    this.selectMultiple = dojo.attr(this.domNode, 'selectMultiple');                            
                    this.groupClass = dojo.attr(this.domNode, 'groupClass');
                    this.itemClass = dojo.attr(this.domNode, 'itemClass');
                    this.selectedClass = dojo.attr(this.domNode, 'selectedClass');
                    this.lastSelectedClass = dojo.attr(this.domNode, 'lastSelectedClass');                    
                            
                    this.selectMultiple = (this.selectMultiple == null) ? "true" : eval(this.selectMultiple);
                    
                    this.groupClass = (this.groupClass == null) ? 'zoListControlGroup' : this.groupClass;
                    this.itemClass = (this.itemClass == null) ? 'zoListControlItem' : this.itemClass;
                    this.selectedClass = (this.selectedClass == null) ? 'zoListControlSelected' : this.selectedClass;
                    this.lastSelectedClass = (this.lastSelectedClass == null) ? 'zoListControlLastSelected' : this.lastSelectedClass;
                                    
                    this._itemClassQuery = '.' + this.itemClass;
                    this._groupClassQuery = '.' + this.groupClass;
                    this._selectedClassQuery = '.' + this.selectedClass;
                    this._lastSelectedClassQuery = '.' + this.lastSelectedClass;
                
                    if (dojo.isIE)
                        document.onselectstart = function() { return false; }
                    else {            
                        document.onmousedown=this._disableselect;
                        document.onclick=this._reEnable;
                    }
                    
                    // associate onclick events
                    var selectables = dojo.query(this._itemClassQuery);
                    for (i=0;i<selectables.length;i++) {
                        dojo.addClass(selectables[i], this.groupClass);
                        this.connect(selectables[i], 'onclick', this._captureClick);
                    };
                    
                    return;                            
            },
            destroy: function() {
                    dijit.registry.remove(this.id);
                    dojo.forEach(this._connects, this.disconnect);
                    this.inherited(arguments);        
            },
            pop: function() {
                    var nodes = dojo.query(this._groupClassQuery + this._selectedClassQuery);
                    for (i=0;i<nodes.length;i++) {
                        nodes[i].parentNode.removeChild(nodes[i]);
                        this.disconnect(this._connects[nodes[i].id]);
                    }
                    nodes.removeClass(this.selectedClass);
                    nodes.removeClass(this.itemClass);
                    nodes.removeClass(this.groupClass);
                    nodes.removeClass(this.lastSelectedClass);
                    return nodes;                    
            },
            push: function(nodes, select) {
                    nodes.addClass(this.groupClass);
                    nodes.addClass(this.itemClass);
                    if (select) nodes.addClass(this.selectedClass);
                    
                    for (i=0;i<nodes.length;i++) {
                        this.domNode.appendChild(nodes[i]);
                        this.connect(nodes[i], 'onclick', this._captureClick);
                    }
            },
            connect: function(
                    /*Object|null*/ obj,
                    /*String|Function*/ event,
                    /*String|Function*/ method){
                var d = dojo;
                var dc = dojo.connect;
                this._connects[obj.id] = dc(obj, event, this, method);
                return this._connects[obj.id];
            },
            disconnect: function(/*Object*/ handle){
                // summary:
                //        Disconnects handle created by this.connect.
                //        Also removes handle from this widget's list of connects
                // tags:
                //        protected
                var i = dojo.indexOf(this._connects, handle);
                dojo.disconnect(handle);
                this._connects.splice(i, 1);
                return;
            },            
            // PRIVATE MEMBERS    
            _selectItem: function(selection, e) {
                    
                    var selNode = dojo.byId(selection);
                    
                    if (((!e.ctrlKey) && (!e.shiftKey)) || !this.selectMultiple) {
                    
                      dojo.query(this._groupClassQuery + this._itemClassQuery).removeClass(this.selectedClass);
                      dojo.addClass(selNode, this.selectedClass);
                    
                    } else if ((e.ctrlKey) && (!e.shiftKey))
                    
                      if (dojo.hasClass(selNode, this.selectedClass))
                        dojo.removeClass(selNode, this.selectedClass)
                      else
                        dojo.addClass(selNode, this.selectedClass);
                    
                    else if (e.shiftKey) {
                    
                      var idx = dojo.indexOf(selNode.parentNode.childNodes, selNode);
                      var lastIdx = dojo.indexOf(selNode.parentNode.childNodes, dojo.query(this._groupClassQuery +
                              this._groupClassQuery + this._lastSelectedClassQuery)[0])
                    
                      if (idx == lastIdx) {
                          // user just shift clicked the same node again, don't do anything
                          return true;
                      }
                
                      if (!e.ctrlKey) {
                        // clear all previous selections first, re-add lastSelected, then process on
                        dojo.query(this._groupClassQuery + this._selectedClassQuery).removeClass(this.selectedClass);
                        dojo.query(this._groupClassQuery + this._selectedClassQuery).addClass(this.selectedClass);
                        dojo.addClass(selection, this.selectedClass);
                      }
                      
                      if (lastIdx == -1) {  // the last selected was not in the same parent, so we can't shift click, so just reset the focused nodes and select the single node ...
        
                        dojo.query(this._groupClassQuery + this._selectedClassQuery).removeClass(this.selectedClass);      
                        dojo.addClass(selNode, this.selectedClass);
        
                      } else {
        
                          if (idx < lastIdx) {
                            var el = selNode;
                            for ( ; lastIdx >= idx; idx++) {
                              if ((el.nodeType == 1) && (dojo.hasClass(el, this.groupClass)))
                                dojo.addClass(el, this.selectedClass);
                              el = el.nextSibling;
                            }        
                          } else if (idx > lastIdx) {
                            var el = selNode;
                            for ( ; lastIdx <= idx; idx--) {
                              if ((el.nodeType == 1) && (dojo.hasClass(el, this.groupClass)))
                                dojo.addClass(el, this.selectedClass);
                              el = el.previousSibling;
                          }
                        }        
                      }    
                    }
                    
                    dojo.query(this._groupClassQuery + this._lastSelectedClassQuery).removeClass(this.lastSelectedClass);
                    dojo.addClass(selNode, this.lastSelectedClass)
            },    
            _disableselect: function(e) { return false; },
            _reEnable: function() { return true; },
            _captureClick: function() {
                var e = (!arguments[0]) ? window.event : arguments[0];
                var selection = (e.target) ? e.target : e.srcElement;
                this._selectItem(selection.id, e);
        
            }
        })
})();    

Copy and paste that code into your "com/ZetaOne/widget/listControl.js" library, then save and close it.
Now in a different database, let's create an XPage that consumes this object. First create an XPage with the following code, making sure to set dojo ParseOnLoad and dojoTheme to true in the XPage's All Properties:

        <xp:div dojoType="com.ZetaOne.widget.listControl" styleClass="sourceUserList"
            id="userSourceList">
            <xp:this.dojoAttributes>
                <xp:dojoAttribute name="groupClass" value="userGroup" />
                <xp:dojoAttribute name="itemClass" value="userItem" />
                <xp:dojoAttribute name="selectedClass" value="focused" />
            </xp:this.dojoAttributes>
            <xp:repeat value="#{javascript:return new Array('Item 1','Item 2', 'Item 3', 'Item 4');}" var="profileVar" indexVar="profileIndex"
                rows="4">
                <xp:div id="userSelectable" styleClass="userSelectable userItem">
                    <xp:text escape="true" value="#{profileVar}" disableTheme="true" />
                </xp:div>
            </xp:repeat>
        </xp:div>


There are a couple of things to point out in the above code. First, notice the first xp:div tag has a dojoType that matches the class we defined earlier "com.ZetaOne.widget.listControl".  This instructs dojo to create an object out of this HTML element based on that class.  The <xp:dojoAttribute> tags are used to pass parameters to the constructor function of the class. Additionally, if you have been following along in this series so far, you might notice that we are don't have code in the XSP markup to handle the onClick events when the user selects an item. This has been automated in the class, and we'll review in a future post.

To make this a working model, you'll also need to assemble the Cascading Style Sheet from the previous two posts, place it in the database, and include it as a reference on this page.
We now have a complete implementation of the listControl object in an application, BUT we still need to tell the browser to actually load the javascript class that we created in our new javascript repository.

We'll cover that in Part 3.

Sunday
14Mar2010

Writing Client-Side Javascript for Re-Use

Many of us "grew up" through the evolution of Lotus Notes and Domino and have been conditioned to not use our tools in the the cleanest, most effective manner. For example, quite often we have scores of LotusScript libraries, chocked full of one-off sub-routines, functions that work together, but have no logical connection in code like a class, and quite frequently these libraries are not well document, coded "ugly" instead of readable, and have lots of "legacy implementations" making it difficult for us to adopt better, if not best practices.

With the advent of XPages, and a new set of tools with which to build our applications, we get a unique opportunity to "get it right" from the beginning. The thoughts of "best practices" and how to implement Server Side and Client Side Javascript libraries for re-use in XPages has been on my mind heavily of late. I've come to the conclusion that we don't have to re-invent the wheel. We already have a powerful model to follow in Dojo.

What does this mean? Well over the next few posts, I'll take a look at a few concepts, and some practical applications around them.

Lets talk first about client-side javascript and namespaces.

If you haven't spent a lot of time learning the intricacies of how Dojo, or any other javascript framework works, such as Ext, jQuery, or the like, and haven't delved into Java, you may not be that familiar  with the concept of namespaces.  While the full bredth and scope of why namespaces are a "best practice" and what they mean overall is beyond this post, let me briefly describe them and point out some obvious reasons why they are a good idea.

If you looked at all into using dojo in XPages, or looked at the XSP object at all, you may have noticed how all the dojo modules are named similar to "dojo.dnd.Source" or "dijit.layout.BorderContainer". Quite simply these are "namespaces" and are meant to uniquely identify a block of code, similar in concept to classes. While javascript does not have classes per se, the flexibility of the language has allowed the creation of constructs that give javascript objects class-like functionality.

From the simplest perspective, these namespaces help us to keep code from one script from stepping on code from another. The flexibility inherent in javascript to .prototype objects, or to create objects without private or protected members easily allows this to happen.

So as a best practice, you should start to define your own namespaces in your code, creating a base of re-usable components that you can use from XPage application to XPage application.

That being said, because domino is SO good at being a distributed platform, it often makes it difficult to effectively share objects between applications. Especially javascript libraraies. Code control can become a nightmare as you develop revisions, and then place them in different nsf stores. Sure you can do overly complex multi-template inheritance to create a psuedo code-revision control "framework", but it is very fragmented at best.

What I've done here at ZetaOne is to create a single script library database for all re-usable client side Javascript libraries. What this allows me to do is "require" any reusable piece of javascript code from this single library. Every logical block of code is then coded into a namespace, designed after the "Dojo" way of defining namespaces and objects. As we continue this series, we'll start to build onto these namespaces to enable greater and greater functionality, and integration into the dojo namespace.

So, lets look specifically at what at namespace is, and how to implement it. Simply put, namespaces are a unique identifier assigned to a block of code. The "Dojo Way" is to start with a base names set (in dojo's namespace, its "dojo", and then from there you add a "." and then the name of the "class" you want to implement. For example, all dojo drag and drop functionality is located under the "dojo.dnd" namespace.

Ok, so what do we use for our own namespace?  Well, the accepted convention is your reverse domain name. For example, all my code is under the "com.ZetaOne" namespace. I then add on from there for each particular class or application I create.  For example, as I build reusable objects that represent backend functions and operations, I create them under the "com.ZetaOne.api" namespace.  Objects that are UI controls, for users to interact with are under the "com.ZetaOne.widget" namespace. In single database applications, each application has it's own namespace under "com.ZetaOne.app", and larger, multi-applcation, multi-database systems usually get their own namespace directly under "com.ZetaOne", such as "com.ZetaOne.AppName".

Ok, so there is the basics on what we call our namespace, how do we actually define it? Well, in my next post, i'll start by taking the HTML selection function I wrote about in my last few posts, and convert it to a re-usable, namespaced object.

Stay tuned and Happy Coding!

Tuesday
09Mar2010

Follow The XPages Blog on Twitter

The XPages Blog now has a Twitter account:

http://twitter.com/xpagesblog

Please follow us to get the latest and greatest XPages goodness from the industries leading XPages experts.

Tuesday
09Mar2010

Advanced HTML Element Select with Control and Shift Click

In my last post I discussed how to use dojo and CSS classes to create a user-selectable set of HTML elements. The example provided a list-like single-selection interface that allowed the user to only select single elements.

Today, I am going to expand upon that example to create a multiple-select interface allowing the user to control-click or shift-click to select multiple elements.

We are going to start off right where we left off last time, by modifying the focusObject() function we previously definded. The function is going to be a bit more involved as we need to check for 4 mouse states (click, control click, shift click, and control shift click). Let's start the update by redefining the parameters to our function. If you remember, our previous function template was as follows

function focusObject(class,objId) { }

We're going to change this template to include a new parameter "e" which will be the first argument that the event handler in non-IE browsers receives, or for IE browsers, the event object. So, redefined, our function template now looks like this:

function focusObject(class,objId,e) { }

The e parameter holds the two boolean properties ctrlKey and shiftKey which tells us the state of the keys at the time the click ocurred. Quite simply, if they are true, the appropriate key was pressed. So, lets expand our function a litle bit, and create a statement that will check for each of the four conditions we need to check. Also, because we are going to refer to the HTML node the user selected quite frequently, lets go ahead and capture that node to a variable right from the start.

function focusObject(class,objId,e) {
  // Capture the selected node...
  var selNode = dojo.byId(objId);

  // JUST CLICK
  if ((!e.ctrlKey) && (!e.shiftKey)) {

  // CONTROL SHIFT CLICK
  } else if ((e.ctrlKey) && (e.shiftKey)) {

  // CONTROL CLICK
  } else if (e.ctrlKey) {

  // SHIFT CLICK
  } else if (e.shiftKey) {

  }
}

Ok, let's tackle the obvious first, just plain ol' click, which we defined yesterday. The same code we created then will work now, so lets go ahead and insert that. At the same time, we need to start to not only track which items have been selected (with the CSS 'focused' class), but also which item was the last item the user clicked before the current one. This way we have a starting point, and an ending point for the range of elements the user wants to select when they shift click. To do this, we are going to introduce a new CSS class called 'lastSelected'. On every click we need to clear any item with the lastSelected class, and set the currently clicked item as 'lastSelected'. We want to do this after we have done all our processing so we know during shift-select what the last item was. So, at the end of the function, we are going to add two lines almost identical to the focusObject function from the last post, but in addition to using the passed in class name, we are going to add the new .lastSelected class as well.

function focusObject(class,objId,e) {

    // Capture the selected node...
    var selNode = dojo.byId(objId);

    // JUST CLICK
    if ((!e.ctrlKey) && (!e.shiftKey)) {
      dojo.query(class+'.focused').removeClass('focused');
      dojo.addClass(selNode, 'focused');
    // CONTROL SHIFT CLICK
    } else if ((e.ctrlKey) && (e.shiftKey)) {

    // CONTROL CLICK
    } else if (e.ctrlKey) {

    // SHIFT CLICK
    } else if (e.shiftKey) {

    }
    
    // Update the last click item to the item the user clicked this time
    dojo.query(class + '.lastSelected').removeClass('lastSelected');
    dojo.addClass(selNode, 'lastSelected')
}

Notice also how I have removed the calls to dojo.byId() and replaced them with selNode, since we have previously acquired it.

Now lets tackle the next easiest click, the control click. On a control-click the user is simply adding, or removing a previously selected item to the list. There is no need to clear the focused items. So we just need to check to see if selNode already has the focused CSS class, and if so remove it, otherwise, we need to add it. Let's plug in that code now.

function focusObject(class,objId,e) {

    // Capture the selected node...
    var selNode = dojo.byId(objId);

    // JUST CLICK
    if ((!e.ctrlKey) && (!e.shiftKey)) {
      dojo.query(class+'.focused').removeClass('focused');
      dojo.addClass(selNode, 'focused');
    // CONTROL SHIFT CLICK
    } else if ((e.ctrlKey) && (e.shiftKey)) {

    // CONTROL CLICK
    } else if (e.ctrlKey) {
      if (dojo.hasClass(selNode, 'focused'))
        dojo.removeClass(selNode, 'focused')
      else
        dojo.addClass(selNode, 'focused');
    // SHIFT CLICK
    } else if (e.shiftKey) {

    }
    
    // Update the last click item to the item the user clicked this time
    dojo.query(class + '.lastSelected').removeClass('lastSelected');
    dojo.addClass(selNode, 'lastSelected')
}

Ok, now we start to get to the tricky parts. The Shifty clicks. Two expected behaviors are expected here. First, if I Shift-Click, I would expect the only items selected to be the items between the last item I selected and the one I select this time, inclusive, or if Control-Shift-Click, I would expect my selections to be logically AND'ed with my previous selections, using again the last item I clicked, and the item clicked this time. Let's start with the Control-Shift-Click.

To accomplish the selection of items within a list of elements, we'll need to introduce a couple of new functions. Here's a run down of them real quick.

dojo.indexOf() - Basically this takes an array in the first argument, and a single item in the second argument, and returns the index of the array in argument 1 where argument 2 appears. If no match is found, it returns a -1.

HTMLNode.parentNode, HTMLNode.childNodes, HTMLNode.previousSibling, and HTML.nextSibling are all functions related to the relative traversal of HTML elements located in the DOM. parentNode returns an element's parent, childNodes returns an array of an element's immediate children, previousSibling and nextSibling return the previous/next immediate html node that is a sibling to itself. This includes whitespace nodes, text, etc, so we have to do a bit of type checking when using these functions.

I chose to start the shifty clicks with the Control Shift Click because we don't have to worry about what has been selected before. We just need to find, and select then inclusive items between our two elements. To do this, we first find the index of our two bounding nodes within our parent node. We do this with a combination of the parentNode, childNodes and dojo.indexOf functions. Here is the code:

var idx = dojo.indexOf(selNode.parentNode.childNodes, selNode);
var lastIdx = dojo.indexOf(selNode.parentNode.childNodes, dojo.query(class+'.lastSelected')[0]);

idx locates the index of the currently selected node in it's parent, and lastIdx locates the index of the last selected item within the currently selected node's parent. We check for the last selected node in the current selected node's parent because this will allow us to have multiple lists on a single page, and would prevent us from selecting from one item in one list to another item in another list. If the last selection is not in the same parent as the current, lastIdx will be -1 and we know we need to reset the selected items, and start a new selection.

Assuming we are in the same list, we compare idx and lastIdx to determine which direction we need to traverse from the currently selected node. If idx > lastIdx, we use nextSibling, and if idx < lastIdx, we use previousSibling. In either case we just traverse from selNode to the last selected nodes, turning on focus for each node along the path that has the passed in class. This is where we need to check the nodeType of each sibling node we retrieve to make sure it is an element that we want to select. You can read about nodeTypes here, but suffice it to say, we are looking for the Element node type, which is type 1. Here is the code to determine if the last two selected nodes are in the same parent, and if so determine the direction to traverse, and then actually performs the selection.

if (lastIdx == -1) {  // the last selected was not in the same parent, so we can't shift click, so just reset the focused nodes and select the single node ...
  dojo.query(class + '.focused').removeClass('focused');      
  dojo.addClass(selNode, 'focused');
} else {
  if (idx < lastIdx) {
    var el = selNode;
    for ( ; lastIdx >= idx; idx++) {
      if ((el.nodeType == 1) && (dojo.hasClass(el, class.split('.').join(''))))
        dojo.addClass(el, 'focused');
      el = el.nextSibling;
    }        
  } else if (idx > lastIdx) {
    var el = selNode;
    for ( ; lastIdx <= idx; idx--) {
      if ((el.nodeType == 1) && (dojo.hasClass(el, class.split('.').join(''))))
        dojo.addClass(el, 'focused');
      el = el.previousSibling;
    }
  }        
}

Now we can worry about the straight shift click. It operates just like control shift click except we need to clear any previously selected items first. Since that is the case, we can simplify our code by removing the seperate check for the control shift click, and integrate the check for a non-control shift click into the same code as above. So, lets start by tweaking our original expanded function template a bit, and add in some code during our shift click code to check for the control key. We'll make a check during the control shift click code we did above, and if they aren't holding down control, we'll clear out all the current selections, re-select the last selected element, and perform the same selection operations as control-shift-click, and wallah, we have shift-click covered too.

function focusObject(class,objId,e) {

  // Capture the selected node...
  var selNode = dojo.byId(objId);

  // JUST CLICK
  if ((!e.ctrlKey) && (!e.shiftKey)) {

  // JUST CONTROL CLICK
  } else if ((e.ctrlKey) && (!e.shiftKey)) {


  // SHIFTY CLICKS
  } else if (e.shiftKey) {

    if (!e.ctrlKey) {
      // clear all previous selections first, re-add lastSelected, then process on
      dojo.query(class + '.focused').removeClass('focused');
      dojo.query(class + '.lastSelected').addClass('focused');
      dojo.addClass(selection, 'focused');

    }

  }
    
}

Now we can put it all together, and make one last simple adjustment. If the user shift clicks the same element twice, we need to just dump out of our routine since we shouldn't deselect anything, and the last item is already selected. We'll do that right after we get idx and lastIdx, and if they equal each other, we'll just drop out.

So here it is, our final assembled function

function focusObject(class,objId,e) {
  var selNode = dojo.byId(objId);
  if ((!e.ctrlKey) && (!e.shiftKey)) {
    dojo.query(class + '.focused').removeClass('focused');
    dojo.addClass(selNode, 'focused');
  } else if ((e.ctrlKey) && (!e.shiftKey))
    if (dojo.hasClass(selNode, 'focused'))
      dojo.removeClass(selNode, 'focused')
    else
      dojo.addClass(selNode, 'focused');
  else if (e.shiftKey) {
    var idx = dojo.indexOf(selNode.parentNode.childNodes, selNode);
    var lastIdx = dojo.indexOf(selNode.parentNode.childNodes, dojo.query(class + '.lastSelected')[0])
    if (idx == lastIdx) {
        return true; // user just shift clicked the same node again, don't do anything
    }

    if (!e.ctrlKey) {
      // clear all previous selections first, re-add lastSelected, then process on
      dojo.query(class + '.focused').removeClass('focused');
      dojo.query(class + '.lastSelected').addClass('focused');
      dojo.addClass(selection, 'focused');
    }
      
    if (lastIdx == -1) {  // the last selected was not in the same parent, so we can't shift click, so just reset the focused nodes and select the single node ...
      dojo.query(class + '.focused').removeClass('focused');      
      dojo.addClass(selNode, 'focused');
    } else {
      if (idx < lastIdx) {
        var el = selNode;
        for ( ; lastIdx >= idx; idx++) {
          if ((el.nodeType == 1) && (dojo.hasClass(el, class.split('.').join(''))))
            dojo.addClass(el, 'focused');
          el = el.nextSibling;
        }        
      } else if (idx > lastIdx) {
        var el = selNode;
        for ( ; lastIdx <= idx; idx--) {
          if ((el.nodeType == 1) && (dojo.hasClass(el, class.split('.').join(''))))
            dojo.addClass(el, 'focused');
          el = el.previousSibling;
        }
      }        
    }    
  }
    
  dojo.query(class + '.lastSelected').removeClass('lastSelected');
  dojo.addClass(selNode, 'lastSelected')
}

Now, we are almost there, there are a few more things we need to take care of. First, as in the last post, we need to call focusObject() when our item gets clicked, but now, we have to get the event object and pass it in as well. So our event handler code from the last post for the element changes from this:

<xp:eventHandler event="onclick" submit="false">
  <xp:this.script>
    <![CDATA[
      focusObject('.userSelectable', '#{id:REPLACEWITHOBJECTID}');
    ]]>
  </xp:this.script>
</xp:eventHandler>

to this:

<xp:eventHandler event="onclick" submit="false">
  <xp:this.script>
    <![CDATA[
      focusObject('.userSelectable', '#{id:REPLACEWITHOBJECTID}', ((navigator.appName=="Netscape") ? arguments[0] : event));
    ]]>
  </xp:this.script>
</xp:eventHandler>

And one last thing, the default browser behavior when you shift click within the body of the HTML page is to high-light the text of the page, of which the end result of all our work would rather hacky, like this:

To prevent this, we'll use some javascript to disable the selection of text on the page.  This disables selection of text on the entire document, which for my current use case, is fine, but I am fairly certain this can be adapted to specific elements as well, it is just untested at the moment.

To prevent the selection of text, at the beginning of your client-side javascript library, add the following code:

function disableselect(e){ return false; }
function reEnable(){ return true; }

document.onselectstart=new Function ("return false");

if (window.sidebar){
    document.onmousedown=disableselect;
    document.onclick=reEnable;
}

And you'll get a nice, clean shift-select:

Well, thats the book (quite literally it appears) on single and multple html element selection. In an upcoming post, I'll show you how to implement this as a practical application add-in along with identifying and working with the selected elements.

Happy Coding!

Sunday
07Mar2010

QuickTip: HTML Element Selection Using Dojo

You can quickly create a user selectable list of objects using CSS and a couple of quick dojo functions. First create a series of elements that you want the user to be able to select, and assign them all the same CSS class to describe their appearance. For example we'll create three <xp:div>'s.

<xp:div styleClass="userSelectable" id="div1">Example One</xp:div>
<xp:div styleClass="userSelectable" id="div2">Example Two</xp:div>
<xp:div styleClass="userSelectable" id="div3">Example Three</xp:div>

Now, lets define the userSelectable CSS class

.userSelectable {
    margin:4px;
    height:20px;
    border:1px solid #DDD;
    background-color:#F3F3F3;
    width:auto;
    padding-left:24px;
    background-image:url(AppIcon_mini.gif);
    background-position:2px 2px;
    background-repeat:no-repeat;
    line-height:20px;
    cursor:pointer;
    cursor:hand;
}

That should create an list that looks like this:

Now, let's add two more definitions to our CSS. the first adds the :hover psuedo class, describing how the blocks should appear when the mouse is hovering over each item. We'll just override the background and border colors:

.userSelectable:hover {
    background-color:#EFEFEF;
    border-color:#CCC;
}

The final CSS markup we'll add is to define an additional class called focused that should alter the display of the item when the user has selected the item. To do this, we are going to specify ".userSelectable.focused" as the CSS selector. The lack of white space between the two class names means that both classes must be applied to the element for this markup to be applied.

.userSelectable.focused {
    background-color:#CCE6EE;
    border-color:#92CADA;
}

Now let's add a bit of javascript in the mix. You can place this in a client-side javascript library and include it in your XPage.

function focusObject(class,objId) {
  dojo.query(class+'.focused').removeClass('focused');
  dojo.addClass(dojo.byId(objId), 'focused');
}

The first line of the function finds any currently focused object and removes the focus from it, and the second line applies the focus to the object with the ID passed in as objId.

Now, let's modify our divs from above to include a bit of code to handle the click. On each div, place the following event handler code:

<xp:eventHandler event="onclick" submit="false">
  <xp:this.script>
    <![CDATA[
      focusObject('.userSelectable', '#{id:REPLACEWITHOBJECTID}');
    ]]>
  </xp:this.script>
</xp:eventHandler>

Make sure to replace the "REPLACEWITHOBJECTID" in the code above with the server side ID of the object. In our case, it would be div1, div2, or div3

Now, you have a user selectable list of elements. To retreive which element is selected at anytime, you can use

dojo.query('.userSelectable.focused')[0]

to get the node.

Here is what the code produces, the first div is in its normal state, the second is currently being hovered over (very subtle effect), and the third has been selected.

Friday
05Mar2010

SSJS @Adjust on Javascript Dates - Be Backwarned!

The following code will generate results that you may not expect:

var myDate = new Date(2010,2,15);  //this is MARCH 15th, months for JSDate is zero based
myDate = @Adjust(myDate,0,-1,0,0,0,0);

You might expect myDate to now be 2/15/2010 ... its what the classic @Adjust @Formula would output.  But there is a little more than meets the eye going on here.

new Date(2010,2,15) creates a new Date in the local time zone... In SSJS @Adjust() by default treats the date as a UTC timezone, so the results you will actually get is:

2/14/2010

Why you ask? Because we didnt specify a time in the date object, it's set to 12 Midnight, and going from March back to February crosses the line for Daylight savings, and the adjusted time changes from 12:00:00 midnight to 11:00:00 PM the previous day as you cross back.

The key is to specify the "[InLocalTime]" keyword to the @Adjust function, so the correct call would be:

@Adjust(myDate,0,-1,0,0,0,0,"[InLocalTime]")

Thanks to Dave Connelly and the IBM Team for chasing this one down!

 

Sunday
21Feb2010

Embedding Contextual Data for Client-Side Logic in XPages

One of the difficulties I have faced when writing user-friendly UI's using XPages is the limited amount of "data" I can embed in a generated XPage for consumption by client side scripts.  Quite often I find myself wanting to do an dojo.xhrPost() to a domino agent to update the state of a document in the background, or programmatically post a partial refresh back to the server with programmatically contrived data, based on the user's actions, or the state of a dataset client-side. Quite often that action, or the target of that action is dependant upon one or more pieces of data that is not normally located in the DOM.

The typical way to embed this data in the document is through the use of the hidden input (<xp:inputHidden /> in XPages terms.)  When dealing with small amounts of data, this is fine, but what about more complex data sets, or large data sets. For example a list generated by an repeat control (<xp:repeat />) can generate a large amount of hidden inputs, and you are still faced with the dilemma of how to programmatically determine which hidden input belongs to which UI element.

The solution is rather simple. We can embed our own contextual data tags directly into the HTML! Since browsers grew up and lived through the browser wars, when standards were something to be scoffed at, and the way to win user base was to have more cool tags than your competitor (<blink> anyone??) browsers have been taught to IGNORE tags they don't understand. We can use this to our advantage to embded data contextually within our HTML, then programmatically retrieve it.

To do this, you go to the code of your XPage, and just type in the new contextual data tag. For example, lets say we are dealing with a repeat control, and we want to get the UNID of each document that is rendered by the repeat. To do this we can code the following:

<xp:repeat var="listData" ...>
   <xp:panel id="myPanel">
     <UNID value="#{javascript:listData.getUniversalID()}">

      [ ... other Xpages controls go here ... ]
   </xp:panel>
</xp:repeat>

Now, when a user interacts with your "myPanel" for example, in a client-side onClick() event, you can easily retrieve the UNID for this document with the following code:

var unid = dojo.attr(dojo.query('UNID', this.node)[0], 'value');

This code has two important dojo functions. First the query function, which allows you to use almost all CSS3 selectors to return a nodelist (an array of HTML nodes). The first argument to this function is the CSS selector used to located your nodes. In this case, we are asking for all UNID tags. This is a VERY powerful function. I would recommend that you study and learn its power. The second argument is the "root node" at which you want to start your search. If you omit this argument, the entire DOM is searched. If you include a Node, only descendant nodes of that Node will be searched. (Again this is very powerful. For example, XPages developers can use this to locate nodes based on the element's programmed ID, instead of the full client side-id by using the CSS3 selector [id$="myPanel"])

The second function is dojo.attr which returns the value of the attribute specified in argument 2, from the node specified in argument 1.

While this example is over-simplified, you can imagine the powerful things you could put this to use in. For example, off the top of my head, i can think of using contextual data tags to:

  • pass information that may be required for display in tooltips, popups, or other display-only interfaces, without having to pass all the markup associated with it (especially when it can be generated client-side, then discarded
  • passing identifying data, such as UNIDs, to use in ajax calls back to the server to simulate RESTful API/CRUD calls.
  • And, since you can nest the tags, you can give the data relational meaning (think embedded XML)

Happy Coding!

 

Monday
15Feb2010

XPages101 Online offers affordable XPages training

On Friday I announced over on my blog about XPages101 Online. It's an online version of my one day introduction to XPages course with full videos and supporting content to guide you through building an XPages application from start to finish.

But to add extra value, and help you as you progress in your XPages development, I'll be uploading a new video based lesson every week which will walk you through some aspect of XPages development, whether it be using Java classes in your Server Side Javascript, making the most of the Dojo framework which ships with Domino or extending the standard controls which you can use in your XPages and Custom Controls.

Until 26th February, there is a special offer running which offers a 33% discount on the subscription fee if you enter the coupon code "earlybird" at checkout.