一个可以拖拽的Web树.

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

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 NULL,
  PRIMARY KEY (`id`),
  KEY parent_id (parent_id),
  KEY rght (rght),
  KEY lft (lft, rght)
) ENGINE=MyISAM;
/app/model/employee.php

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

/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

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

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 a beforeFilter method that implements some basic security, but the reorder and reparent 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 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!

/app/views/employees/index.ctp

// 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!");
        }
    
    });

});

 

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, the verify 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 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.

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!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值