Search
Categories

Entries in dojo (4)

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.

Wednesday
15Jul2009

Advanced In-Page Login Custom Control for XPages

Declan has a post on his blog on how to create a login window using a Dojo dialog box. Its a very nice example on how to do a quick login control using Dojo and AJAX to log in a user versus using the standard Domino style login.

I use a slightly different approach using an XPage custom control that allows me to have more control over the look of the login, so that I can integrate it with the site's overall look and feel, as well as use some dojo fadeIn/fadeOut animations to give it a little bit of extra flair. It also allows me to authenticate against a specific domino resource of my choice so that I can not only verify they are an authenticated user for the application, but also that they might have a specific role that allows them access to a particular resource.

To use the custom control, you just place it on the XPage where you want the "Log In" prompt to appear, set the control's properties, and customize the prompt's text through the use of some editable areas (facets).

Here is a description of the different properties:

loginText: This is the text you want to appear on the page as the link for the end user to click. I usually use 'Log In'

loginURL: This is the URL that the control uses to actually process the login. 99% of the time, its /names.nsf?login.

verifyAccessURL: After the control issues a successful login attempt to loginURL, the control validates access against this URL, verifying they user has access to what I want to redirect them to. If they don't have access, I tell them so.

logoutText: If a user is currently logged in, the control displays this text, instead of the loginText. I usually use "Log Out"

logoutURL: This is the URL to use for the logout. usually its /names.nsf?logout which takes them back to the default site for the domain/domino server. If I want to redirect them to a different page, then i might use /names.nsf?logout&RedirectTo=/mynew/url/page

successURL: This is the URL to send them to after a successful login and verification.

There are also 2 other properties which I haven't implemented yet, numberAttempts and failureURL, the idea being after numberAttempts failed logins, I'd redirect to failureURL.

In order for the control to work, there are two additional resources you'll need to add to your database. The first is a stylesheet that controls the look of the login window, and the other is a short script that sets up the controls on page load. There isn't alot to them, the control just needs them. If you want to change the look and feel of the control, then you modify the login.css. Both files are included in the download for the control at the end of the article.

Let's get down into the meat of the control so you can see how it works, and see how to develop a custom control.

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

<xp:this.resources>
    <xp:script src="/startsHidden.js" clientSide="true"></xp:script>
    <xp:styleSheet href="/login.css"></xp:styleSheet>
</xp:this.resources>

Above is the first 8 lines of the custom control, pretty basic for any XPage. Two things to note off the bat, is first the xp:view has a custom style set to "display:inline". This allows the "Log In" link to appear right along with and next to any code or elements surrounding it, without causeing the margins to clear on either side. By default, a custom control renders as an XHTML div tag, which is a block element (clearing both margins). The other item to note is the inclusion of the startsHidden javascript and login.css stylesheet.

This particular version of the login custom control displays as a floating panel above the page. To help distinguish it from the rest of the page when it is displayed, I "lower the lights" on the rest of the page by creating a div that is 100% width and height, and black, then set the opacity to 70%. This keeps the user from clicking on other controls, and focuses their attention on the login. I call this the 'dither panel.' That is done with the next three lines of code:

<xp:panel
     styleClass="pnlDither startsHidden" id="pnlDither">
</xp:panel>

The properties of this panel are all controlled in the login.css, and later, i'll show you the code that makes it fade in and out with the login panel.

After that, the next bit of code creates the link on the page where the custom control is embedded.

There is a hefty amount of code in there, so I'll just explain and break down a little bit, download the code below and follow along if you like. The first block <xp:text>...</xp:text> creates the first part of the link, the <a> XHTML tag. If the user is logged in, the link's href is set to the logoutURL property. If the user is not logged in, the onclick of the anchor is used to execute some javascript that makes the dither panel, and the login panel itself visible, then animates them with a fade in. We can use a straight-forward dojo.fadeIn for the panel because we are going to display it at 100% opacity, however we want to stop the opacity of the dither panel at 70% opacity so we can see through it, so we use the dojo.animateProperty to animate the opacity of the panel from it's current setting (currently 0) to 70% (css: opacity: 0.7).

The next block of code, again an <xp:text></xp:text> block decides whether the user is logged in or not, and displays the appropriate text, either loginText or logoutText.

And the final block simply closes the XHTML anchor tag.

The next bit of code just lays out the actual login panel, called loginWindow. It uses several nested <xp:panel> elements (the XHTML equiv to div tags) to define the window, its title bar, etc. There are facets (editable regions) that allow you to set the text for the title bar, the welcome message, etc. I won't post all the code for it as it's lengthly, but its all in the download at the end of the article.

I will however go through the actual login script so you can see how the exchange actually happens.

The first part of the code simply posts a the username and password to the loginURL using dojo.xhrPost:

dojo.xhrPost({ url : '#{javascript:compositeData.get("loginURL");}',
handleAs : "text",
preventCache : true,
content: {
     "UserName": dojo.byId('#{javascript:getClientId("inpUser")}').value,
     "Password": dojo.byId('#{javascript:getClientId("inpPassword")}').value },

You can see in the second line how I retreive the value of the loginURL parameter using the inline XSP javascript command #{javascript: ... } that block is executed on the server side and the result is put back into the code in replacement. In this case the server executes compositeData.get("loginURL") to get the value entered in the parameter loginURL.

Next we have the load function for the xhrPost. This gets executed if the xhrPost is is successful, and data is returned. Here's the first part of that code:

load : function(response, ioArgs) {
    if (response.indexOf('reasonType')==-1) {
           dojo.xhrGet({

Here all we do is take the response back from the server, held in the variable 'response', and look for a string 'reasonType.' That is the key to telling if the login was successful. If it was not a successful login, then the returned HTML includes a variable reasonType indicating the type of error. So far, I haven't cared enough about what the actual error is with the login, so I display a generic message if it fails, shown below (but actually appears later in the script):

} else {
     dojo.byId('#{javascript:getClientId("loginErrorPanel")}').innerHTML = "You have entered an invalid username or password, or do not have sufficient credentials."; }

If the login was successful, I then proceed to actually check against the specific element that I want to veryfiy they have access to. This is done by issueing a dojo.xhrGet to the verifyAccessURL property, then checking again if the access attempt was successful. This time, i do not check for reasonType, as it has caused me a few problems here and there in the past that are really irrelevent to this code. Instead I check for the string 'action="/names.nsf?Login"' that appears in the <form> tag of a login prompt. If the check there is successful, then I redirect the page to the value of the successURL parameter, if not, I force a logout, display my standard message, and have them try to log in again. Here's most of that code:

dojo.xhrGet({
  url : '#{javascript:compositeData.get("verifyAccessURL");}',
  handleAs : 'text',
  preventCache : true,
  load : function(response, ioArgs) {
    if (response.indexOf('action="/names.nsf?Login"')==-1){
      document.cookie = "BackupSesID=; expires=Thu, 01-Jan-70 00:00:01 GMT";
      window.location.href = '#{javascript:compositeData.get("successURL");}';
    }else{
        dojo.xhrGet({
            url : '#{javascript:compositeData.get("logoutURL");}',
            handleAs : "text",
            preventCache : true,
            load : function(response, ioArgs) {
                   if (document.cookie.indexOf('BackupSesId=')!=-1){
                       document.cookie = "DomAuthSessId=" + document.cookie.split('BackupSesID=')[1].substr(0, 32);
                   }
                   document.cookie = "BackupSesId=; expires=Thu, 01-Jan-70 00:00:01 GMT";
                   dojo.byId('#{javascript:getClientId("loginErrorPanel")}').innerHTML = "You have entered an invalid username or password, or do not have sufficient credentials.";
            },
            error : function(response, ioArgs) {
                 dojo.byId('#{javascript:getClientId("loginErrorPanel")}').innerHTML = "You have entered an invalid username or password, or do not have sufficient credentials.";
            }
         })
      }
   },
   error : function(response, ioArgs) {
              dojo.byId('#{javascript:getClientId("loginErrorPanel")}').innerHTML = "You have entered an invalid username or password, or do not have sufficient credentials.";
   }
})

That's pretty much it in a nutshell. Download the Zip with all three elements, the custom control, the css and the javascript (which needs to be created a a javascript script library in your database) from my blog, and let me know if you have any questions!

Happy Coding!

Update: Here is a link to a flash movie of the control in action (sorry about the poor quality)

Monday
13Jul2009

Using Dojo ToolTips in an XPage

I posted an entry on my blog about how to using the dijit.ToolTip plugin in an XPage. Slightly tangential to pure XPages dev, but Dojo is pretty integral to any serious XPages work so it's worth a look.