browser/devtools/webconsole/console-output.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 /* vim: set ts=2 et sw=2 tw=80: */
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 "use strict";
michael@0 7
michael@0 8 const {Cc, Ci, Cu} = require("chrome");
michael@0 9
michael@0 10 loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
michael@0 11 loader.lazyImporter(this, "escapeHTML", "resource:///modules/devtools/VariablesView.jsm");
michael@0 12 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
michael@0 13 loader.lazyImporter(this, "Task","resource://gre/modules/Task.jsm");
michael@0 14
michael@0 15 const Heritage = require("sdk/core/heritage");
michael@0 16 const XHTML_NS = "http://www.w3.org/1999/xhtml";
michael@0 17 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
michael@0 18 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
michael@0 19
michael@0 20 const WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
michael@0 21 const l10n = new WebConsoleUtils.l10n(STRINGS_URI);
michael@0 22
michael@0 23 // Constants for compatibility with the Web Console output implementation before
michael@0 24 // bug 778766.
michael@0 25 // TODO: remove these once bug 778766 is fixed.
michael@0 26 const COMPAT = {
michael@0 27 // The various categories of messages.
michael@0 28 CATEGORIES: {
michael@0 29 NETWORK: 0,
michael@0 30 CSS: 1,
michael@0 31 JS: 2,
michael@0 32 WEBDEV: 3,
michael@0 33 INPUT: 4,
michael@0 34 OUTPUT: 5,
michael@0 35 SECURITY: 6,
michael@0 36 },
michael@0 37
michael@0 38 // The possible message severities.
michael@0 39 SEVERITIES: {
michael@0 40 ERROR: 0,
michael@0 41 WARNING: 1,
michael@0 42 INFO: 2,
michael@0 43 LOG: 3,
michael@0 44 },
michael@0 45
michael@0 46 // The preference keys to use for each category/severity combination, indexed
michael@0 47 // first by category (rows) and then by severity (columns).
michael@0 48 //
michael@0 49 // Most of these rather idiosyncratic names are historical and predate the
michael@0 50 // division of message type into "category" and "severity".
michael@0 51 PREFERENCE_KEYS: [
michael@0 52 // Error Warning Info Log
michael@0 53 [ "network", "netwarn", null, "networkinfo", ], // Network
michael@0 54 [ "csserror", "cssparser", null, null, ], // CSS
michael@0 55 [ "exception", "jswarn", null, "jslog", ], // JS
michael@0 56 [ "error", "warn", "info", "log", ], // Web Developer
michael@0 57 [ null, null, null, null, ], // Input
michael@0 58 [ null, null, null, null, ], // Output
michael@0 59 [ "secerror", "secwarn", null, null, ], // Security
michael@0 60 ],
michael@0 61
michael@0 62 // The fragment of a CSS class name that identifies each category.
michael@0 63 CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
michael@0 64 "input", "output", "security" ],
michael@0 65
michael@0 66 // The fragment of a CSS class name that identifies each severity.
michael@0 67 SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ],
michael@0 68
michael@0 69 // The indent of a console group in pixels.
michael@0 70 GROUP_INDENT: 12,
michael@0 71 };
michael@0 72
michael@0 73 // A map from the console API call levels to the Web Console severities.
michael@0 74 const CONSOLE_API_LEVELS_TO_SEVERITIES = {
michael@0 75 error: "error",
michael@0 76 exception: "error",
michael@0 77 assert: "error",
michael@0 78 warn: "warning",
michael@0 79 info: "info",
michael@0 80 log: "log",
michael@0 81 trace: "log",
michael@0 82 debug: "log",
michael@0 83 dir: "log",
michael@0 84 group: "log",
michael@0 85 groupCollapsed: "log",
michael@0 86 groupEnd: "log",
michael@0 87 time: "log",
michael@0 88 timeEnd: "log",
michael@0 89 count: "log"
michael@0 90 };
michael@0 91
michael@0 92 // Array of known message source URLs we need to hide from output.
michael@0 93 const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"];
michael@0 94
michael@0 95 // The maximum length of strings to be displayed by the Web Console.
michael@0 96 const MAX_LONG_STRING_LENGTH = 200000;
michael@0 97
michael@0 98 // Regular expression that matches the allowed CSS property names when using
michael@0 99 // the `window.console` API.
michael@0 100 const RE_ALLOWED_STYLES = /^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|margin|padding|text|transition|outline|white-space|word|writing|(?:min-|max-)?width|(?:min-|max-)?height)/;
michael@0 101
michael@0 102 // Regular expressions to search and replace with 'notallowed' in the styles
michael@0 103 // given to the `window.console` API methods.
michael@0 104 const RE_CLEANUP_STYLES = [
michael@0 105 // url(), -moz-element()
michael@0 106 /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
michael@0 107
michael@0 108 // various URL protocols
michael@0 109 /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
michael@0 110 ];
michael@0 111
michael@0 112 /**
michael@0 113 * The ConsoleOutput object is used to manage output of messages in the Web
michael@0 114 * Console.
michael@0 115 *
michael@0 116 * @constructor
michael@0 117 * @param object owner
michael@0 118 * The console output owner. This usually the WebConsoleFrame instance.
michael@0 119 * Any other object can be used, as long as it has the following
michael@0 120 * properties and methods:
michael@0 121 * - window
michael@0 122 * - document
michael@0 123 * - outputMessage(category, methodOrNode[, methodArguments])
michael@0 124 * TODO: this is needed temporarily, until bug 778766 is fixed.
michael@0 125 */
michael@0 126 function ConsoleOutput(owner)
michael@0 127 {
michael@0 128 this.owner = owner;
michael@0 129 this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this);
michael@0 130 }
michael@0 131
michael@0 132 ConsoleOutput.prototype = {
michael@0 133 _dummyElement: null,
michael@0 134
michael@0 135 /**
michael@0 136 * The output container.
michael@0 137 * @type DOMElement
michael@0 138 */
michael@0 139 get element() {
michael@0 140 return this.owner.outputNode;
michael@0 141 },
michael@0 142
michael@0 143 /**
michael@0 144 * The document that holds the output.
michael@0 145 * @type DOMDocument
michael@0 146 */
michael@0 147 get document() {
michael@0 148 return this.owner ? this.owner.document : null;
michael@0 149 },
michael@0 150
michael@0 151 /**
michael@0 152 * The DOM window that holds the output.
michael@0 153 * @type Window
michael@0 154 */
michael@0 155 get window() {
michael@0 156 return this.owner.window;
michael@0 157 },
michael@0 158
michael@0 159 /**
michael@0 160 * Getter for the debugger WebConsoleClient.
michael@0 161 * @type object
michael@0 162 */
michael@0 163 get webConsoleClient() {
michael@0 164 return this.owner.webConsoleClient;
michael@0 165 },
michael@0 166
michael@0 167 /**
michael@0 168 * Getter for the current toolbox debuggee target.
michael@0 169 * @type Target
michael@0 170 */
michael@0 171 get toolboxTarget() {
michael@0 172 return this.owner.owner.target;
michael@0 173 },
michael@0 174
michael@0 175 /**
michael@0 176 * Release an actor.
michael@0 177 *
michael@0 178 * @private
michael@0 179 * @param string actorId
michael@0 180 * The actor ID you want to release.
michael@0 181 */
michael@0 182 _releaseObject: function(actorId)
michael@0 183 {
michael@0 184 this.owner._releaseObject(actorId);
michael@0 185 },
michael@0 186
michael@0 187 /**
michael@0 188 * Add a message to output.
michael@0 189 *
michael@0 190 * @param object ...args
michael@0 191 * Any number of Message objects.
michael@0 192 * @return this
michael@0 193 */
michael@0 194 addMessage: function(...args)
michael@0 195 {
michael@0 196 for (let msg of args) {
michael@0 197 msg.init(this);
michael@0 198 this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage,
michael@0 199 [msg]);
michael@0 200 }
michael@0 201 return this;
michael@0 202 },
michael@0 203
michael@0 204 /**
michael@0 205 * Message renderer used for compatibility with the current Web Console output
michael@0 206 * implementation. This method is invoked for every message object that is
michael@0 207 * flushed to output. The message object is initialized and rendered, then it
michael@0 208 * is displayed.
michael@0 209 *
michael@0 210 * TODO: remove this method once bug 778766 is fixed.
michael@0 211 *
michael@0 212 * @private
michael@0 213 * @param object message
michael@0 214 * The message object to render.
michael@0 215 * @return DOMElement
michael@0 216 * The message DOM element that can be added to the console output.
michael@0 217 */
michael@0 218 _onFlushOutputMessage: function(message)
michael@0 219 {
michael@0 220 return message.render().element;
michael@0 221 },
michael@0 222
michael@0 223 /**
michael@0 224 * Get an array of selected messages. This list is based on the text selection
michael@0 225 * start and end points.
michael@0 226 *
michael@0 227 * @param number [limit]
michael@0 228 * Optional limit of selected messages you want. If no value is given,
michael@0 229 * all of the selected messages are returned.
michael@0 230 * @return array
michael@0 231 * Array of DOM elements for each message that is currently selected.
michael@0 232 */
michael@0 233 getSelectedMessages: function(limit)
michael@0 234 {
michael@0 235 let selection = this.window.getSelection();
michael@0 236 if (selection.isCollapsed) {
michael@0 237 return [];
michael@0 238 }
michael@0 239
michael@0 240 if (selection.containsNode(this.element, true)) {
michael@0 241 return Array.slice(this.element.children);
michael@0 242 }
michael@0 243
michael@0 244 let anchor = this.getMessageForElement(selection.anchorNode);
michael@0 245 let focus = this.getMessageForElement(selection.focusNode);
michael@0 246 if (!anchor || !focus) {
michael@0 247 return [];
michael@0 248 }
michael@0 249
michael@0 250 let start, end;
michael@0 251 if (anchor.timestamp > focus.timestamp) {
michael@0 252 start = focus;
michael@0 253 end = anchor;
michael@0 254 } else {
michael@0 255 start = anchor;
michael@0 256 end = focus;
michael@0 257 }
michael@0 258
michael@0 259 let result = [];
michael@0 260 let current = start;
michael@0 261 while (current) {
michael@0 262 result.push(current);
michael@0 263 if (current == end || (limit && result.length == limit)) {
michael@0 264 break;
michael@0 265 }
michael@0 266 current = current.nextSibling;
michael@0 267 }
michael@0 268 return result;
michael@0 269 },
michael@0 270
michael@0 271 /**
michael@0 272 * Find the DOM element of a message for any given descendant.
michael@0 273 *
michael@0 274 * @param DOMElement elem
michael@0 275 * The element to start the search from.
michael@0 276 * @return DOMElement|null
michael@0 277 * The DOM element of the message, if any.
michael@0 278 */
michael@0 279 getMessageForElement: function(elem)
michael@0 280 {
michael@0 281 while (elem && elem.parentNode) {
michael@0 282 if (elem.classList && elem.classList.contains("message")) {
michael@0 283 return elem;
michael@0 284 }
michael@0 285 elem = elem.parentNode;
michael@0 286 }
michael@0 287 return null;
michael@0 288 },
michael@0 289
michael@0 290 /**
michael@0 291 * Select all messages.
michael@0 292 */
michael@0 293 selectAllMessages: function()
michael@0 294 {
michael@0 295 let selection = this.window.getSelection();
michael@0 296 selection.removeAllRanges();
michael@0 297 let range = this.document.createRange();
michael@0 298 range.selectNodeContents(this.element);
michael@0 299 selection.addRange(range);
michael@0 300 },
michael@0 301
michael@0 302 /**
michael@0 303 * Add a message to the selection.
michael@0 304 *
michael@0 305 * @param DOMElement elem
michael@0 306 * The message element to select.
michael@0 307 */
michael@0 308 selectMessage: function(elem)
michael@0 309 {
michael@0 310 let selection = this.window.getSelection();
michael@0 311 selection.removeAllRanges();
michael@0 312 let range = this.document.createRange();
michael@0 313 range.selectNodeContents(elem);
michael@0 314 selection.addRange(range);
michael@0 315 },
michael@0 316
michael@0 317 /**
michael@0 318 * Open an URL in a new tab.
michael@0 319 * @see WebConsole.openLink() in hudservice.js
michael@0 320 */
michael@0 321 openLink: function()
michael@0 322 {
michael@0 323 this.owner.owner.openLink.apply(this.owner.owner, arguments);
michael@0 324 },
michael@0 325
michael@0 326 /**
michael@0 327 * Open the variables view to inspect an object actor.
michael@0 328 * @see JSTerm.openVariablesView() in webconsole.js
michael@0 329 */
michael@0 330 openVariablesView: function()
michael@0 331 {
michael@0 332 this.owner.jsterm.openVariablesView.apply(this.owner.jsterm, arguments);
michael@0 333 },
michael@0 334
michael@0 335 /**
michael@0 336 * Destroy this ConsoleOutput instance.
michael@0 337 */
michael@0 338 destroy: function()
michael@0 339 {
michael@0 340 this._dummyElement = null;
michael@0 341 this.owner = null;
michael@0 342 },
michael@0 343 }; // ConsoleOutput.prototype
michael@0 344
michael@0 345 /**
michael@0 346 * Message objects container.
michael@0 347 * @type object
michael@0 348 */
michael@0 349 let Messages = {};
michael@0 350
michael@0 351 /**
michael@0 352 * The BaseMessage object is used for all types of messages. Every kind of
michael@0 353 * message should use this object as its base.
michael@0 354 *
michael@0 355 * @constructor
michael@0 356 */
michael@0 357 Messages.BaseMessage = function()
michael@0 358 {
michael@0 359 this.widgets = new Set();
michael@0 360 this._onClickAnchor = this._onClickAnchor.bind(this);
michael@0 361 this._repeatID = { uid: gSequenceId() };
michael@0 362 this.textContent = "";
michael@0 363 };
michael@0 364
michael@0 365 Messages.BaseMessage.prototype = {
michael@0 366 /**
michael@0 367 * Reference to the ConsoleOutput owner.
michael@0 368 *
michael@0 369 * @type object|null
michael@0 370 * This is |null| if the message is not yet initialized.
michael@0 371 */
michael@0 372 output: null,
michael@0 373
michael@0 374 /**
michael@0 375 * Reference to the parent message object, if this message is in a group or if
michael@0 376 * it is otherwise owned by another message.
michael@0 377 *
michael@0 378 * @type object|null
michael@0 379 */
michael@0 380 parent: null,
michael@0 381
michael@0 382 /**
michael@0 383 * Message DOM element.
michael@0 384 *
michael@0 385 * @type DOMElement|null
michael@0 386 * This is |null| if the message is not yet rendered.
michael@0 387 */
michael@0 388 element: null,
michael@0 389
michael@0 390 /**
michael@0 391 * Tells if this message is visible or not.
michael@0 392 * @type boolean
michael@0 393 */
michael@0 394 get visible() {
michael@0 395 return this.element && this.element.parentNode;
michael@0 396 },
michael@0 397
michael@0 398 /**
michael@0 399 * The owner DOM document.
michael@0 400 * @type DOMElement
michael@0 401 */
michael@0 402 get document() {
michael@0 403 return this.output.document;
michael@0 404 },
michael@0 405
michael@0 406 /**
michael@0 407 * Holds the text-only representation of the message.
michael@0 408 * @type string
michael@0 409 */
michael@0 410 textContent: null,
michael@0 411
michael@0 412 /**
michael@0 413 * Set of widgets included in this message.
michael@0 414 * @type Set
michael@0 415 */
michael@0 416 widgets: null,
michael@0 417
michael@0 418 // Properties that allow compatibility with the current Web Console output
michael@0 419 // implementation.
michael@0 420 _categoryCompat: null,
michael@0 421 _severityCompat: null,
michael@0 422 _categoryNameCompat: null,
michael@0 423 _severityNameCompat: null,
michael@0 424 _filterKeyCompat: null,
michael@0 425
michael@0 426 /**
michael@0 427 * Object that is JSON-ified and used as a non-unique ID for tracking
michael@0 428 * duplicate messages.
michael@0 429 * @private
michael@0 430 * @type object
michael@0 431 */
michael@0 432 _repeatID: null,
michael@0 433
michael@0 434 /**
michael@0 435 * Initialize the message.
michael@0 436 *
michael@0 437 * @param object output
michael@0 438 * The ConsoleOutput owner.
michael@0 439 * @param object [parent=null]
michael@0 440 * Optional: a different message object that owns this instance.
michael@0 441 * @return this
michael@0 442 */
michael@0 443 init: function(output, parent=null)
michael@0 444 {
michael@0 445 this.output = output;
michael@0 446 this.parent = parent;
michael@0 447 return this;
michael@0 448 },
michael@0 449
michael@0 450 /**
michael@0 451 * Non-unique ID for this message object used for tracking duplicate messages.
michael@0 452 * Different message kinds can identify themselves based their own criteria.
michael@0 453 *
michael@0 454 * @return string
michael@0 455 */
michael@0 456 getRepeatID: function()
michael@0 457 {
michael@0 458 return JSON.stringify(this._repeatID);
michael@0 459 },
michael@0 460
michael@0 461 /**
michael@0 462 * Render the message. After this method is invoked the |element| property
michael@0 463 * will point to the DOM element of this message.
michael@0 464 * @return this
michael@0 465 */
michael@0 466 render: function()
michael@0 467 {
michael@0 468 if (!this.element) {
michael@0 469 this.element = this._renderCompat();
michael@0 470 }
michael@0 471 return this;
michael@0 472 },
michael@0 473
michael@0 474 /**
michael@0 475 * Prepare the message container for the Web Console, such that it is
michael@0 476 * compatible with the current implementation.
michael@0 477 * TODO: remove this once bug 778766 is fixed.
michael@0 478 *
michael@0 479 * @private
michael@0 480 * @return Element
michael@0 481 * The DOM element that wraps the message.
michael@0 482 */
michael@0 483 _renderCompat: function()
michael@0 484 {
michael@0 485 let doc = this.output.document;
michael@0 486 let container = doc.createElementNS(XHTML_NS, "div");
michael@0 487 container.id = "console-msg-" + gSequenceId();
michael@0 488 container.className = "message";
michael@0 489 container.category = this._categoryCompat;
michael@0 490 container.severity = this._severityCompat;
michael@0 491 container.setAttribute("category", this._categoryNameCompat);
michael@0 492 container.setAttribute("severity", this._severityNameCompat);
michael@0 493 container.setAttribute("filter", this._filterKeyCompat);
michael@0 494 container.clipboardText = this.textContent;
michael@0 495 container.timestamp = this.timestamp;
michael@0 496 container._messageObject = this;
michael@0 497
michael@0 498 return container;
michael@0 499 },
michael@0 500
michael@0 501 /**
michael@0 502 * Add a click callback to a given DOM element.
michael@0 503 *
michael@0 504 * @private
michael@0 505 * @param Element element
michael@0 506 * The DOM element to which you want to add a click event handler.
michael@0 507 * @param function [callback=this._onClickAnchor]
michael@0 508 * Optional click event handler. The default event handler is
michael@0 509 * |this._onClickAnchor|.
michael@0 510 */
michael@0 511 _addLinkCallback: function(element, callback = this._onClickAnchor)
michael@0 512 {
michael@0 513 // This is going into the WebConsoleFrame object instance that owns
michael@0 514 // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole
michael@0 515 // object instance from hudservice.js.
michael@0 516 // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766
michael@0 517 // is fixed.
michael@0 518 this.output.owner._addMessageLinkCallback(element, callback);
michael@0 519 },
michael@0 520
michael@0 521 /**
michael@0 522 * The default |click| event handler for links in the output. This function
michael@0 523 * opens the anchor's link in a new tab.
michael@0 524 *
michael@0 525 * @private
michael@0 526 * @param Event event
michael@0 527 * The DOM event that invoked this function.
michael@0 528 */
michael@0 529 _onClickAnchor: function(event)
michael@0 530 {
michael@0 531 this.output.openLink(event.target.href);
michael@0 532 },
michael@0 533
michael@0 534 destroy: function()
michael@0 535 {
michael@0 536 // Destroy all widgets that have registered themselves in this.widgets
michael@0 537 for (let widget of this.widgets) {
michael@0 538 widget.destroy();
michael@0 539 }
michael@0 540 this.widgets.clear();
michael@0 541 }
michael@0 542 }; // Messages.BaseMessage.prototype
michael@0 543
michael@0 544
michael@0 545 /**
michael@0 546 * The NavigationMarker is used to show a page load event.
michael@0 547 *
michael@0 548 * @constructor
michael@0 549 * @extends Messages.BaseMessage
michael@0 550 * @param string url
michael@0 551 * The URL to display.
michael@0 552 * @param number timestamp
michael@0 553 * The message date and time, milliseconds elapsed since 1 January 1970
michael@0 554 * 00:00:00 UTC.
michael@0 555 */
michael@0 556 Messages.NavigationMarker = function(url, timestamp)
michael@0 557 {
michael@0 558 Messages.BaseMessage.call(this);
michael@0 559 this._url = url;
michael@0 560 this.textContent = "------ " + url;
michael@0 561 this.timestamp = timestamp;
michael@0 562 };
michael@0 563
michael@0 564 Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.prototype,
michael@0 565 {
michael@0 566 /**
michael@0 567 * The address of the loading page.
michael@0 568 * @private
michael@0 569 * @type string
michael@0 570 */
michael@0 571 _url: null,
michael@0 572
michael@0 573 /**
michael@0 574 * Message timestamp.
michael@0 575 *
michael@0 576 * @type number
michael@0 577 * Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
michael@0 578 */
michael@0 579 timestamp: 0,
michael@0 580
michael@0 581 _categoryCompat: COMPAT.CATEGORIES.NETWORK,
michael@0 582 _severityCompat: COMPAT.SEVERITIES.LOG,
michael@0 583 _categoryNameCompat: "network",
michael@0 584 _severityNameCompat: "info",
michael@0 585 _filterKeyCompat: "networkinfo",
michael@0 586
michael@0 587 /**
michael@0 588 * Prepare the DOM element for this message.
michael@0 589 * @return this
michael@0 590 */
michael@0 591 render: function()
michael@0 592 {
michael@0 593 if (this.element) {
michael@0 594 return this;
michael@0 595 }
michael@0 596
michael@0 597 let url = this._url;
michael@0 598 let pos = url.indexOf("?");
michael@0 599 if (pos > -1) {
michael@0 600 url = url.substr(0, pos);
michael@0 601 }
michael@0 602
michael@0 603 let doc = this.output.document;
michael@0 604 let urlnode = doc.createElementNS(XHTML_NS, "a");
michael@0 605 urlnode.className = "url";
michael@0 606 urlnode.textContent = url;
michael@0 607 urlnode.title = this._url;
michael@0 608 urlnode.href = this._url;
michael@0 609 urlnode.draggable = false;
michael@0 610 this._addLinkCallback(urlnode);
michael@0 611
michael@0 612 let render = Messages.BaseMessage.prototype.render.bind(this);
michael@0 613 render().element.appendChild(urlnode);
michael@0 614 this.element.classList.add("navigation-marker");
michael@0 615 this.element.url = this._url;
michael@0 616 this.element.appendChild(doc.createTextNode("\n"));
michael@0 617
michael@0 618 return this;
michael@0 619 },
michael@0 620 }); // Messages.NavigationMarker.prototype
michael@0 621
michael@0 622
michael@0 623 /**
michael@0 624 * The Simple message is used to show any basic message in the Web Console.
michael@0 625 *
michael@0 626 * @constructor
michael@0 627 * @extends Messages.BaseMessage
michael@0 628 * @param string|Node|function message
michael@0 629 * The message to display.
michael@0 630 * @param object [options]
michael@0 631 * Options for this message:
michael@0 632 * - category: (string) category that this message belongs to. Defaults
michael@0 633 * to no category.
michael@0 634 * - severity: (string) severity of the message. Defaults to no severity.
michael@0 635 * - timestamp: (number) date and time when the message was recorded.
michael@0 636 * Defaults to |Date.now()|.
michael@0 637 * - link: (string) if provided, the message will be wrapped in an anchor
michael@0 638 * pointing to the given URL here.
michael@0 639 * - linkCallback: (function) if provided, the message will be wrapped in
michael@0 640 * an anchor. The |linkCallback| function will be added as click event
michael@0 641 * handler.
michael@0 642 * - location: object that tells the message source: url, line, column
michael@0 643 * and lineText.
michael@0 644 * - className: (string) additional element class names for styling
michael@0 645 * purposes.
michael@0 646 * - private: (boolean) mark this as a private message.
michael@0 647 * - filterDuplicates: (boolean) true if you do want this message to be
michael@0 648 * filtered as a potential duplicate message, false otherwise.
michael@0 649 */
michael@0 650 Messages.Simple = function(message, options = {})
michael@0 651 {
michael@0 652 Messages.BaseMessage.call(this);
michael@0 653
michael@0 654 this.category = options.category;
michael@0 655 this.severity = options.severity;
michael@0 656 this.location = options.location;
michael@0 657 this.timestamp = options.timestamp || Date.now();
michael@0 658 this.private = !!options.private;
michael@0 659
michael@0 660 this._message = message;
michael@0 661 this._className = options.className;
michael@0 662 this._link = options.link;
michael@0 663 this._linkCallback = options.linkCallback;
michael@0 664 this._filterDuplicates = options.filterDuplicates;
michael@0 665 };
michael@0 666
michael@0 667 Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
michael@0 668 {
michael@0 669 /**
michael@0 670 * Message category.
michael@0 671 * @type string
michael@0 672 */
michael@0 673 category: null,
michael@0 674
michael@0 675 /**
michael@0 676 * Message severity.
michael@0 677 * @type string
michael@0 678 */
michael@0 679 severity: null,
michael@0 680
michael@0 681 /**
michael@0 682 * Message source location. Properties: url, line, column, lineText.
michael@0 683 * @type object
michael@0 684 */
michael@0 685 location: null,
michael@0 686
michael@0 687 /**
michael@0 688 * Tells if this message comes from a private browsing context.
michael@0 689 * @type boolean
michael@0 690 */
michael@0 691 private: false,
michael@0 692
michael@0 693 /**
michael@0 694 * Custom class name for the DOM element of the message.
michael@0 695 * @private
michael@0 696 * @type string
michael@0 697 */
michael@0 698 _className: null,
michael@0 699
michael@0 700 /**
michael@0 701 * Message link - if this message is clicked then this URL opens in a new tab.
michael@0 702 * @private
michael@0 703 * @type string
michael@0 704 */
michael@0 705 _link: null,
michael@0 706
michael@0 707 /**
michael@0 708 * Message click event handler.
michael@0 709 * @private
michael@0 710 * @type function
michael@0 711 */
michael@0 712 _linkCallback: null,
michael@0 713
michael@0 714 /**
michael@0 715 * Tells if this message should be checked if it is a duplicate of another
michael@0 716 * message or not.
michael@0 717 */
michael@0 718 _filterDuplicates: false,
michael@0 719
michael@0 720 /**
michael@0 721 * The raw message displayed by this Message object. This can be a function,
michael@0 722 * DOM node or a string.
michael@0 723 *
michael@0 724 * @private
michael@0 725 * @type mixed
michael@0 726 */
michael@0 727 _message: null,
michael@0 728
michael@0 729 _afterMessage: null,
michael@0 730 _objectActors: null,
michael@0 731 _groupDepthCompat: 0,
michael@0 732
michael@0 733 /**
michael@0 734 * Message timestamp.
michael@0 735 *
michael@0 736 * @type number
michael@0 737 * Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
michael@0 738 */
michael@0 739 timestamp: 0,
michael@0 740
michael@0 741 get _categoryCompat() {
michael@0 742 return this.category ?
michael@0 743 COMPAT.CATEGORIES[this.category.toUpperCase()] : null;
michael@0 744 },
michael@0 745 get _severityCompat() {
michael@0 746 return this.severity ?
michael@0 747 COMPAT.SEVERITIES[this.severity.toUpperCase()] : null;
michael@0 748 },
michael@0 749 get _categoryNameCompat() {
michael@0 750 return this.category ?
michael@0 751 COMPAT.CATEGORY_CLASS_FRAGMENTS[this._categoryCompat] : null;
michael@0 752 },
michael@0 753 get _severityNameCompat() {
michael@0 754 return this.severity ?
michael@0 755 COMPAT.SEVERITY_CLASS_FRAGMENTS[this._severityCompat] : null;
michael@0 756 },
michael@0 757
michael@0 758 get _filterKeyCompat() {
michael@0 759 return this._categoryCompat !== null && this._severityCompat !== null ?
michael@0 760 COMPAT.PREFERENCE_KEYS[this._categoryCompat][this._severityCompat] :
michael@0 761 null;
michael@0 762 },
michael@0 763
michael@0 764 init: function()
michael@0 765 {
michael@0 766 Messages.BaseMessage.prototype.init.apply(this, arguments);
michael@0 767 this._groupDepthCompat = this.output.owner.groupDepth;
michael@0 768 this._initRepeatID();
michael@0 769 return this;
michael@0 770 },
michael@0 771
michael@0 772 _initRepeatID: function()
michael@0 773 {
michael@0 774 if (!this._filterDuplicates) {
michael@0 775 return;
michael@0 776 }
michael@0 777
michael@0 778 // Add the properties we care about for identifying duplicate messages.
michael@0 779 let rid = this._repeatID;
michael@0 780 delete rid.uid;
michael@0 781
michael@0 782 rid.category = this.category;
michael@0 783 rid.severity = this.severity;
michael@0 784 rid.private = this.private;
michael@0 785 rid.location = this.location;
michael@0 786 rid.link = this._link;
michael@0 787 rid.linkCallback = this._linkCallback + "";
michael@0 788 rid.className = this._className;
michael@0 789 rid.groupDepth = this._groupDepthCompat;
michael@0 790 rid.textContent = "";
michael@0 791 },
michael@0 792
michael@0 793 getRepeatID: function()
michael@0 794 {
michael@0 795 // No point in returning a string that includes other properties when there
michael@0 796 // is a unique ID.
michael@0 797 if (this._repeatID.uid) {
michael@0 798 return JSON.stringify({ uid: this._repeatID.uid });
michael@0 799 }
michael@0 800
michael@0 801 return JSON.stringify(this._repeatID);
michael@0 802 },
michael@0 803
michael@0 804 render: function()
michael@0 805 {
michael@0 806 if (this.element) {
michael@0 807 return this;
michael@0 808 }
michael@0 809
michael@0 810 let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render();
michael@0 811
michael@0 812 let icon = this.document.createElementNS(XHTML_NS, "span");
michael@0 813 icon.className = "icon";
michael@0 814
michael@0 815 // Apply the current group by indenting appropriately.
michael@0 816 // TODO: remove this once bug 778766 is fixed.
michael@0 817 let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT;
michael@0 818 let indentNode = this.document.createElementNS(XHTML_NS, "span");
michael@0 819 indentNode.className = "indent";
michael@0 820 indentNode.style.width = indent + "px";
michael@0 821
michael@0 822 let body = this._renderBody();
michael@0 823 this._repeatID.textContent += "|" + body.textContent;
michael@0 824
michael@0 825 let repeatNode = this._renderRepeatNode();
michael@0 826 let location = this._renderLocation();
michael@0 827
michael@0 828 Messages.BaseMessage.prototype.render.call(this);
michael@0 829 if (this._className) {
michael@0 830 this.element.className += " " + this._className;
michael@0 831 }
michael@0 832
michael@0 833 this.element.appendChild(timestamp.element);
michael@0 834 this.element.appendChild(indentNode);
michael@0 835 this.element.appendChild(icon);
michael@0 836 this.element.appendChild(body);
michael@0 837 if (repeatNode) {
michael@0 838 this.element.appendChild(repeatNode);
michael@0 839 }
michael@0 840 if (location) {
michael@0 841 this.element.appendChild(location);
michael@0 842 }
michael@0 843 this.element.appendChild(this.document.createTextNode("\n"));
michael@0 844
michael@0 845 this.element.clipboardText = this.element.textContent;
michael@0 846
michael@0 847 if (this.private) {
michael@0 848 this.element.setAttribute("private", true);
michael@0 849 }
michael@0 850
michael@0 851 if (this._afterMessage) {
michael@0 852 this.element._outputAfterNode = this._afterMessage.element;
michael@0 853 this._afterMessage = null;
michael@0 854 }
michael@0 855
michael@0 856 // TODO: handle object releasing in a more elegant way once all console
michael@0 857 // messages use the new API - bug 778766.
michael@0 858 this.element._objectActors = this._objectActors;
michael@0 859 this._objectActors = null;
michael@0 860
michael@0 861 return this;
michael@0 862 },
michael@0 863
michael@0 864 /**
michael@0 865 * Render the message body DOM element.
michael@0 866 * @private
michael@0 867 * @return Element
michael@0 868 */
michael@0 869 _renderBody: function()
michael@0 870 {
michael@0 871 let body = this.document.createElementNS(XHTML_NS, "span");
michael@0 872 body.className = "message-body-wrapper message-body devtools-monospace";
michael@0 873
michael@0 874 let anchor, container = body;
michael@0 875 if (this._link || this._linkCallback) {
michael@0 876 container = anchor = this.document.createElementNS(XHTML_NS, "a");
michael@0 877 anchor.href = this._link || "#";
michael@0 878 anchor.draggable = false;
michael@0 879 this._addLinkCallback(anchor, this._linkCallback);
michael@0 880 body.appendChild(anchor);
michael@0 881 }
michael@0 882
michael@0 883 if (typeof this._message == "function") {
michael@0 884 container.appendChild(this._message(this));
michael@0 885 } else if (this._message instanceof Ci.nsIDOMNode) {
michael@0 886 container.appendChild(this._message);
michael@0 887 } else {
michael@0 888 container.textContent = this._message;
michael@0 889 }
michael@0 890
michael@0 891 return body;
michael@0 892 },
michael@0 893
michael@0 894 /**
michael@0 895 * Render the repeat bubble DOM element part of the message.
michael@0 896 * @private
michael@0 897 * @return Element
michael@0 898 */
michael@0 899 _renderRepeatNode: function()
michael@0 900 {
michael@0 901 if (!this._filterDuplicates) {
michael@0 902 return null;
michael@0 903 }
michael@0 904
michael@0 905 let repeatNode = this.document.createElementNS(XHTML_NS, "span");
michael@0 906 repeatNode.setAttribute("value", "1");
michael@0 907 repeatNode.className = "message-repeats";
michael@0 908 repeatNode.textContent = 1;
michael@0 909 repeatNode._uid = this.getRepeatID();
michael@0 910 return repeatNode;
michael@0 911 },
michael@0 912
michael@0 913 /**
michael@0 914 * Render the message source location DOM element.
michael@0 915 * @private
michael@0 916 * @return Element
michael@0 917 */
michael@0 918 _renderLocation: function()
michael@0 919 {
michael@0 920 if (!this.location) {
michael@0 921 return null;
michael@0 922 }
michael@0 923
michael@0 924 let {url, line} = this.location;
michael@0 925 if (IGNORED_SOURCE_URLS.indexOf(url) != -1) {
michael@0 926 return null;
michael@0 927 }
michael@0 928
michael@0 929 // The ConsoleOutput owner is a WebConsoleFrame instance from webconsole.js.
michael@0 930 // TODO: move createLocationNode() into this file when bug 778766 is fixed.
michael@0 931 return this.output.owner.createLocationNode(url, line);
michael@0 932 },
michael@0 933 }); // Messages.Simple.prototype
michael@0 934
michael@0 935
michael@0 936 /**
michael@0 937 * The Extended message.
michael@0 938 *
michael@0 939 * @constructor
michael@0 940 * @extends Messages.Simple
michael@0 941 * @param array messagePieces
michael@0 942 * The message to display given as an array of elements. Each array
michael@0 943 * element can be a DOM node, function, ObjectActor, LongString or
michael@0 944 * a string.
michael@0 945 * @param object [options]
michael@0 946 * Options for rendering this message:
michael@0 947 * - quoteStrings: boolean that tells if you want strings to be wrapped
michael@0 948 * in quotes or not.
michael@0 949 */
michael@0 950 Messages.Extended = function(messagePieces, options = {})
michael@0 951 {
michael@0 952 Messages.Simple.call(this, null, options);
michael@0 953
michael@0 954 this._messagePieces = messagePieces;
michael@0 955
michael@0 956 if ("quoteStrings" in options) {
michael@0 957 this._quoteStrings = options.quoteStrings;
michael@0 958 }
michael@0 959
michael@0 960 this._repeatID.quoteStrings = this._quoteStrings;
michael@0 961 this._repeatID.messagePieces = messagePieces + "";
michael@0 962 this._repeatID.actors = new Set(); // using a set to avoid duplicates
michael@0 963 };
michael@0 964
michael@0 965 Messages.Extended.prototype = Heritage.extend(Messages.Simple.prototype,
michael@0 966 {
michael@0 967 /**
michael@0 968 * The message pieces displayed by this message instance.
michael@0 969 * @private
michael@0 970 * @type array
michael@0 971 */
michael@0 972 _messagePieces: null,
michael@0 973
michael@0 974 /**
michael@0 975 * Boolean that tells if the strings displayed in this message are wrapped.
michael@0 976 * @private
michael@0 977 * @type boolean
michael@0 978 */
michael@0 979 _quoteStrings: true,
michael@0 980
michael@0 981 getRepeatID: function()
michael@0 982 {
michael@0 983 if (this._repeatID.uid) {
michael@0 984 return JSON.stringify({ uid: this._repeatID.uid });
michael@0 985 }
michael@0 986
michael@0 987 // Sets are not stringified correctly. Temporarily switching to an array.
michael@0 988 let actors = this._repeatID.actors;
michael@0 989 this._repeatID.actors = [...actors];
michael@0 990 let result = JSON.stringify(this._repeatID);
michael@0 991 this._repeatID.actors = actors;
michael@0 992 return result;
michael@0 993 },
michael@0 994
michael@0 995 render: function()
michael@0 996 {
michael@0 997 let result = this.document.createDocumentFragment();
michael@0 998
michael@0 999 for (let i = 0; i < this._messagePieces.length; i++) {
michael@0 1000 let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
michael@0 1001 if (separator) {
michael@0 1002 result.appendChild(separator);
michael@0 1003 }
michael@0 1004
michael@0 1005 let piece = this._messagePieces[i];
michael@0 1006 result.appendChild(this._renderBodyPiece(piece));
michael@0 1007 }
michael@0 1008
michael@0 1009 this._message = result;
michael@0 1010 this._messagePieces = null;
michael@0 1011 return Messages.Simple.prototype.render.call(this);
michael@0 1012 },
michael@0 1013
michael@0 1014 /**
michael@0 1015 * Render the separator between the pieces of the message.
michael@0 1016 *
michael@0 1017 * @private
michael@0 1018 * @return Element
michael@0 1019 */
michael@0 1020 _renderBodyPieceSeparator: function() { return null; },
michael@0 1021
michael@0 1022 /**
michael@0 1023 * Render one piece/element of the message array.
michael@0 1024 *
michael@0 1025 * @private
michael@0 1026 * @param mixed piece
michael@0 1027 * Message element to display - this can be a LongString, ObjectActor,
michael@0 1028 * DOM node or a function to invoke.
michael@0 1029 * @return Element
michael@0 1030 */
michael@0 1031 _renderBodyPiece: function(piece)
michael@0 1032 {
michael@0 1033 if (piece instanceof Ci.nsIDOMNode) {
michael@0 1034 return piece;
michael@0 1035 }
michael@0 1036 if (typeof piece == "function") {
michael@0 1037 return piece(this);
michael@0 1038 }
michael@0 1039
michael@0 1040 return this._renderValueGrip(piece);
michael@0 1041 },
michael@0 1042
michael@0 1043 /**
michael@0 1044 * Render a grip that represents a value received from the server. This method
michael@0 1045 * picks the appropriate widget to render the value with.
michael@0 1046 *
michael@0 1047 * @private
michael@0 1048 * @param object grip
michael@0 1049 * The value grip received from the server.
michael@0 1050 * @param object options
michael@0 1051 * Options for displaying the value. Available options:
michael@0 1052 * - noStringQuotes - boolean that tells the renderer to not use quotes
michael@0 1053 * around strings.
michael@0 1054 * - concise - boolean that tells the renderer to compactly display the
michael@0 1055 * grip. This is typically set to true when the object needs to be
michael@0 1056 * displayed in an array preview, or as a property value in object
michael@0 1057 * previews, etc.
michael@0 1058 * @return DOMElement
michael@0 1059 * The DOM element that displays the given grip.
michael@0 1060 */
michael@0 1061 _renderValueGrip: function(grip, options = {})
michael@0 1062 {
michael@0 1063 let isPrimitive = VariablesView.isPrimitive({ value: grip });
michael@0 1064 let isActorGrip = WebConsoleUtils.isActorGrip(grip);
michael@0 1065 let noStringQuotes = !this._quoteStrings;
michael@0 1066 if ("noStringQuotes" in options) {
michael@0 1067 noStringQuotes = options.noStringQuotes;
michael@0 1068 }
michael@0 1069
michael@0 1070 if (isActorGrip) {
michael@0 1071 this._repeatID.actors.add(grip.actor);
michael@0 1072
michael@0 1073 if (!isPrimitive) {
michael@0 1074 return this._renderObjectActor(grip, options);
michael@0 1075 }
michael@0 1076 if (grip.type == "longString") {
michael@0 1077 let widget = new Widgets.LongString(this, grip, options).render();
michael@0 1078 return widget.element;
michael@0 1079 }
michael@0 1080 }
michael@0 1081
michael@0 1082 let result = this.document.createElementNS(XHTML_NS, "span");
michael@0 1083 if (isPrimitive) {
michael@0 1084 let className = this.getClassNameForValueGrip(grip);
michael@0 1085 if (className) {
michael@0 1086 result.className = className;
michael@0 1087 }
michael@0 1088
michael@0 1089 result.textContent = VariablesView.getString(grip, {
michael@0 1090 noStringQuotes: noStringQuotes,
michael@0 1091 concise: options.concise,
michael@0 1092 });
michael@0 1093 } else {
michael@0 1094 result.textContent = grip;
michael@0 1095 }
michael@0 1096
michael@0 1097 return result;
michael@0 1098 },
michael@0 1099
michael@0 1100 /**
michael@0 1101 * Get a CodeMirror-compatible class name for a given value grip.
michael@0 1102 *
michael@0 1103 * @param object grip
michael@0 1104 * Value grip from the server.
michael@0 1105 * @return string
michael@0 1106 * The class name for the grip.
michael@0 1107 */
michael@0 1108 getClassNameForValueGrip: function(grip)
michael@0 1109 {
michael@0 1110 let map = {
michael@0 1111 "number": "cm-number",
michael@0 1112 "longstring": "console-string",
michael@0 1113 "string": "console-string",
michael@0 1114 "regexp": "cm-string-2",
michael@0 1115 "boolean": "cm-atom",
michael@0 1116 "-infinity": "cm-atom",
michael@0 1117 "infinity": "cm-atom",
michael@0 1118 "null": "cm-atom",
michael@0 1119 "undefined": "cm-comment",
michael@0 1120 };
michael@0 1121
michael@0 1122 let className = map[typeof grip];
michael@0 1123 if (!className && grip && grip.type) {
michael@0 1124 className = map[grip.type.toLowerCase()];
michael@0 1125 }
michael@0 1126 if (!className && grip && grip.class) {
michael@0 1127 className = map[grip.class.toLowerCase()];
michael@0 1128 }
michael@0 1129
michael@0 1130 return className;
michael@0 1131 },
michael@0 1132
michael@0 1133 /**
michael@0 1134 * Display an object actor with the appropriate renderer.
michael@0 1135 *
michael@0 1136 * @private
michael@0 1137 * @param object objectActor
michael@0 1138 * The ObjectActor to display.
michael@0 1139 * @param object options
michael@0 1140 * Options to use for displaying the ObjectActor.
michael@0 1141 * @see this._renderValueGrip for the available options.
michael@0 1142 * @return DOMElement
michael@0 1143 * The DOM element that displays the object actor.
michael@0 1144 */
michael@0 1145 _renderObjectActor: function(objectActor, options = {})
michael@0 1146 {
michael@0 1147 let widget = null;
michael@0 1148 let {preview} = objectActor;
michael@0 1149
michael@0 1150 if (preview && preview.kind) {
michael@0 1151 widget = Widgets.ObjectRenderers.byKind[preview.kind];
michael@0 1152 }
michael@0 1153
michael@0 1154 if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
michael@0 1155 widget = Widgets.ObjectRenderers.byClass[objectActor.class];
michael@0 1156 }
michael@0 1157
michael@0 1158 if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
michael@0 1159 widget = Widgets.JSObject;
michael@0 1160 }
michael@0 1161
michael@0 1162 let instance = new widget(this, objectActor, options).render();
michael@0 1163 return instance.element;
michael@0 1164 },
michael@0 1165 }); // Messages.Extended.prototype
michael@0 1166
michael@0 1167
michael@0 1168
michael@0 1169 /**
michael@0 1170 * The JavaScriptEvalOutput message.
michael@0 1171 *
michael@0 1172 * @constructor
michael@0 1173 * @extends Messages.Extended
michael@0 1174 * @param object evalResponse
michael@0 1175 * The evaluation response packet received from the server.
michael@0 1176 * @param string [errorMessage]
michael@0 1177 * Optional error message to display.
michael@0 1178 */
michael@0 1179 Messages.JavaScriptEvalOutput = function(evalResponse, errorMessage)
michael@0 1180 {
michael@0 1181 let severity = "log", msg, quoteStrings = true;
michael@0 1182
michael@0 1183 if (errorMessage) {
michael@0 1184 severity = "error";
michael@0 1185 msg = errorMessage;
michael@0 1186 quoteStrings = false;
michael@0 1187 } else {
michael@0 1188 msg = evalResponse.result;
michael@0 1189 }
michael@0 1190
michael@0 1191 let options = {
michael@0 1192 className: "cm-s-mozilla",
michael@0 1193 timestamp: evalResponse.timestamp,
michael@0 1194 category: "output",
michael@0 1195 severity: severity,
michael@0 1196 quoteStrings: quoteStrings,
michael@0 1197 };
michael@0 1198 Messages.Extended.call(this, [msg], options);
michael@0 1199 };
michael@0 1200
michael@0 1201 Messages.JavaScriptEvalOutput.prototype = Messages.Extended.prototype;
michael@0 1202
michael@0 1203 /**
michael@0 1204 * The ConsoleGeneric message is used for console API calls.
michael@0 1205 *
michael@0 1206 * @constructor
michael@0 1207 * @extends Messages.Extended
michael@0 1208 * @param object packet
michael@0 1209 * The Console API call packet received from the server.
michael@0 1210 */
michael@0 1211 Messages.ConsoleGeneric = function(packet)
michael@0 1212 {
michael@0 1213 let options = {
michael@0 1214 className: "cm-s-mozilla",
michael@0 1215 timestamp: packet.timeStamp,
michael@0 1216 category: "webdev",
michael@0 1217 severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
michael@0 1218 private: packet.private,
michael@0 1219 filterDuplicates: true,
michael@0 1220 location: {
michael@0 1221 url: packet.filename,
michael@0 1222 line: packet.lineNumber,
michael@0 1223 },
michael@0 1224 };
michael@0 1225
michael@0 1226 switch (packet.level) {
michael@0 1227 case "count": {
michael@0 1228 let counter = packet.counter, label = counter.label;
michael@0 1229 if (!label) {
michael@0 1230 label = l10n.getStr("noCounterLabel");
michael@0 1231 }
michael@0 1232 Messages.Extended.call(this, [label+ ": " + counter.count], options);
michael@0 1233 break;
michael@0 1234 }
michael@0 1235 default:
michael@0 1236 Messages.Extended.call(this, packet.arguments, options);
michael@0 1237 break;
michael@0 1238 }
michael@0 1239
michael@0 1240 this._repeatID.consoleApiLevel = packet.level;
michael@0 1241 this._repeatID.styles = packet.styles;
michael@0 1242 this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
michael@0 1243 this._styles = packet.styles || [];
michael@0 1244
michael@0 1245 this._onClickCollapsible = this._onClickCollapsible.bind(this);
michael@0 1246 };
michael@0 1247
michael@0 1248 Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
michael@0 1249 {
michael@0 1250 _styles: null,
michael@0 1251 _stacktrace: null,
michael@0 1252
michael@0 1253 /**
michael@0 1254 * Tells if the message can be expanded/collapsed.
michael@0 1255 * @type boolean
michael@0 1256 */
michael@0 1257 collapsible: false,
michael@0 1258
michael@0 1259 /**
michael@0 1260 * Getter that tells if this message is collapsed - no details are shown.
michael@0 1261 * @type boolean
michael@0 1262 */
michael@0 1263 get collapsed() {
michael@0 1264 return this.collapsible && this.element && !this.element.hasAttribute("open");
michael@0 1265 },
michael@0 1266
michael@0 1267 _renderBodyPieceSeparator: function()
michael@0 1268 {
michael@0 1269 return this.document.createTextNode(" ");
michael@0 1270 },
michael@0 1271
michael@0 1272 render: function()
michael@0 1273 {
michael@0 1274 let msg = this.document.createElementNS(XHTML_NS, "span");
michael@0 1275 msg.className = "message-body devtools-monospace";
michael@0 1276
michael@0 1277 this._renderBodyPieces(msg);
michael@0 1278
michael@0 1279 let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
michael@0 1280 let location = Messages.Simple.prototype._renderLocation.call(this);
michael@0 1281 if (location) {
michael@0 1282 location.target = "jsdebugger";
michael@0 1283 }
michael@0 1284
michael@0 1285 let stack = null;
michael@0 1286 let twisty = null;
michael@0 1287 if (this._stacktrace && this._stacktrace.length > 0) {
michael@0 1288 stack = new Widgets.Stacktrace(this, this._stacktrace).render().element;
michael@0 1289
michael@0 1290 twisty = this.document.createElementNS(XHTML_NS, "a");
michael@0 1291 twisty.className = "theme-twisty";
michael@0 1292 twisty.href = "#";
michael@0 1293 twisty.title = l10n.getStr("messageToggleDetails");
michael@0 1294 twisty.addEventListener("click", this._onClickCollapsible);
michael@0 1295 }
michael@0 1296
michael@0 1297 let flex = this.document.createElementNS(XHTML_NS, "span");
michael@0 1298 flex.className = "message-flex-body";
michael@0 1299
michael@0 1300 if (twisty) {
michael@0 1301 flex.appendChild(twisty);
michael@0 1302 }
michael@0 1303
michael@0 1304 flex.appendChild(msg);
michael@0 1305
michael@0 1306 if (repeatNode) {
michael@0 1307 flex.appendChild(repeatNode);
michael@0 1308 }
michael@0 1309 if (location) {
michael@0 1310 flex.appendChild(location);
michael@0 1311 }
michael@0 1312
michael@0 1313 let result = this.document.createDocumentFragment();
michael@0 1314 result.appendChild(flex);
michael@0 1315
michael@0 1316 if (stack) {
michael@0 1317 result.appendChild(this.document.createTextNode("\n"));
michael@0 1318 result.appendChild(stack);
michael@0 1319 }
michael@0 1320
michael@0 1321 this._message = result;
michael@0 1322 this._stacktrace = null;
michael@0 1323
michael@0 1324 Messages.Simple.prototype.render.call(this);
michael@0 1325
michael@0 1326 if (stack) {
michael@0 1327 this.collapsible = true;
michael@0 1328 this.element.setAttribute("collapsible", true);
michael@0 1329
michael@0 1330 let icon = this.element.querySelector(".icon");
michael@0 1331 icon.addEventListener("click", this._onClickCollapsible);
michael@0 1332 }
michael@0 1333
michael@0 1334 return this;
michael@0 1335 },
michael@0 1336
michael@0 1337 _renderBody: function()
michael@0 1338 {
michael@0 1339 let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
michael@0 1340 body.classList.remove("devtools-monospace", "message-body");
michael@0 1341 return body;
michael@0 1342 },
michael@0 1343
michael@0 1344 _renderBodyPieces: function(container)
michael@0 1345 {
michael@0 1346 let lastStyle = null;
michael@0 1347
michael@0 1348 for (let i = 0; i < this._messagePieces.length; i++) {
michael@0 1349 let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
michael@0 1350 if (separator) {
michael@0 1351 container.appendChild(separator);
michael@0 1352 }
michael@0 1353
michael@0 1354 let piece = this._messagePieces[i];
michael@0 1355 let style = this._styles[i];
michael@0 1356
michael@0 1357 // No long string support.
michael@0 1358 if (style && typeof style == "string" ) {
michael@0 1359 lastStyle = this.cleanupStyle(style);
michael@0 1360 }
michael@0 1361
michael@0 1362 container.appendChild(this._renderBodyPiece(piece, lastStyle));
michael@0 1363 }
michael@0 1364
michael@0 1365 this._messagePieces = null;
michael@0 1366 this._styles = null;
michael@0 1367 },
michael@0 1368
michael@0 1369 _renderBodyPiece: function(piece, style)
michael@0 1370 {
michael@0 1371 let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece);
michael@0 1372 let result = elem;
michael@0 1373
michael@0 1374 if (style) {
michael@0 1375 if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
michael@0 1376 elem.style = style;
michael@0 1377 } else {
michael@0 1378 let span = this.document.createElementNS(XHTML_NS, "span");
michael@0 1379 span.style = style;
michael@0 1380 span.appendChild(elem);
michael@0 1381 result = span;
michael@0 1382 }
michael@0 1383 }
michael@0 1384
michael@0 1385 return result;
michael@0 1386 },
michael@0 1387
michael@0 1388 // no-op for the message location and .repeats elements.
michael@0 1389 // |this.render()| handles customized message output.
michael@0 1390 _renderLocation: function() { },
michael@0 1391 _renderRepeatNode: function() { },
michael@0 1392
michael@0 1393 /**
michael@0 1394 * Expand/collapse message details.
michael@0 1395 */
michael@0 1396 toggleDetails: function()
michael@0 1397 {
michael@0 1398 let twisty = this.element.querySelector(".theme-twisty");
michael@0 1399 if (this.element.hasAttribute("open")) {
michael@0 1400 this.element.removeAttribute("open");
michael@0 1401 twisty.removeAttribute("open");
michael@0 1402 } else {
michael@0 1403 this.element.setAttribute("open", true);
michael@0 1404 twisty.setAttribute("open", true);
michael@0 1405 }
michael@0 1406 },
michael@0 1407
michael@0 1408 /**
michael@0 1409 * The click event handler for the message expander arrow element. This method
michael@0 1410 * toggles the display of message details.
michael@0 1411 *
michael@0 1412 * @private
michael@0 1413 * @param nsIDOMEvent ev
michael@0 1414 * The DOM event object.
michael@0 1415 * @see this.toggleDetails()
michael@0 1416 */
michael@0 1417 _onClickCollapsible: function(ev)
michael@0 1418 {
michael@0 1419 ev.preventDefault();
michael@0 1420 this.toggleDetails();
michael@0 1421 },
michael@0 1422
michael@0 1423 /**
michael@0 1424 * Given a style attribute value, return a cleaned up version of the string
michael@0 1425 * such that:
michael@0 1426 *
michael@0 1427 * - no external URL is allowed to load. See RE_CLEANUP_STYLES.
michael@0 1428 * - only some of the properties are allowed, based on a whitelist. See
michael@0 1429 * RE_ALLOWED_STYLES.
michael@0 1430 *
michael@0 1431 * @param string style
michael@0 1432 * The style string to cleanup.
michael@0 1433 * @return string
michael@0 1434 * The style value after cleanup.
michael@0 1435 */
michael@0 1436 cleanupStyle: function(style)
michael@0 1437 {
michael@0 1438 for (let r of RE_CLEANUP_STYLES) {
michael@0 1439 style = style.replace(r, "notallowed");
michael@0 1440 }
michael@0 1441
michael@0 1442 let dummy = this.output._dummyElement;
michael@0 1443 if (!dummy) {
michael@0 1444 dummy = this.output._dummyElement =
michael@0 1445 this.document.createElementNS(XHTML_NS, "div");
michael@0 1446 }
michael@0 1447 dummy.style = style;
michael@0 1448
michael@0 1449 let toRemove = [];
michael@0 1450 for (let i = 0; i < dummy.style.length; i++) {
michael@0 1451 let prop = dummy.style[i];
michael@0 1452 if (!RE_ALLOWED_STYLES.test(prop)) {
michael@0 1453 toRemove.push(prop);
michael@0 1454 }
michael@0 1455 }
michael@0 1456
michael@0 1457 for (let prop of toRemove) {
michael@0 1458 dummy.style.removeProperty(prop);
michael@0 1459 }
michael@0 1460
michael@0 1461 style = dummy.style.cssText;
michael@0 1462
michael@0 1463 dummy.style = "";
michael@0 1464
michael@0 1465 return style;
michael@0 1466 },
michael@0 1467 }); // Messages.ConsoleGeneric.prototype
michael@0 1468
michael@0 1469 /**
michael@0 1470 * The ConsoleTrace message is used for console.trace() calls.
michael@0 1471 *
michael@0 1472 * @constructor
michael@0 1473 * @extends Messages.Simple
michael@0 1474 * @param object packet
michael@0 1475 * The Console API call packet received from the server.
michael@0 1476 */
michael@0 1477 Messages.ConsoleTrace = function(packet)
michael@0 1478 {
michael@0 1479 let options = {
michael@0 1480 className: "cm-s-mozilla",
michael@0 1481 timestamp: packet.timeStamp,
michael@0 1482 category: "webdev",
michael@0 1483 severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
michael@0 1484 private: packet.private,
michael@0 1485 filterDuplicates: true,
michael@0 1486 location: {
michael@0 1487 url: packet.filename,
michael@0 1488 line: packet.lineNumber,
michael@0 1489 },
michael@0 1490 };
michael@0 1491
michael@0 1492 this._renderStack = this._renderStack.bind(this);
michael@0 1493 Messages.Simple.call(this, this._renderStack, options);
michael@0 1494
michael@0 1495 this._repeatID.consoleApiLevel = packet.level;
michael@0 1496 this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
michael@0 1497 this._arguments = packet.arguments;
michael@0 1498 };
michael@0 1499
michael@0 1500 Messages.ConsoleTrace.prototype = Heritage.extend(Messages.Simple.prototype,
michael@0 1501 {
michael@0 1502 /**
michael@0 1503 * Holds the stackframes received from the server.
michael@0 1504 *
michael@0 1505 * @private
michael@0 1506 * @type array
michael@0 1507 */
michael@0 1508 _stacktrace: null,
michael@0 1509
michael@0 1510 /**
michael@0 1511 * Holds the arguments the content script passed to the console.trace()
michael@0 1512 * method. This array is cleared when the message is initialized, and
michael@0 1513 * associated actors are released.
michael@0 1514 *
michael@0 1515 * @private
michael@0 1516 * @type array
michael@0 1517 */
michael@0 1518 _arguments: null,
michael@0 1519
michael@0 1520 init: function()
michael@0 1521 {
michael@0 1522 let result = Messages.Simple.prototype.init.apply(this, arguments);
michael@0 1523
michael@0 1524 // We ignore console.trace() arguments. Release object actors.
michael@0 1525 if (Array.isArray(this._arguments)) {
michael@0 1526 for (let arg of this._arguments) {
michael@0 1527 if (WebConsoleUtils.isActorGrip(arg)) {
michael@0 1528 this.output._releaseObject(arg.actor);
michael@0 1529 }
michael@0 1530 }
michael@0 1531 }
michael@0 1532 this._arguments = null;
michael@0 1533
michael@0 1534 return result;
michael@0 1535 },
michael@0 1536
michael@0 1537 render: function()
michael@0 1538 {
michael@0 1539 Messages.Simple.prototype.render.apply(this, arguments);
michael@0 1540 this.element.setAttribute("open", true);
michael@0 1541 return this;
michael@0 1542 },
michael@0 1543
michael@0 1544 /**
michael@0 1545 * Render the stack frames.
michael@0 1546 *
michael@0 1547 * @private
michael@0 1548 * @return DOMElement
michael@0 1549 */
michael@0 1550 _renderStack: function()
michael@0 1551 {
michael@0 1552 let cmvar = this.document.createElementNS(XHTML_NS, "span");
michael@0 1553 cmvar.className = "cm-variable";
michael@0 1554 cmvar.textContent = "console";
michael@0 1555
michael@0 1556 let cmprop = this.document.createElementNS(XHTML_NS, "span");
michael@0 1557 cmprop.className = "cm-property";
michael@0 1558 cmprop.textContent = "trace";
michael@0 1559
michael@0 1560 let title = this.document.createElementNS(XHTML_NS, "span");
michael@0 1561 title.className = "message-body devtools-monospace";
michael@0 1562 title.appendChild(cmvar);
michael@0 1563 title.appendChild(this.document.createTextNode("."));
michael@0 1564 title.appendChild(cmprop);
michael@0 1565 title.appendChild(this.document.createTextNode("():"));
michael@0 1566
michael@0 1567 let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
michael@0 1568 let location = Messages.Simple.prototype._renderLocation.call(this);
michael@0 1569 if (location) {
michael@0 1570 location.target = "jsdebugger";
michael@0 1571 }
michael@0 1572
michael@0 1573 let widget = new Widgets.Stacktrace(this, this._stacktrace).render();
michael@0 1574
michael@0 1575 let body = this.document.createElementNS(XHTML_NS, "span");
michael@0 1576 body.className = "message-flex-body";
michael@0 1577 body.appendChild(title);
michael@0 1578 if (repeatNode) {
michael@0 1579 body.appendChild(repeatNode);
michael@0 1580 }
michael@0 1581 if (location) {
michael@0 1582 body.appendChild(location);
michael@0 1583 }
michael@0 1584 body.appendChild(this.document.createTextNode("\n"));
michael@0 1585
michael@0 1586 let frag = this.document.createDocumentFragment();
michael@0 1587 frag.appendChild(body);
michael@0 1588 frag.appendChild(widget.element);
michael@0 1589
michael@0 1590 return frag;
michael@0 1591 },
michael@0 1592
michael@0 1593 _renderBody: function()
michael@0 1594 {
michael@0 1595 let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
michael@0 1596 body.classList.remove("devtools-monospace", "message-body");
michael@0 1597 return body;
michael@0 1598 },
michael@0 1599
michael@0 1600 // no-op for the message location and .repeats elements.
michael@0 1601 // |this._renderStack| handles customized message output.
michael@0 1602 _renderLocation: function() { },
michael@0 1603 _renderRepeatNode: function() { },
michael@0 1604 }); // Messages.ConsoleTrace.prototype
michael@0 1605
michael@0 1606 let Widgets = {};
michael@0 1607
michael@0 1608 /**
michael@0 1609 * The base widget class.
michael@0 1610 *
michael@0 1611 * @constructor
michael@0 1612 * @param object message
michael@0 1613 * The owning message.
michael@0 1614 */
michael@0 1615 Widgets.BaseWidget = function(message)
michael@0 1616 {
michael@0 1617 this.message = message;
michael@0 1618 };
michael@0 1619
michael@0 1620 Widgets.BaseWidget.prototype = {
michael@0 1621 /**
michael@0 1622 * The owning message object.
michael@0 1623 * @type object
michael@0 1624 */
michael@0 1625 message: null,
michael@0 1626
michael@0 1627 /**
michael@0 1628 * The DOM element of the rendered widget.
michael@0 1629 * @type Element
michael@0 1630 */
michael@0 1631 element: null,
michael@0 1632
michael@0 1633 /**
michael@0 1634 * Getter for the DOM document that holds the output.
michael@0 1635 * @type Document
michael@0 1636 */
michael@0 1637 get document() {
michael@0 1638 return this.message.document;
michael@0 1639 },
michael@0 1640
michael@0 1641 /**
michael@0 1642 * The ConsoleOutput instance that owns this widget instance.
michael@0 1643 */
michael@0 1644 get output() {
michael@0 1645 return this.message.output;
michael@0 1646 },
michael@0 1647
michael@0 1648 /**
michael@0 1649 * Render the widget DOM element.
michael@0 1650 * @return this
michael@0 1651 */
michael@0 1652 render: function() { },
michael@0 1653
michael@0 1654 /**
michael@0 1655 * Destroy this widget instance.
michael@0 1656 */
michael@0 1657 destroy: function() { },
michael@0 1658
michael@0 1659 /**
michael@0 1660 * Helper for creating DOM elements for widgets.
michael@0 1661 *
michael@0 1662 * Usage:
michael@0 1663 * this.el("tag#id.class.names"); // create element "tag" with ID "id" and
michael@0 1664 * two class names, .class and .names.
michael@0 1665 *
michael@0 1666 * this.el("span", { attr1: "value1", ... }) // second argument can be an
michael@0 1667 * object that holds element attributes and values for the new DOM element.
michael@0 1668 *
michael@0 1669 * this.el("p", { attr1: "value1", ... }, "text content"); // the third
michael@0 1670 * argument can include the default .textContent of the new DOM element.
michael@0 1671 *
michael@0 1672 * this.el("p", "text content"); // if the second argument is not an object,
michael@0 1673 * it will be used as .textContent for the new DOM element.
michael@0 1674 *
michael@0 1675 * @param string tagNameIdAndClasses
michael@0 1676 * Tag name for the new element, optionally followed by an ID and/or
michael@0 1677 * class names. Examples: "span", "div#fooId", "div.class.names",
michael@0 1678 * "p#id.class".
michael@0 1679 * @param string|object [attributesOrTextContent]
michael@0 1680 * If this argument is an object it will be used to set the attributes
michael@0 1681 * of the new DOM element. Otherwise, the value becomes the
michael@0 1682 * .textContent of the new DOM element.
michael@0 1683 * @param string [textContent]
michael@0 1684 * If this argument is provided the value is used as the textContent of
michael@0 1685 * the new DOM element.
michael@0 1686 * @return DOMElement
michael@0 1687 * The new DOM element.
michael@0 1688 */
michael@0 1689 el: function(tagNameIdAndClasses)
michael@0 1690 {
michael@0 1691 let attrs, text;
michael@0 1692 if (typeof arguments[1] == "object") {
michael@0 1693 attrs = arguments[1];
michael@0 1694 text = arguments[2];
michael@0 1695 } else {
michael@0 1696 text = arguments[1];
michael@0 1697 }
michael@0 1698
michael@0 1699 let tagName = tagNameIdAndClasses.split(/#|\./)[0];
michael@0 1700
michael@0 1701 let elem = this.document.createElementNS(XHTML_NS, tagName);
michael@0 1702 for (let name of Object.keys(attrs || {})) {
michael@0 1703 elem.setAttribute(name, attrs[name]);
michael@0 1704 }
michael@0 1705 if (text !== undefined && text !== null) {
michael@0 1706 elem.textContent = text;
michael@0 1707 }
michael@0 1708
michael@0 1709 let idAndClasses = tagNameIdAndClasses.match(/([#.][^#.]+)/g);
michael@0 1710 for (let idOrClass of (idAndClasses || [])) {
michael@0 1711 if (idOrClass.charAt(0) == "#") {
michael@0 1712 elem.id = idOrClass.substr(1);
michael@0 1713 } else {
michael@0 1714 elem.classList.add(idOrClass.substr(1));
michael@0 1715 }
michael@0 1716 }
michael@0 1717
michael@0 1718 return elem;
michael@0 1719 },
michael@0 1720 };
michael@0 1721
michael@0 1722 /**
michael@0 1723 * The timestamp widget.
michael@0 1724 *
michael@0 1725 * @constructor
michael@0 1726 * @param object message
michael@0 1727 * The owning message.
michael@0 1728 * @param number timestamp
michael@0 1729 * The UNIX timestamp to display.
michael@0 1730 */
michael@0 1731 Widgets.MessageTimestamp = function(message, timestamp)
michael@0 1732 {
michael@0 1733 Widgets.BaseWidget.call(this, message);
michael@0 1734 this.timestamp = timestamp;
michael@0 1735 };
michael@0 1736
michael@0 1737 Widgets.MessageTimestamp.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
michael@0 1738 {
michael@0 1739 /**
michael@0 1740 * The UNIX timestamp.
michael@0 1741 * @type number
michael@0 1742 */
michael@0 1743 timestamp: 0,
michael@0 1744
michael@0 1745 render: function()
michael@0 1746 {
michael@0 1747 if (this.element) {
michael@0 1748 return this;
michael@0 1749 }
michael@0 1750
michael@0 1751 this.element = this.document.createElementNS(XHTML_NS, "span");
michael@0 1752 this.element.className = "timestamp devtools-monospace";
michael@0 1753 this.element.textContent = l10n.timestampString(this.timestamp) + " ";
michael@0 1754
michael@0 1755 return this;
michael@0 1756 },
michael@0 1757 }); // Widgets.MessageTimestamp.prototype
michael@0 1758
michael@0 1759
michael@0 1760 /**
michael@0 1761 * Widget used for displaying ObjectActors that have no specialised renderers.
michael@0 1762 *
michael@0 1763 * @constructor
michael@0 1764 * @param object message
michael@0 1765 * The owning message.
michael@0 1766 * @param object objectActor
michael@0 1767 * The ObjectActor to display.
michael@0 1768 * @param object [options]
michael@0 1769 * Options for displaying the given ObjectActor. See
michael@0 1770 * Messages.Extended.prototype._renderValueGrip for the available
michael@0 1771 * options.
michael@0 1772 */
michael@0 1773 Widgets.JSObject = function(message, objectActor, options = {})
michael@0 1774 {
michael@0 1775 Widgets.BaseWidget.call(this, message);
michael@0 1776 this.objectActor = objectActor;
michael@0 1777 this.options = options;
michael@0 1778 this._onClick = this._onClick.bind(this);
michael@0 1779 };
michael@0 1780
michael@0 1781 Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
michael@0 1782 {
michael@0 1783 /**
michael@0 1784 * The ObjectActor displayed by the widget.
michael@0 1785 * @type object
michael@0 1786 */
michael@0 1787 objectActor: null,
michael@0 1788
michael@0 1789 render: function()
michael@0 1790 {
michael@0 1791 if (!this.element) {
michael@0 1792 this._render();
michael@0 1793 }
michael@0 1794
michael@0 1795 return this;
michael@0 1796 },
michael@0 1797
michael@0 1798 _render: function()
michael@0 1799 {
michael@0 1800 let str = VariablesView.getString(this.objectActor, this.options);
michael@0 1801 let className = this.message.getClassNameForValueGrip(this.objectActor);
michael@0 1802 if (!className && this.objectActor.class == "Object") {
michael@0 1803 className = "cm-variable";
michael@0 1804 }
michael@0 1805
michael@0 1806 this.element = this._anchor(str, { className: className });
michael@0 1807 },
michael@0 1808
michael@0 1809 /**
michael@0 1810 * Render an anchor with a given text content and link.
michael@0 1811 *
michael@0 1812 * @private
michael@0 1813 * @param string text
michael@0 1814 * Text to show in the anchor.
michael@0 1815 * @param object [options]
michael@0 1816 * Available options:
michael@0 1817 * - onClick (function): "click" event handler.By default a click on
michael@0 1818 * the anchor opens the variables view for the current object actor
michael@0 1819 * (this.objectActor).
michael@0 1820 * - href (string): if given the string is used as a link, and clicks
michael@0 1821 * on the anchor open the link in a new tab.
michael@0 1822 * - appendTo (DOMElement): append the element to the given DOM
michael@0 1823 * element. If not provided, the anchor is appended to |this.element|
michael@0 1824 * if it is available. If |appendTo| is provided and if it is a falsy
michael@0 1825 * value, the anchor is not appended to any element.
michael@0 1826 * @return DOMElement
michael@0 1827 * The DOM element of the new anchor.
michael@0 1828 */
michael@0 1829 _anchor: function(text, options = {})
michael@0 1830 {
michael@0 1831 if (!options.onClick && !options.href) {
michael@0 1832 options.onClick = this._onClick;
michael@0 1833 }
michael@0 1834
michael@0 1835 let anchor = this.el("a", {
michael@0 1836 class: options.className,
michael@0 1837 draggable: false,
michael@0 1838 href: options.href || "#",
michael@0 1839 }, text);
michael@0 1840
michael@0 1841 this.message._addLinkCallback(anchor, !options.href ? options.onClick : null);
michael@0 1842
michael@0 1843 if (options.appendTo) {
michael@0 1844 options.appendTo.appendChild(anchor);
michael@0 1845 } else if (!("appendTo" in options) && this.element) {
michael@0 1846 this.element.appendChild(anchor);
michael@0 1847 }
michael@0 1848
michael@0 1849 return anchor;
michael@0 1850 },
michael@0 1851
michael@0 1852 /**
michael@0 1853 * The click event handler for objects shown inline.
michael@0 1854 * @private
michael@0 1855 */
michael@0 1856 _onClick: function()
michael@0 1857 {
michael@0 1858 this.output.openVariablesView({
michael@0 1859 label: VariablesView.getString(this.objectActor, { concise: true }),
michael@0 1860 objectActor: this.objectActor,
michael@0 1861 autofocus: true,
michael@0 1862 });
michael@0 1863 },
michael@0 1864
michael@0 1865 /**
michael@0 1866 * Add a string to the message.
michael@0 1867 *
michael@0 1868 * @private
michael@0 1869 * @param string str
michael@0 1870 * String to add.
michael@0 1871 * @param DOMElement [target = this.element]
michael@0 1872 * Optional DOM element to append the string to. The default is
michael@0 1873 * this.element.
michael@0 1874 */
michael@0 1875 _text: function(str, target = this.element)
michael@0 1876 {
michael@0 1877 target.appendChild(this.document.createTextNode(str));
michael@0 1878 },
michael@0 1879 }); // Widgets.JSObject.prototype
michael@0 1880
michael@0 1881 Widgets.ObjectRenderers = {};
michael@0 1882 Widgets.ObjectRenderers.byKind = {};
michael@0 1883 Widgets.ObjectRenderers.byClass = {};
michael@0 1884
michael@0 1885 /**
michael@0 1886 * Add an object renderer.
michael@0 1887 *
michael@0 1888 * @param object obj
michael@0 1889 * An object that represents the renderer. Properties:
michael@0 1890 * - byClass (string, optional): this renderer will be used for the given
michael@0 1891 * object class.
michael@0 1892 * - byKind (string, optional): this renderer will be used for the given
michael@0 1893 * object kind.
michael@0 1894 * One of byClass or byKind must be provided.
michael@0 1895 * - extends (object, optional): the renderer object extends the given
michael@0 1896 * object. Default: Widgets.JSObject.
michael@0 1897 * - canRender (function, optional): this method is invoked when
michael@0 1898 * a candidate object needs to be displayed. The method is invoked as
michael@0 1899 * a static method, as such, none of the properties of the renderer
michael@0 1900 * object will be available. You get one argument: the object actor grip
michael@0 1901 * received from the server. If the method returns true, then this
michael@0 1902 * renderer is used for displaying the object, otherwise not.
michael@0 1903 * - initialize (function, optional): the constructor of the renderer
michael@0 1904 * widget. This function is invoked with the following arguments: the
michael@0 1905 * owner message object instance, the object actor grip to display, and
michael@0 1906 * an options object. See Messages.Extended.prototype._renderValueGrip()
michael@0 1907 * for details about the options object.
michael@0 1908 * - render (function, required): the method that displays the given
michael@0 1909 * object actor.
michael@0 1910 */
michael@0 1911 Widgets.ObjectRenderers.add = function(obj)
michael@0 1912 {
michael@0 1913 let extendObj = obj.extends || Widgets.JSObject;
michael@0 1914
michael@0 1915 let constructor = function() {
michael@0 1916 if (obj.initialize) {
michael@0 1917 obj.initialize.apply(this, arguments);
michael@0 1918 } else {
michael@0 1919 extendObj.apply(this, arguments);
michael@0 1920 }
michael@0 1921 };
michael@0 1922
michael@0 1923 let proto = WebConsoleUtils.cloneObject(obj, false, function(key) {
michael@0 1924 if (key == "initialize" || key == "canRender" ||
michael@0 1925 (key == "render" && extendObj === Widgets.JSObject)) {
michael@0 1926 return false;
michael@0 1927 }
michael@0 1928 return true;
michael@0 1929 });
michael@0 1930
michael@0 1931 if (extendObj === Widgets.JSObject) {
michael@0 1932 proto._render = obj.render;
michael@0 1933 }
michael@0 1934
michael@0 1935 constructor.canRender = obj.canRender;
michael@0 1936 constructor.prototype = Heritage.extend(extendObj.prototype, proto);
michael@0 1937
michael@0 1938 if (obj.byClass) {
michael@0 1939 Widgets.ObjectRenderers.byClass[obj.byClass] = constructor;
michael@0 1940 } else if (obj.byKind) {
michael@0 1941 Widgets.ObjectRenderers.byKind[obj.byKind] = constructor;
michael@0 1942 } else {
michael@0 1943 throw new Error("You are adding an object renderer without any byClass or " +
michael@0 1944 "byKind property.");
michael@0 1945 }
michael@0 1946 };
michael@0 1947
michael@0 1948
michael@0 1949 /**
michael@0 1950 * The widget used for displaying Date objects.
michael@0 1951 */
michael@0 1952 Widgets.ObjectRenderers.add({
michael@0 1953 byClass: "Date",
michael@0 1954
michael@0 1955 render: function()
michael@0 1956 {
michael@0 1957 let {preview} = this.objectActor;
michael@0 1958 this.element = this.el("span.class-" + this.objectActor.class);
michael@0 1959
michael@0 1960 let anchorText = this.objectActor.class;
michael@0 1961 let anchorClass = "cm-variable";
michael@0 1962 if ("timestamp" in preview && typeof preview.timestamp != "number") {
michael@0 1963 anchorText = new Date(preview.timestamp).toString(); // invalid date
michael@0 1964 anchorClass = "";
michael@0 1965 }
michael@0 1966
michael@0 1967 this._anchor(anchorText, { className: anchorClass });
michael@0 1968
michael@0 1969 if (!("timestamp" in preview) || typeof preview.timestamp != "number") {
michael@0 1970 return;
michael@0 1971 }
michael@0 1972
michael@0 1973 this._text(" ");
michael@0 1974
michael@0 1975 let elem = this.el("span.cm-string-2", new Date(preview.timestamp).toISOString());
michael@0 1976 this.element.appendChild(elem);
michael@0 1977 },
michael@0 1978 });
michael@0 1979
michael@0 1980 /**
michael@0 1981 * The widget used for displaying Function objects.
michael@0 1982 */
michael@0 1983 Widgets.ObjectRenderers.add({
michael@0 1984 byClass: "Function",
michael@0 1985
michael@0 1986 render: function()
michael@0 1987 {
michael@0 1988 let grip = this.objectActor;
michael@0 1989 this.element = this.el("span.class-" + this.objectActor.class);
michael@0 1990
michael@0 1991 // TODO: Bug 948484 - support arrow functions and ES6 generators
michael@0 1992 let name = grip.userDisplayName || grip.displayName || grip.name || "";
michael@0 1993 name = VariablesView.getString(name, { noStringQuotes: true });
michael@0 1994
michael@0 1995 let str = this.options.concise ? name || "function " : "function " + name;
michael@0 1996
michael@0 1997 if (this.options.concise) {
michael@0 1998 this._anchor(name || "function", {
michael@0 1999 className: name ? "cm-variable" : "cm-keyword",
michael@0 2000 });
michael@0 2001 if (!name) {
michael@0 2002 this._text(" ");
michael@0 2003 }
michael@0 2004 } else if (name) {
michael@0 2005 this.element.appendChild(this.el("span.cm-keyword", "function"));
michael@0 2006 this._text(" ");
michael@0 2007 this._anchor(name, { className: "cm-variable" });
michael@0 2008 } else {
michael@0 2009 this._anchor("function", { className: "cm-keyword" });
michael@0 2010 this._text(" ");
michael@0 2011 }
michael@0 2012
michael@0 2013 this._text("(");
michael@0 2014
michael@0 2015 // TODO: Bug 948489 - Support functions with destructured parameters and
michael@0 2016 // rest parameters
michael@0 2017 let params = grip.parameterNames || [];
michael@0 2018 let shown = 0;
michael@0 2019 for (let param of params) {
michael@0 2020 if (shown > 0) {
michael@0 2021 this._text(", ");
michael@0 2022 }
michael@0 2023 this.element.appendChild(this.el("span.cm-def", param));
michael@0 2024 shown++;
michael@0 2025 }
michael@0 2026
michael@0 2027 this._text(")");
michael@0 2028 },
michael@0 2029 }); // Widgets.ObjectRenderers.byClass.Function
michael@0 2030
michael@0 2031 /**
michael@0 2032 * The widget used for displaying ArrayLike objects.
michael@0 2033 */
michael@0 2034 Widgets.ObjectRenderers.add({
michael@0 2035 byKind: "ArrayLike",
michael@0 2036
michael@0 2037 render: function()
michael@0 2038 {
michael@0 2039 let {preview} = this.objectActor;
michael@0 2040 let {items} = preview;
michael@0 2041 this.element = this.el("span.kind-" + preview.kind);
michael@0 2042
michael@0 2043 this._anchor(this.objectActor.class, { className: "cm-variable" });
michael@0 2044
michael@0 2045 if (!items || this.options.concise) {
michael@0 2046 this._text("[");
michael@0 2047 this.element.appendChild(this.el("span.cm-number", preview.length));
michael@0 2048 this._text("]");
michael@0 2049 return this;
michael@0 2050 }
michael@0 2051
michael@0 2052 this._text(" [ ");
michael@0 2053
michael@0 2054 let shown = 0;
michael@0 2055 for (let item of items) {
michael@0 2056 if (shown > 0) {
michael@0 2057 this._text(", ");
michael@0 2058 }
michael@0 2059
michael@0 2060 if (item !== null) {
michael@0 2061 let elem = this.message._renderValueGrip(item, { concise: true });
michael@0 2062 this.element.appendChild(elem);
michael@0 2063 } else if (shown == (items.length - 1)) {
michael@0 2064 this._text(", ");
michael@0 2065 }
michael@0 2066
michael@0 2067 shown++;
michael@0 2068 }
michael@0 2069
michael@0 2070 if (shown < preview.length) {
michael@0 2071 this._text(", ");
michael@0 2072
michael@0 2073 let n = preview.length - shown;
michael@0 2074 let str = VariablesView.stringifiers._getNMoreString(n);
michael@0 2075 this._anchor(str);
michael@0 2076 }
michael@0 2077
michael@0 2078 this._text(" ]");
michael@0 2079 },
michael@0 2080 }); // Widgets.ObjectRenderers.byKind.ArrayLike
michael@0 2081
michael@0 2082 /**
michael@0 2083 * The widget used for displaying MapLike objects.
michael@0 2084 */
michael@0 2085 Widgets.ObjectRenderers.add({
michael@0 2086 byKind: "MapLike",
michael@0 2087
michael@0 2088 render: function()
michael@0 2089 {
michael@0 2090 let {preview} = this.objectActor;
michael@0 2091 let {entries} = preview;
michael@0 2092
michael@0 2093 let container = this.element = this.el("span.kind-" + preview.kind);
michael@0 2094 this._anchor(this.objectActor.class, { className: "cm-variable" });
michael@0 2095
michael@0 2096 if (!entries || this.options.concise) {
michael@0 2097 if (typeof preview.size == "number") {
michael@0 2098 this._text("[");
michael@0 2099 container.appendChild(this.el("span.cm-number", preview.size));
michael@0 2100 this._text("]");
michael@0 2101 }
michael@0 2102 return;
michael@0 2103 }
michael@0 2104
michael@0 2105 this._text(" { ");
michael@0 2106
michael@0 2107 let shown = 0;
michael@0 2108 for (let [key, value] of entries) {
michael@0 2109 if (shown > 0) {
michael@0 2110 this._text(", ");
michael@0 2111 }
michael@0 2112
michael@0 2113 let keyElem = this.message._renderValueGrip(key, {
michael@0 2114 concise: true,
michael@0 2115 noStringQuotes: true,
michael@0 2116 });
michael@0 2117
michael@0 2118 // Strings are property names.
michael@0 2119 if (keyElem.classList && keyElem.classList.contains("console-string")) {
michael@0 2120 keyElem.classList.remove("console-string");
michael@0 2121 keyElem.classList.add("cm-property");
michael@0 2122 }
michael@0 2123
michael@0 2124 container.appendChild(keyElem);
michael@0 2125
michael@0 2126 this._text(": ");
michael@0 2127
michael@0 2128 let valueElem = this.message._renderValueGrip(value, { concise: true });
michael@0 2129 container.appendChild(valueElem);
michael@0 2130
michael@0 2131 shown++;
michael@0 2132 }
michael@0 2133
michael@0 2134 if (typeof preview.size == "number" && shown < preview.size) {
michael@0 2135 this._text(", ");
michael@0 2136
michael@0 2137 let n = preview.size - shown;
michael@0 2138 let str = VariablesView.stringifiers._getNMoreString(n);
michael@0 2139 this._anchor(str);
michael@0 2140 }
michael@0 2141
michael@0 2142 this._text(" }");
michael@0 2143 },
michael@0 2144 }); // Widgets.ObjectRenderers.byKind.MapLike
michael@0 2145
michael@0 2146 /**
michael@0 2147 * The widget used for displaying objects with a URL.
michael@0 2148 */
michael@0 2149 Widgets.ObjectRenderers.add({
michael@0 2150 byKind: "ObjectWithURL",
michael@0 2151
michael@0 2152 render: function()
michael@0 2153 {
michael@0 2154 this.element = this._renderElement(this.objectActor,
michael@0 2155 this.objectActor.preview.url);
michael@0 2156 },
michael@0 2157
michael@0 2158 _renderElement: function(objectActor, url)
michael@0 2159 {
michael@0 2160 let container = this.el("span.kind-" + objectActor.preview.kind);
michael@0 2161
michael@0 2162 this._anchor(objectActor.class, {
michael@0 2163 className: "cm-variable",
michael@0 2164 appendTo: container,
michael@0 2165 });
michael@0 2166
michael@0 2167 if (!VariablesView.isFalsy({ value: url })) {
michael@0 2168 this._text(" \u2192 ", container);
michael@0 2169 let shortUrl = WebConsoleUtils.abbreviateSourceURL(url, {
michael@0 2170 onlyCropQuery: !this.options.concise
michael@0 2171 });
michael@0 2172 this._anchor(shortUrl, { href: url, appendTo: container });
michael@0 2173 }
michael@0 2174
michael@0 2175 return container;
michael@0 2176 },
michael@0 2177 }); // Widgets.ObjectRenderers.byKind.ObjectWithURL
michael@0 2178
michael@0 2179 /**
michael@0 2180 * The widget used for displaying objects with a string next to them.
michael@0 2181 */
michael@0 2182 Widgets.ObjectRenderers.add({
michael@0 2183 byKind: "ObjectWithText",
michael@0 2184
michael@0 2185 render: function()
michael@0 2186 {
michael@0 2187 let {preview} = this.objectActor;
michael@0 2188 this.element = this.el("span.kind-" + preview.kind);
michael@0 2189
michael@0 2190 this._anchor(this.objectActor.class, { className: "cm-variable" });
michael@0 2191
michael@0 2192 if (!this.options.concise) {
michael@0 2193 this._text(" ");
michael@0 2194 this.element.appendChild(this.el("span.console-string",
michael@0 2195 VariablesView.getString(preview.text)));
michael@0 2196 }
michael@0 2197 },
michael@0 2198 });
michael@0 2199
michael@0 2200 /**
michael@0 2201 * The widget used for displaying DOM event previews.
michael@0 2202 */
michael@0 2203 Widgets.ObjectRenderers.add({
michael@0 2204 byKind: "DOMEvent",
michael@0 2205
michael@0 2206 render: function()
michael@0 2207 {
michael@0 2208 let {preview} = this.objectActor;
michael@0 2209
michael@0 2210 let container = this.element = this.el("span.kind-" + preview.kind);
michael@0 2211
michael@0 2212 this._anchor(preview.type || this.objectActor.class,
michael@0 2213 { className: "cm-variable" });
michael@0 2214
michael@0 2215 if (this.options.concise) {
michael@0 2216 return;
michael@0 2217 }
michael@0 2218
michael@0 2219 if (preview.eventKind == "key" && preview.modifiers &&
michael@0 2220 preview.modifiers.length) {
michael@0 2221 this._text(" ");
michael@0 2222
michael@0 2223 let mods = 0;
michael@0 2224 for (let mod of preview.modifiers) {
michael@0 2225 if (mods > 0) {
michael@0 2226 this._text("-");
michael@0 2227 }
michael@0 2228 container.appendChild(this.el("span.cm-keyword", mod));
michael@0 2229 mods++;
michael@0 2230 }
michael@0 2231 }
michael@0 2232
michael@0 2233 this._text(" { ");
michael@0 2234
michael@0 2235 let shown = 0;
michael@0 2236 if (preview.target) {
michael@0 2237 container.appendChild(this.el("span.cm-property", "target"));
michael@0 2238 this._text(": ");
michael@0 2239 let target = this.message._renderValueGrip(preview.target, { concise: true });
michael@0 2240 container.appendChild(target);
michael@0 2241 shown++;
michael@0 2242 }
michael@0 2243
michael@0 2244 for (let key of Object.keys(preview.properties || {})) {
michael@0 2245 if (shown > 0) {
michael@0 2246 this._text(", ");
michael@0 2247 }
michael@0 2248
michael@0 2249 container.appendChild(this.el("span.cm-property", key));
michael@0 2250 this._text(": ");
michael@0 2251
michael@0 2252 let value = preview.properties[key];
michael@0 2253 let valueElem = this.message._renderValueGrip(value, { concise: true });
michael@0 2254 container.appendChild(valueElem);
michael@0 2255
michael@0 2256 shown++;
michael@0 2257 }
michael@0 2258
michael@0 2259 this._text(" }");
michael@0 2260 },
michael@0 2261 }); // Widgets.ObjectRenderers.byKind.DOMEvent
michael@0 2262
michael@0 2263 /**
michael@0 2264 * The widget used for displaying DOM node previews.
michael@0 2265 */
michael@0 2266 Widgets.ObjectRenderers.add({
michael@0 2267 byKind: "DOMNode",
michael@0 2268
michael@0 2269 canRender: function(objectActor) {
michael@0 2270 let {preview} = objectActor;
michael@0 2271 if (!preview) {
michael@0 2272 return false;
michael@0 2273 }
michael@0 2274
michael@0 2275 switch (preview.nodeType) {
michael@0 2276 case Ci.nsIDOMNode.DOCUMENT_NODE:
michael@0 2277 case Ci.nsIDOMNode.ATTRIBUTE_NODE:
michael@0 2278 case Ci.nsIDOMNode.TEXT_NODE:
michael@0 2279 case Ci.nsIDOMNode.COMMENT_NODE:
michael@0 2280 case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
michael@0 2281 case Ci.nsIDOMNode.ELEMENT_NODE:
michael@0 2282 return true;
michael@0 2283 default:
michael@0 2284 return false;
michael@0 2285 }
michael@0 2286 },
michael@0 2287
michael@0 2288 render: function()
michael@0 2289 {
michael@0 2290 switch (this.objectActor.preview.nodeType) {
michael@0 2291 case Ci.nsIDOMNode.DOCUMENT_NODE:
michael@0 2292 this._renderDocumentNode();
michael@0 2293 break;
michael@0 2294 case Ci.nsIDOMNode.ATTRIBUTE_NODE: {
michael@0 2295 let {preview} = this.objectActor;
michael@0 2296 this.element = this.el("span.attributeNode.kind-" + preview.kind);
michael@0 2297 let attr = this._renderAttributeNode(preview.nodeName, preview.value, true);
michael@0 2298 this.element.appendChild(attr);
michael@0 2299 break;
michael@0 2300 }
michael@0 2301 case Ci.nsIDOMNode.TEXT_NODE:
michael@0 2302 this._renderTextNode();
michael@0 2303 break;
michael@0 2304 case Ci.nsIDOMNode.COMMENT_NODE:
michael@0 2305 this._renderCommentNode();
michael@0 2306 break;
michael@0 2307 case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
michael@0 2308 this._renderDocumentFragmentNode();
michael@0 2309 break;
michael@0 2310 case Ci.nsIDOMNode.ELEMENT_NODE:
michael@0 2311 this._renderElementNode();
michael@0 2312 break;
michael@0 2313 default:
michael@0 2314 throw new Error("Unsupported nodeType: " + preview.nodeType);
michael@0 2315 }
michael@0 2316 },
michael@0 2317
michael@0 2318 _renderDocumentNode: function()
michael@0 2319 {
michael@0 2320 let fn = Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement;
michael@0 2321 this.element = fn.call(this, this.objectActor,
michael@0 2322 this.objectActor.preview.location);
michael@0 2323 this.element.classList.add("documentNode");
michael@0 2324 },
michael@0 2325
michael@0 2326 _renderAttributeNode: function(nodeName, nodeValue, addLink)
michael@0 2327 {
michael@0 2328 let value = VariablesView.getString(nodeValue, { noStringQuotes: true });
michael@0 2329
michael@0 2330 let fragment = this.document.createDocumentFragment();
michael@0 2331 if (addLink) {
michael@0 2332 this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment });
michael@0 2333 } else {
michael@0 2334 fragment.appendChild(this.el("span.cm-attribute", nodeName));
michael@0 2335 }
michael@0 2336
michael@0 2337 this._text("=", fragment);
michael@0 2338 fragment.appendChild(this.el("span.console-string",
michael@0 2339 '"' + escapeHTML(value) + '"'));
michael@0 2340
michael@0 2341 return fragment;
michael@0 2342 },
michael@0 2343
michael@0 2344 _renderTextNode: function()
michael@0 2345 {
michael@0 2346 let {preview} = this.objectActor;
michael@0 2347 this.element = this.el("span.textNode.kind-" + preview.kind);
michael@0 2348
michael@0 2349 this._anchor(preview.nodeName, { className: "cm-variable" });
michael@0 2350 this._text(" ");
michael@0 2351
michael@0 2352 let text = VariablesView.getString(preview.textContent);
michael@0 2353 this.element.appendChild(this.el("span.console-string", text));
michael@0 2354 },
michael@0 2355
michael@0 2356 _renderCommentNode: function()
michael@0 2357 {
michael@0 2358 let {preview} = this.objectActor;
michael@0 2359 let comment = "<!-- " + VariablesView.getString(preview.textContent, {
michael@0 2360 noStringQuotes: true,
michael@0 2361 }) + " -->";
michael@0 2362
michael@0 2363 this.element = this._anchor(comment, {
michael@0 2364 className: "kind-" + preview.kind + " commentNode cm-comment",
michael@0 2365 });
michael@0 2366 },
michael@0 2367
michael@0 2368 _renderDocumentFragmentNode: function()
michael@0 2369 {
michael@0 2370 let {preview} = this.objectActor;
michael@0 2371 let {childNodes} = preview;
michael@0 2372 let container = this.element = this.el("span.documentFragmentNode.kind-" +
michael@0 2373 preview.kind);
michael@0 2374
michael@0 2375 this._anchor(this.objectActor.class, { className: "cm-variable" });
michael@0 2376
michael@0 2377 if (!childNodes || this.options.concise) {
michael@0 2378 this._text("[");
michael@0 2379 container.appendChild(this.el("span.cm-number", preview.childNodesLength));
michael@0 2380 this._text("]");
michael@0 2381 return;
michael@0 2382 }
michael@0 2383
michael@0 2384 this._text(" [ ");
michael@0 2385
michael@0 2386 let shown = 0;
michael@0 2387 for (let item of childNodes) {
michael@0 2388 if (shown > 0) {
michael@0 2389 this._text(", ");
michael@0 2390 }
michael@0 2391
michael@0 2392 let elem = this.message._renderValueGrip(item, { concise: true });
michael@0 2393 container.appendChild(elem);
michael@0 2394 shown++;
michael@0 2395 }
michael@0 2396
michael@0 2397 if (shown < preview.childNodesLength) {
michael@0 2398 this._text(", ");
michael@0 2399
michael@0 2400 let n = preview.childNodesLength - shown;
michael@0 2401 let str = VariablesView.stringifiers._getNMoreString(n);
michael@0 2402 this._anchor(str);
michael@0 2403 }
michael@0 2404
michael@0 2405 this._text(" ]");
michael@0 2406 },
michael@0 2407
michael@0 2408 _renderElementNode: function()
michael@0 2409 {
michael@0 2410 let doc = this.document;
michael@0 2411 let {attributes, nodeName} = this.objectActor.preview;
michael@0 2412
michael@0 2413 this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode");
michael@0 2414
michael@0 2415 let openTag = this.el("span.cm-tag");
michael@0 2416 openTag.textContent = "<";
michael@0 2417 this.element.appendChild(openTag);
michael@0 2418
michael@0 2419 let tagName = this._anchor(nodeName, {
michael@0 2420 className: "cm-tag",
michael@0 2421 appendTo: openTag
michael@0 2422 });
michael@0 2423
michael@0 2424 if (this.options.concise) {
michael@0 2425 if (attributes.id) {
michael@0 2426 tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id));
michael@0 2427 }
michael@0 2428 if (attributes.class) {
michael@0 2429 tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(/\s+/g).join(".")));
michael@0 2430 }
michael@0 2431 } else {
michael@0 2432 for (let name of Object.keys(attributes)) {
michael@0 2433 let attr = this._renderAttributeNode(" " + name, attributes[name]);
michael@0 2434 this.element.appendChild(attr);
michael@0 2435 }
michael@0 2436 }
michael@0 2437
michael@0 2438 let closeTag = this.el("span.cm-tag");
michael@0 2439 closeTag.textContent = ">";
michael@0 2440 this.element.appendChild(closeTag);
michael@0 2441
michael@0 2442 // Register this widget in the owner message so that it gets destroyed when
michael@0 2443 // the message is destroyed.
michael@0 2444 this.message.widgets.add(this);
michael@0 2445
michael@0 2446 this.linkToInspector();
michael@0 2447 },
michael@0 2448
michael@0 2449 /**
michael@0 2450 * If the DOMNode being rendered can be highlit in the page, this function
michael@0 2451 * will attach mouseover/out event listeners to do so, and the inspector icon
michael@0 2452 * to open the node in the inspector.
michael@0 2453 * @return a promise (always the same) that resolves when the node has been
michael@0 2454 * linked to the inspector, or rejects if it wasn't (either if no toolbox
michael@0 2455 * could be found to access the inspector, or if the node isn't present in the
michael@0 2456 * inspector, i.e. if the node is in a DocumentFragment or not part of the
michael@0 2457 * tree, or not of type Ci.nsIDOMNode.ELEMENT_NODE).
michael@0 2458 */
michael@0 2459 linkToInspector: function()
michael@0 2460 {
michael@0 2461 if (this._linkedToInspector) {
michael@0 2462 return this._linkedToInspector;
michael@0 2463 }
michael@0 2464
michael@0 2465 this._linkedToInspector = Task.spawn(function*() {
michael@0 2466 // Checking the node type
michael@0 2467 if (this.objectActor.preview.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
michael@0 2468 throw null;
michael@0 2469 }
michael@0 2470
michael@0 2471 // Checking the presence of a toolbox
michael@0 2472 let target = this.message.output.toolboxTarget;
michael@0 2473 this.toolbox = gDevTools.getToolbox(target);
michael@0 2474 if (!this.toolbox) {
michael@0 2475 throw null;
michael@0 2476 }
michael@0 2477
michael@0 2478 // Checking that the inspector supports the node
michael@0 2479 yield this.toolbox.initInspector();
michael@0 2480 this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor);
michael@0 2481 if (!this._nodeFront) {
michael@0 2482 throw null;
michael@0 2483 }
michael@0 2484
michael@0 2485 // At this stage, the message may have been cleared already
michael@0 2486 if (!this.document) {
michael@0 2487 throw null;
michael@0 2488 }
michael@0 2489
michael@0 2490 this.highlightDomNode = this.highlightDomNode.bind(this);
michael@0 2491 this.element.addEventListener("mouseover", this.highlightDomNode, false);
michael@0 2492 this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
michael@0 2493 this.element.addEventListener("mouseout", this.unhighlightDomNode, false);
michael@0 2494
michael@0 2495 this._openInspectorNode = this._anchor("", {
michael@0 2496 className: "open-inspector",
michael@0 2497 onClick: this.openNodeInInspector.bind(this)
michael@0 2498 });
michael@0 2499 this._openInspectorNode.title = l10n.getStr("openNodeInInspector");
michael@0 2500 }.bind(this));
michael@0 2501
michael@0 2502 return this._linkedToInspector;
michael@0 2503 },
michael@0 2504
michael@0 2505 /**
michael@0 2506 * Highlight the DOMNode corresponding to the ObjectActor in the page.
michael@0 2507 * @return a promise that resolves when the node has been highlighted, or
michael@0 2508 * rejects if the node cannot be highlighted (detached from the DOM)
michael@0 2509 */
michael@0 2510 highlightDomNode: function()
michael@0 2511 {
michael@0 2512 return Task.spawn(function*() {
michael@0 2513 yield this.linkToInspector();
michael@0 2514 let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
michael@0 2515 if (isAttached) {
michael@0 2516 yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
michael@0 2517 } else {
michael@0 2518 throw null;
michael@0 2519 }
michael@0 2520 }.bind(this));
michael@0 2521 },
michael@0 2522
michael@0 2523 /**
michael@0 2524 * Unhighlight a previously highlit node
michael@0 2525 * @see highlightDomNode
michael@0 2526 * @return a promise that resolves when the highlighter has been hidden
michael@0 2527 */
michael@0 2528 unhighlightDomNode: function()
michael@0 2529 {
michael@0 2530 return this.linkToInspector().then(() => {
michael@0 2531 return this.toolbox.highlighterUtils.unhighlight();
michael@0 2532 });
michael@0 2533 },
michael@0 2534
michael@0 2535 /**
michael@0 2536 * Open the DOMNode corresponding to the ObjectActor in the inspector panel
michael@0 2537 * @return a promise that resolves when the inspector has been switched to
michael@0 2538 * and the node has been selected, or rejects if the node cannot be selected
michael@0 2539 * (detached from the DOM). Note that in any case, the inspector panel will
michael@0 2540 * be switched to.
michael@0 2541 */
michael@0 2542 openNodeInInspector: function()
michael@0 2543 {
michael@0 2544 return Task.spawn(function*() {
michael@0 2545 yield this.linkToInspector();
michael@0 2546 yield this.toolbox.selectTool("inspector");
michael@0 2547
michael@0 2548 let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
michael@0 2549 if (isAttached) {
michael@0 2550 let onReady = this.toolbox.inspector.once("inspector-updated");
michael@0 2551 yield this.toolbox.selection.setNodeFront(this._nodeFront, "console");
michael@0 2552 yield onReady;
michael@0 2553 } else {
michael@0 2554 throw null;
michael@0 2555 }
michael@0 2556 }.bind(this));
michael@0 2557 },
michael@0 2558
michael@0 2559 destroy: function()
michael@0 2560 {
michael@0 2561 if (this.toolbox && this._nodeFront) {
michael@0 2562 this.element.removeEventListener("mouseover", this.highlightDomNode, false);
michael@0 2563 this.element.removeEventListener("mouseout", this.unhighlightDomNode, false);
michael@0 2564 this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true);
michael@0 2565 this.toolbox = null;
michael@0 2566 this._nodeFront = null;
michael@0 2567 }
michael@0 2568 },
michael@0 2569 }); // Widgets.ObjectRenderers.byKind.DOMNode
michael@0 2570
michael@0 2571 /**
michael@0 2572 * The widget used for displaying generic JS object previews.
michael@0 2573 */
michael@0 2574 Widgets.ObjectRenderers.add({
michael@0 2575 byKind: "Object",
michael@0 2576
michael@0 2577 render: function()
michael@0 2578 {
michael@0 2579 let {preview} = this.objectActor;
michael@0 2580 let {ownProperties, safeGetterValues} = preview;
michael@0 2581
michael@0 2582 if ((!ownProperties && !safeGetterValues) || this.options.concise) {
michael@0 2583 this.element = this._anchor(this.objectActor.class,
michael@0 2584 { className: "cm-variable" });
michael@0 2585 return;
michael@0 2586 }
michael@0 2587
michael@0 2588 let container = this.element = this.el("span.kind-" + preview.kind);
michael@0 2589 this._anchor(this.objectActor.class, { className: "cm-variable" });
michael@0 2590 this._text(" { ");
michael@0 2591
michael@0 2592 let addProperty = (str) => {
michael@0 2593 container.appendChild(this.el("span.cm-property", str));
michael@0 2594 };
michael@0 2595
michael@0 2596 let shown = 0;
michael@0 2597 for (let key of Object.keys(ownProperties || {})) {
michael@0 2598 if (shown > 0) {
michael@0 2599 this._text(", ");
michael@0 2600 }
michael@0 2601
michael@0 2602 let value = ownProperties[key];
michael@0 2603
michael@0 2604 addProperty(key);
michael@0 2605 this._text(": ");
michael@0 2606
michael@0 2607 if (value.get) {
michael@0 2608 addProperty("Getter");
michael@0 2609 } else if (value.set) {
michael@0 2610 addProperty("Setter");
michael@0 2611 } else {
michael@0 2612 let valueElem = this.message._renderValueGrip(value.value, { concise: true });
michael@0 2613 container.appendChild(valueElem);
michael@0 2614 }
michael@0 2615
michael@0 2616 shown++;
michael@0 2617 }
michael@0 2618
michael@0 2619 let ownPropertiesShown = shown;
michael@0 2620
michael@0 2621 for (let key of Object.keys(safeGetterValues || {})) {
michael@0 2622 if (shown > 0) {
michael@0 2623 this._text(", ");
michael@0 2624 }
michael@0 2625
michael@0 2626 addProperty(key);
michael@0 2627 this._text(": ");
michael@0 2628
michael@0 2629 let value = safeGetterValues[key].getterValue;
michael@0 2630 let valueElem = this.message._renderValueGrip(value, { concise: true });
michael@0 2631 container.appendChild(valueElem);
michael@0 2632
michael@0 2633 shown++;
michael@0 2634 }
michael@0 2635
michael@0 2636 if (typeof preview.ownPropertiesLength == "number" &&
michael@0 2637 ownPropertiesShown < preview.ownPropertiesLength) {
michael@0 2638 this._text(", ");
michael@0 2639
michael@0 2640 let n = preview.ownPropertiesLength - ownPropertiesShown;
michael@0 2641 let str = VariablesView.stringifiers._getNMoreString(n);
michael@0 2642 this._anchor(str);
michael@0 2643 }
michael@0 2644
michael@0 2645 this._text(" }");
michael@0 2646 },
michael@0 2647 }); // Widgets.ObjectRenderers.byKind.Object
michael@0 2648
michael@0 2649 /**
michael@0 2650 * The long string widget.
michael@0 2651 *
michael@0 2652 * @constructor
michael@0 2653 * @param object message
michael@0 2654 * The owning message.
michael@0 2655 * @param object longStringActor
michael@0 2656 * The LongStringActor to display.
michael@0 2657 */
michael@0 2658 Widgets.LongString = function(message, longStringActor)
michael@0 2659 {
michael@0 2660 Widgets.BaseWidget.call(this, message);
michael@0 2661 this.longStringActor = longStringActor;
michael@0 2662 this._onClick = this._onClick.bind(this);
michael@0 2663 this._onSubstring = this._onSubstring.bind(this);
michael@0 2664 };
michael@0 2665
michael@0 2666 Widgets.LongString.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
michael@0 2667 {
michael@0 2668 /**
michael@0 2669 * The LongStringActor displayed by the widget.
michael@0 2670 * @type object
michael@0 2671 */
michael@0 2672 longStringActor: null,
michael@0 2673
michael@0 2674 render: function()
michael@0 2675 {
michael@0 2676 if (this.element) {
michael@0 2677 return this;
michael@0 2678 }
michael@0 2679
michael@0 2680 let result = this.element = this.document.createElementNS(XHTML_NS, "span");
michael@0 2681 result.className = "longString console-string";
michael@0 2682 this._renderString(this.longStringActor.initial);
michael@0 2683 result.appendChild(this._renderEllipsis());
michael@0 2684
michael@0 2685 return this;
michael@0 2686 },
michael@0 2687
michael@0 2688 /**
michael@0 2689 * Render the long string in the widget element.
michael@0 2690 * @private
michael@0 2691 * @param string str
michael@0 2692 * The string to display.
michael@0 2693 */
michael@0 2694 _renderString: function(str)
michael@0 2695 {
michael@0 2696 this.element.textContent = VariablesView.getString(str, {
michael@0 2697 noStringQuotes: !this.message._quoteStrings,
michael@0 2698 noEllipsis: true,
michael@0 2699 });
michael@0 2700 },
michael@0 2701
michael@0 2702 /**
michael@0 2703 * Render the anchor ellipsis that allows the user to expand the long string.
michael@0 2704 *
michael@0 2705 * @private
michael@0 2706 * @return Element
michael@0 2707 */
michael@0 2708 _renderEllipsis: function()
michael@0 2709 {
michael@0 2710 let ellipsis = this.document.createElementNS(XHTML_NS, "a");
michael@0 2711 ellipsis.className = "longStringEllipsis";
michael@0 2712 ellipsis.textContent = l10n.getStr("longStringEllipsis");
michael@0 2713 ellipsis.href = "#";
michael@0 2714 ellipsis.draggable = false;
michael@0 2715 this.message._addLinkCallback(ellipsis, this._onClick);
michael@0 2716
michael@0 2717 return ellipsis;
michael@0 2718 },
michael@0 2719
michael@0 2720 /**
michael@0 2721 * The click event handler for the ellipsis shown after the short string. This
michael@0 2722 * function expands the element to show the full string.
michael@0 2723 * @private
michael@0 2724 */
michael@0 2725 _onClick: function()
michael@0 2726 {
michael@0 2727 let longString = this.output.webConsoleClient.longString(this.longStringActor);
michael@0 2728 let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH);
michael@0 2729
michael@0 2730 longString.substring(longString.initial.length, toIndex, this._onSubstring);
michael@0 2731 },
michael@0 2732
michael@0 2733 /**
michael@0 2734 * The longString substring response callback.
michael@0 2735 *
michael@0 2736 * @private
michael@0 2737 * @param object response
michael@0 2738 * Response packet.
michael@0 2739 */
michael@0 2740 _onSubstring: function(response)
michael@0 2741 {
michael@0 2742 if (response.error) {
michael@0 2743 Cu.reportError("LongString substring failure: " + response.error);
michael@0 2744 return;
michael@0 2745 }
michael@0 2746
michael@0 2747 this.element.lastChild.remove();
michael@0 2748 this.element.classList.remove("longString");
michael@0 2749
michael@0 2750 this._renderString(this.longStringActor.initial + response.substring);
michael@0 2751
michael@0 2752 this.output.owner.emit("messages-updated", new Set([this.message.element]));
michael@0 2753
michael@0 2754 let toIndex = Math.min(this.longStringActor.length, MAX_LONG_STRING_LENGTH);
michael@0 2755 if (toIndex != this.longStringActor.length) {
michael@0 2756 this._logWarningAboutStringTooLong();
michael@0 2757 }
michael@0 2758 },
michael@0 2759
michael@0 2760 /**
michael@0 2761 * Inform user that the string he tries to view is too long.
michael@0 2762 * @private
michael@0 2763 */
michael@0 2764 _logWarningAboutStringTooLong: function()
michael@0 2765 {
michael@0 2766 let msg = new Messages.Simple(l10n.getStr("longStringTooLong"), {
michael@0 2767 category: "output",
michael@0 2768 severity: "warning",
michael@0 2769 });
michael@0 2770 this.output.addMessage(msg);
michael@0 2771 },
michael@0 2772 }); // Widgets.LongString.prototype
michael@0 2773
michael@0 2774
michael@0 2775 /**
michael@0 2776 * The stacktrace widget.
michael@0 2777 *
michael@0 2778 * @constructor
michael@0 2779 * @extends Widgets.BaseWidget
michael@0 2780 * @param object message
michael@0 2781 * The owning message.
michael@0 2782 * @param array stacktrace
michael@0 2783 * The stacktrace to display, array of frames as supplied by the server,
michael@0 2784 * over the remote protocol.
michael@0 2785 */
michael@0 2786 Widgets.Stacktrace = function(message, stacktrace)
michael@0 2787 {
michael@0 2788 Widgets.BaseWidget.call(this, message);
michael@0 2789 this.stacktrace = stacktrace;
michael@0 2790 };
michael@0 2791
michael@0 2792 Widgets.Stacktrace.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
michael@0 2793 {
michael@0 2794 /**
michael@0 2795 * The stackframes received from the server.
michael@0 2796 * @type array
michael@0 2797 */
michael@0 2798 stacktrace: null,
michael@0 2799
michael@0 2800 render: function()
michael@0 2801 {
michael@0 2802 if (this.element) {
michael@0 2803 return this;
michael@0 2804 }
michael@0 2805
michael@0 2806 let result = this.element = this.document.createElementNS(XHTML_NS, "ul");
michael@0 2807 result.className = "stacktrace devtools-monospace";
michael@0 2808
michael@0 2809 for (let frame of this.stacktrace) {
michael@0 2810 result.appendChild(this._renderFrame(frame));
michael@0 2811 }
michael@0 2812
michael@0 2813 return this;
michael@0 2814 },
michael@0 2815
michael@0 2816 /**
michael@0 2817 * Render a frame object received from the server.
michael@0 2818 *
michael@0 2819 * @param object frame
michael@0 2820 * The stack frame to display. This object should have the following
michael@0 2821 * properties: functionName, filename and lineNumber.
michael@0 2822 * @return DOMElement
michael@0 2823 * The DOM element to display for the given frame.
michael@0 2824 */
michael@0 2825 _renderFrame: function(frame)
michael@0 2826 {
michael@0 2827 let fn = this.document.createElementNS(XHTML_NS, "span");
michael@0 2828 fn.className = "function";
michael@0 2829 if (frame.functionName) {
michael@0 2830 let span = this.document.createElementNS(XHTML_NS, "span");
michael@0 2831 span.className = "cm-variable";
michael@0 2832 span.textContent = frame.functionName;
michael@0 2833 fn.appendChild(span);
michael@0 2834 fn.appendChild(this.document.createTextNode("()"));
michael@0 2835 } else {
michael@0 2836 fn.classList.add("cm-comment");
michael@0 2837 fn.textContent = l10n.getStr("stacktrace.anonymousFunction");
michael@0 2838 }
michael@0 2839
michael@0 2840 let location = this.output.owner.createLocationNode(frame.filename,
michael@0 2841 frame.lineNumber,
michael@0 2842 "jsdebugger");
michael@0 2843
michael@0 2844 // .devtools-monospace sets font-size to 80%, however .body already has
michael@0 2845 // .devtools-monospace. If we keep it here, the location would be rendered
michael@0 2846 // smaller.
michael@0 2847 location.classList.remove("devtools-monospace");
michael@0 2848
michael@0 2849 let elem = this.document.createElementNS(XHTML_NS, "li");
michael@0 2850 elem.appendChild(fn);
michael@0 2851 elem.appendChild(location);
michael@0 2852 elem.appendChild(this.document.createTextNode("\n"));
michael@0 2853
michael@0 2854 return elem;
michael@0 2855 },
michael@0 2856 }); // Widgets.Stacktrace.prototype
michael@0 2857
michael@0 2858
michael@0 2859 function gSequenceId()
michael@0 2860 {
michael@0 2861 return gSequenceId.n++;
michael@0 2862 }
michael@0 2863 gSequenceId.n = 0;
michael@0 2864
michael@0 2865 exports.ConsoleOutput = ConsoleOutput;
michael@0 2866 exports.Messages = Messages;
michael@0 2867 exports.Widgets = Widgets;

mercurial