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