Fix some lingering WebUI focus issues.
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_error_overlay.js
blob1f7975e6605c39366ae46d65b64c9953838df136
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 cr.define('extensions', function() {
6   'use strict';
8   /**
9    * Clear all the content of a given element.
10    * @param {HTMLElement} element The element to be cleared.
11    */
12   function clearElement(element) {
13     while (element.firstChild)
14       element.removeChild(element.firstChild);
15   }
17   /**
18    * Get the url relative to the main extension url. If the url is
19    * unassociated with the extension, this will be the full url.
20    * @param {string} url The url to make relative.
21    * @param {string} extensionUrl The url for the extension resources, in the
22    *     form "chrome-etxension://<extension_id>/".
23    * @return {string} The url relative to the host.
24    */
25   function getRelativeUrl(url, extensionUrl) {
26     return url.substring(0, extensionUrl.length) == extensionUrl ?
27         url.substring(extensionUrl.length) : url;
28   }
30   /**
31    * The RuntimeErrorContent manages all content specifically associated with
32    * runtime errors; this includes stack frames and the context url.
33    * @constructor
34    * @extends {HTMLDivElement}
35    */
36   function RuntimeErrorContent() {
37     var contentArea = $('template-collection-extension-error-overlay').
38         querySelector('.extension-error-overlay-runtime-content').
39         cloneNode(true);
40     contentArea.__proto__ = RuntimeErrorContent.prototype;
41     contentArea.init();
42     return contentArea;
43   }
45   /**
46    * The name of the "active" class specific to extension errors (so as to
47    * not conflict with other rules).
48    * @type {string}
49    * @const
50    */
51   RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active';
53   /**
54    * Determine whether or not we should display the url to the user. We don't
55    * want to include any of our own code in stack traces.
56    * @param {string} url The url in question.
57    * @return {boolean} True if the url should be displayed, and false
58    *     otherwise (i.e., if it is an internal script).
59    */
60   RuntimeErrorContent.shouldDisplayForUrl = function(url) {
61     // All our internal scripts are in the 'extensions::' namespace.
62     return !/^extensions::/.test(url);
63   };
65   RuntimeErrorContent.prototype = {
66     __proto__: HTMLDivElement.prototype,
68     /**
69      * The underlying error whose details are being displayed.
70      * @type {?(RuntimeError|ManifestError)}
71      * @private
72      */
73     error_: null,
75     /**
76      * The URL associated with this extension, i.e. chrome-extension://<id>/.
77      * @type {?string}
78      * @private
79      */
80     extensionUrl_: null,
82     /**
83      * The node of the stack trace which is currently active.
84      * @type {?HTMLElement}
85      * @private
86      */
87     currentFrameNode_: null,
89     /**
90      * Initialize the RuntimeErrorContent for the first time.
91      */
92     init: function() {
93       /**
94        * The stack trace element in the overlay.
95        * @type {HTMLElement}
96        * @private
97        */
98       this.stackTrace_ = /** @type {HTMLElement} */(
99           this.querySelector('.extension-error-overlay-stack-trace-list'));
100       assert(this.stackTrace_);
102       /**
103        * The context URL element in the overlay.
104        * @type {HTMLElement}
105        * @private
106        */
107       this.contextUrl_ = /** @type {HTMLElement} */(
108           this.querySelector('.extension-error-overlay-context-url'));
109       assert(this.contextUrl_);
110     },
112     /**
113      * Sets the error for the content.
114      * @param {(RuntimeError|ManifestError)} error The error whose content
115      *     should be displayed.
116      * @param {string} extensionUrl The URL associated with this extension.
117      */
118     setError: function(error, extensionUrl) {
119       this.clearError();
121       this.error_ = error;
122       this.extensionUrl_ = extensionUrl;
123       this.contextUrl_.textContent = error.contextUrl ?
124           getRelativeUrl(error.contextUrl, this.extensionUrl_) :
125           loadTimeData.getString('extensionErrorOverlayContextUnknown');
126       this.initStackTrace_();
127     },
129     /**
130      * Wipe content associated with a specific error.
131      */
132     clearError: function() {
133       this.error_ = null;
134       this.extensionUrl_ = null;
135       this.currentFrameNode_ = null;
136       clearElement(this.stackTrace_);
137       this.stackTrace_.hidden = true;
138     },
140     /**
141      * Makes |frame| active and deactivates the previously active frame (if
142      * there was one).
143      * @param {HTMLElement} frameNode The frame to activate.
144      * @private
145      */
146     setActiveFrame_: function(frameNode) {
147       if (this.currentFrameNode_) {
148         this.currentFrameNode_.classList.remove(
149             RuntimeErrorContent.ACTIVE_CLASS_NAME);
150       }
152       this.currentFrameNode_ = frameNode;
153       this.currentFrameNode_.classList.add(
154           RuntimeErrorContent.ACTIVE_CLASS_NAME);
155     },
157     /**
158      * Initialize the stack trace element of the overlay.
159      * @private
160      */
161     initStackTrace_: function() {
162       for (var i = 0; i < this.error_.stackTrace.length; ++i) {
163         var frame = this.error_.stackTrace[i];
164         // Don't include any internal calls (e.g., schemaBindings) in the
165         // stack trace.
166         if (!RuntimeErrorContent.shouldDisplayForUrl(frame.url))
167           continue;
169         var frameNode = document.createElement('li');
170         // Attach the index of the frame to which this node refers (since we
171         // may skip some, this isn't a 1-to-1 match).
172         frameNode.indexIntoTrace = i;
174         // The description is a human-readable summation of the frame, in the
175         // form "<relative_url>:<line_number> (function)", e.g.
176         // "myfile.js:25 (myFunction)".
177         var description = getRelativeUrl(frame.url,
178             assert(this.extensionUrl_)) + ':' + frame.lineNumber;
179         if (frame.functionName) {
180           var functionName = frame.functionName == '(anonymous function)' ?
181               loadTimeData.getString('extensionErrorOverlayAnonymousFunction') :
182               frame.functionName;
183           description += ' (' + functionName + ')';
184         }
185         frameNode.textContent = description;
187         // When the user clicks on a frame in the stack trace, we should
188         // highlight that overlay in the list, display the appropriate source
189         // code with the line highlighted, and link the "Open DevTools" button
190         // with that frame.
191         frameNode.addEventListener('click', function(frame, frameNode, e) {
192           this.setActiveFrame_(frameNode);
194           // Request the file source with the section highlighted.
195           extensions.ExtensionErrorOverlay.getInstance().requestFileSource(
196               {extensionId: this.error_.extensionId,
197                message: this.error_.message,
198                pathSuffix: getRelativeUrl(frame.url,
199                                           assert(this.extensionUrl_)),
200                lineNumber: frame.lineNumber});
201         }.bind(this, frame, frameNode));
203         this.stackTrace_.appendChild(frameNode);
204       }
206       // Set the current stack frame to the first stack frame and show the
207       // trace, if one exists. (We can't just check error.stackTrace, because
208       // it's possible the trace was purely internal, and we don't show
209       // internal frames.)
210       if (this.stackTrace_.children.length > 0) {
211         this.stackTrace_.hidden = false;
212         this.setActiveFrame_(assertInstanceof(this.stackTrace_.firstChild,
213             HTMLElement));
214       }
215     },
217     /**
218      * Open the developer tools for the active stack frame.
219      */
220     openDevtools: function() {
221       var stackFrame =
222           this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace];
224       chrome.developerPrivate.openDevTools(
225           {renderProcessId: this.error_.renderProcessId || -1,
226            renderViewId: this.error_.renderViewId || -1,
227            url: stackFrame.url,
228            lineNumber: stackFrame.lineNumber || 0,
229            columnNumber: stackFrame.columnNumber || 0});
230     }
231   };
233   /**
234    * The ExtensionErrorOverlay will show the contents of a file which pertains
235    * to the ExtensionError; this is either the manifest file (for manifest
236    * errors) or a source file (for runtime errors). If possible, the portion
237    * of the file which caused the error will be highlighted.
238    * @constructor
239    */
240   function ExtensionErrorOverlay() {
241     /**
242      * The content section for runtime errors; this is re-used for all
243      * runtime errors and attached/detached from the overlay as needed.
244      * @type {RuntimeErrorContent}
245      * @private
246      */
247     this.runtimeErrorContent_ = new RuntimeErrorContent();
248   }
250   /**
251    * The manifest filename.
252    * @type {string}
253    * @const
254    * @private
255    */
256   ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json';
258   /**
259    * Determine whether or not chrome can load the source for a given file; this
260    * can only be done if the file belongs to the extension.
261    * @param {string} file The file to load.
262    * @param {string} extensionUrl The url for the extension, in the form
263    *     chrome-extension://<extension-id>/.
264    * @return {boolean} True if the file can be loaded, false otherwise.
265    * @private
266    */
267   ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) {
268     return file.substr(0, extensionUrl.length) == extensionUrl ||
269            file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_;
270   };
272   cr.addSingletonGetter(ExtensionErrorOverlay);
274   ExtensionErrorOverlay.prototype = {
275     /**
276      * The underlying error whose details are being displayed.
277      * @type {?(RuntimeError|ManifestError)}
278      * @private
279      */
280     selectedError_: null,
282     /**
283      * Initialize the page.
284      * @param {function(HTMLDivElement)} showOverlay The function to show or
285      *     hide the ExtensionErrorOverlay; this should take a single parameter
286      *     which is either the overlay Div if the overlay should be displayed,
287      *     or null if the overlay should be hidden.
288      */
289     initializePage: function(showOverlay) {
290       var overlay = $('overlay');
291       cr.ui.overlay.setupOverlay(overlay);
292       cr.ui.overlay.globalInitialization();
293       overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this));
295       $('extension-error-overlay-dismiss').addEventListener('click',
296           function() {
297         cr.dispatchSimpleEvent(overlay, 'cancelOverlay');
298       });
300       /**
301        * The element of the full overlay.
302        * @type {HTMLDivElement}
303        * @private
304        */
305       this.overlayDiv_ = /** @type {HTMLDivElement} */(
306           $('extension-error-overlay'));
308       /**
309        * The portion of the overlay which shows the code relating to the error
310        * and the corresponding line numbers.
311        * @type {extensions.ExtensionCode}
312        * @private
313        */
314       this.codeDiv_ =
315           new extensions.ExtensionCode($('extension-error-overlay-code'));
317       /**
318        * The function to show or hide the ExtensionErrorOverlay.
319        * @param {boolean} isVisible Whether the overlay should be visible.
320        */
321       this.setVisible = function(isVisible) {
322         showOverlay(isVisible ? this.overlayDiv_ : null);
323         if (isVisible)
324           this.codeDiv_.scrollToError();
325       };
327       /**
328        * The button to open the developer tools (only available for runtime
329        * errors).
330        * @type {HTMLButtonElement}
331        * @private
332        */
333       this.openDevtoolsButton_ = /** @type {HTMLButtonElement} */(
334           $('extension-error-overlay-devtools-button'));
335       this.openDevtoolsButton_.addEventListener('click', function() {
336           this.runtimeErrorContent_.openDevtools();
337       }.bind(this));
338     },
340     /**
341      * Handles a click on the dismiss ("OK" or close) buttons.
342      * @param {Event} e The click event.
343      * @private
344      */
345     handleDismiss_: function(e) {
346       this.setVisible(false);
348       // There's a chance that the overlay receives multiple dismiss events; in
349       // this case, handle it gracefully and return (since all necessary work
350       // will already have been done).
351       if (!this.selectedError_)
352         return;
354       // Remove all previous content.
355       this.codeDiv_.clear();
357       this.overlayDiv_.querySelector('.extension-error-list').onRemoved();
359       this.clearRuntimeContent_();
361       this.selectedError_ = null;
362     },
364     /**
365      * Clears the current content.
366      * @private
367      */
368     clearRuntimeContent_: function() {
369       if (this.runtimeErrorContent_.parentNode) {
370         this.runtimeErrorContent_.parentNode.removeChild(
371             this.runtimeErrorContent_);
372         this.runtimeErrorContent_.clearError();
373       }
374       this.openDevtoolsButton_.hidden = true;
375     },
377     /**
378      * Sets the active error for the overlay.
379      * @param {?(ManifestError|RuntimeError)} error The error to make active.
380      * TODO(dbeam): add URL externs and re-enable typechecking in this method.
381      * @suppress {missingProperties}
382      * @private
383      */
384     setActiveError_: function(error) {
385       this.selectedError_ = error;
387       // If there is no error (this can happen if, e.g., the user deleted all
388       // the errors), then clear the content.
389       if (!error) {
390         this.codeDiv_.populate(
391             null, loadTimeData.getString('extensionErrorNoErrorsCodeMessage'));
392         this.clearRuntimeContent_();
393         return;
394       }
396       var extensionUrl = 'chrome-extension://' + error.extensionId + '/';
397       // Set or hide runtime content.
398       if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) {
399         this.runtimeErrorContent_.setError(error, extensionUrl);
400         this.overlayDiv_.querySelector('.content-area').insertBefore(
401             this.runtimeErrorContent_,
402             this.codeDiv_.nextSibling);
403         this.openDevtoolsButton_.hidden = false;
404         this.openDevtoolsButton_.disabled = !error.canInspect;
405       } else {
406         this.clearRuntimeContent_();
407       }
409       // Read the file source to populate the code section, or set it to null if
410       // the file is unreadable.
411       if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) {
412         // Use pathname instead of relativeUrl.
413         var requestFileSourceArgs = {extensionId: error.extensionId,
414                                      message: error.message};
415         switch (error.type) {
416           case chrome.developerPrivate.ErrorType.MANIFEST:
417             requestFileSourceArgs.pathSuffix = error.source;
418             requestFileSourceArgs.manifestKey = error.manifestKey;
419             requestFileSourceArgs.manifestSpecific = error.manifestSpecific;
420             break;
421           case chrome.developerPrivate.ErrorType.RUNTIME:
422             // slice(1) because pathname starts with a /.
423             var pathname = new URL(error.source).pathname.slice(1);
424             requestFileSourceArgs.pathSuffix = pathname;
425             requestFileSourceArgs.lineNumber =
426                 error.stackTrace && error.stackTrace[0] ?
427                     error.stackTrace[0].lineNumber : 0;
428             break;
429           default:
430             assertNotReached();
431         }
432         this.requestFileSource(requestFileSourceArgs);
433       } else {
434         this.onFileSourceResponse_(null);
435       }
436     },
438     /**
439      * Associate an error with the overlay. This will set the error for the
440      * overlay, and, if possible, will populate the code section of the overlay
441      * with the relevant file, load the stack trace, and generate links for
442      * opening devtools (the latter two only happen for runtime errors).
443      * @param {Array<(RuntimeError|ManifestError)>} errors The error to show in
444      *     the overlay.
445      * @param {string} extensionId The id of the extension.
446      * @param {string} extensionName The name of the extension.
447      */
448     setErrorsAndShowOverlay: function(errors, extensionId, extensionName) {
449       document.querySelector(
450           '#extension-error-overlay .extension-error-overlay-title').
451               textContent = extensionName;
452       var errorsDiv = this.overlayDiv_.querySelector('.extension-error-list');
453       var extensionErrors =
454           new extensions.ExtensionErrorList(errors, extensionId);
455       errorsDiv.parentNode.replaceChild(extensionErrors, errorsDiv);
456       extensionErrors.addEventListener('activeExtensionErrorChanged',
457                                        function(e) {
458         this.setActiveError_(e.detail);
459       }.bind(this));
461       if (errors.length > 0)
462         this.setActiveError_(errors[0]);
463       this.setVisible(true);
464     },
466     /**
467      * Requests a file's source.
468      * @param {RequestFileSourceProperties} args The arguments for the call.
469      */
470     requestFileSource: function(args) {
471       chrome.developerPrivate.requestFileSource(
472           args, this.onFileSourceResponse_.bind(this));
473     },
475     /**
476      * Set the code to be displayed in the code portion of the overlay.
477      * @see ExtensionErrorOverlay.requestFileSourceResponse().
478      * @param {?RequestFileSourceResponse} response The response from the
479      *     request file source call, which will be shown as code. If |response|
480      *     is null, then a "Could not display code" message will be displayed
481      *     instead.
482      */
483     onFileSourceResponse_: function(response) {
484       this.codeDiv_.populate(
485           response,  // ExtensionCode can handle a null response.
486           loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay'));
487       this.setVisible(true);
488     },
489   };
491   // Export
492   return {
493     ExtensionErrorOverlay: ExtensionErrorOverlay
494   };