Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
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, "&") |
michael@0 | 3792 | .replace(/"/g, """) |
michael@0 | 3793 | .replace(/</g, "<") |
michael@0 | 3794 | .replace(/>/g, ">"); |
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 | }); |