| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| cr.define('extensions', function() { |
| 'use strict'; |
| |
| /** |
| * Clone a template within the extension error template collection. |
| * @param {string} templateName The class name of the template to clone. |
| * @return {HTMLElement} The clone of the template. |
| */ |
| function cloneTemplate(templateName) { |
| return /** @type {HTMLElement} */($('template-collection-extension-error'). |
| querySelector('.' + templateName).cloneNode(true)); |
| } |
| |
| /** |
| * Checks that an Extension ID follows the proper format (i.e., is 32 |
| * characters long, is lowercase, and contains letters in the range [a, p]). |
| * @param {string} id The Extension ID to test. |
| * @return {boolean} Whether or not the ID is valid. |
| */ |
| function idIsValid(id) { |
| return /^[a-p]{32}$/.test(id); |
| } |
| |
| /** |
| * @param {!Array<(ManifestError|RuntimeError)>} errors |
| * @param {number} id |
| * @return {number} The index of the error with |id|, or -1 if not found. |
| */ |
| function findErrorById(errors, id) { |
| for (var i = 0; i < errors.length; ++i) { |
| if (errors[i].id == id) |
| return i; |
| } |
| return -1; |
| } |
| |
| /** |
| * Creates a new ExtensionError HTMLElement; this is used to show a |
| * notification to the user when an error is caused by an extension. |
| * @param {(RuntimeError|ManifestError)} error The error the element should |
| * represent. |
| * @constructor |
| * @extends {HTMLElement} |
| */ |
| function ExtensionError(error) { |
| var div = cloneTemplate('extension-error-metadata'); |
| div.__proto__ = ExtensionError.prototype; |
| div.decorate(error); |
| return div; |
| } |
| |
| ExtensionError.prototype = { |
| __proto__: HTMLElement.prototype, |
| |
| /** |
| * @param {(RuntimeError|ManifestError)} error The error the element should |
| * represent. |
| * @private |
| */ |
| decorate: function(error) { |
| /** |
| * The backing error. |
| * @type {(ManifestError|RuntimeError)} |
| */ |
| this.error = error; |
| var iconAltTextKey = 'extensionLogLevelWarn'; |
| |
| // Add an additional class for the severity level. |
| if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) { |
| switch (error.severity) { |
| case chrome.developerPrivate.ErrorLevel.LOG: |
| this.classList.add('extension-error-severity-info'); |
| iconAltTextKey = 'extensionLogLevelInfo'; |
| break; |
| case chrome.developerPrivate.ErrorLevel.WARN: |
| this.classList.add('extension-error-severity-warning'); |
| break; |
| case chrome.developerPrivate.ErrorLevel.ERROR: |
| this.classList.add('extension-error-severity-fatal'); |
| iconAltTextKey = 'extensionLogLevelError'; |
| break; |
| default: |
| assertNotReached(); |
| } |
| } else { |
| // We classify manifest errors as "warnings". |
| this.classList.add('extension-error-severity-warning'); |
| } |
| |
| var iconNode = document.createElement('img'); |
| iconNode.className = 'extension-error-icon'; |
| iconNode.alt = loadTimeData.getString(iconAltTextKey); |
| this.insertBefore(iconNode, this.firstChild); |
| |
| var messageSpan = this.querySelector('.extension-error-message'); |
| messageSpan.textContent = error.message; |
| |
| var deleteButton = this.querySelector('.error-delete-button'); |
| deleteButton.addEventListener('click', function(e) { |
| this.dispatchEvent( |
| new CustomEvent('deleteExtensionError', |
| {bubbles: true, detail: this.error})); |
| }.bind(this)); |
| |
| this.addEventListener('click', function(e) { |
| if (e.target != deleteButton) |
| this.requestActive_(); |
| }.bind(this)); |
| |
| this.addEventListener('keydown', function(e) { |
| if (e.key == 'Enter' && e.target != deleteButton) |
| this.requestActive_(); |
| }); |
| }, |
| |
| /** |
| * Bubble up an event to request to become active. |
| * @private |
| */ |
| requestActive_: function() { |
| this.dispatchEvent( |
| new CustomEvent('highlightExtensionError', |
| {bubbles: true, detail: this.error})); |
| }, |
| }; |
| |
| /** |
| * A variable length list of runtime or manifest errors for a given extension. |
| * @param {Array<(RuntimeError|ManifestError)>} errors The list of extension |
| * errors with which to populate the list. |
| * @param {string} extensionId The id of the extension. |
| * @constructor |
| * @extends {HTMLDivElement} |
| */ |
| function ExtensionErrorList(errors, extensionId) { |
| var div = cloneTemplate('extension-error-list'); |
| div.__proto__ = ExtensionErrorList.prototype; |
| div.extensionId_ = extensionId; |
| div.decorate(errors); |
| return div; |
| } |
| |
| /** |
| * @param {!Element} root |
| * @param {?Element} boundary |
| * @constructor |
| * @extends {cr.ui.FocusRow} |
| */ |
| ExtensionErrorList.FocusRow = function(root, boundary) { |
| cr.ui.FocusRow.call(this, root, boundary); |
| |
| this.addItem('message', '.extension-error-message'); |
| this.addItem('delete', '.error-delete-button'); |
| }; |
| |
| ExtensionErrorList.FocusRow.prototype = { |
| __proto__: cr.ui.FocusRow.prototype, |
| }; |
| |
| ExtensionErrorList.prototype = { |
| __proto__: HTMLDivElement.prototype, |
| |
| /** |
| * Initializes the extension error list. |
| * @param {Array<(RuntimeError|ManifestError)>} errors The list of errors. |
| */ |
| decorate: function(errors) { |
| /** @private {!Array<(ManifestError|RuntimeError)>} */ |
| this.errors_ = []; |
| |
| /** @private {!cr.ui.FocusGrid} */ |
| this.focusGrid_ = new cr.ui.FocusGrid(); |
| |
| /** @private {Element} */ |
| this.listContents_ = this.querySelector('.extension-error-list-contents'); |
| |
| errors.forEach(this.addError_, this); |
| |
| this.focusGrid_.ensureRowActive(); |
| |
| this.addEventListener('highlightExtensionError', function(e) { |
| this.setActiveErrorNode_(e.target); |
| }); |
| this.addEventListener('deleteExtensionError', function(e) { |
| this.removeError_(e.detail); |
| }); |
| |
| this.querySelector('#extension-error-list-clear').addEventListener( |
| 'click', function(e) { |
| this.clear(true); |
| }.bind(this)); |
| |
| /** |
| * The callback for the extension changed event. |
| * @private {function(chrome.developerPrivate.EventData):void} |
| */ |
| this.onItemStateChangedListener_ = function(data) { |
| var type = chrome.developerPrivate.EventType; |
| if ((data.event_type == type.ERRORS_REMOVED || |
| data.event_type == type.ERROR_ADDED) && |
| data.extensionInfo.id == this.extensionId_) { |
| var newErrors = data.extensionInfo.runtimeErrors.concat( |
| data.extensionInfo.manifestErrors); |
| this.updateErrors_(newErrors); |
| } |
| }.bind(this); |
| |
| chrome.developerPrivate.onItemStateChanged.addListener( |
| this.onItemStateChangedListener_); |
| |
| /** |
| * The active error element in the list. |
| * @private {?} |
| */ |
| this.activeError_ = null; |
| |
| this.setActiveError(0); |
| }, |
| |
| /** |
| * Adds an error to the list. |
| * @param {(RuntimeError|ManifestError)} error The error to add. |
| * @private |
| */ |
| addError_: function(error) { |
| this.querySelector('#no-errors-span').hidden = true; |
| this.errors_.push(error); |
| |
| var extensionError = new ExtensionError(error); |
| this.listContents_.appendChild(extensionError); |
| |
| this.focusGrid_.addRow( |
| new ExtensionErrorList.FocusRow(extensionError, this.listContents_)); |
| }, |
| |
| /** |
| * Removes an error from the list. |
| * @param {(RuntimeError|ManifestError)} error The error to remove. |
| * @private |
| */ |
| removeError_: function(error) { |
| var index = 0; |
| for (; index < this.errors_.length; ++index) { |
| if (this.errors_[index].id == error.id) |
| break; |
| } |
| assert(index != this.errors_.length); |
| var errorList = this.querySelector('.extension-error-list-contents'); |
| |
| var wasActive = |
| this.activeError_ && this.activeError_.error.id == error.id; |
| |
| this.errors_.splice(index, 1); |
| var listElement = errorList.children[index]; |
| |
| var focusRow = this.focusGrid_.getRowForRoot(listElement); |
| this.focusGrid_.removeRow(focusRow); |
| this.focusGrid_.ensureRowActive(); |
| focusRow.destroy(); |
| |
| // TODO(dbeam): in a world where this UI is actually used, we should |
| // probably move the focus before removing |listElement|. |
| listElement.parentNode.removeChild(listElement); |
| |
| if (wasActive) { |
| index = Math.min(index, this.errors_.length - 1); |
| this.setActiveError(index); // Gracefully handles the -1 case. |
| } |
| |
| chrome.developerPrivate.deleteExtensionErrors({ |
| extensionId: error.extensionId, |
| errorIds: [error.id] |
| }); |
| |
| if (this.errors_.length == 0) |
| this.querySelector('#no-errors-span').hidden = false; |
| }, |
| |
| /** |
| * Updates the list of errors. |
| * @param {!Array<(ManifestError|RuntimeError)>} newErrors The new list of |
| * errors. |
| * @private |
| */ |
| updateErrors_: function(newErrors) { |
| this.errors_.forEach(function(error) { |
| if (findErrorById(newErrors, error.id) == -1) |
| this.removeError_(error); |
| }, this); |
| newErrors.forEach(function(error) { |
| var index = findErrorById(this.errors_, error.id); |
| if (index == -1) |
| this.addError_(error); |
| else |
| this.errors_[index] = error; // Update the existing reference. |
| }, this); |
| }, |
| |
| /** |
| * Called when the list is being removed. |
| */ |
| onRemoved: function() { |
| chrome.developerPrivate.onItemStateChanged.removeListener( |
| this.onItemStateChangedListener_); |
| this.clear(false); |
| }, |
| |
| /** |
| * Sets the active error in the list. |
| * @param {number} index The index to set to be active. |
| */ |
| setActiveError: function(index) { |
| var errorList = this.querySelector('.extension-error-list-contents'); |
| var item = errorList.children[index]; |
| this.setActiveErrorNode_( |
| item ? item.querySelector('.extension-error-metadata') : null); |
| var node = null; |
| if (index >= 0 && index < errorList.children.length) { |
| node = errorList.children[index].querySelector( |
| '.extension-error-metadata'); |
| } |
| this.setActiveErrorNode_(node); |
| }, |
| |
| /** |
| * Clears the list of all errors. |
| * @param {boolean} deleteErrors Whether or not the errors should be deleted |
| * on the backend. |
| */ |
| clear: function(deleteErrors) { |
| if (this.errors_.length == 0) |
| return; |
| |
| if (deleteErrors) { |
| var ids = this.errors_.map(function(error) { return error.id; }); |
| chrome.developerPrivate.deleteExtensionErrors({ |
| extensionId: this.extensionId_, |
| errorIds: ids |
| }); |
| } |
| |
| this.setActiveErrorNode_(null); |
| this.errors_.length = 0; |
| var errorList = this.querySelector('.extension-error-list-contents'); |
| while (errorList.firstChild) |
| errorList.removeChild(errorList.firstChild); |
| }, |
| |
| /** |
| * Sets the active error in the list. |
| * @param {?} node The error to make active. |
| * @private |
| */ |
| setActiveErrorNode_: function(node) { |
| if (this.activeError_) |
| this.activeError_.classList.remove('extension-error-active'); |
| |
| if (node) |
| node.classList.add('extension-error-active'); |
| |
| this.activeError_ = node; |
| |
| this.dispatchEvent( |
| new CustomEvent('activeExtensionErrorChanged', |
| {bubbles: true, detail: node ? node.error : null})); |
| }, |
| }; |
| |
| return { |
| ExtensionErrorList: ExtensionErrorList |
| }; |
| }); |