1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/domplate",
  7     "firebug/chrome/reps",
  8     "firebug/lib/locale",
  9     "firebug/html/htmlLib",
 10     "firebug/lib/events",
 11     "firebug/js/sourceLink",
 12     "firebug/lib/css",
 13     "firebug/lib/dom",
 14     "firebug/chrome/window",
 15     "firebug/lib/options",
 16     "firebug/lib/xpath",
 17     "firebug/lib/string",
 18     "firebug/lib/xml",
 19     "firebug/lib/array",
 20     "firebug/lib/persist",
 21     "firebug/chrome/menu",
 22     "firebug/lib/url",
 23     "firebug/css/cssModule",
 24     "firebug/css/cssReps",
 25     "firebug/js/breakpoint",
 26     "firebug/editor/editor",
 27     "firebug/chrome/searchBox",
 28     "firebug/html/insideOutBox",
 29     "firebug/html/inspector",
 30     "firebug/html/layout"
 31 ],
 32 function(Obj, Firebug, Domplate, FirebugReps, Locale, HTMLLib, Events,
 33     SourceLink, Css, Dom, Win, Options, Xpath, Str, Xml, Arr, Persist, Menu,
 34     Url, CSSModule, CSSInfoTip) {
 35 
 36 with (Domplate) {
 37 
 38 // ********************************************************************************************* //
 39 // Constants
 40 
 41 const Cc = Components.classes;
 42 const Ci = Components.interfaces;
 43 
 44 const MODIFICATION = window.MutationEvent.MODIFICATION;
 45 const ADDITION = window.MutationEvent.ADDITION;
 46 const REMOVAL = window.MutationEvent.REMOVAL;
 47 
 48 const BP_BREAKONATTRCHANGE = 1;
 49 const BP_BREAKONCHILDCHANGE = 2;
 50 const BP_BREAKONREMOVE = 3;
 51 const BP_BREAKONTEXT = 4;
 52 
 53 var KeyEvent = window.KeyEvent;
 54 
 55 // ********************************************************************************************* //
 56 
 57 Firebug.HTMLModule = Obj.extend(Firebug.Module,
 58 {
 59     dispatchName: "htmlModule",
 60 
 61     initialize: function(prefDomain, prefNames)
 62     {
 63         Firebug.Module.initialize.apply(this, arguments);
 64         Firebug.connection.addListener(this.DebuggerListener);
 65     },
 66 
 67     shutdown: function()
 68     {
 69         Firebug.Module.shutdown.apply(this, arguments);
 70         Firebug.connection.removeListener(this.DebuggerListener);
 71     },
 72 
 73     initContext: function(context, persistedState)
 74     {
 75         Firebug.Module.initContext.apply(this, arguments);
 76         context.mutationBreakpoints = new MutationBreakpointGroup();
 77     },
 78 
 79     loadedContext: function(context, persistedState)
 80     {
 81         context.mutationBreakpoints.load(context);
 82 
 83         // If there are mutation breakpoints, make sure the HTML panel
 84         // is automatically created and mutation listeners registered.
 85         // Mutation breakpoints should work even if the HTML panel has
 86         // never been selected by the user since the page load.
 87         if (!context.mutationBreakpoints.isEmpty())
 88         {
 89             var panel = context.getPanel("html");
 90             panel.registerMutationListeners();
 91         }
 92     },
 93 
 94     destroyContext: function(context, persistedState)
 95     {
 96         Firebug.Module.destroyContext.apply(this, arguments);
 97 
 98         context.mutationBreakpoints.store(context);
 99     },
100 
101     deleteNode: function(node, context)
102     {
103         Events.dispatch(this.fbListeners, "onBeginFirebugChange", [node, context]);
104         node.parentNode.removeChild(node);
105         Events.dispatch(this.fbListeners, "onEndFirebugChange", [node, context]);
106     },
107 
108     deleteAttribute: function(node, attr, context)
109     {
110         Events.dispatch(this.fbListeners, "onBeginFirebugChange", [node, context]);
111         node.removeAttribute(attr);
112         Events.dispatch(this.fbListeners, "onEndFirebugChange", [node, context]);
113     }
114 });
115 
116 // ********************************************************************************************* //
117 
118 Firebug.HTMLPanel = function() {};
119 
120 var WalkingPanel = Obj.extend(Firebug.Panel, HTMLLib.ElementWalkerFunctions);
121 
122 Firebug.HTMLPanel.prototype = Obj.extend(WalkingPanel,
123 {
124     inspectable: true,
125 
126     toggleEditing: function()
127     {
128         if (this.editing)
129             this.stopEditing();
130         else
131             this.editNode(this.selection);
132     },
133 
134     stopEditing: function()
135     {
136         Firebug.Editor.stopEditing();
137 
138         // After mutation listeners have made the element appear in the panel,
139         // re-select it (and also update the disable state of the "Edit" button).
140         this.context.delay(function()
141         {
142             this.select(this.selection, true);
143         }.bind(this));
144     },
145 
146     isEditing: function()
147     {
148         var editButton = Firebug.chrome.$("fbToggleHTMLEditing");
149         return (this.editing && editButton.getAttribute("checked") === "true");
150     },
151 
152     resetSearch: function()
153     {
154         delete this.lastSearch;
155     },
156 
157     select: function(object, forceUpdate, noEditChange)
158     {
159         if (!object)
160             object = this.getDefaultSelection();
161 
162         if (FBTrace.DBG_PANELS)
163         {
164             FBTrace.sysout("firebug.select " + this.name + " forceUpdate: " + forceUpdate + " " +
165                 object + ((object == this.selection) ? "==" : "!=") + this.selection);
166         }
167 
168         if (forceUpdate || object != this.selection)
169         {
170             this.selection = object;
171             this.updateSelection(object);
172 
173             // Update the Edit button to reflect editability of the selection.
174             // (Except during editing, when it should always be possible to click it.)
175             var editButton = Firebug.chrome.$("fbToggleHTMLEditing");
176             editButton.disabled = (this.selection && !this.isEditing() &&
177                 Css.nonEditableTags.hasOwnProperty(this.selection.localName));
178 
179             // Distribute selection change further to listeners.
180             Events.dispatch(Firebug.uiListeners, "onObjectSelected", [object, this]);
181 
182             // If the 'free text' edit mode is active change the current markup
183             // displayed in the editor (textarea) so that it corresponds to the current
184             // selection. This typically happens when the user clicks on object-status-path
185             // buttons in the toolbar.
186             // For the case when the selection is changed from within the editor, don't
187             // change the edited element.
188             if (this.isEditing() && !noEditChange)
189                 this.editNode(object);
190         }
191     },
192 
193     selectNext: function()
194     {
195         var objectBox = this.ioBox.createObjectBox(this.selection);
196         var next = this.ioBox.getNextObjectBox(objectBox);
197         if (next)
198         {
199             this.select(next.repObject);
200 
201             if (Firebug.Inspector.inspecting)
202                 Firebug.Inspector.inspectNode(next.repObject);
203         }
204     },
205 
206     selectPrevious: function()
207     {
208         var objectBox = this.ioBox.createObjectBox(this.selection);
209         var previous = this.ioBox.getPreviousObjectBox(objectBox);
210         if (previous)
211         {
212             this.select(previous.repObject);
213 
214             if (Firebug.Inspector.inspecting)
215                 Firebug.Inspector.inspectNode(previous.repObject);
216         }
217     },
218 
219     selectNodeBy: function(dir)
220     {
221         if (dir == "up")
222         {
223             this.selectPrevious();
224         }
225         else if (dir == "down")
226         {
227             this.selectNext();
228         }
229         else if (dir == "left")
230         {
231             var box = this.ioBox.createObjectBox(this.selection);
232             if (Css.hasClass(box, "open"))
233             {
234                 this.ioBox.contractObjectBox(box);
235             }
236             else
237             {
238                 var parentBox = this.ioBox.getParentObjectBox(box);
239                 if (parentBox && parentBox.repObject instanceof window.Element)
240                     this.select(parentBox.repObject);
241             }
242         }
243         else if (dir == "right")
244         {
245             var box = this.ioBox.createObjectBox(this.selection);
246             if (!Css.hasClass(box, "open"))
247                 this.ioBox.expandObject(this.selection);
248             else
249                 this.selectNext();
250         }
251 
252         Firebug.Inspector.highlightObject(this.selection, this.context);
253     },
254 
255     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
256 
257     editNewAttribute: function(elt)
258     {
259         var objectNodeBox = this.ioBox.findObjectBox(elt);
260         if (objectNodeBox)
261         {
262             var labelBox = objectNodeBox.querySelector("*> .nodeLabel > .nodeLabelBox");
263             var bracketBox = labelBox.querySelector("*> .nodeBracket");
264             Firebug.Editor.insertRow(bracketBox, "before");
265         }
266     },
267 
268     editAttribute: function(elt, attrName)
269     {
270         var objectNodeBox = this.ioBox.findObjectBox(elt);
271         if (objectNodeBox)
272         {
273             var attrBox = HTMLLib.findNodeAttrBox(objectNodeBox, attrName);
274             if (attrBox)
275             {
276                 var attrValueBox = attrBox.childNodes[3];
277                 var value = elt.getAttribute(attrName);
278                 Firebug.Editor.startEditing(attrValueBox, value);
279             }
280         }
281     },
282 
283     deleteAttribute: function(elt, attrName)
284     {
285         Firebug.HTMLModule.deleteAttribute(elt, attrName, this.context);
286     },
287 
288     localEditors:{}, // instantiated editor cache
289     editNode: function(node)
290     {
291         var objectNodeBox = this.ioBox.findObjectBox(node);
292         if (objectNodeBox)
293         {
294             var type = Xml.getElementType(node);
295             var editor = this.localEditors[type];
296             if (!editor)
297             {
298                 // look for special purpose editor (inserted by an extension),
299                 // otherwise use our html editor
300                 var specializedEditor = Firebug.HTMLPanel.Editors[type] ||
301                     Firebug.HTMLPanel.Editors["html"];
302                 editor = this.localEditors[type] = new specializedEditor(this.document);
303             }
304 
305             this.startEditingNode(node, objectNodeBox, editor, type);
306         }
307     },
308 
309     startEditingNode: function(node, box, editor, type)
310     {
311         switch (type)
312         {
313             case "html":
314             case "xhtml":
315                 this.startEditingHTMLNode(node, box, editor);
316                 break;
317             default:
318                 this.startEditingXMLNode(node, box, editor);
319         }
320     },
321 
322     startEditingXMLNode: function(node, box, editor)
323     {
324         var xml = Xml.getElementXML(node);
325         Firebug.Editor.startEditing(box, xml, editor);
326     },
327 
328     startEditingHTMLNode: function(node, box, editor)
329     {
330         if (Css.nonEditableTags.hasOwnProperty(node.localName))
331             return;
332 
333         editor.innerEditMode = node.localName in Css.innerEditableTags;
334 
335         var html = editor.innerEditMode ? node.innerHTML : Xml.getElementHTML(node);
336         html = Str.escapeForHtmlEditor(html);
337         Firebug.Editor.startEditing(box, html, editor);
338     },
339 
340     deleteNode: function(node, dir)
341     {
342         var box = this.ioBox.createObjectBox(node);
343         if (Css.hasClass(box, "open"))
344             this.ioBox.contractObjectBox(box);
345 
346         if (dir === "up")
347         {
348             // We want a "backspace"-like behavior, including traversing parents.
349             this.selectPrevious();
350         }
351         else
352         {
353             // Move to the next sibling if there is one, else backwards.
354             var nextSelection = this.ioBox.getNextSiblingObjectBox(box);
355             if (nextSelection)
356                 this.select(nextSelection.repObject);
357             else
358                 this.selectPrevious();
359         }
360 
361         Firebug.HTMLModule.deleteNode(node, this.context);
362 
363         Firebug.Inspector.highlightObject(this.selection, this.context);
364     },
365 
366     toggleAll: function(event, node)
367     {
368         var expandExternalContentNodes = Events.isShift(event);
369         this.ioBox.toggleObject(node, true, expandExternalContentNodes ?
370             null : ["link", "script", "style"]);
371     },
372 
373     updateNodeVisibility: function(node)
374     {
375         var wasHidden = node.classList.contains("nodeHidden");
376         if (!Xml.isVisible(node.repObject))
377         {
378             // Hide this node and, through CSS, every descendant.
379             node.classList.add("nodeHidden");
380         }
381         else if (wasHidden)
382         {
383             // The node has changed state from hidden to shown. While in the
384             // hidden state, some descendants may have been explicitly marked
385             // with .nodeHidden (not just through CSS inheritance), so we need
386             // to recheck the visibility of those.
387             node.classList.remove("nodeHidden");
388             var desc = Arr.cloneArray(node.getElementsByClassName("nodeHidden"));
389             for (var i = 0; i < desc.length; ++i)
390             {
391                 if (Xml.isVisible(desc[i].repObject))
392                     desc[i].classList.remove("nodeHidden");
393             }
394         }
395     },
396 
397     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
398 
399     getElementSourceText: function(node)
400     {
401         if (this.sourceElements)
402         {
403             var index = this.sourceElementNodes.indexOf(node);
404             if (index != -1)
405                 return this.sourceElements[index];
406         }
407 
408         var lines;
409 
410         var url = HTMLLib.getSourceHref(node);
411         if (url)
412         {
413             lines = this.context.sourceCache.load(url);
414         }
415         else
416         {
417             var text = HTMLLib.getSourceText(node);
418             lines = Str.splitLines(text);
419         }
420 
421         var sourceElt = new Firebug.HTMLModule.SourceText(lines, node);
422 
423         if (!this.sourceElements)
424         {
425             this.sourceElements =  [sourceElt];
426             this.sourceElementNodes = [node];
427         }
428         else
429         {
430             this.sourceElements.push(sourceElt);
431             this.sourceElementNodes.push(node);
432         }
433 
434         return sourceElt;
435     },
436 
437     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
438 
439     registerMutationListeners: function(win)
440     {
441         // The 'attachedMutation' flag should be maintained per window. Otherwise
442         // we can miss some registration. Events.addEventListener is safe for multiple
443         // calls so, let's remove the condition for now as part of issue 5761 fix.
444         // This should be improved together with issue 5490
445         //if (this.context.attachedMutation)
446         //    return;
447 
448         this.context.attachedMutation = true;
449 
450         var self = this;
451         function addListeners(win)
452         {
453             var doc = win.document;
454 
455             // xxxHonza: an iframe doesn't have to be loaded yet, so do not
456             // register mutation elements in such cases since they wouldn't
457             // be removed.
458             // The listeners can be registered later in watchWindowDelayed,
459             // but it's also risky. Mutation listeners should be registered
460             // at the moment when it's clear that the window/frame has been
461             // loaded.
462 
463             // This break HTML panel for about:blank pages (see issue 5120).
464             //if (doc.location == "about:blank")
465             //    return;
466 
467             Events.addEventListener(doc, "DOMAttrModified", self.onMutateAttr, false);
468             Events.addEventListener(doc, "DOMCharacterDataModified", self.onMutateText, false);
469             Events.addEventListener(doc, "DOMNodeInserted", self.onMutateNode, false);
470             Events.addEventListener(doc, "DOMNodeRemoved", self.onMutateNode, false);
471         }
472 
473         // If a window is specified use it, otherwise register listeners for all
474         // context windows (including the main window and all embedded iframes).
475         if (win)
476             addListeners(win);
477         else
478             Win.iterateWindows(this.context.window, addListeners);
479     },
480 
481     unregisterMutationListeners: function(win)
482     {
483         var self = this;
484         function removeListeners(win)
485         {
486             var doc = win.document;
487             Events.removeEventListener(doc, "DOMAttrModified", self.onMutateAttr, false);
488             Events.removeEventListener(doc, "DOMCharacterDataModified", self.onMutateText, false);
489             Events.removeEventListener(doc, "DOMNodeInserted", self.onMutateNode, false);
490             Events.removeEventListener(doc, "DOMNodeRemoved", self.onMutateNode, false);
491         }
492 
493         if (win)
494             removeListeners(win);
495         else
496             Win.iterateWindows(this.context.window, removeListeners);
497     },
498 
499     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
500 
501     mutateAttr: function(target, attrChange, attrName, attrValue)
502     {
503         // Every time the user scrolls we get this pointless mutation event, which
504         // is only bad for performance
505         if (attrName == "curpos")
506             return;
507 
508         // Due to the delay call this may or may not exist in the tree anymore
509         if (!this.ioBox.isInExistingRoot(target))
510         {
511             if (FBTrace.DBG_HTML)
512                 FBTrace.sysout("mutateAttr: different tree " + target, target);
513             return;
514         }
515 
516         if (FBTrace.DBG_HTML)
517         {
518             FBTrace.sysout("html.mutateAttr target:"+target+" attrChange:"+attrChange+
519                 " attrName:"+attrName+" attrValue: "+attrValue, target);
520         }
521 
522         this.markChange();
523 
524         var objectNodeBox = Firebug.scrollToMutations || Firebug.expandMutations ?
525             this.ioBox.createObjectBox(target) : this.ioBox.findObjectBox(target);
526 
527         if (!objectNodeBox)
528             return;
529 
530         this.updateNodeVisibility(objectNodeBox);
531 
532         if (attrChange == MODIFICATION || attrChange == ADDITION)
533         {
534             var nodeAttr = HTMLLib.findNodeAttrBox(objectNodeBox, attrName);
535 
536             if (FBTrace.DBG_HTML)
537                 FBTrace.sysout("mutateAttr " + attrChange + " " + attrName + "=" + attrValue +
538                     " node: " + nodeAttr, nodeAttr);
539 
540             if (nodeAttr && nodeAttr.childNodes.length > 3)
541             {
542                 var attrValueBox = nodeAttr.querySelector("*> .nodeValue");
543                 var attrValueText = attrValueBox.firstChild;
544                 if (attrValueText)
545                     attrValueText.nodeValue = attrValue;
546                 else
547                     attrValueBox.innerHTML = Str.escapeForTextNode(attrValue);
548 
549                 this.highlightMutation(attrValueBox, objectNodeBox, "mutated");
550             }
551             else
552             {
553                 var attr = target.getAttributeNode(attrName);
554 
555                 if (FBTrace.DBG_HTML)
556                     FBTrace.sysout("mutateAttr getAttributeNode " + attrChange + " " + attrName +
557                         "=" + attrValue + " node: " + attr, attr);
558 
559                 if (attr)
560                 {
561                     var nodeAttr = Firebug.HTMLPanel.AttrNode.tag.replace({attr: attr},
562                         this.document);
563 
564                     var labelBox = objectNodeBox.querySelector("*> .nodeLabel > .nodeLabelBox");
565                     var bracketBox = labelBox.querySelector("*> .nodeBracket");
566                     labelBox.insertBefore(nodeAttr, bracketBox);
567 
568                     this.highlightMutation(nodeAttr, objectNodeBox, "mutated");
569                 }
570             }
571         }
572         else if (attrChange == REMOVAL)
573         {
574             var nodeAttr = HTMLLib.findNodeAttrBox(objectNodeBox, attrName);
575             if (nodeAttr)
576                 nodeAttr.parentNode.removeChild(nodeAttr);
577 
578             // We want to highlight regardless as the domplate may have been
579             // generated after the attribute was removed from the node
580             this.highlightMutation(objectNodeBox, objectNodeBox, "mutated");
581         }
582 
583         Firebug.Inspector.repaint();
584     },
585 
586     mutateText: function(target, parent, textValue)
587     {
588         // Due to the delay call this may or may not exist in the tree anymore
589         if (!this.ioBox.isInExistingRoot(target))
590         {
591             if (FBTrace.DBG_HTML)
592                 FBTrace.sysout("mutateText: different tree " + target, target);
593             return;
594         }
595 
596         this.markChange();
597 
598         var parentNodeBox = Firebug.scrollToMutations || Firebug.expandMutations ?
599             this.ioBox.createObjectBox(parent) : this.ioBox.findObjectBox(parent);
600 
601         if (!parentNodeBox)
602         {
603             if (FBTrace.DBG_HTML)
604                 FBTrace.sysout("html.mutateText failed to update text, parent node " +
605                     "box does not exist");
606             return;
607         }
608 
609         if (!Firebug.showFullTextNodes)
610             textValue = Str.cropMultipleLines(textValue);
611 
612         var parentTag = getNodeBoxTag(parentNodeBox);
613         if (parentTag == Firebug.HTMLPanel.TextElement.tag)
614         {
615             if (FBTrace.DBG_HTML)
616                 FBTrace.sysout("html.mutateText target: " + target + " parent: " + parent);
617 
618             // Rerender the entire parentNodeBox. Proper entity-display logic will
619             // be automatically applied according to the preferences.
620             var newParentNodeBox = parentTag.replace({object: parentNodeBox.repObject}, this.document);
621             if (parentNodeBox.parentNode)
622                 parentNodeBox.parentNode.replaceChild(newParentNodeBox, parentNodeBox);
623 
624             // Reselect if the element was selected before.
625             if (this.selection && (!this.selection.parentNode || parent == this.selection))
626                 this.ioBox.select(parent, true);
627 
628             var nodeText = HTMLLib.getTextElementTextBox(newParentNodeBox);
629             if (!nodeText.firstChild)
630             {
631                 if (FBTrace.DBG_HTML)
632                 {
633                     FBTrace.sysout("html.mutateText failed to update text, " +
634                         "TextElement firstChild does not exist");
635                 }
636                 return;
637             }
638 
639             // Highlight the text box only (not the entire parentNodeBox/element).
640             this.highlightMutation(nodeText, newParentNodeBox, "mutated");
641         }
642         else
643         {
644             var childBox = this.ioBox.getChildObjectBox(parentNodeBox);
645             if (!childBox)
646             {
647                 if (FBTrace.DBG_HTML)
648                 {
649                     FBTrace.sysout("html.mutateText failed to update text, " +
650                         "no child object box found");
651                 }
652                 return;
653             }
654 
655             var textNodeBox = this.ioBox.findChildObjectBox(childBox, target);
656             if (textNodeBox)
657             {
658                 // structure for comment and cdata. Are there others?
659                 textNodeBox.firstChild.firstChild.nodeValue = textValue;
660 
661                 this.highlightMutation(textNodeBox, parentNodeBox, "mutated");
662             }
663             else if (Firebug.scrollToMutations || Firebug.expandMutations)
664             {
665                 // We are not currently rendered but we are set to highlight
666                 var objectBox = this.ioBox.createObjectBox(target);
667                 this.highlightMutation(objectBox, objectBox, "mutated");
668             }
669         }
670     },
671 
672     mutateNode: function(target, parent, nextSibling, removal)
673     {
674         if (FBTrace.DBG_HTML)
675             FBTrace.sysout("html.mutateNode target:" + target + " parent:" + parent +
676                 (removal ? "REMOVE" : ""));
677 
678         // Due to the delay call this may or may not exist in the tree anymore
679         if (!removal && !this.ioBox.isInExistingRoot(target))
680         {
681             if (FBTrace.DBG_HTML)
682                 FBTrace.sysout("mutateNode: different tree " + target, target);
683             return;
684         }
685 
686         this.markChange();  // This invalidates the panels for every mutate
687 
688         var parentNodeBox = Firebug.scrollToMutations || Firebug.expandMutations
689             ? this.ioBox.createObjectBox(parent)
690             : this.ioBox.findObjectBox(parent);
691 
692         if (FBTrace.DBG_HTML)
693             FBTrace.sysout("html.mutateNode parent:" + parent + " parentNodeBox:" +
694                 parentNodeBox);
695 
696         if (!parentNodeBox)
697             return;
698 
699         if (!Firebug.showTextNodesWithWhitespace && this.isWhitespaceText(target))
700             return;
701 
702         // target is only whitespace
703 
704         var newParentTag = getNodeTag(parent);
705         var oldParentTag = getNodeBoxTag(parentNodeBox);
706 
707         if (newParentTag == oldParentTag)
708         {
709             if (parentNodeBox.populated)
710             {
711                 if (removal)
712                 {
713                     this.ioBox.removeChildBox(parentNodeBox, target);
714 
715                     // Special case for docType.
716                     if (target instanceof HTMLHtmlElement)
717                         this.ioBox.removeChildBox(parentNodeBox, target.parentNode.doctype);
718 
719                     this.highlightMutation(parentNodeBox, parentNodeBox, "mutated");
720                 }
721                 else
722                 {
723                     var childBox = this.ioBox.getChildObjectBox(parentNodeBox);
724 
725                     var comments = Firebug.showCommentNodes;
726                     var whitespaces = Firebug.showTextNodesWithWhitespace;
727 
728                     // Get the right next sibling that match following criteria:
729                     // 1) It's not a whitespace text node in case 'show whitespaces' is false.
730                     // 2) It's not a comment in case 'show comments' is false.
731                     // 3) There is a child box already created for it in the HTML panel UI.
732                     // The new node will then be inserted before that sibling's child box, or
733                     // appended at the end (issue 5255).
734                     while (nextSibling && (
735                        (!whitespaces && HTMLLib.isWhitespaceText(nextSibling)) ||
736                        (!comments && nextSibling instanceof window.Comment) ||
737                        (!this.ioBox.findChildObjectBox(childBox, nextSibling))))
738                     {
739                        nextSibling = this.findNextSibling(nextSibling);
740                     }
741 
742                     var objectBox = nextSibling ?
743                         this.ioBox.insertChildBoxBefore(parentNodeBox, target, nextSibling) :
744                         this.ioBox.appendChildBox(parentNodeBox, target);
745 
746                     // Special case for docType.
747                     if (target instanceof HTMLHtmlElement)
748                     {
749                         this.ioBox.insertChildBoxBefore(parentNodeBox,
750                             target.parentNode.doctype, target);
751                     }
752 
753                     this.highlightMutation(objectBox, objectBox, "mutated");
754                 }
755             }
756             else // !parentNodeBox.populated
757             {
758                 var newParentNodeBox = newParentTag.replace({object: parent}, this.document);
759                 parentNodeBox.parentNode.replaceChild(newParentNodeBox, parentNodeBox);
760 
761                 if (this.selection && (!this.selection.parentNode || parent == this.selection))
762                     this.ioBox.select(parent, true);
763 
764                 this.highlightMutation(newParentNodeBox, newParentNodeBox, "mutated");
765 
766                 if (!removal && (Firebug.scrollToMutations || Firebug.expandMutations))
767                 {
768                     var objectBox = this.ioBox.createObjectBox(target);
769                     this.highlightMutation(objectBox, objectBox, "mutated");
770                 }
771             }
772         }
773         else // newParentTag != oldParentTag
774         {
775             var newParentNodeBox = newParentTag.replace({object: parent}, this.document);
776             if (parentNodeBox.parentNode)
777                 parentNodeBox.parentNode.replaceChild(newParentNodeBox, parentNodeBox);
778 
779             if (Css.hasClass(parentNodeBox, "open"))
780                 this.ioBox.toggleObjectBox(newParentNodeBox, true);
781 
782             if (this.selection && (!this.selection.parentNode || parent == this.selection))
783                 this.ioBox.select(parent, true);
784 
785             this.highlightMutation(newParentNodeBox, newParentNodeBox, "mutated");
786 
787             if (!removal && (Firebug.scrollToMutations || Firebug.expandMutations))
788             {
789                 var objectBox = this.ioBox.createObjectBox(target);
790                 this.highlightMutation(objectBox, objectBox, "mutated");
791             }
792         }
793     },
794 
795     highlightMutation: function(elt, objectBox, type)
796     {
797         if (FBTrace.DBG_HTML)
798             FBTrace.sysout("html.highlightMutation Firebug.highlightMutations:" +
799                 Firebug.highlightMutations, {elt: elt, objectBox: objectBox, type: type});
800 
801         if (!elt)
802             return;
803 
804         if (Firebug.scrollToMutations || Firebug.expandMutations)
805         {
806             if (this.context.mutationTimeout)
807             {
808                 this.context.clearTimeout(this.context.mutationTimeout);
809                 delete this.context.mutationTimeout;
810             }
811 
812             var ioBox = this.ioBox;
813             var panelNode = this.panelNode;
814 
815             this.context.mutationTimeout = this.context.setTimeout(function()
816             {
817                 ioBox.openObjectBox(objectBox);
818 
819                 if (Firebug.scrollToMutations)
820                     Dom.scrollIntoCenterView(objectBox, panelNode);
821             }, 200);
822         }
823 
824         if (Firebug.highlightMutations)
825             Css.setClassTimed(elt, type, this.context);
826     },
827 
828     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
829     // InsideOutBoxView implementation
830 
831     createObjectBox: function(object, isRoot)
832     {
833         if (FBTrace.DBG_HTML)
834         {
835             FBTrace.sysout("html.createObjectBox(" + Css.getElementCSSSelector(object) +
836                 ", isRoot:" + (isRoot? "true" : "false")+")");
837         }
838 
839         var tag = getNodeTag(object);
840         if (tag)
841             return tag.replace({object: object}, this.document);
842     },
843 
844     getParentObject: function(node)
845     {
846         if (node instanceof Firebug.HTMLModule.SourceText)
847             return node.owner;
848 
849         var parentNode = this.getParentNode(node);
850 
851         // for chromebug to avoid climbing out to browser.xul
852         if (node.nodeName == "#document")
853             return null;
854 
855         //if (FBTrace.DBG_HTML)
856         //    FBTrace.sysout("html.getParentObject for "+node.nodeName+" parentNode:"+
857         //        Css.getElementCSSSelector(parentNode));
858 
859         if (parentNode)
860         {
861             if (parentNode.nodeType == Node.DOCUMENT_NODE)
862             {
863                 if (parentNode.defaultView)
864                 {
865                     if (parentNode.defaultView == this.context.window)
866                         return parentNode;
867 
868                     if (FBTrace.DBG_HTML)
869                     {
870                         FBTrace.sysout("getParentObject; node is document node"+
871                             ", frameElement:" + parentNode.defaultView.frameElement);
872                     }
873 
874                     return parentNode.defaultView.frameElement;
875                 }
876                 else
877                 {
878                     var skipParent = this.getEmbedConnection(parentNode);
879                     if (FBTrace.DBG_HTML)
880                         FBTrace.sysout("getParentObject skipParent:" +
881                             (skipParent ? skipParent.nodeName : "none"));
882 
883                     if (skipParent)
884                         return skipParent;
885                     else
886                         return null; // parent is document element, but no window at defaultView.
887                 }
888             }
889             else if (!parentNode.localName)
890             {
891                 if (FBTrace.DBG_HTML)
892                     FBTrace.sysout("getParentObject: null localName must be window, no parentObject");
893                 return null;
894             }
895             else
896             {
897                 return parentNode;
898             }
899         }
900         else
901         {
902             // Documents have no parentNode; Attr, Document, DocumentFragment, Entity,
903             // and Notation. top level windows have no parentNode
904             if (node && node.nodeType == Node.DOCUMENT_NODE)
905             {
906                 // generally a reference to the window object for the document, however
907                 // that is not defined in the specification
908                 if (node.defaultView)
909                 {
910                     var embeddingFrame = node.defaultView.frameElement;
911                     if (embeddingFrame)
912                         return embeddingFrame.contentDocument;
913                 }
914                 else
915                 {
916                     // a Document object without a parentNode or window
917                     return null;  // top level has no parent
918                 }
919             }
920         }
921     },
922 
923     setEmbedConnection: function(node, skipChild)
924     {
925         if (!this.embeddedBrowserParents)
926         {
927             this.embeddedBrowserParents = [];
928             this.embeddedBrowserDocument = [];
929         }
930 
931         this.embeddedBrowserDocument.push(skipChild);
932 
933         // store our adopted child in a side table
934         this.embeddedBrowserParents.push(node);
935 
936         if (FBTrace.DBG_HTML)
937             FBTrace.sysout("Found skipChild " + Css.getElementCSSSelector(skipChild) +
938                 " for  " + Css.getElementCSSSelector(node) + " with node.contentDocument " +
939                 node.contentDocument);
940 
941         return skipChild;
942     },
943 
944     getEmbedConnection: function(node)
945     {
946         if (this.embeddedBrowserParents)
947         {
948             var index = this.embeddedBrowserParents.indexOf(node);
949             if (index !== -1)
950                 return this.embeddedBrowserDocument[index];
951         }
952     },
953 
954     /**
955      * @param: node a DOM node from the Web page
956      * @param: index counter for important children, may skip whitespace
957      * @param: previousSibling a node from the web page
958      */
959     getChildObject: function(node, index, previousSibling)
960     {
961         if (!node)
962         {
963             FBTrace.sysout("getChildObject: null node");
964             return;
965         }
966 
967         if (FBTrace.DBG_HTML)
968             FBTrace.sysout("getChildObject " + node.tagName + " index " + index +
969                 " previousSibling: " +
970                 (previousSibling ? Css.getElementCSSSelector(previousSibling) : "null"),
971                 {node: node, previousSibling:previousSibling});
972 
973         if (this.isSourceElement(node))
974         {
975             if (index == 0)
976                 return this.getElementSourceText(node);
977             else
978                 return null;  // no siblings of source elements
979         }
980         else if (node instanceof window.Document)
981         {
982             if (previousSibling !== null)
983                 return this.getNextSibling(previousSibling);
984             else
985                 return this.getFirstChild(node);
986         }
987         else if (node.contentDocument)  // then the node is a frame
988         {
989             if (index == 0)
990             {
991                 // punch thru and adopt the document node as our child
992                 var skipChild = node.contentDocument.firstChild;
993 
994                 // (the node's).(type 9 document).(HTMLElement)
995                 return this.setEmbedConnection(node, skipChild);
996             }
997             else if (previousSibling)
998             {
999                 // Next child of a document (after doc-type) is <html>.
1000                 return this.getNextSibling(previousSibling);
1001             }
1002         }
1003         else if (node.getSVGDocument && node.getSVGDocument())  // then the node is a frame
1004         {
1005             if (index == 0)
1006             {
1007                 var skipChild = node.getSVGDocument().documentElement; // unwrap
1008 
1009                 // (the node's).(type 9 document).(HTMLElement)
1010                 return this.setEmbedConnection(node, skipChild);
1011             }
1012             else
1013             {
1014                 return null;
1015             }
1016         }
1017 
1018         var child;
1019         if (previousSibling)  // then we are walking
1020             child = this.getNextSibling(previousSibling);  // may return null, meaning done with iteration.
1021         else
1022             child = this.getFirstChild(node); // child is set to at the beginning of an iteration.
1023 
1024         if (FBTrace.DBG_HTML)
1025             FBTrace.sysout("getChildObject firstChild " + Css.getElementCSSSelector(child) +
1026                 " with Firebug.showTextNodesWithWhitespace " +
1027                 Firebug.showTextNodesWithWhitespace);
1028 
1029         if (Firebug.showTextNodesWithWhitespace)  // then the index is true to the node list
1030         {
1031             return child;
1032         }
1033         else
1034         {
1035             for (; child; child = this.getNextSibling(child))
1036             {
1037                 if (!this.isWhitespaceText(child))
1038                     return child;
1039             }
1040         }
1041 
1042         return null;  // we have no children worth showing.
1043     },
1044 
1045     isWhitespaceText: function(node)
1046     {
1047         return HTMLLib.isWhitespaceText(node);
1048     },
1049 
1050     findNextSibling: function (node)
1051     {
1052         return HTMLLib.findNextSibling(node);
1053     },
1054 
1055     isSourceElement: function(element)
1056     {
1057         return HTMLLib.isSourceElement(element);
1058     },
1059 
1060     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1061     // Events
1062 
1063     onMutateAttr: function(event)
1064     {
1065         var target = event.target;
1066         if (Firebug.shouldIgnore(target))
1067             return;
1068 
1069         var attrChange = event.attrChange;
1070         var attrName = event.attrName;
1071         var newValue = event.newValue;
1072 
1073         this.context.delay(function()
1074         {
1075             this.mutateAttr(target, attrChange, attrName, newValue);
1076         }, this);
1077 
1078         Firebug.HTMLModule.MutationBreakpoints.onMutateAttr(event, this.context);
1079     },
1080 
1081     onMutateText: function(event)
1082     {
1083         if (FBTrace.DBG_HTML)
1084             FBTrace.sysout("html.onMutateText; ", event);
1085 
1086         var target = event.target;
1087         var parent = target.parentNode;
1088 
1089         var newValue = event.newValue;
1090 
1091         this.context.delay(function()
1092         {
1093             this.mutateText(target, parent, newValue);
1094         }, this);
1095 
1096         Firebug.HTMLModule.MutationBreakpoints.onMutateText(event, this.context);
1097     },
1098 
1099     onMutateNode: function(event)
1100     {
1101         var target = event.target;
1102         if (Firebug.shouldIgnore(target))
1103             return;
1104 
1105         var parent = event.relatedNode;
1106         var removal = event.type == "DOMNodeRemoved";
1107         var nextSibling = removal ? null : this.findNextSibling(target);
1108 
1109         this.context.delay(function()
1110         {
1111             try
1112             {
1113                  this.mutateNode(target, parent, nextSibling, removal);
1114             }
1115             catch (exc)
1116             {
1117                 if (FBTrace.DBG_ERRORS && FBTrace.DBG_HTML)
1118                     FBTrace.sysout("html.onMutateNode FAILS:", exc);
1119             }
1120         }, this);
1121 
1122         Firebug.HTMLModule.MutationBreakpoints.onMutateNode(event, this.context);
1123     },
1124 
1125     onClick: function(event)
1126     {
1127         if (Events.isLeftClick(event) && Events.isDoubleClick(event))
1128         {
1129             // The double-click (detail == 2) expands an HTML element, but the user must click
1130             // on the element itself not on the twisty.
1131             // The logic should be as follow:
1132             // - click on the twisty expands/collapses the element
1133             // - double click on the element name expands/collapses it
1134             // - click on the element name selects it
1135             if (!Css.hasClass(event.target, "twisty") && !Css.hasClass(event.target, "nodeLabel"))
1136                 this.toggleNode(event);
1137         }
1138         else if (Events.isAltClick(event) && Events.isDoubleClick(event) && !this.editing)
1139         {
1140             var node = Firebug.getRepObject(event.target);
1141             this.editNode(node);
1142         }
1143     },
1144 
1145     onMouseDown: function(event)
1146     {
1147         if (!Events.isLeftClick(event))
1148             return;
1149 
1150         if (Dom.getAncestorByClass(event.target, "nodeTag"))
1151         {
1152             var node = Firebug.getRepObject(event.target);
1153             this.noScrollIntoView = true;
1154             this.select(node);
1155 
1156             delete this.noScrollIntoView;
1157 
1158             if (Css.hasClass(event.target, "twisty"))
1159                 this.toggleNode(event);
1160         }
1161     },
1162 
1163     toggleNode: function(event)
1164     {
1165         var node = Firebug.getRepObject(event.target);
1166         var box = this.ioBox.createObjectBox(node);
1167         if (!Css.hasClass(box, "open"))
1168             this.ioBox.expandObject(node);
1169         else
1170             this.ioBox.contractObject(this.selection);
1171     },
1172 
1173     onKeyPress: function(event)
1174     {
1175         if (this.editing)
1176             return;
1177 
1178         var node = this.selection;
1179         if (!node)
1180             return;
1181 
1182         // * expands the node with all its children
1183         // + expands the node
1184         // - collapses the node
1185         var ch = String.fromCharCode(event.charCode);
1186         if (ch == "*")
1187             this.toggleAll(event, node);
1188 
1189         if (!Events.noKeyModifiers(event))
1190           return;
1191 
1192         if (ch == "+")
1193             this.ioBox.expandObject(node);
1194         else if (ch == "-")
1195             this.ioBox.contractObject(node);
1196 
1197         if (event.keyCode == KeyEvent.DOM_VK_UP)
1198             this.selectNodeBy("up");
1199         else if (event.keyCode == KeyEvent.DOM_VK_DOWN)
1200             this.selectNodeBy("down");
1201         else if (event.keyCode == KeyEvent.DOM_VK_LEFT)
1202             this.selectNodeBy("left");
1203         else if (event.keyCode == KeyEvent.DOM_VK_RIGHT)
1204             this.selectNodeBy("right");
1205         else if (event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
1206         {
1207             if (!Css.nonDeletableTags.hasOwnProperty(node.localName))
1208                 this.deleteNode(node, "up");
1209         }
1210         else if (event.keyCode == KeyEvent.DOM_VK_DELETE)
1211         {
1212             if (!Css.nonDeletableTags.hasOwnProperty(node.localName))
1213                 this.deleteNode(node, "down");
1214         }
1215         else
1216             return;
1217 
1218         Events.cancelEvent(event);
1219     },
1220 
1221     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1222     // CSS Listener
1223 
1224     updateVisibilitiesForSelectorInSheet: function(sheet, selector)
1225     {
1226         if (!selector)
1227             return;
1228         var doc = (sheet && sheet.ownerNode && sheet.ownerNode.ownerDocument);
1229         if (!doc)
1230             return;
1231 
1232         var affected = doc.querySelectorAll(selector);
1233         if (!affected.length || !this.ioBox.isInExistingRoot(affected[0]))
1234             return;
1235 
1236         for (var i = 0; i < affected.length; ++i)
1237         {
1238             var node = this.ioBox.findObjectBox(affected[i]);
1239             if (node)
1240                 this.updateNodeVisibility(node);
1241         }
1242     },
1243 
1244     updateVisibilitiesForRule: function(rule)
1245     {
1246         this.updateVisibilitiesForSelectorInSheet(rule.parentStyleSheet, rule.selectorText);
1247     },
1248 
1249     cssPropAffectsVisibility: function(propName)
1250     {
1251         // Pretend that "display" is the only property which affects visibility,
1252         // which is a half-truth. We could make this more technically correct
1253         // by unconditionally returning true, but forcing a synchronous reflow
1254         // and computing offsetWidth/Height on up to every element on the page
1255         // isn't worth it.
1256         return (propName === "display");
1257     },
1258 
1259     cssTextAffectsVisibility: function(cssText)
1260     {
1261         return (cssText.indexOf("display:") !== -1);
1262     },
1263 
1264     onAfterCSSDeleteRule: function(styleSheet, cssText, selector)
1265     {
1266         if (this.cssTextAffectsVisibility(cssText))
1267             this.updateVisibilitiesForSelectorInSheet(styleSheet, selector);
1268     },
1269 
1270     onCSSInsertRule: function(styleSheet, cssText, ruleIndex)
1271     {
1272         if (this.cssTextAffectsVisibility(cssText))
1273             this.updateVisibilitiesForRule(styleSheet.cssRules[ruleIndex]);
1274     },
1275 
1276     onCSSSetProperty: function(style, propName, propValue, propPriority, prevValue,
1277         prevPriority, rule, baseText)
1278     {
1279         if (this.cssPropAffectsVisibility(propName))
1280             this.updateVisibilitiesForRule(rule);
1281     },
1282 
1283     onCSSRemoveProperty: function(style, propName, prevValue, prevPriority, rule, baseText)
1284     {
1285         if (this.cssPropAffectsVisibility(propName))
1286             this.updateVisibilitiesForRule(rule);
1287     },
1288 
1289     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1290     // extends Panel
1291 
1292     name: "html",
1293     searchable: true,
1294     breakable: true,
1295     dependents: ["css", "computed", "layout", "dom", "domSide", "watch"],
1296     inspectorHistory: new Array(5),
1297     enableA11y: true,
1298     order: 20,
1299 
1300     initialize: function()
1301     {
1302         this.onMutateText = Obj.bind(this.onMutateText, this);
1303         this.onMutateAttr = Obj.bind(this.onMutateAttr, this);
1304         this.onMutateNode = Obj.bind(this.onMutateNode, this);
1305         this.onClick = Obj.bind(this.onClick, this);
1306         this.onMouseDown = Obj.bind(this.onMouseDown, this);
1307         this.onKeyPress = Obj.bind(this.onKeyPress, this);
1308 
1309         Firebug.Panel.initialize.apply(this, arguments);
1310         Firebug.CSSModule.addListener(this);
1311     },
1312 
1313     destroy: function(state)
1314     {
1315         Persist.persistObjects(this, state);
1316 
1317         Firebug.Panel.destroy.apply(this, arguments);
1318 
1319         delete this.embeddedBrowserParents;
1320         delete this.embeddedBrowserDocument;
1321 
1322         // xxxHonza: I don't know why this helps, but it helps to release the
1323         // page compartment (at least by observing about:memory);
1324         // Note that inspectorHistory holds references to page elements.
1325         for (var i=0; i<this.inspectorHistory.length; i++)
1326             delete this.inspectorHistory[i];
1327         delete this.inspectorHistory;
1328 
1329         Firebug.CSSModule.removeListener(this);
1330         this.unregisterMutationListeners();
1331     },
1332 
1333     initializeNode: function(oldPanelNode)
1334     {
1335         if (!this.ioBox)
1336             this.ioBox = new Firebug.InsideOutBox(this, this.panelNode);
1337 
1338         Events.addEventListener(this.panelNode, "click", this.onClick, false);
1339         Events.addEventListener(this.panelNode, "mousedown", this.onMouseDown, false);
1340 
1341         Firebug.Panel.initializeNode.apply(this, arguments);
1342     },
1343 
1344     destroyNode: function()
1345     {
1346         Events.removeEventListener(this.panelNode, "click", this.onClick, false);
1347         Events.removeEventListener(this.panelNode, "mousedown", this.onMouseDown, false);
1348 
1349         Events.removeEventListener(this.panelNode.ownerDocument, "keypress",
1350             this.onKeyPress, true);
1351 
1352         if (this.ioBox)
1353         {
1354             this.ioBox.destroy();
1355             delete this.ioBox;
1356         }
1357 
1358         Firebug.Panel.destroyNode.apply(this, arguments);
1359     },
1360 
1361     show: function(state)
1362     {
1363         this.showToolbarButtons("fbHTMLButtons", true);
1364         this.showToolbarButtons("fbStatusButtons", true);
1365 
1366         Events.addEventListener(this.panelNode.ownerDocument, "keypress", this.onKeyPress, true);
1367 
1368         if (this.context.loaded)
1369         {
1370             this.registerMutationListeners();
1371 
1372             Persist.restoreObjects(this, state);
1373         }
1374     },
1375 
1376     hide: function()
1377     {
1378         // clear the state that is tracking the infotip so it is reset after next show()
1379         delete this.infoTipURL;
1380 
1381         Events.removeEventListener(this.panelNode.ownerDocument, "keypress", this.onKeyPress, true);
1382     },
1383 
1384     watchWindow: function(context, win)
1385     {
1386         var self = this;
1387         setTimeout(function() {
1388             self.watchWindowDelayed(context, win);
1389         }, 100);
1390     },
1391 
1392     watchWindowDelayed: function(context, win)
1393     {
1394         if (this.context.window && this.context.window != win)
1395         {
1396             // then I guess we are an embedded window
1397             var htmlPanel = this;
1398             Win.iterateWindows(this.context.window, function(subwin)
1399             {
1400                 if (win == subwin)
1401                 {
1402                     if (FBTrace.DBG_HTML)
1403                         FBTrace.sysout("html.watchWindow found subwin.location.href="+
1404                             win.location.href);
1405 
1406                     htmlPanel.mutateDocumentEmbedded(win, false);
1407                 }
1408             });
1409         }
1410 
1411         if (this.context.attachedMutation)
1412             this.registerMutationListeners(win);
1413     },
1414 
1415     unwatchWindow: function(context, win)
1416     {
1417         if (this.context.window && this.context.window != win)
1418         {
1419             // then I guess we are an embedded window
1420             var htmlPanel = this;
1421             Win.iterateWindows(this.context.window, function(subwin)
1422             {
1423                 if (win == subwin)
1424                 {
1425                     if (FBTrace.DBG_HTML)
1426                         FBTrace.sysout("html.unwatchWindow found subwin.location.href="+
1427                             win.location.href);
1428 
1429                     htmlPanel.mutateDocumentEmbedded(win, true);
1430                 }
1431             });
1432         }
1433 
1434         this.unregisterMutationListeners(win);
1435     },
1436 
1437     mutateDocumentEmbedded: function(win, remove)
1438     {
1439         //xxxHonza: win.document.documentElement is null if this method is synchronously
1440         // called after watchWindow. This is why watchWindowDelayed is introduced.
1441         // See issue 3342
1442 
1443         // document.documentElement - Returns the Element that is a direct child of document.
1444         // For HTML documents, this normally the HTML element.
1445         var self = this;
1446         var target = win.document.documentElement;
1447         var parent = win.frameElement;
1448         var nextSibling = self.findNextSibling(target || parent);
1449         self.mutateNode(target, parent, nextSibling, remove);
1450     },
1451 
1452     supportsObject: function(object, type)
1453     {
1454         if (object instanceof window.Element || object instanceof window.Text ||
1455             object instanceof window.CDATASection)
1456         {
1457             return 2;
1458         }
1459         else if (object instanceof SourceLink.SourceLink && object.type == "css" &&
1460             !Url.reCSS.test(object.href))
1461         {
1462             return 2;
1463         }
1464         else
1465         {
1466             return 0;
1467         }
1468     },
1469 
1470     updateOption: function(name, value)
1471     {
1472         var options = new Set();
1473         options.add("showCommentNodes");
1474         options.add("entityDisplay");
1475         options.add("showTextNodesWithWhitespace");
1476         options.add("showFullTextNodes");
1477 
1478         if (options.has(name))
1479         {
1480             this.resetSearch();
1481             Dom.clearNode(this.panelNode);
1482             if (this.ioBox)
1483                 this.ioBox.destroy();
1484 
1485             this.ioBox = new Firebug.InsideOutBox(this, this.panelNode);
1486             this.ioBox.select(this.selection, true, true);
1487         }
1488     },
1489 
1490     updateSelection: function(object)
1491     {
1492         if (FBTrace.DBG_HTML)
1493             FBTrace.sysout("html.updateSelection " + object, object);
1494 
1495         if (this.ioBox.sourceRow)
1496             this.ioBox.sourceRow.removeAttribute("exe_line");
1497 
1498         // && object.type == "css" and !Url.reCSS(object.href) by supports
1499         if (object instanceof SourceLink.SourceLink)
1500         {
1501             var sourceLink = object;
1502             var stylesheet = Css.getStyleSheetByHref(sourceLink.href, this.context);
1503             if (stylesheet)
1504             {
1505                 var ownerNode = stylesheet.ownerNode;
1506 
1507                 if (FBTrace.DBG_CSS)
1508                 {
1509                     FBTrace.sysout("html panel updateSelection stylesheet.ownerNode=" +
1510                         stylesheet.ownerNode + " href:" + sourceLink.href);
1511                 }
1512 
1513                 if (ownerNode)
1514                 {
1515                     var objectbox = this.ioBox.select(ownerNode, true, true, this.noScrollIntoView);
1516 
1517                     // XXXjjb seems like this could be bad for errors at the end of long files
1518                     // first source row in style
1519                     var sourceRow = objectbox.getElementsByClassName("sourceRow").item(0);
1520                     for (var lineNo = 1; lineNo < sourceLink.line; lineNo++)
1521                     {
1522                         if (!sourceRow) break;
1523                         sourceRow = Dom.getNextByClass(sourceRow,  "sourceRow");
1524                     }
1525 
1526                     if (FBTrace.DBG_CSS)
1527                     {
1528                         FBTrace.sysout("html panel updateSelection sourceLink.line=" +
1529                             sourceLink.line + " sourceRow=" +
1530                             (sourceRow ? sourceRow.innerHTML : "undefined"));
1531                     }
1532 
1533                     if (sourceRow)
1534                     {
1535                         this.ioBox.sourceRow = sourceRow;
1536                         this.ioBox.sourceRow.setAttribute("exe_line", "true");
1537 
1538                         Dom.scrollIntoCenterView(sourceRow);
1539 
1540                         // sourceRow isn't an objectBox, but the function should work anyway...
1541                         this.ioBox.selectObjectBox(sourceRow, false);
1542                     }
1543                 }
1544             }
1545         }
1546         else if (Firebug.Inspector.inspecting)
1547         {
1548             this.ioBox.highlight(object);
1549         }
1550         else
1551         {
1552             var found = this.ioBox.select(object, true, false, this.noScrollIntoView);
1553             if (!found)
1554             {
1555                 // Look up for an enclosing parent. NB this will mask failures in createObjectBoxes
1556                 var parentNode = this.getParentObject(object);
1557 
1558                 if (FBTrace.DBG_ERRORS && FBTrace.DBG_HTML)
1559                     FBTrace.sysout("html.updateSelect no objectBox for object:"+
1560                         Css.getElementCSSSelector(object) + " trying "+
1561                         Css.getElementCSSSelector(parentNode));
1562 
1563                 this.updateSelection(parentNode);
1564                 return;
1565             }
1566 
1567             this.inspectorHistory.unshift(object);
1568             if (this.inspectorHistory.length > 5)
1569                 this.inspectorHistory.pop();
1570         }
1571     },
1572 
1573     stopInspecting: function(object, canceled)
1574     {
1575         if (object != this.inspectorHistory)
1576         {
1577             // Manage history of selection for later access in the command line.
1578             this.inspectorHistory.unshift(object);
1579             if (this.inspectorHistory.length > 5)
1580                 this.inspectorHistory.pop();
1581 
1582             if (FBTrace.DBG_HTML)
1583                 FBTrace.sysout("html.stopInspecting: inspectoryHistory updated",
1584                     this.inspectorHistory);
1585         }
1586 
1587         this.ioBox.highlight(null);
1588 
1589         if (!canceled)
1590             this.ioBox.select(object, true);
1591     },
1592 
1593     search: function(text, reverse)
1594     {
1595         if (!text)
1596             return;
1597 
1598         var search;
1599         if (text == this.searchText && this.lastSearch)
1600         {
1601             search = this.lastSearch;
1602         }
1603         else
1604         {
1605             var doc = this.context.window.document;
1606             search = this.lastSearch = new HTMLLib.NodeSearch(text, doc, this.panelNode, this.ioBox);
1607         }
1608 
1609         var loopAround = search.find(reverse, Firebug.Search.isCaseSensitive(text));
1610         if (loopAround)
1611         {
1612             this.resetSearch();
1613             this.search(text, reverse);
1614         }
1615 
1616         return !search.noMatch && (loopAround ?  "wraparound" : true);
1617     },
1618 
1619     getSearchOptionsMenuItems: function()
1620     {
1621         return [
1622             Firebug.Search.searchOptionMenu("search.Case_Sensitive", "searchCaseSensitive",
1623                 "search.tip.Case_Sensitive")
1624         ];
1625     },
1626 
1627     getDefaultSelection: function()
1628     {
1629         try
1630         {
1631             var doc = this.context.window.document;
1632             return doc.body ? doc.body : Dom.getPreviousElement(doc.documentElement.lastChild);
1633         }
1634         catch (exc)
1635         {
1636             return null;
1637         }
1638     },
1639 
1640     getObjectPath: function(element)
1641     {
1642         var path = [];
1643         for (; element; element = this.getParentObject(element))
1644         {
1645             // Ignore the document itself, it shouldn't be displayed in
1646             // the object path (aka breadcrumbs).
1647             if (element instanceof window.Document)
1648                 continue;
1649 
1650             // Ignore elements without parent
1651             if (!element.parentNode)
1652                 continue;
1653 
1654             path.push(element);
1655         }
1656         return path;
1657     },
1658 
1659     getPopupObject: function(target)
1660     {
1661         return Firebug.getRepObject(target);
1662     },
1663 
1664     getTooltipObject: function(target)
1665     {
1666         if (Dom.getAncestorByClass(target, "nodeLabelBox") ||
1667             Dom.getAncestorByClass(target, "nodeCloseLabelBox"))
1668         {
1669             return Firebug.getRepObject(target);
1670         }
1671     },
1672 
1673     getOptionsMenuItems: function()
1674     {
1675         return [
1676             Menu.optionMenu("ShowFullText", "showFullTextNodes",
1677                 "html.option.tip.Show_Full_Text"),
1678             Menu.optionMenu("ShowWhitespace", "showTextNodesWithWhitespace",
1679                 "html.option.tip.Show_Whitespace"),
1680             Menu.optionMenu("ShowComments", "showCommentNodes",
1681                 "html.option.tip.Show_Comments"),
1682             "-",
1683             {
1684                 label: "html.option.Show_Entities_As_Symbols",
1685                 tooltiptext: "html.option.tip.Show_Entities_As_Symbols",
1686                 type: "radio",
1687                 name: "entityDisplay",
1688                 id: "entityDisplaySymbols",
1689                 command: Obj.bind(this.setEntityDisplay, this, "symbols"),
1690                 checked: Options.get("entityDisplay") == "symbols"
1691             },
1692             {
1693                 label: "html.option.Show_Entities_As_Names",
1694                 tooltiptext: "html.option.tip.Show_Entities_As_Names",
1695                 type: "radio",
1696                 name: "entityDisplay",
1697                 id: "entityDisplayNames",
1698                 command: Obj.bind(this.setEntityDisplay, this, "names"),
1699                 checked: Options.get("entityDisplay") == "names"
1700             },
1701             {
1702                 label: "html.option.Show_Entities_As_Unicode",
1703                 tooltiptext: "html.option.tip.Show_Entities_As_Unicode",
1704                 type: "radio",
1705                 name: "entityDisplay",
1706                 id: "entityDisplayUnicode",
1707                 command: Obj.bind(this.setEntityDisplay, this, "unicode"),
1708                 checked: Options.get("entityDisplay") == "unicode"
1709             },
1710             "-",
1711             Menu.optionMenu("HighlightMutations", "highlightMutations",
1712                 "html.option.tip.Highlight_Mutations"),
1713             Menu.optionMenu("ExpandMutations", "expandMutations",
1714                 "html.option.tip.Expand_Mutations"),
1715             Menu.optionMenu("ScrollToMutations", "scrollToMutations",
1716                 "html.option.tip.Scroll_To_Mutations"),
1717             "-",
1718             Menu.optionMenu("ShadeBoxModel", "shadeBoxModel",
1719                 "inspect.option.tip.Shade_Box_Model"),
1720             Menu.optionMenu("ShowQuickInfoBox","showQuickInfoBox",
1721                 "inspect.option.tip.Show_Quick_Info_Box")
1722         ];
1723     },
1724 
1725     getContextMenuItems: function(node, target)
1726     {
1727         if (!node)
1728             return null;
1729 
1730         var items = [];
1731 
1732         if (node.nodeType == Node.ELEMENT_NODE)
1733         {
1734             items.push(
1735                 "-",
1736                 {
1737                     label: "NewAttribute",
1738                     id: "htmlNewAttribute",
1739                     tooltiptext: "html.tip.New_Attribute",
1740                     command: Obj.bindFixed(this.editNewAttribute, this, node)
1741                 }
1742             );
1743 
1744             var attrBox = Dom.getAncestorByClass(target, "nodeAttr");
1745             if (Dom.getAncestorByClass(target, "nodeAttr"))
1746             {
1747                 var attrName = attrBox.childNodes[1].textContent;
1748 
1749                 items.push(
1750                     {
1751                         label: Locale.$STRF("EditAttribute", [attrName]),
1752                         tooltiptext: Locale.$STRF("html.tip.Edit_Attribute", [attrName]),
1753                         nol10n: true,
1754                         command: Obj.bindFixed(this.editAttribute, this, node, attrName)
1755                     },
1756                     {
1757                         label: Locale.$STRF("DeleteAttribute", [attrName]),
1758                         tooltiptext: Locale.$STRF("html.tip.Delete_Attribute", [attrName]),
1759                         nol10n: true,
1760                         command: Obj.bindFixed(this.deleteAttribute, this, node, attrName)
1761                     }
1762                 );
1763             }
1764 
1765             if (!(Css.nonEditableTags.hasOwnProperty(node.localName)))
1766             {
1767                 var type;
1768 
1769                 if (Xml.isElementHTML(node) || Xml.isElementXHTML(node))
1770                     type = "HTML";
1771                 else if (Xml.isElementMathML(node))
1772                     type = "MathML";
1773                 else if (Xml.isElementSVG(node))
1774                     type = "SVG";
1775                 else if (Xml.isElementXUL(node))
1776                     type = "XUL";
1777                 else
1778                     type = "XML";
1779 
1780                 items.push("-",
1781                 {
1782                     label: Locale.$STRF("html.Edit_Node", [type]),
1783                     tooltiptext: Locale.$STRF("html.tip.Edit_Node", [type]),
1784                     nol10n: true,
1785                     command: Obj.bindFixed(this.editNode, this, node)
1786                 },
1787                 {
1788                     label: "DeleteElement",
1789                     tooltiptext: "html.Delete_Element",
1790 
1791                     // xxxsz: 'Del' needs to be translated, but therefore customizeShortcuts
1792                     // must be turned into a module
1793                     acceltext: "Del",
1794                     command: Obj.bindFixed(this.deleteNode, this, node),
1795                     disabled:(node.localName in Css.innerEditableTags)
1796                 });
1797             }
1798 
1799             var objectBox = Dom.getAncestorByClass(target, "nodeBox");
1800             var nodeChildBox = this.ioBox.getChildObjectBox(objectBox);
1801             if (nodeChildBox)
1802             {
1803                 items.push(
1804                     "-",
1805                     {
1806                         label: "html.label.Expand/Contract_All",
1807                         tooltiptext: "html.tip.Expand/Contract_All",
1808                         acceltext: "*",
1809                         command: Obj.bind(this.toggleAll, this, node)
1810                     }
1811                 );
1812             }
1813         }
1814         else
1815         {
1816             var nodeLabel = Locale.$STR("html.Node");
1817             items.push(
1818                 "-",
1819                 {
1820                     label: Locale.$STRF("html.Edit_Node", [nodeLabel]),
1821                     tooltiptext: Locale.$STRF("html.tip.Edit_Node", [nodeLabel]),
1822                     nol10n: true,
1823                     command: Obj.bindFixed(this.editNode, this, node)
1824                 },
1825                 {
1826                     label: "DeleteNode",
1827                     tooltiptext: "html.Delete_Node",
1828                     command: Obj.bindFixed(this.deleteNode, this, node)
1829                 }
1830             );
1831         }
1832 
1833         Firebug.HTMLModule.MutationBreakpoints.getContextMenuItems(
1834             this.context, node, target, items);
1835 
1836         return items;
1837     },
1838 
1839     showInfoTip: function(infoTip, target, x, y)
1840     {
1841         if (!Css.hasClass(target, "nodeValue"))
1842             return;
1843 
1844         var node = Firebug.getRepObject(target);
1845         if (node && node.nodeType == Node.ELEMENT_NODE)
1846         {
1847             var nodeName = node.localName.toUpperCase();
1848             var attribute = Dom.getAncestorByClass(target, "nodeAttr");
1849             var attributeName = attribute.getElementsByClassName("nodeName").item(0).textContent;
1850 
1851             if ((nodeName == "IMG" || nodeName == "INPUT") && attributeName == "src")
1852             {
1853                 var url = node.src;
1854 
1855                 // This state cleared in hide()
1856                 if (url == this.infoTipURL)
1857                     return true;
1858 
1859                 this.infoTipURL = url;
1860                 return CSSInfoTip.populateImageInfoTip(infoTip, url);
1861             }
1862         }
1863     },
1864 
1865     getEditor: function(target, value)
1866     {
1867         if (Css.hasClass(target, "nodeName") || Css.hasClass(target, "nodeValue") ||
1868             Css.hasClass(target, "nodeBracket"))
1869         {
1870             if (!this.attrEditor)
1871                 this.attrEditor = new Firebug.HTMLPanel.Editors.Attribute(this.document);
1872 
1873             return this.attrEditor;
1874         }
1875         else if (Css.hasClass(target, "nodeComment") || Css.hasClass(target, "nodeCDATA"))
1876         {
1877             if (!this.textDataEditor)
1878                 this.textDataEditor = new Firebug.HTMLPanel.Editors.TextData(this.document);
1879 
1880             return this.textDataEditor;
1881         }
1882         else if (Css.hasClass(target, "nodeText"))
1883         {
1884             if (!this.textNodeEditor)
1885                 this.textNodeEditor = new Firebug.HTMLPanel.Editors.TextNode(this.document);
1886 
1887             return this.textNodeEditor;
1888         }
1889     },
1890 
1891     getInspectorVars: function()
1892     {
1893         var vars = {};
1894         for (var i=0; i<2; i++)
1895             vars["$"+i] = this.inspectorHistory[i];
1896 
1897         return vars;
1898     },
1899 
1900     setEntityDisplay: function(event, type)
1901     {
1902         Options.set("entityDisplay", type);
1903 
1904         var menuItem = event.target;
1905         menuItem.setAttribute("checked", "true");
1906     },
1907 
1908     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1909     // Break on Mutate
1910 
1911     breakOnNext: function(breaking)
1912     {
1913         Firebug.HTMLModule.MutationBreakpoints.breakOnNext(this.context, breaking);
1914     },
1915 
1916     shouldBreakOnNext: function()
1917     {
1918         return this.context.breakOnNextMutate;
1919     },
1920 
1921     getBreakOnNextTooltip: function(enabled)
1922     {
1923         return (enabled ? Locale.$STR("html.Disable Break On Mutate") :
1924             Locale.$STR("html.Break On Mutate"));
1925     }
1926 });
1927 
1928 // ********************************************************************************************* //
1929 
1930 var AttrTag = Firebug.HTMLPanel.AttrTag =
1931     SPAN({"class": "nodeAttr editGroup"},
1932         " ", SPAN({"class": "nodeName editable"}, "$attr.name"), "="",
1933         SPAN({"class": "nodeValue editable"}, "$attr|getAttrValue"), """
1934     );
1935 
1936 var TextTag = Firebug.HTMLPanel.TextTag =
1937     SPAN({"class": "nodeText editable"},
1938         FOR("char", "$object|getNodeTextGroups",
1939             SPAN({"class": "$char.class $char.extra"}, "$char.str")
1940         )
1941     );
1942 
1943 // ********************************************************************************************* //
1944 
1945 Firebug.HTMLPanel.CompleteElement = domplate(FirebugReps.Element,
1946 {
1947     tag:
1948         DIV({"class": "nodeBox open $object|getHidden", _repObject: "$object", role : 'presentation'},
1949             DIV({"class": "nodeLabel", role: "presentation"},
1950                 SPAN({"class": "nodeLabelBox repTarget", role : 'treeitem', 'aria-expanded' : 'false'},
1951                     "<",
1952                     SPAN({"class": "nodeTag"}, "$object|getNodeName"),
1953                     FOR("attr", "$object|attrIterator", AttrTag),
1954                     SPAN({"class": "nodeBracket"}, ">")
1955                 )
1956             ),
1957             DIV({"class": "nodeChildBox", role :"group"},
1958                 FOR("child", "$object|childIterator",
1959                     TAG("$child|getNodeTag", {object: "$child"})
1960                 )
1961             ),
1962             DIV({"class": "nodeCloseLabel", role:"presentation"},
1963                 "</",
1964                 SPAN({"class": "nodeTag"}, "$object|getNodeName"),
1965                 ">"
1966              )
1967         ),
1968 
1969     getNodeTag: function(node)
1970     {
1971         return getNodeTag(node, true);
1972     },
1973 
1974     childIterator: function(node)
1975     {
1976         if (node.contentDocument)
1977             return [node.contentDocument.documentElement];
1978 
1979         if (Firebug.showTextNodesWithWhitespace)
1980         {
1981             return Arr.cloneArray(node.childNodes);
1982         }
1983         else
1984         {
1985             var nodes = [];
1986             var walker = new HTMLLib.ElementWalker();
1987 
1988             for (var child = walker.getFirstChild(node); child; child = walker.getNextSibling(child))
1989             {
1990                 if (child.nodeType != Node.TEXT_NODE || !HTMLLib.isWhitespaceText(child))
1991                     nodes.push(child);
1992             }
1993 
1994             return nodes;
1995         }
1996     }
1997 });
1998 
1999 Firebug.HTMLPanel.SoloElement = domplate(Firebug.HTMLPanel.CompleteElement,
2000 {
2001     tag:
2002         DIV({"class": "soloElement", onmousedown: "$onMouseDown"},
2003             Firebug.HTMLPanel.CompleteElement.tag
2004         ),
2005 
2006     onMouseDown: function(event)
2007     {
2008         for (var child = event.target; child; child = child.parentNode)
2009         {
2010             if (child.repObject)
2011             {
2012                 var panel = Firebug.getElementPanel(child);
2013                 Firebug.chrome.select(child.repObject);
2014                 break;
2015             }
2016         }
2017     }
2018 });
2019 
2020 Firebug.HTMLPanel.Element = domplate(FirebugReps.Element,
2021 {
2022     tag:
2023     DIV({"class": "nodeBox containerNodeBox $object|getHidden", _repObject: "$object",
2024             role: "presentation"},
2025         DIV({"class": "nodeLabel", role: "presentation"},
2026             DIV({"class": "twisty", role: "presentation"}),
2027             SPAN({"class": "nodeLabelBox repTarget", role: "treeitem", "aria-expanded": "false"},
2028                 "<",
2029                 SPAN({"class": "nodeTag"}, "$object|getNodeName"),
2030                 FOR("attr", "$object|attrIterator", AttrTag),
2031                 SPAN({"class": "nodeBracket editable insertBefore"}, ">")
2032             )
2033         ),
2034         DIV({"class": "nodeChildBox", role: "group"}), /* nodeChildBox is special signal in insideOutBox */
2035         DIV({"class": "nodeCloseLabel", role: "presentation"},
2036             SPAN({"class": "nodeCloseLabelBox repTarget"},
2037                 "</",
2038                 SPAN({"class": "nodeTag"}, "$object|getNodeName"),
2039                 ">"
2040             )
2041         )
2042     )
2043 });
2044 
2045 Firebug.HTMLPanel.HTMLDocument = domplate(FirebugReps.Element,
2046 {
2047     tag:
2048         DIV({"class": "nodeBox documentNodeBox containerNodeBox",
2049             _repObject: "$object", role: "presentation"},
2050             DIV({"class": "nodeChildBox", role: "group"})
2051         )
2052 });
2053 
2054 Firebug.HTMLPanel.HTMLDocType = domplate(FirebugReps.Element,
2055 {
2056     tag:
2057         DIV({"class": "nodeBox docTypeNodeBox containerNodeBox",
2058             _repObject: "$object", role: "presentation"},
2059             DIV({"class": "docType"},
2060                 "$object|getDocType"
2061             )
2062         ),
2063 
2064     getDocType: function(doctype)
2065     {
2066         return '<!DOCTYPE ' + doctype.name + (doctype.publicId ? ' PUBLIC "' + doctype.publicId +
2067             '"': '') + (doctype.systemId ? ' "' + doctype.systemId + '"' : '') + '>';
2068     }
2069 });
2070 
2071 Firebug.HTMLPanel.HTMLHtmlElement = domplate(FirebugReps.Element,
2072 {
2073     tag:
2074         DIV({"class": "nodeBox htmlNodeBox containerNodeBox $object|getHidden",
2075             _repObject: "$object", role: "presentation"},
2076             DIV({"class": "nodeLabel", role: "presentation"},
2077                 DIV({"class": "twisty", role: "presentation"}),
2078                 SPAN({"class": "nodeLabelBox repTarget", role: "treeitem",
2079                     "aria-expanded": "false"},
2080                     "<",
2081                     SPAN({"class": "nodeTag"}, "$object|getNodeName"),
2082                     FOR("attr", "$object|attrIterator", AttrTag),
2083                     SPAN({"class": "nodeBracket editable insertBefore"}, ">")
2084                 )
2085             ),
2086             DIV({"class": "nodeChildBox", role: "group"}), /* nodeChildBox is special signal in insideOutBox */
2087             DIV({"class": "nodeCloseLabel", role: "presentation"},
2088                 SPAN({"class": "nodeCloseLabelBox repTarget"},
2089                     "</",
2090                     SPAN({"class": "nodeTag"}, "$object|getNodeName"),
2091                     ">"
2092                 )
2093             )
2094         )
2095 });
2096 
2097 Firebug.HTMLPanel.TextElement = domplate(FirebugReps.Element,
2098 {
2099     tag:
2100         DIV({"class": "nodeBox textNodeBox $object|getHidden", _repObject: "$object", role : 'presentation'},
2101             DIV({"class": "nodeLabel", role: "presentation"},
2102                 SPAN({"class": "nodeLabelBox repTarget", role : 'treeitem'},
2103                     "<",
2104                     SPAN({"class": "nodeTag"}, "$object|getNodeName"),
2105                     FOR("attr", "$object|attrIterator", AttrTag),
2106                     SPAN({"class": "nodeBracket editable insertBefore"}, ">"),
2107                     TextTag,
2108                     "</",
2109                     SPAN({"class": "nodeTag"}, "$object|getNodeName"),
2110                     ">"
2111                 )
2112             )
2113         )
2114 });
2115 
2116 Firebug.HTMLPanel.EmptyElement = domplate(FirebugReps.Element,
2117 {
2118     tag:
2119         DIV({"class": "nodeBox emptyNodeBox $object|getHidden", _repObject: "$object", role : 'presentation'},
2120             DIV({"class": "nodeLabel", role: "presentation"},
2121                 SPAN({"class": "nodeLabelBox repTarget", role : 'treeitem'},
2122                     "<",
2123                     SPAN({"class": "nodeTag"}, "$object|getNodeName"),
2124                     FOR("attr", "$object|attrIterator", AttrTag),
2125                     SPAN({"class": "nodeBracket editable insertBefore"}, ">")
2126                 )
2127             )
2128         )
2129 });
2130 
2131 Firebug.HTMLPanel.XEmptyElement = domplate(FirebugReps.Element,
2132 {
2133     tag:
2134         DIV({"class": "nodeBox emptyNodeBox $object|getHidden", _repObject: "$object", role : 'presentation'},
2135             DIV({"class": "nodeLabel", role: "presentation"},
2136                 SPAN({"class": "nodeLabelBox repTarget", role : 'treeitem'},
2137                     "<",
2138                     SPAN({"class": "nodeTag"}, "$object|getNodeName"),
2139                     FOR("attr", "$object|attrIterator", AttrTag),
2140                     SPAN({"class": "nodeBracket editable insertBefore"}, "/>")
2141                 )
2142             )
2143         )
2144 });
2145 
2146 Firebug.HTMLPanel.AttrNode = domplate(FirebugReps.Element,
2147 {
2148     tag: AttrTag
2149 });
2150 
2151 Firebug.HTMLPanel.TextNode = domplate(FirebugReps.Element,
2152 {
2153     tag:
2154         DIV({"class": "nodeBox", _repObject: "$object", role : 'presentation'},
2155             TextTag
2156         )
2157 });
2158 
2159 Firebug.HTMLPanel.CDATANode = domplate(FirebugReps.Element,
2160 {
2161     tag:
2162         DIV({"class": "nodeBox", _repObject: "$object", role : 'presentation'},
2163             "<![CDATA[",
2164             SPAN({"class": "nodeText nodeCDATA editable"}, "$object.nodeValue"),
2165             "]]>"
2166         )
2167 });
2168 
2169 Firebug.HTMLPanel.CommentNode = domplate(FirebugReps.Element,
2170 {
2171     tag:
2172         DIV({"class": "nodeBox nodeComment", _repObject: "$object", role : 'presentation'},
2173             "<!--",
2174             SPAN({"class": "nodeComment editable"}, "$object.nodeValue"),
2175             "-->"
2176         )
2177 });
2178 
2179 // ********************************************************************************************* //
2180 // TextDataEditor
2181 
2182 /**
2183  * TextDataEditor deals with text of comments and cdata nodes
2184  */
2185 function TextDataEditor(doc)
2186 {
2187     this.initializeInline(doc);
2188 }
2189 
2190 TextDataEditor.prototype = domplate(Firebug.InlineEditor.prototype,
2191 {
2192     saveEdit: function(target, value, previousValue)
2193     {
2194         var node = Firebug.getRepObject(target);
2195         if (!node)
2196             return;
2197 
2198         target.data = value;
2199         node.data = value;
2200     }
2201 });
2202 
2203 // ********************************************************************************************* //
2204 // TextNodeEditor
2205 
2206 /**
2207  * TextNodeEditor deals with text nodes that do and do not have sibling elements. If
2208  * there are no sibling elements, the parent is known as a TextElement. In other cases
2209  * we keep track of their position via a range (this is in part because as people type
2210  * html, the range will keep track of the text nodes and elements that the user
2211  * is creating as they type, and this range could be in the middle of the parent
2212  * elements children).
2213  */
2214 function TextNodeEditor(doc)
2215 {
2216     this.initializeInline(doc);
2217 }
2218 
2219 TextNodeEditor.prototype = domplate(Firebug.InlineEditor.prototype,
2220 {
2221     getInitialValue: function(target, value)
2222     {
2223         // The text displayed within the HTML panel can be shortened if the 'Show Full Text'
2224         // option is false, so get the original textContent from the associated page element
2225         // (issue 2183).
2226         var repObject = Firebug.getRepObject(target);
2227         if (repObject)
2228             return repObject.textContent;
2229 
2230         return value;
2231     },
2232 
2233     beginEditing: function(target, value)
2234     {
2235         var node = Firebug.getRepObject(target);
2236         if (!node || node instanceof window.Element)
2237             return;
2238 
2239         var document = node.ownerDocument;
2240         this.range = document.createRange();
2241         this.range.setStartBefore(node);
2242         this.range.setEndAfter(node);
2243     },
2244 
2245     endEditing: function(target, value, cancel)
2246     {
2247         if (this.range)
2248         {
2249             this.range.detach();
2250             delete this.range;
2251         }
2252 
2253         // Remove empty groups by default
2254         return true;
2255     },
2256 
2257     saveEdit: function(target, value, previousValue)
2258     {
2259         var node = Firebug.getRepObject(target);
2260         if (!node)
2261             return;
2262 
2263         value = Str.unescapeForTextNode(value || "");
2264         target.innerHTML = Str.escapeForTextNode(value);
2265 
2266         if (node instanceof window.Element)
2267         {
2268             if (Xml.isElementMathML(node) || Xml.isElementSVG(node))
2269                 node.textContent = value;
2270             else
2271                 node.innerHTML = value;
2272         }
2273         else
2274         {
2275             try
2276             {
2277                 var documentFragment = this.range.createContextualFragment(value);
2278                 var cnl = documentFragment.childNodes.length;
2279                 this.range.deleteContents();
2280                 this.range.insertNode(documentFragment);
2281                 var r = this.range, sc = r.startContainer, so = r.startOffset;
2282                 this.range.setEnd(sc,so+cnl);
2283             }
2284             catch (e)
2285             {
2286                 if (FBTrace.DBG_ERRORS)
2287                     FBTrace.sysout("TextNodeEditor.saveEdit; EXCEPTION " + e, e);
2288             }
2289         }
2290     }
2291 });
2292 
2293 // ********************************************************************************************* //
2294 // AttributeEditor
2295 
2296 function AttributeEditor(doc)
2297 {
2298     this.initializeInline(doc);
2299 }
2300 
2301 AttributeEditor.prototype = domplate(Firebug.InlineEditor.prototype,
2302 {
2303     saveEdit: function(target, value, previousValue)
2304     {
2305         var element = Firebug.getRepObject(target);
2306         if (!element)
2307             return;
2308 
2309         // XXXstr unescape value
2310         target.innerHTML = Str.escapeForElementAttribute(value);
2311 
2312         if (Css.hasClass(target, "nodeName"))
2313         {
2314             if (value != previousValue)
2315                 element.removeAttribute(previousValue);
2316 
2317             if (value)
2318             {
2319                 var attrValue = Dom.getNextByClass(target, "nodeValue").textContent;
2320                 element.setAttribute(value, attrValue);
2321             }
2322             else
2323             {
2324                 element.removeAttribute(value);
2325             }
2326         }
2327         else if (Css.hasClass(target, "nodeValue"))
2328         {
2329             var attrName = Dom.getPreviousByClass(target, "nodeName").textContent;
2330             element.setAttribute(attrName, value);
2331         }
2332 
2333         var panel = Firebug.getElementPanel(target);
2334         Events.dispatch(Firebug.uiListeners, "onObjectChanged", [element, panel]);
2335 
2336         //this.panel.markChange();
2337     },
2338 
2339     advanceToNext: function(target, charCode)
2340     {
2341         if (charCode == 61 /* '=' */ && Css.hasClass(target, "nodeName"))
2342         {
2343             return true;
2344         }
2345         else if ((charCode == 34 /* '"' */ || charCode == 39 /* ''' */) &&
2346             Css.hasClass(target, "nodeValue"))
2347         {
2348             var nonRestrictiveAttributes =
2349             [
2350                 "onabort",
2351                 "onblur",
2352                 "onchange",
2353                 "onclick",
2354                 "ondblclick",
2355                 "onerror",
2356                 "onfocus",
2357                 "onkeydown",
2358                 "onkeypress",
2359                 "onkeyup",
2360                 "onload",
2361                 "onmousedown",
2362                 "onmousemove",
2363                 "onmouseout",
2364                 "onmouseover",
2365                 "onmouseup",
2366                 "onreset",
2367                 "onselect",
2368                 "onsubmit",
2369                 "onunload",
2370                 "title",
2371                 "alt",
2372                 "style"
2373             ]
2374 
2375             var attrName = Dom.getPreviousByClass(target, "nodeName").textContent;
2376 
2377             // This should cover most of the cases where quotes are allowed inside the value
2378             // See issue 4542
2379             for (var i = 0; i < nonRestrictiveAttributes.length; i++)
2380             {
2381                 if (attrName == nonRestrictiveAttributes[i])
2382                     return false;
2383             }
2384             return true;
2385         }
2386     },
2387 
2388     insertNewRow: function(target, insertWhere)
2389     {
2390         var emptyAttr = {name: "", value: ""};
2391         var sibling = insertWhere == "before" ? target.previousSibling : target;
2392         return AttrTag.insertAfter({attr: emptyAttr}, sibling);
2393     },
2394 
2395     getInitialValue: function(target, value)
2396     {
2397         if (value == "")
2398             return value;
2399 
2400         var element = Firebug.getRepObject(target);
2401         if (element && element instanceof window.Element)
2402         {
2403             // If object that was clicked to edit was
2404             // attribute value, not attribute name.
2405             if (Css.hasClass(target, "nodeValue"))
2406             {
2407                 var attributeName = Dom.getPreviousByClass(target, "nodeName").textContent;
2408                 return element.getAttribute(attributeName);
2409             }
2410         }
2411         return value;
2412     }
2413 });
2414 
2415 // ********************************************************************************************* //
2416 // HTMLEditor
2417 
2418 function HTMLEditor(doc)
2419 {
2420     this.box = this.tag.replace({}, doc, this);
2421     this.input = this.box.firstChild;
2422     this.multiLine = true;
2423     this.tabNavigation = false;
2424     this.arrowCompletion = false;
2425 }
2426 
2427 HTMLEditor.prototype = domplate(Firebug.BaseEditor,
2428 {
2429     tag:
2430         DIV(
2431             TEXTAREA({"class": "htmlEditor fullPanelEditor", oninput: "$onInput"})
2432         ),
2433 
2434     getValue: function()
2435     {
2436         return this.input.value;
2437     },
2438 
2439     setValue: function(value)
2440     {
2441         return this.input.value = value;
2442     },
2443 
2444     show: function(target, panel, value, textSize)
2445     {
2446         this.target = target;
2447         this.panel = panel;
2448         var el = target.repObject;
2449         if (this.innerEditMode)
2450         {
2451             this.editingParent = el;
2452         }
2453         else
2454         {
2455             this.editingRange = el.ownerDocument.createRange();
2456             this.editingRange.selectNode(el);
2457             this.originalLocalName = el.localName;
2458         }
2459 
2460         this.panel.panelNode.appendChild(this.box);
2461 
2462         this.input.value = value;
2463         this.input.focus();
2464 
2465         var command = Firebug.chrome.$("cmd_firebug_toggleHTMLEditing");
2466         command.setAttribute("checked", true);
2467     },
2468 
2469     hide: function()
2470     {
2471         var command = Firebug.chrome.$("cmd_firebug_toggleHTMLEditing");
2472         command.setAttribute("checked", false);
2473 
2474         this.panel.panelNode.removeChild(this.box);
2475 
2476         delete this.editingParent;
2477         delete this.editingRange;
2478         delete this.originalLocalName;
2479         delete this.target;
2480         delete this.panel;
2481     },
2482 
2483     getNewSelection: function(fragment)
2484     {
2485         // Get a new element to select in the HTML panel. An element with the
2486         // same localName is preferred, or just any element. If there is none,
2487         // we choose the parent instead.
2488         var found = null;
2489         var nodes = fragment.childNodes;
2490         for (var i = 0; i < nodes.length; ++i)
2491         {
2492             var n = nodes[i];
2493             if (n.nodeType === Node.ELEMENT_NODE)
2494             {
2495                 if (n.localName === this.originalLocalName)
2496                     return n;
2497                 if (!found)
2498                     found = n;
2499             }
2500         }
2501         if (found)
2502             return found;
2503         return this.editingRange.startContainer;
2504     },
2505 
2506     saveEdit: function(target, value, previousValue)
2507     {
2508         if (this.innerEditMode)
2509         {
2510             try
2511             {
2512                 // xxxHonza: Catch "can't access dead object" exception.
2513                 this.editingParent.innerHTML = value;
2514             }
2515             catch (e)
2516             {
2517                 FBTrace.sysout("htmlPanel.saveEdit; EXCEPTION " + e, e);
2518             }
2519         }
2520         else
2521         {
2522             try
2523             {
2524                 var range = this.editingRange;
2525                 var fragment = range.createContextualFragment(value);
2526                 var sel = this.getNewSelection(fragment);
2527 
2528                 var cnl = fragment.childNodes.length;
2529                 range.deleteContents();
2530                 range.insertNode(fragment);
2531                 var sc = range.startContainer, so = range.startOffset;
2532                 range.setEnd(sc, so + cnl);
2533 
2534                 this.panel.select(sel, false, true);
2535 
2536                 // Clear and update the status path, to make sure it doesn't
2537                 // show elements no longer in the DOM.
2538                 Firebug.chrome.clearStatusPath();
2539                 Firebug.chrome.syncStatusPath();
2540             }
2541             catch (e)
2542             {
2543                 if (FBTrace.DBG_ERRORS)
2544                     FBTrace.sysout("HTMLEditor.saveEdit; EXCEPTION " + e, e);
2545             }
2546         }
2547     },
2548 
2549     endEditing: function()
2550     {
2551         //this.panel.markChange();
2552         return true;
2553     },
2554 
2555     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
2556 
2557     onInput: function()
2558     {
2559         Firebug.Editor.update();
2560     }
2561 });
2562 
2563 // ********************************************************************************************* //
2564 // Editors
2565 
2566 Firebug.HTMLPanel.Editors = {
2567     html : HTMLEditor,
2568     Attribute : AttributeEditor,
2569     TextNode: TextNodeEditor,
2570     TextData: TextDataEditor
2571 };
2572 
2573 // ********************************************************************************************* //
2574 // Local Helpers
2575 
2576 function getEmptyElementTag(node)
2577 {
2578     var isXhtml= Xml.isElementXHTML(node);
2579     if (isXhtml)
2580         return Firebug.HTMLPanel.XEmptyElement.tag;
2581     else
2582         return Firebug.HTMLPanel.EmptyElement.tag;
2583 }
2584 
2585 function getNodeTag(node, expandAll)
2586 {
2587     if (node instanceof window.Element)
2588     {
2589         if (node instanceof window.HTMLHtmlElement && node.ownerDocument && node.ownerDocument.doctype)
2590             return Firebug.HTMLPanel.HTMLHtmlElement.tag;
2591         else if (node instanceof window.HTMLAppletElement)
2592             return getEmptyElementTag(node);
2593         else if (Firebug.shouldIgnore(node))
2594             return null;
2595         else if (HTMLLib.isContainerElement(node))
2596             return expandAll ? Firebug.HTMLPanel.CompleteElement.tag : Firebug.HTMLPanel.Element.tag;
2597         else if (HTMLLib.isEmptyElement(node))
2598             return getEmptyElementTag(node);
2599         else if (Firebug.showCommentNodes && HTMLLib.hasCommentChildren(node))
2600             return expandAll ? Firebug.HTMLPanel.CompleteElement.tag : Firebug.HTMLPanel.Element.tag;
2601         else if (HTMLLib.hasNoElementChildren(node))
2602             return Firebug.HTMLPanel.TextElement.tag;
2603         else
2604             return expandAll ? Firebug.HTMLPanel.CompleteElement.tag : Firebug.HTMLPanel.Element.tag;
2605     }
2606     else if (node instanceof window.Text)
2607         return Firebug.HTMLPanel.TextNode.tag;
2608     else if (node instanceof window.CDATASection)
2609         return Firebug.HTMLPanel.CDATANode.tag;
2610     else if (node instanceof window.Comment && (Firebug.showCommentNodes || expandAll))
2611         return Firebug.HTMLPanel.CommentNode.tag;
2612     else if (node instanceof Firebug.HTMLModule.SourceText)
2613         return FirebugReps.SourceText.tag;
2614     else if (node instanceof window.Document)
2615         return Firebug.HTMLPanel.HTMLDocument.tag;
2616     else if (node instanceof window.DocumentType)
2617         return Firebug.HTMLPanel.HTMLDocType.tag;
2618     else
2619         return FirebugReps.Nada.tag;
2620 }
2621 
2622 function getNodeBoxTag(nodeBox)
2623 {
2624     var re = /([^\s]+)NodeBox/;
2625     var m = re.exec(nodeBox.className);
2626     if (!m)
2627         return null;
2628 
2629     var nodeBoxType = m[1];
2630     if (nodeBoxType == "container")
2631         return Firebug.HTMLPanel.Element.tag;
2632     else if (nodeBoxType == "text")
2633         return Firebug.HTMLPanel.TextElement.tag;
2634     else if (nodeBoxType == "empty")
2635         return Firebug.HTMLPanel.EmptyElement.tag;
2636 }
2637 
2638 // ********************************************************************************************* //
2639 
2640 Firebug.HTMLModule.SourceText = function(lines, owner)
2641 {
2642     this.lines = lines;
2643     this.owner = owner;
2644 };
2645 
2646 Firebug.HTMLModule.SourceText.getLineAsHTML = function(lineNo)
2647 {
2648     return Str.escapeForSourceLine(this.lines[lineNo-1]);
2649 };
2650 
2651 // ********************************************************************************************* //
2652 // Mutation Breakpoints
2653 
2654 /**
2655  * @class Represents {@link Firebug.Debugger} listener. This listener is reponsible for
2656  * providing a list of mutation-breakpoints into the Breakpoints side-panel.
2657  */
2658 Firebug.HTMLModule.DebuggerListener =
2659 {
2660     getBreakpoints: function(context, groups)
2661     {
2662         if (!context.mutationBreakpoints.isEmpty())
2663             groups.push(context.mutationBreakpoints);
2664     }
2665 };
2666 
2667 Firebug.HTMLModule.MutationBreakpoints =
2668 {
2669     breakOnNext: function(context, breaking)
2670     {
2671         context.breakOnNextMutate = breaking;
2672     },
2673 
2674     breakOnNextMutate: function(event, context, type)
2675     {
2676         if (!context.breakOnNextMutate)
2677             return false;
2678 
2679         // Ignore changes in ignored branches
2680         if (isAncestorIgnored(event.target))
2681             return false;
2682 
2683         context.breakOnNextMutate = false;
2684 
2685         this.breakWithCause(event, context, type);
2686     },
2687 
2688     breakWithCause: function(event, context, type)
2689     {
2690         var changeLabel = Firebug.HTMLModule.BreakpointRep.getChangeLabel({type: type});
2691         context.breakingCause = {
2692             title: Locale.$STR("html.Break On Mutate"),
2693             message: changeLabel,
2694             type: event.type,
2695             target: event.target,
2696             relatedNode: event.relatedNode, // http://www.w3.org/TR/DOM-Level-2-Events/events.html
2697             prevValue: event.prevValue,
2698             newValue: event.newValue,
2699             attrName: event.attrName,
2700             attrChange: event.attrChange,
2701         };
2702 
2703         Firebug.Breakpoint.breakNow(context.getPanel("html", true));
2704         return true;
2705     },
2706 
2707     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
2708     // Mutation event handlers.
2709 
2710     onMutateAttr: function(event, context)
2711     {
2712         if (this.breakOnNextMutate(event, context, BP_BREAKONATTRCHANGE))
2713             return;
2714 
2715         var breakpoints = context.mutationBreakpoints;
2716         var self = this;
2717         breakpoints.enumerateBreakpoints(function(bp) {
2718             if (bp.checked && bp.node == event.target && bp.type == BP_BREAKONATTRCHANGE) {
2719                 self.breakWithCause(event, context, BP_BREAKONATTRCHANGE);
2720                 return true;
2721             }
2722         });
2723     },
2724 
2725     onMutateText: function(event, context)
2726     {
2727         if (this.breakOnNextMutate(event, context, BP_BREAKONTEXT))
2728             return;
2729     },
2730 
2731     onMutateNode: function(event, context)
2732     {
2733         var node = event.target;
2734         var removal = event.type == "DOMNodeRemoved";
2735 
2736         if (this.breakOnNextMutate(event, context, removal ?
2737             BP_BREAKONREMOVE : BP_BREAKONCHILDCHANGE))
2738         {
2739             return;
2740         }
2741 
2742         var breakpoints = context.mutationBreakpoints;
2743         var breaked = false;
2744 
2745         if (removal)
2746         {
2747             var self = this;
2748             breaked = breakpoints.enumerateBreakpoints(function(bp) {
2749                 if (bp.checked && bp.node == node && bp.type == BP_BREAKONREMOVE) {
2750                     self.breakWithCause(event, context, BP_BREAKONREMOVE);
2751                     return true;
2752                 }
2753             });
2754         }
2755 
2756         if (!breaked)
2757         {
2758             // Collect all parents of the mutated node.
2759             var parents = [];
2760             for (var parent = node.parentNode; parent; parent = parent.parentNode)
2761                 parents.push(parent);
2762 
2763             // Iterate over all parents and see if some of them has a breakpoint.
2764             var self = this;
2765             breakpoints.enumerateBreakpoints(function(bp)
2766             {
2767                 for (var i=0; i<parents.length; i++)
2768                 {
2769                     if (bp.checked && bp.node == parents[i] && bp.type == BP_BREAKONCHILDCHANGE)
2770                     {
2771                         self.breakWithCause(event, context, BP_BREAKONCHILDCHANGE);
2772                         return true;
2773                     }
2774                 }
2775             });
2776         }
2777 
2778         if (removal)
2779         {
2780             // Remove all breakpoints assocaited with removed node.
2781             var invalidate = false;
2782             breakpoints.enumerateBreakpoints(function(bp)
2783             {
2784                 if (bp.node == node)
2785                 {
2786                     breakpoints.removeBreakpoint(bp);
2787                     invalidate = true;
2788                 }
2789             });
2790 
2791             if (invalidate)
2792                 context.invalidatePanels("breakpoints");
2793         }
2794     },
2795 
2796     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
2797     // Context menu items
2798 
2799     getContextMenuItems: function(context, node, target, items)
2800     {
2801         if (!(node && node.nodeType == Node.ELEMENT_NODE))
2802             return;
2803 
2804         var breakpoints = context.mutationBreakpoints;
2805 
2806         var attrBox = Dom.getAncestorByClass(target, "nodeAttr");
2807         if (Dom.getAncestorByClass(target, "nodeAttr"))
2808         {
2809         }
2810 
2811         if (!(Css.nonEditableTags.hasOwnProperty(node.localName)))
2812         {
2813             items.push(
2814                 "-",
2815                 {
2816                     label: "html.label.Break_On_Attribute_Change",
2817                     tooltiptext: "html.tip.Break_On_Attribute_Change",
2818                     type: "checkbox",
2819                     checked: breakpoints.findBreakpoint(node, BP_BREAKONATTRCHANGE),
2820                     command: Obj.bindFixed(this.onModifyBreakpoint, this, context, node,
2821                         BP_BREAKONATTRCHANGE)
2822                 },
2823                 {
2824                     label: "html.label.Break_On_Child_Addition_or_Removal",
2825                     tooltiptext: "html.tip.Break_On_Child_Addition_or_Removal",
2826                     type: "checkbox",
2827                     checked: breakpoints.findBreakpoint(node, BP_BREAKONCHILDCHANGE),
2828                     command: Obj.bindFixed(this.onModifyBreakpoint, this, context, node,
2829                         BP_BREAKONCHILDCHANGE)
2830                 },
2831                 {
2832                     label: "html.label.Break_On_Element_Removal",
2833                     tooltiptext: "html.tip.Break_On_Element_Removal",
2834                     type: "checkbox",
2835                     checked: breakpoints.findBreakpoint(node, BP_BREAKONREMOVE),
2836                     command: Obj.bindFixed(this.onModifyBreakpoint, this, context, node,
2837                         BP_BREAKONREMOVE)
2838                 }
2839             );
2840         }
2841     },
2842 
2843     onModifyBreakpoint: function(context, node, type)
2844     {
2845         var xpath = Xpath.getElementXPath(node);
2846         if (FBTrace.DBG_HTML)
2847             FBTrace.sysout("html.onModifyBreakpoint " + xpath );
2848 
2849         var breakpoints = context.mutationBreakpoints;
2850         var bp = breakpoints.findBreakpoint(node, type);
2851 
2852         // Remove an existing or create new breakpoint.
2853         if (bp)
2854             breakpoints.removeBreakpoint(bp);
2855         else
2856             context.mutationBreakpoints.addBreakpoint(node, type);
2857 
2858         Events.dispatch( Firebug.HTMLModule.fbListeners, "onModifyBreakpoint",
2859             [context, xpath, type]);
2860     },
2861 };
2862 
2863 Firebug.HTMLModule.Breakpoint = function(node, type)
2864 {
2865     this.node = node;
2866     this.xpath = Xpath.getElementXPath(node);
2867     this.checked = true;
2868     this.type = type;
2869 };
2870 
2871 Firebug.HTMLModule.BreakpointRep = domplate(Firebug.Rep,
2872 {
2873     inspectable: false,
2874 
2875     tag:
2876         DIV({"class": "breakpointRow focusRow", $disabled: "$bp|isDisabled", _repObject: "$bp",
2877             role: "option", "aria-checked": "$bp.checked"},
2878             DIV({"class": "breakpointBlockHead"},
2879                 INPUT({"class": "breakpointCheckbox", type: "checkbox",
2880                     _checked: "$bp.checked", tabindex : "-1", onclick: "$onEnable"}),
2881                 TAG("$bp.node|getNodeTag", {object: "$bp.node"}),
2882                 DIV({"class": "breakpointMutationType"}, "$bp|getChangeLabel"),
2883                 IMG({"class": "closeButton", src: "blank.gif", onclick: "$onRemove"})
2884             ),
2885             DIV({"class": "breakpointCode"},
2886                 TAG("$bp.node|getSourceLine", {object: "$bp.node"})
2887             )
2888         ),
2889 
2890     getNodeTag: function(node)
2891     {
2892         var rep = Firebug.getRep(node, Firebug.currentContext);
2893         return rep.shortTag ? rep.shortTag : rep.tag;
2894     },
2895 
2896     getSourceLine: function(node)
2897     {
2898         return getNodeTag(node, false);
2899     },
2900 
2901     getChangeLabel: function(bp)
2902     {
2903         switch (bp.type)
2904         {
2905         case BP_BREAKONATTRCHANGE:
2906             return Locale.$STR("html.label.Break On Attribute Change");
2907         case BP_BREAKONCHILDCHANGE:
2908             return Locale.$STR("html.label.Break On Child Addition or Removal");
2909         case BP_BREAKONREMOVE:
2910             return Locale.$STR("html.label.Break On Element Removal");
2911         case BP_BREAKONTEXT:
2912             return Locale.$STR("html.label.Break On Text Change");
2913         }
2914 
2915         return "";
2916     },
2917 
2918     isDisabled: function(bp)
2919     {
2920         return !bp.checked;
2921     },
2922 
2923     onRemove: function(event)
2924     {
2925         Events.cancelEvent(event);
2926 
2927         var bpPanel = Firebug.getElementPanel(event.target);
2928         var context = bpPanel.context;
2929 
2930         if (Css.hasClass(event.target, "closeButton"))
2931         {
2932             // Remove from list of breakpoints.
2933             var row = Dom.getAncestorByClass(event.target, "breakpointRow");
2934             context.mutationBreakpoints.removeBreakpoint(row.repObject);
2935 
2936             bpPanel.refresh();
2937         }
2938     },
2939 
2940     onEnable: function(event)
2941     {
2942         var checkBox = event.target;
2943         var bpRow = Dom.getAncestorByClass(checkBox, "breakpointRow");
2944 
2945         if (checkBox.checked)
2946         {
2947             Css.removeClass(bpRow, "disabled");
2948             bpRow.setAttribute("aria-checked", "true");
2949         }
2950         else
2951         {
2952             Css.setClass(bpRow, "disabled");
2953             bpRow.setAttribute("aria-checked", "false");
2954         }
2955 
2956         var bp = bpRow.repObject;
2957         bp.checked = checkBox.checked;
2958 
2959         var bpPanel = Firebug.getElementPanel(event.target);
2960         var context = bpPanel.context;
2961     },
2962 
2963     supportsObject: function(object, type)
2964     {
2965         return object instanceof Firebug.HTMLModule.Breakpoint;
2966     }
2967 });
2968 
2969 // ********************************************************************************************* //
2970 
2971 function MutationBreakpointGroup()
2972 {
2973     this.breakpoints = [];
2974 }
2975 
2976 MutationBreakpointGroup.prototype = Obj.extend(new Firebug.Breakpoint.BreakpointGroup(),
2977 {
2978     name: "mutationBreakpoints",
2979     title: Locale.$STR("html.label.HTML Breakpoints"),
2980 
2981     addBreakpoint: function(node, type)
2982     {
2983         this.breakpoints.push(new Firebug.HTMLModule.Breakpoint(node, type));
2984     },
2985 
2986     matchBreakpoint: function(bp, args)
2987     {
2988         var node = args[0];
2989         var type = args[1];
2990         return (bp.node == node) && (!bp.type || bp.type == type);
2991     },
2992 
2993     removeBreakpoint: function(bp)
2994     {
2995         Arr.remove(this.breakpoints, bp);
2996     },
2997 
2998     // Persistence
2999     load: function(context)
3000     {
3001         var panelState = Persist.getPersistedState(context, "html");
3002         if (panelState.breakpoints)
3003             this.breakpoints = panelState.breakpoints;
3004 
3005         this.enumerateBreakpoints(function(bp)
3006         {
3007             var elts = Xpath.getElementsByXPath(context.window.document, bp.xpath);
3008             bp.node = elts && elts.length ? elts[0] : null;
3009         });
3010     },
3011 
3012     store: function(context)
3013     {
3014         this.enumerateBreakpoints(function(bp)
3015         {
3016             bp.node = null;
3017         });
3018 
3019         var panelState = Persist.getPersistedState(context, "html");
3020         panelState.breakpoints = this.breakpoints;
3021     },
3022 });
3023 
3024 function isAncestorIgnored(node)
3025 {
3026     for (var parent = node; parent; parent = parent.parentNode)
3027     {
3028         if (Firebug.shouldIgnore(parent))
3029             return true;
3030     }
3031 
3032     return false;
3033 }
3034 
3035 // ********************************************************************************************* //
3036 // Registration
3037 
3038 Firebug.registerPanel(Firebug.HTMLPanel);
3039 Firebug.registerModule(Firebug.HTMLModule);
3040 Firebug.registerRep(Firebug.HTMLModule.BreakpointRep);
3041 
3042 return Firebug.HTMLModule;
3043 
3044 // ********************************************************************************************* //
3045 }});
3046