]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - jssource/src_files/include/ytree/TreeView/Node.js
Release 6.5.0
[Github/sugarcrm.git] / jssource / src_files / include / ytree / TreeView / Node.js
1 /* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */
2
3 /**
4  * The base class for all tree nodes.  The node's presentation and behavior in
5  * response to mouse events is handled in Node subclasses.
6  *
7  * @param oData {object} a string or object containing the data that will
8  * be used to render this node
9  * @param oParent {Node} this node's parent node
10  * @param expanded {boolean} the initial expanded/collapsed state
11  * @constructor
12  */
13 YAHOO.widget.Node = function(oData, oParent, expanded) {
14         if (oParent) { this.init(oData, oParent, expanded); }
15 };
16
17 YAHOO.widget.Node.prototype = {
18
19     /**
20      * The index for this instance obtained from global counter in YAHOO.widget.TreeView.
21      *
22      * @type int
23      */
24     index: 0,
25
26     /**
27      * This node's child node collection.
28      *
29      * @type Node[] 
30      */
31     children: null,
32
33     /**
34      * Tree instance this node is part of
35      *
36      * @type TreeView
37      */
38     tree: null,
39
40     /**
41      * The data linked to this node.  This can be any object or primitive
42      * value, and the data can be used in getNodeHtml().
43      *
44      * @type object
45      */
46     data: null,
47
48     /**
49      * Parent node
50      *
51      * @type Node
52      */
53     parent: null,
54
55     /**
56      * The depth of this node.  We start at -1 for the root node.
57      *
58      * @type int
59      */
60     depth: -1,
61
62     /**
63      * The href for the node's label.  If one is not specified, the href will
64      * be set so that it toggles the node.
65      *
66      * @type string
67      */
68     href: null,
69
70     /**
71      * The label href target, defaults to current window
72      *
73      * @type string
74      */
75     target: "_self",
76
77     /**
78      * The node's expanded/collapsed state
79      *
80      * @type boolean
81      */
82     expanded: false,
83
84     /**
85      * Can multiple children be expanded at once?
86      *
87      * @type boolean
88      */
89     multiExpand: true,
90
91     /**
92      * Should we render children for a collapsed node?  It is possible that the
93      * implementer will want to render the hidden data...  @todo verify that we 
94      * need this, and implement it if we do.
95      *
96      * @type boolean
97      */
98     renderHidden: false,
99
100     /**
101      * Flag that is set to true the first time this node's children are rendered.
102      *
103      * @type boolean
104      */
105     childrenRendered: false,
106
107     /**
108      * This node's previous sibling
109      *
110      * @type Node
111      */
112     previousSibling: null,
113
114     /**
115      * This node's next sibling
116      *
117      * @type Node
118      */
119     nextSibling: null,
120
121     /**
122      * We can set the node up to call an external method to get the child
123      * data dynamically.
124      *
125      * @type boolean
126      * @private
127      */
128     _dynLoad: false,
129
130     /**
131      * Function to execute when we need to get this node's child data.
132      *
133      * @type function
134      */
135     dataLoader: null,
136
137     /**
138      * This is true for dynamically loading nodes while waiting for the
139      * callback to return.
140      *
141      * @type boolean
142      */
143     isLoading: false,
144
145     /**
146      * The toggle/branch icon will not show if this is set to false.  This
147      * could be useful if the implementer wants to have the child contain
148      * extra info about the parent, rather than an actual node.
149      *
150      * @type boolean
151      */
152     hasIcon: true,
153
154     /**
155      * Initializes this node, gets some of the properties from the parent
156      *
157      * @param oData {object} a string or object containing the data that will
158      * be used to render this node
159      * @param oParent {Node} this node's parent node
160      * @param expanded {boolean} the initial expanded/collapsed state
161      */
162     init: function(oData, oParent, expanded) {
163         this.data               = oData;
164         this.children   = [];
165         this.index              = YAHOO.widget.TreeView.nodeCount;
166         ++YAHOO.widget.TreeView.nodeCount;
167         this.logger             = new ygLogger("Node");
168         this.expanded   = expanded;
169
170         // oParent should never be null except when we create the root node.
171         if (oParent) {
172             this.tree                   = oParent.tree;
173             this.parent                 = oParent;
174             this.href                   = "javascript:" + this.getToggleLink();
175             this.depth                  = oParent.depth + 1;
176             this.multiExpand    = oParent.multiExpand;
177
178             oParent.appendChild(this);
179         }
180     },
181
182     /**
183      * Appends a node to the child collection.
184      *
185      * @param node {Node} the new node
186      * @return {Node} the child node
187      * @private
188      */
189     appendChild: function(node) {
190         if (this.hasChildren()) {
191             var sib = this.children[this.children.length - 1];
192             sib.nextSibling = node;
193             node.previousSibling = sib;
194         }
195
196         this.tree.regNode(node);
197         this.children[this.children.length] = node;
198         return node;
199
200     },
201
202     /**
203      * Returns a node array of this node's siblings, null if none.
204      *
205      * @return Node[]
206      */
207     getSiblings: function() {
208         return this.parent.children;
209     },
210
211     /**
212      * Shows this node's children
213      */
214     showChildren: function() {
215         if (!this.tree.animateExpand(this.getChildrenEl())) {
216             if (this.hasChildren()) {
217                 this.getChildrenEl().style.display = "";
218             }
219         }
220     },
221
222     /**
223      * Hides this node's children
224      */
225     hideChildren: function() {
226         this.logger.debug("hiding " + this.index);
227
228         if (!this.tree.animateCollapse(this.getChildrenEl())) {
229             this.getChildrenEl().style.display = "none";
230         }
231     },
232
233     /**
234      * Returns the id for this node's container div
235      *
236      * @return {string} the element id
237      */
238     getElId: function() {
239         return "ygtv" + this.index;
240     },
241
242     /**
243      * Returns the id for this node's children div
244      *
245      * @return {string} the element id for this node's children div
246      */
247     getChildrenElId: function() {
248         return "ygtvc" + this.index;
249     },
250
251     /**
252      * Returns the id for this node's toggle element
253      *
254      * @return {string} the toggel element id
255      */
256     getToggleElId: function() {
257         return "ygtvt" + this.index;
258     },
259
260     /**
261      * Returns this node's container html element
262      *
263      * @return {HTMLElement} the container html element
264      */
265     getEl: function() {
266         return document.getElementById(this.getElId());
267     },
268
269     /**
270      * Returns the div that was generated for this node's children
271      *
272      * @return {HTMLElement} this node's children div
273      */
274     getChildrenEl: function() {
275         return document.getElementById(this.getChildrenElId());
276     },
277
278     /**
279      * Returns the element that is being used for this node's toggle.
280      *
281      * @return {HTMLElement} this node's toggel html element
282      */
283     getToggleEl: function() {
284         return document.getElementById(this.getToggleElId());
285     },
286
287     /**
288      * Generates the link that will invoke this node's toggle method
289      *
290      * @return {string} the javascript url for toggling this node
291      */
292     getToggleLink: function() {
293         return "YAHOO.widget.TreeView.getNode(\'" + this.tree.id + "\'," + 
294             this.index + ").toggle()";
295     },
296
297     /**
298      * Hides this nodes children (creating them if necessary), changes the
299      * toggle style.
300      */
301     collapse: function() {
302         // Only collapse if currently expanded
303         if (!this.expanded) { return; }
304
305         if (!this.getEl()) {
306             this.expanded = false;
307             return;
308         }
309
310         // hide the child div
311         this.hideChildren();
312         this.expanded = false;
313
314         if (this.hasIcon) {
315             this.getToggleEl().className = this.getStyle();
316         }
317
318         // fire the collapse event handler
319         this.tree.onCollapse(this);
320     },
321
322     /**
323      * Shows this nodes children (creating them if necessary), changes the
324      * toggle style, and collapses its siblings if multiExpand is not set.
325      */
326     expand: function() {
327         // Only expand if currently collapsed.
328         if (this.expanded) { return; }
329
330         if (!this.getEl()) {
331             this.expanded = true;
332             return;
333         }
334
335         if (! this.childrenRendered) {
336             this.getChildrenEl().innerHTML = this.renderChildren();
337         }
338
339         this.expanded = true;
340         if (this.hasIcon) {
341             this.getToggleEl().className = this.getStyle();
342         }
343
344         // We do an extra check for children here because the lazy
345         // load feature can expose nodes that have no children.
346
347         // if (!this.hasChildren()) {
348         if (this.isLoading) {
349             this.expanded = false;
350             return;
351         }
352
353         if (! this.multiExpand) {
354             var sibs = this.getSiblings();
355             for (var i=0; i<sibs.length; ++i) {
356                 if (sibs[i] != this && sibs[i].expanded) { 
357                     sibs[i].collapse(); 
358                 }
359             }
360         }
361
362         this.showChildren();
363
364         // fire the expand event handler
365         this.tree.onExpand(this);
366     },
367
368     /**
369      * Returns the css style name for the toggle
370      *
371      * @return {string} the css class for this node's toggle
372      */
373     getStyle: function() {
374         if (this.isLoading) {
375             this.logger.debug("returning the loading icon");
376             return "ygtvloading";
377         } else {
378             // location top or bottom, middle nodes also get the top style
379             var loc = (this.nextSibling) ? "t" : "l";
380
381             // type p=plus(expand), m=minus(collapase), n=none(no children)
382             var type = "n";
383             if (this.hasChildren(true) || this.isDynamic()) {
384                 type = (this.expanded) ? "m" : "p";
385             }
386
387             this.logger.debug("ygtv" + loc + type);
388             return "ygtv" + loc + type;
389         }
390     },
391
392     /**
393      * Returns the hover style for the icon
394      * @return {string} the css class hover state
395      */
396     getHoverStyle: function() { 
397         var s = this.getStyle();
398         if (this.hasChildren(true) && !this.isLoading) { 
399             s += "h"; 
400         }
401         return s;
402     },
403
404     /**
405      * Recursively expands all of this node's children.
406      */
407     expandAll: function() { 
408         for (var i=0;i<this.children.length;++i) {
409             var c = this.children[i];
410             if (c.isDynamic()) {
411                 alert("Not supported (lazy load + expand all)");
412                 break;
413             } else if (! c.multiExpand) {
414                 alert("Not supported (no multi-expand + expand all)");
415                 break;
416             } else {
417                 c.expand();
418                 c.expandAll();
419             }
420         }
421     },
422
423     /**
424      * Recursively collapses all of this node's children.
425      */
426     collapseAll: function() { 
427         for (var i=0;i<this.children.length;++i) {
428             this.children[i].collapse();
429             this.children[i].collapseAll();
430         }
431     },
432
433     /**
434      * Configures this node for dynamically obtaining the child data
435      * when the node is first expanded.
436      *
437      * @param fmDataLoader {function} the function that will be used to get the data.
438      */
439     setDynamicLoad: function(fnDataLoader) { 
440         this.dataLoader = fnDataLoader;
441         this._dynLoad = true;
442     },
443
444     /**
445      * Evaluates if this node is the root node of the tree
446      *
447      * @return {boolean} true if this is the root node
448      */
449     isRoot: function() { 
450         return (this == this.tree.root);
451     },
452
453     /**
454      * Evaluates if this node's children should be loaded dynamically.  Looks for
455      * the property both in this instance and the root node.  If the tree is
456      * defined to load all children dynamically, the data callback function is
457      * defined in the root node
458      *
459      * @return {boolean} true if this node's children are to be loaded dynamically
460      */
461     isDynamic: function() { 
462         var lazy = (!this.isRoot() && (this._dynLoad || this.tree.root._dynLoad));
463         // this.logger.debug("isDynamic: " + lazy);
464         return lazy;
465     },
466
467     /**
468      * Checks if this node has children.  If this node is lazy-loading and the
469      * children have not been rendered, we do not know whether or not there
470      * are actual children.  In most cases, we need to assume that there are
471      * children (for instance, the toggle needs to show the expandable 
472      * presentation state).  In other times we want to know if there are rendered
473      * children.  For the latter, "checkForLazyLoad" should be false.
474      *
475      * @param checkForLazyLoad {boolean} should we check for unloaded children?
476      * @return {boolean} true if this has children or if it might and we are
477      * checking for this condition.
478      */
479     hasChildren: function(checkForLazyLoad) { 
480         return ( this.children.length > 0 || 
481                 (checkForLazyLoad && this.isDynamic() && !this.childrenRendered) );
482     },
483
484     /**
485      * Expands if node is collapsed, collapses otherwise.
486      */
487     toggle: function() {
488         if (!this.tree.locked && ( this.hasChildren(true) || this.isDynamic()) ) {
489             if (this.expanded) { this.collapse(); } else { this.expand(); }
490         }
491     },
492
493     /**
494      * Returns the markup for this node and its children.
495      *
496      * @return {string} the markup for this node and its expanded children.
497      */
498     getHtml: function() {
499         var sb = [];
500         sb[sb.length] = '<div class="ygtvitem" id="' + this.getElId() + '">';
501         sb[sb.length] = this.getNodeHtml();
502         sb[sb.length] = this.getChildrenHtml();
503         sb[sb.length] = '</div>';
504         return sb.join("");
505     },
506
507     /**
508      * Called when first rendering the tree.  We always build the div that will
509      * contain this nodes children, but we don't render the children themselves
510      * unless this node is expanded.
511      *
512      * @return {string} the children container div html and any expanded children
513      * @private
514      */
515     getChildrenHtml: function() {
516         var sb = [];
517         sb[sb.length] = '<div class="ygtvchildren"';
518         sb[sb.length] = ' id="' + this.getChildrenElId() + '"';
519         if (!this.expanded) {
520             sb[sb.length] = ' style="display:none;"';
521         }
522         sb[sb.length] = '>';
523
524         // Don't render the actual child node HTML unless this node is expanded.
525         if (this.hasChildren(true) && this.expanded) {
526             sb[sb.length] = this.renderChildren();
527         }
528
529         sb[sb.length] = '</div>';
530
531         return sb.join("");
532     },
533
534     /**
535      * Generates the markup for the child nodes.  This is not done until the node
536      * is expanded.
537      *
538      * @return {string} the html for this node's children
539      * @private
540      */
541     renderChildren: function() {
542
543         this.logger.debug("rendering children for " + this.index);
544
545         var node = this;
546
547         if (this.isDynamic() && !this.childrenRendered) {
548             this.isLoading = true;
549             this.tree.locked = true;
550
551             if (this.dataLoader) {
552                 this.logger.debug("Using dynamic loader defined for this node");
553                 setTimeout( 
554                     function() {
555                         node.dataLoader(node, 
556                             function() { 
557                                 node.loadComplete(); 
558                             });
559                     }, 10);
560                 
561             } else if (this.tree.root.dataLoader) {
562                 this.logger.debug("Using the tree-level dynamic loader");
563
564                 setTimeout( 
565                     function() {
566                         node.tree.root.dataLoader(node, 
567                             function() { 
568                                 node.loadComplete(); 
569                             });
570                     }, 10);
571
572             } else {
573                 this.logger.debug("no loader found");
574                 return "Error: data loader not found or not specified.";
575             }
576
577             return "";
578
579         } else {
580             return this.completeRender();
581         }
582     },
583
584     /**
585      * Called when we know we have all the child data.
586      * @return {string} children html
587      */
588     completeRender: function() {
589         this.logger.debug("renderComplete: " + this.index);
590         var sb = [];
591
592         for (var i=0; i < this.children.length; ++i) {
593             sb[sb.length] = this.children[i].getHtml();
594         }
595         
596         this.childrenRendered = true;
597
598         return sb.join("");
599     },
600
601     /**
602      * Load complete is the callback function we pass to the data provider
603      * in dynamic load situations.
604      */
605     loadComplete: function() {
606         this.logger.debug("loadComplete: " + this.index);
607         this.getChildrenEl().innerHTML = this.completeRender();
608         this.isLoading = false;
609         this.expand();
610         this.tree.locked = false;
611     },
612
613     /**
614      * Returns this node's ancestor at the specified depth.
615      *
616      * @param {int} depth the depth of the ancestor.
617      * @return {Node} the ancestor
618      */
619     getAncestor: function(depth) {
620         if (depth >= this.depth || depth < 0)  {
621             this.logger.debug("illegal getAncestor depth: " + depth);
622             return null;
623         }
624
625         var p = this.parent;
626         
627         while (p.depth > depth) {
628             p = p.parent;
629         }
630
631         return p;
632     },
633
634     /**
635      * Returns the css class for the spacer at the specified depth for
636      * this node.  If this node's ancestor at the specified depth
637      * has a next sibling the presentation is different than if it
638      * does not have a next sibling
639      *
640      * @param {int} depth the depth of the ancestor.
641      * @return {string} the css class for the spacer
642      */
643     getDepthStyle: function(depth) {
644         return (this.getAncestor(depth).nextSibling) ? 
645             "ygtvdepthcell" : "ygtvblankdepthcell";
646     },
647
648     /**
649      * Get the markup for the node.  This is designed to be overrided so that we can
650      * support different types of nodes.
651      *
652      * @return {string} The HTML that will render this node.
653      */
654     getNodeHtml: function() { 
655         return ""; 
656     }
657
658 };
659