1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* eslint-disable mozilla/no-aArgs */
7 const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
8 const LAZY_EMPTY_DELAY = 150; // ms
9 const SCROLL_PAGE_SIZE_DEFAULT = 0;
10 const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
11 const PAGE_SIZE_MAX_JUMPS = 30;
12 const SEARCH_ACTION_MAX_DELAY = 300; // ms
13 const ITEM_FLASH_DURATION = 300; // ms
15 import { require } from "resource://devtools/shared/loader/Loader.sys.mjs";
17 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
19 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
20 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
23 } = require("resource://devtools/client/shared/source-utils.js");
24 const { extend } = require("resource://devtools/shared/extend.js");
28 } = require("resource://devtools/client/shared/widgets/view-helpers.js");
29 const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
30 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
31 const { PluralForm } = require("resource://devtools/shared/plural-form.js");
35 } = require("resource://devtools/shared/l10n.js");
37 const L10N = new LocalizationHelper(DBG_STRINGS_URI);
38 const HTML_NS = "http://www.w3.org/1999/xhtml";
42 XPCOMUtils.defineLazyServiceGetter(
45 "@mozilla.org/widget/clipboardhelper;1",
50 * A tree view for inspecting scopes, objects and properties.
51 * Iterable via "for (let [id, scope] of instance) { }".
52 * Requires the devtools common.css and debugger.css skin stylesheets.
54 * To allow replacing variable or property values in this view, provide an
55 * "eval" function property. To allow replacing variable or property names,
56 * provide a "switch" function. To handle deleting variables or properties,
57 * provide a "delete" function.
59 * @param Node aParentNode
60 * The parent node to hold this view.
61 * @param object aFlags [optional]
62 * An object contaning initialization options for this view.
63 * e.g. { lazyEmpty: true, searchEnabled: true ... }
65 export function VariablesView(aParentNode, aFlags = {}) {
66 this._store = []; // Can't use a Map because Scope names needn't be unique.
67 this._itemsByElement = new WeakMap();
68 this._prevHierarchy = new Map();
69 this._currHierarchy = new Map();
71 this._parent = aParentNode;
72 this._parent.classList.add("variables-view-container");
73 this._parent.classList.add("theme-body");
74 this._appendEmptyNotice();
76 this._onSearchboxInput = this._onSearchboxInput.bind(this);
77 this._onSearchboxKeyDown = this._onSearchboxKeyDown.bind(this);
78 this._onViewKeyDown = this._onViewKeyDown.bind(this);
80 // Create an internal scrollbox container.
81 this._list = this.document.createXULElement("scrollbox");
82 this._list.setAttribute("orient", "vertical");
83 this._list.addEventListener("keydown", this._onViewKeyDown);
84 this._parent.appendChild(this._list);
86 for (const name in aFlags) {
87 this[name] = aFlags[name];
90 EventEmitter.decorate(this);
93 VariablesView.prototype = {
95 * Helper setter for populating this container with a raw object.
97 * @param object aObject
98 * The raw object to display. You can only provide this object
99 * if you want the variables view to work in sync mode.
101 set rawObject(aObject) {
104 .addItem(undefined, { enumerable: true })
105 .populate(aObject, { sorted: true });
109 * Adds a scope to contain any inspected variables.
111 * This new scope will be considered the parent of any other scope
114 * @param string l10nId
115 * The scope localized string id.
116 * @param string aCustomClass
117 * An additional class name for the containing element.
119 * The newly created Scope instance.
121 addScope(l10nId = "", aCustomClass = "") {
122 this._removeEmptyNotice();
123 this._toggleSearchVisibility(true);
125 const scope = new Scope(this, l10nId, { customClass: aCustomClass });
126 this._store.push(scope);
127 this._itemsByElement.set(scope._target, scope);
128 this._currHierarchy.set(l10nId, scope);
129 scope.header = !!l10nId;
135 * Removes all items from this container.
137 * @param number aTimeout [optional]
138 * The number of milliseconds to delay the operation if
139 * lazy emptying of this container is enabled.
141 empty(aTimeout = this.lazyEmptyDelay) {
142 // If there are no items in this container, emptying is useless.
143 if (!this._store.length) {
147 this._store.length = 0;
148 this._itemsByElement = new WeakMap();
149 this._prevHierarchy = this._currHierarchy;
150 this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
152 // Check if this empty operation may be executed lazily.
153 if (this.lazyEmpty && aTimeout > 0) {
154 this._emptySoon(aTimeout);
158 while (this._list.hasChildNodes()) {
159 this._list.firstChild.remove();
162 this._appendEmptyNotice();
163 this._toggleSearchVisibility(false);
167 * Emptying this container and rebuilding it immediately afterwards would
168 * result in a brief redraw flicker, because the previously expanded nodes
169 * may get asynchronously re-expanded, after fetching the prototype and
170 * properties from a server.
172 * To avoid such behaviour, a normal container list is rebuild, but not
173 * immediately attached to the parent container. The old container list
174 * is kept around for a short period of time, hopefully accounting for the
175 * data fetching delay. In the meantime, any operations can be executed
178 * @see VariablesView.empty
179 * @see VariablesView.commitHierarchy
181 _emptySoon(aTimeout) {
182 const prevList = this._list;
183 const currList = (this._list = this.document.createXULElement("scrollbox"));
185 this.window.setTimeout(() => {
186 prevList.removeEventListener("keydown", this._onViewKeyDown);
187 currList.addEventListener("keydown", this._onViewKeyDown);
188 currList.setAttribute("orient", "vertical");
190 this._parent.removeChild(prevList);
191 this._parent.appendChild(currList);
193 if (!this._store.length) {
194 this._appendEmptyNotice();
195 this._toggleSearchVisibility(false);
201 * Optional DevTools toolbox containing this VariablesView. Used to
202 * communicate with the inspector and highlighter.
207 * The controller for this VariablesView, if it has one.
212 * The amount of time (in milliseconds) it takes to empty this view lazily.
214 lazyEmptyDelay: LAZY_EMPTY_DELAY,
217 * Specifies if this view may be emptied lazily.
218 * @see VariablesView.prototype.empty
223 * Specifies if nodes in this view may be searched lazily.
228 * The number of elements in this container to jump when Page Up or Page Down
229 * keys are pressed. If falsy, then the page size will be based on the
232 scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT,
235 * Function called each time a variable or property's value is changed via
236 * user interaction. If null, then value changes are disabled.
238 * This property is applied recursively onto each scope in this view and
239 * affects only the child nodes when they're created.
244 * Function called each time a variable or property's name is changed via
245 * user interaction. If null, then name changes are disabled.
247 * This property is applied recursively onto each scope in this view and
248 * affects only the child nodes when they're created.
253 * Function called each time a variable or property is deleted via
254 * user interaction. If null, then deletions are disabled.
256 * This property is applied recursively onto each scope in this view and
257 * affects only the child nodes when they're created.
262 * Function called each time a property is added via user interaction. If
263 * null, then property additions are disabled.
265 * This property is applied recursively onto each scope in this view and
266 * affects only the child nodes when they're created.
271 * Specifies if after an eval or switch operation, the variable or property
272 * which has been edited should be disabled.
274 preventDisableOnChange: false,
277 * Specifies if, whenever a variable or property descriptor is available,
278 * configurable, enumerable, writable, frozen, sealed and extensible
279 * attributes should not affect presentation.
281 * This flag is applied recursively onto each scope in this view and
282 * affects only the child nodes when they're created.
284 preventDescriptorModifiers: false,
287 * The tooltip text shown on a variable or property's value if an |eval|
288 * function is provided, in order to change the variable or property's value.
290 * This flag is applied recursively onto each scope in this view and
291 * affects only the child nodes when they're created.
293 editableValueTooltip: L10N.getStr("variablesEditableValueTooltip"),
296 * The tooltip text shown on a variable or property's name if a |switch|
297 * function is provided, in order to change the variable or property's name.
299 * This flag is applied recursively onto each scope in this view and
300 * affects only the child nodes when they're created.
302 editableNameTooltip: L10N.getStr("variablesEditableNameTooltip"),
305 * The tooltip text shown on a variable or property's edit button if an
306 * |eval| function is provided and a getter/setter descriptor is present,
307 * in order to change the variable or property to a plain value.
309 * This flag is applied recursively onto each scope in this view and
310 * affects only the child nodes when they're created.
312 editButtonTooltip: L10N.getStr("variablesEditButtonTooltip"),
315 * The tooltip text shown on a variable or property's value if that value is
316 * a DOMNode that can be highlighted and selected in the inspector.
318 * This flag is applied recursively onto each scope in this view and
319 * affects only the child nodes when they're created.
321 domNodeValueTooltip: L10N.getStr("variablesDomNodeValueTooltip"),
324 * The tooltip text shown on a variable or property's delete button if a
325 * |delete| function is provided, in order to delete the variable or property.
327 * This flag is applied recursively onto each scope in this view and
328 * affects only the child nodes when they're created.
330 deleteButtonTooltip: L10N.getStr("variablesCloseButtonTooltip"),
333 * Specifies the context menu attribute set on variables and properties.
335 * This flag is applied recursively onto each scope in this view and
336 * affects only the child nodes when they're created.
341 * The separator label between the variables or properties name and value.
343 * This flag is applied recursively onto each scope in this view and
344 * affects only the child nodes when they're created.
346 separatorStr: L10N.getStr("variablesSeparatorLabel"),
349 * Specifies if enumerable properties and variables should be displayed.
350 * These variables and properties are visible by default.
351 * @param boolean aFlag
353 set enumVisible(aFlag) {
354 this._enumVisible = aFlag;
356 for (const scope of this._store) {
357 scope._enumVisible = aFlag;
362 * Specifies if non-enumerable properties and variables should be displayed.
363 * These variables and properties are visible by default.
364 * @param boolean aFlag
366 set nonEnumVisible(aFlag) {
367 this._nonEnumVisible = aFlag;
369 for (const scope of this._store) {
370 scope._nonEnumVisible = aFlag;
375 * Specifies if only enumerable properties and variables should be displayed.
376 * Both types of these variables and properties are visible by default.
377 * @param boolean aFlag
379 set onlyEnumVisible(aFlag) {
381 this.enumVisible = true;
382 this.nonEnumVisible = false;
384 this.enumVisible = true;
385 this.nonEnumVisible = true;
390 * Sets if the variable and property searching is enabled.
391 * @param boolean aFlag
393 set searchEnabled(aFlag) {
394 aFlag ? this._enableSearch() : this._disableSearch();
398 * Gets if the variable and property searching is enabled.
401 get searchEnabled() {
402 return !!this._searchboxContainer;
406 * Enables variable and property searching in this view.
407 * Use the "searchEnabled" setter to enable searching.
410 // If searching was already enabled, no need to re-enable it again.
411 if (this._searchboxContainer) {
414 const document = this.document;
415 const ownerNode = this._parent.parentNode;
417 const container = (this._searchboxContainer =
418 document.createXULElement("hbox"));
419 container.className = "devtools-toolbar devtools-input-toolbar";
421 // Hide the variables searchbox container if there are no variables or
422 // properties to display.
423 container.hidden = !this._store.length;
425 const searchbox = (this._searchboxNode = document.createElementNS(
429 searchbox.className = "variables-view-searchinput devtools-filterinput";
430 document.l10n.setAttributes(searchbox, "storage-variable-view-search-box");
431 searchbox.addEventListener("input", this._onSearchboxInput);
432 searchbox.addEventListener("keydown", this._onSearchboxKeyDown);
434 container.appendChild(searchbox);
435 ownerNode.insertBefore(container, this._parent);
439 * Disables variable and property searching in this view.
440 * Use the "searchEnabled" setter to disable searching.
443 // If searching was already disabled, no need to re-disable it again.
444 if (!this._searchboxContainer) {
447 this._searchboxContainer.remove();
448 this._searchboxNode.removeEventListener("input", this._onSearchboxInput);
449 this._searchboxNode.removeEventListener(
451 this._onSearchboxKeyDown
454 this._searchboxContainer = null;
455 this._searchboxNode = null;
459 * Sets the variables searchbox container hidden or visible.
460 * It's hidden by default.
462 * @param boolean aVisibleFlag
463 * Specifies the intended visibility.
465 _toggleSearchVisibility(aVisibleFlag) {
466 // If searching was already disabled, there's no need to hide it.
467 if (!this._searchboxContainer) {
470 this._searchboxContainer.hidden = !aVisibleFlag;
474 * Listener handling the searchbox input event.
476 _onSearchboxInput() {
477 this.scheduleSearch(this._searchboxNode.value);
481 * Listener handling the searchbox keydown event.
483 _onSearchboxKeyDown(e) {
485 case KeyCodes.DOM_VK_RETURN:
486 this._onSearchboxInput();
488 case KeyCodes.DOM_VK_ESCAPE:
489 this._searchboxNode.value = "";
490 this._onSearchboxInput();
495 * Schedules searching for variables or properties matching the query.
497 * @param string aToken
498 * The variable or property to search for.
499 * @param number aWait
500 * The amount of milliseconds to wait until draining.
502 scheduleSearch(aToken, aWait) {
503 // Check if this search operation may not be executed lazily.
504 if (!this.lazySearch) {
505 this._doSearch(aToken);
509 // The amount of time to wait for the requests to settle.
510 const maxDelay = SEARCH_ACTION_MAX_DELAY;
511 const delay = aWait === undefined ? maxDelay / aToken.length : aWait;
513 // Allow requests to settle down first.
514 setNamedTimeout("vview-search", delay, () => this._doSearch(aToken));
518 * Performs a case insensitive search for variables or properties matching
519 * the query, and hides non-matched items.
521 * If aToken is falsy, then all the scopes are unhidden and expanded,
522 * while the available variables and properties inside those scopes are
525 * @param string aToken
526 * The variable or property to search for.
529 if (this.controller && this.controller.supportsSearch()) {
530 // Retrieve the main Scope in which we add attributes
531 const scope = this._store[0]._store.get(undefined);
533 // Prune the view from old previous content
534 // so that we delete the intermediate search results
535 // we created in previous searches
536 for (const property of scope._store.values()) {
540 // Retrieve new attributes eventually hidden in splits
541 this.controller.performSearch(scope, aToken);
542 // Filter already displayed attributes
544 scope._performSearch(aToken.toLowerCase());
548 for (const scope of this._store) {
554 scope._performSearch("");
557 scope._performSearch(aToken.toLowerCase());
564 * Find the first item in the tree of visible items in this container that
565 * matches the predicate. Searches in visual order (the order seen by the
566 * user). Descends into each scope to check the scope and its children.
568 * @param function aPredicate
569 * A function that returns true when a match is found.
570 * @return Scope | Variable | Property
571 * The first visible scope, variable or property, or null if nothing
574 _findInVisibleItems(aPredicate) {
575 for (const scope of this._store) {
576 const result = scope._findInVisibleItems(aPredicate);
585 * Find the last item in the tree of visible items in this container that
586 * matches the predicate. Searches in reverse visual order (opposite of the
587 * order seen by the user). Descends into each scope to check the scope and
590 * @param function aPredicate
591 * A function that returns true when a match is found.
592 * @return Scope | Variable | Property
593 * The last visible scope, variable or property, or null if nothing
596 _findInVisibleItemsReverse(aPredicate) {
597 for (let i = this._store.length - 1; i >= 0; i--) {
598 const scope = this._store[i];
599 const result = scope._findInVisibleItemsReverse(aPredicate);
608 * Gets the scope at the specified index.
610 * @param number aIndex
613 * The scope if found, undefined if not.
615 getScopeAtIndex(aIndex) {
616 return this._store[aIndex];
620 * Recursively searches this container for the scope, variable or property
621 * displayed by the specified node.
624 * The node to search for.
625 * @return Scope | Variable | Property
626 * The matched scope, variable or property, or null if nothing is found.
628 getItemForNode(aNode) {
629 return this._itemsByElement.get(aNode);
633 * Gets the scope owning a Variable or Property.
635 * @param Variable | Property
636 * The variable or property to retrieven the owner scope for.
640 getOwnerScopeForVariableOrProperty(aItem) {
644 // If this is a Scope, return it.
645 if (!(aItem instanceof Variable)) {
648 // If this is a Variable or Property, find its owner scope.
649 if (aItem instanceof Variable && aItem.ownerView) {
650 return this.getOwnerScopeForVariableOrProperty(aItem.ownerView);
656 * Gets the parent scopes for a specified Variable or Property.
657 * The returned list will not include the owner scope.
659 * @param Variable | Property
660 * The variable or property for which to find the parent scopes.
662 * A list of parent Scopes.
664 getParentScopesForVariableOrProperty(aItem) {
665 const scope = this.getOwnerScopeForVariableOrProperty(aItem);
666 return this._store.slice(0, Math.max(this._store.indexOf(scope), 0));
670 * Gets the currently focused scope, variable or property in this view.
672 * @return Scope | Variable | Property
673 * The focused scope, variable or property, or null if nothing is found.
676 const focused = this.document.commandDispatcher.focusedElement;
677 return this.getItemForNode(focused);
681 * Focuses the first visible scope, variable, or property in this container.
683 focusFirstVisibleItem() {
684 const focusableItem = this._findInVisibleItems(item => item.focusable);
686 this._focusItem(focusableItem);
688 this._parent.scrollTop = 0;
689 this._parent.scrollLeft = 0;
693 * Focuses the last visible scope, variable, or property in this container.
695 focusLastVisibleItem() {
696 const focusableItem = this._findInVisibleItemsReverse(
697 item => item.focusable
700 this._focusItem(focusableItem);
702 this._parent.scrollTop = this._parent.scrollHeight;
703 this._parent.scrollLeft = 0;
707 * Focuses the next scope, variable or property in this view.
710 this.focusItemAtDelta(+1);
714 * Focuses the previous scope, variable or property in this view.
717 this.focusItemAtDelta(-1);
721 * Focuses another scope, variable or property in this view, based on
722 * the index distance from the currently focused item.
724 * @param number aDelta
725 * A scalar specifying by how many items should the selection change.
727 focusItemAtDelta(aDelta) {
728 const direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
729 let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
731 if (!this._focusChange(direction)) {
732 break; // Out of bounds.
738 * Focuses the next or previous scope, variable or property in this view.
740 * @param string aDirection
741 * Either "advanceFocus" or "rewindFocus".
743 * False if the focus went out of bounds and the first or last element
744 * in this view was focused instead.
746 _focusChange(aDirection) {
747 const commandDispatcher = this.document.commandDispatcher;
748 const prevFocusedElement = commandDispatcher.focusedElement;
749 let currFocusedItem = null;
752 commandDispatcher[aDirection]();
754 // Make sure the newly focused item is a part of this view.
755 // If the focus goes out of bounds, revert the previously focused item.
756 if (!(currFocusedItem = this.getFocusedItem())) {
757 prevFocusedElement.focus();
760 } while (!currFocusedItem.focusable);
762 // Focus remained within bounds.
767 * Focuses a scope, variable or property and makes sure it's visible.
769 * @param aItem Scope | Variable | Property
771 * @param boolean aCollapseFlag
772 * True if the focused item should also be collapsed.
774 * True if the item was successfully focused.
776 _focusItem(aItem, aCollapseFlag) {
777 if (!aItem.focusable) {
783 aItem._target.focus();
784 aItem._arrow.scrollIntoView({ block: "nearest" });
789 * Copy current selection to clipboard.
792 const item = this.getFocusedItem();
793 lazy.clipboardHelper.copyString(
794 item._nameString + item.separatorStr + item._valueString
799 * Listener handling a key down event on the view.
801 // eslint-disable-next-line complexity
803 const item = this.getFocusedItem();
805 // Prevent scrolling when pressing navigation keys.
806 ViewHelpers.preventScrolling(e);
809 case KeyCodes.DOM_VK_C:
810 if (e.ctrlKey || e.metaKey) {
815 case KeyCodes.DOM_VK_UP:
816 // Always rewind focus.
817 this.focusPrevItem(true);
820 case KeyCodes.DOM_VK_DOWN:
821 // Always advance focus.
822 this.focusNextItem(true);
825 case KeyCodes.DOM_VK_LEFT:
826 // Collapse scopes, variables and properties before rewinding focus.
827 if (item._isExpanded && item._isArrowVisible) {
830 this._focusItem(item.ownerView);
834 case KeyCodes.DOM_VK_RIGHT:
835 // Nothing to do here if this item never expands.
836 if (!item._isArrowVisible) {
839 // Expand scopes, variables and properties before advancing focus.
840 if (!item._isExpanded) {
843 this.focusNextItem(true);
847 case KeyCodes.DOM_VK_PAGE_UP:
848 // Rewind a certain number of elements based on the container height.
849 this.focusItemAtDelta(
851 this.scrollPageSize ||
854 this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
862 case KeyCodes.DOM_VK_PAGE_DOWN:
863 // Advance a certain number of elements based on the container height.
864 this.focusItemAtDelta(
866 this.scrollPageSize ||
869 this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
877 case KeyCodes.DOM_VK_HOME:
878 this.focusFirstVisibleItem();
881 case KeyCodes.DOM_VK_END:
882 this.focusLastVisibleItem();
885 case KeyCodes.DOM_VK_RETURN:
886 // Start editing the value or name of the Variable or Property.
887 if (item instanceof Variable) {
888 if (e.metaKey || e.altKey || e.shiftKey) {
889 item._activateNameInput();
891 item._activateValueInput();
896 case KeyCodes.DOM_VK_DELETE:
897 case KeyCodes.DOM_VK_BACK_SPACE:
898 // Delete the Variable or Property if allowed.
899 if (item instanceof Variable) {
904 case KeyCodes.DOM_VK_INSERT:
905 item._onAddProperty(e);
910 * Sets the text displayed in this container when there are no available items.
911 * @param string aValue
913 set emptyText(aValue) {
914 if (this._emptyTextNode) {
915 this._emptyTextNode.setAttribute("value", aValue);
917 this._emptyTextValue = aValue;
918 this._appendEmptyNotice();
922 * Creates and appends a label signaling that this container is empty.
924 _appendEmptyNotice() {
925 if (this._emptyTextNode || !this._emptyTextValue) {
929 const label = this.document.createXULElement("label");
930 label.className = "variables-view-empty-notice";
931 label.setAttribute("value", this._emptyTextValue);
933 this._parent.appendChild(label);
934 this._emptyTextNode = label;
938 * Removes the label signaling that this container is empty.
940 _removeEmptyNotice() {
941 if (!this._emptyTextNode) {
945 this._parent.removeChild(this._emptyTextNode);
946 this._emptyTextNode = null;
950 * Gets if all values should be aligned together.
953 get alignedValues() {
954 return this._alignedValues;
958 * Sets if all values should be aligned together.
959 * @param boolean aFlag
961 set alignedValues(aFlag) {
962 this._alignedValues = aFlag;
964 this._parent.setAttribute("aligned-values", "");
966 this._parent.removeAttribute("aligned-values");
971 * Gets if action buttons (like delete) should be placed at the beginning or
976 return this._actionsFirst;
980 * Sets if action buttons (like delete) should be placed at the beginning or
982 * @param boolean aFlag
984 set actionsFirst(aFlag) {
985 this._actionsFirst = aFlag;
987 this._parent.setAttribute("actions-first", "");
989 this._parent.removeAttribute("actions-first");
994 * Gets the parent node holding this view.
1002 * Gets the owner document holding this view.
1003 * @return HTMLDocument
1006 return this._document || (this._document = this._parent.ownerDocument);
1010 * Gets the default window holding this view.
1011 * @return nsIDOMWindow
1014 return this._window || (this._window = this.document.defaultView);
1021 _itemsByElement: null,
1022 _prevHierarchy: null,
1023 _currHierarchy: null,
1026 _nonEnumVisible: true,
1027 _alignedValues: false,
1028 _actionsFirst: false,
1032 _searchboxNode: null,
1033 _searchboxContainer: null,
1034 _emptyTextNode: null,
1035 _emptyTextValue: "",
1038 VariablesView.NON_SORTABLE_CLASSES = [
1042 "Uint8ClampedArray",
1053 * Determine whether an object's properties should be sorted based on its class.
1055 * @param string aClassName
1056 * The class of the object.
1058 VariablesView.isSortable = function (aClassName) {
1059 return !VariablesView.NON_SORTABLE_CLASSES.includes(aClassName);
1063 * Generates the string evaluated when performing simple value changes.
1065 * @param Variable | Property aItem
1066 * The current variable or property.
1067 * @param string aCurrentString
1068 * The trimmed user inputted string.
1069 * @param string aPrefix [optional]
1070 * Prefix for the symbolic name.
1072 * The string to be evaluated.
1074 VariablesView.simpleValueEvalMacro = function (
1079 return aPrefix + aItem.symbolicName + "=" + aCurrentString;
1083 * Generates the string evaluated when overriding getters and setters with
1086 * @param Property aItem
1087 * The current getter or setter property.
1088 * @param string aCurrentString
1089 * The trimmed user inputted string.
1090 * @param string aPrefix [optional]
1091 * Prefix for the symbolic name.
1093 * The string to be evaluated.
1095 VariablesView.overrideValueEvalMacro = function (
1100 const property = escapeString(aItem._nameString);
1101 const parent = aPrefix + aItem.ownerView.symbolicName || "this";
1104 "Object.defineProperty(" +
1113 ".propertyIsEnumerable(" +
1116 ", configurable: true" +
1117 ", writable: true" +
1123 * Generates the string evaluated when performing getters and setters changes.
1125 * @param Property aItem
1126 * The current getter or setter property.
1127 * @param string aCurrentString
1128 * The trimmed user inputted string.
1129 * @param string aPrefix [optional]
1130 * Prefix for the symbolic name.
1132 * The string to be evaluated.
1134 VariablesView.getterOrSetterEvalMacro = function (
1139 const type = aItem._nameString;
1140 const propertyObject = aItem.ownerView;
1141 const parentObject = propertyObject.ownerView;
1142 const property = escapeString(propertyObject._nameString);
1143 const parent = aPrefix + parentObject.symbolicName || "this";
1145 switch (aCurrentString) {
1149 const mirrorType = type == "get" ? "set" : "get";
1150 const mirrorLookup =
1151 type == "get" ? "__lookupSetter__" : "__lookupGetter__";
1153 // If the parent object will end up without any getter or setter,
1154 // morph it into a plain value.
1156 (type == "set" && propertyObject.getter.type == "undefined") ||
1157 (type == "get" && propertyObject.setter.type == "undefined")
1159 // Make sure the right getter/setter to value override macro is applied
1160 // to the target object.
1161 return propertyObject.evaluationMacro(
1168 // Construct and return the getter/setter removal evaluation string.
1169 // e.g: Object.defineProperty(foo, "bar", {
1170 // get: foo.__lookupGetter__("bar"),
1172 // enumerable: true,
1173 // configurable: true
1176 "Object.defineProperty(" +
1196 ".propertyIsEnumerable(" +
1199 ", configurable: true" +
1204 // Wrap statements inside a function declaration if not already wrapped.
1205 if (!aCurrentString.startsWith("function")) {
1206 const header = "function(" + (type == "set" ? "value" : "") + ")";
1208 // If there's a return statement explicitly written, always use the
1209 // standard function definition syntax
1210 if (aCurrentString.includes("return ")) {
1211 body = "{" + aCurrentString + "}";
1212 } else if (aCurrentString.startsWith("{")) {
1213 // If block syntax is used, use the whole string as the function body.
1214 body = aCurrentString;
1216 // Prefer an expression closure.
1217 body = "(" + aCurrentString + ")";
1219 aCurrentString = header + body;
1222 // Determine if a new getter or setter should be defined.
1224 type == "get" ? "__defineGetter__" : "__defineSetter__";
1226 // Make sure all quotes are escaped in the expression's syntax,
1228 'eval("(' + aCurrentString.replace(/"/g, "\\$&") + ')")';
1230 // Construct and return the getter/setter evaluation string.
1231 // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
1233 parent + "." + defineType + "(" + property + "," + defineFunc + ")"
1239 * Function invoked when a getter or setter is deleted.
1241 * @param Property aItem
1242 * The current getter or setter property.
1244 VariablesView.getterOrSetterDeleteCallback = function (aItem) {
1247 // Make sure the right getter/setter to value override macro is applied
1248 // to the target object.
1249 aItem.ownerView.eval(aItem, "");
1251 return true; // Don't hide the element.
1255 * A Scope is an object holding Variable instances.
1256 * Iterable via "for (let [name, variable] of instance) { }".
1258 * @param VariablesView aView
1259 * The view to contain this scope.
1260 * @param string l10nId
1261 * The scope localized string id.
1262 * @param object aFlags [optional]
1263 * Additional options or flags for this scope.
1265 function Scope(aView, l10nId, aFlags = {}) {
1266 this.ownerView = aView;
1268 this._onClick = this._onClick.bind(this);
1269 this._openEnum = this._openEnum.bind(this);
1270 this._openNonEnum = this._openNonEnum.bind(this);
1272 // Inherit properties and flags from the parent view. You can override
1273 // each of these directly onto any scope, variable or property instance.
1274 this.scrollPageSize = aView.scrollPageSize;
1275 this.eval = aView.eval;
1276 this.switch = aView.switch;
1277 this.delete = aView.delete;
1278 this.new = aView.new;
1279 this.preventDisableOnChange = aView.preventDisableOnChange;
1280 this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
1281 this.editableNameTooltip = aView.editableNameTooltip;
1282 this.editableValueTooltip = aView.editableValueTooltip;
1283 this.editButtonTooltip = aView.editButtonTooltip;
1284 this.deleteButtonTooltip = aView.deleteButtonTooltip;
1285 this.domNodeValueTooltip = aView.domNodeValueTooltip;
1286 this.contextMenuId = aView.contextMenuId;
1287 this.separatorStr = aView.separatorStr;
1289 this._init(l10nId, aFlags);
1294 * Whether this Scope should be prefetched when it is remoted.
1296 shouldPrefetch: true,
1299 * Whether this Scope should paginate its contents.
1301 allowPaginate: false,
1304 * The class name applied to this scope's target element.
1306 targetClassName: "variables-view-scope",
1309 * Create a new Variable that is a child of this Scope.
1311 * @param string aName
1312 * The name of the new Property.
1313 * @param object aDescriptor
1314 * The variable's descriptor.
1315 * @param object aOptions
1316 * Options of the form accepted by addItem.
1318 * The newly created child Variable.
1320 _createChild(aName, aDescriptor, aOptions) {
1321 return new Variable(this, aName, aDescriptor, aOptions);
1325 * Adds a child to contain any inspected properties.
1327 * @param string aName
1329 * @param object aDescriptor
1330 * Specifies the value and/or type & class of the child,
1331 * or 'get' & 'set' accessor properties. If the type is implicit,
1332 * it will be inferred from the value. If this parameter is omitted,
1333 * a property without a value will be added (useful for branch nodes).
1334 * e.g. - { value: 42 }
1336 * - { value: "nasu" }
1337 * - { value: { type: "undefined" } }
1338 * - { value: { type: "null" } }
1339 * - { value: { type: "object", class: "Object" } }
1340 * - { get: { type: "object", class: "Function" },
1341 * set: { type: "undefined" } }
1342 * @param object aOptions
1343 * Specifies some options affecting the new variable.
1344 * Recognized properties are
1345 * * boolean relaxed true if name duplicates should be allowed.
1346 * You probably shouldn't do it. Use this
1348 * * boolean internalItem true if the item is internally generated.
1349 * This is used for special variables
1350 * like <return> or <exception> and distinguishes
1351 * them from ordinary properties that happen
1352 * to have the same name
1354 * The newly created Variable instance, null if it already exists.
1356 addItem(aName, aDescriptor = {}, aOptions = {}) {
1357 const { relaxed } = aOptions;
1358 if (this._store.has(aName) && !relaxed) {
1359 return this._store.get(aName);
1362 const child = this._createChild(aName, aDescriptor, aOptions);
1363 this._store.set(aName, child);
1364 this._variablesView._itemsByElement.set(child._target, child);
1365 this._variablesView._currHierarchy.set(child.absoluteName, child);
1366 child.header = aName !== undefined;
1372 * Adds items for this variable.
1374 * @param object aItems
1375 * An object containing some { name: descriptor } data properties,
1376 * specifying the value and/or type & class of the variable,
1377 * or 'get' & 'set' accessor properties. If the type is implicit,
1378 * it will be inferred from the value.
1379 * e.g. - { someProp0: { value: 42 },
1380 * someProp1: { value: true },
1381 * someProp2: { value: "nasu" },
1382 * someProp3: { value: { type: "undefined" } },
1383 * someProp4: { value: { type: "null" } },
1384 * someProp5: { value: { type: "object", class: "Object" } },
1385 * someProp6: { get: { type: "object", class: "Function" },
1386 * set: { type: "undefined" } } }
1387 * @param object aOptions [optional]
1388 * Additional options for adding the properties. Supported options:
1389 * - sorted: true to sort all the properties before adding them
1390 * - callback: function invoked after each item is added
1392 addItems(aItems, aOptions = {}) {
1393 const names = Object.keys(aItems);
1395 // Sort all of the properties before adding them, if preferred.
1396 if (aOptions.sorted) {
1397 names.sort(this._naturalSort);
1400 // Add the properties to the current scope.
1401 for (const name of names) {
1402 const descriptor = aItems[name];
1403 const item = this.addItem(name, descriptor);
1405 if (aOptions.callback) {
1406 aOptions.callback(item, descriptor && descriptor.value);
1412 * Remove this Scope from its parent and remove all children recursively.
1415 const view = this._variablesView;
1416 view._store.splice(view._store.indexOf(this), 1);
1417 view._itemsByElement.delete(this._target);
1418 view._currHierarchy.delete(this._nameString);
1420 this._target.remove();
1422 for (const variable of this._store.values()) {
1428 * Gets the variable in this container having the specified name.
1430 * @param string aName
1431 * The name of the variable to get.
1433 * The matched variable, or null if nothing is found.
1436 return this._store.get(aName);
1440 * Recursively searches for the variable or property in this container
1441 * displayed by the specified node.
1444 * The node to search for.
1445 * @return Variable | Property
1446 * The matched variable or property, or null if nothing is found.
1449 for (const [, variable] of this._store) {
1451 if (variable._target == aNode) {
1454 match = variable.find(aNode);
1464 * Determines if this scope is a direct child of a parent variables view,
1465 * scope, variable or property.
1467 * @param VariablesView | Scope | Variable | Property
1468 * The parent to check.
1470 * True if the specified item is a direct child, false otherwise.
1472 isChildOf(aParent) {
1473 return this.ownerView == aParent;
1477 * Determines if this scope is a descendant of a parent variables view,
1478 * scope, variable or property.
1480 * @param VariablesView | Scope | Variable | Property
1481 * The parent to check.
1483 * True if the specified item is a descendant, false otherwise.
1485 isDescendantOf(aParent) {
1486 if (this.isChildOf(aParent)) {
1490 // Recurse to parent if it is a Scope, Variable, or Property.
1491 if (this.ownerView instanceof Scope) {
1492 return this.ownerView.isDescendantOf(aParent);
1502 this._target.hidden = false;
1503 this._isContentVisible = true;
1514 this._target.hidden = true;
1515 this._isContentVisible = false;
1523 * Expands the scope, showing all the added details.
1526 if (this._isExpanded || this._isLocked) {
1529 if (this._variablesView._enumVisible) {
1532 if (this._variablesView._nonEnumVisible) {
1533 Services.tm.dispatchToMainThread({ run: this._openNonEnum });
1535 this._isExpanded = true;
1537 if (this.onexpand) {
1538 // We return onexpand as it sometimes returns a promise
1539 // (up to the user of VariableView to do it)
1540 // that can indicate when the view is done expanding
1541 // and attributes are available. (Mostly used for tests)
1542 await this.onexpand(this);
1547 * Collapses the scope, hiding all the added details.
1550 if (!this._isExpanded || this._isLocked) {
1553 this._arrow.removeAttribute("open");
1554 this._enum.removeAttribute("open");
1555 this._nonenum.removeAttribute("open");
1556 this._isExpanded = false;
1558 if (this.oncollapse) {
1559 this.oncollapse(this);
1564 * Toggles between the scope's collapsed and expanded state.
1567 if (e && e.button != 0) {
1568 // Only allow left-click to trigger this event.
1573 // Make sure the scope and its contents are visibile.
1574 for (const [, variable] of this._store) {
1575 variable.header = true;
1576 variable._matched = true;
1578 if (this.ontoggle) {
1579 this.ontoggle(this);
1584 * Shows the scope's title header.
1587 if (this._isHeaderVisible || !this._nameString) {
1590 this._target.removeAttribute("untitled");
1591 this._isHeaderVisible = true;
1595 * Hides the scope's title header.
1596 * This action will automatically expand the scope.
1599 if (!this._isHeaderVisible) {
1603 this._target.setAttribute("untitled", "");
1604 this._isHeaderVisible = false;
1608 * Sort in ascending order
1609 * This only needs to compare non-numbers since it is dealing with an array
1610 * which numeric-based indices are placed in order.
1615 * -1 if a is less than b, 0 if no change in order, +1 if a is greater than 0
1617 _naturalSort(a, b) {
1618 if (isNaN(parseFloat(a)) && isNaN(parseFloat(b))) {
1619 return a < b ? -1 : 1;
1625 * Shows the scope's expand/collapse arrow.
1628 if (this._isArrowVisible) {
1631 this._arrow.removeAttribute("invisible");
1632 this._isArrowVisible = true;
1636 * Hides the scope's expand/collapse arrow.
1639 if (!this._isArrowVisible) {
1642 this._arrow.setAttribute("invisible", "");
1643 this._isArrowVisible = false;
1647 * Gets the visibility state.
1651 return this._isContentVisible;
1655 * Gets the expanded state.
1659 return this._isExpanded;
1663 * Gets the header visibility state.
1667 return this._isHeaderVisible;
1671 * Gets the twisty visibility state.
1675 return this._isArrowVisible;
1679 * Gets the expand lock state.
1683 return this._isLocked;
1687 * Sets the visibility state.
1688 * @param boolean aFlag
1690 set visible(aFlag) {
1691 aFlag ? this.show() : this.hide();
1695 * Sets the expanded state.
1696 * @param boolean aFlag
1698 set expanded(aFlag) {
1699 aFlag ? this.expand() : this.collapse();
1703 * Sets the header visibility state.
1704 * @param boolean aFlag
1707 aFlag ? this.showHeader() : this.hideHeader();
1711 * Sets the twisty visibility state.
1712 * @param boolean aFlag
1715 aFlag ? this.showArrow() : this.hideArrow();
1719 * Sets the expand lock state.
1720 * @param boolean aFlag
1723 this._isLocked = aFlag;
1727 * Specifies if this target node may be focused.
1731 // Check if this target node is actually visibile.
1733 !this._nameString ||
1734 !this._isContentVisible ||
1735 !this._isHeaderVisible ||
1740 // Check if all parent objects are expanded.
1743 // Recurse while parent is a Scope, Variable, or Property
1744 while ((item = item.ownerView) && item instanceof Scope) {
1745 if (!item._isExpanded) {
1756 this._variablesView._focusItem(this);
1760 * Adds an event listener for a certain event on this scope's title.
1761 * @param string aName
1762 * @param function aCallback
1763 * @param boolean aCapture
1765 addEventListener(aName, aCallback, aCapture) {
1766 this._title.addEventListener(aName, aCallback, aCapture);
1770 * Removes an event listener for a certain event on this scope's title.
1771 * @param string aName
1772 * @param function aCallback
1773 * @param boolean aCapture
1775 removeEventListener(aName, aCallback, aCapture) {
1776 this._title.removeEventListener(aName, aCallback, aCapture);
1780 * Gets the id associated with this item.
1784 return this._idString;
1788 * Gets the name associated with this item.
1792 return this._nameString;
1796 * Gets the displayed value for this item.
1799 get displayValue() {
1800 return this._valueString;
1804 * Gets the class names used for the displayed value.
1807 get displayValueClassName() {
1808 return this._valueClassName;
1812 * Gets the element associated with this item.
1816 return this._target;
1820 * Initializes this scope's id, view and binds event listeners.
1822 * @param string l10nId
1823 * The scope localized string id.
1824 * @param object aFlags [optional]
1825 * Additional options or flags for this scope.
1827 _init(l10nId, aFlags) {
1828 this._idString = generateId((this._nameString = l10nId));
1829 this._displayScope({
1831 targetClassName: `${this.targetClassName} ${aFlags.customClass}`,
1832 titleClassName: "devtools-toolbar",
1834 this._addEventListeners();
1835 this.parentNode.appendChild(this._target);
1839 * Creates the necessary nodes for this scope.
1841 * @param Object options
1842 * @param string options.l10nId [optional]
1843 * The scope localized string id.
1844 * @param string options.value [optional]
1845 * The scope's name. Either this or l10nId need to be passed
1846 * @param string options.targetClassName
1847 * A custom class name for this scope's target element.
1848 * @param string options.titleClassName [optional]
1849 * A custom class name for this scope's title element.
1851 _displayScope({ l10nId, value, targetClassName, titleClassName = "" }) {
1852 const document = this.document;
1854 const element = (this._target = document.createXULElement("vbox"));
1855 element.id = this._idString;
1856 element.className = targetClassName;
1858 const arrow = (this._arrow = document.createXULElement("hbox"));
1859 arrow.className = "arrow theme-twisty";
1861 const name = (this._name = document.createXULElement("label"));
1862 name.className = "name";
1864 document.l10n.setAttributes(name, l10nId);
1866 name.setAttribute("value", value);
1868 name.setAttribute("crop", "end");
1870 const title = (this._title = document.createXULElement("hbox"));
1871 title.className = "title " + titleClassName;
1872 title.setAttribute("align", "center");
1874 const enumerable = (this._enum = document.createXULElement("vbox"));
1875 const nonenum = (this._nonenum = document.createXULElement("vbox"));
1876 enumerable.className = "variables-view-element-details enum";
1877 nonenum.className = "variables-view-element-details nonenum";
1879 title.appendChild(arrow);
1880 title.appendChild(name);
1882 element.appendChild(title);
1883 element.appendChild(enumerable);
1884 element.appendChild(nonenum);
1888 * Adds the necessary event listeners for this scope.
1890 _addEventListeners() {
1891 this._title.addEventListener("mousedown", this._onClick);
1895 * The click listener for this scope's title.
1901 e.target == this._editNode ||
1902 e.target == this._deleteNode ||
1903 e.target == this._addPropertyNode
1912 * Opens the enumerable items container.
1915 this._arrow.setAttribute("open", "");
1916 this._enum.setAttribute("open", "");
1920 * Opens the non-enumerable items container.
1923 this._nonenum.setAttribute("open", "");
1927 * Specifies if enumerable properties and variables should be displayed.
1928 * @param boolean aFlag
1930 set _enumVisible(aFlag) {
1931 for (const [, variable] of this._store) {
1932 variable._enumVisible = aFlag;
1934 if (!this._isExpanded) {
1938 this._enum.setAttribute("open", "");
1940 this._enum.removeAttribute("open");
1946 * Specifies if non-enumerable properties and variables should be displayed.
1947 * @param boolean aFlag
1949 set _nonEnumVisible(aFlag) {
1950 for (const [, variable] of this._store) {
1951 variable._nonEnumVisible = aFlag;
1953 if (!this._isExpanded) {
1957 this._nonenum.setAttribute("open", "");
1959 this._nonenum.removeAttribute("open");
1965 * Performs a case insensitive search for variables or properties matching
1966 * the query, and hides non-matched items.
1968 * @param string aLowerCaseQuery
1969 * The lowercased name of the variable or property to search for.
1971 _performSearch(aLowerCaseQuery) {
1972 for (let [, variable] of this._store) {
1973 const currentObject = variable;
1974 const lowerCaseName = variable._nameString.toLowerCase();
1975 const lowerCaseValue = variable._valueString.toLowerCase();
1977 // Non-matched variables or properties require a corresponding attribute.
1979 !lowerCaseName.includes(aLowerCaseQuery) &&
1980 !lowerCaseValue.includes(aLowerCaseQuery)
1982 variable._matched = false;
1984 // Variable or property is matched.
1985 variable._matched = true;
1987 // If the variable was ever expanded, there's a possibility it may
1988 // contain some matched properties, so make sure they're visible
1989 // ("expand downwards").
1990 if (variable._store.size) {
1994 // If the variable is contained in another Scope, Variable, or Property,
1995 // the parent may not be a match, thus hidden. It should be visible
1996 // ("expand upwards").
1997 while ((variable = variable.ownerView) && variable instanceof Scope) {
1998 variable._matched = true;
2003 // Proceed with the search recursively inside this variable or property.
2005 currentObject._store.size ||
2006 currentObject.getter ||
2007 currentObject.setter
2009 currentObject._performSearch(aLowerCaseQuery);
2015 * Sets if this object instance is a matched or non-matched item.
2016 * @param boolean aStatus
2018 set _matched(aStatus) {
2019 if (this._isMatch == aStatus) {
2023 this._isMatch = true;
2024 this.target.removeAttribute("unmatched");
2026 this._isMatch = false;
2027 this.target.setAttribute("unmatched", "");
2032 * Find the first item in the tree of visible items in this item that matches
2033 * the predicate. Searches in visual order (the order seen by the user).
2034 * Tests itself, then descends into first the enumerable children and then
2035 * the non-enumerable children (since they are presented in separate groups).
2037 * @param function aPredicate
2038 * A function that returns true when a match is found.
2039 * @return Scope | Variable | Property
2040 * The first visible scope, variable or property, or null if nothing
2043 _findInVisibleItems(aPredicate) {
2044 if (aPredicate(this)) {
2048 if (this._isExpanded) {
2049 if (this._variablesView._enumVisible) {
2050 for (const item of this._enumItems) {
2051 const result = item._findInVisibleItems(aPredicate);
2058 if (this._variablesView._nonEnumVisible) {
2059 for (const item of this._nonEnumItems) {
2060 const result = item._findInVisibleItems(aPredicate);
2072 * Find the last item in the tree of visible items in this item that matches
2073 * the predicate. Searches in reverse visual order (opposite of the order
2074 * seen by the user). Descends into first the non-enumerable children, then
2075 * the enumerable children (since they are presented in separate groups), and
2076 * finally tests itself.
2078 * @param function aPredicate
2079 * A function that returns true when a match is found.
2080 * @return Scope | Variable | Property
2081 * The last visible scope, variable or property, or null if nothing
2084 _findInVisibleItemsReverse(aPredicate) {
2085 if (this._isExpanded) {
2086 if (this._variablesView._nonEnumVisible) {
2087 for (let i = this._nonEnumItems.length - 1; i >= 0; i--) {
2088 const item = this._nonEnumItems[i];
2089 const result = item._findInVisibleItemsReverse(aPredicate);
2096 if (this._variablesView._enumVisible) {
2097 for (let i = this._enumItems.length - 1; i >= 0; i--) {
2098 const item = this._enumItems[i];
2099 const result = item._findInVisibleItemsReverse(aPredicate);
2107 if (aPredicate(this)) {
2115 * Gets top level variables view instance.
2116 * @return VariablesView
2118 get _variablesView() {
2121 (this._topView = (() => {
2122 let parentView = this.ownerView;
2125 while ((topView = parentView.ownerView)) {
2126 parentView = topView;
2134 * Gets the parent node holding this scope.
2138 return this.ownerView._list;
2142 * Gets the owner document holding this scope.
2143 * @return HTMLDocument
2146 return this._document || (this._document = this.ownerView.document);
2150 * Gets the default window holding this scope.
2151 * @return nsIDOMWindow
2154 return this._window || (this._window = this.ownerView.window);
2166 preventDisableOnChange: false,
2167 preventDescriptorModifiers: false,
2169 editableNameTooltip: "",
2170 editableValueTooltip: "",
2171 editButtonTooltip: "",
2172 deleteButtonTooltip: "",
2173 domNodeValueTooltip: "",
2179 _nonEnumItems: null,
2184 _isContentVisible: true,
2185 _isHeaderVisible: true,
2186 _isArrowVisible: true,
2198 // Creating maps and arrays thousands of times for variables or properties
2199 // with a large number of children fills up a lot of memory. Make sure
2200 // these are instantiated only if needed.
2201 DevToolsUtils.defineLazyPrototypeGetter(
2206 DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array);
2207 DevToolsUtils.defineLazyPrototypeGetter(
2214 * A Variable is a Scope holding Property instances.
2215 * Iterable via "for (let [name, property] of instance) { }".
2217 * @param Scope aScope
2218 * The scope to contain this variable.
2219 * @param string aName
2220 * The variable's name.
2221 * @param object aDescriptor
2222 * The variable's descriptor.
2223 * @param object aOptions
2224 * Options of the form accepted by Scope.addItem
2226 function Variable(aScope, aName, aDescriptor, aOptions) {
2227 this._setTooltips = this._setTooltips.bind(this);
2228 this._activateNameInput = this._activateNameInput.bind(this);
2229 this._activateValueInput = this._activateValueInput.bind(this);
2230 this.openNodeInInspector = this.openNodeInInspector.bind(this);
2231 this.highlightDomNode = this.highlightDomNode.bind(this);
2232 this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
2233 this._internalItem = aOptions.internalItem;
2235 // Treat safe getter descriptors as descriptors with a value.
2236 if ("getterValue" in aDescriptor) {
2237 aDescriptor.value = aDescriptor.getterValue;
2238 delete aDescriptor.get;
2239 delete aDescriptor.set;
2242 Scope.call(this, aScope, aName, (this._initialDescriptor = aDescriptor));
2243 this.setGrip(aDescriptor.value);
2246 Variable.prototype = extend(Scope.prototype, {
2248 * Whether this Variable should be prefetched when it is remoted.
2250 get shouldPrefetch() {
2251 return this.name == "window" || this.name == "this";
2255 * Whether this Variable should paginate its contents.
2257 get allowPaginate() {
2258 return this.name != "window" && this.name != "this";
2262 * The class name applied to this variable's target element.
2264 targetClassName: "variables-view-variable variable-or-property",
2267 * Create a new Property that is a child of Variable.
2269 * @param string aName
2270 * The name of the new Property.
2271 * @param object aDescriptor
2272 * The property's descriptor.
2273 * @param object aOptions
2274 * Options of the form accepted by Scope.addItem
2276 * The newly created child Property.
2278 _createChild(aName, aDescriptor, aOptions) {
2279 return new Property(this, aName, aDescriptor, aOptions);
2283 * Remove this Variable from its parent and remove all children recursively.
2286 if (this._linkedToInspector) {
2287 this.unhighlightDomNode();
2288 this._valueLabel.removeEventListener("mouseover", this.highlightDomNode);
2289 this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode);
2290 this._openInspectorNode.removeEventListener(
2292 this.openNodeInInspector
2296 this.ownerView._store.delete(this._nameString);
2297 this._variablesView._itemsByElement.delete(this._target);
2298 this._variablesView._currHierarchy.delete(this.absoluteName);
2300 this._target.remove();
2302 for (const property of this._store.values()) {
2308 * Populates this variable to contain all the properties of an object.
2310 * @param object aObject
2311 * The raw object you want to display.
2312 * @param object aOptions [optional]
2313 * Additional options for adding the properties. Supported options:
2314 * - sorted: true to sort all the properties before adding them
2315 * - expanded: true to expand all the properties after adding them
2317 populate(aObject, aOptions = {}) {
2318 // Retrieve the properties only once.
2319 if (this._fetched) {
2322 this._fetched = true;
2324 const propertyNames = Object.getOwnPropertyNames(aObject);
2325 const prototype = Object.getPrototypeOf(aObject);
2327 // Sort all of the properties before adding them, if preferred.
2328 if (aOptions.sorted) {
2329 propertyNames.sort(this._naturalSort);
2332 // Add all the variable properties.
2333 for (const name of propertyNames) {
2334 const descriptor = Object.getOwnPropertyDescriptor(aObject, name);
2335 if (descriptor.get || descriptor.set) {
2336 const prop = this._addRawNonValueProperty(name, descriptor);
2337 if (aOptions.expanded) {
2338 prop.expanded = true;
2341 const prop = this._addRawValueProperty(name, descriptor, aObject[name]);
2342 if (aOptions.expanded) {
2343 prop.expanded = true;
2347 // Add the variable's __proto__.
2349 this._addRawValueProperty("__proto__", {}, prototype);
2354 * Populates a specific variable or property instance to contain all the
2355 * properties of an object
2357 * @param Variable | Property aVar
2358 * The target variable to populate.
2359 * @param object aObject [optional]
2360 * The raw object you want to display. If unspecified, the object is
2361 * assumed to be defined in a _sourceValue property on the target.
2363 _populateTarget(aVar, aObject = aVar._sourceValue) {
2364 aVar.populate(aObject);
2368 * Adds a property for this variable based on a raw value descriptor.
2370 * @param string aName
2371 * The property's name.
2372 * @param object aDescriptor
2373 * Specifies the exact property descriptor as returned by a call to
2374 * Object.getOwnPropertyDescriptor.
2375 * @param object aValue
2376 * The raw property value you want to display.
2378 * The newly added property instance.
2380 _addRawValueProperty(aName, aDescriptor, aValue) {
2381 const descriptor = Object.create(aDescriptor);
2382 descriptor.value = VariablesView.getGrip(aValue);
2384 const propertyItem = this.addItem(aName, descriptor);
2385 propertyItem._sourceValue = aValue;
2387 // Add an 'onexpand' callback for the property, lazily handling
2388 // the addition of new child properties.
2389 if (!VariablesView.isPrimitive(descriptor)) {
2390 propertyItem.onexpand = this._populateTarget;
2392 return propertyItem;
2396 * Adds a property for this variable based on a getter/setter descriptor.
2398 * @param string aName
2399 * The property's name.
2400 * @param object aDescriptor
2401 * Specifies the exact property descriptor as returned by a call to
2402 * Object.getOwnPropertyDescriptor.
2404 * The newly added property instance.
2406 _addRawNonValueProperty(aName, aDescriptor) {
2407 const descriptor = Object.create(aDescriptor);
2408 descriptor.get = VariablesView.getGrip(aDescriptor.get);
2409 descriptor.set = VariablesView.getGrip(aDescriptor.set);
2411 return this.addItem(aName, descriptor);
2415 * Gets this variable's path to the topmost scope in the form of a string
2416 * meant for use via eval() or a similar approach.
2417 * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
2420 get symbolicName() {
2421 return this._nameString || "";
2425 * Gets full path to this variable, including name of the scope.
2428 get absoluteName() {
2429 if (this._absoluteName) {
2430 return this._absoluteName;
2433 this._absoluteName =
2434 this.ownerView._nameString + "[" + escapeString(this._nameString) + "]";
2435 return this._absoluteName;
2439 * Gets this variable's symbolic path to the topmost scope.
2441 * @see Variable._buildSymbolicPath
2443 get symbolicPath() {
2444 if (this._symbolicPath) {
2445 return this._symbolicPath;
2447 this._symbolicPath = this._buildSymbolicPath();
2448 return this._symbolicPath;
2452 * Build this variable's path to the topmost scope in form of an array of
2453 * strings, one for each segment of the path.
2454 * For example, a symbolic path may look like ["0", "foo", "bar"].
2457 _buildSymbolicPath(path = []) {
2459 path.unshift(this.name);
2460 if (this.ownerView instanceof Variable) {
2461 return this.ownerView._buildSymbolicPath(path);
2468 * Returns this variable's value from the descriptor if available.
2472 return this._initialDescriptor.value;
2476 * Returns this variable's getter from the descriptor if available.
2480 return this._initialDescriptor.get;
2484 * Returns this variable's getter from the descriptor if available.
2488 return this._initialDescriptor.set;
2492 * Sets the specific grip for this variable (applies the text content and
2493 * class name to the value label).
2495 * The grip should contain the value or the type & class, as defined in the
2496 * remote debugger protocol. For convenience, undefined and null are
2497 * both considered types.
2500 * Specifies the value and/or type & class of the variable.
2504 * - { type: "undefined" }
2505 * - { type: "null" }
2506 * - { type: "object", class: "Object" }
2509 // Don't allow displaying grip information if there's no name available
2510 // or the grip is malformed.
2512 this._nameString === undefined ||
2513 aGrip === undefined ||
2518 // Getters and setters should display grip information in sub-properties.
2519 if (this.getter || this.setter) {
2523 const prevGrip = this._valueGrip;
2525 this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
2527 this._valueGrip = aGrip;
2531 (aGrip.optimizedOut || aGrip.uninitialized || aGrip.missingArguments)
2533 if (aGrip.optimizedOut) {
2534 this._valueString = L10N.getStr("variablesViewOptimizedOut");
2535 } else if (aGrip.uninitialized) {
2536 this._valueString = L10N.getStr("variablesViewUninitialized");
2537 } else if (aGrip.missingArguments) {
2538 this._valueString = L10N.getStr("variablesViewMissingArgs");
2542 this._valueString = VariablesView.getString(aGrip, {
2546 this.eval = this.ownerView.eval;
2549 this._valueClassName = VariablesView.getClass(aGrip);
2551 this._valueLabel.classList.add(this._valueClassName);
2552 this._valueLabel.setAttribute("value", this._valueString);
2553 this._separatorLabel.hidden = false;
2555 // DOMNodes get special treatment since they can be linked to the inspector
2556 if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") {
2557 this._linkToInspector();
2562 * Marks this variable as overridden.
2564 * @param boolean aFlag
2565 * Whether this variable is overridden or not.
2567 setOverridden(aFlag) {
2569 this._target.setAttribute("overridden", "");
2571 this._target.removeAttribute("overridden");
2576 * Briefly flashes this variable.
2578 * @param number aDuration [optional]
2579 * An optional flash animation duration.
2581 flash(aDuration = ITEM_FLASH_DURATION) {
2582 const fadeInDelay = this._variablesView.lazyEmptyDelay + 1;
2583 const fadeOutDelay = fadeInDelay + aDuration;
2585 setNamedTimeout("vview-flash-in" + this.absoluteName, fadeInDelay, () =>
2586 this._target.setAttribute("changed", "")
2589 setNamedTimeout("vview-flash-out" + this.absoluteName, fadeOutDelay, () =>
2590 this._target.removeAttribute("changed")
2595 * Initializes this variable's id, view and binds event listeners.
2597 * @param string aName
2598 * The variable's name.
2601 this._idString = generateId((this._nameString = aName));
2602 this._displayScope({ value: aName, targetClassName: this.targetClassName });
2603 this._displayVariable();
2604 this._customizeVariable();
2605 this._prepareTooltips();
2606 this._setAttributes();
2607 this._addEventListeners();
2610 this._initialDescriptor.enumerable ||
2611 this._nameString == "this" ||
2614 this.ownerView._enum.appendChild(this._target);
2615 this.ownerView._enumItems.push(this);
2617 this.ownerView._nonenum.appendChild(this._target);
2618 this.ownerView._nonEnumItems.push(this);
2623 * Creates the necessary nodes for this variable.
2625 _displayVariable() {
2626 const document = this.document;
2627 const descriptor = this._initialDescriptor;
2629 const separatorLabel = (this._separatorLabel =
2630 document.createXULElement("label"));
2631 separatorLabel.className = "separator";
2632 separatorLabel.setAttribute("value", this.separatorStr + " ");
2634 const valueLabel = (this._valueLabel = document.createXULElement("label"));
2635 valueLabel.className = "value";
2636 valueLabel.setAttribute("flex", "1");
2637 valueLabel.setAttribute("crop", "center");
2639 this._title.appendChild(separatorLabel);
2640 this._title.appendChild(valueLabel);
2642 if (VariablesView.isPrimitive(descriptor)) {
2646 // If no value will be displayed, we don't need the separator.
2647 if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
2648 separatorLabel.hidden = true;
2651 // If this is a getter/setter property, create two child pseudo-properties
2652 // called "get" and "set" that display the corresponding functions.
2653 if (descriptor.get || descriptor.set) {
2654 separatorLabel.hidden = true;
2655 valueLabel.hidden = true;
2657 // Changing getter/setter names is never allowed.
2660 // Getter/setter properties require special handling when it comes to
2661 // evaluation and deletion.
2662 if (this.ownerView.eval) {
2663 this.delete = VariablesView.getterOrSetterDeleteCallback;
2664 this.evaluationMacro = VariablesView.overrideValueEvalMacro;
2666 // Deleting getters and setters individually is not allowed if no
2667 // evaluation method is provided.
2669 this.evaluationMacro = null;
2672 const getter = this.addItem("get", { value: descriptor.get });
2673 const setter = this.addItem("set", { value: descriptor.set });
2674 getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
2675 setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
2684 * Adds specific nodes for this variable based on custom flags.
2686 _customizeVariable() {
2687 const ownerView = this.ownerView;
2688 const descriptor = this._initialDescriptor;
2690 if ((ownerView.eval && this.getter) || this.setter) {
2691 const editNode = (this._editNode =
2692 this.document.createXULElement("toolbarbutton"));
2693 editNode.className = "variables-view-edit";
2694 editNode.addEventListener("mousedown", this._onEdit.bind(this));
2695 this._title.insertBefore(editNode, this._spacer);
2698 if (ownerView.delete) {
2699 const deleteNode = (this._deleteNode =
2700 this.document.createXULElement("toolbarbutton"));
2701 deleteNode.className = "variables-view-delete";
2702 deleteNode.addEventListener("click", this._onDelete.bind(this));
2703 this._title.appendChild(deleteNode);
2706 if (ownerView.new) {
2707 const addPropertyNode = (this._addPropertyNode =
2708 this.document.createXULElement("toolbarbutton"));
2709 addPropertyNode.className = "variables-view-add-property";
2710 addPropertyNode.addEventListener(
2712 this._onAddProperty.bind(this)
2714 this._title.appendChild(addPropertyNode);
2716 // Can't add properties to primitive values, hide the node in those cases.
2717 if (VariablesView.isPrimitive(descriptor)) {
2718 addPropertyNode.setAttribute("invisible", "");
2722 if (ownerView.contextMenuId) {
2723 this._title.setAttribute("context", ownerView.contextMenuId);
2726 if (ownerView.preventDescriptorModifiers) {
2730 if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
2731 const nonWritableIcon = this.document.createXULElement("hbox");
2732 nonWritableIcon.className = "variable-or-property-non-writable-icon";
2733 nonWritableIcon.setAttribute("optional-visibility", "");
2734 this._title.appendChild(nonWritableIcon);
2736 if (descriptor.value && typeof descriptor.value == "object") {
2737 if (descriptor.value.frozen) {
2738 const frozenLabel = this.document.createXULElement("label");
2739 frozenLabel.className = "variable-or-property-frozen-label";
2740 frozenLabel.setAttribute("optional-visibility", "");
2741 frozenLabel.setAttribute("value", "F");
2742 this._title.appendChild(frozenLabel);
2744 if (descriptor.value.sealed) {
2745 const sealedLabel = this.document.createXULElement("label");
2746 sealedLabel.className = "variable-or-property-sealed-label";
2747 sealedLabel.setAttribute("optional-visibility", "");
2748 sealedLabel.setAttribute("value", "S");
2749 this._title.appendChild(sealedLabel);
2751 if (!descriptor.value.extensible) {
2752 const nonExtensibleLabel = this.document.createXULElement("label");
2753 nonExtensibleLabel.className =
2754 "variable-or-property-non-extensible-label";
2755 nonExtensibleLabel.setAttribute("optional-visibility", "");
2756 nonExtensibleLabel.setAttribute("value", "N");
2757 this._title.appendChild(nonExtensibleLabel);
2763 * Prepares all tooltips for this variable.
2765 _prepareTooltips() {
2766 this._target.addEventListener("mouseover", this._setTooltips);
2770 * Sets all tooltips for this variable.
2773 this._target.removeEventListener("mouseover", this._setTooltips);
2775 const ownerView = this.ownerView;
2776 if (ownerView.preventDescriptorModifiers) {
2780 const tooltip = this.document.createXULElement("tooltip");
2781 tooltip.id = "tooltip-" + this._idString;
2782 tooltip.setAttribute("orient", "horizontal");
2795 for (const type of labels) {
2796 const labelElement = this.document.createXULElement("label");
2797 labelElement.className = type;
2798 labelElement.setAttribute("value", L10N.getStr(type + "Tooltip"));
2799 tooltip.appendChild(labelElement);
2802 this._target.appendChild(tooltip);
2803 this._target.setAttribute("tooltip", tooltip.id);
2805 if (this._editNode && ownerView.eval) {
2806 this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
2808 if (this._openInspectorNode && this._linkedToInspector) {
2809 this._openInspectorNode.setAttribute(
2811 this.ownerView.domNodeValueTooltip
2814 if (this._valueLabel && ownerView.eval) {
2815 this._valueLabel.setAttribute(
2817 ownerView.editableValueTooltip
2820 if (this._name && ownerView.switch) {
2821 this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip);
2823 if (this._deleteNode && ownerView.delete) {
2824 this._deleteNode.setAttribute(
2826 ownerView.deleteButtonTooltip
2832 * Get the parent variablesview toolbox, if any.
2835 return this._variablesView.toolbox;
2839 * Checks if this variable is a DOMNode and is part of a variablesview that
2840 * has been linked to the toolbox, so that highlighting and jumping to the
2841 * inspector can be done.
2843 _isLinkableToInspector() {
2845 this._valueGrip && this._valueGrip.preview.kind === "DOMNode";
2846 const hasBeenLinked = this._linkedToInspector;
2847 const hasToolbox = !!this.toolbox;
2849 return isDomNode && !hasBeenLinked && hasToolbox;
2853 * If the variable is a DOMNode, and if a toolbox is set, then link it to the
2854 * inspector (highlight on hover, and jump to markup-view on click)
2856 _linkToInspector() {
2857 if (!this._isLinkableToInspector()) {
2861 // Listen to value mouseover/click events to highlight and jump
2862 this._valueLabel.addEventListener("mouseover", this.highlightDomNode);
2863 this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode);
2865 // Add a button to open the node in the inspector
2866 this._openInspectorNode = this.document.createXULElement("toolbarbutton");
2867 this._openInspectorNode.className = "variables-view-open-inspector";
2868 this._openInspectorNode.addEventListener(
2870 this.openNodeInInspector
2872 this._title.appendChild(this._openInspectorNode);
2874 this._linkedToInspector = true;
2878 * In case this variable is a DOMNode and part of a variablesview that has been
2879 * linked to the toolbox's inspector, then select the corresponding node in
2880 * the inspector, and switch the inspector tool in the toolbox
2881 * @return a promise that resolves when the node is selected and the inspector
2882 * has been switched to and is ready
2884 openNodeInInspector(event) {
2885 if (!this.toolbox) {
2886 return Promise.reject(new Error("Toolbox not available"));
2889 event && event.stopPropagation();
2891 return async function () {
2892 let nodeFront = this._nodeFront;
2894 const inspectorFront = await this.toolbox.target.getFront("inspector");
2895 nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(
2901 await this.toolbox.selectTool("inspector");
2903 const inspectorReady = new Promise(resolve => {
2904 this.toolbox.getPanel("inspector").once("inspector-updated", resolve);
2907 await this.toolbox.selection.setNodeFront(nodeFront, {
2908 reason: "variables-view",
2910 await inspectorReady;
2916 * In case this variable is a DOMNode and part of a variablesview that has been
2917 * linked to the toolbox's inspector, then highlight the corresponding node
2919 async highlightDomNode() {
2920 if (!this.toolbox) {
2924 if (!this._nodeFront) {
2925 const inspectorFront = await this.toolbox.target.getFront("inspector");
2926 this._nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(
2931 await this.toolbox.getHighlighter().highlight(this._nodeFront);
2935 * Unhighlight a previously highlit node
2936 * @see highlightDomNode
2938 unhighlightDomNode() {
2939 if (!this.toolbox) {
2943 this.toolbox.getHighlighter().unhighlight();
2947 * Sets a variable's configurable, enumerable and writable attributes,
2948 * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__'
2951 // eslint-disable-next-line complexity
2953 const ownerView = this.ownerView;
2954 if (ownerView.preventDescriptorModifiers) {
2958 const descriptor = this._initialDescriptor;
2959 const target = this._target;
2960 const name = this._nameString;
2962 if (ownerView.eval) {
2963 target.setAttribute("editable", "");
2966 if (!descriptor.configurable) {
2967 target.setAttribute("non-configurable", "");
2969 if (!descriptor.enumerable) {
2970 target.setAttribute("non-enumerable", "");
2972 if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
2973 target.setAttribute("non-writable", "");
2976 if (descriptor.value && typeof descriptor.value == "object") {
2977 if (descriptor.value.frozen) {
2978 target.setAttribute("frozen", "");
2980 if (descriptor.value.sealed) {
2981 target.setAttribute("sealed", "");
2983 if (!descriptor.value.extensible) {
2984 target.setAttribute("non-extensible", "");
2988 if (descriptor && "getterValue" in descriptor) {
2989 target.setAttribute("safe-getter", "");
2992 if (name == "this") {
2993 target.setAttribute("self", "");
2994 } else if (this._internalItem && name == "<exception>") {
2995 target.setAttribute("exception", "");
2996 target.setAttribute("pseudo-item", "");
2997 } else if (this._internalItem && name == "<return>") {
2998 target.setAttribute("return", "");
2999 target.setAttribute("pseudo-item", "");
3000 } else if (name == "__proto__") {
3001 target.setAttribute("proto", "");
3002 target.setAttribute("pseudo-item", "");
3005 if (!Object.keys(descriptor).length) {
3006 target.setAttribute("pseudo-item", "");
3011 * Adds the necessary event listeners for this variable.
3013 _addEventListeners() {
3014 this._name.addEventListener("dblclick", this._activateNameInput);
3015 this._valueLabel.addEventListener("mousedown", this._activateValueInput);
3016 this._title.addEventListener("mousedown", this._onClick);
3020 * Makes this variable's name editable.
3022 _activateNameInput(e) {
3023 if (!this._variablesView.alignedValues) {
3024 this._separatorLabel.hidden = true;
3025 this._valueLabel.hidden = true;
3028 EditableName.create(
3032 if (!this._variablesView.preventDisableOnChange) {
3035 this.ownerView.switch(this, aKey);
3038 if (!this._variablesView.alignedValues) {
3039 this._separatorLabel.hidden = false;
3040 this._valueLabel.hidden = false;
3049 * Makes this variable's value editable.
3051 _activateValueInput(e) {
3052 EditableValue.create(
3055 onSave: aString => {
3056 if (this._linkedToInspector) {
3057 this.unhighlightDomNode();
3059 if (!this._variablesView.preventDisableOnChange) {
3062 this.ownerView.eval(this, aString);
3070 * Disables this variable prior to a new name switch or value evaluation.
3073 // Prevent the variable from being collapsed or expanded.
3076 // Hide any nodes that may offer information about the variable.
3077 for (const node of this._title.childNodes) {
3078 node.hidden = node != this._arrow && node != this._name;
3080 this._enum.hidden = true;
3081 this._nonenum.hidden = true;
3085 * The current macro used to generate the string evaluated when performing
3086 * a variable or property value change.
3088 evaluationMacro: VariablesView.simpleValueEvalMacro,
3091 * The click listener for the edit button.
3094 if (e.button != 0) {
3099 e.stopPropagation();
3100 this._activateValueInput();
3104 * The click listener for the delete button.
3107 if ("button" in e && e.button != 0) {
3112 e.stopPropagation();
3114 if (this.ownerView.delete) {
3115 if (!this.ownerView.delete(this)) {
3122 * The click listener for the add property button.
3125 if ("button" in e && e.button != 0) {
3130 e.stopPropagation();
3132 this.expanded = true;
3134 const item = this.addItem(
3145 // Force showing the separator.
3146 item._separatorLabel.hidden = false;
3148 EditableNameAndValue.create(
3151 onSave: ([aKey, aValue]) => {
3152 if (!this._variablesView.preventDisableOnChange) {
3155 this.ownerView.new(this, aKey, aValue);
3162 _symbolicName: null,
3163 _symbolicPath: null,
3164 _absoluteName: null,
3165 _initialDescriptor: null,
3166 _separatorLabel: null,
3171 _addPropertyNode: null,
3175 _valueClassName: "",
3176 _prevExpandable: false,
3177 _prevExpanded: false,
3181 * A Property is a Variable holding additional child Property instances.
3182 * Iterable via "for (let [name, property] of instance) { }".
3184 * @param Variable aVar
3185 * The variable to contain this property.
3186 * @param string aName
3187 * The property's name.
3188 * @param object aDescriptor
3189 * The property's descriptor.
3190 * @param object aOptions
3191 * Options of the form accepted by Scope.addItem
3193 function Property(aVar, aName, aDescriptor, aOptions) {
3194 Variable.call(this, aVar, aName, aDescriptor, aOptions);
3197 Property.prototype = extend(Variable.prototype, {
3199 * The class name applied to this property's target element.
3201 targetClassName: "variables-view-property variable-or-property",
3204 * @see Variable.symbolicName
3207 get symbolicName() {
3208 if (this._symbolicName) {
3209 return this._symbolicName;
3212 this._symbolicName =
3213 this.ownerView.symbolicName + "[" + escapeString(this._nameString) + "]";
3214 return this._symbolicName;
3218 * @see Variable.absoluteName
3221 get absoluteName() {
3222 if (this._absoluteName) {
3223 return this._absoluteName;
3226 this._absoluteName =
3227 this.ownerView.absoluteName + "[" + escapeString(this._nameString) + "]";
3228 return this._absoluteName;
3233 * A generator-iterator over the VariablesView, Scopes, Variables and Properties.
3235 VariablesView.prototype[Symbol.iterator] =
3236 Scope.prototype[Symbol.iterator] =
3237 Variable.prototype[Symbol.iterator] =
3238 Property.prototype[Symbol.iterator] =
3244 * Forget everything recorded about added scopes, variables or properties.
3245 * @see VariablesView.commitHierarchy
3247 VariablesView.prototype.clearHierarchy = function () {
3248 this._prevHierarchy.clear();
3249 this._currHierarchy.clear();
3253 * Perform operations on all the VariablesView Scopes, Variables and Properties
3254 * after you've added all the items you wanted.
3256 * Calling this method is optional, and does the following:
3257 * - styles the items overridden by other items in parent scopes
3258 * - reopens the items which were previously expanded
3259 * - flashes the items whose values changed
3261 VariablesView.prototype.commitHierarchy = function () {
3262 for (const [, currItem] of this._currHierarchy) {
3263 // Avoid performing expensive operations.
3264 if (this.commitHierarchyIgnoredItems[currItem._nameString]) {
3267 const overridden = this.isOverridden(currItem);
3269 currItem.setOverridden(true);
3271 const expanded = !currItem._committed && this.wasExpanded(currItem);
3275 const changed = !currItem._committed && this.hasChanged(currItem);
3279 currItem._committed = true;
3281 if (this.oncommit) {
3282 this.oncommit(this);
3286 // Some variables are likely to contain a very large number of properties.
3287 // It would be a bad idea to re-expand them or perform expensive operations.
3288 VariablesView.prototype.commitHierarchyIgnoredItems = extend(null, {
3294 * Checks if the an item was previously expanded, if it existed in a
3295 * previous hierarchy.
3297 * @param Scope | Variable | Property aItem
3298 * The item to verify.
3300 * Whether the item was expanded.
3302 VariablesView.prototype.wasExpanded = function (aItem) {
3303 if (!(aItem instanceof Scope)) {
3306 const prevItem = this._prevHierarchy.get(
3307 aItem.absoluteName || aItem._nameString
3309 return prevItem ? prevItem._isExpanded : false;
3313 * Checks if the an item's displayed value (a representation of the grip)
3314 * has changed, if it existed in a previous hierarchy.
3316 * @param Variable | Property aItem
3317 * The item to verify.
3319 * Whether the item has changed.
3321 VariablesView.prototype.hasChanged = function (aItem) {
3322 // Only analyze Variables and Properties for displayed value changes.
3323 // Scopes are just collections of Variables and Properties and
3324 // don't have a "value", so they can't change.
3325 if (!(aItem instanceof Variable)) {
3328 const prevItem = this._prevHierarchy.get(aItem.absoluteName);
3329 return prevItem ? prevItem._valueString != aItem._valueString : false;
3333 * Checks if the an item was previously expanded, if it existed in a
3334 * previous hierarchy.
3336 * @param Scope | Variable | Property aItem
3337 * The item to verify.
3339 * Whether the item was expanded.
3341 VariablesView.prototype.isOverridden = function (aItem) {
3342 // Only analyze Variables for being overridden in different Scopes.
3343 if (!(aItem instanceof Variable) || aItem instanceof Property) {
3346 const currVariableName = aItem._nameString;
3347 const parentScopes = this.getParentScopesForVariableOrProperty(aItem);
3349 for (const otherScope of parentScopes) {
3350 for (const [otherVariableName] of otherScope) {
3351 if (otherVariableName == currVariableName) {
3360 * Returns true if the descriptor represents an undefined, null or
3363 * @param object aDescriptor
3364 * The variable's descriptor.
3366 VariablesView.isPrimitive = function (aDescriptor) {
3367 // For accessor property descriptors, the getter and setter need to be
3368 // contained in 'get' and 'set' properties.
3369 const getter = aDescriptor.get;
3370 const setter = aDescriptor.set;
3371 if (getter || setter) {
3375 // As described in the remote debugger protocol, the value grip
3376 // must be contained in a 'value' property.
3377 const grip = aDescriptor.value;
3378 if (typeof grip != "object") {
3382 // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long
3383 // strings are considered types.
3384 const type = grip.type;
3386 type == "undefined" ||
3388 type == "Infinity" ||
3389 type == "-Infinity" ||
3393 type == "longString"
3402 * Returns true if the descriptor represents an undefined value.
3404 * @param object aDescriptor
3405 * The variable's descriptor.
3407 VariablesView.isUndefined = function (aDescriptor) {
3408 // For accessor property descriptors, the getter and setter need to be
3409 // contained in 'get' and 'set' properties.
3410 const getter = aDescriptor.get;
3411 const setter = aDescriptor.set;
3413 typeof getter == "object" &&
3414 getter.type == "undefined" &&
3415 typeof setter == "object" &&
3416 setter.type == "undefined"
3421 // As described in the remote debugger protocol, the value grip
3422 // must be contained in a 'value' property.
3423 const grip = aDescriptor.value;
3424 if (typeof grip == "object" && grip.type == "undefined") {
3432 * Returns true if the descriptor represents a falsy value.
3434 * @param object aDescriptor
3435 * The variable's descriptor.
3437 VariablesView.isFalsy = function (aDescriptor) {
3438 // As described in the remote debugger protocol, the value grip
3439 // must be contained in a 'value' property.
3440 const grip = aDescriptor.value;
3441 if (typeof grip != "object") {
3445 // For convenience, undefined, null, NaN, and -0 are all considered types.
3446 const type = grip.type;
3447 if (type == "undefined" || type == "null" || type == "NaN" || type == "-0") {
3455 * Returns true if the value is an instance of Variable or Property.
3458 * The value to test.
3460 VariablesView.isVariable = function (aValue) {
3461 return aValue instanceof Variable;
3465 * Returns a standard grip for a value.
3468 * The raw value to get a grip for.
3472 VariablesView.getGrip = function (aValue) {
3473 switch (typeof aValue) {
3478 if (aValue === Infinity) {
3479 return { type: "Infinity" };
3480 } else if (aValue === -Infinity) {
3481 return { type: "-Infinity" };
3482 } else if (Number.isNaN(aValue)) {
3483 return { type: "NaN" };
3484 } else if (1 / aValue === -Infinity) {
3485 return { type: "-0" };
3489 // document.all is also "undefined"
3490 if (aValue === undefined) {
3491 return { type: "undefined" };
3495 if (aValue === null) {
3496 return { type: "null" };
3500 return { type: "object", class: getObjectClassName(aValue) };
3503 "Failed to provide a grip for value of " + typeof value + ": " + aValue
3509 // Match the function name from the result of toString() or toSource().
3512 // (function foobar(a, b) { ...
3513 // function foobar2(a) { ...
3515 const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
3518 * Helper function to deduce the name of the provided function.
3520 * @param function function
3521 * The function whose name will be returned.
3525 function getFunctionName(func) {
3532 desc = func.getOwnPropertyDescriptor("displayName");
3536 if (desc && typeof desc.value == "string") {
3542 const str = (func.toString() || func.toSource()) + "";
3543 name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1];
3552 * Get the object class name. For example, the |window| object has the Window
3553 * class name (based on [object Window]).
3555 * @param object object
3556 * The object you want to get the class name for.
3558 * The object class name.
3560 function getObjectClassName(object) {
3561 if (object === null) {
3564 if (object === undefined) {
3568 const type = typeof object;
3569 if (type != "object") {
3570 // Grip class names should start with an uppercase letter.
3571 return type.charAt(0).toUpperCase() + type.substr(1);
3577 className = ((object + "").match(/^\[object (\S+)\]$/) || [])[1];
3579 className = ((object.constructor + "").match(/^\[object (\S+)\]$/) ||
3582 if (!className && typeof object.constructor == "function") {
3583 className = getFunctionName(object.constructor);
3593 * Returns a custom formatted property string for a grip.
3596 * @see Variable.setGrip
3597 * @param object aOptions
3599 * - concise: boolean that tells you want a concisely formatted string.
3600 * - noStringQuotes: boolean that tells to not quote strings.
3601 * - noEllipsis: boolean that tells to not add an ellipsis after the
3602 * initial text of a longString.
3604 * The formatted property string.
3606 VariablesView.getString = function (aGrip, aOptions = {}) {
3607 if (aGrip && typeof aGrip == "object") {
3608 switch (aGrip.type) {
3617 const stringifier = VariablesView.stringifiers.byType[aGrip.type];
3619 const result = stringifier(aGrip, aOptions);
3620 if (result != null) {
3625 if (aGrip.displayString) {
3626 return VariablesView.getString(aGrip.displayString, aOptions);
3629 if (aGrip.type == "object" && aOptions.concise) {
3633 return "[" + aGrip.type + " " + aGrip.class + "]";
3637 switch (typeof aGrip) {
3639 return VariablesView.stringifiers.byType.string(aGrip, aOptions);
3641 return aGrip ? "true" : "false";
3643 if (!aGrip && 1 / aGrip === -Infinity) {
3653 * The VariablesView stringifiers are used by VariablesView.getString(). These
3654 * are organized by object type, object class and by object actor preview kind.
3655 * Some objects share identical ways for previews, for example Arrays, Sets and
3658 * Any stringifier function must return a string. If null is returned, * then
3659 * the default stringifier will be used. When invoked, the stringifier is
3660 * given the same two arguments as those given to VariablesView.getString().
3662 VariablesView.stringifiers = {};
3664 VariablesView.stringifiers.byType = {
3665 string(aGrip, { noStringQuotes }) {
3666 if (noStringQuotes) {
3669 return '"' + aGrip + '"';
3672 longString({ initial }, { noStringQuotes, noEllipsis }) {
3673 const ellipsis = noEllipsis ? "" : ELLIPSIS;
3674 if (noStringQuotes) {
3675 return initial + ellipsis;
3677 const result = '"' + initial + '"';
3681 return result.substr(0, result.length - 1) + ellipsis + '"';
3684 object(aGrip, aOptions) {
3685 const { preview } = aGrip;
3688 stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class];
3690 if (!stringifier && preview && preview.kind) {
3691 stringifier = VariablesView.stringifiers.byObjectKind[preview.kind];
3694 return stringifier(aGrip, aOptions);
3700 const name = aGrip.name || "";
3701 return "Symbol(" + name + ")";
3706 preview: { key, value },
3709 const keyString = VariablesView.getString(key, {
3711 noStringQuotes: true,
3713 const valueString = VariablesView.getString(value, { concise: true });
3715 return keyString + " \u2192 " + valueString;
3717 }; // VariablesView.stringifiers.byType
3719 VariablesView.stringifiers.byObjectClass = {
3720 Function(aGrip, { concise }) {
3721 // TODO: Bug 948484 - support arrow functions and ES6 generators
3723 let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
3724 name = VariablesView.getString(name, { noStringQuotes: true });
3726 // TODO: Bug 948489 - Support functions with destructured parameters and
3728 const params = aGrip.parameterNames || "";
3730 return "function " + name + "(" + params + ")";
3732 return (name || "function ") + "(" + params + ")";
3735 RegExp({ displayString }) {
3736 return VariablesView.getString(displayString, { noStringQuotes: true });
3740 if (!preview || !("timestamp" in preview)) {
3744 if (typeof preview.timestamp != "number") {
3745 return new Date(preview.timestamp).toString(); // invalid date
3748 return "Date " + new Date(preview.timestamp).toISOString();
3752 const { preview } = aGrip;
3753 if (preview === undefined) {
3757 aGrip.class + " { " + VariablesView.getString(preview.wrappedValue) + " }"
3760 }; // VariablesView.stringifiers.byObjectClass
3762 VariablesView.stringifiers.byObjectClass.Boolean =
3763 VariablesView.stringifiers.byObjectClass.Number;
3765 VariablesView.stringifiers.byObjectKind = {
3766 ArrayLike(aGrip, { concise }) {
3767 const { preview } = aGrip;
3769 return aGrip.class + "[" + preview.length + "]";
3772 if (!preview.items) {
3779 for (const item of preview.items) {
3780 if (item === null) {
3781 if (lastHole !== null) {
3782 result[lastHole] += ",";
3786 lastHole = result.length - 1;
3789 result.push(VariablesView.getString(item, { concise: true }));
3794 if (shown < preview.length) {
3795 const n = preview.length - shown;
3796 result.push(VariablesView.stringifiers._getNMoreString(n));
3797 } else if (lastHole !== null) {
3798 // make sure we have the right number of commas...
3799 result[lastHole] += ",";
3802 const prefix = aGrip.class == "Array" ? "" : aGrip.class + " ";
3803 return prefix + "[" + result.join(", ") + "]";
3806 MapLike(aGrip, { concise }) {
3807 const { preview } = aGrip;
3808 if (concise || !preview.entries) {
3810 typeof preview.size == "number" ? "[" + preview.size + "]" : "";
3811 return aGrip.class + size;
3815 for (const [key, value] of preview.entries) {
3816 const keyString = VariablesView.getString(key, {
3818 noStringQuotes: true,
3820 const valueString = VariablesView.getString(value, { concise: true });
3821 entries.push(keyString + ": " + valueString);
3824 if (typeof preview.size == "number" && preview.size > entries.length) {
3825 const n = preview.size - entries.length;
3826 entries.push(VariablesView.stringifiers._getNMoreString(n));
3829 return aGrip.class + " {" + entries.join(", ") + "}";
3832 ObjectWithText(aGrip, { concise }) {
3837 return aGrip.class + " " + VariablesView.getString(aGrip.preview.text);
3840 ObjectWithURL(aGrip, { concise }) {
3841 let result = aGrip.class;
3842 const url = aGrip.preview.url;
3843 if (!VariablesView.isFalsy({ value: url })) {
3844 result += ` \u2192 ${getSourceNames(url)[concise ? "short" : "long"]}`;
3849 // Stringifier for any kind of object.
3850 Object(aGrip, { concise }) {
3855 const { preview } = aGrip;
3858 if (aGrip.class == "Promise" && aGrip.promiseState) {
3859 const { state, value, reason } = aGrip.promiseState;
3860 props.push("<state>: " + VariablesView.getString(state));
3861 if (state == "fulfilled") {
3863 "<value>: " + VariablesView.getString(value, { concise: true })
3865 } else if (state == "rejected") {
3867 "<reason>: " + VariablesView.getString(reason, { concise: true })
3872 for (const key of Object.keys(preview.ownProperties || {})) {
3873 const value = preview.ownProperties[key];
3874 let valueString = "";
3876 valueString = "Getter";
3877 } else if (value.set) {
3878 valueString = "Setter";
3880 valueString = VariablesView.getString(value.value, { concise: true });
3882 props.push(key + ": " + valueString);
3885 for (const key of Object.keys(preview.safeGetterValues || {})) {
3886 const value = preview.safeGetterValues[key];
3887 const valueString = VariablesView.getString(value.getterValue, {
3890 props.push(key + ": " + valueString);
3893 if (!props.length) {
3897 if (preview.ownPropertiesLength) {
3898 const previewLength = Object.keys(preview.ownProperties).length;
3899 const diff = preview.ownPropertiesLength - previewLength;
3901 props.push(VariablesView.stringifiers._getNMoreString(diff));
3905 const prefix = aGrip.class != "Object" ? aGrip.class + " " : "";
3906 return prefix + "{" + props.join(", ") + "}";
3909 Error(aGrip, { concise }) {
3910 const { preview } = aGrip;
3911 const name = VariablesView.getString(preview.name, {
3912 noStringQuotes: true,
3915 return name || aGrip.class;
3921 VariablesView.getString(preview.message, { noStringQuotes: true });
3923 if (!VariablesView.isFalsy({ value: preview.stack })) {
3926 L10N.getStr("variablesViewErrorStacktrace") +
3934 DOMException(aGrip, { concise }) {
3935 const { preview } = aGrip;
3937 return preview.name || aGrip.class;
3945 VariablesView.getString(preview.message) +
3951 (+preview.result).toString(16);
3953 if (preview.filename) {
3954 msg += "\nlocation: " + preview.filename;
3955 if (preview.lineNumber) {
3956 msg += ":" + preview.lineNumber;
3963 DOMEvent(aGrip, { concise }) {
3964 const { preview } = aGrip;
3965 if (!preview.type) {
3970 return aGrip.class + " " + preview.type;
3973 let result = preview.type;
3976 preview.eventKind == "key" &&
3977 preview.modifiers &&
3978 preview.modifiers.length
3980 result += " " + preview.modifiers.join("-");
3984 if (preview.target) {
3985 const target = VariablesView.getString(preview.target, { concise: true });
3986 props.push("target: " + target);
3989 for (const prop in preview.properties) {
3990 const value = preview.properties[prop];
3992 prop + ": " + VariablesView.getString(value, { concise: true })
3996 return result + " {" + props.join(", ") + "}";
3999 DOMNode(aGrip, { concise }) {
4000 const { preview } = aGrip;
4002 switch (preview.nodeType) {
4003 case nodeConstants.DOCUMENT_NODE: {
4004 let result = aGrip.class;
4005 if (preview.location) {
4006 result += ` \u2192 ${
4007 getSourceNames(preview.location)[concise ? "short" : "long"]
4014 case nodeConstants.ATTRIBUTE_NODE: {
4015 const value = VariablesView.getString(preview.value, {
4016 noStringQuotes: true,
4018 return preview.nodeName + '="' + escapeHTML(value) + '"';
4021 case nodeConstants.TEXT_NODE:
4023 preview.nodeName + " " + VariablesView.getString(preview.textContent)
4026 case nodeConstants.COMMENT_NODE: {
4027 const comment = VariablesView.getString(preview.textContent, {
4028 noStringQuotes: true,
4030 return "<!--" + comment + "-->";
4033 case nodeConstants.DOCUMENT_FRAGMENT_NODE: {
4034 if (concise || !preview.childNodes) {
4035 return aGrip.class + "[" + preview.childNodesLength + "]";
4038 for (const node of preview.childNodes) {
4039 nodes.push(VariablesView.getString(node));
4041 if (nodes.length < preview.childNodesLength) {
4042 const n = preview.childNodesLength - nodes.length;
4043 nodes.push(VariablesView.stringifiers._getNMoreString(n));
4045 return aGrip.class + " [" + nodes.join(", ") + "]";
4048 case nodeConstants.ELEMENT_NODE: {
4049 const attrs = preview.attributes;
4052 result = "<" + preview.nodeName;
4053 for (const name in attrs) {
4054 const value = VariablesView.getString(attrs[name], {
4055 noStringQuotes: true,
4057 result += " " + name + '="' + escapeHTML(value) + '"';
4060 if (preview.attributesLength > n) {
4061 result += " " + ELLIPSIS;
4063 return result + ">";
4066 let result = "<" + preview.nodeName;
4068 result += "#" + attrs.id;
4072 result += "." + attrs.class.trim().replace(/\s+/, ".");
4074 return result + ">";
4081 }; // VariablesView.stringifiers.byObjectKind
4084 * Get the "N more…" formatted string, given an N. This is used for displaying
4085 * how many elements are not displayed in an object preview (eg. an array).
4088 * @param number aNumber
4091 VariablesView.stringifiers._getNMoreString = function (aNumber) {
4092 const str = L10N.getStr("variablesViewMoreObjects");
4093 return PluralForm.get(aNumber, str).replace("#1", aNumber);
4097 * Returns a custom class style for a grip.
4100 * @see Variable.setGrip
4102 * The custom class style.
4104 VariablesView.getClass = function (aGrip) {
4105 if (aGrip && typeof aGrip == "object") {
4106 if (aGrip.preview) {
4107 switch (aGrip.preview.kind) {
4109 return "token-domnode";
4113 switch (aGrip.type) {
4115 return "token-undefined";
4117 return "token-null";
4122 return "token-number";
4124 return "token-string";
4127 switch (typeof aGrip) {
4129 return "token-string";
4131 return "token-boolean";
4133 return "token-number";
4135 return "token-other";
4140 * A monotonically-increasing counter, that guarantees the uniqueness of scope,
4141 * variables and properties ids.
4143 * @param string aName
4144 * An optional string to prefix the id with.
4148 var generateId = (function () {
4150 return function (aName = "") {
4151 return aName.toLowerCase().trim().replace(/\s+/g, "-") + ++count;
4156 * Quote and escape a string. The result will be another string containing an
4157 * ECMAScript StringLiteral which will produce the original one when evaluated
4158 * by `eval` or similar.
4160 * @param string aString
4161 * An optional string to be escaped. If no string is passed, the function
4162 * returns an empty string.
4165 function escapeString(aString) {
4166 if (typeof aString !== "string") {
4169 // U+2028 and U+2029 are allowed in JSON but not in ECMAScript string literals.
4170 return JSON.stringify(aString)
4171 .replace(/\u2028/g, "\\u2028")
4172 .replace(/\u2029/g, "\\u2029");
4176 * Escape some HTML special characters. We do not need full HTML serialization
4177 * here, we just want to make strings safe to display in HTML attributes, for
4180 * @param string aString
4183 export function escapeHTML(aString) {
4185 .replace(/&/g, "&")
4186 .replace(/"/g, """)
4187 .replace(/</g, "<")
4188 .replace(/>/g, ">");
4192 * An Editable encapsulates the UI of an edit box that overlays a label,
4193 * allowing the user to edit the value.
4195 * @param Variable aVariable
4196 * The Variable or Property to make editable.
4197 * @param object aOptions
4199 * The callback to call with the value when editing is complete.
4201 * The callback to call when the editable is removed for any reason.
4203 function Editable(aVariable, aOptions) {
4204 this._variable = aVariable;
4205 this._onSave = aOptions.onSave;
4206 this._onCleanup = aOptions.onCleanup;
4209 Editable.create = function (aVariable, aOptions, aEvent) {
4210 const editable = new this(aVariable, aOptions);
4211 editable.activate(aEvent);
4215 Editable.prototype = {
4217 * The class name for targeting this Editable type's label element. Overridden
4218 * by inheriting classes.
4223 * Boolean indicating whether this Editable should activate. Overridden by
4224 * inheriting classes.
4226 shouldActivate: null,
4229 * The label element for this Editable. Overridden by inheriting classes.
4234 * Activate this editable by replacing the input box it overlays and
4235 * initialize the handlers.
4237 * @param Event e [optional]
4238 * Optionally, the Event object that was used to activate the Editable.
4241 if (!this.shouldActivate) {
4242 this._onCleanup && this._onCleanup();
4246 const { label } = this;
4247 const initialString = label.getAttribute("value");
4251 e.stopPropagation();
4254 // Create a texbox input element which will be shown in the current
4255 // element's specified label location.
4256 const input = (this._input = this._variable.document.createElementNS(
4260 input.className = this.className;
4261 input.setAttribute("value", initialString);
4263 // Replace the specified label with a textbox input element.
4264 label.parentNode.replaceChild(input, label);
4265 input.scrollIntoView({ block: "nearest" });
4268 // When the value is a string (displayed as "value"), then we probably want
4269 // to change it to another string in the textbox, so to avoid typing the ""
4270 // again, tackle with the selection bounds just a bit.
4271 if (initialString.match(/^".+"$/)) {
4272 input.selectionEnd--;
4273 input.selectionStart++;
4276 this._onKeydown = this._onKeydown.bind(this);
4277 this._onBlur = this._onBlur.bind(this);
4278 input.addEventListener("keydown", this._onKeydown);
4279 input.addEventListener("blur", this._onBlur);
4281 this._prevExpandable = this._variable.twisty;
4282 this._prevExpanded = this._variable.expanded;
4283 this._variable.collapse();
4284 this._variable.hideArrow();
4285 this._variable.locked = true;
4286 this._variable.editing = true;
4290 * Remove the input box and restore the Variable or Property to its previous
4294 this._input.removeEventListener("keydown", this._onKeydown);
4295 this._input.removeEventListener("blur", this.deactivate);
4296 this._input.parentNode.replaceChild(this.label, this._input);
4299 const scrollbox = this._variable._variablesView._list;
4300 scrollbox.scrollBy(-this._variable._target, 0);
4301 this._variable.locked = false;
4302 this._variable.twisty = this._prevExpandable;
4303 this._variable.expanded = this._prevExpanded;
4304 this._variable.editing = false;
4305 this._onCleanup && this._onCleanup();
4309 * Save the current value and deactivate the Editable.
4312 const initial = this.label.getAttribute("value");
4313 const current = this._input.value.trim();
4315 if (initial != current) {
4316 this._onSave(current);
4321 * Called when tab is pressed, allowing subclasses to link different
4322 * behavior to tabbing if desired.
4329 * Called when escape is pressed, indicating a cancelling of editing without
4334 this._variable.focus();
4338 * Event handler for when the input loses focus.
4345 * Event handler for when the input receives a key press.
4348 e.stopPropagation();
4350 switch (e.keyCode) {
4351 case KeyCodes.DOM_VK_TAB:
4354 case KeyCodes.DOM_VK_RETURN:
4357 case KeyCodes.DOM_VK_ESCAPE:
4365 * An Editable specific to editing the name of a Variable or Property.
4367 function EditableName(aVariable, aOptions) {
4368 Editable.call(this, aVariable, aOptions);
4371 EditableName.create = Editable.create;
4373 EditableName.prototype = extend(Editable.prototype, {
4374 className: "element-name-input",
4377 return this._variable._name;
4380 get shouldActivate() {
4381 return !!this._variable.ownerView.switch;
4386 * An Editable specific to editing the value of a Variable or Property.
4388 function EditableValue(aVariable, aOptions) {
4389 Editable.call(this, aVariable, aOptions);
4392 EditableValue.create = Editable.create;
4394 EditableValue.prototype = extend(Editable.prototype, {
4395 className: "element-value-input",
4398 return this._variable._valueLabel;
4401 get shouldActivate() {
4402 return !!this._variable.ownerView.eval;
4407 * An Editable specific to editing the key and value of a new property.
4409 function EditableNameAndValue(aVariable, aOptions) {
4410 EditableName.call(this, aVariable, aOptions);
4413 EditableNameAndValue.create = Editable.create;
4415 EditableNameAndValue.prototype = extend(EditableName.prototype, {
4417 // Hide the Variable or Property if the user presses escape.
4418 this._variable.remove();
4423 // Override _next so as to set both key and value at the same time.
4424 const key = this._input.value;
4425 this.label.setAttribute("value", key);
4427 const valueEditable = EditableValue.create(this._variable, {
4429 this._onSave([key, aValue]);
4432 valueEditable._reset = () => {
4433 this._variable.remove();
4434 valueEditable.deactivate();
4439 // Both _save and _next activate the value edit box.