OLD | NEW |
1 // Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 cr.define('extensions', function() { | 5 cr.define('extensions', function() { |
6 'use strict'; | 6 'use strict'; |
7 | 7 |
8 /** | 8 /** |
9 * Clone a template within the extension error template collection. | 9 * Clone a template within the extension error template collection. |
10 * @param {string} templateName The class name of the template to clone. | 10 * @param {string} templateName The class name of the template to clone. |
11 * @return {HTMLElement} The clone of the template. | 11 * @return {HTMLElement} The clone of the template. |
12 */ | 12 */ |
13 function cloneTemplate(templateName) { | 13 function cloneTemplate(templateName) { |
14 return /** @type {HTMLElement} */($('template-collection-extension-error'). | 14 return /** @type {HTMLElement} */($('template-collection-extension-error'). |
15 querySelector('.' + templateName).cloneNode(true)); | 15 querySelector('.' + templateName).cloneNode(true)); |
16 } | 16 } |
17 | 17 |
18 /** | 18 /** |
19 * Checks that an Extension ID follows the proper format (i.e., is 32 | 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]). | 20 * characters long, is lowercase, and contains letters in the range [a, p]). |
21 * @param {string} id The Extension ID to test. | 21 * @param {string} id The Extension ID to test. |
22 * @return {boolean} Whether or not the ID is valid. | 22 * @return {boolean} Whether or not the ID is valid. |
23 */ | 23 */ |
24 function idIsValid(id) { | 24 function idIsValid(id) { |
25 return /^[a-p]{32}$/.test(id); | 25 return /^[a-p]{32}$/.test(id); |
26 } | 26 } |
27 | 27 |
28 /** | 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 } |
| 40 |
| 41 /** |
29 * Creates a new ExtensionError HTMLElement; this is used to show a | 42 * Creates a new ExtensionError HTMLElement; this is used to show a |
30 * notification to the user when an error is caused by an extension. | 43 * notification to the user when an error is caused by an extension. |
31 * @param {(RuntimeError|ManifestError)} error The error the element should | 44 * @param {(RuntimeError|ManifestError)} error The error the element should |
32 * represent. | 45 * represent. |
33 * @param {Element} boundary The boundary for the focus grid. | 46 * @param {Element} boundary The boundary for the focus grid. |
34 * @constructor | 47 * @constructor |
35 * @extends {cr.ui.FocusRow} | 48 * @extends {cr.ui.FocusRow} |
36 */ | 49 */ |
37 function ExtensionError(error, boundary) { | 50 function ExtensionError(error, boundary) { |
38 var div = cloneTemplate('extension-error-metadata'); | 51 var div = cloneTemplate('extension-error-metadata'); |
39 div.__proto__ = ExtensionError.prototype; | 52 div.__proto__ = ExtensionError.prototype; |
40 div.decorateWithError_(error, boundary); | 53 div.decorateWithError_(error, boundary); |
41 return div; | 54 return div; |
42 } | 55 } |
43 | 56 |
44 ExtensionError.prototype = { | 57 ExtensionError.prototype = { |
45 __proto__: cr.ui.FocusRow.prototype, | 58 __proto__: cr.ui.FocusRow.prototype, |
46 | 59 |
47 /** @override */ | 60 /** @override */ |
48 getEquivalentElement: function(element) { | 61 getEquivalentElement: function(element) { |
49 return assert(this.querySelector('.extension-error-view-details')); | 62 if (element.classList.contains('extension-error-metadata')) |
| 63 return this; |
| 64 if (element.classList.contains('error-delete-button')) { |
| 65 return /** @type {!HTMLElement} */ (this.querySelector( |
| 66 '.error-delete-button')); |
| 67 } |
| 68 assertNotReached(); |
| 69 return element; |
50 }, | 70 }, |
51 | 71 |
52 /** | 72 /** |
53 * @param {(RuntimeError|ManifestError)} error The error the element should | 73 * @param {(RuntimeError|ManifestError)} error The error the element should |
54 * represent. | 74 * represent. |
55 * @param {Element} boundary The boundary for the FocusGrid. | 75 * @param {Element} boundary The boundary for the FocusGrid. |
56 * @private | 76 * @private |
57 */ | 77 */ |
58 decorateWithError_: function(error, boundary) { | 78 decorateWithError_: function(error, boundary) { |
59 this.decorate(boundary); | 79 this.decorate(boundary); |
60 | 80 |
| 81 /** |
| 82 * The backing error. |
| 83 * @type {(ManifestError|RuntimeError)} |
| 84 */ |
| 85 this.error = error; |
| 86 |
61 // Add an additional class for the severity level. | 87 // Add an additional class for the severity level. |
62 if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) { | 88 if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) { |
63 switch (error.severity) { | 89 switch (error.severity) { |
64 case chrome.developerPrivate.ErrorLevel.LOG: | 90 case chrome.developerPrivate.ErrorLevel.LOG: |
65 this.classList.add('extension-error-severity-info'); | 91 this.classList.add('extension-error-severity-info'); |
66 break; | 92 break; |
67 case chrome.developerPrivate.ErrorLevel.WARN: | 93 case chrome.developerPrivate.ErrorLevel.WARN: |
68 this.classList.add('extension-error-severity-warning'); | 94 this.classList.add('extension-error-severity-warning'); |
69 break; | 95 break; |
70 case chrome.developerPrivate.ErrorLevel.ERROR: | 96 case chrome.developerPrivate.ErrorLevel.ERROR: |
71 this.classList.add('extension-error-severity-fatal'); | 97 this.classList.add('extension-error-severity-fatal'); |
72 break; | 98 break; |
73 default: | 99 default: |
74 assertNotReached(); | 100 assertNotReached(); |
75 } | 101 } |
76 } else { | 102 } else { |
77 // We classify manifest errors as "warnings". | 103 // We classify manifest errors as "warnings". |
78 this.classList.add('extension-error-severity-warning'); | 104 this.classList.add('extension-error-severity-warning'); |
79 } | 105 } |
80 | 106 |
81 var iconNode = document.createElement('img'); | 107 var iconNode = document.createElement('img'); |
82 iconNode.className = 'extension-error-icon'; | 108 iconNode.className = 'extension-error-icon'; |
83 // TODO(hcarmona): Populate alt text with a proper description since this | 109 // TODO(hcarmona): Populate alt text with a proper description since this |
84 // icon conveys the severity of the error. (info, warning, fatal). | 110 // icon conveys the severity of the error. (info, warning, fatal). |
85 iconNode.alt = ''; | 111 iconNode.alt = ''; |
86 this.insertBefore(iconNode, this.firstChild); | 112 this.insertBefore(iconNode, this.firstChild); |
87 | 113 |
88 var messageSpan = this.querySelector('.extension-error-message'); | 114 var messageSpan = this.querySelector('.extension-error-message'); |
89 messageSpan.textContent = error.message; | 115 messageSpan.textContent = error.message; |
90 messageSpan.title = error.message; | |
91 | 116 |
92 var extensionUrl = 'chrome-extension://' + error.extensionId + '/'; | 117 var deleteButton = this.querySelector('.error-delete-button'); |
93 var viewDetailsLink = this.querySelector('.extension-error-view-details'); | 118 deleteButton.addEventListener('click', function(e) { |
| 119 this.dispatchEvent( |
| 120 new CustomEvent('deleteExtensionError', |
| 121 {bubbles: true, detail: this.error})); |
| 122 }.bind(this)); |
94 | 123 |
95 // If we cannot open the file source and there are no external frames in | 124 this.addEventListener('click', function(e) { |
96 // the stack, then there are no details to display. | 125 if (e.target != deleteButton) |
97 if (!extensions.ExtensionErrorOverlay.canShowOverlayForError( | 126 this.requestActive_(); |
98 error, extensionUrl)) { | 127 }.bind(this)); |
99 viewDetailsLink.hidden = true; | 128 this.addEventListener('keydown', function(e) { |
100 } else { | 129 if (e.keyIdentifier == 'Enter' && e.target != deleteButton) |
101 var stringId = extensionUrl.toLowerCase() == 'manifest.json' ? | 130 this.requestActive_(); |
102 'extensionErrorViewManifest' : 'extensionErrorViewDetails'; | 131 }); |
103 viewDetailsLink.textContent = loadTimeData.getString(stringId); | |
104 | 132 |
105 viewDetailsLink.addEventListener('click', function(e) { | 133 this.addFocusableElement(this); |
106 extensions.ExtensionErrorOverlay.getInstance().setErrorAndShowOverlay( | 134 this.addFocusableElement(this.querySelector('.error-delete-button')); |
107 error, extensionUrl); | 135 }, |
108 }); | |
109 | 136 |
110 this.addFocusableElement(viewDetailsLink); | 137 /** |
111 } | 138 * Bubble up an event to request to become active. |
| 139 * @private |
| 140 */ |
| 141 requestActive_: function() { |
| 142 this.dispatchEvent( |
| 143 new CustomEvent('highlightExtensionError', |
| 144 {bubbles: true, detail: this.error})); |
112 }, | 145 }, |
113 }; | 146 }; |
114 | 147 |
115 /** | 148 /** |
116 * A variable length list of runtime or manifest errors for a given extension. | 149 * A variable length list of runtime or manifest errors for a given extension. |
117 * @param {Array<(RuntimeError|ManifestError)>} errors The list of extension | 150 * @param {Array<(RuntimeError|ManifestError)>} errors The list of extension |
118 * errors with which to populate the list. | 151 * errors with which to populate the list. |
| 152 * @param {string} extensionId The id of the extension. |
119 * @constructor | 153 * @constructor |
120 * @extends {HTMLDivElement} | 154 * @extends {HTMLDivElement} |
121 */ | 155 */ |
122 function ExtensionErrorList(errors) { | 156 function ExtensionErrorList(errors, extensionId) { |
123 var div = cloneTemplate('extension-error-list'); | 157 var div = cloneTemplate('extension-error-list'); |
124 div.__proto__ = ExtensionErrorList.prototype; | 158 div.__proto__ = ExtensionErrorList.prototype; |
125 div.errors_ = errors; | 159 div.extensionId_ = extensionId; |
126 div.decorate(); | 160 div.decorate(errors); |
127 return div; | 161 return div; |
128 } | 162 } |
129 | 163 |
130 /** | |
131 * @private | |
132 * @const | |
133 * @type {number} | |
134 */ | |
135 ExtensionErrorList.MAX_ERRORS_TO_SHOW_ = 3; | |
136 | |
137 ExtensionErrorList.prototype = { | 164 ExtensionErrorList.prototype = { |
138 __proto__: HTMLDivElement.prototype, | 165 __proto__: HTMLDivElement.prototype, |
139 | 166 |
140 decorate: function() { | 167 /** |
| 168 * Initializes the extension error list. |
| 169 * @param {Array<(RuntimeError|ManifestError)>} errors The list of errors. |
| 170 */ |
| 171 decorate: function(errors) { |
| 172 /** |
| 173 * @private {!Array<(ManifestError|RuntimeError)>} |
| 174 */ |
| 175 this.errors_ = []; |
| 176 |
141 this.focusGrid_ = new cr.ui.FocusGrid(); | 177 this.focusGrid_ = new cr.ui.FocusGrid(); |
142 this.gridBoundary_ = this.querySelector('.extension-error-list-contents'); | 178 this.gridBoundary_ = this.querySelector('.extension-error-list-contents'); |
143 this.gridBoundary_.addEventListener('focus', this.onFocus_.bind(this)); | 179 this.gridBoundary_.addEventListener('focus', this.onFocus_.bind(this)); |
144 this.gridBoundary_.addEventListener('focusin', | 180 this.gridBoundary_.addEventListener('focusin', |
145 this.onFocusin_.bind(this)); | 181 this.onFocusin_.bind(this)); |
| 182 errors.forEach(this.addError_, this); |
| 183 |
| 184 this.addEventListener('highlightExtensionError', function(e) { |
| 185 this.setActiveErrorNode_(e.target); |
| 186 }); |
| 187 this.addEventListener('deleteExtensionError', function(e) { |
| 188 this.removeError_(e.detail); |
| 189 }); |
| 190 |
| 191 this.querySelector('#extension-error-list-clear').addEventListener( |
| 192 'click', function(e) { |
| 193 this.clear(true); |
| 194 }.bind(this)); |
| 195 |
| 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); |
| 210 |
| 211 chrome.developerPrivate.onItemStateChanged.addListener( |
| 212 this.onItemStateChangedListener_); |
| 213 |
| 214 /** |
| 215 * The active error element in the list. |
| 216 * @private {?} |
| 217 */ |
| 218 this.activeError_ = null; |
| 219 |
| 220 this.setActiveError(0); |
| 221 }, |
| 222 |
| 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); |
| 231 var focusRow = new ExtensionError(error, this.gridBoundary_); |
| 232 this.gridBoundary_.appendChild(document.createElement('li')). |
| 233 appendChild(focusRow); |
| 234 this.focusGrid_.addRow(focusRow); |
| 235 }, |
| 236 |
| 237 /** |
| 238 * Removes an error from the list. |
| 239 * @param {(RuntimeError|ManifestError)} error The error to remove. |
| 240 * @private |
| 241 */ |
| 242 removeError_: function(error) { |
| 243 var index = 0; |
| 244 for (; index < this.errors_.length; ++index) { |
| 245 if (this.errors_[index].id == error.id) |
| 246 break; |
| 247 } |
| 248 assert(index != this.errors_.length); |
| 249 var errorList = this.querySelector('.extension-error-list-contents'); |
| 250 |
| 251 var wasActive = |
| 252 this.activeError_ && this.activeError_.error.id == error.id; |
| 253 |
| 254 this.errors_.splice(index, 1); |
| 255 var listElement = errorList.children[index]; |
| 256 listElement.parentNode.removeChild(listElement); |
| 257 |
| 258 if (wasActive) { |
| 259 index = Math.min(index, this.errors_.length - 1); |
| 260 this.setActiveError(index); // Gracefully handles the -1 case. |
| 261 } |
| 262 |
| 263 chrome.developerPrivate.deleteExtensionErrors({ |
| 264 extensionId: error.extensionId, |
| 265 errorIds: [error.id] |
| 266 }); |
| 267 |
| 268 if (this.errors_.length == 0) |
| 269 this.querySelector('#no-errors-span').hidden = false; |
| 270 }, |
| 271 |
| 272 /** |
| 273 * Updates the list of errors. |
| 274 * @param {!Array<(ManifestError|RuntimeError)>} newErrors The new list of |
| 275 * errors. |
| 276 * @private |
| 277 */ |
| 278 updateErrors_: function(newErrors) { |
146 this.errors_.forEach(function(error) { | 279 this.errors_.forEach(function(error) { |
147 if (idIsValid(error.extensionId)) { | 280 if (findErrorById(newErrors, error.id) == -1) |
148 var focusRow = new ExtensionError(error, this.gridBoundary_); | 281 this.removeError_(error); |
149 this.gridBoundary_.appendChild( | |
150 document.createElement('li')).appendChild(focusRow); | |
151 this.focusGrid_.addRow(focusRow); | |
152 } | |
153 }, this); | 282 }, this); |
154 this.focusGrid_.ensureRowActive(); | 283 newErrors.forEach(function(error) { |
155 | 284 var index = findErrorById(this.errors_, error.id); |
156 var numShowing = this.focusGrid_.rows.length; | 285 if (index == -1) |
157 if (numShowing > ExtensionErrorList.MAX_ERRORS_TO_SHOW_) | 286 this.addError_(error); |
158 this.initShowMoreLink_(); | 287 else |
159 }, | 288 this.errors_[index] = error; // Update the existing reference. |
160 | 289 }, this); |
161 /** | 290 }, |
162 * @return {?Element} The element that toggles between show more and show | 291 |
163 * less, or null if it's hidden. Button will be hidden if there are less | 292 /** |
164 * errors than |MAX_ERRORS_TO_SHOW_|. | 293 * Called when the list is being removed. |
165 */ | 294 */ |
166 getToggleElement: function() { | 295 onRemoved: function() { |
167 return this.querySelector( | 296 chrome.developerPrivate.onItemStateChanged.removeListener( |
168 '.extension-error-list-show-more [is="action-link"]:not([hidden])'); | 297 this.onItemStateChangedListener_); |
169 }, | 298 this.clear(false); |
170 | 299 }, |
171 /** @return {!Element} The element containing the list of errors. */ | 300 |
172 getErrorListElement: function() { | 301 /** |
173 return this.gridBoundary_; | 302 * Sets the active error in the list. |
| 303 * @param {number} index The index to set to be active. |
| 304 */ |
| 305 setActiveError: function(index) { |
| 306 var errorList = this.querySelector('.extension-error-list-contents'); |
| 307 var item = errorList.children[index]; |
| 308 this.setActiveErrorNode_( |
| 309 item ? item.querySelector('.extension-error-metadata') : null); |
| 310 var node = null; |
| 311 if (index >= 0 && index < errorList.children.length) { |
| 312 node = errorList.children[index].querySelector( |
| 313 '.extension-error-metadata'); |
| 314 } |
| 315 this.setActiveErrorNode_(node); |
| 316 }, |
| 317 |
| 318 /** |
| 319 * Clears the list of all errors. |
| 320 * @param {boolean} deleteErrors Whether or not the errors should be deleted |
| 321 * on the backend. |
| 322 */ |
| 323 clear: function(deleteErrors) { |
| 324 if (this.errors_.length == 0) |
| 325 return; |
| 326 |
| 327 if (deleteErrors) { |
| 328 var ids = this.errors_.map(function(error) { return error.id; }); |
| 329 chrome.developerPrivate.deleteExtensionErrors({ |
| 330 extensionId: this.extensionId_, |
| 331 errorIds: ids |
| 332 }); |
| 333 } |
| 334 |
| 335 this.setActiveErrorNode_(null); |
| 336 this.errors_.length = 0; |
| 337 var errorList = this.querySelector('.extension-error-list-contents'); |
| 338 while (errorList.firstChild) |
| 339 errorList.removeChild(errorList.firstChild); |
| 340 }, |
| 341 |
| 342 /** |
| 343 * Sets the active error in the list. |
| 344 * @param {?} node The error to make active. |
| 345 * @private |
| 346 */ |
| 347 setActiveErrorNode_: function(node) { |
| 348 if (this.activeError_) |
| 349 this.activeError_.classList.remove('extension-error-active'); |
| 350 |
| 351 if (node) |
| 352 node.classList.add('extension-error-active'); |
| 353 |
| 354 this.activeError_ = node; |
| 355 |
| 356 this.dispatchEvent( |
| 357 new CustomEvent('activeExtensionErrorChanged', |
| 358 {bubbles: true, detail: node ? node.error : null})); |
174 }, | 359 }, |
175 | 360 |
176 /** | 361 /** |
177 * The grid should not be focusable once it or an element inside it is | 362 * The grid should not be focusable once it or an element inside it is |
178 * focused. This is necessary to allow tabbing out of the grid in reverse. | 363 * focused. This is necessary to allow tabbing out of the grid in reverse. |
179 * @private | 364 * @private |
180 */ | 365 */ |
181 onFocusin_: function() { | 366 onFocusin_: function() { |
182 this.gridBoundary_.tabIndex = -1; | 367 this.gridBoundary_.tabIndex = -1; |
183 }, | 368 }, |
184 | 369 |
185 /** | 370 /** |
186 * Focus the first focusable row when tabbing into the grid for the | 371 * Focus the first focusable row when tabbing into the grid for the |
187 * first time. | 372 * first time. |
188 * @private | 373 * @private |
189 */ | 374 */ |
190 onFocus_: function() { | 375 onFocus_: function() { |
191 var activeRow = this.gridBoundary_.querySelector('.focus-row-active'); | 376 var activeRow = this.gridBoundary_.querySelector('.focus-row-active'); |
192 var toggleButton = this.getToggleElement(); | |
193 | |
194 if (toggleButton && !toggleButton.isShowingAll) { | |
195 var rows = this.focusGrid_.rows; | |
196 assert(rows.length > ExtensionErrorList.MAX_ERRORS_TO_SHOW_); | |
197 | |
198 var firstVisible = rows.length - ExtensionErrorList.MAX_ERRORS_TO_SHOW_; | |
199 if (rows.indexOf(activeRow) < firstVisible) | |
200 activeRow = rows[firstVisible]; | |
201 } else if (!activeRow) { | |
202 activeRow = this.focusGrid_.rows[0]; | |
203 } | |
204 | |
205 activeRow.getEquivalentElement(null).focus(); | 377 activeRow.getEquivalentElement(null).focus(); |
206 }, | 378 }, |
207 | |
208 /** | |
209 * Initialize the "Show More" link for the error list. If there are more | |
210 * than |MAX_ERRORS_TO_SHOW_| errors in the list. | |
211 * @private | |
212 */ | |
213 initShowMoreLink_: function() { | |
214 var link = this.querySelector( | |
215 '.extension-error-list-show-more [is="action-link"]'); | |
216 link.hidden = false; | |
217 link.isShowingAll = false; | |
218 | |
219 var listContents = this.querySelector('.extension-error-list-contents'); | |
220 | |
221 // TODO(dbeam/kalman): trade all this transition voodoo for .animate()? | |
222 listContents.addEventListener('webkitTransitionEnd', function(e) { | |
223 if (listContents.classList.contains('deactivating')) | |
224 listContents.classList.remove('deactivating', 'active'); | |
225 else | |
226 listContents.classList.add('scrollable'); | |
227 }); | |
228 | |
229 link.addEventListener('click', function(e) { | |
230 // Needs to be enabled in case the focused row is now hidden. | |
231 this.gridBoundary_.tabIndex = 0; | |
232 | |
233 link.isShowingAll = !link.isShowingAll; | |
234 | |
235 var message = link.isShowingAll ? 'extensionErrorsShowFewer' : | |
236 'extensionErrorsShowMore'; | |
237 link.textContent = loadTimeData.getString(message); | |
238 | |
239 // Disable scrolling while transitioning. If the element is active, | |
240 // scrolling is enabled when the transition ends. | |
241 listContents.classList.remove('scrollable'); | |
242 | |
243 if (link.isShowingAll) { | |
244 listContents.classList.add('active'); | |
245 listContents.classList.remove('deactivating'); | |
246 } else { | |
247 listContents.classList.add('deactivating'); | |
248 } | |
249 }.bind(this)); | |
250 } | |
251 }; | 379 }; |
252 | 380 |
253 return { | 381 return { |
254 ExtensionErrorList: ExtensionErrorList | 382 ExtensionErrorList: ExtensionErrorList |
255 }; | 383 }; |
256 }); | 384 }); |
OLD | NEW |