browser/devtools/shared/widgets/VariablesView.jsm

Thu, 15 Jan 2015 21:13:52 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 21:13:52 +0100
branch
TOR_BUG_9701
changeset 12
7540298fafa1
permissions
-rw-r--r--

Remove forgotten relic of ABI crash risk averse overloaded method change.

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

mercurial