506 lines
14 KiB
JavaScript
506 lines
14 KiB
JavaScript
/**
|
|
* Provides the javascript class to create dynamic trees.
|
|
*
|
|
* Optionally uses the Horde_Tooltip class (tooltips.js).
|
|
*
|
|
* Custom Events
|
|
* -------------
|
|
* The 'memo' property of the Event object contains the original event object.
|
|
*
|
|
* 'Horde_Tree:expand'
|
|
* Fired when a tree element is expanded.
|
|
*
|
|
* 'Horde_Tree:collapse'
|
|
* Fired when a tree element is collapsed.
|
|
*
|
|
* @author Marko Djukic <marko@oblo.com>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @copyright 2003-2015 Horde LLC
|
|
* @license LGPL-2.1 (http://www.horde.org/licenses/lgpl21)
|
|
*/
|
|
|
|
var Horde_Tree = Class.create({
|
|
|
|
charlist: 'abcdefghijklmnopqrstuvwxyz',
|
|
childid: '',
|
|
randstrlen: 4,
|
|
toggleid: '',
|
|
|
|
initialize: function(opts)
|
|
{
|
|
var i, randstr = '';
|
|
|
|
this.opts = opts;
|
|
|
|
for (i = 0; i < this.randstrlen; ++i) {
|
|
randstr += this.charlist.charAt(Math.floor(Math.random() * 26));
|
|
}
|
|
|
|
this.childid = 'horde-tree-child-' + randstr + '-';
|
|
this.toggleid = 'horde-tree-toggle-' + randstr + '-';
|
|
|
|
if (this.opts.initTree) {
|
|
this.renderTree(this.opts.initTree.nodes, this.opts.initTree.root_nodes, this.opts.initTree.is_static);
|
|
this.opts.initTree = null;
|
|
}
|
|
|
|
$(this.opts.target).observe('click', this._onClick.bindAsEventListener(this));
|
|
},
|
|
|
|
renderTree: function(nodes, rootNodes, renderStatic)
|
|
{
|
|
this.nodes = nodes;
|
|
this.rootNodes = rootNodes;
|
|
this.renderStatic = renderStatic;
|
|
this.dropline = [];
|
|
this.output = document.createDocumentFragment();
|
|
|
|
this._buildHeader();
|
|
|
|
this.rootNodes.each(function(r) {
|
|
this.buildTree(r, this.output);
|
|
}, this);
|
|
|
|
$(this.opts.target).update('');
|
|
$(this.opts.target).appendChild(this.output);
|
|
|
|
// If using alternating row shading, work out correct shade.
|
|
if (this.opts.options.alternate) {
|
|
this.stripe();
|
|
}
|
|
},
|
|
|
|
_buildHeader: function()
|
|
{
|
|
if (this.opts.options.hideHeaders ||
|
|
!this.opts.header.size()) {
|
|
return;
|
|
}
|
|
|
|
var div = new Element('DIV');
|
|
|
|
this.opts.header.each(function(h) {
|
|
var tmp = new Element('SPAN').insert(h.html ? h.html : ' ');
|
|
|
|
if (h['class']) {
|
|
tmp.addClassName(h['class']);
|
|
}
|
|
|
|
div.appendChild(tmp);
|
|
}, this);
|
|
|
|
this.output.appendChild(div);
|
|
},
|
|
|
|
// Recursive function to walk through the tree array and build
|
|
// the output.
|
|
buildTree: function(nodeId, p)
|
|
{
|
|
var last_subnode, tmp,
|
|
node = this.nodes[nodeId];
|
|
|
|
this.buildLine(nodeId, p);
|
|
|
|
if (!Object.isUndefined(node.children)) {
|
|
last_subnode = node.children.last();
|
|
tmp = new Element('DIV', { id: this.childid + nodeId });
|
|
[ tmp ].invoke(node.expanded ? 'show' : 'hide');
|
|
|
|
node.children.each(function(c) {
|
|
if (c == last_subnode) {
|
|
this.nodes[c].node_last = true;
|
|
}
|
|
this.buildTree(c, tmp);
|
|
}, this);
|
|
|
|
p.appendChild(tmp);
|
|
}
|
|
},
|
|
|
|
buildLine: function(nodeId, p)
|
|
{
|
|
var div, label, tmp,
|
|
column = 0,
|
|
node = this.nodes[nodeId];
|
|
|
|
div = new Element('DIV', { className: 'horde-tree-row' });
|
|
if (node['class']) {
|
|
div.addClassName(node['class']);
|
|
}
|
|
|
|
// If we have headers, track which logical "column" we're in for
|
|
// any given cell of content.
|
|
if (node.extra && node.extra[0]) {
|
|
node.extra[0].each(function(n) {
|
|
div.insert(this._divClass(new Element('SPAN').update(n), column++));
|
|
}, this);
|
|
}
|
|
|
|
for (; column < this.opts.extraColsLeft; ++column) {
|
|
div.insert(this._divClass(new Element('SPAN').update(' '), column));
|
|
}
|
|
|
|
div.insert(this._divClass(new Element('SPAN'), column));
|
|
|
|
tmp = document.createDocumentFragment();
|
|
for (i = Number(this.renderStatic); i < node.indent; ++i) {
|
|
tmp.appendChild(new Element('SPAN').addClassName('horde-tree-image').addClassName(
|
|
'horde-tree-image-' + ((this.dropline[i] && this.opts.options.lines)
|
|
? this.opts.imgLine
|
|
: this.opts.imgBlank)
|
|
));
|
|
}
|
|
|
|
tmp.appendChild(this._setNodeToggle(nodeId));
|
|
|
|
if (node.url) {
|
|
label = new Element('A', { href: node.url }).insert(
|
|
this._setNodeIcon(nodeId)
|
|
).insert(
|
|
node.label
|
|
);
|
|
|
|
if (node.urlclass) {
|
|
label.addClassName(node.urlclass);
|
|
} else if (this.opts.options.urlclass) {
|
|
label.addClassName(this.opts.options.urlclass);
|
|
}
|
|
|
|
if (node.title) {
|
|
label.writeAttribute('title', node.title);
|
|
}
|
|
|
|
if (node.target) {
|
|
label.writeAttribute('target', node.target);
|
|
} else if (this.opts.options.target) {
|
|
label.writeAttribute('target', this.opts.options.target);
|
|
}
|
|
|
|
label = label.wrap('SPAN');
|
|
} else {
|
|
label = new Element('SPAN').addClassName('horde-toggle').insert(
|
|
this._setNodeIcon(nodeId)
|
|
).insert(
|
|
node.label
|
|
);
|
|
}
|
|
|
|
if (this.opts.options.multiline) {
|
|
div.insert(new Element('TABLE').insert(
|
|
new Element('TR').insert(
|
|
new Element('TD').appendChild(tmp)
|
|
).insert(
|
|
new Element('TD').insert(label)
|
|
)
|
|
));
|
|
} else {
|
|
div.appendChild(tmp);
|
|
div.insert(label);
|
|
}
|
|
|
|
++column;
|
|
|
|
if (node.extra && node.extra[1]) {
|
|
node.extra[1].each(function(n) {
|
|
div.insert(this._divClass(new Element('SPAN').update(n), column++));
|
|
}, this);
|
|
}
|
|
|
|
for (; column < this.opts.extraColsRight; ++column) {
|
|
div.insert(this._divClass(new Element('SPAN').update(' '), column));
|
|
}
|
|
|
|
p.appendChild(div);
|
|
},
|
|
|
|
_divClass: function(div, c)
|
|
{
|
|
if (this.opts.header[c] && this.opts.header[c]['class']) {
|
|
div.addClassName(this.opts.header[c]['class']);
|
|
}
|
|
|
|
return div;
|
|
},
|
|
|
|
_setNodeToggle: function(nodeId)
|
|
{
|
|
var node = this.nodes[nodeId];
|
|
|
|
if (node.indent == '0') {
|
|
// Top level with children.
|
|
this.dropline[0] = false;
|
|
|
|
if (this.renderStatic) {
|
|
return '';
|
|
}
|
|
|
|
switch (this._getLineType(nodeId)) {
|
|
case 1:
|
|
case 2:
|
|
this.dropline[0] = true;
|
|
break;
|
|
}
|
|
} else {
|
|
this.dropline[node.indent] = !node.node_last;
|
|
}
|
|
|
|
return new Element('SPAN', { id: this.toggleid + nodeId }).addClassName('horde-tree-toggle').addClassName('horde-tree-image').addClassName('horde-tree-image-' + this._getNodeToggle(nodeId));
|
|
},
|
|
|
|
_getNodeToggle: function(nodeId)
|
|
{
|
|
var type,
|
|
node = this.nodes[nodeId];
|
|
|
|
if (node.indent == '0') {
|
|
if (this.renderStatic) {
|
|
return '';
|
|
}
|
|
|
|
if (!this.opts.options.lines) {
|
|
return this.opts.imgBlank;
|
|
}
|
|
|
|
type = this._getLineType(nodeId);
|
|
|
|
if (node.children) {
|
|
// Top level with children.
|
|
if (node.expanded) {
|
|
return type
|
|
? ((type == 2) ? this.opts.imgMinus : this.opts.imgMinusBottom)
|
|
: this.opts.imgMinusOnly;
|
|
}
|
|
|
|
return type
|
|
? ((type == 2) ? this.opts.imgPlus : this.opts.imgPlusBottom)
|
|
: this.opts.imgPlusOnly;
|
|
}
|
|
|
|
switch (type) {
|
|
case 0:
|
|
return this.opts.imgNullOnly;
|
|
|
|
case 1:
|
|
return this.opts.imgJoinTop;
|
|
|
|
case 2:
|
|
return this.opts.imgJoin;
|
|
|
|
case 3:
|
|
return this.opts.imgJoinBottom;
|
|
}
|
|
}
|
|
|
|
if (node.children) {
|
|
// Node with children.
|
|
if (!node.node_last) {
|
|
// Not last node.
|
|
if (!this.opts.options.lines) {
|
|
return this.opts.imgBlank;
|
|
} else if (this.renderStatic) {
|
|
return this.opts.imgJoin;
|
|
} else if (node.expanded) {
|
|
return this.opts.imgMinus;
|
|
}
|
|
|
|
return this.opts.imgPlus;
|
|
}
|
|
|
|
// Last node.
|
|
if (!this.opts.options.lines) {
|
|
return this.opts.imgBlank;
|
|
} else if (this.renderStatic) {
|
|
return this.opts.imgJoinBottom;
|
|
} else if (node.expanded) {
|
|
return this.opts.imgMinusBottom;
|
|
}
|
|
|
|
return this.opts.imgPlusBottom;
|
|
}
|
|
|
|
// Node no children.
|
|
if (!node.node_last) {
|
|
// Not last node.
|
|
return this.opts.options.lines
|
|
? this.opts.imgJoin
|
|
: this.opts.imgBlank;
|
|
}
|
|
|
|
// Last node.
|
|
return this.opts.options.lines
|
|
? this.opts.imgJoinBottom
|
|
: this.opts.imgBlank;
|
|
},
|
|
|
|
_getLineType: function(nodeId)
|
|
{
|
|
if (this.opts.options.lines_base &&
|
|
this.rootNodes.size() > 1) {
|
|
switch (this.rootNodes.indexOf(nodeId)) {
|
|
case 0:
|
|
return 1;
|
|
|
|
case (this.rootNodes.size() - 1):
|
|
return 3;
|
|
|
|
default:
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
},
|
|
|
|
_setNodeIcon: function(nodeId)
|
|
{
|
|
var img,
|
|
node = this.nodes[nodeId];
|
|
|
|
// Image.
|
|
if (!Object.isUndefined(node.icon)) {
|
|
if (!node.icon) {
|
|
return '';
|
|
} else {
|
|
// Node has a user defined icon.
|
|
img = new Element('IMG', { id: 'horde-node-icon-' + nodeId, src: (node.iconopen && node.expanded ? node.iconopen : node.icon) }).addClassName('horde-tree-icon');
|
|
}
|
|
} else {
|
|
img = new Element('SPAN', { id: 'horde-node-icon-' + nodeId }).addClassName('horde-tree-icon');
|
|
if (node.children) {
|
|
// Standard icon: node with children.
|
|
img.addClassName('horde-tree-image-' + (node.expanded ? this.opts.imgFolderOpen : this.opts.imgFolder));
|
|
} else {
|
|
// Standard icon: node, no children.
|
|
img.addClassName('horde-tree-image-' + this.opts.imgLeaf);
|
|
}
|
|
}
|
|
|
|
if (node.iconalt) {
|
|
img.writeAttribute('alt', node.iconalt);
|
|
}
|
|
|
|
return img;
|
|
},
|
|
|
|
toggle: function(nodeId)
|
|
{
|
|
var icon, toggle, children,
|
|
node = this.nodes[nodeId];
|
|
|
|
if (!node.children) {
|
|
return;
|
|
}
|
|
|
|
node.expanded = !node.expanded;
|
|
if ((children = $(this.childid + nodeId))) {
|
|
children.setStyle({ display: node.expanded ? 'block' : 'none' });
|
|
}
|
|
|
|
// Toggle the node's icon if it has separate open and closed
|
|
// icons.
|
|
if ((icon = $('horde-node-icon-' + nodeId))) {
|
|
// Image.
|
|
if (node.icon) {
|
|
icon.writeAttribute('src', (node.expanded && node.iconopen) ? node.iconopen : node.icon);
|
|
} else {
|
|
// Use standard icon set.
|
|
icon.writeAttribute('src', node.expanded ? this.opts.imgFolderOpen : this.opts.imgFolder);
|
|
}
|
|
}
|
|
|
|
// If using alternating row shading, work out correct shade.
|
|
if (this.opts.options.alternate) {
|
|
this.stripe();
|
|
}
|
|
|
|
if ((toggle = $(this.toggleid + nodeId))) {
|
|
toggle.writeAttribute('class', 'horde-tree-toggle horde-tree-image').addClassName('horde-tree-image-' + this._getNodeToggle(nodeId));
|
|
}
|
|
|
|
$(this.opts.target).fire(node.expanded ? 'Horde_Tree:expand' : 'Horde_Tree:collapse', nodeId);
|
|
|
|
this.saveState(nodeId, node.expanded);
|
|
},
|
|
|
|
stripe: function()
|
|
{
|
|
var classes = [ 'rowEven', 'rowOdd' ],
|
|
i = 0;
|
|
|
|
$(this.opts.target).select('DIV.horde-tree-row').each(function(r) {
|
|
classes.each(r.removeClassName.bind(r));
|
|
if (r.clientHeight) {
|
|
r.addClassName(classes[++i % 2]);
|
|
}
|
|
});
|
|
},
|
|
|
|
saveState: function(nodeId, expanded)
|
|
{
|
|
if (this.opts.nocookie) {
|
|
return;
|
|
}
|
|
|
|
var newCookie = '',
|
|
newNodes = [],
|
|
oldCookie = this._getCookie(this.opts.target + '_expanded');
|
|
|
|
if (expanded) {
|
|
// Expand requested so add to cookie.
|
|
newCookie = (oldCookie ? oldCookie + ',' : '') + nodeId;
|
|
} else {
|
|
// Collapse requested so remove from cookie.
|
|
oldCookie.split(',').each(function(n) {
|
|
if (n != nodeId) {
|
|
newNodes[newNodes.length] = n;
|
|
}
|
|
});
|
|
newCookie = newNodes.join(',');
|
|
}
|
|
|
|
document.cookie = this.opts.target + '_expanded=exp' + escape(newCookie) + ';DOMAIN=' + this.opts.cookieDomain + ';PATH=' + this.opts.cookiePath + ';';
|
|
},
|
|
|
|
_getCookie: function(name)
|
|
{
|
|
var end,
|
|
dc = document.cookie,
|
|
prefix = name + '=exp',
|
|
begin = dc.indexOf('; ' + prefix);
|
|
|
|
if (begin == -1) {
|
|
begin = dc.indexOf(prefix);
|
|
if (begin !== 0) {
|
|
return '';
|
|
}
|
|
} else {
|
|
begin += 2;
|
|
}
|
|
|
|
end = document.cookie.indexOf(';', begin);
|
|
if (end == -1) {
|
|
end = dc.length;
|
|
}
|
|
|
|
return unescape(dc.substring(begin + prefix.length, end));
|
|
},
|
|
|
|
_onClick: function(e)
|
|
{
|
|
var elt = e.element(), id;
|
|
|
|
if (elt.hasClassName('horde-tree-icon')) {
|
|
elt = elt.up().previous();
|
|
} else if (elt.hasClassName('horde-toggle')) {
|
|
elt = elt.previous();
|
|
}
|
|
|
|
id = elt.readAttribute('id');
|
|
if (id && id.startsWith(this.toggleid)) {
|
|
this.toggle(id.substr(this.toggleid.length));
|
|
e.stop();
|
|
}
|
|
}
|
|
|
|
});
|