Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ |
michael@0 | 2 | /* vim: set 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 | |
michael@0 | 7 | "use strict"; |
michael@0 | 8 | |
michael@0 | 9 | const {Cc, Ci, Cu} = require("chrome"); |
michael@0 | 10 | |
michael@0 | 11 | let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; |
michael@0 | 12 | |
michael@0 | 13 | loader.lazyServiceGetter(this, "clipboardHelper", |
michael@0 | 14 | "@mozilla.org/widget/clipboardhelper;1", |
michael@0 | 15 | "nsIClipboardHelper"); |
michael@0 | 16 | loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); |
michael@0 | 17 | loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise"); |
michael@0 | 18 | loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter")); |
michael@0 | 19 | loader.lazyGetter(this, "AutocompletePopup", |
michael@0 | 20 | () => require("devtools/shared/autocomplete-popup").AutocompletePopup); |
michael@0 | 21 | loader.lazyGetter(this, "ToolSidebar", |
michael@0 | 22 | () => require("devtools/framework/sidebar").ToolSidebar); |
michael@0 | 23 | loader.lazyGetter(this, "NetworkPanel", |
michael@0 | 24 | () => require("devtools/webconsole/network-panel").NetworkPanel); |
michael@0 | 25 | loader.lazyGetter(this, "ConsoleOutput", |
michael@0 | 26 | () => require("devtools/webconsole/console-output").ConsoleOutput); |
michael@0 | 27 | loader.lazyGetter(this, "Messages", |
michael@0 | 28 | () => require("devtools/webconsole/console-output").Messages); |
michael@0 | 29 | loader.lazyImporter(this, "EnvironmentClient", "resource://gre/modules/devtools/dbg-client.jsm"); |
michael@0 | 30 | loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm"); |
michael@0 | 31 | loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm"); |
michael@0 | 32 | loader.lazyImporter(this, "VariablesViewController", "resource:///modules/devtools/VariablesViewController.jsm"); |
michael@0 | 33 | loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); |
michael@0 | 34 | loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); |
michael@0 | 35 | |
michael@0 | 36 | const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; |
michael@0 | 37 | let l10n = new WebConsoleUtils.l10n(STRINGS_URI); |
michael@0 | 38 | |
michael@0 | 39 | const XHTML_NS = "http://www.w3.org/1999/xhtml"; |
michael@0 | 40 | |
michael@0 | 41 | const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent"; |
michael@0 | 42 | |
michael@0 | 43 | const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords"; |
michael@0 | 44 | |
michael@0 | 45 | const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security"; |
michael@0 | 46 | |
michael@0 | 47 | const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; |
michael@0 | 48 | |
michael@0 | 49 | const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul"; |
michael@0 | 50 | |
michael@0 | 51 | const CONSOLE_DIR_VIEW_HEIGHT = 0.6; |
michael@0 | 52 | |
michael@0 | 53 | const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"]; |
michael@0 | 54 | |
michael@0 | 55 | // The amount of time in milliseconds that we wait before performing a live |
michael@0 | 56 | // search. |
michael@0 | 57 | const SEARCH_DELAY = 200; |
michael@0 | 58 | |
michael@0 | 59 | // The number of lines that are displayed in the console output by default, for |
michael@0 | 60 | // each category. The user can change this number by adjusting the hidden |
michael@0 | 61 | // "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences. |
michael@0 | 62 | const DEFAULT_LOG_LIMIT = 200; |
michael@0 | 63 | |
michael@0 | 64 | // The various categories of messages. We start numbering at zero so we can |
michael@0 | 65 | // use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below. |
michael@0 | 66 | const CATEGORY_NETWORK = 0; |
michael@0 | 67 | const CATEGORY_CSS = 1; |
michael@0 | 68 | const CATEGORY_JS = 2; |
michael@0 | 69 | const CATEGORY_WEBDEV = 3; |
michael@0 | 70 | const CATEGORY_INPUT = 4; // always on |
michael@0 | 71 | const CATEGORY_OUTPUT = 5; // always on |
michael@0 | 72 | const CATEGORY_SECURITY = 6; |
michael@0 | 73 | |
michael@0 | 74 | // The possible message severities. As before, we start at zero so we can use |
michael@0 | 75 | // these as indexes into MESSAGE_PREFERENCE_KEYS. |
michael@0 | 76 | const SEVERITY_ERROR = 0; |
michael@0 | 77 | const SEVERITY_WARNING = 1; |
michael@0 | 78 | const SEVERITY_INFO = 2; |
michael@0 | 79 | const SEVERITY_LOG = 3; |
michael@0 | 80 | |
michael@0 | 81 | // The fragment of a CSS class name that identifies each category. |
michael@0 | 82 | const CATEGORY_CLASS_FRAGMENTS = [ |
michael@0 | 83 | "network", |
michael@0 | 84 | "cssparser", |
michael@0 | 85 | "exception", |
michael@0 | 86 | "console", |
michael@0 | 87 | "input", |
michael@0 | 88 | "output", |
michael@0 | 89 | "security", |
michael@0 | 90 | ]; |
michael@0 | 91 | |
michael@0 | 92 | // The fragment of a CSS class name that identifies each severity. |
michael@0 | 93 | const SEVERITY_CLASS_FRAGMENTS = [ |
michael@0 | 94 | "error", |
michael@0 | 95 | "warn", |
michael@0 | 96 | "info", |
michael@0 | 97 | "log", |
michael@0 | 98 | ]; |
michael@0 | 99 | |
michael@0 | 100 | // The preference keys to use for each category/severity combination, indexed |
michael@0 | 101 | // first by category (rows) and then by severity (columns). |
michael@0 | 102 | // |
michael@0 | 103 | // Most of these rather idiosyncratic names are historical and predate the |
michael@0 | 104 | // division of message type into "category" and "severity". |
michael@0 | 105 | const MESSAGE_PREFERENCE_KEYS = [ |
michael@0 | 106 | // Error Warning Info Log |
michael@0 | 107 | [ "network", "netwarn", null, "networkinfo", ], // Network |
michael@0 | 108 | [ "csserror", "cssparser", null, "csslog", ], // CSS |
michael@0 | 109 | [ "exception", "jswarn", null, "jslog", ], // JS |
michael@0 | 110 | [ "error", "warn", "info", "log", ], // Web Developer |
michael@0 | 111 | [ null, null, null, null, ], // Input |
michael@0 | 112 | [ null, null, null, null, ], // Output |
michael@0 | 113 | [ "secerror", "secwarn", null, null, ], // Security |
michael@0 | 114 | ]; |
michael@0 | 115 | |
michael@0 | 116 | // A mapping from the console API log event levels to the Web Console |
michael@0 | 117 | // severities. |
michael@0 | 118 | const LEVELS = { |
michael@0 | 119 | error: SEVERITY_ERROR, |
michael@0 | 120 | exception: SEVERITY_ERROR, |
michael@0 | 121 | assert: SEVERITY_ERROR, |
michael@0 | 122 | warn: SEVERITY_WARNING, |
michael@0 | 123 | info: SEVERITY_INFO, |
michael@0 | 124 | log: SEVERITY_LOG, |
michael@0 | 125 | trace: SEVERITY_LOG, |
michael@0 | 126 | debug: SEVERITY_LOG, |
michael@0 | 127 | dir: SEVERITY_LOG, |
michael@0 | 128 | group: SEVERITY_LOG, |
michael@0 | 129 | groupCollapsed: SEVERITY_LOG, |
michael@0 | 130 | groupEnd: SEVERITY_LOG, |
michael@0 | 131 | time: SEVERITY_LOG, |
michael@0 | 132 | timeEnd: SEVERITY_LOG, |
michael@0 | 133 | count: SEVERITY_LOG |
michael@0 | 134 | }; |
michael@0 | 135 | |
michael@0 | 136 | // The lowest HTTP response code (inclusive) that is considered an error. |
michael@0 | 137 | const MIN_HTTP_ERROR_CODE = 400; |
michael@0 | 138 | // The highest HTTP response code (inclusive) that is considered an error. |
michael@0 | 139 | const MAX_HTTP_ERROR_CODE = 599; |
michael@0 | 140 | |
michael@0 | 141 | // Constants used for defining the direction of JSTerm input history navigation. |
michael@0 | 142 | const HISTORY_BACK = -1; |
michael@0 | 143 | const HISTORY_FORWARD = 1; |
michael@0 | 144 | |
michael@0 | 145 | // The indent of a console group in pixels. |
michael@0 | 146 | const GROUP_INDENT = 12; |
michael@0 | 147 | |
michael@0 | 148 | // The number of messages to display in a single display update. If we display |
michael@0 | 149 | // too many messages at once we slow the Firefox UI too much. |
michael@0 | 150 | const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT; |
michael@0 | 151 | |
michael@0 | 152 | // The delay between display updates - tells how often we should *try* to push |
michael@0 | 153 | // new messages to screen. This value is optimistic, updates won't always |
michael@0 | 154 | // happen. Keep this low so the Web Console output feels live. |
michael@0 | 155 | const OUTPUT_INTERVAL = 50; // milliseconds |
michael@0 | 156 | |
michael@0 | 157 | // When the output queue has more than MESSAGES_IN_INTERVAL items we throttle |
michael@0 | 158 | // output updates to this number of milliseconds. So during a lot of output we |
michael@0 | 159 | // update every N milliseconds given here. |
michael@0 | 160 | const THROTTLE_UPDATES = 1000; // milliseconds |
michael@0 | 161 | |
michael@0 | 162 | // The preference prefix for all of the Web Console filters. |
michael@0 | 163 | const FILTER_PREFS_PREFIX = "devtools.webconsole.filter."; |
michael@0 | 164 | |
michael@0 | 165 | // The minimum font size. |
michael@0 | 166 | const MIN_FONT_SIZE = 10; |
michael@0 | 167 | |
michael@0 | 168 | const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout"; |
michael@0 | 169 | const PREF_PERSISTLOG = "devtools.webconsole.persistlog"; |
michael@0 | 170 | const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; |
michael@0 | 171 | |
michael@0 | 172 | /** |
michael@0 | 173 | * A WebConsoleFrame instance is an interactive console initialized *per target* |
michael@0 | 174 | * that displays console log data as well as provides an interactive terminal to |
michael@0 | 175 | * manipulate the target's document content. |
michael@0 | 176 | * |
michael@0 | 177 | * The WebConsoleFrame is responsible for the actual Web Console UI |
michael@0 | 178 | * implementation. |
michael@0 | 179 | * |
michael@0 | 180 | * @constructor |
michael@0 | 181 | * @param object aWebConsoleOwner |
michael@0 | 182 | * The WebConsole owner object. |
michael@0 | 183 | */ |
michael@0 | 184 | function WebConsoleFrame(aWebConsoleOwner) |
michael@0 | 185 | { |
michael@0 | 186 | this.owner = aWebConsoleOwner; |
michael@0 | 187 | this.hudId = this.owner.hudId; |
michael@0 | 188 | this.window = this.owner.iframeWindow; |
michael@0 | 189 | |
michael@0 | 190 | this._repeatNodes = {}; |
michael@0 | 191 | this._outputQueue = []; |
michael@0 | 192 | this._pruneCategoriesQueue = {}; |
michael@0 | 193 | this._networkRequests = {}; |
michael@0 | 194 | this.filterPrefs = {}; |
michael@0 | 195 | |
michael@0 | 196 | this.output = new ConsoleOutput(this); |
michael@0 | 197 | |
michael@0 | 198 | this._toggleFilter = this._toggleFilter.bind(this); |
michael@0 | 199 | this._onPanelSelected = this._onPanelSelected.bind(this); |
michael@0 | 200 | this._flushMessageQueue = this._flushMessageQueue.bind(this); |
michael@0 | 201 | this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this); |
michael@0 | 202 | |
michael@0 | 203 | this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
michael@0 | 204 | this._outputTimerInitialized = false; |
michael@0 | 205 | |
michael@0 | 206 | EventEmitter.decorate(this); |
michael@0 | 207 | } |
michael@0 | 208 | exports.WebConsoleFrame = WebConsoleFrame; |
michael@0 | 209 | |
michael@0 | 210 | WebConsoleFrame.prototype = { |
michael@0 | 211 | /** |
michael@0 | 212 | * The WebConsole instance that owns this frame. |
michael@0 | 213 | * @see hudservice.js::WebConsole |
michael@0 | 214 | * @type object |
michael@0 | 215 | */ |
michael@0 | 216 | owner: null, |
michael@0 | 217 | |
michael@0 | 218 | /** |
michael@0 | 219 | * Proxy between the Web Console and the remote Web Console instance. This |
michael@0 | 220 | * object holds methods used for connecting, listening and disconnecting from |
michael@0 | 221 | * the remote server, using the remote debugging protocol. |
michael@0 | 222 | * |
michael@0 | 223 | * @see WebConsoleConnectionProxy |
michael@0 | 224 | * @type object |
michael@0 | 225 | */ |
michael@0 | 226 | proxy: null, |
michael@0 | 227 | |
michael@0 | 228 | /** |
michael@0 | 229 | * Getter for the xul:popupset that holds any popups we open. |
michael@0 | 230 | * @type nsIDOMElement |
michael@0 | 231 | */ |
michael@0 | 232 | get popupset() this.owner.mainPopupSet, |
michael@0 | 233 | |
michael@0 | 234 | /** |
michael@0 | 235 | * Holds the initialization promise object. |
michael@0 | 236 | * @private |
michael@0 | 237 | * @type object |
michael@0 | 238 | */ |
michael@0 | 239 | _initDefer: null, |
michael@0 | 240 | |
michael@0 | 241 | /** |
michael@0 | 242 | * Holds the network requests currently displayed by the Web Console. Each key |
michael@0 | 243 | * represents the connection ID and the value is network request information. |
michael@0 | 244 | * @private |
michael@0 | 245 | * @type object |
michael@0 | 246 | */ |
michael@0 | 247 | _networkRequests: null, |
michael@0 | 248 | |
michael@0 | 249 | /** |
michael@0 | 250 | * Last time when we displayed any message in the output. |
michael@0 | 251 | * |
michael@0 | 252 | * @private |
michael@0 | 253 | * @type number |
michael@0 | 254 | * Timestamp in milliseconds since the Unix epoch. |
michael@0 | 255 | */ |
michael@0 | 256 | _lastOutputFlush: 0, |
michael@0 | 257 | |
michael@0 | 258 | /** |
michael@0 | 259 | * Message nodes are stored here in a queue for later display. |
michael@0 | 260 | * |
michael@0 | 261 | * @private |
michael@0 | 262 | * @type array |
michael@0 | 263 | */ |
michael@0 | 264 | _outputQueue: null, |
michael@0 | 265 | |
michael@0 | 266 | /** |
michael@0 | 267 | * Keep track of the categories we need to prune from time to time. |
michael@0 | 268 | * |
michael@0 | 269 | * @private |
michael@0 | 270 | * @type array |
michael@0 | 271 | */ |
michael@0 | 272 | _pruneCategoriesQueue: null, |
michael@0 | 273 | |
michael@0 | 274 | /** |
michael@0 | 275 | * Function invoked whenever the output queue is emptied. This is used by some |
michael@0 | 276 | * tests. |
michael@0 | 277 | * |
michael@0 | 278 | * @private |
michael@0 | 279 | * @type function |
michael@0 | 280 | */ |
michael@0 | 281 | _flushCallback: null, |
michael@0 | 282 | |
michael@0 | 283 | /** |
michael@0 | 284 | * Timer used for flushing the messages output queue. |
michael@0 | 285 | * |
michael@0 | 286 | * @private |
michael@0 | 287 | * @type nsITimer |
michael@0 | 288 | */ |
michael@0 | 289 | _outputTimer: null, |
michael@0 | 290 | _outputTimerInitialized: null, |
michael@0 | 291 | |
michael@0 | 292 | /** |
michael@0 | 293 | * Store for tracking repeated nodes. |
michael@0 | 294 | * @private |
michael@0 | 295 | * @type object |
michael@0 | 296 | */ |
michael@0 | 297 | _repeatNodes: null, |
michael@0 | 298 | |
michael@0 | 299 | /** |
michael@0 | 300 | * Preferences for filtering messages by type. |
michael@0 | 301 | * @see this._initDefaultFilterPrefs() |
michael@0 | 302 | * @type object |
michael@0 | 303 | */ |
michael@0 | 304 | filterPrefs: null, |
michael@0 | 305 | |
michael@0 | 306 | /** |
michael@0 | 307 | * Prefix used for filter preferences. |
michael@0 | 308 | * @private |
michael@0 | 309 | * @type string |
michael@0 | 310 | */ |
michael@0 | 311 | _filterPrefsPrefix: FILTER_PREFS_PREFIX, |
michael@0 | 312 | |
michael@0 | 313 | /** |
michael@0 | 314 | * The nesting depth of the currently active console group. |
michael@0 | 315 | */ |
michael@0 | 316 | groupDepth: 0, |
michael@0 | 317 | |
michael@0 | 318 | /** |
michael@0 | 319 | * The current target location. |
michael@0 | 320 | * @type string |
michael@0 | 321 | */ |
michael@0 | 322 | contentLocation: "", |
michael@0 | 323 | |
michael@0 | 324 | /** |
michael@0 | 325 | * The JSTerm object that manage the console's input. |
michael@0 | 326 | * @see JSTerm |
michael@0 | 327 | * @type object |
michael@0 | 328 | */ |
michael@0 | 329 | jsterm: null, |
michael@0 | 330 | |
michael@0 | 331 | /** |
michael@0 | 332 | * The element that holds all of the messages we display. |
michael@0 | 333 | * @type nsIDOMElement |
michael@0 | 334 | */ |
michael@0 | 335 | outputNode: null, |
michael@0 | 336 | |
michael@0 | 337 | /** |
michael@0 | 338 | * The ConsoleOutput instance that manages all output. |
michael@0 | 339 | * @type object |
michael@0 | 340 | */ |
michael@0 | 341 | output: null, |
michael@0 | 342 | |
michael@0 | 343 | /** |
michael@0 | 344 | * The input element that allows the user to filter messages by string. |
michael@0 | 345 | * @type nsIDOMElement |
michael@0 | 346 | */ |
michael@0 | 347 | filterBox: null, |
michael@0 | 348 | |
michael@0 | 349 | /** |
michael@0 | 350 | * Getter for the debugger WebConsoleClient. |
michael@0 | 351 | * @type object |
michael@0 | 352 | */ |
michael@0 | 353 | get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null, |
michael@0 | 354 | |
michael@0 | 355 | _destroyer: null, |
michael@0 | 356 | |
michael@0 | 357 | // Used in tests. |
michael@0 | 358 | _saveRequestAndResponseBodies: false, |
michael@0 | 359 | |
michael@0 | 360 | // Chevron width at the starting of Web Console's input box. |
michael@0 | 361 | _chevronWidth: 0, |
michael@0 | 362 | // Width of the monospace characters in Web Console's input box. |
michael@0 | 363 | _inputCharWidth: 0, |
michael@0 | 364 | |
michael@0 | 365 | /** |
michael@0 | 366 | * Tells whether to save the bodies of network requests and responses. |
michael@0 | 367 | * Disabled by default to save memory. |
michael@0 | 368 | * |
michael@0 | 369 | * @return boolean |
michael@0 | 370 | * The saveRequestAndResponseBodies pref value. |
michael@0 | 371 | */ |
michael@0 | 372 | getSaveRequestAndResponseBodies: |
michael@0 | 373 | function WCF_getSaveRequestAndResponseBodies() { |
michael@0 | 374 | let deferred = promise.defer(); |
michael@0 | 375 | let toGet = [ |
michael@0 | 376 | "NetworkMonitor.saveRequestAndResponseBodies" |
michael@0 | 377 | ]; |
michael@0 | 378 | |
michael@0 | 379 | // Make sure the web console client connection is established first. |
michael@0 | 380 | this.webConsoleClient.getPreferences(toGet, aResponse => { |
michael@0 | 381 | if (!aResponse.error) { |
michael@0 | 382 | this._saveRequestAndResponseBodies = aResponse.preferences[toGet[0]]; |
michael@0 | 383 | deferred.resolve(this._saveRequestAndResponseBodies); |
michael@0 | 384 | } |
michael@0 | 385 | else { |
michael@0 | 386 | deferred.reject(aResponse.error); |
michael@0 | 387 | } |
michael@0 | 388 | }); |
michael@0 | 389 | |
michael@0 | 390 | return deferred.promise; |
michael@0 | 391 | }, |
michael@0 | 392 | |
michael@0 | 393 | /** |
michael@0 | 394 | * Setter for saving of network request and response bodies. |
michael@0 | 395 | * |
michael@0 | 396 | * @param boolean aValue |
michael@0 | 397 | * The new value you want to set. |
michael@0 | 398 | */ |
michael@0 | 399 | setSaveRequestAndResponseBodies: |
michael@0 | 400 | function WCF_setSaveRequestAndResponseBodies(aValue) { |
michael@0 | 401 | if (!this.webConsoleClient) { |
michael@0 | 402 | // Don't continue if the webconsole disconnected. |
michael@0 | 403 | return promise.resolve(null); |
michael@0 | 404 | } |
michael@0 | 405 | |
michael@0 | 406 | let deferred = promise.defer(); |
michael@0 | 407 | let newValue = !!aValue; |
michael@0 | 408 | let toSet = { |
michael@0 | 409 | "NetworkMonitor.saveRequestAndResponseBodies": newValue, |
michael@0 | 410 | }; |
michael@0 | 411 | |
michael@0 | 412 | // Make sure the web console client connection is established first. |
michael@0 | 413 | this.webConsoleClient.setPreferences(toSet, aResponse => { |
michael@0 | 414 | if (!aResponse.error) { |
michael@0 | 415 | this._saveRequestAndResponseBodies = newValue; |
michael@0 | 416 | deferred.resolve(aResponse); |
michael@0 | 417 | } |
michael@0 | 418 | else { |
michael@0 | 419 | deferred.reject(aResponse.error); |
michael@0 | 420 | } |
michael@0 | 421 | }); |
michael@0 | 422 | |
michael@0 | 423 | return deferred.promise; |
michael@0 | 424 | }, |
michael@0 | 425 | |
michael@0 | 426 | /** |
michael@0 | 427 | * Getter for the persistent logging preference. |
michael@0 | 428 | * @type boolean |
michael@0 | 429 | */ |
michael@0 | 430 | get persistLog() { |
michael@0 | 431 | return Services.prefs.getBoolPref(PREF_PERSISTLOG); |
michael@0 | 432 | }, |
michael@0 | 433 | |
michael@0 | 434 | /** |
michael@0 | 435 | * Initialize the WebConsoleFrame instance. |
michael@0 | 436 | * @return object |
michael@0 | 437 | * A promise object for the initialization. |
michael@0 | 438 | */ |
michael@0 | 439 | init: function WCF_init() |
michael@0 | 440 | { |
michael@0 | 441 | this._initUI(); |
michael@0 | 442 | return this._initConnection(); |
michael@0 | 443 | }, |
michael@0 | 444 | |
michael@0 | 445 | /** |
michael@0 | 446 | * Connect to the server using the remote debugging protocol. |
michael@0 | 447 | * |
michael@0 | 448 | * @private |
michael@0 | 449 | * @return object |
michael@0 | 450 | * A promise object that is resolved/reject based on the connection |
michael@0 | 451 | * result. |
michael@0 | 452 | */ |
michael@0 | 453 | _initConnection: function WCF__initConnection() |
michael@0 | 454 | { |
michael@0 | 455 | if (this._initDefer) { |
michael@0 | 456 | return this._initDefer.promise; |
michael@0 | 457 | } |
michael@0 | 458 | |
michael@0 | 459 | this._initDefer = promise.defer(); |
michael@0 | 460 | this.proxy = new WebConsoleConnectionProxy(this, this.owner.target); |
michael@0 | 461 | |
michael@0 | 462 | this.proxy.connect().then(() => { // on success |
michael@0 | 463 | this._initDefer.resolve(this); |
michael@0 | 464 | }, (aReason) => { // on failure |
michael@0 | 465 | let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR, |
michael@0 | 466 | aReason.error + ": " + aReason.message); |
michael@0 | 467 | this.outputMessage(CATEGORY_JS, node); |
michael@0 | 468 | this._initDefer.reject(aReason); |
michael@0 | 469 | }).then(() => { |
michael@0 | 470 | let id = WebConsoleUtils.supportsString(this.hudId); |
michael@0 | 471 | Services.obs.notifyObservers(id, "web-console-created", null); |
michael@0 | 472 | }); |
michael@0 | 473 | |
michael@0 | 474 | return this._initDefer.promise; |
michael@0 | 475 | }, |
michael@0 | 476 | |
michael@0 | 477 | /** |
michael@0 | 478 | * Find the Web Console UI elements and setup event listeners as needed. |
michael@0 | 479 | * @private |
michael@0 | 480 | */ |
michael@0 | 481 | _initUI: function WCF__initUI() |
michael@0 | 482 | { |
michael@0 | 483 | this.document = this.window.document; |
michael@0 | 484 | this.rootElement = this.document.documentElement; |
michael@0 | 485 | |
michael@0 | 486 | this._initDefaultFilterPrefs(); |
michael@0 | 487 | |
michael@0 | 488 | // Register the controller to handle "select all" properly. |
michael@0 | 489 | this._commandController = new CommandController(this); |
michael@0 | 490 | this.window.controllers.insertControllerAt(0, this._commandController); |
michael@0 | 491 | |
michael@0 | 492 | this._contextMenuHandler = new ConsoleContextMenu(this); |
michael@0 | 493 | |
michael@0 | 494 | let doc = this.document; |
michael@0 | 495 | |
michael@0 | 496 | this.filterBox = doc.querySelector(".hud-filter-box"); |
michael@0 | 497 | this.outputNode = doc.getElementById("output-container"); |
michael@0 | 498 | this.completeNode = doc.querySelector(".jsterm-complete-node"); |
michael@0 | 499 | this.inputNode = doc.querySelector(".jsterm-input-node"); |
michael@0 | 500 | |
michael@0 | 501 | this._setFilterTextBoxEvents(); |
michael@0 | 502 | this._initFilterButtons(); |
michael@0 | 503 | |
michael@0 | 504 | let fontSize = this.owner._browserConsole ? |
michael@0 | 505 | Services.prefs.getIntPref("devtools.webconsole.fontSize") : 0; |
michael@0 | 506 | |
michael@0 | 507 | if (fontSize != 0) { |
michael@0 | 508 | fontSize = Math.max(MIN_FONT_SIZE, fontSize); |
michael@0 | 509 | |
michael@0 | 510 | this.outputNode.style.fontSize = fontSize + "px"; |
michael@0 | 511 | this.completeNode.style.fontSize = fontSize + "px"; |
michael@0 | 512 | this.inputNode.style.fontSize = fontSize + "px"; |
michael@0 | 513 | } |
michael@0 | 514 | |
michael@0 | 515 | if (this.owner._browserConsole) { |
michael@0 | 516 | for (let id of ["Enlarge", "Reduce", "Reset"]) { |
michael@0 | 517 | this.document.getElementById("cmd_fullZoom" + id) |
michael@0 | 518 | .removeAttribute("disabled"); |
michael@0 | 519 | } |
michael@0 | 520 | } |
michael@0 | 521 | |
michael@0 | 522 | // Update the character width and height needed for the popup offset |
michael@0 | 523 | // calculations. |
michael@0 | 524 | this._updateCharSize(); |
michael@0 | 525 | |
michael@0 | 526 | let updateSaveBodiesPrefUI = (aElement) => { |
michael@0 | 527 | this.getSaveRequestAndResponseBodies().then(aValue => { |
michael@0 | 528 | aElement.setAttribute("checked", aValue); |
michael@0 | 529 | this.emit("save-bodies-ui-toggled"); |
michael@0 | 530 | }); |
michael@0 | 531 | } |
michael@0 | 532 | |
michael@0 | 533 | let reverseSaveBodiesPref = ({ target: aElement }) => { |
michael@0 | 534 | this.getSaveRequestAndResponseBodies().then(aValue => { |
michael@0 | 535 | this.setSaveRequestAndResponseBodies(!aValue); |
michael@0 | 536 | aElement.setAttribute("checked", aValue); |
michael@0 | 537 | this.emit("save-bodies-pref-reversed"); |
michael@0 | 538 | }); |
michael@0 | 539 | } |
michael@0 | 540 | |
michael@0 | 541 | let saveBodies = doc.getElementById("saveBodies"); |
michael@0 | 542 | saveBodies.addEventListener("command", reverseSaveBodiesPref); |
michael@0 | 543 | saveBodies.disabled = !this.getFilterState("networkinfo") && |
michael@0 | 544 | !this.getFilterState("network"); |
michael@0 | 545 | |
michael@0 | 546 | let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu"); |
michael@0 | 547 | saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref); |
michael@0 | 548 | saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") && |
michael@0 | 549 | !this.getFilterState("network"); |
michael@0 | 550 | |
michael@0 | 551 | saveBodies.parentNode.addEventListener("popupshowing", () => { |
michael@0 | 552 | updateSaveBodiesPrefUI(saveBodies); |
michael@0 | 553 | saveBodies.disabled = !this.getFilterState("networkinfo") && |
michael@0 | 554 | !this.getFilterState("network"); |
michael@0 | 555 | }); |
michael@0 | 556 | |
michael@0 | 557 | saveBodiesContextMenu.parentNode.addEventListener("popupshowing", () => { |
michael@0 | 558 | updateSaveBodiesPrefUI(saveBodiesContextMenu); |
michael@0 | 559 | saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") && |
michael@0 | 560 | !this.getFilterState("network"); |
michael@0 | 561 | }); |
michael@0 | 562 | |
michael@0 | 563 | let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0]; |
michael@0 | 564 | clearButton.addEventListener("command", () => { |
michael@0 | 565 | this.owner._onClearButton(); |
michael@0 | 566 | this.jsterm.clearOutput(true); |
michael@0 | 567 | }); |
michael@0 | 568 | |
michael@0 | 569 | this.jsterm = new JSTerm(this); |
michael@0 | 570 | this.jsterm.init(); |
michael@0 | 571 | |
michael@0 | 572 | let toolbox = gDevTools.getToolbox(this.owner.target); |
michael@0 | 573 | if (toolbox) { |
michael@0 | 574 | toolbox.on("webconsole-selected", this._onPanelSelected); |
michael@0 | 575 | } |
michael@0 | 576 | |
michael@0 | 577 | /* |
michael@0 | 578 | * Focus input line whenever the output area is clicked. |
michael@0 | 579 | * Reusing _addMEssageLinkCallback since it correctly filters |
michael@0 | 580 | * drag and select events. |
michael@0 | 581 | */ |
michael@0 | 582 | this._addFocusCallback(this.outputNode, (evt) => { |
michael@0 | 583 | if ((evt.target.nodeName.toLowerCase() != "a") && |
michael@0 | 584 | (evt.target.parentNode.nodeName.toLowerCase() != "a")) { |
michael@0 | 585 | this.jsterm.inputNode.focus(); |
michael@0 | 586 | } |
michael@0 | 587 | }); |
michael@0 | 588 | |
michael@0 | 589 | // Toggle the timestamp on preference change |
michael@0 | 590 | gDevTools.on("pref-changed", this._onToolboxPrefChanged); |
michael@0 | 591 | this._onToolboxPrefChanged("pref-changed", { |
michael@0 | 592 | pref: PREF_MESSAGE_TIMESTAMP, |
michael@0 | 593 | newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP), |
michael@0 | 594 | }); |
michael@0 | 595 | |
michael@0 | 596 | // focus input node |
michael@0 | 597 | this.jsterm.inputNode.focus(); |
michael@0 | 598 | }, |
michael@0 | 599 | |
michael@0 | 600 | /** |
michael@0 | 601 | * Sets the focus to JavaScript input field when the web console tab is |
michael@0 | 602 | * selected or when there is a split console present. |
michael@0 | 603 | * @private |
michael@0 | 604 | */ |
michael@0 | 605 | _onPanelSelected: function WCF__onPanelSelected(evt, id) |
michael@0 | 606 | { |
michael@0 | 607 | this.jsterm.inputNode.focus(); |
michael@0 | 608 | }, |
michael@0 | 609 | |
michael@0 | 610 | /** |
michael@0 | 611 | * Initialize the default filter preferences. |
michael@0 | 612 | * @private |
michael@0 | 613 | */ |
michael@0 | 614 | _initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs() |
michael@0 | 615 | { |
michael@0 | 616 | let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog", |
michael@0 | 617 | "exception", "jswarn", "jslog", "error", "info", "warn", "log", |
michael@0 | 618 | "secerror", "secwarn", "netwarn"]; |
michael@0 | 619 | for (let pref of prefs) { |
michael@0 | 620 | this.filterPrefs[pref] = Services.prefs |
michael@0 | 621 | .getBoolPref(this._filterPrefsPrefix + pref); |
michael@0 | 622 | } |
michael@0 | 623 | }, |
michael@0 | 624 | |
michael@0 | 625 | /** |
michael@0 | 626 | * Attach / detach reflow listeners depending on the checked status |
michael@0 | 627 | * of the `CSS > Log` menuitem. |
michael@0 | 628 | * |
michael@0 | 629 | * @param function [aCallback=null] |
michael@0 | 630 | * Optional function to invoke when the listener has been |
michael@0 | 631 | * added/removed. |
michael@0 | 632 | * |
michael@0 | 633 | */ |
michael@0 | 634 | _updateReflowActivityListener: |
michael@0 | 635 | function WCF__updateReflowActivityListener(aCallback) |
michael@0 | 636 | { |
michael@0 | 637 | if (this.webConsoleClient) { |
michael@0 | 638 | let pref = this._filterPrefsPrefix + "csslog"; |
michael@0 | 639 | if (Services.prefs.getBoolPref(pref)) { |
michael@0 | 640 | this.webConsoleClient.startListeners(["ReflowActivity"], aCallback); |
michael@0 | 641 | } else { |
michael@0 | 642 | this.webConsoleClient.stopListeners(["ReflowActivity"], aCallback); |
michael@0 | 643 | } |
michael@0 | 644 | } |
michael@0 | 645 | }, |
michael@0 | 646 | |
michael@0 | 647 | /** |
michael@0 | 648 | * Sets the events for the filter input field. |
michael@0 | 649 | * @private |
michael@0 | 650 | */ |
michael@0 | 651 | _setFilterTextBoxEvents: function WCF__setFilterTextBoxEvents() |
michael@0 | 652 | { |
michael@0 | 653 | let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
michael@0 | 654 | let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this); |
michael@0 | 655 | |
michael@0 | 656 | let onChange = function _onChange() { |
michael@0 | 657 | // To improve responsiveness, we let the user finish typing before we |
michael@0 | 658 | // perform the search. |
michael@0 | 659 | timer.cancel(); |
michael@0 | 660 | timer.initWithCallback(timerEvent, SEARCH_DELAY, |
michael@0 | 661 | Ci.nsITimer.TYPE_ONE_SHOT); |
michael@0 | 662 | }; |
michael@0 | 663 | |
michael@0 | 664 | this.filterBox.addEventListener("command", onChange, false); |
michael@0 | 665 | this.filterBox.addEventListener("input", onChange, false); |
michael@0 | 666 | }, |
michael@0 | 667 | |
michael@0 | 668 | /** |
michael@0 | 669 | * Creates one of the filter buttons on the toolbar. |
michael@0 | 670 | * |
michael@0 | 671 | * @private |
michael@0 | 672 | * @param nsIDOMNode aParent |
michael@0 | 673 | * The node to which the filter button should be appended. |
michael@0 | 674 | * @param object aDescriptor |
michael@0 | 675 | * A descriptor that contains info about the button. Contains "name", |
michael@0 | 676 | * "category", and "prefKey" properties, and optionally a "severities" |
michael@0 | 677 | * property. |
michael@0 | 678 | */ |
michael@0 | 679 | _initFilterButtons: function WCF__initFilterButtons() |
michael@0 | 680 | { |
michael@0 | 681 | let categories = this.document |
michael@0 | 682 | .querySelectorAll(".webconsole-filter-button[category]"); |
michael@0 | 683 | Array.forEach(categories, function(aButton) { |
michael@0 | 684 | aButton.addEventListener("click", this._toggleFilter, false); |
michael@0 | 685 | |
michael@0 | 686 | let someChecked = false; |
michael@0 | 687 | let severities = aButton.querySelectorAll("menuitem[prefKey]"); |
michael@0 | 688 | Array.forEach(severities, function(aMenuItem) { |
michael@0 | 689 | aMenuItem.addEventListener("command", this._toggleFilter, false); |
michael@0 | 690 | |
michael@0 | 691 | let prefKey = aMenuItem.getAttribute("prefKey"); |
michael@0 | 692 | let checked = this.filterPrefs[prefKey]; |
michael@0 | 693 | aMenuItem.setAttribute("checked", checked); |
michael@0 | 694 | someChecked = someChecked || checked; |
michael@0 | 695 | }, this); |
michael@0 | 696 | |
michael@0 | 697 | aButton.setAttribute("checked", someChecked); |
michael@0 | 698 | }, this); |
michael@0 | 699 | |
michael@0 | 700 | if (!this.owner._browserConsole) { |
michael@0 | 701 | // The Browser Console displays nsIConsoleMessages which are messages that |
michael@0 | 702 | // end up in the JS category, but they are not errors or warnings, they |
michael@0 | 703 | // are just log messages. The Web Console does not show such messages. |
michael@0 | 704 | let jslog = this.document.querySelector("menuitem[prefKey=jslog]"); |
michael@0 | 705 | jslog.hidden = true; |
michael@0 | 706 | } |
michael@0 | 707 | |
michael@0 | 708 | if (Services.appinfo.OS == "Darwin") { |
michael@0 | 709 | let net = this.document.querySelector("toolbarbutton[category=net]"); |
michael@0 | 710 | let accesskey = net.getAttribute("accesskeyMacOSX"); |
michael@0 | 711 | net.setAttribute("accesskey", accesskey); |
michael@0 | 712 | |
michael@0 | 713 | let logging = this.document.querySelector("toolbarbutton[category=logging]"); |
michael@0 | 714 | logging.removeAttribute("accesskey"); |
michael@0 | 715 | } |
michael@0 | 716 | }, |
michael@0 | 717 | |
michael@0 | 718 | /** |
michael@0 | 719 | * Increase, decrease or reset the font size. |
michael@0 | 720 | * |
michael@0 | 721 | * @param string size |
michael@0 | 722 | * The size of the font change. Accepted values are "+" and "-". |
michael@0 | 723 | * An unmatched size assumes a font reset. |
michael@0 | 724 | */ |
michael@0 | 725 | changeFontSize: function WCF_changeFontSize(aSize) |
michael@0 | 726 | { |
michael@0 | 727 | let fontSize = this.window |
michael@0 | 728 | .getComputedStyle(this.outputNode, null) |
michael@0 | 729 | .getPropertyValue("font-size").replace("px", ""); |
michael@0 | 730 | |
michael@0 | 731 | if (this.outputNode.style.fontSize) { |
michael@0 | 732 | fontSize = this.outputNode.style.fontSize.replace("px", ""); |
michael@0 | 733 | } |
michael@0 | 734 | |
michael@0 | 735 | if (aSize == "+" || aSize == "-") { |
michael@0 | 736 | fontSize = parseInt(fontSize, 10); |
michael@0 | 737 | |
michael@0 | 738 | if (aSize == "+") { |
michael@0 | 739 | fontSize += 1; |
michael@0 | 740 | } |
michael@0 | 741 | else { |
michael@0 | 742 | fontSize -= 1; |
michael@0 | 743 | } |
michael@0 | 744 | |
michael@0 | 745 | if (fontSize < MIN_FONT_SIZE) { |
michael@0 | 746 | fontSize = MIN_FONT_SIZE; |
michael@0 | 747 | } |
michael@0 | 748 | |
michael@0 | 749 | Services.prefs.setIntPref("devtools.webconsole.fontSize", fontSize); |
michael@0 | 750 | fontSize = fontSize + "px"; |
michael@0 | 751 | |
michael@0 | 752 | this.completeNode.style.fontSize = fontSize; |
michael@0 | 753 | this.inputNode.style.fontSize = fontSize; |
michael@0 | 754 | this.outputNode.style.fontSize = fontSize; |
michael@0 | 755 | } |
michael@0 | 756 | else { |
michael@0 | 757 | this.completeNode.style.fontSize = ""; |
michael@0 | 758 | this.inputNode.style.fontSize = ""; |
michael@0 | 759 | this.outputNode.style.fontSize = ""; |
michael@0 | 760 | Services.prefs.clearUserPref("devtools.webconsole.fontSize"); |
michael@0 | 761 | } |
michael@0 | 762 | this._updateCharSize(); |
michael@0 | 763 | }, |
michael@0 | 764 | |
michael@0 | 765 | /** |
michael@0 | 766 | * Calculates the width and height of a single character of the input box. |
michael@0 | 767 | * This will be used in opening the popup at the correct offset. |
michael@0 | 768 | * |
michael@0 | 769 | * @private |
michael@0 | 770 | */ |
michael@0 | 771 | _updateCharSize: function WCF__updateCharSize() |
michael@0 | 772 | { |
michael@0 | 773 | let doc = this.document; |
michael@0 | 774 | let tempLabel = doc.createElementNS(XHTML_NS, "span"); |
michael@0 | 775 | let style = tempLabel.style; |
michael@0 | 776 | style.position = "fixed"; |
michael@0 | 777 | style.padding = "0"; |
michael@0 | 778 | style.margin = "0"; |
michael@0 | 779 | style.width = "auto"; |
michael@0 | 780 | style.color = "transparent"; |
michael@0 | 781 | WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel); |
michael@0 | 782 | tempLabel.textContent = "x"; |
michael@0 | 783 | doc.documentElement.appendChild(tempLabel); |
michael@0 | 784 | this._inputCharWidth = tempLabel.offsetWidth; |
michael@0 | 785 | tempLabel.parentNode.removeChild(tempLabel); |
michael@0 | 786 | // Calculate the width of the chevron placed at the beginning of the input |
michael@0 | 787 | // box. Remove 4 more pixels to accomodate the padding of the popup. |
michael@0 | 788 | this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode) |
michael@0 | 789 | .paddingLeft.replace(/[^0-9.]/g, "") - 4; |
michael@0 | 790 | }, |
michael@0 | 791 | |
michael@0 | 792 | /** |
michael@0 | 793 | * The event handler that is called whenever a user switches a filter on or |
michael@0 | 794 | * off. |
michael@0 | 795 | * |
michael@0 | 796 | * @private |
michael@0 | 797 | * @param nsIDOMEvent aEvent |
michael@0 | 798 | * The event that triggered the filter change. |
michael@0 | 799 | */ |
michael@0 | 800 | _toggleFilter: function WCF__toggleFilter(aEvent) |
michael@0 | 801 | { |
michael@0 | 802 | let target = aEvent.target; |
michael@0 | 803 | let tagName = target.tagName; |
michael@0 | 804 | if (tagName != aEvent.currentTarget.tagName) { |
michael@0 | 805 | return; |
michael@0 | 806 | } |
michael@0 | 807 | |
michael@0 | 808 | switch (tagName) { |
michael@0 | 809 | case "toolbarbutton": { |
michael@0 | 810 | let originalTarget = aEvent.originalTarget; |
michael@0 | 811 | let classes = originalTarget.classList; |
michael@0 | 812 | |
michael@0 | 813 | if (originalTarget.localName !== "toolbarbutton") { |
michael@0 | 814 | // Oddly enough, the click event is sent to the menu button when |
michael@0 | 815 | // selecting a menu item with the mouse. Detect this case and bail |
michael@0 | 816 | // out. |
michael@0 | 817 | break; |
michael@0 | 818 | } |
michael@0 | 819 | |
michael@0 | 820 | if (!classes.contains("toolbarbutton-menubutton-button") && |
michael@0 | 821 | originalTarget.getAttribute("type") === "menu-button") { |
michael@0 | 822 | // This is a filter button with a drop-down. The user clicked the |
michael@0 | 823 | // drop-down, so do nothing. (The menu will automatically appear |
michael@0 | 824 | // without our intervention.) |
michael@0 | 825 | break; |
michael@0 | 826 | } |
michael@0 | 827 | |
michael@0 | 828 | // Toggle on the targeted filter button, and if the user alt clicked, |
michael@0 | 829 | // toggle off all other filter buttons and their associated filters. |
michael@0 | 830 | let state = target.getAttribute("checked") !== "true"; |
michael@0 | 831 | if (aEvent.getModifierState("Alt")) { |
michael@0 | 832 | let buttons = this.document |
michael@0 | 833 | .querySelectorAll(".webconsole-filter-button"); |
michael@0 | 834 | Array.forEach(buttons, (button) => { |
michael@0 | 835 | if (button !== target) { |
michael@0 | 836 | button.setAttribute("checked", false); |
michael@0 | 837 | this._setMenuState(button, false); |
michael@0 | 838 | } |
michael@0 | 839 | }); |
michael@0 | 840 | state = true; |
michael@0 | 841 | } |
michael@0 | 842 | target.setAttribute("checked", state); |
michael@0 | 843 | |
michael@0 | 844 | // This is a filter button with a drop-down, and the user clicked the |
michael@0 | 845 | // main part of the button. Go through all the severities and toggle |
michael@0 | 846 | // their associated filters. |
michael@0 | 847 | this._setMenuState(target, state); |
michael@0 | 848 | |
michael@0 | 849 | // CSS reflow logging can decrease web page performance. |
michael@0 | 850 | // Make sure the option is always unchecked when the CSS filter button is selected. |
michael@0 | 851 | // See bug 971798. |
michael@0 | 852 | if (target.getAttribute("category") == "css" && state) { |
michael@0 | 853 | let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]"); |
michael@0 | 854 | csslogMenuItem.setAttribute("checked", false); |
michael@0 | 855 | this.setFilterState("csslog", false); |
michael@0 | 856 | } |
michael@0 | 857 | |
michael@0 | 858 | break; |
michael@0 | 859 | } |
michael@0 | 860 | |
michael@0 | 861 | case "menuitem": { |
michael@0 | 862 | let state = target.getAttribute("checked") !== "true"; |
michael@0 | 863 | target.setAttribute("checked", state); |
michael@0 | 864 | |
michael@0 | 865 | let prefKey = target.getAttribute("prefKey"); |
michael@0 | 866 | this.setFilterState(prefKey, state); |
michael@0 | 867 | |
michael@0 | 868 | // Disable the log response and request body if network logging is off. |
michael@0 | 869 | if (prefKey == "networkinfo" || prefKey == "network") { |
michael@0 | 870 | let checkState = !this.getFilterState("networkinfo") && |
michael@0 | 871 | !this.getFilterState("network"); |
michael@0 | 872 | this.document.getElementById("saveBodies").disabled = checkState; |
michael@0 | 873 | this.document.getElementById("saveBodiesContextMenu").disabled = checkState; |
michael@0 | 874 | } |
michael@0 | 875 | |
michael@0 | 876 | // Adjust the state of the button appropriately. |
michael@0 | 877 | let menuPopup = target.parentNode; |
michael@0 | 878 | |
michael@0 | 879 | let someChecked = false; |
michael@0 | 880 | let menuItem = menuPopup.firstChild; |
michael@0 | 881 | while (menuItem) { |
michael@0 | 882 | if (menuItem.hasAttribute("prefKey") && |
michael@0 | 883 | menuItem.getAttribute("checked") === "true") { |
michael@0 | 884 | someChecked = true; |
michael@0 | 885 | break; |
michael@0 | 886 | } |
michael@0 | 887 | menuItem = menuItem.nextSibling; |
michael@0 | 888 | } |
michael@0 | 889 | let toolbarButton = menuPopup.parentNode; |
michael@0 | 890 | toolbarButton.setAttribute("checked", someChecked); |
michael@0 | 891 | break; |
michael@0 | 892 | } |
michael@0 | 893 | } |
michael@0 | 894 | }, |
michael@0 | 895 | |
michael@0 | 896 | /** |
michael@0 | 897 | * Set the menu attributes for a specific toggle button. |
michael@0 | 898 | * |
michael@0 | 899 | * @private |
michael@0 | 900 | * @param XULElement aTarget |
michael@0 | 901 | * Button with drop down items to be toggled. |
michael@0 | 902 | * @param boolean aState |
michael@0 | 903 | * True if the menu item is being toggled on, and false otherwise. |
michael@0 | 904 | */ |
michael@0 | 905 | _setMenuState: function WCF__setMenuState(aTarget, aState) |
michael@0 | 906 | { |
michael@0 | 907 | let menuItems = aTarget.querySelectorAll("menuitem"); |
michael@0 | 908 | Array.forEach(menuItems, (item) => { |
michael@0 | 909 | item.setAttribute("checked", aState); |
michael@0 | 910 | let prefKey = item.getAttribute("prefKey"); |
michael@0 | 911 | this.setFilterState(prefKey, aState); |
michael@0 | 912 | }); |
michael@0 | 913 | }, |
michael@0 | 914 | |
michael@0 | 915 | /** |
michael@0 | 916 | * Set the filter state for a specific toggle button. |
michael@0 | 917 | * |
michael@0 | 918 | * @param string aToggleType |
michael@0 | 919 | * @param boolean aState |
michael@0 | 920 | * @returns void |
michael@0 | 921 | */ |
michael@0 | 922 | setFilterState: function WCF_setFilterState(aToggleType, aState) |
michael@0 | 923 | { |
michael@0 | 924 | this.filterPrefs[aToggleType] = aState; |
michael@0 | 925 | this.adjustVisibilityForMessageType(aToggleType, aState); |
michael@0 | 926 | Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState); |
michael@0 | 927 | this._updateReflowActivityListener(); |
michael@0 | 928 | }, |
michael@0 | 929 | |
michael@0 | 930 | /** |
michael@0 | 931 | * Get the filter state for a specific toggle button. |
michael@0 | 932 | * |
michael@0 | 933 | * @param string aToggleType |
michael@0 | 934 | * @returns boolean |
michael@0 | 935 | */ |
michael@0 | 936 | getFilterState: function WCF_getFilterState(aToggleType) |
michael@0 | 937 | { |
michael@0 | 938 | return this.filterPrefs[aToggleType]; |
michael@0 | 939 | }, |
michael@0 | 940 | |
michael@0 | 941 | /** |
michael@0 | 942 | * Check that the passed string matches the filter arguments. |
michael@0 | 943 | * |
michael@0 | 944 | * @param String aString |
michael@0 | 945 | * to search for filter words in. |
michael@0 | 946 | * @param String aFilter |
michael@0 | 947 | * is a string containing all of the words to filter on. |
michael@0 | 948 | * @returns boolean |
michael@0 | 949 | */ |
michael@0 | 950 | stringMatchesFilters: function WCF_stringMatchesFilters(aString, aFilter) |
michael@0 | 951 | { |
michael@0 | 952 | if (!aFilter || !aString) { |
michael@0 | 953 | return true; |
michael@0 | 954 | } |
michael@0 | 955 | |
michael@0 | 956 | let searchStr = aString.toLowerCase(); |
michael@0 | 957 | let filterStrings = aFilter.toLowerCase().split(/\s+/); |
michael@0 | 958 | return !filterStrings.some(function (f) { |
michael@0 | 959 | return searchStr.indexOf(f) == -1; |
michael@0 | 960 | }); |
michael@0 | 961 | }, |
michael@0 | 962 | |
michael@0 | 963 | /** |
michael@0 | 964 | * Turns the display of log nodes on and off appropriately to reflect the |
michael@0 | 965 | * adjustment of the message type filter named by @aPrefKey. |
michael@0 | 966 | * |
michael@0 | 967 | * @param string aPrefKey |
michael@0 | 968 | * The preference key for the message type being filtered: one of the |
michael@0 | 969 | * values in the MESSAGE_PREFERENCE_KEYS table. |
michael@0 | 970 | * @param boolean aState |
michael@0 | 971 | * True if the filter named by @aMessageType is being turned on; false |
michael@0 | 972 | * otherwise. |
michael@0 | 973 | * @returns void |
michael@0 | 974 | */ |
michael@0 | 975 | adjustVisibilityForMessageType: |
michael@0 | 976 | function WCF_adjustVisibilityForMessageType(aPrefKey, aState) |
michael@0 | 977 | { |
michael@0 | 978 | let outputNode = this.outputNode; |
michael@0 | 979 | let doc = this.document; |
michael@0 | 980 | |
michael@0 | 981 | // Look for message nodes (".message") with the given preference key |
michael@0 | 982 | // (filter="error", filter="cssparser", etc.) and add or remove the |
michael@0 | 983 | // "filtered-by-type" class, which turns on or off the display. |
michael@0 | 984 | |
michael@0 | 985 | let xpath = ".//*[contains(@class, 'message') and " + |
michael@0 | 986 | "@filter='" + aPrefKey + "']"; |
michael@0 | 987 | let result = doc.evaluate(xpath, outputNode, null, |
michael@0 | 988 | Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); |
michael@0 | 989 | for (let i = 0; i < result.snapshotLength; i++) { |
michael@0 | 990 | let node = result.snapshotItem(i); |
michael@0 | 991 | if (aState) { |
michael@0 | 992 | node.classList.remove("filtered-by-type"); |
michael@0 | 993 | } |
michael@0 | 994 | else { |
michael@0 | 995 | node.classList.add("filtered-by-type"); |
michael@0 | 996 | } |
michael@0 | 997 | } |
michael@0 | 998 | }, |
michael@0 | 999 | |
michael@0 | 1000 | /** |
michael@0 | 1001 | * Turns the display of log nodes on and off appropriately to reflect the |
michael@0 | 1002 | * adjustment of the search string. |
michael@0 | 1003 | */ |
michael@0 | 1004 | adjustVisibilityOnSearchStringChange: |
michael@0 | 1005 | function WCF_adjustVisibilityOnSearchStringChange() |
michael@0 | 1006 | { |
michael@0 | 1007 | let nodes = this.outputNode.getElementsByClassName("message"); |
michael@0 | 1008 | let searchString = this.filterBox.value; |
michael@0 | 1009 | |
michael@0 | 1010 | for (let i = 0, n = nodes.length; i < n; ++i) { |
michael@0 | 1011 | let node = nodes[i]; |
michael@0 | 1012 | |
michael@0 | 1013 | // hide nodes that match the strings |
michael@0 | 1014 | let text = node.textContent; |
michael@0 | 1015 | |
michael@0 | 1016 | // if the text matches the words in aSearchString... |
michael@0 | 1017 | if (this.stringMatchesFilters(text, searchString)) { |
michael@0 | 1018 | node.classList.remove("filtered-by-string"); |
michael@0 | 1019 | } |
michael@0 | 1020 | else { |
michael@0 | 1021 | node.classList.add("filtered-by-string"); |
michael@0 | 1022 | } |
michael@0 | 1023 | } |
michael@0 | 1024 | }, |
michael@0 | 1025 | |
michael@0 | 1026 | /** |
michael@0 | 1027 | * Applies the user's filters to a newly-created message node via CSS |
michael@0 | 1028 | * classes. |
michael@0 | 1029 | * |
michael@0 | 1030 | * @param nsIDOMNode aNode |
michael@0 | 1031 | * The newly-created message node. |
michael@0 | 1032 | * @return boolean |
michael@0 | 1033 | * True if the message was filtered or false otherwise. |
michael@0 | 1034 | */ |
michael@0 | 1035 | filterMessageNode: function WCF_filterMessageNode(aNode) |
michael@0 | 1036 | { |
michael@0 | 1037 | let isFiltered = false; |
michael@0 | 1038 | |
michael@0 | 1039 | // Filter by the message type. |
michael@0 | 1040 | let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity]; |
michael@0 | 1041 | if (prefKey && !this.getFilterState(prefKey)) { |
michael@0 | 1042 | // The node is filtered by type. |
michael@0 | 1043 | aNode.classList.add("filtered-by-type"); |
michael@0 | 1044 | isFiltered = true; |
michael@0 | 1045 | } |
michael@0 | 1046 | |
michael@0 | 1047 | // Filter on the search string. |
michael@0 | 1048 | let search = this.filterBox.value; |
michael@0 | 1049 | let text = aNode.clipboardText; |
michael@0 | 1050 | |
michael@0 | 1051 | // if string matches the filter text |
michael@0 | 1052 | if (!this.stringMatchesFilters(text, search)) { |
michael@0 | 1053 | aNode.classList.add("filtered-by-string"); |
michael@0 | 1054 | isFiltered = true; |
michael@0 | 1055 | } |
michael@0 | 1056 | |
michael@0 | 1057 | if (isFiltered && aNode.classList.contains("inlined-variables-view")) { |
michael@0 | 1058 | aNode.classList.add("hidden-message"); |
michael@0 | 1059 | } |
michael@0 | 1060 | |
michael@0 | 1061 | return isFiltered; |
michael@0 | 1062 | }, |
michael@0 | 1063 | |
michael@0 | 1064 | /** |
michael@0 | 1065 | * Merge the attributes of the two nodes that are about to be filtered. |
michael@0 | 1066 | * Increment the number of repeats of aOriginal. |
michael@0 | 1067 | * |
michael@0 | 1068 | * @param nsIDOMNode aOriginal |
michael@0 | 1069 | * The Original Node. The one being merged into. |
michael@0 | 1070 | * @param nsIDOMNode aFiltered |
michael@0 | 1071 | * The node being filtered out because it is repeated. |
michael@0 | 1072 | */ |
michael@0 | 1073 | mergeFilteredMessageNode: |
michael@0 | 1074 | function WCF_mergeFilteredMessageNode(aOriginal, aFiltered) |
michael@0 | 1075 | { |
michael@0 | 1076 | let repeatNode = aOriginal.getElementsByClassName("message-repeats")[0]; |
michael@0 | 1077 | if (!repeatNode) { |
michael@0 | 1078 | return; // no repeat node, return early. |
michael@0 | 1079 | } |
michael@0 | 1080 | |
michael@0 | 1081 | let occurrences = parseInt(repeatNode.getAttribute("value")) + 1; |
michael@0 | 1082 | repeatNode.setAttribute("value", occurrences); |
michael@0 | 1083 | repeatNode.textContent = occurrences; |
michael@0 | 1084 | let str = l10n.getStr("messageRepeats.tooltip2"); |
michael@0 | 1085 | repeatNode.title = PluralForm.get(occurrences, str) |
michael@0 | 1086 | .replace("#1", occurrences); |
michael@0 | 1087 | }, |
michael@0 | 1088 | |
michael@0 | 1089 | /** |
michael@0 | 1090 | * Filter the message node from the output if it is a repeat. |
michael@0 | 1091 | * |
michael@0 | 1092 | * @private |
michael@0 | 1093 | * @param nsIDOMNode aNode |
michael@0 | 1094 | * The message node to be filtered or not. |
michael@0 | 1095 | * @returns nsIDOMNode|null |
michael@0 | 1096 | * Returns the duplicate node if the message was filtered, null |
michael@0 | 1097 | * otherwise. |
michael@0 | 1098 | */ |
michael@0 | 1099 | _filterRepeatedMessage: function WCF__filterRepeatedMessage(aNode) |
michael@0 | 1100 | { |
michael@0 | 1101 | let repeatNode = aNode.getElementsByClassName("message-repeats")[0]; |
michael@0 | 1102 | if (!repeatNode) { |
michael@0 | 1103 | return null; |
michael@0 | 1104 | } |
michael@0 | 1105 | |
michael@0 | 1106 | let uid = repeatNode._uid; |
michael@0 | 1107 | let dupeNode = null; |
michael@0 | 1108 | |
michael@0 | 1109 | if (aNode.category == CATEGORY_CSS || |
michael@0 | 1110 | aNode.category == CATEGORY_SECURITY) { |
michael@0 | 1111 | dupeNode = this._repeatNodes[uid]; |
michael@0 | 1112 | if (!dupeNode) { |
michael@0 | 1113 | this._repeatNodes[uid] = aNode; |
michael@0 | 1114 | } |
michael@0 | 1115 | } |
michael@0 | 1116 | else if ((aNode.category == CATEGORY_WEBDEV || |
michael@0 | 1117 | aNode.category == CATEGORY_JS) && |
michael@0 | 1118 | aNode.category != CATEGORY_NETWORK && |
michael@0 | 1119 | !aNode.classList.contains("inlined-variables-view")) { |
michael@0 | 1120 | let lastMessage = this.outputNode.lastChild; |
michael@0 | 1121 | if (!lastMessage) { |
michael@0 | 1122 | return null; |
michael@0 | 1123 | } |
michael@0 | 1124 | |
michael@0 | 1125 | let lastRepeatNode = lastMessage.getElementsByClassName("message-repeats")[0]; |
michael@0 | 1126 | if (lastRepeatNode && lastRepeatNode._uid == uid) { |
michael@0 | 1127 | dupeNode = lastMessage; |
michael@0 | 1128 | } |
michael@0 | 1129 | } |
michael@0 | 1130 | |
michael@0 | 1131 | if (dupeNode) { |
michael@0 | 1132 | this.mergeFilteredMessageNode(dupeNode, aNode); |
michael@0 | 1133 | return dupeNode; |
michael@0 | 1134 | } |
michael@0 | 1135 | |
michael@0 | 1136 | return null; |
michael@0 | 1137 | }, |
michael@0 | 1138 | |
michael@0 | 1139 | /** |
michael@0 | 1140 | * Display cached messages that may have been collected before the UI is |
michael@0 | 1141 | * displayed. |
michael@0 | 1142 | * |
michael@0 | 1143 | * @param array aRemoteMessages |
michael@0 | 1144 | * Array of cached messages coming from the remote Web Console |
michael@0 | 1145 | * content instance. |
michael@0 | 1146 | */ |
michael@0 | 1147 | displayCachedMessages: function WCF_displayCachedMessages(aRemoteMessages) |
michael@0 | 1148 | { |
michael@0 | 1149 | if (!aRemoteMessages.length) { |
michael@0 | 1150 | return; |
michael@0 | 1151 | } |
michael@0 | 1152 | |
michael@0 | 1153 | aRemoteMessages.forEach(function(aMessage) { |
michael@0 | 1154 | switch (aMessage._type) { |
michael@0 | 1155 | case "PageError": { |
michael@0 | 1156 | let category = Utils.categoryForScriptError(aMessage); |
michael@0 | 1157 | this.outputMessage(category, this.reportPageError, |
michael@0 | 1158 | [category, aMessage]); |
michael@0 | 1159 | break; |
michael@0 | 1160 | } |
michael@0 | 1161 | case "LogMessage": |
michael@0 | 1162 | this.handleLogMessage(aMessage); |
michael@0 | 1163 | break; |
michael@0 | 1164 | case "ConsoleAPI": |
michael@0 | 1165 | this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, |
michael@0 | 1166 | [aMessage]); |
michael@0 | 1167 | break; |
michael@0 | 1168 | } |
michael@0 | 1169 | }, this); |
michael@0 | 1170 | }, |
michael@0 | 1171 | |
michael@0 | 1172 | /** |
michael@0 | 1173 | * Logs a message to the Web Console that originates from the Web Console |
michael@0 | 1174 | * server. |
michael@0 | 1175 | * |
michael@0 | 1176 | * @param object aMessage |
michael@0 | 1177 | * The message received from the server. |
michael@0 | 1178 | * @return nsIDOMElement|null |
michael@0 | 1179 | * The message element to display in the Web Console output. |
michael@0 | 1180 | */ |
michael@0 | 1181 | logConsoleAPIMessage: function WCF_logConsoleAPIMessage(aMessage) |
michael@0 | 1182 | { |
michael@0 | 1183 | let body = null; |
michael@0 | 1184 | let clipboardText = null; |
michael@0 | 1185 | let sourceURL = aMessage.filename; |
michael@0 | 1186 | let sourceLine = aMessage.lineNumber; |
michael@0 | 1187 | let level = aMessage.level; |
michael@0 | 1188 | let args = aMessage.arguments; |
michael@0 | 1189 | let objectActors = new Set(); |
michael@0 | 1190 | let node = null; |
michael@0 | 1191 | |
michael@0 | 1192 | // Gather the actor IDs. |
michael@0 | 1193 | args.forEach((aValue) => { |
michael@0 | 1194 | if (WebConsoleUtils.isActorGrip(aValue)) { |
michael@0 | 1195 | objectActors.add(aValue.actor); |
michael@0 | 1196 | } |
michael@0 | 1197 | }); |
michael@0 | 1198 | |
michael@0 | 1199 | switch (level) { |
michael@0 | 1200 | case "log": |
michael@0 | 1201 | case "info": |
michael@0 | 1202 | case "warn": |
michael@0 | 1203 | case "error": |
michael@0 | 1204 | case "exception": |
michael@0 | 1205 | case "assert": |
michael@0 | 1206 | case "debug": { |
michael@0 | 1207 | let msg = new Messages.ConsoleGeneric(aMessage); |
michael@0 | 1208 | node = msg.init(this.output).render().element; |
michael@0 | 1209 | break; |
michael@0 | 1210 | } |
michael@0 | 1211 | case "trace": { |
michael@0 | 1212 | let msg = new Messages.ConsoleTrace(aMessage); |
michael@0 | 1213 | node = msg.init(this.output).render().element; |
michael@0 | 1214 | break; |
michael@0 | 1215 | } |
michael@0 | 1216 | case "dir": { |
michael@0 | 1217 | body = { arguments: args }; |
michael@0 | 1218 | let clipboardArray = []; |
michael@0 | 1219 | args.forEach((aValue) => { |
michael@0 | 1220 | clipboardArray.push(VariablesView.getString(aValue)); |
michael@0 | 1221 | }); |
michael@0 | 1222 | clipboardText = clipboardArray.join(" "); |
michael@0 | 1223 | break; |
michael@0 | 1224 | } |
michael@0 | 1225 | |
michael@0 | 1226 | case "group": |
michael@0 | 1227 | case "groupCollapsed": |
michael@0 | 1228 | clipboardText = body = aMessage.groupName; |
michael@0 | 1229 | this.groupDepth++; |
michael@0 | 1230 | break; |
michael@0 | 1231 | |
michael@0 | 1232 | case "groupEnd": |
michael@0 | 1233 | if (this.groupDepth > 0) { |
michael@0 | 1234 | this.groupDepth--; |
michael@0 | 1235 | } |
michael@0 | 1236 | break; |
michael@0 | 1237 | |
michael@0 | 1238 | case "time": { |
michael@0 | 1239 | let timer = aMessage.timer; |
michael@0 | 1240 | if (!timer) { |
michael@0 | 1241 | return null; |
michael@0 | 1242 | } |
michael@0 | 1243 | if (timer.error) { |
michael@0 | 1244 | Cu.reportError(l10n.getStr(timer.error)); |
michael@0 | 1245 | return null; |
michael@0 | 1246 | } |
michael@0 | 1247 | body = l10n.getFormatStr("timerStarted", [timer.name]); |
michael@0 | 1248 | clipboardText = body; |
michael@0 | 1249 | break; |
michael@0 | 1250 | } |
michael@0 | 1251 | |
michael@0 | 1252 | case "timeEnd": { |
michael@0 | 1253 | let timer = aMessage.timer; |
michael@0 | 1254 | if (!timer) { |
michael@0 | 1255 | return null; |
michael@0 | 1256 | } |
michael@0 | 1257 | let duration = Math.round(timer.duration * 100) / 100; |
michael@0 | 1258 | body = l10n.getFormatStr("timeEnd", [timer.name, duration]); |
michael@0 | 1259 | clipboardText = body; |
michael@0 | 1260 | break; |
michael@0 | 1261 | } |
michael@0 | 1262 | |
michael@0 | 1263 | case "count": { |
michael@0 | 1264 | let counter = aMessage.counter; |
michael@0 | 1265 | if (!counter) { |
michael@0 | 1266 | return null; |
michael@0 | 1267 | } |
michael@0 | 1268 | if (counter.error) { |
michael@0 | 1269 | Cu.reportError(l10n.getStr(counter.error)); |
michael@0 | 1270 | return null; |
michael@0 | 1271 | } |
michael@0 | 1272 | let msg = new Messages.ConsoleGeneric(aMessage); |
michael@0 | 1273 | node = msg.init(this.output).render().element; |
michael@0 | 1274 | break; |
michael@0 | 1275 | } |
michael@0 | 1276 | |
michael@0 | 1277 | default: |
michael@0 | 1278 | Cu.reportError("Unknown Console API log level: " + level); |
michael@0 | 1279 | return null; |
michael@0 | 1280 | } |
michael@0 | 1281 | |
michael@0 | 1282 | // Release object actors for arguments coming from console API methods that |
michael@0 | 1283 | // we ignore their arguments. |
michael@0 | 1284 | switch (level) { |
michael@0 | 1285 | case "group": |
michael@0 | 1286 | case "groupCollapsed": |
michael@0 | 1287 | case "groupEnd": |
michael@0 | 1288 | case "time": |
michael@0 | 1289 | case "timeEnd": |
michael@0 | 1290 | case "count": |
michael@0 | 1291 | for (let actor of objectActors) { |
michael@0 | 1292 | this._releaseObject(actor); |
michael@0 | 1293 | } |
michael@0 | 1294 | objectActors.clear(); |
michael@0 | 1295 | } |
michael@0 | 1296 | |
michael@0 | 1297 | if (level == "groupEnd") { |
michael@0 | 1298 | return null; // no need to continue |
michael@0 | 1299 | } |
michael@0 | 1300 | |
michael@0 | 1301 | if (!node) { |
michael@0 | 1302 | node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body, |
michael@0 | 1303 | sourceURL, sourceLine, clipboardText, |
michael@0 | 1304 | level, aMessage.timeStamp); |
michael@0 | 1305 | if (aMessage.private) { |
michael@0 | 1306 | node.setAttribute("private", true); |
michael@0 | 1307 | } |
michael@0 | 1308 | } |
michael@0 | 1309 | |
michael@0 | 1310 | if (objectActors.size > 0) { |
michael@0 | 1311 | node._objectActors = objectActors; |
michael@0 | 1312 | |
michael@0 | 1313 | if (!node._messageObject) { |
michael@0 | 1314 | let repeatNode = node.getElementsByClassName("message-repeats")[0]; |
michael@0 | 1315 | repeatNode._uid += [...objectActors].join("-"); |
michael@0 | 1316 | } |
michael@0 | 1317 | } |
michael@0 | 1318 | |
michael@0 | 1319 | return node; |
michael@0 | 1320 | }, |
michael@0 | 1321 | |
michael@0 | 1322 | /** |
michael@0 | 1323 | * Handle ConsoleAPICall objects received from the server. This method outputs |
michael@0 | 1324 | * the window.console API call. |
michael@0 | 1325 | * |
michael@0 | 1326 | * @param object aMessage |
michael@0 | 1327 | * The console API message received from the server. |
michael@0 | 1328 | */ |
michael@0 | 1329 | handleConsoleAPICall: function WCF_handleConsoleAPICall(aMessage) |
michael@0 | 1330 | { |
michael@0 | 1331 | this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]); |
michael@0 | 1332 | }, |
michael@0 | 1333 | |
michael@0 | 1334 | /** |
michael@0 | 1335 | * Reports an error in the page source, either JavaScript or CSS. |
michael@0 | 1336 | * |
michael@0 | 1337 | * @param nsIScriptError aScriptError |
michael@0 | 1338 | * The error message to report. |
michael@0 | 1339 | * @return nsIDOMElement|undefined |
michael@0 | 1340 | * The message element to display in the Web Console output. |
michael@0 | 1341 | */ |
michael@0 | 1342 | reportPageError: function WCF_reportPageError(aCategory, aScriptError) |
michael@0 | 1343 | { |
michael@0 | 1344 | // Warnings and legacy strict errors become warnings; other types become |
michael@0 | 1345 | // errors. |
michael@0 | 1346 | let severity = SEVERITY_ERROR; |
michael@0 | 1347 | if (aScriptError.warning || aScriptError.strict) { |
michael@0 | 1348 | severity = SEVERITY_WARNING; |
michael@0 | 1349 | } |
michael@0 | 1350 | |
michael@0 | 1351 | let objectActors = new Set(); |
michael@0 | 1352 | |
michael@0 | 1353 | // Gather the actor IDs. |
michael@0 | 1354 | for (let prop of ["errorMessage", "lineText"]) { |
michael@0 | 1355 | let grip = aScriptError[prop]; |
michael@0 | 1356 | if (WebConsoleUtils.isActorGrip(grip)) { |
michael@0 | 1357 | objectActors.add(grip.actor); |
michael@0 | 1358 | } |
michael@0 | 1359 | } |
michael@0 | 1360 | |
michael@0 | 1361 | let errorMessage = aScriptError.errorMessage; |
michael@0 | 1362 | if (errorMessage.type && errorMessage.type == "longString") { |
michael@0 | 1363 | errorMessage = errorMessage.initial; |
michael@0 | 1364 | } |
michael@0 | 1365 | |
michael@0 | 1366 | let node = this.createMessageNode(aCategory, severity, |
michael@0 | 1367 | errorMessage, |
michael@0 | 1368 | aScriptError.sourceName, |
michael@0 | 1369 | aScriptError.lineNumber, null, null, |
michael@0 | 1370 | aScriptError.timeStamp); |
michael@0 | 1371 | |
michael@0 | 1372 | // Select the body of the message node that is displayed in the console |
michael@0 | 1373 | let msgBody = node.getElementsByClassName("message-body")[0]; |
michael@0 | 1374 | // Add the more info link node to messages that belong to certain categories |
michael@0 | 1375 | this.addMoreInfoLink(msgBody, aScriptError); |
michael@0 | 1376 | |
michael@0 | 1377 | if (aScriptError.private) { |
michael@0 | 1378 | node.setAttribute("private", true); |
michael@0 | 1379 | } |
michael@0 | 1380 | |
michael@0 | 1381 | if (objectActors.size > 0) { |
michael@0 | 1382 | node._objectActors = objectActors; |
michael@0 | 1383 | } |
michael@0 | 1384 | |
michael@0 | 1385 | return node; |
michael@0 | 1386 | }, |
michael@0 | 1387 | |
michael@0 | 1388 | /** |
michael@0 | 1389 | * Handle PageError objects received from the server. This method outputs the |
michael@0 | 1390 | * given error. |
michael@0 | 1391 | * |
michael@0 | 1392 | * @param nsIScriptError aPageError |
michael@0 | 1393 | * The error received from the server. |
michael@0 | 1394 | */ |
michael@0 | 1395 | handlePageError: function WCF_handlePageError(aPageError) |
michael@0 | 1396 | { |
michael@0 | 1397 | let category = Utils.categoryForScriptError(aPageError); |
michael@0 | 1398 | this.outputMessage(category, this.reportPageError, [category, aPageError]); |
michael@0 | 1399 | }, |
michael@0 | 1400 | |
michael@0 | 1401 | /** |
michael@0 | 1402 | * Handle log messages received from the server. This method outputs the given |
michael@0 | 1403 | * message. |
michael@0 | 1404 | * |
michael@0 | 1405 | * @param object aPacket |
michael@0 | 1406 | * The message packet received from the server. |
michael@0 | 1407 | */ |
michael@0 | 1408 | handleLogMessage: function WCF_handleLogMessage(aPacket) |
michael@0 | 1409 | { |
michael@0 | 1410 | if (aPacket.message) { |
michael@0 | 1411 | this.outputMessage(CATEGORY_JS, this._reportLogMessage, [aPacket]); |
michael@0 | 1412 | } |
michael@0 | 1413 | }, |
michael@0 | 1414 | |
michael@0 | 1415 | /** |
michael@0 | 1416 | * Display log messages received from the server. |
michael@0 | 1417 | * |
michael@0 | 1418 | * @private |
michael@0 | 1419 | * @param object aPacket |
michael@0 | 1420 | * The message packet received from the server. |
michael@0 | 1421 | * @return nsIDOMElement |
michael@0 | 1422 | * The message element to render for the given log message. |
michael@0 | 1423 | */ |
michael@0 | 1424 | _reportLogMessage: function WCF__reportLogMessage(aPacket) |
michael@0 | 1425 | { |
michael@0 | 1426 | let msg = aPacket.message; |
michael@0 | 1427 | if (msg.type && msg.type == "longString") { |
michael@0 | 1428 | msg = msg.initial; |
michael@0 | 1429 | } |
michael@0 | 1430 | let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null, |
michael@0 | 1431 | null, null, null, aPacket.timeStamp); |
michael@0 | 1432 | if (WebConsoleUtils.isActorGrip(aPacket.message)) { |
michael@0 | 1433 | node._objectActors = new Set([aPacket.message.actor]); |
michael@0 | 1434 | } |
michael@0 | 1435 | return node; |
michael@0 | 1436 | }, |
michael@0 | 1437 | |
michael@0 | 1438 | /** |
michael@0 | 1439 | * Log network event. |
michael@0 | 1440 | * |
michael@0 | 1441 | * @param object aActorId |
michael@0 | 1442 | * The network event actor ID to log. |
michael@0 | 1443 | * @return nsIDOMElement|null |
michael@0 | 1444 | * The message element to display in the Web Console output. |
michael@0 | 1445 | */ |
michael@0 | 1446 | logNetEvent: function WCF_logNetEvent(aActorId) |
michael@0 | 1447 | { |
michael@0 | 1448 | let networkInfo = this._networkRequests[aActorId]; |
michael@0 | 1449 | if (!networkInfo) { |
michael@0 | 1450 | return null; |
michael@0 | 1451 | } |
michael@0 | 1452 | |
michael@0 | 1453 | let request = networkInfo.request; |
michael@0 | 1454 | let clipboardText = request.method + " " + request.url; |
michael@0 | 1455 | let severity = SEVERITY_LOG; |
michael@0 | 1456 | let mixedRequest = |
michael@0 | 1457 | WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation); |
michael@0 | 1458 | if (mixedRequest) { |
michael@0 | 1459 | severity = SEVERITY_WARNING; |
michael@0 | 1460 | } |
michael@0 | 1461 | |
michael@0 | 1462 | let methodNode = this.document.createElementNS(XHTML_NS, "span"); |
michael@0 | 1463 | methodNode.className = "method"; |
michael@0 | 1464 | methodNode.textContent = request.method + " "; |
michael@0 | 1465 | |
michael@0 | 1466 | let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity, |
michael@0 | 1467 | methodNode, null, null, |
michael@0 | 1468 | clipboardText); |
michael@0 | 1469 | if (networkInfo.private) { |
michael@0 | 1470 | messageNode.setAttribute("private", true); |
michael@0 | 1471 | } |
michael@0 | 1472 | messageNode._connectionId = aActorId; |
michael@0 | 1473 | messageNode.url = request.url; |
michael@0 | 1474 | |
michael@0 | 1475 | let body = methodNode.parentNode; |
michael@0 | 1476 | body.setAttribute("aria-haspopup", true); |
michael@0 | 1477 | |
michael@0 | 1478 | let displayUrl = request.url; |
michael@0 | 1479 | let pos = displayUrl.indexOf("?"); |
michael@0 | 1480 | if (pos > -1) { |
michael@0 | 1481 | displayUrl = displayUrl.substr(0, pos); |
michael@0 | 1482 | } |
michael@0 | 1483 | |
michael@0 | 1484 | let urlNode = this.document.createElementNS(XHTML_NS, "a"); |
michael@0 | 1485 | urlNode.className = "url"; |
michael@0 | 1486 | urlNode.setAttribute("title", request.url); |
michael@0 | 1487 | urlNode.href = request.url; |
michael@0 | 1488 | urlNode.textContent = displayUrl; |
michael@0 | 1489 | urlNode.draggable = false; |
michael@0 | 1490 | body.appendChild(urlNode); |
michael@0 | 1491 | body.appendChild(this.document.createTextNode(" ")); |
michael@0 | 1492 | |
michael@0 | 1493 | if (mixedRequest) { |
michael@0 | 1494 | messageNode.classList.add("mixed-content"); |
michael@0 | 1495 | this.makeMixedContentNode(body); |
michael@0 | 1496 | } |
michael@0 | 1497 | |
michael@0 | 1498 | let statusNode = this.document.createElementNS(XHTML_NS, "a"); |
michael@0 | 1499 | statusNode.className = "status"; |
michael@0 | 1500 | body.appendChild(statusNode); |
michael@0 | 1501 | |
michael@0 | 1502 | let onClick = () => { |
michael@0 | 1503 | if (!messageNode._panelOpen) { |
michael@0 | 1504 | this.openNetworkPanel(messageNode, networkInfo); |
michael@0 | 1505 | } |
michael@0 | 1506 | }; |
michael@0 | 1507 | |
michael@0 | 1508 | this._addMessageLinkCallback(urlNode, onClick); |
michael@0 | 1509 | this._addMessageLinkCallback(statusNode, onClick); |
michael@0 | 1510 | |
michael@0 | 1511 | networkInfo.node = messageNode; |
michael@0 | 1512 | |
michael@0 | 1513 | this._updateNetMessage(aActorId); |
michael@0 | 1514 | |
michael@0 | 1515 | return messageNode; |
michael@0 | 1516 | }, |
michael@0 | 1517 | |
michael@0 | 1518 | /** |
michael@0 | 1519 | * Create a mixed content warning Node. |
michael@0 | 1520 | * |
michael@0 | 1521 | * @param aLinkNode |
michael@0 | 1522 | * Parent to the requested urlNode. |
michael@0 | 1523 | */ |
michael@0 | 1524 | makeMixedContentNode: function WCF_makeMixedContentNode(aLinkNode) |
michael@0 | 1525 | { |
michael@0 | 1526 | let mixedContentWarning = "[" + l10n.getStr("webConsoleMixedContentWarning") + "]"; |
michael@0 | 1527 | |
michael@0 | 1528 | // Mixed content warning message links to a Learn More page |
michael@0 | 1529 | let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a"); |
michael@0 | 1530 | mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE; |
michael@0 | 1531 | mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE; |
michael@0 | 1532 | mixedContentWarningNode.className = "learn-more-link"; |
michael@0 | 1533 | mixedContentWarningNode.textContent = mixedContentWarning; |
michael@0 | 1534 | mixedContentWarningNode.draggable = false; |
michael@0 | 1535 | |
michael@0 | 1536 | aLinkNode.appendChild(mixedContentWarningNode); |
michael@0 | 1537 | |
michael@0 | 1538 | this._addMessageLinkCallback(mixedContentWarningNode, (aEvent) => { |
michael@0 | 1539 | aEvent.stopPropagation(); |
michael@0 | 1540 | this.owner.openLink(MIXED_CONTENT_LEARN_MORE); |
michael@0 | 1541 | }); |
michael@0 | 1542 | }, |
michael@0 | 1543 | |
michael@0 | 1544 | /** |
michael@0 | 1545 | * Adds a more info link node to messages based on the nsIScriptError object |
michael@0 | 1546 | * that we need to report to the console |
michael@0 | 1547 | * |
michael@0 | 1548 | * @param aNode |
michael@0 | 1549 | * The node to which we will be adding the more info link node |
michael@0 | 1550 | * @param aScriptError |
michael@0 | 1551 | * The script error object that we are reporting to the console |
michael@0 | 1552 | */ |
michael@0 | 1553 | addMoreInfoLink: function WCF_addMoreInfoLink(aNode, aScriptError) |
michael@0 | 1554 | { |
michael@0 | 1555 | let url; |
michael@0 | 1556 | switch (aScriptError.category) { |
michael@0 | 1557 | case "Insecure Password Field": |
michael@0 | 1558 | url = INSECURE_PASSWORDS_LEARN_MORE; |
michael@0 | 1559 | break; |
michael@0 | 1560 | case "Mixed Content Message": |
michael@0 | 1561 | case "Mixed Content Blocker": |
michael@0 | 1562 | url = MIXED_CONTENT_LEARN_MORE; |
michael@0 | 1563 | break; |
michael@0 | 1564 | case "Invalid HSTS Headers": |
michael@0 | 1565 | url = STRICT_TRANSPORT_SECURITY_LEARN_MORE; |
michael@0 | 1566 | break; |
michael@0 | 1567 | default: |
michael@0 | 1568 | // Unknown category. Return without adding more info node. |
michael@0 | 1569 | return; |
michael@0 | 1570 | } |
michael@0 | 1571 | |
michael@0 | 1572 | this.addLearnMoreWarningNode(aNode, url); |
michael@0 | 1573 | }, |
michael@0 | 1574 | |
michael@0 | 1575 | /* |
michael@0 | 1576 | * Appends a clickable warning node to the node passed |
michael@0 | 1577 | * as a parameter to the function. When a user clicks on the appended |
michael@0 | 1578 | * warning node, the browser navigates to the provided url. |
michael@0 | 1579 | * |
michael@0 | 1580 | * @param aNode |
michael@0 | 1581 | * The node to which we will be adding a clickable warning node. |
michael@0 | 1582 | * @param aURL |
michael@0 | 1583 | * The url which points to the page where the user can learn more |
michael@0 | 1584 | * about security issues associated with the specific message that's |
michael@0 | 1585 | * being logged. |
michael@0 | 1586 | */ |
michael@0 | 1587 | addLearnMoreWarningNode: |
michael@0 | 1588 | function WCF_addLearnMoreWarningNode(aNode, aURL) |
michael@0 | 1589 | { |
michael@0 | 1590 | let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]"; |
michael@0 | 1591 | |
michael@0 | 1592 | let warningNode = this.document.createElementNS(XHTML_NS, "a"); |
michael@0 | 1593 | warningNode.title = aURL; |
michael@0 | 1594 | warningNode.href = aURL; |
michael@0 | 1595 | warningNode.draggable = false; |
michael@0 | 1596 | warningNode.textContent = moreInfoLabel; |
michael@0 | 1597 | warningNode.className = "learn-more-link"; |
michael@0 | 1598 | |
michael@0 | 1599 | this._addMessageLinkCallback(warningNode, (aEvent) => { |
michael@0 | 1600 | aEvent.stopPropagation(); |
michael@0 | 1601 | this.owner.openLink(aURL); |
michael@0 | 1602 | }); |
michael@0 | 1603 | |
michael@0 | 1604 | aNode.appendChild(warningNode); |
michael@0 | 1605 | }, |
michael@0 | 1606 | |
michael@0 | 1607 | /** |
michael@0 | 1608 | * Log file activity. |
michael@0 | 1609 | * |
michael@0 | 1610 | * @param string aFileURI |
michael@0 | 1611 | * The file URI that was loaded. |
michael@0 | 1612 | * @return nsIDOMElement|undefined |
michael@0 | 1613 | * The message element to display in the Web Console output. |
michael@0 | 1614 | */ |
michael@0 | 1615 | logFileActivity: function WCF_logFileActivity(aFileURI) |
michael@0 | 1616 | { |
michael@0 | 1617 | let urlNode = this.document.createElementNS(XHTML_NS, "a"); |
michael@0 | 1618 | urlNode.setAttribute("title", aFileURI); |
michael@0 | 1619 | urlNode.className = "url"; |
michael@0 | 1620 | urlNode.textContent = aFileURI; |
michael@0 | 1621 | urlNode.draggable = false; |
michael@0 | 1622 | urlNode.href = aFileURI; |
michael@0 | 1623 | |
michael@0 | 1624 | let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG, |
michael@0 | 1625 | urlNode, null, null, aFileURI); |
michael@0 | 1626 | |
michael@0 | 1627 | this._addMessageLinkCallback(urlNode, () => { |
michael@0 | 1628 | this.owner.viewSource(aFileURI); |
michael@0 | 1629 | }); |
michael@0 | 1630 | |
michael@0 | 1631 | return outputNode; |
michael@0 | 1632 | }, |
michael@0 | 1633 | |
michael@0 | 1634 | /** |
michael@0 | 1635 | * Handle the file activity messages coming from the remote Web Console. |
michael@0 | 1636 | * |
michael@0 | 1637 | * @param string aFileURI |
michael@0 | 1638 | * The file URI that was requested. |
michael@0 | 1639 | */ |
michael@0 | 1640 | handleFileActivity: function WCF_handleFileActivity(aFileURI) |
michael@0 | 1641 | { |
michael@0 | 1642 | this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [aFileURI]); |
michael@0 | 1643 | }, |
michael@0 | 1644 | |
michael@0 | 1645 | /** |
michael@0 | 1646 | * Handle the reflow activity messages coming from the remote Web Console. |
michael@0 | 1647 | * |
michael@0 | 1648 | * @param object aMessage |
michael@0 | 1649 | * An object holding information about a reflow batch. |
michael@0 | 1650 | */ |
michael@0 | 1651 | logReflowActivity: function WCF_logReflowActivity(aMessage) |
michael@0 | 1652 | { |
michael@0 | 1653 | let {start, end, sourceURL, sourceLine} = aMessage; |
michael@0 | 1654 | let duration = Math.round((end - start) * 100) / 100; |
michael@0 | 1655 | let node = this.document.createElementNS(XHTML_NS, "span"); |
michael@0 | 1656 | if (sourceURL) { |
michael@0 | 1657 | node.textContent = l10n.getFormatStr("reflow.messageWithLink", [duration]); |
michael@0 | 1658 | let a = this.document.createElementNS(XHTML_NS, "a"); |
michael@0 | 1659 | a.href = "#"; |
michael@0 | 1660 | a.draggable = "false"; |
michael@0 | 1661 | let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL); |
michael@0 | 1662 | let functionName = aMessage.functionName || l10n.getStr("stacktrace.anonymousFunction"); |
michael@0 | 1663 | a.textContent = l10n.getFormatStr("reflow.messageLinkText", |
michael@0 | 1664 | [functionName, filename, sourceLine]); |
michael@0 | 1665 | this._addMessageLinkCallback(a, () => { |
michael@0 | 1666 | this.owner.viewSourceInDebugger(sourceURL, sourceLine); |
michael@0 | 1667 | }); |
michael@0 | 1668 | node.appendChild(a); |
michael@0 | 1669 | } else { |
michael@0 | 1670 | node.textContent = l10n.getFormatStr("reflow.messageWithNoLink", [duration]); |
michael@0 | 1671 | } |
michael@0 | 1672 | return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node); |
michael@0 | 1673 | }, |
michael@0 | 1674 | |
michael@0 | 1675 | |
michael@0 | 1676 | handleReflowActivity: function WCF_handleReflowActivity(aMessage) |
michael@0 | 1677 | { |
michael@0 | 1678 | this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [aMessage]); |
michael@0 | 1679 | }, |
michael@0 | 1680 | |
michael@0 | 1681 | /** |
michael@0 | 1682 | * Inform user that the window.console API has been replaced by a script |
michael@0 | 1683 | * in a content page. |
michael@0 | 1684 | */ |
michael@0 | 1685 | logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI() |
michael@0 | 1686 | { |
michael@0 | 1687 | let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, |
michael@0 | 1688 | l10n.getStr("ConsoleAPIDisabled")); |
michael@0 | 1689 | this.outputMessage(CATEGORY_JS, node); |
michael@0 | 1690 | }, |
michael@0 | 1691 | |
michael@0 | 1692 | /** |
michael@0 | 1693 | * Handle the network events coming from the remote Web Console. |
michael@0 | 1694 | * |
michael@0 | 1695 | * @param object aActor |
michael@0 | 1696 | * The NetworkEventActor grip. |
michael@0 | 1697 | */ |
michael@0 | 1698 | handleNetworkEvent: function WCF_handleNetworkEvent(aActor) |
michael@0 | 1699 | { |
michael@0 | 1700 | let networkInfo = { |
michael@0 | 1701 | node: null, |
michael@0 | 1702 | actor: aActor.actor, |
michael@0 | 1703 | discardRequestBody: true, |
michael@0 | 1704 | discardResponseBody: true, |
michael@0 | 1705 | startedDateTime: aActor.startedDateTime, |
michael@0 | 1706 | request: { |
michael@0 | 1707 | url: aActor.url, |
michael@0 | 1708 | method: aActor.method, |
michael@0 | 1709 | }, |
michael@0 | 1710 | response: {}, |
michael@0 | 1711 | timings: {}, |
michael@0 | 1712 | updates: [], // track the list of network event updates |
michael@0 | 1713 | private: aActor.private, |
michael@0 | 1714 | }; |
michael@0 | 1715 | |
michael@0 | 1716 | this._networkRequests[aActor.actor] = networkInfo; |
michael@0 | 1717 | this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor.actor]); |
michael@0 | 1718 | }, |
michael@0 | 1719 | |
michael@0 | 1720 | /** |
michael@0 | 1721 | * Handle network event updates coming from the server. |
michael@0 | 1722 | * |
michael@0 | 1723 | * @param string aActorId |
michael@0 | 1724 | * The network event actor ID. |
michael@0 | 1725 | * @param string aType |
michael@0 | 1726 | * Update type. |
michael@0 | 1727 | * @param object aPacket |
michael@0 | 1728 | * Update details. |
michael@0 | 1729 | */ |
michael@0 | 1730 | handleNetworkEventUpdate: |
michael@0 | 1731 | function WCF_handleNetworkEventUpdate(aActorId, aType, aPacket) |
michael@0 | 1732 | { |
michael@0 | 1733 | let networkInfo = this._networkRequests[aActorId]; |
michael@0 | 1734 | if (!networkInfo) { |
michael@0 | 1735 | return; |
michael@0 | 1736 | } |
michael@0 | 1737 | |
michael@0 | 1738 | networkInfo.updates.push(aType); |
michael@0 | 1739 | |
michael@0 | 1740 | switch (aType) { |
michael@0 | 1741 | case "requestHeaders": |
michael@0 | 1742 | networkInfo.request.headersSize = aPacket.headersSize; |
michael@0 | 1743 | break; |
michael@0 | 1744 | case "requestPostData": |
michael@0 | 1745 | networkInfo.discardRequestBody = aPacket.discardRequestBody; |
michael@0 | 1746 | networkInfo.request.bodySize = aPacket.dataSize; |
michael@0 | 1747 | break; |
michael@0 | 1748 | case "responseStart": |
michael@0 | 1749 | networkInfo.response.httpVersion = aPacket.response.httpVersion; |
michael@0 | 1750 | networkInfo.response.status = aPacket.response.status; |
michael@0 | 1751 | networkInfo.response.statusText = aPacket.response.statusText; |
michael@0 | 1752 | networkInfo.response.headersSize = aPacket.response.headersSize; |
michael@0 | 1753 | networkInfo.discardResponseBody = aPacket.response.discardResponseBody; |
michael@0 | 1754 | break; |
michael@0 | 1755 | case "responseContent": |
michael@0 | 1756 | networkInfo.response.content = { |
michael@0 | 1757 | mimeType: aPacket.mimeType, |
michael@0 | 1758 | }; |
michael@0 | 1759 | networkInfo.response.bodySize = aPacket.contentSize; |
michael@0 | 1760 | networkInfo.discardResponseBody = aPacket.discardResponseBody; |
michael@0 | 1761 | break; |
michael@0 | 1762 | case "eventTimings": |
michael@0 | 1763 | networkInfo.totalTime = aPacket.totalTime; |
michael@0 | 1764 | break; |
michael@0 | 1765 | } |
michael@0 | 1766 | |
michael@0 | 1767 | if (networkInfo.node && this._updateNetMessage(aActorId)) { |
michael@0 | 1768 | this.emit("messages-updated", new Set([networkInfo.node])); |
michael@0 | 1769 | } |
michael@0 | 1770 | |
michael@0 | 1771 | // For unit tests we pass the HTTP activity object to the test callback, |
michael@0 | 1772 | // once requests complete. |
michael@0 | 1773 | if (this.owner.lastFinishedRequestCallback && |
michael@0 | 1774 | networkInfo.updates.indexOf("responseContent") > -1 && |
michael@0 | 1775 | networkInfo.updates.indexOf("eventTimings") > -1) { |
michael@0 | 1776 | this.owner.lastFinishedRequestCallback(networkInfo, this); |
michael@0 | 1777 | } |
michael@0 | 1778 | }, |
michael@0 | 1779 | |
michael@0 | 1780 | /** |
michael@0 | 1781 | * Update an output message to reflect the latest state of a network request, |
michael@0 | 1782 | * given a network event actor ID. |
michael@0 | 1783 | * |
michael@0 | 1784 | * @private |
michael@0 | 1785 | * @param string aActorId |
michael@0 | 1786 | * The network event actor ID for which you want to update the message. |
michael@0 | 1787 | * @return boolean |
michael@0 | 1788 | * |true| if the message node was updated, or |false| otherwise. |
michael@0 | 1789 | */ |
michael@0 | 1790 | _updateNetMessage: function WCF__updateNetMessage(aActorId) |
michael@0 | 1791 | { |
michael@0 | 1792 | let networkInfo = this._networkRequests[aActorId]; |
michael@0 | 1793 | if (!networkInfo || !networkInfo.node) { |
michael@0 | 1794 | return; |
michael@0 | 1795 | } |
michael@0 | 1796 | |
michael@0 | 1797 | let messageNode = networkInfo.node; |
michael@0 | 1798 | let updates = networkInfo.updates; |
michael@0 | 1799 | let hasEventTimings = updates.indexOf("eventTimings") > -1; |
michael@0 | 1800 | let hasResponseStart = updates.indexOf("responseStart") > -1; |
michael@0 | 1801 | let request = networkInfo.request; |
michael@0 | 1802 | let response = networkInfo.response; |
michael@0 | 1803 | let updated = false; |
michael@0 | 1804 | |
michael@0 | 1805 | if (hasEventTimings || hasResponseStart) { |
michael@0 | 1806 | let status = []; |
michael@0 | 1807 | if (response.httpVersion && response.status) { |
michael@0 | 1808 | status = [response.httpVersion, response.status, response.statusText]; |
michael@0 | 1809 | } |
michael@0 | 1810 | if (hasEventTimings) { |
michael@0 | 1811 | status.push(l10n.getFormatStr("NetworkPanel.durationMS", |
michael@0 | 1812 | [networkInfo.totalTime])); |
michael@0 | 1813 | } |
michael@0 | 1814 | let statusText = "[" + status.join(" ") + "]"; |
michael@0 | 1815 | |
michael@0 | 1816 | let statusNode = messageNode.getElementsByClassName("status")[0]; |
michael@0 | 1817 | statusNode.textContent = statusText; |
michael@0 | 1818 | |
michael@0 | 1819 | messageNode.clipboardText = [request.method, request.url, statusText] |
michael@0 | 1820 | .join(" "); |
michael@0 | 1821 | |
michael@0 | 1822 | if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE && |
michael@0 | 1823 | response.status <= MAX_HTTP_ERROR_CODE) { |
michael@0 | 1824 | this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR); |
michael@0 | 1825 | } |
michael@0 | 1826 | |
michael@0 | 1827 | updated = true; |
michael@0 | 1828 | } |
michael@0 | 1829 | |
michael@0 | 1830 | if (messageNode._netPanel) { |
michael@0 | 1831 | messageNode._netPanel.update(); |
michael@0 | 1832 | } |
michael@0 | 1833 | |
michael@0 | 1834 | return updated; |
michael@0 | 1835 | }, |
michael@0 | 1836 | |
michael@0 | 1837 | /** |
michael@0 | 1838 | * Opens a NetworkPanel. |
michael@0 | 1839 | * |
michael@0 | 1840 | * @param nsIDOMNode aNode |
michael@0 | 1841 | * The message node you want the panel to be anchored to. |
michael@0 | 1842 | * @param object aHttpActivity |
michael@0 | 1843 | * The HTTP activity object that holds network request and response |
michael@0 | 1844 | * information. This object is given to the NetworkPanel constructor. |
michael@0 | 1845 | * @return object |
michael@0 | 1846 | * The new NetworkPanel instance. |
michael@0 | 1847 | */ |
michael@0 | 1848 | openNetworkPanel: function WCF_openNetworkPanel(aNode, aHttpActivity) |
michael@0 | 1849 | { |
michael@0 | 1850 | let actor = aHttpActivity.actor; |
michael@0 | 1851 | |
michael@0 | 1852 | if (actor) { |
michael@0 | 1853 | this.webConsoleClient.getRequestHeaders(actor, function(aResponse) { |
michael@0 | 1854 | if (aResponse.error) { |
michael@0 | 1855 | Cu.reportError("WCF_openNetworkPanel getRequestHeaders:" + |
michael@0 | 1856 | aResponse.error); |
michael@0 | 1857 | return; |
michael@0 | 1858 | } |
michael@0 | 1859 | |
michael@0 | 1860 | aHttpActivity.request.headers = aResponse.headers; |
michael@0 | 1861 | |
michael@0 | 1862 | this.webConsoleClient.getRequestCookies(actor, onRequestCookies); |
michael@0 | 1863 | }.bind(this)); |
michael@0 | 1864 | } |
michael@0 | 1865 | |
michael@0 | 1866 | let onRequestCookies = function(aResponse) { |
michael@0 | 1867 | if (aResponse.error) { |
michael@0 | 1868 | Cu.reportError("WCF_openNetworkPanel getRequestCookies:" + |
michael@0 | 1869 | aResponse.error); |
michael@0 | 1870 | return; |
michael@0 | 1871 | } |
michael@0 | 1872 | |
michael@0 | 1873 | aHttpActivity.request.cookies = aResponse.cookies; |
michael@0 | 1874 | |
michael@0 | 1875 | this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders); |
michael@0 | 1876 | }.bind(this); |
michael@0 | 1877 | |
michael@0 | 1878 | let onResponseHeaders = function(aResponse) { |
michael@0 | 1879 | if (aResponse.error) { |
michael@0 | 1880 | Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" + |
michael@0 | 1881 | aResponse.error); |
michael@0 | 1882 | return; |
michael@0 | 1883 | } |
michael@0 | 1884 | |
michael@0 | 1885 | aHttpActivity.response.headers = aResponse.headers; |
michael@0 | 1886 | |
michael@0 | 1887 | this.webConsoleClient.getResponseCookies(actor, onResponseCookies); |
michael@0 | 1888 | }.bind(this); |
michael@0 | 1889 | |
michael@0 | 1890 | let onResponseCookies = function(aResponse) { |
michael@0 | 1891 | if (aResponse.error) { |
michael@0 | 1892 | Cu.reportError("WCF_openNetworkPanel getResponseCookies:" + |
michael@0 | 1893 | aResponse.error); |
michael@0 | 1894 | return; |
michael@0 | 1895 | } |
michael@0 | 1896 | |
michael@0 | 1897 | aHttpActivity.response.cookies = aResponse.cookies; |
michael@0 | 1898 | |
michael@0 | 1899 | this.webConsoleClient.getRequestPostData(actor, onRequestPostData); |
michael@0 | 1900 | }.bind(this); |
michael@0 | 1901 | |
michael@0 | 1902 | let onRequestPostData = function(aResponse) { |
michael@0 | 1903 | if (aResponse.error) { |
michael@0 | 1904 | Cu.reportError("WCF_openNetworkPanel getRequestPostData:" + |
michael@0 | 1905 | aResponse.error); |
michael@0 | 1906 | return; |
michael@0 | 1907 | } |
michael@0 | 1908 | |
michael@0 | 1909 | aHttpActivity.request.postData = aResponse.postData; |
michael@0 | 1910 | aHttpActivity.discardRequestBody = aResponse.postDataDiscarded; |
michael@0 | 1911 | |
michael@0 | 1912 | this.webConsoleClient.getResponseContent(actor, onResponseContent); |
michael@0 | 1913 | }.bind(this); |
michael@0 | 1914 | |
michael@0 | 1915 | let onResponseContent = function(aResponse) { |
michael@0 | 1916 | if (aResponse.error) { |
michael@0 | 1917 | Cu.reportError("WCF_openNetworkPanel getResponseContent:" + |
michael@0 | 1918 | aResponse.error); |
michael@0 | 1919 | return; |
michael@0 | 1920 | } |
michael@0 | 1921 | |
michael@0 | 1922 | aHttpActivity.response.content = aResponse.content; |
michael@0 | 1923 | aHttpActivity.discardResponseBody = aResponse.contentDiscarded; |
michael@0 | 1924 | |
michael@0 | 1925 | this.webConsoleClient.getEventTimings(actor, onEventTimings); |
michael@0 | 1926 | }.bind(this); |
michael@0 | 1927 | |
michael@0 | 1928 | let onEventTimings = function(aResponse) { |
michael@0 | 1929 | if (aResponse.error) { |
michael@0 | 1930 | Cu.reportError("WCF_openNetworkPanel getEventTimings:" + |
michael@0 | 1931 | aResponse.error); |
michael@0 | 1932 | return; |
michael@0 | 1933 | } |
michael@0 | 1934 | |
michael@0 | 1935 | aHttpActivity.timings = aResponse.timings; |
michael@0 | 1936 | |
michael@0 | 1937 | openPanel(); |
michael@0 | 1938 | }.bind(this); |
michael@0 | 1939 | |
michael@0 | 1940 | let openPanel = function() { |
michael@0 | 1941 | aNode._netPanel = netPanel; |
michael@0 | 1942 | |
michael@0 | 1943 | let panel = netPanel.panel; |
michael@0 | 1944 | panel.openPopup(aNode, "after_pointer", 0, 0, false, false); |
michael@0 | 1945 | panel.sizeTo(450, 500); |
michael@0 | 1946 | panel.setAttribute("hudId", this.hudId); |
michael@0 | 1947 | |
michael@0 | 1948 | panel.addEventListener("popuphiding", function WCF_netPanel_onHide() { |
michael@0 | 1949 | panel.removeEventListener("popuphiding", WCF_netPanel_onHide); |
michael@0 | 1950 | |
michael@0 | 1951 | aNode._panelOpen = false; |
michael@0 | 1952 | aNode._netPanel = null; |
michael@0 | 1953 | }); |
michael@0 | 1954 | |
michael@0 | 1955 | aNode._panelOpen = true; |
michael@0 | 1956 | }.bind(this); |
michael@0 | 1957 | |
michael@0 | 1958 | let netPanel = new NetworkPanel(this.popupset, aHttpActivity, this); |
michael@0 | 1959 | netPanel.linkNode = aNode; |
michael@0 | 1960 | |
michael@0 | 1961 | if (!actor) { |
michael@0 | 1962 | openPanel(); |
michael@0 | 1963 | } |
michael@0 | 1964 | |
michael@0 | 1965 | return netPanel; |
michael@0 | 1966 | }, |
michael@0 | 1967 | |
michael@0 | 1968 | /** |
michael@0 | 1969 | * Handler for page location changes. |
michael@0 | 1970 | * |
michael@0 | 1971 | * @param string aURI |
michael@0 | 1972 | * New page location. |
michael@0 | 1973 | * @param string aTitle |
michael@0 | 1974 | * New page title. |
michael@0 | 1975 | */ |
michael@0 | 1976 | onLocationChange: function WCF_onLocationChange(aURI, aTitle) |
michael@0 | 1977 | { |
michael@0 | 1978 | this.contentLocation = aURI; |
michael@0 | 1979 | if (this.owner.onLocationChange) { |
michael@0 | 1980 | this.owner.onLocationChange(aURI, aTitle); |
michael@0 | 1981 | } |
michael@0 | 1982 | }, |
michael@0 | 1983 | |
michael@0 | 1984 | /** |
michael@0 | 1985 | * Handler for the tabNavigated notification. |
michael@0 | 1986 | * |
michael@0 | 1987 | * @param string aEvent |
michael@0 | 1988 | * Event name. |
michael@0 | 1989 | * @param object aPacket |
michael@0 | 1990 | * Notification packet received from the server. |
michael@0 | 1991 | */ |
michael@0 | 1992 | handleTabNavigated: function WCF_handleTabNavigated(aEvent, aPacket) |
michael@0 | 1993 | { |
michael@0 | 1994 | if (aEvent == "will-navigate") { |
michael@0 | 1995 | if (this.persistLog) { |
michael@0 | 1996 | let marker = new Messages.NavigationMarker(aPacket.url, Date.now()); |
michael@0 | 1997 | this.output.addMessage(marker); |
michael@0 | 1998 | } |
michael@0 | 1999 | else { |
michael@0 | 2000 | this.jsterm.clearOutput(); |
michael@0 | 2001 | } |
michael@0 | 2002 | } |
michael@0 | 2003 | |
michael@0 | 2004 | if (aPacket.url) { |
michael@0 | 2005 | this.onLocationChange(aPacket.url, aPacket.title); |
michael@0 | 2006 | } |
michael@0 | 2007 | |
michael@0 | 2008 | if (aEvent == "navigate" && !aPacket.nativeConsoleAPI) { |
michael@0 | 2009 | this.logWarningAboutReplacedAPI(); |
michael@0 | 2010 | } |
michael@0 | 2011 | }, |
michael@0 | 2012 | |
michael@0 | 2013 | /** |
michael@0 | 2014 | * Output a message node. This filters a node appropriately, then sends it to |
michael@0 | 2015 | * the output, regrouping and pruning output as necessary. |
michael@0 | 2016 | * |
michael@0 | 2017 | * Note: this call is async - the given message node may not be displayed when |
michael@0 | 2018 | * you call this method. |
michael@0 | 2019 | * |
michael@0 | 2020 | * @param integer aCategory |
michael@0 | 2021 | * The category of the message you want to output. See the CATEGORY_* |
michael@0 | 2022 | * constants. |
michael@0 | 2023 | * @param function|nsIDOMElement aMethodOrNode |
michael@0 | 2024 | * The method that creates the message element to send to the output or |
michael@0 | 2025 | * the actual element. If a method is given it will be bound to the HUD |
michael@0 | 2026 | * object and the arguments will be |aArguments|. |
michael@0 | 2027 | * @param array [aArguments] |
michael@0 | 2028 | * If a method is given to output the message element then the method |
michael@0 | 2029 | * will be invoked with the list of arguments given here. |
michael@0 | 2030 | */ |
michael@0 | 2031 | outputMessage: function WCF_outputMessage(aCategory, aMethodOrNode, aArguments) |
michael@0 | 2032 | { |
michael@0 | 2033 | if (!this._outputQueue.length) { |
michael@0 | 2034 | // If the queue is empty we consider that now was the last output flush. |
michael@0 | 2035 | // This avoid an immediate output flush when the timer executes. |
michael@0 | 2036 | this._lastOutputFlush = Date.now(); |
michael@0 | 2037 | } |
michael@0 | 2038 | |
michael@0 | 2039 | this._outputQueue.push([aCategory, aMethodOrNode, aArguments]); |
michael@0 | 2040 | |
michael@0 | 2041 | if (!this._outputTimerInitialized) { |
michael@0 | 2042 | this._initOutputTimer(); |
michael@0 | 2043 | } |
michael@0 | 2044 | }, |
michael@0 | 2045 | |
michael@0 | 2046 | /** |
michael@0 | 2047 | * Try to flush the output message queue. This takes the messages in the |
michael@0 | 2048 | * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL. |
michael@0 | 2049 | * Further output is queued to happen later - see OUTPUT_INTERVAL. |
michael@0 | 2050 | * |
michael@0 | 2051 | * @private |
michael@0 | 2052 | */ |
michael@0 | 2053 | _flushMessageQueue: function WCF__flushMessageQueue() |
michael@0 | 2054 | { |
michael@0 | 2055 | if (!this._outputTimer) { |
michael@0 | 2056 | return; |
michael@0 | 2057 | } |
michael@0 | 2058 | |
michael@0 | 2059 | let timeSinceFlush = Date.now() - this._lastOutputFlush; |
michael@0 | 2060 | if (this._outputQueue.length > MESSAGES_IN_INTERVAL && |
michael@0 | 2061 | timeSinceFlush < THROTTLE_UPDATES) { |
michael@0 | 2062 | this._initOutputTimer(); |
michael@0 | 2063 | return; |
michael@0 | 2064 | } |
michael@0 | 2065 | |
michael@0 | 2066 | // Determine how many messages we can display now. |
michael@0 | 2067 | let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL); |
michael@0 | 2068 | if (toDisplay < 1) { |
michael@0 | 2069 | this._outputTimerInitialized = false; |
michael@0 | 2070 | return; |
michael@0 | 2071 | } |
michael@0 | 2072 | |
michael@0 | 2073 | // Try to prune the message queue. |
michael@0 | 2074 | let shouldPrune = false; |
michael@0 | 2075 | if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) { |
michael@0 | 2076 | toDisplay = Math.min(this._outputQueue.length, toDisplay); |
michael@0 | 2077 | shouldPrune = true; |
michael@0 | 2078 | } |
michael@0 | 2079 | |
michael@0 | 2080 | let batch = this._outputQueue.splice(0, toDisplay); |
michael@0 | 2081 | if (!batch.length) { |
michael@0 | 2082 | this._outputTimerInitialized = false; |
michael@0 | 2083 | return; |
michael@0 | 2084 | } |
michael@0 | 2085 | |
michael@0 | 2086 | let outputNode = this.outputNode; |
michael@0 | 2087 | let lastVisibleNode = null; |
michael@0 | 2088 | let scrollNode = outputNode.parentNode; |
michael@0 | 2089 | let scrolledToBottom = Utils.isOutputScrolledToBottom(outputNode); |
michael@0 | 2090 | let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId); |
michael@0 | 2091 | |
michael@0 | 2092 | // Output the current batch of messages. |
michael@0 | 2093 | let newMessages = new Set(); |
michael@0 | 2094 | let updatedMessages = new Set(); |
michael@0 | 2095 | for (let item of batch) { |
michael@0 | 2096 | let result = this._outputMessageFromQueue(hudIdSupportsString, item); |
michael@0 | 2097 | if (result) { |
michael@0 | 2098 | if (result.isRepeated) { |
michael@0 | 2099 | updatedMessages.add(result.isRepeated); |
michael@0 | 2100 | } |
michael@0 | 2101 | else { |
michael@0 | 2102 | newMessages.add(result.node); |
michael@0 | 2103 | } |
michael@0 | 2104 | if (result.visible && result.node == this.outputNode.lastChild) { |
michael@0 | 2105 | lastVisibleNode = result.node; |
michael@0 | 2106 | } |
michael@0 | 2107 | } |
michael@0 | 2108 | } |
michael@0 | 2109 | |
michael@0 | 2110 | let oldScrollHeight = 0; |
michael@0 | 2111 | |
michael@0 | 2112 | // Prune messages if needed. We do not do this for every flush call to |
michael@0 | 2113 | // improve performance. |
michael@0 | 2114 | let removedNodes = 0; |
michael@0 | 2115 | if (shouldPrune || !this._outputQueue.length) { |
michael@0 | 2116 | oldScrollHeight = scrollNode.scrollHeight; |
michael@0 | 2117 | |
michael@0 | 2118 | let categories = Object.keys(this._pruneCategoriesQueue); |
michael@0 | 2119 | categories.forEach(function _pruneOutput(aCategory) { |
michael@0 | 2120 | removedNodes += this.pruneOutputIfNecessary(aCategory); |
michael@0 | 2121 | }, this); |
michael@0 | 2122 | this._pruneCategoriesQueue = {}; |
michael@0 | 2123 | } |
michael@0 | 2124 | |
michael@0 | 2125 | let isInputOutput = lastVisibleNode && |
michael@0 | 2126 | (lastVisibleNode.category == CATEGORY_INPUT || |
michael@0 | 2127 | lastVisibleNode.category == CATEGORY_OUTPUT); |
michael@0 | 2128 | |
michael@0 | 2129 | // Scroll to the new node if it is not filtered, and if the output node is |
michael@0 | 2130 | // scrolled at the bottom or if the new node is a jsterm input/output |
michael@0 | 2131 | // message. |
michael@0 | 2132 | if (lastVisibleNode && (scrolledToBottom || isInputOutput)) { |
michael@0 | 2133 | Utils.scrollToVisible(lastVisibleNode); |
michael@0 | 2134 | } |
michael@0 | 2135 | else if (!scrolledToBottom && removedNodes > 0 && |
michael@0 | 2136 | oldScrollHeight != scrollNode.scrollHeight) { |
michael@0 | 2137 | // If there were pruned messages and if scroll is not at the bottom, then |
michael@0 | 2138 | // we need to adjust the scroll location. |
michael@0 | 2139 | scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight; |
michael@0 | 2140 | } |
michael@0 | 2141 | |
michael@0 | 2142 | if (newMessages.size) { |
michael@0 | 2143 | this.emit("messages-added", newMessages); |
michael@0 | 2144 | } |
michael@0 | 2145 | if (updatedMessages.size) { |
michael@0 | 2146 | this.emit("messages-updated", updatedMessages); |
michael@0 | 2147 | } |
michael@0 | 2148 | |
michael@0 | 2149 | // If the queue is not empty, schedule another flush. |
michael@0 | 2150 | if (this._outputQueue.length > 0) { |
michael@0 | 2151 | this._initOutputTimer(); |
michael@0 | 2152 | } |
michael@0 | 2153 | else { |
michael@0 | 2154 | this._outputTimerInitialized = false; |
michael@0 | 2155 | if (this._flushCallback && this._flushCallback() === false) { |
michael@0 | 2156 | this._flushCallback = null; |
michael@0 | 2157 | } |
michael@0 | 2158 | } |
michael@0 | 2159 | |
michael@0 | 2160 | this._lastOutputFlush = Date.now(); |
michael@0 | 2161 | }, |
michael@0 | 2162 | |
michael@0 | 2163 | /** |
michael@0 | 2164 | * Initialize the output timer. |
michael@0 | 2165 | * @private |
michael@0 | 2166 | */ |
michael@0 | 2167 | _initOutputTimer: function WCF__initOutputTimer() |
michael@0 | 2168 | { |
michael@0 | 2169 | if (!this._outputTimer) { |
michael@0 | 2170 | return; |
michael@0 | 2171 | } |
michael@0 | 2172 | |
michael@0 | 2173 | this._outputTimerInitialized = true; |
michael@0 | 2174 | this._outputTimer.initWithCallback(this._flushMessageQueue, |
michael@0 | 2175 | OUTPUT_INTERVAL, |
michael@0 | 2176 | Ci.nsITimer.TYPE_ONE_SHOT); |
michael@0 | 2177 | }, |
michael@0 | 2178 | |
michael@0 | 2179 | /** |
michael@0 | 2180 | * Output a message from the queue. |
michael@0 | 2181 | * |
michael@0 | 2182 | * @private |
michael@0 | 2183 | * @param nsISupportsString aHudIdSupportsString |
michael@0 | 2184 | * The HUD ID as an nsISupportsString. |
michael@0 | 2185 | * @param array aItem |
michael@0 | 2186 | * An item from the output queue - this item represents a message. |
michael@0 | 2187 | * @return object |
michael@0 | 2188 | * An object that holds the following properties: |
michael@0 | 2189 | * - node: the DOM element of the message. |
michael@0 | 2190 | * - isRepeated: the DOM element of the original message, if this is |
michael@0 | 2191 | * a repeated message, otherwise null. |
michael@0 | 2192 | * - visible: boolean that tells if the message is visible. |
michael@0 | 2193 | */ |
michael@0 | 2194 | _outputMessageFromQueue: |
michael@0 | 2195 | function WCF__outputMessageFromQueue(aHudIdSupportsString, aItem) |
michael@0 | 2196 | { |
michael@0 | 2197 | let [category, methodOrNode, args] = aItem; |
michael@0 | 2198 | |
michael@0 | 2199 | let node = typeof methodOrNode == "function" ? |
michael@0 | 2200 | methodOrNode.apply(this, args || []) : |
michael@0 | 2201 | methodOrNode; |
michael@0 | 2202 | if (!node) { |
michael@0 | 2203 | return null; |
michael@0 | 2204 | } |
michael@0 | 2205 | |
michael@0 | 2206 | let afterNode = node._outputAfterNode; |
michael@0 | 2207 | if (afterNode) { |
michael@0 | 2208 | delete node._outputAfterNode; |
michael@0 | 2209 | } |
michael@0 | 2210 | |
michael@0 | 2211 | let isFiltered = this.filterMessageNode(node); |
michael@0 | 2212 | |
michael@0 | 2213 | let isRepeated = this._filterRepeatedMessage(node); |
michael@0 | 2214 | |
michael@0 | 2215 | let visible = !isRepeated && !isFiltered; |
michael@0 | 2216 | if (!isRepeated) { |
michael@0 | 2217 | this.outputNode.insertBefore(node, |
michael@0 | 2218 | afterNode ? afterNode.nextSibling : null); |
michael@0 | 2219 | this._pruneCategoriesQueue[node.category] = true; |
michael@0 | 2220 | |
michael@0 | 2221 | let nodeID = node.getAttribute("id"); |
michael@0 | 2222 | Services.obs.notifyObservers(aHudIdSupportsString, |
michael@0 | 2223 | "web-console-message-created", nodeID); |
michael@0 | 2224 | |
michael@0 | 2225 | } |
michael@0 | 2226 | |
michael@0 | 2227 | if (node._onOutput) { |
michael@0 | 2228 | node._onOutput(); |
michael@0 | 2229 | delete node._onOutput; |
michael@0 | 2230 | } |
michael@0 | 2231 | |
michael@0 | 2232 | return { |
michael@0 | 2233 | visible: visible, |
michael@0 | 2234 | node: node, |
michael@0 | 2235 | isRepeated: isRepeated, |
michael@0 | 2236 | }; |
michael@0 | 2237 | }, |
michael@0 | 2238 | |
michael@0 | 2239 | /** |
michael@0 | 2240 | * Prune the queue of messages to display. This avoids displaying messages |
michael@0 | 2241 | * that will be removed at the end of the queue anyway. |
michael@0 | 2242 | * @private |
michael@0 | 2243 | */ |
michael@0 | 2244 | _pruneOutputQueue: function WCF__pruneOutputQueue() |
michael@0 | 2245 | { |
michael@0 | 2246 | let nodes = {}; |
michael@0 | 2247 | |
michael@0 | 2248 | // Group the messages per category. |
michael@0 | 2249 | this._outputQueue.forEach(function(aItem, aIndex) { |
michael@0 | 2250 | let [category] = aItem; |
michael@0 | 2251 | if (!(category in nodes)) { |
michael@0 | 2252 | nodes[category] = []; |
michael@0 | 2253 | } |
michael@0 | 2254 | nodes[category].push(aIndex); |
michael@0 | 2255 | }, this); |
michael@0 | 2256 | |
michael@0 | 2257 | let pruned = 0; |
michael@0 | 2258 | |
michael@0 | 2259 | // Loop through the categories we found and prune if needed. |
michael@0 | 2260 | for (let category in nodes) { |
michael@0 | 2261 | let limit = Utils.logLimitForCategory(category); |
michael@0 | 2262 | let indexes = nodes[category]; |
michael@0 | 2263 | if (indexes.length > limit) { |
michael@0 | 2264 | let n = Math.max(0, indexes.length - limit); |
michael@0 | 2265 | pruned += n; |
michael@0 | 2266 | for (let i = n - 1; i >= 0; i--) { |
michael@0 | 2267 | this._pruneItemFromQueue(this._outputQueue[indexes[i]]); |
michael@0 | 2268 | this._outputQueue.splice(indexes[i], 1); |
michael@0 | 2269 | } |
michael@0 | 2270 | } |
michael@0 | 2271 | } |
michael@0 | 2272 | |
michael@0 | 2273 | return pruned; |
michael@0 | 2274 | }, |
michael@0 | 2275 | |
michael@0 | 2276 | /** |
michael@0 | 2277 | * Prune an item from the output queue. |
michael@0 | 2278 | * |
michael@0 | 2279 | * @private |
michael@0 | 2280 | * @param array aItem |
michael@0 | 2281 | * The item you want to remove from the output queue. |
michael@0 | 2282 | */ |
michael@0 | 2283 | _pruneItemFromQueue: function WCF__pruneItemFromQueue(aItem) |
michael@0 | 2284 | { |
michael@0 | 2285 | // TODO: handle object releasing in a more elegant way once all console |
michael@0 | 2286 | // messages use the new API - bug 778766. |
michael@0 | 2287 | |
michael@0 | 2288 | let [category, methodOrNode, args] = aItem; |
michael@0 | 2289 | if (typeof methodOrNode != "function" && methodOrNode._objectActors) { |
michael@0 | 2290 | for (let actor of methodOrNode._objectActors) { |
michael@0 | 2291 | this._releaseObject(actor); |
michael@0 | 2292 | } |
michael@0 | 2293 | methodOrNode._objectActors.clear(); |
michael@0 | 2294 | } |
michael@0 | 2295 | |
michael@0 | 2296 | if (methodOrNode == this.output._flushMessageQueue && |
michael@0 | 2297 | args[0]._objectActors) { |
michael@0 | 2298 | for (let arg of args) { |
michael@0 | 2299 | if (!arg._objectActors) { |
michael@0 | 2300 | continue; |
michael@0 | 2301 | } |
michael@0 | 2302 | for (let actor of arg._objectActors) { |
michael@0 | 2303 | this._releaseObject(actor); |
michael@0 | 2304 | } |
michael@0 | 2305 | arg._objectActors.clear(); |
michael@0 | 2306 | } |
michael@0 | 2307 | } |
michael@0 | 2308 | |
michael@0 | 2309 | if (category == CATEGORY_NETWORK) { |
michael@0 | 2310 | let connectionId = null; |
michael@0 | 2311 | if (methodOrNode == this.logNetEvent) { |
michael@0 | 2312 | connectionId = args[0]; |
michael@0 | 2313 | } |
michael@0 | 2314 | else if (typeof methodOrNode != "function") { |
michael@0 | 2315 | connectionId = methodOrNode._connectionId; |
michael@0 | 2316 | } |
michael@0 | 2317 | if (connectionId && connectionId in this._networkRequests) { |
michael@0 | 2318 | delete this._networkRequests[connectionId]; |
michael@0 | 2319 | this._releaseObject(connectionId); |
michael@0 | 2320 | } |
michael@0 | 2321 | } |
michael@0 | 2322 | else if (category == CATEGORY_WEBDEV && |
michael@0 | 2323 | methodOrNode == this.logConsoleAPIMessage) { |
michael@0 | 2324 | args[0].arguments.forEach((aValue) => { |
michael@0 | 2325 | if (WebConsoleUtils.isActorGrip(aValue)) { |
michael@0 | 2326 | this._releaseObject(aValue.actor); |
michael@0 | 2327 | } |
michael@0 | 2328 | }); |
michael@0 | 2329 | } |
michael@0 | 2330 | else if (category == CATEGORY_JS && |
michael@0 | 2331 | methodOrNode == this.reportPageError) { |
michael@0 | 2332 | let pageError = args[1]; |
michael@0 | 2333 | for (let prop of ["errorMessage", "lineText"]) { |
michael@0 | 2334 | let grip = pageError[prop]; |
michael@0 | 2335 | if (WebConsoleUtils.isActorGrip(grip)) { |
michael@0 | 2336 | this._releaseObject(grip.actor); |
michael@0 | 2337 | } |
michael@0 | 2338 | } |
michael@0 | 2339 | } |
michael@0 | 2340 | else if (category == CATEGORY_JS && |
michael@0 | 2341 | methodOrNode == this._reportLogMessage) { |
michael@0 | 2342 | if (WebConsoleUtils.isActorGrip(args[0].message)) { |
michael@0 | 2343 | this._releaseObject(args[0].message.actor); |
michael@0 | 2344 | } |
michael@0 | 2345 | } |
michael@0 | 2346 | }, |
michael@0 | 2347 | |
michael@0 | 2348 | /** |
michael@0 | 2349 | * Ensures that the number of message nodes of type aCategory don't exceed that |
michael@0 | 2350 | * category's line limit by removing old messages as needed. |
michael@0 | 2351 | * |
michael@0 | 2352 | * @param integer aCategory |
michael@0 | 2353 | * The category of message nodes to prune if needed. |
michael@0 | 2354 | * @return number |
michael@0 | 2355 | * The number of removed nodes. |
michael@0 | 2356 | */ |
michael@0 | 2357 | pruneOutputIfNecessary: function WCF_pruneOutputIfNecessary(aCategory) |
michael@0 | 2358 | { |
michael@0 | 2359 | let logLimit = Utils.logLimitForCategory(aCategory); |
michael@0 | 2360 | let messageNodes = this.outputNode.querySelectorAll(".message[category=" + |
michael@0 | 2361 | CATEGORY_CLASS_FRAGMENTS[aCategory] + "]"); |
michael@0 | 2362 | let n = Math.max(0, messageNodes.length - logLimit); |
michael@0 | 2363 | let toRemove = Array.prototype.slice.call(messageNodes, 0, n); |
michael@0 | 2364 | toRemove.forEach(this.removeOutputMessage, this); |
michael@0 | 2365 | |
michael@0 | 2366 | return n; |
michael@0 | 2367 | }, |
michael@0 | 2368 | |
michael@0 | 2369 | /** |
michael@0 | 2370 | * Remove a given message from the output. |
michael@0 | 2371 | * |
michael@0 | 2372 | * @param nsIDOMNode aNode |
michael@0 | 2373 | * The message node you want to remove. |
michael@0 | 2374 | */ |
michael@0 | 2375 | removeOutputMessage: function WCF_removeOutputMessage(aNode) |
michael@0 | 2376 | { |
michael@0 | 2377 | if (aNode._messageObject) { |
michael@0 | 2378 | aNode._messageObject.destroy(); |
michael@0 | 2379 | } |
michael@0 | 2380 | |
michael@0 | 2381 | if (aNode._objectActors) { |
michael@0 | 2382 | for (let actor of aNode._objectActors) { |
michael@0 | 2383 | this._releaseObject(actor); |
michael@0 | 2384 | } |
michael@0 | 2385 | aNode._objectActors.clear(); |
michael@0 | 2386 | } |
michael@0 | 2387 | |
michael@0 | 2388 | if (aNode.category == CATEGORY_CSS || |
michael@0 | 2389 | aNode.category == CATEGORY_SECURITY) { |
michael@0 | 2390 | let repeatNode = aNode.getElementsByClassName("message-repeats")[0]; |
michael@0 | 2391 | if (repeatNode && repeatNode._uid) { |
michael@0 | 2392 | delete this._repeatNodes[repeatNode._uid]; |
michael@0 | 2393 | } |
michael@0 | 2394 | } |
michael@0 | 2395 | else if (aNode._connectionId && |
michael@0 | 2396 | aNode.category == CATEGORY_NETWORK) { |
michael@0 | 2397 | delete this._networkRequests[aNode._connectionId]; |
michael@0 | 2398 | this._releaseObject(aNode._connectionId); |
michael@0 | 2399 | } |
michael@0 | 2400 | else if (aNode.classList.contains("inlined-variables-view")) { |
michael@0 | 2401 | let view = aNode._variablesView; |
michael@0 | 2402 | if (view) { |
michael@0 | 2403 | view.controller.releaseActors(); |
michael@0 | 2404 | } |
michael@0 | 2405 | aNode._variablesView = null; |
michael@0 | 2406 | } |
michael@0 | 2407 | |
michael@0 | 2408 | if (aNode.parentNode) { |
michael@0 | 2409 | aNode.parentNode.removeChild(aNode); |
michael@0 | 2410 | } |
michael@0 | 2411 | }, |
michael@0 | 2412 | |
michael@0 | 2413 | /** |
michael@0 | 2414 | * Given a category and message body, creates a DOM node to represent an |
michael@0 | 2415 | * incoming message. The timestamp is automatically added. |
michael@0 | 2416 | * |
michael@0 | 2417 | * @param number aCategory |
michael@0 | 2418 | * The category of the message: one of the CATEGORY_* constants. |
michael@0 | 2419 | * @param number aSeverity |
michael@0 | 2420 | * The severity of the message: one of the SEVERITY_* constants; |
michael@0 | 2421 | * @param string|nsIDOMNode aBody |
michael@0 | 2422 | * The body of the message, either a simple string or a DOM node. |
michael@0 | 2423 | * @param string aSourceURL [optional] |
michael@0 | 2424 | * The URL of the source file that emitted the error. |
michael@0 | 2425 | * @param number aSourceLine [optional] |
michael@0 | 2426 | * The line number on which the error occurred. If zero or omitted, |
michael@0 | 2427 | * there is no line number associated with this message. |
michael@0 | 2428 | * @param string aClipboardText [optional] |
michael@0 | 2429 | * The text that should be copied to the clipboard when this node is |
michael@0 | 2430 | * copied. If omitted, defaults to the body text. If `aBody` is not |
michael@0 | 2431 | * a string, then the clipboard text must be supplied. |
michael@0 | 2432 | * @param number aLevel [optional] |
michael@0 | 2433 | * The level of the console API message. |
michael@0 | 2434 | * @param number aTimeStamp [optional] |
michael@0 | 2435 | * The timestamp to use for this message node. If omitted, the current |
michael@0 | 2436 | * date and time is used. |
michael@0 | 2437 | * @return nsIDOMNode |
michael@0 | 2438 | * The message node: a DIV ready to be inserted into the Web Console |
michael@0 | 2439 | * output node. |
michael@0 | 2440 | */ |
michael@0 | 2441 | createMessageNode: |
michael@0 | 2442 | function WCF_createMessageNode(aCategory, aSeverity, aBody, aSourceURL, |
michael@0 | 2443 | aSourceLine, aClipboardText, aLevel, aTimeStamp) |
michael@0 | 2444 | { |
michael@0 | 2445 | if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) { |
michael@0 | 2446 | aClipboardText = aBody.innerText; |
michael@0 | 2447 | } |
michael@0 | 2448 | |
michael@0 | 2449 | let indentNode = this.document.createElementNS(XHTML_NS, "span"); |
michael@0 | 2450 | indentNode.className = "indent"; |
michael@0 | 2451 | |
michael@0 | 2452 | // Apply the current group by indenting appropriately. |
michael@0 | 2453 | let indent = this.groupDepth * GROUP_INDENT; |
michael@0 | 2454 | indentNode.style.width = indent + "px"; |
michael@0 | 2455 | |
michael@0 | 2456 | // Make the icon container, which is a vertical box. Its purpose is to |
michael@0 | 2457 | // ensure that the icon stays anchored at the top of the message even for |
michael@0 | 2458 | // long multi-line messages. |
michael@0 | 2459 | let iconContainer = this.document.createElementNS(XHTML_NS, "span"); |
michael@0 | 2460 | iconContainer.className = "icon"; |
michael@0 | 2461 | |
michael@0 | 2462 | // Create the message body, which contains the actual text of the message. |
michael@0 | 2463 | let bodyNode = this.document.createElementNS(XHTML_NS, "span"); |
michael@0 | 2464 | bodyNode.className = "message-body-wrapper message-body devtools-monospace"; |
michael@0 | 2465 | |
michael@0 | 2466 | // Store the body text, since it is needed later for the variables view. |
michael@0 | 2467 | let body = aBody; |
michael@0 | 2468 | // If a string was supplied for the body, turn it into a DOM node and an |
michael@0 | 2469 | // associated clipboard string now. |
michael@0 | 2470 | aClipboardText = aClipboardText || |
michael@0 | 2471 | (aBody + (aSourceURL ? " @ " + aSourceURL : "") + |
michael@0 | 2472 | (aSourceLine ? ":" + aSourceLine : "")); |
michael@0 | 2473 | |
michael@0 | 2474 | let timestamp = aTimeStamp || Date.now(); |
michael@0 | 2475 | |
michael@0 | 2476 | // Create the containing node and append all its elements to it. |
michael@0 | 2477 | let node = this.document.createElementNS(XHTML_NS, "div"); |
michael@0 | 2478 | node.id = "console-msg-" + gSequenceId(); |
michael@0 | 2479 | node.className = "message"; |
michael@0 | 2480 | node.clipboardText = aClipboardText; |
michael@0 | 2481 | node.timestamp = timestamp; |
michael@0 | 2482 | this.setMessageType(node, aCategory, aSeverity); |
michael@0 | 2483 | |
michael@0 | 2484 | if (aBody instanceof Ci.nsIDOMNode) { |
michael@0 | 2485 | bodyNode.appendChild(aBody); |
michael@0 | 2486 | } |
michael@0 | 2487 | else { |
michael@0 | 2488 | let str = undefined; |
michael@0 | 2489 | if (aLevel == "dir") { |
michael@0 | 2490 | str = VariablesView.getString(aBody.arguments[0]); |
michael@0 | 2491 | } |
michael@0 | 2492 | else { |
michael@0 | 2493 | str = aBody; |
michael@0 | 2494 | } |
michael@0 | 2495 | |
michael@0 | 2496 | if (str !== undefined) { |
michael@0 | 2497 | aBody = this.document.createTextNode(str); |
michael@0 | 2498 | bodyNode.appendChild(aBody); |
michael@0 | 2499 | } |
michael@0 | 2500 | } |
michael@0 | 2501 | |
michael@0 | 2502 | // Add the message repeats node only when needed. |
michael@0 | 2503 | let repeatNode = null; |
michael@0 | 2504 | if (aCategory != CATEGORY_INPUT && |
michael@0 | 2505 | aCategory != CATEGORY_OUTPUT && |
michael@0 | 2506 | aCategory != CATEGORY_NETWORK && |
michael@0 | 2507 | !(aCategory == CATEGORY_CSS && aSeverity == SEVERITY_LOG)) { |
michael@0 | 2508 | repeatNode = this.document.createElementNS(XHTML_NS, "span"); |
michael@0 | 2509 | repeatNode.setAttribute("value", "1"); |
michael@0 | 2510 | repeatNode.className = "message-repeats"; |
michael@0 | 2511 | repeatNode.textContent = 1; |
michael@0 | 2512 | repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel, |
michael@0 | 2513 | aSourceURL, aSourceLine].join(":"); |
michael@0 | 2514 | } |
michael@0 | 2515 | |
michael@0 | 2516 | // Create the timestamp. |
michael@0 | 2517 | let timestampNode = this.document.createElementNS(XHTML_NS, "span"); |
michael@0 | 2518 | timestampNode.className = "timestamp devtools-monospace"; |
michael@0 | 2519 | |
michael@0 | 2520 | let timestampString = l10n.timestampString(timestamp); |
michael@0 | 2521 | timestampNode.textContent = timestampString + " "; |
michael@0 | 2522 | |
michael@0 | 2523 | // Create the source location (e.g. www.example.com:6) that sits on the |
michael@0 | 2524 | // right side of the message, if applicable. |
michael@0 | 2525 | let locationNode; |
michael@0 | 2526 | if (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) { |
michael@0 | 2527 | locationNode = this.createLocationNode(aSourceURL, aSourceLine); |
michael@0 | 2528 | } |
michael@0 | 2529 | |
michael@0 | 2530 | node.appendChild(timestampNode); |
michael@0 | 2531 | node.appendChild(indentNode); |
michael@0 | 2532 | node.appendChild(iconContainer); |
michael@0 | 2533 | |
michael@0 | 2534 | // Display the variables view after the message node. |
michael@0 | 2535 | if (aLevel == "dir") { |
michael@0 | 2536 | bodyNode.style.height = (this.window.innerHeight * |
michael@0 | 2537 | CONSOLE_DIR_VIEW_HEIGHT) + "px"; |
michael@0 | 2538 | |
michael@0 | 2539 | let options = { |
michael@0 | 2540 | objectActor: body.arguments[0], |
michael@0 | 2541 | targetElement: bodyNode, |
michael@0 | 2542 | hideFilterInput: true, |
michael@0 | 2543 | }; |
michael@0 | 2544 | this.jsterm.openVariablesView(options).then((aView) => { |
michael@0 | 2545 | node._variablesView = aView; |
michael@0 | 2546 | if (node.classList.contains("hidden-message")) { |
michael@0 | 2547 | node.classList.remove("hidden-message"); |
michael@0 | 2548 | } |
michael@0 | 2549 | }); |
michael@0 | 2550 | |
michael@0 | 2551 | node.classList.add("inlined-variables-view"); |
michael@0 | 2552 | } |
michael@0 | 2553 | |
michael@0 | 2554 | node.appendChild(bodyNode); |
michael@0 | 2555 | if (repeatNode) { |
michael@0 | 2556 | node.appendChild(repeatNode); |
michael@0 | 2557 | } |
michael@0 | 2558 | if (locationNode) { |
michael@0 | 2559 | node.appendChild(locationNode); |
michael@0 | 2560 | } |
michael@0 | 2561 | node.appendChild(this.document.createTextNode("\n")); |
michael@0 | 2562 | |
michael@0 | 2563 | return node; |
michael@0 | 2564 | }, |
michael@0 | 2565 | |
michael@0 | 2566 | /** |
michael@0 | 2567 | * Creates the anchor that displays the textual location of an incoming |
michael@0 | 2568 | * message. |
michael@0 | 2569 | * |
michael@0 | 2570 | * @param string aSourceURL |
michael@0 | 2571 | * The URL of the source file responsible for the error. |
michael@0 | 2572 | * @param number aSourceLine [optional] |
michael@0 | 2573 | * The line number on which the error occurred. If zero or omitted, |
michael@0 | 2574 | * there is no line number associated with this message. |
michael@0 | 2575 | * @param string aTarget [optional] |
michael@0 | 2576 | * Tells which tool to open the link with, on click. Supported tools: |
michael@0 | 2577 | * jsdebugger, styleeditor, scratchpad. |
michael@0 | 2578 | * @return nsIDOMNode |
michael@0 | 2579 | * The new anchor element, ready to be added to the message node. |
michael@0 | 2580 | */ |
michael@0 | 2581 | createLocationNode: |
michael@0 | 2582 | function WCF_createLocationNode(aSourceURL, aSourceLine, aTarget) |
michael@0 | 2583 | { |
michael@0 | 2584 | if (!aSourceURL) { |
michael@0 | 2585 | aSourceURL = ""; |
michael@0 | 2586 | } |
michael@0 | 2587 | let locationNode = this.document.createElementNS(XHTML_NS, "a"); |
michael@0 | 2588 | let filenameNode = this.document.createElementNS(XHTML_NS, "span"); |
michael@0 | 2589 | |
michael@0 | 2590 | // Create the text, which consists of an abbreviated version of the URL |
michael@0 | 2591 | // Scratchpad URLs should not be abbreviated. |
michael@0 | 2592 | let filename; |
michael@0 | 2593 | let fullURL; |
michael@0 | 2594 | let isScratchpad = false; |
michael@0 | 2595 | |
michael@0 | 2596 | if (/^Scratchpad\/\d+$/.test(aSourceURL)) { |
michael@0 | 2597 | filename = aSourceURL; |
michael@0 | 2598 | fullURL = aSourceURL; |
michael@0 | 2599 | isScratchpad = true; |
michael@0 | 2600 | } |
michael@0 | 2601 | else { |
michael@0 | 2602 | fullURL = aSourceURL.split(" -> ").pop(); |
michael@0 | 2603 | filename = WebConsoleUtils.abbreviateSourceURL(fullURL); |
michael@0 | 2604 | } |
michael@0 | 2605 | |
michael@0 | 2606 | filenameNode.className = "filename"; |
michael@0 | 2607 | filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation")); |
michael@0 | 2608 | locationNode.appendChild(filenameNode); |
michael@0 | 2609 | |
michael@0 | 2610 | locationNode.href = isScratchpad || !fullURL ? "#" : fullURL; |
michael@0 | 2611 | locationNode.draggable = false; |
michael@0 | 2612 | if (aTarget) { |
michael@0 | 2613 | locationNode.target = aTarget; |
michael@0 | 2614 | } |
michael@0 | 2615 | locationNode.setAttribute("title", aSourceURL); |
michael@0 | 2616 | locationNode.className = "message-location theme-link devtools-monospace"; |
michael@0 | 2617 | |
michael@0 | 2618 | // Make the location clickable. |
michael@0 | 2619 | let onClick = () => { |
michael@0 | 2620 | let target = locationNode.target; |
michael@0 | 2621 | if (target == "scratchpad" || isScratchpad) { |
michael@0 | 2622 | this.owner.viewSourceInScratchpad(aSourceURL); |
michael@0 | 2623 | return; |
michael@0 | 2624 | } |
michael@0 | 2625 | |
michael@0 | 2626 | let category = locationNode.parentNode.category; |
michael@0 | 2627 | if (target == "styleeditor" || category == CATEGORY_CSS) { |
michael@0 | 2628 | this.owner.viewSourceInStyleEditor(fullURL, aSourceLine); |
michael@0 | 2629 | } |
michael@0 | 2630 | else if (target == "jsdebugger" || |
michael@0 | 2631 | category == CATEGORY_JS || category == CATEGORY_WEBDEV) { |
michael@0 | 2632 | this.owner.viewSourceInDebugger(fullURL, aSourceLine); |
michael@0 | 2633 | } |
michael@0 | 2634 | else { |
michael@0 | 2635 | this.owner.viewSource(fullURL, aSourceLine); |
michael@0 | 2636 | } |
michael@0 | 2637 | }; |
michael@0 | 2638 | |
michael@0 | 2639 | if (fullURL) { |
michael@0 | 2640 | this._addMessageLinkCallback(locationNode, onClick); |
michael@0 | 2641 | } |
michael@0 | 2642 | |
michael@0 | 2643 | if (aSourceLine) { |
michael@0 | 2644 | let lineNumberNode = this.document.createElementNS(XHTML_NS, "span"); |
michael@0 | 2645 | lineNumberNode.className = "line-number"; |
michael@0 | 2646 | lineNumberNode.textContent = ":" + aSourceLine; |
michael@0 | 2647 | locationNode.appendChild(lineNumberNode); |
michael@0 | 2648 | locationNode.sourceLine = aSourceLine; |
michael@0 | 2649 | } |
michael@0 | 2650 | |
michael@0 | 2651 | return locationNode; |
michael@0 | 2652 | }, |
michael@0 | 2653 | |
michael@0 | 2654 | /** |
michael@0 | 2655 | * Adjusts the category and severity of the given message. |
michael@0 | 2656 | * |
michael@0 | 2657 | * @param nsIDOMNode aMessageNode |
michael@0 | 2658 | * The message node to alter. |
michael@0 | 2659 | * @param number aCategory |
michael@0 | 2660 | * The category for the message; one of the CATEGORY_ constants. |
michael@0 | 2661 | * @param number aSeverity |
michael@0 | 2662 | * The severity for the message; one of the SEVERITY_ constants. |
michael@0 | 2663 | * @return void |
michael@0 | 2664 | */ |
michael@0 | 2665 | setMessageType: |
michael@0 | 2666 | function WCF_setMessageType(aMessageNode, aCategory, aSeverity) |
michael@0 | 2667 | { |
michael@0 | 2668 | aMessageNode.category = aCategory; |
michael@0 | 2669 | aMessageNode.severity = aSeverity; |
michael@0 | 2670 | aMessageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[aCategory]); |
michael@0 | 2671 | aMessageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[aSeverity]); |
michael@0 | 2672 | aMessageNode.setAttribute("filter", MESSAGE_PREFERENCE_KEYS[aCategory][aSeverity]); |
michael@0 | 2673 | }, |
michael@0 | 2674 | |
michael@0 | 2675 | /** |
michael@0 | 2676 | * Add the mouse event handlers needed to make a link. |
michael@0 | 2677 | * |
michael@0 | 2678 | * @private |
michael@0 | 2679 | * @param nsIDOMNode aNode |
michael@0 | 2680 | * The node for which you want to add the event handlers. |
michael@0 | 2681 | * @param function aCallback |
michael@0 | 2682 | * The function you want to invoke on click. |
michael@0 | 2683 | */ |
michael@0 | 2684 | _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback) |
michael@0 | 2685 | { |
michael@0 | 2686 | aNode.addEventListener("mousedown", (aEvent) => { |
michael@0 | 2687 | this._mousedown = true; |
michael@0 | 2688 | this._startX = aEvent.clientX; |
michael@0 | 2689 | this._startY = aEvent.clientY; |
michael@0 | 2690 | }, false); |
michael@0 | 2691 | |
michael@0 | 2692 | aNode.addEventListener("click", (aEvent) => { |
michael@0 | 2693 | let mousedown = this._mousedown; |
michael@0 | 2694 | this._mousedown = false; |
michael@0 | 2695 | |
michael@0 | 2696 | aEvent.preventDefault(); |
michael@0 | 2697 | |
michael@0 | 2698 | // Do not allow middle/right-click or 2+ clicks. |
michael@0 | 2699 | if (aEvent.detail != 1 || aEvent.button != 0) { |
michael@0 | 2700 | return; |
michael@0 | 2701 | } |
michael@0 | 2702 | |
michael@0 | 2703 | // If this event started with a mousedown event and it ends at a different |
michael@0 | 2704 | // location, we consider this text selection. |
michael@0 | 2705 | if (mousedown && |
michael@0 | 2706 | (this._startX != aEvent.clientX) && |
michael@0 | 2707 | (this._startY != aEvent.clientY)) |
michael@0 | 2708 | { |
michael@0 | 2709 | this._startX = this._startY = undefined; |
michael@0 | 2710 | return; |
michael@0 | 2711 | } |
michael@0 | 2712 | |
michael@0 | 2713 | this._startX = this._startY = undefined; |
michael@0 | 2714 | |
michael@0 | 2715 | aCallback.call(this, aEvent); |
michael@0 | 2716 | }, false); |
michael@0 | 2717 | }, |
michael@0 | 2718 | |
michael@0 | 2719 | _addFocusCallback: function WCF__addFocusCallback(aNode, aCallback) |
michael@0 | 2720 | { |
michael@0 | 2721 | aNode.addEventListener("mousedown", (aEvent) => { |
michael@0 | 2722 | this._mousedown = true; |
michael@0 | 2723 | this._startX = aEvent.clientX; |
michael@0 | 2724 | this._startY = aEvent.clientY; |
michael@0 | 2725 | }, false); |
michael@0 | 2726 | |
michael@0 | 2727 | aNode.addEventListener("click", (aEvent) => { |
michael@0 | 2728 | let mousedown = this._mousedown; |
michael@0 | 2729 | this._mousedown = false; |
michael@0 | 2730 | |
michael@0 | 2731 | // Do not allow middle/right-click or 2+ clicks. |
michael@0 | 2732 | if (aEvent.detail != 1 || aEvent.button != 0) { |
michael@0 | 2733 | return; |
michael@0 | 2734 | } |
michael@0 | 2735 | |
michael@0 | 2736 | // If this event started with a mousedown event and it ends at a different |
michael@0 | 2737 | // location, we consider this text selection. |
michael@0 | 2738 | // Add a fuzz modifier of two pixels in any direction to account for sloppy |
michael@0 | 2739 | // clicking. |
michael@0 | 2740 | if (mousedown && |
michael@0 | 2741 | (Math.abs(aEvent.clientX - this._startX) >= 2) && |
michael@0 | 2742 | (Math.abs(aEvent.clientY - this._startY) >= 1)) |
michael@0 | 2743 | { |
michael@0 | 2744 | this._startX = this._startY = undefined; |
michael@0 | 2745 | return; |
michael@0 | 2746 | } |
michael@0 | 2747 | |
michael@0 | 2748 | this._startX = this._startY = undefined; |
michael@0 | 2749 | |
michael@0 | 2750 | aCallback.call(this, aEvent); |
michael@0 | 2751 | }, false); |
michael@0 | 2752 | }, |
michael@0 | 2753 | |
michael@0 | 2754 | /** |
michael@0 | 2755 | * Handler for the pref-changed event coming from the toolbox. |
michael@0 | 2756 | * Currently this function only handles the timestamps preferences. |
michael@0 | 2757 | * |
michael@0 | 2758 | * @private |
michael@0 | 2759 | * @param object aEvent |
michael@0 | 2760 | * This parameter is a string that holds the event name |
michael@0 | 2761 | * pref-changed in this case. |
michael@0 | 2762 | * @param object aData |
michael@0 | 2763 | * This is the pref-changed data object. |
michael@0 | 2764 | */ |
michael@0 | 2765 | _onToolboxPrefChanged: function WCF__onToolboxPrefChanged(aEvent, aData) |
michael@0 | 2766 | { |
michael@0 | 2767 | if (aData.pref == PREF_MESSAGE_TIMESTAMP) { |
michael@0 | 2768 | if (aData.newValue) { |
michael@0 | 2769 | this.outputNode.classList.remove("hideTimestamps"); |
michael@0 | 2770 | } |
michael@0 | 2771 | else { |
michael@0 | 2772 | this.outputNode.classList.add("hideTimestamps"); |
michael@0 | 2773 | } |
michael@0 | 2774 | } |
michael@0 | 2775 | }, |
michael@0 | 2776 | |
michael@0 | 2777 | /** |
michael@0 | 2778 | * Copies the selected items to the system clipboard. |
michael@0 | 2779 | * |
michael@0 | 2780 | * @param object aOptions |
michael@0 | 2781 | * - linkOnly: |
michael@0 | 2782 | * An optional flag to copy only URL without timestamp and |
michael@0 | 2783 | * other meta-information. Default is false. |
michael@0 | 2784 | */ |
michael@0 | 2785 | copySelectedItems: function WCF_copySelectedItems(aOptions) |
michael@0 | 2786 | { |
michael@0 | 2787 | aOptions = aOptions || { linkOnly: false, contextmenu: false }; |
michael@0 | 2788 | |
michael@0 | 2789 | // Gather up the selected items and concatenate their clipboard text. |
michael@0 | 2790 | let strings = []; |
michael@0 | 2791 | |
michael@0 | 2792 | let children = this.output.getSelectedMessages(); |
michael@0 | 2793 | if (!children.length && aOptions.contextmenu) { |
michael@0 | 2794 | children = [this._contextMenuHandler.lastClickedMessage]; |
michael@0 | 2795 | } |
michael@0 | 2796 | |
michael@0 | 2797 | for (let item of children) { |
michael@0 | 2798 | // Ensure the selected item hasn't been filtered by type or string. |
michael@0 | 2799 | if (!item.classList.contains("filtered-by-type") && |
michael@0 | 2800 | !item.classList.contains("filtered-by-string")) { |
michael@0 | 2801 | let timestampString = l10n.timestampString(item.timestamp); |
michael@0 | 2802 | if (aOptions.linkOnly) { |
michael@0 | 2803 | strings.push(item.url); |
michael@0 | 2804 | } |
michael@0 | 2805 | else { |
michael@0 | 2806 | strings.push("[" + timestampString + "] " + item.clipboardText); |
michael@0 | 2807 | } |
michael@0 | 2808 | } |
michael@0 | 2809 | } |
michael@0 | 2810 | |
michael@0 | 2811 | clipboardHelper.copyString(strings.join("\n"), this.document); |
michael@0 | 2812 | }, |
michael@0 | 2813 | |
michael@0 | 2814 | /** |
michael@0 | 2815 | * Object properties provider. This function gives you the properties of the |
michael@0 | 2816 | * remote object you want. |
michael@0 | 2817 | * |
michael@0 | 2818 | * @param string aActor |
michael@0 | 2819 | * The object actor ID from which you want the properties. |
michael@0 | 2820 | * @param function aCallback |
michael@0 | 2821 | * Function you want invoked once the properties are received. |
michael@0 | 2822 | */ |
michael@0 | 2823 | objectPropertiesProvider: |
michael@0 | 2824 | function WCF_objectPropertiesProvider(aActor, aCallback) |
michael@0 | 2825 | { |
michael@0 | 2826 | this.webConsoleClient.inspectObjectProperties(aActor, |
michael@0 | 2827 | function(aResponse) { |
michael@0 | 2828 | if (aResponse.error) { |
michael@0 | 2829 | Cu.reportError("Failed to retrieve the object properties from the " + |
michael@0 | 2830 | "server. Error: " + aResponse.error); |
michael@0 | 2831 | return; |
michael@0 | 2832 | } |
michael@0 | 2833 | aCallback(aResponse.properties); |
michael@0 | 2834 | }); |
michael@0 | 2835 | }, |
michael@0 | 2836 | |
michael@0 | 2837 | /** |
michael@0 | 2838 | * Release an actor. |
michael@0 | 2839 | * |
michael@0 | 2840 | * @private |
michael@0 | 2841 | * @param string aActor |
michael@0 | 2842 | * The actor ID you want to release. |
michael@0 | 2843 | */ |
michael@0 | 2844 | _releaseObject: function WCF__releaseObject(aActor) |
michael@0 | 2845 | { |
michael@0 | 2846 | if (this.proxy) { |
michael@0 | 2847 | this.proxy.releaseActor(aActor); |
michael@0 | 2848 | } |
michael@0 | 2849 | }, |
michael@0 | 2850 | |
michael@0 | 2851 | /** |
michael@0 | 2852 | * Open the selected item's URL in a new tab. |
michael@0 | 2853 | */ |
michael@0 | 2854 | openSelectedItemInTab: function WCF_openSelectedItemInTab() |
michael@0 | 2855 | { |
michael@0 | 2856 | let item = this.output.getSelectedMessages(1)[0] || |
michael@0 | 2857 | this._contextMenuHandler.lastClickedMessage; |
michael@0 | 2858 | |
michael@0 | 2859 | if (!item || !item.url) { |
michael@0 | 2860 | return; |
michael@0 | 2861 | } |
michael@0 | 2862 | |
michael@0 | 2863 | this.owner.openLink(item.url); |
michael@0 | 2864 | }, |
michael@0 | 2865 | |
michael@0 | 2866 | /** |
michael@0 | 2867 | * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks |
michael@0 | 2868 | * when the Web Console is closed. |
michael@0 | 2869 | * |
michael@0 | 2870 | * @return object |
michael@0 | 2871 | * A promise that is resolved when the WebConsoleFrame instance is |
michael@0 | 2872 | * destroyed. |
michael@0 | 2873 | */ |
michael@0 | 2874 | destroy: function WCF_destroy() |
michael@0 | 2875 | { |
michael@0 | 2876 | if (this._destroyer) { |
michael@0 | 2877 | return this._destroyer.promise; |
michael@0 | 2878 | } |
michael@0 | 2879 | |
michael@0 | 2880 | this._destroyer = promise.defer(); |
michael@0 | 2881 | |
michael@0 | 2882 | let toolbox = gDevTools.getToolbox(this.owner.target); |
michael@0 | 2883 | if (toolbox) { |
michael@0 | 2884 | toolbox.off("webconsole-selected", this._onPanelSelected); |
michael@0 | 2885 | } |
michael@0 | 2886 | |
michael@0 | 2887 | gDevTools.off("pref-changed", this._onToolboxPrefChanged); |
michael@0 | 2888 | |
michael@0 | 2889 | this._repeatNodes = {}; |
michael@0 | 2890 | this._outputQueue = []; |
michael@0 | 2891 | this._pruneCategoriesQueue = {}; |
michael@0 | 2892 | this._networkRequests = {}; |
michael@0 | 2893 | |
michael@0 | 2894 | if (this._outputTimerInitialized) { |
michael@0 | 2895 | this._outputTimerInitialized = false; |
michael@0 | 2896 | this._outputTimer.cancel(); |
michael@0 | 2897 | } |
michael@0 | 2898 | this._outputTimer = null; |
michael@0 | 2899 | |
michael@0 | 2900 | if (this.jsterm) { |
michael@0 | 2901 | this.jsterm.destroy(); |
michael@0 | 2902 | this.jsterm = null; |
michael@0 | 2903 | } |
michael@0 | 2904 | this.output.destroy(); |
michael@0 | 2905 | this.output = null; |
michael@0 | 2906 | |
michael@0 | 2907 | if (this._contextMenuHandler) { |
michael@0 | 2908 | this._contextMenuHandler.destroy(); |
michael@0 | 2909 | this._contextMenuHandler = null; |
michael@0 | 2910 | } |
michael@0 | 2911 | |
michael@0 | 2912 | this._commandController = null; |
michael@0 | 2913 | |
michael@0 | 2914 | let onDestroy = function() { |
michael@0 | 2915 | this._destroyer.resolve(null); |
michael@0 | 2916 | }.bind(this); |
michael@0 | 2917 | |
michael@0 | 2918 | if (this.proxy) { |
michael@0 | 2919 | this.proxy.disconnect().then(onDestroy); |
michael@0 | 2920 | this.proxy = null; |
michael@0 | 2921 | } |
michael@0 | 2922 | else { |
michael@0 | 2923 | onDestroy(); |
michael@0 | 2924 | } |
michael@0 | 2925 | |
michael@0 | 2926 | return this._destroyer.promise; |
michael@0 | 2927 | }, |
michael@0 | 2928 | }; |
michael@0 | 2929 | |
michael@0 | 2930 | |
michael@0 | 2931 | /** |
michael@0 | 2932 | * @see VariablesView.simpleValueEvalMacro |
michael@0 | 2933 | */ |
michael@0 | 2934 | function simpleValueEvalMacro(aItem, aCurrentString) |
michael@0 | 2935 | { |
michael@0 | 2936 | return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self"); |
michael@0 | 2937 | }; |
michael@0 | 2938 | |
michael@0 | 2939 | |
michael@0 | 2940 | /** |
michael@0 | 2941 | * @see VariablesView.overrideValueEvalMacro |
michael@0 | 2942 | */ |
michael@0 | 2943 | function overrideValueEvalMacro(aItem, aCurrentString) |
michael@0 | 2944 | { |
michael@0 | 2945 | return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self"); |
michael@0 | 2946 | }; |
michael@0 | 2947 | |
michael@0 | 2948 | |
michael@0 | 2949 | /** |
michael@0 | 2950 | * @see VariablesView.getterOrSetterEvalMacro |
michael@0 | 2951 | */ |
michael@0 | 2952 | function getterOrSetterEvalMacro(aItem, aCurrentString) |
michael@0 | 2953 | { |
michael@0 | 2954 | return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self"); |
michael@0 | 2955 | } |
michael@0 | 2956 | |
michael@0 | 2957 | |
michael@0 | 2958 | |
michael@0 | 2959 | /** |
michael@0 | 2960 | * Create a JSTerminal (a JavaScript command line). This is attached to an |
michael@0 | 2961 | * existing HeadsUpDisplay (a Web Console instance). This code is responsible |
michael@0 | 2962 | * with handling command line input, code evaluation and result output. |
michael@0 | 2963 | * |
michael@0 | 2964 | * @constructor |
michael@0 | 2965 | * @param object aWebConsoleFrame |
michael@0 | 2966 | * The WebConsoleFrame object that owns this JSTerm instance. |
michael@0 | 2967 | */ |
michael@0 | 2968 | function JSTerm(aWebConsoleFrame) |
michael@0 | 2969 | { |
michael@0 | 2970 | this.hud = aWebConsoleFrame; |
michael@0 | 2971 | this.hudId = this.hud.hudId; |
michael@0 | 2972 | |
michael@0 | 2973 | this.lastCompletion = { value: null }; |
michael@0 | 2974 | this.history = []; |
michael@0 | 2975 | |
michael@0 | 2976 | // Holds the number of entries in history. This value is incremented in |
michael@0 | 2977 | // this.execute(). |
michael@0 | 2978 | this.historyIndex = 0; // incremented on this.execute() |
michael@0 | 2979 | |
michael@0 | 2980 | // Holds the index of the history entry that the user is currently viewing. |
michael@0 | 2981 | // This is reset to this.history.length when this.execute() is invoked. |
michael@0 | 2982 | this.historyPlaceHolder = 0; |
michael@0 | 2983 | this._objectActorsInVariablesViews = new Map(); |
michael@0 | 2984 | |
michael@0 | 2985 | this._keyPress = this._keyPress.bind(this); |
michael@0 | 2986 | this._inputEventHandler = this._inputEventHandler.bind(this); |
michael@0 | 2987 | this._focusEventHandler = this._focusEventHandler.bind(this); |
michael@0 | 2988 | this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this); |
michael@0 | 2989 | this._blurEventHandler = this._blurEventHandler.bind(this); |
michael@0 | 2990 | |
michael@0 | 2991 | EventEmitter.decorate(this); |
michael@0 | 2992 | } |
michael@0 | 2993 | |
michael@0 | 2994 | JSTerm.prototype = { |
michael@0 | 2995 | SELECTED_FRAME: -1, |
michael@0 | 2996 | |
michael@0 | 2997 | /** |
michael@0 | 2998 | * Stores the data for the last completion. |
michael@0 | 2999 | * @type object |
michael@0 | 3000 | */ |
michael@0 | 3001 | lastCompletion: null, |
michael@0 | 3002 | |
michael@0 | 3003 | /** |
michael@0 | 3004 | * Array that caches the user input suggestions received from the server. |
michael@0 | 3005 | * @private |
michael@0 | 3006 | * @type array |
michael@0 | 3007 | */ |
michael@0 | 3008 | _autocompleteCache: null, |
michael@0 | 3009 | |
michael@0 | 3010 | /** |
michael@0 | 3011 | * The input that caused the last request to the server, whose response is |
michael@0 | 3012 | * cached in the _autocompleteCache array. |
michael@0 | 3013 | * @private |
michael@0 | 3014 | * @type string |
michael@0 | 3015 | */ |
michael@0 | 3016 | _autocompleteQuery: null, |
michael@0 | 3017 | |
michael@0 | 3018 | /** |
michael@0 | 3019 | * The frameActorId used in the last autocomplete query. Whenever this changes |
michael@0 | 3020 | * the autocomplete cache must be invalidated. |
michael@0 | 3021 | * @private |
michael@0 | 3022 | * @type string |
michael@0 | 3023 | */ |
michael@0 | 3024 | _lastFrameActorId: null, |
michael@0 | 3025 | |
michael@0 | 3026 | /** |
michael@0 | 3027 | * The Web Console sidebar. |
michael@0 | 3028 | * @see this._createSidebar() |
michael@0 | 3029 | * @see Sidebar.jsm |
michael@0 | 3030 | */ |
michael@0 | 3031 | sidebar: null, |
michael@0 | 3032 | |
michael@0 | 3033 | /** |
michael@0 | 3034 | * The Variables View instance shown in the sidebar. |
michael@0 | 3035 | * @private |
michael@0 | 3036 | * @type object |
michael@0 | 3037 | */ |
michael@0 | 3038 | _variablesView: null, |
michael@0 | 3039 | |
michael@0 | 3040 | /** |
michael@0 | 3041 | * Tells if you want the variables view UI updates to be lazy or not. Tests |
michael@0 | 3042 | * disable lazy updates. |
michael@0 | 3043 | * |
michael@0 | 3044 | * @private |
michael@0 | 3045 | * @type boolean |
michael@0 | 3046 | */ |
michael@0 | 3047 | _lazyVariablesView: true, |
michael@0 | 3048 | |
michael@0 | 3049 | /** |
michael@0 | 3050 | * Holds a map between VariablesView instances and sets of ObjectActor IDs |
michael@0 | 3051 | * that have been retrieved from the server. This allows us to release the |
michael@0 | 3052 | * objects when needed. |
michael@0 | 3053 | * |
michael@0 | 3054 | * @private |
michael@0 | 3055 | * @type Map |
michael@0 | 3056 | */ |
michael@0 | 3057 | _objectActorsInVariablesViews: null, |
michael@0 | 3058 | |
michael@0 | 3059 | /** |
michael@0 | 3060 | * Last input value. |
michael@0 | 3061 | * @type string |
michael@0 | 3062 | */ |
michael@0 | 3063 | lastInputValue: "", |
michael@0 | 3064 | |
michael@0 | 3065 | /** |
michael@0 | 3066 | * Tells if the input node changed since the last focus. |
michael@0 | 3067 | * |
michael@0 | 3068 | * @private |
michael@0 | 3069 | * @type boolean |
michael@0 | 3070 | */ |
michael@0 | 3071 | _inputChanged: false, |
michael@0 | 3072 | |
michael@0 | 3073 | /** |
michael@0 | 3074 | * Tells if the autocomplete popup was navigated since the last open. |
michael@0 | 3075 | * |
michael@0 | 3076 | * @private |
michael@0 | 3077 | * @type boolean |
michael@0 | 3078 | */ |
michael@0 | 3079 | _autocompletePopupNavigated: false, |
michael@0 | 3080 | |
michael@0 | 3081 | /** |
michael@0 | 3082 | * History of code that was executed. |
michael@0 | 3083 | * @type array |
michael@0 | 3084 | */ |
michael@0 | 3085 | history: null, |
michael@0 | 3086 | autocompletePopup: null, |
michael@0 | 3087 | inputNode: null, |
michael@0 | 3088 | completeNode: null, |
michael@0 | 3089 | |
michael@0 | 3090 | /** |
michael@0 | 3091 | * Getter for the element that holds the messages we display. |
michael@0 | 3092 | * @type nsIDOMElement |
michael@0 | 3093 | */ |
michael@0 | 3094 | get outputNode() this.hud.outputNode, |
michael@0 | 3095 | |
michael@0 | 3096 | /** |
michael@0 | 3097 | * Getter for the debugger WebConsoleClient. |
michael@0 | 3098 | * @type object |
michael@0 | 3099 | */ |
michael@0 | 3100 | get webConsoleClient() this.hud.webConsoleClient, |
michael@0 | 3101 | |
michael@0 | 3102 | COMPLETE_FORWARD: 0, |
michael@0 | 3103 | COMPLETE_BACKWARD: 1, |
michael@0 | 3104 | COMPLETE_HINT_ONLY: 2, |
michael@0 | 3105 | COMPLETE_PAGEUP: 3, |
michael@0 | 3106 | COMPLETE_PAGEDOWN: 4, |
michael@0 | 3107 | |
michael@0 | 3108 | /** |
michael@0 | 3109 | * Initialize the JSTerminal UI. |
michael@0 | 3110 | */ |
michael@0 | 3111 | init: function JST_init() |
michael@0 | 3112 | { |
michael@0 | 3113 | let autocompleteOptions = { |
michael@0 | 3114 | onSelect: this.onAutocompleteSelect.bind(this), |
michael@0 | 3115 | onClick: this.acceptProposedCompletion.bind(this), |
michael@0 | 3116 | panelId: "webConsole_autocompletePopup", |
michael@0 | 3117 | listBoxId: "webConsole_autocompletePopupListBox", |
michael@0 | 3118 | position: "before_start", |
michael@0 | 3119 | theme: "auto", |
michael@0 | 3120 | direction: "ltr", |
michael@0 | 3121 | autoSelect: true |
michael@0 | 3122 | }; |
michael@0 | 3123 | this.autocompletePopup = new AutocompletePopup(this.hud.document, |
michael@0 | 3124 | autocompleteOptions); |
michael@0 | 3125 | |
michael@0 | 3126 | let doc = this.hud.document; |
michael@0 | 3127 | let inputContainer = doc.querySelector(".jsterm-input-container"); |
michael@0 | 3128 | this.completeNode = doc.querySelector(".jsterm-complete-node"); |
michael@0 | 3129 | this.inputNode = doc.querySelector(".jsterm-input-node"); |
michael@0 | 3130 | |
michael@0 | 3131 | if (this.hud.owner._browserConsole && |
michael@0 | 3132 | !Services.prefs.getBoolPref("devtools.chrome.enabled")) { |
michael@0 | 3133 | inputContainer.style.display = "none"; |
michael@0 | 3134 | } |
michael@0 | 3135 | else { |
michael@0 | 3136 | this.inputNode.addEventListener("keypress", this._keyPress, false); |
michael@0 | 3137 | this.inputNode.addEventListener("input", this._inputEventHandler, false); |
michael@0 | 3138 | this.inputNode.addEventListener("keyup", this._inputEventHandler, false); |
michael@0 | 3139 | this.inputNode.addEventListener("focus", this._focusEventHandler, false); |
michael@0 | 3140 | } |
michael@0 | 3141 | |
michael@0 | 3142 | this.hud.window.addEventListener("blur", this._blurEventHandler, false); |
michael@0 | 3143 | this.lastInputValue && this.setInputValue(this.lastInputValue); |
michael@0 | 3144 | }, |
michael@0 | 3145 | |
michael@0 | 3146 | /** |
michael@0 | 3147 | * The JavaScript evaluation response handler. |
michael@0 | 3148 | * |
michael@0 | 3149 | * @private |
michael@0 | 3150 | * @param object [aAfterMessage] |
michael@0 | 3151 | * Optional message after which the evaluation result will be |
michael@0 | 3152 | * inserted. |
michael@0 | 3153 | * @param function [aCallback] |
michael@0 | 3154 | * Optional function to invoke when the evaluation result is added to |
michael@0 | 3155 | * the output. |
michael@0 | 3156 | * @param object aResponse |
michael@0 | 3157 | * The message received from the server. |
michael@0 | 3158 | */ |
michael@0 | 3159 | _executeResultCallback: |
michael@0 | 3160 | function JST__executeResultCallback(aAfterMessage, aCallback, aResponse) |
michael@0 | 3161 | { |
michael@0 | 3162 | if (!this.hud) { |
michael@0 | 3163 | return; |
michael@0 | 3164 | } |
michael@0 | 3165 | if (aResponse.error) { |
michael@0 | 3166 | Cu.reportError("Evaluation error " + aResponse.error + ": " + |
michael@0 | 3167 | aResponse.message); |
michael@0 | 3168 | return; |
michael@0 | 3169 | } |
michael@0 | 3170 | let errorMessage = aResponse.exceptionMessage; |
michael@0 | 3171 | let result = aResponse.result; |
michael@0 | 3172 | let helperResult = aResponse.helperResult; |
michael@0 | 3173 | let helperHasRawOutput = !!(helperResult || {}).rawOutput; |
michael@0 | 3174 | |
michael@0 | 3175 | if (helperResult && helperResult.type) { |
michael@0 | 3176 | switch (helperResult.type) { |
michael@0 | 3177 | case "clearOutput": |
michael@0 | 3178 | this.clearOutput(); |
michael@0 | 3179 | break; |
michael@0 | 3180 | case "inspectObject": |
michael@0 | 3181 | if (aAfterMessage) { |
michael@0 | 3182 | if (!aAfterMessage._objectActors) { |
michael@0 | 3183 | aAfterMessage._objectActors = new Set(); |
michael@0 | 3184 | } |
michael@0 | 3185 | aAfterMessage._objectActors.add(helperResult.object.actor); |
michael@0 | 3186 | } |
michael@0 | 3187 | this.openVariablesView({ |
michael@0 | 3188 | label: VariablesView.getString(helperResult.object, { concise: true }), |
michael@0 | 3189 | objectActor: helperResult.object, |
michael@0 | 3190 | }); |
michael@0 | 3191 | break; |
michael@0 | 3192 | case "error": |
michael@0 | 3193 | try { |
michael@0 | 3194 | errorMessage = l10n.getStr(helperResult.message); |
michael@0 | 3195 | } |
michael@0 | 3196 | catch (ex) { |
michael@0 | 3197 | errorMessage = helperResult.message; |
michael@0 | 3198 | } |
michael@0 | 3199 | break; |
michael@0 | 3200 | case "help": |
michael@0 | 3201 | this.hud.owner.openLink(HELP_URL); |
michael@0 | 3202 | break; |
michael@0 | 3203 | } |
michael@0 | 3204 | } |
michael@0 | 3205 | |
michael@0 | 3206 | // Hide undefined results coming from JSTerm helper functions. |
michael@0 | 3207 | if (!errorMessage && result && typeof result == "object" && |
michael@0 | 3208 | result.type == "undefined" && |
michael@0 | 3209 | helperResult && !helperHasRawOutput) { |
michael@0 | 3210 | aCallback && aCallback(); |
michael@0 | 3211 | return; |
michael@0 | 3212 | } |
michael@0 | 3213 | |
michael@0 | 3214 | let msg = new Messages.JavaScriptEvalOutput(aResponse, errorMessage); |
michael@0 | 3215 | this.hud.output.addMessage(msg); |
michael@0 | 3216 | |
michael@0 | 3217 | if (aCallback) { |
michael@0 | 3218 | let oldFlushCallback = this.hud._flushCallback; |
michael@0 | 3219 | this.hud._flushCallback = () => { |
michael@0 | 3220 | aCallback(msg.element); |
michael@0 | 3221 | if (oldFlushCallback) { |
michael@0 | 3222 | oldFlushCallback(); |
michael@0 | 3223 | this.hud._flushCallback = oldFlushCallback; |
michael@0 | 3224 | return true; |
michael@0 | 3225 | } |
michael@0 | 3226 | |
michael@0 | 3227 | return false; |
michael@0 | 3228 | }; |
michael@0 | 3229 | } |
michael@0 | 3230 | |
michael@0 | 3231 | msg._afterMessage = aAfterMessage; |
michael@0 | 3232 | msg._objectActors = new Set(); |
michael@0 | 3233 | |
michael@0 | 3234 | if (WebConsoleUtils.isActorGrip(aResponse.exception)) { |
michael@0 | 3235 | msg._objectActors.add(aResponse.exception.actor); |
michael@0 | 3236 | } |
michael@0 | 3237 | |
michael@0 | 3238 | if (WebConsoleUtils.isActorGrip(result)) { |
michael@0 | 3239 | msg._objectActors.add(result.actor); |
michael@0 | 3240 | } |
michael@0 | 3241 | }, |
michael@0 | 3242 | |
michael@0 | 3243 | /** |
michael@0 | 3244 | * Execute a string. Execution happens asynchronously in the content process. |
michael@0 | 3245 | * |
michael@0 | 3246 | * @param string [aExecuteString] |
michael@0 | 3247 | * The string you want to execute. If this is not provided, the current |
michael@0 | 3248 | * user input is used - taken from |this.inputNode.value|. |
michael@0 | 3249 | * @param function [aCallback] |
michael@0 | 3250 | * Optional function to invoke when the result is displayed. |
michael@0 | 3251 | */ |
michael@0 | 3252 | execute: function JST_execute(aExecuteString, aCallback) |
michael@0 | 3253 | { |
michael@0 | 3254 | // attempt to execute the content of the inputNode |
michael@0 | 3255 | aExecuteString = aExecuteString || this.inputNode.value; |
michael@0 | 3256 | if (!aExecuteString) { |
michael@0 | 3257 | return; |
michael@0 | 3258 | } |
michael@0 | 3259 | |
michael@0 | 3260 | let message = new Messages.Simple(aExecuteString, { |
michael@0 | 3261 | category: "input", |
michael@0 | 3262 | severity: "log", |
michael@0 | 3263 | }); |
michael@0 | 3264 | this.hud.output.addMessage(message); |
michael@0 | 3265 | let onResult = this._executeResultCallback.bind(this, message, aCallback); |
michael@0 | 3266 | |
michael@0 | 3267 | let options = { frame: this.SELECTED_FRAME }; |
michael@0 | 3268 | this.requestEvaluation(aExecuteString, options).then(onResult, onResult); |
michael@0 | 3269 | |
michael@0 | 3270 | // Append a new value in the history of executed code, or overwrite the most |
michael@0 | 3271 | // recent entry. The most recent entry may contain the last edited input |
michael@0 | 3272 | // value that was not evaluated yet. |
michael@0 | 3273 | this.history[this.historyIndex++] = aExecuteString; |
michael@0 | 3274 | this.historyPlaceHolder = this.history.length; |
michael@0 | 3275 | this.setInputValue(""); |
michael@0 | 3276 | this.clearCompletion(); |
michael@0 | 3277 | }, |
michael@0 | 3278 | |
michael@0 | 3279 | /** |
michael@0 | 3280 | * Request a JavaScript string evaluation from the server. |
michael@0 | 3281 | * |
michael@0 | 3282 | * @param string aString |
michael@0 | 3283 | * String to execute. |
michael@0 | 3284 | * @param object [aOptions] |
michael@0 | 3285 | * Options for evaluation: |
michael@0 | 3286 | * - bindObjectActor: tells the ObjectActor ID for which you want to do |
michael@0 | 3287 | * the evaluation. The Debugger.Object of the OA will be bound to |
michael@0 | 3288 | * |_self| during evaluation, such that it's usable in the string you |
michael@0 | 3289 | * execute. |
michael@0 | 3290 | * - frame: tells the stackframe depth to evaluate the string in. If |
michael@0 | 3291 | * the jsdebugger is paused, you can pick the stackframe to be used for |
michael@0 | 3292 | * evaluation. Use |this.SELECTED_FRAME| to always pick the |
michael@0 | 3293 | * user-selected stackframe. |
michael@0 | 3294 | * If you do not provide a |frame| the string will be evaluated in the |
michael@0 | 3295 | * global content window. |
michael@0 | 3296 | * @return object |
michael@0 | 3297 | * A promise object that is resolved when the server response is |
michael@0 | 3298 | * received. |
michael@0 | 3299 | */ |
michael@0 | 3300 | requestEvaluation: function JST_requestEvaluation(aString, aOptions = {}) |
michael@0 | 3301 | { |
michael@0 | 3302 | let deferred = promise.defer(); |
michael@0 | 3303 | |
michael@0 | 3304 | function onResult(aResponse) { |
michael@0 | 3305 | if (!aResponse.error) { |
michael@0 | 3306 | deferred.resolve(aResponse); |
michael@0 | 3307 | } |
michael@0 | 3308 | else { |
michael@0 | 3309 | deferred.reject(aResponse); |
michael@0 | 3310 | } |
michael@0 | 3311 | } |
michael@0 | 3312 | |
michael@0 | 3313 | let frameActor = null; |
michael@0 | 3314 | if ("frame" in aOptions) { |
michael@0 | 3315 | frameActor = this.getFrameActor(aOptions.frame); |
michael@0 | 3316 | } |
michael@0 | 3317 | |
michael@0 | 3318 | let evalOptions = { |
michael@0 | 3319 | bindObjectActor: aOptions.bindObjectActor, |
michael@0 | 3320 | frameActor: frameActor, |
michael@0 | 3321 | }; |
michael@0 | 3322 | |
michael@0 | 3323 | this.webConsoleClient.evaluateJS(aString, onResult, evalOptions); |
michael@0 | 3324 | return deferred.promise; |
michael@0 | 3325 | }, |
michael@0 | 3326 | |
michael@0 | 3327 | /** |
michael@0 | 3328 | * Retrieve the FrameActor ID given a frame depth. |
michael@0 | 3329 | * |
michael@0 | 3330 | * @param number aFrame |
michael@0 | 3331 | * Frame depth. |
michael@0 | 3332 | * @return string|null |
michael@0 | 3333 | * The FrameActor ID for the given frame depth. |
michael@0 | 3334 | */ |
michael@0 | 3335 | getFrameActor: function JST_getFrameActor(aFrame) |
michael@0 | 3336 | { |
michael@0 | 3337 | let state = this.hud.owner.getDebuggerFrames(); |
michael@0 | 3338 | if (!state) { |
michael@0 | 3339 | return null; |
michael@0 | 3340 | } |
michael@0 | 3341 | |
michael@0 | 3342 | let grip; |
michael@0 | 3343 | if (aFrame == this.SELECTED_FRAME) { |
michael@0 | 3344 | grip = state.frames[state.selected]; |
michael@0 | 3345 | } |
michael@0 | 3346 | else { |
michael@0 | 3347 | grip = state.frames[aFrame]; |
michael@0 | 3348 | } |
michael@0 | 3349 | |
michael@0 | 3350 | return grip ? grip.actor : null; |
michael@0 | 3351 | }, |
michael@0 | 3352 | |
michael@0 | 3353 | /** |
michael@0 | 3354 | * Opens a new variables view that allows the inspection of the given object. |
michael@0 | 3355 | * |
michael@0 | 3356 | * @param object aOptions |
michael@0 | 3357 | * Options for the variables view: |
michael@0 | 3358 | * - objectActor: grip of the ObjectActor you want to show in the |
michael@0 | 3359 | * variables view. |
michael@0 | 3360 | * - rawObject: the raw object you want to show in the variables view. |
michael@0 | 3361 | * - label: label to display in the variables view for inspected |
michael@0 | 3362 | * object. |
michael@0 | 3363 | * - hideFilterInput: optional boolean, |true| if you want to hide the |
michael@0 | 3364 | * variables view filter input. |
michael@0 | 3365 | * - targetElement: optional nsIDOMElement to append the variables view |
michael@0 | 3366 | * to. An iframe element is used as a container for the view. If this |
michael@0 | 3367 | * option is not used, then the variables view opens in the sidebar. |
michael@0 | 3368 | * - autofocus: optional boolean, |true| if you want to give focus to |
michael@0 | 3369 | * the variables view window after open, |false| otherwise. |
michael@0 | 3370 | * @return object |
michael@0 | 3371 | * A promise object that is resolved when the variables view has |
michael@0 | 3372 | * opened. The new variables view instance is given to the callbacks. |
michael@0 | 3373 | */ |
michael@0 | 3374 | openVariablesView: function JST_openVariablesView(aOptions) |
michael@0 | 3375 | { |
michael@0 | 3376 | let onContainerReady = (aWindow) => { |
michael@0 | 3377 | let container = aWindow.document.querySelector("#variables"); |
michael@0 | 3378 | let view = this._variablesView; |
michael@0 | 3379 | if (!view || aOptions.targetElement) { |
michael@0 | 3380 | let viewOptions = { |
michael@0 | 3381 | container: container, |
michael@0 | 3382 | hideFilterInput: aOptions.hideFilterInput, |
michael@0 | 3383 | }; |
michael@0 | 3384 | view = this._createVariablesView(viewOptions); |
michael@0 | 3385 | if (!aOptions.targetElement) { |
michael@0 | 3386 | this._variablesView = view; |
michael@0 | 3387 | aWindow.addEventListener("keypress", this._onKeypressInVariablesView); |
michael@0 | 3388 | } |
michael@0 | 3389 | } |
michael@0 | 3390 | aOptions.view = view; |
michael@0 | 3391 | this._updateVariablesView(aOptions); |
michael@0 | 3392 | |
michael@0 | 3393 | if (!aOptions.targetElement && aOptions.autofocus) { |
michael@0 | 3394 | aWindow.focus(); |
michael@0 | 3395 | } |
michael@0 | 3396 | |
michael@0 | 3397 | this.emit("variablesview-open", view, aOptions); |
michael@0 | 3398 | return view; |
michael@0 | 3399 | }; |
michael@0 | 3400 | |
michael@0 | 3401 | let openPromise; |
michael@0 | 3402 | if (aOptions.targetElement) { |
michael@0 | 3403 | let deferred = promise.defer(); |
michael@0 | 3404 | openPromise = deferred.promise; |
michael@0 | 3405 | let document = aOptions.targetElement.ownerDocument; |
michael@0 | 3406 | let iframe = document.createElementNS(XHTML_NS, "iframe"); |
michael@0 | 3407 | |
michael@0 | 3408 | iframe.addEventListener("load", function onIframeLoad(aEvent) { |
michael@0 | 3409 | iframe.removeEventListener("load", onIframeLoad, true); |
michael@0 | 3410 | iframe.style.visibility = "visible"; |
michael@0 | 3411 | deferred.resolve(iframe.contentWindow); |
michael@0 | 3412 | }, true); |
michael@0 | 3413 | |
michael@0 | 3414 | iframe.flex = 1; |
michael@0 | 3415 | iframe.style.visibility = "hidden"; |
michael@0 | 3416 | iframe.setAttribute("src", VARIABLES_VIEW_URL); |
michael@0 | 3417 | aOptions.targetElement.appendChild(iframe); |
michael@0 | 3418 | } |
michael@0 | 3419 | else { |
michael@0 | 3420 | if (!this.sidebar) { |
michael@0 | 3421 | this._createSidebar(); |
michael@0 | 3422 | } |
michael@0 | 3423 | openPromise = this._addVariablesViewSidebarTab(); |
michael@0 | 3424 | } |
michael@0 | 3425 | |
michael@0 | 3426 | return openPromise.then(onContainerReady); |
michael@0 | 3427 | }, |
michael@0 | 3428 | |
michael@0 | 3429 | /** |
michael@0 | 3430 | * Create the Web Console sidebar. |
michael@0 | 3431 | * |
michael@0 | 3432 | * @see devtools/framework/sidebar.js |
michael@0 | 3433 | * @private |
michael@0 | 3434 | */ |
michael@0 | 3435 | _createSidebar: function JST__createSidebar() |
michael@0 | 3436 | { |
michael@0 | 3437 | let tabbox = this.hud.document.querySelector("#webconsole-sidebar"); |
michael@0 | 3438 | this.sidebar = new ToolSidebar(tabbox, this, "webconsole"); |
michael@0 | 3439 | this.sidebar.show(); |
michael@0 | 3440 | }, |
michael@0 | 3441 | |
michael@0 | 3442 | /** |
michael@0 | 3443 | * Add the variables view tab to the sidebar. |
michael@0 | 3444 | * |
michael@0 | 3445 | * @private |
michael@0 | 3446 | * @return object |
michael@0 | 3447 | * A promise object for the adding of the new tab. |
michael@0 | 3448 | */ |
michael@0 | 3449 | _addVariablesViewSidebarTab: function JST__addVariablesViewSidebarTab() |
michael@0 | 3450 | { |
michael@0 | 3451 | let deferred = promise.defer(); |
michael@0 | 3452 | |
michael@0 | 3453 | let onTabReady = () => { |
michael@0 | 3454 | let window = this.sidebar.getWindowForTab("variablesview"); |
michael@0 | 3455 | deferred.resolve(window); |
michael@0 | 3456 | }; |
michael@0 | 3457 | |
michael@0 | 3458 | let tab = this.sidebar.getTab("variablesview"); |
michael@0 | 3459 | if (tab) { |
michael@0 | 3460 | if (this.sidebar.getCurrentTabID() == "variablesview") { |
michael@0 | 3461 | onTabReady(); |
michael@0 | 3462 | } |
michael@0 | 3463 | else { |
michael@0 | 3464 | this.sidebar.once("variablesview-selected", onTabReady); |
michael@0 | 3465 | this.sidebar.select("variablesview"); |
michael@0 | 3466 | } |
michael@0 | 3467 | } |
michael@0 | 3468 | else { |
michael@0 | 3469 | this.sidebar.once("variablesview-ready", onTabReady); |
michael@0 | 3470 | this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true); |
michael@0 | 3471 | } |
michael@0 | 3472 | |
michael@0 | 3473 | return deferred.promise; |
michael@0 | 3474 | }, |
michael@0 | 3475 | |
michael@0 | 3476 | /** |
michael@0 | 3477 | * The keypress event handler for the Variables View sidebar. Currently this |
michael@0 | 3478 | * is used for removing the sidebar when Escape is pressed. |
michael@0 | 3479 | * |
michael@0 | 3480 | * @private |
michael@0 | 3481 | * @param nsIDOMEvent aEvent |
michael@0 | 3482 | * The keypress DOM event object. |
michael@0 | 3483 | */ |
michael@0 | 3484 | _onKeypressInVariablesView: function JST__onKeypressInVariablesView(aEvent) |
michael@0 | 3485 | { |
michael@0 | 3486 | let tag = aEvent.target.nodeName; |
michael@0 | 3487 | if (aEvent.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE || aEvent.shiftKey || |
michael@0 | 3488 | aEvent.altKey || aEvent.ctrlKey || aEvent.metaKey || |
michael@0 | 3489 | ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) { |
michael@0 | 3490 | return; |
michael@0 | 3491 | } |
michael@0 | 3492 | |
michael@0 | 3493 | this._sidebarDestroy(); |
michael@0 | 3494 | this.inputNode.focus(); |
michael@0 | 3495 | aEvent.stopPropagation(); |
michael@0 | 3496 | }, |
michael@0 | 3497 | |
michael@0 | 3498 | /** |
michael@0 | 3499 | * Create a variables view instance. |
michael@0 | 3500 | * |
michael@0 | 3501 | * @private |
michael@0 | 3502 | * @param object aOptions |
michael@0 | 3503 | * Options for the new Variables View instance: |
michael@0 | 3504 | * - container: the DOM element where the variables view is inserted. |
michael@0 | 3505 | * - hideFilterInput: boolean, if true the variables filter input is |
michael@0 | 3506 | * hidden. |
michael@0 | 3507 | * @return object |
michael@0 | 3508 | * The new Variables View instance. |
michael@0 | 3509 | */ |
michael@0 | 3510 | _createVariablesView: function JST__createVariablesView(aOptions) |
michael@0 | 3511 | { |
michael@0 | 3512 | let view = new VariablesView(aOptions.container); |
michael@0 | 3513 | view.toolbox = gDevTools.getToolbox(this.hud.owner.target); |
michael@0 | 3514 | view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder"); |
michael@0 | 3515 | view.emptyText = l10n.getStr("emptyPropertiesList"); |
michael@0 | 3516 | view.searchEnabled = !aOptions.hideFilterInput; |
michael@0 | 3517 | view.lazyEmpty = this._lazyVariablesView; |
michael@0 | 3518 | |
michael@0 | 3519 | VariablesViewController.attach(view, { |
michael@0 | 3520 | getEnvironmentClient: aGrip => { |
michael@0 | 3521 | return new EnvironmentClient(this.hud.proxy.client, aGrip); |
michael@0 | 3522 | }, |
michael@0 | 3523 | getObjectClient: aGrip => { |
michael@0 | 3524 | return new ObjectClient(this.hud.proxy.client, aGrip); |
michael@0 | 3525 | }, |
michael@0 | 3526 | getLongStringClient: aGrip => { |
michael@0 | 3527 | return this.webConsoleClient.longString(aGrip); |
michael@0 | 3528 | }, |
michael@0 | 3529 | releaseActor: aActor => { |
michael@0 | 3530 | this.hud._releaseObject(aActor); |
michael@0 | 3531 | }, |
michael@0 | 3532 | simpleValueEvalMacro: simpleValueEvalMacro, |
michael@0 | 3533 | overrideValueEvalMacro: overrideValueEvalMacro, |
michael@0 | 3534 | getterOrSetterEvalMacro: getterOrSetterEvalMacro, |
michael@0 | 3535 | }); |
michael@0 | 3536 | |
michael@0 | 3537 | // Relay events from the VariablesView. |
michael@0 | 3538 | view.on("fetched", (aEvent, aType, aVar) => { |
michael@0 | 3539 | this.emit("variablesview-fetched", aVar); |
michael@0 | 3540 | }); |
michael@0 | 3541 | |
michael@0 | 3542 | return view; |
michael@0 | 3543 | }, |
michael@0 | 3544 | |
michael@0 | 3545 | /** |
michael@0 | 3546 | * Update the variables view. |
michael@0 | 3547 | * |
michael@0 | 3548 | * @private |
michael@0 | 3549 | * @param object aOptions |
michael@0 | 3550 | * Options for updating the variables view: |
michael@0 | 3551 | * - view: the view you want to update. |
michael@0 | 3552 | * - objectActor: the grip of the new ObjectActor you want to show in |
michael@0 | 3553 | * the view. |
michael@0 | 3554 | * - rawObject: the new raw object you want to show. |
michael@0 | 3555 | * - label: the new label for the inspected object. |
michael@0 | 3556 | */ |
michael@0 | 3557 | _updateVariablesView: function JST__updateVariablesView(aOptions) |
michael@0 | 3558 | { |
michael@0 | 3559 | let view = aOptions.view; |
michael@0 | 3560 | view.empty(); |
michael@0 | 3561 | |
michael@0 | 3562 | // We need to avoid pruning the object inspection starting point. |
michael@0 | 3563 | // That one is pruned when the console message is removed. |
michael@0 | 3564 | view.controller.releaseActors(aActor => { |
michael@0 | 3565 | return view._consoleLastObjectActor != aActor; |
michael@0 | 3566 | }); |
michael@0 | 3567 | |
michael@0 | 3568 | if (aOptions.objectActor && |
michael@0 | 3569 | (!this.hud.owner._browserConsole || |
michael@0 | 3570 | Services.prefs.getBoolPref("devtools.chrome.enabled"))) { |
michael@0 | 3571 | // Make sure eval works in the correct context. |
michael@0 | 3572 | view.eval = this._variablesViewEvaluate.bind(this, aOptions); |
michael@0 | 3573 | view.switch = this._variablesViewSwitch.bind(this, aOptions); |
michael@0 | 3574 | view.delete = this._variablesViewDelete.bind(this, aOptions); |
michael@0 | 3575 | } |
michael@0 | 3576 | else { |
michael@0 | 3577 | view.eval = null; |
michael@0 | 3578 | view.switch = null; |
michael@0 | 3579 | view.delete = null; |
michael@0 | 3580 | } |
michael@0 | 3581 | |
michael@0 | 3582 | let { variable, expanded } = view.controller.setSingleVariable(aOptions); |
michael@0 | 3583 | variable.evaluationMacro = simpleValueEvalMacro; |
michael@0 | 3584 | |
michael@0 | 3585 | if (aOptions.objectActor) { |
michael@0 | 3586 | view._consoleLastObjectActor = aOptions.objectActor.actor; |
michael@0 | 3587 | } |
michael@0 | 3588 | else if (aOptions.rawObject) { |
michael@0 | 3589 | view._consoleLastObjectActor = null; |
michael@0 | 3590 | } |
michael@0 | 3591 | else { |
michael@0 | 3592 | throw new Error("Variables View cannot open without giving it an object " + |
michael@0 | 3593 | "display."); |
michael@0 | 3594 | } |
michael@0 | 3595 | |
michael@0 | 3596 | expanded.then(() => { |
michael@0 | 3597 | this.emit("variablesview-updated", view, aOptions); |
michael@0 | 3598 | }); |
michael@0 | 3599 | }, |
michael@0 | 3600 | |
michael@0 | 3601 | /** |
michael@0 | 3602 | * The evaluation function used by the variables view when editing a property |
michael@0 | 3603 | * value. |
michael@0 | 3604 | * |
michael@0 | 3605 | * @private |
michael@0 | 3606 | * @param object aOptions |
michael@0 | 3607 | * The options used for |this._updateVariablesView()|. |
michael@0 | 3608 | * @param object aVar |
michael@0 | 3609 | * The Variable object instance for the edited property. |
michael@0 | 3610 | * @param string aValue |
michael@0 | 3611 | * The value the edited property was changed to. |
michael@0 | 3612 | */ |
michael@0 | 3613 | _variablesViewEvaluate: |
michael@0 | 3614 | function JST__variablesViewEvaluate(aOptions, aVar, aValue) |
michael@0 | 3615 | { |
michael@0 | 3616 | let updater = this._updateVariablesView.bind(this, aOptions); |
michael@0 | 3617 | let onEval = this._silentEvalCallback.bind(this, updater); |
michael@0 | 3618 | let string = aVar.evaluationMacro(aVar, aValue); |
michael@0 | 3619 | |
michael@0 | 3620 | let evalOptions = { |
michael@0 | 3621 | frame: this.SELECTED_FRAME, |
michael@0 | 3622 | bindObjectActor: aOptions.objectActor.actor, |
michael@0 | 3623 | }; |
michael@0 | 3624 | |
michael@0 | 3625 | this.requestEvaluation(string, evalOptions).then(onEval, onEval); |
michael@0 | 3626 | }, |
michael@0 | 3627 | |
michael@0 | 3628 | /** |
michael@0 | 3629 | * The property deletion function used by the variables view when a property |
michael@0 | 3630 | * is deleted. |
michael@0 | 3631 | * |
michael@0 | 3632 | * @private |
michael@0 | 3633 | * @param object aOptions |
michael@0 | 3634 | * The options used for |this._updateVariablesView()|. |
michael@0 | 3635 | * @param object aVar |
michael@0 | 3636 | * The Variable object instance for the deleted property. |
michael@0 | 3637 | */ |
michael@0 | 3638 | _variablesViewDelete: function JST__variablesViewDelete(aOptions, aVar) |
michael@0 | 3639 | { |
michael@0 | 3640 | let onEval = this._silentEvalCallback.bind(this, null); |
michael@0 | 3641 | |
michael@0 | 3642 | let evalOptions = { |
michael@0 | 3643 | frame: this.SELECTED_FRAME, |
michael@0 | 3644 | bindObjectActor: aOptions.objectActor.actor, |
michael@0 | 3645 | }; |
michael@0 | 3646 | |
michael@0 | 3647 | this.requestEvaluation("delete _self" + aVar.symbolicName, evalOptions) |
michael@0 | 3648 | .then(onEval, onEval); |
michael@0 | 3649 | }, |
michael@0 | 3650 | |
michael@0 | 3651 | /** |
michael@0 | 3652 | * The property rename function used by the variables view when a property |
michael@0 | 3653 | * is renamed. |
michael@0 | 3654 | * |
michael@0 | 3655 | * @private |
michael@0 | 3656 | * @param object aOptions |
michael@0 | 3657 | * The options used for |this._updateVariablesView()|. |
michael@0 | 3658 | * @param object aVar |
michael@0 | 3659 | * The Variable object instance for the renamed property. |
michael@0 | 3660 | * @param string aNewName |
michael@0 | 3661 | * The new name for the property. |
michael@0 | 3662 | */ |
michael@0 | 3663 | _variablesViewSwitch: |
michael@0 | 3664 | function JST__variablesViewSwitch(aOptions, aVar, aNewName) |
michael@0 | 3665 | { |
michael@0 | 3666 | let updater = this._updateVariablesView.bind(this, aOptions); |
michael@0 | 3667 | let onEval = this._silentEvalCallback.bind(this, updater); |
michael@0 | 3668 | |
michael@0 | 3669 | let evalOptions = { |
michael@0 | 3670 | frame: this.SELECTED_FRAME, |
michael@0 | 3671 | bindObjectActor: aOptions.objectActor.actor, |
michael@0 | 3672 | }; |
michael@0 | 3673 | |
michael@0 | 3674 | let newSymbolicName = aVar.ownerView.symbolicName + '["' + aNewName + '"]'; |
michael@0 | 3675 | if (newSymbolicName == aVar.symbolicName) { |
michael@0 | 3676 | return; |
michael@0 | 3677 | } |
michael@0 | 3678 | |
michael@0 | 3679 | let code = "_self" + newSymbolicName + " = _self" + aVar.symbolicName + ";" + |
michael@0 | 3680 | "delete _self" + aVar.symbolicName; |
michael@0 | 3681 | |
michael@0 | 3682 | this.requestEvaluation(code, evalOptions).then(onEval, onEval); |
michael@0 | 3683 | }, |
michael@0 | 3684 | |
michael@0 | 3685 | /** |
michael@0 | 3686 | * A noop callback for JavaScript evaluation. This method releases any |
michael@0 | 3687 | * result ObjectActors that come from the server for evaluation requests. This |
michael@0 | 3688 | * is used for editing, renaming and deleting properties in the variables |
michael@0 | 3689 | * view. |
michael@0 | 3690 | * |
michael@0 | 3691 | * Exceptions are displayed in the output. |
michael@0 | 3692 | * |
michael@0 | 3693 | * @private |
michael@0 | 3694 | * @param function aCallback |
michael@0 | 3695 | * Function to invoke once the response is received. |
michael@0 | 3696 | * @param object aResponse |
michael@0 | 3697 | * The response packet received from the server. |
michael@0 | 3698 | */ |
michael@0 | 3699 | _silentEvalCallback: function JST__silentEvalCallback(aCallback, aResponse) |
michael@0 | 3700 | { |
michael@0 | 3701 | if (aResponse.error) { |
michael@0 | 3702 | Cu.reportError("Web Console evaluation failed. " + aResponse.error + ":" + |
michael@0 | 3703 | aResponse.message); |
michael@0 | 3704 | |
michael@0 | 3705 | aCallback && aCallback(aResponse); |
michael@0 | 3706 | return; |
michael@0 | 3707 | } |
michael@0 | 3708 | |
michael@0 | 3709 | if (aResponse.exceptionMessage) { |
michael@0 | 3710 | let message = new Messages.Simple(aResponse.exceptionMessage, { |
michael@0 | 3711 | category: "output", |
michael@0 | 3712 | severity: "error", |
michael@0 | 3713 | timestamp: aResponse.timestamp, |
michael@0 | 3714 | }); |
michael@0 | 3715 | this.hud.output.addMessage(message); |
michael@0 | 3716 | message._objectActors = new Set(); |
michael@0 | 3717 | if (WebConsoleUtils.isActorGrip(aResponse.exception)) { |
michael@0 | 3718 | message._objectActors.add(aResponse.exception.actor); |
michael@0 | 3719 | } |
michael@0 | 3720 | } |
michael@0 | 3721 | |
michael@0 | 3722 | let helper = aResponse.helperResult || { type: null }; |
michael@0 | 3723 | let helperGrip = null; |
michael@0 | 3724 | if (helper.type == "inspectObject") { |
michael@0 | 3725 | helperGrip = helper.object; |
michael@0 | 3726 | } |
michael@0 | 3727 | |
michael@0 | 3728 | let grips = [aResponse.result, helperGrip]; |
michael@0 | 3729 | for (let grip of grips) { |
michael@0 | 3730 | if (WebConsoleUtils.isActorGrip(grip)) { |
michael@0 | 3731 | this.hud._releaseObject(grip.actor); |
michael@0 | 3732 | } |
michael@0 | 3733 | } |
michael@0 | 3734 | |
michael@0 | 3735 | aCallback && aCallback(aResponse); |
michael@0 | 3736 | }, |
michael@0 | 3737 | |
michael@0 | 3738 | |
michael@0 | 3739 | /** |
michael@0 | 3740 | * Clear the Web Console output. |
michael@0 | 3741 | * |
michael@0 | 3742 | * This method emits the "messages-cleared" notification. |
michael@0 | 3743 | * |
michael@0 | 3744 | * @param boolean aClearStorage |
michael@0 | 3745 | * True if you want to clear the console messages storage associated to |
michael@0 | 3746 | * this Web Console. |
michael@0 | 3747 | */ |
michael@0 | 3748 | clearOutput: function JST_clearOutput(aClearStorage) |
michael@0 | 3749 | { |
michael@0 | 3750 | let hud = this.hud; |
michael@0 | 3751 | let outputNode = hud.outputNode; |
michael@0 | 3752 | let node; |
michael@0 | 3753 | while ((node = outputNode.firstChild)) { |
michael@0 | 3754 | hud.removeOutputMessage(node); |
michael@0 | 3755 | } |
michael@0 | 3756 | |
michael@0 | 3757 | hud.groupDepth = 0; |
michael@0 | 3758 | hud._outputQueue.forEach(hud._pruneItemFromQueue, hud); |
michael@0 | 3759 | hud._outputQueue = []; |
michael@0 | 3760 | hud._networkRequests = {}; |
michael@0 | 3761 | hud._repeatNodes = {}; |
michael@0 | 3762 | |
michael@0 | 3763 | if (aClearStorage) { |
michael@0 | 3764 | this.webConsoleClient.clearMessagesCache(); |
michael@0 | 3765 | } |
michael@0 | 3766 | |
michael@0 | 3767 | this.emit("messages-cleared"); |
michael@0 | 3768 | }, |
michael@0 | 3769 | |
michael@0 | 3770 | /** |
michael@0 | 3771 | * Remove all of the private messages from the Web Console output. |
michael@0 | 3772 | * |
michael@0 | 3773 | * This method emits the "private-messages-cleared" notification. |
michael@0 | 3774 | */ |
michael@0 | 3775 | clearPrivateMessages: function JST_clearPrivateMessages() |
michael@0 | 3776 | { |
michael@0 | 3777 | let nodes = this.hud.outputNode.querySelectorAll(".message[private]"); |
michael@0 | 3778 | for (let node of nodes) { |
michael@0 | 3779 | this.hud.removeOutputMessage(node); |
michael@0 | 3780 | } |
michael@0 | 3781 | this.emit("private-messages-cleared"); |
michael@0 | 3782 | }, |
michael@0 | 3783 | |
michael@0 | 3784 | /** |
michael@0 | 3785 | * Updates the size of the input field (command line) to fit its contents. |
michael@0 | 3786 | * |
michael@0 | 3787 | * @returns void |
michael@0 | 3788 | */ |
michael@0 | 3789 | resizeInput: function JST_resizeInput() |
michael@0 | 3790 | { |
michael@0 | 3791 | let inputNode = this.inputNode; |
michael@0 | 3792 | |
michael@0 | 3793 | // Reset the height so that scrollHeight will reflect the natural height of |
michael@0 | 3794 | // the contents of the input field. |
michael@0 | 3795 | inputNode.style.height = "auto"; |
michael@0 | 3796 | |
michael@0 | 3797 | // Now resize the input field to fit its contents. |
michael@0 | 3798 | let scrollHeight = inputNode.inputField.scrollHeight; |
michael@0 | 3799 | if (scrollHeight > 0) { |
michael@0 | 3800 | inputNode.style.height = scrollHeight + "px"; |
michael@0 | 3801 | } |
michael@0 | 3802 | }, |
michael@0 | 3803 | |
michael@0 | 3804 | /** |
michael@0 | 3805 | * Sets the value of the input field (command line), and resizes the field to |
michael@0 | 3806 | * fit its contents. This method is preferred over setting "inputNode.value" |
michael@0 | 3807 | * directly, because it correctly resizes the field. |
michael@0 | 3808 | * |
michael@0 | 3809 | * @param string aNewValue |
michael@0 | 3810 | * The new value to set. |
michael@0 | 3811 | * @returns void |
michael@0 | 3812 | */ |
michael@0 | 3813 | setInputValue: function JST_setInputValue(aNewValue) |
michael@0 | 3814 | { |
michael@0 | 3815 | this.inputNode.value = aNewValue; |
michael@0 | 3816 | this.lastInputValue = aNewValue; |
michael@0 | 3817 | this.completeNode.value = ""; |
michael@0 | 3818 | this.resizeInput(); |
michael@0 | 3819 | this._inputChanged = true; |
michael@0 | 3820 | }, |
michael@0 | 3821 | |
michael@0 | 3822 | /** |
michael@0 | 3823 | * The inputNode "input" and "keyup" event handler. |
michael@0 | 3824 | * @private |
michael@0 | 3825 | */ |
michael@0 | 3826 | _inputEventHandler: function JST__inputEventHandler() |
michael@0 | 3827 | { |
michael@0 | 3828 | if (this.lastInputValue != this.inputNode.value) { |
michael@0 | 3829 | this.resizeInput(); |
michael@0 | 3830 | this.complete(this.COMPLETE_HINT_ONLY); |
michael@0 | 3831 | this.lastInputValue = this.inputNode.value; |
michael@0 | 3832 | this._inputChanged = true; |
michael@0 | 3833 | } |
michael@0 | 3834 | }, |
michael@0 | 3835 | |
michael@0 | 3836 | /** |
michael@0 | 3837 | * The window "blur" event handler. |
michael@0 | 3838 | * @private |
michael@0 | 3839 | */ |
michael@0 | 3840 | _blurEventHandler: function JST__blurEventHandler() |
michael@0 | 3841 | { |
michael@0 | 3842 | if (this.autocompletePopup) { |
michael@0 | 3843 | this.clearCompletion(); |
michael@0 | 3844 | } |
michael@0 | 3845 | }, |
michael@0 | 3846 | |
michael@0 | 3847 | /** |
michael@0 | 3848 | * The inputNode "keypress" event handler. |
michael@0 | 3849 | * |
michael@0 | 3850 | * @private |
michael@0 | 3851 | * @param nsIDOMEvent aEvent |
michael@0 | 3852 | */ |
michael@0 | 3853 | _keyPress: function JST__keyPress(aEvent) |
michael@0 | 3854 | { |
michael@0 | 3855 | let inputNode = this.inputNode; |
michael@0 | 3856 | let inputUpdated = false; |
michael@0 | 3857 | |
michael@0 | 3858 | if (aEvent.ctrlKey) { |
michael@0 | 3859 | switch (aEvent.charCode) { |
michael@0 | 3860 | case 101: |
michael@0 | 3861 | // control-e |
michael@0 | 3862 | if (Services.appinfo.OS == "WINNT") { |
michael@0 | 3863 | break; |
michael@0 | 3864 | } |
michael@0 | 3865 | let lineEndPos = inputNode.value.length; |
michael@0 | 3866 | if (this.hasMultilineInput()) { |
michael@0 | 3867 | // find index of closest newline >= cursor |
michael@0 | 3868 | for (let i = inputNode.selectionEnd; i<lineEndPos; i++) { |
michael@0 | 3869 | if (inputNode.value.charAt(i) == "\r" || |
michael@0 | 3870 | inputNode.value.charAt(i) == "\n") { |
michael@0 | 3871 | lineEndPos = i; |
michael@0 | 3872 | break; |
michael@0 | 3873 | } |
michael@0 | 3874 | } |
michael@0 | 3875 | } |
michael@0 | 3876 | inputNode.setSelectionRange(lineEndPos, lineEndPos); |
michael@0 | 3877 | aEvent.preventDefault(); |
michael@0 | 3878 | this.clearCompletion(); |
michael@0 | 3879 | break; |
michael@0 | 3880 | |
michael@0 | 3881 | case 110: |
michael@0 | 3882 | // Control-N differs from down arrow: it ignores autocomplete state. |
michael@0 | 3883 | // Note that we preserve the default 'down' navigation within |
michael@0 | 3884 | // multiline text. |
michael@0 | 3885 | if (Services.appinfo.OS == "Darwin" && |
michael@0 | 3886 | this.canCaretGoNext() && |
michael@0 | 3887 | this.historyPeruse(HISTORY_FORWARD)) { |
michael@0 | 3888 | aEvent.preventDefault(); |
michael@0 | 3889 | // Ctrl-N is also used to focus the Network category button on MacOSX. |
michael@0 | 3890 | // The preventDefault() call doesn't prevent the focus from moving |
michael@0 | 3891 | // away from the input. |
michael@0 | 3892 | inputNode.focus(); |
michael@0 | 3893 | } |
michael@0 | 3894 | this.clearCompletion(); |
michael@0 | 3895 | break; |
michael@0 | 3896 | |
michael@0 | 3897 | case 112: |
michael@0 | 3898 | // Control-P differs from up arrow: it ignores autocomplete state. |
michael@0 | 3899 | // Note that we preserve the default 'up' navigation within |
michael@0 | 3900 | // multiline text. |
michael@0 | 3901 | if (Services.appinfo.OS == "Darwin" && |
michael@0 | 3902 | this.canCaretGoPrevious() && |
michael@0 | 3903 | this.historyPeruse(HISTORY_BACK)) { |
michael@0 | 3904 | aEvent.preventDefault(); |
michael@0 | 3905 | // Ctrl-P may also be used to focus some category button on MacOSX. |
michael@0 | 3906 | // The preventDefault() call doesn't prevent the focus from moving |
michael@0 | 3907 | // away from the input. |
michael@0 | 3908 | inputNode.focus(); |
michael@0 | 3909 | } |
michael@0 | 3910 | this.clearCompletion(); |
michael@0 | 3911 | break; |
michael@0 | 3912 | default: |
michael@0 | 3913 | break; |
michael@0 | 3914 | } |
michael@0 | 3915 | return; |
michael@0 | 3916 | } |
michael@0 | 3917 | else if (aEvent.shiftKey && |
michael@0 | 3918 | aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) { |
michael@0 | 3919 | // shift return |
michael@0 | 3920 | // TODO: expand the inputNode height by one line |
michael@0 | 3921 | return; |
michael@0 | 3922 | } |
michael@0 | 3923 | |
michael@0 | 3924 | switch (aEvent.keyCode) { |
michael@0 | 3925 | case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE: |
michael@0 | 3926 | if (this.autocompletePopup.isOpen) { |
michael@0 | 3927 | this.clearCompletion(); |
michael@0 | 3928 | aEvent.preventDefault(); |
michael@0 | 3929 | aEvent.stopPropagation(); |
michael@0 | 3930 | } |
michael@0 | 3931 | else if (this.sidebar) { |
michael@0 | 3932 | this._sidebarDestroy(); |
michael@0 | 3933 | aEvent.preventDefault(); |
michael@0 | 3934 | aEvent.stopPropagation(); |
michael@0 | 3935 | } |
michael@0 | 3936 | break; |
michael@0 | 3937 | |
michael@0 | 3938 | case Ci.nsIDOMKeyEvent.DOM_VK_RETURN: |
michael@0 | 3939 | if (this._autocompletePopupNavigated && |
michael@0 | 3940 | this.autocompletePopup.isOpen && |
michael@0 | 3941 | this.autocompletePopup.selectedIndex > -1) { |
michael@0 | 3942 | this.acceptProposedCompletion(); |
michael@0 | 3943 | } |
michael@0 | 3944 | else { |
michael@0 | 3945 | this.execute(); |
michael@0 | 3946 | this._inputChanged = false; |
michael@0 | 3947 | } |
michael@0 | 3948 | aEvent.preventDefault(); |
michael@0 | 3949 | break; |
michael@0 | 3950 | |
michael@0 | 3951 | case Ci.nsIDOMKeyEvent.DOM_VK_UP: |
michael@0 | 3952 | if (this.autocompletePopup.isOpen) { |
michael@0 | 3953 | inputUpdated = this.complete(this.COMPLETE_BACKWARD); |
michael@0 | 3954 | if (inputUpdated) { |
michael@0 | 3955 | this._autocompletePopupNavigated = true; |
michael@0 | 3956 | } |
michael@0 | 3957 | } |
michael@0 | 3958 | else if (this.canCaretGoPrevious()) { |
michael@0 | 3959 | inputUpdated = this.historyPeruse(HISTORY_BACK); |
michael@0 | 3960 | } |
michael@0 | 3961 | if (inputUpdated) { |
michael@0 | 3962 | aEvent.preventDefault(); |
michael@0 | 3963 | } |
michael@0 | 3964 | break; |
michael@0 | 3965 | |
michael@0 | 3966 | case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: |
michael@0 | 3967 | if (this.autocompletePopup.isOpen) { |
michael@0 | 3968 | inputUpdated = this.complete(this.COMPLETE_FORWARD); |
michael@0 | 3969 | if (inputUpdated) { |
michael@0 | 3970 | this._autocompletePopupNavigated = true; |
michael@0 | 3971 | } |
michael@0 | 3972 | } |
michael@0 | 3973 | else if (this.canCaretGoNext()) { |
michael@0 | 3974 | inputUpdated = this.historyPeruse(HISTORY_FORWARD); |
michael@0 | 3975 | } |
michael@0 | 3976 | if (inputUpdated) { |
michael@0 | 3977 | aEvent.preventDefault(); |
michael@0 | 3978 | } |
michael@0 | 3979 | break; |
michael@0 | 3980 | |
michael@0 | 3981 | case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: |
michael@0 | 3982 | if (this.autocompletePopup.isOpen) { |
michael@0 | 3983 | inputUpdated = this.complete(this.COMPLETE_PAGEUP); |
michael@0 | 3984 | if (inputUpdated) { |
michael@0 | 3985 | this._autocompletePopupNavigated = true; |
michael@0 | 3986 | } |
michael@0 | 3987 | } |
michael@0 | 3988 | else { |
michael@0 | 3989 | this.hud.outputNode.parentNode.scrollTop = |
michael@0 | 3990 | Math.max(0, |
michael@0 | 3991 | this.hud.outputNode.parentNode.scrollTop - |
michael@0 | 3992 | this.hud.outputNode.parentNode.clientHeight |
michael@0 | 3993 | ); |
michael@0 | 3994 | } |
michael@0 | 3995 | aEvent.preventDefault(); |
michael@0 | 3996 | break; |
michael@0 | 3997 | |
michael@0 | 3998 | case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: |
michael@0 | 3999 | if (this.autocompletePopup.isOpen) { |
michael@0 | 4000 | inputUpdated = this.complete(this.COMPLETE_PAGEDOWN); |
michael@0 | 4001 | if (inputUpdated) { |
michael@0 | 4002 | this._autocompletePopupNavigated = true; |
michael@0 | 4003 | } |
michael@0 | 4004 | } |
michael@0 | 4005 | else { |
michael@0 | 4006 | this.hud.outputNode.parentNode.scrollTop = |
michael@0 | 4007 | Math.min(this.hud.outputNode.parentNode.scrollHeight, |
michael@0 | 4008 | this.hud.outputNode.parentNode.scrollTop + |
michael@0 | 4009 | this.hud.outputNode.parentNode.clientHeight |
michael@0 | 4010 | ); |
michael@0 | 4011 | } |
michael@0 | 4012 | aEvent.preventDefault(); |
michael@0 | 4013 | break; |
michael@0 | 4014 | |
michael@0 | 4015 | case Ci.nsIDOMKeyEvent.DOM_VK_HOME: |
michael@0 | 4016 | case Ci.nsIDOMKeyEvent.DOM_VK_END: |
michael@0 | 4017 | case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: |
michael@0 | 4018 | if (this.autocompletePopup.isOpen || this.lastCompletion.value) { |
michael@0 | 4019 | this.clearCompletion(); |
michael@0 | 4020 | } |
michael@0 | 4021 | break; |
michael@0 | 4022 | |
michael@0 | 4023 | case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: { |
michael@0 | 4024 | let cursorAtTheEnd = this.inputNode.selectionStart == |
michael@0 | 4025 | this.inputNode.selectionEnd && |
michael@0 | 4026 | this.inputNode.selectionStart == |
michael@0 | 4027 | this.inputNode.value.length; |
michael@0 | 4028 | let haveSuggestion = this.autocompletePopup.isOpen || |
michael@0 | 4029 | this.lastCompletion.value; |
michael@0 | 4030 | let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated; |
michael@0 | 4031 | if (haveSuggestion && useCompletion && |
michael@0 | 4032 | this.complete(this.COMPLETE_HINT_ONLY) && |
michael@0 | 4033 | this.lastCompletion.value && |
michael@0 | 4034 | this.acceptProposedCompletion()) { |
michael@0 | 4035 | aEvent.preventDefault(); |
michael@0 | 4036 | } |
michael@0 | 4037 | if (this.autocompletePopup.isOpen) { |
michael@0 | 4038 | this.clearCompletion(); |
michael@0 | 4039 | } |
michael@0 | 4040 | break; |
michael@0 | 4041 | } |
michael@0 | 4042 | case Ci.nsIDOMKeyEvent.DOM_VK_TAB: |
michael@0 | 4043 | // Generate a completion and accept the first proposed value. |
michael@0 | 4044 | if (this.complete(this.COMPLETE_HINT_ONLY) && |
michael@0 | 4045 | this.lastCompletion && |
michael@0 | 4046 | this.acceptProposedCompletion()) { |
michael@0 | 4047 | aEvent.preventDefault(); |
michael@0 | 4048 | } |
michael@0 | 4049 | else if (this._inputChanged) { |
michael@0 | 4050 | this.updateCompleteNode(l10n.getStr("Autocomplete.blank")); |
michael@0 | 4051 | aEvent.preventDefault(); |
michael@0 | 4052 | } |
michael@0 | 4053 | break; |
michael@0 | 4054 | default: |
michael@0 | 4055 | break; |
michael@0 | 4056 | } |
michael@0 | 4057 | }, |
michael@0 | 4058 | |
michael@0 | 4059 | /** |
michael@0 | 4060 | * The inputNode "focus" event handler. |
michael@0 | 4061 | * @private |
michael@0 | 4062 | */ |
michael@0 | 4063 | _focusEventHandler: function JST__focusEventHandler() |
michael@0 | 4064 | { |
michael@0 | 4065 | this._inputChanged = false; |
michael@0 | 4066 | }, |
michael@0 | 4067 | |
michael@0 | 4068 | /** |
michael@0 | 4069 | * Go up/down the history stack of input values. |
michael@0 | 4070 | * |
michael@0 | 4071 | * @param number aDirection |
michael@0 | 4072 | * History navigation direction: HISTORY_BACK or HISTORY_FORWARD. |
michael@0 | 4073 | * |
michael@0 | 4074 | * @returns boolean |
michael@0 | 4075 | * True if the input value changed, false otherwise. |
michael@0 | 4076 | */ |
michael@0 | 4077 | historyPeruse: function JST_historyPeruse(aDirection) |
michael@0 | 4078 | { |
michael@0 | 4079 | if (!this.history.length) { |
michael@0 | 4080 | return false; |
michael@0 | 4081 | } |
michael@0 | 4082 | |
michael@0 | 4083 | // Up Arrow key |
michael@0 | 4084 | if (aDirection == HISTORY_BACK) { |
michael@0 | 4085 | if (this.historyPlaceHolder <= 0) { |
michael@0 | 4086 | return false; |
michael@0 | 4087 | } |
michael@0 | 4088 | let inputVal = this.history[--this.historyPlaceHolder]; |
michael@0 | 4089 | |
michael@0 | 4090 | // Save the current input value as the latest entry in history, only if |
michael@0 | 4091 | // the user is already at the last entry. |
michael@0 | 4092 | // Note: this code does not store changes to items that are already in |
michael@0 | 4093 | // history. |
michael@0 | 4094 | if (this.historyPlaceHolder+1 == this.historyIndex) { |
michael@0 | 4095 | this.history[this.historyIndex] = this.inputNode.value || ""; |
michael@0 | 4096 | } |
michael@0 | 4097 | |
michael@0 | 4098 | this.setInputValue(inputVal); |
michael@0 | 4099 | } |
michael@0 | 4100 | // Down Arrow key |
michael@0 | 4101 | else if (aDirection == HISTORY_FORWARD) { |
michael@0 | 4102 | if (this.historyPlaceHolder >= (this.history.length-1)) { |
michael@0 | 4103 | return false; |
michael@0 | 4104 | } |
michael@0 | 4105 | |
michael@0 | 4106 | let inputVal = this.history[++this.historyPlaceHolder]; |
michael@0 | 4107 | this.setInputValue(inputVal); |
michael@0 | 4108 | } |
michael@0 | 4109 | else { |
michael@0 | 4110 | throw new Error("Invalid argument 0"); |
michael@0 | 4111 | } |
michael@0 | 4112 | |
michael@0 | 4113 | return true; |
michael@0 | 4114 | }, |
michael@0 | 4115 | |
michael@0 | 4116 | /** |
michael@0 | 4117 | * Test for multiline input. |
michael@0 | 4118 | * |
michael@0 | 4119 | * @return boolean |
michael@0 | 4120 | * True if CR or LF found in node value; else false. |
michael@0 | 4121 | */ |
michael@0 | 4122 | hasMultilineInput: function JST_hasMultilineInput() |
michael@0 | 4123 | { |
michael@0 | 4124 | return /[\r\n]/.test(this.inputNode.value); |
michael@0 | 4125 | }, |
michael@0 | 4126 | |
michael@0 | 4127 | /** |
michael@0 | 4128 | * Check if the caret is at a location that allows selecting the previous item |
michael@0 | 4129 | * in history when the user presses the Up arrow key. |
michael@0 | 4130 | * |
michael@0 | 4131 | * @return boolean |
michael@0 | 4132 | * True if the caret is at a location that allows selecting the |
michael@0 | 4133 | * previous item in history when the user presses the Up arrow key, |
michael@0 | 4134 | * otherwise false. |
michael@0 | 4135 | */ |
michael@0 | 4136 | canCaretGoPrevious: function JST_canCaretGoPrevious() |
michael@0 | 4137 | { |
michael@0 | 4138 | let node = this.inputNode; |
michael@0 | 4139 | if (node.selectionStart != node.selectionEnd) { |
michael@0 | 4140 | return false; |
michael@0 | 4141 | } |
michael@0 | 4142 | |
michael@0 | 4143 | let multiline = /[\r\n]/.test(node.value); |
michael@0 | 4144 | return node.selectionStart == 0 ? true : |
michael@0 | 4145 | node.selectionStart == node.value.length && !multiline; |
michael@0 | 4146 | }, |
michael@0 | 4147 | |
michael@0 | 4148 | /** |
michael@0 | 4149 | * Check if the caret is at a location that allows selecting the next item in |
michael@0 | 4150 | * history when the user presses the Down arrow key. |
michael@0 | 4151 | * |
michael@0 | 4152 | * @return boolean |
michael@0 | 4153 | * True if the caret is at a location that allows selecting the next |
michael@0 | 4154 | * item in history when the user presses the Down arrow key, otherwise |
michael@0 | 4155 | * false. |
michael@0 | 4156 | */ |
michael@0 | 4157 | canCaretGoNext: function JST_canCaretGoNext() |
michael@0 | 4158 | { |
michael@0 | 4159 | let node = this.inputNode; |
michael@0 | 4160 | if (node.selectionStart != node.selectionEnd) { |
michael@0 | 4161 | return false; |
michael@0 | 4162 | } |
michael@0 | 4163 | |
michael@0 | 4164 | let multiline = /[\r\n]/.test(node.value); |
michael@0 | 4165 | return node.selectionStart == node.value.length ? true : |
michael@0 | 4166 | node.selectionStart == 0 && !multiline; |
michael@0 | 4167 | }, |
michael@0 | 4168 | |
michael@0 | 4169 | /** |
michael@0 | 4170 | * Completes the current typed text in the inputNode. Completion is performed |
michael@0 | 4171 | * only if the selection/cursor is at the end of the string. If no completion |
michael@0 | 4172 | * is found, the current inputNode value and cursor/selection stay. |
michael@0 | 4173 | * |
michael@0 | 4174 | * @param int aType possible values are |
michael@0 | 4175 | * - this.COMPLETE_FORWARD: If there is more than one possible completion |
michael@0 | 4176 | * and the input value stayed the same compared to the last time this |
michael@0 | 4177 | * function was called, then the next completion of all possible |
michael@0 | 4178 | * completions is used. If the value changed, then the first possible |
michael@0 | 4179 | * completion is used and the selection is set from the current |
michael@0 | 4180 | * cursor position to the end of the completed text. |
michael@0 | 4181 | * If there is only one possible completion, then this completion |
michael@0 | 4182 | * value is used and the cursor is put at the end of the completion. |
michael@0 | 4183 | * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the |
michael@0 | 4184 | * value stayed the same as the last time the function was called, |
michael@0 | 4185 | * then the previous completion of all possible completions is used. |
michael@0 | 4186 | * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the first |
michael@0 | 4187 | * item. |
michael@0 | 4188 | * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select the |
michael@0 | 4189 | * last item. |
michael@0 | 4190 | * - this.COMPLETE_HINT_ONLY: If there is more than one possible |
michael@0 | 4191 | * completion and the input value stayed the same compared to the |
michael@0 | 4192 | * last time this function was called, then the same completion is |
michael@0 | 4193 | * used again. If there is only one possible completion, then |
michael@0 | 4194 | * the inputNode.value is set to this value and the selection is set |
michael@0 | 4195 | * from the current cursor position to the end of the completed text. |
michael@0 | 4196 | * @param function aCallback |
michael@0 | 4197 | * Optional function invoked when the autocomplete properties are |
michael@0 | 4198 | * updated. |
michael@0 | 4199 | * @returns boolean true if there existed a completion for the current input, |
michael@0 | 4200 | * or false otherwise. |
michael@0 | 4201 | */ |
michael@0 | 4202 | complete: function JSTF_complete(aType, aCallback) |
michael@0 | 4203 | { |
michael@0 | 4204 | let inputNode = this.inputNode; |
michael@0 | 4205 | let inputValue = inputNode.value; |
michael@0 | 4206 | let frameActor = this.getFrameActor(this.SELECTED_FRAME); |
michael@0 | 4207 | |
michael@0 | 4208 | // If the inputNode has no value, then don't try to complete on it. |
michael@0 | 4209 | if (!inputValue) { |
michael@0 | 4210 | this.clearCompletion(); |
michael@0 | 4211 | aCallback && aCallback(this); |
michael@0 | 4212 | this.emit("autocomplete-updated"); |
michael@0 | 4213 | return false; |
michael@0 | 4214 | } |
michael@0 | 4215 | |
michael@0 | 4216 | // Only complete if the selection is empty. |
michael@0 | 4217 | if (inputNode.selectionStart != inputNode.selectionEnd) { |
michael@0 | 4218 | this.clearCompletion(); |
michael@0 | 4219 | aCallback && aCallback(this); |
michael@0 | 4220 | this.emit("autocomplete-updated"); |
michael@0 | 4221 | return false; |
michael@0 | 4222 | } |
michael@0 | 4223 | |
michael@0 | 4224 | // Update the completion results. |
michael@0 | 4225 | if (this.lastCompletion.value != inputValue || frameActor != this._lastFrameActorId) { |
michael@0 | 4226 | this._updateCompletionResult(aType, aCallback); |
michael@0 | 4227 | return false; |
michael@0 | 4228 | } |
michael@0 | 4229 | |
michael@0 | 4230 | let popup = this.autocompletePopup; |
michael@0 | 4231 | let accepted = false; |
michael@0 | 4232 | |
michael@0 | 4233 | if (aType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { |
michael@0 | 4234 | this.acceptProposedCompletion(); |
michael@0 | 4235 | accepted = true; |
michael@0 | 4236 | } |
michael@0 | 4237 | else if (aType == this.COMPLETE_BACKWARD) { |
michael@0 | 4238 | popup.selectPreviousItem(); |
michael@0 | 4239 | } |
michael@0 | 4240 | else if (aType == this.COMPLETE_FORWARD) { |
michael@0 | 4241 | popup.selectNextItem(); |
michael@0 | 4242 | } |
michael@0 | 4243 | else if (aType == this.COMPLETE_PAGEUP) { |
michael@0 | 4244 | popup.selectPreviousPageItem(); |
michael@0 | 4245 | } |
michael@0 | 4246 | else if (aType == this.COMPLETE_PAGEDOWN) { |
michael@0 | 4247 | popup.selectNextPageItem(); |
michael@0 | 4248 | } |
michael@0 | 4249 | |
michael@0 | 4250 | aCallback && aCallback(this); |
michael@0 | 4251 | this.emit("autocomplete-updated"); |
michael@0 | 4252 | return accepted || popup.itemCount > 0; |
michael@0 | 4253 | }, |
michael@0 | 4254 | |
michael@0 | 4255 | /** |
michael@0 | 4256 | * Update the completion result. This operation is performed asynchronously by |
michael@0 | 4257 | * fetching updated results from the content process. |
michael@0 | 4258 | * |
michael@0 | 4259 | * @private |
michael@0 | 4260 | * @param int aType |
michael@0 | 4261 | * Completion type. See this.complete() for details. |
michael@0 | 4262 | * @param function [aCallback] |
michael@0 | 4263 | * Optional, function to invoke when completion results are received. |
michael@0 | 4264 | */ |
michael@0 | 4265 | _updateCompletionResult: |
michael@0 | 4266 | function JST__updateCompletionResult(aType, aCallback) |
michael@0 | 4267 | { |
michael@0 | 4268 | let frameActor = this.getFrameActor(this.SELECTED_FRAME); |
michael@0 | 4269 | if (this.lastCompletion.value == this.inputNode.value && frameActor == this._lastFrameActorId) { |
michael@0 | 4270 | return; |
michael@0 | 4271 | } |
michael@0 | 4272 | |
michael@0 | 4273 | let requestId = gSequenceId(); |
michael@0 | 4274 | let cursor = this.inputNode.selectionStart; |
michael@0 | 4275 | let input = this.inputNode.value.substring(0, cursor); |
michael@0 | 4276 | let cache = this._autocompleteCache; |
michael@0 | 4277 | |
michael@0 | 4278 | // If the current input starts with the previous input, then we already |
michael@0 | 4279 | // have a list of suggestions and we just need to filter the cached |
michael@0 | 4280 | // suggestions. When the current input ends with a non-alphanumeric |
michael@0 | 4281 | // character we ask the server again for suggestions. |
michael@0 | 4282 | |
michael@0 | 4283 | // Check if last character is non-alphanumeric |
michael@0 | 4284 | if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) { |
michael@0 | 4285 | this._autocompleteQuery = null; |
michael@0 | 4286 | this._autocompleteCache = null; |
michael@0 | 4287 | } |
michael@0 | 4288 | |
michael@0 | 4289 | if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) { |
michael@0 | 4290 | let filterBy = input; |
michael@0 | 4291 | // Find the last non-alphanumeric if exists. |
michael@0 | 4292 | let lastNonAlpha = input.match(/[^a-zA-Z0-9][a-zA-Z0-9]*$/); |
michael@0 | 4293 | // If input contains non-alphanumerics, use the part after the last one |
michael@0 | 4294 | // to filter the cache |
michael@0 | 4295 | if (lastNonAlpha) { |
michael@0 | 4296 | filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1); |
michael@0 | 4297 | } |
michael@0 | 4298 | |
michael@0 | 4299 | let newList = cache.sort().filter(function(l) { |
michael@0 | 4300 | return l.startsWith(filterBy); |
michael@0 | 4301 | }); |
michael@0 | 4302 | |
michael@0 | 4303 | this.lastCompletion = { |
michael@0 | 4304 | requestId: null, |
michael@0 | 4305 | completionType: aType, |
michael@0 | 4306 | value: null, |
michael@0 | 4307 | }; |
michael@0 | 4308 | |
michael@0 | 4309 | let response = { matches: newList, matchProp: filterBy }; |
michael@0 | 4310 | this._receiveAutocompleteProperties(null, aCallback, response); |
michael@0 | 4311 | return; |
michael@0 | 4312 | } |
michael@0 | 4313 | |
michael@0 | 4314 | this._lastFrameActorId = frameActor; |
michael@0 | 4315 | |
michael@0 | 4316 | this.lastCompletion = { |
michael@0 | 4317 | requestId: requestId, |
michael@0 | 4318 | completionType: aType, |
michael@0 | 4319 | value: null, |
michael@0 | 4320 | }; |
michael@0 | 4321 | |
michael@0 | 4322 | let callback = this._receiveAutocompleteProperties.bind(this, requestId, |
michael@0 | 4323 | aCallback); |
michael@0 | 4324 | |
michael@0 | 4325 | this.webConsoleClient.autocomplete(input, cursor, callback, frameActor); |
michael@0 | 4326 | }, |
michael@0 | 4327 | |
michael@0 | 4328 | /** |
michael@0 | 4329 | * Handler for the autocompletion results. This method takes |
michael@0 | 4330 | * the completion result received from the server and updates the UI |
michael@0 | 4331 | * accordingly. |
michael@0 | 4332 | * |
michael@0 | 4333 | * @param number aRequestId |
michael@0 | 4334 | * Request ID. |
michael@0 | 4335 | * @param function [aCallback=null] |
michael@0 | 4336 | * Optional, function to invoke when the completion result is received. |
michael@0 | 4337 | * @param object aMessage |
michael@0 | 4338 | * The JSON message which holds the completion results received from |
michael@0 | 4339 | * the content process. |
michael@0 | 4340 | */ |
michael@0 | 4341 | _receiveAutocompleteProperties: |
michael@0 | 4342 | function JST__receiveAutocompleteProperties(aRequestId, aCallback, aMessage) |
michael@0 | 4343 | { |
michael@0 | 4344 | let inputNode = this.inputNode; |
michael@0 | 4345 | let inputValue = inputNode.value; |
michael@0 | 4346 | if (this.lastCompletion.value == inputValue || |
michael@0 | 4347 | aRequestId != this.lastCompletion.requestId) { |
michael@0 | 4348 | return; |
michael@0 | 4349 | } |
michael@0 | 4350 | // Cache whatever came from the server if the last char is alphanumeric or '.' |
michael@0 | 4351 | let cursor = inputNode.selectionStart; |
michael@0 | 4352 | let inputUntilCursor = inputValue.substring(0, cursor); |
michael@0 | 4353 | |
michael@0 | 4354 | if (aRequestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) { |
michael@0 | 4355 | this._autocompleteCache = aMessage.matches; |
michael@0 | 4356 | this._autocompleteQuery = inputUntilCursor; |
michael@0 | 4357 | } |
michael@0 | 4358 | |
michael@0 | 4359 | let matches = aMessage.matches; |
michael@0 | 4360 | let lastPart = aMessage.matchProp; |
michael@0 | 4361 | if (!matches.length) { |
michael@0 | 4362 | this.clearCompletion(); |
michael@0 | 4363 | aCallback && aCallback(this); |
michael@0 | 4364 | this.emit("autocomplete-updated"); |
michael@0 | 4365 | return; |
michael@0 | 4366 | } |
michael@0 | 4367 | |
michael@0 | 4368 | let items = matches.reverse().map(function(aMatch) { |
michael@0 | 4369 | return { preLabel: lastPart, label: aMatch }; |
michael@0 | 4370 | }); |
michael@0 | 4371 | |
michael@0 | 4372 | let popup = this.autocompletePopup; |
michael@0 | 4373 | popup.setItems(items); |
michael@0 | 4374 | |
michael@0 | 4375 | let completionType = this.lastCompletion.completionType; |
michael@0 | 4376 | this.lastCompletion = { |
michael@0 | 4377 | value: inputValue, |
michael@0 | 4378 | matchProp: lastPart, |
michael@0 | 4379 | }; |
michael@0 | 4380 | |
michael@0 | 4381 | if (items.length > 1 && !popup.isOpen) { |
michael@0 | 4382 | let str = this.inputNode.value.substr(0, this.inputNode.selectionStart); |
michael@0 | 4383 | let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length; |
michael@0 | 4384 | let x = offset * this.hud._inputCharWidth; |
michael@0 | 4385 | popup.openPopup(inputNode, x + this.hud._chevronWidth); |
michael@0 | 4386 | this._autocompletePopupNavigated = false; |
michael@0 | 4387 | } |
michael@0 | 4388 | else if (items.length < 2 && popup.isOpen) { |
michael@0 | 4389 | popup.hidePopup(); |
michael@0 | 4390 | this._autocompletePopupNavigated = false; |
michael@0 | 4391 | } |
michael@0 | 4392 | |
michael@0 | 4393 | if (items.length == 1) { |
michael@0 | 4394 | popup.selectedIndex = 0; |
michael@0 | 4395 | } |
michael@0 | 4396 | |
michael@0 | 4397 | this.onAutocompleteSelect(); |
michael@0 | 4398 | |
michael@0 | 4399 | if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { |
michael@0 | 4400 | this.acceptProposedCompletion(); |
michael@0 | 4401 | } |
michael@0 | 4402 | else if (completionType == this.COMPLETE_BACKWARD) { |
michael@0 | 4403 | popup.selectPreviousItem(); |
michael@0 | 4404 | } |
michael@0 | 4405 | else if (completionType == this.COMPLETE_FORWARD) { |
michael@0 | 4406 | popup.selectNextItem(); |
michael@0 | 4407 | } |
michael@0 | 4408 | |
michael@0 | 4409 | aCallback && aCallback(this); |
michael@0 | 4410 | this.emit("autocomplete-updated"); |
michael@0 | 4411 | }, |
michael@0 | 4412 | |
michael@0 | 4413 | onAutocompleteSelect: function JSTF_onAutocompleteSelect() |
michael@0 | 4414 | { |
michael@0 | 4415 | // Render the suggestion only if the cursor is at the end of the input. |
michael@0 | 4416 | if (this.inputNode.selectionStart != this.inputNode.value.length) { |
michael@0 | 4417 | return; |
michael@0 | 4418 | } |
michael@0 | 4419 | |
michael@0 | 4420 | let currentItem = this.autocompletePopup.selectedItem; |
michael@0 | 4421 | if (currentItem && this.lastCompletion.value) { |
michael@0 | 4422 | let suffix = currentItem.label.substring(this.lastCompletion. |
michael@0 | 4423 | matchProp.length); |
michael@0 | 4424 | this.updateCompleteNode(suffix); |
michael@0 | 4425 | } |
michael@0 | 4426 | else { |
michael@0 | 4427 | this.updateCompleteNode(""); |
michael@0 | 4428 | } |
michael@0 | 4429 | }, |
michael@0 | 4430 | |
michael@0 | 4431 | /** |
michael@0 | 4432 | * Clear the current completion information and close the autocomplete popup, |
michael@0 | 4433 | * if needed. |
michael@0 | 4434 | */ |
michael@0 | 4435 | clearCompletion: function JSTF_clearCompletion() |
michael@0 | 4436 | { |
michael@0 | 4437 | this.autocompletePopup.clearItems(); |
michael@0 | 4438 | this.lastCompletion = { value: null }; |
michael@0 | 4439 | this.updateCompleteNode(""); |
michael@0 | 4440 | if (this.autocompletePopup.isOpen) { |
michael@0 | 4441 | this.autocompletePopup.hidePopup(); |
michael@0 | 4442 | this._autocompletePopupNavigated = false; |
michael@0 | 4443 | } |
michael@0 | 4444 | }, |
michael@0 | 4445 | |
michael@0 | 4446 | /** |
michael@0 | 4447 | * Accept the proposed input completion. |
michael@0 | 4448 | * |
michael@0 | 4449 | * @return boolean |
michael@0 | 4450 | * True if there was a selected completion item and the input value |
michael@0 | 4451 | * was updated, false otherwise. |
michael@0 | 4452 | */ |
michael@0 | 4453 | acceptProposedCompletion: function JSTF_acceptProposedCompletion() |
michael@0 | 4454 | { |
michael@0 | 4455 | let updated = false; |
michael@0 | 4456 | |
michael@0 | 4457 | let currentItem = this.autocompletePopup.selectedItem; |
michael@0 | 4458 | if (currentItem && this.lastCompletion.value) { |
michael@0 | 4459 | let suffix = currentItem.label.substring(this.lastCompletion. |
michael@0 | 4460 | matchProp.length); |
michael@0 | 4461 | let cursor = this.inputNode.selectionStart; |
michael@0 | 4462 | let value = this.inputNode.value; |
michael@0 | 4463 | this.setInputValue(value.substr(0, cursor) + suffix + value.substr(cursor)); |
michael@0 | 4464 | let newCursor = cursor + suffix.length; |
michael@0 | 4465 | this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor; |
michael@0 | 4466 | updated = true; |
michael@0 | 4467 | } |
michael@0 | 4468 | |
michael@0 | 4469 | this.clearCompletion(); |
michael@0 | 4470 | |
michael@0 | 4471 | return updated; |
michael@0 | 4472 | }, |
michael@0 | 4473 | |
michael@0 | 4474 | /** |
michael@0 | 4475 | * Update the node that displays the currently selected autocomplete proposal. |
michael@0 | 4476 | * |
michael@0 | 4477 | * @param string aSuffix |
michael@0 | 4478 | * The proposed suffix for the inputNode value. |
michael@0 | 4479 | */ |
michael@0 | 4480 | updateCompleteNode: function JSTF_updateCompleteNode(aSuffix) |
michael@0 | 4481 | { |
michael@0 | 4482 | // completion prefix = input, with non-control chars replaced by spaces |
michael@0 | 4483 | let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : ""; |
michael@0 | 4484 | this.completeNode.value = prefix + aSuffix; |
michael@0 | 4485 | }, |
michael@0 | 4486 | |
michael@0 | 4487 | |
michael@0 | 4488 | /** |
michael@0 | 4489 | * Destroy the sidebar. |
michael@0 | 4490 | * @private |
michael@0 | 4491 | */ |
michael@0 | 4492 | _sidebarDestroy: function JST__sidebarDestroy() |
michael@0 | 4493 | { |
michael@0 | 4494 | if (this._variablesView) { |
michael@0 | 4495 | this._variablesView.controller.releaseActors(); |
michael@0 | 4496 | this._variablesView = null; |
michael@0 | 4497 | } |
michael@0 | 4498 | |
michael@0 | 4499 | if (this.sidebar) { |
michael@0 | 4500 | this.sidebar.hide(); |
michael@0 | 4501 | this.sidebar.destroy(); |
michael@0 | 4502 | this.sidebar = null; |
michael@0 | 4503 | } |
michael@0 | 4504 | |
michael@0 | 4505 | this.emit("sidebar-closed"); |
michael@0 | 4506 | }, |
michael@0 | 4507 | |
michael@0 | 4508 | /** |
michael@0 | 4509 | * Destroy the JSTerm object. Call this method to avoid memory leaks. |
michael@0 | 4510 | */ |
michael@0 | 4511 | destroy: function JST_destroy() |
michael@0 | 4512 | { |
michael@0 | 4513 | this._sidebarDestroy(); |
michael@0 | 4514 | |
michael@0 | 4515 | this.clearCompletion(); |
michael@0 | 4516 | this.clearOutput(); |
michael@0 | 4517 | |
michael@0 | 4518 | this.autocompletePopup.destroy(); |
michael@0 | 4519 | this.autocompletePopup = null; |
michael@0 | 4520 | |
michael@0 | 4521 | let popup = this.hud.owner.chromeWindow.document |
michael@0 | 4522 | .getElementById("webConsole_autocompletePopup"); |
michael@0 | 4523 | if (popup) { |
michael@0 | 4524 | popup.parentNode.removeChild(popup); |
michael@0 | 4525 | } |
michael@0 | 4526 | |
michael@0 | 4527 | this.inputNode.removeEventListener("keypress", this._keyPress, false); |
michael@0 | 4528 | this.inputNode.removeEventListener("input", this._inputEventHandler, false); |
michael@0 | 4529 | this.inputNode.removeEventListener("keyup", this._inputEventHandler, false); |
michael@0 | 4530 | this.inputNode.removeEventListener("focus", this._focusEventHandler, false); |
michael@0 | 4531 | this.hud.window.removeEventListener("blur", this._blurEventHandler, false); |
michael@0 | 4532 | |
michael@0 | 4533 | this.hud = null; |
michael@0 | 4534 | }, |
michael@0 | 4535 | }; |
michael@0 | 4536 | |
michael@0 | 4537 | /** |
michael@0 | 4538 | * Utils: a collection of globally used functions. |
michael@0 | 4539 | */ |
michael@0 | 4540 | var Utils = { |
michael@0 | 4541 | /** |
michael@0 | 4542 | * Scrolls a node so that it's visible in its containing element. |
michael@0 | 4543 | * |
michael@0 | 4544 | * @param nsIDOMNode aNode |
michael@0 | 4545 | * The node to make visible. |
michael@0 | 4546 | * @returns void |
michael@0 | 4547 | */ |
michael@0 | 4548 | scrollToVisible: function Utils_scrollToVisible(aNode) |
michael@0 | 4549 | { |
michael@0 | 4550 | aNode.scrollIntoView(false); |
michael@0 | 4551 | }, |
michael@0 | 4552 | |
michael@0 | 4553 | /** |
michael@0 | 4554 | * Check if the given output node is scrolled to the bottom. |
michael@0 | 4555 | * |
michael@0 | 4556 | * @param nsIDOMNode aOutputNode |
michael@0 | 4557 | * @return boolean |
michael@0 | 4558 | * True if the output node is scrolled to the bottom, or false |
michael@0 | 4559 | * otherwise. |
michael@0 | 4560 | */ |
michael@0 | 4561 | isOutputScrolledToBottom: function Utils_isOutputScrolledToBottom(aOutputNode) |
michael@0 | 4562 | { |
michael@0 | 4563 | let lastNodeHeight = aOutputNode.lastChild ? |
michael@0 | 4564 | aOutputNode.lastChild.clientHeight : 0; |
michael@0 | 4565 | let scrollNode = aOutputNode.parentNode; |
michael@0 | 4566 | return scrollNode.scrollTop + scrollNode.clientHeight >= |
michael@0 | 4567 | scrollNode.scrollHeight - lastNodeHeight / 2; |
michael@0 | 4568 | }, |
michael@0 | 4569 | |
michael@0 | 4570 | /** |
michael@0 | 4571 | * Determine the category of a given nsIScriptError. |
michael@0 | 4572 | * |
michael@0 | 4573 | * @param nsIScriptError aScriptError |
michael@0 | 4574 | * The script error you want to determine the category for. |
michael@0 | 4575 | * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY |
michael@0 | 4576 | * Depending on the script error CATEGORY_JS, CATEGORY_CSS, or |
michael@0 | 4577 | * CATEGORY_SECURITY can be returned. |
michael@0 | 4578 | */ |
michael@0 | 4579 | categoryForScriptError: function Utils_categoryForScriptError(aScriptError) |
michael@0 | 4580 | { |
michael@0 | 4581 | let category = aScriptError.category; |
michael@0 | 4582 | |
michael@0 | 4583 | if (/^(?:CSS|Layout)\b/.test(category)) { |
michael@0 | 4584 | return CATEGORY_CSS; |
michael@0 | 4585 | } |
michael@0 | 4586 | |
michael@0 | 4587 | switch (category) { |
michael@0 | 4588 | case "Mixed Content Blocker": |
michael@0 | 4589 | case "Mixed Content Message": |
michael@0 | 4590 | case "CSP": |
michael@0 | 4591 | case "Invalid HSTS Headers": |
michael@0 | 4592 | case "Insecure Password Field": |
michael@0 | 4593 | case "SSL": |
michael@0 | 4594 | case "CORS": |
michael@0 | 4595 | return CATEGORY_SECURITY; |
michael@0 | 4596 | |
michael@0 | 4597 | default: |
michael@0 | 4598 | return CATEGORY_JS; |
michael@0 | 4599 | } |
michael@0 | 4600 | }, |
michael@0 | 4601 | |
michael@0 | 4602 | /** |
michael@0 | 4603 | * Retrieve the limit of messages for a specific category. |
michael@0 | 4604 | * |
michael@0 | 4605 | * @param number aCategory |
michael@0 | 4606 | * The category of messages you want to retrieve the limit for. See the |
michael@0 | 4607 | * CATEGORY_* constants. |
michael@0 | 4608 | * @return number |
michael@0 | 4609 | * The number of messages allowed for the specific category. |
michael@0 | 4610 | */ |
michael@0 | 4611 | logLimitForCategory: function Utils_logLimitForCategory(aCategory) |
michael@0 | 4612 | { |
michael@0 | 4613 | let logLimit = DEFAULT_LOG_LIMIT; |
michael@0 | 4614 | |
michael@0 | 4615 | try { |
michael@0 | 4616 | let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory]; |
michael@0 | 4617 | logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName); |
michael@0 | 4618 | logLimit = Math.max(logLimit, 1); |
michael@0 | 4619 | } |
michael@0 | 4620 | catch (e) { } |
michael@0 | 4621 | |
michael@0 | 4622 | return logLimit; |
michael@0 | 4623 | }, |
michael@0 | 4624 | }; |
michael@0 | 4625 | |
michael@0 | 4626 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 4627 | // CommandController |
michael@0 | 4628 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 4629 | |
michael@0 | 4630 | /** |
michael@0 | 4631 | * A controller (an instance of nsIController) that makes editing actions |
michael@0 | 4632 | * behave appropriately in the context of the Web Console. |
michael@0 | 4633 | */ |
michael@0 | 4634 | function CommandController(aWebConsole) |
michael@0 | 4635 | { |
michael@0 | 4636 | this.owner = aWebConsole; |
michael@0 | 4637 | } |
michael@0 | 4638 | |
michael@0 | 4639 | CommandController.prototype = { |
michael@0 | 4640 | /** |
michael@0 | 4641 | * Selects all the text in the HUD output. |
michael@0 | 4642 | */ |
michael@0 | 4643 | selectAll: function CommandController_selectAll() |
michael@0 | 4644 | { |
michael@0 | 4645 | this.owner.output.selectAllMessages(); |
michael@0 | 4646 | }, |
michael@0 | 4647 | |
michael@0 | 4648 | /** |
michael@0 | 4649 | * Open the URL of the selected message in a new tab. |
michael@0 | 4650 | */ |
michael@0 | 4651 | openURL: function CommandController_openURL() |
michael@0 | 4652 | { |
michael@0 | 4653 | this.owner.openSelectedItemInTab(); |
michael@0 | 4654 | }, |
michael@0 | 4655 | |
michael@0 | 4656 | copyURL: function CommandController_copyURL() |
michael@0 | 4657 | { |
michael@0 | 4658 | this.owner.copySelectedItems({ linkOnly: true, contextmenu: true }); |
michael@0 | 4659 | }, |
michael@0 | 4660 | |
michael@0 | 4661 | supportsCommand: function CommandController_supportsCommand(aCommand) |
michael@0 | 4662 | { |
michael@0 | 4663 | if (!this.owner || !this.owner.output) { |
michael@0 | 4664 | return false; |
michael@0 | 4665 | } |
michael@0 | 4666 | return this.isCommandEnabled(aCommand); |
michael@0 | 4667 | }, |
michael@0 | 4668 | |
michael@0 | 4669 | isCommandEnabled: function CommandController_isCommandEnabled(aCommand) |
michael@0 | 4670 | { |
michael@0 | 4671 | switch (aCommand) { |
michael@0 | 4672 | case "consoleCmd_openURL": |
michael@0 | 4673 | case "consoleCmd_copyURL": { |
michael@0 | 4674 | // Only enable URL-related actions if node is Net Activity. |
michael@0 | 4675 | let selectedItem = this.owner.output.getSelectedMessages(1)[0] || |
michael@0 | 4676 | this.owner._contextMenuHandler.lastClickedMessage; |
michael@0 | 4677 | return selectedItem && "url" in selectedItem; |
michael@0 | 4678 | } |
michael@0 | 4679 | case "consoleCmd_clearOutput": |
michael@0 | 4680 | case "cmd_selectAll": |
michael@0 | 4681 | case "cmd_find": |
michael@0 | 4682 | return true; |
michael@0 | 4683 | case "cmd_fontSizeEnlarge": |
michael@0 | 4684 | case "cmd_fontSizeReduce": |
michael@0 | 4685 | case "cmd_fontSizeReset": |
michael@0 | 4686 | case "cmd_close": |
michael@0 | 4687 | return this.owner.owner._browserConsole; |
michael@0 | 4688 | } |
michael@0 | 4689 | return false; |
michael@0 | 4690 | }, |
michael@0 | 4691 | |
michael@0 | 4692 | doCommand: function CommandController_doCommand(aCommand) |
michael@0 | 4693 | { |
michael@0 | 4694 | switch (aCommand) { |
michael@0 | 4695 | case "consoleCmd_openURL": |
michael@0 | 4696 | this.openURL(); |
michael@0 | 4697 | break; |
michael@0 | 4698 | case "consoleCmd_copyURL": |
michael@0 | 4699 | this.copyURL(); |
michael@0 | 4700 | break; |
michael@0 | 4701 | case "consoleCmd_clearOutput": |
michael@0 | 4702 | this.owner.jsterm.clearOutput(true); |
michael@0 | 4703 | break; |
michael@0 | 4704 | case "cmd_find": |
michael@0 | 4705 | this.owner.filterBox.focus(); |
michael@0 | 4706 | break; |
michael@0 | 4707 | case "cmd_selectAll": |
michael@0 | 4708 | this.selectAll(); |
michael@0 | 4709 | break; |
michael@0 | 4710 | case "cmd_fontSizeEnlarge": |
michael@0 | 4711 | this.owner.changeFontSize("+"); |
michael@0 | 4712 | break; |
michael@0 | 4713 | case "cmd_fontSizeReduce": |
michael@0 | 4714 | this.owner.changeFontSize("-"); |
michael@0 | 4715 | break; |
michael@0 | 4716 | case "cmd_fontSizeReset": |
michael@0 | 4717 | this.owner.changeFontSize(""); |
michael@0 | 4718 | break; |
michael@0 | 4719 | case "cmd_close": |
michael@0 | 4720 | this.owner.window.close(); |
michael@0 | 4721 | break; |
michael@0 | 4722 | } |
michael@0 | 4723 | } |
michael@0 | 4724 | }; |
michael@0 | 4725 | |
michael@0 | 4726 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 4727 | // Web Console connection proxy |
michael@0 | 4728 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 4729 | |
michael@0 | 4730 | /** |
michael@0 | 4731 | * The WebConsoleConnectionProxy handles the connection between the Web Console |
michael@0 | 4732 | * and the application we connect to through the remote debug protocol. |
michael@0 | 4733 | * |
michael@0 | 4734 | * @constructor |
michael@0 | 4735 | * @param object aWebConsole |
michael@0 | 4736 | * The Web Console instance that owns this connection proxy. |
michael@0 | 4737 | * @param RemoteTarget aTarget |
michael@0 | 4738 | * The target that the console will connect to. |
michael@0 | 4739 | */ |
michael@0 | 4740 | function WebConsoleConnectionProxy(aWebConsole, aTarget) |
michael@0 | 4741 | { |
michael@0 | 4742 | this.owner = aWebConsole; |
michael@0 | 4743 | this.target = aTarget; |
michael@0 | 4744 | |
michael@0 | 4745 | this._onPageError = this._onPageError.bind(this); |
michael@0 | 4746 | this._onLogMessage = this._onLogMessage.bind(this); |
michael@0 | 4747 | this._onConsoleAPICall = this._onConsoleAPICall.bind(this); |
michael@0 | 4748 | this._onNetworkEvent = this._onNetworkEvent.bind(this); |
michael@0 | 4749 | this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this); |
michael@0 | 4750 | this._onFileActivity = this._onFileActivity.bind(this); |
michael@0 | 4751 | this._onReflowActivity = this._onReflowActivity.bind(this); |
michael@0 | 4752 | this._onTabNavigated = this._onTabNavigated.bind(this); |
michael@0 | 4753 | this._onAttachConsole = this._onAttachConsole.bind(this); |
michael@0 | 4754 | this._onCachedMessages = this._onCachedMessages.bind(this); |
michael@0 | 4755 | this._connectionTimeout = this._connectionTimeout.bind(this); |
michael@0 | 4756 | this._onLastPrivateContextExited = this._onLastPrivateContextExited.bind(this); |
michael@0 | 4757 | } |
michael@0 | 4758 | |
michael@0 | 4759 | WebConsoleConnectionProxy.prototype = { |
michael@0 | 4760 | /** |
michael@0 | 4761 | * The owning Web Console instance. |
michael@0 | 4762 | * |
michael@0 | 4763 | * @see WebConsoleFrame |
michael@0 | 4764 | * @type object |
michael@0 | 4765 | */ |
michael@0 | 4766 | owner: null, |
michael@0 | 4767 | |
michael@0 | 4768 | /** |
michael@0 | 4769 | * The target that the console connects to. |
michael@0 | 4770 | * @type RemoteTarget |
michael@0 | 4771 | */ |
michael@0 | 4772 | target: null, |
michael@0 | 4773 | |
michael@0 | 4774 | /** |
michael@0 | 4775 | * The DebuggerClient object. |
michael@0 | 4776 | * |
michael@0 | 4777 | * @see DebuggerClient |
michael@0 | 4778 | * @type object |
michael@0 | 4779 | */ |
michael@0 | 4780 | client: null, |
michael@0 | 4781 | |
michael@0 | 4782 | /** |
michael@0 | 4783 | * The WebConsoleClient object. |
michael@0 | 4784 | * |
michael@0 | 4785 | * @see WebConsoleClient |
michael@0 | 4786 | * @type object |
michael@0 | 4787 | */ |
michael@0 | 4788 | webConsoleClient: null, |
michael@0 | 4789 | |
michael@0 | 4790 | /** |
michael@0 | 4791 | * Tells if the connection is established. |
michael@0 | 4792 | * @type boolean |
michael@0 | 4793 | */ |
michael@0 | 4794 | connected: false, |
michael@0 | 4795 | |
michael@0 | 4796 | /** |
michael@0 | 4797 | * Timer used for the connection. |
michael@0 | 4798 | * @private |
michael@0 | 4799 | * @type object |
michael@0 | 4800 | */ |
michael@0 | 4801 | _connectTimer: null, |
michael@0 | 4802 | |
michael@0 | 4803 | _connectDefer: null, |
michael@0 | 4804 | _disconnecter: null, |
michael@0 | 4805 | |
michael@0 | 4806 | /** |
michael@0 | 4807 | * The WebConsoleActor ID. |
michael@0 | 4808 | * |
michael@0 | 4809 | * @private |
michael@0 | 4810 | * @type string |
michael@0 | 4811 | */ |
michael@0 | 4812 | _consoleActor: null, |
michael@0 | 4813 | |
michael@0 | 4814 | /** |
michael@0 | 4815 | * Tells if the window.console object of the remote web page is the native |
michael@0 | 4816 | * object or not. |
michael@0 | 4817 | * @private |
michael@0 | 4818 | * @type boolean |
michael@0 | 4819 | */ |
michael@0 | 4820 | _hasNativeConsoleAPI: false, |
michael@0 | 4821 | |
michael@0 | 4822 | /** |
michael@0 | 4823 | * Initialize a debugger client and connect it to the debugger server. |
michael@0 | 4824 | * |
michael@0 | 4825 | * @return object |
michael@0 | 4826 | * A promise object that is resolved/rejected based on the success of |
michael@0 | 4827 | * the connection initialization. |
michael@0 | 4828 | */ |
michael@0 | 4829 | connect: function WCCP_connect() |
michael@0 | 4830 | { |
michael@0 | 4831 | if (this._connectDefer) { |
michael@0 | 4832 | return this._connectDefer.promise; |
michael@0 | 4833 | } |
michael@0 | 4834 | |
michael@0 | 4835 | this._connectDefer = promise.defer(); |
michael@0 | 4836 | |
michael@0 | 4837 | let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT); |
michael@0 | 4838 | this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
michael@0 | 4839 | this._connectTimer.initWithCallback(this._connectionTimeout, |
michael@0 | 4840 | timeout, Ci.nsITimer.TYPE_ONE_SHOT); |
michael@0 | 4841 | |
michael@0 | 4842 | let connPromise = this._connectDefer.promise; |
michael@0 | 4843 | connPromise.then(function _onSucess() { |
michael@0 | 4844 | this._connectTimer.cancel(); |
michael@0 | 4845 | this._connectTimer = null; |
michael@0 | 4846 | }.bind(this), function _onFailure() { |
michael@0 | 4847 | this._connectTimer = null; |
michael@0 | 4848 | }.bind(this)); |
michael@0 | 4849 | |
michael@0 | 4850 | let client = this.client = this.target.client; |
michael@0 | 4851 | |
michael@0 | 4852 | client.addListener("logMessage", this._onLogMessage); |
michael@0 | 4853 | client.addListener("pageError", this._onPageError); |
michael@0 | 4854 | client.addListener("consoleAPICall", this._onConsoleAPICall); |
michael@0 | 4855 | client.addListener("networkEvent", this._onNetworkEvent); |
michael@0 | 4856 | client.addListener("networkEventUpdate", this._onNetworkEventUpdate); |
michael@0 | 4857 | client.addListener("fileActivity", this._onFileActivity); |
michael@0 | 4858 | client.addListener("reflowActivity", this._onReflowActivity); |
michael@0 | 4859 | client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited); |
michael@0 | 4860 | this.target.on("will-navigate", this._onTabNavigated); |
michael@0 | 4861 | this.target.on("navigate", this._onTabNavigated); |
michael@0 | 4862 | |
michael@0 | 4863 | this._consoleActor = this.target.form.consoleActor; |
michael@0 | 4864 | if (!this.target.chrome) { |
michael@0 | 4865 | let tab = this.target.form; |
michael@0 | 4866 | this.owner.onLocationChange(tab.url, tab.title); |
michael@0 | 4867 | } |
michael@0 | 4868 | this._attachConsole(); |
michael@0 | 4869 | |
michael@0 | 4870 | return connPromise; |
michael@0 | 4871 | }, |
michael@0 | 4872 | |
michael@0 | 4873 | /** |
michael@0 | 4874 | * Connection timeout handler. |
michael@0 | 4875 | * @private |
michael@0 | 4876 | */ |
michael@0 | 4877 | _connectionTimeout: function WCCP__connectionTimeout() |
michael@0 | 4878 | { |
michael@0 | 4879 | let error = { |
michael@0 | 4880 | error: "timeout", |
michael@0 | 4881 | message: l10n.getStr("connectionTimeout"), |
michael@0 | 4882 | }; |
michael@0 | 4883 | |
michael@0 | 4884 | this._connectDefer.reject(error); |
michael@0 | 4885 | }, |
michael@0 | 4886 | |
michael@0 | 4887 | /** |
michael@0 | 4888 | * Attach to the Web Console actor. |
michael@0 | 4889 | * @private |
michael@0 | 4890 | */ |
michael@0 | 4891 | _attachConsole: function WCCP__attachConsole() |
michael@0 | 4892 | { |
michael@0 | 4893 | let listeners = ["PageError", "ConsoleAPI", "NetworkActivity", |
michael@0 | 4894 | "FileActivity"]; |
michael@0 | 4895 | this.client.attachConsole(this._consoleActor, listeners, |
michael@0 | 4896 | this._onAttachConsole); |
michael@0 | 4897 | }, |
michael@0 | 4898 | |
michael@0 | 4899 | /** |
michael@0 | 4900 | * The "attachConsole" response handler. |
michael@0 | 4901 | * |
michael@0 | 4902 | * @private |
michael@0 | 4903 | * @param object aResponse |
michael@0 | 4904 | * The JSON response object received from the server. |
michael@0 | 4905 | * @param object aWebConsoleClient |
michael@0 | 4906 | * The WebConsoleClient instance for the attached console, for the |
michael@0 | 4907 | * specific tab we work with. |
michael@0 | 4908 | */ |
michael@0 | 4909 | _onAttachConsole: function WCCP__onAttachConsole(aResponse, aWebConsoleClient) |
michael@0 | 4910 | { |
michael@0 | 4911 | if (aResponse.error) { |
michael@0 | 4912 | Cu.reportError("attachConsole failed: " + aResponse.error + " " + |
michael@0 | 4913 | aResponse.message); |
michael@0 | 4914 | this._connectDefer.reject(aResponse); |
michael@0 | 4915 | return; |
michael@0 | 4916 | } |
michael@0 | 4917 | |
michael@0 | 4918 | this.webConsoleClient = aWebConsoleClient; |
michael@0 | 4919 | |
michael@0 | 4920 | this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI; |
michael@0 | 4921 | |
michael@0 | 4922 | let msgs = ["PageError", "ConsoleAPI"]; |
michael@0 | 4923 | this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages); |
michael@0 | 4924 | |
michael@0 | 4925 | this.owner._updateReflowActivityListener(); |
michael@0 | 4926 | }, |
michael@0 | 4927 | |
michael@0 | 4928 | /** |
michael@0 | 4929 | * The "cachedMessages" response handler. |
michael@0 | 4930 | * |
michael@0 | 4931 | * @private |
michael@0 | 4932 | * @param object aResponse |
michael@0 | 4933 | * The JSON response object received from the server. |
michael@0 | 4934 | */ |
michael@0 | 4935 | _onCachedMessages: function WCCP__onCachedMessages(aResponse) |
michael@0 | 4936 | { |
michael@0 | 4937 | if (aResponse.error) { |
michael@0 | 4938 | Cu.reportError("Web Console getCachedMessages error: " + aResponse.error + |
michael@0 | 4939 | " " + aResponse.message); |
michael@0 | 4940 | this._connectDefer.reject(aResponse); |
michael@0 | 4941 | return; |
michael@0 | 4942 | } |
michael@0 | 4943 | |
michael@0 | 4944 | if (!this._connectTimer) { |
michael@0 | 4945 | // This happens if the promise is rejected (eg. a timeout), but the |
michael@0 | 4946 | // connection attempt is successful, nonetheless. |
michael@0 | 4947 | Cu.reportError("Web Console getCachedMessages error: invalid state."); |
michael@0 | 4948 | } |
michael@0 | 4949 | |
michael@0 | 4950 | this.owner.displayCachedMessages(aResponse.messages); |
michael@0 | 4951 | |
michael@0 | 4952 | if (!this._hasNativeConsoleAPI) { |
michael@0 | 4953 | this.owner.logWarningAboutReplacedAPI(); |
michael@0 | 4954 | } |
michael@0 | 4955 | |
michael@0 | 4956 | this.connected = true; |
michael@0 | 4957 | this._connectDefer.resolve(this); |
michael@0 | 4958 | }, |
michael@0 | 4959 | |
michael@0 | 4960 | /** |
michael@0 | 4961 | * The "pageError" message type handler. We redirect any page errors to the UI |
michael@0 | 4962 | * for displaying. |
michael@0 | 4963 | * |
michael@0 | 4964 | * @private |
michael@0 | 4965 | * @param string aType |
michael@0 | 4966 | * Message type. |
michael@0 | 4967 | * @param object aPacket |
michael@0 | 4968 | * The message received from the server. |
michael@0 | 4969 | */ |
michael@0 | 4970 | _onPageError: function WCCP__onPageError(aType, aPacket) |
michael@0 | 4971 | { |
michael@0 | 4972 | if (this.owner && aPacket.from == this._consoleActor) { |
michael@0 | 4973 | this.owner.handlePageError(aPacket.pageError); |
michael@0 | 4974 | } |
michael@0 | 4975 | }, |
michael@0 | 4976 | |
michael@0 | 4977 | /** |
michael@0 | 4978 | * The "logMessage" message type handler. We redirect any message to the UI |
michael@0 | 4979 | * for displaying. |
michael@0 | 4980 | * |
michael@0 | 4981 | * @private |
michael@0 | 4982 | * @param string aType |
michael@0 | 4983 | * Message type. |
michael@0 | 4984 | * @param object aPacket |
michael@0 | 4985 | * The message received from the server. |
michael@0 | 4986 | */ |
michael@0 | 4987 | _onLogMessage: function WCCP__onLogMessage(aType, aPacket) |
michael@0 | 4988 | { |
michael@0 | 4989 | if (this.owner && aPacket.from == this._consoleActor) { |
michael@0 | 4990 | this.owner.handleLogMessage(aPacket); |
michael@0 | 4991 | } |
michael@0 | 4992 | }, |
michael@0 | 4993 | |
michael@0 | 4994 | /** |
michael@0 | 4995 | * The "consoleAPICall" message type handler. We redirect any message to |
michael@0 | 4996 | * the UI for displaying. |
michael@0 | 4997 | * |
michael@0 | 4998 | * @private |
michael@0 | 4999 | * @param string aType |
michael@0 | 5000 | * Message type. |
michael@0 | 5001 | * @param object aPacket |
michael@0 | 5002 | * The message received from the server. |
michael@0 | 5003 | */ |
michael@0 | 5004 | _onConsoleAPICall: function WCCP__onConsoleAPICall(aType, aPacket) |
michael@0 | 5005 | { |
michael@0 | 5006 | if (this.owner && aPacket.from == this._consoleActor) { |
michael@0 | 5007 | this.owner.handleConsoleAPICall(aPacket.message); |
michael@0 | 5008 | } |
michael@0 | 5009 | }, |
michael@0 | 5010 | |
michael@0 | 5011 | /** |
michael@0 | 5012 | * The "networkEvent" message type handler. We redirect any message to |
michael@0 | 5013 | * the UI for displaying. |
michael@0 | 5014 | * |
michael@0 | 5015 | * @private |
michael@0 | 5016 | * @param string aType |
michael@0 | 5017 | * Message type. |
michael@0 | 5018 | * @param object aPacket |
michael@0 | 5019 | * The message received from the server. |
michael@0 | 5020 | */ |
michael@0 | 5021 | _onNetworkEvent: function WCCP__onNetworkEvent(aType, aPacket) |
michael@0 | 5022 | { |
michael@0 | 5023 | if (this.owner && aPacket.from == this._consoleActor) { |
michael@0 | 5024 | this.owner.handleNetworkEvent(aPacket.eventActor); |
michael@0 | 5025 | } |
michael@0 | 5026 | }, |
michael@0 | 5027 | |
michael@0 | 5028 | /** |
michael@0 | 5029 | * The "networkEventUpdate" message type handler. We redirect any message to |
michael@0 | 5030 | * the UI for displaying. |
michael@0 | 5031 | * |
michael@0 | 5032 | * @private |
michael@0 | 5033 | * @param string aType |
michael@0 | 5034 | * Message type. |
michael@0 | 5035 | * @param object aPacket |
michael@0 | 5036 | * The message received from the server. |
michael@0 | 5037 | */ |
michael@0 | 5038 | _onNetworkEventUpdate: function WCCP__onNetworkEvenUpdatet(aType, aPacket) |
michael@0 | 5039 | { |
michael@0 | 5040 | if (this.owner) { |
michael@0 | 5041 | this.owner.handleNetworkEventUpdate(aPacket.from, aPacket.updateType, |
michael@0 | 5042 | aPacket); |
michael@0 | 5043 | } |
michael@0 | 5044 | }, |
michael@0 | 5045 | |
michael@0 | 5046 | /** |
michael@0 | 5047 | * The "fileActivity" message type handler. We redirect any message to |
michael@0 | 5048 | * the UI for displaying. |
michael@0 | 5049 | * |
michael@0 | 5050 | * @private |
michael@0 | 5051 | * @param string aType |
michael@0 | 5052 | * Message type. |
michael@0 | 5053 | * @param object aPacket |
michael@0 | 5054 | * The message received from the server. |
michael@0 | 5055 | */ |
michael@0 | 5056 | _onFileActivity: function WCCP__onFileActivity(aType, aPacket) |
michael@0 | 5057 | { |
michael@0 | 5058 | if (this.owner && aPacket.from == this._consoleActor) { |
michael@0 | 5059 | this.owner.handleFileActivity(aPacket.uri); |
michael@0 | 5060 | } |
michael@0 | 5061 | }, |
michael@0 | 5062 | |
michael@0 | 5063 | _onReflowActivity: function WCCP__onReflowActivity(aType, aPacket) |
michael@0 | 5064 | { |
michael@0 | 5065 | if (this.owner && aPacket.from == this._consoleActor) { |
michael@0 | 5066 | this.owner.handleReflowActivity(aPacket); |
michael@0 | 5067 | } |
michael@0 | 5068 | }, |
michael@0 | 5069 | |
michael@0 | 5070 | /** |
michael@0 | 5071 | * The "lastPrivateContextExited" message type handler. When this message is |
michael@0 | 5072 | * received the Web Console UI is cleared. |
michael@0 | 5073 | * |
michael@0 | 5074 | * @private |
michael@0 | 5075 | * @param string aType |
michael@0 | 5076 | * Message type. |
michael@0 | 5077 | * @param object aPacket |
michael@0 | 5078 | * The message received from the server. |
michael@0 | 5079 | */ |
michael@0 | 5080 | _onLastPrivateContextExited: |
michael@0 | 5081 | function WCCP__onLastPrivateContextExited(aType, aPacket) |
michael@0 | 5082 | { |
michael@0 | 5083 | if (this.owner && aPacket.from == this._consoleActor) { |
michael@0 | 5084 | this.owner.jsterm.clearPrivateMessages(); |
michael@0 | 5085 | } |
michael@0 | 5086 | }, |
michael@0 | 5087 | |
michael@0 | 5088 | /** |
michael@0 | 5089 | * The "will-navigate" and "navigate" event handlers. We redirect any message |
michael@0 | 5090 | * to the UI for displaying. |
michael@0 | 5091 | * |
michael@0 | 5092 | * @private |
michael@0 | 5093 | * @param string aEvent |
michael@0 | 5094 | * Event type. |
michael@0 | 5095 | * @param object aPacket |
michael@0 | 5096 | * The message received from the server. |
michael@0 | 5097 | */ |
michael@0 | 5098 | _onTabNavigated: function WCCP__onTabNavigated(aEvent, aPacket) |
michael@0 | 5099 | { |
michael@0 | 5100 | if (!this.owner) { |
michael@0 | 5101 | return; |
michael@0 | 5102 | } |
michael@0 | 5103 | |
michael@0 | 5104 | this.owner.handleTabNavigated(aEvent, aPacket); |
michael@0 | 5105 | }, |
michael@0 | 5106 | |
michael@0 | 5107 | /** |
michael@0 | 5108 | * Release an object actor. |
michael@0 | 5109 | * |
michael@0 | 5110 | * @param string aActor |
michael@0 | 5111 | * The actor ID to send the request to. |
michael@0 | 5112 | */ |
michael@0 | 5113 | releaseActor: function WCCP_releaseActor(aActor) |
michael@0 | 5114 | { |
michael@0 | 5115 | if (this.client) { |
michael@0 | 5116 | this.client.release(aActor); |
michael@0 | 5117 | } |
michael@0 | 5118 | }, |
michael@0 | 5119 | |
michael@0 | 5120 | /** |
michael@0 | 5121 | * Disconnect the Web Console from the remote server. |
michael@0 | 5122 | * |
michael@0 | 5123 | * @return object |
michael@0 | 5124 | * A promise object that is resolved when disconnect completes. |
michael@0 | 5125 | */ |
michael@0 | 5126 | disconnect: function WCCP_disconnect() |
michael@0 | 5127 | { |
michael@0 | 5128 | if (this._disconnecter) { |
michael@0 | 5129 | return this._disconnecter.promise; |
michael@0 | 5130 | } |
michael@0 | 5131 | |
michael@0 | 5132 | this._disconnecter = promise.defer(); |
michael@0 | 5133 | |
michael@0 | 5134 | if (!this.client) { |
michael@0 | 5135 | this._disconnecter.resolve(null); |
michael@0 | 5136 | return this._disconnecter.promise; |
michael@0 | 5137 | } |
michael@0 | 5138 | |
michael@0 | 5139 | this.client.removeListener("logMessage", this._onLogMessage); |
michael@0 | 5140 | this.client.removeListener("pageError", this._onPageError); |
michael@0 | 5141 | this.client.removeListener("consoleAPICall", this._onConsoleAPICall); |
michael@0 | 5142 | this.client.removeListener("networkEvent", this._onNetworkEvent); |
michael@0 | 5143 | this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate); |
michael@0 | 5144 | this.client.removeListener("fileActivity", this._onFileActivity); |
michael@0 | 5145 | this.client.removeListener("reflowActivity", this._onReflowActivity); |
michael@0 | 5146 | this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited); |
michael@0 | 5147 | this.target.off("will-navigate", this._onTabNavigated); |
michael@0 | 5148 | this.target.off("navigate", this._onTabNavigated); |
michael@0 | 5149 | |
michael@0 | 5150 | this.client = null; |
michael@0 | 5151 | this.webConsoleClient = null; |
michael@0 | 5152 | this.target = null; |
michael@0 | 5153 | this.connected = false; |
michael@0 | 5154 | this.owner = null; |
michael@0 | 5155 | this._disconnecter.resolve(null); |
michael@0 | 5156 | |
michael@0 | 5157 | return this._disconnecter.promise; |
michael@0 | 5158 | }, |
michael@0 | 5159 | }; |
michael@0 | 5160 | |
michael@0 | 5161 | function gSequenceId() |
michael@0 | 5162 | { |
michael@0 | 5163 | return gSequenceId.n++; |
michael@0 | 5164 | } |
michael@0 | 5165 | gSequenceId.n = 0; |
michael@0 | 5166 | |
michael@0 | 5167 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 5168 | // Context Menu |
michael@0 | 5169 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 5170 | |
michael@0 | 5171 | /* |
michael@0 | 5172 | * ConsoleContextMenu this used to handle the visibility of context menu items. |
michael@0 | 5173 | * |
michael@0 | 5174 | * @constructor |
michael@0 | 5175 | * @param object aOwner |
michael@0 | 5176 | * The WebConsoleFrame instance that owns this object. |
michael@0 | 5177 | */ |
michael@0 | 5178 | function ConsoleContextMenu(aOwner) |
michael@0 | 5179 | { |
michael@0 | 5180 | this.owner = aOwner; |
michael@0 | 5181 | this.popup = this.owner.document.getElementById("output-contextmenu"); |
michael@0 | 5182 | this.build = this.build.bind(this); |
michael@0 | 5183 | this.popup.addEventListener("popupshowing", this.build); |
michael@0 | 5184 | } |
michael@0 | 5185 | |
michael@0 | 5186 | ConsoleContextMenu.prototype = { |
michael@0 | 5187 | lastClickedMessage: null, |
michael@0 | 5188 | |
michael@0 | 5189 | /* |
michael@0 | 5190 | * Handle to show/hide context menu item. |
michael@0 | 5191 | */ |
michael@0 | 5192 | build: function CCM_build(aEvent) |
michael@0 | 5193 | { |
michael@0 | 5194 | let metadata = this.getSelectionMetadata(aEvent.rangeParent); |
michael@0 | 5195 | for (let element of this.popup.children) { |
michael@0 | 5196 | element.hidden = this.shouldHideMenuItem(element, metadata); |
michael@0 | 5197 | } |
michael@0 | 5198 | }, |
michael@0 | 5199 | |
michael@0 | 5200 | /* |
michael@0 | 5201 | * Get selection information from the view. |
michael@0 | 5202 | * |
michael@0 | 5203 | * @param nsIDOMElement aClickElement |
michael@0 | 5204 | * The DOM element the user clicked on. |
michael@0 | 5205 | * @return object |
michael@0 | 5206 | * Selection metadata. |
michael@0 | 5207 | */ |
michael@0 | 5208 | getSelectionMetadata: function CCM_getSelectionMetadata(aClickElement) |
michael@0 | 5209 | { |
michael@0 | 5210 | let metadata = { |
michael@0 | 5211 | selectionType: "", |
michael@0 | 5212 | selection: new Set(), |
michael@0 | 5213 | }; |
michael@0 | 5214 | let selectedItems = this.owner.output.getSelectedMessages(); |
michael@0 | 5215 | if (!selectedItems.length) { |
michael@0 | 5216 | let clickedItem = this.owner.output.getMessageForElement(aClickElement); |
michael@0 | 5217 | if (clickedItem) { |
michael@0 | 5218 | this.lastClickedMessage = clickedItem; |
michael@0 | 5219 | selectedItems = [clickedItem]; |
michael@0 | 5220 | } |
michael@0 | 5221 | } |
michael@0 | 5222 | |
michael@0 | 5223 | metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single"; |
michael@0 | 5224 | |
michael@0 | 5225 | let selection = metadata.selection; |
michael@0 | 5226 | for (let item of selectedItems) { |
michael@0 | 5227 | switch (item.category) { |
michael@0 | 5228 | case CATEGORY_NETWORK: |
michael@0 | 5229 | selection.add("network"); |
michael@0 | 5230 | break; |
michael@0 | 5231 | case CATEGORY_CSS: |
michael@0 | 5232 | selection.add("css"); |
michael@0 | 5233 | break; |
michael@0 | 5234 | case CATEGORY_JS: |
michael@0 | 5235 | selection.add("js"); |
michael@0 | 5236 | break; |
michael@0 | 5237 | case CATEGORY_WEBDEV: |
michael@0 | 5238 | selection.add("webdev"); |
michael@0 | 5239 | break; |
michael@0 | 5240 | } |
michael@0 | 5241 | } |
michael@0 | 5242 | |
michael@0 | 5243 | return metadata; |
michael@0 | 5244 | }, |
michael@0 | 5245 | |
michael@0 | 5246 | /* |
michael@0 | 5247 | * Determine if an item should be hidden. |
michael@0 | 5248 | * |
michael@0 | 5249 | * @param nsIDOMElement aMenuItem |
michael@0 | 5250 | * @param object aMetadata |
michael@0 | 5251 | * @return boolean |
michael@0 | 5252 | * Whether the given item should be hidden or not. |
michael@0 | 5253 | */ |
michael@0 | 5254 | shouldHideMenuItem: function CCM_shouldHideMenuItem(aMenuItem, aMetadata) |
michael@0 | 5255 | { |
michael@0 | 5256 | let selectionType = aMenuItem.getAttribute("selectiontype"); |
michael@0 | 5257 | if (selectionType && !aMetadata.selectionType == selectionType) { |
michael@0 | 5258 | return true; |
michael@0 | 5259 | } |
michael@0 | 5260 | |
michael@0 | 5261 | let selection = aMenuItem.getAttribute("selection"); |
michael@0 | 5262 | if (!selection) { |
michael@0 | 5263 | return false; |
michael@0 | 5264 | } |
michael@0 | 5265 | |
michael@0 | 5266 | let shouldHide = true; |
michael@0 | 5267 | let itemData = selection.split("|"); |
michael@0 | 5268 | for (let type of aMetadata.selection) { |
michael@0 | 5269 | // check whether this menu item should show or not. |
michael@0 | 5270 | if (itemData.indexOf(type) !== -1) { |
michael@0 | 5271 | shouldHide = false; |
michael@0 | 5272 | break; |
michael@0 | 5273 | } |
michael@0 | 5274 | } |
michael@0 | 5275 | |
michael@0 | 5276 | return shouldHide; |
michael@0 | 5277 | }, |
michael@0 | 5278 | |
michael@0 | 5279 | /** |
michael@0 | 5280 | * Destroy the ConsoleContextMenu object instance. |
michael@0 | 5281 | */ |
michael@0 | 5282 | destroy: function CCM_destroy() |
michael@0 | 5283 | { |
michael@0 | 5284 | this.popup.removeEventListener("popupshowing", this.build); |
michael@0 | 5285 | this.popup = null; |
michael@0 | 5286 | this.owner = null; |
michael@0 | 5287 | this.lastClickedMessage = null; |
michael@0 | 5288 | }, |
michael@0 | 5289 | }; |
michael@0 | 5290 |