/* | |
Title: Ajax Tree | |
Created by Colin Mollenhour | |
Description: | |
This tree class can be used with or without Ajax. You simply define node types and supply options, | |
hooks, handlers, etc. for each type and start creating nodes. All node types can be created using | |
the same constructor by passing it the type as a string. The way it handles data and reacts to user input | |
is completely customizable for each individual node type. | |
LIVE DEMO: | |
<http://colin.mollenhour.com/ajaxtree/ajaxtreetest.html> | |
Section: Usage | |
Usage specifications and examples. | |
Requirements: | |
Prototype 1.5.0 rc1 <http://prototype.conio.net/> | |
LIVE DEMO: | |
<http://colin.mollenhour.com/ajaxtree/ajaxtreetest.html> | |
Source Code: | |
<http://colin.mollenhour.com/ajaxtree/ajaxtree.js> | |
Group: Using the Javascript | |
Ajax.Tree is designed to be used in two steps:: | |
- Extend the base class with your customizations. See <Ajax.Tree.Usage> | |
- Instantiate nodes of your new class. See <Ajax.Tree.Base.Usage> | |
Group: Handling Server Requests | |
The default format for server requests, which can be customized via the <callback> hook: | |
getContents - 'action=getContents&id='this.element.id | |
Now just handle those post variables however you like and use correct format in your <Server Response>. | |
Group: Server Response | |
The server response is expected to be in JSON format. It can be sent either in the responseText, or an X-JSON header. | |
The X-JSON header evaluated result is check for the presence of a *nodes* key, which is expected to be an Array. | |
If the X-JSON header does not contain a *nodes* key, the responseText is evaluated. If the evaluated responseText | |
does not have a *nodes* key, no nodes are built and this is *not* an error. Any related hooks will still be called | |
and passed the server response variables as usual. | |
Topic: Specifications | |
- Must contain a "nodes" array of nodes. | |
- If the "nodes" array does not exist, no nodes will be created. However, | |
the <onContentLoaded> and <onGetContentsComplete> hooks will still be called. | |
- Each element of "nodes" must contain: | |
id - The new node's element id. See <prependParentId>. | |
type - The new node's <type> keyword, as defined in the structure passed to <Ajax.Tree.create>. | |
data - Either a string (for default <insertion>) or some other object to be passed to <insertion>. | |
- Each node can contain nested nodes by adding a "nodes" array to a node. | |
- Other attributes can be scattered throughout the response data. It can be accessed by the | |
<onContentLoaded> and <getContentsComplete> hooks or for each individual node through the <insertion> option. | |
Topic: Example | |
This response example coincides with the <Ajax.Tree.Usage> example: | |
(start code) | |
{ | |
nodes: [ | |
{ | |
id: 'dir-1', | |
type: 'directory', | |
data: 'Work', | |
nodes: [ | |
{ | |
id: 'dir-1_subdir-1', | |
type: 'directory', | |
data: 'Accounting' | |
} | |
] | |
}, | |
{ | |
id: 'file-1', | |
type: 'file', | |
data: { | |
name: 'Presentation.ppt', | |
size: '30.2KB', | |
fileid: 53255596 | |
}, | |
} | |
] | |
} | |
(end) | |
Class: Ajax.Tree | |
Ajax.Tree is the utility class for <Ajax.Tree.Base>. Using <create>, you can | |
create a new class that is an extension of <Ajax.Tree.Base>. Using this new | |
constructor, you can build a tree dynamically using multiple node types | |
without the need for separate constructors. The node types are defined in a hash | |
passed to <create> which also defines settings and handlers for each type. | |
Group: Usage | |
Use Ajax.Tree.create to create a customized tree class. | |
This example creates a simple file browser tree. It also demonstrates the use of an overridden | |
<insertion>, and the <onClick> hook. | |
See <Ajax.Tree.Base.Usage> for information on using the constructor produced by this example. | |
For more detail on defining your own types and handling server responses, see <Options> and <Ajax.Tree.Base>. | |
(start code) | |
Ajax.FileBrowser = Ajax.Tree.create({ | |
types: { | |
directory: { | |
page: 'filebrowser.php' | |
}, | |
file: { | |
leafNode: true, | |
insertion: function(element,data){ | |
Element.update(element, data.name+' Size: '+data.size); | |
this.dlLink = Builder.node('span',{ | |
id: data.fileid, | |
className: 'download' | |
},['download']); | |
this.events.observe(this.dlLink, 'click', this.options.download.bindAsEventListener(this)); | |
}, | |
onClick: function(event){ | |
showThumbnail(this.element.id); | |
}, | |
download: function(event){ | |
window.location.href = 'getfile.php?fileid='+Event.element(event).id; | |
} | |
} | |
} | |
}); | |
(end) | |
Group: Functions | |
*/ | |
Ajax.Tree = { | |
/* Function: create | |
Returns a constructor for a new class that is specific to the structure passed. | |
This new class is an extension of <Ajax.Tree.Base> | |
structure - The structure that defines node types and their options and hooks. | |
*/ | |
create: function(structure){ | |
if( !structure.types._root ){ structure.types._root = {}; } | |
for(var type in structure.types){ | |
/* Group: Options | |
All options are unique per type, and can be accessed inside class functions by "this.options.<option>". */ | |
var options = { | |
/* Option: className | |
The className for the newly created element div and span elements, defaults to the node type */ | |
className: type, | |
/* Option: draggable | |
Not yet implimented. */ | |
draggable: false, | |
/* Option: leafNode | |
If true, the <mark> will get the className 'leaf' and clicks will not fire a <toggleChildren>. */ | |
leafNode: false, | |
/* Option: page | |
If specified, <getContents> will be called on clicking the <mark>. */ | |
page: null, | |
/* Option: prependParentId | |
If not false, the new element id will be prepended with it's parent's id and this option's value as a separator. | |
Example:: | |
|prependParentId: '_', parent.id: 'one-4', id: 'two-3' | |
|newid = 'one-4_two-3' */ | |
prependParentId: false, | |
/* Option: insertion | |
The insertion function used to handle the node "data". | |
See <Ajax.Tree.Base.insertion> */ | |
insertion: Element.update | |
}; | |
structure.types[type] = Object.extend(options,structure.types[type]); | |
} | |
var newTreeClass = Class.create(); | |
Object.extend(newTreeClass.prototype,Object.extend(Ajax.Tree.Base.prototype,structure)); | |
newTreeClass.prototype.constructor = newTreeClass; | |
return newTreeClass; | |
}, | |
error: { | |
ajax: function(transport){ | |
var msg = 'There was an error communicating with the server:\n'+transport.statusText; | |
Ajax.Tree.showError(msg); | |
} | |
}, | |
showError: function(message){ | |
alert(message); | |
} | |
}; | |
/* | |
Class: Ajax.Tree.Base | |
Ajax.Tree.Base is designed to be extended using <Ajax.Tree.create>. The extension | |
of this base class lets you add nodes to a tree in any way you like. | |
Group: Usage | |
Use this class as the base for your own customized Ajax.Tree class. | |
The Ajax.Tree.Base class is not intended to be used directly. Instead, create your own extension | |
of this class with <Ajax.Tree.create>. With the extended class, you may now start creating nodes | |
using a typical javascript constructor. | |
Topic: constructor | |
The class returned from <Ajax.Tree.create> can be instantied with the following arguments: | |
Arguments: | |
parent - The id or element of the new node's parent | |
id - The new node's id | |
type - The new node's type, corresponding to one of the keys of the *types* hash passed to <Ajax.Tree.create> | |
data - A string or other object containing data to be processed by the type's <insertion> function | |
Example code: | |
See <Ajax.Tree.Usage> for the corresponding, more detailed usage example of <Ajax.Tree.create> | |
(start code) | |
<div id="file_browser"></div> | |
<script type="text/javascript"> | |
Ajax.FileBrowser = Ajax.Tree.create({...}); | |
new Ajax.FileBrowser('file_browser','root','directory','My Files'); | |
</script> | |
(end) | |
Group: DOM Elements | |
The DOM elements created on instantiation of a new node. | |
All DOM elements are accessible using *this.<element>*. | |
parent - (div) - the node's parent element | |
element - (div) - the primary div, contains mark, span and children. has className: 'treenode' and this.options.className || this.type | |
mark - (span) - the expanded/collapsed mark. has className: 'mark' and one of 'expanded', 'collapsed', 'leaf' | |
span - (span) - the primary container for node data. has className: 'treedata' and this.options.className || this.type | |
Group: Hooks | |
Hooks are provided for fine control over interactivity. Implement hooks separately for each node type. | |
NOTE: | |
For all hooks, *this* is a reference to the node's class instance. | |
Example: | |
Alerts the user as to how many child nodes are loaded. | |
(start code) | |
TreeOfNodes = Ajax.Tree.create({ | |
types: { | |
node: { | |
page: 'nodes.php', | |
onContentLoaded: function(xhr,json){ | |
alert('Got '+$H(json.nodes).keys().length+' new nodes'); | |
} | |
} | |
} | |
}); | |
(end code) | |
Hook: callback | |
Called to build query parameters for the Ajax.Request. Should return a query string. | |
id - The id of the node's element. | |
Hook: onClearContents | |
Called after a node's children have been cleared. | |
Hook: onClick | |
Called on user clicking the mark. If this function returns false, the click is effectively cancelled. | |
event - The click event object | |
Hook: onContentLoaded | |
Called after all new nodes have been constructed. | |
xhr - The XMLHttpRequest transport | |
json - The evaluated X-JOSN header object | |
Hook: dispose | |
Called during a node disposal. | |
Hook: onGetContents | |
Called immediately after the Ajax.Request is sent. | |
request - The Ajax.Request object | |
Hook: onGetContentsComplete | |
Called after the Ajax.Request onComplete | |
xhr - The XMLHttpRequest transport | |
json - The evaluated X-JOSN header object | |
Hook: insertion | |
Called by the constructor after the basic node element has been built. | |
element - The tree node's *span* DOM element | |
data - The *data* value from the <getContents> response | |
Default: | |
|Element.update(element,data) | |
Group: Flags | |
All flags listed here are accessed by *this.<flag>* and initialized to the values on the left. | |
loaded - false - The data below this node has been loaded (<loadContents> sets to true, <getContents> sets to false) | |
opening - false - Ajax.Request is in progress (<getContents> sets to true, <getContentsComplete> sets to false) | |
Group: Functions | |
clearContents - clears the node's children and sets loaded to false | |
deleteChildNode - deletes the given child node from the tree, performing all necessary cleanup | |
deleteSelf - deletes this node from the tree, performing all necessary cleanup | |
dispose - calls clearContents and cleans up the node's events, references, etc.. | |
getContents - triggers the Ajax.Request if loaded is true, otherwise, calls clearContents | |
hide - hides the node's element | |
hideChildren - hides all of the node's children's elements and sets the mark's class to 'collapsed' | |
show - shows the node's element | |
showChildren - shows all of the node's children's elements and sets the mark's class to 'expanded' | |
toggle - calls show/hide as appropriate | |
toggleChildren - calls showChildren/hideChildren as appropriate | |
Group: Options | |
See <Ajax.Tree.Options> | |
Group: Properties | |
All properties are accessed by *this.<property>*. | |
children - An Array (with Prototype extensions) of all children of the current node | |
element.treeNode - Reference to the tree node (*this*) | |
parent.treeNode - The node's parent's tree node (if exists, else undefined) | |
type - The tree node's type (string) | |
*/ | |
Ajax.Tree.Base = {}; | |
Ajax.Tree.Base.prototype = { | |
initialize: function(parent,id,type,data){ | |
this.type = type || '_root'; | |
this.options = this.types[this.type]; | |
this.children = []; | |
this.loaded = this.opening = this.root = false; | |
this.disposables = [(this.events = new EventCache())]; | |
/* create special purpose root node if parent == null */ | |
if(parent == null){ | |
this.element = $(id); | |
this.element.addClassName(this.type); | |
this.element.treeNode = this; | |
this.parent = this.element.parentNode || document.body; | |
this.root = true; | |
if(data){ | |
if(this.options){ this.options.insertion.call(this, this.element,data.data || data); } | |
if(data.nodes){ this.createNodes(data.nodes); } | |
} | |
return; | |
} | |
this.parent = $(parent); | |
this.id = id; | |
this.createNode(); | |
this.options.insertion.call(this, this.span,data.data || data); | |
/* if this node's parent doesn't have a tree node, create a special purpose one */ | |
if(!this.parent.treeNode){ var newNode = new this.constructor(null,this.parent); } | |
this.parent.treeNode.children.push(this); | |
if(data.nodes){ this.createNodes(data.nodes); } | |
}, | |
clearContents: function(){ | |
while(this.children.length){ | |
var node = this.children.shift(); | |
node.dispose(); | |
} | |
this.loaded = false; | |
this.hideChildren(); | |
if(this.options.onClearContents){ this.options.onClearContents.call(this); } | |
}, | |
createNode: function(){ | |
var linkType = (this.options.leafNode ? 'leaf':'collapsed'); | |
var newID = (this.options.prependParentId !== false ? this.parent.id+this.options.prependParentId:'')+this.id; | |
this.mark = Builder.node('span',{className:'mark '+linkType}); | |
this.span = Builder.node('span',{className:this.options.className+' treedata'}); | |
this.element = Builder.node('div',{id:newID,className:this.options.className+' treenode'},[ | |
this.mark,this.span | |
]); | |
this.events.observe(this.mark, 'click', this.onClick.bindAsEventListener(this)); | |
if(this.options.mouseOver){ | |
this.events.observe(this.span, 'mouseover', this.options.mouseOver.bindAsEventListener(this)); | |
} | |
if(this.options.mouseOut){ | |
this.events.observe(this.span, 'mouseout', this.options.mouseOut.bindAsEventListener(this)); | |
} | |
this.element.treeNode = this; | |
this.parent.appendChild(this.element); | |
}, | |
createNodes: function(nodes){ | |
this.showChildren(); | |
this.loaded = true; | |
for(var i=0; i < nodes.length; i++){ | |
var newNode = new this.constructor(this.element,nodes[i].id,nodes[i].type,nodes[i]); | |
} | |
if(nodes.length && this.options.sortable){ this.createSortable(); } | |
}, | |
createSortable: function(){ | |
//if(!this.options.sortable) return; | |
Sortable.create(this.element,{ | |
tag: 'div', | |
only: 'treenode' | |
}); | |
}, | |
deleteChildNode: function(node){ | |
this.children = this.children.without(node); | |
node.dispose(); | |
}, | |
deleteSelf: function(){ | |
if(this.parent.treeNode){ this.parent.treeNode.deleteChildNode(this); } | |
else{ this.dispose(); } | |
if(this.options.onDeleteSelf){ this.options.onDeleteSelf.call(this); } | |
}, | |
dispose: function(){ | |
//if(this.options.sortable){ Sortable.destroy(this.sortable); } | |
this.clearContents(); | |
if(this.options.dispose){ this.options.dispose.call(this); } | |
while(this.disposables.length){ this.disposables.shift().dispose(); } | |
this.element.treeNode = null; | |
this.parent.removeChild(this.element); | |
}, | |
getContents: function(onSuccess){ | |
if(this.opening || !this.options.page){ return; } | |
this.opening = true; | |
var params = 'action=getContents&' + ((this.options.callback) ? | |
this.options.callback.call(this, this.element.id) : 'id='+this.element.id); | |
var request = new Ajax.Request(this.options.page,{ | |
parameters: params, | |
onComplete: this.getContentsComplete.bind(this), | |
onSuccess: function(xhr,json){ | |
if(json && json.error){ Ajax.Tree.showError(json.error); return; } | |
if(!xhr.responseText){ Ajax.Tree.showError('Error, empty response from server'); return; } | |
var data = xhr.responseText.evalJSON(); | |
this.clearContents(); | |
this.showChildren(); | |
this.loadContents(data,json); | |
if(onSuccess) onSuccess(); | |
}.bind(this), | |
onFailure: Ajax.Tree.error.ajax | |
}); | |
if(this.options.onGetContents){ this.options.onGetContents.call(this, request); } | |
}, | |
getContentsComplete: function(xhr,json){ | |
this.opening = false; | |
if(this.options.onGetContentsComplete){ this.options.onGetContentsComplete.call(this, xhr, json); } | |
}, | |
hide: function(el){ | |
Element.hide((el || this).element); | |
}, | |
hideChildren: function(){ | |
this.children.each(this.hide); | |
Element.removeClassName(this.mark,'expanded'); | |
Element.addClassName(this.mark,'collapsed'); | |
}, | |
loadContents: function(data,json){ | |
if(this.options.onLoadContent){ this.options.onLoadContent.call(this, data, json); } | |
if(data.nodes){ | |
this.createNodes(data.nodes); | |
if(this.options.onContentLoaded){ this.options.onContentLoaded.call(this, data, json); } | |
} | |
}, | |
onClick: function(event){ | |
if(this.options.onClick){ | |
if(this.options.onClick.call(this, event) === false) return; | |
} | |
if(this.options.page){ | |
if(this.loaded){ this.clearContents(); } | |
else{ this.getContents(); } | |
} | |
else if(!this.options.leafNode){ this.toggleChildren(); } | |
}, | |
show: function(el){ | |
Element.show((el || this).element); | |
}, | |
showChildren: function(){ | |
this.children.each(this.show); | |
Element.removeClassName(this.mark,'collapsed'); | |
Element.addClassName(this.mark,'expanded'); | |
}, | |
toggle: function(){ | |
this.element.visible() ? this.hide() : this.show(); | |
}, | |
toggleChildren: function(){ | |
this.isExpanded() ? this.hideChildren() : this.showChildren(); | |
}, | |
isExpanded: function(){ return this.mark.hasClassName('expanded'); } | |
}; |