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