一个可以拖拽的Web树.

来源:互联网 发布:狼人杀记录软件 编辑:程序博客网 时间:2024/05/16 23:38

代码讲解: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.

 

Requirements

There are a few dependencies this tutorial relies upon.

  • CakePHP 1.2
  • Ext JS 2.0

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!

Setting up the database and model

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 NULLPRIMARY KEY (`id`)KEY parent_id (parent_id)KEY rght (rght)KEY lft (lft, rght)) ENGINE=MyISAM;

/app/model/employee.php

<?phpclass 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 yourEmployee model.

/app/model/employee.php

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.

/app/controllers/employees_controller.php

<?phpclass 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 thelft and rght fields automatically populated with their correct values. You should disable or remove thepopulate() method once you have imported the data.

Displaying Ext JS Tree using CakePHP Tree Behavior

You need to include the Ext JS javascript and css files in your layout.

/app/views/layouts/default.ctp

<!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.

/app/views/employees/index.ctp

<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.

/app/controllers/employees_controller.php

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.

/app/views/employees/getnodes.ctp

<?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.

/app/views/layouts/ajax.ctp

<?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.

Updating the tree structure using Ajax

Let's get cooking! Add the following methods to your controller. I've defined abeforeFilter method that implements some basic security, but the reorder andreparent methods do all the work.

/app/controllers/employees_controller.php

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 thebeforeRender 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 thetree.render(); line in your view, and go and get yourself a beer!

/app/views/employees/index.ctp

// track what nodes are moved and send to server to savevar 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!");        }        });})

What if something bad happens?

Life was never meant to be this easy! There are a few pitfalls with using the above example "as-is".

Wrap the save/update calls in a transaction

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, theverify and recover methods of the Tree Behavior are at your disposal if something goes terribly wrong.

Validate tree data before saving

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 amodified 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.

Extending this example

Inline tree node editing

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!

Styling the tree with icons, and display as "leaf"

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!