| By James Benson, Jay Fienberg | Article Rating: |
|
| May 16, 2007 11:00 AM EDT | Reads: |
5,623 |
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
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.
Published May 16, 2007 Reads 5,623
Copyright © 2007 Ulitzer, Inc. — All Rights Reserved.
Syndicated stories and blog feeds, all rights reserved by the author.
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.
- Feature: Web 2.0 In-Depth – A Transient Life with Web 2.0
- JSON - An Attempt to Bring XSS Back
- AJAX IM Client
- Real-World AJAX Book Preview: A Safer More Secure AJAX
- AJAX Load Buddies
- Real-World AJAX Book Preview: Input Focus and Blur in Chat Windows
- AJAX Logging Functions
- Real-World AJAX Book Preview: Designing the Server API
- Real-World AJAX Book Preview: The Basic HTML Structure




















Ulitzer content is offered under Creative Commons "Attribution Non-Commercial No Derivatives" License.
For any reuse or distribution, you must make clear to others the license terms of this work.
The best way to do this is with a link to this web page.
Any of the above conditions can be waived if you get written permission from Ulitzer, Inc., the copyright holder.
Nothing in this license impairs or restricts the author's moral rights.