blob: 0f911ed32c8fd8c809d00dab502c87370d769233 [file] [log] [blame]
/*****************************************************************************
*
* Copyright (c) 2004 Guido Wesdorp. All rights reserved.
*
* This software is distributed under the terms of the tree.js
* License. See LICENSE.txt for license text.
*
* E-mail: johnny@debris.demon.nl
*
*****************************************************************************/
/*
Copyright (c) 2004, Guido Wesdorp
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of tree.js nor the names of its contributors may
be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
function Node(id, parent) {
this.id = id;
this.parent = parent;
// this is just here for show, will be overwritten in init()
this.items = {};
};
Node.prototype.init = function() {
this.depth = this.parent.depth + 1;
this.tree = this.parent.tree;
this.reopen = false;
this.items = {};
};
Node.prototype.open = function(reload, endhandler) {
if (!this.isCollection()) {
if (reload) {
this.reloadSingle();
};
return;
};
if (!reload && this.opened) {
return;
};
this.opened = true;
var newopensign = this.tree.getOpenSign(this);
this.opensign.parentNode.replaceChild(newopensign, this.opensign);
this.opensign = newopensign;
addEventHandler(this.opensign, 'click', this.tree.handleItemSignClick,
this.tree, this);
if (!this.itemids || this.itemids.length==0) {
// get the items, getItems() should be defined on the subclass
this.tree.doc.getElementsByTagName('body')[0].style.cursor = 'wait';
this.element.className = this.element.className;
this.tree.getItems(this, this._continueOpen, !reload, endhandler);
} else {
for (var itemid in this.items) {
var item = this.items[itemid];
item.render();
if (item.reopen) {
item.open();
};
};
if (endhandler) {
endhandler();
};
};
};
Node.prototype._continueOpen = function(items, endhandler) {
this.items = {};
this.itemids = [];
for (var i = items.length - 1; i >= 0; i--) {
var item = items[i];
this.items[item.id] = item;
this.itemids.unshift(item.id);
item.render();
if (item.reopen) {
item.open();
};
};
this.tree.doc.getElementsByTagName('body')[0].style.cursor = 'default';
if (endhandler) {
endhandler();
};
};
Node.prototype.reloadSingle = function() {
/* called on opening a non-collection item with a reload arg
can be overridden in subclasses
*/
};
Node.prototype.close = function(reopen_when_opening_parent) {
if (!this.opened || !this.isCollection()) {
return;
};
this.reopen = reopen_when_opening_parent;
var newopensign = this.tree.getCloseSign(this);
this.opensign.parentNode.replaceChild(newopensign, this.opensign);
this.opensign = newopensign;
this.opened = false;
addEventHandler(this.opensign, 'click', this.tree.handleItemSignClick,
this.tree, this);
for (var itemid in this.items) {
var item = this.items[itemid];
item.close(true);
this.tree.unrenderItem(item);
};
};
Node.prototype.reload = function() {
this.itemids = null;
var currel = this.element;
this.element = this.tree.createItemHtml(this);
currel.parentNode.replaceChild(this.element, currel);
if (this.opened) {
this.close();
this.open(true);
};
};
Node.prototype.render = function() {
this.tree.renderItem(this);
};
Node.prototype.isCollection = function() {
return true;
};
Node.prototype.getPath = function() {
var parentpath;
if (this.parent) {
parentpath = this.parent.getPath();
} else {
parentpath = '';
}
return parentpath + '/' + this.id;
};
Node.prototype.setSelectClass = function() {
if (this.element) {
this.element.className = 'selected_node';
};
};
Node.prototype.unsetSelectClass = function() {
if (this.element) {
this.element.className = 'unselected_node';
};
};
Node.prototype.getItemByPath = function(path) {
/* return an item by its path
path should be relative from the current item, excluding
the current item's id
*/
var item = null;
for (var itemid in this.items) {
// see if the path starts with the next item id
if (path.indexOf(itemid + '/') == 0 || path == itemid) {
item = this.items[itemid];
break;
};
};
if (!item) {
return false;
};
if (path == item.id) {
return item;
} else {
return item.getItemByPath(path.substr(item.id.length+1));
};
return false;
};
function Tree(doc, treeElement) {
this.doc = doc;
this.treeElement = treeElement;
this.selected = null;
};
Tree.prototype.init = function(id) {
this.root = new Node(id);
this.root.tree = this;
this.root.depth = 0;
this.root.reopen = false;
this._currentId = 0;
// addEventHandler(this.doc, 'contextmenu',
// this.createContextMenu, this);
//addEventHandler(this.doc, 'mouseup', this.removeContextMenu, this);
};
Tree.prototype.getItems = function(item, handler, reload, endhandler) {
/* this should be overridden in subclasses */
var items = [];
var ids = ['foo', 'bar', 'baz'];
for (var i=0; i < ids.length; i++) {
var child = new Node(ids[i], item);
child.init();
items.push(child);
};
handler.call(item, items, endhandler);
};
Tree.prototype.render = function() {
var element = this.createItemHtml(this.root);
this.root.element = element;
element.tree_item = this.root;
this.doc.getElementsByTagName('body')[0].style.cursor = 'default';
this.treeElement.appendChild(element);
};
Tree.prototype.renderItem = function(item) {
var element = this.createItemHtml(item);
this.doc.getElementsByTagName('body')[0].style.cursor = 'default';
item.element = element;
element.tree_item = item;
if (item.parent.element.nextSibling) {
item.parent.element.parentNode.insertBefore(item.element,
item.parent.element.nextSibling);
} else {
item.parent.element.parentNode.appendChild(item.element);
};
};
Tree.prototype.createItemHtml = function(item) {
/* you may want to override this */
var div = this.doc.createElement('div');
div.style.whiteSpace = 'nowrap';
div.className = 'treenode';
// place a reference to the item on the div
div.treeitem = item;
var space = '';
for (var i=0; i < item.depth; i++) {
space += '\xa0\xa0\xa0';
};
var text = this.doc.createTextNode(space);
div.appendChild(text);
item.opensign = this.getCloseSign(item);
item.opensign.className = 'treenode_opensign';
div.appendChild(item.opensign);
// the actual icon and name are generated from another method
// to improve extending
var line = this.createItemLine(item);
div.appendChild(line);
addEventHandler(line, 'click', this.handleItemClick, this, item);
addEventHandler(item.opensign, 'click', this.handleItemSignClick,
this, item);
if (this.selected == item.getPath()) {
this.unselect();
this.select(item);
div.className = 'selected_node';
};
return div;
};
Tree.prototype.createItemLine = function(item) {
var div = this.doc.createElement('div');
/* creates the item name and any icons and such */
var span = this.doc.createElement('span');
var icon = this.getIcon(item);
if (icon.nodeType == 1) {
icon.className = 'treenode_icon';
};
div.appendChild(icon);
var text = this.doc.createTextNode(item.name ? item.name : item.id);
span.className = 'treenode_label';
span.appendChild(text);
div.appendChild(span);
return div;
};
Tree.prototype.unrenderItem = function(item) {
if (item.element) {
item.element.parentNode.removeChild(item.element);
delete item.element;
};
};
Tree.prototype.handleItemClick = function(item, event) {
if (item.skip_next_click) {
item.skip_next_click = false;
return;
};
this.select(item);
if (!item.opened) {
item.open();
} else {
item.close();
};
if (item.element && item.element.scrollIntoView) {
item.element.scrollIntoView();
};
};
Tree.prototype.select = function(item) {
this.unselect();
this.selected = item.getPath();
item.setSelectClass();
};
Tree.prototype.unselect = function() {
if (typeof(this.selected) == 'string') {
var item = this.getItemByPath(this.selected);
item.unsetSelectClass();
};
};
Tree.prototype.handleItemSignClick = function(item, event) {
if (!item.opened) {
item.open();
} else {
item.close();
};
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
};
};
Tree.prototype.createUniqueId = function() {
this._currentId++;
return this._currentId.toString();
};
Tree.prototype.createContextMenu = function(event) {
var clickel = event.target;
var item = clickel.tree_item;
if (!item) {
return;
};
if (!this.contextmenu) {
var contextels = this.getContextMenuElements();
this.renderContextMenu(item, contextels, event);
};
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
};
return false;
};
Tree.prototype.getContextMenuElements = function() {
/* you will probably want to override this
this should return a mapping (object) from context menu
entry (string) to the name of a method on the Node object
(also a string), the method will be called when the element
is clicked
*/
return {'open': 'open',
'close': 'close'};
};
Tree.prototype.renderContextMenu = function(item, elements, event) {
/* you may want to override this
item is the item clicked on
elements should be a mapping from menu element (string) to
the name of a method located on the clicked Node
*/
var menu = this.doc.createElement('div');
menu.style.position = 'absolute';
menu.className = 'contextmenu';
addEventHandler(menu, 'click', this.removeContextMenu, this);
for (var el in elements) {
var div = this.doc.createElement('div');
var text = this.doc.createTextNode(el);
div.appendChild(text);
addEventHandler(div, 'click', item[elements[el]], item);
menu.appendChild(div);
};
this._positionMenu(event, menu);
this.doc.getElementsByTagName('body')[0].appendChild(menu);
this.contextmenu = menu;
};
Tree.prototype.removeContextMenu = function() {
if (!this.contextmenu) {
return;
};
this.contextmenu.parentNode.removeChild(this.contextmenu);
delete this.contextmenu;
};
Tree.prototype.getCloseSign = function(item) {
/* get the open sign for a collection or resource */
var opensign = this.doc.createElement('img');
if (item.isCollection()) {
opensign.setAttribute('src', 'images/closed-collection.png');
} else {
opensign.setAttribute('src', 'images/non-collection.png');
};
return opensign;
};
Tree.prototype.getOpenSign = function(item) {
/* get the close sign for a collection */
var opensign = this.doc.createElement('img');
opensign.setAttribute('src', 'images/opened-collection.png');
return opensign;
};
Tree.prototype.getIcon = function(item) {
/* return an img object that represents the file type */
return this.doc.createTextNode('\xa0');
};
//Root.prototype.getPath = function() {
// return '/' + this.id; // FIXME
//};
Tree.prototype._positionMenu = function(event, menu) {
var left = event.layerX;
var top = event.layerY;
menu.style.left = left + 'px';
menu.style.top = top + 'px';
};
Tree.prototype.getItemByPath = function(path) {
/* return an item by its path
path should be relative from the current item, excluding
the current item's id
*/
// if the path starts with a slash, remove it
if (path.charAt(0) == '/') {
path = path.substr(1);
};
if (path == this.root.id) {
return this.root;
}
if (path.indexOf(this.root.id) == 0) {
path = path.substr(this.root.id.length+1);
return this.root.getItemByPath(path);
} else {
return false;
}
};
// Copied from helpers.js to not have a dependency
function ContextFixer(func, context) {
/* Make sure 'this' inside a method points to its class */
this.func = func;
this.context = context;
this.args = arguments;
var self = this;
this.execute = function() {
/* execute the method */
var args = new Array();
// the first arguments will be the extra ones of the class
for (var i=0; i < self.args.length - 2; i++) {
args.push(self.args[i + 2]);
};
// the last are the ones passed on to the execute method
for (var i=0; i < arguments.length; i++) {
args.push(arguments[i]);
};
return self.func.apply(self.context, args);
};
};
// this is used to make sure that all registered event handlers get a unique
// id, this id will be returned by addEventHandler and can be used to remove
// the event handler later on
// of course you *must never* change this id!
LAST_HANDLER_ID = 0;
// a mapping from id to handler (required because the handlers are wrapped
// before they're registered, and the mapped version is not available to the
// client, while it is required for de-registering the event later on
// needless to say this should never be touched...
ID_TO_HANDLER = {};
function addEventHandler(element, event, method, context) {
/* method to add an event handler for both IE and Mozilla */
var wrappedmethod = new ContextFixer(method, context);
// store it, return the id, that can be used to de-register the
// handler later on
var id = LAST_HANDLER_ID++;
ID_TO_HANDLER[id] = wrappedmethod;
var args = new Array(null, null);
for (var i=4; i < arguments.length; i++) {
args.push(arguments[i]);
};
wrappedmethod.args = args;
try {
if (element.addEventListener) {
element.addEventListener(event, wrappedmethod.execute, false);
} else if (element.attachEvent) {
element.attachEvent("on" + event, wrappedmethod.execute);
} else {
throw "Unsupported browser!";
};
} catch(e) {
alert('exception ' + e.message + ' while registering an event handler for element ' + element + ', event ' + event + ', method ' + method);
};
return id;
};
function removeEventHandler(element, event, id) {
/* method to remove an event handler for both IE and Mozilla */
var method = ID_TO_HANDLER[id].execute;
if (element.removeEventListener) {
element.removeEventListener(event, method, false);
} else if (element.detachEvent) {
element.detachEvent("on" + event, method);
} else {
throw "Unsupported browser!";
};
};
String.prototype.strip = function() {
var stripspace = /^\s*([\s\S]*?)\s*$/;
return stripspace.exec(this)[1];
};