browser/devtools/webconsole/console-output.js

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

mercurial