fsf changes, meant to be rebased on upstream
[squirrelmail.git] / templates / default_advanced / js / dtree.js
1 /*--------------------------------------------------|
2 | dTree 2.05 | www.destroydrop.com/javascript/tree/ |
3 |---------------------------------------------------|
4 | Copyright (c) 2002-2003 Geir Landr? |
5 | |
6 | This script can be used freely as long as all |
7 | copyright messages are intact. |
8 | |
9 | Updated: 17.04.2003 |
10 |--------------------------------------------------*/
11
12 // Node object
13 function Node(id, pid, name, url, title, target, icon, iconOpen, accesskey, open) {
14 this.id = id;
15 this.pid = pid;
16 this.name = name;
17 this.url = url;
18 this.title = title;
19 this.target = target;
20 this.icon = icon;
21 this.iconOpen = iconOpen;
22 this.accesskey = accesskey;
23 this._io = open || false;
24 this._is = false;
25 this._ls = false;
26 this._hc = false;
27 this._ai = 0;
28 this._p;
29 };
30
31 // Tree object// imagePath parameter added by SquirrelMail Team
32 function dTree(objName, imagePath) {
33 this.config = {
34 target : null,
35 folderLinks : true,
36 useSelection : true,
37 useCookies : true,
38 useLines : true,
39 useIcons : true,
40 useStatusText : false,
41 closeSameLevel : false,
42 inOrder : false
43 }
44 this.icon = {
45 root : imagePath+'/base.png',
46 folder : imagePath+'/folder.png',
47 folderOpen : imagePath+'/folderopen.png',
48 node : imagePath+'/page.png',
49 empty : imagePath+'/empty.png',
50 line : imagePath+'/line.png',
51 join : imagePath+'/join.png',
52 joinBottom : imagePath+'/joinbottom.png',
53 plus : imagePath+'/plus_mid.png',
54 plusBottom : imagePath+'/plusbottom.png',
55 minus : imagePath+'/minus_mid.png',
56 minusBottom : imagePath+'/minusbottom.png',
57 nlPlus : imagePath+'/nolines_plus.png',
58 nlMinus : imagePath+'/nolines_minus.png'
59 };
60 this.obj = objName;
61 this.aNodes = [];
62 this.aIndent = [];
63 this.root = new Node(-1);
64 this.selectedNode = null;
65 this.selectedFound = false;
66 this.completed = false; this.imagePath = imagePath;
67 };
68
69 // Adds a new node to the node array
70 dTree.prototype.add = function(id, pid, name, url, title, target, icon, iconOpen, accesskey, open) {
71 this.aNodes[this.aNodes.length] = new Node(id, pid, name, url, title, target, icon, iconOpen, accesskey, open);
72 };
73
74 // Open/close all nodes
75 dTree.prototype.openAll = function() {
76 this.oAll(true);
77 };
78 dTree.prototype.closeAll = function() {
79 this.oAll(false);
80 };
81
82 // Outputs the tree to the page
83 dTree.prototype.toString = function() {
84 var str = '<div class="dtree">\n';
85 if (document.getElementById) {
86 if (this.config.useCookies) this.selectedNode = this.getSelected();
87 str += this.addNode(this.root);
88 } else str += 'Browser not supported.';
89 str += '</div>';
90 if (!this.selectedFound) this.selectedNode = null;
91 this.completed = true;
92 return str;
93 };
94
95 // Creates the tree structure
96 dTree.prototype.addNode = function(pNode) {
97 var str = '';
98 var n=0;
99 if (this.config.inOrder) n = pNode._ai;
100 for (n; n<this.aNodes.length; n++) {
101 if (this.aNodes[n].pid == pNode.id) {
102 var cn = this.aNodes[n];
103 cn._p = pNode;
104 cn._ai = n;
105 this.setCS(cn);
106 if (!cn.target && this.config.target) cn.target = this.config.target;
107 if (cn._hc && !cn._io && this.config.useCookies) cn._io = this.isOpen(cn.id);
108 if (!this.config.folderLinks && cn._hc) cn.url = null;
109 if (this.config.useSelection && cn.id == this.selectedNode && !this.selectedFound) {
110 cn._is = true;
111 this.selectedNode = n;
112 this.selectedFound = true;
113 }
114 str += this.node(cn, n);
115 if (cn._ls) break;
116 }
117 }
118 return str;
119 };
120
121 // Creates the node icon, url and text
122 dTree.prototype.node = function(node, nodeId) {
123 var str = '<div class="dTreeNode">' + this.indent(node, nodeId);
124 if (this.config.useIcons) {
125 if (!node.icon) node.icon = (this.root.id == node.pid) ? this.icon.root : ((node._hc) ? this.icon.folder : this.icon.node);
126 if (!node.iconOpen) node.iconOpen = (node._hc) ? this.icon.folderOpen : this.icon.node;
127 if (this.root.id == node.pid) {
128 node.icon = this.icon.root;
129 node.iconOpen = this.icon.root;
130 }
131 str += '<img id="i' + this.obj + nodeId + '" src="' + ((node._io) ? node.iconOpen : node.icon) + '" alt="" />';
132 }
133 if (node.url) {
134 str += '<a id="s' + this.obj + nodeId + '" class="' + ((this.config.useSelection) ? ((node._is ? 'nodeSel' : 'node')) : 'node') + '" href="' + node.url + '"';
135 if (node.title) str += ' title="' + node.title + '"';
136 if (node.target) str += ' target="' + node.target + '"';
137 if (node.accesskey) str += ' accesskey="' + node.accesskey + '"';
138 if (this.config.useStatusText) str += ' onmouseover="window.status=\'' + node.name + '\';return true;" onmouseout="window.status=\'\';return true;" ';
139 if (this.config.useSelection && ((node._hc && this.config.folderLinks) || !node._hc))
140 str += ' onclick="javascript: ' + this.obj + '.s(' + nodeId + ');"';
141 str += '>';
142 }
143 else if ((!this.config.folderLinks || !node.url) && node._hc && node.pid != this.root.id)
144 str += '<a href="javascript: ' + this.obj + '.o(' + nodeId + ');" class="node">';
145 str += node.name;
146 //FIXME: if node.name contains a hyperlink, either the plugin that put it there should be responsible for adding the </a> for the main folder link or the line below should be changed to detect inner links and close the main one before any; for now, it seems to work anyway although I think the resultant HTML is invalid (hyperlink within a hyperlink and extra, out of place closing </a> at the end)
147 if (node.url || ((!this.config.folderLinks || !node.url) && node._hc)) str += '</a>';
148 str += '</div>';
149 if (node._hc) {
150 str += '<div id="d' + this.obj + nodeId + '" class="clip" style="display:' + ((this.root.id == node.pid || node._io) ? 'block' : 'none') + ';">';
151 str += this.addNode(node);
152 str += '</div>';
153 }
154 this.aIndent.pop();
155 return str;
156 };
157
158 // Adds the empty and line icons
159 dTree.prototype.indent = function(node, nodeId) {
160 var str = '';
161 if (this.root.id != node.pid) {
162 for (var n=0; n<this.aIndent.length; n++)
163 str += '<img src="' + ( (this.aIndent[n] == 1 && this.config.useLines) ? this.icon.line : this.icon.empty ) + '" alt="" />';
164 (node._ls) ? this.aIndent.push(0) : this.aIndent.push(1);
165 if (node._hc) {
166 str += '<a href="javascript: ' + this.obj + '.o(' + nodeId + ');"><img id="j' + this.obj + nodeId + '" src="';
167 if (!this.config.useLines) str += (node._io) ? this.icon.nlMinus : this.icon.nlPlus;
168 else str += ( (node._io) ? ((node._ls && this.config.useLines) ? this.icon.minusBottom : this.icon.minus) : ((node._ls && this.config.useLines) ? this.icon.plusBottom : this.icon.plus ) );
169 str += '" alt="" /></a>';
170 } else str += '<img src="' + ( (this.config.useLines) ? ((node._ls) ? this.icon.joinBottom : this.icon.join ) : this.icon.empty) + '" alt="" />';
171 }
172 return str;
173 };
174
175 // Checks if a node has any children and if it is the last sibling
176 dTree.prototype.setCS = function(node) {
177 var lastId;
178 for (var n=0; n<this.aNodes.length; n++) {
179 if (this.aNodes[n].pid == node.id) node._hc = true;
180 if (this.aNodes[n].pid == node.pid) lastId = this.aNodes[n].id;
181 }
182 if (lastId==node.id) node._ls = true;
183 };
184
185 // Returns the selected node
186 dTree.prototype.getSelected = function() {
187 var sn = this.getCookie('cs' + this.obj);
188 return (sn) ? sn : null;
189 };
190
191 // Highlights the selected node
192 dTree.prototype.s = function(id) {
193 if (!this.config.useSelection) return;
194 var cn = this.aNodes[id];
195 if (cn._hc && !this.config.folderLinks) return;
196 if (this.selectedNode != id) {
197 if (this.selectedNode || this.selectedNode==0) {
198 eOld = document.getElementById("s" + this.obj + this.selectedNode);
199 eOld.className = "node";
200 }
201 eNew = document.getElementById("s" + this.obj + id);
202 eNew.className = "nodeSel";
203 this.selectedNode = id;
204 if (this.config.useCookies) this.setCookie('cs' + this.obj, cn.id);
205 }
206 };
207
208 // Toggle Open or close
209 dTree.prototype.o = function(id) {
210 var cn = this.aNodes[id];
211 this.nodeStatus(!cn._io, id, cn._ls);
212 cn._io = !cn._io;
213 if (this.config.closeSameLevel) this.closeLevel(cn);
214 if (this.config.useCookies) this.updateCookie();
215 };
216
217 // Open or close all nodes
218 dTree.prototype.oAll = function(status) {
219 for (var n=0; n<this.aNodes.length; n++) {
220 if (this.aNodes[n]._hc && this.aNodes[n].pid != this.root.id) {
221 this.nodeStatus(status, n, this.aNodes[n]._ls)
222 this.aNodes[n]._io = status;
223 }
224 }
225 if (this.config.useCookies) this.updateCookie();
226 };
227
228 // Opens the tree to a specific node
229 dTree.prototype.openTo = function(nId, bSelect, bFirst) {
230 if (!bFirst) {
231 for (var n=0; n<this.aNodes.length; n++) {
232 if (this.aNodes[n].id == nId) {
233 nId=n;
234 break;
235 }
236 }
237 }
238 var cn=this.aNodes[nId];
239 if (cn.pid==this.root.id || !cn._p) return;
240 cn._io = true;
241 cn._is = bSelect;
242 if (this.completed && cn._hc) this.nodeStatus(true, cn._ai, cn._ls);
243 if (this.completed && bSelect) this.s(cn._ai);
244 else if (bSelect) this._sn=cn._ai;
245 this.openTo(cn._p._ai, false, true);
246 };
247
248 // Closes all nodes on the same level as certain node
249 dTree.prototype.closeLevel = function(node) {
250 for (var n=0; n<this.aNodes.length; n++) {
251 if (this.aNodes[n].pid == node.pid && this.aNodes[n].id != node.id && this.aNodes[n]._hc) {
252 this.nodeStatus(false, n, this.aNodes[n]._ls);
253 this.aNodes[n]._io = false;
254 this.closeAllChildren(this.aNodes[n]);
255 }
256 }
257 }
258
259 // Closes all children of a node
260 dTree.prototype.closeAllChildren = function(node) {
261 for (var n=0; n<this.aNodes.length; n++) {
262 if (this.aNodes[n].pid == node.id && this.aNodes[n]._hc) {
263 if (this.aNodes[n]._io) this.nodeStatus(false, n, this.aNodes[n]._ls);
264 this.aNodes[n]._io = false;
265 this.closeAllChildren(this.aNodes[n]);
266 }
267 }
268 }
269
270 // Change the status of a node(open or closed)
271 dTree.prototype.nodeStatus = function(status, id, bottom) {
272 eDiv = document.getElementById('d' + this.obj + id);
273 eJoin = document.getElementById('j' + this.obj + id);
274 if (this.config.useIcons) {
275 eIcon = document.getElementById('i' + this.obj + id);
276 eIcon.src = (status) ? this.aNodes[id].iconOpen : this.aNodes[id].icon;
277 }
278 eJoin.src = (this.config.useLines)?
279 ((status)?((bottom)?this.icon.minusBottom:this.icon.minus):((bottom)?this.icon.plusBottom:this.icon.plus)):
280 ((status)?this.icon.nlMinus:this.icon.nlPlus);
281 eDiv.style.display = (status) ? 'block': 'none';
282 };
283
284
285 // [Cookie] Clears a cookie
286 dTree.prototype.clearCookie = function() {
287 var now = new Date();
288 var yesterday = new Date(now.getTime() - 1000 * 60 * 60 * 24);
289 this.setCookie('co'+this.obj, 'cookieValue', yesterday);
290 this.setCookie('cs'+this.obj, 'cookieValue', yesterday);
291 };
292
293 // [Cookie] Sets value in a cookie
294 // FIXME: although setCookie() supports the secure flag, it isn't used when called anywhere in this file; ideally it should correspond to how the flag is determined in the SM core in sqsetcookie() (including the admin config that turns the secure flag off)... also, would be good to add the HTTP only flag
295 dTree.prototype.setCookie = function(cookieName, cookieValue, expires, path, domain, secure) {
296 document.cookie =
297 escape(cookieName) + '=' + escape(cookieValue)
298 + (expires ? '; expires=' + expires.toGMTString() : '')
299 + (path ? '; path=' + path : '')
300 + (domain ? '; domain=' + domain : '')
301 + (secure ? '; secure' : '');
302 };
303
304 // [Cookie] Gets a value from a cookie
305 dTree.prototype.getCookie = function(cookieName) {
306 var cookieValue = '';
307 var posName = document.cookie.indexOf(escape(cookieName) + '=');
308 if (posName != -1) {
309 var posValue = posName + (escape(cookieName) + '=').length;
310 var endPos = document.cookie.indexOf(';', posValue);
311 if (endPos != -1) cookieValue = unescape(document.cookie.substring(posValue, endPos));
312 else cookieValue = unescape(document.cookie.substring(posValue));
313 }
314 return (cookieValue);
315 };
316
317 // [Cookie] Returns ids of open nodes as a string
318 dTree.prototype.updateCookie = function() {
319 var str = '';
320 for (var n=0; n<this.aNodes.length; n++) {
321 if (this.aNodes[n]._io && this.aNodes[n].pid != this.root.id) {
322 if (str) str += '.';
323 str += this.aNodes[n].id;
324 }
325 }
326 this.setCookie('co' + this.obj, str);
327 };
328
329 // [Cookie] Checks if a node id is in a cookie
330 dTree.prototype.isOpen = function(id) {
331 var aOpen = this.getCookie('co' + this.obj).split('.');
332 for (var n=0; n<aOpen.length; n++)
333 if (aOpen[n] == id) return true;
334 return false;
335 };
336
337 // If Push and pop is not implemented by the browser
338 if (!Array.prototype.push) {
339 Array.prototype.push = function array_push() {
340 for(var i=0;i<arguments.length;i++)
341 this[this.length]=arguments[i];
342 return this.length;
343 }
344 };
345 if (!Array.prototype.pop) {
346 Array.prototype.pop = function array_pop() {
347 lastElement = this[this.length-1];
348 this.length = Math.max(this.length-1,0);
349 return lastElement;
350 }
351 };