Tuesday, July 14, 2009

AJAX and PHP Building Responsive Web Applications by Cristian Darie, Bogdan Brinzarea Chapter 5

AJAX Chat

We are living in a world where communication has become very important; there's a real need to be able to communicate quickly and easily with others. Email, phone texting, postal letters, and online chat offer media through which people can exchange ideas in the form of written words. An important aspect when communicating is the responsiveness factor. While emails and letters don't offer a live feedback from the other participants, phone and online chat offer a more dynamic way to communicate. In this chapter, we will build an AJAX-enabled online chat solution.

Introducing AJAX Chat
Most of the communication that takes place through the computer is done via desktop applications. These applications communicate with each other in a decentralized way using Peer to Peer (P2P) systems. However, these may not be viable options if you are inside a company whose security policy prevents users from opening connections on other ports than the HTTP port
80. If that is the case, you are facing a real problem.

There are numerous audio and video web chat solutions out there, most of them based on Java applets. Applets are known for their common security problems across browsers and sometimes they don't even use port 80 for communication. So, they are not a solution for getting in touch with your friends outside the company either.

This is where AJAX comes into play and brings one answer for our problem. With a little effort one can even integrate into a browser an Internet Relay Chat (IRC) client or you can develop your own web chat solution such as the one you'll build later.

Are you getting tired of being told that you cannot install or use your favorite messenger when you are at work, or when you are in an Internet Café? You might well have found yourself in such a situation before. This is the right time to see how we can break out of this unfortunate situation by using AJAX chat solution.

AJAX Chat Solutions
Probably the most impressive solution available today is www.meebo.com. We are pretty sure that some of you have heard about it, and if you haven't, it is time to have a look at it. The first and the
most important feature is that it allows you to log in into your favorite instant messaging system
by using only a web interface. See Meebo's login screen in Figure 5.1.

Figure 5.1: Meebo

Meebo offers access to all these services from a single start web page with a user friendly interface, with no pop-up windows, Java applets and so on. By using a solution based on AJAX you can forget about all the problems mentioned in the beginning.

Meebo isn't the only web application that offers chat functionality. Even if AJAX is very young, you can already find several other online chat applications and even solutions based on it:

• http://www.plasticshore.com/projects/chat/index.html
• http://treehouse.ofb.net/chat/?lang=en.
• http://www.chategory.org
• http://www.socket7.net/lace/
• http://drupal.org/node/27689.

It's time to get to work. In the rest of the chapter, we'll implement our own online chat application.

Implementing AJAX Chat
We'll keep the application simple, modular, and extensible. For this we won't implement a login module, chat rooms, the online users list, etc. By keeping it simple we try to focus on what the goal of this chapter is—AJAX Chat. We will implement the basic chat functions: posting and retrieving messages without causing any page reloads. We'll also let the user pick a color for her or his messages, because this involves an AJAX mechanism that will be another good exercise.

Starting from the following application that will be presented in this chapter, we can easily extend it by implementing any other modules that can be found in the solutions presented above and that are not presented here. Take this part as homework for those of you who are interested in it.

In order to have these example working you need the GD library. The installation instructions in Appendix A include support for the GD library.

The chat application can be tested online at http://ajaxphp.packtpub.com, and it looks like in
Figure 5.2.

Figure 5.2: AJAX Chat

A novelty in this chapter is that you will have two XMLHttpRequest objects. The first one will handle updating the chat window and the second will handle the color picker (when you click on the image, the coordinates are sent to the server, and the server replies with the color code).

The messages for the AJAX Chat are saved in a queue (a FIFO structure), such as you learned about in Chapter 4, so that messages are not lost even if the server is slow, and they always get to the server in the same order as you sent them. Unlike with other patterns you can find on Internet these days,
we also ensure we don't load the server with any more requests until the current one is finished.

Time for Action—Ajax Chat
1. Connect to the ajax database, and create a table named chat with the following code:

CREATE TABLE chat
(
chat_id int(11) NOT NULL auto_increment, posted_on datetime NOT NULL,
user_name varchar(255) NOT NULL,
message text NOT NULL,
color char(7) default '#000000', PRIMARY KEY (chat_id)
);

2. In your ajax folder, create a new folder named chat.

3. Copy the palette.png file from the code download to the chat folder.

4. We will create the application starting with the server functionality. In the chat folder, create a file named config.php, and add the database configuration code to it (change these values to match your configuration):

<?php
// defines database connection data
define('DB_HOST', 'localhost'); define('DB_USER', 'ajaxuser'); define('DB_PASSWORD', 'practical'); define('DB_DATABASE', 'ajax');
?>

5. Now add the standard error handling file, error_handler.php:

<?php
// set the user error handler method to be error_handler
set_error_handler('error_handler', E_ALL);
// error handler function
function error_handler($errNo, $errStr, $errFile, $errLine)
{
// clear any output that has already been generated if(ob_get_length()) ob_clean();
// output the error message
$error_message = 'ERRNO: ' . $errNo . chr(10) .
'TEXT: ' . $errStr . chr(10) .
'LOCATION: ' . $errFile .
', line ' . $errLine;
echo $error_message;
// prevent processing any more PHP scripts exit;
}
?>

6. Create another file named chat.php and add this code to it:

<?php
// reference the file containing the Chat class require_once("chat.class.php");
// retrieve the operation to be performed
$mode = $_POST['mode'];

// default the last id to 0
$id = 0;
// create a new Chat instance
$chat = new Chat();
// if the operation is SendAndRetrieve
if($mode == 'SendAndRetrieveNew')
{
// retrieve the action parameters used to add a new message
$name = $_POST['name'];
$message = $_POST['message'];
$color = $_POST['color'];
$id = $_POST['id'];

// check if we have valid values
if ($name != '' && $message != '' && $color != '')
{
// post the message to the database
$chat->postMessage($name, $message, $color);
}
}
// if the operation is DeleteAndRetrieve elseif($mode == 'DeleteAndRetrieveNew')
{
// delete all existing messages
$chat->deleteMessages();
}
// if the operation is Retrieve elseif($mode == 'RetrieveNew')
{
// get the id of the last message retrieved by the client
$id = $_POST['id'];
}
// Clear the output if(ob_get_length()) ob_clean();
// Headers are sent to prevent browsers from caching
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
header('Content-Type: text/xml');
// retrieve new messages from the server
echo $chat->retrieveNewMessages($id);
?>

7. Create another file named chat.class.php, and add this code to it:

<?php
// load configuration file require_once('config.php');
// load error handling module require_once('error_handler.php');

// class that contains server-side chat functionality class Chat
{
// database handler private $mMysqli;

// constructor opens database connection function __construct()
{
// connect to the database
$this->mMysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE);
}

// destructor closes database connection public function __destruct()
{
$this->mMysqli->close();
}

// truncates the table containing the messages public function deleteMessages()
{
// build the SQL query that adds a new message to the server
$query = 'TRUNCATE TABLE chat';
// execute the SQL query
$result = $this->mMysqli->query($query);
}

/*
The postMessages method inserts a message into the database
- $name represents the name of the user that posted the message
- $messsage is the posted message
- $color contains the color chosen by the user
*/
public function postMessage($name, $message, $color)
{
// escape the variable data for safely adding them to the database
$name = $this->mMysqli->real_escape_string($name);
$message = $this->mMysqli->real_escape_string($message);
$color = $this->mMysqli->real_escape_string($color);
// build the SQL query that adds a new message to the server
$query = 'INSERT INTO chat(posted_on, user_name, message, color) ' .
'VALUES (NOW(), "' . $name . '" , "' . $message .
'","' . $color . '")';
// execute the SQL query
$result = $this->mMysqli->query($query);
}

/*
The retrieveNewMessages method retrieves the new messages that have been posted to the server.
- the $id parameter is sent by the client and it
represents the id of the last message received by the client. Messages more recent by $id will be fetched from the database and returned to
the client in XML format.
*/
public function retrieveNewMessages($id=0)
{
// escape the variable data
$id = $this->mMysqli->real_escape_string($id);
// compose the SQL query that retrieves new messages if($id>0)
{
// retrieve messages newer than $id
$query =
'SELECT chat_id, user_name, message, color, ' .
' DATE_FORMAT(posted_on, "%Y-%m-%d %H:%i:%s") ' .
' AS posted_on ' .
' FROM chat WHERE chat_id > ' . $id .
' ORDER BY chat_id ASC';
}
else
{
// on the first load only retrieve the last 50 messages from server
$query =
' SELECT chat_id, user_name, message, color, posted_on FROM ' .
' (SELECT chat_id, user_name, message, color, ' .

' DATE_FORMAT(posted_on, "%Y-%m-%d %H:%i:%s") AS posted_on ' .
' FROM chat ' .
' ORDER BY chat_id DESC ' .
' LIMIT 50) AS Last50' .
' ORDER BY chat_id ASC';
}
// execute the query
$result = $this->mMysqli->query($query);

// build the XML response
$response = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
$response .= '<response>';
// output the clear flag
$response .= $this->isDatabaseCleared($id);
// check to see if we have any results if($result->num_rows)
{
// loop through all the fetched messages to build the result message while ($row = $result->fetch_array(MYSQLI_ASSOC))
{
$id = $row['chat_id'];
$color = $row['color'];
$userName = $row['user_name'];
$time = $row['posted_on'];
$message = $row['message'];
$response .= '<id>' . $id . '</id>' .
'<color>' . $color . '</color>' .
'<time>' . $time . '</time>' .
'<name>' . $userName . '</name>' .
'<message>' . $message . '</message>';
}
// close the database connection as soon as possible
$result->close();
}

// finish the XML response and return it
$response = $response . '</response>';
return $response;
}

/*
The isDatabaseCleared method checks to see if the database has been cleared since last call to the server
- the $id parameter contains the id of the last message received by
the client
*/
private function isDatabaseCleared($id)
{
if($id>0)
{
// by checking the number of rows with ids smaller than the client's
// last id we check to see if a truncate operation was performed in
// the meantime
$check_clear = 'SELECT count(*) old FROM chat where chat_id<=' . $id;
$result = $this->mMysqli->query($check_clear);
$row = $result->fetch_array(MYSQLI_ASSOC);

// if a truncate operation occured the whiteboard needs to be reset if($row['old']==0)
return '<clear>true</clear>';
}
return '<clear>false</clear>';

}
}
?>

8. Create another file named get_color.php and add this code to it:

<?php
// the name of the image file
$imgfile='palette.png';
// load the image file
$img=imagecreatefrompng($imgfile);
// obtain the coordinates of the point clicked by the user
$offsetx=$_GET['offsetx'];
$offsety=$_GET['offsety'];
// get the clicked color
$rgb = ImageColorAt($img, $offsetx, $offsety);
$r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF;
// return the color code
printf('#%02s%02s%02s', dechex($r), dechex($g), dechex($b));
?>

9. Let's deal with the client now. Start by creating chat.css and adding this code to it:

body
{
font-family: Tahoma, Helvetica, sans-serif;
margin: 1px;
font-size: 12px;
text-align: left
}

#content
{
border: DarkGreen 1px solid;
margin-bottom: 10px
}

input
{
border: #999 1px solid;
font-size: 10px
}

#scroll
{
position: relative; width: 340px; height: 270px; overflow: auto
}

.item
{
margin-bottom: 6px
}

#colorpicker
{
text-align:center
}

10. Create a new file named index.html, and add this code to it:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>AJAX Chat</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link href="chat.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="chat.js" ></script>
</head>
<body onload="init();">
<noscript>
Your browser does not support JavaScript!!
</noscript>
<table id="content">
<tr>
<td>
<div id="scroll">
</div>
</td>
<td id="colorpicker">
<img src="palette.png" id="palette" alt="Color
Palette" border="1" onclick="getColor(event);"/>
<br />
<input id="color" type="hidden" readonly="true" value="#000000" />
<span id="sampleText">
(text will look like this)
</span>
</td>
</tr>
</table>
<div>
<input type="text" id="userName" maxlength="10" size="10"
onblur="checkUsername();"/>
<input type="text" id="messageBox" maxlength="2000" size="50" onkeydown="handleKey(event)"/>
<input type="button" value="Send" onclick="sendMessage();" />
<input type="button" value="Delete All" onclick="deleteMessages();" />
</div>
</body>
</html>

11. Create another file named chat.js and add this code to it:

/* chatURL - URL for updating chat messages */
var chatURL = "chat.php";
/* getColorURL - URL for retrieving the chosen RGB color */
var getColorURL = "get_color.php";
/* create XMLHttpRequest objects for updating the chat messages and getting the selected color */
var xmlHttpGetMessages = createXmlHttpRequestObject();
var xmlHttpGetColor = createXmlHttpRequestObject();
/* variables that establish how often to access the server */
var updateInterval = 1000; // how many miliseconds to wait to get new message
// when set to true, display detailed error messages
var debugMode = true;
/* initialize the messages cache */
var cache = new Array();
/* lastMessageID - the ID of the most recent chat message */
var lastMessageID = -1;
/* mouseX, mouseY - the event's mouse coordinates */
var mouseX,mouseY;

/* creates an XMLHttpRequest instance */
function createXmlHttpRequestObject()
{
// will store the reference to the XMLHttpRequest object var xmlHttp;
// this should work for all browsers except IE6 and older try
{
// try to create XMLHttpRequest object xmlHttp = new XMLHttpRequest();
}
catch(e)
{
// assume IE6 or older
var XmlHttpVersions = new Array("MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.5.0", "MSXML2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP");
// try every prog id until one works
for (var i=0; i<XmlHttpVersions.length && !xmlHttp; i++)
{
try
{
// try to create XMLHttpRequest object
xmlHttp = new ActiveXObject(XmlHttpVersions[i]);
}
catch (e) {}
}
}
// return the created object or display an error message
if (!xmlHttp)
alert("Error creating the XMLHttpRequest object.");
else
return xmlHttp;
}

/* this function initiates the chat; it executes when the chat page loads
*/
function init()
{
// get a reference to the text box where the user writes new messages var oMessageBox = document.getElementById("messageBox");
// prevents the autofill function from starting oMessageBox.setAttribute("autocomplete", "off");
// references the "Text will look like this" message
var oSampleText = document.getElementById("sampleText");
// set the default color to black oSampleText.style.color = "black";
// ensures our user has a default random name when the form loads checkUsername();
// initiates updating the chat window
requestNewMessages();
}

// function that ensures that the username is never empty and if so
// a random name is generated function checkUsername()
{
// ensures our user has a default random name when the form loads var oUser=document.getElementById("userName");
if(oUser.value == "")
oUser.value = "Guest" + Math.floor(Math.random() * 1000);
}

/* function called when the Send button is pressed */
function sendMessage()
{
// save the message to a local variable and clear the text box var oCurrentMessage = document.getElementById("messageBox");
var currentUser = document.getElementById("userName").value;
var currentColor = document.getElementById("color").value;
// don't send void messages
if (trim(oCurrentMessage.value) != "" &&
trim(currentUser) != "" && trim (currentColor) != "")
{
// if we need to send and retrieve messages params = "mode=SendAndRetrieveNew" +
"&id=" + encodeURIComponent(lastMessageID) +
"&color=" + encodeURIComponent(currentColor) + "&name=" + encodeURIComponent(currentUser) +
"&message=" + encodeURIComponent(oCurrentMessage.value);
// add the message to the queue cache.push(params);
// clear the text box
oCurrentMessage.value = "";
}
}

/* function called when the Delete Messages button is pressed */
function deleteMessages()
{
// set the flag that specifies we're deleting the messages params = "mode=DeleteAndRetrieveNew";
// add the message to the queue cache.push(params);
}

/* makes asynchronous request to retrieve new messages, post new messages, delete messages */
function requestNewMessages()
{
// retrieve the username and color from the page
var currentUser = document.getElementById("userName").value;
var currentColor = document.getElementById("color").value;
// only continue if xmlHttpGetMessages isn't void
if(xmlHttpGetMessages)
{
try
{
// don't start another server operation if such an operation
// is already in progress
if (xmlHttpGetMessages.readyState == 4 ||
xmlHttpGetMessages.readyState == 0)
{
// we will store the parameters used to make the server request var params = "";
// if there are requests stored in queue, take the oldest one
if (cache.length>0)
params = cache.shift();
// if the cache is empty, just retrieve new messages
else
params = "mode=RetrieveNew" + "&id=" +lastMessageID;
// call the server page to execute the server-side operation xmlHttpGetMessages.open("POST", chatURL, true); xmlHttpGetMessages.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
xmlHttpGetMessages.onreadystatechange = handleReceivingMessages;

xmlHttpGetMessages.send(params);
}
else
{
// we will check again for new messages
setTimeout("requestNewMessages();", updateInterval);
}
}
catch(e)
{
displayError(e.toString());
}
}
}

/* function that handles the http response when updating messages */
function handleReceivingMessages()
{
// continue if the process is completed if (xmlHttpGetMessages.readyState == 4)
{
// continue only if HTTP status is "OK" if (xmlHttpGetMessages.status == 200)
{
try
{
// process the server's response readMessages();
}
catch(e)
{
// display the error message
displayError(e.toString());
}
}
else
{
// display the error message
displayError(xmlHttpGetMessages.statusText);
}
}
}

/* function that processes the server's response when updating messages */
function readMessages()
{
// retrieve the server's response
var response = xmlHttpGetMessages.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0
|| response.indexOf("error:") >= 0
|| response.length == 0)
throw(response.length == 0 ? "Void server response." : response);
// retrieve the document element
response = xmlHttpGetMessages.responseXML.documentElement;
// retrieve the flag that says if the chat window has been cleared or not
clearChat =
response.getElementsByTagName("clear").item(0).firstChild.data;
// if the flag is set to true, we need to clear the chat window
if(clearChat == "true")
{
// clear chat window and reset the id
document.getElementById("scroll").innerHTML = "";
lastMessageID = -1;

}
// retrieve the arrays from the server's response
idArray = response.getElementsByTagName("id"); colorArray = response.getElementsByTagName("color"); nameArray = response.getElementsByTagName("name"); timeArray = response.getElementsByTagName("time"); messageArray = response.getElementsByTagName("message");
// add the new messages to the chat window
displayMessages(idArray, colorArray, nameArray, timeArray, messageArray);
// the ID of the last received message is stored locally
if(idArray.length>0)
lastMessageID = idArray.item(idArray.length - 1).firstChild.data;
// restart sequence
setTimeout("requestNewMessages();", updateInterval);
}

/* function that appends the new messages to the chat list */
function displayMessages(idArray, colorArray, nameArray, timeArray, messageArray)
{
// each loop adds a new message for(var i=0; i<idArray.length; i++)
{
// get the message details
var color = colorArray.item(i).firstChild.data.toString();
var time = timeArray.item(i).firstChild.data.toString();
var name = nameArray.item(i).firstChild.data.toString();
var message = messageArray.item(i).firstChild.data.toString();
// compose the HTML code that displays the message var htmlMessage = "";
htmlMessage += "<div class=\"item\" style=\"color:" + color + "\">";
htmlMessage += "[" + time + "] " + name + " said: <br/>";
htmlMessage += message.toString();
htmlMessage += "</div>";
// display the message displayMessage (htmlMessage);
}
}

// displays a message
function displayMessage(message)
{
// get the scroll object
var oScroll = document.getElementById("scroll");
// check if the scroll is down
var scrollDown = (oScroll.scrollHeight - oScroll.scrollTop <=
oScroll.offsetHeight );
// display the message oScroll.innerHTML += message;
// scroll down the scrollbar
oScroll.scrollTop = scrollDown ? oScroll.scrollHeight :
oScroll.scrollTop;
}

// function that displays an error message function displayError(message)
{
// display error message, with more technical details if debugMode is true
displayMessage("Error accessing the server! "+ (debugMode ? "<br/>" + message : ""));
}

/* handles keydown to detect when enter is pressed */

function handleKey(e)
{
// get the event
e = (!e) ? window.event : e;
// get the code of the character that has been pressed
code = (e.charCode) ? e.charCode : ((e.keyCode) ? e.keyCode : ((e.which) ? e.which : 0));
// handle the keydown event if (e.type == "keydown")
{
// if enter (code 13) is pressed if(code == 13)
{
// send the current message sendMessage();
}
}
}

/* removes leading and trailing spaces from the string */
function trim(s)
{
return s.replace(/(^\s+)|(\s+$)/g, "")
}

/* function that computes the mouse's coordinates in page */
function getMouseXY(e)
{
// browser specific if(window.ActiveXObject)
{
mouseX = window.event.x + document.body.scrollLeft;
mouseY = window.event.y + document.body.scrollTop;
}
else
{
mouseX = e.pageX;
mouseY = e.pageY;
}
}

/* makes a server call to get the RGB code of the chosen color */
function getColor(e)
{
getMouseXY(e);
// don't do anything if the XMLHttpRequest object is null
if(xmlHttpGetColor)
{
// initialize the offset position with the mouse current position
var offsetX = mouseX;
var offsetY = mouseY;
// get references
var oPalette = document.getElementById("palette");
var oTd = document.getElementById("colorpicker");
// compute the offset position in our window
if(window.ActiveXObject)
{
offsetX = window.event.offsetX;
offsetY = window.event.offsetY;
}
else
{
offsetX -= oPalette.offsetLeft + oTd.offsetLeft;
offsetY -= oPalette.offsetTop + oTd.offsetTop;

}
// call server asynchronously to find out the clicked color
try
{
if (xmlHttpGetColor.readyState == 4 ||
xmlHttpGetColor.readyState == 0)
{
params = "?offsetx=" + offsetX + "&offsety=" + offsetY;
xmlHttpGetColor.open("GET", getColorURL+params, true); xmlHttpGetColor.onreadystatechange = handleGettingColor; xmlHttpGetColor.send(null);
}
}
catch(e)
{
// display error message displayError(xmlHttp.statusText);
}
}
}

/* function that handles the http response */
function handleGettingColor()
{
// if the process is completed, decide to do with the returned data if (xmlHttpGetColor.readyState == 4)
{
// only if HTTP status is "OK"
if (xmlHttpGetColor.status == 200)
{
try
{
//change the color changeColor();
}
catch(e)
{
// display error message
displayError(xmlHttpGetColor.statusText);
}
}
else
{
// display error message
displayError(xmlHttpGetColor.statusText);
}
}
}

/* function that changes the color used for displaying messages */
function changeColor()
{
response=xmlHttpGetColor.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0
|| response.indexOf("error:") >= 0
|| response.length == 0)
throw(response.length == 0 ? "Can't change color!" : response);
// change color
var oColor = document.getElementById("color");
var oSampleText = document.getElementById("sampleText");
oColor.value = response;
oSampleText.style.color = response;
}

12. After having talked about it, it is time to see it in action. Let's see how the chat window looks in the beginning. Load http://localhost/ajax/chat/index.html with a web browser.

Figure 5.3: The Chat Window

You can observe the default color of your messages is black (RGB code: #000000). In Figure 5.3 we can also see a default random name Guest91. When initially loading the chat window, all previously posted messages are displayed. You can change your messages' color by simply clicking on the palette image on the desired color.

What just happened?
Technically, the application is split in two smaller applications that build our final solution:

• The chat application
• Choosing a color application

The chat application implements the basic functions of posting and retrieving messages. Each user can choose a nickname and post a message. The chat window containing all the posted messages
is updated by retrieving the messages asynchronously from the server.

We use a palette containing the entire spectrum of colors to allow the user pick a color for the text he or she writes. When clicking on the palette, the mouse coordinates are sent to the server, which obtains the color code.

If you analyze the code for a bit, the details will become clear. Let's have a look at it starting with the index.html file. The only part that is really interesting in this script is a scroll region that can be implemented in DHTML. A little piece of information regarding scrolling can be found at http://www.dyn-web.com/dhtml/scroll/. Basically, the idea for having a part of the page with a scrollbar next to it is to have two layers one inside another. In our example, the div scroll and its inner layers do the trick.

The outer layer is scroll. It has a fixed width and height and the most useful property of it is overflow. Generally, the content of a block box is confined to the content edges of the box. In certain cases, a box may overflow, meaning its content lies partly or entirely outside of the box. In CSS, this property specifies what happens when an element overflows its area. For more details, please see overflow's specification, at http://www.w3.org/TR/REC-CSS2/visufx.html.

OK, now that we have defined our block box and what happens when its content exceeds its area, we can easily guess that the inner content of the block box is the one that will eventually exceed the dimensions of the box. The inner content contains the messages written in the chat.

Next, we move to the chat.js file containing the JavaScript part for our application.

The whole file can be divided in two parts: the one that handles choosing a color and the other that handles chat messages.

We will start by choosing a color. This part, which, in the beginning, might seem pretty difficult proves to be far easier to implement. Let's have a panoramic view of the entire process. First, we have a palette image that contains the entire spectrum of visible colors. PHP has two functions that will help us in finding the RGB code of the chosen color, imagecreatefrompng and
imagecolorat. When talking about the get_color.php page we will see more about these functions. For now all we need to know is that these two functions allow us to obtain the RGB code of a pixel given the x and y position in the image. The position of the pixel is retrieved in the
getMouseXY function.

The getColor function retrieves the RGB code of the color chosen by the user when clicking the palette image. First of all it retrieves the mouse coordinates from the event. Then, it computes the coordinates where the click event has been produced as relative values within the image. This is done by subtracting from the positions obtained by the getMouseXY function the relative position of the image inside the td element and the td position in the window. Having computed the relative coordinates as the offsetx and offsety, the server page that will return the RGB code of the chosen color is called. The change of state of the HTTP request object is handled by the handleGettingColor function.

The handleGettingColor function checks to see when the request to the server is completed and if no errors occurred, the changeColor function is called. This function populates the text field with the RGB code returned by the server and colors the sample text with the given code.

OK, let's now see how the chat works.

By default when the page initializes and the onblur event occurs, the checkUsername function is called. This function ensures that the name of the user isn't empty by generating an arbitrary username.

On pressing the Send button, the sendMessage function is called. This function adds the current message to the message queue to be sent to the server. Before adding it into the queue the function trims the message by calling the trim function, and we encode the message using encodeURIComponent to make sure it gets through successfully.

The handleKey function is called whenever a keydown event occurs. When the Enter key is pressed the sendMessage function is called so that both pressing the Send button and pressing Enter within the messageBox control have the same effect.
The deleteMessages function adds the delete message to the messages to be sent to the server. The requestNewMessages function is responsible for sending chat messages. It retrieves a
message from the queue and sends it to the server. The change of state of the HTTP request object
is handled by the handleReceivingMessages function.

The handleReceivingMessages checks to see when the request to the server is completed and if no errors occurred then the readMessages function is called.

The readMessages function checks to see if someone else erased all the chat messages and if so the client's chat window is also emptied. In order to append new messages to the chat, we call the displayMessages function. This function takes as parameters the arrays that correspond to the new messages. It composes the new messages as HTML and it appends them to those already in the chat by calling the displayMessage function. In the beginning, the displayMessage function checks to see if the scroll bar is at the bottom of the list of messages. This is necessary in order to reposition it at the end of the function so that the focus is now on the last new messages.

The last function presented is the init function. Its role is to retrieve the chat messages, to ensure that the username is not null, to set the text's color to black, and to turn off the auto complete functionality.

For the error handling part, we use the displayError function, which calls the displayMessage
function in turn with the error message as parameter.

Let's move on to the server side of the application by first presenting the chat.php file. The server deals with clients' requests like this:

• Retrieves the client's parameters
• Identifies the operations that need to be performed
• Performs the necessary operations
• Sends the results back to the client

The request includes the mode parameter that specifies one of the following operations to be performed by the server:

• SendAndRetrieve: First the new messages are inserted in the database and then all new messages are retrieved and sent back to the client.
• DeleteAndRetrieve: All messages are erased and the new messages that might exist are fetched and sent back to the client.
• Retrieve: The new messages are fetched and sent back to the client.

The business logic behind chat.php lies in the chat.class.php script, which contains the
Chat class.

The deleteMessages method truncates the data table erasing all the information. The postMessages method inserts all the new messages into the database.
The isDatabaseCleared method checks to see if all messages have been erased. Basically, by providing the ID of the last message retrieved from the server and by checking if it still exists, we can detect if all messages have been erased.

The retrieveNewMessages method gets all new messages since the last message (identified by its id) retrieved from the server during the last request (if a last request exists; or all messages in other cases) and also checks to see if the database has been emptied by calling the isDatabaseCleared method. This function composes the response for the client and sends it.

The config.php file contains the database configuration parameters and the error_handler.php
file contains the module for handling errors.

Now, let's see how the color-choosing functionality is implemented on the server side in the
get_color.php file.

We mentioned above two PHP functions that we used to retrieve the RGB code of a pixel in an image. Let's see how they work:

• imagecreatefrompng(string filename) returns an image identifier representing the image in PNG format obtained from the given filename.
• int imagecolorat(resource image, int x, int y) returns the index of the color of the pixel at the specified location in the image specified by image. Returns the index of the color of the pixel at the specified location in the image specified by image. If PHP is compiled against GD library 2.0 or higher and the image is a
true-color image, this function returns the RGB value of that pixel as an integer.

The first 8 bits of the result contains the blue code, the next 8 bits the green code and the next
8 bits the red code. Using bit shifting and masking we obtain the distinct red, green, and blue components as integer values. All that's left for us to do is to convert them to their hexadecimal value, to concatenate these values, and to send them to the client.
Let's wrap things up! We started with the interface that is presented to the user, the client side of the application composed by the HTML, CSS, and JavaScript files implemented in the index.html, chat.css, and chat.js files. After having seen how the interface looks and how the data retrieved from the web server is processed in order to be presented to the user, we went one step further and took a look at the server side of the application.

We saw the files that are called by the client side, chat.php and get_color.php. The last step consisted in presenting the parameters to connect to the database (config.php), the error handling module (error_handler.php), and the script containing the core of the functionality (chat.class.php).

Summary
At the beginning of the chapter we saw why one can face problems when communicating with other people in a dynamic way over the Internet. We saw what the solutions for these problems are and how AJAX chat solutions can bring something new, useful, and ergonomic. After seeing some other AJAX chat implementations, we started building our own solution. Step by step we have implemented our AJAX chat solution keeping it simple, easily extensible, and modular.

After reading this chapter, you can try improving the solution, by adding new features like:

• Chat rooms
• Simple command lines (joining/leaving a chat room, switching between chat room)
• Private messaging

0 comments: