browser/devtools/shared/widgets/VariablesView.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 "use strict";
     8 const Ci = Components.interfaces;
     9 const Cu = Components.utils;
    11 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
    12 const LAZY_EMPTY_DELAY = 150; // ms
    13 const LAZY_EXPAND_DELAY = 50; // ms
    14 const SCROLL_PAGE_SIZE_DEFAULT = 0;
    15 const APPEND_PAGE_SIZE_DEFAULT = 500;
    16 const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
    17 const PAGE_SIZE_MAX_JUMPS = 30;
    18 const SEARCH_ACTION_MAX_DELAY = 300; // ms
    19 const ITEM_FLASH_DURATION = 300 // ms
    21 Cu.import("resource://gre/modules/Services.jsm");
    22 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    23 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    24 Cu.import("resource://gre/modules/devtools/event-emitter.js");
    25 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
    26 Cu.import("resource://gre/modules/Task.jsm");
    27 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
    29 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
    30   "resource://gre/modules/devtools/Loader.jsm");
    32 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    33   "resource://gre/modules/PluralForm.jsm");
    35 XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
    36   "@mozilla.org/widget/clipboardhelper;1",
    37   "nsIClipboardHelper");
    39 Object.defineProperty(this, "WebConsoleUtils", {
    40   get: function() {
    41     return devtools.require("devtools/toolkit/webconsole/utils").Utils;
    42   },
    43   configurable: true,
    44   enumerable: true
    45 });
    47 Object.defineProperty(this, "NetworkHelper", {
    48   get: function() {
    49     return devtools.require("devtools/toolkit/webconsole/network-helper");
    50   },
    51   configurable: true,
    52   enumerable: true
    53 });
    55 this.EXPORTED_SYMBOLS = ["VariablesView", "escapeHTML"];
    57 /**
    58  * Debugger localization strings.
    59  */
    60 const STR = Services.strings.createBundle(DBG_STRINGS_URI);
    62 /**
    63  * A tree view for inspecting scopes, objects and properties.
    64  * Iterable via "for (let [id, scope] of instance) { }".
    65  * Requires the devtools common.css and debugger.css skin stylesheets.
    66  *
    67  * To allow replacing variable or property values in this view, provide an
    68  * "eval" function property. To allow replacing variable or property names,
    69  * provide a "switch" function. To handle deleting variables or properties,
    70  * provide a "delete" function.
    71  *
    72  * @param nsIDOMNode aParentNode
    73  *        The parent node to hold this view.
    74  * @param object aFlags [optional]
    75  *        An object contaning initialization options for this view.
    76  *        e.g. { lazyEmpty: true, searchEnabled: true ... }
    77  */
    78 this.VariablesView = function VariablesView(aParentNode, aFlags = {}) {
    79   this._store = []; // Can't use a Map because Scope names needn't be unique.
    80   this._itemsByElement = new WeakMap();
    81   this._prevHierarchy = new Map();
    82   this._currHierarchy = new Map();
    84   this._parent = aParentNode;
    85   this._parent.classList.add("variables-view-container");
    86   this._parent.classList.add("theme-body");
    87   this._appendEmptyNotice();
    89   this._onSearchboxInput = this._onSearchboxInput.bind(this);
    90   this._onSearchboxKeyPress = this._onSearchboxKeyPress.bind(this);
    91   this._onViewKeyPress = this._onViewKeyPress.bind(this);
    92   this._onViewKeyDown = this._onViewKeyDown.bind(this);
    94   // Create an internal scrollbox container.
    95   this._list = this.document.createElement("scrollbox");
    96   this._list.setAttribute("orient", "vertical");
    97   this._list.addEventListener("keypress", this._onViewKeyPress, false);
    98   this._list.addEventListener("keydown", this._onViewKeyDown, false);
    99   this._parent.appendChild(this._list);
   101   for (let name in aFlags) {
   102     this[name] = aFlags[name];
   103   }
   105   EventEmitter.decorate(this);
   106 };
   108 VariablesView.prototype = {
   109   /**
   110    * Helper setter for populating this container with a raw object.
   111    *
   112    * @param object aObject
   113    *        The raw object to display. You can only provide this object
   114    *        if you want the variables view to work in sync mode.
   115    */
   116   set rawObject(aObject) {
   117     this.empty();
   118     this.addScope()
   119         .addItem("", { enumerable: true })
   120         .populate(aObject, { sorted: true });
   121   },
   123   /**
   124    * Adds a scope to contain any inspected variables.
   125    *
   126    * This new scope will be considered the parent of any other scope
   127    * added afterwards.
   128    *
   129    * @param string aName
   130    *        The scope's name (e.g. "Local", "Global" etc.).
   131    * @return Scope
   132    *         The newly created Scope instance.
   133    */
   134   addScope: function(aName = "") {
   135     this._removeEmptyNotice();
   136     this._toggleSearchVisibility(true);
   138     let scope = new Scope(this, aName);
   139     this._store.push(scope);
   140     this._itemsByElement.set(scope._target, scope);
   141     this._currHierarchy.set(aName, scope);
   142     scope.header = !!aName;
   144     return scope;
   145   },
   147   /**
   148    * Removes all items from this container.
   149    *
   150    * @param number aTimeout [optional]
   151    *        The number of milliseconds to delay the operation if
   152    *        lazy emptying of this container is enabled.
   153    */
   154   empty: function(aTimeout = this.lazyEmptyDelay) {
   155     // If there are no items in this container, emptying is useless.
   156     if (!this._store.length) {
   157       return;
   158     }
   160     this._store.length = 0;
   161     this._itemsByElement.clear();
   162     this._prevHierarchy = this._currHierarchy;
   163     this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
   165     // Check if this empty operation may be executed lazily.
   166     if (this.lazyEmpty && aTimeout > 0) {
   167       this._emptySoon(aTimeout);
   168       return;
   169     }
   171     while (this._list.hasChildNodes()) {
   172       this._list.firstChild.remove();
   173     }
   175     this._appendEmptyNotice();
   176     this._toggleSearchVisibility(false);
   177   },
   179   /**
   180    * Emptying this container and rebuilding it immediately afterwards would
   181    * result in a brief redraw flicker, because the previously expanded nodes
   182    * may get asynchronously re-expanded, after fetching the prototype and
   183    * properties from a server.
   184    *
   185    * To avoid such behaviour, a normal container list is rebuild, but not
   186    * immediately attached to the parent container. The old container list
   187    * is kept around for a short period of time, hopefully accounting for the
   188    * data fetching delay. In the meantime, any operations can be executed
   189    * normally.
   190    *
   191    * @see VariablesView.empty
   192    * @see VariablesView.commitHierarchy
   193    */
   194   _emptySoon: function(aTimeout) {
   195     let prevList = this._list;
   196     let currList = this._list = this.document.createElement("scrollbox");
   198     this.window.setTimeout(() => {
   199       prevList.removeEventListener("keypress", this._onViewKeyPress, false);
   200       prevList.removeEventListener("keydown", this._onViewKeyDown, false);
   201       currList.addEventListener("keypress", this._onViewKeyPress, false);
   202       currList.addEventListener("keydown", this._onViewKeyDown, false);
   203       currList.setAttribute("orient", "vertical");
   205       this._parent.removeChild(prevList);
   206       this._parent.appendChild(currList);
   208       if (!this._store.length) {
   209         this._appendEmptyNotice();
   210         this._toggleSearchVisibility(false);
   211       }
   212     }, aTimeout);
   213   },
   215   /**
   216    * Optional DevTools toolbox containing this VariablesView. Used to
   217    * communicate with the inspector and highlighter.
   218    */
   219   toolbox: null,
   221   /**
   222    * The controller for this VariablesView, if it has one.
   223    */
   224   controller: null,
   226   /**
   227    * The amount of time (in milliseconds) it takes to empty this view lazily.
   228    */
   229   lazyEmptyDelay: LAZY_EMPTY_DELAY,
   231   /**
   232    * Specifies if this view may be emptied lazily.
   233    * @see VariablesView.prototype.empty
   234    */
   235   lazyEmpty: false,
   237   /**
   238    * Specifies if nodes in this view may be searched lazily.
   239    */
   240   lazySearch: true,
   242   /**
   243    * The number of elements in this container to jump when Page Up or Page Down
   244    * keys are pressed. If falsy, then the page size will be based on the
   245    * container height.
   246    */
   247   scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT,
   249   /**
   250    * The maximum number of elements allowed in a scope, variable or property
   251    * that allows pagination when appending children.
   252    */
   253   appendPageSize: APPEND_PAGE_SIZE_DEFAULT,
   255   /**
   256    * Function called each time a variable or property's value is changed via
   257    * user interaction. If null, then value changes are disabled.
   258    *
   259    * This property is applied recursively onto each scope in this view and
   260    * affects only the child nodes when they're created.
   261    */
   262   eval: null,
   264   /**
   265    * Function called each time a variable or property's name is changed via
   266    * user interaction. If null, then name changes are disabled.
   267    *
   268    * This property is applied recursively onto each scope in this view and
   269    * affects only the child nodes when they're created.
   270    */
   271   switch: null,
   273   /**
   274    * Function called each time a variable or property is deleted via
   275    * user interaction. If null, then deletions are disabled.
   276    *
   277    * This property is applied recursively onto each scope in this view and
   278    * affects only the child nodes when they're created.
   279    */
   280   delete: null,
   282   /**
   283    * Function called each time a property is added via user interaction. If
   284    * null, then property additions are disabled.
   285    *
   286    * This property is applied recursively onto each scope in this view and
   287    * affects only the child nodes when they're created.
   288    */
   289   new: null,
   291   /**
   292    * Specifies if after an eval or switch operation, the variable or property
   293    * which has been edited should be disabled.
   294    */
   295   preventDisableOnChange: false,
   297   /**
   298    * Specifies if, whenever a variable or property descriptor is available,
   299    * configurable, enumerable, writable, frozen, sealed and extensible
   300    * attributes should not affect presentation.
   301    *
   302    * This flag is applied recursively onto each scope in this view and
   303    * affects only the child nodes when they're created.
   304    */
   305   preventDescriptorModifiers: false,
   307   /**
   308    * The tooltip text shown on a variable or property's value if an |eval|
   309    * function is provided, in order to change the variable or property's value.
   310    *
   311    * This flag is applied recursively onto each scope in this view and
   312    * affects only the child nodes when they're created.
   313    */
   314   editableValueTooltip: STR.GetStringFromName("variablesEditableValueTooltip"),
   316   /**
   317    * The tooltip text shown on a variable or property's name if a |switch|
   318    * function is provided, in order to change the variable or property's name.
   319    *
   320    * This flag is applied recursively onto each scope in this view and
   321    * affects only the child nodes when they're created.
   322    */
   323   editableNameTooltip: STR.GetStringFromName("variablesEditableNameTooltip"),
   325   /**
   326    * The tooltip text shown on a variable or property's edit button if an
   327    * |eval| function is provided and a getter/setter descriptor is present,
   328    * in order to change the variable or property to a plain value.
   329    *
   330    * This flag is applied recursively onto each scope in this view and
   331    * affects only the child nodes when they're created.
   332    */
   333   editButtonTooltip: STR.GetStringFromName("variablesEditButtonTooltip"),
   335   /**
   336    * The tooltip text shown on a variable or property's value if that value is
   337    * a DOMNode that can be highlighted and selected in the inspector.
   338    *
   339    * This flag is applied recursively onto each scope in this view and
   340    * affects only the child nodes when they're created.
   341    */
   342   domNodeValueTooltip: STR.GetStringFromName("variablesDomNodeValueTooltip"),
   344   /**
   345    * The tooltip text shown on a variable or property's delete button if a
   346    * |delete| function is provided, in order to delete the variable or property.
   347    *
   348    * This flag is applied recursively onto each scope in this view and
   349    * affects only the child nodes when they're created.
   350    */
   351   deleteButtonTooltip: STR.GetStringFromName("variablesCloseButtonTooltip"),
   353   /**
   354    * Specifies the context menu attribute set on variables and properties.
   355    *
   356    * This flag is applied recursively onto each scope in this view and
   357    * affects only the child nodes when they're created.
   358    */
   359   contextMenuId: "",
   361   /**
   362    * The separator label between the variables or properties name and value.
   363    *
   364    * This flag is applied recursively onto each scope in this view and
   365    * affects only the child nodes when they're created.
   366    */
   367   separatorStr: STR.GetStringFromName("variablesSeparatorLabel"),
   369   /**
   370    * Specifies if enumerable properties and variables should be displayed.
   371    * These variables and properties are visible by default.
   372    * @param boolean aFlag
   373    */
   374   set enumVisible(aFlag) {
   375     this._enumVisible = aFlag;
   377     for (let scope of this._store) {
   378       scope._enumVisible = aFlag;
   379     }
   380   },
   382   /**
   383    * Specifies if non-enumerable properties and variables should be displayed.
   384    * These variables and properties are visible by default.
   385    * @param boolean aFlag
   386    */
   387   set nonEnumVisible(aFlag) {
   388     this._nonEnumVisible = aFlag;
   390     for (let scope of this._store) {
   391       scope._nonEnumVisible = aFlag;
   392     }
   393   },
   395   /**
   396    * Specifies if only enumerable properties and variables should be displayed.
   397    * Both types of these variables and properties are visible by default.
   398    * @param boolean aFlag
   399    */
   400   set onlyEnumVisible(aFlag) {
   401     if (aFlag) {
   402       this.enumVisible = true;
   403       this.nonEnumVisible = false;
   404     } else {
   405       this.enumVisible = true;
   406       this.nonEnumVisible = true;
   407     }
   408   },
   410   /**
   411    * Sets if the variable and property searching is enabled.
   412    * @param boolean aFlag
   413    */
   414   set searchEnabled(aFlag) aFlag ? this._enableSearch() : this._disableSearch(),
   416   /**
   417    * Gets if the variable and property searching is enabled.
   418    * @return boolean
   419    */
   420   get searchEnabled() !!this._searchboxContainer,
   422   /**
   423    * Sets the text displayed for the searchbox in this container.
   424    * @param string aValue
   425    */
   426   set searchPlaceholder(aValue) {
   427     if (this._searchboxNode) {
   428       this._searchboxNode.setAttribute("placeholder", aValue);
   429     }
   430     this._searchboxPlaceholder = aValue;
   431   },
   433   /**
   434    * Gets the text displayed for the searchbox in this container.
   435    * @return string
   436    */
   437   get searchPlaceholder() this._searchboxPlaceholder,
   439   /**
   440    * Enables variable and property searching in this view.
   441    * Use the "searchEnabled" setter to enable searching.
   442    */
   443   _enableSearch: function() {
   444     // If searching was already enabled, no need to re-enable it again.
   445     if (this._searchboxContainer) {
   446       return;
   447     }
   448     let document = this.document;
   449     let ownerNode = this._parent.parentNode;
   451     let container = this._searchboxContainer = document.createElement("hbox");
   452     container.className = "devtools-toolbar";
   454     // Hide the variables searchbox container if there are no variables or
   455     // properties to display.
   456     container.hidden = !this._store.length;
   458     let searchbox = this._searchboxNode = document.createElement("textbox");
   459     searchbox.className = "variables-view-searchinput devtools-searchinput";
   460     searchbox.setAttribute("placeholder", this._searchboxPlaceholder);
   461     searchbox.setAttribute("type", "search");
   462     searchbox.setAttribute("flex", "1");
   463     searchbox.addEventListener("input", this._onSearchboxInput, false);
   464     searchbox.addEventListener("keypress", this._onSearchboxKeyPress, false);
   466     container.appendChild(searchbox);
   467     ownerNode.insertBefore(container, this._parent);
   468   },
   470   /**
   471    * Disables variable and property searching in this view.
   472    * Use the "searchEnabled" setter to disable searching.
   473    */
   474   _disableSearch: function() {
   475     // If searching was already disabled, no need to re-disable it again.
   476     if (!this._searchboxContainer) {
   477       return;
   478     }
   479     this._searchboxContainer.remove();
   480     this._searchboxNode.removeEventListener("input", this._onSearchboxInput, false);
   481     this._searchboxNode.removeEventListener("keypress", this._onSearchboxKeyPress, false);
   483     this._searchboxContainer = null;
   484     this._searchboxNode = null;
   485   },
   487   /**
   488    * Sets the variables searchbox container hidden or visible.
   489    * It's hidden by default.
   490    *
   491    * @param boolean aVisibleFlag
   492    *        Specifies the intended visibility.
   493    */
   494   _toggleSearchVisibility: function(aVisibleFlag) {
   495     // If searching was already disabled, there's no need to hide it.
   496     if (!this._searchboxContainer) {
   497       return;
   498     }
   499     this._searchboxContainer.hidden = !aVisibleFlag;
   500   },
   502   /**
   503    * Listener handling the searchbox input event.
   504    */
   505   _onSearchboxInput: function() {
   506     this.scheduleSearch(this._searchboxNode.value);
   507   },
   509   /**
   510    * Listener handling the searchbox key press event.
   511    */
   512   _onSearchboxKeyPress: function(e) {
   513     switch(e.keyCode) {
   514       case e.DOM_VK_RETURN:
   515         this._onSearchboxInput();
   516         return;
   517       case e.DOM_VK_ESCAPE:
   518         this._searchboxNode.value = "";
   519         this._onSearchboxInput();
   520         return;
   521     }
   522   },
   524   /**
   525    * Schedules searching for variables or properties matching the query.
   526    *
   527    * @param string aToken
   528    *        The variable or property to search for.
   529    * @param number aWait
   530    *        The amount of milliseconds to wait until draining.
   531    */
   532   scheduleSearch: function(aToken, aWait) {
   533     // Check if this search operation may not be executed lazily.
   534     if (!this.lazySearch) {
   535       this._doSearch(aToken);
   536       return;
   537     }
   539     // The amount of time to wait for the requests to settle.
   540     let maxDelay = SEARCH_ACTION_MAX_DELAY;
   541     let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
   543     // Allow requests to settle down first.
   544     setNamedTimeout("vview-search", delay, () => this._doSearch(aToken));
   545   },
   547   /**
   548    * Performs a case insensitive search for variables or properties matching
   549    * the query, and hides non-matched items.
   550    *
   551    * If aToken is falsy, then all the scopes are unhidden and expanded,
   552    * while the available variables and properties inside those scopes are
   553    * just unhidden.
   554    *
   555    * @param string aToken
   556    *        The variable or property to search for.
   557    */
   558   _doSearch: function(aToken) {
   559     for (let scope of this._store) {
   560       switch (aToken) {
   561         case "":
   562         case null:
   563         case undefined:
   564           scope.expand();
   565           scope._performSearch("");
   566           break;
   567         default:
   568           scope._performSearch(aToken.toLowerCase());
   569           break;
   570       }
   571     }
   572   },
   574   /**
   575    * Find the first item in the tree of visible items in this container that
   576    * matches the predicate. Searches in visual order (the order seen by the
   577    * user). Descends into each scope to check the scope and its children.
   578    *
   579    * @param function aPredicate
   580    *        A function that returns true when a match is found.
   581    * @return Scope | Variable | Property
   582    *         The first visible scope, variable or property, or null if nothing
   583    *         is found.
   584    */
   585   _findInVisibleItems: function(aPredicate) {
   586     for (let scope of this._store) {
   587       let result = scope._findInVisibleItems(aPredicate);
   588       if (result) {
   589         return result;
   590       }
   591     }
   592     return null;
   593   },
   595   /**
   596    * Find the last item in the tree of visible items in this container that
   597    * matches the predicate. Searches in reverse visual order (opposite of the
   598    * order seen by the user). Descends into each scope to check the scope and
   599    * its children.
   600    *
   601    * @param function aPredicate
   602    *        A function that returns true when a match is found.
   603    * @return Scope | Variable | Property
   604    *         The last visible scope, variable or property, or null if nothing
   605    *         is found.
   606    */
   607   _findInVisibleItemsReverse: function(aPredicate) {
   608     for (let i = this._store.length - 1; i >= 0; i--) {
   609       let scope = this._store[i];
   610       let result = scope._findInVisibleItemsReverse(aPredicate);
   611       if (result) {
   612         return result;
   613       }
   614     }
   615     return null;
   616   },
   618   /**
   619    * Gets the scope at the specified index.
   620    *
   621    * @param number aIndex
   622    *        The scope's index.
   623    * @return Scope
   624    *         The scope if found, undefined if not.
   625    */
   626   getScopeAtIndex: function(aIndex) {
   627     return this._store[aIndex];
   628   },
   630   /**
   631    * Recursively searches this container for the scope, variable or property
   632    * displayed by the specified node.
   633    *
   634    * @param nsIDOMNode aNode
   635    *        The node to search for.
   636    * @return Scope | Variable | Property
   637    *         The matched scope, variable or property, or null if nothing is found.
   638    */
   639   getItemForNode: function(aNode) {
   640     return this._itemsByElement.get(aNode);
   641   },
   643   /**
   644    * Gets the scope owning a Variable or Property.
   645    *
   646    * @param Variable | Property
   647    *        The variable or property to retrieven the owner scope for.
   648    * @return Scope
   649    *         The owner scope.
   650    */
   651   getOwnerScopeForVariableOrProperty: function(aItem) {
   652     if (!aItem) {
   653       return null;
   654     }
   655     // If this is a Scope, return it.
   656     if (!(aItem instanceof Variable)) {
   657       return aItem;
   658     }
   659     // If this is a Variable or Property, find its owner scope.
   660     if (aItem instanceof Variable && aItem.ownerView) {
   661       return this.getOwnerScopeForVariableOrProperty(aItem.ownerView);
   662     }
   663     return null;
   664   },
   666   /**
   667    * Gets the parent scopes for a specified Variable or Property.
   668    * The returned list will not include the owner scope.
   669    *
   670    * @param Variable | Property
   671    *        The variable or property for which to find the parent scopes.
   672    * @return array
   673    *         A list of parent Scopes.
   674    */
   675   getParentScopesForVariableOrProperty: function(aItem) {
   676     let scope = this.getOwnerScopeForVariableOrProperty(aItem);
   677     return this._store.slice(0, Math.max(this._store.indexOf(scope), 0));
   678   },
   680   /**
   681    * Gets the currently focused scope, variable or property in this view.
   682    *
   683    * @return Scope | Variable | Property
   684    *         The focused scope, variable or property, or null if nothing is found.
   685    */
   686   getFocusedItem: function() {
   687     let focused = this.document.commandDispatcher.focusedElement;
   688     return this.getItemForNode(focused);
   689   },
   691   /**
   692    * Focuses the first visible scope, variable, or property in this container.
   693    */
   694   focusFirstVisibleItem: function() {
   695     let focusableItem = this._findInVisibleItems(item => item.focusable);
   696     if (focusableItem) {
   697       this._focusItem(focusableItem);
   698     }
   699     this._parent.scrollTop = 0;
   700     this._parent.scrollLeft = 0;
   701   },
   703   /**
   704    * Focuses the last visible scope, variable, or property in this container.
   705    */
   706   focusLastVisibleItem: function() {
   707     let focusableItem = this._findInVisibleItemsReverse(item => item.focusable);
   708     if (focusableItem) {
   709       this._focusItem(focusableItem);
   710     }
   711     this._parent.scrollTop = this._parent.scrollHeight;
   712     this._parent.scrollLeft = 0;
   713   },
   715   /**
   716    * Focuses the next scope, variable or property in this view.
   717    */
   718   focusNextItem: function() {
   719     this.focusItemAtDelta(+1);
   720   },
   722   /**
   723    * Focuses the previous scope, variable or property in this view.
   724    */
   725   focusPrevItem: function() {
   726     this.focusItemAtDelta(-1);
   727   },
   729   /**
   730    * Focuses another scope, variable or property in this view, based on
   731    * the index distance from the currently focused item.
   732    *
   733    * @param number aDelta
   734    *        A scalar specifying by how many items should the selection change.
   735    */
   736   focusItemAtDelta: function(aDelta) {
   737     let direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
   738     let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
   739     while (distance--) {
   740       if (!this._focusChange(direction)) {
   741         break; // Out of bounds.
   742       }
   743     }
   744   },
   746   /**
   747    * Focuses the next or previous scope, variable or property in this view.
   748    *
   749    * @param string aDirection
   750    *        Either "advanceFocus" or "rewindFocus".
   751    * @return boolean
   752    *         False if the focus went out of bounds and the first or last element
   753    *         in this view was focused instead.
   754    */
   755   _focusChange: function(aDirection) {
   756     let commandDispatcher = this.document.commandDispatcher;
   757     let prevFocusedElement = commandDispatcher.focusedElement;
   758     let currFocusedItem = null;
   760     do {
   761       commandDispatcher.suppressFocusScroll = true;
   762       commandDispatcher[aDirection]();
   764       // Make sure the newly focused item is a part of this view.
   765       // If the focus goes out of bounds, revert the previously focused item.
   766       if (!(currFocusedItem = this.getFocusedItem())) {
   767         prevFocusedElement.focus();
   768         return false;
   769       }
   770     } while (!currFocusedItem.focusable);
   772     // Focus remained within bounds.
   773     return true;
   774   },
   776   /**
   777    * Focuses a scope, variable or property and makes sure it's visible.
   778    *
   779    * @param aItem Scope | Variable | Property
   780    *        The item to focus.
   781    * @param boolean aCollapseFlag
   782    *        True if the focused item should also be collapsed.
   783    * @return boolean
   784    *         True if the item was successfully focused.
   785    */
   786   _focusItem: function(aItem, aCollapseFlag) {
   787     if (!aItem.focusable) {
   788       return false;
   789     }
   790     if (aCollapseFlag) {
   791       aItem.collapse();
   792     }
   793     aItem._target.focus();
   794     this.boxObject.ensureElementIsVisible(aItem._arrow);
   795     return true;
   796   },
   798   /**
   799    * Listener handling a key press event on the view.
   800    */
   801   _onViewKeyPress: function(e) {
   802     let item = this.getFocusedItem();
   804     // Prevent scrolling when pressing navigation keys.
   805     ViewHelpers.preventScrolling(e);
   807     switch (e.keyCode) {
   808       case e.DOM_VK_UP:
   809         // Always rewind focus.
   810         this.focusPrevItem(true);
   811         return;
   813       case e.DOM_VK_DOWN:
   814         // Always advance focus.
   815         this.focusNextItem(true);
   816         return;
   818       case e.DOM_VK_LEFT:
   819         // Collapse scopes, variables and properties before rewinding focus.
   820         if (item._isExpanded && item._isArrowVisible) {
   821           item.collapse();
   822         } else {
   823           this._focusItem(item.ownerView);
   824         }
   825         return;
   827       case e.DOM_VK_RIGHT:
   828         // Nothing to do here if this item never expands.
   829         if (!item._isArrowVisible) {
   830           return;
   831         }
   832         // Expand scopes, variables and properties before advancing focus.
   833         if (!item._isExpanded) {
   834           item.expand();
   835         } else {
   836           this.focusNextItem(true);
   837         }
   838         return;
   840       case e.DOM_VK_PAGE_UP:
   841         // Rewind a certain number of elements based on the container height.
   842         this.focusItemAtDelta(-(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight /
   843           PAGE_SIZE_SCROLL_HEIGHT_RATIO),
   844           PAGE_SIZE_MAX_JUMPS)));
   845         return;
   847       case e.DOM_VK_PAGE_DOWN:
   848         // Advance a certain number of elements based on the container height.
   849         this.focusItemAtDelta(+(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight /
   850           PAGE_SIZE_SCROLL_HEIGHT_RATIO),
   851           PAGE_SIZE_MAX_JUMPS)));
   852         return;
   854       case e.DOM_VK_HOME:
   855         this.focusFirstVisibleItem();
   856         return;
   858       case e.DOM_VK_END:
   859         this.focusLastVisibleItem();
   860         return;
   862       case e.DOM_VK_RETURN:
   863         // Start editing the value or name of the Variable or Property.
   864         if (item instanceof Variable) {
   865           if (e.metaKey || e.altKey || e.shiftKey) {
   866             item._activateNameInput();
   867           } else {
   868             item._activateValueInput();
   869           }
   870         }
   871         return;
   873       case e.DOM_VK_DELETE:
   874       case e.DOM_VK_BACK_SPACE:
   875         // Delete the Variable or Property if allowed.
   876         if (item instanceof Variable) {
   877           item._onDelete(e);
   878         }
   879         return;
   881       case e.DOM_VK_INSERT:
   882         item._onAddProperty(e);
   883         return;
   884     }
   885   },
   887   /**
   888    * Listener handling a key down event on the view.
   889    */
   890   _onViewKeyDown: function(e) {
   891     if (e.keyCode == e.DOM_VK_C) {
   892       // Copy current selection to clipboard.
   893       if (e.ctrlKey || e.metaKey) {
   894         let item = this.getFocusedItem();
   895         clipboardHelper.copyString(
   896           item._nameString + item.separatorStr + item._valueString
   897         );
   898       }
   899     }
   900   },
   902   /**
   903    * Sets the text displayed in this container when there are no available items.
   904    * @param string aValue
   905    */
   906   set emptyText(aValue) {
   907     if (this._emptyTextNode) {
   908       this._emptyTextNode.setAttribute("value", aValue);
   909     }
   910     this._emptyTextValue = aValue;
   911     this._appendEmptyNotice();
   912   },
   914   /**
   915    * Creates and appends a label signaling that this container is empty.
   916    */
   917   _appendEmptyNotice: function() {
   918     if (this._emptyTextNode || !this._emptyTextValue) {
   919       return;
   920     }
   922     let label = this.document.createElement("label");
   923     label.className = "variables-view-empty-notice";
   924     label.setAttribute("value", this._emptyTextValue);
   926     this._parent.appendChild(label);
   927     this._emptyTextNode = label;
   928   },
   930   /**
   931    * Removes the label signaling that this container is empty.
   932    */
   933   _removeEmptyNotice: function() {
   934     if (!this._emptyTextNode) {
   935       return;
   936     }
   938     this._parent.removeChild(this._emptyTextNode);
   939     this._emptyTextNode = null;
   940   },
   942   /**
   943    * Gets if all values should be aligned together.
   944    * @return boolean
   945    */
   946   get alignedValues() {
   947     return this._alignedValues;
   948   },
   950   /**
   951    * Sets if all values should be aligned together.
   952    * @param boolean aFlag
   953    */
   954   set alignedValues(aFlag) {
   955     this._alignedValues = aFlag;
   956     if (aFlag) {
   957       this._parent.setAttribute("aligned-values", "");
   958     } else {
   959       this._parent.removeAttribute("aligned-values");
   960     }
   961   },
   963   /**
   964    * Gets if action buttons (like delete) should be placed at the beginning or
   965    * end of a line.
   966    * @return boolean
   967    */
   968   get actionsFirst() {
   969     return this._actionsFirst;
   970   },
   972   /**
   973    * Sets if action buttons (like delete) should be placed at the beginning or
   974    * end of a line.
   975    * @param boolean aFlag
   976    */
   977   set actionsFirst(aFlag) {
   978     this._actionsFirst = aFlag;
   979     if (aFlag) {
   980       this._parent.setAttribute("actions-first", "");
   981     } else {
   982       this._parent.removeAttribute("actions-first");
   983     }
   984   },
   986   /**
   987    * Gets the parent node holding this view.
   988    * @return nsIDOMNode
   989    */
   990   get boxObject() this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject),
   992   /**
   993    * Gets the parent node holding this view.
   994    * @return nsIDOMNode
   995    */
   996   get parentNode() this._parent,
   998   /**
   999    * Gets the owner document holding this view.
  1000    * @return nsIHTMLDocument
  1001    */
  1002   get document() this._document || (this._document = this._parent.ownerDocument),
  1004   /**
  1005    * Gets the default window holding this view.
  1006    * @return nsIDOMWindow
  1007    */
  1008   get window() this._window || (this._window = this.document.defaultView),
  1010   _document: null,
  1011   _window: null,
  1013   _store: null,
  1014   _itemsByElement: null,
  1015   _prevHierarchy: null,
  1016   _currHierarchy: null,
  1018   _enumVisible: true,
  1019   _nonEnumVisible: true,
  1020   _alignedValues: false,
  1021   _actionsFirst: false,
  1023   _parent: null,
  1024   _list: null,
  1025   _searchboxNode: null,
  1026   _searchboxContainer: null,
  1027   _searchboxPlaceholder: "",
  1028   _emptyTextNode: null,
  1029   _emptyTextValue: ""
  1030 };
  1032 VariablesView.NON_SORTABLE_CLASSES = [
  1033   "Array",
  1034   "Int8Array",
  1035   "Uint8Array",
  1036   "Uint8ClampedArray",
  1037   "Int16Array",
  1038   "Uint16Array",
  1039   "Int32Array",
  1040   "Uint32Array",
  1041   "Float32Array",
  1042   "Float64Array"
  1043 ];
  1045 /**
  1046  * Determine whether an object's properties should be sorted based on its class.
  1048  * @param string aClassName
  1049  *        The class of the object.
  1050  */
  1051 VariablesView.isSortable = function(aClassName) {
  1052   return VariablesView.NON_SORTABLE_CLASSES.indexOf(aClassName) == -1;
  1053 };
  1055 /**
  1056  * Generates the string evaluated when performing simple value changes.
  1058  * @param Variable | Property aItem
  1059  *        The current variable or property.
  1060  * @param string aCurrentString
  1061  *        The trimmed user inputted string.
  1062  * @param string aPrefix [optional]
  1063  *        Prefix for the symbolic name.
  1064  * @return string
  1065  *         The string to be evaluated.
  1066  */
  1067 VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
  1068   return aPrefix + aItem._symbolicName + "=" + aCurrentString;
  1069 };
  1071 /**
  1072  * Generates the string evaluated when overriding getters and setters with
  1073  * plain values.
  1075  * @param Property aItem
  1076  *        The current getter or setter property.
  1077  * @param string aCurrentString
  1078  *        The trimmed user inputted string.
  1079  * @param string aPrefix [optional]
  1080  *        Prefix for the symbolic name.
  1081  * @return string
  1082  *         The string to be evaluated.
  1083  */
  1084 VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
  1085   let property = "\"" + aItem._nameString + "\"";
  1086   let parent = aPrefix + aItem.ownerView._symbolicName || "this";
  1088   return "Object.defineProperty(" + parent + "," + property + "," +
  1089     "{ value: " + aCurrentString +
  1090     ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
  1091     ", configurable: true" +
  1092     ", writable: true" +
  1093     "})";
  1094 };
  1096 /**
  1097  * Generates the string evaluated when performing getters and setters changes.
  1099  * @param Property aItem
  1100  *        The current getter or setter property.
  1101  * @param string aCurrentString
  1102  *        The trimmed user inputted string.
  1103  * @param string aPrefix [optional]
  1104  *        Prefix for the symbolic name.
  1105  * @return string
  1106  *         The string to be evaluated.
  1107  */
  1108 VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
  1109   let type = aItem._nameString;
  1110   let propertyObject = aItem.ownerView;
  1111   let parentObject = propertyObject.ownerView;
  1112   let property = "\"" + propertyObject._nameString + "\"";
  1113   let parent = aPrefix + parentObject._symbolicName || "this";
  1115   switch (aCurrentString) {
  1116     case "":
  1117     case "null":
  1118     case "undefined":
  1119       let mirrorType = type == "get" ? "set" : "get";
  1120       let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__";
  1122       // If the parent object will end up without any getter or setter,
  1123       // morph it into a plain value.
  1124       if ((type == "set" && propertyObject.getter.type == "undefined") ||
  1125           (type == "get" && propertyObject.setter.type == "undefined")) {
  1126         // Make sure the right getter/setter to value override macro is applied
  1127         // to the target object.
  1128         return propertyObject.evaluationMacro(propertyObject, "undefined", aPrefix);
  1131       // Construct and return the getter/setter removal evaluation string.
  1132       // e.g: Object.defineProperty(foo, "bar", {
  1133       //   get: foo.__lookupGetter__("bar"),
  1134       //   set: undefined,
  1135       //   enumerable: true,
  1136       //   configurable: true
  1137       // })
  1138       return "Object.defineProperty(" + parent + "," + property + "," +
  1139         "{" + mirrorType + ":" + parent + "." + mirrorLookup + "(" + property + ")" +
  1140         "," + type + ":" + undefined +
  1141         ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
  1142         ", configurable: true" +
  1143         "})";
  1145     default:
  1146       // Wrap statements inside a function declaration if not already wrapped.
  1147       if (!aCurrentString.startsWith("function")) {
  1148         let header = "function(" + (type == "set" ? "value" : "") + ")";
  1149         let body = "";
  1150         // If there's a return statement explicitly written, always use the
  1151         // standard function definition syntax
  1152         if (aCurrentString.contains("return ")) {
  1153           body = "{" + aCurrentString + "}";
  1155         // If block syntax is used, use the whole string as the function body.
  1156         else if (aCurrentString.startsWith("{")) {
  1157           body = aCurrentString;
  1159         // Prefer an expression closure.
  1160         else {
  1161           body = "(" + aCurrentString + ")";
  1163         aCurrentString = header + body;
  1166       // Determine if a new getter or setter should be defined.
  1167       let defineType = type == "get" ? "__defineGetter__" : "__defineSetter__";
  1169       // Make sure all quotes are escaped in the expression's syntax,
  1170       let defineFunc = "eval(\"(" + aCurrentString.replace(/"/g, "\\$&") + ")\")";
  1172       // Construct and return the getter/setter evaluation string.
  1173       // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
  1174       return parent + "." + defineType + "(" + property + "," + defineFunc + ")";
  1176 };
  1178 /**
  1179  * Function invoked when a getter or setter is deleted.
  1181  * @param Property aItem
  1182  *        The current getter or setter property.
  1183  */
  1184 VariablesView.getterOrSetterDeleteCallback = function(aItem) {
  1185   aItem._disable();
  1187   // Make sure the right getter/setter to value override macro is applied
  1188   // to the target object.
  1189   aItem.ownerView.eval(aItem, "");
  1191   return true; // Don't hide the element.
  1192 };
  1195 /**
  1196  * A Scope is an object holding Variable instances.
  1197  * Iterable via "for (let [name, variable] of instance) { }".
  1199  * @param VariablesView aView
  1200  *        The view to contain this scope.
  1201  * @param string aName
  1202  *        The scope's name.
  1203  * @param object aFlags [optional]
  1204  *        Additional options or flags for this scope.
  1205  */
  1206 function Scope(aView, aName, aFlags = {}) {
  1207   this.ownerView = aView;
  1209   this._onClick = this._onClick.bind(this);
  1210   this._openEnum = this._openEnum.bind(this);
  1211   this._openNonEnum = this._openNonEnum.bind(this);
  1213   // Inherit properties and flags from the parent view. You can override
  1214   // each of these directly onto any scope, variable or property instance.
  1215   this.scrollPageSize = aView.scrollPageSize;
  1216   this.appendPageSize = aView.appendPageSize;
  1217   this.eval = aView.eval;
  1218   this.switch = aView.switch;
  1219   this.delete = aView.delete;
  1220   this.new = aView.new;
  1221   this.preventDisableOnChange = aView.preventDisableOnChange;
  1222   this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
  1223   this.editableNameTooltip = aView.editableNameTooltip;
  1224   this.editableValueTooltip = aView.editableValueTooltip;
  1225   this.editButtonTooltip = aView.editButtonTooltip;
  1226   this.deleteButtonTooltip = aView.deleteButtonTooltip;
  1227   this.domNodeValueTooltip = aView.domNodeValueTooltip;
  1228   this.contextMenuId = aView.contextMenuId;
  1229   this.separatorStr = aView.separatorStr;
  1231   this._init(aName.trim(), aFlags);
  1234 Scope.prototype = {
  1235   /**
  1236    * Whether this Scope should be prefetched when it is remoted.
  1237    */
  1238   shouldPrefetch: true,
  1240   /**
  1241    * Whether this Scope should paginate its contents.
  1242    */
  1243   allowPaginate: false,
  1245   /**
  1246    * The class name applied to this scope's target element.
  1247    */
  1248   targetClassName: "variables-view-scope",
  1250   /**
  1251    * Create a new Variable that is a child of this Scope.
  1253    * @param string aName
  1254    *        The name of the new Property.
  1255    * @param object aDescriptor
  1256    *        The variable's descriptor.
  1257    * @return Variable
  1258    *         The newly created child Variable.
  1259    */
  1260   _createChild: function(aName, aDescriptor) {
  1261     return new Variable(this, aName, aDescriptor);
  1262   },
  1264   /**
  1265    * Adds a child to contain any inspected properties.
  1267    * @param string aName
  1268    *        The child's name.
  1269    * @param object aDescriptor
  1270    *        Specifies the value and/or type & class of the child,
  1271    *        or 'get' & 'set' accessor properties. If the type is implicit,
  1272    *        it will be inferred from the value. If this parameter is omitted,
  1273    *        a property without a value will be added (useful for branch nodes).
  1274    *        e.g. - { value: 42 }
  1275    *             - { value: true }
  1276    *             - { value: "nasu" }
  1277    *             - { value: { type: "undefined" } }
  1278    *             - { value: { type: "null" } }
  1279    *             - { value: { type: "object", class: "Object" } }
  1280    *             - { get: { type: "object", class: "Function" },
  1281    *                 set: { type: "undefined" } }
  1282    * @param boolean aRelaxed [optional]
  1283    *        Pass true if name duplicates should be allowed.
  1284    *        You probably shouldn't do it. Use this with caution.
  1285    * @return Variable
  1286    *         The newly created Variable instance, null if it already exists.
  1287    */
  1288   addItem: function(aName = "", aDescriptor = {}, aRelaxed = false) {
  1289     if (this._store.has(aName) && !aRelaxed) {
  1290       return null;
  1293     let child = this._createChild(aName, aDescriptor);
  1294     this._store.set(aName, child);
  1295     this._variablesView._itemsByElement.set(child._target, child);
  1296     this._variablesView._currHierarchy.set(child._absoluteName, child);
  1297     child.header = !!aName;
  1299     return child;
  1300   },
  1302   /**
  1303    * Adds items for this variable.
  1305    * @param object aItems
  1306    *        An object containing some { name: descriptor } data properties,
  1307    *        specifying the value and/or type & class of the variable,
  1308    *        or 'get' & 'set' accessor properties. If the type is implicit,
  1309    *        it will be inferred from the value.
  1310    *        e.g. - { someProp0: { value: 42 },
  1311    *                 someProp1: { value: true },
  1312    *                 someProp2: { value: "nasu" },
  1313    *                 someProp3: { value: { type: "undefined" } },
  1314    *                 someProp4: { value: { type: "null" } },
  1315    *                 someProp5: { value: { type: "object", class: "Object" } },
  1316    *                 someProp6: { get: { type: "object", class: "Function" },
  1317    *                              set: { type: "undefined" } } }
  1318    * @param object aOptions [optional]
  1319    *        Additional options for adding the properties. Supported options:
  1320    *        - sorted: true to sort all the properties before adding them
  1321    *        - callback: function invoked after each item is added
  1322    * @param string aKeysType [optional]
  1323    *        Helper argument in the case of paginated items. Can be either
  1324    *        "just-strings" or "just-numbers". Humans shouldn't use this argument.
  1325    */
  1326   addItems: function(aItems, aOptions = {}, aKeysType = "") {
  1327     let names = Object.keys(aItems);
  1329     // Building the view when inspecting an object with a very large number of
  1330     // properties may take a long time. To avoid blocking the UI, group
  1331     // the items into several lazily populated pseudo-items.
  1332     let exceedsThreshold = names.length >= this.appendPageSize;
  1333     let shouldPaginate = exceedsThreshold && aKeysType != "just-strings";
  1334     if (shouldPaginate && this.allowPaginate) {
  1335       // Group the items to append into two separate arrays, one containing
  1336       // number-like keys, the other one containing string keys.
  1337       if (aKeysType == "just-numbers") {
  1338         var numberKeys = names;
  1339         var stringKeys = [];
  1340       } else {
  1341         var numberKeys = [];
  1342         var stringKeys = [];
  1343         for (let name of names) {
  1344           // Be very careful. Avoid Infinity, NaN and non Natural number keys.
  1345           let coerced = +name;
  1346           if (Number.isInteger(coerced) && coerced > -1) {
  1347             numberKeys.push(name);
  1348           } else {
  1349             stringKeys.push(name);
  1354       // This object contains a very large number of properties, but they're
  1355       // almost all strings that can't be coerced to numbers. Don't paginate.
  1356       if (numberKeys.length < this.appendPageSize) {
  1357         this.addItems(aItems, aOptions, "just-strings");
  1358         return;
  1361       // Slices a section of the { name: descriptor } data properties.
  1362       let paginate = (aArray, aBegin = 0, aEnd = aArray.length) => {
  1363         let store = {}
  1364         for (let i = aBegin; i < aEnd; i++) {
  1365           let name = aArray[i];
  1366           store[name] = aItems[name];
  1368         return store;
  1369       };
  1371       // Creates a pseudo-item that populates itself with the data properties
  1372       // from the corresponding page range.
  1373       let createRangeExpander = (aArray, aBegin, aEnd, aOptions, aKeyTypes) => {
  1374         let rangeVar = this.addItem(aArray[aBegin] + Scope.ellipsis + aArray[aEnd - 1]);
  1375         rangeVar.onexpand = () => {
  1376           let pageItems = paginate(aArray, aBegin, aEnd);
  1377           rangeVar.addItems(pageItems, aOptions, aKeyTypes);
  1379         rangeVar.showArrow();
  1380         rangeVar.target.setAttribute("pseudo-item", "");
  1381       };
  1383       // Divide the number keys into quarters.
  1384       let page = +Math.round(numberKeys.length / 4).toPrecision(1);
  1385       createRangeExpander(numberKeys, 0, page, aOptions, "just-numbers");
  1386       createRangeExpander(numberKeys, page, page * 2, aOptions, "just-numbers");
  1387       createRangeExpander(numberKeys, page * 2, page * 3, aOptions, "just-numbers");
  1388       createRangeExpander(numberKeys, page * 3, numberKeys.length, aOptions, "just-numbers");
  1390       // Append all the string keys together.
  1391       this.addItems(paginate(stringKeys), aOptions, "just-strings");
  1392       return;
  1395     // Sort all of the properties before adding them, if preferred.
  1396     if (aOptions.sorted && aKeysType != "just-numbers") {
  1397       names.sort();
  1400     // Add the properties to the current scope.
  1401     for (let name of names) {
  1402       let descriptor = aItems[name];
  1403       let item = this.addItem(name, descriptor);
  1405       if (aOptions.callback) {
  1406         aOptions.callback(item, descriptor.value);
  1409   },
  1411   /**
  1412    * Remove this Scope from its parent and remove all children recursively.
  1413    */
  1414   remove: function() {
  1415     let view = this._variablesView;
  1416     view._store.splice(view._store.indexOf(this), 1);
  1417     view._itemsByElement.delete(this._target);
  1418     view._currHierarchy.delete(this._nameString);
  1420     this._target.remove();
  1422     for (let variable of this._store.values()) {
  1423       variable.remove();
  1425   },
  1427   /**
  1428    * Gets the variable in this container having the specified name.
  1430    * @param string aName
  1431    *        The name of the variable to get.
  1432    * @return Variable
  1433    *         The matched variable, or null if nothing is found.
  1434    */
  1435   get: function(aName) {
  1436     return this._store.get(aName);
  1437   },
  1439   /**
  1440    * Recursively searches for the variable or property in this container
  1441    * displayed by the specified node.
  1443    * @param nsIDOMNode aNode
  1444    *        The node to search for.
  1445    * @return Variable | Property
  1446    *         The matched variable or property, or null if nothing is found.
  1447    */
  1448   find: function(aNode) {
  1449     for (let [, variable] of this._store) {
  1450       let match;
  1451       if (variable._target == aNode) {
  1452         match = variable;
  1453       } else {
  1454         match = variable.find(aNode);
  1456       if (match) {
  1457         return match;
  1460     return null;
  1461   },
  1463   /**
  1464    * Determines if this scope is a direct child of a parent variables view,
  1465    * scope, variable or property.
  1467    * @param VariablesView | Scope | Variable | Property
  1468    *        The parent to check.
  1469    * @return boolean
  1470    *         True if the specified item is a direct child, false otherwise.
  1471    */
  1472   isChildOf: function(aParent) {
  1473     return this.ownerView == aParent;
  1474   },
  1476   /**
  1477    * Determines if this scope is a descendant of a parent variables view,
  1478    * scope, variable or property.
  1480    * @param VariablesView | Scope | Variable | Property
  1481    *        The parent to check.
  1482    * @return boolean
  1483    *         True if the specified item is a descendant, false otherwise.
  1484    */
  1485   isDescendantOf: function(aParent) {
  1486     if (this.isChildOf(aParent)) {
  1487       return true;
  1490     // Recurse to parent if it is a Scope, Variable, or Property.
  1491     if (this.ownerView instanceof Scope) {
  1492       return this.ownerView.isDescendantOf(aParent);
  1495     return false;
  1496   },
  1498   /**
  1499    * Shows the scope.
  1500    */
  1501   show: function() {
  1502     this._target.hidden = false;
  1503     this._isContentVisible = true;
  1505     if (this.onshow) {
  1506       this.onshow(this);
  1508   },
  1510   /**
  1511    * Hides the scope.
  1512    */
  1513   hide: function() {
  1514     this._target.hidden = true;
  1515     this._isContentVisible = false;
  1517     if (this.onhide) {
  1518       this.onhide(this);
  1520   },
  1522   /**
  1523    * Expands the scope, showing all the added details.
  1524    */
  1525   expand: function() {
  1526     if (this._isExpanded || this._isLocked) {
  1527       return;
  1529     if (this._variablesView._enumVisible) {
  1530       this._openEnum();
  1532     if (this._variablesView._nonEnumVisible) {
  1533       Services.tm.currentThread.dispatch({ run: this._openNonEnum }, 0);
  1535     this._isExpanded = true;
  1537     if (this.onexpand) {
  1538       this.onexpand(this);
  1540   },
  1542   /**
  1543    * Collapses the scope, hiding all the added details.
  1544    */
  1545   collapse: function() {
  1546     if (!this._isExpanded || this._isLocked) {
  1547       return;
  1549     this._arrow.removeAttribute("open");
  1550     this._enum.removeAttribute("open");
  1551     this._nonenum.removeAttribute("open");
  1552     this._isExpanded = false;
  1554     if (this.oncollapse) {
  1555       this.oncollapse(this);
  1557   },
  1559   /**
  1560    * Toggles between the scope's collapsed and expanded state.
  1561    */
  1562   toggle: function(e) {
  1563     if (e && e.button != 0) {
  1564       // Only allow left-click to trigger this event.
  1565       return;
  1567     this.expanded ^= 1;
  1569     // Make sure the scope and its contents are visibile.
  1570     for (let [, variable] of this._store) {
  1571       variable.header = true;
  1572       variable._matched = true;
  1574     if (this.ontoggle) {
  1575       this.ontoggle(this);
  1577   },
  1579   /**
  1580    * Shows the scope's title header.
  1581    */
  1582   showHeader: function() {
  1583     if (this._isHeaderVisible || !this._nameString) {
  1584       return;
  1586     this._target.removeAttribute("untitled");
  1587     this._isHeaderVisible = true;
  1588   },
  1590   /**
  1591    * Hides the scope's title header.
  1592    * This action will automatically expand the scope.
  1593    */
  1594   hideHeader: function() {
  1595     if (!this._isHeaderVisible) {
  1596       return;
  1598     this.expand();
  1599     this._target.setAttribute("untitled", "");
  1600     this._isHeaderVisible = false;
  1601   },
  1603   /**
  1604    * Shows the scope's expand/collapse arrow.
  1605    */
  1606   showArrow: function() {
  1607     if (this._isArrowVisible) {
  1608       return;
  1610     this._arrow.removeAttribute("invisible");
  1611     this._isArrowVisible = true;
  1612   },
  1614   /**
  1615    * Hides the scope's expand/collapse arrow.
  1616    */
  1617   hideArrow: function() {
  1618     if (!this._isArrowVisible) {
  1619       return;
  1621     this._arrow.setAttribute("invisible", "");
  1622     this._isArrowVisible = false;
  1623   },
  1625   /**
  1626    * Gets the visibility state.
  1627    * @return boolean
  1628    */
  1629   get visible() this._isContentVisible,
  1631   /**
  1632    * Gets the expanded state.
  1633    * @return boolean
  1634    */
  1635   get expanded() this._isExpanded,
  1637   /**
  1638    * Gets the header visibility state.
  1639    * @return boolean
  1640    */
  1641   get header() this._isHeaderVisible,
  1643   /**
  1644    * Gets the twisty visibility state.
  1645    * @return boolean
  1646    */
  1647   get twisty() this._isArrowVisible,
  1649   /**
  1650    * Gets the expand lock state.
  1651    * @return boolean
  1652    */
  1653   get locked() this._isLocked,
  1655   /**
  1656    * Sets the visibility state.
  1657    * @param boolean aFlag
  1658    */
  1659   set visible(aFlag) aFlag ? this.show() : this.hide(),
  1661   /**
  1662    * Sets the expanded state.
  1663    * @param boolean aFlag
  1664    */
  1665   set expanded(aFlag) aFlag ? this.expand() : this.collapse(),
  1667   /**
  1668    * Sets the header visibility state.
  1669    * @param boolean aFlag
  1670    */
  1671   set header(aFlag) aFlag ? this.showHeader() : this.hideHeader(),
  1673   /**
  1674    * Sets the twisty visibility state.
  1675    * @param boolean aFlag
  1676    */
  1677   set twisty(aFlag) aFlag ? this.showArrow() : this.hideArrow(),
  1679   /**
  1680    * Sets the expand lock state.
  1681    * @param boolean aFlag
  1682    */
  1683   set locked(aFlag) this._isLocked = aFlag,
  1685   /**
  1686    * Specifies if this target node may be focused.
  1687    * @return boolean
  1688    */
  1689   get focusable() {
  1690     // Check if this target node is actually visibile.
  1691     if (!this._nameString ||
  1692         !this._isContentVisible ||
  1693         !this._isHeaderVisible ||
  1694         !this._isMatch) {
  1695       return false;
  1697     // Check if all parent objects are expanded.
  1698     let item = this;
  1700     // Recurse while parent is a Scope, Variable, or Property
  1701     while ((item = item.ownerView) && item instanceof Scope) {
  1702       if (!item._isExpanded) {
  1703         return false;
  1706     return true;
  1707   },
  1709   /**
  1710    * Focus this scope.
  1711    */
  1712   focus: function() {
  1713     this._variablesView._focusItem(this);
  1714   },
  1716   /**
  1717    * Adds an event listener for a certain event on this scope's title.
  1718    * @param string aName
  1719    * @param function aCallback
  1720    * @param boolean aCapture
  1721    */
  1722   addEventListener: function(aName, aCallback, aCapture) {
  1723     this._title.addEventListener(aName, aCallback, aCapture);
  1724   },
  1726   /**
  1727    * Removes an event listener for a certain event on this scope's title.
  1728    * @param string aName
  1729    * @param function aCallback
  1730    * @param boolean aCapture
  1731    */
  1732   removeEventListener: function(aName, aCallback, aCapture) {
  1733     this._title.removeEventListener(aName, aCallback, aCapture);
  1734   },
  1736   /**
  1737    * Gets the id associated with this item.
  1738    * @return string
  1739    */
  1740   get id() this._idString,
  1742   /**
  1743    * Gets the name associated with this item.
  1744    * @return string
  1745    */
  1746   get name() this._nameString,
  1748   /**
  1749    * Gets the displayed value for this item.
  1750    * @return string
  1751    */
  1752   get displayValue() this._valueString,
  1754   /**
  1755    * Gets the class names used for the displayed value.
  1756    * @return string
  1757    */
  1758   get displayValueClassName() this._valueClassName,
  1760   /**
  1761    * Gets the element associated with this item.
  1762    * @return nsIDOMNode
  1763    */
  1764   get target() this._target,
  1766   /**
  1767    * Initializes this scope's id, view and binds event listeners.
  1769    * @param string aName
  1770    *        The scope's name.
  1771    * @param object aFlags [optional]
  1772    *        Additional options or flags for this scope.
  1773    */
  1774   _init: function(aName, aFlags) {
  1775     this._idString = generateId(this._nameString = aName);
  1776     this._displayScope(aName, this.targetClassName, "devtools-toolbar");
  1777     this._addEventListeners();
  1778     this.parentNode.appendChild(this._target);
  1779   },
  1781   /**
  1782    * Creates the necessary nodes for this scope.
  1784    * @param string aName
  1785    *        The scope's name.
  1786    * @param string aTargetClassName
  1787    *        A custom class name for this scope's target element.
  1788    * @param string aTitleClassName [optional]
  1789    *        A custom class name for this scope's title element.
  1790    */
  1791   _displayScope: function(aName, aTargetClassName, aTitleClassName = "") {
  1792     let document = this.document;
  1794     let element = this._target = document.createElement("vbox");
  1795     element.id = this._idString;
  1796     element.className = aTargetClassName;
  1798     let arrow = this._arrow = document.createElement("hbox");
  1799     arrow.className = "arrow";
  1801     let name = this._name = document.createElement("label");
  1802     name.className = "plain name";
  1803     name.setAttribute("value", aName);
  1805     let title = this._title = document.createElement("hbox");
  1806     title.className = "title " + aTitleClassName;
  1807     title.setAttribute("align", "center");
  1809     let enumerable = this._enum = document.createElement("vbox");
  1810     let nonenum = this._nonenum = document.createElement("vbox");
  1811     enumerable.className = "variables-view-element-details enum";
  1812     nonenum.className = "variables-view-element-details nonenum";
  1814     title.appendChild(arrow);
  1815     title.appendChild(name);
  1817     element.appendChild(title);
  1818     element.appendChild(enumerable);
  1819     element.appendChild(nonenum);
  1820   },
  1822   /**
  1823    * Adds the necessary event listeners for this scope.
  1824    */
  1825   _addEventListeners: function() {
  1826     this._title.addEventListener("mousedown", this._onClick, false);
  1827   },
  1829   /**
  1830    * The click listener for this scope's title.
  1831    */
  1832   _onClick: function(e) {
  1833     if (this.editing ||
  1834         e.button != 0 ||
  1835         e.target == this._editNode ||
  1836         e.target == this._deleteNode ||
  1837         e.target == this._addPropertyNode) {
  1838       return;
  1840     this.toggle();
  1841     this.focus();
  1842   },
  1844   /**
  1845    * Opens the enumerable items container.
  1846    */
  1847   _openEnum: function() {
  1848     this._arrow.setAttribute("open", "");
  1849     this._enum.setAttribute("open", "");
  1850   },
  1852   /**
  1853    * Opens the non-enumerable items container.
  1854    */
  1855   _openNonEnum: function() {
  1856     this._nonenum.setAttribute("open", "");
  1857   },
  1859   /**
  1860    * Specifies if enumerable properties and variables should be displayed.
  1861    * @param boolean aFlag
  1862    */
  1863   set _enumVisible(aFlag) {
  1864     for (let [, variable] of this._store) {
  1865       variable._enumVisible = aFlag;
  1867       if (!this._isExpanded) {
  1868         continue;
  1870       if (aFlag) {
  1871         this._enum.setAttribute("open", "");
  1872       } else {
  1873         this._enum.removeAttribute("open");
  1876   },
  1878   /**
  1879    * Specifies if non-enumerable properties and variables should be displayed.
  1880    * @param boolean aFlag
  1881    */
  1882   set _nonEnumVisible(aFlag) {
  1883     for (let [, variable] of this._store) {
  1884       variable._nonEnumVisible = aFlag;
  1886       if (!this._isExpanded) {
  1887         continue;
  1889       if (aFlag) {
  1890         this._nonenum.setAttribute("open", "");
  1891       } else {
  1892         this._nonenum.removeAttribute("open");
  1895   },
  1897   /**
  1898    * Performs a case insensitive search for variables or properties matching
  1899    * the query, and hides non-matched items.
  1901    * @param string aLowerCaseQuery
  1902    *        The lowercased name of the variable or property to search for.
  1903    */
  1904   _performSearch: function(aLowerCaseQuery) {
  1905     for (let [, variable] of this._store) {
  1906       let currentObject = variable;
  1907       let lowerCaseName = variable._nameString.toLowerCase();
  1908       let lowerCaseValue = variable._valueString.toLowerCase();
  1910       // Non-matched variables or properties require a corresponding attribute.
  1911       if (!lowerCaseName.contains(aLowerCaseQuery) &&
  1912           !lowerCaseValue.contains(aLowerCaseQuery)) {
  1913         variable._matched = false;
  1915       // Variable or property is matched.
  1916       else {
  1917         variable._matched = true;
  1919         // If the variable was ever expanded, there's a possibility it may
  1920         // contain some matched properties, so make sure they're visible
  1921         // ("expand downwards").
  1922         if (variable._store.size) {
  1923           variable.expand();
  1926         // If the variable is contained in another Scope, Variable, or Property,
  1927         // the parent may not be a match, thus hidden. It should be visible
  1928         // ("expand upwards").
  1929         while ((variable = variable.ownerView) && variable instanceof Scope) {
  1930           variable._matched = true;
  1931           variable.expand();
  1935       // Proceed with the search recursively inside this variable or property.
  1936       if (currentObject._store.size || currentObject.getter || currentObject.setter) {
  1937         currentObject._performSearch(aLowerCaseQuery);
  1940   },
  1942   /**
  1943    * Sets if this object instance is a matched or non-matched item.
  1944    * @param boolean aStatus
  1945    */
  1946   set _matched(aStatus) {
  1947     if (this._isMatch == aStatus) {
  1948       return;
  1950     if (aStatus) {
  1951       this._isMatch = true;
  1952       this.target.removeAttribute("unmatched");
  1953     } else {
  1954       this._isMatch = false;
  1955       this.target.setAttribute("unmatched", "");
  1957   },
  1959   /**
  1960    * Find the first item in the tree of visible items in this item that matches
  1961    * the predicate. Searches in visual order (the order seen by the user).
  1962    * Tests itself, then descends into first the enumerable children and then
  1963    * the non-enumerable children (since they are presented in separate groups).
  1965    * @param function aPredicate
  1966    *        A function that returns true when a match is found.
  1967    * @return Scope | Variable | Property
  1968    *         The first visible scope, variable or property, or null if nothing
  1969    *         is found.
  1970    */
  1971   _findInVisibleItems: function(aPredicate) {
  1972     if (aPredicate(this)) {
  1973       return this;
  1976     if (this._isExpanded) {
  1977       if (this._variablesView._enumVisible) {
  1978         for (let item of this._enumItems) {
  1979           let result = item._findInVisibleItems(aPredicate);
  1980           if (result) {
  1981             return result;
  1986       if (this._variablesView._nonEnumVisible) {
  1987         for (let item of this._nonEnumItems) {
  1988           let result = item._findInVisibleItems(aPredicate);
  1989           if (result) {
  1990             return result;
  1996     return null;
  1997   },
  1999   /**
  2000    * Find the last item in the tree of visible items in this item that matches
  2001    * the predicate. Searches in reverse visual order (opposite of the order
  2002    * seen by the user). Descends into first the non-enumerable children, then
  2003    * the enumerable children (since they are presented in separate groups), and
  2004    * finally tests itself.
  2006    * @param function aPredicate
  2007    *        A function that returns true when a match is found.
  2008    * @return Scope | Variable | Property
  2009    *         The last visible scope, variable or property, or null if nothing
  2010    *         is found.
  2011    */
  2012   _findInVisibleItemsReverse: function(aPredicate) {
  2013     if (this._isExpanded) {
  2014       if (this._variablesView._nonEnumVisible) {
  2015         for (let i = this._nonEnumItems.length - 1; i >= 0; i--) {
  2016           let item = this._nonEnumItems[i];
  2017           let result = item._findInVisibleItemsReverse(aPredicate);
  2018           if (result) {
  2019             return result;
  2024       if (this._variablesView._enumVisible) {
  2025         for (let i = this._enumItems.length - 1; i >= 0; i--) {
  2026           let item = this._enumItems[i];
  2027           let result = item._findInVisibleItemsReverse(aPredicate);
  2028           if (result) {
  2029             return result;
  2035     if (aPredicate(this)) {
  2036       return this;
  2039     return null;
  2040   },
  2042   /**
  2043    * Gets top level variables view instance.
  2044    * @return VariablesView
  2045    */
  2046   get _variablesView() this._topView || (this._topView = (function(self) {
  2047     let parentView = self.ownerView;
  2048     let topView;
  2050     while (topView = parentView.ownerView) {
  2051       parentView = topView;
  2053     return parentView;
  2054   })(this)),
  2056   /**
  2057    * Gets the parent node holding this scope.
  2058    * @return nsIDOMNode
  2059    */
  2060   get parentNode() this.ownerView._list,
  2062   /**
  2063    * Gets the owner document holding this scope.
  2064    * @return nsIHTMLDocument
  2065    */
  2066   get document() this._document || (this._document = this.ownerView.document),
  2068   /**
  2069    * Gets the default window holding this scope.
  2070    * @return nsIDOMWindow
  2071    */
  2072   get window() this._window || (this._window = this.ownerView.window),
  2074   _topView: null,
  2075   _document: null,
  2076   _window: null,
  2078   ownerView: null,
  2079   eval: null,
  2080   switch: null,
  2081   delete: null,
  2082   new: null,
  2083   preventDisableOnChange: false,
  2084   preventDescriptorModifiers: false,
  2085   editing: false,
  2086   editableNameTooltip: "",
  2087   editableValueTooltip: "",
  2088   editButtonTooltip: "",
  2089   deleteButtonTooltip: "",
  2090   domNodeValueTooltip: "",
  2091   contextMenuId: "",
  2092   separatorStr: "",
  2094   _store: null,
  2095   _enumItems: null,
  2096   _nonEnumItems: null,
  2097   _fetched: false,
  2098   _committed: false,
  2099   _isLocked: false,
  2100   _isExpanded: false,
  2101   _isContentVisible: true,
  2102   _isHeaderVisible: true,
  2103   _isArrowVisible: true,
  2104   _isMatch: true,
  2105   _idString: "",
  2106   _nameString: "",
  2107   _target: null,
  2108   _arrow: null,
  2109   _name: null,
  2110   _title: null,
  2111   _enum: null,
  2112   _nonenum: null,
  2113 };
  2115 // Creating maps and arrays thousands of times for variables or properties
  2116 // with a large number of children fills up a lot of memory. Make sure
  2117 // these are instantiated only if needed.
  2118 DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_store", Map);
  2119 DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array);
  2120 DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_nonEnumItems", Array);
  2122 // An ellipsis symbol (usually "…") used for localization.
  2123 XPCOMUtils.defineLazyGetter(Scope, "ellipsis", () =>
  2124   Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data);
  2126 /**
  2127  * A Variable is a Scope holding Property instances.
  2128  * Iterable via "for (let [name, property] of instance) { }".
  2130  * @param Scope aScope
  2131  *        The scope to contain this variable.
  2132  * @param string aName
  2133  *        The variable's name.
  2134  * @param object aDescriptor
  2135  *        The variable's descriptor.
  2136  */
  2137 function Variable(aScope, aName, aDescriptor) {
  2138   this._setTooltips = this._setTooltips.bind(this);
  2139   this._activateNameInput = this._activateNameInput.bind(this);
  2140   this._activateValueInput = this._activateValueInput.bind(this);
  2141   this.openNodeInInspector = this.openNodeInInspector.bind(this);
  2142   this.highlightDomNode = this.highlightDomNode.bind(this);
  2143   this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
  2145   // Treat safe getter descriptors as descriptors with a value.
  2146   if ("getterValue" in aDescriptor) {
  2147     aDescriptor.value = aDescriptor.getterValue;
  2148     delete aDescriptor.get;
  2149     delete aDescriptor.set;
  2152   Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
  2153   this.setGrip(aDescriptor.value);
  2154   this._symbolicName = aName;
  2155   this._absoluteName = aScope.name + "[\"" + aName + "\"]";
  2158 Variable.prototype = Heritage.extend(Scope.prototype, {
  2159   /**
  2160    * Whether this Variable should be prefetched when it is remoted.
  2161    */
  2162   get shouldPrefetch() {
  2163     return this.name == "window" || this.name == "this";
  2164   },
  2166   /**
  2167    * Whether this Variable should paginate its contents.
  2168    */
  2169   get allowPaginate() {
  2170     return this.name != "window" && this.name != "this";
  2171   },
  2173   /**
  2174    * The class name applied to this variable's target element.
  2175    */
  2176   targetClassName: "variables-view-variable variable-or-property",
  2178   /**
  2179    * Create a new Property that is a child of Variable.
  2181    * @param string aName
  2182    *        The name of the new Property.
  2183    * @param object aDescriptor
  2184    *        The property's descriptor.
  2185    * @return Property
  2186    *         The newly created child Property.
  2187    */
  2188   _createChild: function(aName, aDescriptor) {
  2189     return new Property(this, aName, aDescriptor);
  2190   },
  2192   /**
  2193    * Remove this Variable from its parent and remove all children recursively.
  2194    */
  2195   remove: function() {
  2196     if (this._linkedToInspector) {
  2197       this.unhighlightDomNode();
  2198       this._valueLabel.removeEventListener("mouseover", this.highlightDomNode, false);
  2199       this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode, false);
  2200       this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, false);
  2203     this.ownerView._store.delete(this._nameString);
  2204     this._variablesView._itemsByElement.delete(this._target);
  2205     this._variablesView._currHierarchy.delete(this._absoluteName);
  2207     this._target.remove();
  2209     for (let property of this._store.values()) {
  2210       property.remove();
  2212   },
  2214   /**
  2215    * Populates this variable to contain all the properties of an object.
  2217    * @param object aObject
  2218    *        The raw object you want to display.
  2219    * @param object aOptions [optional]
  2220    *        Additional options for adding the properties. Supported options:
  2221    *        - sorted: true to sort all the properties before adding them
  2222    *        - expanded: true to expand all the properties after adding them
  2223    */
  2224   populate: function(aObject, aOptions = {}) {
  2225     // Retrieve the properties only once.
  2226     if (this._fetched) {
  2227       return;
  2229     this._fetched = true;
  2231     let propertyNames = Object.getOwnPropertyNames(aObject);
  2232     let prototype = Object.getPrototypeOf(aObject);
  2234     // Sort all of the properties before adding them, if preferred.
  2235     if (aOptions.sorted) {
  2236       propertyNames.sort();
  2238     // Add all the variable properties.
  2239     for (let name of propertyNames) {
  2240       let descriptor = Object.getOwnPropertyDescriptor(aObject, name);
  2241       if (descriptor.get || descriptor.set) {
  2242         let prop = this._addRawNonValueProperty(name, descriptor);
  2243         if (aOptions.expanded) {
  2244           prop.expanded = true;
  2246       } else {
  2247         let prop = this._addRawValueProperty(name, descriptor, aObject[name]);
  2248         if (aOptions.expanded) {
  2249           prop.expanded = true;
  2253     // Add the variable's __proto__.
  2254     if (prototype) {
  2255       this._addRawValueProperty("__proto__", {}, prototype);
  2257   },
  2259   /**
  2260    * Populates a specific variable or property instance to contain all the
  2261    * properties of an object
  2263    * @param Variable | Property aVar
  2264    *        The target variable to populate.
  2265    * @param object aObject [optional]
  2266    *        The raw object you want to display. If unspecified, the object is
  2267    *        assumed to be defined in a _sourceValue property on the target.
  2268    */
  2269   _populateTarget: function(aVar, aObject = aVar._sourceValue) {
  2270     aVar.populate(aObject);
  2271   },
  2273   /**
  2274    * Adds a property for this variable based on a raw value descriptor.
  2276    * @param string aName
  2277    *        The property's name.
  2278    * @param object aDescriptor
  2279    *        Specifies the exact property descriptor as returned by a call to
  2280    *        Object.getOwnPropertyDescriptor.
  2281    * @param object aValue
  2282    *        The raw property value you want to display.
  2283    * @return Property
  2284    *         The newly added property instance.
  2285    */
  2286   _addRawValueProperty: function(aName, aDescriptor, aValue) {
  2287     let descriptor = Object.create(aDescriptor);
  2288     descriptor.value = VariablesView.getGrip(aValue);
  2290     let propertyItem = this.addItem(aName, descriptor);
  2291     propertyItem._sourceValue = aValue;
  2293     // Add an 'onexpand' callback for the property, lazily handling
  2294     // the addition of new child properties.
  2295     if (!VariablesView.isPrimitive(descriptor)) {
  2296       propertyItem.onexpand = this._populateTarget;
  2298     return propertyItem;
  2299   },
  2301   /**
  2302    * Adds a property for this variable based on a getter/setter descriptor.
  2304    * @param string aName
  2305    *        The property's name.
  2306    * @param object aDescriptor
  2307    *        Specifies the exact property descriptor as returned by a call to
  2308    *        Object.getOwnPropertyDescriptor.
  2309    * @return Property
  2310    *         The newly added property instance.
  2311    */
  2312   _addRawNonValueProperty: function(aName, aDescriptor) {
  2313     let descriptor = Object.create(aDescriptor);
  2314     descriptor.get = VariablesView.getGrip(aDescriptor.get);
  2315     descriptor.set = VariablesView.getGrip(aDescriptor.set);
  2317     return this.addItem(aName, descriptor);
  2318   },
  2320   /**
  2321    * Gets this variable's path to the topmost scope in the form of a string
  2322    * meant for use via eval() or a similar approach.
  2323    * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
  2324    * @return string
  2325    */
  2326   get symbolicName() this._symbolicName,
  2328   /**
  2329    * Gets this variable's symbolic path to the topmost scope.
  2330    * @return array
  2331    * @see Variable._buildSymbolicPath
  2332    */
  2333   get symbolicPath() {
  2334     if (this._symbolicPath) {
  2335       return this._symbolicPath;
  2337     this._symbolicPath = this._buildSymbolicPath();
  2338     return this._symbolicPath;
  2339   },
  2341   /**
  2342    * Build this variable's path to the topmost scope in form of an array of
  2343    * strings, one for each segment of the path.
  2344    * For example, a symbolic path may look like ["0", "foo", "bar"].
  2345    * @return array
  2346    */
  2347   _buildSymbolicPath: function(path = []) {
  2348     if (this.name) {
  2349       path.unshift(this.name);
  2350       if (this.ownerView instanceof Variable) {
  2351         return this.ownerView._buildSymbolicPath(path);
  2354     return path;
  2355   },
  2357   /**
  2358    * Returns this variable's value from the descriptor if available.
  2359    * @return any
  2360    */
  2361   get value() this._initialDescriptor.value,
  2363   /**
  2364    * Returns this variable's getter from the descriptor if available.
  2365    * @return object
  2366    */
  2367   get getter() this._initialDescriptor.get,
  2369   /**
  2370    * Returns this variable's getter from the descriptor if available.
  2371    * @return object
  2372    */
  2373   get setter() this._initialDescriptor.set,
  2375   /**
  2376    * Sets the specific grip for this variable (applies the text content and
  2377    * class name to the value label).
  2379    * The grip should contain the value or the type & class, as defined in the
  2380    * remote debugger protocol. For convenience, undefined and null are
  2381    * both considered types.
  2383    * @param any aGrip
  2384    *        Specifies the value and/or type & class of the variable.
  2385    *        e.g. - 42
  2386    *             - true
  2387    *             - "nasu"
  2388    *             - { type: "undefined" }
  2389    *             - { type: "null" }
  2390    *             - { type: "object", class: "Object" }
  2391    */
  2392   setGrip: function(aGrip) {
  2393     // Don't allow displaying grip information if there's no name available
  2394     // or the grip is malformed.
  2395     if (!this._nameString || aGrip === undefined || aGrip === null) {
  2396       return;
  2398     // Getters and setters should display grip information in sub-properties.
  2399     if (this.getter || this.setter) {
  2400       return;
  2403     let prevGrip = this._valueGrip;
  2404     if (prevGrip) {
  2405       this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
  2407     this._valueGrip = aGrip;
  2408     this._valueString = VariablesView.getString(aGrip, {
  2409       concise: true,
  2410       noEllipsis: true,
  2411     });
  2412     this._valueClassName = VariablesView.getClass(aGrip);
  2414     this._valueLabel.classList.add(this._valueClassName);
  2415     this._valueLabel.setAttribute("value", this._valueString);
  2416     this._separatorLabel.hidden = false;
  2418     // DOMNodes get special treatment since they can be linked to the inspector
  2419     if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") {
  2420       this._linkToInspector();
  2422   },
  2424   /**
  2425    * Marks this variable as overridden.
  2427    * @param boolean aFlag
  2428    *        Whether this variable is overridden or not.
  2429    */
  2430   setOverridden: function(aFlag) {
  2431     if (aFlag) {
  2432       this._target.setAttribute("overridden", "");
  2433     } else {
  2434       this._target.removeAttribute("overridden");
  2436   },
  2438   /**
  2439    * Briefly flashes this variable.
  2441    * @param number aDuration [optional]
  2442    *        An optional flash animation duration.
  2443    */
  2444   flash: function(aDuration = ITEM_FLASH_DURATION) {
  2445     let fadeInDelay = this._variablesView.lazyEmptyDelay + 1;
  2446     let fadeOutDelay = fadeInDelay + aDuration;
  2448     setNamedTimeout("vview-flash-in" + this._absoluteName,
  2449       fadeInDelay, () => this._target.setAttribute("changed", ""));
  2451     setNamedTimeout("vview-flash-out" + this._absoluteName,
  2452       fadeOutDelay, () => this._target.removeAttribute("changed"));
  2453   },
  2455   /**
  2456    * Initializes this variable's id, view and binds event listeners.
  2458    * @param string aName
  2459    *        The variable's name.
  2460    * @param object aDescriptor
  2461    *        The variable's descriptor.
  2462    */
  2463   _init: function(aName, aDescriptor) {
  2464     this._idString = generateId(this._nameString = aName);
  2465     this._displayScope(aName, this.targetClassName);
  2466     this._displayVariable();
  2467     this._customizeVariable();
  2468     this._prepareTooltips();
  2469     this._setAttributes();
  2470     this._addEventListeners();
  2472     if (this._initialDescriptor.enumerable ||
  2473         this._nameString == "this" ||
  2474         this._nameString == "<return>" ||
  2475         this._nameString == "<exception>") {
  2476       this.ownerView._enum.appendChild(this._target);
  2477       this.ownerView._enumItems.push(this);
  2478     } else {
  2479       this.ownerView._nonenum.appendChild(this._target);
  2480       this.ownerView._nonEnumItems.push(this);
  2482   },
  2484   /**
  2485    * Creates the necessary nodes for this variable.
  2486    */
  2487   _displayVariable: function() {
  2488     let document = this.document;
  2489     let descriptor = this._initialDescriptor;
  2491     let separatorLabel = this._separatorLabel = document.createElement("label");
  2492     separatorLabel.className = "plain separator";
  2493     separatorLabel.setAttribute("value", this.separatorStr + " ");
  2495     let valueLabel = this._valueLabel = document.createElement("label");
  2496     valueLabel.className = "plain value";
  2497     valueLabel.setAttribute("flex", "1");
  2498     valueLabel.setAttribute("crop", "center");
  2500     this._title.appendChild(separatorLabel);
  2501     this._title.appendChild(valueLabel);
  2503     if (VariablesView.isPrimitive(descriptor)) {
  2504       this.hideArrow();
  2507     // If no value will be displayed, we don't need the separator.
  2508     if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
  2509       separatorLabel.hidden = true;
  2512     // If this is a getter/setter property, create two child pseudo-properties
  2513     // called "get" and "set" that display the corresponding functions.
  2514     if (descriptor.get || descriptor.set) {
  2515       separatorLabel.hidden = true;
  2516       valueLabel.hidden = true;
  2518       // Changing getter/setter names is never allowed.
  2519       this.switch = null;
  2521       // Getter/setter properties require special handling when it comes to
  2522       // evaluation and deletion.
  2523       if (this.ownerView.eval) {
  2524         this.delete = VariablesView.getterOrSetterDeleteCallback;
  2525         this.evaluationMacro = VariablesView.overrideValueEvalMacro;
  2527       // Deleting getters and setters individually is not allowed if no
  2528       // evaluation method is provided.
  2529       else {
  2530         this.delete = null;
  2531         this.evaluationMacro = null;
  2534       let getter = this.addItem("get", { value: descriptor.get });
  2535       let setter = this.addItem("set", { value: descriptor.set });
  2536       getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
  2537       setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
  2539       getter.hideArrow();
  2540       setter.hideArrow();
  2541       this.expand();
  2543   },
  2545   /**
  2546    * Adds specific nodes for this variable based on custom flags.
  2547    */
  2548   _customizeVariable: function() {
  2549     let ownerView = this.ownerView;
  2550     let descriptor = this._initialDescriptor;
  2552     if (ownerView.eval && this.getter || this.setter) {
  2553       let editNode = this._editNode = this.document.createElement("toolbarbutton");
  2554       editNode.className = "plain variables-view-edit";
  2555       editNode.addEventListener("mousedown", this._onEdit.bind(this), false);
  2556       this._title.insertBefore(editNode, this._spacer);
  2559     if (ownerView.delete) {
  2560       let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
  2561       deleteNode.className = "plain variables-view-delete";
  2562       deleteNode.addEventListener("click", this._onDelete.bind(this), false);
  2563       this._title.appendChild(deleteNode);
  2566     if (ownerView.new) {
  2567       let addPropertyNode = this._addPropertyNode = this.document.createElement("toolbarbutton");
  2568       addPropertyNode.className = "plain variables-view-add-property";
  2569       addPropertyNode.addEventListener("mousedown", this._onAddProperty.bind(this), false);
  2570       this._title.appendChild(addPropertyNode);
  2572       // Can't add properties to primitive values, hide the node in those cases.
  2573       if (VariablesView.isPrimitive(descriptor)) {
  2574         addPropertyNode.setAttribute("invisible", "");
  2578     if (ownerView.contextMenuId) {
  2579       this._title.setAttribute("context", ownerView.contextMenuId);
  2582     if (ownerView.preventDescriptorModifiers) {
  2583       return;
  2586     if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
  2587       let nonWritableIcon = this.document.createElement("hbox");
  2588       nonWritableIcon.className = "plain variable-or-property-non-writable-icon";
  2589       nonWritableIcon.setAttribute("optional-visibility", "");
  2590       this._title.appendChild(nonWritableIcon);
  2592     if (descriptor.value && typeof descriptor.value == "object") {
  2593       if (descriptor.value.frozen) {
  2594         let frozenLabel = this.document.createElement("label");
  2595         frozenLabel.className = "plain variable-or-property-frozen-label";
  2596         frozenLabel.setAttribute("optional-visibility", "");
  2597         frozenLabel.setAttribute("value", "F");
  2598         this._title.appendChild(frozenLabel);
  2600       if (descriptor.value.sealed) {
  2601         let sealedLabel = this.document.createElement("label");
  2602         sealedLabel.className = "plain variable-or-property-sealed-label";
  2603         sealedLabel.setAttribute("optional-visibility", "");
  2604         sealedLabel.setAttribute("value", "S");
  2605         this._title.appendChild(sealedLabel);
  2607       if (!descriptor.value.extensible) {
  2608         let nonExtensibleLabel = this.document.createElement("label");
  2609         nonExtensibleLabel.className = "plain variable-or-property-non-extensible-label";
  2610         nonExtensibleLabel.setAttribute("optional-visibility", "");
  2611         nonExtensibleLabel.setAttribute("value", "N");
  2612         this._title.appendChild(nonExtensibleLabel);
  2615   },
  2617   /**
  2618    * Prepares all tooltips for this variable.
  2619    */
  2620   _prepareTooltips: function() {
  2621     this._target.addEventListener("mouseover", this._setTooltips, false);
  2622   },
  2624   /**
  2625    * Sets all tooltips for this variable.
  2626    */
  2627   _setTooltips: function() {
  2628     this._target.removeEventListener("mouseover", this._setTooltips, false);
  2630     let ownerView = this.ownerView;
  2631     if (ownerView.preventDescriptorModifiers) {
  2632       return;
  2635     let tooltip = this.document.createElement("tooltip");
  2636     tooltip.id = "tooltip-" + this._idString;
  2637     tooltip.setAttribute("orient", "horizontal");
  2639     let labels = [
  2640       "configurable", "enumerable", "writable",
  2641       "frozen", "sealed", "extensible", "overridden", "WebIDL"];
  2643     for (let type of labels) {
  2644       let labelElement = this.document.createElement("label");
  2645       labelElement.className = type;
  2646       labelElement.setAttribute("value", STR.GetStringFromName(type + "Tooltip"));
  2647       tooltip.appendChild(labelElement);
  2650     this._target.appendChild(tooltip);
  2651     this._target.setAttribute("tooltip", tooltip.id);
  2653     if (this._editNode && ownerView.eval) {
  2654       this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
  2656     if (this._openInspectorNode && this._linkedToInspector) {
  2657       this._openInspectorNode.setAttribute("tooltiptext", this.ownerView.domNodeValueTooltip);
  2659     if (this._valueLabel && ownerView.eval) {
  2660       this._valueLabel.setAttribute("tooltiptext", ownerView.editableValueTooltip);
  2662     if (this._name && ownerView.switch) {
  2663       this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip);
  2665     if (this._deleteNode && ownerView.delete) {
  2666       this._deleteNode.setAttribute("tooltiptext", ownerView.deleteButtonTooltip);
  2668   },
  2670   /**
  2671    * Get the parent variablesview toolbox, if any.
  2672    */
  2673   get toolbox() {
  2674     return this._variablesView.toolbox;
  2675   },
  2677   /**
  2678    * Checks if this variable is a DOMNode and is part of a variablesview that
  2679    * has been linked to the toolbox, so that highlighting and jumping to the
  2680    * inspector can be done.
  2681    */
  2682   _isLinkableToInspector: function() {
  2683     let isDomNode = this._valueGrip && this._valueGrip.preview.kind === "DOMNode";
  2684     let hasBeenLinked = this._linkedToInspector;
  2685     let hasToolbox = !!this.toolbox;
  2687     return isDomNode && !hasBeenLinked && hasToolbox;
  2688   },
  2690   /**
  2691    * If the variable is a DOMNode, and if a toolbox is set, then link it to the
  2692    * inspector (highlight on hover, and jump to markup-view on click)
  2693    */
  2694   _linkToInspector: function() {
  2695     if (!this._isLinkableToInspector()) {
  2696       return;
  2699     // Listen to value mouseover/click events to highlight and jump
  2700     this._valueLabel.addEventListener("mouseover", this.highlightDomNode, false);
  2701     this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode, false);
  2703     // Add a button to open the node in the inspector
  2704     this._openInspectorNode = this.document.createElement("toolbarbutton");
  2705     this._openInspectorNode.className = "plain variables-view-open-inspector";
  2706     this._openInspectorNode.addEventListener("mousedown", this.openNodeInInspector, false);
  2707     this._title.insertBefore(this._openInspectorNode, this._title.querySelector("toolbarbutton"));
  2709     this._linkedToInspector = true;
  2710   },
  2712   /**
  2713    * In case this variable is a DOMNode and part of a variablesview that has been
  2714    * linked to the toolbox's inspector, then select the corresponding node in
  2715    * the inspector, and switch the inspector tool in the toolbox
  2716    * @return a promise that resolves when the node is selected and the inspector
  2717    * has been switched to and is ready
  2718    */
  2719   openNodeInInspector: function(event) {
  2720     if (!this.toolbox) {
  2721       return promise.reject(new Error("Toolbox not available"));
  2724     event && event.stopPropagation();
  2726     return Task.spawn(function*() {
  2727       yield this.toolbox.initInspector();
  2729       let nodeFront = this._nodeFront;
  2730       if (!nodeFront) {
  2731         nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this._valueGrip.actor);
  2734       if (nodeFront) {
  2735         yield this.toolbox.selectTool("inspector");
  2737         let inspectorReady = promise.defer();
  2738         this.toolbox.getPanel("inspector").once("inspector-updated", inspectorReady.resolve);
  2739         yield this.toolbox.selection.setNodeFront(nodeFront, "variables-view");
  2740         yield inspectorReady.promise;
  2742     }.bind(this));
  2743   },
  2745   /**
  2746    * In case this variable is a DOMNode and part of a variablesview that has been
  2747    * linked to the toolbox's inspector, then highlight the corresponding node
  2748    */
  2749   highlightDomNode: function() {
  2750     if (this.toolbox) {
  2751       if (this._nodeFront) {
  2752         // If the nodeFront has been retrieved before, no need to ask the server
  2753         // again for it
  2754         this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
  2755         return;
  2758       this.toolbox.highlighterUtils.highlightDomValueGrip(this._valueGrip).then(front => {
  2759         this._nodeFront = front;
  2760       });
  2762   },
  2764   /**
  2765    * Unhighlight a previously highlit node
  2766    * @see highlightDomNode
  2767    */
  2768   unhighlightDomNode: function() {
  2769     if (this.toolbox) {
  2770       this.toolbox.highlighterUtils.unhighlight();
  2772   },
  2774   /**
  2775    * Sets a variable's configurable, enumerable and writable attributes,
  2776    * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__'
  2777    * reference.
  2778    */
  2779   _setAttributes: function() {
  2780     let ownerView = this.ownerView;
  2781     if (ownerView.preventDescriptorModifiers) {
  2782       return;
  2785     let descriptor = this._initialDescriptor;
  2786     let target = this._target;
  2787     let name = this._nameString;
  2789     if (ownerView.eval) {
  2790       target.setAttribute("editable", "");
  2793     if (!descriptor.configurable) {
  2794       target.setAttribute("non-configurable", "");
  2796     if (!descriptor.enumerable) {
  2797       target.setAttribute("non-enumerable", "");
  2799     if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
  2800       target.setAttribute("non-writable", "");
  2803     if (descriptor.value && typeof descriptor.value == "object") {
  2804       if (descriptor.value.frozen) {
  2805         target.setAttribute("frozen", "");
  2807       if (descriptor.value.sealed) {
  2808         target.setAttribute("sealed", "");
  2810       if (!descriptor.value.extensible) {
  2811         target.setAttribute("non-extensible", "");
  2815     if (descriptor && "getterValue" in descriptor) {
  2816       target.setAttribute("safe-getter", "");
  2819     if (name == "this") {
  2820       target.setAttribute("self", "");
  2822     else if (name == "<exception>") {
  2823       target.setAttribute("exception", "");
  2824       target.setAttribute("pseudo-item", "");
  2826     else if (name == "<return>") {
  2827       target.setAttribute("return", "");
  2828       target.setAttribute("pseudo-item", "");
  2830     else if (name == "__proto__") {
  2831       target.setAttribute("proto", "");
  2832       target.setAttribute("pseudo-item", "");
  2835     if (Object.keys(descriptor).length == 0) {
  2836       target.setAttribute("pseudo-item", "");
  2838   },
  2840   /**
  2841    * Adds the necessary event listeners for this variable.
  2842    */
  2843   _addEventListeners: function() {
  2844     this._name.addEventListener("dblclick", this._activateNameInput, false);
  2845     this._valueLabel.addEventListener("mousedown", this._activateValueInput, false);
  2846     this._title.addEventListener("mousedown", this._onClick, false);
  2847   },
  2849   /**
  2850    * Makes this variable's name editable.
  2851    */
  2852   _activateNameInput: function(e) {
  2853     if (!this._variablesView.alignedValues) {
  2854       this._separatorLabel.hidden = true;
  2855       this._valueLabel.hidden = true;
  2858     EditableName.create(this, {
  2859       onSave: aKey => {
  2860         if (!this._variablesView.preventDisableOnChange) {
  2861           this._disable();
  2863         this.ownerView.switch(this, aKey);
  2864       },
  2865       onCleanup: () => {
  2866         if (!this._variablesView.alignedValues) {
  2867           this._separatorLabel.hidden = false;
  2868           this._valueLabel.hidden = false;
  2871     }, e);
  2872   },
  2874   /**
  2875    * Makes this variable's value editable.
  2876    */
  2877   _activateValueInput: function(e) {
  2878     EditableValue.create(this, {
  2879       onSave: aString => {
  2880         if (this._linkedToInspector) {
  2881           this.unhighlightDomNode();
  2883         if (!this._variablesView.preventDisableOnChange) {
  2884           this._disable();
  2886         this.ownerView.eval(this, aString);
  2888     }, e);
  2889   },
  2891   /**
  2892    * Disables this variable prior to a new name switch or value evaluation.
  2893    */
  2894   _disable: function() {
  2895     // Prevent the variable from being collapsed or expanded.
  2896     this.hideArrow();
  2898     // Hide any nodes that may offer information about the variable.
  2899     for (let node of this._title.childNodes) {
  2900       node.hidden = node != this._arrow && node != this._name;
  2902     this._enum.hidden = true;
  2903     this._nonenum.hidden = true;
  2904   },
  2906   /**
  2907    * The current macro used to generate the string evaluated when performing
  2908    * a variable or property value change.
  2909    */
  2910   evaluationMacro: VariablesView.simpleValueEvalMacro,
  2912   /**
  2913    * The click listener for the edit button.
  2914    */
  2915   _onEdit: function(e) {
  2916     if (e.button != 0) {
  2917       return;
  2920     e.preventDefault();
  2921     e.stopPropagation();
  2922     this._activateValueInput();
  2923   },
  2925   /**
  2926    * The click listener for the delete button.
  2927    */
  2928   _onDelete: function(e) {
  2929     if ("button" in e && e.button != 0) {
  2930       return;
  2933     e.preventDefault();
  2934     e.stopPropagation();
  2936     if (this.ownerView.delete) {
  2937       if (!this.ownerView.delete(this)) {
  2938         this.hide();
  2941   },
  2943   /**
  2944    * The click listener for the add property button.
  2945    */
  2946   _onAddProperty: function(e) {
  2947     if ("button" in e && e.button != 0) {
  2948       return;
  2951     e.preventDefault();
  2952     e.stopPropagation();
  2954     this.expanded = true;
  2956     let item = this.addItem(" ", {
  2957       value: undefined,
  2958       configurable: true,
  2959       enumerable: true,
  2960       writable: true
  2961     }, true);
  2963     // Force showing the separator.
  2964     item._separatorLabel.hidden = false;
  2966     EditableNameAndValue.create(item, {
  2967       onSave: ([aKey, aValue]) => {
  2968         if (!this._variablesView.preventDisableOnChange) {
  2969           this._disable();
  2971         this.ownerView.new(this, aKey, aValue);
  2973     }, e);
  2974   },
  2976   _symbolicName: "",
  2977   _symbolicPath: null,
  2978   _absoluteName: "",
  2979   _initialDescriptor: null,
  2980   _separatorLabel: null,
  2981   _valueLabel: null,
  2982   _spacer: null,
  2983   _editNode: null,
  2984   _deleteNode: null,
  2985   _addPropertyNode: null,
  2986   _tooltip: null,
  2987   _valueGrip: null,
  2988   _valueString: "",
  2989   _valueClassName: "",
  2990   _prevExpandable: false,
  2991   _prevExpanded: false
  2992 });
  2994 /**
  2995  * A Property is a Variable holding additional child Property instances.
  2996  * Iterable via "for (let [name, property] of instance) { }".
  2998  * @param Variable aVar
  2999  *        The variable to contain this property.
  3000  * @param string aName
  3001  *        The property's name.
  3002  * @param object aDescriptor
  3003  *        The property's descriptor.
  3004  */
  3005 function Property(aVar, aName, aDescriptor) {
  3006   Variable.call(this, aVar, aName, aDescriptor);
  3007   this._symbolicName = aVar._symbolicName + "[\"" + aName + "\"]";
  3008   this._absoluteName = aVar._absoluteName + "[\"" + aName + "\"]";
  3011 Property.prototype = Heritage.extend(Variable.prototype, {
  3012   /**
  3013    * The class name applied to this property's target element.
  3014    */
  3015   targetClassName: "variables-view-property variable-or-property"
  3016 });
  3018 /**
  3019  * A generator-iterator over the VariablesView, Scopes, Variables and Properties.
  3020  */
  3021 VariablesView.prototype["@@iterator"] =
  3022 Scope.prototype["@@iterator"] =
  3023 Variable.prototype["@@iterator"] =
  3024 Property.prototype["@@iterator"] = function*() {
  3025   yield* this._store;
  3026 };
  3028 /**
  3029  * Forget everything recorded about added scopes, variables or properties.
  3030  * @see VariablesView.commitHierarchy
  3031  */
  3032 VariablesView.prototype.clearHierarchy = function() {
  3033   this._prevHierarchy.clear();
  3034   this._currHierarchy.clear();
  3035 };
  3037 /**
  3038  * Perform operations on all the VariablesView Scopes, Variables and Properties
  3039  * after you've added all the items you wanted.
  3041  * Calling this method is optional, and does the following:
  3042  *   - styles the items overridden by other items in parent scopes
  3043  *   - reopens the items which were previously expanded
  3044  *   - flashes the items whose values changed
  3045  */
  3046 VariablesView.prototype.commitHierarchy = function() {
  3047   for (let [, currItem] of this._currHierarchy) {
  3048     // Avoid performing expensive operations.
  3049     if (this.commitHierarchyIgnoredItems[currItem._nameString]) {
  3050       continue;
  3052     let overridden = this.isOverridden(currItem);
  3053     if (overridden) {
  3054       currItem.setOverridden(true);
  3056     let expanded = !currItem._committed && this.wasExpanded(currItem);
  3057     if (expanded) {
  3058       currItem.expand();
  3060     let changed = !currItem._committed && this.hasChanged(currItem);
  3061     if (changed) {
  3062       currItem.flash();
  3064     currItem._committed = true;
  3066   if (this.oncommit) {
  3067     this.oncommit(this);
  3069 };
  3071 // Some variables are likely to contain a very large number of properties.
  3072 // It would be a bad idea to re-expand them or perform expensive operations.
  3073 VariablesView.prototype.commitHierarchyIgnoredItems = Heritage.extend(null, {
  3074   "window": true,
  3075   "this": true
  3076 });
  3078 /**
  3079  * Checks if the an item was previously expanded, if it existed in a
  3080  * previous hierarchy.
  3082  * @param Scope | Variable | Property aItem
  3083  *        The item to verify.
  3084  * @return boolean
  3085  *         Whether the item was expanded.
  3086  */
  3087 VariablesView.prototype.wasExpanded = function(aItem) {
  3088   if (!(aItem instanceof Scope)) {
  3089     return false;
  3091   let prevItem = this._prevHierarchy.get(aItem._absoluteName || aItem._nameString);
  3092   return prevItem ? prevItem._isExpanded : false;
  3093 };
  3095 /**
  3096  * Checks if the an item's displayed value (a representation of the grip)
  3097  * has changed, if it existed in a previous hierarchy.
  3099  * @param Variable | Property aItem
  3100  *        The item to verify.
  3101  * @return boolean
  3102  *         Whether the item has changed.
  3103  */
  3104 VariablesView.prototype.hasChanged = function(aItem) {
  3105   // Only analyze Variables and Properties for displayed value changes.
  3106   // Scopes are just collections of Variables and Properties and
  3107   // don't have a "value", so they can't change.
  3108   if (!(aItem instanceof Variable)) {
  3109     return false;
  3111   let prevItem = this._prevHierarchy.get(aItem._absoluteName);
  3112   return prevItem ? prevItem._valueString != aItem._valueString : false;
  3113 };
  3115 /**
  3116  * Checks if the an item was previously expanded, if it existed in a
  3117  * previous hierarchy.
  3119  * @param Scope | Variable | Property aItem
  3120  *        The item to verify.
  3121  * @return boolean
  3122  *         Whether the item was expanded.
  3123  */
  3124 VariablesView.prototype.isOverridden = function(aItem) {
  3125   // Only analyze Variables for being overridden in different Scopes.
  3126   if (!(aItem instanceof Variable) || aItem instanceof Property) {
  3127     return false;
  3129   let currVariableName = aItem._nameString;
  3130   let parentScopes = this.getParentScopesForVariableOrProperty(aItem);
  3132   for (let otherScope of parentScopes) {
  3133     for (let [otherVariableName] of otherScope) {
  3134       if (otherVariableName == currVariableName) {
  3135         return true;
  3139   return false;
  3140 };
  3142 /**
  3143  * Returns true if the descriptor represents an undefined, null or
  3144  * primitive value.
  3146  * @param object aDescriptor
  3147  *        The variable's descriptor.
  3148  */
  3149 VariablesView.isPrimitive = function(aDescriptor) {
  3150   // For accessor property descriptors, the getter and setter need to be
  3151   // contained in 'get' and 'set' properties.
  3152   let getter = aDescriptor.get;
  3153   let setter = aDescriptor.set;
  3154   if (getter || setter) {
  3155     return false;
  3158   // As described in the remote debugger protocol, the value grip
  3159   // must be contained in a 'value' property.
  3160   let grip = aDescriptor.value;
  3161   if (typeof grip != "object") {
  3162     return true;
  3165   // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long
  3166   // strings are considered types.
  3167   let type = grip.type;
  3168   if (type == "undefined" ||
  3169       type == "null" ||
  3170       type == "Infinity" ||
  3171       type == "-Infinity" ||
  3172       type == "NaN" ||
  3173       type == "-0" ||
  3174       type == "longString") {
  3175     return true;
  3178   return false;
  3179 };
  3181 /**
  3182  * Returns true if the descriptor represents an undefined value.
  3184  * @param object aDescriptor
  3185  *        The variable's descriptor.
  3186  */
  3187 VariablesView.isUndefined = function(aDescriptor) {
  3188   // For accessor property descriptors, the getter and setter need to be
  3189   // contained in 'get' and 'set' properties.
  3190   let getter = aDescriptor.get;
  3191   let setter = aDescriptor.set;
  3192   if (typeof getter == "object" && getter.type == "undefined" &&
  3193       typeof setter == "object" && setter.type == "undefined") {
  3194     return true;
  3197   // As described in the remote debugger protocol, the value grip
  3198   // must be contained in a 'value' property.
  3199   let grip = aDescriptor.value;
  3200   if (typeof grip == "object" && grip.type == "undefined") {
  3201     return true;
  3204   return false;
  3205 };
  3207 /**
  3208  * Returns true if the descriptor represents a falsy value.
  3210  * @param object aDescriptor
  3211  *        The variable's descriptor.
  3212  */
  3213 VariablesView.isFalsy = function(aDescriptor) {
  3214   // As described in the remote debugger protocol, the value grip
  3215   // must be contained in a 'value' property.
  3216   let grip = aDescriptor.value;
  3217   if (typeof grip != "object") {
  3218     return !grip;
  3221   // For convenience, undefined, null, NaN, and -0 are all considered types.
  3222   let type = grip.type;
  3223   if (type == "undefined" ||
  3224       type == "null" ||
  3225       type == "NaN" ||
  3226       type == "-0") {
  3227     return true;
  3230   return false;
  3231 };
  3233 /**
  3234  * Returns true if the value is an instance of Variable or Property.
  3236  * @param any aValue
  3237  *        The value to test.
  3238  */
  3239 VariablesView.isVariable = function(aValue) {
  3240   return aValue instanceof Variable;
  3241 };
  3243 /**
  3244  * Returns a standard grip for a value.
  3246  * @param any aValue
  3247  *        The raw value to get a grip for.
  3248  * @return any
  3249  *         The value's grip.
  3250  */
  3251 VariablesView.getGrip = function(aValue) {
  3252   switch (typeof aValue) {
  3253     case "boolean":
  3254     case "string":
  3255       return aValue;
  3256     case "number":
  3257       if (aValue === Infinity) {
  3258         return { type: "Infinity" };
  3259       } else if (aValue === -Infinity) {
  3260         return { type: "-Infinity" };
  3261       } else if (Number.isNaN(aValue)) {
  3262         return { type: "NaN" };
  3263       } else if (1 / aValue === -Infinity) {
  3264         return { type: "-0" };
  3266       return aValue;
  3267     case "undefined":
  3268       // document.all is also "undefined"
  3269       if (aValue === undefined) {
  3270         return { type: "undefined" };
  3272     case "object":
  3273       if (aValue === null) {
  3274         return { type: "null" };
  3276     case "function":
  3277       return { type: "object",
  3278                class: WebConsoleUtils.getObjectClassName(aValue) };
  3279     default:
  3280       Cu.reportError("Failed to provide a grip for value of " + typeof value +
  3281                      ": " + aValue);
  3282       return null;
  3284 };
  3286 /**
  3287  * Returns a custom formatted property string for a grip.
  3289  * @param any aGrip
  3290  *        @see Variable.setGrip
  3291  * @param object aOptions
  3292  *        Options:
  3293  *        - concise: boolean that tells you want a concisely formatted string.
  3294  *        - noStringQuotes: boolean that tells to not quote strings.
  3295  *        - noEllipsis: boolean that tells to not add an ellipsis after the
  3296  *        initial text of a longString.
  3297  * @return string
  3298  *         The formatted property string.
  3299  */
  3300 VariablesView.getString = function(aGrip, aOptions = {}) {
  3301   if (aGrip && typeof aGrip == "object") {
  3302     switch (aGrip.type) {
  3303       case "undefined":
  3304       case "null":
  3305       case "NaN":
  3306       case "Infinity":
  3307       case "-Infinity":
  3308       case "-0":
  3309         return aGrip.type;
  3310       default:
  3311         let stringifier = VariablesView.stringifiers.byType[aGrip.type];
  3312         if (stringifier) {
  3313           let result = stringifier(aGrip, aOptions);
  3314           if (result != null) {
  3315             return result;
  3319         if (aGrip.displayString) {
  3320           return VariablesView.getString(aGrip.displayString, aOptions);
  3323         if (aGrip.type == "object" && aOptions.concise) {
  3324           return aGrip.class;
  3327         return "[" + aGrip.type + " " + aGrip.class + "]";
  3331   switch (typeof aGrip) {
  3332     case "string":
  3333       return VariablesView.stringifiers.byType.string(aGrip, aOptions);
  3334     case "boolean":
  3335       return aGrip ? "true" : "false";
  3336     case "number":
  3337       if (!aGrip && 1 / aGrip === -Infinity) {
  3338         return "-0";
  3340     default:
  3341       return aGrip + "";
  3343 };
  3345 /**
  3346  * The VariablesView stringifiers are used by VariablesView.getString(). These
  3347  * are organized by object type, object class and by object actor preview kind.
  3348  * Some objects share identical ways for previews, for example Arrays, Sets and
  3349  * NodeLists.
  3351  * Any stringifier function must return a string. If null is returned, * then
  3352  * the default stringifier will be used. When invoked, the stringifier is
  3353  * given the same two arguments as those given to VariablesView.getString().
  3354  */
  3355 VariablesView.stringifiers = {};
  3357 VariablesView.stringifiers.byType = {
  3358   string: function(aGrip, {noStringQuotes}) {
  3359     if (noStringQuotes) {
  3360       return aGrip;
  3362     return '"' + aGrip + '"';
  3363   },
  3365   longString: function({initial}, {noStringQuotes, noEllipsis}) {
  3366     let ellipsis = noEllipsis ? "" : Scope.ellipsis;
  3367     if (noStringQuotes) {
  3368       return initial + ellipsis;
  3370     let result = '"' + initial + '"';
  3371     if (!ellipsis) {
  3372       return result;
  3374     return result.substr(0, result.length - 1) + ellipsis + '"';
  3375   },
  3377   object: function(aGrip, aOptions) {
  3378     let {preview} = aGrip;
  3379     let stringifier;
  3380     if (preview && preview.kind) {
  3381       stringifier = VariablesView.stringifiers.byObjectKind[preview.kind];
  3383     if (!stringifier && aGrip.class) {
  3384       stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class];
  3386     if (stringifier) {
  3387       return stringifier(aGrip, aOptions);
  3389     return null;
  3390   },
  3391 }; // VariablesView.stringifiers.byType
  3393 VariablesView.stringifiers.byObjectClass = {
  3394   Function: function(aGrip, {concise}) {
  3395     // TODO: Bug 948484 - support arrow functions and ES6 generators
  3397     let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
  3398     name = VariablesView.getString(name, { noStringQuotes: true });
  3400     // TODO: Bug 948489 - Support functions with destructured parameters and
  3401     // rest parameters
  3402     let params = aGrip.parameterNames || "";
  3403     if (!concise) {
  3404       return "function " + name + "(" + params + ")";
  3406     return (name || "function ") + "(" + params + ")";
  3407   },
  3409   RegExp: function({displayString}) {
  3410     return VariablesView.getString(displayString, { noStringQuotes: true });
  3411   },
  3413   Date: function({preview}) {
  3414     if (!preview || !("timestamp" in preview)) {
  3415       return null;
  3418     if (typeof preview.timestamp != "number") {
  3419       return new Date(preview.timestamp).toString(); // invalid date
  3422     return "Date " + new Date(preview.timestamp).toISOString();
  3423   },
  3425   String: function({displayString}) {
  3426     if (displayString === undefined) {
  3427       return null;
  3429     return VariablesView.getString(displayString);
  3430   },
  3432   Number: function({preview}) {
  3433     if (preview === undefined) {
  3434       return null;
  3436     return VariablesView.getString(preview.value);
  3437   },
  3438 }; // VariablesView.stringifiers.byObjectClass
  3440 VariablesView.stringifiers.byObjectClass.Boolean =
  3441   VariablesView.stringifiers.byObjectClass.Number;
  3443 VariablesView.stringifiers.byObjectKind = {
  3444   ArrayLike: function(aGrip, {concise}) {
  3445     let {preview} = aGrip;
  3446     if (concise) {
  3447       return aGrip.class + "[" + preview.length + "]";
  3450     if (!preview.items) {
  3451       return null;
  3454     let shown = 0, result = [], lastHole = null;
  3455     for (let item of preview.items) {
  3456       if (item === null) {
  3457         if (lastHole !== null) {
  3458           result[lastHole] += ",";
  3459         } else {
  3460           result.push("");
  3462         lastHole = result.length - 1;
  3463       } else {
  3464         lastHole = null;
  3465         result.push(VariablesView.getString(item, { concise: true }));
  3467       shown++;
  3470     if (shown < preview.length) {
  3471       let n = preview.length - shown;
  3472       result.push(VariablesView.stringifiers._getNMoreString(n));
  3473     } else if (lastHole !== null) {
  3474       // make sure we have the right number of commas...
  3475       result[lastHole] += ",";
  3478     let prefix = aGrip.class == "Array" ? "" : aGrip.class + " ";
  3479     return prefix + "[" + result.join(", ") + "]";
  3480   },
  3482   MapLike: function(aGrip, {concise}) {
  3483     let {preview} = aGrip;
  3484     if (concise || !preview.entries) {
  3485       let size = typeof preview.size == "number" ?
  3486                    "[" + preview.size + "]" : "";
  3487       return aGrip.class + size;
  3490     let entries = [];
  3491     for (let [key, value] of preview.entries) {
  3492       let keyString = VariablesView.getString(key, {
  3493         concise: true,
  3494         noStringQuotes: true,
  3495       });
  3496       let valueString = VariablesView.getString(value, { concise: true });
  3497       entries.push(keyString + ": " + valueString);
  3500     if (typeof preview.size == "number" && preview.size > entries.length) {
  3501       let n = preview.size - entries.length;
  3502       entries.push(VariablesView.stringifiers._getNMoreString(n));
  3505     return aGrip.class + " {" + entries.join(", ") + "}";
  3506   },
  3508   ObjectWithText: function(aGrip, {concise}) {
  3509     if (concise) {
  3510       return aGrip.class;
  3513     return aGrip.class + " " + VariablesView.getString(aGrip.preview.text);
  3514   },
  3516   ObjectWithURL: function(aGrip, {concise}) {
  3517     let result = aGrip.class;
  3518     let url = aGrip.preview.url;
  3519     if (!VariablesView.isFalsy({ value: url })) {
  3520       result += " \u2192 " + WebConsoleUtils.abbreviateSourceURL(url,
  3521                              { onlyCropQuery: !concise });
  3523     return result;
  3524   },
  3526   // Stringifier for any kind of object.
  3527   Object: function(aGrip, {concise}) {
  3528     if (concise) {
  3529       return aGrip.class;
  3532     let {preview} = aGrip;
  3533     let props = [];
  3534     for (let key of Object.keys(preview.ownProperties || {})) {
  3535       let value = preview.ownProperties[key];
  3536       let valueString = "";
  3537       if (value.get) {
  3538         valueString = "Getter";
  3539       } else if (value.set) {
  3540         valueString = "Setter";
  3541       } else {
  3542         valueString = VariablesView.getString(value.value, { concise: true });
  3544       props.push(key + ": " + valueString);
  3547     for (let key of Object.keys(preview.safeGetterValues || {})) {
  3548       let value = preview.safeGetterValues[key];
  3549       let valueString = VariablesView.getString(value.getterValue,
  3550                                                 { concise: true });
  3551       props.push(key + ": " + valueString);
  3554     if (!props.length) {
  3555       return null;
  3558     if (preview.ownPropertiesLength) {
  3559       let previewLength = Object.keys(preview.ownProperties).length;
  3560       let diff = preview.ownPropertiesLength - previewLength;
  3561       if (diff > 0) {
  3562         props.push(VariablesView.stringifiers._getNMoreString(diff));
  3566     let prefix = aGrip.class != "Object" ? aGrip.class + " " : "";
  3567     return prefix + "{" + props.join(", ") + "}";
  3568   }, // Object
  3570   Error: function(aGrip, {concise}) {
  3571     let {preview} = aGrip;
  3572     let name = VariablesView.getString(preview.name, { noStringQuotes: true });
  3573     if (concise) {
  3574       return name || aGrip.class;
  3577     let msg = name + ": " +
  3578               VariablesView.getString(preview.message, { noStringQuotes: true });
  3580     if (!VariablesView.isFalsy({ value: preview.stack })) {
  3581       msg += "\n" + STR.GetStringFromName("variablesViewErrorStacktrace") +
  3582              "\n" + preview.stack;
  3585     return msg;
  3586   },
  3588   DOMException: function(aGrip, {concise}) {
  3589     let {preview} = aGrip;
  3590     if (concise) {
  3591       return preview.name || aGrip.class;
  3594     let msg = aGrip.class + " [" + preview.name + ": " +
  3595               VariablesView.getString(preview.message) + "\n" +
  3596               "code: " + preview.code + "\n" +
  3597               "nsresult: 0x" + (+preview.result).toString(16);
  3599     if (preview.filename) {
  3600       msg += "\nlocation: " + preview.filename;
  3601       if (preview.lineNumber) {
  3602         msg += ":" + preview.lineNumber;
  3606     return msg + "]";
  3607   },
  3609   DOMEvent: function(aGrip, {concise}) {
  3610     let {preview} = aGrip;
  3611     if (!preview.type) {
  3612       return null;
  3615     if (concise) {
  3616       return aGrip.class + " " + preview.type;
  3619     let result = preview.type;
  3621     if (preview.eventKind == "key" && preview.modifiers &&
  3622         preview.modifiers.length) {
  3623       result += " " + preview.modifiers.join("-");
  3626     let props = [];
  3627     if (preview.target) {
  3628       let target = VariablesView.getString(preview.target, { concise: true });
  3629       props.push("target: " + target);
  3632     for (let prop in preview.properties) {
  3633       let value = preview.properties[prop];
  3634       props.push(prop + ": " + VariablesView.getString(value, { concise: true }));
  3637     return result + " {" + props.join(", ") + "}";
  3638   }, // DOMEvent
  3640   DOMNode: function(aGrip, {concise}) {
  3641     let {preview} = aGrip;
  3643     switch (preview.nodeType) {
  3644       case Ci.nsIDOMNode.DOCUMENT_NODE: {
  3645         let location = WebConsoleUtils.abbreviateSourceURL(preview.location,
  3646                                                            { onlyCropQuery: !concise });
  3647         return aGrip.class + " \u2192 " + location;
  3650       case Ci.nsIDOMNode.ATTRIBUTE_NODE: {
  3651         let value = VariablesView.getString(preview.value, { noStringQuotes: true });
  3652         return preview.nodeName + '="' + escapeHTML(value) + '"';
  3655       case Ci.nsIDOMNode.TEXT_NODE:
  3656         return preview.nodeName + " " +
  3657                VariablesView.getString(preview.textContent);
  3659       case Ci.nsIDOMNode.COMMENT_NODE: {
  3660         let comment = VariablesView.getString(preview.textContent,
  3661                                               { noStringQuotes: true });
  3662         return "<!--" + comment + "-->";
  3665       case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: {
  3666         if (concise || !preview.childNodes) {
  3667           return aGrip.class + "[" + preview.childNodesLength + "]";
  3669         let nodes = [];
  3670         for (let node of preview.childNodes) {
  3671           nodes.push(VariablesView.getString(node));
  3673         if (nodes.length < preview.childNodesLength) {
  3674           let n = preview.childNodesLength - nodes.length;
  3675           nodes.push(VariablesView.stringifiers._getNMoreString(n));
  3677         return aGrip.class + " [" + nodes.join(", ") + "]";
  3680       case Ci.nsIDOMNode.ELEMENT_NODE: {
  3681         let attrs = preview.attributes;
  3682         if (!concise) {
  3683           let n = 0, result = "<" + preview.nodeName;
  3684           for (let name in attrs) {
  3685             let value = VariablesView.getString(attrs[name],
  3686                                                 { noStringQuotes: true });
  3687             result += " " + name + '="' + escapeHTML(value) + '"';
  3688             n++;
  3690           if (preview.attributesLength > n) {
  3691             result += " " + Scope.ellipsis;
  3693           return result + ">";
  3696         let result = "<" + preview.nodeName;
  3697         if (attrs.id) {
  3698           result += "#" + attrs.id;
  3700         return result + ">";
  3703       default:
  3704         return null;
  3706   }, // DOMNode
  3707 }; // VariablesView.stringifiers.byObjectKind
  3710 /**
  3711  * Get the "N more…" formatted string, given an N. This is used for displaying
  3712  * how many elements are not displayed in an object preview (eg. an array).
  3714  * @private
  3715  * @param number aNumber
  3716  * @return string
  3717  */
  3718 VariablesView.stringifiers._getNMoreString = function(aNumber) {
  3719   let str = STR.GetStringFromName("variablesViewMoreObjects");
  3720   return PluralForm.get(aNumber, str).replace("#1", aNumber);
  3721 };
  3723 /**
  3724  * Returns a custom class style for a grip.
  3726  * @param any aGrip
  3727  *        @see Variable.setGrip
  3728  * @return string
  3729  *         The custom class style.
  3730  */
  3731 VariablesView.getClass = function(aGrip) {
  3732   if (aGrip && typeof aGrip == "object") {
  3733     if (aGrip.preview) {
  3734       switch (aGrip.preview.kind) {
  3735         case "DOMNode":
  3736           return "token-domnode";
  3740     switch (aGrip.type) {
  3741       case "undefined":
  3742         return "token-undefined";
  3743       case "null":
  3744         return "token-null";
  3745       case "Infinity":
  3746       case "-Infinity":
  3747       case "NaN":
  3748       case "-0":
  3749         return "token-number";
  3750       case "longString":
  3751         return "token-string";
  3754   switch (typeof aGrip) {
  3755     case "string":
  3756       return "token-string";
  3757     case "boolean":
  3758       return "token-boolean";
  3759     case "number":
  3760       return "token-number";
  3761     default:
  3762       return "token-other";
  3764 };
  3766 /**
  3767  * A monotonically-increasing counter, that guarantees the uniqueness of scope,
  3768  * variables and properties ids.
  3770  * @param string aName
  3771  *        An optional string to prefix the id with.
  3772  * @return number
  3773  *         A unique id.
  3774  */
  3775 let generateId = (function() {
  3776   let count = 0;
  3777   return function(aName = "") {
  3778     return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count);
  3779   };
  3780 })();
  3782 /**
  3783  * Escape some HTML special characters. We do not need full HTML serialization
  3784  * here, we just want to make strings safe to display in HTML attributes, for
  3785  * the stringifiers.
  3787  * @param string aString
  3788  * @return string
  3789  */
  3790 function escapeHTML(aString) {
  3791   return aString.replace(/&/g, "&amp;")
  3792                 .replace(/"/g, "&quot;")
  3793                 .replace(/</g, "&lt;")
  3794                 .replace(/>/g, "&gt;");
  3798 /**
  3799  * An Editable encapsulates the UI of an edit box that overlays a label,
  3800  * allowing the user to edit the value.
  3802  * @param Variable aVariable
  3803  *        The Variable or Property to make editable.
  3804  * @param object aOptions
  3805  *        - onSave
  3806  *          The callback to call with the value when editing is complete.
  3807  *        - onCleanup
  3808  *          The callback to call when the editable is removed for any reason.
  3809  */
  3810 function Editable(aVariable, aOptions) {
  3811   this._variable = aVariable;
  3812   this._onSave = aOptions.onSave;
  3813   this._onCleanup = aOptions.onCleanup;
  3816 Editable.create = function(aVariable, aOptions, aEvent) {
  3817   let editable = new this(aVariable, aOptions);
  3818   editable.activate(aEvent);
  3819   return editable;
  3820 };
  3822 Editable.prototype = {
  3823   /**
  3824    * The class name for targeting this Editable type's label element. Overridden
  3825    * by inheriting classes.
  3826    */
  3827   className: null,
  3829   /**
  3830    * Boolean indicating whether this Editable should activate. Overridden by
  3831    * inheriting classes.
  3832    */
  3833   shouldActivate: null,
  3835   /**
  3836    * The label element for this Editable. Overridden by inheriting classes.
  3837    */
  3838   label: null,
  3840   /**
  3841    * Activate this editable by replacing the input box it overlays and
  3842    * initialize the handlers.
  3844    * @param Event e [optional]
  3845    *        Optionally, the Event object that was used to activate the Editable.
  3846    */
  3847   activate: function(e) {
  3848     if (!this.shouldActivate) {
  3849       this._onCleanup && this._onCleanup();
  3850       return;
  3853     let { label } = this;
  3854     let initialString = label.getAttribute("value");
  3856     if (e) {
  3857       e.preventDefault();
  3858       e.stopPropagation();
  3861     // Create a texbox input element which will be shown in the current
  3862     // element's specified label location.
  3863     let input = this._input = this._variable.document.createElement("textbox");
  3864     input.className = "plain " + this.className;
  3865     input.setAttribute("value", initialString);
  3866     input.setAttribute("flex", "1");
  3868     // Replace the specified label with a textbox input element.
  3869     label.parentNode.replaceChild(input, label);
  3870     this._variable._variablesView.boxObject.ensureElementIsVisible(input);
  3871     input.select();
  3873     // When the value is a string (displayed as "value"), then we probably want
  3874     // to change it to another string in the textbox, so to avoid typing the ""
  3875     // again, tackle with the selection bounds just a bit.
  3876     if (initialString.match(/^".+"$/)) {
  3877       input.selectionEnd--;
  3878       input.selectionStart++;
  3881     this._onKeypress = this._onKeypress.bind(this);
  3882     this._onBlur = this._onBlur.bind(this);
  3883     input.addEventListener("keypress", this._onKeypress);
  3884     input.addEventListener("blur", this._onBlur);
  3886     this._prevExpandable = this._variable.twisty;
  3887     this._prevExpanded = this._variable.expanded;
  3888     this._variable.collapse();
  3889     this._variable.hideArrow();
  3890     this._variable.locked = true;
  3891     this._variable.editing = true;
  3892   },
  3894   /**
  3895    * Remove the input box and restore the Variable or Property to its previous
  3896    * state.
  3897    */
  3898   deactivate: function() {
  3899     this._input.removeEventListener("keypress", this._onKeypress);
  3900     this._input.removeEventListener("blur", this.deactivate);
  3901     this._input.parentNode.replaceChild(this.label, this._input);
  3902     this._input = null;
  3904     let { boxObject } = this._variable._variablesView;
  3905     boxObject.scrollBy(-this._variable._target, 0);
  3906     this._variable.locked = false;
  3907     this._variable.twisty = this._prevExpandable;
  3908     this._variable.expanded = this._prevExpanded;
  3909     this._variable.editing = false;
  3910     this._onCleanup && this._onCleanup();
  3911   },
  3913   /**
  3914    * Save the current value and deactivate the Editable.
  3915    */
  3916   _save: function() {
  3917     let initial = this.label.getAttribute("value");
  3918     let current = this._input.value.trim();
  3919     this.deactivate();
  3920     if (initial != current) {
  3921       this._onSave(current);
  3923   },
  3925   /**
  3926    * Called when tab is pressed, allowing subclasses to link different
  3927    * behavior to tabbing if desired.
  3928    */
  3929   _next: function() {
  3930     this._save();
  3931   },
  3933   /**
  3934    * Called when escape is pressed, indicating a cancelling of editing without
  3935    * saving.
  3936    */
  3937   _reset: function() {
  3938     this.deactivate();
  3939     this._variable.focus();
  3940   },
  3942   /**
  3943    * Event handler for when the input loses focus.
  3944    */
  3945   _onBlur: function() {
  3946     this.deactivate();
  3947   },
  3949   /**
  3950    * Event handler for when the input receives a key press.
  3951    */
  3952   _onKeypress: function(e) {
  3953     e.stopPropagation();
  3955     switch (e.keyCode) {
  3956       case e.DOM_VK_TAB:
  3957         this._next();
  3958         break;
  3959       case e.DOM_VK_RETURN:
  3960         this._save();
  3961         break;
  3962       case e.DOM_VK_ESCAPE:
  3963         this._reset();
  3964         break;
  3966   },
  3967 };
  3970 /**
  3971  * An Editable specific to editing the name of a Variable or Property.
  3972  */
  3973 function EditableName(aVariable, aOptions) {
  3974   Editable.call(this, aVariable, aOptions);
  3977 EditableName.create = Editable.create;
  3979 EditableName.prototype = Heritage.extend(Editable.prototype, {
  3980   className: "element-name-input",
  3982   get label() {
  3983     return this._variable._name;
  3984   },
  3986   get shouldActivate() {
  3987     return !!this._variable.ownerView.switch;
  3988   },
  3989 });
  3992 /**
  3993  * An Editable specific to editing the value of a Variable or Property.
  3994  */
  3995 function EditableValue(aVariable, aOptions) {
  3996   Editable.call(this, aVariable, aOptions);
  3999 EditableValue.create = Editable.create;
  4001 EditableValue.prototype = Heritage.extend(Editable.prototype, {
  4002   className: "element-value-input",
  4004   get label() {
  4005     return this._variable._valueLabel;
  4006   },
  4008   get shouldActivate() {
  4009     return !!this._variable.ownerView.eval;
  4010   },
  4011 });
  4014 /**
  4015  * An Editable specific to editing the key and value of a new property.
  4016  */
  4017 function EditableNameAndValue(aVariable, aOptions) {
  4018   EditableName.call(this, aVariable, aOptions);
  4021 EditableNameAndValue.create = Editable.create;
  4023 EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, {
  4024   _reset: function(e) {
  4025     // Hide the Variable or Property if the user presses escape.
  4026     this._variable.remove();
  4027     this.deactivate();
  4028   },
  4030   _next: function(e) {
  4031     // Override _next so as to set both key and value at the same time.
  4032     let key = this._input.value;
  4033     this.label.setAttribute("value", key);
  4035     let valueEditable = EditableValue.create(this._variable, {
  4036       onSave: aValue => {
  4037         this._onSave([key, aValue]);
  4039     });
  4040     valueEditable._reset = () => {
  4041       this._variable.remove();
  4042       valueEditable.deactivate();
  4043     };
  4044   },
  4046   _save: function(e) {
  4047     // Both _save and _next activate the value edit box.
  4048     this._next(e);
  4050 });

mercurial