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