Hi,
I looked for it some time ago on this forum and didn't find, so I wrote it.
It allows create, modify, delete and save Tree nodes either in XML or JSON (here it just alerts the resulted tree)
Basically, I modified existing ColumnTree and treeSerialiser code. I'm pretty new to EXTJS so if I done something wrong - please correct it.
editable-column-tree.js
Code:
/*
* Ext JS Library 2.0 RC 1
* Copyright(c) 2006-2007, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
Ext.onReady(function(){
Ext.BLANK_IMAGE_URL = 'ext-2.0/resources/images/default/s.gif';
var tree = new Ext.tree.ColumnTree({
el:'tree-ct',
width:700,
autoHeight:true,
rootVisible:false,
autoScroll:true,
expandable:false,
enableDD:true,
title: 'Menu Configuration',
tbar: [{
text:'Save Menu',
tooltip: 'Save Menu',
iconCls:'save-icon',
listeners: {
'click': function() {
var json = tree.toJsonString(null,
function(key, val) {
return (key == 'leaf' || key == 'id' || key =='menu_item'|| key == 'menu_url');
}, {
menu_item: 'text',
menu_url: 'href'
}
);
alert(json);
},
scope:this
}
},{
xtype:'tbseparator'
},{
text:'Add Folder Item',
tooltip: 'Add Folder Item',
iconCls:'folder-icon',
listeners: {
'click' : function(){
var selectedItem = tree.getSelectionModel().getSelectedNode();
if (!selectedItem) {
selectedItem = tree.getRootNode();
}
handleCreate = function (btn, text, cBoxes){
if(btn == 'ok' && text) {
var newNode = new Ext.tree.TreeNode({
menu_item: text,
menu_url: '',
leaf: false,
expandable: true,
uiProvider: Ext.tree.ColumnNodeUI
});
if(selectedItem.isLeaf()) {
selectedItem.parentNode.insertBefore(newNode, selectedItem.nextSibling);
} else {
selectedItem.insertBefore(newNode, selectedItem.firstChild);
}
}
}
Ext.MessageBox.show({
title:'Add new Folder Item',
msg: 'Name of Folder Item:',
buttons: Ext.MessageBox.OKCANCEL,
prompt:true,
fn: handleCreate
});
}
}
},{
xtype:'tbseparator'
},{
text:'Add Page Item',
tooltip: 'Add Page Item',
iconCls:'page-icon',
listeners: {
'click' : function(){
var selectedItem = tree.getSelectionModel().getSelectedNode();
if (!selectedItem) {
Ext.Msg.alert('Warning', 'Please select an Item after which you want to add a new one.');
return false;
}
handleCreate = function (btn, text, cBoxes){
if(btn == 'ok' && text) {
var newNode = new Ext.tree.TreeNode({
menu_item: text,
menu_url: '',
leaf: true,
allowChildren:false,
uiProvider: Ext.tree.ColumnNodeUI
});
if(selectedItem.isLeaf()) {
selectedItem.parentNode.insertBefore(newNode, selectedItem.nextSibling);
} else {
selectedItem.insertBefore(newNode, selectedItem.firstChild);
}
}
}
Ext.MessageBox.show({
title:'Add new Page Item',
msg: 'Name of Page Item:',
buttons: Ext.MessageBox.OKCANCEL,
prompt:true,
fn: handleCreate
});
}
}
},{
xtype:'tbseparator'
},{
text:'Delete Item',
tooltip: 'Delete Item',
iconCls:'delete-icon',
listeners: {
'click' : function(){
var selectedItem = tree.getSelectionModel().getSelectedNode();
if (!selectedItem) {
Ext.Msg.alert('Warning', 'Please select an Item to delete.');
return false;
}
handleDelete = function (btn){
if(btn == 'ok') {
selectedItem.remove();
}
}
Ext.MessageBox.show({
title:'Confirm your action',
msg: 'Are you sure you want to delete this item and its children?',
buttons: Ext.MessageBox.OKCANCEL,
fn: handleDelete
});
}
}
},{
xtype:'tbseparator'
}],
columns:[{
header:'Menu Item',
width:300,
dataIndex:'menu_item'
},{
header:'URL',
width:398,
dataIndex:'menu_url'
}],
loader: new Ext.tree.TreeLoader({
preloadChildren:true,
uiProviders:{
'col': Ext.tree.ColumnNodeUI
}
}),
root: new Ext.tree.AsyncTreeNode({
allowChildren: true,
children: [{
menu_item: "Current State",
menu_url: 'blah blah',
uiProvider:'col',
children: [{
menu_item: "Executive Summary",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Vulnerability Scorecard",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Vulnerability Distribution",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
}]
},{
menu_item: "Trends",
menu_url: 'blah blah',
uiProvider:'col',
children: [{
menu_item: "Host Changes",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Hosts and Vulnerability",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Application and Vulnerability",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Vulnerability by Operation System",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Vulnerability by Networks",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Vulnerability by Application Group",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
}]
},{
menu_item: "Technical",
menu_url: 'blah blah',
uiProvider:'col',
children: [{
menu_item: "Risk Matrix",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Host Inventory",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Application Inventory",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Vulnerability Inventory",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Most Common Vulnerabilites",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Most Common SANS Vulnerabilities",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Most Vulnerable Applications",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "Most Vulnerable Hosts",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
},{
menu_item: "PCI Compliance",
menu_url: 'blah blah',
uiProvider:'col',
leaf:true
}]
}]
})
});
tree.render();
tree.expandAll();
var te = new Ext.tree.ColumnTreeEditor(tree,{
completeOnEnter: true,
autosize: true,
ignoreNoChange: true
});
});
ColumnNodeUI.js
Code:
/*
* Ext JS Library 2.0 RC 1
* Copyright(c) 2006-2007, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
Ext.tree.ColumnTree = Ext.extend(Ext.tree.TreePanel, {
lines:false,
borderWidth: Ext.isBorderBox ? 0 : 2, // the combined left/right border for each cell
cls:'x-column-tree',
collapsible: false,
onRender : function(){
Ext.tree.ColumnTree.superclass.onRender.apply(this, arguments);
this.headers = this.body.createChild(
{cls:'x-tree-headers'},this.innerCt.dom);
var cols = this.columns, c;
var totalWidth = 0;
for(var i = 0, len = cols.length; i < len; i++){
c = cols[i];
totalWidth += c.width;
this.headers.createChild({
cls:'x-tree-hd ' + (c.cls?c.cls+'-hd':''),
cn: {
cls:'x-tree-hd-text',
html: c.header
},
style:'width:'+(c.width-this.borderWidth)+'px;'
});
}
this.headers.createChild({cls:'x-clear'});
// prevent floats from wrapping when clipped
this.headers.setWidth(totalWidth);
this.innerCt.setWidth(totalWidth);
}
});
Ext.tree.ColumnNodeUI = Ext.extend(Ext.tree.TreeNodeUI, {
focus: Ext.emptyFn, // prevent odd scrolling behavior
renderElements : function(n, a, targetNode, bulkRender){
this.indentMarkup = n.parentNode ? n.parentNode.ui.getChildIndent() : '';
var t = n.getOwnerTree();
var cols = t.columns;
var bw = t.borderWidth;
var c = cols[0];
n.cols = new Array();
var text = n.text || (c.renderer ? c.renderer(a[c.dataIndex], n, a) : a[c.dataIndex]);
n.cols[cols[0].dataIndex] = text;
var buf = [
'<li class="x-tree-node" unselectable="on"><div ext:tree-node-id="',n.id,'" class="x-tree-node-el x-tree-node-leaf ', a.cls,'" unselectable="on">',
'<div class="x-tree-col" style="width:',c.width-bw,'px;" unselectable="on">',
'<span class="x-tree-node-indent" unselectable="on">',this.indentMarkup,"</span>",
'<img src="', this.emptyIcon, '" class="x-tree-ec-icon x-tree-elbow" unselectable="on">',
'<img src="', a.icon || this.emptyIcon, '" class="x-tree-node-icon',(a.icon ? " x-tree-node-inline-icon" : ""),(a.iconCls ? " "+a.iconCls : ""),'" unselectable="on">',
'<a hidefocus="on" class="x-tree-node-anchor" href="',a.href ? a.href : "#",'" tabIndex="1" ',
a.hrefTarget ? ' target="'+a.hrefTarget+'"' : "", ' unselectable="on">',
'<span unselectable="on">', text,"</span></a>",
"</div>"];
for(var i = 1, len = cols.length; i < len; i++){
c = cols[i];
var text = (c.renderer ? c.renderer(a[c.dataIndex], n, a) : a[c.dataIndex]);
n.cols[cols[i].dataIndex] = text;
buf.push('<div class="x-tree-col ',(c.cls?c.cls:''),'" style="width:',c.width-bw,'px;" unselectable="on">',
'<div class="x-tree-col-text" unselectable="on">',text,"</div>",
"</div>");
}
buf.push(
'<div class="x-clear" unselectable="on"></div></div>',
'<ul class="x-tree-node-ct" style="display:none;" unselectable="on"></ul>',
"</li>");
if(bulkRender !== true && n.nextSibling && n.nextSibling.ui.getEl()){
this.wrap = Ext.DomHelper.insertHtml("beforeBegin",
n.nextSibling.ui.getEl(), buf.join(""));
}else{
this.wrap = Ext.DomHelper.insertHtml("beforeEnd", targetNode, buf.join(""));
}
this.elNode = this.wrap.childNodes[0];
this.ctNode = this.wrap.childNodes[1];
var cs = this.elNode.firstChild.childNodes;
this.indentNode = cs[0];
this.ecNode = cs[1];
this.iconNode = cs[2];
this.anchor = cs[3];
this.textNode = cs[3].firstChild;
}
});
Ext.tree.ColumnTreeEditor = function(tree, config){
config = config || {};
var field = config.events ? config : new Ext.form.TextField(config);
Ext.tree.TreeEditor.superclass.constructor.call(this, field);
this.tree = tree;
if(!tree.rendered){
tree.on('render', this.initEditor, this);
}else{
this.initEditor(tree);
}
};
Ext.extend(Ext.tree.ColumnTreeEditor, Ext.Editor, {
alignment: "l-l",
autoSize: false,
hideEl : false,
cls: "x-small-editor x-tree-editor",
shim:false,
shadow:"frame",
maxWidth: 250,
editDelay: 0,
initEditor : function(tree){
tree.on('beforeclick', this.beforeNodeClick, this);
this.on('complete', this.updateNode, this);
this.on('beforestartedit', this.fitToTree, this);
this.on('startedit', this.bindScroll, this, {delay:10});
this.on('specialkey', this.onSpecialKey, this);
},
fitToTree : function(ed, el){
var td = this.tree.getTreeEl().dom, nd = el.dom;
if(td.scrollLeft > nd.offsetLeft){ td.scrollLeft = nd.offsetLeft;
}
var w = Math.min(
this.maxWidth,
(td.clientWidth > 20 ? td.clientWidth : td.offsetWidth) - Math.max(0, nd.offsetLeft-td.scrollLeft) - 5);
this.setSize(w, '');
},
triggerEdit : function(node, e){
var obj = e.target;
if (Ext.select(".x-tree-node-anchor", false, obj).getCount() == 1) {
obj = Ext.select(".x-tree-node-anchor", false, obj).elements[0].firstChild;
} else if (obj.nodeName == 'SPAN' || obj.nodeName == 'DIV'){
obj = e.target;
} else {
return false;
}
var colIndex = 0;
for (var i in node.cols) {
if (node.cols[i] == obj.innerHTML) {
colIndex = i;
}
}
this.completeEdit();
this.editNode = node;
this.editCol = obj;
this.editColIndex = colIndex;
this.startEdit(obj);
if (obj.nodeName == 'DIV') {
var width = obj.offsetWidth;
this.setSize(width);
}
},
bindScroll : function(){
this.tree.getTreeEl().on('scroll', this.cancelEdit, this);
},
beforeNodeClick : function(node, e){
var sinceLast = (this.lastClick ? this.lastClick.getElapsed() : 0);
this.lastClick = new Date();
if(sinceLast > this.editDelay && this.tree.getSelectionModel().isSelected(node)){
e.stopEvent();
this.triggerEdit(node, e);
return false;
} else {
this.completeEdit();
}
},
updateNode : function(ed, value){
this.tree.getTreeEl().un('scroll', this.cancelEdit, this);
this.editNode.cols[this.editColIndex] = value; //for internal use only
this.editNode.attributes[this.editColIndex] = value;//duplicate into array of node attributes
this.editCol.innerHTML = value;
},
onHide : function(){
Ext.tree.TreeEditor.superclass.onHide.call(this);
if(this.editNode){
this.editNode.ui.focus();
}
},
onSpecialKey : function(field, e){
var k = e.getKey();
if(k == e.ESC){
e.stopEvent();
this.cancelEdit();
}else if(k == e.ENTER && !e.hasModifier()){
e.stopEvent();
this.completeEdit();
}
}
});
editable-column-tree.html
HTML Code:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<title>Menu Configuration</title>
<link rel="stylesheet" type="text/css" href="ext-2.0/resources/css/ext-all.css" />
<!-- GC -->
<!-- LIBS -->
<script type="text/javascript" src="ext-2.0/adapter/ext/ext-base.js"></script>
<!-- ENDLIBS -->
<script type="text/javascript" src="ext-2.0/ext-all.js"></script>
<script type="text/javascript" src="ColumnNodeUI.js"></script>
<script type="text/javascript" src="editable-column-tree.js"></script>
<script type="text/javascript" src="treeSerializer.js"></script>
<link rel="stylesheet" type="text/css" href="editable-column-tree.css" />
</head>
<body>
<table border="0" width="100%" height="100%">
<tr>
<td align="center" valign="top" style="padding-top:25px;">
<div id="tree-ct" style="text-align:left"></div>
</td>
</tr>
</table>
</body>
</html>
editable-column-tree.css
Code:
/*
* Ext JS Library 2.0 RC 1
* Copyright(c) 2006-2007, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
.x-column-tree .x-tree-node {
zoom:1;
}
.x-column-tree .x-tree-node-el {
/*border-bottom:1px solid #eee; borders? */
zoom:1;
}
.x-column-tree .x-tree-selected {
background: #d9e8fb;
}
.x-column-tree .x-tree-node a {
line-height:18px;
vertical-align:middle;
}
.x-column-tree .x-tree-node a span{
}
.x-column-tree .x-tree-node .x-tree-selected a span{
background:transparent;
color:#000;
}
.x-tree-col {
float:left;
overflow:hidden;
padding:0 1px;
zoom:1;
}
.x-tree-col-text, .x-tree-hd-text {
overflow:hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
padding:3px 3px 3px 5px;
white-space: nowrap;
height:1.2em;//height:20px;
font:normal 11px arial, tahoma, helvetica, sans-serif;
}
.x-tree-hd-text {
height:1.3em;//height:20px;
}
.x-tree-headers {
background: #f9f9f9 url(ext-2.0/resources/images/default/grid/grid3-hrow.gif) repeat-x 0 bottom;
cursor:default;
zoom:1;
}
.x-tree-hd {
float:left;
overflow:hidden;
border-left:1px solid #eee;
border-right:1px solid #d0d0d0;
}
.page-icon {
background-image:url(images/leaf-add-icon.gif) !important;
}
.delete-icon {
background-image:url(images/delete-icon.gif) !important;
}
.folder-icon {
background-image:url(images/folder-add-icon.gif) !important;
}
.save-icon {
background-image:url(images/save-icon.gif) !important;
}
treeSerializer.js
Code:
/* 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.tree.TreePanel.prototype.toJsonString = function(nodeFilter, attributeFilter, attributeMapping){
return this.getRootNode().toJsonString(nodeFilter, attributeFilter, attributeMapping);
};
/**
* 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.tree.TreeNode.prototype.toJsonString = function(nodeFilter, attributeFilter, attributeMapping){
// 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 += ',';
if (attributeMapping && attributeMapping[key]) {
thisKey = attributeMapping[key];
} else {
thisKey = key;
}
result += '"' + thisKey + '":"' + 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, attributeMapping);
}
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.tree.TreePanel.prototype.toXmlString = function(nodeFilter, attributeFilter, attributeMapping){
return '\u003C?xml version="1.0"?>\u003Ctree>' +
this.getRootNode().toXmlString(nodeFilter, attributeFilter, attributeMapping) +
'\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.tree.TreeNode.prototype.toXmlString = function(nodeFilter, attributeFilter, attributeMapping){
// 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]))) {
if (attributeMapping && attributeMapping[key]) {
thisKey = attributeMapping[key];
} else {
thisKey = key;
}
result += ' ' + thisKey + '="' + 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, attributeMapping);
}
result += '\u003C/node>';
}
return result;
};