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