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