1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/events",
  7     "firebug/lib/css",
  8     "firebug/lib/dom",
  9     "firebug/lib/xml",
 10 ],
 11 function(Obj, Firebug, Events, Css, Dom, Xml) {
 12 
 13 // ************************************************************************************************
 14 
 15 /**
 16  * View interface used to populate an InsideOutBox object.
 17  *
 18  * All views must implement this interface (directly or via duck typing).
 19  */
 20 var InsideOutBoxView = {
 21     /**
 22      * Retrieves the parent object for a given child object.
 23      */
 24     getParentObject: function(child) {},
 25 
 26     /**
 27      * Retrieves a given child node.
 28      *
 29      * If both index and previousSibling are passed, the implementation
 30      * may assume that previousSibling will be the return for getChildObject
 31      * with index-1.
 32      */
 33     getChildObject: function(parent, index, previousSibling) {},
 34 
 35     /**
 36      * Renders the HTML representation of the object. Should return an HTML
 37      * object which will be displayed to the user.
 38      */
 39     createObjectBox: function(object, isRoot) {}
 40 };
 41 
 42 /**
 43  * Creates a tree based on objects provided by a separate "view" object.
 44  *
 45  * Construction uses an "inside-out" algorithm, meaning that the view's job is first
 46  * to tell us the ancestry of each object, and secondarily its descendants.
 47  */
 48 Firebug.InsideOutBox = function(view, box)
 49 {
 50     this.view = view;
 51     this.box = box;
 52 
 53     this.rootObject = null;
 54 
 55     this.rootObjectBox = null;
 56     this.selectedObjectBox = null;
 57     this.highlightedObjectBox = null;
 58 
 59     this.onMouseDown = Obj.bind(this.onMouseDown, this);
 60     Events.addEventListener(this.box, "mousedown", this.onMouseDown, false);
 61 };
 62 
 63 Firebug.InsideOutBox.prototype =
 64 {
 65     destroy: function()
 66     {
 67         Events.removeEventListener(this.box, "mousedown", this.onMouseDown, false);
 68     },
 69 
 70     highlight: function(object)
 71     {
 72         var objectBox = this.createObjectBox(object);
 73         this.highlightObjectBox(objectBox);
 74         return objectBox;
 75     },
 76 
 77     openObject: function(object)
 78     {
 79         var firstChild = this.view.getChildObject(object, 0);
 80         if (firstChild)
 81             object = firstChild;
 82 
 83         var objectBox = this.createObjectBox(object);
 84         this.openObjectBox(objectBox);
 85         return objectBox;
 86     },
 87 
 88     openToObject: function(object)
 89     {
 90         var objectBox = this.createObjectBox(object);
 91         this.openObjectBox(objectBox);
 92         return objectBox;
 93     },
 94 
 95     select: function(object, makeBoxVisible, forceOpen, noScrollIntoView)
 96     {
 97         if (FBTrace.DBG_HTML)
 98             FBTrace.sysout("insideOutBox.select object:"+object, object);
 99 
100         var objectBox = this.createObjectBox(object);
101         this.selectObjectBox(objectBox, forceOpen);
102 
103         if (makeBoxVisible)
104         {
105             this.openObjectBox(objectBox);
106             if (!noScrollIntoView)
107                 Dom.scrollIntoCenterView(objectBox);
108         }
109 
110         return objectBox;
111     },
112 
113     toggleObject: function(object, all, exceptions)
114     {
115         var objectBox = this.createObjectBox(object);
116         if (!objectBox)
117             return;
118 
119         if (Css.hasClass(objectBox, "open"))
120             this.contractObjectBox(objectBox, all);
121         else
122             this.expandObjectBox(objectBox, all, exceptions);
123     },
124 
125     expandObject: function(object, expandAll)
126     {
127         var objectBox = this.createObjectBox(object);
128         if (objectBox)
129             this.expandObjectBox(objectBox, expandAll);
130     },
131 
132     contractObject: function(object, contractAll)
133     {
134         var objectBox = this.createObjectBox(object);
135         if (objectBox)
136             this.contractObjectBox(objectBox, contractAll);
137     },
138 
139     highlightObjectBox: function(objectBox)
140     {
141         if (this.highlightedObjectBox)
142         {
143             Css.removeClass(this.highlightedObjectBox, "highlighted");
144 
145             var highlightedBox = this.getParentObjectBox(this.highlightedObjectBox);
146             for (; highlightedBox; highlightedBox = this.getParentObjectBox(highlightedBox))
147                 Css.removeClass(highlightedBox, "highlightOpen");
148         }
149 
150         this.highlightedObjectBox = objectBox;
151 
152         if (objectBox)
153         {
154             Css.setClass(objectBox, "highlighted");
155 
156             var highlightedBox = this.getParentObjectBox(objectBox);
157             for (; highlightedBox; highlightedBox = this.getParentObjectBox(highlightedBox))
158                 Css.setClass(highlightedBox, "highlightOpen");
159 
160            Dom.scrollIntoCenterView(objectBox);
161         }
162     },
163 
164     selectObjectBox: function(objectBox, forceOpen)
165     {
166         var panel = Firebug.getElementPanel(objectBox);
167 
168         if (!panel)
169         {
170             if (FBTrace.DBG_ERRORS && FBTrace.DBG_HTML)
171                 FBTrace.sysout("selectObjectBox no panel for " + objectBox, objectBox);
172             return;
173         }
174 
175         var isSelected = this.selectedObjectBox && objectBox == this.selectedObjectBox;
176         if (!isSelected)
177         {
178             Css.removeClass(this.selectedObjectBox, "selected");
179             Events.dispatch(panel.fbListeners, "onObjectBoxUnselected", [this.selectedObjectBox]);
180             this.selectedObjectBox = objectBox;
181 
182             if (objectBox)
183             {
184                 Css.setClass(objectBox, "selected");
185 
186                 // Force it open the first time it is selected
187                 if (forceOpen)
188                     this.toggleObjectBox(objectBox, true);
189             }
190         }
191         Events.dispatch(panel.fbListeners, "onObjectBoxSelected", [objectBox]);
192     },
193 
194     openObjectBox: function(objectBox)
195     {
196         if (objectBox)
197         {
198             // Set all of the node's ancestors to be permanently open
199             var parentBox = this.getParentObjectBox(objectBox);
200             var labelBox;
201             for (; parentBox; parentBox = this.getParentObjectBox(parentBox))
202             {
203                 Css.setClass(parentBox, "open");
204                 labelBox = parentBox.getElementsByClassName("nodeLabelBox").item(0);
205                 if (labelBox)
206                     labelBox.setAttribute("aria-expanded", "true")
207             }
208         }
209     },
210 
211     expandObjectBox: function(objectBox, expandAll, exceptions)
212     {
213         var nodeChildBox = this.getChildObjectBox(objectBox);
214         if (!nodeChildBox)
215             return;
216 
217         if (!objectBox.populated)
218         {
219             var firstChild = this.view.getChildObject(objectBox.repObject, 0);
220             this.populateChildBox(firstChild, nodeChildBox);
221         }
222 
223         var labelBox = objectBox.getElementsByClassName("nodeLabelBox").item(0);
224         if (labelBox)
225             labelBox.setAttribute("aria-expanded", "true");
226         Css.setClass(objectBox, "open");
227 
228         // Recursively expand all child boxes
229         if (expandAll)
230         {
231             for (var child = nodeChildBox.firstChild; child; child = child.nextSibling)
232             {
233                 if (exceptions && child.repObject)
234                 {
235                     var shouldBeExpanded = true;
236                     var localName = child.repObject.localName;
237                     localName = localName ? localName.toLowerCase() : "";
238 
239                     for (var i=0; i<exceptions.length; i++)
240                     {
241                         if (localName == exceptions[i] &&
242                             (Xml.isElementHTML(child.repObject) || Xml.isElementXHTML(child.repObject)))
243                         {
244                             shouldBeExpanded = false;
245                             break;
246                         }
247                     }
248                     if (!shouldBeExpanded)
249                         continue;
250                 }
251 
252                 if (Css.hasClass(child, "containerNodeBox"))
253                     this.expandObjectBox(child, expandAll, exceptions);
254             }
255         }
256     },
257 
258     contractObjectBox: function(objectBox, contractAll)
259     {
260         Css.removeClass(objectBox, "open");
261 
262         var nodeLabel = objectBox.getElementsByClassName("nodeLabel").item(0);
263         var labelBox = nodeLabel.getElementsByClassName('nodeLabelBox').item(0);
264         if (labelBox)
265             labelBox.setAttribute("aria-expanded", "false");
266 
267         if (contractAll)
268         {
269             // Recursively contract all child boxes
270             var nodeChildBox = this.getChildObjectBox(objectBox);
271             if (!nodeChildBox)
272                 return;
273 
274             for (var child = nodeChildBox.firstChild; child; child = child.nextSibling)
275             {
276                 if (Css.hasClass(child, "containerNodeBox") && Css.hasClass(child, "open"))
277                     this.contractObjectBox(child, contractAll);
278             }
279         }
280     },
281 
282     toggleObjectBox: function(objectBox, forceOpen)
283     {
284         var isOpen = Css.hasClass(objectBox, "open");
285         var nodeLabel = objectBox.getElementsByClassName("nodeLabel").item(0);
286         var labelBox = nodeLabel.getElementsByClassName('nodeLabelBox').item(0);
287         if (labelBox)
288             labelBox.setAttribute("aria-expanded", isOpen);
289 
290         if (!forceOpen && isOpen)
291             this.contractObjectBox(objectBox);
292         else if (!isOpen)
293             this.expandObjectBox(objectBox);
294     },
295 
296     getNextObjectBox: function(objectBox)
297     {
298         return Dom.findNext(objectBox, isVisibleTarget, false, this.box);
299     },
300 
301     getPreviousObjectBox: function(objectBox)
302     {
303         return Dom.findPrevious(objectBox, isVisibleTarget, true, this.box);
304     },
305 
306     getNextSiblingObjectBox: function(objectBox)
307     {
308         if (!objectBox)
309             return null;
310         return Dom.findNext(objectBox, isVisibleTarget, true, objectBox.parentNode);
311     },
312 
313     /**
314      * Creates all of the boxes for an object, its ancestors, and siblings.
315      */
316     createObjectBox: function(object)
317     {
318         if (!object)
319             return null;
320 
321         this.rootObject = this.getRootNode(object);
322 
323         // Get or create all of the boxes for the target and its ancestors
324         var objectBox = this.createObjectBoxes(object, this.rootObject);
325 
326         if (FBTrace.DBG_HTML)
327             FBTrace.sysout("----insideOutBox.createObjectBox: createObjectBoxes(object="+
328                 formatNode(object)+", rootObject="+formatNode(this.rootObject)+") ="+
329                 formatNode(objectBox), objectBox);
330 
331         if (!objectBox)  // we found an object outside of the navigatible tree
332             return;
333         else if (object == this.rootObject)
334             return objectBox;
335         else
336             return this.populateChildBox(object, objectBox.parentNode);
337     },
338 
339     /**
340      * Creates all of the boxes for an object, its ancestors, and siblings up to a root.
341      */
342     createObjectBoxes: function(object, rootObject)
343     {
344         if (!object)
345             return null;
346 
347         if (object == rootObject)
348         {
349             if (!this.rootObjectBox || this.rootObjectBox.repObject != rootObject)
350             {
351                 if (this.rootObjectBox)
352                 {
353                     try
354                     {
355                         this.box.removeChild(this.rootObjectBox);
356                     }
357                     catch (exc)
358                     {
359                         if (FBTrace.DBG_HTML)
360                             FBTrace.sysout(" this.box.removeChild(this.rootObjectBox) FAILS "+
361                                 this.box+" must not contain "+this.rootObjectBox);
362                     }
363                 }
364 
365                 this.highlightedObjectBox = null;
366                 this.selectedObjectBox = null;
367                 this.rootObjectBox = this.view.createObjectBox(object, true);
368                 this.box.appendChild(this.rootObjectBox);
369             }
370 
371             if (FBTrace.DBG_HTML)
372             {
373                 FBTrace.sysout("insideOutBox.createObjectBoxes("+formatNode(object)+","+
374                     formatNode(rootObject)+") rootObjectBox: "+this.rootObjectBox, object);
375             }
376 
377             if ((FBTrace.DBG_HTML || FBTrace.DBG_ERRORS) && !this.rootObjectBox.parentNode)
378                 FBTrace.sysout("insideOutBox.createObjectBoxes; ERROR - null parent node. "+
379                     "object: " + formatNode(object)+", rootObjectBox: "+
380                         formatObjectBox(this.rootObjectBox), object);
381 
382             return this.rootObjectBox;
383         }
384         else
385         {
386             var parentNode = this.view.getParentObject(object);
387 
388             if (FBTrace.DBG_HTML)
389                 FBTrace.sysout("insideOutBox.createObjectBoxes createObjectBoxes recursing " +
390                     "with parentNode "+formatNode(parentNode)+" from object "+formatNode(object));
391 
392             // recurse towards parent, eventually returning rootObjectBox
393             var parentObjectBox = this.createObjectBoxes(parentNode, rootObject);
394 
395             if (FBTrace.DBG_HTML)
396                 FBTrace.sysout("insideOutBox.createObjectBoxes createObjectBoxes("+
397                     formatNode(parentNode)+","+formatNode(rootObject)+"):parentObjectBox: "+
398                         formatObjectBox(parentObjectBox), parentObjectBox);
399 
400             if (!parentObjectBox)
401                 return null;
402 
403             // Returns an inner box (nodeChildBox) that contains list of child boxes (nodeBox).
404             var childrenBox = this.getChildObjectBox(parentObjectBox);
405 
406             if (FBTrace.DBG_HTML)
407                 FBTrace.sysout("insideOutBox.createObjectBoxes getChildObjectBox("+
408                     formatObjectBox(parentObjectBox)+")= childrenBox: "+formatObjectBox(childrenBox));
409 
410             if (!childrenBox)
411             {
412                 if (FBTrace.DBG_ERRORS)
413                     FBTrace.sysout("insideOutBox.createObjectBoxes FAILS for "+formatNode(object)+
414                         " getChildObjectBox("+formatObjectBox(parentObjectBox)+")= childrenBox: "+
415                         formatObjectBox(childrenBox));
416 
417                 // This is where we could try to create a box for objects we cannot get to by
418                 // navigation via walker or DOM nodes (native anonymous)
419                 return null;
420             }
421 
422             var childObjectBox = this.findChildObjectBox(childrenBox, object);
423 
424             if (FBTrace.DBG_HTML)
425                 FBTrace.sysout("insideOutBox.createObjectBoxes findChildObjectBox("+
426                     formatNode(childrenBox)+","+formatNode(object)+"): childObjectBox: "+
427                         formatObjectBox(childObjectBox), childObjectBox);
428 
429             return childObjectBox ? childObjectBox : this.populateChildBox(object, childrenBox);
430         }
431     },
432 
433     findObjectBox: function(object)
434     {
435         if (!object)
436             return null;
437 
438         if (object == this.rootObject)
439             return this.rootObjectBox;
440         else
441         {
442             var parentNode = this.view.getParentObject(object);
443             var parentObjectBox = this.findObjectBox(parentNode);
444             if (!parentObjectBox)
445                 return null;
446 
447             var childrenBox = this.getChildObjectBox(parentObjectBox);
448             if (!childrenBox)
449                 return null;
450 
451             return this.findChildObjectBox(childrenBox, object);
452         }
453     },
454 
455     appendChildBox: function(parentNodeBox, repObject)
456     {
457         var childBox = this.getChildObjectBox(parentNodeBox);
458         var objectBox = this.findChildObjectBox(childBox, repObject);
459         if (objectBox)
460             return objectBox;
461 
462         objectBox = this.view.createObjectBox(repObject);
463         if (objectBox)
464         {
465             var childBox = this.getChildObjectBox(parentNodeBox);
466             childBox.appendChild(objectBox);
467         }
468         return objectBox;
469     },
470 
471     insertChildBoxBefore: function(parentNodeBox, repObject, nextSibling)
472     {
473         var childBox = this.getChildObjectBox(parentNodeBox);
474         var objectBox = this.findChildObjectBox(childBox, repObject);
475         if (objectBox)
476             return objectBox;
477 
478         objectBox = this.view.createObjectBox(repObject);
479         if (objectBox)
480         {
481             var siblingBox = this.findChildObjectBox(childBox, nextSibling);
482             childBox.insertBefore(objectBox, siblingBox);
483         }
484         return objectBox;
485     },
486 
487     removeChildBox: function(parentNodeBox, repObject)
488     {
489         var childBox = this.getChildObjectBox(parentNodeBox);
490         var objectBox = this.findChildObjectBox(childBox, repObject);
491         if (objectBox)
492             childBox.removeChild(objectBox);
493     },
494 
495     // We want all children of the parent of repObject.
496     populateChildBox: function(repObject, nodeChildBox)
497     {
498         if (!repObject)
499             return null;
500 
501         var parentObjectBox = Dom.getAncestorByClass(nodeChildBox, "nodeBox");
502 
503         if (FBTrace.DBG_HTML)
504             FBTrace.sysout("+++insideOutBox.populateChildBox("+
505                 Css.getElementCSSSelector(repObject)+") parentObjectBox.populated "+
506                 parentObjectBox.populated);
507 
508         if (parentObjectBox.populated)
509             return this.findChildObjectBox(nodeChildBox, repObject);
510 
511         var lastSiblingBox = this.getChildObjectBox(nodeChildBox);
512         var siblingBox = nodeChildBox.firstChild;
513         var targetBox = null;
514 
515         var view = this.view;
516 
517         var targetSibling = null;
518         var parentNode = view.getParentObject(repObject);
519         for (var i = 0; 1; ++i)
520         {
521             targetSibling = view.getChildObject(parentNode, i, targetSibling);
522             if (!targetSibling)
523                 break;
524 
525             // Check if we need to start appending, or continue to insert before
526             if (lastSiblingBox && lastSiblingBox.repObject == targetSibling)
527                 lastSiblingBox = null;
528 
529             if (!siblingBox || siblingBox.repObject != targetSibling)
530             {
531                 var newBox = view.createObjectBox(targetSibling);
532                 if (newBox)
533                 {
534                     if (!nodeChildBox)
535                         FBTrace.sysout("insideOutBox FAILS no nodeChildBox "+repObject, repObject)
536 
537                     if (lastSiblingBox)
538                     {
539                         try
540                         {
541                             nodeChildBox.insertBefore(newBox, lastSiblingBox);
542                         }
543                         catch(exc)
544                         {
545                             FBTrace.sysout("insideOutBox FAILS insertBefore",
546                                 {repObject:repObject, nodeChildBox: nodeChildBox, newBox: newBox,
547                                 lastSiblingBox: lastSiblingBox});
548                         }
549                     }
550                     else
551                         nodeChildBox.appendChild(newBox);
552                 }
553 
554                 siblingBox = newBox;
555             }
556 
557             if (targetSibling == repObject)
558                 targetBox = siblingBox;
559 
560             if (siblingBox && siblingBox.repObject == targetSibling)
561                 siblingBox = siblingBox.nextSibling;
562         }
563 
564         if (targetBox)
565             parentObjectBox.populated = true;
566 
567         if (FBTrace.DBG_HTML)
568             FBTrace.sysout("---insideOutBox.populateChildBox("+
569                 (repObject.localName?repObject.localName:repObject)+") targetBox "+targetBox);
570 
571         return targetBox;
572     },
573 
574     getParentObjectBox: function(objectBox)
575     {
576         var parent = objectBox.parentNode ? objectBox.parentNode.parentNode : null;
577         return parent && parent.repObject ? parent : null;
578     },
579 
580     getChildObjectBox: function(objectBox)
581     {
582         return objectBox.getElementsByClassName("nodeChildBox").item(0);
583     },
584 
585     findChildObjectBox: function(parentNodeBox, repObject)
586     {
587         for (var childBox = parentNodeBox.firstChild; childBox; childBox = childBox.nextSibling)
588         {
589             if (FBTrace.DBG_HTML)
590                 FBTrace.sysout("insideOutBox.findChildObjectBox repObject: " +
591                     formatNode(repObject)+" in "+formatNode(childBox)+" = "+
592                     formatNode(childBox.repObject),
593                     {childBoxRepObject: childBox.repObject,repObject:repObject});
594 
595             if (childBox.repObject == repObject)
596                 return childBox;
597         }
598 
599         if (FBTrace.DBG_HTML)
600             FBTrace.sysout("insideOutBox.findChildObjectBox no match for repObject: " +
601                 formatNode(repObject)+" in "+formatNode(parentNodeBox));
602     },
603 
604     /**
605      * Determines if the given node is an ancestor of the current root.
606      */
607     isInExistingRoot: function(node)
608     {
609         if (FBTrace.DBG_HTML)
610             var dbg_isInExistingRoot = "";
611 
612         var parentNode = node;
613         while (parentNode && parentNode != this.rootObject)
614         {
615             if (FBTrace.DBG_HTML)
616                 dbg_isInExistingRoot = dbg_isInExistingRoot + parentNode.localName+" < ";
617 
618             parentNode = this.view.getParentObject(parentNode);
619         }
620 
621         if (FBTrace.DBG_HTML)
622             FBTrace.sysout("insideOutBox.isInExistingRoot  "+dbg_isInExistingRoot+
623                 ": (parentNode == this.rootObject)="+(parentNode == this.rootObject));
624 
625         return parentNode == this.rootObject;
626     },
627 
628     getRootNode: function(node)
629     {
630         if (FBTrace.DBG_HTML)
631             var dbg_getRootNode = "";
632 
633         while (1)
634         {
635             var parentNode = this.view.getParentObject(node);
636 
637             if (!parentNode)
638                 break;
639 
640             if (FBTrace.DBG_HTML)
641                 dbg_getRootNode += node.localName+" < ";
642 
643             node = parentNode;
644         }
645 
646         if (FBTrace.DBG_HTML)
647             FBTrace.sysout("insideOutBox.getRootNode "+dbg_getRootNode);
648 
649         return node;
650     },
651 
652     // ********************************************************************************************
653 
654     onMouseDown: function(event)
655     {
656         var hitTwisty = false;
657         for (var child = event.target; child; child = child.parentNode)
658         {
659             if (Css.hasClass(child, "twisty"))
660                 hitTwisty = true;
661             else if (child.repObject)
662             {
663                 if (hitTwisty)
664                     this.toggleObjectBox(child);
665                 break;
666             }
667         }
668     }
669 };
670 
671 // ************************************************************************************************
672 // Local Helpers
673 
674 function isVisibleTarget(node)
675 {
676     if (node.repObject && node.repObject.nodeType == Node.ELEMENT_NODE)
677     {
678         for (var parent = node.parentNode; parent; parent = parent.parentNode)
679         {
680             if (Css.hasClass(parent, "nodeChildBox")
681                 && !Css.hasClass(parent.parentNode, "open")
682                 && !Css.hasClass(parent.parentNode, "highlightOpen"))
683                 return false;
684         }
685         return true;
686     }
687 }
688 
689 function formatNode(object)
690 {
691     if (object)
692     {
693         if (!object.localName)
694         {
695             var str = object.toString();
696             if (str)
697                 return str;
698             else
699                 return "(an object with no localName or toString result)";
700         }
701         else  return Css.getElementCSSSelector(object);
702     }
703     else
704         return "(null object)";
705 }
706 
707 function formatObjectBox(object)
708 {
709     if (object)
710     {
711         if (object.localName)
712             return Css.getElementCSSSelector(object);
713         return object.textContent;
714     }
715     else
716         return "(null object)";
717 }
718 
719 function getObjectPath(element, aView)
720 {
721     var path = [];
722     for (; element; element = aView.getParentObject(element))
723         path.push(element);
724 
725     return path;
726 }
727 
728 // ************************************************************************************************
729 // Registration
730 
731 return Firebug.InsideOutBox;
732 
733 // ************************************************************************************************
734 });
735