Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_error.js
blob9ad71e8d7532913323c8a96965dc3241d6a59d49
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    * Clone a template within the extension error template collection.
10    * @param {string} templateName The class name of the template to clone.
11    * @return {HTMLElement} The clone of the template.
12    */
13   function cloneTemplate(templateName) {
14     return /** @type {HTMLElement} */($('template-collection-extension-error').
15         querySelector('.' + templateName).cloneNode(true));
16   }
18   /**
19    * Checks that an Extension ID follows the proper format (i.e., is 32
20    * characters long, is lowercase, and contains letters in the range [a, p]).
21    * @param {string} id The Extension ID to test.
22    * @return {boolean} Whether or not the ID is valid.
23    */
24   function idIsValid(id) {
25     return /^[a-p]{32}$/.test(id);
26   }
28   /**
29    * @param {!Array<(ManifestError|RuntimeError)>} errors
30    * @param {number} id
31    * @return {number} The index of the error with |id|, or -1 if not found.
32    */
33   function findErrorById(errors, id) {
34     for (var i = 0; i < errors.length; ++i) {
35       if (errors[i].id == id)
36         return i;
37     }
38     return -1;
39   }
41   /**
42    * Creates a new ExtensionError HTMLElement; this is used to show a
43    * notification to the user when an error is caused by an extension.
44    * @param {(RuntimeError|ManifestError)} error The error the element should
45    *     represent.
46    * @constructor
47    * @extends {HTMLElement}
48    */
49   function ExtensionError(error) {
50     var div = cloneTemplate('extension-error-metadata');
51     div.__proto__ = ExtensionError.prototype;
52     div.decorate(error);
53     return div;
54   }
56   ExtensionError.prototype = {
57     __proto__: HTMLElement.prototype,
59     /**
60      * @param {(RuntimeError|ManifestError)} error The error the element should
61      *     represent.
62      * @private
63      */
64     decorate: function(error) {
65       /**
66        * The backing error.
67        * @type {(ManifestError|RuntimeError)}
68        */
69       this.error = error;
71       // Add an additional class for the severity level.
72       if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) {
73         switch (error.severity) {
74           case chrome.developerPrivate.ErrorLevel.LOG:
75             this.classList.add('extension-error-severity-info');
76             break;
77           case chrome.developerPrivate.ErrorLevel.WARN:
78             this.classList.add('extension-error-severity-warning');
79             break;
80           case chrome.developerPrivate.ErrorLevel.ERROR:
81             this.classList.add('extension-error-severity-fatal');
82             break;
83           default:
84             assertNotReached();
85         }
86       } else {
87         // We classify manifest errors as "warnings".
88         this.classList.add('extension-error-severity-warning');
89       }
91       var iconNode = document.createElement('img');
92       iconNode.className = 'extension-error-icon';
93       // TODO(hcarmona): Populate alt text with a proper description since this
94       // icon conveys the severity of the error. (info, warning, fatal).
95       iconNode.alt = '';
96       this.insertBefore(iconNode, this.firstChild);
98       var messageSpan = this.querySelector('.extension-error-message');
99       messageSpan.textContent = error.message;
101       var deleteButton = this.querySelector('.error-delete-button');
102       deleteButton.addEventListener('click', function(e) {
103         this.dispatchEvent(
104             new CustomEvent('deleteExtensionError',
105                             {bubbles: true, detail: this.error}));
106       }.bind(this));
108       this.addEventListener('click', function(e) {
109         if (e.target != deleteButton)
110           this.requestActive_();
111       }.bind(this));
113       this.addEventListener('keydown', function(e) {
114         if (e.keyIdentifier == 'Enter' && e.target != deleteButton)
115           this.requestActive_();
116       });
117     },
119     /**
120      * Bubble up an event to request to become active.
121      * @private
122      */
123     requestActive_: function() {
124       this.dispatchEvent(
125           new CustomEvent('highlightExtensionError',
126                           {bubbles: true, detail: this.error}));
127     },
128   };
130   /**
131    * A variable length list of runtime or manifest errors for a given extension.
132    * @param {Array<(RuntimeError|ManifestError)>} errors The list of extension
133    *     errors with which to populate the list.
134    * @param {string} extensionId The id of the extension.
135    * @constructor
136    * @extends {HTMLDivElement}
137    */
138   function ExtensionErrorList(errors, extensionId) {
139     var div = cloneTemplate('extension-error-list');
140     div.__proto__ = ExtensionErrorList.prototype;
141     div.extensionId_ = extensionId;
142     div.decorate(errors);
143     return div;
144   }
146   /**
147    * @param {!Element} root
148    * @param {?Node} boundary
149    * @constructor
150    * @extends {cr.ui.FocusRow}
151    */
152   ExtensionErrorList.FocusRow = function(root, boundary) {
153     cr.ui.FocusRow.call(this, root, boundary);
155     this.addItem('message', '.extension-error-message');
156     this.addItem('delete', '.error-delete-button');
157   };
159   ExtensionErrorList.FocusRow.prototype = {
160     __proto__: cr.ui.FocusRow.prototype,
161   };
163   ExtensionErrorList.prototype = {
164     __proto__: HTMLDivElement.prototype,
166     /**
167      * Initializes the extension error list.
168      * @param {Array<(RuntimeError|ManifestError)>} errors The list of errors.
169      */
170     decorate: function(errors) {
171       /** @private {!Array<(ManifestError|RuntimeError)>} */
172       this.errors_ = [];
174       /** @private {!cr.ui.FocusGrid} */
175       this.focusGrid_ = new cr.ui.FocusGrid();
177       /** @private {Element} */
178       this.listContents_ = this.querySelector('.extension-error-list-contents');
180       errors.forEach(this.addError_, this);
182       this.focusGrid_.ensureRowActive();
184       this.addEventListener('highlightExtensionError', function(e) {
185         this.setActiveErrorNode_(e.target);
186       });
187       this.addEventListener('deleteExtensionError', function(e) {
188         this.removeError_(e.detail);
189       });
191       this.querySelector('#extension-error-list-clear').addEventListener(
192           'click', function(e) {
193         this.clear(true);
194       }.bind(this));
196       /**
197        * The callback for the extension changed event.
198        * @private {function(EventData):void}
199        */
200       this.onItemStateChangedListener_ = function(data) {
201         var type = chrome.developerPrivate.EventType;
202         if ((data.event_type == type.ERRORS_REMOVED ||
203              data.event_type == type.ERROR_ADDED) &&
204             data.extensionInfo.id == this.extensionId_) {
205           var newErrors = data.extensionInfo.runtimeErrors.concat(
206               data.extensionInfo.manifestErrors);
207           this.updateErrors_(newErrors);
208         }
209       }.bind(this);
211       chrome.developerPrivate.onItemStateChanged.addListener(
212           this.onItemStateChangedListener_);
214       /**
215        * The active error element in the list.
216        * @private {?}
217        */
218       this.activeError_ = null;
220       this.setActiveError(0);
221     },
223     /**
224      * Adds an error to the list.
225      * @param {(RuntimeError|ManifestError)} error The error to add.
226      * @private
227      */
228     addError_: function(error) {
229       this.querySelector('#no-errors-span').hidden = true;
230       this.errors_.push(error);
232       var extensionError = new ExtensionError(error);
233       this.listContents_.appendChild(extensionError);
235       this.focusGrid_.addRow(
236           new ExtensionErrorList.FocusRow(extensionError, this.listContents_));
237     },
239     /**
240      * Removes an error from the list.
241      * @param {(RuntimeError|ManifestError)} error The error to remove.
242      * @private
243      */
244     removeError_: function(error) {
245       var index = 0;
246       for (; index < this.errors_.length; ++index) {
247         if (this.errors_[index].id == error.id)
248           break;
249       }
250       assert(index != this.errors_.length);
251       var errorList = this.querySelector('.extension-error-list-contents');
253       var wasActive =
254           this.activeError_ && this.activeError_.error.id == error.id;
256       this.errors_.splice(index, 1);
257       var listElement = errorList.children[index];
259       var focusRow = this.focusGrid_.getRowForRoot(listElement);
260       this.focusGrid_.removeRow(focusRow);
261       this.focusGrid_.ensureRowActive();
262       focusRow.destroy();
264       // TODO(dbeam): in a world where this UI is actually used, we should
265       // probably move the focus before removing |listElement|.
266       listElement.parentNode.removeChild(listElement);
268       if (wasActive) {
269         index = Math.min(index, this.errors_.length - 1);
270         this.setActiveError(index);  // Gracefully handles the -1 case.
271       }
273       chrome.developerPrivate.deleteExtensionErrors({
274         extensionId: error.extensionId,
275         errorIds: [error.id]
276       });
278       if (this.errors_.length == 0)
279         this.querySelector('#no-errors-span').hidden = false;
280     },
282     /**
283      * Updates the list of errors.
284      * @param {!Array<(ManifestError|RuntimeError)>} newErrors The new list of
285      *     errors.
286      * @private
287      */
288     updateErrors_: function(newErrors) {
289       this.errors_.forEach(function(error) {
290         if (findErrorById(newErrors, error.id) == -1)
291           this.removeError_(error);
292       }, this);
293       newErrors.forEach(function(error) {
294         var index = findErrorById(this.errors_, error.id);
295         if (index == -1)
296           this.addError_(error);
297         else
298           this.errors_[index] = error;  // Update the existing reference.
299       }, this);
300     },
302     /**
303      * Called when the list is being removed.
304      */
305     onRemoved: function() {
306       chrome.developerPrivate.onItemStateChanged.removeListener(
307           this.onItemStateChangedListener_);
308       this.clear(false);
309     },
311     /**
312      * Sets the active error in the list.
313      * @param {number} index The index to set to be active.
314      */
315     setActiveError: function(index) {
316       var errorList = this.querySelector('.extension-error-list-contents');
317       var item = errorList.children[index];
318       this.setActiveErrorNode_(
319           item ? item.querySelector('.extension-error-metadata') : null);
320       var node = null;
321       if (index >= 0 && index < errorList.children.length) {
322         node = errorList.children[index].querySelector(
323                    '.extension-error-metadata');
324       }
325       this.setActiveErrorNode_(node);
326     },
328     /**
329      * Clears the list of all errors.
330      * @param {boolean} deleteErrors Whether or not the errors should be deleted
331      *     on the backend.
332      */
333     clear: function(deleteErrors) {
334       if (this.errors_.length == 0)
335         return;
337       if (deleteErrors) {
338         var ids = this.errors_.map(function(error) { return error.id; });
339         chrome.developerPrivate.deleteExtensionErrors({
340           extensionId: this.extensionId_,
341           errorIds: ids
342         });
343       }
345       this.setActiveErrorNode_(null);
346       this.errors_.length = 0;
347       var errorList = this.querySelector('.extension-error-list-contents');
348       while (errorList.firstChild)
349         errorList.removeChild(errorList.firstChild);
350     },
352     /**
353      * Sets the active error in the list.
354      * @param {?} node The error to make active.
355      * @private
356      */
357     setActiveErrorNode_: function(node) {
358       if (this.activeError_)
359         this.activeError_.classList.remove('extension-error-active');
361       if (node)
362         node.classList.add('extension-error-active');
364       this.activeError_ = node;
366       this.dispatchEvent(
367           new CustomEvent('activeExtensionErrorChanged',
368                           {bubbles: true, detail: node ? node.error : null}));
369     },
370   };
372   return {
373     ExtensionErrorList: ExtensionErrorList
374   };