1 /* See license.txt for terms of usage */
  2 
  3 FBL.ns(function() { with (FBL) {
  4 
  5 // ************************************************************************************************
  6 // Constants
  7 
  8 const Cc = Components.classes;
  9 const Ci = Components.interfaces;
 10 const jsdIStackFrame = Ci.jsdIStackFrame;
 11 
 12 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 13 
 14 const insertSliceSize = 18;
 15 const insertInterval = 40;
 16 
 17 const rxIdentifier = /^[$_A-Za-z][$_A-Za-z0-9]*$/
 18 
 19 const ignoreVars =
 20 {
 21     "__firebug__": 1,
 22     "eval": 1,
 23 
 24     // We are forced to ignore Java-related variables, because
 25     // trying to access them causes browser freeze
 26     "java": 1,
 27     "sun": 1,
 28     "Packages": 1,
 29     "JavaArray": 1,
 30     "JavaMember": 1,
 31     "JavaObject": 1,
 32     "JavaClass": 1,
 33     "JavaPackage": 1,
 34     // internal firebug things
 35     "_firebug": 1,
 36     "_FirebugConsole": 1,
 37     "_FirebugCommandLine": 1,
 38     "loadFirebugConsole": 1,
 39     "_getFirebugConsoleElement": 1,
 40 };
 41 
 42 // ************************************************************************************************
 43 
 44 Firebug.DOMModule = extend(Firebug.Module,
 45 {
 46     initialize: function(prefDomain, prefNames)
 47     {
 48         Firebug.Module.initialize.apply(this, arguments);
 49         Firebug.Debugger.addListener(this.DebuggerListener);
 50     },
 51 
 52     initContext: function(context, persistedState)
 53     {
 54         Firebug.Module.initContext.apply(this, arguments);
 55         context.dom = {breakpoints: new DOMBreakpointGroup()};
 56     },
 57 
 58     loadedContext: function(context, persistedState)
 59     {
 60         context.dom.breakpoints.load(context);
 61     },
 62 
 63     destroyContext: function(context, persistedState)
 64     {
 65         Firebug.Module.destroyContext.apply(this, arguments);
 66 
 67         context.dom.breakpoints.store(context);
 68     },
 69 
 70     shutdown: function()
 71     {
 72         Firebug.Module.shutdown.apply(this, arguments);
 73         Firebug.Debugger.removeListener(this.DebuggerListener);
 74     },
 75 });
 76 
 77 // ************************************************************************************************
 78 
 79 const WatchRowTag =
 80     TR({"class": "watchNewRow", level: 0},
 81         TD({"class": "watchEditCell", colspan: 3},
 82             DIV({"class": "watchEditBox a11yFocusNoTab", role: "button", 'tabindex' : '0',
 83                 'aria-label' : $STR('a11y.labels.press enter to add new watch expression')},
 84                     $STR("NewWatch")
 85             )
 86         )
 87     );
 88 
 89 const SizerRow =
 90     TR({role : 'presentation'},
 91         TD(),
 92         TD({width: "30%"}),
 93         TD({width: "70%"})
 94     );
 95 
 96 const DirTablePlate = domplate(Firebug.Rep,
 97 {
 98     memberRowTag:
 99         TR({"class": "memberRow $member.open $member.type\\Row", _domObject: "$member",
100             $hasChildren: "$member.hasChildren",
101             role: "presentation",
102             level: "$member.level",
103             breakable: "$member.breakable",
104             breakpoint: "$member.breakpoint",
105             disabledBreakpoint: "$member.disabledBreakpoint"},
106             TD({"class": "memberHeaderCell"},
107                DIV({"class": "sourceLine memberRowHeader", onclick: "$onClickRowHeader"},
108                     " "
109                )
110             ),
111             TD({"class": "memberLabelCell", style: "padding-left: $member.indent\\px",
112                 role: 'presentation'},
113                 DIV({"class": "memberLabel $member.type\\Label"},
114                     SPAN({"class": "memberLabelPrefix"}, "$member.prefix"),
115                     SPAN("$member.name")
116                 )
117             ),
118             TD({"class": "memberValueCell", role : 'presentation'},
119                 TAG("$member.tag", {object: "$member.value"})
120             )
121         ),
122 
123     tag:
124         TABLE({"class": "domTable", cellpadding: 0, cellspacing: 0, onclick: "$onClick",
125             role: "tree", 'aria-label': $STR('aria.labels.dom properties')},
126             TBODY({role: 'presentation'},
127                 SizerRow,
128                 FOR("member", "$object|memberIterator",
129                     TAG("$memberRowTag", {member: "$member"})
130                 )
131             )
132         ),
133 
134     watchTag:
135         TABLE({"class": "domTable", cellpadding: 0, cellspacing: 0,
136                _toggles: "$toggles", _domPanel: "$domPanel", onclick: "$onClick", role : 'tree'},
137             TBODY({role : 'presentation'},
138                 SizerRow,
139                 WatchRowTag
140             )
141         ),
142 
143     tableTag:
144         TABLE({"class": "domTable", cellpadding: 0, cellspacing: 0,
145             _toggles: "$toggles", _domPanel: "$domPanel", onclick: "$onClick",
146             role: 'tree', 'aria-label': 'DOM properties'},
147             TBODY({role : 'presentation'},
148                 SizerRow
149             )
150         ),
151 
152     rowTag:
153         FOR("member", "$members",
154             TAG("$memberRowTag", {member: "$member"})
155         ),
156 
157     memberIterator: function(object, level)
158     {
159         return Firebug.DOMBasePanel.prototype.getMembers(object, level, this.context);
160     },
161 
162     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
163 
164     onClick: function(event)
165     {
166         if (!isLeftClick(event))
167             return;
168 
169         var row = getAncestorByClass(event.target, "memberRow");
170         var label = getAncestorByClass(event.target, "memberLabel");
171         var valueCell = row.getElementsByClassName("memberValueCell").item(0);
172         var object = Firebug.getRepObject(event.target);
173         var target = row.lastChild.firstChild;
174         var isString = hasClass(target,"objectBox-string");
175         var inValueCell = event.target == valueCell || event.target == target;
176 
177         if (label && hasClass(row, "hasChildren") && !(isString && inValueCell))
178         {
179             var row = label.parentNode.parentNode;
180             this.toggleRow(row);
181             cancelEvent(event);
182         }
183         else
184         {
185             if (typeof(object) == "function")
186             {
187                 Firebug.chrome.select(object, "script");
188                 cancelEvent(event);
189             }
190             else if (event.detail == 2 && !object)
191             {
192                 var panel = row.parentNode.parentNode.domPanel;
193                 if (panel)
194                 {
195                     var rowValue = panel.getRowPropertyValue(row);
196                     if (typeof(rowValue) == "boolean")
197                         panel.setPropertyValue(row, !rowValue);
198                     else
199                         panel.editProperty(row);
200 
201                     cancelEvent(event);
202                 }
203             }
204         }
205     },
206 
207     toggleRow: function(row)
208     {
209         var level = parseInt(row.getAttribute("level"));
210         var toggles = row.parentNode.parentNode.toggles;
211 
212         var panel = row.parentNode.parentNode.domPanel;
213         var target = row.lastChild.firstChild;
214         var isString = hasClass(target,"objectBox-string");
215 
216         if (hasClass(row, "opened"))
217         {
218             removeClass(row, "opened");
219 
220             if (isString)
221             {
222                 var rowValue = panel.getRowPropertyValue(row);
223                 row.lastChild.firstChild.textContent = '"' + cropMultipleLines(rowValue) + '"';
224             }
225             else
226             {
227                 if (toggles)
228                 {
229                     var path = getPath(row);
230 
231                     // Remove the path from the toggle tree
232                     for (var i = 0; i < path.length; ++i)
233                     {
234                         if (i == path.length-1)
235                             delete toggles[path[i]];
236                         else
237                             toggles = toggles[path[i]];
238                     }
239                 }
240 
241                 var rowTag = this.rowTag;
242                 var tbody = row.parentNode;
243 
244                 setTimeout(function()
245                 {
246                     for (var firstRow = row.nextSibling; firstRow; firstRow = row.nextSibling)
247                     {
248                         if (parseInt(firstRow.getAttribute("level")) <= level)
249                             break;
250 
251                         tbody.removeChild(firstRow);
252                     }
253                 }, row.insertTimeout ? row.insertTimeout : 0);
254             }
255         }
256         else
257         {
258             setClass(row, "opened");
259             if (isString)
260             {
261                 var rowValue = panel.getRowPropertyValue(row);
262                 row.lastChild.firstChild.textContent = '"' + rowValue + '"';
263             }
264             else
265             {
266 
267                 if (toggles)
268                 {
269                     var path = getPath(row);
270 
271                     // Mark the path in the toggle tree
272                     for (var i = 0; i < path.length; ++i)
273                     {
274                         var name = path[i];
275                         if (toggles.hasOwnProperty(name))
276                             toggles = toggles[name];
277                         else
278                             toggles = toggles[name] = {};
279                     }
280                 }
281 
282                 var context = panel ? panel.context : null;
283                 var members = Firebug.DOMBasePanel.prototype.getMembers(target.repObject, level+1, context);
284 
285                 var rowTag = this.rowTag;
286                 var lastRow = row;
287 
288                 var delay = 0;
289                 var setSize = members.length;
290                 var rowCount = 1;
291                 while (members.length)
292                 {
293                     setTimeout(function(slice, isLast)
294                     {
295                         if (lastRow.parentNode)
296                         {
297                             var result = rowTag.insertRows({members: slice}, lastRow);
298                             lastRow = result[1];
299                             dispatch([Firebug.A11yModel], 'onMemberRowSliceAdded', [null, result, rowCount, setSize]);
300                             rowCount += insertSliceSize;
301                         }
302                         if (isLast)
303                             delete row.insertTimeout;
304                     }, delay, members.splice(0, insertSliceSize), !members.length);
305 
306                     delay += insertInterval;
307                 }
308 
309                 row.insertTimeout = delay;
310             }
311         }
312     },
313 
314     onClickRowHeader: function(event)
315     {
316         cancelEvent(event);
317 
318         var rowHeader = event.target;
319         if (!hasClass(rowHeader, "memberRowHeader"))
320             return;
321 
322         var row = getAncestorByClass(event.target, "memberRow");
323         if (!row)
324             return;
325 
326         var panel = row.parentNode.parentNode.domPanel;
327         if (panel)
328             panel.breakOnProperty(row);
329     }
330 });
331 
332 const ToolboxPlate = domplate(
333 {
334     tag:
335         DIV({"class": "watchToolbox", _domPanel: "$domPanel", onclick: "$onClick"},
336             IMG({"class": "watchDeleteButton closeButton", src: "blank.gif"})
337         ),
338 
339     onClick: function(event)
340     {
341         var toolbox = event.currentTarget;
342         toolbox.domPanel.deleteWatch(toolbox.watchRow);
343     }
344 });
345 
346 // ************************************************************************************************
347 
348 Firebug.DOMBasePanel = function() {}
349 
350 Firebug.DOMBasePanel.prototype = extend(Firebug.ActivablePanel,
351 {
352     tag: DirTablePlate.tableTag,
353 
354     getRealObject: function(object)
355     {
356         return unwrapObject(object);
357     },
358 
359     rebuild: function(update, scrollTop)
360     {
361         dispatch([Firebug.A11yModel], 'onBeforeDomUpdateSelection', [this]);
362         var members = this.getMembers(this.selection, 0, this.context);
363         this.expandMembers(members, this.toggles, 0, 0, this.context);
364 
365         this.showMembers(members, update, scrollTop);
366     },
367     /*
368      *  @param object a user-level object wrapped in security blanket
369      *  @param level for a.b.c, level is 2
370      *  @param context
371      */
372     getMembers: function(object, level, context)
373     {
374         if (!level)
375             level = 0;
376 
377         var ordinals = [], userProps = [], userClasses = [], userFuncs = [],
378             domProps = [], domFuncs = [], domConstants = [];
379 
380         try
381         {
382             // Special case for "arguments", which is not enumerable by for...in statement.
383             if (isArguments(object))
384                 object = cloneArray(object);
385 
386             var domMembers = getDOMMembers(object);
387             var insecureObject = unwrapObject(object);
388 
389             for (var name in insecureObject)  // enumeration is safe
390             {
391                 // Ignore only global variables (properties of the |window| object).
392                 // javascript.options.strict says ignoreVars is undefined.
393                 if (ignoreVars[name] == 1 && (object instanceof Window))
394                 {
395                     if (FBTrace.DBG_DOM)
396                         FBTrace.sysout("dom.getMembers: ignoreVars: " + name + ", " + level, object);
397                     continue;
398                 }
399 
400                 var val;
401                 try
402                 {
403                     val = insecureObject[name];  // getter is safe
404                 }
405                 catch (exc)
406                 {
407                     // Sometimes we get exceptions trying to access certain members
408                     if (FBTrace.DBG_ERRORS && FBTrace.DBG_DOM)
409                         FBTrace.sysout("dom.getMembers cannot access "+name, exc);
410                 }
411 
412                 var ordinal = parseInt(name);
413                 if (ordinal || ordinal == 0)
414                 {
415                     addMember(object, "ordinal", ordinals, name, val, level, 0, context);
416                 }
417                 else if (typeof(val) == "function")
418                 {
419                     if (isClassFunction(val))
420                         addMember(object, "userClass", userClasses, name, val, level, 0, context);
421                     else if (name in domMembers)
422                         addMember(object, "domFunction", domFuncs, name, val, level, domMembers[name], context);
423                     else
424                         addMember(object, "userFunction", userFuncs, name, val, level, 0, context);
425                 }
426                 else
427                 {
428                     if (name in domMembers)
429                         addMember(object, "dom", domProps, name, val, level, domMembers[name], context);
430                     else if (name in domConstantMap)
431                         addMember(object, "dom", domConstants, name, val, level, 0, context);
432                     else
433                         addMember(object, "user", userProps, name, val, level, 0, context);
434                 }
435             }
436         }
437         catch (exc)
438         {
439             // Sometimes we get exceptions just from trying to iterate the members
440             // of certain objects, like StorageList, but don't let that gum up the works
441             //throw exc;
442             if (FBTrace.DBG_ERRORS && FBTrace.DBG_DOM)
443                 FBTrace.sysout("dom.getMembers FAILS: ", exc);
444         }
445 
446         function sortName(a, b) { return a.name > b.name ? 1 : -1; }
447         function sortOrder(a, b) { return a.order > b.order ? 1 : -1; }
448 
449         var members = [];
450 
451         members.push.apply(members, ordinals);
452 
453         if (Firebug.showUserProps)
454         {
455             userProps.sort(sortName);
456             members.push.apply(members, userProps);
457         }
458 
459         if (Firebug.showUserFuncs)
460         {
461             userClasses.sort(sortName);
462             members.push.apply(members, userClasses);
463 
464             userFuncs.sort(sortName);
465             members.push.apply(members, userFuncs);
466         }
467 
468         if (Firebug.showDOMProps)
469         {
470             domProps.sort(sortName);
471             members.push.apply(members, domProps);
472         }
473 
474         if (Firebug.showDOMFuncs)
475         {
476             domFuncs.sort(sortName);
477             members.push.apply(members, domFuncs);
478         }
479 
480         if (Firebug.showDOMConstants)
481             members.push.apply(members, domConstants);
482 
483         return members;
484     },
485 
486     expandMembers: function (members, toggles, offset, level, context)  // recursion starts with offset=0, level=0
487     {
488         var expanded = 0;
489         for (var i = offset; i < members.length; ++i)
490         {
491             var member = members[i];
492             if (member.level > level)
493                 break;
494 
495             if ( toggles.hasOwnProperty(member.name) )
496             {
497                 member.open = "opened";  // member.level <= level && member.name in toggles.
498                 if (member.type == 'string')
499                     continue;
500                 var newMembers = this.getMembers(member.value, level+1, context);  // sets newMembers.level to level+1
501 
502                 var args = [i+1, 0];
503                 args.push.apply(args, newMembers);
504                 members.splice.apply(members, args);
505                 if (FBTrace.DBG_DOM)
506                 {
507                     FBTrace.sysout("expandMembers member.name", member.name);
508                     FBTrace.sysout("expandMembers toggles", toggles);
509                     FBTrace.sysout("expandMembers toggles[member.name]", toggles[member.name]);
510                     FBTrace.sysout("dom.expandedMembers level: "+level+" member", member);
511                 }
512 
513                 expanded += newMembers.length;
514                 i += newMembers.length + this.expandMembers(members, toggles[member.name], i+1, level+1, context);
515             }
516         }
517 
518         return expanded;
519     },
520 
521     showMembers: function(members, update, scrollTop)
522     {
523         // If we are still in the midst of inserting rows, cancel all pending
524         // insertions here - this is a big speedup when stepping in the debugger
525         if (this.timeouts)
526         {
527             for (var i = 0; i < this.timeouts.length; ++i)
528                 this.context.clearTimeout(this.timeouts[i]);
529             delete this.timeouts;
530         }
531 
532         if (!members.length)
533             return this.showEmptyMembers();
534 
535         var panelNode = this.panelNode;
536         var priorScrollTop = scrollTop == undefined ? panelNode.scrollTop : scrollTop;
537 
538         // If we are asked to "update" the current view, then build the new table
539         // offscreen and swap it in when it's done
540         var offscreen = update && panelNode.firstChild;
541         var dest = offscreen ? this.document : panelNode;
542 
543         var table = this.tag.replace({domPanel: this, toggles: this.toggles}, dest);
544         var tbody = table.lastChild;
545         var rowTag = DirTablePlate.rowTag;
546 
547         // Insert the first slice immediately
548         var setSize = members.length;
549         var slice = members.splice(0, insertSliceSize);
550         var result = rowTag.insertRows({members: slice}, tbody.lastChild);
551         var rowCount = 1;
552         var panel = this;
553         dispatch([Firebug.A11yModel], 'onMemberRowSliceAdded', [panel, result, rowCount, setSize]);
554         var timeouts = [];
555 
556         var delay = 0;
557         while (members.length)
558         {
559             timeouts.push(this.context.setTimeout(function(slice)
560             {
561                 result = rowTag.insertRows({members: slice}, tbody.lastChild);
562                 rowCount += insertSliceSize;
563                 dispatch([Firebug.A11yModel], 'onMemberRowSliceAdded', [panel, result, rowCount, setSize]);
564 
565                 if ((panelNode.scrollHeight+panelNode.offsetHeight) >= priorScrollTop)
566                     panelNode.scrollTop = priorScrollTop;
567             }, delay, members.splice(0, insertSliceSize)));
568 
569             delay += insertInterval;
570         }
571 
572         if (offscreen)
573         {
574             timeouts.push(this.context.setTimeout(function()
575             {
576                 if (panelNode.firstChild)
577                     panelNode.replaceChild(table, panelNode.firstChild);
578                 else
579                     panelNode.appendChild(table);
580 
581                 // Scroll back to where we were before
582                 panelNode.scrollTop = priorScrollTop;
583             }, delay));
584         }
585         else
586         {
587             timeouts.push(this.context.setTimeout(function()
588             {
589                 panelNode.scrollTop = scrollTop == undefined ? 0 : scrollTop;
590             }, delay));
591         }
592         this.timeouts = timeouts;
593     },
594 
595     showEmptyMembers: function()
596     {
597         FirebugReps.Warning.tag.replace({object: "NoMembersWarning"}, this.panelNode);
598     },
599 
600     findPathIndex: function(object)
601     {
602         var pathIndex = -1;
603         for (var i = 0; i < this.objectPath.length; ++i)
604         {
605             if (this.getPathObject(i) == object)
606                 return i;
607         }
608 
609         return -1;
610     },
611 
612     getPathObject: function(index)
613     {
614         var object = this.objectPath[index];
615         if (object instanceof Property)
616             return object.getObject();
617         else
618             return object;
619     },
620 
621     getRowObject: function(row)
622     {
623         var object = getRowOwnerObject(row);
624         return object ? object : this.selection;
625     },
626 
627     getRealRowObject: function(row)
628     {
629         var object = this.getRowObject(row);
630         return this.getRealObject(object);
631     },
632 
633     getRowPropertyValue: function(row)
634     {
635         var object = this.getRealRowObject(row);
636         return this.getObjectPropertyValue(object, row.domObject.name);
637     },
638 
639     getObjectPropertyValue: function(object, propName)
640     {
641         if (!object)
642             return;
643 
644         // Get the value with try-catch statement. This method is used also wihin
645         // getContextMenuItems where the exception would break the context menu.
646         // 1) The Firebug.Debugger.evaluate can throw
647         // 2) object[propName] can also throws in case of e.g. non existing "abc.abc" prop name.
648         try
649         {
650             if (object instanceof jsdIStackFrame)
651                 return Firebug.Debugger.evaluate(propName, this.context);
652             else
653                 return object[propName];
654         }
655         catch (err)
656         {
657             if(FBTrace.DBG_DOM || FBTrace.DBG_ERRORS)
658                 FBTrace.sysout("dom.getObjectPropertyValue; EXCEPTION " + propName, object);
659         }
660     },
661 
662     getRowPathName: function(row)
663     {
664         var name = row.domObject.name;
665         var seperator = "";
666 
667         if(name.match(/^[\d]+$/))//ordinal
668             return ["", "["+name+"]"];
669         else if(name.match(rxIdentifier))//identifier
670             return [".", name];
671         else//map keys
672             return ["", "[\""+name.replace(/\\/g, "\\\\").replace(/"/g,"\\\"") + "\"]"];
673     },
674 
675     copyName: function(row)
676     {
677         var value = this.getRowPathName(row);
678         value = value[1];//don't want the seperator
679         copyToClipboard(value);
680     },
681 
682     copyPath: function(row)
683     {
684         var path = this.getPropertyPath(row);
685         copyToClipboard(path.join(""));
686     },
687 
688     /*
689      * Walk from the current row up to the most ancient parent, building an array.
690      * @return array of property names and separators, eg ['foo','.','bar'].
691      */
692     getPropertyPath: function(row)
693     {
694         var path = [];
695         for(var current = row; current ; current = getParentRow(current))
696             path = this.getRowPathName(current).concat(path);
697         path.splice(0,1); //don't want the first seperator
698         return path;
699     },
700 
701     copyProperty: function(row)
702     {
703         var value = this.getRowPropertyValue(row);
704         copyToClipboard(value);
705     },
706 
707     editProperty: function(row, editValue)
708     {
709         if (hasClass(row, "watchNewRow"))
710         {
711             if (this.context.stopped)
712                 Firebug.Editor.startEditing(row, "");
713             else if (Firebug.Console.isAlwaysEnabled())  // not stopped in debugger, need command line
714             {
715                 if (Firebug.CommandLine.onCommandLineFocus())
716                     Firebug.Editor.startEditing(row, "");
717                 else
718                     row.innerHTML = $STR("warning.Command line blocked?");
719             }
720             else
721                 row.innerHTML = $STR("warning.Console must be enabled");
722         }
723         else if (hasClass(row, "watchRow"))
724         {
725             Firebug.Editor.startEditing(row, getRowName(row));
726         }
727         else
728         {
729             var object = this.getRowObject(row);
730             this.context.thisValue = object;
731 
732             if (!editValue)
733             {
734                 var propValue = this.getRowPropertyValue(row);
735 
736                 var type = typeof(propValue);
737                 if (type == "undefined" || type == "number" || type == "boolean")
738                     editValue = propValue;
739                 else if (type == "string")
740                     editValue = "\"" + escapeJS(propValue) + "\"";
741                 else if (propValue == null)
742                     editValue = "null";
743                 else if (object instanceof Window || object instanceof jsdIStackFrame)
744                     editValue = getRowName(row);
745                 else
746                     editValue = "this." + getRowName(row);
747             }
748 
749             Firebug.Editor.startEditing(row, editValue);
750         }
751     },
752 
753     deleteProperty: function(row)
754     {
755         if (hasClass(row, "watchRow"))
756             this.deleteWatch(row);
757         else
758         {
759             var object = getRowOwnerObject(row);
760             if (!object)
761                 object = this.selection;
762             object = this.getRealObject(object);
763 
764             if (object)
765             {
766                 var name = getRowName(row);
767                 try
768                 {
769                     delete object[name];
770                 }
771                 catch (exc)
772                 {
773                     return;
774                 }
775 
776                 this.rebuild(true);
777                 this.markChange();
778             }
779         }
780     },
781 
782     setPropertyValue: function(row, value)  // value must be string
783     {
784         if(FBTrace.DBG_DOM)
785         {
786             FBTrace.sysout("row: "+row);
787             FBTrace.sysout("value: "+value+" type "+typeof(value), value);
788         }
789 
790         var name = getRowName(row);
791         if (name == "this")
792             return;
793 
794         var object = this.getRealRowObject(row);
795         if (object && !(object instanceof jsdIStackFrame))
796         {
797              // unwrappedJSObject.property = unwrappedJSObject
798              Firebug.CommandLine.evaluate(value, this.context, object, this.context.getGlobalScope(),
799                  function success(result, context)
800                  {
801                      if (FBTrace.DBG_DOM)
802                          FBTrace.sysout("setPropertyValue evaluate success object["+name+"]="+result+" type "+typeof(result), result);
803                      object[name] = result;
804                  },
805                  function failed(exc, context)
806                  {
807                      try
808                      {
809                          if (FBTrace.DBG_DOM)
810                               FBTrace.sysout("setPropertyValue evaluate failed with exc:"+exc+" object["+name+"]="+value+" type "+typeof(value), exc);
811                          // If the value doesn't parse, then just store it as a string.  Some users will
812                          // not realize they're supposed to enter a JavaScript expression and just type
813                          // literal text
814                          object[name] = String(value);  // unwrappedJSobject.property = string
815                      }
816                      catch (exc)
817                      {
818                          return;
819                      }
820                   }
821              );
822         }
823         else if (this.context.stopped)
824         {
825             try
826             {
827                 Firebug.CommandLine.evaluate(name+"="+value, this.context);
828             }
829             catch (exc)
830             {
831                 try
832                 {
833                     // See catch block above...
834                     object[name] = String(value); // unwrappedJSobject.property = string
835                 }
836                 catch (exc)
837                 {
838                     return;
839                 }
840             }
841         }
842 
843         this.rebuild(true);
844         this.markChange();
845     },
846 
847     highlightRow: function(row)
848     {
849         if (this.highlightedRow)
850             cancelClassTimed(this.highlightedRow, "jumpHighlight", this.context);
851 
852         this.highlightedRow = row;
853 
854         if (row)
855             setClassTimed(row, "jumpHighlight", this.context);
856     },
857 
858     breakOnProperty: function(row)
859     {
860         var member = row.domObject;
861         if (!member)
862             return;
863 
864         // Bail out if this property is not breakable.
865         if (!member.breakable)
866             return;
867 
868         //xxxHonza: don't use getRowName to get the prop name. From some reason
869         // unwatch doesn't work if row.firstChild.textContent is used.
870         // It works only from within the watch handler method if the passed param
871         // name is used.
872         var name = member.name;
873         if (name == "this")
874             return;
875 
876         var object = this.getRowObject(row);
877         object = this.getRealObject(object);
878         if (!object)
879             return;
880 
881         // Create new or remove an existing breakpoint.
882         var breakpoints = this.context.dom.breakpoints;
883         var bp = breakpoints.findBreakpoint(object, name);
884         if (bp)
885         {
886             row.removeAttribute("breakpoint");
887             breakpoints.removeBreakpoint(object, name);
888         }
889         else
890         {
891             breakpoints.addBreakpoint(object, name, this, row);
892             row.setAttribute("breakpoint", "true");
893         }
894     },
895 
896     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
897     // extends Panel
898 
899     initialize: function()
900     {
901         this.objectPath = [];
902         this.propertyPath = [];
903         this.viewPath = [];
904         this.pathIndex = -1;
905         this.toggles = {};
906 
907         Firebug.Panel.initialize.apply(this, arguments);
908     },
909 
910     destroy: function(state)
911     {
912         var view = this.viewPath[this.pathIndex];
913         if (view && this.panelNode.scrollTop)
914             view.scrollTop = this.panelNode.scrollTop;
915 
916         if (this.pathIndex > -1)
917             state.pathIndex = this.pathIndex;
918         if (this.viewPath)
919             state.viewPath = this.viewPath;
920         if (this.propertyPath)
921             state.propertyPath = this.propertyPath;
922 
923         if (this.propertyPath.length > 0 && !this.propertyPath[1])
924             state.firstSelection = persistObject(this.getPathObject(1), this.context);
925 
926         if (FBTrace.DBG_DOM)
927             FBTrace.sysout("dom.destroy; state:", state);
928 
929         Firebug.Panel.destroy.apply(this, arguments);
930     },
931 
932     show: function(state)
933     {
934         if (!this.selection)
935         {
936             if (!state)
937             {
938                 this.select(null);
939                 return;
940             }
941             if (state.pathIndex > -1)
942                 this.pathIndex = state.pathIndex;
943             if (state.viewPath)
944                 this.viewPath = state.viewPath;
945             if (state.propertyPath)
946                 this.propertyPath = state.propertyPath;
947 
948             var selectObject = defaultObject = this.getDefaultSelection(this.context);
949 
950             if (state.firstSelection)
951             {
952                 var restored = state.firstSelection(this.context);
953                 if (restored)
954                 {
955                     selectObject = restored;
956                     this.objectPath = [defaultObject, restored];
957                 }
958                 else
959                     this.objectPath = [defaultObject];
960             }
961             else
962                 this.objectPath = [defaultObject];
963 
964             if (this.propertyPath.length > 1)
965                 selectObject = this.resetPaths(selectObject);
966             else
967                 this.propertyPath.push(null);   // Sync with objectPath always containing a defalt object.
968 
969             var selection = state.pathIndex < this.objectPath.length
970                 ? this.getPathObject(state.pathIndex)
971                 : this.getPathObject(this.objectPath.length-1);
972 
973             if (FBTrace.DBG_DOM)
974                 FBTrace.sysout("dom.show; selection:", selection);
975 
976             this.select(selection);
977         }
978     },
979 
980     resetPaths: function(selectObject)
981     {
982         for (var i = 1; i < this.propertyPath.length; ++i)
983         {
984             var name = this.propertyPath[i];
985             if (!name)
986                 continue;
987 
988             var object = selectObject;
989             try
990             {
991                 selectObject = object[name];
992             }
993             catch (exc)
994             {
995                 selectObject = null;
996             }
997 
998             if (selectObject)
999             {
1000                 this.objectPath.push(new Property(object, name));
1001             }
1002             else
1003             {
1004                 // If we can't access a property, just stop
1005                 this.viewPath.splice(i);
1006                 this.propertyPath.splice(i);
1007                 this.objectPath.splice(i);
1008                 selectObject = this.getPathObject(this.objectPath.length-1);
1009                 break;
1010             }
1011         }
1012     },
1013 
1014     hide: function()
1015     {
1016         var view = this.viewPath[this.pathIndex];
1017         if (view && this.panelNode.scrollTop)
1018             view.scrollTop = this.panelNode.scrollTop;
1019     },
1020 
1021     getBreakOnNextTooltip: function(enabled)
1022     {
1023         return (enabled ? $STR("dom.Disable Break On Property Change") :
1024             $STR("dom.Break On Property Change"));
1025     },
1026 
1027     supportsObject: function(object)
1028     {
1029         if (object == null)
1030             return 1000;
1031 
1032         if (typeof(object) == "undefined")
1033             return 1000;
1034         else if (object instanceof SourceLink)
1035             return 0;
1036         else
1037             return 1; // just agree to support everything but not aggressively.
1038     },
1039 
1040     refresh: function()
1041     {
1042         this.rebuild(true);
1043     },
1044 
1045     updateSelection: function(object)
1046     {
1047         if (FBTrace.DBG_DOM)
1048             FBTrace.sysout("dom.updateSelection; object=" + object, object);
1049 
1050         var previousIndex = this.pathIndex;
1051         var previousView = previousIndex == -1 ? null : this.viewPath[previousIndex];
1052 
1053         var newPath = this.pathToAppend;
1054         delete this.pathToAppend;
1055 
1056         var pathIndex = this.findPathIndex(object);
1057         if (newPath || pathIndex == -1)
1058         {
1059             this.toggles = {};
1060 
1061             if (newPath)
1062             {
1063                 // Remove everything after the point where we are inserting, so we
1064                 // essentially replace it with the new path
1065                 if (previousView)
1066                 {
1067                     if (this.panelNode.scrollTop)
1068                         previousView.scrollTop = this.panelNode.scrollTop;
1069 
1070                     this.objectPath.splice(previousIndex+1);
1071                     this.propertyPath.splice(previousIndex+1);
1072                     this.viewPath.splice(previousIndex+1);
1073                 }
1074 
1075                 var value = this.getPathObject(previousIndex);
1076                 if (!value)
1077                 {
1078                     if (FBTrace.DBG_ERRORS)
1079                         FBTrace.sysout("dom.updateSelection no pathObject for "+previousIndex+"\n");
1080                     return;
1081                 }
1082 
1083                 for (var i = 0; i < newPath.length; ++i)
1084                 {
1085                     var name = newPath[i];
1086                     var object = value;
1087                     try
1088                     {
1089                         value = value[name];
1090                     }
1091                     catch(exc)
1092                     {
1093                         if (FBTrace.DBG_ERRORS)
1094                                 FBTrace.sysout("dom.updateSelection FAILS at path_i="+i+" for name:"+name+"\n");
1095                         return;
1096                     }
1097 
1098                     ++this.pathIndex;
1099                     this.objectPath.push(new Property(object, name));
1100                     this.propertyPath.push(name);
1101                     this.viewPath.push({toggles: this.toggles, scrollTop: 0});
1102                 }
1103             }
1104             else
1105             {
1106                 this.toggles = {};
1107 
1108                 var win = this.context.getGlobalScope();
1109                 if (object == win)
1110                 {
1111                     this.pathIndex = 0;
1112                     this.objectPath = [win];
1113                     this.propertyPath = [null];
1114                     this.viewPath = [{toggles: this.toggles, scrollTop: 0}];
1115                 }
1116                 else
1117                 {
1118                     this.pathIndex = 1;
1119                     this.objectPath = [win, object];
1120                     this.propertyPath = [null, null];
1121                     this.viewPath = [
1122                         {toggles: {}, scrollTop: 0},
1123                         {toggles: this.toggles, scrollTop: 0}
1124                     ];
1125                 }
1126             }
1127 
1128             this.panelNode.scrollTop = 0;
1129             this.rebuild();
1130         }
1131         else
1132         {
1133             this.pathIndex = pathIndex;
1134 
1135             var view = this.viewPath[pathIndex];
1136             this.toggles = view ? view.toggles : {};
1137 
1138             // Persist the current scroll location
1139             if (previousView && this.panelNode.scrollTop)
1140                 previousView.scrollTop = this.panelNode.scrollTop;
1141 
1142             this.rebuild(false, view ? view.scrollTop : 0);
1143         }
1144 
1145     },
1146 
1147     getObjectPath: function(object)
1148     {
1149         return this.objectPath;
1150     },
1151 
1152     getDefaultSelection: function()
1153     {
1154         return this.context.getGlobalScope();
1155     },
1156 
1157     updateOption: function(name, value)
1158     {
1159         const optionMap = {showUserProps: 1, showUserFuncs: 1, showDOMProps: 1,
1160             showDOMFuncs: 1, showDOMConstants: 1};
1161         if ( optionMap.hasOwnProperty(name) )
1162             this.rebuild(true);
1163     },
1164 
1165     getOptionsMenuItems: function()
1166     {
1167         return [
1168             optionMenu("ShowUserProps", "showUserProps"),
1169             optionMenu("ShowUserFuncs", "showUserFuncs"),
1170             optionMenu("ShowDOMProps", "showDOMProps"),
1171             optionMenu("ShowDOMFuncs", "showDOMFuncs"),
1172             optionMenu("ShowDOMConstants", "showDOMConstants"),
1173             "-",
1174             {label: "Refresh", command: bindFixed(this.rebuild, this, true) }
1175         ];
1176     },
1177 
1178     getContextMenuItems: function(object, target)
1179     {
1180         if (FBTrace.DBG_DOM)
1181             FBTrace.sysout("dom.getContextMenuItems;", object);
1182 
1183         var row = getAncestorByClass(target, "memberRow");
1184 
1185         var items = [];
1186 
1187         if (row)
1188         {
1189             var rowName = getRowName(row);
1190             var rowObject = this.getRowObject(row);
1191             var rowValue = this.getRowPropertyValue(row);
1192 
1193             var isWatch = hasClass(row, "watchRow");
1194             var isStackFrame = rowObject instanceof jsdIStackFrame;
1195 
1196             items.push(
1197                 "-",
1198                 {label: "Copy Name",
1199                     command: bindFixed(this.copyName, this, row) },
1200                 {label: "Copy Path",
1201                     command: bindFixed(this.copyPath, this, row) }
1202             );
1203 
1204             if (typeof(rowValue) == "string" || typeof(rowValue) == "number")
1205             {
1206                 // Functions already have a copy item in their context menu
1207                 items.push(
1208                     {label: "CopyValue",
1209                         command: bindFixed(this.copyProperty, this, row) }
1210                 );
1211             }
1212 
1213             items.push(
1214                 "-",
1215                 {label: isWatch ? "EditWatch" : (isStackFrame ? "EditVariable" : "EditProperty"),
1216                     command: bindFixed(this.editProperty, this, row) }
1217             );
1218 
1219             if (isWatch || (!isStackFrame && !isDOMMember(rowObject, rowName)))
1220             {
1221                 items.push(
1222                     {label: isWatch ? "DeleteWatch" : "DeleteProperty",
1223                         command: bindFixed(this.deleteProperty, this, row) }
1224                 );
1225             }
1226 
1227             var member = row ? row.domObject : null;
1228             if (!isDOMMember(rowObject, rowName) && member && member.breakable)
1229             {
1230                 items.push(
1231                     "-",
1232                     {label: "html.dom.label.Break On Property Change", type: "checkbox",
1233                         checked: this.context.dom.breakpoints.findBreakpoint(rowObject, rowName),
1234                         command: bindFixed(this.breakOnProperty, this, row)}
1235                 );
1236             }
1237         }
1238 
1239         items.push(
1240             "-",
1241             {label: "Refresh", command: bindFixed(this.rebuild, this, true) }
1242         );
1243 
1244         return items;
1245     },
1246 
1247     getEditor: function(target, value)
1248     {
1249         if (!this.editor)
1250             this.editor = new DOMEditor(this.document);
1251 
1252         return this.editor;
1253     }
1254 });
1255 
1256 // ************************************************************************************************
1257 
1258 var DOMMainPanel = Firebug.DOMPanel = function () {};
1259 
1260 Firebug.DOMPanel.DirTable = DirTablePlate;
1261 
1262 DOMMainPanel.prototype = extend(Firebug.DOMBasePanel.prototype,
1263 {
1264     selectRow: function(row, target)
1265     {
1266         if (!target)
1267             target = row.lastChild.firstChild;
1268 
1269         if (!target || !target.repObject)
1270             return;
1271 
1272         this.pathToAppend = getPath(row);
1273 
1274         // If the object is inside an array, look up its index
1275         var valueBox = row.lastChild.firstChild;
1276         if (hasClass(valueBox, "objectBox-array"))
1277         {
1278             var arrayIndex = FirebugReps.Arr.getItemIndex(target);
1279             this.pathToAppend.push(arrayIndex);
1280         }
1281 
1282         // Make sure we get a fresh status path for the object, since otherwise
1283         // it might find the object in the existing path and not refresh it
1284         Firebug.chrome.clearStatusPath();
1285 
1286         this.select(target.repObject, true);
1287     },
1288 
1289     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1290 
1291     onClick: function(event)
1292     {
1293         var repNode = Firebug.getRepNode(event.target);
1294         if (repNode)
1295         {
1296             var row = getAncestorByClass(event.target, "memberRow");
1297             if (row)
1298             {
1299                 this.selectRow(row, repNode);
1300                 cancelEvent(event);
1301             }
1302         }
1303     },
1304 
1305     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1306     // extends Panel
1307 
1308     name: "dom",
1309     searchable: true,
1310     statusSeparator: ">",
1311 
1312     initialize: function()
1313     {
1314         this.onClick = bind(this.onClick, this);
1315 
1316         Firebug.DOMBasePanel.prototype.initialize.apply(this, arguments);
1317     },
1318 
1319     initializeNode: function(oldPanelNode)
1320     {
1321         this.panelNode.addEventListener("click", this.onClick, false);
1322         dispatch([Firebug.A11yModel], 'onInitializeNode', [this, 'console']);
1323     },
1324 
1325     destroyNode: function()
1326     {
1327         this.panelNode.removeEventListener("click", this.onClick, false);
1328         dispatch([Firebug.A11yModel], 'onDestroyNode', [this, 'console']);
1329     },
1330 
1331     search: function(text, reverse)
1332     {
1333         if (!text)
1334         {
1335             delete this.currentSearch;
1336             this.highlightRow(null);
1337             return false;
1338         }
1339 
1340         var row;
1341         if (this.currentSearch && text == this.currentSearch.text)
1342             row = this.currentSearch.findNext(true, undefined, reverse, Firebug.Search.isCaseSensitive(text));
1343         else
1344         {
1345             function findRow(node) { return getAncestorByClass(node, "memberRow"); }
1346             this.currentSearch = new TextSearch(this.panelNode, findRow);
1347             row = this.currentSearch.find(text, reverse, Firebug.Search.isCaseSensitive(text));
1348         }
1349 
1350         if (row)
1351         {
1352             var sel = this.document.defaultView.getSelection();
1353             sel.removeAllRanges();
1354             sel.addRange(this.currentSearch.range);
1355 
1356             scrollIntoCenterView(row, this.panelNode);
1357 
1358             this.highlightRow(row);
1359             dispatch([Firebug.A11yModel], 'onDomSearchMatchFound', [this, text, row]);
1360             return true;
1361         }
1362         else
1363         {
1364             dispatch([Firebug.A11yModel], 'onDomSearchMatchFound', [this, text, null]);
1365             return false;
1366         }
1367     }
1368 });
1369 
1370 // ************************************************************************************************
1371 
1372 function DOMSidePanel() {}
1373 
1374 DOMSidePanel.prototype = extend(Firebug.DOMBasePanel.prototype,
1375 {
1376     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1377     // extends Panel
1378 
1379     name: "domSide",
1380     parentPanel: "html",
1381     order: 3,
1382 
1383     initializeNode: function(oldPanelNode)
1384     {
1385         dispatch([Firebug.A11yModel], 'onInitializeNode', [this, 'console']);
1386     },
1387 
1388     destroyNode: function()
1389     {
1390         dispatch([Firebug.A11yModel], 'onDestroyNode', [this, 'console']);
1391     },
1392 });
1393 
1394 // ************************************************************************************************
1395 
1396 function WatchPanel() {}
1397 
1398 WatchPanel.prototype = extend(Firebug.DOMBasePanel.prototype,
1399 {
1400     tag: DirTablePlate.watchTag,
1401 
1402     rebuild: function()
1403     {
1404         this.updateSelection(this.selection);
1405     },
1406 
1407     showEmptyMembers: function()
1408     {
1409         this.tag.replace({domPanel: this, toggles: {}}, this.panelNode);
1410     },
1411 
1412     addWatch: function(expression)
1413     {
1414         if (!this.watches)
1415             this.watches = [];
1416 
1417         this.watches.splice(0, 0, expression);
1418         this.rebuild(true);
1419     },
1420 
1421     removeWatch: function(expression)
1422     {
1423         if (!this.watches)
1424             return;
1425 
1426         var index = this.watches.indexOf(expression);
1427         if (index != -1)
1428             this.watches.splice(index, 1);
1429     },
1430 
1431     editNewWatch: function(value)
1432     {
1433         var watchNewRow = this.panelNode.getElementsByClassName("watchNewRow").item(0);
1434         if (watchNewRow)
1435             this.editProperty(watchNewRow, value);
1436     },
1437 
1438     setWatchValue: function(row, value)
1439     {
1440         var rowIndex = getWatchRowIndex(row);
1441         this.watches[rowIndex] = value;
1442         this.rebuild(true);
1443     },
1444 
1445     deleteWatch: function(row)
1446     {
1447         var rowIndex = getWatchRowIndex(row);
1448         this.watches.splice(rowIndex, 1);
1449         this.rebuild(true);
1450 
1451         this.context.setTimeout(bindFixed(function()
1452         {
1453             this.showToolbox(null);
1454         }, this));
1455     },
1456 
1457     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1458 
1459     showToolbox: function(row)
1460     {
1461         var toolbox = this.getToolbox();
1462         if (row)
1463         {
1464             if (hasClass(row, "editing"))
1465                 return;
1466 
1467             toolbox.watchRow = row;
1468 
1469             var offset = getClientOffset(row);
1470             toolbox.style.top = offset.y + "px";
1471             this.panelNode.appendChild(toolbox);
1472         }
1473         else
1474         {
1475             delete toolbox.watchRow;
1476             if (toolbox.parentNode)
1477                 toolbox.parentNode.removeChild(toolbox);
1478         }
1479     },
1480 
1481     getToolbox: function()
1482     {
1483         if (!this.toolbox)
1484             this.toolbox = ToolboxPlate.tag.replace({domPanel: this}, this.document);
1485 
1486         return this.toolbox;
1487     },
1488 
1489     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1490 
1491     onMouseDown: function(event)
1492     {
1493         var watchNewRow = getAncestorByClass(event.target, "watchNewRow");
1494         if (watchNewRow)
1495         {
1496             this.editProperty(watchNewRow);
1497             cancelEvent(event);
1498         }
1499     },
1500 
1501     onMouseOver: function(event)
1502     {
1503         var watchRow = getAncestorByClass(event.target, "watchRow");
1504         if (watchRow)
1505             this.showToolbox(watchRow);
1506     },
1507 
1508     onMouseOut: function(event)
1509     {
1510         if (isAncestor(event.relatedTarget, this.getToolbox()))
1511             return;
1512 
1513         var watchRow = getAncestorByClass(event.relatedTarget, "watchRow");
1514         if (!watchRow)
1515             this.showToolbox(null);
1516     },
1517 
1518     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1519     // extends Panel
1520 
1521     name: "watches",
1522     order: 0,
1523     parentPanel: "script",
1524 
1525     initialize: function()
1526     {
1527         this.onMouseDown = bind(this.onMouseDown, this);
1528         this.onMouseOver = bind(this.onMouseOver, this);
1529         this.onMouseOut = bind(this.onMouseOut, this);
1530 
1531         Firebug.DOMBasePanel.prototype.initialize.apply(this, arguments);
1532     },
1533 
1534     destroy: function(state)
1535     {
1536         state.watches = this.watches;
1537 
1538         Firebug.Panel.destroy.apply(this, arguments);
1539     },
1540 
1541     show: function(state)
1542     {
1543         if (state && state.watches)
1544             this.watches = state.watches;
1545     },
1546 
1547     initializeNode: function(oldPanelNode)
1548     {
1549         this.panelNode.addEventListener("mousedown", this.onMouseDown, false);
1550         this.panelNode.addEventListener("mouseover", this.onMouseOver, false);
1551         this.panelNode.addEventListener("mouseout", this.onMouseOut, false);
1552         dispatch([Firebug.A11yModel], "onInitializeNode", [this, 'console']);
1553     },
1554 
1555     destroyNode: function()
1556     {
1557         this.panelNode.removeEventListener("mousedown", this.onMouseDown, false);
1558         this.panelNode.removeEventListener("mouseover", this.onMouseOver, false);
1559         this.panelNode.removeEventListener("mouseout", this.onMouseOut, false);
1560         dispatch([Firebug.A11yModel], "onDestroyNode", [this, 'console']);
1561     },
1562 
1563     refresh: function()
1564     {
1565         this.rebuild(true);
1566 
1567     },
1568 
1569     updateSelection: function(object)
1570     {
1571         dispatch([Firebug.A11yModel], 'onBeforeDomUpdateSelection', [this]);
1572         var frame = this.context.currentFrame;
1573 
1574         var newFrame = frame && frame.isValid && frame.script != this.lastScript;
1575         if (newFrame)
1576         {
1577             this.toggles = {};
1578             this.lastScript = frame.script;
1579         }
1580 
1581         var members = [];
1582 
1583         if (this.watches)
1584         {
1585             for (var i = 0; i < this.watches.length; ++i)
1586             {
1587                 var expr = this.watches[i];
1588                 var value = null;
1589                 Firebug.CommandLine.evaluate(expr, this.context, null, this.context.getGlobalScope(),
1590                     function success(result, context)
1591                     {
1592                         value = result;
1593                     },
1594                     function failed(result, context)
1595                     {
1596                         var exc = result;
1597                         value = new ErrorCopy(exc+"");
1598                     }
1599                 );
1600 
1601                 addMember(object, "watch", members, expr, value, 0);
1602             }
1603         }
1604 
1605         if (frame && frame.isValid)
1606         {
1607             var thisVar = unwrapIValue(frame.thisValue);
1608             addMember(object, "user", members, "this", thisVar, 0);
1609 
1610             var scopeChain = this.generateScopeChain(frame.scope);
1611             addMember(object, "scopes", members, "scopeChain", scopeChain, 0);
1612 
1613             members.push.apply(members, this.getMembers(scopeChain[0], 0, this.context));
1614         }
1615 
1616         this.expandMembers(members, this.toggles, 0, 0, this.context);
1617         this.showMembers(members, !newFrame);
1618     },
1619 
1620     generateScopeChain: function (scope)
1621     {
1622         var ret = [];
1623         while (scope) {
1624             var scopeVars;
1625             // getWrappedValue will not contain any variables for closure
1626             // scopes, so we want to special case this to get all variables
1627             // in all cases.
1628             if (scope.jsClassName == "Call") {
1629                 scopeVars = {};
1630                 var listValue = {value: null}, lengthValue = {value: 0};
1631                 scope.getProperties(listValue, lengthValue);
1632 
1633                 for (var i = 0; i < lengthValue.value; ++i)
1634                 {
1635                     var prop = listValue.value[i];
1636                     var name = unwrapIValue(prop.name);
1637                     if (ignoreVars[name] == 1)
1638                     {
1639                         if (FBTrace.DBG_DOM)
1640                             FBTrace.sysout("dom.generateScopeChain: ignoreVars: " + name);
1641                         continue;
1642                     }
1643 
1644                     scopeVars[name] = unwrapIValue(prop.value);
1645                 }
1646             } else {
1647                 scopeVars = unwrapIValue(scope);
1648             }
1649 
1650             if (scopeVars && scopeVars.hasOwnProperty)
1651             {
1652                 if (!scopeVars.hasOwnProperty("toString")) {
1653                     (function() {
1654                         var className = scope.jsClassName;
1655                         scopeVars.toString = function() {
1656                             return $STR(className + " Scope");
1657                         };
1658                     })();
1659                 }
1660 
1661                 ret.push(scopeVars);
1662             }
1663             else
1664             {
1665                 if (FBTrace.DBG_ERRORS)
1666                     FBTrace.sysout("dom .generateScopeChain: bad scopeVars");
1667             }
1668             scope = scope.jsParent;
1669         }
1670 
1671         ret.toString = function() {
1672             return $STR("Scope Chain");
1673         };
1674 
1675         return ret;
1676     },
1677 
1678 });
1679 
1680 // ************************************************************************************************
1681 // Local Helpers
1682 
1683 function DOMEditor(doc)
1684 {
1685     this.box = this.tag.replace({}, doc, this);
1686     this.input = this.box;
1687 
1688     this.tabNavigation = false;
1689     this.tabCompletion = true;
1690     this.completeAsYouType = false;
1691     this.fixedWidth = true;
1692 
1693     this.autoCompleter = Firebug.CommandLine.autoCompleter;
1694 }
1695 
1696 DOMEditor.prototype = domplate(Firebug.InlineEditor.prototype,
1697 {
1698     tag:
1699         INPUT({"class": "fixedWidthEditor a11yFocusNoTab",
1700             type: "text", title:$STR("NewWatch"),
1701             oninput: "$onInput", onkeypress: "$onKeyPress"}),
1702 
1703     endEditing: function(target, value, cancel)
1704     {
1705         // XXXjoe Kind of hackish - fix me
1706         delete this.panel.context.thisValue;
1707 
1708         if (cancel || value == "")
1709             return;
1710 
1711         var row = getAncestorByClass(target, "memberRow");
1712         dispatch([Firebug.A11yModel], 'onWatchEndEditing', [this.panel]);
1713         if (!row)
1714             this.panel.addWatch(value);
1715         else if (hasClass(row, "watchRow"))
1716             this.panel.setWatchValue(row, value);
1717         else
1718             this.panel.setPropertyValue(row, value);
1719     }
1720 });
1721 
1722 // ************************************************************************************************
1723 // Local Helpers
1724 
1725 function isClassFunction(fn)
1726 {
1727     try
1728     {
1729         for (var name in fn.prototype)
1730             return true;
1731     } catch (exc) {}
1732     return false;
1733 }
1734 
1735 function isArguments(obj)
1736 {
1737     try
1738     {
1739         return isFinite(obj.length) && obj.length > 0 && typeof obj.callee === "function";
1740     } catch (exc) {}
1741     return false;
1742 }
1743 
1744 function addMember(object, type, props, name, value, level, order, context)
1745 {
1746     var rep = Firebug.getRep(value);    // do this first in case a call to instanceof reveals contents
1747     var tag = rep.shortTag ? rep.shortTag : rep.tag;
1748 
1749     var valueType = typeof(value);
1750     var hasChildren = hasProperties(value) && !(value instanceof ErrorCopy) &&
1751         (valueType == "function" || (valueType == "object" && value != null)
1752         || (valueType == "string" && value.length > Firebug.stringCropLength));
1753 
1754     // Special case for "arguments", which is not enumerable by for...in statement
1755     // and so, hasProperties always returns false.
1756     if (!hasChildren && value) // arguments will never be falsy if the arguments exist
1757         hasChildren = isArguments(value);
1758 
1759     var member = {
1760         object: object,
1761         name: name,
1762         value: value,
1763         type: type,
1764         rowClass: "memberRow-"+type,
1765         open: "",
1766         order: order,
1767         level: level,
1768         indent: level*16,
1769         hasChildren: hasChildren,
1770         tag: tag
1771     };
1772 
1773     // The context doesn't have to be specified (e.g. in case of Watch panel that is based
1774     // on the same template as the DOM panel, but doesn't show any breakpoints).
1775     if (context)
1776     {
1777         // xxxHonza: Support for object change not implemented yet.
1778         member.breakable = !hasChildren;
1779 
1780         // xxxHonza: Disable breaking on direct window properties, see #520572
1781         if (object instanceof Ci.nsIDOMWindow)
1782             member.breakable = false;
1783 
1784         var breakpoints = context.dom.breakpoints;
1785         var bp = breakpoints.findBreakpoint(object, name);
1786         if (bp)
1787         {
1788             member.breakpoint = true;
1789             member.disabledBreakpoint = !bp.checked;
1790         }
1791     }
1792 
1793     // If the property is implemented using a getter function (and there is no setter
1794     // implemented) use a "get" prefix that is displayed in the UI.
1795     var o = unwrapObject(object);
1796     member.prefix = (o.__lookupGetter__(name) && !o.__lookupSetter__(name)) ? "get " : "";
1797 
1798     props.push(member);
1799     return member;
1800 }
1801 
1802 function getWatchRowIndex(row)
1803 {
1804     var index = -1;
1805     for (; row && hasClass(row, "watchRow"); row = row.previousSibling)
1806         ++index;
1807     return index;
1808 }
1809 
1810 function getRowName(row)
1811 {
1812     var labelNode = row.getElementsByClassName("memberLabelCell").item(0);
1813     return labelNode.textContent;
1814 }
1815 
1816 function getRowValue(row)
1817 {
1818     var valueNode = row.getElementsByClassName("memberValueCell").item(0);
1819     return valueNode.firstChild.repObject;
1820 }
1821 
1822 function getRowOwnerObject(row)
1823 {
1824     var parentRow = getParentRow(row);
1825     if (parentRow)
1826         return getRowValue(parentRow);
1827 }
1828 
1829 function getParentRow(row)
1830 {
1831     var level = parseInt(row.getAttribute("level"))-1;
1832     for (row = row.previousSibling; row; row = row.previousSibling)
1833     {
1834         if (parseInt(row.getAttribute("level")) == level)
1835             return row;
1836     }
1837 }
1838 
1839 function getPath(row)
1840 {
1841     var name = getRowName(row);
1842     var path = [name];
1843 
1844     var level = parseInt(row.getAttribute("level"))-1;
1845     for (row = row.previousSibling; row; row = row.previousSibling)
1846     {
1847         if (parseInt(row.getAttribute("level")) == level)
1848         {
1849             var name = getRowName(row);
1850             path.splice(0, 0, name);
1851 
1852             --level;
1853         }
1854     }
1855 
1856     return path;
1857 }
1858 
1859 function findRow(parentNode, object)
1860 {
1861     var rows = parentNode.getElementsByClassName("memberRow");
1862     for (var i=0; i<rows.length; i++)
1863     {
1864         var row = rows[i];
1865         if (object == row.domObject.object)
1866             return row;
1867     }
1868 
1869     return row;
1870 }
1871 
1872 // ************************************************************************************************
1873 
1874 Firebug.DOMModule.DebuggerListener =
1875 {
1876     getBreakpoints: function(context, groups)
1877     {
1878         if (!context.dom.breakpoints.isEmpty())
1879             groups.push(context.dom.breakpoints);
1880     }
1881 };
1882 
1883 Firebug.DOMModule.BreakpointRep = domplate(Firebug.Rep,
1884 {
1885     inspectable: false,
1886 
1887     tag:
1888         DIV({"class": "breakpointRow focusRow", _repObject: "$bp",
1889             role: "option", "aria-checked": "$bp.checked"},
1890             DIV({"class": "breakpointBlockHead", onclick: "$onEnable"},
1891                 INPUT({"class": "breakpointCheckbox", type: "checkbox",
1892                     _checked: "$bp.checked", tabindex : "-1"}),
1893                 SPAN({"class": "breakpointName"}, "$bp.propName"),
1894                 IMG({"class": "closeButton", src: "blank.gif", onclick: "$onRemove"})
1895             ),
1896             DIV({"class": "breakpointCode"},
1897                 TAG("$bp.object|getObjectTag", {object: "$bp.object"})
1898             )
1899         ),
1900 
1901     getObjectTag: function(object)
1902     {
1903         var rep = Firebug.getRep(object);
1904         return rep.shortTag ? rep.shortTag : rep.tag;
1905     },
1906 
1907     onRemove: function(event)
1908     {
1909         cancelEvent(event);
1910 
1911         if (!hasClass(event.target, "closeButton"))
1912             return;
1913 
1914         var bpPanel = Firebug.getElementPanel(event.target);
1915         var context = bpPanel.context;
1916 
1917         // Remove from list of breakpoints.
1918         var row = getAncestorByClass(event.target, "breakpointRow");
1919         var bp = row.repObject;
1920         context.dom.breakpoints.removeBreakpoint(bp.object, bp.propName);
1921 
1922         // Remove from the UI.
1923         bpPanel.noRefresh = true;
1924         bpPanel.removeRow(row);
1925         bpPanel.noRefresh = false;
1926 
1927         var domPanel = context.getPanel("dom", true);
1928         if (domPanel)
1929         {
1930             var domRow = findRow(domPanel.panelNode, bp.object);
1931             if (domRow)
1932             {
1933                 domRow.removeAttribute("breakpoint");
1934                 domRow.removeAttribute("disabledBreakpoint");
1935             }
1936         }
1937     },
1938 
1939     onEnable: function(event)
1940     {
1941         var checkBox = event.target;
1942         if (!hasClass(checkBox, "breakpointCheckbox"))
1943             return;
1944 
1945         var bpPanel = Firebug.getElementPanel(event.target);
1946         var context = bpPanel.context;
1947 
1948         var bp = getAncestorByClass(checkBox, "breakpointRow").repObject;
1949         bp.checked = checkBox.checked;
1950 
1951         var domPanel = context.getPanel("dom", true);
1952         if (domPanel)
1953         {
1954             var row = findRow(domPanel.panelNode, bp.object);
1955             if (row)
1956                 row.setAttribute("disabledBreakpoint", bp.checked ? "false" : "true");
1957         }
1958     },
1959 
1960     supportsObject: function(object)
1961     {
1962         return object instanceof Breakpoint;
1963     }
1964 });
1965 
1966 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1967 
1968 function Breakpoint(object, propName, objectPath, context)
1969 {
1970     this.context = context;
1971     this.propName = propName;
1972     this.objectPath = objectPath;
1973     this.object = object;
1974     this.checked = true;
1975 }
1976 
1977 Breakpoint.prototype =
1978 {
1979     watchProperty: function()
1980     {
1981         if (FBTrace.DBG_DOM)
1982             FBTrace.sysout("dom.watch; property: " + this.propName);
1983 
1984         if (!this.object)
1985             return;
1986 
1987         try
1988         {
1989             var self = this;
1990             this.object.watch(this.propName, function handler(prop, oldval, newval)
1991             {
1992                 // XXXjjb Beware: in playing with this feature I hit too much recursion multiple times with console.log
1993                 // TODO Do something cute in the UI with the error bubble thing
1994                 if (self.checked)
1995                 {
1996                     self.context.breakingCause = {
1997                         title: $STR("dom.Break On Property"),
1998                         message: cropString(prop, 200),
1999                         prevValue: oldval,
2000                         newValue: newval
2001                     };
2002 
2003                     Firebug.Breakpoint.breakNow(self.context.getPanel("dom", true));
2004                 }
2005                 return newval;
2006             });
2007         }
2008         catch (exc)
2009         {
2010             if (FBTrace.DBG_ERRORS)
2011                 FBTrace.sysout("dom.watch; object FAILS " + exc, exc);
2012             return false;
2013         }
2014 
2015         return true;
2016     },
2017 
2018     unwatchProperty: function()
2019     {
2020         if (FBTrace.DBG_DOM)
2021             FBTrace.sysout("dom.unwatch; property: " + this.propName, this.object);
2022 
2023         if (!this.object)
2024             return;
2025 
2026         try
2027         {
2028             this.object.unwatch(this.propName);
2029         }
2030         catch (exc)
2031         {
2032             if (FBTrace.DBG_ERRORS)
2033                 FBTrace.sysout("dom.unwatch; object FAILS " + exc, exc);
2034         }
2035     }
2036 }
2037 
2038 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2039 
2040 function DOMBreakpointGroup()
2041 {
2042     this.breakpoints = [];
2043 }
2044 
2045 DOMBreakpointGroup.prototype = extend(new Firebug.Breakpoint.BreakpointGroup(),
2046 {
2047     name: "domBreakpoints",
2048     title: $STR("dom.label.DOM Breakpoints"),
2049 
2050     addBreakpoint: function(object, propName, panel, row)
2051     {
2052         var path = panel.getPropertyPath(row);
2053         path.pop();
2054 
2055         // We don't want the last dot.
2056         if (path.length > 0 && path[path.length-1] == ".")
2057             path.pop();
2058 
2059         var objectPath = path.join("");
2060         if (FBTrace.DBG_DOM)
2061             FBTrace.sysout("dom.addBreakpoint; " + objectPath, path);
2062 
2063         var bp = new Breakpoint(object, propName, objectPath, panel.context);
2064         if (bp.watchProperty());
2065             this.breakpoints.push(bp);
2066     },
2067 
2068     removeBreakpoint: function(object, propName)
2069     {
2070         var bp = this.findBreakpoint(object, propName);
2071         if (bp)
2072         {
2073             bp.unwatchProperty();
2074             remove(this.breakpoints, bp);
2075         }
2076     },
2077 
2078     matchBreakpoint: function(bp, args)
2079     {
2080         var object = args[0];
2081         var propName = args[1];
2082         return bp.object == object && bp.propName == propName;
2083     },
2084 
2085     // Persistence
2086     load: function(context)
2087     {
2088         var panelState = getPersistedState(context, "dom");
2089         if (panelState.breakpoints)
2090             this.breakpoints = panelState.breakpoints;
2091 
2092         this.enumerateBreakpoints(function(bp)
2093         {
2094             try
2095             {
2096                 // xxxHonza: Firebug.CommandLine.evaluate should be reused if possible.
2097                 // xxxJJB: The Components.utils.evalInSandbox fails from some reason.
2098                 var expr = "context.window.wrappedJSObject." + bp.objectPath;
2099                 bp.object = eval(expr);
2100                 bp.watchProperty();
2101 
2102                 if (FBTrace.DBG_DOM)
2103                     FBTrace.sysout("dom.DOMBreakpointGroup.load; " + bp.objectPath, bp);
2104             }
2105             catch (err)
2106             {
2107                 if (FBTrace.DBG_ERROR || FBTrace.DBG_DOM)
2108                     FBTrace.sysout("dom.DOMBreakpointGroup.load; ERROR " + bp.objectPath, err);
2109             }
2110         });
2111     },
2112 
2113     store: function(context)
2114     {
2115         this.enumerateBreakpoints(function(bp)
2116         {
2117             bp.object = null;
2118         });
2119 
2120         var panelState = getPersistedState(context, "dom");
2121         panelState.breakpoints = this.breakpoints;
2122     },
2123 });
2124 
2125 // ************************************************************************************************
2126 
2127 Firebug.registerModule(Firebug.DOMModule);
2128 Firebug.registerPanel(DOMMainPanel);
2129 Firebug.registerPanel(DOMSidePanel);
2130 Firebug.registerPanel(WatchPanel);
2131 Firebug.registerRep(Firebug.DOMModule.BreakpointRep);
2132 
2133 // ************************************************************************************************
2134 
2135 }});
2136 
2137