1 /* See license.txt for terms of usage */ 2 3 FBL.ns(function() { with (FBL) { 4 5 const Ci = Components.interfaces; 6 const SHOW_ALL = Ci.nsIDOMNodeFilter.SHOW_ALL; 7 8 /** 9 * @class Static utility class. Contains utilities used for displaying and 10 * searching a HTML tree. 11 */ 12 Firebug.HTMLLib = 13 { 14 //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 15 // Node Search Utilities 16 //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 17 /** 18 * Constructs a NodeSearch instance. 19 * 20 * @class Class used to search a DOM tree for the given text. Will display 21 * the search results in a IO Box. 22 * 23 * @constructor 24 * @param {String} text Text to search for 25 * @param {Object} root Root of search. This may be an element or a document 26 * @param {Object} panelNode Panel node containing the IO Box representing the DOM tree. 27 * @param {Object} ioBox IO Box to display the search results in 28 * @param {Object} walker Optional walker parameter. 29 */ 30 NodeSearch: function(text, root, panelNode, ioBox, walker) 31 { 32 root = root.documentElement || root; 33 walker = walker || new Firebug.HTMLLib.DOMWalker(root); 34 var re = new ReversibleRegExp(text, "m"); 35 var matchCount = 0; 36 37 /** 38 * Finds the first match within the document. 39 * 40 * @param {boolean} revert true to search backward, false to search forward 41 * @param {boolean} caseSensitive true to match exact case, false to ignore case 42 * @return true if no more matches were found, but matches were found previously. 43 */ 44 this.find = function(reverse, caseSensitive) 45 { 46 var match = this.findNextMatch(reverse, caseSensitive); 47 if (match) 48 { 49 this.lastMatch = match; 50 ++matchCount; 51 52 var node = match.node; 53 var nodeBox = this.openToNode(node, match.isValue); 54 55 this.selectMatched(nodeBox, node, match, reverse); 56 } 57 else if (matchCount) 58 return true; 59 else 60 { 61 this.noMatch = true; 62 dispatch([Firebug.A11yModel], 'onHTMLSearchNoMatchFound', [panelNode.ownerPanel, text]); 63 } 64 }; 65 66 /** 67 * Resets the search to the beginning of the document. 68 */ 69 this.reset = function() 70 { 71 delete this.lastMatch; 72 }; 73 74 /** 75 * Finds the next match in the document. 76 * 77 * The return value is an object with the fields 78 * - node: Node that contains the match 79 * - isValue: true if the match is a match due to the value of the node, false if it is due to the name 80 * - match: Regular expression result from the match 81 * 82 * @param {boolean} revert true to search backward, false to search forward 83 * @param {boolean} caseSensitive true to match exact case, false to ignore case 84 * @return Match object if found 85 */ 86 this.findNextMatch = function(reverse, caseSensitive) 87 { 88 var innerMatch = this.findNextInnerMatch(reverse, caseSensitive); 89 if (innerMatch) 90 return innerMatch; 91 else 92 this.reset(); 93 94 function walkNode() { return reverse ? walker.previousNode() : walker.nextNode(); } 95 96 var node; 97 while (node = walkNode()) 98 { 99 if (node.nodeType == Node.TEXT_NODE) 100 { 101 if (Firebug.HTMLLib.isSourceElement(node.parentNode)) 102 continue; 103 } 104 105 var m = this.checkNode(node, reverse, caseSensitive); 106 if (m) 107 return m; 108 } 109 }; 110 111 /** 112 * Helper util used to scan the current search result for more results 113 * in the same object. 114 * 115 * @private 116 */ 117 this.findNextInnerMatch = function(reverse, caseSensitive) 118 { 119 if (this.lastMatch) 120 { 121 var lastMatchNode = this.lastMatch.node; 122 var lastReMatch = this.lastMatch.match; 123 var m = re.exec(lastReMatch.input, reverse, lastReMatch.caseSensitive, lastReMatch); 124 if (m) 125 { 126 return { 127 node: lastMatchNode, 128 isValue: this.lastMatch.isValue, 129 match: m 130 }; 131 } 132 133 // May need to check the pair for attributes 134 if (lastMatchNode.nodeType == Node.ATTRIBUTE_NODE 135 && this.lastMatch.isValue == !!reverse) 136 { 137 return this.checkNode(lastMatchNode, reverse, caseSensitive, 1); 138 } 139 } 140 }; 141 142 /** 143 * Checks a given node for a search match. 144 * 145 * @private 146 */ 147 this.checkNode = function(node, reverse, caseSensitive, firstStep) 148 { 149 var checkOrder; 150 if (node.nodeType != Node.TEXT_NODE) 151 { 152 var nameCheck = { name: "nodeName", isValue: false, caseSensitive: false }; 153 var valueCheck = { name: "nodeValue", isValue: true, caseSensitive: caseSensitive }; 154 checkOrder = reverse ? [ valueCheck, nameCheck ] : [ nameCheck, valueCheck ]; 155 } 156 else 157 { 158 checkOrder = [{name: "nodeValue", isValue: false, caseSensitive: caseSensitive }]; 159 } 160 161 for (var i = firstStep || 0; i < checkOrder.length; i++) { 162 var m = re.exec(node[checkOrder[i].name], reverse, checkOrder[i].caseSensitive); 163 if (m) 164 return { 165 node: node, 166 isValue: checkOrder[i].isValue, 167 match: m 168 }; 169 } 170 }; 171 172 /** 173 * Opens the given node in the associated IO Box. 174 * 175 * @private 176 */ 177 this.openToNode = function(node, isValue) 178 { 179 if (node.nodeType == Node.ELEMENT_NODE) 180 { 181 var nodeBox = ioBox.openToObject(node); 182 return nodeBox.getElementsByClassName("nodeTag")[0]; 183 } 184 else if (node.nodeType == Node.ATTRIBUTE_NODE) 185 { 186 var nodeBox = ioBox.openToObject(node.ownerElement); 187 if (nodeBox) 188 { 189 var attrNodeBox = Firebug.HTMLLib.findNodeAttrBox(nodeBox, node.nodeName); 190 if (isValue) 191 return getChildByClass(attrNodeBox, "nodeValue"); 192 else 193 return getChildByClass(attrNodeBox, "nodeName"); 194 } 195 } 196 else if (node.nodeType == Node.TEXT_NODE) 197 { 198 var nodeBox = ioBox.openToObject(node); 199 if (nodeBox) 200 return nodeBox; 201 else 202 { 203 var nodeBox = ioBox.openToObject(node.parentNode); 204 if (hasClass(nodeBox, "textNodeBox")) 205 nodeBox = Firebug.HTMLLib.getTextElementTextBox(nodeBox); 206 return nodeBox; 207 } 208 } 209 }; 210 211 /** 212 * Selects the search results. 213 * 214 * @private 215 */ 216 this.selectMatched = function(nodeBox, node, match, reverse) 217 { 218 setTimeout(bindFixed(function() 219 { 220 var reMatch = match.match; 221 this.selectNodeText(nodeBox, node, reMatch[0], reMatch.index, reverse, reMatch.caseSensitive); 222 dispatch([Firebug.A11yModel], 'onHTMLSearchMatchFound', [panelNode.ownerPanel, match]); 223 }, this)); 224 }; 225 226 /** 227 * Select text node search results. 228 * 229 * @private 230 */ 231 this.selectNodeText = function(nodeBox, node, text, index, reverse, caseSensitive) 232 { 233 var row; 234 235 // If we are still inside the same node as the last search, advance the range 236 // to the next substring within that node 237 if (nodeBox == this.lastNodeBox) 238 { 239 row = this.textSearch.findNext(false, true, reverse, caseSensitive); 240 } 241 242 if (!row) 243 { 244 // Search for the first instance of the string inside the node 245 function findRow(node) { return node.nodeType == Node.ELEMENT_NODE ? node : node.parentNode; } 246 this.textSearch = new TextSearch(nodeBox, findRow); 247 row = this.textSearch.find(text, reverse, caseSensitive); 248 this.lastNodeBox = nodeBox; 249 } 250 251 if (row) 252 { 253 var trueNodeBox = getAncestorByClass(nodeBox, "nodeBox"); 254 setClass(trueNodeBox,'search-selection'); 255 256 scrollIntoCenterView(row, panelNode); 257 var sel = panelNode.ownerDocument.defaultView.getSelection(); 258 sel.removeAllRanges(); 259 sel.addRange(this.textSearch.range); 260 261 removeClass(trueNodeBox,'search-selection'); 262 return true; 263 } 264 }; 265 }, 266 267 //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 268 269 /** XXXjjb this code is no longer called and won't be in 1.5; if FireFinder works out we can delete this. 270 * Constructs a SelectorSearch instance. 271 * 272 * @class Class used to search a DOM tree for elements matching the given 273 * CSS selector. 274 * 275 * @constructor 276 * @param {String} text CSS selector to search for 277 * @param {Document} doc Document to search 278 * @param {Object} panelNode Panel node containing the IO Box representing the DOM tree. 279 * @param {Object} ioBox IO Box to display the search results in 280 */ 281 SelectorSearch: function(text, doc, panelNode, ioBox) 282 { 283 this.parent = new Firebug.HTMLLib.NodeSearch(text, doc, panelNode, ioBox); 284 285 /** 286 * Finds the first match within the document. 287 * 288 * @param {boolean} revert true to search backward, false to search forward 289 * @param {boolean} caseSensitive true to match exact case, false to ignore case 290 * @return true if no more matches were found, but matches were found previously. 291 */ 292 this.find = this.parent.find; 293 294 /** 295 * Resets the search to the beginning of the document. 296 */ 297 this.reset = this.parent.reset; 298 299 /** 300 * Opens the given node in the associated IO Box. 301 * 302 * @private 303 */ 304 this.openToNode = this.parent.openToNode; 305 306 try 307 { 308 // http://dev.w3.org/2006/webapi/selectors-api/ 309 this.matchingNodes = doc.querySelectorAll(text); 310 this.matchIndex = 0; 311 } 312 catch(exc) 313 { 314 FBTrace.sysout("SelectorSearch FAILS "+exc, exc); 315 } 316 317 /** 318 * Finds the next match in the document. 319 * 320 * The return value is an object with the fields 321 * - node: Node that contains the match 322 * - isValue: true if the match is a match due to the value of the node, false if it is due to the name 323 * - match: Regular expression result from the match 324 * 325 * @param {boolean} revert true to search backward, false to search forward 326 * @param {boolean} caseSensitive true to match exact case, false to ignore case 327 * @return Match object if found 328 */ 329 this.findNextMatch = function(reverse, caseSensitive) 330 { 331 if (!this.matchingNodes || !this.matchingNodes.length) 332 return undefined; 333 334 if (reverse) 335 { 336 if (this.matchIndex > 0) 337 return { node: this.matchingNodes[this.matchIndex--], isValue: false, match: "?XX?"}; 338 else 339 return undefined; 340 } 341 else 342 { 343 if (this.matchIndex < this.matchingNodes.length) 344 return { node: this.matchingNodes[this.matchIndex++], isValue: false, match: "?XX?"}; 345 else 346 return undefined; 347 } 348 }; 349 350 /** 351 * Selects the search results. 352 * 353 * @private 354 */ 355 this.selectMatched = function(nodeBox, node, match, reverse) 356 { 357 setTimeout(bindFixed(function() 358 { 359 ioBox.select(node, true, true); 360 dispatch([Firebug.A11yModel], 'onHTMLSearchMatchFound', [panelNode.ownerPanel, match]); 361 }, this)); 362 }; 363 }, 364 365 366 /** 367 * Constructs a DOMWalker instance. 368 * 369 * @constructor 370 * @class Implements an ordered traveral of the document, including attributes and 371 * iframe contents within the results. 372 * 373 * Note that the order for attributes is not defined. This will follow the 374 * same order as the Element.attributes accessor. 375 * @param {Element} root Element to traverse 376 */ 377 DOMWalker: function(root) 378 { 379 var walker; 380 var currentNode, attrIndex; 381 var pastStart, pastEnd; 382 var doc = root.ownerDocument; 383 384 function createWalker(docElement) { 385 var walk = doc.createTreeWalker(docElement, SHOW_ALL, null, true); 386 walker.unshift(walk); 387 } 388 function getLastAncestor() { 389 while (walker[0].lastChild()) {} 390 return walker[0].currentNode; 391 } 392 393 /** 394 * Move to the previous node. 395 * 396 * @return The previous node if one exists, undefined otherwise. 397 */ 398 this.previousNode = function() { 399 if (pastStart) { 400 return undefined; 401 } 402 403 if (attrIndex) { 404 attrIndex--; 405 } else { 406 var prevNode; 407 if (currentNode == walker[0].root) { 408 if (walker.length > 1) { 409 walker.shift(); 410 prevNode = walker[0].currentNode; 411 } else { 412 prevNode = undefined; 413 } 414 } else { 415 if (!currentNode) { 416 prevNode = getLastAncestor(); 417 } else { 418 prevNode = walker[0].previousNode(); 419 } 420 if (!prevNode) { // Really shouldn't occur, but to be safe 421 prevNode = walker[0].root; 422 } 423 while ((prevNode.nodeName || "").toUpperCase() == "IFRAME") { 424 createWalker(prevNode.contentDocument.documentElement); 425 prevNode = getLastAncestor(); 426 } 427 } 428 currentNode = prevNode; 429 attrIndex = ((prevNode || {}).attributes || []).length; 430 } 431 432 if (!currentNode) { 433 pastStart = true; 434 } else { 435 pastEnd = false; 436 } 437 438 return this.currentNode(); 439 }; 440 441 /** 442 * Move to the next node. 443 * 444 * @return The next node if one exists, otherwise undefined. 445 */ 446 this.nextNode = function() { 447 if (pastEnd) { 448 return undefined; 449 } 450 451 if (!currentNode) { 452 // We are working with a new tree walker 453 currentNode = walker[0].root; 454 attrIndex = 0; 455 } else { 456 // First check attributes 457 var attrs = currentNode.attributes || []; 458 if (attrIndex < attrs.length) { 459 attrIndex++; 460 } else if ((currentNode.nodeName || "").toUpperCase() == "IFRAME") { 461 // Attributes have completed, check for iframe contents 462 createWalker(currentNode.contentDocument.documentElement); 463 currentNode = walker[0].root; 464 attrIndex = 0; 465 } else { 466 // Next node 467 var nextNode = walker[0].nextNode(); 468 while (!nextNode && walker.length > 1) { 469 walker.shift(); 470 nextNode = walker[0].nextNode(); 471 } 472 currentNode = nextNode; 473 attrIndex = 0; 474 } 475 } 476 477 if (!currentNode) { 478 pastEnd = true; 479 } else { 480 pastStart = false; 481 } 482 483 return this.currentNode(); 484 }; 485 486 /** 487 * Retrieves the current node. 488 * 489 * @return The current node, if not past the beginning or end of the iteration. 490 */ 491 this.currentNode = function() { 492 if (!attrIndex) { 493 return currentNode; 494 } else { 495 return currentNode.attributes[attrIndex-1]; 496 } 497 }; 498 499 /** 500 * Resets the walker position back to the initial position. 501 */ 502 this.reset = function() { 503 pastStart = false; 504 pastEnd = false; 505 walker = []; 506 currentNode = undefined; 507 attrIndex = 0; 508 509 createWalker(root); 510 }; 511 512 this.reset(); 513 }, 514 515 //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 516 // Node/Element Utilities 517 //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 518 519 /** 520 * Determines if the given element is the source for a non-DOM resource such 521 * as Javascript source or CSS definition. 522 * 523 * @param {Element} element Element to test 524 * @return true if the element is a source element 525 */ 526 isSourceElement: function(element) 527 { 528 var tag = element.localName.toLowerCase(); 529 return tag == "script" || tag == "link" || tag == "style" 530 || (tag == "link" && element.getAttribute("rel") == "stylesheet"); 531 }, 532 533 /** 534 * Retrieves the source URL for any external resource associated with a node. 535 * 536 * @param {Element} element Element to examine 537 * @return URL of the external resouce. 538 */ 539 getSourceHref: function(element) 540 { 541 var tag = element.localName.toLowerCase(); 542 if (tag == "script" && element.src) 543 return element.src; 544 else if (tag == "link") 545 return element.href; 546 else 547 return null; 548 }, 549 550 /** 551 * Retrieves the source text for inline script and style elements. 552 * 553 * @param {Element} element Script or style element 554 * @return Source text 555 */ 556 getSourceText: function(element) 557 { 558 var tag = element.localName.toLowerCase(); 559 if (tag == "script" && !element.src) 560 return element.textContent; 561 else if (tag == "style") 562 return element.textContent; 563 else 564 return null; 565 }, 566 567 /** 568 * Determines if the given element is a container element. 569 * 570 * @param {Element} element Element to test 571 * @return True if the element is a container element. 572 */ 573 isContainerElement: function(element) 574 { 575 var tag = element.localName.toLowerCase(); 576 switch (tag) 577 { 578 case "script": 579 case "style": 580 case "iframe": 581 case "frame": 582 case "tabbrowser": 583 case "browser": 584 return true; 585 case "link": 586 return element.getAttribute("rel") == "stylesheet"; 587 case "embed": 588 return element.getSVGDocument(); 589 } 590 return false; 591 }, 592 593 /** 594 * Determines if the given node has any children which are elements. 595 * 596 * @param {Element} element Element to test. 597 * @return true if immediate children of type Element exist, false otherwise 598 */ 599 hasNoElementChildren: function(element) 600 { 601 if (element.childElementCount != 0) // FF 3.5+ 602 return false; 603 604 // https://developer.mozilla.org/en/XBL/XBL_1.0_Reference/DOM_Interfaces 605 if (element.ownerDocument instanceof Ci.nsIDOMDocumentXBL) 606 { 607 var anonChildren = element.ownerDocument.getAnonymousNodes(element); 608 if (anonChildren) 609 { 610 for (var i = 0; i < anonChildren.length; i++) 611 { 612 if (anonChildren[i].nodeType == Node.ELEMENT_NODE) 613 return false; 614 } 615 } 616 } 617 if (FBTrace.DBG_HTML) 618 FBTrace.sysout("hasNoElementChildren TRUE "+element.tagName, element); 619 return true; 620 }, 621 622 623 /** 624 * Determines if the given node has any children which are comments. 625 * 626 * @param {Element} element Element to test. 627 * @return true if immediate children of type Comment exist, false otherwise 628 */ 629 hasCommentChildren: function(element) 630 { 631 if (element.hasChildNodes()) 632 { 633 var children = element.childNodes; 634 for (var i = 0; i < children.length; i++) 635 { 636 if (children[i] instanceof Comment) 637 return true; 638 } 639 }; 640 return false; 641 }, 642 643 644 /** 645 * Determines if the given node consists solely of whitespace text. 646 * 647 * @param {Node} node Node to test. 648 * @return true if the node is a whitespace text node 649 */ 650 isWhitespaceText: function(node) 651 { 652 if (node instanceof HTMLAppletElement) 653 return false; 654 return node.nodeType == Node.TEXT_NODE && isWhitespace(node.nodeValue); 655 }, 656 657 /** 658 * Determines if a given element is empty. When the 659 * {@link Firebug#showTextNodesWithWhitespace} parameter is true, an element is 660 * considered empty if it has no child elements and is self closing. When 661 * false, an element is considered empty if the only children are whitespace 662 * nodes. 663 * 664 * @param {Element} element Element to test 665 * @return true if the element is empty, false otherwise 666 */ 667 isEmptyElement: function(element) 668 { 669 // XXXjjb the commented code causes issues 48, 240, and 244. I think the lines should be deleted. 670 // If the DOM has whitespace children, then the element is not empty even if 671 // we decide not to show the whitespace in the UI. 672 673 // XXXsroussey reverted above but added a check for self closing tags 674 if (Firebug.showTextNodesWithWhitespace) 675 { 676 return !element.firstChild && isSelfClosing(element); 677 } 678 else 679 { 680 for (var child = element.firstChild; child; child = child.nextSibling) 681 { 682 if (!Firebug.HTMLLib.isWhitespaceText(child)) 683 return false; 684 } 685 } 686 return isSelfClosing(element); 687 }, 688 689 /** 690 * Finds the next sibling of the given node. If the 691 * {@link Firebug#showTextNodesWithWhitespace} parameter is set to true, the next 692 * sibling may be a whitespace, otherwise the next is the first adjacent 693 * non-whitespace node. 694 * 695 * @param {Node} node Node to analyze. 696 * @return Next sibling node, if one exists 697 */ 698 findNextSibling: function(node) 699 { 700 if (Firebug.showTextNodesWithWhitespace) 701 return node.nextSibling; 702 else 703 { 704 // only return a non-whitespace node 705 for (var child = node.nextSibling; child; child = child.nextSibling) 706 { 707 if (!Firebug.HTMLLib.isWhitespaceText(child)) 708 return child; 709 } 710 } 711 }, 712 713 //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 714 // Domplate Utilities 715 //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 716 717 /** 718 * Locates the attribute domplate node for a given element domplate. This method will 719 * only examine notes marked with the "nodeAttr" class that are the direct 720 * children of the given element. 721 * 722 * @param {Object} objectNodeBox The domplate element to look up the attribute for. 723 * @param {String} attrName Attribute name 724 * @return Attribute's domplate node 725 */ 726 findNodeAttrBox: function(objectNodeBox, attrName) 727 { 728 var child = objectNodeBox.firstChild.lastChild.firstChild; 729 for (; child; child = child.nextSibling) 730 { 731 if (hasClass(child, "nodeAttr") && child.childNodes[1].firstChild 732 && child.childNodes[1].firstChild.nodeValue == attrName) 733 { 734 return child; 735 } 736 } 737 }, 738 739 /** 740 * Locates the text domplate node for a given text element domplate. 741 * @param {Object} nodeBox Text element domplate 742 * @return Element's domplate text node 743 */ 744 getTextElementTextBox: function(nodeBox) 745 { 746 var nodeLabelBox = nodeBox.firstChild.lastChild; 747 return getChildByClass(nodeLabelBox, "nodeText"); 748 } 749 }; 750 751 }}); 752