1 /* See license.txt for terms of usage */
  2 
  3 FBL.ns(function() { with (FBL) {
  4 
  5 // ************************************************************************************************
  6 // Constants
  7 
  8 const Cc = Components.classes;
  9 const Ci = Components.interfaces;
 10 
 11 const MODIFICATION = MutationEvent.MODIFICATION;
 12 const ADDITION = MutationEvent.ADDITION;
 13 const REMOVAL = MutationEvent.REMOVAL;
 14 
 15 const HTMLLib = Firebug.HTMLLib;
 16 
 17 const BP_BREAKONATTRCHANGE = 1;
 18 const BP_BREAKONCHILDCHANGE = 2;
 19 const BP_BREAKONREMOVE = 3;
 20 const BP_BREAKONTEXT = 4;
 21 
 22 // ************************************************************************************************
 23 
 24 Firebug.HTMLModule = extend(Firebug.Module,
 25 {
 26     initialize: function(prefDomain, prefNames)
 27     {
 28         Firebug.Module.initialize.apply(this, arguments);
 29         Firebug.Debugger.addListener(this.DebuggerListener);
 30     },
 31 
 32     initContext: function(context, persistedState)
 33     {
 34         Firebug.Module.initContext.apply(this, arguments);
 35         context.mutationBreakpoints = new MutationBreakpointGroup();
 36     },
 37 
 38     loadedContext: function(context, persistedState)
 39     {
 40         context.mutationBreakpoints.load(context);
 41     },
 42 
 43     destroyContext: function(context, persistedState)
 44     {
 45         Firebug.Module.destroyContext.apply(this, arguments);
 46 
 47         context.mutationBreakpoints.store(context);
 48     },
 49 
 50     shutdown: function()
 51     {
 52         Firebug.Module.shutdown.apply(this, arguments);
 53         Firebug.Debugger.removeListener(this.DebuggerListener);
 54     },
 55 
 56     deleteNode: function(node, context)
 57     {
 58         dispatch(this.fbListeners, "onBeginFirebugChange", [node, context]);
 59         node.parentNode.removeChild(node);
 60         dispatch(this.fbListeners, "onEndFirebugChange", [node, context]);
 61     },
 62 
 63     deleteAttribute: function(node, attr, context)
 64     {
 65         dispatch(this.fbListeners, "onBeginFirebugChange", [node, context]);
 66         node.removeAttribute(attr);
 67         dispatch(this.fbListeners, "onEndFirebugChange", [node, context]);
 68     }
 69 });
 70 
 71 // ************************************************************************************************
 72 
 73 Firebug.HTMLPanel = function() {};
 74 
 75 Firebug.HTMLPanel.prototype = extend(Firebug.Panel,
 76 {
 77     toggleEditing: function()
 78     {
 79         if (this.editing)
 80             Firebug.Editor.stopEditing();
 81         else
 82             this.editNode(this.selection);
 83     },
 84 
 85     resetSearch: function()
 86     {
 87         delete this.lastSearch;
 88     },
 89 
 90     selectNext: function()
 91     {
 92         var objectBox = this.ioBox.createObjectBox(this.selection);
 93         var next = this.ioBox.getNextObjectBox(objectBox);
 94         if (next)
 95         {
 96             this.select(next.repObject);
 97 
 98             if (Firebug.Inspector.inspecting)
 99                 Firebug.Inspector.inspectNode(next.repObject);
100 
101         }
102     },
103 
104     selectPrevious: function()
105     {
106         var objectBox = this.ioBox.createObjectBox(this.selection);
107         var previous = this.ioBox.getPreviousObjectBox(objectBox);
108         if (previous)
109         {
110             this.select(previous.repObject);
111 
112             if (Firebug.Inspector.inspecting)
113                 Firebug.Inspector.inspectNode(previous.repObject);
114         }
115     },
116 
117     selectNodeBy: function(dir)
118     {
119         if (dir == "up")
120             this.selectPrevious();
121         else if (dir == "down")
122             this.selectNext();
123         else if (dir == "left")
124         {
125             var box = this.ioBox.createObjectBox(this.selection);
126             if (!hasClass(box, "open"))
127                 this.select(this.ioBox.getParentObjectBox(box).repObject);
128             else
129                 this.ioBox.contractObject(this.selection);
130         }
131         else if (dir == "right")
132         {
133             var box = this.ioBox.createObjectBox(this.selection);
134             if (!hasClass(box, "open"))
135                 this.ioBox.expandObject(this.selection);
136             else
137                 this.selectNext();
138         }
139         Firebug.Inspector.highlightObject(this.selection, this.context);
140     },
141 
142     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
143 
144     editNewAttribute: function(elt)
145     {
146         var objectNodeBox = this.ioBox.findObjectBox(elt);
147         if (objectNodeBox)
148         {
149             var labelBox = objectNodeBox.firstChild.lastChild;
150             var bracketBox = labelBox.getElementsByClassName("nodeBracket").item(0);
151             Firebug.Editor.insertRow(bracketBox, "before");
152         }
153     },
154 
155     editAttribute: function(elt, attrName)
156     {
157         var objectNodeBox = this.ioBox.findObjectBox(elt);
158         if (objectNodeBox)
159         {
160             var attrBox = HTMLLib.findNodeAttrBox(objectNodeBox, attrName);
161             if (attrBox)
162             {
163                 var attrValueBox = attrBox.childNodes[3];
164                 var value = elt.getAttribute(attrName);
165                 Firebug.Editor.startEditing(attrValueBox, value);
166             }
167         }
168     },
169 
170     deleteAttribute: function(elt, attrName)
171     {
172         Firebug.HTMLModule.deleteAttribute(elt, attrName, this.context);
173     },
174 
175     localEditors:{}, // instantiated editor cache
176     editNode: function(node)
177     {
178         var objectNodeBox = this.ioBox.findObjectBox(node);
179         if (objectNodeBox)
180         {
181             var type = getElementType(node);
182             var editor = this.localEditors[type];
183             if (!editor)
184             {
185              // look for special purpose editor (inserted by an extension), otherwise use our html editor
186                 var specializedEditor = Firebug.HTMLPanel.Editors[type] || Firebug.HTMLPanel.Editors['html'];
187                 editor = this.localEditors[type] = new specializedEditor(this.document);
188             }
189             this.startEditingNode(node, objectNodeBox, editor, type);
190         }
191     },
192 
193     startEditingNode: function(node, box, editor, type)
194     {
195         switch (type)
196         {
197             case 'html':
198             case 'xhtml':
199                 this.startEditingHTMLNode(node, box, editor);
200                 break;
201             default:
202                 this.startEditingXMLNode(node, box, editor);
203         }
204     },
205 
206     startEditingXMLNode: function(node, box, editor)
207     {
208         var xml = getElementXML(node);
209         Firebug.Editor.startEditing(box, xml, editor);
210     },
211 
212     startEditingHTMLNode: function(node, box, editor)
213     {
214         if ( nonEditableTags.hasOwnProperty(node.localName) )
215             return;
216         editor.innerEditMode = node.localName in innerEditableTags;
217 
218         var html = editor.innerEditMode ? node.innerHTML : getElementHTML(node);
219         Firebug.Editor.startEditing(box, html, editor);
220     },
221 
222     deleteNode: function(node, dir)
223     {
224         dir = dir || 'up';
225         var box = this.ioBox.createObjectBox(node);
226         if (hasClass(box, "open"))
227             this.ioBox.contractObject(this.selection);
228         this.selectNodeBy(dir);
229         Firebug.HTMLModule.deleteNode(node, this.context);
230     },
231 
232     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
233 
234     getElementSourceText: function(node)
235     {
236         if (this.sourceElements)
237         {
238             var index = this.sourceElementNodes.indexOf(node);
239             if (index != -1)
240                 return this.sourceElements[index];
241         }
242 
243         var lines;
244 
245         var url = HTMLLib.getSourceHref(node);
246         if (url)
247             lines = this.context.sourceCache.load(url);
248         else
249         {
250             var text = HTMLLib.getSourceText(node);
251             lines = splitLines(text);
252         }
253 
254         var sourceElt = new SourceText(lines, node);
255 
256         if (!this.sourceElements)
257         {
258             this.sourceElements =  [sourceElt];
259             this.sourceElementNodes = [node];
260         }
261         else
262         {
263             this.sourceElements.push(sourceElt);
264             this.sourceElementNodes.push(node);
265         }
266 
267         return sourceElt;
268     },
269 
270     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
271 
272     mutateAttr: function(target, attrChange, attrName, attrValue)
273     {
274         // Every time the user scrolls we get this pointless mutation event, which
275         // is only bad for performance
276         if (attrName == "curpos")
277             return;
278 
279         // Due to the delay call this may or may not exist in the tree anymore
280         if (!this.ioBox.isInExistingRoot(target))
281         {
282             if (FBTrace.DBG_HTML)   FBTrace.sysout("mutateAttr: different tree " + target, target);
283             return;
284         }
285 
286         if (FBTrace.DBG_HTML)
287             FBTrace.sysout("html.mutateAttr target:"+target+" attrChange:"+attrChange+" attrName:"+attrName+" attrValue: "+attrValue, target);
288 
289         this.markChange();
290 
291         var objectNodeBox = Firebug.scrollToMutations || Firebug.expandMutations
292             ? this.ioBox.createObjectBox(target)
293             : this.ioBox.findObjectBox(target);
294 
295         if (!objectNodeBox)
296             return;
297 
298         if (isVisible(objectNodeBox.repObject))
299             removeClass(objectNodeBox, "nodeHidden");
300         else
301             setClass(objectNodeBox, "nodeHidden");
302 
303         if (attrChange == MODIFICATION || attrChange == ADDITION)
304         {
305             var nodeAttr = HTMLLib.findNodeAttrBox(objectNodeBox, attrName);
306             if (FBTrace.DBG_HTML)
307                 FBTrace.sysout("mutateAttr "+attrChange+" "+attrName+"="+attrValue+" node: "+nodeAttr, nodeAttr);
308             if (nodeAttr && nodeAttr.childNodes.length > 3)
309             {
310                 var attrValueBox = nodeAttr.childNodes[3];
311                 var attrValueText = nodeAttr.childNodes[3].firstChild;
312                 if (attrValueText)
313                     attrValueText.nodeValue = attrValue;
314 
315                 this.highlightMutation(attrValueBox, objectNodeBox, "mutated");
316             }
317             else
318             {
319                 var attr = target.getAttributeNode(attrName);
320                 if (FBTrace.DBG_HTML)
321                     FBTrace.sysout("mutateAttr getAttributeNode "+attrChange+" "+attrName+"="+attrValue+" node: "+attr, attr);
322                 if (attr)
323                 {
324                     var nodeAttr = Firebug.HTMLPanel.AttrNode.tag.replace({attr: attr},
325                             this.document);
326 
327                     var labelBox = objectNodeBox.firstChild.lastChild;
328                     var bracketBox = labelBox.getElementsByClassName("nodeBracket").item(0);
329                     labelBox.insertBefore(nodeAttr, bracketBox);
330 
331                     this.highlightMutation(nodeAttr, objectNodeBox, "mutated");
332                 }
333             }
334         }
335         else if (attrChange == REMOVAL)
336         {
337             var nodeAttr = HTMLLib.findNodeAttrBox(objectNodeBox, attrName);
338             if (nodeAttr)
339             {
340                 nodeAttr.parentNode.removeChild(nodeAttr);
341             }
342 
343             // We want to highlight regardless as the domplate may have been
344             // generated after the attribute was removed from the node
345             this.highlightMutation(objectNodeBox, objectNodeBox, "mutated");
346         }
347     },
348 
349     mutateText: function(target, parent, textValue)
350     {
351         // Due to the delay call this may or may not exist in the tree anymore
352         if (!this.ioBox.isInExistingRoot(target))
353         {
354             if (FBTrace.DBG_HTML)   FBTrace.sysout("mutateText: different tree " + target, target);
355             return;
356         }
357 
358         this.markChange();
359 
360         var parentNodeBox = Firebug.scrollToMutations || Firebug.expandMutations
361             ? this.ioBox.createObjectBox(parent)
362             : this.ioBox.findObjectBox(parent);
363 
364         if (!parentNodeBox)
365         {
366             if (FBTrace.DBG_HTML)   FBTrace.sysout("html.mutateText failed to update text, parent node box does not exist");
367             return;
368         }
369 
370         if (!Firebug.showFullTextNodes)
371             textValue = cropMultipleLines(textValue);
372 
373         var parentTag = getNodeBoxTag(parentNodeBox);
374         if (parentTag == Firebug.HTMLPanel.TextElement.tag)
375         {
376             if (FBTrace.DBG_HTML)
377                 FBTrace.sysout("html.mutateText target: " + target + " parent: " + parent);
378 
379             var nodeText = HTMLLib.getTextElementTextBox(parentNodeBox);
380             if (!nodeText.firstChild)
381             {
382                 if (FBTrace.DBG_HTML)   FBTrace.sysout("html.mutateText failed to update text, TextElement firstChild does not exist");
383                 return;
384             }
385 
386             nodeText.firstChild.nodeValue = textValue;
387 
388             this.highlightMutation(nodeText, parentNodeBox, "mutated");
389         }
390         else
391         {
392             var childBox = this.ioBox.getChildObjectBox(parentNodeBox);
393             if (!childBox)
394             {
395                 if (FBTrace.DBG_HTML)   FBTrace.sysout("html.mutateText failed to update text, no child object box found");
396                 return;
397             }
398 
399             var textNodeBox = this.ioBox.findChildObjectBox(childBox, target);
400             if (textNodeBox)
401             {
402                 // structure for comment and cdata. Are there others?
403                 textNodeBox.children[0].firstChild.nodeValue = textValue;
404 
405                 this.highlightMutation(textNodeBox, parentNodeBox, "mutated");
406             }
407             else if (Firebug.scrollToMutations || Firebug.expandMutations)
408             {
409                 // We are not currently rendered but we are set to highlight
410                 var objectBox = this.ioBox.createObjectBox(target);
411                 this.highlightMutation(objectBox, objectBox, "mutated");
412             }
413         }
414     },
415 
416     mutateNode: function(target, parent, nextSibling, removal)
417     {
418         if (FBTrace.DBG_HTML)
419             FBTrace.sysout("\nhtml.mutateNode target:"+target+" parent:"+parent+(removal?"REMOVE":"")+"\n");
420 
421         // Due to the delay call this may or may not exist in the tree anymore
422         if (!removal && !this.ioBox.isInExistingRoot(target))
423         {
424             if (FBTrace.DBG_HTML)   FBTrace.sysout("mutateNode: different tree " + target, target);
425             return;
426         }
427 
428         this.markChange();  // This invalidates the panels for every mutate
429 
430         var parentNodeBox = Firebug.scrollToMutations || Firebug.expandMutations
431             ? this.ioBox.createObjectBox(parent)
432             : this.ioBox.findObjectBox(parent);
433 
434         if (FBTrace.DBG_HTML)
435             FBTrace.sysout("html.mutateNode parent:"+parent+" parentNodeBox:"+parentNodeBox+"\n");
436 
437         if (!parentNodeBox)
438             return;
439 
440         if (!Firebug.showTextNodesWithWhitespace && this.isWhitespaceText(target))
441             return;
442 
443         // target is only whitespace
444 
445         var newParentTag = getNodeTag(parent);
446         var oldParentTag = getNodeBoxTag(parentNodeBox);
447 
448         if (newParentTag == oldParentTag)
449         {
450             if (parentNodeBox.populated)
451             {
452                 if (removal)
453                 {
454                     this.ioBox.removeChildBox(parentNodeBox, target);
455 
456                     this.highlightMutation(parentNodeBox, parentNodeBox, "mutated");
457                 }
458                 else
459                 {
460                     if (nextSibling)
461                     {
462                         while (
463                                 (!Firebug.showTextNodesWithWhitespace && Firebug.HTMLLib.isWhitespaceText(nextSibling)) ||
464                                 (!Firebug.showCommentNodes && nextSibling instanceof Comment)
465                               )
466                         {
467                             nextSibling = this.findNextSibling(nextSibling);
468                         }
469                     }
470 
471                     var objectBox = nextSibling
472                         ? this.ioBox.insertChildBoxBefore(parentNodeBox, target, nextSibling)
473                         : this.ioBox.appendChildBox(parentNodeBox, target);
474 
475                     this.highlightMutation(objectBox, objectBox, "mutated");
476                 }
477             }
478             else // !parentNodeBox.populated
479             {
480                 var newParentNodeBox = newParentTag.replace({object: parent}, this.document);
481                 parentNodeBox.parentNode.replaceChild(newParentNodeBox, parentNodeBox);
482 
483                 if (this.selection && (!this.selection.parentNode || parent == this.selection))
484                     this.ioBox.select(parent, true);
485 
486                 this.highlightMutation(newParentNodeBox, newParentNodeBox, "mutated");
487 
488                 if (!removal && (Firebug.scrollToMutations || Firebug.expandMutations))
489                 {
490                     var objectBox = this.ioBox.createObjectBox(target);
491                     this.highlightMutation(objectBox, objectBox, "mutated");
492                 }
493             }
494         }
495         else // newParentTag != oldParentTag
496         {
497             var newParentNodeBox = newParentTag.replace({object: parent}, this.document);
498             if (parentNodeBox.parentNode)
499                 parentNodeBox.parentNode.replaceChild(newParentNodeBox, parentNodeBox);
500 
501             if (hasClass(parentNodeBox, "open"))
502                 this.ioBox.toggleObjectBox(newParentNodeBox, true);
503 
504             if (this.selection && (!this.selection.parentNode || parent == this.selection))
505                 this.ioBox.select(parent, true);
506 
507             this.highlightMutation(newParentNodeBox, newParentNodeBox, "mutated");
508 
509             if (!removal && (Firebug.scrollToMutations || Firebug.expandMutations))
510             {
511                 var objectBox = this.ioBox.createObjectBox(target);
512                 this.highlightMutation(objectBox, objectBox, "mutated");
513             }
514         }
515     },
516 
517     highlightMutation: function(elt, objectBox, type)
518     {
519         if (!elt)
520             return;
521 
522         if (Firebug.scrollToMutations || Firebug.expandMutations)
523         {
524             if (this.context.mutationTimeout)
525             {
526                 this.context.clearTimeout(this.context.mutationTimeout);
527                 delete this.context.mutationTimeout;
528             }
529 
530             var ioBox = this.ioBox;
531             var panelNode = this.panelNode;
532 
533             this.context.mutationTimeout = this.context.setTimeout(function()
534             {
535                 ioBox.openObjectBox(objectBox);
536 
537                 if (Firebug.scrollToMutations)
538                     scrollIntoCenterView(objectBox, panelNode);
539             }, 200);
540         }
541 
542         if (Firebug.highlightMutations)
543             setClassTimed(elt, type, this.context);
544     },
545 
546     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
547     // SourceBox proxy
548 
549     createObjectBox: function(object, isRoot)
550     {
551         if (FBTrace.DBG_HTML) FBTrace.sysout("html.createObjectBox("+(object.tagName?object.tagName:object)+", isRoot:"+(isRoot?"true":"false")+")\n");
552         var tag = getNodeTag(object);
553         if (tag)
554             return tag.replace({object: object}, this.document);
555     },
556 
557     getParentObject: function(node)
558     {
559         if (node instanceof SourceText)
560             return node.owner;
561 
562         var parentNode = node ? node.parentNode : null;
563 
564         if (FBTrace.DBG_HTML)
565             FBTrace.sysout("ChromeBugPanel.getParentObject for "+node.nodeName+" parentNode:"+(parentNode?parentNode.nodeName:"null-or-false")+"\n");
566 
567         if (parentNode)
568         {
569 
570             if (parentNode.nodeType == 9) // then parentNode is Document element
571             {
572                 if (parentNode.defaultView)
573                 {
574                     if (parentNode.defaultView == this.context.window) // for chromebug to avoid climbing put to browser.xul
575                         return null;
576 
577                     if (FBTrace.DBG_HTML)
578                         FBTrace.sysout("getParentObject parentNode.nodeType 9, frameElement:"+parentNode.defaultView.frameElement+"\n");                  /*@explore*/
579                     return parentNode.defaultView.frameElement;
580                 }
581                 else if (this.embeddedBrowserParents)
582                 {
583                     var skipParent = this.embeddedBrowserParents[node];  // better be HTML element, could be iframe
584                     if (FBTrace.DBG_HTML)
585                         FBTrace.sysout("getParentObject skipParent:"+(skipParent?skipParent.nodeName:"none")+"\n");                  /*@explore*/
586                     if (skipParent)
587                         return skipParent;
588                 }
589                 else // parent is document element, but no window at defaultView.
590                     return null;
591             }
592             else if (!parentNode.localName)
593             {
594                 if (FBTrace.DBG_HTML)
595                     FBTrace.sysout("getParentObject: null localName must be window, no parentObject");
596                 return null;
597             }
598             else
599                 return parentNode;
600         }
601         else  // Documents have no parentNode; Attr, Document, DocumentFragment, Entity, and Notation. top level windows have no parentNode
602         {
603             if (node && node.nodeType == 9) // document type
604             {
605                 if (node.defaultView) // generally a reference to the window object for the document, however that is not defined in the specification
606                 {
607                     var embeddingFrame = node.defaultView.frameElement;
608                     if (embeddingFrame)
609                         return embeddingFrame.parentNode;
610                 }
611                 else // a Document object without a parentNode or window
612                     return null;  // top level has no parent
613             }
614         }
615     },
616 
617     getChildObject: function(node, index, previousSibling)
618     {
619         if (!node)
620         {
621             FBTrace.sysout("getChildObject: null node");
622             return;
623         }
624         if (FBTrace.DBG_HTML)
625             FBTrace.sysout("getChildObject "+node.tagName+" index "+index+" previousSibling: "+previousSibling, {node: node, previousSibling:previousSibling});
626 
627         if (this.isSourceElement(node))
628         {
629             if (index == 0)
630                 return this.getElementSourceText(node);
631             else
632                 return null;  // no siblings of source elements
633         }
634         else if (node.contentDocument)  // then the node is a frame
635         {
636             if (index == 0)
637             {
638                 if (!this.embeddedBrowserParents)
639                     this.embeddedBrowserParents = {};
640                 var skipChild = node.contentDocument.documentElement; // unwrap
641                 this.embeddedBrowserParents[skipChild] = node;
642 
643                 return skipChild;  // (the node's).(type 9 document).(HTMLElement)
644             }
645             else
646                 return null;
647         }
648         else if (node.getSVGDocument && node.getSVGDocument())  // then the node is a frame
649         {
650             if (index == 0)
651             {
652                 if (!this.embeddedBrowserParents)
653                     this.embeddedBrowserParents = {};
654                 var skipChild = node.getSVGDocument().documentElement; // unwrap
655                 this.embeddedBrowserParents[skipChild] = node;
656 
657                 return skipChild;  // (the node's).(type 9 document).(SVGElement)
658             }
659             else
660                 return null;
661         }
662 
663         if (previousSibling)  // then we are walking
664             var child = this.getNextSibling(previousSibling);  // may return null, meaning done with iteration.
665         else
666             var child = this.getFirstChild(node); // child is set to at the beginning of an iteration.
667 
668         if (Firebug.showTextNodesWithWhitespace)  // then the index is true to the node list
669             return child;
670         else
671         {
672             for (; child; child = this.getNextSibling(child))
673             {
674                 if (!this.isWhitespaceText(child))
675                     return child;
676             }
677         }
678         return null;  // we have no children worth showing.
679     },
680 
681     isWhitespaceText: function(node)
682     {
683         return HTMLLib.isWhitespaceText(node);
684     },
685 
686     getFirstChild: function(node)
687     {
688         this.treeWalker = node.ownerDocument.createTreeWalker(
689                  node, NodeFilter.SHOW_ALL, null, false);
690         return this.treeWalker.firstChild();
691     },
692 
693     getNextSibling: function(node)
694     {
695         if (FBTrace.DBG_HTML || FBTrace.DBG_ERRORS)
696         {
697             if (node != this.treeWalker.currentNode)
698                 FBTrace.sysout("getNextSibling FAILS treeWalker "+this.treeWalker.currentNode+" out of sync with node "+node, this.treeWalker);
699         }
700         var next = this.treeWalker.nextSibling();
701 
702         if (!next)
703             delete this.treeWalker;
704 
705         return next;
706     },
707 
708     findNextSibling: function (node)
709     {
710         return HTMLLib.findNextSibling(node);
711     },
712 
713     isSourceElement: function(element)
714     {
715         return HTMLLib.isSourceElement(element);
716     },
717 
718     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
719     // Events
720 
721     onMutateAttr: function(event)
722     {
723         var target = event.target;
724         if (unwrapObject(target).firebugIgnore)
725             return;
726 
727         var attrChange = event.attrChange;
728         var attrName = event.attrName;
729         var newValue = event.newValue;
730 
731         this.context.delay(function()
732         {
733             this.mutateAttr(target, attrChange, attrName, newValue);
734         }, this);
735 
736         Firebug.HTMLModule.MutationBreakpoints.onMutateAttr(event, this.context);
737     },
738 
739     onMutateText: function(event)
740     {
741         if (FBTrace.DBG_HTML)
742             FBTrace.sysout("html.onMutateText; ", event);
743 
744         var target = event.target;
745         var parent = target.parentNode;
746 
747         var newValue = event.newValue;
748 
749         this.context.delay(function()
750         {
751             this.mutateText(target, parent, newValue);
752         }, this);
753 
754         Firebug.HTMLModule.MutationBreakpoints.onMutateText(event, this.context);
755     },
756 
757     onMutateNode: function(event)
758     {
759         var target = event.target;
760         if (unwrapObject(target).firebugIgnore)
761             return;
762 
763         var parent = event.relatedNode;
764         var removal = event.type == "DOMNodeRemoved";
765         var nextSibling = removal ? null : this.findNextSibling(target);
766 
767         this.context.delay(function()
768         {
769             try
770             {
771                  this.mutateNode(target, parent, nextSibling, removal);
772             }
773             catch (exc)
774             {
775                 if (FBTrace.DBG_ERRORS || FBTrace.DBG_HTML)
776                     FBTrace.sysout("html.onMutateNode FAILS:", exc);
777             }
778         }, this);
779 
780         Firebug.HTMLModule.MutationBreakpoints.onMutateNode(event, this.context);
781     },
782 
783     onClick: function(event)
784     {
785         if (isLeftClick(event) && event.detail == 2)
786         {
787             this.toggleNode(event);
788         }
789         else if (isAltClick(event) && event.detail == 2 && !this.editing)
790         {
791             this.editNode(this.selection);
792         }
793     },
794 
795     onMouseDown: function(event)
796     {
797         if (!isLeftClick(event))
798             return;
799         if (getAncestorByClass(event.target, "nodeTag"))
800         {
801             var node = Firebug.getRepObject(event.target);
802             this.noScrollIntoView = true;
803             this.select(node);
804             delete this.noScrollIntoView;
805             if (hasClass(event.target, "twisty"))
806                 this.toggleNode(event);
807         }
808     },
809 
810     toggleNode: function(event)
811     {
812         var node = Firebug.getRepObject(event.target);
813         var box = this.ioBox.createObjectBox(node);
814         if (!hasClass(box, "open"))
815             this.ioBox.expandObject(node);
816         else
817             this.ioBox.contractObject(this.selection);
818     },
819 
820     onKeyPress: function(event)
821     {
822         if (this.editing || isControl(event) || isShift(event))
823             return;
824 
825         var node = this.selection;
826         if (!node)
827             return;
828         if (event.keyCode == KeyEvent.DOM_VK_UP)
829             this.selectNodeBy("up");
830         else if (event.keyCode == KeyEvent.DOM_VK_DOWN)
831             this.selectNodeBy("down");
832         else if (event.keyCode == KeyEvent.DOM_VK_LEFT)
833             this.selectNodeBy("left");
834         else if (event.keyCode == KeyEvent.DOM_VK_RIGHT)
835             this.selectNodeBy("right");
836         else if (event.keyCode == KeyEvent.DOM_VK_BACK_SPACE && !(node.localName in innerEditableTags) && !(nonEditableTags.hasOwnProperty(node.localName)))
837             this.deleteNode(node, "up");
838         else if (event.keyCode == KeyEvent.DOM_VK_DELETE && !(node.localName in innerEditableTags) && !(nonEditableTags.hasOwnProperty(node.localName)))
839             this.deleteNode(node, "down");
840         else
841             return;
842 
843         cancelEvent(event);
844     },
845 
846     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
847     // extends Panel
848 
849     name: "html",
850     searchable: true,
851     breakable: true,
852     dependents: ["css", "computed", "layout", "dom", "domSide", "watch"],
853     inspectorHistory: new Array(5),
854 
855     initialize: function()
856     {
857         this.onMutateText = bind(this.onMutateText, this);
858         this.onMutateAttr = bind(this.onMutateAttr, this);
859         this.onMutateNode = bind(this.onMutateNode, this);
860         this.onClick = bind(this.onClick, this);
861         this.onMouseDown = bind(this.onMouseDown, this);
862         this.onKeyPress = bind(this.onKeyPress, this);
863 
864         Firebug.Panel.initialize.apply(this, arguments);
865     },
866 
867     destroy: function(state)
868     {
869         persistObjects(this, state);
870 
871         Firebug.Panel.destroy.apply(this, arguments);
872     },
873 
874     initializeNode: function(oldPanelNode)
875     {
876         if (!this.ioBox)
877             this.ioBox = new InsideOutBox(this, this.panelNode);
878 
879         this.panelNode.addEventListener("click", this.onClick, false);
880         this.panelNode.addEventListener("mousedown", this.onMouseDown, false);
881         dispatch([Firebug.A11yModel], "onInitializeNode", [this]);
882     },
883 
884     destroyNode: function()
885     {
886         this.panelNode.removeEventListener("click", this.onClick, false);
887         this.panelNode.removeEventListener("mousedown", this.onMouseDown, false);
888         this.panelNode.ownerDocument.removeEventListener("keypress", this.onKeyPress, true);
889 
890         if (this.ioBox)
891         {
892             this.ioBox.destroy();
893             delete this.ioBox;
894         }
895         dispatch([Firebug.A11yModel], "onDestroyNode", [this]);
896     },
897 
898     show: function(state)
899     {
900         this.showToolbarButtons("fbHTMLButtons", true);
901 
902         this.panelNode.ownerDocument.addEventListener("keypress", this.onKeyPress, true);
903 
904         if (this.context.loaded)
905         {
906             if (!this.context.attachedMutation)
907             {
908                 this.context.attachedMutation = true;
909 
910                 iterateWindows(this.context.window, bind(function(win)
911                 {
912                     var doc = win.document;
913                     doc.addEventListener("DOMAttrModified", this.onMutateAttr, false);
914                     doc.addEventListener("DOMCharacterDataModified", this.onMutateText, false);
915                     doc.addEventListener("DOMNodeInserted", this.onMutateNode, false);
916                     doc.addEventListener("DOMNodeRemoved", this.onMutateNode, false);
917                 }, this));
918             }
919 
920             restoreObjects(this, state);
921         }
922     },
923 
924     hide: function()
925     {
926         this.showToolbarButtons("fbHTMLButtons", false);
927         delete this.infoTipURL;  // clear the state that is tracking the infotip so it is reset after next show()
928         this.panelNode.ownerDocument.removeEventListener("keypress", this.onKeyPress, true);
929     },
930 
931     watchWindow: function(win)
932     {
933         if (this.context.window && this.context.window != win) // then I guess we are an embedded window
934         {
935             var htmlPanel = this;
936             iterateWindows(this.context.window, function(subwin)
937             {
938                 if (win == subwin)
939                 {
940                     if (FBTrace.DBG_HTML)
941                         FBTrace.sysout("html.watchWindow found subwin.location.href="+win.location.href+"\n");
942                     htmlPanel.mutateDocumentEmbedded(win, false);
943                 }
944             });
945 
946         }
947         if (this.context.attachedMutation)
948         {
949             var doc = win.document;
950             doc.addEventListener("DOMAttrModified", this.onMutateAttr, false);
951             doc.addEventListener("DOMCharacterDataModified", this.onMutateText, false);
952             doc.addEventListener("DOMNodeInserted", this.onMutateNode, false);
953             doc.addEventListener("DOMNodeRemoved", this.onMutateNode, false);
954         }
955     },
956 
957     unwatchWindow: function(win)
958     {
959         if (this.context.window && this.context.window != win) // then I guess we are an embedded window
960         {
961             var htmlPanel = this;
962             iterateWindows(this.context.window, function(subwin)
963             {
964                 if (win == subwin)
965                 {
966                     if (FBTrace.DBG_HTML)
967                         FBTrace.sysout("html.unwatchWindow found subwin.location.href="+win.location.href+"\n");
968                     htmlPanel.mutateDocumentEmbedded(win, true);
969                 }
970             });
971 
972         }
973         var doc = win.document;
974         doc.removeEventListener("DOMAttrModified", this.onMutateAttr, false);
975         doc.removeEventListener("DOMCharacterDataModified", this.onMutateText, false);
976         doc.removeEventListener("DOMNodeInserted", this.onMutateNode, false);
977         doc.removeEventListener("DOMNodeRemoved", this.onMutateNode, false);
978     },
979 
980     mutateDocumentEmbedded: function(win, remove)
981     {
982         // document.documentElement    Returns the Element that is a direct child of document. For HTML documents, this normally the HTML element.
983         var target = win.document.documentElement;
984         var parent = win.frameElement;
985         var nextSibling = this.findNextSibling(target || parent);
986         this.mutateNode(target, parent, nextSibling, remove);
987     },
988 
989     supportsObject: function(object)
990     {
991         if (object instanceof Element || object instanceof Text || object instanceof CDATASection)
992             return 2;
993         else if (object instanceof SourceLink && object.type == "css" && !reCSS.test(object.href))
994             return 2;
995         else
996             return 0;
997     },
998 
999     updateOption: function(name, value)
1000     {
1001         var viewOptionNames = {
1002                 showCommentNodes:1,
1003                 showTextNodesWithEntities:1,
1004                 showTextNodesWithWhitespace:1,
1005                 showFullTextNodes:1
1006         };
1007         if (name in viewOptionNames)
1008         {
1009             this.resetSearch();
1010             clearNode(this.panelNode);
1011             if (this.ioBox)
1012                 this.ioBox.destroy();
1013 
1014             this.ioBox = new InsideOutBox(this, this.panelNode);
1015             this.ioBox.select(this.selection, true, true);
1016         }
1017     },
1018 
1019     updateSelection: function(object)
1020     {
1021         if (FBTrace.DBG_HTML)
1022             FBTrace.sysout("html.updateSelection "+object);
1023         if (this.ioBox.sourceRow)
1024             this.ioBox.sourceRow.removeAttribute("exe_line");
1025 
1026         if (object instanceof SourceLink) // && object.type == "css" and !reCSS(object.href) by supports
1027         {
1028             var sourceLink = object;
1029             var stylesheet = getStyleSheetByHref(sourceLink.href, this.context);
1030             if (stylesheet)
1031             {
1032                 var ownerNode = stylesheet.ownerNode;
1033                 if (FBTrace.DBG_CSS)
1034                         FBTrace.sysout("html panel updateSelection stylesheet.ownerNode="+stylesheet.ownerNode
1035                                           +" href:"+sourceLink.href+"\n");
1036                 if (ownerNode)
1037                 {
1038                     var objectbox = this.ioBox.select(ownerNode, true, true, this.noScrollIntoView);
1039 
1040                     // XXXjjb seems like this could be bad for errors at the end of long files
1041                     //
1042                     var sourceRow = objectbox.getElementsByClassName("sourceRow").item(0); // first source row in style
1043                     for (var lineNo = 1; lineNo < sourceLink.line; lineNo++)
1044                     {
1045                         if (!sourceRow) break;
1046                         sourceRow = FBL.getNextByClass(sourceRow,  "sourceRow");
1047                     }
1048                     if (FBTrace.DBG_CSS)
1049                         FBTrace.sysout("html panel updateSelection sourceLink.line="+sourceLink.line
1050                                           +" sourceRow="+(sourceRow?sourceRow.innerHTML:"undefined")+"\n");
1051                     if (sourceRow)
1052                     {
1053                         this.ioBox.sourceRow = sourceRow;
1054                         this.ioBox.sourceRow.setAttribute("exe_line", "true");
1055                         scrollIntoCenterView(sourceRow);
1056                         this.ioBox.selectObjectBox(sourceRow, false);  // sourceRow isn't an objectBox, but the function should work anyway...
1057                     }
1058                 }
1059             }
1060         }
1061         else if (Firebug.Inspector.inspecting)
1062         {
1063             this.ioBox.highlight(object);
1064         }
1065         else
1066         {
1067             Firebug.chrome.getSelectedSidePanel().panelNode.scrollTop = 0;
1068             this.ioBox.select(object, true, false, this.noScrollIntoView);
1069             this.inspectorHistory.unshift(object);
1070             if (this.inspectorHistory.length > 5)
1071                 this.inspectorHistory.pop();
1072         }
1073     },
1074 
1075     stopInspecting: function(object, cancelled)
1076     {
1077         if (object != this.inspectorHistory)
1078         {
1079             // Manage history of selection for later access in the command line.
1080             this.inspectorHistory.unshift(object);
1081             if (this.inspectorHistory.length > 5)
1082                 this.inspectorHistory.pop();
1083 
1084             if (FBTrace.DBG_HTML)
1085                 FBTrace.sysout("html.stopInspecting: inspectoryHistory updated", this.inspectorHistory);
1086         }
1087 
1088         this.ioBox.highlight(null);
1089 
1090         if (!cancelled)
1091             this.ioBox.select(object, true);
1092     },
1093 
1094     search: function(text, reverse)
1095     {
1096         if (!text)
1097             return;
1098 
1099         var search;
1100         if (text == this.searchText && this.lastSearch)
1101             search = this.lastSearch;
1102         else
1103         {
1104             var doc = this.context.window.document;
1105             search = this.lastSearch = new HTMLLib.NodeSearch(text, doc, this.panelNode, this.ioBox);
1106         }
1107 
1108         var loopAround = search.find(reverse, Firebug.Search.isCaseSensitive(text));
1109         if (loopAround)
1110         {
1111             this.resetSearch();
1112             this.search(text, reverse);
1113         }
1114 
1115         return !search.noMatch;
1116     },
1117 
1118     getSearchOptionsMenuItems: function()
1119     {
1120         return [
1121             Firebug.Search.searchOptionMenu("search.Case_Sensitive", "searchCaseSensitive")
1122         ];
1123     },
1124 
1125     getDefaultSelection: function()
1126     {
1127         try
1128         {
1129             var doc = this.context.window.document;
1130             return doc.body ? doc.body : getPreviousElement(doc.documentElement.lastChild);
1131         }
1132         catch (exc)
1133         {
1134             return null;
1135         }
1136     },
1137 
1138     getObjectPath: function(element)
1139     {
1140         var path = [];
1141         for (; element; element = this.getParentObject(element))
1142             path.push(element);
1143 
1144         return path;
1145     },
1146 
1147     getPopupObject: function(target)
1148     {
1149         return Firebug.getRepObject(target);
1150     },
1151 
1152     getTooltipObject: function(target)
1153     {
1154         return null;
1155     },
1156 
1157     getOptionsMenuItems: function()
1158     {
1159         return [
1160             optionMenu("ShowFullText", "showFullTextNodes"),
1161             optionMenu("ShowWhitespace", "showTextNodesWithWhitespace"),
1162             optionMenu("ShowComments", "showCommentNodes"),
1163             optionMenu("ShowTextNodesWithEntities", "showTextNodesWithEntities"),
1164             "-",
1165             optionMenu("HighlightMutations", "highlightMutations"),
1166             optionMenu("ExpandMutations", "expandMutations"),
1167             optionMenu("ScrollToMutations", "scrollToMutations"),
1168             "-",
1169             optionMenu("ShadeBoxModel", "shadeBoxModel"),
1170             optionMenu("ShowQuickInfoBox","showQuickInfoBox")
1171         ];
1172     },
1173 
1174     getContextMenuItems: function(node, target)
1175     {
1176         if (!node)
1177             return null;
1178 
1179         var items = [];
1180 
1181         if (node && node.nodeType == 1)
1182         {
1183             items.push(
1184                 "-",
1185                 {label: "NewAttribute", command: bindFixed(this.editNewAttribute, this, node) }
1186             );
1187 
1188             var attrBox = getAncestorByClass(target, "nodeAttr");
1189             if (getAncestorByClass(target, "nodeAttr"))
1190             {
1191                 var attrName = attrBox.childNodes[1].textContent;
1192 
1193                 items.push(
1194                     {label: $STRF("EditAttribute", [attrName]), nol10n: true,
1195                         command: bindFixed(this.editAttribute, this, node, attrName) },
1196                     {label: $STRF("DeleteAttribute", [attrName]), nol10n: true,
1197                         command: bindFixed(this.deleteAttribute, this, node, attrName) }
1198                 );
1199             }
1200 
1201             if (!( nonEditableTags.hasOwnProperty(node.localName) ))
1202             {
1203                 var EditElement = "EditHTMLElement";
1204 
1205                 if (isElementMathML(node))
1206                     EditElement = "EditMathMLElement"
1207                 else if (isElementSVG(node))
1208                     EditElement = "EditSVGElement";
1209 
1210                 items.push("-", { label: EditElement, command: bindFixed(this.editNode, this, node)},
1211                             { label: "DeleteElement", command: bindFixed(this.deleteNode, this, node), disabled:(node.localName in innerEditableTags)}
1212                            );
1213             }
1214         }
1215         else
1216         {
1217             items.push(
1218                 "-",
1219                 {label: "EditNode", command: bindFixed(this.editNode, this, node) },
1220                 {label: "DeleteNode", command: bindFixed(this.deleteNode, this, node) }
1221             );
1222         }
1223 
1224         Firebug.HTMLModule.MutationBreakpoints.getContextMenuItems(
1225             this.context,node, target, items);
1226 
1227         return items;
1228     },
1229 
1230     showInfoTip: function(infoTip, target, x, y)
1231     {
1232         if (!hasClass(target, "nodeValue"))
1233             return;
1234 
1235         var targetNode = Firebug.getRepObject(target);
1236         if (targetNode && targetNode.nodeType == 1 && targetNode.localName.toUpperCase() == "IMG")
1237         {
1238             var url = targetNode.src;
1239             if (url == this.infoTipURL) // This state cleared in hide()
1240                 return true;
1241 
1242             this.infoTipURL = url;
1243             return Firebug.InfoTip.populateImageInfoTip(infoTip, url);
1244         }
1245     },
1246 
1247     getEditor: function(target, value)
1248     {
1249         if (hasClass(target, "nodeName") || hasClass(target, "nodeValue") || hasClass(target, "nodeBracket"))
1250         {
1251             if (!this.attrEditor)
1252                 this.attrEditor = new Firebug.HTMLPanel.Editors.Attribute(this.document);
1253 
1254             return this.attrEditor;
1255         }
1256         else if (hasClass(target, "nodeComment") || hasClass(target, "nodeCDATA"))
1257         {
1258             if (!this.textDataEditor)
1259                 this.textDataEditor = new Firebug.HTMLPanel.Editors.TextData(this.document);
1260 
1261             return this.textDataEditor;
1262         }
1263         else if (hasClass(target, "nodeText"))
1264         {
1265             if (!this.textNodeEditor)
1266                 this.textNodeEditor = new Firebug.HTMLPanel.Editors.TextNode(this.document);
1267 
1268             return this.textNodeEditor;
1269         }
1270     },
1271 
1272     getInspectorVars: function()
1273     {
1274         var vars = {};
1275         for (var i=0; i<2; i++)
1276             vars["$"+i] = this.inspectorHistory[i];
1277 
1278         return vars;
1279     },
1280 
1281     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1282     // Break on Mutate
1283 
1284     breakOnNext: function(breaking)
1285     {
1286         Firebug.HTMLModule.MutationBreakpoints.breakOnNext(this.context, breaking);
1287     },
1288 
1289     shouldBreakOnNext: function()
1290     {
1291         return this.context.breakOnNextMutate;
1292     },
1293 
1294     getBreakOnNextTooltip: function(enabled)
1295     {
1296         return (enabled ? $STR("html.Disable Break On Mutate") : $STR("html.Break On Mutate"));
1297     },
1298 });
1299 
1300 // ************************************************************************************************
1301 
1302 var AttrTag = Firebug.HTMLPanel.AttrTag =
1303     SPAN({"class": "nodeAttr editGroup"},
1304         " ", SPAN({"class": "nodeName editable"}, "$attr.nodeName"), "="",
1305         SPAN({"class": "nodeValue editable"}, "$attr.nodeValue"), """
1306     );
1307 
1308 var TextTag = Firebug.HTMLPanel.TextTag =
1309     SPAN({"class": "nodeText editable"},
1310         FOR("char", "$object|getNodeTextGroups",
1311             SPAN({"class": "$char.class $char.extra"}, "$char.str")
1312         )
1313     );
1314 
1315 // ************************************************************************************************
1316 
1317 Firebug.HTMLPanel.CompleteElement = domplate(FirebugReps.Element,
1318 {
1319     tag:
1320         DIV({"class": "nodeBox open $object|getHidden repIgnore", _repObject: "$object", role : 'presentation'},
1321             DIV({"class": "nodeLabel", role: "presentation"},
1322                 SPAN({"class": "nodeLabelBox repTarget repTarget", role : 'treeitem', 'aria-expanded' : 'false'},
1323                     "<",
1324                     SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"),
1325                     FOR("attr", "$object|attrIterator", AttrTag),
1326                     SPAN({"class": "nodeBracket"}, ">")
1327                 )
1328             ),
1329             DIV({"class": "nodeChildBox", role :"group"},
1330                 FOR("child", "$object|childIterator",
1331                     TAG("$child|getNodeTag", {object: "$child"})
1332                 )
1333             ),
1334             DIV({"class": "nodeCloseLabel", role:"presentation"},
1335                 "</",
1336                 SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"),
1337                 ">"
1338              )
1339         ),
1340 
1341     getNodeTag: function(node)
1342     {
1343         return getNodeTag(node, true);
1344     },
1345 
1346     childIterator: function(node)
1347     {
1348         if (node.contentDocument)
1349             return [node.contentDocument.documentElement];
1350 
1351         if (Firebug.showTextNodesWithWhitespace)
1352             return cloneArray(node.childNodes);
1353         else
1354         {
1355             var nodes = [];
1356             for (var child = node.firstChild; child; child = child.nextSibling)
1357             {
1358                 if (child.nodeType != Node.TEXT_NODE || !HTMLLib.isWhitespaceText(child))
1359                     nodes.push(child);
1360             }
1361             return nodes;
1362         }
1363     }
1364 });
1365 
1366 Firebug.HTMLPanel.SoloElement = domplate(Firebug.HTMLPanel.CompleteElement,
1367 {
1368     tag:
1369         DIV({"class": "soloElement", onmousedown: "$onMouseDown"},
1370             Firebug.HTMLPanel.CompleteElement.tag
1371         ),
1372 
1373     onMouseDown: function(event)
1374     {
1375         for (var child = event.target; child; child = child.parentNode)
1376         {
1377             if (child.repObject)
1378             {
1379                 var panel = Firebug.getElementPanel(child);
1380                 Firebug.chrome.select(child.repObject);
1381                 break;
1382             }
1383         }
1384     }
1385 });
1386 
1387 Firebug.HTMLPanel.Element = domplate(FirebugReps.Element,
1388 {
1389     tag:
1390         DIV({"class": "nodeBox containerNodeBox $object|getHidden repIgnore", _repObject: "$object", role :"presentation"},
1391             DIV({"class": "nodeLabel", role: "presentation"},
1392                 IMG({"class": "twisty", role: "presentation"}),
1393                 SPAN({"class": "nodeLabelBox repTarget", role : 'treeitem', 'aria-expanded' : 'false'},
1394                     "<",
1395                     SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"),
1396                     FOR("attr", "$object|attrIterator", AttrTag),
1397                     SPAN({"class": "nodeBracket editable insertBefore"}, ">")
1398                 )
1399             ),
1400             DIV({"class": "nodeChildBox", role :"group"}), /* nodeChildBox is special signal in insideOutBox */
1401             DIV({"class": "nodeCloseLabel", role : "presentation"},
1402                 SPAN({"class": "nodeCloseLabelBox repTarget"},
1403                     "</",
1404                     SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"),
1405                     ">"
1406                 )
1407             )
1408         )
1409 });
1410 
1411 Firebug.HTMLPanel.TextElement = domplate(FirebugReps.Element,
1412 {
1413     tag:
1414         DIV({"class": "nodeBox textNodeBox $object|getHidden repIgnore", _repObject: "$object", role : 'presentation'},
1415             DIV({"class": "nodeLabel", role: "presentation"},
1416                 SPAN({"class": "nodeLabelBox repTarget", role : 'treeitem'},
1417                     "<",
1418                     SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"),
1419                     FOR("attr", "$object|attrIterator", AttrTag),
1420                     SPAN({"class": "nodeBracket editable insertBefore"}, ">"),
1421                     TextTag,
1422                     "</",
1423                     SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"),
1424                     ">"
1425                 )
1426             )
1427         )
1428 });
1429 
1430 Firebug.HTMLPanel.EmptyElement = domplate(FirebugReps.Element,
1431 {
1432     tag:
1433         DIV({"class": "nodeBox emptyNodeBox $object|getHidden repIgnore", _repObject: "$object", role : 'presentation'},
1434             DIV({"class": "nodeLabel", role: "presentation"},
1435                 SPAN({"class": "nodeLabelBox repTarget", role : 'treeitem'},
1436                     "<",
1437                     SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"),
1438                     FOR("attr", "$object|attrIterator", AttrTag),
1439                     SPAN({"class": "nodeBracket editable insertBefore"}, ">")
1440                 )
1441             )
1442         )
1443 });
1444 
1445 Firebug.HTMLPanel.XEmptyElement = domplate(FirebugReps.Element,
1446 {
1447     tag:
1448         DIV({"class": "nodeBox emptyNodeBox $object|getHidden repIgnore", _repObject: "$object", role : 'presentation'},
1449             DIV({"class": "nodeLabel", role: "presentation"},
1450                 SPAN({"class": "nodeLabelBox repTarget", role : 'treeitem'},
1451                     "<",
1452                     SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"),
1453                     FOR("attr", "$object|attrIterator", AttrTag),
1454                     SPAN({"class": "nodeBracket editable insertBefore"}, "/>")
1455                 )
1456             )
1457         )
1458 });
1459 
1460 Firebug.HTMLPanel.AttrNode = domplate(FirebugReps.Element,
1461 {
1462     tag: AttrTag
1463 });
1464 
1465 Firebug.HTMLPanel.TextNode = domplate(FirebugReps.Element,
1466 {
1467     tag:
1468         DIV({"class": "nodeBox", _repObject: "$object", role : 'presentation'},
1469             TextTag
1470         )
1471 });
1472 
1473 Firebug.HTMLPanel.CDATANode = domplate(FirebugReps.Element,
1474 {
1475     tag:
1476         DIV({"class": "nodeBox", _repObject: "$object", role : 'presentation'},
1477             "<![CDATA[",
1478             SPAN({"class": "nodeText nodeCDATA editable"}, "$object.nodeValue"),
1479             "]]>"
1480         )
1481 });
1482 
1483 Firebug.HTMLPanel.CommentNode = domplate(FirebugReps.Element,
1484 {
1485     tag:
1486         DIV({"class": "nodeBox nodeComment", _repObject: "$object", role : 'presentation'},
1487             "<!--",
1488             SPAN({"class": "nodeComment editable"}, "$object.nodeValue"),
1489             "-->"
1490         )
1491 });
1492 
1493 
1494 // ************************************************************************************************
1495 // TextDataEditor
1496 
1497 /*
1498  * TextDataEditor deals with text of comments and cdata nodes
1499  */
1500 
1501 function TextDataEditor(doc)
1502 {
1503     this.initializeInline(doc);
1504 }
1505 
1506 TextDataEditor.prototype = domplate(Firebug.InlineEditor.prototype,
1507 {
1508 
1509     saveEdit: function(target, value, previousValue)
1510     {
1511         var node = Firebug.getRepObject(target);
1512         if (!node)
1513             return;
1514         target.data = value;
1515         node.data = value;
1516     }
1517 });
1518 
1519 //************************************************************************************************
1520 // TextNodeEditor
1521 
1522 /*
1523  * TextNodeEditor deals with text nodes that do and do not have sibling elements. If
1524  * there are no sibling elements, the parent is known as a TextElement. In other cases
1525  * we keep track of their position via a range (this is in part because as people type
1526  * html, the range will keep track of the text nodes and elements that the user
1527  * is creating as they type, and this range could be in the middle of the parent
1528  * elements children).
1529  */
1530 
1531 function TextNodeEditor(doc)
1532 {
1533     this.initializeInline(doc);
1534 }
1535 
1536 TextNodeEditor.prototype = domplate(Firebug.InlineEditor.prototype,
1537 {
1538 
1539     beginEditing: function(target, value)
1540     {
1541         var node = Firebug.getRepObject(target);
1542         if (!node || node instanceof Element)
1543             return;
1544         var document = node.ownerDocument;
1545         this.range = document.createRange();
1546         this.range.setStartBefore(node);
1547         this.range.setEndAfter(node);
1548     },
1549 
1550     endEditing: function(target, value, cancel)
1551     {
1552         if (this.range)
1553         {
1554             this.range.detach();
1555             delete this.range;
1556         }
1557         // Remove empty groups by default
1558         return true;
1559     },
1560 
1561     saveEdit: function(target, value, previousValue)
1562     {
1563         var node = Firebug.getRepObject(target);
1564         if (!node)
1565             return;
1566         value = unescapeForTextNode(value || '');
1567         target.innerHTML = escapeForTextNode(value);
1568         if (node instanceof Element)
1569         {
1570             if (isElementMathML(node) || isElementSVG(node))
1571                 node.textContent=value;
1572             else
1573                 node.innerHTML=value;
1574         }
1575         else
1576         {
1577             try
1578             {
1579                 var documentFragment = this.range.createContextualFragment(value);
1580                 var cnl=documentFragment.childNodes.length;
1581                 this.range.deleteContents();
1582                 this.range.insertNode(documentFragment);
1583                 var r = this.range, sc = r.startContainer, so = r.startOffset;
1584                 this.range.setEnd(sc,so+cnl);
1585             } catch (e) {}
1586         }
1587     }
1588 });
1589 
1590 //************************************************************************************************
1591 //AttributeEditor
1592 
1593 function AttributeEditor(doc)
1594 {
1595     this.initializeInline(doc);
1596 }
1597 
1598 AttributeEditor.prototype = domplate(Firebug.InlineEditor.prototype,
1599 {
1600     saveEdit: function(target, value, previousValue)
1601     {
1602         var element = Firebug.getRepObject(target);
1603         if (!element)
1604             return;
1605 
1606         // XXXstr unescape value
1607 
1608         target.innerHTML = escapeForElementAttribute(value);
1609 
1610         if (hasClass(target, "nodeName"))
1611         {
1612             if (value != previousValue)
1613                 element.removeAttribute(previousValue);
1614             if (value)
1615             {
1616                 var attrValue = getNextByClass(target, "nodeValue").textContent;
1617                 element.setAttribute(value, attrValue);
1618             }
1619             else
1620                 element.removeAttribute(value);
1621         }
1622         else if (hasClass(target, "nodeValue"))
1623         {
1624             var attrName = getPreviousByClass(target, "nodeName").textContent;
1625             element.setAttribute(attrName, value);
1626         }
1627         //this.panel.markChange();
1628     },
1629 
1630     advanceToNext: function(target, charCode)
1631     {
1632         if (charCode == 61 && hasClass(target, "nodeName"))
1633             return true;
1634     },
1635 
1636     insertNewRow: function(target, insertWhere)
1637     {
1638         var emptyAttr = {nodeName: "", nodeValue: ""};
1639         var sibling = insertWhere == "before" ? target.previousSibling : target;
1640         return AttrTag.insertAfter({attr: emptyAttr}, sibling);
1641     }
1642 });
1643 
1644 //************************************************************************************************
1645 //HTMLEditor
1646 
1647 function HTMLEditor(doc)
1648 {
1649  this.box = this.tag.replace({}, doc, this);
1650  this.input = this.box.firstChild;
1651 
1652  this.multiLine = true;
1653  this.tabNavigation = false;
1654  this.arrowCompletion = false;
1655 }
1656 
1657 HTMLEditor.prototype = domplate(Firebug.BaseEditor,
1658 {
1659  tag: DIV(
1660      TEXTAREA({"class": "htmlEditor fullPanelEditor", oninput: "$onInput"})
1661  ),
1662 
1663  getValue: function()
1664  {
1665      return this.input.value;
1666  },
1667 
1668  setValue: function(value)
1669  {
1670      return this.input.value = value;
1671  },
1672 
1673  show: function(target, panel, value, textSize, targetSize)
1674  {
1675      this.target = target;
1676      this.panel = panel;
1677      this.editingElements = [target.repObject, null];
1678 
1679      this.panel.panelNode.appendChild(this.box);
1680 
1681      this.input.value = value;
1682      this.input.focus();
1683 
1684      var command = Firebug.chrome.$("cmd_toggleHTMLEditing");
1685      command.setAttribute("checked", true);
1686  },
1687 
1688  hide: function()
1689  {
1690      var command = Firebug.chrome.$("cmd_toggleHTMLEditing");
1691      command.setAttribute("checked", false);
1692 
1693      this.panel.panelNode.removeChild(this.box);
1694 
1695      delete this.editingElements;
1696      delete this.target;
1697      delete this.panel;
1698  },
1699 
1700  saveEdit: function(target, value, previousValue)
1701  {
1702      // Remove all of the nodes in the last range we created, except for
1703      // the first one, because setOuterHTML will replace it
1704      var first = this.editingElements[0], last = this.editingElements[1];
1705      if (last && last != first)
1706      {
1707          for (var child = first.nextSibling; child;)
1708          {
1709              var next = child.nextSibling;
1710              child.parentNode.removeChild(child);
1711              if (child == last)
1712                  break;
1713              else
1714                  child = next;
1715          }
1716      }
1717 
1718      // Make sure that we create at least one node here, even if it's just
1719      // an empty space, because this code depends on having something to replace
1720      if (!value)
1721          value = " ";
1722 
1723      if (this.innerEditMode)
1724          this.editingElements[0].innerHTML = value;
1725      else
1726          this.editingElements = setOuterHTML(this.editingElements[0], value);
1727  },
1728 
1729  endEditing: function()
1730  {
1731      //this.panel.markChange();
1732      return true;
1733  },
1734 
1735  // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1736 
1737  onInput: function()
1738  {
1739      Firebug.Editor.update();
1740  }
1741 });
1742 
1743 
1744 // ************************************************************************************************
1745 // Editors
1746 
1747 Firebug.HTMLPanel.Editors = {
1748     html : HTMLEditor,
1749     Attribute : AttributeEditor,
1750     TextNode: TextNodeEditor,
1751     TextData: TextDataEditor
1752 };
1753 
1754 
1755 // ************************************************************************************************
1756 // Local Helpers
1757 
1758 function getEmptyElementTag(node)
1759 {
1760     var isXhtml= isElementXHTML(node);
1761     if (isXhtml)
1762         return Firebug.HTMLPanel.XEmptyElement.tag;
1763     else
1764         return Firebug.HTMLPanel.EmptyElement.tag;
1765 }
1766 
1767 function getNodeTag(node, expandAll)
1768 {
1769     if (node instanceof Element)
1770     {
1771         if (node instanceof HTMLAppletElement)
1772             return getEmptyElementTag(node);
1773         else if (unwrapObject(node).firebugIgnore)
1774             return null;
1775         else if (HTMLLib.isContainerElement(node))
1776             return expandAll ? Firebug.HTMLPanel.CompleteElement.tag : Firebug.HTMLPanel.Element.tag;
1777         else if (HTMLLib.isEmptyElement(node))
1778             return getEmptyElementTag(node);
1779         else if (Firebug.showCommentNodes && HTMLLib.hasCommentChildren(node))
1780             return expandAll ? Firebug.HTMLPanel.CompleteElement.tag : Firebug.HTMLPanel.Element.tag;
1781         else if (HTMLLib.hasNoElementChildren(node))
1782             return Firebug.HTMLPanel.TextElement.tag;
1783         else
1784             return expandAll ? Firebug.HTMLPanel.CompleteElement.tag : Firebug.HTMLPanel.Element.tag;
1785     }
1786     else if (node instanceof Text)
1787         return Firebug.HTMLPanel.TextNode.tag;
1788     else if (node instanceof CDATASection)
1789         return Firebug.HTMLPanel.CDATANode.tag;
1790     else if (node instanceof Comment && (Firebug.showCommentNodes || expandAll))
1791         return Firebug.HTMLPanel.CommentNode.tag;
1792     else if (node instanceof SourceText)
1793         return FirebugReps.SourceText.tag;
1794     else
1795         return FirebugReps.Nada.tag;
1796 }
1797 
1798 function getNodeBoxTag(nodeBox)
1799 {
1800     var re = /([^\s]+)NodeBox/;
1801     var m = re.exec(nodeBox.className);
1802     if (!m)
1803         return null;
1804 
1805     var nodeBoxType = m[1];
1806     if (nodeBoxType == "container")
1807         return Firebug.HTMLPanel.Element.tag;
1808     else if (nodeBoxType == "text")
1809         return Firebug.HTMLPanel.TextElement.tag;
1810     else if (nodeBoxType == "empty")
1811         return Firebug.HTMLPanel.EmptyElement.tag;
1812 }
1813 
1814 // ************************************************************************************************
1815 // Mutation Breakpoints
1816 
1817 /**
1818  * @class Represents {@link Firebug.Debugger} listener. This listener is reponsible for
1819  * providing a list of mutation-breakpoints into the Breakpoints side-panel.
1820  */
1821 Firebug.HTMLModule.DebuggerListener =
1822 {
1823     getBreakpoints: function(context, groups)
1824     {
1825         if (!context.mutationBreakpoints.isEmpty())
1826             groups.push(context.mutationBreakpoints);
1827     }
1828 };
1829 
1830 Firebug.HTMLModule.MutationBreakpoints =
1831 {
1832     breakOnNext: function(context, breaking)
1833     {
1834         context.breakOnNextMutate = breaking;
1835     },
1836 
1837     breakOnNextMutate: function(event, context, type)
1838     {
1839         if (!context.breakOnNextMutate)
1840             return false;
1841 
1842         // Ignore changes in trees marked with firebugIgnore.
1843         if (isAncestorIgnored(event.target))
1844             return false;
1845 
1846         context.breakOnNextMutate = false;
1847 
1848         this.breakWithCause(event, context, type);
1849     },
1850 
1851     breakWithCause: function(event, context, type)
1852     {
1853         var changeLabel = Firebug.HTMLModule.BreakpointRep.getChangeLabel({type: type});
1854         context.breakingCause = {
1855             title: $STR("net.Break On Mutate"),
1856             message: changeLabel,
1857             type: event.type,
1858             target: event.target,
1859             relatedNode: event.relatedNode, // http://www.w3.org/TR/DOM-Level-2-Events/events.html
1860             prevValue: event.prevValue,
1861             newValue: event.newValue,
1862             attrName: event.attrName,
1863             attrChange: event.attrChange,
1864         };
1865 
1866         Firebug.Breakpoint.breakNow(context.getPanel("html", true));
1867         return true;
1868     },
1869 
1870     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1871     // Mutation event handlers.
1872 
1873     onMutateAttr: function(event, context)
1874     {
1875         if (this.breakOnNextMutate(event, context, BP_BREAKONATTRCHANGE))
1876             return;
1877 
1878         var breakpoints = context.mutationBreakpoints;
1879         var self = this;
1880         breakpoints.enumerateBreakpoints(function(bp) {
1881             if (bp.checked && bp.node == event.target && bp.type == BP_BREAKONATTRCHANGE) {
1882                 self.breakWithCause(event, context, BP_BREAKONATTRCHANGE);
1883                 return true;
1884             }
1885         });
1886     },
1887 
1888     onMutateText: function(event, context)
1889     {
1890         if (this.breakOnNextMutate(event, context, BP_BREAKONTEXT))
1891             return;
1892     },
1893 
1894     onMutateNode: function(event, context)
1895     {
1896         var node = event.target;
1897         var removal = event.type == "DOMNodeRemoved";
1898 
1899         if (this.breakOnNextMutate(event, context, removal ? BP_BREAKONREMOVE : BP_BREAKONCHILDCHANGE))
1900             return;
1901 
1902         var breakpoints = context.mutationBreakpoints;
1903         var breaked = false;
1904 
1905         if (removal)
1906         {
1907             var self = this;
1908             breaked = breakpoints.enumerateBreakpoints(function(bp) {
1909                 if (bp.checked && bp.node == node && bp.type == BP_BREAKONREMOVE) {
1910                     self.breakWithCause(event, context, BP_BREAKONREMOVE);
1911                     return true;
1912                 }
1913             });
1914         }
1915 
1916         if (!breaked)
1917         {
1918             // Collect all parents of the mutated node.
1919             var parents = [];
1920             for (var parent = node.parentNode; parent; parent = parent.parentNode)
1921                 parents.push(parent);
1922 
1923             // Iterate over all parents and see if some of them has a breakpoint.
1924             var self = this;
1925             breakpoints.enumerateBreakpoints(function(bp) {
1926                 for (var i=0; i<parents.length; i++) {
1927                     if (bp.checked && bp.node == parents[i] && bp.type == BP_BREAKONCHILDCHANGE) {
1928                         self.breakWithCause(event, context, BP_BREAKONCHILDCHANGE);
1929                         return true;
1930                     }
1931                 }
1932             });
1933         }
1934 
1935         if (removal)
1936         {
1937             // Remove all breakpoints assocaited with removed node.
1938             var invalidate = false;
1939             breakpoints.enumerateBreakpoints(function(bp) {
1940                 if (bp.node == node) {
1941                     breakpoints.removeBreakpoint(bp);
1942                     invalidate = true;
1943                 }
1944             });
1945 
1946             if (invalidate)
1947                 context.invalidatePanels("breakpoints");
1948         }
1949     },
1950 
1951     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1952     // Context menu items
1953 
1954     getContextMenuItems: function(context, node, target, items)
1955     {
1956         if (!(node && node.nodeType == 1))
1957             return;
1958 
1959         var breakpoints = context.mutationBreakpoints;
1960 
1961         var attrBox = getAncestorByClass(target, "nodeAttr");
1962         if (getAncestorByClass(target, "nodeAttr"))
1963         {
1964         }
1965 
1966         if (!(nonEditableTags.hasOwnProperty(node.localName)))
1967         {
1968             items.push(
1969                 "-",
1970                 {label: "html.label.Break On Attribute Change",
1971                     type: "checkbox",
1972                     checked: breakpoints.findBreakpoint(node, BP_BREAKONATTRCHANGE),
1973                     command: bindFixed(this.onModifyBreakpoint, this, context, node,
1974                         BP_BREAKONATTRCHANGE)},
1975                 {label: "html.label.Break On Child Addition or Removal",
1976                     type: "checkbox",
1977                     checked: breakpoints.findBreakpoint(node, BP_BREAKONCHILDCHANGE),
1978                     command: bindFixed(this.onModifyBreakpoint, this, context, node,
1979                         BP_BREAKONCHILDCHANGE)},
1980                 {label: "html.label.Break On Element Removal",
1981                     type: "checkbox",
1982                     checked: breakpoints.findBreakpoint(node, BP_BREAKONREMOVE),
1983                     command: bindFixed(this.onModifyBreakpoint, this, context, node,
1984                         BP_BREAKONREMOVE)}
1985             );
1986         }
1987     },
1988 
1989     onModifyBreakpoint: function(context, node, type)
1990     {
1991         if (FBTrace.DBG_HTML)
1992             FBTrace.sysout("html.onModifyBreakpoint " + getElementXPath(node));
1993 
1994         var breakpoints = context.mutationBreakpoints;
1995         var bp = breakpoints.findBreakpoint(node, type);
1996 
1997         // Remove an existing or create new breakpoint.
1998         if (bp)
1999             breakpoints.removeBreakpoint(bp);
2000         else
2001             context.mutationBreakpoints.addBreakpoint(node, type);
2002     },
2003 };
2004 
2005 Firebug.HTMLModule.Breakpoint = function(node, type)
2006 {
2007     this.node = node;
2008     this.xpath = getElementXPath(node);
2009     this.checked = true;
2010     this.type = type;
2011 }
2012 
2013 Firebug.HTMLModule.BreakpointRep = domplate(Firebug.Rep,
2014 {
2015     inspectable: false,
2016 
2017     tag:
2018         DIV({"class": "breakpointRow focusRow", _repObject: "$bp",
2019             role: "option", "aria-checked": "$bp.checked"},
2020             DIV({"class": "breakpointBlockHead", onclick: "$onEnable"},
2021                 INPUT({"class": "breakpointCheckbox", type: "checkbox",
2022                     _checked: "$bp.checked", tabindex : "-1"}),
2023                 TAG("$bp.node|getNodeTag", {object: "$bp.node"}),
2024                 DIV({"class": "breakpointMutationType"}, "$bp|getChangeLabel"),
2025                 IMG({"class": "closeButton", src: "blank.gif", onclick: "$onRemove"})
2026             ),
2027             DIV({"class": "breakpointCode"},
2028                 TAG("$bp.node|getSourceLine", {object: "$bp.node"})
2029             )
2030         ),
2031 
2032     getNodeTag: function(node)
2033     {
2034         var rep = Firebug.getRep(node);
2035         return rep.shortTag ? rep.shortTag : rep.tag;
2036     },
2037 
2038     getSourceLine: function(node)
2039     {
2040         return getNodeTag(node, false);
2041     },
2042 
2043     getChangeLabel: function(bp)
2044     {
2045         switch (bp.type)
2046         {
2047         case BP_BREAKONATTRCHANGE:
2048             return $STR("html.label.Break On Attribute Change");
2049         case BP_BREAKONCHILDCHANGE:
2050             return $STR("html.label.Break On Child Addition or Removal");
2051         case BP_BREAKONREMOVE:
2052             return $STR("html.label.Break On Element Removal");
2053         case BP_BREAKONTEXT:
2054             return $STR("html.label.Break On Text Change");
2055         }
2056 
2057         return "";
2058     },
2059 
2060     onRemove: function(event)
2061     {
2062         cancelEvent(event);
2063 
2064         var bpPanel = Firebug.getElementPanel(event.target);
2065         var context = bpPanel.context;
2066         var htmlPanel = context.getPanel("html");
2067 
2068         if (hasClass(event.target, "closeButton"))
2069         {
2070             // Remove from list of breakpoints.
2071             var row = getAncestorByClass(event.target, "breakpointRow");
2072             context.mutationBreakpoints.removeBreakpoint(row.repObject);
2073 
2074             // Remove from the UI.
2075             bpPanel.noRefresh = true;
2076             bpPanel.removeRow(row);
2077             bpPanel.noRefresh = false;
2078         }
2079     },
2080 
2081     onEnable: function(event)
2082     {
2083         var checkBox = event.target;
2084         if (hasClass(checkBox, "breakpointCheckbox"))
2085         {
2086             var bp = getAncestorByClass(checkBox, "breakpointRow").repObject;
2087             bp.checked = checkBox.checked;
2088         }
2089     },
2090 
2091     supportsObject: function(object)
2092     {
2093         return object instanceof Firebug.HTMLModule.Breakpoint;
2094     }
2095 });
2096 
2097 // ************************************************************************************************
2098 
2099 function MutationBreakpointGroup()
2100 {
2101     this.breakpoints = [];
2102 }
2103 
2104 MutationBreakpointGroup.prototype = extend(new Firebug.Breakpoint.BreakpointGroup(),
2105 {
2106     name: "mutationBreakpoints",
2107     title: $STR("html.label.HTML Breakpoints"),
2108 
2109     addBreakpoint: function(node, type)
2110     {
2111         this.breakpoints.push(new Firebug.HTMLModule.Breakpoint(node, type));
2112     },
2113 
2114     matchBreakpoint: function(bp, args)
2115     {
2116         var node = args[0];
2117         var type = args[1];
2118         return (bp.node == node) && (!bp.type || bp.type == type);
2119     },
2120 
2121     removeBreakpoint: function(bp)
2122     {
2123         remove(this.breakpoints, bp);
2124     },
2125 
2126     // Persistence
2127     load: function(context)
2128     {
2129         var panelState = getPersistedState(context, "html");
2130         if (panelState.breakpoints)
2131             this.breakpoints = panelState.breakpoints;
2132 
2133         this.enumerateBreakpoints(function(bp)
2134         {
2135             var elts = getElementsByXPath(context.window.document, bp.xpath);
2136             bp.node = elts && elts.length ? elts[0] : null;
2137         });
2138     },
2139 
2140     store: function(context)
2141     {
2142         this.enumerateBreakpoints(function(bp)
2143         {
2144             bp.node = null;
2145         });
2146 
2147         var panelState = getPersistedState(context, "html");
2148         panelState.breakpoints = this.breakpoints;
2149     },
2150 });
2151 
2152 
2153 
2154 
2155 // ************************************************************************************************
2156 // Registration
2157 
2158 Firebug.registerPanel(Firebug.HTMLPanel);
2159 Firebug.registerModule(Firebug.HTMLModule);
2160 Firebug.registerRep(Firebug.HTMLModule.BreakpointRep);
2161 
2162 // ************************************************************************************************
2163 }});
2164