|
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; |