This is the source code for the "Live Comments" AJAX application as implemented on gmail dot net. This implementation uses a database to persist user comments and email addresses. The server-side scripting language is PHP and the database is MySQL.
You can use a different backend in combination with the AJAX javascript client-side application listed below. Also included is the CSS that is used for layout and styling Live Comments look-and-feel.
The software is released under the MIT license (i.e., basically, you can do what you want with it!).
First, create the Comments DB and associated tables; ActiveUsers, Comments, and Email.
Next, copy the Comments.php file to a directory accessible by your web server. Let's assume that directory is called webroot/www.
At the same level as www, create another directory for the include files called inc; e.g. webroot/inc. Copy DBLayer.php and Constants.php into this directory.
Now create Css and Js directories under www and copy Comments.css and comments.js into their respective directories.
Next, create CG (common graphics) under www and copy bl.gif and br.gif into the directory.
Your directory should look like:
webroot/www
Comments.php
index.html (see Example.html)
webroot/www/Js
comments.js
webroot/www/Css
Comments.css
WebSite.css (see Example.html)
webroot/www/CG
bl.gif
br.gif
webroot/inc
DBLayer.php
Constants.php
Now, create an HTML file and place in the www directory.
Within the HTML file, where Live Comments is to be added,
add the following to the <head> section
(see Example at bottom of page):
<link rel="stylesheet" href="Css/Comments.css"></link>
<script type="text/javascript" src="Js/comments.js"></script>
In the body of your HTML, add the following div
(note: the id must be "comments"):
<div id="comments" class="invisible"> </div>
Finally, add an onload to the body tag as follows:
<body onload="initComments()">
That should be it! The only difficult part is
implementing your persistence layer. Once implemented
you should be good to go.
Create database and associated tables.
#
# Script to create Live Comments DB and associated tables.
#
DROP DATABASE IF EXISTS `CommentsDB`;
DROP TABLE IF EXISTS `CommentsDB`.`ActiveUsers`;
DROP TABLE IF EXISTS `CommentsDB`.`Email`;
DROP TABLE IF EXISTS `CommentsDB`.`Comments`;
CREATE DATABASE `CommentsDB`;
USE `CommentsDB`;
CREATE TABLE `ActiveUsers` (
`IP` varchar(45) NOT NULL default '',
`TimeStamp` int(10) unsigned NOT NULL default '0',
PRIMARY KEY (`IP`),
KEY `TimeStamp` (`TimeStamp`)
) TYPE=INNODB COMMENT='Used to determine number of active users';
CREATE TABLE `Comments` (
`ID` int(11) NOT NULL auto_increment,
`Name` varchar(32) NOT NULL default '',
`Comment` varchar(255) NOT NULL default '',
`TimeStamp` timestamp(14) NOT NULL,
`IP` varchar(24) NOT NULL default '',
`State` char(1) NOT NULL default 'A',
PRIMARY KEY (`ID`),
KEY `TimeStamp` (`TimeStamp`)
) TYPE=INNODB COMMENT='Gmail Comments';
CREATE TABLE `Email` (
`ID` int(11) NOT NULL auto_increment,
`CID` int(11) NOT NULL default '0',
`Email` varchar(96) NOT NULL default '',
PRIMARY KEY (`ID`),
KEY `CID` (`CID`),
FOREIGN KEY (`CID`) REFERENCES Comments(`ID`) ON DELETE CASCADE
) TYPE=INNODB COMMENT='Poster''s email address (optional)';
This PHP script uses DBLayer.php (see below) as the DB abstraction layer. The DB related code gets included via the include_once statements at the top of the file. You can implement a different abstraction layer. Just change the function query($sql) to implement your persistance model. This file's name is: Comments.php.
<?php
/*===================================================================*
MIT License
===========
Copyright (c) 2005 Jerry Anders. All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*===================================================================*/
/**
* This is the Live Comments server-side script. It handles posting of
* new comments and also getting comments to display in the web
* browser. It always returns an xml string of the form:
*
* "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
* "<results post=\"False\" version=\"2.0.1\" ts=\"1150347196\" numUsers=\"7\">
* "<comment>"
* "<auth>joe</auth>"
* "<email>joe@gmail.net</email>"
* "<text>Four score and seven years ago...</text>"
* "<ts>1137376197</ts>"
* "</comment>"
* "<comment>"
* "<auth>betty</auth>"
* "<email>betty@gmail.net</email>"
* "<text>Fe Fi Fo Fum...</text>"
* "<ts>1137375705</ts>"
* "</comment>"
* ...
* "</results>";
*
* Send comments to: janders (@) gmail (.) net
*
*/
// Find base file path on the server
@define('DIR', dirname(__FILE__) . '/../inc');
// get DB related code
include_once DIR . '/Constants.php';
include_once DIR . '/DBLayer.php';
// process AJAX post/request (xmlhttprequest)
$method = $_POST['method'];
$auth = ($_POST['auth']) ? trim($_POST['auth']) : "Anonymous";
$email = ($_POST['email']) ? strtolower(substr(trim($_POST['email']), 0, 96)) : "";
$comment = ($_POST['comment']) ? trim($_POST['comment']) : "";
$ts = ($_POST['ts']) ? $_POST['ts'] : (($_GET['ts']) ? $_GET['ts'] : 0);
$ip = substr($_SERVER['REMOTE_ADDR'],0, 24);
switch ($method) {
case "post":
$post=1;
if (!empty($comment) && validatePoster()) {
postComment();
$xml = getComments();
} else {
$numUsers = calcActiveUsers();
$error = True;
$xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?><results post=\"True\" version=\"" . VERSION_NO . "\" ts=\"$ts\" numUsers=\"$numUsers\" error=\"$error\"/>";
}
break;
case "get":
default:
$post=0;
$xml = getComments();
break;
}
// http header sent back to browser
header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Content-Type: text/xml"); // very important for xmlhttp so that we can use responseXML in JavaScript
// send xml string back to browser
print($xml);
exit;
// script functions
// really simple submit validation...
// if same user has posted 10 comments in
// a row, reject them until someone else posts a comment
function validatePoster()
{
global $ip;
$DB = new DB;
$DB->connect();
$sql = "SELECT IP FROM Comments WHERE 1 ORDER BY TimeStamp DESC LIMIT 10";
$DB->query($sql);
if ($DB->rs === false) return false;
$result = $DB->rsArray;
$DB->close();
if (!empty($result) && count($result) == LIMIT_REPEAT_POSTS) {
foreach ($result as $row) {
if ($ip != $row["IP"]) return true;
}
return false;
}
// must be an empty db
return true;
}
// use ip to approximate number of active users -- using cookies would
// be more accurate, since ip can be masked by proxy servers
function calcActiveUsers()
{
global $ip;
$now = time(); // now
$DB = new DB;
$DB->connect();
$sql = "SELECT * FROM ActiveUsers WHERE IP = '$ip'";
$DB->query($sql);
if ($DB->rs === false || empty($DB->rsArray)) {
// this must be a new user, add to ActiveUsers
// to track number of users viewing the web page
$sql = "INSERT INTO ActiveUsers (IP, TimeStamp) VALUES('$ip', '$now')";
} else {
// update users timestamp
$sql = "UPDATE ActiveUsers SET TimeStamp = '$now' WHERE IP = '$ip' ";
}
$DB->query($sql);
// flush old users... if user hasn't hit the server
// in the last minute, consider that they are no
// longer looking at Live Comments
$et = time() - 60; // elapsed time: now - 1 minute
$sql = "DELETE FROM ActiveUsers WHERE TimeStamp < $et";
$DB->query($sql);
$sql = "SELECT * FROM ActiveUsers WHERE 1";
$DB->query($sql);
$DB->close();
if ($DB->rs === false) {
return 0;
} else {
return count($DB->rsArray);
}
}
// retrieve last 100 comments from CommentsDB
function getComments()
{
global $ts, $ip, $post;
$numUsers = calcActiveUsers();
// as the DB grows over time, keep things running fast on
// initial page load by grabbing only one week's worth of data
// note: 'YmdHis' is the MySQL 4 timestamp format -- e.g., 20051206152942
// note: 'Y-m-d H:i:s' is the MySQL 4.1, 5+ timestamp format -- e.g., 2005-12-06 15:29:42
// so use UNIX_TIMESTAMP to do comparison
//$tt = date('YmdHis',strtotime('-1 weeks'));
$tt = strtotime('-1 weeks'); // looks like: 1144919156
$DB = new DB;
$DB->connect();
$sql = "SELECT C.ID, C.Name, E.Email, C.Comment, UNIX_TIMESTAMP(C.TimeStamp) AS TS
FROM Comments AS C
LEFT JOIN Email AS E ON E.CID = C.ID
WHERE C.State = 'A' AND UNIX_TIMESTAMP(C.TimeStamp) > $tt AND UNIX_TIMESTAMP(C.TimeStamp) > $ts
ORDER BY C.ID DESC LIMIT 100";
$DB->query($sql);
$result = $DB->rsArray;
$DB->close();
if ($DB->rs === false || empty($result)) {
$xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?><results post=\"$post\" version=\"" . VERSION_NO . "\" ts=\"$ts\" numUsers=\"$numUsers\"/>";
} else {
// timestamp of latest post
$ts = $result[0]["TS"];
$xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" .
"<results post=\"$post\" version=\"" . VERSION_NO . "\" ts=\"$ts\" numUsers=\"$numUsers\">";
foreach ($result as $row) {
$xml .= "<comment>" .
"<auth>" . HTMLSpecialChars($row["Name"]) . "</auth>" .
"<email>" . HTMLSpecialChars($row["Email"]) . "</email>" .
"<text>" . HTMLSpecialChars($row["Comment"]) . "</text>" .
"<ts>" . $row["TS"] . "</ts>" .
"</comment>";
}
$xml .= "</results>";
}
return $xml;
}
// post user comments to DB and store email if submitted
// post comments to Comments table and valid email to Email table
function postComment()
{
global $auth, $email, $comment, $ip;
// Filter profanity
$comment = filter($comment);
$auth = filter($auth);
//$email = filter($email); // this doesn't work since filter() introduces
// illegal email characters -- need different solution
// make sure field lengths don't exceed max length of DB tables
$auth = strtolower(substr($auth, 0, 32));
$comment = substr($comment, 0, 255);
$DB = new DB;
$DB->connect();
$sql = "INSERT INTO Comments (Name, Comment, IP) VALUES('$auth', '$comment', '$ip')";
$DB->query($sql);
if ($DB->rs === false) {
$DB->close();
return false;
} else if (validateEmail()) {
$cid = $DB->getInsertId();
$sql = "INSERT INTO Email (CID, Email) VALUES('$cid', '$email')";
$DB->query($sql);
$DB->close();
}
return true;
}
// Make sure email is of valid form before saving
// simple email syntax validation
function validateEmail()
{
global $email;
// Create the syntactical validation regular expression
$regexp = "^([_a-z0-9-]+)(\.[_a-z0-9-]+)*@([a-z0-9-]+)(\.[a-z0-9-]+)*(\.[a-z]{2,4})$";
// Validate the syntax
if (eregi($regexp, $email)) {
return true;
}
return false;
}
// Filter profanity and other junk
function filter($s)
{
// Words.txt is a simple 2 column file. Column 1 is the word to be replaced
// and column 2 is the replacement. A line begining with # is a comment line.
// Example:
// bitch girl
// bastard boy
// # a comment line
if (file_exists("Words.txt")) {
// Get file into an array
$lines = file('Words.txt');
// Loop through array
foreach ($lines as $line_num => $line) {
$line = trim($line);
if (preg_match("/^#/", $line)) continue; // a comment line
if (preg_match("/^$/", $line)) continue; // a blank line
list($p, $r) = split(" ", $line, 2);
$s = preg_replace("/$p/i", "$r", $s);
}
}
return $s;
}
?>
Constants.php holds constants and sensitive data for the application. It gets included via the include_once statement at the top of Comments.php. This file's name is: Constants.php.
<?php
/*===================================================================*
MIT License
===========
Copyright (c) 2005 Jerry Anders. All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*===================================================================*/
/**
* This is a place holder for common constants that are used within
* the application.
*
*/
// Version ----------------------------------------------------------------
/** @const VERSION_NO The version number as displayed on the UI. */
// The version mnumber can be manually set by changing N.N.N to
// someting like 1.4.3, or it can be set during build process using
// sed or other editor that can edit this file on the fly
define('VERSION_NO', '3.0.4'); // changing this version number on the
// server will cause Live Comments
// client to refresh the page and
// thereby reload Js and Css files
// -- this is cool.
// Directories ------------------------------------------------------------
list($baseDir, $dir) = split("inc", dirname(__FILE__));
/** @const DIR The base directory. */
define('DIR', $baseDir);
/** @const DIR_WWW The base directory to the Web files. */
define('DIR_WWW', $baseDir . 'www');
/** @const DIR_INC The base directory to the Include files. */
define('DIR_INC', $baseDir . 'inc');
// Databases -------------------------------------------------------------
// Never keep passwords and other sensitive data in a directory accessible
// by the web server; e.g. /webroot/www. Make sure that they are in the
// include (inc) directory or other directory below www.
define('DB_PORT', '3306');
define('DB_URL', 'localhost');
define('DB_SOCKET', '/var/lib/mysql/mysql.sock');
define('DB_USER', 'comments');
define('DB_USER_PWD', 'password');
define('DB_NAME', 'CommentsDB');
// Other -----------------------------------------------------------------
define('LIMIT_REPEAT_POSTS', '10');
The DB class found in DBLayer.php also gets included into Comments.php using an include_once statement. This provides and abstract layer to the mysql database. Your implementation may vary. Just implement your own persistance model. This file's name is: DBLayer.php.
<?php
/*===================================================================*
MIT License
===========
Copyright (c) 2005 Jerry Anders. All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*===================================================================*/
/**
* Database abstraction layer
*/
class DB {
// public vars
// default setting (see Constants.php)
var $url = DB_URL;
var $port = DB_PORT;
var $socket = DB_SOCKET;
var $userName = DB_USER;
var $userPwd = DB_USER_PWD;
var $dbName = DB_NAME;
// holds query results (record set)
var $rs = false;
// rs as an array
var $rsArray = Array();
// private vars
// DB link ID
var $_id;
// Constructor
function DB () {}
function connect()
{
// Connect to MySQL
$hostURL = $this->url . ":" . $this->port . ":" . $this->socket;
$this->_id = mysql_pconnect($hostURL, $this->userName, $this->userPwd);
if (!$this->_id) {
die("Could not connect: " . mysql_error());
}
// Select assigned DB
if (!mysql_select_db($this->dbName, $this->_id)) {
die("Could not connect to DB: " . mysql_error());
}
}
// Close connection
function disconnect ()
{
if (!mysql_close($this->_id)) {
die("Could not close DB");
}
}
// pseudonym for disconnect
function close()
{
$this->disconnect();
}
// Query db
function query ($sql)
{
$this->rs = mysql_query($sql);
$this->rsArray = ''; // clear old values
while ($row = @mysql_fetch_assoc($this->rs)) {
$this->rsArray[] = $row;
}
return $this->rs;
}
// get last inserted id
function getInsertId()
{
return mysql_insert_id($this->_id);
}
// get number of rows for results
function getNumRows()
{
mysql_num_rows($this->result);
}
}
?>
This is the client-side script that implements the Comments application using AJAX. This file's name is: comments.js.
/*===================================================================*
MIT License
===========
Copyright (c) 2005 Jerry Anders. All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*===================================================================*/
/**
* xhr namespace
*/
if (typeof(xhr) == "undefined") xhr = {};
/**
* This class is used to manage page comments using AJAX (asynchronous
* javascript and xml). There are also a set of helper functions that
* manage instantiation and page related html to create the dynamic
* look-and-feel.
*
* Send comments to: janders (@) gmail (.) net
*
*/
xhr.Comments = function()
{
// public vars
this.interval = 60000; // get interval (60 seconds)
this.url = "Comments.php"; // pointer to server page to process AJAX request
this.divComments; // holds entire comments html string
this.comment;
// private global vars used by class methods
var xmlhttp; // holds xmlhttprequest object
var url; // holds this.url
var ts = 0; // timestamp sent back from server
var version; // LC version sent back from server
var div; // to get the callback to work need this for mozilla, et al.
var divUsers; // div to display number of active users
var nUsers = 0; // number of active users
var auth; // temp placeholder for inputAuth value
var email; // temp placeholder for inputEmail value
var comment; // temp placeholder for textarea value
var inputAuth; // author html input field
var inputEmail; // email html input field
var textareaComment; // comment textarea
var inputCounter; // counter html input field
// set the xmlhttprequest object
if (window.XMLHttpRequest) {
try {
xmlhttp = new XMLHttpRequest();
} catch (e) {
xmlhttp = false;
}
} else if (window.ActiveXObject) {
try {
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
xmlhttp = false;
}
}
}
// privileged methods
// is this browser capable of xmlHttpRequest?
this.isBrowserXmlHttpCapable = function()
{
return (xmlhttp) ? true : false;
}
// set div where comments are to be displayed
this.setCommentsDiv = function(d)
{
if (typeof(d) == "undefined") {
d = "comments";
}
this.divComments = document.getElementById(d);
if (!xmlhttp) {
return false;
} else if (this.divComments) {
this.divComments.innerHTML = this.innerHTML; // set div contents
this.divComments.className = "visible"; // make comments UI visible if divComments exists
// set vars used by these functions
inputAuth = document.getElementById("name");
inputEmail = document.getElementById("email");
textareaComment = document.getElementById("ctext");
inputCounter = document.getElementById("count");
// div where comments are to be displayed
div = document.getElementById("entries");
// div where number of active users are to be displayed
divUsers = document.getElementById("users");
} else {
alert('Comments class requires that \n\n <div id="comments" class="invisible"></div>\n\n be defined in the HTML file.');
}
}
// Get comments from the server
this.getComments = function()
{
url = this.url;
_sendRequest("get");
}
// Post comments to server
this.postComment = function()
{
if (!xmlhttp) return;
// set params for posting comments
auth = escape(_validChars(inputAuth.value, 32));
email = escape(_validChars(inputEmail.value, 96));
if (this.comment) {
comment = escape(_validChars(this.comment, 255));
this.comment = "";
} else {
comment = escape(_validChars(textareaComment.value, 255));
}
// leave name and email intact, clear text, reset counter and refocus on textarea for next comment
textareaComment.value = "";
inputCounter.value = 255;
textareaComment.focus(); // $todo: doesn't work in Safari if user tabbed from email field into the textarea field.
// if comment contains text then post it, otherwise do nothing
url = this.url;
if (comment) _sendRequest("post");
}
// private methods
var _sendRequest = function(m)
{
xmlhttp.abort(); // this seems to fix uncaught exception in firefox
xmlhttp.open("POST", url, true);
xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlhttp.setRequestHeader("Accept","text/xml");
xmlhttp.onreadystatechange = _commentCallBack;
switch (m) {
case "post":
xmlhttp.send("method=post&ts="+ts+"&auth="+auth+"&email="+email+"&comment="+comment);
break;
case "get":
default:
xmlhttp.send("method=get&ts="+ts);
break;
}
}
// Server results are processed here.
var _commentCallBack = function()
{
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
var comment = "";
var xml = xmlhttp.responseXML;
if (xml != null && xml.getElementsByTagName("results").item(0) != null) {
var a = xml.getElementsByTagName("results").item(0);
// display version, if new version cause page to
// reload which, as a side effect, will reload the
// latest Js and Css files on to the client -- see
// Constants.php for version number
if (typeof(version) == "undefined") {
version = a.getAttribute("version");
document.getElementById('version').innerHTML = a.getAttribute("version");
// do not reload if version is same or if user has typed content in to textarea
} else if (version != a.getAttribute("version") && !textareaComment.value) {
location.reload(true);
}
// update timestamp
ts = a.getAttribute("ts");
// set number of active users viewing this page -- for visual effect add delay
if (nUsers != a.getAttribute("numUsers")) {
nUsers = a.getAttribute("numUsers");
_updateActiveUsers(nUsers);
}
var comments = xml.getElementsByTagName("comment");
var html = _updateCommentList(comments);
if (html) {
div.innerHTML = html + div.innerHTML;
}
// Alert user to max posts
if (a.getAttribute("error")) {
alert("You can only post 10 comments in a row.\n\nYou will be able to post again, once another\nuser posts a comment.");
}
}
}
}
// Update the display of active users
var _updateActiveUsers = function(n)
{
divUsers.innerHTML = " ";
if (n <= 1) {
str = "Currently you are the only viewer...";
} else {
str = "Currently " + n + " people are viewing this page...";
}
setTimeout(function(){divUsers.innerHTML = str},1000);
}
// creates html containing comments
var _updateCommentList = function(comments)
{
var comment = "";
for (i=0; i<comments.length; i++) {
var auth = "anonymous" // $todo: need to figure out how auth can get set to a null string on post, in the meantime this fixes the problem
var email = "";
var n = comments.item(i).childNodes;
if (n.item(0).childNodes.item(0)) {
auth = n.item(0).childNodes.item(0).nodeValue; // $todo: need to figure out how auth can get set to a null string on post, in the meantime this fixes the problem
}
if (n.item(1).childNodes.item(0)) {
email = n.item(1).childNodes.item(0).nodeValue;
}
var text = n.item(2).childNodes.item(0).nodeValue;
var timestamp = n.item(3).childNodes.item(0).nodeValue;
var d = new Date(timestamp*1000); //* 1000 to convert to milliseconds
var date = d.getMonth()+1 + "/" + d.getDate() + "/" + d.getFullYear();
var time = d.toLocaleTimeString();
comment +=
"<div class='by'>" +
" <span class='auth small'>" + auth + "</span>" +
" <span class='date small'>" + date + "</span>" +
" <span class='date small'>" + time + "</span>" +
"</div>";
if (email) {
comment += "<div class='eml small'>" + _obfuscateEmail(email) + "</div>";
}
comment += "<div><div class='para'>" + text + "</div>";
comment += "</div>";
}
return comment;
}
// returns string of only valid characters
var _validChars = function(str, l)
{
// default legal character set is alphanumeric + symbols (note first char is a space)
var validChars = " abcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()_-+=[]{}\\|;:'\",.<>/?";
//remove leading and trailing whitespace
str = str.replace(/^\s+/g, '').replace(/\s+$/g, '');
//convert carriage returns and newlines entered into textarea to spaces
str = str.replace(/\r\n|\n/g, ' ');
// replace <, >, and &
str = str.replace(/</g, "<");
str = str.replace(/>/g, ">");
str = str.replace(/&/g, "&");
var valid = "";
for (var i=0; i < str.length; i++) {
var c = str.charAt(i);
var s = c.toLowerCase();
if (validChars.indexOf(s) == -1) continue;
valid += c;
}
if (typeof(l) != "undefined") {
valid = valid.substr(0, l);
// there is the possibility that once we trim the string we
// may have chopped off part of & to create something
// like &am
// e.g. a name that was entered as <b>abc</b> and is limited
// to 32 characters would break things because it would get
// stored as:
// &lt;b&gt;abc&lt;/b&a
// i.e., the &a on the end breaks things.
// so let's check for and fix such situations
valid = valid.replace(/(&$|&a$|&am$|&$)/, '');
}
return valid;
}
// obfuscate user's email address for display
// note: validation is handled by server-side php code on submit
var _obfuscateEmail = function(e)
{
var m = e.match(/@/g);
if (m == null || m.length != 1) return false;
var r1 = Math.round(Math.random()*3); // random number between 0 and 3
var r2 = Math.round(Math.random()*1); // random number between 0 and 1
var r3 = Math.round(Math.random()*1); // random number between 0 and 1
switch (r1) {
case 0:
e = (r2) ? e.replace(/@/, " (@) ") : e.replace(/@/, " (at) ");
e = (r3) ? e.replace(/\./g, " (.) ") : e.replace(/\./g, " (dot) ");
break;
case 1:
e = (r2) ? e.replace(/@/, " {@} ") : e.replace(/@/, " {at} ");
e = (r3) ? e.replace(/\./g, " {.} ") : e.replace(/\./g, " {dot} ");
break;
case 2:
e = (r2) ? e.replace(/@/, " [@] ") : e.replace(/@/, " [at] ");
e = (r3) ? e.replace(/\./g, " [.] ") : e.replace(/\./g, " [dot] ");
break;
case 3:
e = (r2) ? e.replace(/@/, " @ ") : e.replace(/@/, " at ");
e = (r3) ? e.replace(/\./g, " . ") : e.replace(/\./g, " dot ");
break;
}
return e;
}
}
xhr.Comments.prototype =
{
// public vars and methods
innerHTML:
"<div id='lc'>" +
" <div id='titleCap'><span id='title'>Live Comments</span><span id='version'></span></div>" +
" <div class='cap'>" +
" <span>Name <span class='xx-small'>(optional)</span>:</span> <input tabindex='1' id='name' class='name' maxlength='16' onmouseover=\"this.className='namef'\" onfocus=\"this.className='namef';this.infocus=true\" onmouseout=\"if(!this.infocus)this.className='name'\" onblur=\"this.className='name';this.infocus=false\"> " +
" </div>" +
" <div class='cap'>" +
" <span>Email <span class='xx-small'>(optional)</span>:</span> <input tabindex='2' id='email' class='email' maxlength='96' onmouseover=\"this.className='emailf'\" onfocus=\"this.className='emailf';this.infocus=true\" onmouseout=\"if(!this.infocus)this.className='email'\" onblur=\"this.className='email';this.infocus=false\">" +
" </div>" +
" <div class='cap'>" +
" <span>Comment (plain text only)<span id='counter'><input id='count' disabled='disabled' value='255' maxlength='3'/><span class='xx-small'> characters left</span></span>:</span>" +
" </div>" +
" <div>" +
" <textarea tabindex='3' id='ctext' class='ctext' onKeyDown=\"xhr.Comments.prototype.limitText(this, document.getElementById('count'), 255)\" onKeyUp=\"xhr.Comments.prototype.limitText(this, document.getElementById('count'), 255)\" onmouseover=\"this.className='ctextf'\" onfocus=\"this.className='ctextf';this.infocus=true\" onmouseout=\"if(!this.infocus)className='ctext'\" onblur=\"className='ctext';this.infocus=false\"></textarea>" +
" </div>" +
" <div id='submit'>" +
" <div style='float:right'>" +
" <input tabindex='4' type='submit' value='submit' class='x-small' title='Post your comment' onclick='submitComment()'/>" +
" </div>" +
" </div>" +
" <div id='users'> </div>" +
" <div id='entries'></div>" +
" <span id='so'> </span>" +
"</div>",
// every object has a toString, so what is this class?
toString:function()
{
return "Class to manage page comments using AJAX.";
},
// limit number of characters that can be typed in comment textarea
limitText:function(textField, limitField, limit)
{
if (textField.value.length > limit) {
textField.value = textField.value.substring(0, limit);
} else {
limitField.value = limit - textField.value.length;
}
},
// special UI expand routine (more/less)
more:function(o)
{
o.blur();
o.firstChild.src = "CG/bl.gif";
o.title = "less";
var self = this;
o.onclick = function() {self.less(this)};
var p = o.parentNode;
p.childNodes[0].className = "invisible";
p.childNodes[1].className = "visible";
},
// reset more
less:function(o)
{
o.blur();
o.firstChild.src = "CG/br.gif";
o.title = "more";
var self = this;
o.onclick = function() {self.more(this)};
var p = o.parentNode;
p.childNodes[0].className = "visible";
p.childNodes[1].className = "invisible";
}
}
/**
* General helper functions that use Comments class.
*
* @author janders
*
*/
// initalizes Comments class and sets instance and global vars
function initComments()
{
_c_ = new xhr.Comments;
_c_.setCommentsDiv();
if (_c_.divComments && _c_.isBrowserXmlHttpCapable()) {
_c_.interval = 10000; // override default 60 second interval and set to 10 seconds
getComments(); // start recursive loop watching for new comments
} else if (_c_.divComments) {
_c_.divComments.className = "visible";
}
}
// loop forever getting any new comments on server
function getComments()
{
_c_.getComments(); // use AJAX to get comments on server
setTimeout(getComments,_c_.interval); // recursive loop
}
// submit comment using AJAX
function submitComment()
{
_c_.postComment(); // use AJAX to post comment to server
}
/**
* General helper functions that manage UI.
*
* @author janders
*
*/
// simple, fun rating system
function rateit(o)
{
o.blur();
var rating = null;
var p = o.parentNode;
var r = p.getElementsByTagName("input");
for (var i=0; i<r.length; i++) {
if (r.item(i).checked) {
rating = r.item(i).parentNode.title;
r.item(i).checked = false;
break;
}
}
if (rating == null) return; // nothing selected
_c_.comment = rating;
_c_.postComment();
document.getElementById("rating").style.display = "none";
}
This is the CSS used to format Comments layout and style. This file's name is: Comments.css.
/* CSS for Live Comments */
/* container for live comments */
#lc {
margin:0;
padding:0px 12px 2px 12px;
width:400px; /* change this width to change app's width */
font-family: 'Lucidia Grande', Verdana, Arial, Sans-Serif;
color:#001650;
}
/* general settings for form fields */
#lc input, #lc textarea {
margin:0;
padding:0;
cursor:pointer;
color:#001650;
}
/* Live Comments title and version*/
#lc #titleCap {
color:#5066a0;
}
#lc #titleCap #title {
padding:0 0 8px 0;
font-size:large;
letter-spacing:6px;
font-weight:bold;
}
/* Live Comments version */
#lc #titleCap #version {
padding:0 0 0 8px;
vertical-align:top;
font-size:xx-small;
}
/* form field captions */
#lc .cap {
padding:8px 0 0 0;
font-size:x-small;
}
/* name form input field out of focus (i.e. onblur) */
#lc .name {
padding:1px;
width:143px;
height:12px;
font-size:x-small;
border-width:0 0 1px 0;
border-style:solid;
border-color:#5066a0;
background-color:transparent;
}
/* name form input field in focus (i.e. onfocus) */
#lc .namef {
padding:1px;
width:143px;
height:12px;
font-size:x-small;
border-width:0 0 1px 0;
border-style:solid;
border-color:#d28a2c;
background-color:#ffffff;
}
/* email form input field out of focus */
#lc .email {
padding:1px;
width:225px;
height:12px;
font-size:x-small;
border-width:0 0 1px 0;
border-style:solid;
border-color:#5066a0;
background-color:transparent;
}
/* email form input field in focus */
#lc .emailf {
padding:1px;
width:225px;
height:12px;
font-size:x-small;
border-width:0 0 1px 0;
border-style:solid;
border-color:#d28a2c;
background-color:#ffffff;
}
/* comment form textarea out of focus */
#lc .ctext {
margin:4px 0 4px 0;
padding:1px;
width:100%;
height:65px;
font-size:x-small;
border-width:1px;
border-style:solid;
border-color:#5066a0;
background-color:#c5ee62;
background-color:transparent;
}
/* comment form textarea in focus */
#lc .ctextf {
margin:4px 0 4px 0;
padding:1px;
width:100%;
height:65px;
font-size:x-small;
border-width:1px;
border-style:solid;
border-color:#d28a2c;
background-color:#ffffff;
}
/* counter text below comment textarea */
#lc #counter {
margin:0 0 0 0;
font-size:x-small;
}
#lc #count {
padding:0 0 2px 0;
width:30px;
font-size:x-small;
text-align:right;
border-width:0;
background-color:transparent;
}
/* submit link below comment textarea */
#lc #submit {
margin:2px 2px 40px 2px;
text-align:right;
font-size:x-small;
cursor:pointer;
}
/* active users */
#lc #users {
margin:0 0 2px 0;
width:100%;
font-size:x-small;
letter-spacing:2px;
}
/* entries -- where user comments are displayed */
#lc #entries {
padding:12px 2px 2px 2px;
width:100%;
font-size:medium;
overflow:hidden;
color:#5066a0;
border-width:3px 0 0 0;
border-style:solid;
border-color:#5066a0;
}
/* more and less icons */
#lc #entries img {
width:14px;
height:9px;
border:0px;
}
/* comment */
#lc #entries .para {
margin:0 3px 0 0;
padding:0 0 36px 0;
border-width:1px 0 0 0;
border-style:dashed;
border-color:#5066a0;
}
/* the by line, including auth and date */
#lc #entries .by {
margin:0 3px 0 0;
font-family: arial;
}
/* who wrote the comment */
#lc #entries .auth {
margin:0 4px 0 0;
color:#d28a2c;
}
/* comment date listed with author */
#lc #entries .date {
}
/* email line if present */
#lc #entries .eml {
margin:0 3px 2px 0;
}
/* general css for comments */
#lc a {
cursor:pointer;
color:#5066a0;
border-width:0;
}
#lc a:hover {
cursor:pointer;
color:#d28a2c;
}
#lc a:visited {
cursor:pointer;
}
#lc .pointer {
cursor:pointer;
}
#lc .small {
font-size:small;
}
#lc .x-small {
font-size:x-small;
}
#lc .xx-small {
font-size:xx-small;
}
#lc .medium {
font-size:medium;
}
#lc .large {
font-size:large;
}
#lc .x-large {
font-size:x-large;
}
#lc .xx-large {
font-size:xx-large;
}
#lc .graycol {
color:#bebebe;
}
#lc .darkgraycol {
color:#666666;
}
#lc .hlc {
color:#d28a2c;
}
#lc .invisible {
display:none;
visibility:hidden;
}
#lc .visible {
display:inline;
visibility:visible;
}
These are the images used as the more / less icons as defined in comments.js. To download images, click right mouse button and select to save image file.
Now let's pull it all together. Following is a template html file that you can use as an example. This file's name is: Example.html.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Gmail dot Net: AJAX Example</title>
<!-- Changed by: Jerry Anders, 11-Feb-2006 -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="description" content="Live Comments: An example AJAX application by Jerry Anders">
<meta name="keywords" content="Jerry Anders, live comments, comments, css, javascript, xhtml, xml, ajax, ajax application, web-based">
<link rel="stylesheet" href="Css/Example.css">
<link rel="stylesheet" href="Css/Comments.css">
<script type="text/javascript" src="Js/comments.js">
<script type="text/javascript">
// Disable comments so that they do not work on this page.
// To do so we need to override getComments and submitComments
// which are helper functions defined in comments.js.
// To use this file as a template file for your own
// application, remove this <script ... /script>
// section from this page.
function getComments()
{
document.getElementById("entries").innerHTML = "Comments are disabled on this page.";
}
function submitComment()
{
// do nothing
}
</script>
</head>
<body onload="initComments()">
<!-- Since this is using the same css as gmail.net, I
need to override page width as set in WebSite.css
to get 3 columns to fit in the page.
-->
<div id="page" style="width:812px">
<div class="b hlcol x-large ls5">EXAMPLE</div>
<!-- left column -->
<div id="left">
<div class="sb">
<div class="b hlcol small ls3">FAQ:</div>
<br>
<b>Q:</b> Can I use this as a template for my site?
<br>
<b>A:</b> Yes.
<br><br><br>
<b>Q:</b> Does this application work for all browsers?
<br>
<b>A:</b> It does for all modern browsers.
<br><br><br>
<b>Q:</b> Where do I get the software?
<br>
<b>A:</b> <a href="http://www.gmail.net/GetSource.php">Live Comments</a>.
<br><br><br>
<b>Q:</b> Do I need anything else?
<br>
<b>A:</b> No, but if you want to use this example
file you will need
<a href="http://www.gmail.net/Css/Example.css">Example.css</a>.
This Css file is not published on Live Comments
Source page but is required for Example.html's
layout.
</div>
<div class="sb">
<div class="b hlcol small ls3">LEFT 2:</div>
<br>
Add your text here to create another column entry.
</div>
</div>
<!-- center column -->
<div id="center">
<div class="sb">
<!-- comments application populates this div -->
<div id="comments" class="invisible">
<!-- this gets replaced by live comments for browsers that support xmlhttprequest -->
<div>If you see this, your Browser is old! Upgrade to a new browser that supports XMLHttpRequest.</div>
</div>
</div>
</div>
<!-- right column -->
<div id="right">
<div class="sb">
<div class="b hlcol small ls3">RIGHT 1:</div>
<br>
Add your text here to create another column entry.
<br>
Add your text here to create another column entry.
<br>
Add your text here to create another column entry.
</div>
<div class="sb">
<div class="b hlcol small ls3">RIGHT 2:</div>
<br>
Add your text here to create another column entry.
</div>
<div class="sb">
<div class="b hlcol small ls3">RIGHT 3:</div>
<br>
Add your text here to create another column entry.
</div>
<div class="sb">
<div class="b hlcol small ls3">RIGHT 4:</div>
<br>
Add your text here to create another column entry.
</div>
</div>
<!-- bottom -->
<div style="clear:both"></div>
<div id="footer">
<span class="darkgraycol">Copyright © 2002-2006 Your Name Here. All rights reserved.</span>
</div>
</div>
</body>
</html>