// 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.keyIdentifier == '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 }; });