1 /* See license.txt for terms of usage */
  2 
  3 FBL.ns(function() { with (FBL) {
  4 
  5 /**
  6  * View interface used to populate an InsideOutBox object.
  7  *
  8  * All views must implement this interface (directly or via duck typing).
  9  */
 10 top.InsideOutBoxView = {
 11     /**
 12      * Retrieves the parent object for a given child object.
 13      */
 14     getParentObject: function(child) {},
 15 
 16     /**
 17      * Retrieves a given child node.
 18      *
 19      * If both index and previousSibling are passed, the implementation
 20      * may assume that previousSibling will be the return for getChildObject
 21      * with index-1.
 22      */
 23     getChildObject: function(parent, index, previousSibling) {},
 24 
 25     /**
 26      * Renders the HTML representation of the object. Should return an HTML
 27      * object which will be displayed to the user.
 28      */
 29     createObjectBox: function(object, isRoot) {}
 30 };
 31 
 32 /**
 33  * Creates a tree based on objects provided by a separate "view" object.
 34  *
 35  * Construction uses an "inside-out" algorithm, meaning that the view's job is first
 36  * to tell us the ancestry of each object, and secondarily its descendants.
 37  */
 38 top.InsideOutBox = function(view, box)
 39 {
 40     this.view = view;
 41     this.box = box;
 42 
 43     this.rootObject = null;
 44 
 45     this.rootObjectBox = null;
 46     this.selectedObjectBox = null;
 47     this.highlightedObjectBox = null;
 48 
 49     this.onMouseDown = bind(this.onMouseDown, this);
 50     box.addEventListener("mousedown", this.onMouseDown, false);
 51 };
 52 
 53 InsideOutBox.prototype =
 54 {
 55     destroy: function()
 56     {
 57         this.box.removeEventListener("mousedown", this.onMouseDown, false);
 58     },
 59 
 60     highlight: function(object)
 61     {
 62         var objectBox = this.createObjectBox(object);
 63         this.highlightObjectBox(objectBox);
 64         return objectBox;
 65     },
 66 
 67     openObject: function(object)
 68     {
 69         var firstChild = this.view.getChildObject(object, 0);
 70         if (firstChild)
 71             object = firstChild;
 72 
 73         var objectBox = this.createObjectBox(object);
 74         this.openObjectBox(objectBox);
 75         return objectBox;
 76     },
 77 
 78     openToObject: function(object)
 79     {
 80         var objectBox = this.createObjectBox(object);
 81         this.openObjectBox(objectBox);
 82         return objectBox;
 83     },
 84 
 85     select: function(object, makeBoxVisible, forceOpen, noScrollIntoView)
 86     {
 87         if (FBTrace.DBG_HTML)
 88             FBTrace.sysout("insideOutBox.select object:", object);
 89         var objectBox = this.createObjectBox(object);
 90         this.selectObjectBox(objectBox, forceOpen);
 91         if (makeBoxVisible)
 92         {
 93             this.openObjectBox(objectBox);
 94             if (!noScrollIntoView)
 95                 scrollIntoCenterView(objectBox);
 96         }
 97         return objectBox;
 98     },
 99 
100     expandObject: function(object)
101     {
102         var objectBox = this.createObjectBox(object);
103         if (objectBox)
104             this.expandObjectBox(objectBox);
105     },
106 
107     contractObject: function(object)
108     {
109         var objectBox = this.createObjectBox(object);
110         if (objectBox)
111             this.contractObjectBox(objectBox);
112     },
113 
114     highlightObjectBox: function(objectBox)
115     {
116         if (this.highlightedObjectBox)
117         {
118             removeClass(this.highlightedObjectBox, "highlighted");
119 
120             var highlightedBox = this.getParentObjectBox(this.highlightedObjectBox);
121             for (; highlightedBox; highlightedBox = this.getParentObjectBox(highlightedBox))
122                 removeClass(highlightedBox, "highlightOpen");
123         }
124 
125         this.highlightedObjectBox = objectBox;
126 
127         if (objectBox)
128         {
129             setClass(objectBox, "highlighted");
130 
131             var highlightedBox = this.getParentObjectBox(objectBox);
132             for (; highlightedBox; highlightedBox = this.getParentObjectBox(highlightedBox))
133                 setClass(highlightedBox, "highlightOpen");
134 
135            scrollIntoCenterView(objectBox);
136         }
137     },
138 
139     selectObjectBox: function(objectBox, forceOpen)
140     {
141         var isSelected = this.selectedObjectBox && objectBox == this.selectedObjectBox;
142         if (!isSelected)
143         {
144             removeClass(this.selectedObjectBox, "selected");
145             dispatch([Firebug.A11yModel], 'onObjectBoxUnselected', [this.selectedObjectBox]);
146             this.selectedObjectBox = objectBox;
147 
148             if (objectBox)
149             {
150                 setClass(objectBox, "selected");
151 
152                 // Force it open the first time it is selected
153                 if (forceOpen)
154                     this.toggleObjectBox(objectBox, true);
155             }
156         }
157         dispatch([Firebug.A11yModel], 'onObjectBoxSelected', [objectBox]);
158     },
159 
160     openObjectBox: function(objectBox)
161     {
162         if (objectBox)
163         {
164             // Set all of the node's ancestors to be permanently open
165             var parentBox = this.getParentObjectBox(objectBox);
166             var labelBox;
167             for (; parentBox; parentBox = this.getParentObjectBox(parentBox))
168             {
169                 setClass(parentBox, "open");
170                 labelBox = parentBox.getElementsByClassName('nodeLabelBox').item(0);
171                 if (labelBox)
172                     labelBox.setAttribute('aria-expanded', 'true')
173             }
174         }
175     },
176 
177     expandObjectBox: function(objectBox)
178     {
179         var nodeChildBox = this.getChildObjectBox(objectBox);
180         if (!nodeChildBox)
181             return;
182 
183         if (!objectBox.populated)
184         {
185             var firstChild = this.view.getChildObject(objectBox.repObject, 0);
186             this.populateChildBox(firstChild, nodeChildBox);
187         }
188         var labelBox = objectBox.getElementsByClassName('nodeLabelBox').item(0);
189         if (labelBox)
190             labelBox.setAttribute('aria-expanded', 'true');
191         setClass(objectBox, "open");
192     },
193 
194     contractObjectBox: function(objectBox)
195     {
196         removeClass(objectBox, "open");
197         var nodeLabel = objectBox.getElementsByClassName("nodeLabel").item(0);
198         var labelBox = nodeLabel.getElementsByClassName('nodeLabelBox').item(0);
199         if (labelBox)
200             labelBox.setAttribute('aria-expanded', 'false');
201     },
202 
203     toggleObjectBox: function(objectBox, forceOpen)
204     {
205         var isOpen = hasClass(objectBox, "open");
206         var nodeLabel = objectBox.getElementsByClassName("nodeLabel").item(0);
207         var labelBox = nodeLabel.getElementsByClassName('nodeLabelBox').item(0);
208         if (labelBox)
209             labelBox.setAttribute('aria-expanded', isOpen);
210         if (!forceOpen && isOpen)
211             this.contractObjectBox(objectBox);
212 
213         else if (!isOpen)
214             this.expandObjectBox(objectBox);
215     },
216 
217     getNextObjectBox: function(objectBox)
218     {
219         return findNext(objectBox, isVisibleTarget, false, this.box);
220     },
221 
222     getPreviousObjectBox: function(objectBox)
223     {
224         return findPrevious(objectBox, isVisibleTarget, true, this.box);
225     },
226 
227     /**
228      * Creates all of the boxes for an object, its ancestors, and siblings.
229      */
230     createObjectBox: function(object)
231     {
232         if (!object)
233             return null;
234 
235         this.rootObject = this.getRootNode(object);
236 
237         // Get or create all of the boxes for the target and its ancestors
238         var objectBox = this.createObjectBoxes(object, this.rootObject);
239 
240         if (FBTrace.DBG_HTML)
241             FBTrace.sysout("\n----\ninsideOutBox.createObjectBox for object="+formatNode(object)+" got objectBox="+formatNode(objectBox), objectBox);
242         if (!objectBox)
243             return null;
244         else if (object == this.rootObject)
245             return objectBox;
246         else
247             return this.populateChildBox(object, objectBox.parentNode);
248     },
249 
250     /**
251      * Creates all of the boxes for an object, its ancestors, and siblings up to a root.
252      */
253     createObjectBoxes: function(object, rootObject)
254     {
255         if (FBTrace.DBG_HTML)
256             FBTrace.sysout("\n----\ninsideOutBox.createObjectBoxes("+formatNode(object)+", "+formatNode(rootObject)+")\n");
257         if (!object)
258             return null;
259 
260         if (object == rootObject)
261         {
262             if (!this.rootObjectBox || this.rootObjectBox.repObject != rootObject)
263             {
264                 if (this.rootObjectBox)
265                 {
266                     try {
267                         this.box.removeChild(this.rootObjectBox);
268                     } catch (exc) {
269                         if (FBTrace.DBG_HTML)
270                             FBTrace.sysout(" this.box.removeChild(this.rootObjectBox) FAILS "+this.box+" must not contain "+this.rootObjectBox+"\n");
271                     }
272                 }
273 
274                 this.highlightedObjectBox = null;
275                 this.selectedObjectBox = null;
276                 this.rootObjectBox = this.view.createObjectBox(object, true);
277                 this.box.appendChild(this.rootObjectBox);
278             }
279             if (FBTrace.DBG_HTML)
280             {
281                 FBTrace.sysout("insideOutBox.createObjectBoxes("+formatNode(object)+","+formatNode(rootObject)+") rootObjectBox: "
282                                             +this.rootObjectBox, object);
283             }
284             return this.rootObjectBox;
285         }
286         else
287         {
288             var parentNode = this.view.getParentObject(object);
289 
290             if (FBTrace.DBG_HTML)
291             {
292                 FBTrace.sysout("insideOutBox.createObjectBoxes getObjectPath(object) ", getObjectPath(object, this.view))
293                 FBTrace.sysout("insideOutBox.createObjectBoxes view.getParentObject("+formatNode(object)+")=parentNode: "+formatNode(parentNode), parentNode);
294             }
295 
296             var parentObjectBox = this.createObjectBoxes(parentNode, rootObject);
297             if (FBTrace.DBG_HTML)
298                 FBTrace.sysout("insideOutBox.createObjectBoxes createObjectBoxes("+formatNode(parentNode)+","+formatNode(rootObject)+"):parentObjectBox: "+formatNode(parentObjectBox), parentObjectBox);
299             if (!parentObjectBox)
300                 return null;
301 
302             var parentChildBox = this.getChildObjectBox(parentObjectBox);
303             if (FBTrace.DBG_HTML)
304                 FBTrace.sysout("insideOutBox.createObjectBoxes getChildObjectBox("+formatNode(parentObjectBox)+")= parentChildBox: "+formatNode(parentChildBox)+"\n");
305             if (!parentChildBox)
306                 return null;
307 
308             var childObjectBox = this.findChildObjectBox(parentChildBox, object);
309             if (FBTrace.DBG_HTML)
310                 FBTrace.sysout("insideOutBox.createObjectBoxes findChildObjectBox("+formatNode(parentChildBox)+","+formatNode(object)+"): childObjectBox: "+formatNode(childObjectBox), childObjectBox);
311             return childObjectBox
312                 ? childObjectBox
313                 : this.populateChildBox(object, parentChildBox);
314         }
315     },
316 
317     findObjectBox: function(object)
318     {
319         if (!object)
320             return null;
321 
322         if (object == this.rootObject)
323             return this.rootObjectBox;
324         else
325         {
326             var parentNode = this.view.getParentObject(object);
327             var parentObjectBox = this.findObjectBox(parentNode);
328             if (!parentObjectBox)
329                 return null;
330 
331             var parentChildBox = this.getChildObjectBox(parentObjectBox);
332             if (!parentChildBox)
333                 return null;
334 
335             return this.findChildObjectBox(parentChildBox, object);
336         }
337     },
338 
339     appendChildBox: function(parentNodeBox, repObject)
340     {
341         var childBox = this.getChildObjectBox(parentNodeBox);
342         var objectBox = this.findChildObjectBox(childBox, repObject);
343         if (objectBox)
344             return objectBox;
345 
346         objectBox = this.view.createObjectBox(repObject);
347         if (objectBox)
348         {
349             var childBox = this.getChildObjectBox(parentNodeBox);
350             childBox.appendChild(objectBox);
351         }
352         return objectBox;
353     },
354 
355     insertChildBoxBefore: function(parentNodeBox, repObject, nextSibling)
356     {
357         var childBox = this.getChildObjectBox(parentNodeBox);
358         var objectBox = this.findChildObjectBox(childBox, repObject);
359         if (objectBox)
360             return objectBox;
361 
362         objectBox = this.view.createObjectBox(repObject);
363         if (objectBox)
364         {
365             var siblingBox = this.findChildObjectBox(childBox, nextSibling);
366             childBox.insertBefore(objectBox, siblingBox);
367         }
368         return objectBox;
369     },
370 
371     removeChildBox: function(parentNodeBox, repObject)
372     {
373         var childBox = this.getChildObjectBox(parentNodeBox);
374         var objectBox = this.findChildObjectBox(childBox, repObject);
375         if (objectBox)
376             childBox.removeChild(objectBox);
377     },
378 
379     populateChildBox: function(repObject, nodeChildBox)  // We want all children of the parent of repObject.
380     {
381         if (!repObject)
382             return null;
383 
384         var parentObjectBox = getAncestorByClass(nodeChildBox, "nodeBox");
385         if (FBTrace.DBG_HTML)
386             FBTrace.sysout("+++insideOutBox.populateChildBox("+(repObject.localName?repObject.localName:repObject)+") parentObjectBox.populated "+parentObjectBox.populated+"\n");
387         if (parentObjectBox.populated)
388             return this.findChildObjectBox(nodeChildBox, repObject);
389 
390         var lastSiblingBox = this.getChildObjectBox(nodeChildBox);
391         var siblingBox = nodeChildBox.firstChild;
392         var targetBox = null;
393 
394         var view = this.view;
395 
396         var targetSibling = null;
397         var parentNode = view.getParentObject(repObject);
398         for (var i = 0; 1; ++i)
399         {
400             targetSibling = view.getChildObject(parentNode, i, targetSibling);
401             if (!targetSibling)
402                 break;
403 
404             // Check if we need to start appending, or continue to insert before
405             if (lastSiblingBox && lastSiblingBox.repObject == targetSibling)
406                 lastSiblingBox = null;
407 
408             if (!siblingBox || siblingBox.repObject != targetSibling)
409             {
410                 var newBox = view.createObjectBox(targetSibling);
411                 if (newBox)
412                 {
413                     if (lastSiblingBox)
414                         nodeChildBox.insertBefore(newBox, lastSiblingBox);
415                     else
416                         nodeChildBox.appendChild(newBox);
417                 }
418 
419                 siblingBox = newBox;
420             }
421 
422             if (targetSibling == repObject)
423                 targetBox = siblingBox;
424 
425             if (siblingBox && siblingBox.repObject == targetSibling)
426                 siblingBox = siblingBox.nextSibling;
427         }
428 
429         if (targetBox)
430             parentObjectBox.populated = true;
431         if (FBTrace.DBG_HTML)
432             FBTrace.sysout("---insideOutBox.populateChildBox("+(repObject.localName?repObject.localName:repObject)+") targetBox "+targetBox+"\n");
433 
434         return targetBox;
435     },
436 
437     getParentObjectBox: function(objectBox)
438     {
439         var parent = objectBox.parentNode ? objectBox.parentNode.parentNode : null;
440         return parent && parent.repObject ? parent : null;
441     },
442 
443     getChildObjectBox: function(objectBox)
444     {
445         return objectBox.getElementsByClassName("nodeChildBox").item(0);
446     },
447 
448     findChildObjectBox: function(parentNodeBox, repObject)
449     {
450         for (var childBox = parentNodeBox.firstChild; childBox; childBox = childBox.nextSibling)
451         {
452             if (FBTrace.DBG_HTML)
453                 FBTrace.sysout(
454                     "insideOutBox.findChildObjectBox "
455                     +(childBox.repObject == repObject?"match ":"no match ")
456                     +" childBox.repObject: " + (childBox.repObject && (childBox.repObject.localName || childBox.repObject))
457                     +" repObject: " +(repObject && (repObject.localName || repObject))+"\n", childBox);
458             if (childBox.repObject == repObject)
459                 return childBox;
460         }
461     },
462 
463     /**
464      * Determines if the given node is an ancestor of the current root.
465      */
466     isInExistingRoot: function(node)
467     {
468         if (FBTrace.DBG_HTML)
469           FBTrace.sysout("insideOutBox.isInExistingRoot for ", node);
470         var parentNode = node;
471         while (parentNode && parentNode != this.rootObject)
472         {
473             if (FBTrace.DBG_HTML)
474                 FBTrace.sysout(parentNode.localName+" < ", parentNode);
475             var parentNode = this.view.getParentObject(parentNode);
476             if (FBTrace.DBG_HTML)
477                 FBTrace.sysout((parentNode?" (parent="+parentNode.localName+")":" (null parentNode)"+"\n"), parentNode);
478         }
479         return parentNode == this.rootObject;
480     },
481 
482     getRootNode: function(node)
483     {
484         if (FBTrace.DBG_HTML)
485             FBTrace.sysout("insideOutBox.getRootNode for ", node);
486         while (1)
487         {
488             if (FBTrace.DBG_HTML)
489                 FBTrace.sysout(node.localName+" < ", node);
490             var parentNode = this.view.getParentObject(node);
491             if (FBTrace.DBG_HTML)
492                 FBTrace.sysout((parentNode?" (parent="+parentNode.localName+")":" (null parentNode)"+"\n"), parentNode);
493 
494             if (!parentNode)
495                 return node;
496             else
497                 node = parentNode;
498         }
499         return null;
500     },
501 
502     // ********************************************************************************************
503 
504     onMouseDown: function(event)
505     {
506         var hitTwisty = false;
507         for (var child = event.target; child; child = child.parentNode)
508         {
509             if (hasClass(child, "twisty"))
510                 hitTwisty = true;
511             else if (child.repObject)
512             {
513                 if (hitTwisty)
514                     this.toggleObjectBox(child);
515                 break;
516             }
517         }
518     }
519 };
520 
521 // ************************************************************************************************
522 // Local Helpers
523 
524 function isVisibleTarget(node)
525 {
526     if (node.repObject && node.repObject.nodeType == Node.ELEMENT_NODE)
527     {
528         for (var parent = node.parentNode; parent; parent = parent.parentNode)
529         {
530             if (hasClass(parent, "nodeChildBox")
531                 && !hasClass(parent.parentNode, "open")
532                 && !hasClass(parent.parentNode, "highlightOpen"))
533                 return false;
534         }
535         return true;
536     }
537 }
538 
539 function formatNode(object)
540 {
541     if (object)
542         return (object.localName ? object.localName : object);
543     else
544         return "(null object)";
545 }
546 
547 function getObjectPath(element, aView)
548 {
549     var path = [];
550     for (; element; element = aView.getParentObject(element))
551         path.push(element);
552 
553     return path;
554 }
555 
556 }});
557