代码讲解:http://blogs.bigfish.tv/adam/2008/02/12/drag-and-drop-using-ext-js-with-the-cakephp-tree-behavior/
演示地址:http://blogs.bigfish.tv/adam/examples/tut01-extjs-cakephp/employees/
if you've ever needed to store nested or recursive data you'llrealise how much of a pain it can be. Fortunately for us cake bakers weno longer need to shy away from these data structures to maintain oursanity. With CakePHP's Tree Behavior you can easily add thisfunctionality to any of your models!
Getting data in and out of our tree models is fairly easy using themethods provided, but re-ordering existing data can be frustratingwithout a GUI. Enter stage left... Ext JS!
This tutorial will explain how to use the Ext JS Tree component toallow you to re-order your tree data using drag-and-drop operations.
There are a few dependencies this tutorial relies upon.
Important: Ext JS is a massive javascript library, but you cancreate a custom build to contain only the functionality required. Ihave included a text file in the downloadable source files outliningthe build options I selected when creating this tutorial.
To make things easy I have included all of the files in a zip archive, including a cut-down version of the Ext JS library.
Download the source files used in this tutorial
If you wish to use both Prototype AND Ext JS, my sample files will not work for you!
The Tree Behavior uses three fields to describe the structure of the data - parent_id, lft and rght. It is possible to customise these but I'm sticking with the defaults.
The example I will use in this tutorial is employee heirarchy.
CREATE TABLE `employees` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `parent_id` int(10) UNSIGNED DEFAULT NULL, `lft` int(10) UNSIGNED DEFAULT NULL, `rght` int(10) UNSIGNED DEFAULT NULL, PRIMARY KEY (`id`), KEY parent_id (parent_id), KEY rght (rght), KEY lft (lft, rght) ) ENGINE=MyISAM;
<?php class Employee extends AppModel { var $name = 'Employee'; var $actsAs = array('Tree'); var $order = 'Employee.lft ASC'; } ?>
Now your table is setup, you need some data to play with. Theeasiest way to achieve this is to use a temporary model method andcontroller action. Add the following method to your Employee model.
function populate(){ $this->create(array('name' => 'Harry Potter')); $this->save(); $parent_id = $this->id; $this->create(array('parent_id' => $parent_id, 'name' => 'Ron Weasley')); $this->save(); $this->create(array('parent_id' => $parent_id, 'name' => 'Hermione Granger')); $this->save(); $this->create(array('parent_id' => $parent_id, 'name' => 'Adam Royle')); $this->save(); $this->create(array('parent_id' => $this->id, 'name' => 'Lord Voldemort')); $this->save(); $this->create(array('name' => 'Albus Dumbledore')); $this->save(); $parent_id = $this->id; $this->create(array('parent_id' => $parent_id, 'name' => 'Professor McGonagall')); $this->save(); $this->create(array('parent_id' => $this->id, 'name' => 'Professor Flitwick')); $this->save(); $this->create(array('parent_id' => $parent_id, 'name' => 'Severus Snape')); $this->save(); $this->create(array('parent_id' => $parent_id, 'name' => 'Hagrid')); $this->save(); }
It's time to create your controller. We will add some more meat to it later.
<?php class EmployeesController extends AppController { var $name = 'Employees'; var $components = array('RequestHandler','Security'); var $helpers = array('Form','Html','Javascript'); function populate() { $this->Employee->populate(); echo 'Population complete'; exit; } function index() { } } ?>
Try accessing /employees/populate/ in your browser and you should notice a few records appear in your employees table, with the lft and rght fields automatically populated with their correct values. You should disable or remove the populate() method once you have imported the data.
You need to include the Ext JS javascript and css files in your layout.
<!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=iso-8859-1" /> <title><?php echo h($title_for_layout) ?></title> <?php echo $html->css('/js/ext-2.0.1/resources/css/ext-custom.css'); ?> <?php echo $javascript->link('/js/ext-2.0.1/ext-custom.js'); ?> </head> <body> <div style="margin:40px;"> <?php echo $content_for_layout ?> </div> </body> </html>
Create a view for your index action. Ideally the majority of thiswould be moved to an external javascript file, however to keep thisexample simple I am including it as inline code.
<script type="text/javascript"> Ext.BLANK_IMAGE_URL = '<?php echo $html->url('/js/ext-2.0.1/resources/images/default/s.gif') ?>'; Ext.onReady(function(){ var getnodesUrl = '<?php echo $html->url('/employees/getnodes/') ?>'; var reorderUrl = '<?php echo $html->url('/employees/reorder/') ?>'; var reparentUrl = '<?php echo $html->url('/employees/reparent/') ?>'; var Tree = Ext.tree; var tree = new Tree.TreePanel({ el:'tree-div', autoScroll:true, animate:true, enableDD:true, containerScroll: true, rootVisible: true, loader: new Ext.tree.TreeLoader({ dataUrl:getnodesUrl }) }); var root = new Tree.AsyncTreeNode({ text:'Employees', draggable:false, id:'root' }); tree.setRootNode(root); tree.render(); root.expand(); }); </script> <div id="tree-div" style="height:400px;"></div>
The above snippet covers the basics needed to get a tree to display using Ext JS. If you visit /employees/ you will see it only shows the root node Employees. This is because you haven't defined the /employees/getnodes/ action that is specified in the javascript. Let's implement this now.
function getnodes(){ // retrieve the node id that Ext JS posts via ajax $parent = intval($this->params['form']['node']); // find all the nodes underneath the parent node defined above // the second parameter (true) means we only want direct children $nodes = $this->Employee->children($parent, true); // send the nodes to our view $this->set(compact('nodes')); }
Create a view for the getnodes method. This constructs an array to output as a JSON string, using CakePHP's Javascript Helper.
<?php $data = array(); foreach ($nodes as $node){ $data[] = array( "text" => $node['Employee']['name'], "id" => $node['Employee']['id'], "cls" => "folder" ); } echo $javascript->object($data); ?>
Finally, you need to create an ajax view.
<?php echo $content_for_layout; Configure::write('debug', 0); ?>
Refresh your browser and you should see your data being loadedcorrectly as you open each folder in the tree. Try dragging the nodesaround. If you refresh the page, you'll find the tree is restored toit's original structure. Now it's time to add some ajax callbacks todeal with saving these drag and drop operations.
Let's get cooking! Add the following methods to your controller. I've defined a beforeFilter method that implements some basic security, but the reorder and reparent methods do all the work.
function beforeFilter(){ parent::beforeFilter(); // ensure our ajax methods are posted $this->Security->requirePost('getnodes', 'reorder', 'reparent'); } function reorder(){ // retrieve the node instructions from javascript // delta is the difference in position (1 = next node, -1 = previous node) $node = intval($this->params['form']['node']); $delta = intval($this->params['form']['delta']); if ($delta > 0) { $this->Employee->movedown($node, abs($delta)); } elseif ($delta < 0) { $this->Employee->moveup($node, abs($delta)); } // send success response exit('1'); } function reparent(){ $node = intval($this->params['form']['node']); $parent = intval($this->params['form']['parent']); $position = intval($this->params['form']['position']); // save the employee node with the new parent id // this will move the employee node to the bottom of the parent list $this->Employee->id = $node; $this->Employee->saveField('parent_id', $parent); // If position == 0, then we move it straight to the top // otherwise we calculate the distance to move ($delta). // We have to check if $delta > 0 before moving due to a bug // in the tree behavior (https://trac.cakephp.org/ticket/4037) if ($position == 0){ $this->Employee->moveup($node, true); } else { $count = $this->Employee->childcount($parent, true); $delta = $count-$position-1; if ($delta > 0){ $this->Employee->moveup($node, $delta); } } // send success response exit('1'); }
I've split up the functionality into two different methods, one formoving the node to a different parent node, the other for re-orderingthe node within the existing parent node.
The exit('1'); negates the need to create a view for these methods. Be aware that using this technique will prevent the beforeRender and afterFilter callbacks from being executed. In our case, that does not matter.
The remaining bit of code that pieces everything together is some javascript! Add this snippet above the tree.render(); line in your view, and go and get yourself a beer!
// track what nodes are moved and send to server to save var oldPosition = null; var oldNextSibling = null; tree.on('startdrag', function(tree, node, event){ oldPosition = node.parentNode.indexOf(node); oldNextSibling = node.nextSibling; }); tree.on('movenode', function(tree, node, oldParent, newParent, position){ if (oldParent == newParent){ var url = reorderUrl; var params = {'node':node.id, 'delta':(position-oldPosition)}; } else { var url = reparentUrl; var params = {'node':node.id, 'parent':newParent.id, 'position':position}; } // we disable tree interaction until we've heard a response from the server // this prevents concurrent requests which could yield unusual results tree.disable(); Ext.Ajax.request({ url:url, params:params, success:function(response, request) { // if the first char of our response is zero, then we fail the operation, // otherwise we re-enable the tree if (response.responseText.charAt(0) != 1){ request.failure(); } else { tree.enable(); } }, failure:function() { // we move the node back to where it was beforehand and // we suspendEvents() so that we don't get stuck in a possible infinite loop tree.suspendEvents(); oldParent.appendChild(node); if (oldNextSibling){ oldParent.insertBefore(node, oldNextSibling); } tree.resumeEvents(); tree.enable(); alert("Oh no! Your changes could not be saved!"); } }); });
Life was never meant to be this easy! There are a few pitfalls with using the above example "as-is".
The Tree Behavior doesn't natively use transactions when saving thetree data. This is something that you should think about implementingif data integrity is paramount to your application. A lot of queriesare executed when adding new nodes or moving nodes around. Theslightest error can corrupt your tree data, so it is better to be safethan sorry! However, the verify and recover methods of the Tree Behavior are at your disposal if something goes terribly wrong.
If there are multiple users updating the tree structure at the sametime, you are bound to run into issues as your Ext JS trees becomeunsynchronised with any changes that have been made by other users.
If you feel this could happen in your situation, I would suggestimplementing some validation before saving nodes to their newlocations. You could use a modified datetime field to detectwhen something has changed, and tell the user to refresh their tree andmake the change again. With some cunning code it would be possible todo this behind the scenes with Ext JS without alerting the user unlessthere is a direct conflict.
Ext JS already ships with the ability to edit the node names inside a tree (Ext.tree.TreeEditor). Adding some ajax code to save these edits would be a piece of cake!
Modify getnodes.ctp and customise the tree icons using the "cls" attribute. If you add "leaf" => true to your node array, Ext JS prevents that node from becoming a parent. Handy like a peach!