summaryrefslogtreecommitdiffstats
path: root/devtools/client/storage/VariablesView.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/storage/VariablesView.sys.mjs
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/storage/VariablesView.sys.mjs')
-rw-r--r--devtools/client/storage/VariablesView.sys.mjs4445
1 files changed, 4445 insertions, 0 deletions
diff --git a/devtools/client/storage/VariablesView.sys.mjs b/devtools/client/storage/VariablesView.sys.mjs
new file mode 100644
index 0000000000..769840c022
--- /dev/null
+++ b/devtools/client/storage/VariablesView.sys.mjs
@@ -0,0 +1,4445 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-disable mozilla/no-aArgs */
+
+const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
+const LAZY_EMPTY_DELAY = 150; // ms
+const SCROLL_PAGE_SIZE_DEFAULT = 0;
+const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
+const PAGE_SIZE_MAX_JUMPS = 30;
+const SEARCH_ACTION_MAX_DELAY = 300; // ms
+const ITEM_FLASH_DURATION = 300; // ms
+
+import { require } from "resource://devtools/shared/loader/Loader.sys.mjs";
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const {
+ getSourceNames,
+} = require("resource://devtools/client/shared/source-utils.js");
+const { extend } = require("resource://devtools/shared/extend.js");
+const {
+ ViewHelpers,
+ setNamedTimeout,
+} = require("resource://devtools/client/shared/widgets/view-helpers.js");
+const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
+const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const {
+ LocalizationHelper,
+ ELLIPSIS,
+} = require("resource://devtools/shared/l10n.js");
+
+const L10N = new LocalizationHelper(DBG_STRINGS_URI);
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
+/**
+ * A tree view for inspecting scopes, objects and properties.
+ * Iterable via "for (let [id, scope] of instance) { }".
+ * Requires the devtools common.css and debugger.css skin stylesheets.
+ *
+ * To allow replacing variable or property values in this view, provide an
+ * "eval" function property. To allow replacing variable or property names,
+ * provide a "switch" function. To handle deleting variables or properties,
+ * provide a "delete" function.
+ *
+ * @param Node aParentNode
+ * The parent node to hold this view.
+ * @param object aFlags [optional]
+ * An object contaning initialization options for this view.
+ * e.g. { lazyEmpty: true, searchEnabled: true ... }
+ */
+export function VariablesView(aParentNode, aFlags = {}) {
+ this._store = []; // Can't use a Map because Scope names needn't be unique.
+ this._itemsByElement = new WeakMap();
+ this._prevHierarchy = new Map();
+ this._currHierarchy = new Map();
+
+ this._parent = aParentNode;
+ this._parent.classList.add("variables-view-container");
+ this._parent.classList.add("theme-body");
+ this._appendEmptyNotice();
+
+ this._onSearchboxInput = this._onSearchboxInput.bind(this);
+ this._onSearchboxKeyDown = this._onSearchboxKeyDown.bind(this);
+ this._onViewKeyDown = this._onViewKeyDown.bind(this);
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createXULElement("scrollbox");
+ this._list.setAttribute("orient", "vertical");
+ this._list.addEventListener("keydown", this._onViewKeyDown);
+ this._parent.appendChild(this._list);
+
+ for (const name in aFlags) {
+ this[name] = aFlags[name];
+ }
+
+ EventEmitter.decorate(this);
+}
+
+VariablesView.prototype = {
+ /**
+ * Helper setter for populating this container with a raw object.
+ *
+ * @param object aObject
+ * The raw object to display. You can only provide this object
+ * if you want the variables view to work in sync mode.
+ */
+ set rawObject(aObject) {
+ this.empty();
+ this.addScope()
+ .addItem(undefined, { enumerable: true })
+ .populate(aObject, { sorted: true });
+ },
+
+ /**
+ * Adds a scope to contain any inspected variables.
+ *
+ * This new scope will be considered the parent of any other scope
+ * added afterwards.
+ *
+ * @param string l10nId
+ * The scope localized string id.
+ * @param string aCustomClass
+ * An additional class name for the containing element.
+ * @return Scope
+ * The newly created Scope instance.
+ */
+ addScope(l10nId = "", aCustomClass = "") {
+ this._removeEmptyNotice();
+ this._toggleSearchVisibility(true);
+
+ const scope = new Scope(this, l10nId, { customClass: aCustomClass });
+ this._store.push(scope);
+ this._itemsByElement.set(scope._target, scope);
+ this._currHierarchy.set(l10nId, scope);
+ scope.header = !!l10nId;
+
+ return scope;
+ },
+
+ /**
+ * Removes all items from this container.
+ *
+ * @param number aTimeout [optional]
+ * The number of milliseconds to delay the operation if
+ * lazy emptying of this container is enabled.
+ */
+ empty(aTimeout = this.lazyEmptyDelay) {
+ // If there are no items in this container, emptying is useless.
+ if (!this._store.length) {
+ return;
+ }
+
+ this._store.length = 0;
+ this._itemsByElement = new WeakMap();
+ this._prevHierarchy = this._currHierarchy;
+ this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
+
+ // Check if this empty operation may be executed lazily.
+ if (this.lazyEmpty && aTimeout > 0) {
+ this._emptySoon(aTimeout);
+ return;
+ }
+
+ while (this._list.hasChildNodes()) {
+ this._list.firstChild.remove();
+ }
+
+ this._appendEmptyNotice();
+ this._toggleSearchVisibility(false);
+ },
+
+ /**
+ * Emptying this container and rebuilding it immediately afterwards would
+ * result in a brief redraw flicker, because the previously expanded nodes
+ * may get asynchronously re-expanded, after fetching the prototype and
+ * properties from a server.
+ *
+ * To avoid such behaviour, a normal container list is rebuild, but not
+ * immediately attached to the parent container. The old container list
+ * is kept around for a short period of time, hopefully accounting for the
+ * data fetching delay. In the meantime, any operations can be executed
+ * normally.
+ *
+ * @see VariablesView.empty
+ * @see VariablesView.commitHierarchy
+ */
+ _emptySoon(aTimeout) {
+ const prevList = this._list;
+ const currList = (this._list = this.document.createXULElement("scrollbox"));
+
+ this.window.setTimeout(() => {
+ prevList.removeEventListener("keydown", this._onViewKeyDown);
+ currList.addEventListener("keydown", this._onViewKeyDown);
+ currList.setAttribute("orient", "vertical");
+
+ this._parent.removeChild(prevList);
+ this._parent.appendChild(currList);
+
+ if (!this._store.length) {
+ this._appendEmptyNotice();
+ this._toggleSearchVisibility(false);
+ }
+ }, aTimeout);
+ },
+
+ /**
+ * Optional DevTools toolbox containing this VariablesView. Used to
+ * communicate with the inspector and highlighter.
+ */
+ toolbox: null,
+
+ /**
+ * The controller for this VariablesView, if it has one.
+ */
+ controller: null,
+
+ /**
+ * The amount of time (in milliseconds) it takes to empty this view lazily.
+ */
+ lazyEmptyDelay: LAZY_EMPTY_DELAY,
+
+ /**
+ * Specifies if this view may be emptied lazily.
+ * @see VariablesView.prototype.empty
+ */
+ lazyEmpty: false,
+
+ /**
+ * Specifies if nodes in this view may be searched lazily.
+ */
+ lazySearch: true,
+
+ /**
+ * The number of elements in this container to jump when Page Up or Page Down
+ * keys are pressed. If falsy, then the page size will be based on the
+ * container height.
+ */
+ scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT,
+
+ /**
+ * Function called each time a variable or property's value is changed via
+ * user interaction. If null, then value changes are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ eval: null,
+
+ /**
+ * Function called each time a variable or property's name is changed via
+ * user interaction. If null, then name changes are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ switch: null,
+
+ /**
+ * Function called each time a variable or property is deleted via
+ * user interaction. If null, then deletions are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ delete: null,
+
+ /**
+ * Function called each time a property is added via user interaction. If
+ * null, then property additions are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ new: null,
+
+ /**
+ * Specifies if after an eval or switch operation, the variable or property
+ * which has been edited should be disabled.
+ */
+ preventDisableOnChange: false,
+
+ /**
+ * Specifies if, whenever a variable or property descriptor is available,
+ * configurable, enumerable, writable, frozen, sealed and extensible
+ * attributes should not affect presentation.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ preventDescriptorModifiers: false,
+
+ /**
+ * The tooltip text shown on a variable or property's value if an |eval|
+ * function is provided, in order to change the variable or property's value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editableValueTooltip: L10N.getStr("variablesEditableValueTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's name if a |switch|
+ * function is provided, in order to change the variable or property's name.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editableNameTooltip: L10N.getStr("variablesEditableNameTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's edit button if an
+ * |eval| function is provided and a getter/setter descriptor is present,
+ * in order to change the variable or property to a plain value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editButtonTooltip: L10N.getStr("variablesEditButtonTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's value if that value is
+ * a DOMNode that can be highlighted and selected in the inspector.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ domNodeValueTooltip: L10N.getStr("variablesDomNodeValueTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's delete button if a
+ * |delete| function is provided, in order to delete the variable or property.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ deleteButtonTooltip: L10N.getStr("variablesCloseButtonTooltip"),
+
+ /**
+ * Specifies the context menu attribute set on variables and properties.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ contextMenuId: "",
+
+ /**
+ * The separator label between the variables or properties name and value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ separatorStr: L10N.getStr("variablesSeparatorLabel"),
+
+ /**
+ * Specifies if enumerable properties and variables should be displayed.
+ * These variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set enumVisible(aFlag) {
+ this._enumVisible = aFlag;
+
+ for (const scope of this._store) {
+ scope._enumVisible = aFlag;
+ }
+ },
+
+ /**
+ * Specifies if non-enumerable properties and variables should be displayed.
+ * These variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set nonEnumVisible(aFlag) {
+ this._nonEnumVisible = aFlag;
+
+ for (const scope of this._store) {
+ scope._nonEnumVisible = aFlag;
+ }
+ },
+
+ /**
+ * Specifies if only enumerable properties and variables should be displayed.
+ * Both types of these variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set onlyEnumVisible(aFlag) {
+ if (aFlag) {
+ this.enumVisible = true;
+ this.nonEnumVisible = false;
+ } else {
+ this.enumVisible = true;
+ this.nonEnumVisible = true;
+ }
+ },
+
+ /**
+ * Sets if the variable and property searching is enabled.
+ * @param boolean aFlag
+ */
+ set searchEnabled(aFlag) {
+ aFlag ? this._enableSearch() : this._disableSearch();
+ },
+
+ /**
+ * Gets if the variable and property searching is enabled.
+ * @return boolean
+ */
+ get searchEnabled() {
+ return !!this._searchboxContainer;
+ },
+
+ /**
+ * Enables variable and property searching in this view.
+ * Use the "searchEnabled" setter to enable searching.
+ */
+ _enableSearch() {
+ // If searching was already enabled, no need to re-enable it again.
+ if (this._searchboxContainer) {
+ return;
+ }
+ const document = this.document;
+ const ownerNode = this._parent.parentNode;
+
+ const container = (this._searchboxContainer =
+ document.createXULElement("hbox"));
+ container.className = "devtools-toolbar devtools-input-toolbar";
+
+ // Hide the variables searchbox container if there are no variables or
+ // properties to display.
+ container.hidden = !this._store.length;
+
+ const searchbox = (this._searchboxNode = document.createElementNS(
+ HTML_NS,
+ "input"
+ ));
+ searchbox.className = "variables-view-searchinput devtools-filterinput";
+ searchbox.setAttribute("data-l10n-id", "storage-variable-view-search-box");
+ searchbox.addEventListener("input", this._onSearchboxInput);
+ searchbox.addEventListener("keydown", this._onSearchboxKeyDown);
+
+ container.appendChild(searchbox);
+ ownerNode.insertBefore(container, this._parent);
+ },
+
+ /**
+ * Disables variable and property searching in this view.
+ * Use the "searchEnabled" setter to disable searching.
+ */
+ _disableSearch() {
+ // If searching was already disabled, no need to re-disable it again.
+ if (!this._searchboxContainer) {
+ return;
+ }
+ this._searchboxContainer.remove();
+ this._searchboxNode.removeEventListener("input", this._onSearchboxInput);
+ this._searchboxNode.removeEventListener(
+ "keydown",
+ this._onSearchboxKeyDown
+ );
+
+ this._searchboxContainer = null;
+ this._searchboxNode = null;
+ },
+
+ /**
+ * Sets the variables searchbox container hidden or visible.
+ * It's hidden by default.
+ *
+ * @param boolean aVisibleFlag
+ * Specifies the intended visibility.
+ */
+ _toggleSearchVisibility(aVisibleFlag) {
+ // If searching was already disabled, there's no need to hide it.
+ if (!this._searchboxContainer) {
+ return;
+ }
+ this._searchboxContainer.hidden = !aVisibleFlag;
+ },
+
+ /**
+ * Listener handling the searchbox input event.
+ */
+ _onSearchboxInput() {
+ this.scheduleSearch(this._searchboxNode.value);
+ },
+
+ /**
+ * Listener handling the searchbox keydown event.
+ */
+ _onSearchboxKeyDown(e) {
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ this._onSearchboxInput();
+ return;
+ case KeyCodes.DOM_VK_ESCAPE:
+ this._searchboxNode.value = "";
+ this._onSearchboxInput();
+ }
+ },
+
+ /**
+ * Schedules searching for variables or properties matching the query.
+ *
+ * @param string aToken
+ * The variable or property to search for.
+ * @param number aWait
+ * The amount of milliseconds to wait until draining.
+ */
+ scheduleSearch(aToken, aWait) {
+ // Check if this search operation may not be executed lazily.
+ if (!this.lazySearch) {
+ this._doSearch(aToken);
+ return;
+ }
+
+ // The amount of time to wait for the requests to settle.
+ const maxDelay = SEARCH_ACTION_MAX_DELAY;
+ const delay = aWait === undefined ? maxDelay / aToken.length : aWait;
+
+ // Allow requests to settle down first.
+ setNamedTimeout("vview-search", delay, () => this._doSearch(aToken));
+ },
+
+ /**
+ * Performs a case insensitive search for variables or properties matching
+ * the query, and hides non-matched items.
+ *
+ * If aToken is falsy, then all the scopes are unhidden and expanded,
+ * while the available variables and properties inside those scopes are
+ * just unhidden.
+ *
+ * @param string aToken
+ * The variable or property to search for.
+ */
+ _doSearch(aToken) {
+ if (this.controller && this.controller.supportsSearch()) {
+ // Retrieve the main Scope in which we add attributes
+ const scope = this._store[0]._store.get(undefined);
+ if (!aToken) {
+ // Prune the view from old previous content
+ // so that we delete the intermediate search results
+ // we created in previous searches
+ for (const property of scope._store.values()) {
+ property.remove();
+ }
+ }
+ // Retrieve new attributes eventually hidden in splits
+ this.controller.performSearch(scope, aToken);
+ // Filter already displayed attributes
+ if (aToken) {
+ scope._performSearch(aToken.toLowerCase());
+ }
+ return;
+ }
+ for (const scope of this._store) {
+ switch (aToken) {
+ case "":
+ case null:
+ case undefined:
+ scope.expand();
+ scope._performSearch("");
+ break;
+ default:
+ scope._performSearch(aToken.toLowerCase());
+ break;
+ }
+ }
+ },
+
+ /**
+ * Find the first item in the tree of visible items in this container that
+ * matches the predicate. Searches in visual order (the order seen by the
+ * user). Descends into each scope to check the scope and its children.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The first visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItems(aPredicate) {
+ for (const scope of this._store) {
+ const result = scope._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Find the last item in the tree of visible items in this container that
+ * matches the predicate. Searches in reverse visual order (opposite of the
+ * order seen by the user). Descends into each scope to check the scope and
+ * its children.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The last visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItemsReverse(aPredicate) {
+ for (let i = this._store.length - 1; i >= 0; i--) {
+ const scope = this._store[i];
+ const result = scope._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Gets the scope at the specified index.
+ *
+ * @param number aIndex
+ * The scope's index.
+ * @return Scope
+ * The scope if found, undefined if not.
+ */
+ getScopeAtIndex(aIndex) {
+ return this._store[aIndex];
+ },
+
+ /**
+ * Recursively searches this container for the scope, variable or property
+ * displayed by the specified node.
+ *
+ * @param Node aNode
+ * The node to search for.
+ * @return Scope | Variable | Property
+ * The matched scope, variable or property, or null if nothing is found.
+ */
+ getItemForNode(aNode) {
+ return this._itemsByElement.get(aNode);
+ },
+
+ /**
+ * Gets the scope owning a Variable or Property.
+ *
+ * @param Variable | Property
+ * The variable or property to retrieven the owner scope for.
+ * @return Scope
+ * The owner scope.
+ */
+ getOwnerScopeForVariableOrProperty(aItem) {
+ if (!aItem) {
+ return null;
+ }
+ // If this is a Scope, return it.
+ if (!(aItem instanceof Variable)) {
+ return aItem;
+ }
+ // If this is a Variable or Property, find its owner scope.
+ if (aItem instanceof Variable && aItem.ownerView) {
+ return this.getOwnerScopeForVariableOrProperty(aItem.ownerView);
+ }
+ return null;
+ },
+
+ /**
+ * Gets the parent scopes for a specified Variable or Property.
+ * The returned list will not include the owner scope.
+ *
+ * @param Variable | Property
+ * The variable or property for which to find the parent scopes.
+ * @return array
+ * A list of parent Scopes.
+ */
+ getParentScopesForVariableOrProperty(aItem) {
+ const scope = this.getOwnerScopeForVariableOrProperty(aItem);
+ return this._store.slice(0, Math.max(this._store.indexOf(scope), 0));
+ },
+
+ /**
+ * Gets the currently focused scope, variable or property in this view.
+ *
+ * @return Scope | Variable | Property
+ * The focused scope, variable or property, or null if nothing is found.
+ */
+ getFocusedItem() {
+ const focused = this.document.commandDispatcher.focusedElement;
+ return this.getItemForNode(focused);
+ },
+
+ /**
+ * Focuses the first visible scope, variable, or property in this container.
+ */
+ focusFirstVisibleItem() {
+ const focusableItem = this._findInVisibleItems(item => item.focusable);
+ if (focusableItem) {
+ this._focusItem(focusableItem);
+ }
+ this._parent.scrollTop = 0;
+ this._parent.scrollLeft = 0;
+ },
+
+ /**
+ * Focuses the last visible scope, variable, or property in this container.
+ */
+ focusLastVisibleItem() {
+ const focusableItem = this._findInVisibleItemsReverse(
+ item => item.focusable
+ );
+ if (focusableItem) {
+ this._focusItem(focusableItem);
+ }
+ this._parent.scrollTop = this._parent.scrollHeight;
+ this._parent.scrollLeft = 0;
+ },
+
+ /**
+ * Focuses the next scope, variable or property in this view.
+ */
+ focusNextItem() {
+ this.focusItemAtDelta(+1);
+ },
+
+ /**
+ * Focuses the previous scope, variable or property in this view.
+ */
+ focusPrevItem() {
+ this.focusItemAtDelta(-1);
+ },
+
+ /**
+ * Focuses another scope, variable or property in this view, based on
+ * the index distance from the currently focused item.
+ *
+ * @param number aDelta
+ * A scalar specifying by how many items should the selection change.
+ */
+ focusItemAtDelta(aDelta) {
+ const direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
+ let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
+ while (distance--) {
+ if (!this._focusChange(direction)) {
+ break; // Out of bounds.
+ }
+ }
+ },
+
+ /**
+ * Focuses the next or previous scope, variable or property in this view.
+ *
+ * @param string aDirection
+ * Either "advanceFocus" or "rewindFocus".
+ * @return boolean
+ * False if the focus went out of bounds and the first or last element
+ * in this view was focused instead.
+ */
+ _focusChange(aDirection) {
+ const commandDispatcher = this.document.commandDispatcher;
+ const prevFocusedElement = commandDispatcher.focusedElement;
+ let currFocusedItem = null;
+
+ do {
+ commandDispatcher[aDirection]();
+
+ // Make sure the newly focused item is a part of this view.
+ // If the focus goes out of bounds, revert the previously focused item.
+ if (!(currFocusedItem = this.getFocusedItem())) {
+ prevFocusedElement.focus();
+ return false;
+ }
+ } while (!currFocusedItem.focusable);
+
+ // Focus remained within bounds.
+ return true;
+ },
+
+ /**
+ * Focuses a scope, variable or property and makes sure it's visible.
+ *
+ * @param aItem Scope | Variable | Property
+ * The item to focus.
+ * @param boolean aCollapseFlag
+ * True if the focused item should also be collapsed.
+ * @return boolean
+ * True if the item was successfully focused.
+ */
+ _focusItem(aItem, aCollapseFlag) {
+ if (!aItem.focusable) {
+ return false;
+ }
+ if (aCollapseFlag) {
+ aItem.collapse();
+ }
+ aItem._target.focus();
+ aItem._arrow.scrollIntoView({ block: "nearest" });
+ return true;
+ },
+
+ /**
+ * Copy current selection to clipboard.
+ */
+ _copyItem() {
+ const item = this.getFocusedItem();
+ lazy.clipboardHelper.copyString(
+ item._nameString + item.separatorStr + item._valueString
+ );
+ },
+
+ /**
+ * Listener handling a key down event on the view.
+ */
+ // eslint-disable-next-line complexity
+ _onViewKeyDown(e) {
+ const item = this.getFocusedItem();
+
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(e);
+
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_C:
+ if (e.ctrlKey || e.metaKey) {
+ this._copyItem();
+ }
+ return;
+
+ case KeyCodes.DOM_VK_UP:
+ // Always rewind focus.
+ this.focusPrevItem(true);
+ return;
+
+ case KeyCodes.DOM_VK_DOWN:
+ // Always advance focus.
+ this.focusNextItem(true);
+ return;
+
+ case KeyCodes.DOM_VK_LEFT:
+ // Collapse scopes, variables and properties before rewinding focus.
+ if (item._isExpanded && item._isArrowVisible) {
+ item.collapse();
+ } else {
+ this._focusItem(item.ownerView);
+ }
+ return;
+
+ case KeyCodes.DOM_VK_RIGHT:
+ // Nothing to do here if this item never expands.
+ if (!item._isArrowVisible) {
+ return;
+ }
+ // Expand scopes, variables and properties before advancing focus.
+ if (!item._isExpanded) {
+ item.expand();
+ } else {
+ this.focusNextItem(true);
+ }
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_UP:
+ // Rewind a certain number of elements based on the container height.
+ this.focusItemAtDelta(
+ -(
+ this.scrollPageSize ||
+ Math.min(
+ Math.floor(
+ this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
+ ),
+ PAGE_SIZE_MAX_JUMPS
+ )
+ )
+ );
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ // Advance a certain number of elements based on the container height.
+ this.focusItemAtDelta(
+ +(
+ this.scrollPageSize ||
+ Math.min(
+ Math.floor(
+ this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
+ ),
+ PAGE_SIZE_MAX_JUMPS
+ )
+ )
+ );
+ return;
+
+ case KeyCodes.DOM_VK_HOME:
+ this.focusFirstVisibleItem();
+ return;
+
+ case KeyCodes.DOM_VK_END:
+ this.focusLastVisibleItem();
+ return;
+
+ case KeyCodes.DOM_VK_RETURN:
+ // Start editing the value or name of the Variable or Property.
+ if (item instanceof Variable) {
+ if (e.metaKey || e.altKey || e.shiftKey) {
+ item._activateNameInput();
+ } else {
+ item._activateValueInput();
+ }
+ }
+ return;
+
+ case KeyCodes.DOM_VK_DELETE:
+ case KeyCodes.DOM_VK_BACK_SPACE:
+ // Delete the Variable or Property if allowed.
+ if (item instanceof Variable) {
+ item._onDelete(e);
+ }
+ return;
+
+ case KeyCodes.DOM_VK_INSERT:
+ item._onAddProperty(e);
+ }
+ },
+
+ /**
+ * Sets the text displayed in this container when there are no available items.
+ * @param string aValue
+ */
+ set emptyText(aValue) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", aValue);
+ }
+ this._emptyTextValue = aValue;
+ this._appendEmptyNotice();
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _appendEmptyNotice() {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+
+ const label = this.document.createXULElement("label");
+ label.className = "variables-view-empty-notice";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.appendChild(label);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label signaling that this container is empty.
+ */
+ _removeEmptyNotice() {
+ if (!this._emptyTextNode) {
+ return;
+ }
+
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ /**
+ * Gets if all values should be aligned together.
+ * @return boolean
+ */
+ get alignedValues() {
+ return this._alignedValues;
+ },
+
+ /**
+ * Sets if all values should be aligned together.
+ * @param boolean aFlag
+ */
+ set alignedValues(aFlag) {
+ this._alignedValues = aFlag;
+ if (aFlag) {
+ this._parent.setAttribute("aligned-values", "");
+ } else {
+ this._parent.removeAttribute("aligned-values");
+ }
+ },
+
+ /**
+ * Gets if action buttons (like delete) should be placed at the beginning or
+ * end of a line.
+ * @return boolean
+ */
+ get actionsFirst() {
+ return this._actionsFirst;
+ },
+
+ /**
+ * Sets if action buttons (like delete) should be placed at the beginning or
+ * end of a line.
+ * @param boolean aFlag
+ */
+ set actionsFirst(aFlag) {
+ this._actionsFirst = aFlag;
+ if (aFlag) {
+ this._parent.setAttribute("actions-first", "");
+ } else {
+ this._parent.removeAttribute("actions-first");
+ }
+ },
+
+ /**
+ * Gets the parent node holding this view.
+ * @return Node
+ */
+ get parentNode() {
+ return this._parent;
+ },
+
+ /**
+ * Gets the owner document holding this view.
+ * @return HTMLDocument
+ */
+ get document() {
+ return this._document || (this._document = this._parent.ownerDocument);
+ },
+
+ /**
+ * Gets the default window holding this view.
+ * @return nsIDOMWindow
+ */
+ get window() {
+ return this._window || (this._window = this.document.defaultView);
+ },
+
+ _document: null,
+ _window: null,
+
+ _store: null,
+ _itemsByElement: null,
+ _prevHierarchy: null,
+ _currHierarchy: null,
+
+ _enumVisible: true,
+ _nonEnumVisible: true,
+ _alignedValues: false,
+ _actionsFirst: false,
+
+ _parent: null,
+ _list: null,
+ _searchboxNode: null,
+ _searchboxContainer: null,
+ _emptyTextNode: null,
+ _emptyTextValue: "",
+};
+
+VariablesView.NON_SORTABLE_CLASSES = [
+ "Array",
+ "Int8Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float32Array",
+ "Float64Array",
+ "NodeList",
+];
+
+/**
+ * Determine whether an object's properties should be sorted based on its class.
+ *
+ * @param string aClassName
+ * The class of the object.
+ */
+VariablesView.isSortable = function (aClassName) {
+ return !VariablesView.NON_SORTABLE_CLASSES.includes(aClassName);
+};
+
+/**
+ * Generates the string evaluated when performing simple value changes.
+ *
+ * @param Variable | Property aItem
+ * The current variable or property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.simpleValueEvalMacro = function (
+ aItem,
+ aCurrentString,
+ aPrefix = ""
+) {
+ return aPrefix + aItem.symbolicName + "=" + aCurrentString;
+};
+
+/**
+ * Generates the string evaluated when overriding getters and setters with
+ * plain values.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.overrideValueEvalMacro = function (
+ aItem,
+ aCurrentString,
+ aPrefix = ""
+) {
+ const property = escapeString(aItem._nameString);
+ const parent = aPrefix + aItem.ownerView.symbolicName || "this";
+
+ return (
+ "Object.defineProperty(" +
+ parent +
+ "," +
+ property +
+ "," +
+ "{ value: " +
+ aCurrentString +
+ ", enumerable: " +
+ parent +
+ ".propertyIsEnumerable(" +
+ property +
+ ")" +
+ ", configurable: true" +
+ ", writable: true" +
+ "})"
+ );
+};
+
+/**
+ * Generates the string evaluated when performing getters and setters changes.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.getterOrSetterEvalMacro = function (
+ aItem,
+ aCurrentString,
+ aPrefix = ""
+) {
+ const type = aItem._nameString;
+ const propertyObject = aItem.ownerView;
+ const parentObject = propertyObject.ownerView;
+ const property = escapeString(propertyObject._nameString);
+ const parent = aPrefix + parentObject.symbolicName || "this";
+
+ switch (aCurrentString) {
+ case "":
+ case "null":
+ case "undefined":
+ const mirrorType = type == "get" ? "set" : "get";
+ const mirrorLookup =
+ type == "get" ? "__lookupSetter__" : "__lookupGetter__";
+
+ // If the parent object will end up without any getter or setter,
+ // morph it into a plain value.
+ if (
+ (type == "set" && propertyObject.getter.type == "undefined") ||
+ (type == "get" && propertyObject.setter.type == "undefined")
+ ) {
+ // Make sure the right getter/setter to value override macro is applied
+ // to the target object.
+ return propertyObject.evaluationMacro(
+ propertyObject,
+ "undefined",
+ aPrefix
+ );
+ }
+
+ // Construct and return the getter/setter removal evaluation string.
+ // e.g: Object.defineProperty(foo, "bar", {
+ // get: foo.__lookupGetter__("bar"),
+ // set: undefined,
+ // enumerable: true,
+ // configurable: true
+ // })
+ return (
+ "Object.defineProperty(" +
+ parent +
+ "," +
+ property +
+ "," +
+ "{" +
+ mirrorType +
+ ":" +
+ parent +
+ "." +
+ mirrorLookup +
+ "(" +
+ property +
+ ")" +
+ "," +
+ type +
+ ":" +
+ undefined +
+ ", enumerable: " +
+ parent +
+ ".propertyIsEnumerable(" +
+ property +
+ ")" +
+ ", configurable: true" +
+ "})"
+ );
+
+ default:
+ // Wrap statements inside a function declaration if not already wrapped.
+ if (!aCurrentString.startsWith("function")) {
+ const header = "function(" + (type == "set" ? "value" : "") + ")";
+ let body = "";
+ // If there's a return statement explicitly written, always use the
+ // standard function definition syntax
+ if (aCurrentString.includes("return ")) {
+ body = "{" + aCurrentString + "}";
+ } else if (aCurrentString.startsWith("{")) {
+ // If block syntax is used, use the whole string as the function body.
+ body = aCurrentString;
+ } else {
+ // Prefer an expression closure.
+ body = "(" + aCurrentString + ")";
+ }
+ aCurrentString = header + body;
+ }
+
+ // Determine if a new getter or setter should be defined.
+ const defineType =
+ type == "get" ? "__defineGetter__" : "__defineSetter__";
+
+ // Make sure all quotes are escaped in the expression's syntax,
+ const defineFunc =
+ 'eval("(' + aCurrentString.replace(/"/g, "\\$&") + ')")';
+
+ // Construct and return the getter/setter evaluation string.
+ // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
+ return (
+ parent + "." + defineType + "(" + property + "," + defineFunc + ")"
+ );
+ }
+};
+
+/**
+ * Function invoked when a getter or setter is deleted.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ */
+VariablesView.getterOrSetterDeleteCallback = function (aItem) {
+ aItem._disable();
+
+ // Make sure the right getter/setter to value override macro is applied
+ // to the target object.
+ aItem.ownerView.eval(aItem, "");
+
+ return true; // Don't hide the element.
+};
+
+/**
+ * A Scope is an object holding Variable instances.
+ * Iterable via "for (let [name, variable] of instance) { }".
+ *
+ * @param VariablesView aView
+ * The view to contain this scope.
+ * @param string l10nId
+ * The scope localized string id.
+ * @param object aFlags [optional]
+ * Additional options or flags for this scope.
+ */
+function Scope(aView, l10nId, aFlags = {}) {
+ this.ownerView = aView;
+
+ this._onClick = this._onClick.bind(this);
+ this._openEnum = this._openEnum.bind(this);
+ this._openNonEnum = this._openNonEnum.bind(this);
+
+ // Inherit properties and flags from the parent view. You can override
+ // each of these directly onto any scope, variable or property instance.
+ this.scrollPageSize = aView.scrollPageSize;
+ this.eval = aView.eval;
+ this.switch = aView.switch;
+ this.delete = aView.delete;
+ this.new = aView.new;
+ this.preventDisableOnChange = aView.preventDisableOnChange;
+ this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
+ this.editableNameTooltip = aView.editableNameTooltip;
+ this.editableValueTooltip = aView.editableValueTooltip;
+ this.editButtonTooltip = aView.editButtonTooltip;
+ this.deleteButtonTooltip = aView.deleteButtonTooltip;
+ this.domNodeValueTooltip = aView.domNodeValueTooltip;
+ this.contextMenuId = aView.contextMenuId;
+ this.separatorStr = aView.separatorStr;
+
+ this._init(l10nId, aFlags);
+}
+
+Scope.prototype = {
+ /**
+ * Whether this Scope should be prefetched when it is remoted.
+ */
+ shouldPrefetch: true,
+
+ /**
+ * Whether this Scope should paginate its contents.
+ */
+ allowPaginate: false,
+
+ /**
+ * The class name applied to this scope's target element.
+ */
+ targetClassName: "variables-view-scope",
+
+ /**
+ * Create a new Variable that is a child of this Scope.
+ *
+ * @param string aName
+ * The name of the new Property.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by addItem.
+ * @return Variable
+ * The newly created child Variable.
+ */
+ _createChild(aName, aDescriptor, aOptions) {
+ return new Variable(this, aName, aDescriptor, aOptions);
+ },
+
+ /**
+ * Adds a child to contain any inspected properties.
+ *
+ * @param string aName
+ * The child's name.
+ * @param object aDescriptor
+ * Specifies the value and/or type & class of the child,
+ * or 'get' & 'set' accessor properties. If the type is implicit,
+ * it will be inferred from the value. If this parameter is omitted,
+ * a property without a value will be added (useful for branch nodes).
+ * e.g. - { value: 42 }
+ * - { value: true }
+ * - { value: "nasu" }
+ * - { value: { type: "undefined" } }
+ * - { value: { type: "null" } }
+ * - { value: { type: "object", class: "Object" } }
+ * - { get: { type: "object", class: "Function" },
+ * set: { type: "undefined" } }
+ * @param object aOptions
+ * Specifies some options affecting the new variable.
+ * Recognized properties are
+ * * boolean relaxed true if name duplicates should be allowed.
+ * You probably shouldn't do it. Use this
+ * with caution.
+ * * boolean internalItem true if the item is internally generated.
+ * This is used for special variables
+ * like <return> or <exception> and distinguishes
+ * them from ordinary properties that happen
+ * to have the same name
+ * @return Variable
+ * The newly created Variable instance, null if it already exists.
+ */
+ addItem(aName, aDescriptor = {}, aOptions = {}) {
+ const { relaxed } = aOptions;
+ if (this._store.has(aName) && !relaxed) {
+ return this._store.get(aName);
+ }
+
+ const child = this._createChild(aName, aDescriptor, aOptions);
+ this._store.set(aName, child);
+ this._variablesView._itemsByElement.set(child._target, child);
+ this._variablesView._currHierarchy.set(child.absoluteName, child);
+ child.header = aName !== undefined;
+
+ return child;
+ },
+
+ /**
+ * Adds items for this variable.
+ *
+ * @param object aItems
+ * An object containing some { name: descriptor } data properties,
+ * specifying the value and/or type & class of the variable,
+ * or 'get' & 'set' accessor properties. If the type is implicit,
+ * it will be inferred from the value.
+ * e.g. - { someProp0: { value: 42 },
+ * someProp1: { value: true },
+ * someProp2: { value: "nasu" },
+ * someProp3: { value: { type: "undefined" } },
+ * someProp4: { value: { type: "null" } },
+ * someProp5: { value: { type: "object", class: "Object" } },
+ * someProp6: { get: { type: "object", class: "Function" },
+ * set: { type: "undefined" } } }
+ * @param object aOptions [optional]
+ * Additional options for adding the properties. Supported options:
+ * - sorted: true to sort all the properties before adding them
+ * - callback: function invoked after each item is added
+ */
+ addItems(aItems, aOptions = {}) {
+ const names = Object.keys(aItems);
+
+ // Sort all of the properties before adding them, if preferred.
+ if (aOptions.sorted) {
+ names.sort(this._naturalSort);
+ }
+
+ // Add the properties to the current scope.
+ for (const name of names) {
+ const descriptor = aItems[name];
+ const item = this.addItem(name, descriptor);
+
+ if (aOptions.callback) {
+ aOptions.callback(item, descriptor && descriptor.value);
+ }
+ }
+ },
+
+ /**
+ * Remove this Scope from its parent and remove all children recursively.
+ */
+ remove() {
+ const view = this._variablesView;
+ view._store.splice(view._store.indexOf(this), 1);
+ view._itemsByElement.delete(this._target);
+ view._currHierarchy.delete(this._nameString);
+
+ this._target.remove();
+
+ for (const variable of this._store.values()) {
+ variable.remove();
+ }
+ },
+
+ /**
+ * Gets the variable in this container having the specified name.
+ *
+ * @param string aName
+ * The name of the variable to get.
+ * @return Variable
+ * The matched variable, or null if nothing is found.
+ */
+ get(aName) {
+ return this._store.get(aName);
+ },
+
+ /**
+ * Recursively searches for the variable or property in this container
+ * displayed by the specified node.
+ *
+ * @param Node aNode
+ * The node to search for.
+ * @return Variable | Property
+ * The matched variable or property, or null if nothing is found.
+ */
+ find(aNode) {
+ for (const [, variable] of this._store) {
+ let match;
+ if (variable._target == aNode) {
+ match = variable;
+ } else {
+ match = variable.find(aNode);
+ }
+ if (match) {
+ return match;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Determines if this scope is a direct child of a parent variables view,
+ * scope, variable or property.
+ *
+ * @param VariablesView | Scope | Variable | Property
+ * The parent to check.
+ * @return boolean
+ * True if the specified item is a direct child, false otherwise.
+ */
+ isChildOf(aParent) {
+ return this.ownerView == aParent;
+ },
+
+ /**
+ * Determines if this scope is a descendant of a parent variables view,
+ * scope, variable or property.
+ *
+ * @param VariablesView | Scope | Variable | Property
+ * The parent to check.
+ * @return boolean
+ * True if the specified item is a descendant, false otherwise.
+ */
+ isDescendantOf(aParent) {
+ if (this.isChildOf(aParent)) {
+ return true;
+ }
+
+ // Recurse to parent if it is a Scope, Variable, or Property.
+ if (this.ownerView instanceof Scope) {
+ return this.ownerView.isDescendantOf(aParent);
+ }
+
+ return false;
+ },
+
+ /**
+ * Shows the scope.
+ */
+ show() {
+ this._target.hidden = false;
+ this._isContentVisible = true;
+
+ if (this.onshow) {
+ this.onshow(this);
+ }
+ },
+
+ /**
+ * Hides the scope.
+ */
+ hide() {
+ this._target.hidden = true;
+ this._isContentVisible = false;
+
+ if (this.onhide) {
+ this.onhide(this);
+ }
+ },
+
+ /**
+ * Expands the scope, showing all the added details.
+ */
+ async expand() {
+ if (this._isExpanded || this._isLocked) {
+ return;
+ }
+ if (this._variablesView._enumVisible) {
+ this._openEnum();
+ }
+ if (this._variablesView._nonEnumVisible) {
+ Services.tm.dispatchToMainThread({ run: this._openNonEnum });
+ }
+ this._isExpanded = true;
+
+ if (this.onexpand) {
+ // We return onexpand as it sometimes returns a promise
+ // (up to the user of VariableView to do it)
+ // that can indicate when the view is done expanding
+ // and attributes are available. (Mostly used for tests)
+ await this.onexpand(this);
+ }
+ },
+
+ /**
+ * Collapses the scope, hiding all the added details.
+ */
+ collapse() {
+ if (!this._isExpanded || this._isLocked) {
+ return;
+ }
+ this._arrow.removeAttribute("open");
+ this._enum.removeAttribute("open");
+ this._nonenum.removeAttribute("open");
+ this._isExpanded = false;
+
+ if (this.oncollapse) {
+ this.oncollapse(this);
+ }
+ },
+
+ /**
+ * Toggles between the scope's collapsed and expanded state.
+ */
+ toggle(e) {
+ if (e && e.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+ this.expanded ^= 1;
+
+ // Make sure the scope and its contents are visibile.
+ for (const [, variable] of this._store) {
+ variable.header = true;
+ variable._matched = true;
+ }
+ if (this.ontoggle) {
+ this.ontoggle(this);
+ }
+ },
+
+ /**
+ * Shows the scope's title header.
+ */
+ showHeader() {
+ if (this._isHeaderVisible || !this._nameString) {
+ return;
+ }
+ this._target.removeAttribute("untitled");
+ this._isHeaderVisible = true;
+ },
+
+ /**
+ * Hides the scope's title header.
+ * This action will automatically expand the scope.
+ */
+ hideHeader() {
+ if (!this._isHeaderVisible) {
+ return;
+ }
+ this.expand();
+ this._target.setAttribute("untitled", "");
+ this._isHeaderVisible = false;
+ },
+
+ /**
+ * Sort in ascending order
+ * This only needs to compare non-numbers since it is dealing with an array
+ * which numeric-based indices are placed in order.
+ *
+ * @param string a
+ * @param string b
+ * @return number
+ * -1 if a is less than b, 0 if no change in order, +1 if a is greater than 0
+ */
+ _naturalSort(a, b) {
+ if (isNaN(parseFloat(a)) && isNaN(parseFloat(b))) {
+ return a < b ? -1 : 1;
+ }
+ return 0;
+ },
+
+ /**
+ * Shows the scope's expand/collapse arrow.
+ */
+ showArrow() {
+ if (this._isArrowVisible) {
+ return;
+ }
+ this._arrow.removeAttribute("invisible");
+ this._isArrowVisible = true;
+ },
+
+ /**
+ * Hides the scope's expand/collapse arrow.
+ */
+ hideArrow() {
+ if (!this._isArrowVisible) {
+ return;
+ }
+ this._arrow.setAttribute("invisible", "");
+ this._isArrowVisible = false;
+ },
+
+ /**
+ * Gets the visibility state.
+ * @return boolean
+ */
+ get visible() {
+ return this._isContentVisible;
+ },
+
+ /**
+ * Gets the expanded state.
+ * @return boolean
+ */
+ get expanded() {
+ return this._isExpanded;
+ },
+
+ /**
+ * Gets the header visibility state.
+ * @return boolean
+ */
+ get header() {
+ return this._isHeaderVisible;
+ },
+
+ /**
+ * Gets the twisty visibility state.
+ * @return boolean
+ */
+ get twisty() {
+ return this._isArrowVisible;
+ },
+
+ /**
+ * Gets the expand lock state.
+ * @return boolean
+ */
+ get locked() {
+ return this._isLocked;
+ },
+
+ /**
+ * Sets the visibility state.
+ * @param boolean aFlag
+ */
+ set visible(aFlag) {
+ aFlag ? this.show() : this.hide();
+ },
+
+ /**
+ * Sets the expanded state.
+ * @param boolean aFlag
+ */
+ set expanded(aFlag) {
+ aFlag ? this.expand() : this.collapse();
+ },
+
+ /**
+ * Sets the header visibility state.
+ * @param boolean aFlag
+ */
+ set header(aFlag) {
+ aFlag ? this.showHeader() : this.hideHeader();
+ },
+
+ /**
+ * Sets the twisty visibility state.
+ * @param boolean aFlag
+ */
+ set twisty(aFlag) {
+ aFlag ? this.showArrow() : this.hideArrow();
+ },
+
+ /**
+ * Sets the expand lock state.
+ * @param boolean aFlag
+ */
+ set locked(aFlag) {
+ this._isLocked = aFlag;
+ },
+
+ /**
+ * Specifies if this target node may be focused.
+ * @return boolean
+ */
+ get focusable() {
+ // Check if this target node is actually visibile.
+ if (
+ !this._nameString ||
+ !this._isContentVisible ||
+ !this._isHeaderVisible ||
+ !this._isMatch
+ ) {
+ return false;
+ }
+ // Check if all parent objects are expanded.
+ let item = this;
+
+ // Recurse while parent is a Scope, Variable, or Property
+ while ((item = item.ownerView) && item instanceof Scope) {
+ if (!item._isExpanded) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Focus this scope.
+ */
+ focus() {
+ this._variablesView._focusItem(this);
+ },
+
+ /**
+ * Adds an event listener for a certain event on this scope's title.
+ * @param string aName
+ * @param function aCallback
+ * @param boolean aCapture
+ */
+ addEventListener(aName, aCallback, aCapture) {
+ this._title.addEventListener(aName, aCallback, aCapture);
+ },
+
+ /**
+ * Removes an event listener for a certain event on this scope's title.
+ * @param string aName
+ * @param function aCallback
+ * @param boolean aCapture
+ */
+ removeEventListener(aName, aCallback, aCapture) {
+ this._title.removeEventListener(aName, aCallback, aCapture);
+ },
+
+ /**
+ * Gets the id associated with this item.
+ * @return string
+ */
+ get id() {
+ return this._idString;
+ },
+
+ /**
+ * Gets the name associated with this item.
+ * @return string
+ */
+ get name() {
+ return this._nameString;
+ },
+
+ /**
+ * Gets the displayed value for this item.
+ * @return string
+ */
+ get displayValue() {
+ return this._valueString;
+ },
+
+ /**
+ * Gets the class names used for the displayed value.
+ * @return string
+ */
+ get displayValueClassName() {
+ return this._valueClassName;
+ },
+
+ /**
+ * Gets the element associated with this item.
+ * @return Node
+ */
+ get target() {
+ return this._target;
+ },
+
+ /**
+ * Initializes this scope's id, view and binds event listeners.
+ *
+ * @param string l10nId
+ * The scope localized string id.
+ * @param object aFlags [optional]
+ * Additional options or flags for this scope.
+ */
+ _init(l10nId, aFlags) {
+ this._idString = generateId((this._nameString = l10nId));
+ this._displayScope({
+ l10nId,
+ targetClassName: `${this.targetClassName} ${aFlags.customClass}`,
+ titleClassName: "devtools-toolbar",
+ });
+ this._addEventListeners();
+ this.parentNode.appendChild(this._target);
+ },
+
+ /**
+ * Creates the necessary nodes for this scope.
+ *
+ * @param Object options
+ * @param string options.l10nId [optional]
+ * The scope localized string id.
+ * @param string options.value [optional]
+ * The scope's name. Either this or l10nId need to be passed
+ * @param string options.targetClassName
+ * A custom class name for this scope's target element.
+ * @param string options.titleClassName [optional]
+ * A custom class name for this scope's title element.
+ */
+ _displayScope({ l10nId, value, targetClassName, titleClassName = "" }) {
+ const document = this.document;
+
+ const element = (this._target = document.createXULElement("vbox"));
+ element.id = this._idString;
+ element.className = targetClassName;
+
+ const arrow = (this._arrow = document.createXULElement("hbox"));
+ arrow.className = "arrow theme-twisty";
+
+ const name = (this._name = document.createXULElement("label"));
+ name.className = "plain name";
+ if (l10nId) {
+ name.setAttribute("data-l10n-id", l10nId);
+ } else {
+ name.setAttribute("value", value);
+ }
+ name.setAttribute("crop", "end");
+
+ const title = (this._title = document.createXULElement("hbox"));
+ title.className = "title " + titleClassName;
+ title.setAttribute("align", "center");
+
+ const enumerable = (this._enum = document.createXULElement("vbox"));
+ const nonenum = (this._nonenum = document.createXULElement("vbox"));
+ enumerable.className = "variables-view-element-details enum";
+ nonenum.className = "variables-view-element-details nonenum";
+
+ title.appendChild(arrow);
+ title.appendChild(name);
+
+ element.appendChild(title);
+ element.appendChild(enumerable);
+ element.appendChild(nonenum);
+ },
+
+ /**
+ * Adds the necessary event listeners for this scope.
+ */
+ _addEventListeners() {
+ this._title.addEventListener("mousedown", this._onClick);
+ },
+
+ /**
+ * The click listener for this scope's title.
+ */
+ _onClick(e) {
+ if (
+ this.editing ||
+ e.button != 0 ||
+ e.target == this._editNode ||
+ e.target == this._deleteNode ||
+ e.target == this._addPropertyNode
+ ) {
+ return;
+ }
+ this.toggle();
+ this.focus();
+ },
+
+ /**
+ * Opens the enumerable items container.
+ */
+ _openEnum() {
+ this._arrow.setAttribute("open", "");
+ this._enum.setAttribute("open", "");
+ },
+
+ /**
+ * Opens the non-enumerable items container.
+ */
+ _openNonEnum() {
+ this._nonenum.setAttribute("open", "");
+ },
+
+ /**
+ * Specifies if enumerable properties and variables should be displayed.
+ * @param boolean aFlag
+ */
+ set _enumVisible(aFlag) {
+ for (const [, variable] of this._store) {
+ variable._enumVisible = aFlag;
+
+ if (!this._isExpanded) {
+ continue;
+ }
+ if (aFlag) {
+ this._enum.setAttribute("open", "");
+ } else {
+ this._enum.removeAttribute("open");
+ }
+ }
+ },
+
+ /**
+ * Specifies if non-enumerable properties and variables should be displayed.
+ * @param boolean aFlag
+ */
+ set _nonEnumVisible(aFlag) {
+ for (const [, variable] of this._store) {
+ variable._nonEnumVisible = aFlag;
+
+ if (!this._isExpanded) {
+ continue;
+ }
+ if (aFlag) {
+ this._nonenum.setAttribute("open", "");
+ } else {
+ this._nonenum.removeAttribute("open");
+ }
+ }
+ },
+
+ /**
+ * Performs a case insensitive search for variables or properties matching
+ * the query, and hides non-matched items.
+ *
+ * @param string aLowerCaseQuery
+ * The lowercased name of the variable or property to search for.
+ */
+ _performSearch(aLowerCaseQuery) {
+ for (let [, variable] of this._store) {
+ const currentObject = variable;
+ const lowerCaseName = variable._nameString.toLowerCase();
+ const lowerCaseValue = variable._valueString.toLowerCase();
+
+ // Non-matched variables or properties require a corresponding attribute.
+ if (
+ !lowerCaseName.includes(aLowerCaseQuery) &&
+ !lowerCaseValue.includes(aLowerCaseQuery)
+ ) {
+ variable._matched = false;
+ } else {
+ // Variable or property is matched.
+ variable._matched = true;
+
+ // If the variable was ever expanded, there's a possibility it may
+ // contain some matched properties, so make sure they're visible
+ // ("expand downwards").
+ if (variable._store.size) {
+ variable.expand();
+ }
+
+ // If the variable is contained in another Scope, Variable, or Property,
+ // the parent may not be a match, thus hidden. It should be visible
+ // ("expand upwards").
+ while ((variable = variable.ownerView) && variable instanceof Scope) {
+ variable._matched = true;
+ variable.expand();
+ }
+ }
+
+ // Proceed with the search recursively inside this variable or property.
+ if (
+ currentObject._store.size ||
+ currentObject.getter ||
+ currentObject.setter
+ ) {
+ currentObject._performSearch(aLowerCaseQuery);
+ }
+ }
+ },
+
+ /**
+ * Sets if this object instance is a matched or non-matched item.
+ * @param boolean aStatus
+ */
+ set _matched(aStatus) {
+ if (this._isMatch == aStatus) {
+ return;
+ }
+ if (aStatus) {
+ this._isMatch = true;
+ this.target.removeAttribute("unmatched");
+ } else {
+ this._isMatch = false;
+ this.target.setAttribute("unmatched", "");
+ }
+ },
+
+ /**
+ * Find the first item in the tree of visible items in this item that matches
+ * the predicate. Searches in visual order (the order seen by the user).
+ * Tests itself, then descends into first the enumerable children and then
+ * the non-enumerable children (since they are presented in separate groups).
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The first visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItems(aPredicate) {
+ if (aPredicate(this)) {
+ return this;
+ }
+
+ if (this._isExpanded) {
+ if (this._variablesView._enumVisible) {
+ for (const item of this._enumItems) {
+ const result = item._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ if (this._variablesView._nonEnumVisible) {
+ for (const item of this._nonEnumItems) {
+ const result = item._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Find the last item in the tree of visible items in this item that matches
+ * the predicate. Searches in reverse visual order (opposite of the order
+ * seen by the user). Descends into first the non-enumerable children, then
+ * the enumerable children (since they are presented in separate groups), and
+ * finally tests itself.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The last visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItemsReverse(aPredicate) {
+ if (this._isExpanded) {
+ if (this._variablesView._nonEnumVisible) {
+ for (let i = this._nonEnumItems.length - 1; i >= 0; i--) {
+ const item = this._nonEnumItems[i];
+ const result = item._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ if (this._variablesView._enumVisible) {
+ for (let i = this._enumItems.length - 1; i >= 0; i--) {
+ const item = this._enumItems[i];
+ const result = item._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ }
+
+ if (aPredicate(this)) {
+ return this;
+ }
+
+ return null;
+ },
+
+ /**
+ * Gets top level variables view instance.
+ * @return VariablesView
+ */
+ get _variablesView() {
+ return (
+ this._topView ||
+ (this._topView = (() => {
+ let parentView = this.ownerView;
+ let topView;
+
+ while ((topView = parentView.ownerView)) {
+ parentView = topView;
+ }
+ return parentView;
+ })())
+ );
+ },
+
+ /**
+ * Gets the parent node holding this scope.
+ * @return Node
+ */
+ get parentNode() {
+ return this.ownerView._list;
+ },
+
+ /**
+ * Gets the owner document holding this scope.
+ * @return HTMLDocument
+ */
+ get document() {
+ return this._document || (this._document = this.ownerView.document);
+ },
+
+ /**
+ * Gets the default window holding this scope.
+ * @return nsIDOMWindow
+ */
+ get window() {
+ return this._window || (this._window = this.ownerView.window);
+ },
+
+ _topView: null,
+ _document: null,
+ _window: null,
+
+ ownerView: null,
+ eval: null,
+ switch: null,
+ delete: null,
+ new: null,
+ preventDisableOnChange: false,
+ preventDescriptorModifiers: false,
+ editing: false,
+ editableNameTooltip: "",
+ editableValueTooltip: "",
+ editButtonTooltip: "",
+ deleteButtonTooltip: "",
+ domNodeValueTooltip: "",
+ contextMenuId: "",
+ separatorStr: "",
+
+ _store: null,
+ _enumItems: null,
+ _nonEnumItems: null,
+ _fetched: false,
+ _committed: false,
+ _isLocked: false,
+ _isExpanded: false,
+ _isContentVisible: true,
+ _isHeaderVisible: true,
+ _isArrowVisible: true,
+ _isMatch: true,
+ _idString: "",
+ _nameString: "",
+ _target: null,
+ _arrow: null,
+ _name: null,
+ _title: null,
+ _enum: null,
+ _nonenum: null,
+};
+
+// Creating maps and arrays thousands of times for variables or properties
+// with a large number of children fills up a lot of memory. Make sure
+// these are instantiated only if needed.
+DevToolsUtils.defineLazyPrototypeGetter(
+ Scope.prototype,
+ "_store",
+ () => new Map()
+);
+DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array);
+DevToolsUtils.defineLazyPrototypeGetter(
+ Scope.prototype,
+ "_nonEnumItems",
+ Array
+);
+
+/**
+ * A Variable is a Scope holding Property instances.
+ * Iterable via "for (let [name, property] of instance) { }".
+ *
+ * @param Scope aScope
+ * The scope to contain this variable.
+ * @param string aName
+ * The variable's name.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by Scope.addItem
+ */
+function Variable(aScope, aName, aDescriptor, aOptions) {
+ this._setTooltips = this._setTooltips.bind(this);
+ this._activateNameInput = this._activateNameInput.bind(this);
+ this._activateValueInput = this._activateValueInput.bind(this);
+ this.openNodeInInspector = this.openNodeInInspector.bind(this);
+ this.highlightDomNode = this.highlightDomNode.bind(this);
+ this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
+ this._internalItem = aOptions.internalItem;
+
+ // Treat safe getter descriptors as descriptors with a value.
+ if ("getterValue" in aDescriptor) {
+ aDescriptor.value = aDescriptor.getterValue;
+ delete aDescriptor.get;
+ delete aDescriptor.set;
+ }
+
+ Scope.call(this, aScope, aName, (this._initialDescriptor = aDescriptor));
+ this.setGrip(aDescriptor.value);
+}
+
+Variable.prototype = extend(Scope.prototype, {
+ /**
+ * Whether this Variable should be prefetched when it is remoted.
+ */
+ get shouldPrefetch() {
+ return this.name == "window" || this.name == "this";
+ },
+
+ /**
+ * Whether this Variable should paginate its contents.
+ */
+ get allowPaginate() {
+ return this.name != "window" && this.name != "this";
+ },
+
+ /**
+ * The class name applied to this variable's target element.
+ */
+ targetClassName: "variables-view-variable variable-or-property",
+
+ /**
+ * Create a new Property that is a child of Variable.
+ *
+ * @param string aName
+ * The name of the new Property.
+ * @param object aDescriptor
+ * The property's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by Scope.addItem
+ * @return Property
+ * The newly created child Property.
+ */
+ _createChild(aName, aDescriptor, aOptions) {
+ return new Property(this, aName, aDescriptor, aOptions);
+ },
+
+ /**
+ * Remove this Variable from its parent and remove all children recursively.
+ */
+ remove() {
+ if (this._linkedToInspector) {
+ this.unhighlightDomNode();
+ this._valueLabel.removeEventListener("mouseover", this.highlightDomNode);
+ this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode);
+ this._openInspectorNode.removeEventListener(
+ "mousedown",
+ this.openNodeInInspector
+ );
+ }
+
+ this.ownerView._store.delete(this._nameString);
+ this._variablesView._itemsByElement.delete(this._target);
+ this._variablesView._currHierarchy.delete(this.absoluteName);
+
+ this._target.remove();
+
+ for (const property of this._store.values()) {
+ property.remove();
+ }
+ },
+
+ /**
+ * Populates this variable to contain all the properties of an object.
+ *
+ * @param object aObject
+ * The raw object you want to display.
+ * @param object aOptions [optional]
+ * Additional options for adding the properties. Supported options:
+ * - sorted: true to sort all the properties before adding them
+ * - expanded: true to expand all the properties after adding them
+ */
+ populate(aObject, aOptions = {}) {
+ // Retrieve the properties only once.
+ if (this._fetched) {
+ return;
+ }
+ this._fetched = true;
+
+ const propertyNames = Object.getOwnPropertyNames(aObject);
+ const prototype = Object.getPrototypeOf(aObject);
+
+ // Sort all of the properties before adding them, if preferred.
+ if (aOptions.sorted) {
+ propertyNames.sort(this._naturalSort);
+ }
+
+ // Add all the variable properties.
+ for (const name of propertyNames) {
+ const descriptor = Object.getOwnPropertyDescriptor(aObject, name);
+ if (descriptor.get || descriptor.set) {
+ const prop = this._addRawNonValueProperty(name, descriptor);
+ if (aOptions.expanded) {
+ prop.expanded = true;
+ }
+ } else {
+ const prop = this._addRawValueProperty(name, descriptor, aObject[name]);
+ if (aOptions.expanded) {
+ prop.expanded = true;
+ }
+ }
+ }
+ // Add the variable's __proto__.
+ if (prototype) {
+ this._addRawValueProperty("__proto__", {}, prototype);
+ }
+ },
+
+ /**
+ * Populates a specific variable or property instance to contain all the
+ * properties of an object
+ *
+ * @param Variable | Property aVar
+ * The target variable to populate.
+ * @param object aObject [optional]
+ * The raw object you want to display. If unspecified, the object is
+ * assumed to be defined in a _sourceValue property on the target.
+ */
+ _populateTarget(aVar, aObject = aVar._sourceValue) {
+ aVar.populate(aObject);
+ },
+
+ /**
+ * Adds a property for this variable based on a raw value descriptor.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * Specifies the exact property descriptor as returned by a call to
+ * Object.getOwnPropertyDescriptor.
+ * @param object aValue
+ * The raw property value you want to display.
+ * @return Property
+ * The newly added property instance.
+ */
+ _addRawValueProperty(aName, aDescriptor, aValue) {
+ const descriptor = Object.create(aDescriptor);
+ descriptor.value = VariablesView.getGrip(aValue);
+
+ const propertyItem = this.addItem(aName, descriptor);
+ propertyItem._sourceValue = aValue;
+
+ // Add an 'onexpand' callback for the property, lazily handling
+ // the addition of new child properties.
+ if (!VariablesView.isPrimitive(descriptor)) {
+ propertyItem.onexpand = this._populateTarget;
+ }
+ return propertyItem;
+ },
+
+ /**
+ * Adds a property for this variable based on a getter/setter descriptor.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * Specifies the exact property descriptor as returned by a call to
+ * Object.getOwnPropertyDescriptor.
+ * @return Property
+ * The newly added property instance.
+ */
+ _addRawNonValueProperty(aName, aDescriptor) {
+ const descriptor = Object.create(aDescriptor);
+ descriptor.get = VariablesView.getGrip(aDescriptor.get);
+ descriptor.set = VariablesView.getGrip(aDescriptor.set);
+
+ return this.addItem(aName, descriptor);
+ },
+
+ /**
+ * Gets this variable's path to the topmost scope in the form of a string
+ * meant for use via eval() or a similar approach.
+ * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
+ * @return string
+ */
+ get symbolicName() {
+ return this._nameString || "";
+ },
+
+ /**
+ * Gets full path to this variable, including name of the scope.
+ * @return string
+ */
+ get absoluteName() {
+ if (this._absoluteName) {
+ return this._absoluteName;
+ }
+
+ this._absoluteName =
+ this.ownerView._nameString + "[" + escapeString(this._nameString) + "]";
+ return this._absoluteName;
+ },
+
+ /**
+ * Gets this variable's symbolic path to the topmost scope.
+ * @return array
+ * @see Variable._buildSymbolicPath
+ */
+ get symbolicPath() {
+ if (this._symbolicPath) {
+ return this._symbolicPath;
+ }
+ this._symbolicPath = this._buildSymbolicPath();
+ return this._symbolicPath;
+ },
+
+ /**
+ * Build this variable's path to the topmost scope in form of an array of
+ * strings, one for each segment of the path.
+ * For example, a symbolic path may look like ["0", "foo", "bar"].
+ * @return array
+ */
+ _buildSymbolicPath(path = []) {
+ if (this.name) {
+ path.unshift(this.name);
+ if (this.ownerView instanceof Variable) {
+ return this.ownerView._buildSymbolicPath(path);
+ }
+ }
+ return path;
+ },
+
+ /**
+ * Returns this variable's value from the descriptor if available.
+ * @return any
+ */
+ get value() {
+ return this._initialDescriptor.value;
+ },
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get getter() {
+ return this._initialDescriptor.get;
+ },
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get setter() {
+ return this._initialDescriptor.set;
+ },
+
+ /**
+ * Sets the specific grip for this variable (applies the text content and
+ * class name to the value label).
+ *
+ * The grip should contain the value or the type & class, as defined in the
+ * remote debugger protocol. For convenience, undefined and null are
+ * both considered types.
+ *
+ * @param any aGrip
+ * Specifies the value and/or type & class of the variable.
+ * e.g. - 42
+ * - true
+ * - "nasu"
+ * - { type: "undefined" }
+ * - { type: "null" }
+ * - { type: "object", class: "Object" }
+ */
+ setGrip(aGrip) {
+ // Don't allow displaying grip information if there's no name available
+ // or the grip is malformed.
+ if (
+ this._nameString === undefined ||
+ aGrip === undefined ||
+ aGrip === null
+ ) {
+ return;
+ }
+ // Getters and setters should display grip information in sub-properties.
+ if (this.getter || this.setter) {
+ return;
+ }
+
+ const prevGrip = this._valueGrip;
+ if (prevGrip) {
+ this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
+ }
+ this._valueGrip = aGrip;
+
+ if (
+ aGrip &&
+ (aGrip.optimizedOut || aGrip.uninitialized || aGrip.missingArguments)
+ ) {
+ if (aGrip.optimizedOut) {
+ this._valueString = L10N.getStr("variablesViewOptimizedOut");
+ } else if (aGrip.uninitialized) {
+ this._valueString = L10N.getStr("variablesViewUninitialized");
+ } else if (aGrip.missingArguments) {
+ this._valueString = L10N.getStr("variablesViewMissingArgs");
+ }
+ this.eval = null;
+ } else {
+ this._valueString = VariablesView.getString(aGrip, {
+ concise: true,
+ noEllipsis: true,
+ });
+ this.eval = this.ownerView.eval;
+ }
+
+ this._valueClassName = VariablesView.getClass(aGrip);
+
+ this._valueLabel.classList.add(this._valueClassName);
+ this._valueLabel.setAttribute("value", this._valueString);
+ this._separatorLabel.hidden = false;
+
+ // DOMNodes get special treatment since they can be linked to the inspector
+ if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") {
+ this._linkToInspector();
+ }
+ },
+
+ /**
+ * Marks this variable as overridden.
+ *
+ * @param boolean aFlag
+ * Whether this variable is overridden or not.
+ */
+ setOverridden(aFlag) {
+ if (aFlag) {
+ this._target.setAttribute("overridden", "");
+ } else {
+ this._target.removeAttribute("overridden");
+ }
+ },
+
+ /**
+ * Briefly flashes this variable.
+ *
+ * @param number aDuration [optional]
+ * An optional flash animation duration.
+ */
+ flash(aDuration = ITEM_FLASH_DURATION) {
+ const fadeInDelay = this._variablesView.lazyEmptyDelay + 1;
+ const fadeOutDelay = fadeInDelay + aDuration;
+
+ setNamedTimeout("vview-flash-in" + this.absoluteName, fadeInDelay, () =>
+ this._target.setAttribute("changed", "")
+ );
+
+ setNamedTimeout("vview-flash-out" + this.absoluteName, fadeOutDelay, () =>
+ this._target.removeAttribute("changed")
+ );
+ },
+
+ /**
+ * Initializes this variable's id, view and binds event listeners.
+ *
+ * @param string aName
+ * The variable's name.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+ _init(aName, aDescriptor) {
+ this._idString = generateId((this._nameString = aName));
+ this._displayScope({ value: aName, targetClassName: this.targetClassName });
+ this._displayVariable();
+ this._customizeVariable();
+ this._prepareTooltips();
+ this._setAttributes();
+ this._addEventListeners();
+
+ if (
+ this._initialDescriptor.enumerable ||
+ this._nameString == "this" ||
+ this._internalItem
+ ) {
+ this.ownerView._enum.appendChild(this._target);
+ this.ownerView._enumItems.push(this);
+ } else {
+ this.ownerView._nonenum.appendChild(this._target);
+ this.ownerView._nonEnumItems.push(this);
+ }
+ },
+
+ /**
+ * Creates the necessary nodes for this variable.
+ */
+ _displayVariable() {
+ const document = this.document;
+ const descriptor = this._initialDescriptor;
+
+ const separatorLabel = (this._separatorLabel =
+ document.createXULElement("label"));
+ separatorLabel.className = "plain separator";
+ separatorLabel.setAttribute("value", this.separatorStr + " ");
+
+ const valueLabel = (this._valueLabel = document.createXULElement("label"));
+ valueLabel.className = "plain value";
+ valueLabel.setAttribute("flex", "1");
+ valueLabel.setAttribute("crop", "center");
+
+ this._title.appendChild(separatorLabel);
+ this._title.appendChild(valueLabel);
+
+ if (VariablesView.isPrimitive(descriptor)) {
+ this.hideArrow();
+ }
+
+ // If no value will be displayed, we don't need the separator.
+ if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
+ separatorLabel.hidden = true;
+ }
+
+ // If this is a getter/setter property, create two child pseudo-properties
+ // called "get" and "set" that display the corresponding functions.
+ if (descriptor.get || descriptor.set) {
+ separatorLabel.hidden = true;
+ valueLabel.hidden = true;
+
+ // Changing getter/setter names is never allowed.
+ this.switch = null;
+
+ // Getter/setter properties require special handling when it comes to
+ // evaluation and deletion.
+ if (this.ownerView.eval) {
+ this.delete = VariablesView.getterOrSetterDeleteCallback;
+ this.evaluationMacro = VariablesView.overrideValueEvalMacro;
+ } else {
+ // Deleting getters and setters individually is not allowed if no
+ // evaluation method is provided.
+ this.delete = null;
+ this.evaluationMacro = null;
+ }
+
+ const getter = this.addItem("get", { value: descriptor.get });
+ const setter = this.addItem("set", { value: descriptor.set });
+ getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
+ setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
+
+ getter.hideArrow();
+ setter.hideArrow();
+ this.expand();
+ }
+ },
+
+ /**
+ * Adds specific nodes for this variable based on custom flags.
+ */
+ _customizeVariable() {
+ const ownerView = this.ownerView;
+ const descriptor = this._initialDescriptor;
+
+ if ((ownerView.eval && this.getter) || this.setter) {
+ const editNode = (this._editNode =
+ this.document.createXULElement("toolbarbutton"));
+ editNode.className = "plain variables-view-edit";
+ editNode.addEventListener("mousedown", this._onEdit.bind(this));
+ this._title.insertBefore(editNode, this._spacer);
+ }
+
+ if (ownerView.delete) {
+ const deleteNode = (this._deleteNode =
+ this.document.createXULElement("toolbarbutton"));
+ deleteNode.className = "plain variables-view-delete";
+ deleteNode.addEventListener("click", this._onDelete.bind(this));
+ this._title.appendChild(deleteNode);
+ }
+
+ if (ownerView.new) {
+ const addPropertyNode = (this._addPropertyNode =
+ this.document.createXULElement("toolbarbutton"));
+ addPropertyNode.className = "plain variables-view-add-property";
+ addPropertyNode.addEventListener(
+ "mousedown",
+ this._onAddProperty.bind(this)
+ );
+ this._title.appendChild(addPropertyNode);
+
+ // Can't add properties to primitive values, hide the node in those cases.
+ if (VariablesView.isPrimitive(descriptor)) {
+ addPropertyNode.setAttribute("invisible", "");
+ }
+ }
+
+ if (ownerView.contextMenuId) {
+ this._title.setAttribute("context", ownerView.contextMenuId);
+ }
+
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
+ const nonWritableIcon = this.document.createXULElement("hbox");
+ nonWritableIcon.className =
+ "plain variable-or-property-non-writable-icon";
+ nonWritableIcon.setAttribute("optional-visibility", "");
+ this._title.appendChild(nonWritableIcon);
+ }
+ if (descriptor.value && typeof descriptor.value == "object") {
+ if (descriptor.value.frozen) {
+ const frozenLabel = this.document.createXULElement("label");
+ frozenLabel.className = "plain variable-or-property-frozen-label";
+ frozenLabel.setAttribute("optional-visibility", "");
+ frozenLabel.setAttribute("value", "F");
+ this._title.appendChild(frozenLabel);
+ }
+ if (descriptor.value.sealed) {
+ const sealedLabel = this.document.createXULElement("label");
+ sealedLabel.className = "plain variable-or-property-sealed-label";
+ sealedLabel.setAttribute("optional-visibility", "");
+ sealedLabel.setAttribute("value", "S");
+ this._title.appendChild(sealedLabel);
+ }
+ if (!descriptor.value.extensible) {
+ const nonExtensibleLabel = this.document.createXULElement("label");
+ nonExtensibleLabel.className =
+ "plain variable-or-property-non-extensible-label";
+ nonExtensibleLabel.setAttribute("optional-visibility", "");
+ nonExtensibleLabel.setAttribute("value", "N");
+ this._title.appendChild(nonExtensibleLabel);
+ }
+ }
+ },
+
+ /**
+ * Prepares all tooltips for this variable.
+ */
+ _prepareTooltips() {
+ this._target.addEventListener("mouseover", this._setTooltips);
+ },
+
+ /**
+ * Sets all tooltips for this variable.
+ */
+ _setTooltips() {
+ this._target.removeEventListener("mouseover", this._setTooltips);
+
+ const ownerView = this.ownerView;
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ const tooltip = this.document.createXULElement("tooltip");
+ tooltip.id = "tooltip-" + this._idString;
+ tooltip.setAttribute("orient", "horizontal");
+
+ const labels = [
+ "configurable",
+ "enumerable",
+ "writable",
+ "frozen",
+ "sealed",
+ "extensible",
+ "overridden",
+ "WebIDL",
+ ];
+
+ for (const type of labels) {
+ const labelElement = this.document.createXULElement("label");
+ labelElement.className = type;
+ labelElement.setAttribute("value", L10N.getStr(type + "Tooltip"));
+ tooltip.appendChild(labelElement);
+ }
+
+ this._target.appendChild(tooltip);
+ this._target.setAttribute("tooltip", tooltip.id);
+
+ if (this._editNode && ownerView.eval) {
+ this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
+ }
+ if (this._openInspectorNode && this._linkedToInspector) {
+ this._openInspectorNode.setAttribute(
+ "tooltiptext",
+ this.ownerView.domNodeValueTooltip
+ );
+ }
+ if (this._valueLabel && ownerView.eval) {
+ this._valueLabel.setAttribute(
+ "tooltiptext",
+ ownerView.editableValueTooltip
+ );
+ }
+ if (this._name && ownerView.switch) {
+ this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip);
+ }
+ if (this._deleteNode && ownerView.delete) {
+ this._deleteNode.setAttribute(
+ "tooltiptext",
+ ownerView.deleteButtonTooltip
+ );
+ }
+ },
+
+ /**
+ * Get the parent variablesview toolbox, if any.
+ */
+ get toolbox() {
+ return this._variablesView.toolbox;
+ },
+
+ /**
+ * Checks if this variable is a DOMNode and is part of a variablesview that
+ * has been linked to the toolbox, so that highlighting and jumping to the
+ * inspector can be done.
+ */
+ _isLinkableToInspector() {
+ const isDomNode =
+ this._valueGrip && this._valueGrip.preview.kind === "DOMNode";
+ const hasBeenLinked = this._linkedToInspector;
+ const hasToolbox = !!this.toolbox;
+
+ return isDomNode && !hasBeenLinked && hasToolbox;
+ },
+
+ /**
+ * If the variable is a DOMNode, and if a toolbox is set, then link it to the
+ * inspector (highlight on hover, and jump to markup-view on click)
+ */
+ _linkToInspector() {
+ if (!this._isLinkableToInspector()) {
+ return;
+ }
+
+ // Listen to value mouseover/click events to highlight and jump
+ this._valueLabel.addEventListener("mouseover", this.highlightDomNode);
+ this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode);
+
+ // Add a button to open the node in the inspector
+ this._openInspectorNode = this.document.createXULElement("toolbarbutton");
+ this._openInspectorNode.className = "plain variables-view-open-inspector";
+ this._openInspectorNode.addEventListener(
+ "mousedown",
+ this.openNodeInInspector
+ );
+ this._title.appendChild(this._openInspectorNode);
+
+ this._linkedToInspector = true;
+ },
+
+ /**
+ * In case this variable is a DOMNode and part of a variablesview that has been
+ * linked to the toolbox's inspector, then select the corresponding node in
+ * the inspector, and switch the inspector tool in the toolbox
+ * @return a promise that resolves when the node is selected and the inspector
+ * has been switched to and is ready
+ */
+ openNodeInInspector(event) {
+ if (!this.toolbox) {
+ return Promise.reject(new Error("Toolbox not available"));
+ }
+
+ event && event.stopPropagation();
+
+ return async function () {
+ let nodeFront = this._nodeFront;
+ if (!nodeFront) {
+ const inspectorFront = await this.toolbox.target.getFront("inspector");
+ nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(
+ this._valueGrip
+ );
+ }
+
+ if (nodeFront) {
+ await this.toolbox.selectTool("inspector");
+
+ const inspectorReady = new Promise(resolve => {
+ this.toolbox.getPanel("inspector").once("inspector-updated", resolve);
+ });
+
+ await this.toolbox.selection.setNodeFront(nodeFront, {
+ reason: "variables-view",
+ });
+ await inspectorReady;
+ }
+ }.bind(this)();
+ },
+
+ /**
+ * In case this variable is a DOMNode and part of a variablesview that has been
+ * linked to the toolbox's inspector, then highlight the corresponding node
+ */
+ async highlightDomNode() {
+ if (!this.toolbox) {
+ return;
+ }
+
+ if (!this._nodeFront) {
+ const inspectorFront = await this.toolbox.target.getFront("inspector");
+ this._nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(
+ this._valueGrip
+ );
+ }
+
+ await this.toolbox.getHighlighter().highlight(this._nodeFront);
+ },
+
+ /**
+ * Unhighlight a previously highlit node
+ * @see highlightDomNode
+ */
+ unhighlightDomNode() {
+ if (!this.toolbox) {
+ return;
+ }
+
+ this.toolbox.getHighlighter().unhighlight();
+ },
+
+ /**
+ * Sets a variable's configurable, enumerable and writable attributes,
+ * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__'
+ * reference.
+ */
+ // eslint-disable-next-line complexity
+ _setAttributes() {
+ const ownerView = this.ownerView;
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ const descriptor = this._initialDescriptor;
+ const target = this._target;
+ const name = this._nameString;
+
+ if (ownerView.eval) {
+ target.setAttribute("editable", "");
+ }
+
+ if (!descriptor.configurable) {
+ target.setAttribute("non-configurable", "");
+ }
+ if (!descriptor.enumerable) {
+ target.setAttribute("non-enumerable", "");
+ }
+ if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
+ target.setAttribute("non-writable", "");
+ }
+
+ if (descriptor.value && typeof descriptor.value == "object") {
+ if (descriptor.value.frozen) {
+ target.setAttribute("frozen", "");
+ }
+ if (descriptor.value.sealed) {
+ target.setAttribute("sealed", "");
+ }
+ if (!descriptor.value.extensible) {
+ target.setAttribute("non-extensible", "");
+ }
+ }
+
+ if (descriptor && "getterValue" in descriptor) {
+ target.setAttribute("safe-getter", "");
+ }
+
+ if (name == "this") {
+ target.setAttribute("self", "");
+ } else if (this._internalItem && name == "<exception>") {
+ target.setAttribute("exception", "");
+ target.setAttribute("pseudo-item", "");
+ } else if (this._internalItem && name == "<return>") {
+ target.setAttribute("return", "");
+ target.setAttribute("pseudo-item", "");
+ } else if (name == "__proto__") {
+ target.setAttribute("proto", "");
+ target.setAttribute("pseudo-item", "");
+ }
+
+ if (!Object.keys(descriptor).length) {
+ target.setAttribute("pseudo-item", "");
+ }
+ },
+
+ /**
+ * Adds the necessary event listeners for this variable.
+ */
+ _addEventListeners() {
+ this._name.addEventListener("dblclick", this._activateNameInput);
+ this._valueLabel.addEventListener("mousedown", this._activateValueInput);
+ this._title.addEventListener("mousedown", this._onClick);
+ },
+
+ /**
+ * Makes this variable's name editable.
+ */
+ _activateNameInput(e) {
+ if (!this._variablesView.alignedValues) {
+ this._separatorLabel.hidden = true;
+ this._valueLabel.hidden = true;
+ }
+
+ EditableName.create(
+ this,
+ {
+ onSave: aKey => {
+ if (!this._variablesView.preventDisableOnChange) {
+ this._disable();
+ }
+ this.ownerView.switch(this, aKey);
+ },
+ onCleanup: () => {
+ if (!this._variablesView.alignedValues) {
+ this._separatorLabel.hidden = false;
+ this._valueLabel.hidden = false;
+ }
+ },
+ },
+ e
+ );
+ },
+
+ /**
+ * Makes this variable's value editable.
+ */
+ _activateValueInput(e) {
+ EditableValue.create(
+ this,
+ {
+ onSave: aString => {
+ if (this._linkedToInspector) {
+ this.unhighlightDomNode();
+ }
+ if (!this._variablesView.preventDisableOnChange) {
+ this._disable();
+ }
+ this.ownerView.eval(this, aString);
+ },
+ },
+ e
+ );
+ },
+
+ /**
+ * Disables this variable prior to a new name switch or value evaluation.
+ */
+ _disable() {
+ // Prevent the variable from being collapsed or expanded.
+ this.hideArrow();
+
+ // Hide any nodes that may offer information about the variable.
+ for (const node of this._title.childNodes) {
+ node.hidden = node != this._arrow && node != this._name;
+ }
+ this._enum.hidden = true;
+ this._nonenum.hidden = true;
+ },
+
+ /**
+ * The current macro used to generate the string evaluated when performing
+ * a variable or property value change.
+ */
+ evaluationMacro: VariablesView.simpleValueEvalMacro,
+
+ /**
+ * The click listener for the edit button.
+ */
+ _onEdit(e) {
+ if (e.button != 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ this._activateValueInput();
+ },
+
+ /**
+ * The click listener for the delete button.
+ */
+ _onDelete(e) {
+ if ("button" in e && e.button != 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.ownerView.delete) {
+ if (!this.ownerView.delete(this)) {
+ this.hide();
+ }
+ }
+ },
+
+ /**
+ * The click listener for the add property button.
+ */
+ _onAddProperty(e) {
+ if ("button" in e && e.button != 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.expanded = true;
+
+ const item = this.addItem(
+ " ",
+ {
+ value: undefined,
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ },
+ { relaxed: true }
+ );
+
+ // Force showing the separator.
+ item._separatorLabel.hidden = false;
+
+ EditableNameAndValue.create(
+ item,
+ {
+ onSave: ([aKey, aValue]) => {
+ if (!this._variablesView.preventDisableOnChange) {
+ this._disable();
+ }
+ this.ownerView.new(this, aKey, aValue);
+ },
+ },
+ e
+ );
+ },
+
+ _symbolicName: null,
+ _symbolicPath: null,
+ _absoluteName: null,
+ _initialDescriptor: null,
+ _separatorLabel: null,
+ _valueLabel: null,
+ _spacer: null,
+ _editNode: null,
+ _deleteNode: null,
+ _addPropertyNode: null,
+ _tooltip: null,
+ _valueGrip: null,
+ _valueString: "",
+ _valueClassName: "",
+ _prevExpandable: false,
+ _prevExpanded: false,
+});
+
+/**
+ * A Property is a Variable holding additional child Property instances.
+ * Iterable via "for (let [name, property] of instance) { }".
+ *
+ * @param Variable aVar
+ * The variable to contain this property.
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * The property's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by Scope.addItem
+ */
+function Property(aVar, aName, aDescriptor, aOptions) {
+ Variable.call(this, aVar, aName, aDescriptor, aOptions);
+}
+
+Property.prototype = extend(Variable.prototype, {
+ /**
+ * The class name applied to this property's target element.
+ */
+ targetClassName: "variables-view-property variable-or-property",
+
+ /**
+ * @see Variable.symbolicName
+ * @return string
+ */
+ get symbolicName() {
+ if (this._symbolicName) {
+ return this._symbolicName;
+ }
+
+ this._symbolicName =
+ this.ownerView.symbolicName + "[" + escapeString(this._nameString) + "]";
+ return this._symbolicName;
+ },
+
+ /**
+ * @see Variable.absoluteName
+ * @return string
+ */
+ get absoluteName() {
+ if (this._absoluteName) {
+ return this._absoluteName;
+ }
+
+ this._absoluteName =
+ this.ownerView.absoluteName + "[" + escapeString(this._nameString) + "]";
+ return this._absoluteName;
+ },
+});
+
+/**
+ * A generator-iterator over the VariablesView, Scopes, Variables and Properties.
+ */
+VariablesView.prototype[Symbol.iterator] =
+ Scope.prototype[Symbol.iterator] =
+ Variable.prototype[Symbol.iterator] =
+ Property.prototype[Symbol.iterator] =
+ function* () {
+ yield* this._store;
+ };
+
+/**
+ * Forget everything recorded about added scopes, variables or properties.
+ * @see VariablesView.commitHierarchy
+ */
+VariablesView.prototype.clearHierarchy = function () {
+ this._prevHierarchy.clear();
+ this._currHierarchy.clear();
+};
+
+/**
+ * Perform operations on all the VariablesView Scopes, Variables and Properties
+ * after you've added all the items you wanted.
+ *
+ * Calling this method is optional, and does the following:
+ * - styles the items overridden by other items in parent scopes
+ * - reopens the items which were previously expanded
+ * - flashes the items whose values changed
+ */
+VariablesView.prototype.commitHierarchy = function () {
+ for (const [, currItem] of this._currHierarchy) {
+ // Avoid performing expensive operations.
+ if (this.commitHierarchyIgnoredItems[currItem._nameString]) {
+ continue;
+ }
+ const overridden = this.isOverridden(currItem);
+ if (overridden) {
+ currItem.setOverridden(true);
+ }
+ const expanded = !currItem._committed && this.wasExpanded(currItem);
+ if (expanded) {
+ currItem.expand();
+ }
+ const changed = !currItem._committed && this.hasChanged(currItem);
+ if (changed) {
+ currItem.flash();
+ }
+ currItem._committed = true;
+ }
+ if (this.oncommit) {
+ this.oncommit(this);
+ }
+};
+
+// Some variables are likely to contain a very large number of properties.
+// It would be a bad idea to re-expand them or perform expensive operations.
+VariablesView.prototype.commitHierarchyIgnoredItems = extend(null, {
+ window: true,
+ this: true,
+});
+
+/**
+ * Checks if the an item was previously expanded, if it existed in a
+ * previous hierarchy.
+ *
+ * @param Scope | Variable | Property aItem
+ * The item to verify.
+ * @return boolean
+ * Whether the item was expanded.
+ */
+VariablesView.prototype.wasExpanded = function (aItem) {
+ if (!(aItem instanceof Scope)) {
+ return false;
+ }
+ const prevItem = this._prevHierarchy.get(
+ aItem.absoluteName || aItem._nameString
+ );
+ return prevItem ? prevItem._isExpanded : false;
+};
+
+/**
+ * Checks if the an item's displayed value (a representation of the grip)
+ * has changed, if it existed in a previous hierarchy.
+ *
+ * @param Variable | Property aItem
+ * The item to verify.
+ * @return boolean
+ * Whether the item has changed.
+ */
+VariablesView.prototype.hasChanged = function (aItem) {
+ // Only analyze Variables and Properties for displayed value changes.
+ // Scopes are just collections of Variables and Properties and
+ // don't have a "value", so they can't change.
+ if (!(aItem instanceof Variable)) {
+ return false;
+ }
+ const prevItem = this._prevHierarchy.get(aItem.absoluteName);
+ return prevItem ? prevItem._valueString != aItem._valueString : false;
+};
+
+/**
+ * Checks if the an item was previously expanded, if it existed in a
+ * previous hierarchy.
+ *
+ * @param Scope | Variable | Property aItem
+ * The item to verify.
+ * @return boolean
+ * Whether the item was expanded.
+ */
+VariablesView.prototype.isOverridden = function (aItem) {
+ // Only analyze Variables for being overridden in different Scopes.
+ if (!(aItem instanceof Variable) || aItem instanceof Property) {
+ return false;
+ }
+ const currVariableName = aItem._nameString;
+ const parentScopes = this.getParentScopesForVariableOrProperty(aItem);
+
+ for (const otherScope of parentScopes) {
+ for (const [otherVariableName] of otherScope) {
+ if (otherVariableName == currVariableName) {
+ return true;
+ }
+ }
+ }
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents an undefined, null or
+ * primitive value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isPrimitive = function (aDescriptor) {
+ // For accessor property descriptors, the getter and setter need to be
+ // contained in 'get' and 'set' properties.
+ const getter = aDescriptor.get;
+ const setter = aDescriptor.set;
+ if (getter || setter) {
+ return false;
+ }
+
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ const grip = aDescriptor.value;
+ if (typeof grip != "object") {
+ return true;
+ }
+
+ // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long
+ // strings are considered types.
+ const type = grip.type;
+ if (
+ type == "undefined" ||
+ type == "null" ||
+ type == "Infinity" ||
+ type == "-Infinity" ||
+ type == "NaN" ||
+ type == "-0" ||
+ type == "symbol" ||
+ type == "longString"
+ ) {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents an undefined value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isUndefined = function (aDescriptor) {
+ // For accessor property descriptors, the getter and setter need to be
+ // contained in 'get' and 'set' properties.
+ const getter = aDescriptor.get;
+ const setter = aDescriptor.set;
+ if (
+ typeof getter == "object" &&
+ getter.type == "undefined" &&
+ typeof setter == "object" &&
+ setter.type == "undefined"
+ ) {
+ return true;
+ }
+
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ const grip = aDescriptor.value;
+ if (typeof grip == "object" && grip.type == "undefined") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents a falsy value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isFalsy = function (aDescriptor) {
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ const grip = aDescriptor.value;
+ if (typeof grip != "object") {
+ return !grip;
+ }
+
+ // For convenience, undefined, null, NaN, and -0 are all considered types.
+ const type = grip.type;
+ if (type == "undefined" || type == "null" || type == "NaN" || type == "-0") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the value is an instance of Variable or Property.
+ *
+ * @param any aValue
+ * The value to test.
+ */
+VariablesView.isVariable = function (aValue) {
+ return aValue instanceof Variable;
+};
+
+/**
+ * Returns a standard grip for a value.
+ *
+ * @param any aValue
+ * The raw value to get a grip for.
+ * @return any
+ * The value's grip.
+ */
+VariablesView.getGrip = function (aValue) {
+ switch (typeof aValue) {
+ case "boolean":
+ case "string":
+ return aValue;
+ case "number":
+ if (aValue === Infinity) {
+ return { type: "Infinity" };
+ } else if (aValue === -Infinity) {
+ return { type: "-Infinity" };
+ } else if (Number.isNaN(aValue)) {
+ return { type: "NaN" };
+ } else if (1 / aValue === -Infinity) {
+ return { type: "-0" };
+ }
+ return aValue;
+ case "undefined":
+ // document.all is also "undefined"
+ if (aValue === undefined) {
+ return { type: "undefined" };
+ }
+ // fall through
+ case "object":
+ if (aValue === null) {
+ return { type: "null" };
+ }
+ // fall through
+ case "function":
+ return { type: "object", class: getObjectClassName(aValue) };
+ default:
+ console.error(
+ "Failed to provide a grip for value of " + typeof value + ": " + aValue
+ );
+ return null;
+ }
+};
+
+// Match the function name from the result of toString() or toSource().
+//
+// Examples:
+// (function foobar(a, b) { ...
+// function foobar2(a) { ...
+// function() { ...
+const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
+
+/**
+ * Helper function to deduce the name of the provided function.
+ *
+ * @param function function
+ * The function whose name will be returned.
+ * @return string
+ * Function name.
+ */
+function getFunctionName(func) {
+ let name = null;
+ if (func.name) {
+ name = func.name;
+ } else {
+ let desc;
+ try {
+ desc = func.getOwnPropertyDescriptor("displayName");
+ } catch (ex) {
+ // Ignore.
+ }
+ if (desc && typeof desc.value == "string") {
+ name = desc.value;
+ }
+ }
+ if (!name) {
+ try {
+ const str = (func.toString() || func.toSource()) + "";
+ name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1];
+ } catch (ex) {
+ // Ignore.
+ }
+ }
+ return name;
+}
+
+/**
+ * Get the object class name. For example, the |window| object has the Window
+ * class name (based on [object Window]).
+ *
+ * @param object object
+ * The object you want to get the class name for.
+ * @return string
+ * The object class name.
+ */
+function getObjectClassName(object) {
+ if (object === null) {
+ return "null";
+ }
+ if (object === undefined) {
+ return "undefined";
+ }
+
+ const type = typeof object;
+ if (type != "object") {
+ // Grip class names should start with an uppercase letter.
+ return type.charAt(0).toUpperCase() + type.substr(1);
+ }
+
+ let className;
+
+ try {
+ className = ((object + "").match(/^\[object (\S+)\]$/) || [])[1];
+ if (!className) {
+ className = ((object.constructor + "").match(/^\[object (\S+)\]$/) ||
+ [])[1];
+ }
+ if (!className && typeof object.constructor == "function") {
+ className = getFunctionName(object.constructor);
+ }
+ } catch (ex) {
+ // Ignore.
+ }
+
+ return className;
+}
+
+/**
+ * Returns a custom formatted property string for a grip.
+ *
+ * @param any aGrip
+ * @see Variable.setGrip
+ * @param object aOptions
+ * Options:
+ * - concise: boolean that tells you want a concisely formatted string.
+ * - noStringQuotes: boolean that tells to not quote strings.
+ * - noEllipsis: boolean that tells to not add an ellipsis after the
+ * initial text of a longString.
+ * @return string
+ * The formatted property string.
+ */
+VariablesView.getString = function (aGrip, aOptions = {}) {
+ if (aGrip && typeof aGrip == "object") {
+ switch (aGrip.type) {
+ case "undefined":
+ case "null":
+ case "NaN":
+ case "Infinity":
+ case "-Infinity":
+ case "-0":
+ return aGrip.type;
+ default:
+ const stringifier = VariablesView.stringifiers.byType[aGrip.type];
+ if (stringifier) {
+ const result = stringifier(aGrip, aOptions);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ if (aGrip.displayString) {
+ return VariablesView.getString(aGrip.displayString, aOptions);
+ }
+
+ if (aGrip.type == "object" && aOptions.concise) {
+ return aGrip.class;
+ }
+
+ return "[" + aGrip.type + " " + aGrip.class + "]";
+ }
+ }
+
+ switch (typeof aGrip) {
+ case "string":
+ return VariablesView.stringifiers.byType.string(aGrip, aOptions);
+ case "boolean":
+ return aGrip ? "true" : "false";
+ case "number":
+ if (!aGrip && 1 / aGrip === -Infinity) {
+ return "-0";
+ }
+ // fall through
+ default:
+ return aGrip + "";
+ }
+};
+
+/**
+ * The VariablesView stringifiers are used by VariablesView.getString(). These
+ * are organized by object type, object class and by object actor preview kind.
+ * Some objects share identical ways for previews, for example Arrays, Sets and
+ * NodeLists.
+ *
+ * Any stringifier function must return a string. If null is returned, * then
+ * the default stringifier will be used. When invoked, the stringifier is
+ * given the same two arguments as those given to VariablesView.getString().
+ */
+VariablesView.stringifiers = {};
+
+VariablesView.stringifiers.byType = {
+ string(aGrip, { noStringQuotes }) {
+ if (noStringQuotes) {
+ return aGrip;
+ }
+ return '"' + aGrip + '"';
+ },
+
+ longString({ initial }, { noStringQuotes, noEllipsis }) {
+ const ellipsis = noEllipsis ? "" : ELLIPSIS;
+ if (noStringQuotes) {
+ return initial + ellipsis;
+ }
+ const result = '"' + initial + '"';
+ if (!ellipsis) {
+ return result;
+ }
+ return result.substr(0, result.length - 1) + ellipsis + '"';
+ },
+
+ object(aGrip, aOptions) {
+ const { preview } = aGrip;
+ let stringifier;
+ if (aGrip.class) {
+ stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class];
+ }
+ if (!stringifier && preview && preview.kind) {
+ stringifier = VariablesView.stringifiers.byObjectKind[preview.kind];
+ }
+ if (stringifier) {
+ return stringifier(aGrip, aOptions);
+ }
+ return null;
+ },
+
+ symbol(aGrip, aOptions) {
+ const name = aGrip.name || "";
+ return "Symbol(" + name + ")";
+ },
+
+ mapEntry(aGrip, { concise }) {
+ const {
+ preview: { key, value },
+ } = aGrip;
+
+ const keyString = VariablesView.getString(key, {
+ concise: true,
+ noStringQuotes: true,
+ });
+ const valueString = VariablesView.getString(value, { concise: true });
+
+ return keyString + " \u2192 " + valueString;
+ },
+}; // VariablesView.stringifiers.byType
+
+VariablesView.stringifiers.byObjectClass = {
+ Function(aGrip, { concise }) {
+ // TODO: Bug 948484 - support arrow functions and ES6 generators
+
+ let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
+ name = VariablesView.getString(name, { noStringQuotes: true });
+
+ // TODO: Bug 948489 - Support functions with destructured parameters and
+ // rest parameters
+ const params = aGrip.parameterNames || "";
+ if (!concise) {
+ return "function " + name + "(" + params + ")";
+ }
+ return (name || "function ") + "(" + params + ")";
+ },
+
+ RegExp({ displayString }) {
+ return VariablesView.getString(displayString, { noStringQuotes: true });
+ },
+
+ Date({ preview }) {
+ if (!preview || !("timestamp" in preview)) {
+ return null;
+ }
+
+ if (typeof preview.timestamp != "number") {
+ return new Date(preview.timestamp).toString(); // invalid date
+ }
+
+ return "Date " + new Date(preview.timestamp).toISOString();
+ },
+
+ Number(aGrip) {
+ const { preview } = aGrip;
+ if (preview === undefined) {
+ return null;
+ }
+ return (
+ aGrip.class + " { " + VariablesView.getString(preview.wrappedValue) + " }"
+ );
+ },
+}; // VariablesView.stringifiers.byObjectClass
+
+VariablesView.stringifiers.byObjectClass.Boolean =
+ VariablesView.stringifiers.byObjectClass.Number;
+
+VariablesView.stringifiers.byObjectKind = {
+ ArrayLike(aGrip, { concise }) {
+ const { preview } = aGrip;
+ if (concise) {
+ return aGrip.class + "[" + preview.length + "]";
+ }
+
+ if (!preview.items) {
+ return null;
+ }
+
+ let shown = 0,
+ lastHole = null;
+ const result = [];
+ for (const item of preview.items) {
+ if (item === null) {
+ if (lastHole !== null) {
+ result[lastHole] += ",";
+ } else {
+ result.push("");
+ }
+ lastHole = result.length - 1;
+ } else {
+ lastHole = null;
+ result.push(VariablesView.getString(item, { concise: true }));
+ }
+ shown++;
+ }
+
+ if (shown < preview.length) {
+ const n = preview.length - shown;
+ result.push(VariablesView.stringifiers._getNMoreString(n));
+ } else if (lastHole !== null) {
+ // make sure we have the right number of commas...
+ result[lastHole] += ",";
+ }
+
+ const prefix = aGrip.class == "Array" ? "" : aGrip.class + " ";
+ return prefix + "[" + result.join(", ") + "]";
+ },
+
+ MapLike(aGrip, { concise }) {
+ const { preview } = aGrip;
+ if (concise || !preview.entries) {
+ const size =
+ typeof preview.size == "number" ? "[" + preview.size + "]" : "";
+ return aGrip.class + size;
+ }
+
+ const entries = [];
+ for (const [key, value] of preview.entries) {
+ const keyString = VariablesView.getString(key, {
+ concise: true,
+ noStringQuotes: true,
+ });
+ const valueString = VariablesView.getString(value, { concise: true });
+ entries.push(keyString + ": " + valueString);
+ }
+
+ if (typeof preview.size == "number" && preview.size > entries.length) {
+ const n = preview.size - entries.length;
+ entries.push(VariablesView.stringifiers._getNMoreString(n));
+ }
+
+ return aGrip.class + " {" + entries.join(", ") + "}";
+ },
+
+ ObjectWithText(aGrip, { concise }) {
+ if (concise) {
+ return aGrip.class;
+ }
+
+ return aGrip.class + " " + VariablesView.getString(aGrip.preview.text);
+ },
+
+ ObjectWithURL(aGrip, { concise }) {
+ let result = aGrip.class;
+ const url = aGrip.preview.url;
+ if (!VariablesView.isFalsy({ value: url })) {
+ result += ` \u2192 ${getSourceNames(url)[concise ? "short" : "long"]}`;
+ }
+ return result;
+ },
+
+ // Stringifier for any kind of object.
+ Object(aGrip, { concise }) {
+ if (concise) {
+ return aGrip.class;
+ }
+
+ const { preview } = aGrip;
+ const props = [];
+
+ if (aGrip.class == "Promise" && aGrip.promiseState) {
+ const { state, value, reason } = aGrip.promiseState;
+ props.push("<state>: " + VariablesView.getString(state));
+ if (state == "fulfilled") {
+ props.push(
+ "<value>: " + VariablesView.getString(value, { concise: true })
+ );
+ } else if (state == "rejected") {
+ props.push(
+ "<reason>: " + VariablesView.getString(reason, { concise: true })
+ );
+ }
+ }
+
+ for (const key of Object.keys(preview.ownProperties || {})) {
+ const value = preview.ownProperties[key];
+ let valueString = "";
+ if (value.get) {
+ valueString = "Getter";
+ } else if (value.set) {
+ valueString = "Setter";
+ } else {
+ valueString = VariablesView.getString(value.value, { concise: true });
+ }
+ props.push(key + ": " + valueString);
+ }
+
+ for (const key of Object.keys(preview.safeGetterValues || {})) {
+ const value = preview.safeGetterValues[key];
+ const valueString = VariablesView.getString(value.getterValue, {
+ concise: true,
+ });
+ props.push(key + ": " + valueString);
+ }
+
+ if (!props.length) {
+ return null;
+ }
+
+ if (preview.ownPropertiesLength) {
+ const previewLength = Object.keys(preview.ownProperties).length;
+ const diff = preview.ownPropertiesLength - previewLength;
+ if (diff > 0) {
+ props.push(VariablesView.stringifiers._getNMoreString(diff));
+ }
+ }
+
+ const prefix = aGrip.class != "Object" ? aGrip.class + " " : "";
+ return prefix + "{" + props.join(", ") + "}";
+ }, // Object
+
+ Error(aGrip, { concise }) {
+ const { preview } = aGrip;
+ const name = VariablesView.getString(preview.name, {
+ noStringQuotes: true,
+ });
+ if (concise) {
+ return name || aGrip.class;
+ }
+
+ let msg =
+ name +
+ ": " +
+ VariablesView.getString(preview.message, { noStringQuotes: true });
+
+ if (!VariablesView.isFalsy({ value: preview.stack })) {
+ msg +=
+ "\n" +
+ L10N.getStr("variablesViewErrorStacktrace") +
+ "\n" +
+ preview.stack;
+ }
+
+ return msg;
+ },
+
+ DOMException(aGrip, { concise }) {
+ const { preview } = aGrip;
+ if (concise) {
+ return preview.name || aGrip.class;
+ }
+
+ let msg =
+ aGrip.class +
+ " [" +
+ preview.name +
+ ": " +
+ VariablesView.getString(preview.message) +
+ "\n" +
+ "code: " +
+ preview.code +
+ "\n" +
+ "nsresult: 0x" +
+ (+preview.result).toString(16);
+
+ if (preview.filename) {
+ msg += "\nlocation: " + preview.filename;
+ if (preview.lineNumber) {
+ msg += ":" + preview.lineNumber;
+ }
+ }
+
+ return msg + "]";
+ },
+
+ DOMEvent(aGrip, { concise }) {
+ const { preview } = aGrip;
+ if (!preview.type) {
+ return null;
+ }
+
+ if (concise) {
+ return aGrip.class + " " + preview.type;
+ }
+
+ let result = preview.type;
+
+ if (
+ preview.eventKind == "key" &&
+ preview.modifiers &&
+ preview.modifiers.length
+ ) {
+ result += " " + preview.modifiers.join("-");
+ }
+
+ const props = [];
+ if (preview.target) {
+ const target = VariablesView.getString(preview.target, { concise: true });
+ props.push("target: " + target);
+ }
+
+ for (const prop in preview.properties) {
+ const value = preview.properties[prop];
+ props.push(
+ prop + ": " + VariablesView.getString(value, { concise: true })
+ );
+ }
+
+ return result + " {" + props.join(", ") + "}";
+ }, // DOMEvent
+
+ DOMNode(aGrip, { concise }) {
+ const { preview } = aGrip;
+
+ switch (preview.nodeType) {
+ case nodeConstants.DOCUMENT_NODE: {
+ let result = aGrip.class;
+ if (preview.location) {
+ result += ` \u2192 ${
+ getSourceNames(preview.location)[concise ? "short" : "long"]
+ }`;
+ }
+
+ return result;
+ }
+
+ case nodeConstants.ATTRIBUTE_NODE: {
+ const value = VariablesView.getString(preview.value, {
+ noStringQuotes: true,
+ });
+ return preview.nodeName + '="' + escapeHTML(value) + '"';
+ }
+
+ case nodeConstants.TEXT_NODE:
+ return (
+ preview.nodeName + " " + VariablesView.getString(preview.textContent)
+ );
+
+ case nodeConstants.COMMENT_NODE: {
+ const comment = VariablesView.getString(preview.textContent, {
+ noStringQuotes: true,
+ });
+ return "<!--" + comment + "-->";
+ }
+
+ case nodeConstants.DOCUMENT_FRAGMENT_NODE: {
+ if (concise || !preview.childNodes) {
+ return aGrip.class + "[" + preview.childNodesLength + "]";
+ }
+ const nodes = [];
+ for (const node of preview.childNodes) {
+ nodes.push(VariablesView.getString(node));
+ }
+ if (nodes.length < preview.childNodesLength) {
+ const n = preview.childNodesLength - nodes.length;
+ nodes.push(VariablesView.stringifiers._getNMoreString(n));
+ }
+ return aGrip.class + " [" + nodes.join(", ") + "]";
+ }
+
+ case nodeConstants.ELEMENT_NODE: {
+ const attrs = preview.attributes;
+ if (!concise) {
+ let n = 0,
+ result = "<" + preview.nodeName;
+ for (const name in attrs) {
+ const value = VariablesView.getString(attrs[name], {
+ noStringQuotes: true,
+ });
+ result += " " + name + '="' + escapeHTML(value) + '"';
+ n++;
+ }
+ if (preview.attributesLength > n) {
+ result += " " + ELLIPSIS;
+ }
+ return result + ">";
+ }
+
+ let result = "<" + preview.nodeName;
+ if (attrs.id) {
+ result += "#" + attrs.id;
+ }
+
+ if (attrs.class) {
+ result += "." + attrs.class.trim().replace(/\s+/, ".");
+ }
+ return result + ">";
+ }
+
+ default:
+ return null;
+ }
+ }, // DOMNode
+}; // VariablesView.stringifiers.byObjectKind
+
+/**
+ * Get the "N moreā€¦" formatted string, given an N. This is used for displaying
+ * how many elements are not displayed in an object preview (eg. an array).
+ *
+ * @private
+ * @param number aNumber
+ * @return string
+ */
+VariablesView.stringifiers._getNMoreString = function (aNumber) {
+ const str = L10N.getStr("variablesViewMoreObjects");
+ return PluralForm.get(aNumber, str).replace("#1", aNumber);
+};
+
+/**
+ * Returns a custom class style for a grip.
+ *
+ * @param any aGrip
+ * @see Variable.setGrip
+ * @return string
+ * The custom class style.
+ */
+VariablesView.getClass = function (aGrip) {
+ if (aGrip && typeof aGrip == "object") {
+ if (aGrip.preview) {
+ switch (aGrip.preview.kind) {
+ case "DOMNode":
+ return "token-domnode";
+ }
+ }
+
+ switch (aGrip.type) {
+ case "undefined":
+ return "token-undefined";
+ case "null":
+ return "token-null";
+ case "Infinity":
+ case "-Infinity":
+ case "NaN":
+ case "-0":
+ return "token-number";
+ case "longString":
+ return "token-string";
+ }
+ }
+ switch (typeof aGrip) {
+ case "string":
+ return "token-string";
+ case "boolean":
+ return "token-boolean";
+ case "number":
+ return "token-number";
+ default:
+ return "token-other";
+ }
+};
+
+/**
+ * A monotonically-increasing counter, that guarantees the uniqueness of scope,
+ * variables and properties ids.
+ *
+ * @param string aName
+ * An optional string to prefix the id with.
+ * @return number
+ * A unique id.
+ */
+var generateId = (function () {
+ let count = 0;
+ return function (aName = "") {
+ return aName.toLowerCase().trim().replace(/\s+/g, "-") + ++count;
+ };
+})();
+
+/**
+ * Quote and escape a string. The result will be another string containing an
+ * ECMAScript StringLiteral which will produce the original one when evaluated
+ * by `eval` or similar.
+ *
+ * @param string aString
+ * An optional string to be escaped. If no string is passed, the function
+ * returns an empty string.
+ * @return string
+ */
+function escapeString(aString) {
+ if (typeof aString !== "string") {
+ return "";
+ }
+ // U+2028 and U+2029 are allowed in JSON but not in ECMAScript string literals.
+ return JSON.stringify(aString)
+ .replace(/\u2028/g, "\\u2028")
+ .replace(/\u2029/g, "\\u2029");
+}
+
+/**
+ * Escape some HTML special characters. We do not need full HTML serialization
+ * here, we just want to make strings safe to display in HTML attributes, for
+ * the stringifiers.
+ *
+ * @param string aString
+ * @return string
+ */
+export function escapeHTML(aString) {
+ return aString
+ .replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;");
+}
+
+/**
+ * An Editable encapsulates the UI of an edit box that overlays a label,
+ * allowing the user to edit the value.
+ *
+ * @param Variable aVariable
+ * The Variable or Property to make editable.
+ * @param object aOptions
+ * - onSave
+ * The callback to call with the value when editing is complete.
+ * - onCleanup
+ * The callback to call when the editable is removed for any reason.
+ */
+function Editable(aVariable, aOptions) {
+ this._variable = aVariable;
+ this._onSave = aOptions.onSave;
+ this._onCleanup = aOptions.onCleanup;
+}
+
+Editable.create = function (aVariable, aOptions, aEvent) {
+ const editable = new this(aVariable, aOptions);
+ editable.activate(aEvent);
+ return editable;
+};
+
+Editable.prototype = {
+ /**
+ * The class name for targeting this Editable type's label element. Overridden
+ * by inheriting classes.
+ */
+ className: null,
+
+ /**
+ * Boolean indicating whether this Editable should activate. Overridden by
+ * inheriting classes.
+ */
+ shouldActivate: null,
+
+ /**
+ * The label element for this Editable. Overridden by inheriting classes.
+ */
+ label: null,
+
+ /**
+ * Activate this editable by replacing the input box it overlays and
+ * initialize the handlers.
+ *
+ * @param Event e [optional]
+ * Optionally, the Event object that was used to activate the Editable.
+ */
+ activate(e) {
+ if (!this.shouldActivate) {
+ this._onCleanup && this._onCleanup();
+ return;
+ }
+
+ const { label } = this;
+ const initialString = label.getAttribute("value");
+
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ // Create a texbox input element which will be shown in the current
+ // element's specified label location.
+ const input = (this._input = this._variable.document.createElementNS(
+ HTML_NS,
+ "input"
+ ));
+ input.className = this.className;
+ input.setAttribute("value", initialString);
+
+ // Replace the specified label with a textbox input element.
+ label.parentNode.replaceChild(input, label);
+ input.scrollIntoView({ block: "nearest" });
+ input.select();
+
+ // When the value is a string (displayed as "value"), then we probably want
+ // to change it to another string in the textbox, so to avoid typing the ""
+ // again, tackle with the selection bounds just a bit.
+ if (initialString.match(/^".+"$/)) {
+ input.selectionEnd--;
+ input.selectionStart++;
+ }
+
+ this._onKeydown = this._onKeydown.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+ input.addEventListener("keydown", this._onKeydown);
+ input.addEventListener("blur", this._onBlur);
+
+ this._prevExpandable = this._variable.twisty;
+ this._prevExpanded = this._variable.expanded;
+ this._variable.collapse();
+ this._variable.hideArrow();
+ this._variable.locked = true;
+ this._variable.editing = true;
+ },
+
+ /**
+ * Remove the input box and restore the Variable or Property to its previous
+ * state.
+ */
+ deactivate() {
+ this._input.removeEventListener("keydown", this._onKeydown);
+ this._input.removeEventListener("blur", this.deactivate);
+ this._input.parentNode.replaceChild(this.label, this._input);
+ this._input = null;
+
+ const scrollbox = this._variable._variablesView._list;
+ scrollbox.scrollBy(-this._variable._target, 0);
+ this._variable.locked = false;
+ this._variable.twisty = this._prevExpandable;
+ this._variable.expanded = this._prevExpanded;
+ this._variable.editing = false;
+ this._onCleanup && this._onCleanup();
+ },
+
+ /**
+ * Save the current value and deactivate the Editable.
+ */
+ _save() {
+ const initial = this.label.getAttribute("value");
+ const current = this._input.value.trim();
+ this.deactivate();
+ if (initial != current) {
+ this._onSave(current);
+ }
+ },
+
+ /**
+ * Called when tab is pressed, allowing subclasses to link different
+ * behavior to tabbing if desired.
+ */
+ _next() {
+ this._save();
+ },
+
+ /**
+ * Called when escape is pressed, indicating a cancelling of editing without
+ * saving.
+ */
+ _reset() {
+ this.deactivate();
+ this._variable.focus();
+ },
+
+ /**
+ * Event handler for when the input loses focus.
+ */
+ _onBlur() {
+ this.deactivate();
+ },
+
+ /**
+ * Event handler for when the input receives a key press.
+ */
+ _onKeydown(e) {
+ e.stopPropagation();
+
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_TAB:
+ this._next();
+ break;
+ case KeyCodes.DOM_VK_RETURN:
+ this._save();
+ break;
+ case KeyCodes.DOM_VK_ESCAPE:
+ this._reset();
+ break;
+ }
+ },
+};
+
+/**
+ * An Editable specific to editing the name of a Variable or Property.
+ */
+function EditableName(aVariable, aOptions) {
+ Editable.call(this, aVariable, aOptions);
+}
+
+EditableName.create = Editable.create;
+
+EditableName.prototype = extend(Editable.prototype, {
+ className: "element-name-input",
+
+ get label() {
+ return this._variable._name;
+ },
+
+ get shouldActivate() {
+ return !!this._variable.ownerView.switch;
+ },
+});
+
+/**
+ * An Editable specific to editing the value of a Variable or Property.
+ */
+function EditableValue(aVariable, aOptions) {
+ Editable.call(this, aVariable, aOptions);
+}
+
+EditableValue.create = Editable.create;
+
+EditableValue.prototype = extend(Editable.prototype, {
+ className: "element-value-input",
+
+ get label() {
+ return this._variable._valueLabel;
+ },
+
+ get shouldActivate() {
+ return !!this._variable.ownerView.eval;
+ },
+});
+
+/**
+ * An Editable specific to editing the key and value of a new property.
+ */
+function EditableNameAndValue(aVariable, aOptions) {
+ EditableName.call(this, aVariable, aOptions);
+}
+
+EditableNameAndValue.create = Editable.create;
+
+EditableNameAndValue.prototype = extend(EditableName.prototype, {
+ _reset(e) {
+ // Hide the Variable or Property if the user presses escape.
+ this._variable.remove();
+ this.deactivate();
+ },
+
+ _next(e) {
+ // Override _next so as to set both key and value at the same time.
+ const key = this._input.value;
+ this.label.setAttribute("value", key);
+
+ const valueEditable = EditableValue.create(this._variable, {
+ onSave: aValue => {
+ this._onSave([key, aValue]);
+ },
+ });
+ valueEditable._reset = () => {
+ this._variable.remove();
+ valueEditable.deactivate();
+ };
+ },
+
+ _save(e) {
+ // Both _save and _next activate the value edit box.
+ this._next(e);
+ },
+});