James Benson

Subscribe to James Benson: eMailAlertsEmail Alerts
Get James Benson: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Related Topics: RIA Developer's Journal, AJAX World RIA Conference

RIA & Ajax: Article

Real-World AJAX Book Preview: Input Focus and Blur in Chat Windows

Real-World AJAX Book Preview: Input Focus and Blur in Chat Windows

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs for the special pre-order price, click here for more information. Aimed at everyone from enterprise developers to self-taught scripters, Real-World AJAX: Secrets of the Masters is the perfect book for anyone who wants to start developing AJAX applications.

Input Focus and Blur in Chat Windows
We've also included a couple pure usability features in our ChatWindow class. The focus() and blur() methods simply change the background color of the chat window's text input to help the user know which window is active and where they are typing.

Listing 12.30 Input Focus and Blur

inputFocus: function() {
   this.chatin.style.background = "#ee0";
},
inputBlur: function() {
   this.chatin.style.background = "#fff";
},

Hide Chat Windows
Each chat window, in the HTML, has an [X] button for closing the window. When a user clicks this, the hide() function on the ChatWindow object is called, and this simply sets its style property to "none".

Listing 12.31 Hide

hide: function() {
   log('Closed window to ' + this.to, 'debug');
   this.chatwindow.style.display = "none";
   this.open = false;
   return false;
},

Destroy Chat Windows
As with our Buddies object, we want to destroy our ChatWindow objects, which just means hiding them and cleaning up the Event listeners we've set up earlier.

Listing 12.32 Destroy

   destroy: function() {
     this.hide();
     Event.stopObserving(this.chatin, 'focus', this.inputFocusObserver, true);
     Event.stopObserving(this.chatin, 'blur', this.inputBlurObserver, true);
     Event.stopObserving(this.chatin, 'keypress', this.chatSendObserver, true);

     Event.stopObserving(this.chatsend, 'click', this.chatSendObserver, true);
     Event.stopObserving(this.closer, 'click', this.hideObserver, true);

   }
};

Refresh
The refresh() function, which we've referenced a number of times, is essentially a loop. The refresh() function ends by setting a Timeout timer that re-calls refresh() after the timeout period (which is set in our refreshInterval global variable).

The refresh() function calls load() on the Buddies object, and calls load() on each ChatWindow object that has been created.

Listing 12.33 Refresh

function refresh() {
   buddyMgr.load();
   log('Loaded the buddy list', 'info');
   var ms = false;
   for (i in windowMgr) {
     if (i.indexOf('buddy')==0) {
       windowMgr[i].load();
       log('Loaded Messages for '+ i, 'debug');
     }
     ms = true;
   }
   if (ms) log('Loaded Messages', 'info');

     refreshTimer = setTimeout(refresh,refreshInterval);
   log('Set a new refresh timer', 'debug');
}

The Initialization, Login and Logout Related Functions, Round 2
Now that we've seen how we can call the destory() method on the Buddies and ChatWindows objects, let's look at our final logout() method. Besides calling destroy() on the Buddies object, and on each ChatWindow object that has been created, logout() also clears the Timeout timer we use in the refresh() function.

Listing 12.34 Logout

function logout() {
   log('Logging out ' + activeUser, 'debug');
   $('logout').style.display = 'none';
   $('login').style.display = 'block';

   buddyMgr.destroy();

   for (i in windowMgr) {
     if (i.indexOf('buddy')==0) windowMgr[i].destroy();
   }
   log(activeUser + ' Successfully logged out', 'info');
   clearTimeout(refreshTimer);
   log('Cleared the refresh timer', 'debug');
   buddyMgr = null;
   windowMgr = null;
}

On Unload
Finally, we'll create an onunload function on the window object that will be executed whenever the user closes her browser or refreshes or leaves our im.php URL.

This function simply calls our logout() function, and therein ensures that a user who leaves our IM application is fully logged out. We could place other clean-up code here, if needed.

Listing 12.35 OnUnload

window.onunload = function () {
   if (buddyMgr != null) logout();
   //any other clean-up code can go here
}

Complete JavaScript Code
The complete im.js is listed here in Listing 12.36.

Listing 12.36 Complete JavaScript Code

/* global varaibles that persist across logins */
var refreshInterval = 2 * 1000; //first number = seconds
var logDisplayLevel = 1;
var logWindow = 'logmsgs';
var logLevels = new Array('debug', 'info', 'error');
var logmsgcnt = 0;
var baseURL = 'im.php';
var responseFormat = 'html';
var ChatWindow = Class.create();
var Buddies = Class.create();

/* global varaibles that get re-initialized with each login */
var activeUser, windowsUsed, windowMgr, buddyMgr, refreshtimer;

/* Chat Window class - an instance is created for each chat window */
   ChatWindow.prototype = {
     initialize: function (handle, to) {
       toPrefix = 'buddy_';
       this.chatwindow = $('chat' + handle);
       this.chatmsg = $('chatmsg' + handle);
       this.chatin = $('chatin' + handle);
       this.chatsend = $('chatsend' + handle);
       this.chatinfo = $('chatinfo' + handle);
       this.closer = $('close' + handle);
       this.to = to.substring(toPrefix.length);
       log('Initializing window #' + handle + ' for chat with ' + this.to, 'debug');

       Element.update(this.chatinfo, 'Chat with '+ this.to);
       this.open = false;
       this.content = '';

       this.chatSendObserver = this.chatSend.bindAsEventListener(this);
       this.inputFocusObserver = this.inputFocus.bindAsEventListener(this);
       this.inputBlurObserver = this.inputBlur.bindAsEventListener(this);
       this.hideObserver = this.hide.bindAsEventListener(this);
       Event.observe(this.chatin, 'focus', this.inputFocusObserver, true);
       Event.observe(this.chatin, 'blur', this.inputBlurObserver, true);
       Event.observe(this.chatin, 'keypress', this.chatSendObserver, true);

       Event.observe(this.chatsend, 'click', this.chatSendObserver, true);
       Event.observe(this.closer, 'click', this.hideObserver, true);
     },
     openOrFocus: function () {
       if (!this.open) {
         log('Opened window to ' + this.to, 'debug');
         this.chatwindow.style.display = "block";
         this.chatin.focus();
         this.open = true;
       } else {
         log('Focused window to ' + this.to, 'debug');
         this.chatin.focus();
       }
     },
     hide: function() {
       log('Closed window to ' + this.to, 'debug');
       this.chatwindow.style.display = "none";
       this.open = false;
       return false;
   },
   display: function(xhr) {
     log('Displaying Messages for '+this.to, 'debug');
     this.content = xhr.responseText;
     Element.update(this.chatmsg, this.content);
     this.chatmsg.scrollTop = this.chatmsg.scrollHeight;
   },
     inputFocus: function() {
     this.chatin.style.background = "#ee0";
   },
     inputBlur: function() {
     this.chatin.style.background = "#fff";
   },
   load: function() {
     msgsURL = baseURL + '/' + responseFormat+ '/' + activeUser+'/msgs/'+ this.to;
     log('Loading Messages to '+ this.to +' via AJAX', 'debug');
     new Ajax.Request(
       msgsURL,
       {
         method: 'get',
         parameters: '',
         onComplete: this.display.bindAsEventListener(this)
       });
   },
   chatSend: function(ev) {
     if (ev!=null && (
         (ev.type == 'click') ||
         (ev.type=='keypress' && ev.keyCode == Event.KEY_RETURN)
       )) {
       e = Event.element(ev);
       log(activeUser + ' Sending message from ' + this.chatwindow.id +' to: ' + this.
to, 'debug');
       this.sendMessage();
       this.chatin.value = '';
       this.chatin.focus();
     }
   },
   sendMessage: function() {
     sendmsgURL = baseURL + '/sendmsg';
     new Ajax.Request(
       sendmsgURL,
       {
         method: 'post',
         postBody: 'from='+activeUser+'&to='+this.to+'&msg='+encodeURI(this.chatin.
value)
         });
       log('Message sent to: ' + this.to, 'info');
     },
     destroy: function() {
       this.hide();
       Event.stopObserving(this.chatin, 'focus', this.inputFocusObserver, true);
       Event.stopObserving(this.chatin, 'blur', this.inputBlurObserver, true);
       Event.stopObserving(this.chatin, 'keypress', this.chatSendObserver, true);

       Event.stopObserving(this.chatsend, 'click', this.chatSendObserver, true);
       Event.stopObserving(this.closer, 'click', this.hideObserver, true);

     }
   };
   /* Buddy List class - only instance is created */
   Buddies.prototype = {
     initialize: function () {
       this.listWindow = $('buddy');
       this.listWindow.style.display = 'block';
       this.statusControl = $('status');
       this.statusControl.options.selectedIndex = 0;
       this.postStatusUpdate('Available', '');
       this.changeStatusObserver = this.changeStatus.bindAsEventListener(this);

       Event.observe('status', 'change', this.changeStatusObserver, true);
     },
     budlist: '',
     changeStatus: function () {
       idx = this.statusControl.options.selectedIndex;
       status = this.statusControl.options[idx].value;
       status_msg = '';
       log('Updating Status to: ' + status, 'debug');
       this.postStatusUpdate(status, status_msg);
       if (status=='Offline') {
         logout();
       }
     },
     postStatusUpdate: function (status, status_msg) {
       updatestatusURL = baseURL + '/updatestatus';
       var myAjax = new Ajax.Request(
         updatestatusURL,
         {
         method: 'post',
         postBody: 'from='+activeUser+'&status='+status+'&status_
msg='+encodeURI(status_msg)
       });
     log('Status Updated to: ' + status, 'info');
   },
   load: function () {
     buddylistURL = baseURL + '/' + responseFormat+ '/' + activeUser+'/buddies';
     log('Loading Buddies', 'debug');

     new Ajax.Request(
       buddylistURL,
       {
         method: 'get',
         parameters: '',
         onComplete: this.display.bindAsEventListener(this)
       });
   },
   display: function (xhr) {
     Element.update('buddylist', xhr.responseText);
     this.budlist = document.getElementsByClassName('budlisting', 'buddylist');
     this.budlist.each(function(bud) {
       Event.observe(bud, 'click', goChat, true);
       });
   },
   destroy: function() {
     this.listWindow.style.display = 'none';
     this.postStatusUpdate('Offline', 'Logged Out');
     this.budlist.each(function(bud) {
       Event.stopObserving(bud, 'click', goChat, true);
       });
       Event.stopObserving('status', 'change', this.changeStatusObserver, true);
   }
};

/* Buddy list - window, and refresh cycle functions */
function goChat() {
   log('Clicked ' + this.id + ' to chat', 'debug');

   if (windowMgr[this.id] == undefined) {
     windowsUsed++;
     handle = windowsUsed;
     windowMgr[this.id] = new ChatWindow(handle, this.id);
       log('Created a new handle for window #' + handle, 'debug');
     }

     windowMgr[this.id].load();
     windowMgr[this.id].openOrFocus();
   }

   function refresh() {
     buddyMgr.load();
     log('Loaded the buddy list', 'info');
     var ms = false;
     for (i in windowMgr) {
       if (i.indexOf('buddy')==0) {
         windowMgr[i].load();
         log('Loaded Messages for '+ i, 'debug');
       }
       ms = true;
     }
     if (ms) log('Loaded Messages', 'info');

       refreshTimer = setTimeout(refresh,refreshInterval);
     log('Set a new refresh timer', 'debug');
   }

   /* logging functions */

   function log(msg, level) {
     if (level != null & logLevels.indexOf(level) >= logDisplayLevel) {
       logmsgcnt++
       msg = '<p class="'+level+'text">• ' + level.toUpperCase() +' ('+logmsgcnt+'):
   '+ msg+'</p>';
       new Insertion.Bottom(logWindow, msg);
       $(logWindow).scrollTop = $(logWindow).scrollHeight;
     }
   }

   /*function setLogLevel(level) {
     logDisplayLevel = level;
   }
   */

   /* initialization, login and logout related functions */
   function initialize() {
     activeUser = '';
     windowsUsed = 0;
     windowMgr = new Array();
   }

   function loadUsers() {
     userlistURL = baseURL + '/userlist';
     log('Loading Users into drop down list', 'debug');
     new Ajax.Updater(
       'user',
       userlistURL,
       {
         method: 'get',
         parameters: ''
       });
   }

   function login() {
     idx = this.options.selectedIndex;
     user = this.options[idx].value
     initialize();
     log('Logging in ' + user, 'debug');
     activeUser = user;

     Element.update('loginuser', activeUser);
     $('login').style.display = 'none';
     $('logout').style.display = 'block';
     this.options.selectedIndex = 0;
     log('Successfully logged in as ' + activeUser, 'info');
     buddyMgr = new Buddies();
     refresh();
   }

   function logout() {
     log('Logging out ' + activeUser, 'debug');
     $('logout').style.display = 'none';
     $('login').style.display = 'block';

     buddyMgr.destroy();

     for (i in windowMgr) {
       if (i.indexOf('buddy')==0) windowMgr[i].destroy();
     }

     log(activeUser + ' Successfully logged out', 'info');
     clearTimeout(refreshTimer);
     log('Cleared the refresh timer', 'debug');
     buddyMgr = null;
     windowMgr = null;
   }

   window.onunload = function () {
     if (buddyMgr != null) logout();
     //any other clean-up code can go here
   }

   window.onload = function() {
     log('Window load complete', 'info');
     Event.observe('user', 'change', login, true);
     Event.observe('logoutlink', 'click', logout, true);

     Event.observe('log0', 'click', function() {logDisplayLevel=0; log('Log Level Changed
   to Debug','debug');}, true);
     Event.observe('log1', 'click', function() {logDisplayLevel=1; log('Log Level Changed
   to Info','info');}, true);
     Event.observe('log2', 'click', function() {log('Log Level Changing to Error','info');l
   ogDisplayLevel=2}, true);
     loadUsers();
   }

Exercises for the Reader

  • Dynamic windows that are draggable/resizable
  • Use XML or JSON methods on the server to allow more sophisticated interactions on the client
  • Create a message bus on the client to consolidate refreshes
    - Load then display
    - Sendmessage and updateStatus
    - Single method on the server for updates
    - Single response on the server for all changes
    - Use timestamps to minimize data sent
    - Update local chat windows instantly and then only display subsequent messages
  • Add pushlets/comet to the server, client dealing with constant stream. Mention Jetty 6 continuations and server scale issues
Example of working with JSON results:

var imdata = eval (res);
imdata.buddies.count;
imdata.msgs['Paul'].list[33].msg;

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs, click here to order.

More Stories By James Benson

Jim Benson, AICP, is the COO of Gray Hill Solutions in Seattle. Gray Hill creates tools for government and industry to harness and utilize real-time data. Jim has always driven applications for his clients to store and provide information in easily extensible ways. Web 2.0 has therefore been a natural environment for him. He is also involved with the Cooperation Commons and the Institute for the Future's Future Commons to study human cooperation and envision the future of cooperation. Jim's tags: Gray Hill Solutions (www.grayhillsolutions.com), Jim's Blog (http://ourfounder.typepad.com), Cooperation Commons (www.cooperationcommons.org), Institute for the Future (www.iftf.org).

More Stories By Jay Fienberg

Jay Fienberg is co-founder of Juxtaprose where he designs information architecture and user experience for websites and information systems.

He specializes in design for content and information-rich websites and web-based social and collaboration systems. His current preferred CMS is ExpressionEngine, though he also works with Wordpress and Joomla, and still gets called upon to make SharePoint do interesting CMS-like things for enterprise intranets.

Since the early 1990s, Jay also has designed and developed hypertext, database, and content management systems and worked in a wide range of programming languages including XML, SQL, SGML, Python, PHP, Javascript, Java, HTML, CSS and APL.

For anything more official, please visit jayfienberg.com.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.