browser/devtools/webconsole/webconsole.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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

mercurial