PDA

View Full Version : Sortable trees with Ext and PHP


jkkramer
02-22-2007, 11:11 PM
I've started using the new TreePanel in a CMS to allow users to reorder a hierarchy of website pages. It replaced a homegrown solution which was far less robust (and pretty) than Jack's work.

To use Jack's work, I had to do two things: 1) pull existing data out of the CMS database and give it to the tree; and 2) take the reordered tree data and save back into the database.

Pulling out the existing data and giving it to the tree was pretty easy. Saving the result after the user did some reordering was trickier. I haven't seen any other explanations out there about how to do this, so here's a simplified example of how I accomplished both.

Assume you have a hierarchy of nodes (i.e., a tree) that looks like this:

lorem
ipsum
dolor
sit
amet
consectetur
adipisicing
elit
sed
do
eiusmod


Here is a MySQL database schema to store this hierarchy (you could also use something like modified preorder tree transveral (http://www.sitepoint.com/article/hierarchical-data-database/2) but this layout makes my example simpler):


CREATE TABLE `nodes` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(255) default NULL,
`parent_id` int(11) NOT NULL default '0',
`display_order` int(11) default NULL,
PRIMARY KEY (`id`)
);


And here's the data you'd insert:


+----+-------------+-----------+---------------+
| id | title | parent_id | display_order |
+----+-------------+-----------+---------------+
| 1 | lorem | 0 | 1 |
| 2 | ipsum | 0 | 2 |
| 3 | dolor | 2 | 3 |
| 4 | sit | 2 | 4 |
| 5 | amet | 2 | 5 |
| 6 | consectetur | 0 | 6 |
| 7 | adipisicing | 0 | 7 |
| 8 | elit | 7 | 8 |
| 9 | sed | 8 | 9 |
| 10 | do | 8 | 10 |
| 11 | eiusmod | 0 | 11 |
+----+-------------+-----------+---------------+


Here is an HTML file with embedded JavaScript (same file for the sake of simplicity) which will load a TreePanel-compatible, JSON version of this hierarchy from backend.php, and then submit a JSON tree whenever a node gets drag-and-dropped:


<html>
<head>
<title>Sortable Trees with Ext (yui-ext) and PHP</title>
<script type="text/javascript" src="http://www.yui-ext.com/deploy/ext-1.0-alpha1/yui-utilities.js"></script>
<script type="text/javascript" src="http://www.yui-ext.com/deploy/ext-1.0-alpha1/ext-all.js"></script>
<script type="text/javascript" src="http://www.yui-ext.com/deploy/ext-1.0-alpha1/ext-bridge-yui.js"></script>
<link rel="stylesheet" type="text/css" href="http://www.yui-ext.com/deploy/ext-1.0-alpha1/resources/css/ext-all.css">
</head>
<body>

<div id="tree-container"></div>

<script type="text/javascript">
(function () {

var tree = new Ext.tree.TreePanel('tree-container', {
loader: new Ext.tree.TreeLoader({dataUrl:'backend.php'}),
enableDD: true
});

tree.on("enddrag", function(tree) {

// create a simplified node hierarchy that can be JSONified
function simplifyNodes(node) {
var resultNode = {};
var kids = node.childNodes;
var len = kids.length;
for (var i = 0; i < len; i++) {
resultNode[kids[i].id] = simplifyNodes(kids[i]);
}
return resultNode;
}

// JSON-encode our tree
var encNodes = Ext.encode(simplifyNodes(tree.root));

// send it to the backend to save
YAHOO.util.Connect.asyncRequest(
'POST',
'backend.php',
null,
'action=update&nodes=' + encodeURIComponent(encNodes)
);

});

var root = new Ext.tree.AsyncTreeNode({
text: 'root',
draggable: false,
id: 'root'
});
tree.setRootNode(root);

tree.render();
root.expand(true);

})();
</script>

</body>
</html>


Here is backend.php (requires PHP5 and PEAR DB):


<?php

require_once "DB.php";

$db =& DB::connect('mysql://test:test@localhost/tree_test');
$db->setFetchMode(DB_FETCHMODE_ASSOC);

if ($_POST['action'] == "update") {

function update_nodes($nodes, $parent_id=0, $display_order=0) {
global $db;
foreach ($nodes as $id => $children) {
$db->query(sprintf("update nodes set
parent_id='%d', display_order='%d'
where id='%d'",
$parent_id, $display_order, $id));
$display_order++;
update_nodes($children, $id, $display_order);
}
}

update_nodes(json_decode(
get_magic_quotes_gpc() ?
stripslashes($_POST['nodes']) :
$_POST['nodes']));

} else {

function expand_nodes($nodes, $parent_id=0) {
$new_nodes = array();
foreach ($nodes as $node) {
if ($node['parent_id'] != $parent_id) continue;
$new_nodes[] = array(
"text" => $node['title'],
"id" => $node['id'],
"children" => expand_nodes($nodes, $node['id']),
);
}
return $new_nodes;
}

echo json_encode(expand_nodes($db->getAll(
"select * from nodes order by display_order")));
}

?>


There are plenty of optimizations that could be made but I'm keeping it simple for the sake of clarity. I may make a blog post about this and include everything needed to make it work in one zip file if there's interest.

Question for Jack if you see this: do you think TreePanel ought to include a serialization method for this kind of purpose? Scriptaculous has something like that, although their sortable tree is not nearly as good as TreePanel.

JeffHowden
02-23-2007, 12:42 AM
I will, hopefully, be posting some improvements to the TreePanel to do just that. Those improvements are based on work from various members of the forum as well as enhancements I neeed for my own implementation. I ran them by Jack and he recommended I dig a touch deeper into a couple more tree specific private methods (should be faster than what I'm doing now and less lines of code, he says) which I will try to do tonight. With any luck they'll make it into the next revision as public methods on the tree.

Just so you know what to look out for, they'll be toXMLString() and toJsonString().

jkkramer
02-23-2007, 12:53 AM
Good to know, Jeff. Thanks.

Justin

oxi
02-27-2007, 06:25 AM
Thanks a lot for your example!

I would like to see an example where nodes can be created, deleted and renamed.
An example with 2 trees would be cool too.

MD
03-26-2007, 02:40 PM
I will, hopefully, be posting some improvements to the TreePanel to do just that. Those improvements are based on work from various members of the forum as well as enhancements I neeed for my own implementation. I ran them by Jack and he recommended I dig a touch deeper into a couple more tree specific private methods (should be faster than what I'm doing now and less lines of code, he says) which I will try to do tonight. With any luck they'll make it into the next revision as public methods on the tree.

Just so you know what to look out for, they'll be toXMLString() and toJsonString().

Have any of these improvements made their way into Alpha 3 rev 4 by any chance? Unfortunately, I can't tell from the docs if they have. Serialization and post back (a la Scriptaculous style) of a re-ordered Tree is an absolutely critical part of the first large project we're going to be implementing Ext in. I'd just assumed serialization was a natural part of it, so mistakenly hadn't yet taken too close of a look at that aspect of the Tree :S

JeffHowden
03-26-2007, 03:57 PM
They have not, as of yet. I currently have code locally that serializes the tree (or a selected node and all its descendants) to either JSON or XML. I'll work with Jack to get it integrated into the next release.

MD
03-26-2007, 04:09 PM
Is your code in a useable enough state that you'd be willing to share it as a temporary solution? I don't mind at all having to re-implement it later down the road once it's actually integrated into Ext, but without something to go on initially, it stops the core part of what I'm trying to do dead in it's tracks :S

Animal
03-27-2007, 03:35 AM
Yes, can you share your code and it's capabilities so that we can request features?

I've implemented a basic toXML function which uses the attributes property of TreeNodes to create attributes of <node> elements.

It offers some filtering of what attributes to include, but I can imagine an Ext version benig much smarter, perhaps offering a filter function for excluding attributes, or for excluding Nodes.


/* Retrieve XML for the tree.
* @param node - The tree node to get an XML representation of.
* @param removeAutogenIds Pass as true to remove ids which were created by Ext on anonymous nodes
* @param exclude An Array of string attribute names to exclude from the XML node representation
*/
function toXML(node, removeAutogenIds, exclude) {
var result = "\u003Cnode";
for (var att in node.attributes) {

// Process exclusions
if (exclude && (exclude.indexOf(att) != -1)) {
continue;
}

var v = node.attributes[att];
if (typeof v != "object") {
if (att == 'id') {
if ((typeof v == "string") && (v.match(/^ynode/))) {
if (!removeAutogenIds) {
result += ' id="' + v + '"';
}
} else {
result += ' id="' + v + '"';
}
} else if (att == 'text') {
result += ' description="' + v + '"';
} else {
result += ' ' + att + '="' + v + '"';
}
}
}
result += '>';

// Collect child nodes
if (!node.isLeaf()) {
var cs = node.childNodes;
for(var i = 0, len = cs.length; i < len; i++) {
result += toXML(cs[i], removeAutogenIds, exclude);
}
}
return result + "\u003c/node>";
}

JeffHowden
03-27-2007, 06:05 AM
I would gladly share except that I can't figure out what class it should go into. I can offer it as prototyped methods, but I want to make sure the way it's written will work once it's fully integrated. Thoughts?

Animal
03-27-2007, 06:31 AM
Should it not go into Ext.data.Node?

Any node of any tree-structured dataset should be convertible to XML.

JeffHowden
03-27-2007, 06:57 AM
Should it not go into Ext.data.Node?

Any node of any tree-structured dataset should be convertible to XML.

That's what I thought too, but putting it there, for some reason, doesn't result in it becoming a method of the TreePanel. Ideally, I should be able to call tree.toJsonString() (assuming tree is a reference to my TreePanel instance) and receive an encoded JSON string representation of the tree. However, it's not exposed as a method in any location I can find when it's defined as a method of Ext.data.Node. FWIW, I tried defining at on both Ext.data.Tree and Ext.data.Node (both in /source/data/Tree.js) with no luck.

Animal
03-27-2007, 07:07 AM
No, that won't expose itself directly to users of Ext.tree.TreeNode, but it's the correct place to put the complexity. It's an operation upon data, not a UI operation.

As far as the UI classes are concerned, Ext.tree.TreeNode should implement a toXML the method itself, and delegate to Ext.data.TreeNode.toXml() on the data node that it is wrapping. Ext.tree.TreePanel should implement a toXML method which delegates to its root node's toXML method.

JeffHowden
03-27-2007, 07:14 AM
Well, that all certainly beyond my beginner-level OO skills. Here's what I've got so far (I know the prototype isn't the right way to go). Maybe you can take it to the next level. ;)

http://ext.vosandhowden.com/deploy/ext-1.0-alpha3/rev-4/examples/tree/tostring.html

Animal
03-27-2007, 08:11 AM
Jeff, I can't run that locally because it pulls data off your server, but can you test with this version of tostring.js?

I've only changed the toXML methods, not touched the JSON stuff yet. I've added a toXML method on ext.data.Tree, and Ext.data.Node.

All the attributes that you pass into the constructor of a node in its config object end up in the "attributes" property, so that's all that needs to be scanned.

I've implemented functions to filter what attrbutes you want to include in the tree. In your test case case, you only wanted the "leaf" and "id" attributes, so I've changed it to do that filtering using this new method.

This might be a buggy being a first draft, but I think it's the way to go


/* tostring.js */

Ext.tree.TreePanel.prototype.toJsonString = function(params, node){
this.params = params || {};
node = node || this.getRootNode();
return Ext.util.JSON.encode(this.nodeToJsonString(params, node));
}
Ext.tree.TreePanel.prototype.nodeToJsonString = function(params, node){
var children = node.childNodes;
var temp = {
id: node.id
};
for(var key in params){
if(node[params[key]]){
temp[key] = node[params[key]];
}else if(node.attributes[params[key]]){
temp[key] = node.attributes[params[key]];
}
}
if(children.length){
temp.children = [];
for(var i = 0; i < children.length; i++){
temp.children.push(this.nodeToJsonString(params, children[i]));
}
}
return temp;
},

/**
* Returns a string of XML that represents the tree
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
Ext.data.Tree.prototype.toXMLString = function(nodeFilter, attributeFilter){
return '\u003C?xml version="1.0"?>\u003Ctree>' +
this.getRootNode.toXMLString(nodeFilter, attributeFilter) +
'\u003C/tree>';
};

/**
* Returns a string of XML that represents the node
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
Ext.data.Node.prototype.toXMLString = function(nodeFilter, attributeFilter){
// Exclude nodes based on caller-supplied filtering function
if (nodeFilter && (nodeFilter(this) == false)) {
return '';
}
var result = '\u003Cnode';

// Add the id attribute unless the attribute filter rejects it.
if (!(attributeFilter && (attributeFilter("id", node.id) == false))) {
result += ' id="' + node.id + '"';

// Add all user-added attributes unless rejected by the attributeFilter.
for(var key in this.attributes) {
if (attributeFilter && (attributeFilter(key, attributes[key]) == false)) {
continue;
}
result += ' ' + key + '="' + node.attributes[key] + '"';
}

// Add child nodes if any
var children = this.childNodes;
var clen = children.length;
if(clen == 0){
result += '/>';
}else{
result += '>';
for(var i = 0; i < clen; i++){
result += children[i].toXMLString(nodeFilter, attributeFilter);
}
result += '\u003C/node>';
}
return result;
};

var TreeTest = function(){
// shorthand
var Tree = Ext.tree;
var Toolbar = Ext.Toolbar;
var tlb, btnXML, btnJSON, btnCascade, tree, root;

return {
tree : null,
root : null,
tlb : null,
init : function(){
var tree = new Tree.TreePanel('tree-div', {
animate: true
, loader: new Tree.TreeLoader({dataUrl: 'get-nodes.cfm'})
, enableDD: true
, containerScroll: true
});

// set the root node
root = new Tree.AsyncTreeNode({
text: 'Ext'
, draggable: false
, id: 'source'
});
tree.setRootNode(root);

// render the tree
tree.render();

// false for not recursive (the default), false to disable animation
root.expand(true);

tlb = new Toolbar('buttons');

tlb.addButton({
text: 'XML (Tree)'
, cls: 'ytb-text-only'
, handler: function(){
Ext.get('output').dom.value = tree.toXmlString(null, function(key, val) {
return (key == 'leaf' || key == 'id');
});
}
});

tlb.addButton({
text: 'XML (Node)'
, cls: 'ytb-text-only'
, handler: function(){
var node = tree.getSelectionModel().getSelectedNode();
if (node) Ext.get('output').dom.value = node.toXmlString(null, function(key, val){
return (key == 'leaf' || key == 'id');
});
}
});

tlb.addButton({
text: 'JSON (Tree)'
, cls: 'ytb-text-only'
, handler: function(){
Ext.get('output').dom.value = tree.toJsonString({
is_leaf: 'leaf'
, name: 'text'
});
}
});

tlb.addButton({
text: 'JSON (Node)'
, cls: 'ytb-text-only'
, handler: function(){
Ext.get('output').dom.value = tree.toJsonString({
is_leaf: 'leaf'
, name: 'text'
}
, tree.getSelectionModel().getSelectedNode());
}
});

tlb.addButton({
text: 'Cascade'
, cls: 'ytb-text-only'
, handler: function(){
Ext.get('output').dom.value = '';
this.tree.getRootNode().cascade(function() {
Ext.get('output').dom.value += this.text + ' [' + this.id + ']\r\n';
});
}
});

this.tree = tree;
this.root = root;
this.tlb = tlb;
}
};
}();

Ext.onReady(TreeTest.init, TreeTest, true);

Animal
03-27-2007, 11:07 AM
Well, so much for off the cuff code! It was rubbish!

I've fixed it up. If you copy the two functions at the top heer into your test page, they should work.

Here it is in my Tree test page which drops into examples/tree. "Update" displays the Tree's XML:

MenuBuilder.js:


/**
* Returns a string of XML that represents the tree
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
Ext.data.Tree.prototype.toXMLString = function(nodeFilter, attributeFilter){
return '\u003C?xml version="1.0"?>\u003Ctree>' +
this.getRootNode().toXMLString(nodeFilter, attributeFilter) +
'\u003C/tree>';
};

/**
* Returns a string of XML that represents the node
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
Ext.data.Node.prototype.toXMLString = function(nodeFilter, attributeFilter){
// Exclude nodes based on caller-supplied filtering function
if (nodeFilter && (nodeFilter(this) == false)) {
return '';
}
var result = '\u003Cnode';

// Add the id attribute unless the attribute filter rejects it.
if (!(attributeFilter && (attributeFilter("id", this.id) == false))) {
result += ' id="' + this.id + '"';
}

// Add all user-added attributes unless rejected by the attributeFilter.
for(var key in this.attributes) {
if (attributeFilter && (attributeFilter(key, this.attributes[key]) == false)) {
continue;
}
if (typeof this.attributes[key] != "object") {
result += ' ' + key + '="' + this.attributes[key] + '"';
}
}

// Add child nodes if any
var children = this.childNodes;
var clen = children.length;
if(clen == 0){
result += '/>';
}else{
result += '>';
for(var i = 0; i < clen; i++){
result += children[i].toXMLString(nodeFilter, attributeFilter);
}
result += '\u003C/node>';
}
return result;
};

/*
*
* Add a couple of overrides only needed during Ext 1.0 Alpha test...
*
*/

Ext.menu.Menubar = function(config){
Ext.applyIf(config, {
plain: true,
cls: ""
});
Ext.menu.Menubar.superclass.constructor.call(this, config);
this.cls += " x-menubar";
if (this.orientation == "vertical") {
this.subMenuAlign = "tl-tr?";
this.cls += " x-vertical-menubar";
} else {
this.subMenuAlign = "tl-bl?";
this.cls += " x-horizontal-menubar";
}
};

Ext.extend(Ext.menu.Menubar, Ext.menu.Menu, {
minWidth : 120,
shadow : false,
orientation: "horizontal",

hide: function(){
if(this.activeItem){
this.activeItem.deactivate();
delete this.activeItem;
}
},

onClick : function(e){
if (this.activeItem) {
this.activeItem.deactivate();
delete this.activeItem;
}
else {
var t;
if(t = this.findTargetItem(e)){
if(t.canActivate && !t.disabled){
this.setActiveItem(t, true);
}
}
this.fireEvent("click", this, e, t);
}
},

onMouseOver : function(e){
if (this.activeItem) {
var t;
if(t = this.findTargetItem(e)){
if(t.canActivate && !t.disabled){
this.setActiveItem(t, true);
}
}
this.fireEvent("mouseover", this, e, t);
}
},

onMouseOut : function(e){}
});

Ext.override(Ext.tree.TreeNode, {
ensureVisible : function(callback){
var c = callback;
var a = Ext.get(this.getUI().anchor);
var treeEl = this.getOwnerTree().getEl()
this.getOwnerTree().expandPath(this.getPath(), false, function() {
a.scrollIntoView(treeEl);
if (c) {
c();
}
});
}
});
/*
*
*
* The functionality of the Menu Builder page
*
*
*/

function initPage() {
Ext.pageLayout = new Ext.BorderLayout(document.body, {
north: {
split:false
},
west: {
initialSize: 200,
titlebar: true,
collapsible: true,
split:true,
animate:true,
minSize: 120,
maxSize: 250
},
center: {
titlebar: false,
autoScroll:true
},
south: {
collapsible: true,
collapsed: true,
split: true,
animate:true,
initialSize: 150,
minSize: 25,
maxSize: 250,
titlebar: true,
autoScroll:false
}
});
Ext.headerRegion = Ext.pageLayout.getRegion("north");
Ext.navRegion = Ext.pageLayout.getRegion("west");
Ext.contentRegion = Ext.pageLayout.getRegion("center");
Ext.messageRegion = Ext.pageLayout.getRegion("south");
Ext.pageTitleBar = Ext.get("page-titlebar");
Ext.pageToolbar = new Ext.Toolbar("page-toolbar");
Ext.pageToolbar.addButton({
id: 'home-btn',
cls:'x-btn-text-icon home-button',
text: 'Home',
handler: function(){}
});
Ext.headerPanel = new Ext.ContentPanel("page-header", {
toolbar: Ext.pageToolbar
});
Ext.navPanel = new Ext.ContentPanel("page-leftnav", {});
Ext.contentElement = Ext.get("page-content");
Ext.contentPanel = new Ext.ContentPanel(Ext.contentElement, {});
Ext.messagePanel = new Ext.ContentPanel("page-message", {});
Ext.messageEl = Ext.messageRegion.collapsedEl.createChild({tag:"div", 'float':"left"});

Ext.pageLayout.beginUpdate();
Ext.headerRegion.add(Ext.headerPanel);
Ext.navRegion.add(Ext.navPanel);
Ext.contentRegion.add(Ext.contentPanel);
Ext.messageRegion.add(Ext.messagePanel);
Ext.pageLayout.endUpdate();

Ext.contentUM = Ext.contentPanel.getUpdateManager();
};

function displayMessage(m) {
Ext.messageEl.update("");
Ext.messageEl.update(m);
Ext.messageEl.addClass("error");
setTimeout(function() {
Ext.messageEl.className = "";
Ext.messageEl.update("");
}, 5000);
};

Ext.onReady(function(){
initPage();
Ext.appMenuBar = new Ext.menu.Menubar({orientation:'horizontal'});
Ext.appMenuBar.add({text: 'Static Data',cls: "aspicio-menu ",menu : {items: [
{text: "Area/Postal/Zip Codes",href: "/aspicio/form/Lister.jsp?listEntityType=Area",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Class Control",href: "/aspicio/form/Lister.jsp?listEntityType=ClassControl",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Companies",href: "/aspicio/form/Lister.jsp?listEntityType=Company",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "House Components",href: "/aspicio/form/Lister.jsp?listEntityType=Component",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Contacts",href: "/aspicio/form/Lister.jsp?listEntityType=Contact",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "ContactTypes",href: "/aspicio/form/Lister.jsp?listEntityType=ContactType",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Countries",href: "/aspicio/form/Lister.jsp?listEntityType=Country",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Country sub-entities",href: "/aspicio/form/Lister.jsp?listEntityType=CountrySubEntity",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Currencies",href: "/aspicio/form/Lister.jsp?listEntityType=Currency",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "DateInfo",icon: "../../resources/images/default/tree/leaf.gif",handler:function(){0}},{text: "Economic Groups",href: "/aspicio/form/Lister.jsp?listEntityType=EconomicGroup",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Languages",href: "/aspicio/form/Lister.jsp?listEntityType=Language",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Markets",href: "/aspicio/form/Lister.jsp?listEntityType=Market",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Main Menu Groupings",href: "/aspicio/form/Lister.jsp?listEntityType=Menu",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Supply-Chain Players",href: "/aspicio/form/Lister.jsp?listEntityType=Player",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Player Types",href: "/aspicio/form/Lister.jsp?listEntityType=PlayerType",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "TimeZones",href: "/aspicio/form/Lister.jsp?listEntityType=TimeZone",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Users",href: "/aspicio/form/Lister.jsp?listEntityType=User",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "User Groups",href: "/aspicio/form/Lister.jsp?listEntityType=UserGroup",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick}
]}},
{text: 'Application Data',cls: "aspicio-menu ",menu : {items: [
{text: "Tax No Validations",href: "/aspicio/form/Lister.jsp?listEntityType=AppTaxNoValidation",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Application Classes",href: "/aspicio/form/Lister.jsp?listEntityType=AppClass",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Application Components",href: "/aspicio/form/Lister.jsp?listEntityType=AppComponent",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick}
]}},
{text: 'Test Menu',cls: "aspicio-menu ",menu : {items: [
{text: "Application Classes",href: "/aspicio/form/Lister.jsp?listEntityType=AppClass",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Economic Groups",href: "/aspicio/form/Lister.jsp?listEntityType=EconomicGroup",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Languages",href: "/aspicio/form/Lister.jsp?listEntityType=Language",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: 'Test YUI Submenus',cls: "aspicio-menu ",menu : {items: [
{text: "Application Classes",href: "/aspicio/form/Lister.jsp?listEntityType=AppClass",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Application Components",href: "/aspicio/form/Lister.jsp?listEntityType=AppComponent",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick}
]}},
{text: "Markets",href: "/aspicio/form/Lister.jsp?listEntityType=Market",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Main Menu Groupings",href: "/aspicio/form/Lister.jsp?listEntityType=Menu",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Supply-Chain Players",href: "/aspicio/form/Lister.jsp?listEntityType=Player",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick},{text: "Player Types",href: "/aspicio/form/Lister.jsp?listEntityType=PlayerType",icon: "../../resources/images/default/tree/leaf.gif",handler:Ext.menuClick}
]}});
Ext.appMenuBar.show(Ext.get('aspicio-menu-bar'),'tl-tl');
});

YAHOO.util.Event.onAvailable('menu-container', function(){
var menuContainer = Ext.get("menu-container");
var menuBox = Ext.get(menuContainer.boxWrap("x-box"));
menuBox.child(".x-box-mc").insertFirst({tag: "h3", html: "Menu"});
var tree = new Ext.tree.TreePanel(menuContainer, {
animate: false,
loader: new Ext.tree.TreeLoader(),
enableDD: true,
ddGroup: "menu-tree"
});
var newNodeCount = 0;

// Build the tree of existing Menu entries.
// In a live environment, ths icon would be a path to a servlet serving an appropriate image for the class
var root = new Ext.tree.AsyncTreeNode({
text:'Nigel\u0027s Menu',id:1,className:'com.aspicio.entity.Menu',allowDrag:false,leaf: false, children: [
{text:'Static Data',id:2,className:'com.aspicio.entity.Menu',allowDrag:true,leaf: false, children: [
{text:'Area/Postal/Zip Codes',id:3,className:'com.aspicio.entity.Menu',componentId:4,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Class Control',id:4,className:'com.aspicio.entity.Menu',componentId:5,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Companies',id:5,className:'com.aspicio.entity.Menu',componentId:6,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'House Components',id:6,className:'com.aspicio.entity.Menu',componentId:7,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Contacts',id:7,className:'com.aspicio.entity.Menu',componentId:8,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'ContactTypes',id:8,className:'com.aspicio.entity.Menu',componentId:9,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Countries',id:9,className:'com.aspicio.entity.Menu',componentId:10,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Country sub-entities',id:10,className:'com.aspicio.entity.Menu',componentId:11,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Currencies',id:11,className:'com.aspicio.entity.Menu',componentId:12,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'DateInfo',id:12,className:'com.aspicio.entity.Menu',componentId:13,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Economic Groups',id:13,className:'com.aspicio.entity.Menu',componentId:14,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Languages',id:14,className:'com.aspicio.entity.Menu',componentId:15,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Markets',id:15,className:'com.aspicio.entity.Menu',componentId:16,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Main Menu Groupings',id:16,className:'com.aspicio.entity.Menu',componentId:17,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Supply-Chain Players',id:17,className:'com.aspicio.entity.Menu',componentId:18,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Player Types',id:18,className:'com.aspicio.entity.Menu',componentId:19,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'TimeZones',id:19,className:'com.aspicio.entity.Menu',componentId:20,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Users',id:20,className:'com.aspicio.entity.Menu',componentId:21,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'User Groups',id:31,className:'com.aspicio.entity.Menu',componentId:22,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true}
]},
{text:'Application Data',id:22,className:'com.aspicio.entity.Menu',allowDrag:true,leaf: false, children: [
{text:'Tax No Validations',id:32,className:'com.aspicio.entity.Menu',componentId:3,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Application Classes',id:23,className:'com.aspicio.entity.Menu',componentId:1,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Application Components',id:24,className:'com.aspicio.entity.Menu',componentId:2,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true}
]},
{text:'Test Menu',id:33,className:'com.aspicio.entity.Menu',allowDrag:true,leaf: false, children: [
{text:'Application Classes',id:43,className:'com.aspicio.entity.Menu',componentId:1,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Economic Groups',id:34,className:'com.aspicio.entity.Menu',componentId:14,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Languages',id:35,className:'com.aspicio.entity.Menu',componentId:15,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Test YUI Submenus',id:44,className:'com.aspicio.entity.Menu',allowDrag:true,leaf: false, children: [
{text:'Application Classes',id:45,className:'com.aspicio.entity.Menu',componentId:1,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Application Components',id:46,className:'com.aspicio.entity.Menu',componentId:2,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true}
]},
{text:'Markets',id:36,className:'com.aspicio.entity.Menu',componentId:16,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Main Menu Groupings',id:37,className:'com.aspicio.entity.Menu',componentId:17,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Supply-Chain Players',id:38,className:'com.aspicio.entity.Menu',componentId:18,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true},
{text:'Player Types',id:39,className:'com.aspicio.entity.Menu',componentId:19,icon:'../../resources/images/default/tree/leaf.gif',allowDrag:true,leaf: true}
]}
]
});
tree.setRootNode(root);
tree.render();
root.expand(true);
root.collapse(true);
root.expand();
tree.animate = true;

// add an inline editor for the nodes
var ge = new Ext.Editor(new Ext.form.TextField({
allowBlank: false,
blankText: 'A name is required',
autoSize: true
}), {
parentEl: tree.getEl(),
alignment: 'tl-tl'
});
ge.on('complete', function(e, value){
ge.node.setText(value);
});

// listen for a click on a node that is already selected
// and start editing. Return false to cancel the default click action.
tree.on('beforeclick', function(node){
if(tree.getSelectionModel().isSelected(node)){
ge.node = node;
ge.startEdit(node.ui.getAnchor(), node.text);
return false;
}
});

// Create the tree's context menu
var treeMenu = new Ext.menu.Menu({id: "menu-tree-menu"});
treeMenu.on("itemclick", function(item) {
switch (item.id) {
case "delete":
this.contextNode.parentNode.removeChild(this.contextNode);
break;
case "new":
var newNode = new Ext.tree.TreeNode({
text: "New Menu " + (++newNodeCount),
className: "com.aspicio.entity.Menu",
leaf:false
});
var n = this.contextNode;
if (n.isLeaf()) {
n.parentNode.insertBefore(newNode, n.nextSibling);
} else {
n.appendChild(newNode);
}
}
}, treeMenu, true);

var deleteNode = treeMenu.add({
icon: "../../resources/images/default/delete.gif",
id: "delete",
text: "Delete Item"
});
var newNode = treeMenu.add({
icon: "../../resources/images/default/new.gif",
id: "new",
text: "New Submenu"
});
tree.on("contextmenu", function(node, e) {
contextNode = node;
if (node.isRoot) {
deleteNode.disable();
} else {
deleteNode.enable();
}
treeMenu.contextNode = node;
node.select();
treeMenu.showAt(e.getXY());
});

var menuForm = Ext.get("menuForm");
menuForm.on("submit", function(e)
{
e.stopEvent();
menuForm.dom.elements["updateXML"].value = tree.toXMLString(function(node){
}, function(key, val) {
if ((key == "id") && (typeof val == "string") && (val.match(/^ynode/))) {
return false;
}
return (["componentId", "allowDrag", "leaf", "icon"].indexOf(key) == -1);
});
alert(menuForm.dom.elements["updateXML"].value);
});
});

// Build the source of application Components which may be dragged into the Menu tree
YAHOO.util.Event.onAvailable('component-container', function(){
var componentContainer = Ext.get("component-container");
var componentBox = Ext.get(componentContainer.boxWrap("x-box"));
componentBox.child(".x-box-mc").insertFirst({tag: "h3", html: "Components"});
var components = [
{id:1, desc: "Application Classes", 'class': "com.aspicio.entity.Component", entity: "AppEntity"},
{id:2, desc: "Application Components", 'class': "com.aspicio.entity.Component", entity: "AppComponent"},
{id:3, desc: "Tax No Validations", 'class': "com.aspicio.entity.Component", entity: "AppTaxNoValidation"},
{id:4, desc: "Area/Postal/Zip Codes", 'class': "com.aspicio.entity.Component", entity: "Area"},
{id:5, desc: "Class Control", 'class': "com.aspicio.entity.Component", entity: "ClassControl"},
{id:6, desc: "Companies", 'class': "com.aspicio.entity.Component", entity: "Company"},
{id:7, desc: "House Components", 'class': "com.aspicio.entity.Component", entity: "Component"},
{id:8, desc: "Contacts", 'class': "com.aspicio.entity.Component", entity: "Contact"},
{id:9, desc: "ContactTypes", 'class': "com.aspicio.entity.Component", entity: "ContactType"},
{id:10, desc: "Countries", 'class': "com.aspicio.entity.Component", entity: "Country"},
{id:11, desc: "Country sub-entities", 'class': "com.aspicio.entity.Component", entity: "CountrySubEntity"},
{id:12, desc: "Currencies", 'class': "com.aspicio.entity.Component", entity: "Currency"},
{id:13, desc: "DateInfo", 'class': "com.aspicio.entity.Component", entity: "Currency"},
{id:14, desc: "Economic Groups", 'class': "com.aspicio.entity.Component", entity: "EconomicGroup"},
{id:15, desc: "Languages", 'class': "com.aspicio.entity.Component", entity: "Language"},
{id:16, desc: "Markets", 'class': "com.aspicio.entity.Component", entity: "Market"},
{id:17, desc: "Main Menu Groupings", 'class': "com.aspicio.entity.Component", entity: "Menu"},
{id:18, desc: "Supply-Chain Players", 'class': "com.aspicio.entity.Component", entity: "Player"},
{id:19, desc: "Player Types", 'class': "com.aspicio.entity.Component", entity: "PlayerType"},
{id:20, desc: "TimeZones", 'class': "com.aspicio.entity.Component", entity: "TimeZone"},
{id:21, desc: "Users", 'class': "com.aspicio.entity.Component", entity: "User"},
{id:22, desc: "User Groups", 'class': "com.aspicio.entity.Component", entity: "UserGroup"},
{id:23, desc: "Menu Builder", 'class': "com.aspicio.entity.Component", entity: "Menu"}
];

var CompRecord = Ext.data.Record.create([
{name: 'id'},
{name: 'class'},
{name: 'entity'},
{name: 'desc'}
]);
var reader = new Ext.data.JsonReader({
root: 'components',
}, CompRecord);
var ds = new Ext.data.Store({
proxy: new Ext.data.MemoryProxy({'components': components}),
reader: reader,
remoteSort: true
});

// Create the View of available Components.
// Each draggable element is in a DIV labelled with CSS class "app-component"
// In a live environment, the image src would be a servlet serving up an appropriate image for the class.
var view = new Ext.View(componentContainer,
'<div id="component_{id}" class="{class} app-component">' +
'../../resources/images/default/tree/leaf.gif?entity={entity}{desc}</div>', {
multiSelect: true,
selectedClass: 'component-selected',
jsonRoot: 'components',
store: ds
});
view.getEl().unselectable();
var dragZone = new ComponentDragZone(view, {
containerScroll:true,
ddGroup: 'menu-tree'
});
ds.load();
});

/**
* Create a DragZone instance for our JsonView
*/
ComponentDragZone = function(view, config){
this.view = view;
ComponentDragZone.superclass.constructor.call(this, view.getEl(), config);
};
Ext.extend(ComponentDragZone, Ext.dd.DragZone, {
// We don't want to register our Component elements, so let's
// override the default registry lookup to fetch the Component
// from the event instead by finding the element with the known CSS class "app-component"
getDragData : function(e) {
e = Ext.EventObject.setEvent(e);
var target = e.getTarget('.app-component');
if(target){
var view = this.view;
if(!view.isSelected(target)){
view.select(target, e.ctrlKey);
}
var selNodes = view.getSelectedNodes();
var dragData = {
nodes: selNodes
};
if(selNodes.length == 1){
dragData.ddel = target.cloneNode(true); // the div element
dragData.single = true;
}else{
var div = document.createElement('div'); // create the multi element drag "ghost"
div.className = 'multi-proxy';
for(var i = 0, len = selNodes.length; i < len; i++){
div.appendChild(selNodes[i].cloneNode(true));
}
dragData.ddel = div;
dragData.multi = true;
}
return dragData;
}
return false;
},

/*
* This method is called by the TreeDropZone after a node drop.
* The result of the overriden method above, "getDragData" is in "this.dragData".
* We use this data to create a new node (or nodes) for the tree.
*/
getTreeNode : function(data, targetNode, point, e){
var folderNode = ((point == "above") || (point == "below")) ? targetNode.parentNode : targetNode;
var treeNodes = [];
var nodeData = this.dragData.nodes;
for(var i = 0, len = nodeData.length; i < len; i++){
var data = nodeData[i];
var newId = parseInt(data.id.split("component_")[1]);
var newClassName = data.className.split(" ")[0];

// See if an item for this component exists.
var dup = folderNode.findChildBy(function(node){
var a = node.attributes;
return (((a.className == newClassName) && (a.id == newId)) || (a.componentId == newId));
});

// If we found a duplicate ensure it is visible and highlight it
if (dup) {
dup.ensureVisible(function() {
Ext.get(dup.getUI().anchor).frame('red', 1);
displayMessage("Component already in Menu");
});
} else {
var newText = data.childNodes[1].nodeValue;
treeNodes.push(new Ext.tree.TreeNode({
icon: data.firstChild.src,
text: newText,
id: newId,
className: newClassName,
leaf:true
}));
}
}
return (treeNodes.length > 0) ? treeNodes : false;
},

/**
* The default action is to "highlight" after a bad drop
* but framing in red is better
*/
afterRepair:function(){
for(var i = 0, len = this.dragData.nodes.length; i < len; i++){
Ext.fly(this.dragData.nodes[i]).frame('red', 1);
}
this.dragging = false;
},

/** override the default repairXY with one offset for the margins and padding. */
getRepairXY : function(e){
if(!this.dragData.multi){
var xy = Ext.Element.fly(this.dragData.ddel).getXY();
xy[0]+=3;xy[1]+=3;
return xy;
}
return false;
}
});


MenuBuilder.html:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<title>Building a custom menu. Submitting tree data in XML form</title>
<link rel="stylesheet" type="text/css" href="../../resources/css/ext-all.css" />
<script type="text/javascript" src="../../yui-utilities.js"></script>
<script type="text/javascript" src="../../ext-yui-adapter.js"></script>
<script type="text/javascript" src="../../ext-all-debug.js"></script>
<script type="text/javascript" src="MenuBuilder.js"></script>
<style type="text/css">
.x-menubar {
background-color:#deecfd
}
.x-horizontal-menubar {
width:100%!important;
border-left:0px none;
border-right:0px none;
}
.x-horizontal-menubar > .x-menu-list {
float:left;
width:100%;
border-left:0px none;
border-right:0px none;
}
.x-horizontal-menubar > .x-menu-list > .x-menu-list-item {
text-decoration:none;
padding-left:0px;
padding-right:10px;
margin-left:10px;
float:left;
background-image:none
}
.x-horizontal-menubar > .x-menu-list > .x-menu-list-item.x-menu-item-active {
text-decoration:none;
padding-left:0px;
padding-right:10px;
margin-left:10px;
float:left;
background-image:none;
border:0px none;
}
.x-horizontal-menubar > .x-menu-list > .x-menu-list-item > .x-menu-item-arrow {
background: transparent url(../../resources/images/default/menu/menubar-parent.gif) no-repeat scroll right 0.6em;
padding-right:15px;
}
.x-menubar > .x-menu-list > .x-menu-list-item > .x-menu-item > .x-menu-item-icon {
display:none;
}
</style>
<style type="text/css">
* {
font-family: arial, helvetica, sans-serif;
font-size:small;
color:rgb(102, 102, 102);
}

div#page-title-container {
height:40px;
}

div#page-titlebar {
float:left;
color:white;
font-family:arial;
font-size:30px;
font-weight:300;
padding:2px 0px 0px 2px;
text-align:left;
}

div#page-toolbar {
float:right;
margin-right:5px;
margin-top:7px;
}

div#aspicio-menu-bar {
height:25px;
}

#menu-container {
height:500px;
width:400px;
overflow:auto;
}
#component-container {
height:500px;
width:400px;
overflow:auto;
white-space:nowrap;
}
.app-component {
cursor:pointer;
background-color:lightblue;
border-bottom:1px groove;
}
.component-selected {
background-color:#000070!important;
color:white;
}
.error {
color: red;
font-weight:bold
}
</style>
<title>Menu maintenance</title>
</head>
<body class="x-box-blue">
<div id="page-header">
<div id="page-title-container" class="x-layout-panel-hd">
<div id="page-titlebar">Menu maintenance</div>
<div id="page-toolbar"></div>
</div>
<div id="aspicio-menu-bar"></div>
</div>
<div id="page-leftnav"></div>
<div id="page-content">
<div style="padding:10px">
<div style="float:left;margin-right:10px">
<div id="menu-container"></div>
</div>
<div style="float:left">
<div id="component-container"></div>
</div>
<form id="menuForm" name="menuForm" style="padding-top:10px;clear:both" method="POST" >
<input type="hidden" name="menuId" value="1"></input>
<input type="hidden" name="updateXML"></input>
<input type="submit" value="Update"></input>
</form>
</div>
</div>
<div id="page-message"></div>
</body>
</html>

MD
03-29-2007, 07:41 PM
Wow! You guys truly made my week, thanks Gents ;D

Both examples worked great, and are precisely what I was looking for. Never to look a Gift Horse in the mouth, but any chance of converting the toJSON function as well?

Really looking forward to seeing this officially integrated into Ext, though I know Jack's swamped.

Animal
04-03-2007, 04:02 AM
I'll have a go at making the toJSON methods the same as the toXML methods today sometime...

Animal
04-03-2007, 06:06 AM
Jeff, can you put this "tostring.js" into your example page so we can test it?

I've added the toJsonString methods to Tree and Node. I've polished the code a little too.


/* tostring.js */

/**
* Returns a string of Json that represents the tree
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
Ext.data.Tree.prototype.toJsonString = function(nodeFilter, attributeFilter){
return this.getRootNode().toJsonString(nodeFilter, attributeFilter);
};

/**
* Returns a string of Json that represents the node
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
Ext.data.Node.prototype.toJsonString = function(nodeFilter, attributeFilter){
// Exclude nodes based on caller-supplied filtering function
if (nodeFilter && (nodeFilter(this) == false)) {
return '';
}
var c = false, result = "{";

// Add the id attribute unless the attribute filter rejects it.
if (!attributeFilter || attributeFilter("id", this.id)) {
result += '"id:"' + this.id;
c = true;
}

// Add all user-added attributes unless rejected by the attributeFilter.
for(var key in this.attributes) {
if ((key != 'id') && (!attributeFilter || attributeFilter(key, this.attributes[key]))) {
if (c) result += ',';
result += '"' + key + '":"' + this.attributes[key] + '"';
c = true;
}
}

// Add child nodes if any
var children = this.childNodes;
var clen = children.length;
if(clen != 0){
if (c) result += ',';
result += '"children":['
for(var i = 0; i < clen; i++){
if (i > 0) result += ',';
result += children[i].toJsonString(nodeFilter, attributeFilter);
}
result += ']';
}
return result + "}";
};

/**
* Returns a string of XML that represents the tree
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
Ext.data.Tree.prototype.toXmlString = function(nodeFilter, attributeFilter){
return '\u003C?xml version="1.0"?>\u003Ctree>' +
this.getRootNode().toXmlString(nodeFilter, attributeFilter) +
'\u003C/tree>';
};

/**
* Returns a string of XML that represents the node
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
Ext.data.Node.prototype.toXmlString = function(nodeFilter, attributeFilter){
// Exclude nodes based on caller-supplied filtering function
if (nodeFilter && (nodeFilter(this) == false)) {
return '';
}
var result = '\u003Cnode';

// Add the id attribute unless the attribute filter rejects it.
if (!attributeFilter || attributeFilter("id", this.id)) {
result += ' id="' + this.id + '"';
}

// Add all user-added attributes unless rejected by the attributeFilter.
for(var key in this.attributes) {
if ((key != 'id') && (!attributeFilter || attributeFilter(key, this.attributes[key]))) {
result += ' ' + key + '="' + this.attributes[key] + '"';
}
}

// Add child nodes if any
var children = this.childNodes;
var clen = children.length;
if(clen == 0){
result += '/>';
}else{
result += '>';
for(var i = 0; i < clen; i++){
result += children[i].toXmlString(nodeFilter, attributeFilter);
}
result += '\u003C/node>';
}
return result;
};

var TreeTest = function(){
// shorthand
var Tree = Ext.tree;
var Toolbar = Ext.Toolbar;
var tlb, btnXML, btnJSON, btnCascade, tree, root;

return {
tree : null,
root : null,
tlb : null,
init : function(){
var tree = new Tree.TreePanel('tree-div', {
animate: true
, loader: new Tree.TreeLoader({dataUrl: 'get-nodes.cfm'})
, enableDD: true
, containerScroll: true
});

// set the root node
root = new Tree.AsyncTreeNode({
text: 'Ext'
, draggable: false
, id: 'source'
});
tree.setRootNode(root);

// render the tree
tree.render();

// false for not recursive (the default), false to disable animation
root.expand(true);

tlb = new Toolbar('buttons');

tlb.addButton({
text: 'XML (Tree)'
, cls: 'ytb-text-only'
, handler: function(){
Ext.get('output').dom.value = tree.toXmlString(null, function(key, val) {
return (key == 'leaf' || key == 'id');
});
}
});

tlb.addButton({
text: 'XML (Node)'
, cls: 'ytb-text-only'
, handler: function(){
var node = tree.getSelectionModel().getSelectedNode();
if (node) Ext.get('output').dom.value = node.toXmlString(null, function(key, val){
return (key == 'leaf' || key == 'id');
});
}
});

tlb.addButton({
text: 'JSON (Tree)'
, cls: 'ytb-text-only'
, handler: function(){
Ext.get('output').dom.value = tree.toJsonString(null, function(key, val) {
return (key == 'leaf' || key == 'id');
});
}
});

tlb.addButton({
text: 'JSON (Node)'
, cls: 'ytb-text-only'
, handler: function(){
var node = tree.getSelectionModel().getSelectedNode();
if (node) Ext.get('output').dom.value = node.toJsonString(null, function(key, val){
return (key == 'leaf' || key == 'id');
});
}
});

tlb.addButton({
text: 'Cascade'
, cls: 'ytb-text-only'
, handler: function(){
Ext.get('output').dom.value = '';
this.tree.getRootNode().cascade(function() {
Ext.get('output').dom.value += this.text + ' [' + this.id + ']\r\n';
});
}
});

this.tree = tree;
this.root = root;
this.tlb = tlb;
}
};
}();

Ext.onReady(TreeTest.init, TreeTest, true);

JeffHowden
04-03-2007, 11:41 PM
Done and works very nicely. The only thing missing that was in my original implementation is the ability to map attributes to different names, if necessary. For example, maybe you wanted to have the "leaf" attribute outputted as "is_leaf". Or, maybe you want the text of the node included in a "name" attribute. Obviously this functionality isn't super-critical, but could be really useful in the event you have to interface with a system with very specific requirements that can't be changed to suit the specific way these methods output the tree or selected node.

Animal
04-04-2007, 04:21 AM
Yes, an optional mapping parameter might be useful in some cases. Thanks for hosting the experimental code. It looks like it works OK.

One thing I've noticed is that if you don't expand the nodes, the nodes aren't there. If you click the "toXML" button you get a small document. If you click the Expand All button and then redo the "toXML", you get a larger document.

Perhaps the toXml/toJson functions should expand the nodes they scan (restoring their state afterwards) so that a true picture of the tree emerges? If this is going to be used for sending updates back to a server, then accurately representing the data is essential. I'll have another look at the code today.

JeffHowden
04-04-2007, 11:41 AM
Yes, an optional mapping parameter might be useful in some cases. Thanks for hosting the experimental code. It looks like it works OK.

I was thinking last night about a couple of things.

1. The argument signature should probably be changed to an object literal so additions like this don't break anything for anyone and the object literal is self-documenting.

2. For ultimate flexibility, the XML functions should really have a tagMap and an attrMap, while the JSON functions only need an attrMap.

One thing I've noticed is that if you don't expand the nodes, the nodes aren't there. If you click the "toXML" button you get a small document. If you click the Expand All button and then redo the "toXML", you get a larger document.

Yes, it's an async tree so it doesn't about any child nodes that haven't been loaded. That's why I originally had it using expand(true) on the root. In testing though, it got tiresome to have it constantly do that at the beginning of every refresh.

Perhaps the toXml/toJson functions should expand the nodes they scan (restoring their state afterwards) so that a true picture of the tree emerges? If this is going to be used for sending updates back to a server, then accurately representing the data is essential. I'll have another look at the code today.

Yeah, that's certainly something I've considered, but the code to achieve it will be burdensome, I'd think. Perhaps the documentation can simply suggest that async trees should make a single load of JSON data that contains all the children in the response. That should at least help with the timing issues.

Animal
04-06-2007, 02:50 AM
I added an attrMap parameter yesterday because I had to. My XML structure maps exactly to my Java structure, and the element attribute names have to be the bean property names for automatic updating.

So a Node's "text" attribute had to map to the XML attribute "description".

Also, some more of a Node's internal attributes have to be ignored: "loader" should not be sent pack to the server.

I'll post the new code on Tuesday. (Public holiday here in the UK on Friday and Monday)

ehauser
04-10-2007, 08:35 PM
A couple of suggestions about on the toJsonString function:


Shouldn't it return an array of JSON objects by default?
It assumes that all of the values are strings. The filter gives you the option of filtering out values by key, but uiProvider should probably be excluded by default since it will always be a function
Otherwise, worked great for me. Thanks for posting the example.

ehauser
04-11-2007, 12:36 PM
Shouldn't it return an array of JSON objects by default?

Scratch that one. I see that the returned JSON object is the root tree node and you traverse from there.

Animal
04-13-2007, 09:57 AM
Here are the latest toXXXXString functions as overrides which is much neater:


Ext.override(Ext.data.Tree, {
/**
* Returns a string of XML that represents the tree
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
toXmlString: function(nodeFilter, attributeFilter){
return '\u003C?xml version="1.0"?>\u003Ctree>' +
this.getRootNode().toXmlString(nodeFilter, attributeFilter) +
'\u003C/tree>';
},

/**
* Returns a string of Json that represents the tree
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @return {String}
*/
toJsonString: function(nodeFilter, attributeFilter){
return this.getRootNode().toJsonString(nodeFilter, attributeFilter);
}
});

Ext.override(Ext.data.Node, {
/**
* Default attribute filter.
* Rejects functions, "id", "children" and "loader"
*/
attributeFilter: function(attName, attValue) {
return (typeof attValue != 'function') && (attName != 'id') &&
(attName != 'children') && (attName != 'loader') &&
(attName != 'expanded') && (attName != 'allowDrag') &&
(attName != 'iconCls') && (attName != 'leaf');
},

/**
* Returns a string of XML that represents the node
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @param {Array} (Optional) An associative array mapping Node attribute names to XML attribute names.
* @return {String}
*/
toXmlString: function(nodeFilter, p_attributeFilter, attrMap){
// Exclude nodes based on caller-supplied filtering function
if (nodeFilter && (nodeFilter(this) == false)) {
return '';
}
var result = '\u003Cnode';

// Create our attribute filter from a sequence of the user-supplied attribute filter,
// and the Node's standard attribute filter.
// Add the id attribute unless the user attribute filter rejects it.
var attributeFilter;
if (p_attributeFilter) {
if (p_attributeFilter("id", this.id)) {
result += ' id="' + this.id + '"';
}
attributeFilter = p_attributeFilter.createInterceptor(this.attributeFilter);
} else {
result += ' id="' + this.id + '"';
attributeFilter = this.attributeFilter;
}

// Add all user-added attributes unless rejected by the attributeFilter.
for(var key in this.attributes) {
if (attributeFilter(key, this.attributes[key])) {
result += ' ' + (attrMap ? (attrMap[key] || key) : key) + '="' + this.attributes[key] + '"';
}
}

// Add child nodes if any
var children = this.childNodes;
var clen = children.length;
if(clen == 0){
result += '/>';
}else{
result += '>';
for(var i = 0; i < clen; i++){
result += children[i].toXmlString(nodeFilter, p_attributeFilter, attrMap);
}
result += '\u003C/node>';
}
return result;
},

/**
* Returns a string of Json that represents the node
* @param {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
* @param {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
* @param {Array} (Optional) An associative array mapping Node attribute names to XML attribute names.
* @return {String}
*/
toJsonString: function(nodeFilter, p_attributeFilter, attrMap){
// Exclude nodes based on caller-supplied filtering function
if (nodeFilter && (nodeFilter(this) == false)) {
return '';
}
var c = false, result = "{";

// Create our attribute filter from a sequence of the user-supplied attribute filter,
// and the Node's standard attribute filter.
// Add the id attribute unless the user attribute filter rejects it.
var attributeFilter;
if (p_attributeFilter) {
if (p_attributeFilter("id", this.id)) {
result += '"id":"' + this.id + '"';
c = true;
}
attributeFilter = p_attributeFilter.createInterceptor(this.attributeFilter);
} else {
result += '"id":"' + this.id + '"';
c = true;
attributeFilter = this.attributeFilter;
}

// Add all user-added attributes unless rejected by the attributeFilter.
for(var key in this.attributes) {
if (attributeFilter(key, this.attributes[key])) {
if (c) result += ',';
result += '"' + (attrMap ? (attrMap[key] || key) : key) + '":"' + this.attributes[key] + '"';
c = true;
}
}

// Add child nodes if any
var children = this.childNodes;
var clen = children.length;
if(clen != 0){
if (c) result += ',';
result += '"children":['
for(var i = 0; i < clen; i++){
if (i > 0) result += ',';
result += children[i].toJsonString(nodeFilter, p_attributeFilter, attrMap);
}
result += ']';
}
return result + "}";
}
});

JeffHowden
04-13-2007, 02:50 PM
I've updated my local copy and it works beautifully.

http://ext.vosandhowden.com/deploy/ext-1.0-beta2/examples/tree/tostring.html

mxracer
04-15-2007, 05:27 PM
Hey Guys,

I noticed that in some of the code posted, people are using code similar to the following to iterate through a nodes childnodes:


function deleteKids(node){
var kids = node.childNodes;
var len = kids.length;
for (var i = 0; i < len; i++){
deleteKids(kids[i]);
console.log("Deleting: "+[kids[i].text]); // display in Firebug console
}
}

// delete the kids
deleteKids(node);
// now delete the node
pn = node.parentNode;
console.log("Deleting: "+node.text); // display in Firebug console
pn.removeChild(node);


Here is what I am using that is a little more simple:


function delNode(node){
node.eachChild(delNode);
console.log("Deleting: "+node.text); // display in Firebug console
}

// delete child nodes
node.eachChild(delNode);
// now delete the node
pn = node.parentNode;
console.log("Deleting: "+node.text); // display in Firebug console
pn.removeChild(node);

Animal
04-16-2007, 04:01 AM
BOth those versions will only delete half the nodes. They loop upwards through the Array.

So you'll delete element 0. Element 1 will then move down the Array to be element 0, and the loop will go up to element 1. The new element 0 will be left there.

You have to either remove element 0 until ther Array is zero length, or iterate backwards through the Array.

sgonzalezg
04-17-2007, 11:34 AM
Hello
For my thesis or graduation work I need to build a dynamic tree where the data of that tree are taken out of the database with some conditional ones. The examples that he/she brings the yui-ext are very good but they don't work and me serious useful that was as that because that search the data in a file php. I ask them them to help me with an example of the yui but that if it works.

Thank you

Animal
04-18-2007, 12:24 PM
What do you mean by "they don't work"?

I'm sure there might be a bug or two lurking in the Tree classes, but they most definitely "work". Lots of people are using them.

jack.slocum
04-22-2007, 08:30 AM
Animal, think TreeSerializer classes. What would be really cool about a Serializer is it could be customized to serialize only the info you want (e.g. a TreeStateSerializer subclass), not just the node structure.

This could/should be coupled with Deserializers of course. By separating them into their own class you get more flexibility for customizing them, only including them if you need them and you keep from having toXXX methods for all possible options. My guess is it would work similar to TreeSorter/TreeFilter.

Animal
04-22-2007, 01:13 PM
Something like this:


/**
* @class Ext.tree.TreeSerializer
* A base class for implementations which provide serialization of an
* {@link Ext.tree.TreePanel}.
* <p>
* Implementations must provide a toString method which returns the serialized
* representation of the tree.
*
* @constructor
* @param {TreePanel} tree
* @param {Object} config
*/
Ext.tree.TreeSerializer = function(tree, config){
if (typeof this.toString !== 'function') {
throw 'Ext.tree.TreeSerializer implementation does not implement toString()';
}
this.tree = tree;
if (this.attributeFilter) {
this.attributeFilter = this.attributeFilter.createInterceptor(this.defaultAttributeFilter);
} else {
this.attributeFilter = this.defaultAttributeFilter;
}
if (this.nodeFilter) {
this.nodeFilter = this.nodeFilter.createInterceptor(this.defaultNodeFilter);
} else {
this.nodeFilter = this.defaultFilter;
}
Ext.apply(this, config);
};

Ext.tree.TreeSerializer.prototype = {

/*
* @cfg nodeFilter {Function} (optional) A function, which when passed the node, returns true or false to include
* or exclude the node.
*/
/*
* @cfg attributeFilter {Function} (optional) A function, which when passed an attribute name, and an attribute value,
* returns true or false to include or exclude the attribute.
*/
/*
* @cfg attributeMap {Array} (Optional) An associative array mapping Node attribute names to XML attribute names.
*/

/* @private
* Array of node attributes to ignore.
*/
standardAttributes ["expanded", "allowDrag", "allowDrop", "disabled", "icon",
"cls", "iconCls", "href", "hrefTarget", "qtip", "singleClickExpand", "uiProvider"];


/** @private
* Default attribute filter.
* Rejects functions and standard attributes.
*/
defaultAttributeFilter: function(attName, attValue) {
return (typeof attValue != 'function') &&
(this.standardAttributes.indexOf(attName) == -1);
},

/** @private
* Default node filter.
* Accepts all nodes.
*/
defaultNodeFilter: function(node) {
return true;
}
};

/**
* @class Ext.tree.XmlTreeSerializer
* An implementation of Ext.tree.TreeSerializer which serializes an
* {@link Ext.tree.TreePanel} to an XML string.
*/
Ext.tree.XmlTreeSerializer = function(tree, config){
Ext.tree.XmlTreeSerializer.superclass.constructor.apply(this, arguments);
};

Ext.extend(Ext.tree.XmlTreeSerializer, Ext.tree.TreeSerializer, {
/**
* Returns a string of XML that represents the tree
* @return {String}
*/
toString: function(nodeFilter, attributeFilter){
return '\u003C?xml version="1.0"?>\u003Ctree>' +
nodeToString(this.getRootNode()) + '\u003C/tree>';
},

/**
* Returns a string of XML that represents the node
* @param {Object} node The node to serialize
* @return {String}
*/
nodeToString: function(node){
if (!this.nodeFilter(node)) {
return '';
}
var result = '\u003Cnode';
if (this.attributeFilter("id", node.id)) {
result += ' id="' + node.id + '"';
}

// Add all user-added attributes unless rejected by the attributeFilter.
for(var key in node.attributes) {
if (this.attributeFilter(key, node.attributes[key])) {
result += ' ' + (this.attributeMap ? (this.attributeMap[key] || key) : key) + '="' + node.attributes[key] + '"';
}
}

// Add child nodes if any
var children = node.childNodes;
var clen = children.length;
if(clen == 0){
result += '/>';
}else{
result += '>';
for(var i = 0; i < clen; i++){
result += this.nodeToString(children[i]);
}
result += '\u003C/node>';
}
return result;
}

});

/**
* @class Ext.tree.JsonTreeSerializer
* An implementation of Ext.tree.TreeSerializer which serializes an
* {@link Ext.tree.TreePanel} to a Json string.
*/
Ext.tree.JsonTreeSerializer = function(tree, config){
Ext.tree.JsonTreeSerializer.superclass.constructor.apply(this, arguments);
};

Ext.extend(Ext.tree.JsonTreeSerializer, Ext.tree.TreeSerializer, {

/**
* Returns a string of Json that represents the tree
* @return {String}
*/
toString: function(){
return this.nodeToString(this.getRootNode());
}

/**
* Returns a string of Json that represents the node
* @param {Object} node The node to serialize
*/
nodeToString: function(node){
// Exclude nodes based on caller-supplied filtering function
if (!this.nodeFilter(node)) {
return '';
}
var c = false, result = "{";
if (this.attributeFilter("id", node.id)) {
result += '"id":"' + node.id + '"';
c = true;
}

// Add all user-added attributes unless rejected by the attributeFilter.
for(var key in node.attributes) {
if (this.attributeFilter(key, node.attributes[key])) {
if (c) result += ',';
result += '"' + (this.attributeMap ? (this.attributeMap[key] || key) : key) + '":"' + node.attributes[key] + '"';
c = true;
}
}

// Add child nodes if any
var children = node.childNodes;
var clen = children.length;
if(clen != 0){
if (c) result += ',';
result += '"children":['
for(var i = 0; i < clen; i++){
if (i > 0) result += ',';
result += this.nodeToString(children[i]);
}
result += ']';
}
return result + "}";
}
};

sgonzalezg
04-22-2007, 02:59 PM
Excuse if it expresses me bad, what I meant was that when I open the example it never achieves it to see because the tree is never possible to build. When the he requests me this dir

loader: new Tree.TreeLoader({dataUrl: 'get-nodes.php'}),

I put it to him well but nape ends up calling to this it paginates 'get-nodes.php' and the truth has attempted it in several ways but I have not achieved it. I am sure that the example is very good but I am not able to prove it. Also excuse my English but it is that I am Cuban

Thank you

KimH
04-23-2007, 04:11 AM
Animal... great code as always!

Jack... will and can this be put in the next rev?

akira
06-25-2007, 04:41 PM
http://ext.vosandhowden.com/deploy/ext-1.0-beta2/examples/tree/tostring.html
I just tried this out, it seems to be what I

JeffHowden
06-28-2007, 01:34 AM
[QUOTE=akira;41470]http://ext.vosandhowden.com/deploy/ext-1.0-beta2/examples/tree/tostring.html
I just tried this out, it seems to be what I

aconran
07-18-2007, 09:06 AM
Impressive work Jeff and Animal, great job!

I was thinking about this earlier today, but from a design patterns point of view what would the TreeFilter, TreeSorter and TreeSerializer classes be considered? Their syntax is a bit odd to use in the sense of typical JS. You are creating a new object which you throw away and it adds behavior/functionality to the existing tree.

new Ext.tree.TreeSorter(tree, cfg);


They almost seem to behave like a decorator of the tree except it changes the original object and adds to it instead of wrapping it and returning it as a new object. Because there are no books on JavaScript design patterns it is sometimes difficult to see how typical Java/C++ design patterns apply to JS.

Any thoughts?

Thanks,
Aaron Conran

george.antoniadis
09-25-2007, 06:51 AM
Has anyone tried to make toJsonString and toXmlString play with ext2.0? :)

RedoX
10-17-2007, 10:49 AM
I'm sorry for posting in a old(er) post.

I'm a bit of a Javascript noob and don't know exactly how to implement this piece of code into mine.

What I want to do is saving this tree with a Ajax request. I'm using the tree for categories in my webshop backend. But how can I give the JSON string in a post request to send it to my Ajax file handler?

I already can drop and drag nodes under different parents (changing the parent is easy) but saving the order in what they are is a bit of my problem. I just can't get it to work.

<script type="text/javascript">

Ext.onReady(function(){
// shorthand
var Tree = Ext.tree;

var tree = new Tree.TreePanel('tree-div', {
autoScroll:true,
animate:true,
enableDD:true,
containerScroll: true,
rootVisible: false
});

// set the root node
var root = new Tree.AsyncTreeNode({
text: 'Category Structure',
draggable:false,
id:'source'
});

tree.setRootNode(root);

bildCategoryTree(root, <?php echo $node; ?>);
// render the tree

tree.addListener('beforenodedrop', categoryMove.createDelegate(this));
// tree.addListener('nodedrop', itemDropped.createDelegate(this));

tree.render();
root.expand();
tree.expandAll();

});

/*
itemDropped = function(obj) {
alert(obj.target.indexOf(obj.dropNode));
}
*/

function bildCategoryTree(parent, config){
if (!config) return null;

if (parent && config && config.length){
for (var i = 0; i < config.length; i++){
var node = new Ext.tree.TreeNode(config[i]);
parent.appendChild(node);
if(config[i].children){
bildCategoryTree(node, config[i].children);
}
}
}
}

categoryMove = function(obj){
var data = {id: obj.dropNode.id}

data.point = obj.point;
switch (obj.point) {
case 'above' :
data.pid = obj.target.parentNode.id;
if (obj.target.previousSibling) {
data.aid = obj.target.previousSibling.id;
} else {
data.aid = 0;
}
break;
case 'below' :
data.pid = obj.target.parentNode.id;
data.aid = obj.target.id;
break;
case 'append' :
data.pid = obj.target.id;
if (obj.target.lastChild) {
data.aid = obj.target.lastChild.id;
} else {
data.aid = 0;
}
break;
default :
obj.cancel = true;
return obj;
}

var success = function(o) {
try {
if(o.responseText){
// alert(o.responseText);
}
}
catch(e) {

}
};
var failure = function(o) {
Ext.dump(o.statusText);
};

var pd = [];
for(var key in data) {
pd.push(encodeURIComponent(key), "=", encodeURIComponent(data[key]), "&");
}
pd.splice(pd.length-1,1);

var con = new Ext.lib.Ajax.request(
'POST',
'http://localhost/backoffice/categories/move/',
{success:success,failure:failure, scope:obj, loaderArea:'tree-div'},
pd.join(""));
}

</script>

How do I implement this Serializer code into mine so I can just update the tree on every action?