Introduction

We've all seen the videos (and some even got access to a developer's preview) of Google's latest product - Wave. Although not "ground-braking" and "revolutionary" as we've imagined (wonder why "over-hyped" comes to mind) it still features some great UI that will surely inspire at least a few developers to implement some of it in their works.

I, being one of those inspired people, am going to show you how to create a Google Wave-like history slider. Using it, we will enable our visitors to go back and forth in time to view the changes that take place on a comment thread.

So take a look at the demo (maybe even keep it open in a separate tab), download the example files and keep on reading.

Note: This tutorial is quite old and doesn't work in PHP7 and above. We are keeping it online only as a reference.

Step 1 - XHTML

The technologies we are using include PHP as a back-end, MySQL as data storage, jQuery, CSS and XHTML for the front-end with AJAX in between. The slider itself is a component created with jQuery UI.

First lets take a look at the body section of demo.php

demo.php

<div id="main"> <p id="orig">View the <a href="https://tutorialzine.com/2009/10/google-wave-history-slider-jquery/" target="_blank">original tutorial »</a></p> <h1>Google Wave-like</h1> <h2>History Slider</h2> <div id="wave"> <div id="topBar">Your Demo Wave</div> <div id="subBar"> <img src="img/tutorialzine.png" alt="Tutorialzine" /><img src="img/demo.png" alt="Demo" /><img src="img/curious.png" alt="Curious" /> </div> <div id="sliderContainer"> <div id="slider"></div> <div class="clear"></div> </div> <div id="commentArea"> <?php foreach($comments as $c) { showComment($c); // Showing each comment } ?> </div> <input type="button" class="waveButtonMain" value="Add a comment" onclick="addComment()" /> <div id="bottomBar"> </div> </div> </div>

This is pretty much all the layout we are using. The main reason why the code is so short is that we are using CSS to style it, and that the outputting of the comments is handled by a special PHP function, both of which will be explained in a minute.

Step 2 - CSS

The great thing about jQuery is that, thanks to Google's CDN, you can directly include it in your site, without having to worry about downloading and storing it on your server. This also aids your page loading time.

The same is true for jQuery UI, which contains our slider. Not only that, but the CDN also holds the styling and images needed to display it properly.

However, we still have to include our own custom styles. In the example files you can find them in demo.css. Here only the more interesting bits are shown:

demo.css

#orig{ /* The link that float to the right of the title */ float:right; font-family:"MyRiad Pro",Arial; font-size:10px; letter-spacing:1px; text-transform:uppercase; padding-top:10px; } .clear{ /* Clearfix, needed by IE6 */ clear:both; } #main{ /* The main container */ width:600px; margin:30px auto; } #wave{ /* CSS rounded corners */ -moz-border-radius:6px; -khtml-border-radius: 6px; -webkit-border-radius: 6px; border-radius:6px; background:white; width:100%; overflow:hidden; } #topBar{ background:url(img/bg.jpg) repeat-x; font-size:12px; color:white; height:20px; overflow:hidden; padding:5px 0 0 10px; border-bottom:1px solid #e4f1ff; -moz-border-radius:6px 6px 0 0; /* A Firefox fix, for once */ } #bottomBar{ height:40px; background-color:#c9e2fc; -moz-border-radius:0 0 6px 6px; border-top:1px solid #CCCCCC; } #subBar{ background-color:#c9e2fc; padding-left:10px; } #subBar img{ /* The avatars at the top of the page */ margin:8px 8px 8px 0; border:1px solid #cccccc; } .waveButton,.waveButtonMain{ /* The submit buttons */ background:url(img/button_bg.jpg) repeat-x 50% 50%; border:1px solid #DDDDDD; padding:4px; cursor:pointer; } .waveButtonMain{ display:block; margin:10px 20px; } .textArea{ padding:4px; font-family:Arial,Helvetica,Sans-serif; font-size:12px; color:#666666; border:1px solid #66aff9; margin-bottom:10px; } .replyLink{ float:right; } #commentArea{ padding:10px; color:#444444; } .commentText{ margin-left:40px; } .waveComment .waveComment{ padding-left:30px; } .waveComment .waveComment .replyLink{ /* Hiding the reply link on the comment replies - only 2 levels of ancestry are allowed */ display:none; } .waveTime{ color:#999999; float:right; font-size:10px; } #slider{ width:400px; font-size:10px; float:right; margin-right:10px; } #sliderContainer{ background:url(img/dark_bg.jpg) repeat-x #f5f5f5 50% 50%; padding:9px 10px; border:1px solid #bbbbbb; border-left:0; border-right:0; height:10px; padding:9px 10px; } div.ui-widget-content{ /* Styling the slider */ background:#FFFFFF; border:1px solid #CCCCCC; } .comment{ margin:5px 10px; padding:8px 10px; border:2px solid #cccccc; /* Rounding the comment */ -moz-border-radius:6px; -khtml-border-radius: 6px; -webkit-border-radius: 6px; border-radius:6px; overflow:hidden; } span.name{ font-weight:bold; color:#999999; } .commentAvatar{ width:30px; height:30px; float:left; margin-right:10px; }

Step 3 - PHP

There are four main PHP files that handle the back-end:

demo.php - outputs the comments; ajax/saveComment.php - adds new comments, it's reached by AJAX requests; functions.php - holds some functions used by demo.php; connect.php - handles the DB connection.

We'll be looking only at the first three files here.

demo.php

define("INCLUDE_CHECK",1); require 'connect.php'; require 'functions.php'; // Including the files for the DB connection and our custom functions // Removing comments that are older than an hour. mysql_query("DELETE FROM wave_comments WHERE id>5 AND dt<SUBTIME(NOW(),'0 1:0:0')"); $comments_result = mysql_query("SELECT * FROM wave_comments ORDER BY id ASC"); // Selecting all the comments ordered by id in ascending order $comments=array(); $js_history=''; while($row=mysql_fetch_assoc($comments_result)) { if($row['parent']==0) // If the comment is not a reply to a previous comment, put it into $comments directly $comments[$row['id']] = $row; else { if(!$comments[$row['parent']]) continue; $comments[$row['parent']]['replies'][] = $row; // If it is a reply, put it in the 'replies' property of its parent } $js_history.='addHistory({id:"'.$row['id'].'"});'.PHP_EOL; // Adds JS history for each comment } $js_history='<script type="text/javascript"> '.$js_history.' </script>'; // This is later put into the head and executed on page load

The comments are ether parents (they are added directly to the thread) or children (added as a reply to a parent). Only two levels of ancestry are allowed (meaning that replies are disabled for the children).

The comments are later outputted by the showComment function (you can see it in the XHTML step above).

ajax / saveComment.php

define("INCLUDE_CHECK",1); require'../connect.php'; if(empty($_POST['comment'])) die("0"); // If there isn't a comment text, exit $comment = mysql_real_escape_string(nl2br(strip_tags($_POST['comment']))); $user='Demo'; // This would be a nice place to start customizing - the default user // You can integrate it to any site and show a different username. $addon=''; if($_POST['parent']) $addon=',parent='.(int)$_POST['parent']; mysql_query("INSERT INTO wave_comments SET usr='".$user."', comment='".$comment."', dt=NOW()".$addon); if(mysql_affected_rows($link)==1) echo mysql_insert_id($link); // If the insert was successful, echo the newly assigned ID else echo '0';

And lastly, there is functions.php

functions.php

if(!defined('INCLUDE_CHECK')) die('You are not allowed to execute this file directly'); function showComment($arr) { echo ' <div class="waveComment com-'.$arr['id'].'"> <div class="comment"> <div class="waveTime">'.waveTime($arr['dt']).'</div> <div class="commentAvatar"> <img src="img/'.strtolower($arr['usr']).'.png" width="30" height="30" alt="'.$arr['usr'].'" /> </div> <div class="commentText"> <span class="name">'.$arr['usr'].':</span> '.$arr['comment'].' </div> <div class="replyLink"> <a href="" onclick="addComment(this,'.$arr['id'].');return false;">add a reply »</a> </div> <div class="clear"></div> </div>'; // Output the comment, and its replies, if any if($arr['replies']) { foreach($arr['replies'] as $r) showComment($r); } echo '</div>'; } function waveTime($t) { $t = strtotime($t); if(date('d')==date('d',$t)) return date('h:i A',$t); return date('F jS Y h:i A',$t); // If the comment was written today, output only the hour and minute // if it was not, output a full date/time }

The last step is the trickiest one - in this case the jQuery code.

Step 4 - jQuery

All the JS code is located in script.js. I will split it into two parts:

script.js - part 1

$(document).ready(function(){ // Executed once all the page elements are loaded lastVal = totHistory; // Create the slider: $("#slider").slider({ value:totHistory, min: 1, max: totHistory, animate: true, slide: function(event, ui) { if(lastVal>ui.value) $(buildQ(lastVal,ui.value)).hide('fast').find('.addComment').remove(); // Using buildQ to build the jQuery selector // If we are moving the slider backward, hide the previous comment else if(lastVal<ui.value) $(buildQ(lastVal,ui.value)).show('fast'); // Otherwise show it lastVal = ui.value; } }); }); var totHistory=0; // Holds the number of comments var positions = new Array(); var lastVal; function addHistory(obj) { /* Gets called on page load for each comment, and on comment submit */ totHistory++; positions.push(obj.id); } function buildQ(from,to) { /* Building a jQuery selector from the begin and end point of the slide */ if(from>to) { var tmp=to; to=from; from=tmp; } from++; to++; var query=''; for(var i=from;i<to;i++) { if(i!=from) query+=','; query+='.com-'+positions[i-1]; } /* Each comment has an unique com-(Comment ID) class that we are using to address it */ return query; }

As you remember, we generated a special PHP string, which holds calls to the addHistory function. Each time it runs, it increments the totHistory counter. After all the comments are loaded the $(document).ready runs and the slider is initialized with totHistory as the maximum value of the slider. The minimum value is 1, because we want to have at least one comment visible.

Now lets take a look at the second part of the file.

script.js - part 2

function addComment(where,parent) { /* This functions gets called from both the "Add a comment" button on the bottom of the page, and the add a reply link. It shows the comment submition form */ var $el; if($('.waveButton').length) return false; // If there already is a comment submition form // shown on the page, return and exit if(!where) $el = $('#commentArea'); else $el = $(where).closest('.waveComment'); if(!parent) parent=0; // If we are adding a comment, but there are hidden comments by the slider: $('.waveComment').show('slow'); lastVal = totHistory; $('#slider').slider('option','value',totHistory); // Move the slider to the end point and show all comments var comment = '<div class="waveComment addComment">\ \ <div class="comment">\ <div class="commentAvatar">\ <img src="img/demo.png" width="30" height="30" />\ </div>\ \ <div class="commentText">\ \ <textarea class="textArea" rows="2" cols="70" name="" />\ <div><input type="button" class="waveButton" value="Add comment" onclick="addSubmit(this,'+parent+')" /> or <a href="" onclick="cancelAdd(this);return false">cancel</a></div>\ \ </div>\ </div>\ \ </div>'; $el.append(comment); // Append the form } function cancelAdd(el) { $(el).closest('.waveComment').remove(); } function addSubmit(el,parent) { /* Executed when clicking the submit button */ var cText = $(el).closest('.commentText'); var text = cText.find('textarea').val(); var wC = $(el).closest('.waveComment'); if(text.length<4) { alert("Your comment is too short!"); return false; } $(el).parent().html('<img src="img/ajax_load.gif" width="16" height="16" />'); // Showing the loading gif animation // Send an AJAX request: $.ajax({ type: "POST", url: "ajax/saveComment.php", data: "comment="+encodeURIComponent(text)+"&parent="+parent, /* Sending both the text and the parent of the comment */ success: function(msg){ /* PHP returns the automatically assigned ID of the new comment */ var ins_id = parseInt(msg); if(ins_id) { wC.addClass('com-'+ins_id); addHistory({id:ins_id}); $('#slider').slider('option', 'max', totHistory).slider('option','value',totHistory); lastVal=totHistory; } transForm(text,cText); // Hiding the form and showing the newly-added comment in its place } }); } function transForm(text,cText) { var tmpStr ='<span class="name">Demo:</span> '+text; cText.html(tmpStr); }

The functions in this part of the code handle the comment submission via AJAX to the PHP back-end.

I believe that further clarification is needed for the success function in the AJAX. As you know, it is called when we have successfully executed the AJAX request (in this case it is called if the comment was written to the MySQL database).

Inside this function we check whether a proper insert ID is returned, which corresponds to the internal MySQL id that was given to the auto-increment field (see below for the MySQL part or take a look at table.sql in the example files).

If everything is OK, we call the addHistory function with the new data and update the maximum value of the slider. This ensures that the newly-added comment is history-scrollable with the rest of them.

Step 5 - MySQL

This step is only required if you want to run the demo on your own server. If you run into problems, read through the comments below or write a new one if your problem is not addressed there.

To be able to run the demo, you'd have to create the MySQL table wave_comments with the following code (also available in table.sql in the example files):

CREATE TABLE `wave_comments` ( `id` int(11) NOT NULL auto_increment, `parent` int(11) NOT NULL default '0', `usr` varchar(16) collate utf8_unicode_ci NOT NULL default '', `comment` text collate utf8_unicode_ci NOT NULL, `dt` datetime NOT NULL default '0000-00-00 00:00:00', PRIMARY KEY (`id`), KEY `parent` (`parent`,`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

With this our Google Wave-like History Slider is complete!

Conclusion

You are free to modify the code of this example and use it in your own sites. Also it would be great if you share what you've done with the community, via our newly added feature - Tutorial Mashups (you can find it below every tutorial).