AJAX Drag and Drop 240 Let's take a look at how this is going to look: Figure 10.1: Add, Reorder, and Delete Tasks, in a Simple Visual Interface Dragging items around the screen makes the other items switch positions. Chapter 10 When dropping a task on the DROP HERE TO DELETE area, a confirmation is required before the application proceeds with the actual deletion; as shown in the following figure: Figure 10.2: Confirmation Required Before Deleting a Task Time for Action—Task Management Application with AJAX 1. Connect to the ajax database, and create a table named tasks with the following code: CREATE TABLE tasks ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_no INT UNSIGNED NOT NULL default '0', description VARCHAR(100) NOT NULL default '', PRIMARY KEY (id) ); 2. In your ajax folder, create a new folder named drag-and-drop. 3. In the drag-and-drop folder, create a file named config.php, and add the database configuration code to it: <?php // defines database connection data define('DB_HOST', 'localhost'); define('DB_USER', 'ajaxuser'); define('DB_PASSWORD', 'practical'); define('DB_DATABASE', 'ajax'); ?> 241 AJAX Drag and Drop 4. Now add the standard error-handling file, error_handler.php. Feel free to copy this file from previous chapters. Anyway, here's the code for it: <?php // set the user error handler method to be error_handler set_error_handler('error_handler', E_ALL); // error handler function function error_handler($errNo, $errStr, $errFile, $errLine) { // clear any output that has already been generated if(ob_get_length()) ob_clean(); // output the error message $error_message = 'ERRNO: ' . $errNo . chr(10) . 'TEXT: ' . $errStr . chr(10) . 'LOCATION: ' . $errFile . ', line ' . $errLine; echo $error_message; // prevent processing any more PHP scripts exit; } ?> 5. Download the script.aculo.us library from http://script.aculo.us/downloads and unzip/untar the downloaded archive to your drag-and-drop folder. Change the 242 script.aculo.us folder name from something like scriptaculous-js- x.y.z to simply scriptaculous. 6. Create a new file named index.php, and add this code to it: <?php require_once ('taskslist.class.php'); ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>AJAX Drag and Drop Sortable List</title> <link href="drag-and-drop.css" rel="stylesheet" type="text/css" /> <script src="drag-and-drop.js" type="text/javascript"></script> <script src="scriptaculous/lib/prototype.js" type="text/javascript"> </script> <script src="scriptaculous/src/scriptaculous.js" type="text/javascript"> </script> </head> <body onload="startup()"> <h1>Task Management</h1> <h2>Add a new task</h2> <div> <input type="text" id="txtNewTask" name="txtNewTask" size="30" maxlength="100" onkeydown="handleKey(event)"/> <input type="button" name="submit" value="Add this task" onclick="process('txtNewTask', 'addNewTask')" /> </div> <br /> <h2>All tasks</h2> <ul id="tasksList" class="sortableList" onmouseup="process('tasksList', 'updateList')"> <?php $myTasksList = new TasksList(); echo $myTasksList->BuildTasksList(); ?> Chapter 10 </ul> <br /><br /> <div id="trash"> DROP HERE TO DELETE <br /><br /> </div> </body> </html> 7. Create a new file named taskslist.class.php, and add this code to it: <?php // load error handler and database configuration require_once ('error_handler.php'); require_once ('config.php'); // This class builds a tasks list and // performs add/delete/reorder actions on it class TasksList { // stored database connection private $mMysqli; // constructor opens database connection function __construct() { // connect to the database $this->mMysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE); } // destructor closes database connection public function __destruct() { $this->mMysqli->close(); } // Builds the tasks list public function BuildTasksList() { // initialize output $myList = ''; // build query $result = $this->mMysqli->query('SELECT * FROM tasks ' . 'ORDER BY order_no ASC'); // build task list as <li> elements while ($row = $result->fetch_assoc()) { $myList .= '<li id="' . htmlentities($row['id']) . '">' . htmlentities($row['description']) . '</li>'; } // return the list return $myList; } // Handles the server-side data processing public function Process($content, $action) { // perform action requested by client switch($action) { // Reorder task list case 'updateList': // retrieve update details $new_order = explode('_', $content); // update list 243 AJAX Drag and Drop 244 for ($i=0; $i < count($new_order); $i++) { // escape data received from client $new_order[$i] = $this->mMysqli->real_escape_string($new_order[$i]); // update task $result = $this->mMysqli->query('UPDATE tasks SET order_no="' . $i . '" WHERE id="' . $new_order[$i] . '"'); } $updatedList = $this->BuildTasksList(); return $updatedList; break; // Add a new task case 'addNewTask': // escape input data $task = trim($this->mMysqli->real_escape_string($content)); // continue only if task name is not null if ($task) { // obtain the highest order_no $result = $this->mMysqli->query('SELECT (MAX(order_no) + 1) ' . 'AS order_no FROM tasks'); $row = $result->fetch_assoc(); // if the table is empty, order_no will be null $order = $row['order_no']; if (!$order) $order = 1; // insert the new task as the bottom of the list $result = $this->mMysqli->query ('INSERT INTO tasks (order_no, description) ' . 'VALUES ("' . $order . '", "' . $task . '")'); // return the updated tasks list $updatedList = $this->BuildTasksList(); return $updatedList; } break; // Delete task case 'delTask': // escape input data $content = trim($this->mMysqli->real_escape_string($content)); // delete the task $result = $this->mMysqli->query('DELETE FROM tasks WHERE id="' . $content . '"'); $updatedList = $this->BuildTasksList(); return $updatedList; break; } } } ?> 8. Create a new file named drag-and-drop.php, and add this code to it: <?php // load helper class require_once ('taskslist.class.php'); // create TasksList object $myTasksList = new TasksList(); // read parameters $action = $_GET['action']; $content = $_GET['content']; // clear the output if(ob_get_length()) ob_clean(); // headers are sent to prevent browsers from caching Chapter 10 header('Expires: Fri, 25 Dec 1980 00:00:00 GMT'); // time in the past header('Last-Modified: ' . gmdate( 'D, d M Y H:i:s') . 'GMT'); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: no-cache'); header('Content-Type: text/html'); // execute the client request and return the updated tasks list echo $myTasksList->Process($content, $action); ?> 9. Create a new file named drag-and-drop.js, and add this code to it: // holds an instance of XMLHttpRequest var xmlHttp = createXmlHttpRequestObject(); // when set to true, display detailed error messages var showErrors = true; // initialize the requests cache var cache = new Array(); // creates an XMLHttpRequest instance function createXmlHttpRequestObject() { // will store the reference to the XMLHttpRequest object var xmlHttp; // this should work for all browsers except IE6 and older try { // try to create XMLHttpRequest object xmlHttp = new XMLHttpRequest(); } catch(e) { // assume IE6 or older var XmlHttpVersions = new Array("MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.5.0", "MSXML2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"); // try every prog id until one works for (var i=0; i<XmlHttpVersions.length && !xmlHttp; i++) { try { // try to create XMLHttpRequest object xmlHttp = new ActiveXObject(XmlHttpVersions[i]); } catch (e) {} // ignore potential error } } // return the created object or display an error message if (!xmlHttp) alert("Error creating the XMLHttpRequest object."); else return xmlHttp; } // function that displays an error message function displayError($message) { // ignore errors if showErrors is false if (showErrors) { // turn error displaying Off showErrors = false; // display error message alert("Error encountered: \n" + $message); 245 AJAX Drag and Drop 246 } } // Scriptaculous-specific code to define a sortable list and a drop zone function startup() { // Transform an unordered list into a sortable list with draggable items Sortable.create("tasksList", {tag:"li"}); // Define a drop zone used for deleting tasks Droppables.add("trash", { onDrop: function(element) { var deleteTask = confirm("Are you sure you want to delete this task?") if (deleteTask) { Element.hide(element); process(element.id, "delTask"); } } }); } // Serialize the id values of list items (<li>s) function serialize(listID) { // count the list's items var length = document.getElementById(listID).childNodes.length; var serialized = ""; // loop through each element for (i = 0; i < length; i++) { // get current element var li = document.getElementById(listID).childNodes[i]; // get current element's id without the text part var id = li.getAttribute("id"); // append only the number to the ids array serialized += encodeURIComponent(id) + "_"; } // return the array with the trailing '_' cut off return serialized.substring(0, serialized.length - 1); } // Send request to server function process(content, action) { // only continue if xmlHttp isn't void if (xmlHttp) { // initialize the request query string to empty string params = ""; // escape the values to be safely sent to the server content = encodeURIComponent(content); // send different parameters depending on action if (action == "updateList") params = "?content=" + serialize(content) + "&action=updateList"; else if (action == "addNewTask") { // prepare the task for sending to the server var newTask = trim(encodeURIComponent(document.getElementById(content).value)); // don't add void tasks if (newTask) params = "?content=" + newTask + "&action=addNewTask"; Chapter 10 } else if (action =="delTask") params = "?content=" + content + "&action=delTask"; // don't add null params to cache if (params) cache.push(params); // try to connect to the server try { // only continue if the connection is clear and cache is not empty if ((xmlHttp.readyState == 4 || xmlHttp.readyState == 0) && cache.length>0) { // get next set of values from cache var cacheEntry = cache.shift(); // initiate the request xmlHttp.open("GET", "drag-and-drop.php" + cacheEntry, true); xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xmlHttp.onreadystatechange = handleRequestStateChange; xmlHttp.send(null); } else { setTimeout("process();", 1000); } } // display the error in case of failure catch (e) { displayError(e.toString()); } } } // function that retrieves the HTTP response function handleRequestStateChange() { // when readyState is 4, we also read the server response if (xmlHttp.readyState == 4) { // continue only if HTTP status is "OK" if (xmlHttp.status == 200) { try { postUpdateProcess(); } catch(e) { // display error message displayError(e.toString()); } } else { displayError(xmlHttp.statusText); } } } // Processes server's response function postUpdateProcess() { // read the response var response = xmlHttp.responseText; // server error? 247 AJAX Drag and Drop 248 if (response.indexOf("ERRNO") >= 0 || response.indexOf("error") >= 0) alert(response); // update the tasks list document.getElementById("tasksList").innerHTML = response; Sortable.create("tasksList"); document.getElementById("txtNewTask").value = ""; document.getElementById("txtNewTask").focus(); } /* handles keydown to detect when enter is pressed */ function handleKey(e) { // get the event e = (!e) ? window.event : e; // get the code of the character that has been pressed code = (e.charCode) ? e.charCode : ((e.keyCode) ? e.keyCode : ((e.which) ? e.which : 0)); // handle the keydown event if (e.type == "keydown") { // if enter (code 13) is pressed if(code == 13) { // send the current message process("txtNewTask", "addNewTask"); } } } /* removes leading and trailing spaces from the string */ function trim(s) { return s.replace(/(^\s+)|(\s+$)/g, "") } 10. Create a new file named drag-and-drop.css, and add this code to it: body { font-family: Arial, Helvetica, sans-serif; font-size: 12px; } ul.sortableList { list-style-type: none; padding: 0px; margin: 0px; width: 300px; } ul.sortableList li { cursor: move; padding: 2px 2px; margin: 2px 0px; border: 1px solid #00CC00; background-color: #F4FFF5; } h1 { border-bottom: 1px solid #cccccc; } #trash { border: 4px solid #ff0000; width: 270px; padding: 10px; } Chapter 10 11. Load http://localhost/ajax/drag-and-drop in your web browser and test its functionality to make sure it works as expected (see Figures 10.1 and 10.2 for reference) . What Just Happened? Adding a task is performed as mentioned in the following steps: 1. The user enters task. 2. When the user clicks on Add this task button or presses Enter, the data is sent to the server with an asynchronous HTTP request. The server script inserts the new task into the database, and returns the updated list, which is then injected into the code with JavaScript. When reordering the list, this is what happens: 1. Every task is an XHTML list element: an <li>. The user begins dragging an item; on dropping it, an HTTP request is sent to the server. This request consists of a serialized string of IDs, every list element's ID. 2. On the client you'll see the list reordered, while the server updates the order of each element in the database. This is how deleting a task works: 1. The user drags an item and drops it on the DROP HERE TO DELETE area. 2. An HTTP request is sent to the server, which performs the task deletion from the database and the XHTML element is instantly destroyed. We include in index.php the JavaScript libraries we'll be using: <script src="drag-and-drop.js" type="text/javascript"></script> <script src="scriptaculous/lib/prototype.js" type="text/javascript"> </script> <script src="scriptaculous/src/scriptaculous.js" type="text/javascript"> </script> The first line includes our custom functions and AJAX-related tasks. The second line includes the Prototype library, while the third line includes the script.aculo.us library. The onload event inside the <body> tag calls the startup() function, which defines the unordered list with id="tasksList" as a sortable element (Sortable.create). This ensures drag-and-drop functionality for <li> elements inside the list. The startup() function also defines a droppable element Droppables.add; we use this as an area where we delete tasks. Also, inside the startup() function, we define a behavior for dropping a list item on the drop zone: onDrop: function(element) { var deleteTask = confirm("Are you sure you want to delete this task?") if (deleteTask == true) { Element.hide(element); process(element, "delTask"); } } 249 . In your ajax folder, create a new folder named drag -and- drop. 3. In the drag -and- drop folder, create a file named config .php, and add the database configuration code to it: < ?php // defines. "drag -and- drop .php& quot; + cacheEntry, true); xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xmlHttp.onreadystatechange = handleRequestStateChange;. taskslist.class .php, and add this code to it: < ?php // load error handler and database configuration require_once ('error_handler .php& apos;); require_once ('config .php& apos;);