Now that you know all about the architecture, database, user interface, and server-side components, it's time to glue it all together using JavaScript. To begin, you need to define some constants. The first constants are simply locations of various resources that need to be used by AjaxMail:
var sAjaxMailURL = "AjaxMailAction.php"; var sAjaxMailNavigateURL = "AjaxMailNavigate.php"; var sAjaxMailAttachmentURL = "AjaxMailAttachment.php"; var sAjaxMailSendURL = "AjaxMailSend.php"; var sImagesDir = "images/"; var sRestoreIcon = sImagesDir + "icon_restore.gif"; var sDeleteIcon = sImagesDir + "icon_delete.gif"; var sInfoIcon = sImagesDir + "icon_info.gif"; var sErrorIcon = sImagesDir + "icon_alert.gif"; var aPreloadImages = [sRestoreIcon, sDeleteIcon, sInfoIcon, sErrorIcon];
The first parts of this code simply define the URLs used to make requests back to the server. These will be used later to integrate the Ajax interface. The second part of this code identifies images that are necessary for the user interface and then places them into an array called aPreloadImages. These images are preloaded so that the user interface responds quickly:
for (var i=0; i < aPreloadImages.length; i++) { var oImg = new Image(); oImg.src = aPreloadImages[i]; }
This code uses an Image object, which is essentially an invisible <img/> element. Because not all of these images are necessary when the application is first loaded, most won't be loaded until used for the first time. This could result in a delay that may be confusing to users. Preloading the images prevents this issue from occurring.
Next, there are some messages and strings that need to be displayed to the user. It helps to define these early in the code so that it's easy to change the messages in the future, if necessary:
var sEmptyTrashConfirm = "You are about to permanently delete everything in the Trash. Continue?"; var sEmptyTrashNotice = "The Trash has been emptied."; var sDeleteMailNotice = "The message has been moved to the Trash."; var sRestoreMailNotice = "The message has been moved to your Inbox."; var sRestore = "Restore"; var sDelete = "Move to the Trash"; var sTo = "To "; var sCC = "CC "; var sBCC = "BCC "; var sFrom = "From ";
When one of the notices is displayed, you really want to show it only for a short amount of time so that it doesn't become distracting to the user or blend in with the rest of the screen. The variable iShowNoticeTime indicates the duration (in number of milliseconds) for a notice to appear on the screen. By default, this is 5 seconds (5000 milliseconds):
var iShowNoticeTime = 5000;
The last bit of code to be defined ahead of time is a couple of constants and an array:
var INBOX = 1; var TRASH = 2; var aFolders = [""," Inbox", "Trash"];
In this code, the first two variables are constants defining the numeric identifiers for the Inbox and Trash folders. These coincide with the values they have in the database. The array of strings contains the names for each of the folders so these don't have to be returned from the database all the time. The first string in the array is empty since it will never be used. (There is no folder with a numeric ID of zero.)
Important |
In an actual implementation, you may choose to have these variables generated by some server-side process that reads the values out of the database and outputs appropriate JavaScript code. For simplicity in this example, these values are defined right in the JavaScript file. |
Before diving into the main part of the code, there are some helper functions that are necessary. Helper functions are functions that aren't necessarily specific to a particular application but perform some process that is necessary. AjaxMail has a handful of helper functions.
The first helper function is one that you have seen before. The getRequestBody() function was introduced in Chapter 2 to serialize the data in an HTML form so that it can be passed into an XMLHttp request. This function is necessary once again for AjaxMail. To refresh your memory, here's what the function looks like:
function getRequestBody(oForm) { var aParams = new Array(); for (var i=0 ; i < oForm.elements.length; i++) { var sParam = encodeURIComponent(oForm.elements[i].name); sParam += "="; sParam += encodeURIComponent(oForm.elements[i].value); aParams.push(sParam); } return aParams.join("&"); }
This code is exactly the same as it was in Chapter 2 and will be used to send e-mail messages.
One problem with e-mail addresses is that they can be specified in any number of formats. For example:
myname@somewhere.com
My Real Name <myname@somewhere.com>
"My Real Name" <myname@somewhere.com>
If you use e-mail frequently, you'll probably recognize these formats as they are used in most major e-mail applications. When displaying an e-mail's sender in the folder view, AjaxMail displays the real name only. If no real name is present, the e-mail address is shown. To handle this, a helper function called cleanupEmail() is used:
var reNameAndEmail = /(.*?)<(.*?)>/i; function cleanupEmail(sText) { if (reNameAndEmail.test(sText)) { return RegExp.$1.replace(/"/g, ""); } else { return sText; } }
The most important part of the function is actually the regular expression reNameAndEmail, which matches a string containing both a real name and an e-mail address regardless of the use of quotation marks. Inside the function, the text is tested against this regular expression. If test() returns true, that means the e-mail address contains both pieces of information and you should extract the real name (which is stored in RegExp.$1). However, this name may have quotation marks in it, so the next step is to replace all the quotation marks with an empty string using the replace() method. If, on the other hand, the regular expression doesn't match the text that was passed in, this means that it contains just an e-mail address, so it is returned without any changes.
The last helper function is called htmlEncode(), and it simply replaces greater-than (>), less-than (<), ampersand (&), and quote ("") characters with their appropriate HTML entities. This ensures that no dangerous HTML will be created when reading text from an e-mail:
function htmlEncode(sText) { if (sText) { return sText.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """) } else { return ""; } }
This function also checks to make sure text is passed in. If sText is null, the function returns an empty string; otherwise, the replacements are done using the replace() method.
The main part of the AjaxMail application is the mailbox. This is a single JavaScript object containing all the properties and methods necessary to run the user interface. Because there should be only one instance of this object, it is defined using object literal notation:
var oMailbox = { info: new Object(), processing: false, message: new Object(), nextNotice: null, //more code here }
The mailbox object is stored in a variable named oMailbox and has four properties. The first property, info, is an object that will contain the folder information for the folder view. This object will be returned from the server but is initialized here to a generic object to avoid possible errors. Next is the processing property, which is simply a Boolean flag indicating whether the application is processing a request. When set to true, no other processes can be initiated. The third property is message, which will contain an object describing the message being read in the read view. Once again, this property is initialized to an empty object in order to avoid possible errors. The last property, nextNotice, is used by several callback functions to determine which notice should be displayed once a particular process has completed.
Before you can begin interacting with the user interface, it helps to store references to the elements you'll be using the most. You can do this by using document.getElementById() repeatedly, but that would require a lot of lines of code for all the elements used in AjaxMail. Instead, it's faster and more efficient to iterate over all the elements in a page and add a reference to each one that has an id attribute. This is part of the job of the init() method:
init: function () { var colAllElements = document.getElementsByTagName("*"); if (colAllElements.length == 0) { colAllElements = document.all; } for (var i=0; i < colAllElements.length; i++) { if (colAllElements[i].id.length > 0) { this[colAllElements[i].id] = colAllElements[i]; } } //more code here },
This method first calls document.getElementsByTagName() and passes in an asterisk. In DOM-compliant browsers, this should return a collection of all the elements in the document. However, the Internet Explorer implementation doesn't support this usage, so you'll also need to be prepared for this. If the returned collection has no elements (length is not greater than zero), this means that Internet Explorer is in use. To work around this limitation, you can use the document.all collection (supported in Internet Explorer only) in place of the collection returned from getElementsByTagName(). Once colAllElements contains a usable collection, you can then iterate over the collection using a for loop. If the element has an id property (the length of the id property is greater than zero), a reference is saved on the mailbox object. So divFolderStatus is saved to the mailbox object as a property named divFolderStatus and can be accessed using this.divFolderStatus inside of a method or oMailbox.divFolderStatus outside of a method.
The mailbox object uses two types of data: folder information and message information. Folder information is returned as a JSON string from the server and then parsed into an object containing information about the given folder. A typical folder information object looks like this:
{ "messageCount":2, "page":1, "pageCount":1, "folder":1, "firstMessage":1, "unreadCount": 1, "messages":[ { "id":"64", "from":" Joe Smith <joe@smith.com>", "subject":" Re: How about this weekend?", "date":" Oct 29 2005", "hasAttachments":false, "unread":true }, { "id":"63", "from":" Joe Smith <joe@smith.com>", "subject":" How about this weekend?", "date":" Oct 29 2005", "hasAttachments":false, "unread":false } ] }
This object is stored in the info property of the mailbox so that it can be used by all methods. To assign the data, the loadInfo() method is used. Because you may have an object or a JSON string to assign, this method needs to check the type of the argument that is passed in:
loadInfo: function (vInfo) { if (typeof vInfo == "string") { this.info = JSON.parse(vInfo); } else { this.info = vInfo; } },
If vInfo is a string, it is parsed into an object using JSON.parse(); otherwise, it's an object, so it can be directly assigned to the info property. This object is used whenever a folder page is rendered, but there is also some data needed to display an individual e-mail.
The JSON object representing a single e-mail message is in the following format:
{ "id":"63", "to":" you@somewhere.com", "from":" Joe Smith <joe@smith.com>", "cc":"", "subject":" How about this weekend?", "bcc":"", "date":" Oct 29, 2005 05:15 AM", "hasAttachments":false, "unread":true, "message":" I was thinking this weekend would be good? How about you?<br />Joe", "attachments":[], "unreadCount":8 }
When a message is viewed in AjaxMail, this information is assigned to the message property so that it is accessible from all methods. The loadMessage() method accepts either an object or a JSON string containing this message information and assigns it to the message property:
loadMessage: function (vMessage) { if (typeof vMessage == "string") { this.message = JSON.parse(vMessage); } else { this.message = vMessage; } },
As you can see, this is essentially the same as loadInfo(); it just deals with different data. These two methods are critical because they are the primary means of passing data from the server to the client.
You will remember the processing property from the mailbox object description earlier. To set the value of this property, a special method called setProcessing() is used. The sole argument for this method is a Boolean value, set to true when the mailbox is processing or false when it is not. This method also shows the divFolderStatus element whenever the mailbox is processing:
setProcessing: function (bProcessing) { this.processing = bProcessing; this.divFolderStatus.style.display = bProcessing ? "block" : "none"; },
If the bProcessing argument is true, the divFolderStatus element has its display property set to block, ensuring that it is visible; otherwise, the property is set to none, hiding it from view. This method is used throughout the mailbox object to prevent multiple simultaneous requests from occurring.
Another method used throughout is showNotice(), which displays a notice to the user regarding the state of a request:
showNotice: function (sType, sMessage) { var divNotice = this.divNotice; divNotice.className = sType; divNotice.innerHTML = sMessage; divNotice.style.visibility = "visible"; setTimeout(function () { divNotice.style.visibility = "hidden"; }, iShowNoticeTime); },
This method accepts two arguments: the type of message (either info or error) and the message to be displayed. The type of message also is the CSS class that will be assigned to divNotice, giving it the appropriate format. The message is assigned to the element via the innerHTML property, which means you can include HTML code in the message if necessary. After that, the element is made visible to the user by setting the visibility property to visible. Since the message should be displayed only for a specific amount of time, the setTimeout() function is used to determine when the visibility property should be set back to hidden. The interval is the global variable iShowNoticeTime that was defined earlier. Any notice displayed using this method will be shown immediately and then disappear after the designated amount of time.
There are two different ways that AjaxMail communicates with the server: through XMLHttp and through a hidden iframe. To provide for this, several methods are used to encapsulate most of the communication functionality so that other functions can use them directly.
All XMLHttp GET requests are made through the request() method. This method takes three arguments: the action to perform, a callback function to notify when the request is complete, and an optional e-mail message ID. Every request going through this method goes to AjaxMailAction.php, passing in the action (the first argument) on the query string. Here's the complete method:
request: function (sAction, fnCallback, sId) { if (this.processing) return; try { this.setProcessing(true); var oXmlHttp = zXmlHttp.createRequest(); var sURL = sAjaxMailURL + "?folder=" +this.info.folder + "&page=" + this.info.page + "&action=" + sAction; if (sId) { sURL += "&id=" + sId; } oXmlHttp.open("get", sURL, true); oXmlHttp.onreadystatechange = function (){ try { if (oXmlHttp.readyState == 4) { if (oXmlHttp.status == 200) { fnCallback(oXmlHttp.responseText); } else { throw new Error("An error occurred while attempting to contact the server. The action ("+ sAction + ") did not complete."); } } } catch (oException) { oMailbox.showNotice("error", oException.message); } }; oXmlHttp.send(null); } catch (oException) { this.showNotice("error", oException.message); } },
Note that the very first line checks to see if the mailbox is processing another request. If it is, the function returns without executing the next request. Otherwise, the standard try...catch arrangement involving an XMLHttp object is executed. Before anything is done, setProcessing() is called to indicate that a request has begun. The URL is constructed by adding the current folder ID and page to the query string, followed by the action to perform. If a message ID is specified (sID), that is also added to the query string so that the action can be completed. Next, the XMLHttp object is initialized and the onreadystatechange event handler is assigned. Inside the event handler, the callback function (fnCallback) is called when the request succeeds, passing in the response text. If an error occurs during this process, a custom error is thrown. When the catch statement intervenes, the showNotice() method is used to display details about the message.
The method to send an e-mail is very similar, but uses a POST request instead:
sendMail: function () { if (this.processing) return; this.divComposeMailForm.style.display = "none"; this.divComposeMailStatus.style.display = "block"; try { this.setProcessing(true); var oXmlHttp = zXmlHttp.createRequest(); var sData = getRequestBody(document.forms["frmSendMail"]); oXmlHttp.open("post", sAjaxMailSendURL, true); oXmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); oXmlHttp.onreadystatechange = function (){ try { if (oXmlHttp.readyState == 4) { if (oXmlHttp.status == 200) { sendConfirmation(oXmlHttp.responseText); } else { throw new Error("An error occurred while attempting to contact the server. The mail was not sent."); } } } catch (oException) { oMailbox.showNotice("error", oException.message); } }; oXmlHttp.send(sData); } catch (oException) { this.showNotice("error", oException.message); } },
As with the request() method, the sendMail() method begins by checking to see if the mailbox is processing a request. If there are no other requests active, some user interface changes are made. First, the compose mail form is hidden from view by setting its display property to none. Then, the status area is shown by setting its display property to block. This effectively shows that the mail is being sent as an animated GIF plays.
Next, the setProcessing() method is called to indicate a request has begun and a data string is created by calling getRequestBody() on the mail form. After initializing the oXmlHttp object, the appropriate request header is set. The onreadystatechange event handler is the standard setup, and the response text is passed into the sendConfirmation() function (described in the upcoming Callback Functions section).
The last communication method is navigate(), which is used whenever the user interface change should be recorded in the browser history (allowing the user to click Back and Forward to navigate through the user interface changes). This method uses the hidden iframe to make requests to the server and receive responses back:
navigate: function (sAction, sId) { if (this.processing) return; try { this.setProcessing(true); var sURL = sAjaxMailNavigateURL + "?folder=" +this.info.folder + "&page=" + this.info.page + "&action=" + sAction; if (sId) { sURL += "&id=" + sId; } this.iLoader.src = sURL; } catch (oException) { this.showNotice("error", oException.message); } },
This method accepts only two arguments: an action to perform and an optional message ID (similar to request()). As with the other communication methods, this one begins by checking to see if the mailbox is processing another request and exits the method if that is the case. Otherwise, a standard try…catch block surrounds the rest of the code to catch any errors that may occur. Then, the processing flag is set to true to indicate a new request has begun. The URL is constructed in the same manner as in request(), adding the message ID only if it has been supplied. Last, the URL is assigned to the iframe via the src property. Now it is up to the page returned in the iframe to notify the mailbox that processing has been completed.
The most complex methods of the mailbox object are those relating to the rendering of data onto the screen. There are two methods: renderFolder(), which displays a mailbox folder, and renderMessage(), which displays a single e-mail message. Both of these use a small method called updateUnreadCount() that is responsible for updating the number of unread messages next to the Inbox link:
updateUnreadCount: function (iCount) { this.spnUnreadMail.innerHTML = iCount > 0 ? "("+ iCount + ")" : ""; }
This method expects the number of unread messages to be passed in as an argument. If that number is greater than 0, the spnUnreadMail element is updated to display that number; otherwise, the element is assigned an empty string. With this method defined, it's time to take a look at the two more complicated methods.
The renderFolder() method uses the info property to display the appropriate e-mail messages in the folder view. To begin, this method clears the folder view of all message information so that it can easily build up and insert new information:
renderFolder: function () {; var tblMain = this.tblMain; while (tblMain.tBodies[0].hasChildNodes()) { tblMain.tBodies[0].removeChild(tblMain.tBodies[0].firstChild); } //more code here },
This first part of the method stores a reference to tblMain in a local variable and then proceeds to remove all the child nodes from the <tbody/> element (referenced as tblMain.tBodies[0]). With all of the rows removed, it's now okay to start adding rows.
The next part of the method creates the DOM representation for the messages. Note that for simplicity, only the additions to the method are shown:
renderFolder: function () {; //remove all existing rows var oFragment = document.createDocumentFragment(); if (this.info.messages.length) { for (var i=0; i < this.info.messages.length; i++) { var oMessage = this.info.messages[i]; var oNewTR = this.trTemplate.cloneNode(true); oNewTR.id = "tr" + oMessage.id; oNewTR.onclick = readMail; if (oMessage.unread) { oNewTR.className = "new"; } var colCells = oNewTR.getElementsByTagName("td"); var imgAction = colCells[0].childNodes[0]; imgAction.id = oMessage.id; if (this.info.folder == TRASH) { imgAction.onclick = restoreMail; imgAction.src = sRestoreIcon; imgAction.title = sRestore; } else { imgAction.onclick = deleteMail; imgAction.src = sDeleteIcon; imgAction.title = sDelete; } colCells[1].appendChild( document.createTextNode(cleanupEmail(oMessage.from))); colCells[2].firstChild.style.visibility = oMessage.hasAttachments ? "visible" : "hidden"; colCells[3].appendChild( document.createTextNode(htmlEncode(oMessage.subject))); colCells[4].appendChild(document.createTextNode(oMessage.date)); oFragment.appendChild(oNewTR); } } else { var oNewTR = this.trNoMessages.cloneNode(true); oFragment.appendChild(oNewTR); } tblMain.tBodies[0].appendChild(oFragment); //more code here },
In this section of the code, the first step is to create a document fragment upon which the DOM will be built. Next, the number of messages is checked. If there is at least one message, the view must be built accordingly; otherwise, a clone of trNoMessages is created and added to the fragment in place of any other rows. When there are messages, however, the process is a bit more involved.
For each message, the process begins by storing the message in a local variable, oMessage. This is retrieved from the info property in the messages array. Next, a clone of the template row trTemplate is created and stored in oNewTR (passing in true to cloneNode() ensures that all nodes are cloned, not just the <tr/> element itself). Next, the ID of the row is assigned by prepending tr to the message's ID. Then, the onclick event handler is assigned to be readMail(), which is defined later in the "Event Handlers" section. If the message hasn't been read yet, oMessage.unread will be true, so the row will be assigned a CSS class of new. The next step is to assign data into each of the table cells.
To make references to the cells easier, the getElementsByTagName() method is used to extract a collection of just the table cells (colCells). The action icon, either to delete or restore the message, is in the first cell. The actual <img/> element is stored in imgAction for easy reference. Then, the image is assigned an ID equal to the message ID. To determine what the image should do when clicked, the current folder is checked by using info.folder. If the current folder is the Trash, then the image is set up to restore the e-mail by setting the onclick, src, and title properties to restore-specific values; otherwise, the icon is set up to delete e-mail by setting the same properties to delete-specific values. Both restoreMail() and deleteMail() are global functions used as event handlers. These are discussed in the "Event Handlers" section.
The second cell in each row should display who the e-mail is from, so oMessage.from is passed to the helper function cleanupEmail(), which was defined earlier in the chapter. The result of this function call is passed into document.createTextNode() to create the text for the cell, which is added using appendChild().
For the third cell in the row, you need to decide if the attachments icon should be displayed or not. If oMessage.hasAttachments is true, then the visibility of the icon is set to visible; otherwise. it's set to hidden. This is done using a compound assignment statement instead of an if statement for simplicity.
The fourth table cell contains the e-mail subject, which is passed into htmlEncode() to ensure that all characters are displayed correctly. This text is then used to create a text node that is added to the cell in the same way as the first cell. The fifth cell simply displays the message date after it is added to it as a text node. Then, the entire row is added to the document fragment before the loop begins again.
Regardless of the number of messages, the fragment is passed into the appendChild() method of the table body to add the rows to the folder view. However, the user interface isn't complete yet; there is still other information that must be updated.
Specifically, the folder title must be displayed, the unread message count must be updated, and the pagination control must be initialized:
renderFolder:function () { //delete all existing rows //create rows for messages if (this.hFolderTitle.innerHTML != aFolders[this.info.folder]) { this.hFolderTitle.innerHTML = aFolders[this.info.folder]; } this.updateUnreadCount(this.info.unreadCount); this.spnItems.style.visibility = this.info.messages.length ? "visible" : "hidden"; this.spnItems.innerHTML = this.info.firstMessage + "-" + (this.info.firstMessage + this.info.messages.length - 1) + "of " + this.info.messageCount; if (this.info.pageCount > 1) { this.imgNext.style.visibility = this.info.page < this.info.pageCount ? "visible" : "hidden"; this.imgPrev.style.visibility = this.info.page > 1 ? "visible" : "hidden"; } else { this.imgNext.style.visibility = "hidden"; this.imgPrev.style.visibility = "hidden"; } this.divFolder.style.display = "block"; this.divReadMail.style.display = "none"; this.divComposeMail.style.display = "none"; },
The first step in this section of code is to set the contents of the hFolderTitle element to the name of the folder, but only if it's different from the one currently being displayed. To do so, use the innerHTML element to both get and set the value (if necessary). Next, the number of unread messages is passed into updateUnreadCount() to update the number of unread messages next to the Inbox link.
If there is at least one message, spnItems must be displayed. This is the element that displays the currently viewed message count, such as "1-10 of 21." If there is at least one message, its visibility property is set to visible; otherwise, it is set to hidden. Then, the contents of the element are created by using various properties of the info object. The firstMessage property returns the number of the first message returned in this page. You can then calculate the number of the last message returned by adding the number of messages to firstMessage and then subtracting one. The total number of messages is returned in the messageCount property.
When there is more than one page of messages to be displayed, the imgNext and imgPrev images should be shown, but not always at the same time. If you are on the first page, for instance, imgPrev should not be shown; likewise for imgNext on the last page. By using the page property to get the current page, you can determine whether the image should be visible and display the appropriate value for the visibility property. Of course, if there is only one page, neither image needs to be displayed.
The very last step is to show the divFolder element and hide both divReadMail and divComposeMail from sight. This initializes the application to the folder view. When the user clicks a message in the list, it brings up the read view, which is rendered by the renderMessage() method.
Just as the renderFolder() method used the info property to determine what to render, the renderMessage() method uses the message property for the same reason. All the information necessary to display a single e-mail is contained within the message property. To begin, you assign the contents of each element in the read view using values from message:
renderMessage: function () { this.hSubject.innerHTML = htmlEncode(this.message.subject); this.divMessageFrom.innerHTML = sFrom + ""+ htmlEncode(this.message.from); this.divMessageTo.innerHTML = sTo + ""+ htmlEncode(this.message.to); this.divMessageCC.innerHTML = this.message.cc.length ? sCC + ""+ htmlEncode(this.message.cc) : ""; this.divMessageBCC.innerHTML = this.message.bcc.length ? sBCC + ""+ htmlEncode(this.message.bcc) : ""; this.divMessageDate.innerHTML = this.message.date; this.divMessageBody.innerHTML = this.message.message; //more code here this.updateUnreadCount(this.message.unreadCount); this.divFolder.style.display = "none"; this.divReadMail.style.display = "block"; this.divComposeMail.style.display = "none"; },
Each of the elements responsible for displaying the various parts of the e-mail are assigned data from the message object. Of course, most of these values use htmlEncode() to ensure that the data is displayed correctly. For the CC and BCC fields, their values are assigned only if they contain data to begin with. If not, the divMessageCC and divMessageBCC fields are assigned empty strings, which effectively hides them from view.
Then the unread message count is updated. This is being updated here as well because there's no reason to waste a round trip to the server and not get such a small piece of information. The last step in the process is to hide divFolder and divComposeMail while showing divReadMail. However, there is some code missing from this method. The previous code doesn't take into account attachments.
Dealing with attachments essentially means outputting a list of all the attachments for a message and linking them so that each can be downloaded with a simple click. The ulAttachments element, which is part of the code in index.php, should be shown only when there is at least one attachment. Here's how to build this section of the view:
renderMessage: function () { this.hSubject.innerHTML = htmlEncode(this.message.subject); this.divMessageFrom.innerHTML = sFrom + ""+ htmlEncode(this.message.from); this.divMessageTo.innerHTML = sTo + ""+ htmlEncode(this.message.to); this.divMessageCC.innerHTML = this.message.cc.length ? sCC + ""+ htmlEncode(this.message.cc) : ""; this.divMessageBCC.innerHTML = this.message.bcc.length ? sBCC + ""+ htmlEncode(this.message.bcc) : ""; this.divMessageDate.innerHTML = this.message.date; this.divMessageBody.innerHTML = htmlEncode(this.message.message); if (this.message.hasAttachments) { this.ulAttachments.style.display = ""; var oFragment = document.createDocumentFragment(); for (var i=0; i < this.message.attachments.length; i++) { var oLI = document.createElement("li"); oLI.className = "attachment"; oLI.innerHTML = "<a href=\"" + sAjaxMailAttachmentURL + "?id=" + this.message.attachments[i].id + "\" target=\"_blank\">" + this.message.attachments[i].filename + "</a> (" + this.message.attachments[i].size + ")"; oFragment.appendChild(oLI); } this.ulAttachments.appendChild(oFragment); this.liAttachments.style.display = ""; } else { this.ulAttachments.style.display = "none"; this.liAttachments.style.display = "none"; this.ulAttachments.innerHTML = ""; } this.updateUnreadCount(this.message.unreadCount); this.divFolder.style.display = "none"; this.divReadMail.style.display = "block"; this.divComposeMail.style.display = "none"; },
Naturally, the first step in rendering attachment information is to check if there are any attachments using the hasAttachments property. If there are attachments, the ulAttachments element is displayed and the attachments are iterated over, creating a new <li/> element for each one and assigning additional information using the innerHTML property. Each of these new elements is added to a document fragment for efficiency. When all attachments have had their DOM representation created, the fragment is appended to ulAttachments. Then, the liAttachments element is displayed by setting its display property to an empty string. This element contains the View Attachments link in the message header.
If there are no attachments to the message, ulAttachments and liAttachments are hidden from view by setting their display properties to none. Additionally, is cleared of all its data by setting innerHTML to an empty string. This prevents attachments from showing up on e-mails that they weren't attached to.
With all the Ajax request methods and callback functions in place, you now have all the tools necessary to create the functionality of an e-mail application. For each action, it's important to have a clear idea of how the user interface should respond and what the user would expect.
To begin, consider the task of deleting an e-mail message. When the user clicks on the red X next to a message, the message should be deleted (moved to the Trash). The Back and Forward button are of no use here, because you'd never want to take the user back to a point where the e-mail is still in the list. That means the request() method should be used. Next, should this action cause a user interface change? Yes, the message should disappear from the list. Therefore, you need to use the request() method with the loadAndRender() callback function:
deleteMessage: function (sId) { this.nextNotice = sDeleteMailNotice; this.request("delete", loadAndRender, sId); },
Because you want to delete a specific message, the message ID must be passed into the method. To prepare for the action, the nextNotice property is set to the delete mail notice string. Then, request() is called, passing in the delete string, the loadAndRender() callback function, and the message ID. When the request is completed, the notice is displayed and the user can continue interacting with the application knowing that the message has been moved to the Trash. To restore the message from the Trash, you can use the same methodology.
When the user is viewing the messages in the Trash, a click on the green arrow restores the message (moves it to the Inbox). This is essentially the same as the delete operation; it simply changes where the message is stored. Not surprisingly, the method is very similar:
restoreMessage: function (sId) { this.nextNotice = sRestoreMailNotice; this.request("restore", loadAndRender, sId); },
Once again, this function assigns a notice to be displayed when the request completes and uses request() to restore the message represented by the message ID (sID).
The Trash also has a special action: empty. When the Trash is emptied, all the messages in it are permanently deleted and cannot be recovered. This action is interesting in that it behaves differently depending on what the user is looking at. If the Inbox is being viewed, it's still possible to click the Empty link. In this case, you don't want to change the user interface, aside from letting the user know that the Trash has been emptied. If, on the other hand, the user is viewing the message in the Trash, the user interface should refresh to show that the Trash is empty. Therefore, the emptyTrash() method is a little more involved:
emptyTrash: function () { if (confirm(sEmptyTrashConfirm)) { this.nextNotice = sEmptyTrashNotice; if (this.info.folder == TRASH) { this.request("empty", loadAndRender); } else this.request("empty", execute); } } },
In this method, the first step is to confirm that the user actually wants to empty the Trash. Using the JavaScript confirm() function with sEmptyTrashConfirm presents a dialog box to the user with two options: OK or Cancel. If the user clicks OK, confirm() returns true and the Trash should be emptied. So, the nextNotice property is assigned as with the previous methods. Next, the currently displayed folder is checked. If it's the Trash, request() is called with the loadAndRender() callback function to update the display; if it's not Trash, request() is called with execute() so that the user interface isn't updated.
Thus far, the methods in this section have dealt with performing an action on e-mail messages. The getMessages() method actually is responsible for retrieving the folder information from the server. It accepts the folder ID and the page number to retrieve as arguments and then uses the navigate() method to retrieve the desired information:
getMessages: function (iFolder, iPage) { this.info.folder = iFolder; this.info.page = iPage; this.navigate("getfolder"); },
To retrieve the correct message, the folder and page properties of the info object must be set to the appropriate values. Then, when navigate() is called, the URL will contain the correct folder and page information. This method is then used in both nextPage() and prevPage() to move through the different pages of messages in a given folder:
nextPage: function () { this.getMessages(this.info.folder, this.info.page+1); }, prevPage: function () { this.getMessages(this.info.folder, this.info.page-1); },
Both methods pass in the current folder to getMessages(), but the page argument is different. For nextPage(), the current page number is incremented by one, whereas prevPage() decrements it by one.
There may be times when a user just wants to refresh the information about a folder instead of switching folders. For instance, to check for new mail the user can click the Inbox link while the Inbox is already being displayed. In this case, you don't want to add anything to the browser history because you certainly will never want to go back to an older view of the folder, so you should use request() instead of navigate():
refreshFolder: function (iFolder) { this.info.folder = iFolder; this.info.page = 1; this.request("getfolder", loadAndRender); },
This method is very similar to getMessages() in that the folder ID needs to be passed in and assigned to the info.folder property. The page is set to 1 because any refresh needs to begin with the first page. And because the action requires the user interface to change, the loadAndRender() callback function is passed in when calling request().
Keeping the navigation straight in an Ajax application can be tricky, but thanks to the navigate() method defined earlier, things are much more straight forward. Whenever you need to move from one view of AjaxMail to another, you can simply pass a string to the navigate() method and wait for the action to be completed. To that end, there are four methods that either directly or indirectly make use of the navigate() method to perform their function:
cancelReply: function () { history.go(-1); }, compose: function () { this.navigate("compose"); }, forward: function () { this.navigate("forward"); }, readMessage: function (sId) { this.navigate("getmessage", sId); }, reply: function (blnAll) { this.navigate("reply" + (blnAll ? "all" : "")); },
The first method, cancelReply(), uses the browser's internal history to do its job. When users click Compose Mail, Forward, Reply, or Reply All, the navigate() method is called to put them into compose view. To undo this and move back to the previous view, the history object can be used because the move was recorded in the hidden iframe. Using the go() method with a –1 value moves the browser back to the previous view.
All the other methods in this section simply pass a string value to navigate(), indicating the action that should be taken next. The readMessage() method also accepts the ID of the message to retrieve, and the reply() method accepts a single Boolean argument that indicates whether the action should be reply or replyall; when set to true, it is the latter.
You'll remember from earlier that AjaxMailNavigate.php calls different JavaScript mailbox methods depending on what action has taken place. Each of these methods begins with the word "display," and each has a specific view to initialize.
The displayFolder() method does exactly what it says: it displays a folder of e-mail messages. It accepts a folder info object as its only argument and then renders the folder before setting the processing flag back to false:
displayFolder: function (oInfo) { this.loadInfo(oInfo); this.renderFolder(); this.setProcessing(false); },
A similar method is displayMessage(), which accepts a message information object, loads it, renders the message, and then sets the processing flag to false:
displayMessage: function (oMessage) { this.loadMessage(oMessage); this.renderMessage(); this.setProcessing(false); },
These two methods take care of the Folder view and Read view, respectively. The Compose view is a little bit different because there are so many ways it can be used. It can be used to create a new e-mail, in which case all fields are blank, or it could be used to send a reply, reply all, or forward, in which case different information needs to be pre-filled in the form. To facilitate the different requirements of these user actions, a single method is used:
displayComposeMailForm: function (sTo, sCC, sSubject, sMessage) { this.txtTo.value = sTo; this.txtCC.value = sCC; this.txtSubject.value = sSubject; this.txtMessage.value = sMessage; this.divReadMail.style.display = "none"; this.divComposeMail.style.display = "block"; this.divFolder.style.display = "none"; this.setProcessing(false); },
The displayComposeMailForm() method accepts all the various information that could be assigned to the compose view and places it into the correct fields. Then, divReadMail and divFolder are hidden while divComposeMail is shown. Last, the processing flag is set to true. The displayComposeMailForm() method is not called by AjaxMailNavigate.php, but is instead called by several more specific methods, each catering to a specific action:
displayCompose: function () { this.displayComposeMailForm("", "", "", ""); }, displayForward: function () { this.displayComposeMailForm("", "", "Fwd: " + this.message.subject, "---------- Forwarded message ----------\n" + this.message.message); }, displayReply: function () { var sTo = this.message.from; var sCC = ""; this.displayComposeMailForm(sTo, sCC, "Re: "+ this.message.subject, "\n\n\n\n\n" + this.message.from + "said: \n" + this.message.message); }, displayReplyAll: function () { var sTo = this.message.from + "," + this.message.to; var sCC = this.message.cc; this.displayComposeMailForm(sTo, sCC, "Re: "+ this.message.subject, "\n\n\n\n\n" + this.message.from + "said: \n" + this.message.message); },
The displayCompose() method, which simply displays a blank compose view, passes in an empty string to displayComposeMailForm(). The displayForward() method prepends "Fwd:" to the front of the message subject and "-----Forwarded Message------" to the front of the message from the e-mail that's currently being viewed. Both displayReply() and displayReplyAll() prepend "Re:" in front of the message subject and then include a short string before the current e-mail's body text. The only difference between these two methods is what the pre-filled values of the To and CC fields are. For displayReply(), the To field is simply filled with whoever sent the message initially; for displayReplyAll(), the To field also includes everyone else the e-mail was sent to and the CC field contains the same CC recipients as the original message.
The last section of methods in the mailbox object initializes the properties and data. Earlier you saw the beginnings of the init() method; the next part involves assigning event handlers to various user interface elements:
init: function () { var colAllElements = document.getElementsByTagName("*"); if (!colAllElements.length) { colAllElements = document.all; } for (var i=0; i < colAllElements.length; i++) { if (colAllElements[i].id.length > 0) { this[colAllElements[i].id] = colAllElements[i]; } } this.imgPrev.onclick = function () { oMailbox.prevPage(); }; this.imgNext.onclick = function () { oMailbox.nextPage(); }; this.spnCompose.onclick = function () { oMailbox.compose(); }; this.spnEmpty.onclick = function () { oMailbox.emptyTrash(); }; this.spnReply.onclick = function () { oMailbox.reply(false); }; this.spnReplyAll.onclick = function () { oMailbox.reply(true); }; this.spnForward.onclick = function () { oMailbox.forward(); }; this.spnCancel.onclick = function () { oMailbox.cancelReply(); }; this.spnSend.onclick = function () { oMailbox.sendMail(); }; //more code here },
All the event handlers assigned here simply call a mailbox method that was defined earlier in this chapter. You also need to assign the event handlers for the Inbox and Trash links:
this.spnInbox.onclick = function () { if (oMailbox.info.folder == INBOX) { oMailbox.refreshFolder(INBOX); } else { oMailbox.switchFolder(INBOX); } }; this.spnTrash.onclick = function () { if (oMailbox.info.folder == TRASH) { oMailbox.refreshFolder(TRASH); } else { oMailbox.switchFolder(TRASH); } };
These lines occur where the "more code here" comment is in the previous listing, but are pulled out here for easier explanation. Each of these two links can perform one of two operations: either switching to the folder view or refreshing it. To determine which of these actions to take, each event handler first checks to see what the currently displayed folder is. For the Inbox link, if the current folder is already Inbox, then it calls refreshFolder(); otherwise it calls switchFolder(). The same holds true for the Trash link, except that it checks to see if Trash is the folder already being displayed.
The init() method is actually called by another method called load(), defined as:
load: function () { this.init(); this.getMessages(INBOX, 1); },
This method first initializes the user interface by calling init(), and then makes the initial request for the first page of the Inbox folder using getMessages(). When index.php is loaded, this method must be called (as described later).
To make use of the request() and sendMail() methods of the mailbox object, several callback functions are necessary. These are functions that take over processing once data has been returned from the server. Each of these functions is standalone; that is, they are not methods of the mailbox object.
When the e-mail messages are first downloaded from the server, the data must be loaded into the info property and then rendered:
function loadAndRender(sInfo) { oMailbox.loadInfo(sInfo); oMailbox.renderFolder(); if (oMailbox.nextNotice) { oMailbox.showNotice("info", oMailbox.nextNotice); oMailbox.nextNotice = null; } oMailbox.setProcessing(false); }
The loadAndRender() function expects a JSON string to be passed in as an argument. That data is loaded using the loadInfo() method. Once that happens, the renderFolder() method is called to begin displaying the new information. After that, the function checks to see if there is a notice that needs to be displayed (stored in nextNotice). If so, that notice is displayed and nextNotice is set back to null. The very last step is to set the processing flag to false, indicating that the mailbox is free to make other requests.
The simpler case is when a command has to be executed on the server without returning any information. When the request has completed, you simply want to display any notification that may be waiting and then reset the processing flag back to false. To do so, use the execute() callback function:
function execute(sInfo) { if (oMailbox.nextNotice) { oMailbox.showNotice("info", oMailbox.nextNotice); oMailbox.nextNotice = null; } oMailbox.setProcessing(false); }
Using this callback function instead of loadAndRender() prevents the user interface from updating when the request completes. The action taken is done purely behind the scenes and is indicated only by the notice (if any) that is displayed. As with loadAndRender(), the last step is to set processing back to false.
The last callback function, sendConfirmation(), is used only when sending mail. It expects a simple JSON object to be returned with two properties: error and message. If error is true, an error has occurred and the message property contains an error message to display to the user; otherwise, the mail was sent successfully and message contains a confirmation message to be displayed using showNotice():
function sendConfirmation(sData) { var oResponse = JSON.parse(sData); if (oResponse.error) { alert("An error occurred:\n" + oResponse.message); } else { oMailbox.showNotice("info", oResponse.message); oMailbox.divComposeMail.style.display = "none"; oMailbox.divReadMail.style.display = "none"; oMailbox.divFolder.style.display = "block"; } oMailbox.divComposeMailForm.style.display = "block"; oMailbox.divComposeMailStatus.style.display = "none"; oMailbox.setProcessing(false); }
This function also resets some of the user interface. If the message was sent successfully, it sends the user back to folder view by hiding divComposeMail and divReadMail and then showing divFolder. Regardless of the success, divComposeMailForm has its display property set back to block, whereas divComposeMailStatus has its display property set to none, effectively resetting the compose view.
The "Action Methods" section described methods of the mailbox object that are used to perform specific actions. To facilitate the assigning of event handlers that use these methods, a handful of small functions are used:
function deleteMail() { oMailbox.deleteMessage(this.id); } function restoreMail() { oMailbox.restoreMessage(this.id); } function readMail() { oMailbox.readMessage(this.id.substring(2)); }
Each of these functions simply calls a method of oMailbox and passes in some identifier. Because these functions are used as event handlers, the this object points to the element upon which the event handler has been assigned. (You could also use event.srcElement in Internet Explorer or event.target in DOM-compliant browsers.) For deleteMail() and restoreMail(), the ID of the element is equivalent to a message ID, so it can be passed directly into the deleteMessage() and restoreMessage() methods, respectively. The readMail() function is applied to a table row whose ID is in the format "trID", so the first two character must be stripped off using the substring() method before being passed into readMessage().
Important |
By defining these functions globally, you avoid using closures to assign event handlers. Closures are a manner in which it's possible to define a function that makes use of variables defined outside of it. They also happen to be the main cause of memory leaks in many web browsers. Whenever possible, it is preferable to create standalone functions to use as event handlers. |