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