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