diff options
author | Allan Sandfeld Jensen <allan.jensen@theqtcompany.com> | 2015-06-18 14:10:49 +0200 |
---|---|---|
committer | Oswald Buddenhagen <oswald.buddenhagen@theqtcompany.com> | 2015-06-18 13:53:24 +0000 |
commit | 813fbf95af77a531c57a8c497345ad2c61d475b3 (patch) | |
tree | 821b2c8de8365f21b6c9ba17a236fb3006a1d506 /chromium/chrome/browser/resources/extensions | |
parent | af6588f8d723931a298c995fa97259bb7f7deb55 (diff) |
BASELINE: Update chromium to 44.0.2403.47
Change-Id: Ie056fedba95cf5e5c76b30c4b2c80fca4764aa2f
Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@theqtcompany.com>
Diffstat (limited to 'chromium/chrome/browser/resources/extensions')
26 files changed, 2098 insertions, 1233 deletions
diff --git a/chromium/chrome/browser/resources/extensions/chromeos/kiosk_app_list.js b/chromium/chrome/browser/resources/extensions/chromeos/kiosk_app_list.js index 28b0b3a921f..d92784097d7 100644 --- a/chromium/chrome/browser/resources/extensions/chromeos/kiosk_app_list.js +++ b/chromium/chrome/browser/resources/extensions/chromeos/kiosk_app_list.js @@ -53,7 +53,7 @@ cr.define('extensions', function() { /** * Loads the given list of apps. - * @param {!Array.<!Object>} apps An array of app info objects. + * @param {!Array<!Object>} apps An array of app info objects. */ setApps: function(apps) { this.dataModel = new ArrayDataModel(apps); diff --git a/chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.css b/chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.css index 33c752bb100..18b0168ed94 100644 --- a/chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.css +++ b/chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.css @@ -26,8 +26,8 @@ list .row-delete-button { background-color: transparent; /* TODO(stuartmorgan): Replace with real images once they are available. */ background-image: -webkit-image-set( - url('../../../../../ui/resources/default_100_percent/close_2.png') 1x, - url('../../../../../ui/resources/default_200_percent/close_2.png') 2x); + url(../../../../../ui/resources/default_100_percent/close_2.png) 1x, + url(../../../../../ui/resources/default_200_percent/close_2.png) 2x); border: none; display: block; height: 16px; @@ -46,17 +46,17 @@ list .row-delete-button[disabled] { list .row-delete-button:hover { background-image: -webkit-image-set( - url('../../../../../ui/resources/default_100_percent/close_2_hover.png') + url(../../../../../ui/resources/default_100_percent/close_2_hover.png) 1x, - url('../../../../../ui/resources/default_200_percent/close_2_hover.png') + url(../../../../../ui/resources/default_200_percent/close_2_hover.png) 2x); } list .row-delete-button:active { background-image: -webkit-image-set( - url('../../../../../ui/resources/default_100_percent/close_2_pressed.png') + url(../../../../../ui/resources/default_100_percent/close_2_pressed.png) 1x, - url('../../../../../ui/resources/default_200_percent/close_2_pressed.png') + url(../../../../../ui/resources/default_200_percent/close_2_pressed.png) 2x); } @@ -118,7 +118,7 @@ list .row-delete-button:active { } .kiosk-app-icon.spinner { - background-image: url('chrome://resources/images/spinner.svg') !important; + background-image: url(chrome://resources/images/throbber.svg) !important; } .kiosk-app-name, diff --git a/chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.js b/chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.js index 30187087fff..4697bcaef6e 100644 --- a/chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.js +++ b/chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.js @@ -100,7 +100,7 @@ cr.define('extensions', function() { /** * Sets apps to be displayed in kiosk-app-list. - * @param {!Object.<{apps: !Array.<AppDict>, disableBailout: boolean, + * @param {!Object<{apps: !Array<AppDict>, disableBailout: boolean, * hasAutoLaunchApp: boolean}>} settings An object containing an array of * app info objects and disable bailout shortcut flag. */ diff --git a/chromium/chrome/browser/resources/extensions/compiled_resources.gyp b/chromium/chrome/browser/resources/extensions/compiled_resources.gyp index 5ff1fb854e8..23f5dd0f3d3 100644 --- a/chromium/chrome/browser/resources/extensions/compiled_resources.gyp +++ b/chromium/chrome/browser/resources/extensions/compiled_resources.gyp @@ -15,6 +15,9 @@ '../../../../ui/webui/resources/js/cr/ui.js', '../../../../ui/webui/resources/js/cr/ui/alert_overlay.js', '../../../../ui/webui/resources/js/cr/ui/array_data_model.js', + '../../../../ui/webui/resources/js/cr/ui/bubble.js', + '../../../../ui/webui/resources/js/cr/ui/bubble_button.js', + '../../../../ui/webui/resources/js/cr/ui/controlled_indicator.js', '../../../../ui/webui/resources/js/cr/ui/drag_wrapper.js', '../../../../ui/webui/resources/js/cr/ui/focus_manager.js', '../../../../ui/webui/resources/js/cr/ui/focus_outline_manager.js', @@ -23,10 +26,15 @@ '../../../../ui/webui/resources/js/cr/ui/list_selection_controller.js', '../../../../ui/webui/resources/js/cr/ui/list_selection_model.js', '../../../../ui/webui/resources/js/cr/ui/overlay.js', + '../../../../ui/webui/resources/js/event_tracker.js', '../../../../ui/webui/resources/js/load_time_data.js', '../../../../ui/webui/resources/js/util.js', ], - 'externs': ['<(CLOSURE_DIR)/externs/chrome_send_externs.js'], + 'externs': [ + '<(CLOSURE_DIR)/externs/chrome_extensions.js', + '<(CLOSURE_DIR)/externs/chrome_send_externs.js', + '<(CLOSURE_DIR)/externs/developer_private.js', + ], }, 'includes': ['../../../../third_party/closure_compiler/compile_js.gypi'], } diff --git a/chromium/chrome/browser/resources/extensions/extension_code.js b/chromium/chrome/browser/resources/extensions/extension_code.js index 826625422ce..255909d6352 100644 --- a/chromium/chrome/browser/resources/extensions/extension_code.js +++ b/chromium/chrome/browser/resources/extensions/extension_code.js @@ -30,7 +30,7 @@ cr.define('extensions', function() { /** * Populate the content area of the code div with the given code. This will * highlight the erroneous section (if any). - * @param {ExtensionHighlight} code The 'highlight' strings represent the + * @param {?ExtensionHighlight} code The 'highlight' strings represent the * three portions of the file's content to display - the portion which * is most relevant and should be emphasized (highlight), and the parts * both before and after this portion. The title is the error message, @@ -43,19 +43,20 @@ cr.define('extensions', function() { // Clear any remnant content, so we don't have multiple code listed. this.clear(); - var sourceDiv = document.createElement('div'); - sourceDiv.classList.add('extension-code-source'); - this.appendChild(sourceDiv); - // If there's no code, then display an appropriate message. if (!code || (!code.highlight && !code.beforeHighlight && !code.afterHighlight)) { var span = document.createElement('span'); + span.classList.add('extension-code-empty'); span.textContent = emptyMessage; - sourceDiv.appendChild(span); + this.appendChild(span); return; } + var sourceDiv = document.createElement('div'); + sourceDiv.classList.add('extension-code-source'); + this.appendChild(sourceDiv); + var lineCount = 0; var createSpan = function(source, isHighlighted) { lineCount += source.split('\n').length - 1; diff --git a/chromium/chrome/browser/resources/extensions/extension_command_list.js b/chromium/chrome/browser/resources/extensions/extension_command_list.js index 1f92c19cfda..0a184dc8330 100644 --- a/chromium/chrome/browser/resources/extensions/extension_command_list.js +++ b/chromium/chrome/browser/resources/extensions/extension_command_list.js @@ -39,6 +39,7 @@ cr.define('options', function() { /** @const */ var keyPageUp = 33; /** @const */ var keyPeriod = 190; /** @const */ var keyRight = 39; + /** @const */ var keySpace = 32; /** @const */ var keyTab = 9; /** @const */ var keyUp = 38; @@ -74,6 +75,7 @@ cr.define('options', function() { keyCode == keyPageUp || keyCode == keyPeriod || keyCode == keyRight || + keyCode == keySpace || keyCode == keyTab || keyCode == keyUp || (keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) || @@ -87,62 +89,66 @@ cr.define('options', function() { * @return {string} The keystroke as a string. */ function keystrokeToString(event) { - var output = ''; + var output = []; if (cr.isMac && event.metaKey) - output = 'Command+'; + output.push('Command'); + if (cr.isChromeOS && event.metaKey) + output.push('Search'); if (event.ctrlKey) - output = 'Ctrl+'; + output.push('Ctrl'); if (!event.ctrlKey && event.altKey) - output += 'Alt+'; + output.push('Alt'); if (event.shiftKey) - output += 'Shift+'; + output.push('Shift'); var keyCode = event.keyCode; if (validChar(keyCode)) { if ((keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) || (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0))) { - output += String.fromCharCode('A'.charCodeAt(0) + keyCode - 65); + output.push(String.fromCharCode('A'.charCodeAt(0) + keyCode - 65)); } else { switch (keyCode) { case keyComma: - output += 'Comma'; break; + output.push('Comma'); break; case keyDel: - output += 'Delete'; break; + output.push('Delete'); break; case keyDown: - output += 'Down'; break; + output.push('Down'); break; case keyEnd: - output += 'End'; break; + output.push('End'); break; case keyHome: - output += 'Home'; break; + output.push('Home'); break; case keyIns: - output += 'Insert'; break; + output.push('Insert'); break; case keyLeft: - output += 'Left'; break; + output.push('Left'); break; case keyMediaNextTrack: - output += 'MediaNextTrack'; break; + output.push('MediaNextTrack'); break; case keyMediaPlayPause: - output += 'MediaPlayPause'; break; + output.push('MediaPlayPause'); break; case keyMediaPrevTrack: - output += 'MediaPrevTrack'; break; + output.push('MediaPrevTrack'); break; case keyMediaStop: - output += 'MediaStop'; break; + output.push('MediaStop'); break; case keyPageDown: - output += 'PageDown'; break; + output.push('PageDown'); break; case keyPageUp: - output += 'PageUp'; break; + output.push('PageUp'); break; case keyPeriod: - output += 'Period'; break; + output.push('Period'); break; case keyRight: - output += 'Right'; break; + output.push('Right'); break; + case keySpace: + output.push('Space'); break; case keyTab: - output += 'Tab'; break; + output.push('Tab'); break; case keyUp: - output += 'Up'; break; + output.push('Up'); break; } } } - return output; + return output.join('+'); } /** @@ -174,6 +180,7 @@ cr.define('options', function() { */ function hasModifier(event, countShiftAsModifier) { return event.ctrlKey || event.altKey || (cr.isMac && event.metaKey) || + (cr.isChromeOS && event.metaKey) || (countShiftAsModifier && event.shiftKey); } @@ -205,7 +212,6 @@ cr.define('options', function() { */ capturingElement_: null, - /** @override */ decorate: function() { this.textContent = ''; @@ -437,7 +443,8 @@ cr.define('options', function() { // you have a valid combination, we won't change it until the next // KeyDown message arrives). if (!this.currentKeyEvent_ || !validChar(this.currentKeyEvent_.keyCode)) { - if (!event.ctrlKey && !event.altKey) { + if (!event.ctrlKey && !event.altKey || + ((cr.isMac || cr.isChromeOS) && !event.metaKey)) { // If neither Ctrl nor Alt is pressed then it is not a valid shortcut. // That means we're back at the starting point so we should restart // capture. diff --git a/chromium/chrome/browser/resources/extensions/extension_commands_overlay.js b/chromium/chrome/browser/resources/extensions/extension_commands_overlay.js index 7d2546d2f81..a96ef44780b 100644 --- a/chromium/chrome/browser/resources/extensions/extension_commands_overlay.js +++ b/chromium/chrome/browser/resources/extensions/extension_commands_overlay.js @@ -51,7 +51,7 @@ cr.define('extensions', function() { /** * Called by the dom_ui_ to re-populate the page with data representing * the current state of extension commands. - * @param {!{commands: Array.<{name: string, id: string, commands: ?Array}>}} + * @param {!{commands: Array<{name: string, id: string, commands: ?Array}>}} * extensionsData */ ExtensionCommandsOverlay.returnExtensionsData = function(extensionsData) { diff --git a/chromium/chrome/browser/resources/extensions/extension_error.css b/chromium/chrome/browser/resources/extensions/extension_error.css index db8b9f9a3a5..d6514114bec 100644 --- a/chromium/chrome/browser/resources/extensions/extension_error.css +++ b/chromium/chrome/browser/resources/extensions/extension_error.css @@ -2,96 +2,108 @@ * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ +.extension-error-list-heading { + align-content: flex-start; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 3px; +} + +.extension-error-list-heading span { + font-weight: bold; +} + .extension-error-list a { cursor: pointer; } .extension-error-list-contents { - -webkit-transition: max-height 150ms; + -webkit-padding-start: 0; + cursor: pointer; list-style-type: none; - overflow-y: hidden; + margin-bottom: 0; + margin-top: 0; } -.extension-error-list-contents.active { - max-height: 200px; -} - -.extension-error-list-contents, -.extension-error-list-contents.deactivating { - /* Simply toggling .active on and off doesn't transition both ways as it - * changes the display type of some li elements. To fix this, .deactivating - * is added while the list is closing to change only the max-height. */ - max-height: 50px; +#no-errors-span { + -webkit-margin-start: 10px; } .extension-error-list-contents.scrollable { overflow-y: auto; } -/* These next three rules hide all except for the most recent three entries in - * the list, unless the list is active. */ -.extension-error-list-contents li { - display: none; -} -.extension-error-list-contents.active li, -.extension-error-list ul li:nth-last-child(-n + 3) { - display: initial; -} - -.extension-error-list-contents { - -webkit-padding-start: 20px; +.extension-error-list-contents .extension-error-metadata:hover { + background-color: #eee; } -.extension-error-list-show-more { - text-align: center; - width: 100%; -} - -.extension-error-list-show-more button { - width: auto; +.extension-error-list-contents + .extension-error-metadata.extension-error-active { + background-color: rgba(0, 100, 255, 0.1); } .extension-error-metadata { -webkit-padding-end: 1px; + -webkit-padding-start: 3px; display: flex; flex-direction: row; - margin-bottom: 1px; - margin-top: 1px; } .extension-error-icon { -webkit-margin-end: 3px; - -webkit-margin-start: 3px; height: 15px; - vertical-align: middle; width: 15px; } .extension-error-message { + -webkit-margin-end: 15px; flex: 1; + margin-bottom: 0; + margin-top: 0; overflow: hidden; - text-overflow: ellipsis; - vertical-align: middle; - white-space: nowrap; +} + +.extension-error-metadata { + align-items: center; + display: flex; +} + +.extension-error-metadata > .error-delete-button { + background: url(chrome://theme/IDR_CLOSE_DIALOG) center no-repeat; + height: 14px; + opacity: 0.6; + width: 14px; +} + +.extension-error-metadata > .error-delete-button:hover { + opacity: 0.8; +} + +.extension-error-metadata > .error-delete-button:active { + opacity: 1.0; } .extension-error-severity-info .extension-error-message { color: #333; } -.extension-error-severity-info .extension-error-icon { - content: url('extension_error_severity_info.png'); +.extension-error-severity-info .extension-error-icon, +.extension-error-info-icon { + content: url(extension_error_severity_info.png); } .extension-error-severity-warning .extension-error-message { color: rgba(250, 145, 0, 255); } -.extension-error-severity-warning .extension-error-icon { - content: url('extension_error_severity_warning.png'); +.extension-error-severity-warning .extension-error-icon, +.extension-error-warning-icon { + content: url(extension_error_severity_warning.png); } .extension-error-severity-fatal .extension-error-message { color: rgba(200, 50, 50, 255); } -.extension-error-severity-fatal .extension-error-icon { - content: url('extension_error_severity_fatal.png'); +.extension-error-severity-fatal .extension-error-icon, +.extension-error-fatal-icon { + content: url(extension_error_severity_fatal.png); } diff --git a/chromium/chrome/browser/resources/extensions/extension_error.html b/chromium/chrome/browser/resources/extensions/extension_error.html index 041994b241b..29615777711 100644 --- a/chromium/chrome/browser/resources/extensions/extension_error.html +++ b/chromium/chrome/browser/resources/extensions/extension_error.html @@ -5,13 +5,17 @@ in the LICENSE file. --> <div id="template-collection-extension-error" hidden> <div class="extension-error-list"> - <ul class="extension-error-list-contents"></ul> - <div class="extension-error-list-show-more"> - <a is="action-link" i18n-content="extensionErrorsShowMore" hidden></a> + <div class="extension-error-list-heading"> + <span i18n-content="extensionErrorHeading"></span> + <a id="extension-error-list-clear" is="action-link" + i18n-content="extensionErrorClearAll"></a> </div> + <ul class="extension-error-list-contents"></ul> + <span id="no-errors-span" i18n-content="extensionErrorNoErrors" hidden> + </span> </div> <div class="extension-error-metadata"> - <span class="extension-error-message"></span> - <a is="action-link" class="extension-error-view-details"></a> + <p class="extension-error-message"></p> + <div class="error-delete-button"></div> </div> </div> diff --git a/chromium/chrome/browser/resources/extensions/extension_error.js b/chromium/chrome/browser/resources/extensions/extension_error.js index ef242f21f61..c9296554d9a 100644 --- a/chromium/chrome/browser/resources/extensions/extension_error.js +++ b/chromium/chrome/browser/resources/extensions/extension_error.js @@ -26,144 +26,356 @@ cr.define('extensions', function() { } /** + * @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 {Object} error The error the element should represent. + * @param {(RuntimeError|ManifestError)} error The error the element should + * represent. + * @param {Element} boundary The boundary for the focus grid. * @constructor - * @extends {HTMLDivElement} + * @extends {cr.ui.FocusRow} */ - function ExtensionError(error) { + function ExtensionError(error, boundary) { var div = cloneTemplate('extension-error-metadata'); div.__proto__ = ExtensionError.prototype; - div.decorate(error); + div.decorateWithError_(error, boundary); return div; } ExtensionError.prototype = { - __proto__: HTMLDivElement.prototype, + __proto__: cr.ui.FocusRow.prototype, + + /** @override */ + getEquivalentElement: function(element) { + if (element.classList.contains('extension-error-metadata')) + return this; + if (element.classList.contains('error-delete-button')) { + return /** @type {!HTMLElement} */ (this.querySelector( + '.error-delete-button')); + } + assertNotReached(); + return element; + }, /** - * @param {RuntimeError} error - * @override + * @param {(RuntimeError|ManifestError)} error The error the element should + * represent. + * @param {Element} boundary The boundary for the FocusGrid. + * @private */ - decorate: function(error) { + decorateWithError_: function(error, boundary) { + this.decorate(boundary); + + /** + * The backing error. + * @type {(ManifestError|RuntimeError)} + */ + this.error = error; + // Add an additional class for the severity level. - if (error.level == 0) - this.classList.add('extension-error-severity-info'); - else if (error.level == 1) + if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) { + switch (error.severity) { + case chrome.developerPrivate.ErrorLevel.LOG: + this.classList.add('extension-error-severity-info'); + 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'); + break; + default: + assertNotReached(); + } + } else { + // We classify manifest errors as "warnings". this.classList.add('extension-error-severity-warning'); - else - this.classList.add('extension-error-severity-fatal'); + } var iconNode = document.createElement('img'); iconNode.className = 'extension-error-icon'; + // TODO(hcarmona): Populate alt text with a proper description since this + // icon conveys the severity of the error. (info, warning, fatal). + iconNode.alt = ''; this.insertBefore(iconNode, this.firstChild); var messageSpan = this.querySelector('.extension-error-message'); messageSpan.textContent = error.message; - messageSpan.title = error.message; - var extensionUrl = 'chrome-extension://' + error.extensionId + '/'; - var viewDetailsLink = this.querySelector('.extension-error-view-details'); + var deleteButton = this.querySelector('.error-delete-button'); + deleteButton.addEventListener('click', function(e) { + this.dispatchEvent( + new CustomEvent('deleteExtensionError', + {bubbles: true, detail: this.error})); + }.bind(this)); - // If we cannot open the file source and there are no external frames in - // the stack, then there are no details to display. - if (!extensions.ExtensionErrorOverlay.canShowOverlayForError( - error, extensionUrl)) { - viewDetailsLink.hidden = true; - } else { - var stringId = extensionUrl.toLowerCase() == 'manifest.json' ? - 'extensionErrorViewManifest' : 'extensionErrorViewDetails'; - viewDetailsLink.textContent = loadTimeData.getString(stringId); + 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_(); + }); - viewDetailsLink.addEventListener('click', function(e) { - extensions.ExtensionErrorOverlay.getInstance().setErrorAndShowOverlay( - error, extensionUrl); - }); - } + this.addFocusableElement(this); + this.addFocusableElement(this.querySelector('.error-delete-button')); + }, + + /** + * 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.<Object>} errors The list of extension errors with which - * to populate the list. + * @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) { + function ExtensionErrorList(errors, extensionId) { var div = cloneTemplate('extension-error-list'); div.__proto__ = ExtensionErrorList.prototype; - div.errors_ = errors; - div.decorate(); + div.extensionId_ = extensionId; + div.decorate(errors); return div; } - /** - * @private - * @const - * @type {number} - */ - ExtensionErrorList.MAX_ERRORS_TO_SHOW_ = 3; - ExtensionErrorList.prototype = { __proto__: HTMLDivElement.prototype, - /** @override */ - decorate: function() { - this.contents_ = this.querySelector('.extension-error-list-contents'); - this.errors_.forEach(function(error) { - if (idIsValid(error.extensionId)) { - this.contents_.appendChild(document.createElement('li')).appendChild( - new ExtensionError(error)); + /** + * Initializes the extension error list. + * @param {Array<(RuntimeError|ManifestError)>} errors The list of errors. + */ + decorate: function(errors) { + /** + * @private {!Array<(ManifestError|RuntimeError)>} + */ + this.errors_ = []; + + this.focusGrid_ = new cr.ui.FocusGrid(); + this.gridBoundary_ = this.querySelector('.extension-error-list-contents'); + this.gridBoundary_.addEventListener('focus', this.onFocus_.bind(this)); + this.gridBoundary_.addEventListener('focusin', + this.onFocusin_.bind(this)); + errors.forEach(this.addError_, this); + + 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(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); } - }, this); + }.bind(this); + + chrome.developerPrivate.onItemStateChanged.addListener( + this.onItemStateChangedListener_); + + /** + * The active error element in the list. + * @private {?} + */ + this.activeError_ = null; - var numShowing = this.contents_.children.length; - if (numShowing > ExtensionErrorList.MAX_ERRORS_TO_SHOW_) - this.initShowMoreLink_(); + this.setActiveError(0); }, /** - * Initialize the "Show More" link for the error list. If there are more - * than |MAX_ERRORS_TO_SHOW_| errors in the list. + * Adds an error to the list. + * @param {(RuntimeError|ManifestError)} error The error to add. * @private */ - initShowMoreLink_: function() { - var link = this.querySelector( - '.extension-error-list-show-more [is="action-link"]'); - link.hidden = false; - link.isShowingAll = false; - - var listContents = this.querySelector('.extension-error-list-contents'); - - // TODO(dbeam/kalman): trade all this transition voodoo for .animate()? - listContents.addEventListener('webkitTransitionEnd', function(e) { - if (listContents.classList.contains('deactivating')) - listContents.classList.remove('deactivating', 'active'); - else - listContents.classList.add('scrollable'); + addError_: function(error) { + this.querySelector('#no-errors-span').hidden = true; + this.errors_.push(error); + var focusRow = new ExtensionError(error, this.gridBoundary_); + this.gridBoundary_.appendChild(document.createElement('li')). + appendChild(focusRow); + this.focusGrid_.addRow(focusRow); + }, + + /** + * 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]; + 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] }); - link.addEventListener('click', function(e) { - link.isShowingAll = !link.isShowingAll; + 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); + }, - var message = link.isShowingAll ? 'extensionErrorsShowFewer' : - 'extensionErrorsShowMore'; - link.textContent = loadTimeData.getString(message); + /** + * Called when the list is being removed. + */ + onRemoved: function() { + chrome.developerPrivate.onItemStateChanged.removeListener( + this.onItemStateChangedListener_); + this.clear(false); + }, - // Disable scrolling while transitioning. If the element is active, - // scrolling is enabled when the transition ends. - listContents.classList.remove('scrollable'); + /** + * 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); + }, - if (link.isShowingAll) { - listContents.classList.add('active'); - listContents.classList.remove('deactivating'); - } else { - listContents.classList.add('deactivating'); - } - }.bind(this)); - } + /** + * 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})); + }, + + /** + * The grid should not be focusable once it or an element inside it is + * focused. This is necessary to allow tabbing out of the grid in reverse. + * @private + */ + onFocusin_: function() { + this.gridBoundary_.tabIndex = -1; + }, + + /** + * Focus the first focusable row when tabbing into the grid for the + * first time. + * @private + */ + onFocus_: function() { + var activeRow = this.gridBoundary_.querySelector('.focus-row-active'); + activeRow.getEquivalentElement(null).focus(); + }, }; return { diff --git a/chromium/chrome/browser/resources/extensions/extension_error_overlay.css b/chromium/chrome/browser/resources/extensions/extension_error_overlay.css index 0091e946a53..e6fe5c472da 100644 --- a/chromium/chrome/browser/resources/extensions/extension_error_overlay.css +++ b/chromium/chrome/browser/resources/extensions/extension_error_overlay.css @@ -11,6 +11,12 @@ flex-direction: column; } +#extension-error-overlay .extension-error-list { + border: 1px solid #ccc; + margin-bottom: 3px; + overflow-y: auto; +} + .extension-error-overlay-runtime-content { flex: none; } diff --git a/chromium/chrome/browser/resources/extensions/extension_error_overlay.html b/chromium/chrome/browser/resources/extensions/extension_error_overlay.html index 212583f8dd9..ce95cc11bb3 100644 --- a/chromium/chrome/browser/resources/extensions/extension_error_overlay.html +++ b/chromium/chrome/browser/resources/extensions/extension_error_overlay.html @@ -21,6 +21,7 @@ in the LICENSE file. <div class="close-button"></div> <h1 class="extension-error-overlay-title"></h1> <div class="content-area"> + <div class="extension-error-list"></div> <div id="extension-error-overlay-code" class="extension-code"></div> </div> <div class="action-area"> diff --git a/chromium/chrome/browser/resources/extensions/extension_error_overlay.js b/chromium/chrome/browser/resources/extensions/extension_error_overlay.js index 5d5978c046e..84e32a9f63b 100644 --- a/chromium/chrome/browser/resources/extensions/extension_error_overlay.js +++ b/chromium/chrome/browser/resources/extensions/extension_error_overlay.js @@ -2,35 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/** - * The type of the stack trace object. The definition is based on - * extensions/browser/extension_error.cc:RuntimeError::ToValue(). - * @typedef {{columnNumber: number, - * functionName: string, - * lineNumber: number, - * url: string}} - */ -var StackTrace; - -/** - * The type of the extension error trace object. The definition is based on - * extensions/browser/extension_error.cc:RuntimeError::ToValue(). - * @typedef {{canInspect: (boolean|undefined), - * contextUrl: (string|undefined), - * extensionId: string, - * fromIncognito: boolean, - * level: number, - * manifestKey: string, - * manifestSpecific: string, - * message: string, - * renderProcessId: (number|undefined), - * renderViewId: (number|undefined), - * source: string, - * stackTrace: (Array.<StackTrace>|undefined), - * type: number}} - */ -var RuntimeError; - cr.define('extensions', function() { 'use strict'; @@ -91,30 +62,12 @@ cr.define('extensions', function() { return !/^extensions::/.test(url); }; - /** - * Send a call to chrome to open the developer tools for an error. - * This will call either the bound function in ExtensionErrorHandler or the - * API function from developerPrivate, depending on whether this is being - * used in the native chrome:extensions page or the Apps Developer Tool. - * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h - * @param {Object} args The arguments to pass to openDevTools. - * @private - */ - RuntimeErrorContent.openDevtools_ = function(args) { - if (chrome.send) - chrome.send('extensionErrorOpenDevTools', [args]); - else if (chrome.developerPrivate) - chrome.developerPrivate.openDevTools(args); - else - assertNotReached('Cannot call either openDevTools function.'); - }; - RuntimeErrorContent.prototype = { __proto__: HTMLDivElement.prototype, /** * The underlying error whose details are being displayed. - * @type {?RuntimeError} + * @type {?(RuntimeError|ManifestError)} * @private */ error_: null, @@ -158,11 +111,13 @@ cr.define('extensions', function() { /** * Sets the error for the content. - * @param {RuntimeError} error The error whose content should - * be displayed. + * @param {(RuntimeError|ManifestError)} error The error whose content + * should be displayed. * @param {string} extensionUrl The URL associated with this extension. */ setError: function(error, extensionUrl) { + this.clearError(); + this.error_ = error; this.extensionUrl_ = extensionUrl; this.contextUrl_.textContent = error.contextUrl ? @@ -236,13 +191,12 @@ cr.define('extensions', function() { frameNode.addEventListener('click', function(frame, frameNode, e) { this.setActiveFrame_(frameNode); - // Request the file source with the section highlighted; this will - // call ExtensionErrorOverlay.requestFileSourceResponse() when - // completed, which in turn calls setCode(). - ExtensionErrorOverlay.requestFileSource( + // Request the file source with the section highlighted. + extensions.ExtensionErrorOverlay.getInstance().requestFileSource( {extensionId: this.error_.extensionId, message: this.error_.message, - pathSuffix: getRelativeUrl(frame.url, this.extensionUrl_), + pathSuffix: getRelativeUrl(frame.url, + assert(this.extensionUrl_)), lineNumber: frame.lineNumber}); }.bind(this, frame, frameNode)); @@ -267,9 +221,9 @@ cr.define('extensions', function() { var stackFrame = this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace]; - RuntimeErrorContent.openDevtools_( - {renderProcessId: this.error_.renderProcessId, - renderViewId: this.error_.renderViewId, + chrome.developerPrivate.openDevTools( + {renderProcessId: this.error_.renderProcessId || -1, + renderViewId: this.error_.renderViewId || -1, url: stackFrame.url, lineNumber: stackFrame.lineNumber || 0, columnNumber: stackFrame.columnNumber || 0}); @@ -294,15 +248,6 @@ cr.define('extensions', function() { } /** - * Value of ExtensionError::RUNTIME_ERROR enum. - * @see extensions/browser/extension_error.h - * @type {number} - * @const - * @private - */ - ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_ = 1; - - /** * The manifest filename. * @type {string} * @const @@ -324,58 +269,15 @@ cr.define('extensions', function() { file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_; }; - /** - * Determine whether or not we can show an overlay with more details for - * the given extension error. - * @param {Object} error The extension error. - * @param {string} extensionUrl The url for the extension, in the form - * "chrome-extension://<extension-id>/". - * @return {boolean} True if we can show an overlay for the error, - * false otherwise. - */ - ExtensionErrorOverlay.canShowOverlayForError = function(error, extensionUrl) { - if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) - return true; - - if (error.stackTrace) { - for (var i = 0; i < error.stackTrace.length; ++i) { - if (RuntimeErrorContent.shouldDisplayForUrl(error.stackTrace[i].url)) - return true; - } - } - - return false; - }; - - /** - * Send a call to chrome to request the source of a given file. - * This will call either the bound function in ExtensionErrorHandler or the - * API function from developerPrivate, depending on whether this is being - * used in the native chrome:extensions page or the Apps Developer Tool. - * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h - * @param {Object} args The arguments to pass to requestFileSource. - */ - ExtensionErrorOverlay.requestFileSource = function(args) { - if (chrome.send) { - chrome.send('extensionErrorRequestFileSource', [args]); - } else if (chrome.developerPrivate) { - chrome.developerPrivate.requestFileSource(args, function(result) { - extensions.ExtensionErrorOverlay.requestFileSourceResponse(result); - }); - } else { - assertNotReached('Cannot call either requestFileSource function.'); - } - }; - cr.addSingletonGetter(ExtensionErrorOverlay); ExtensionErrorOverlay.prototype = { /** * The underlying error whose details are being displayed. - * @type {?RuntimeError} + * @type {?(RuntimeError|ManifestError)} * @private */ - error_: null, + selectedError_: null, /** * Initialize the page. @@ -444,99 +346,146 @@ cr.define('extensions', function() { // There's a chance that the overlay receives multiple dismiss events; in // this case, handle it gracefully and return (since all necessary work // will already have been done). - if (!this.error_) + if (!this.selectedError_) return; // Remove all previous content. this.codeDiv_.clear(); - this.openDevtoolsButton_.hidden = true; + this.overlayDiv_.querySelector('.extension-error-list').onRemoved(); + + this.clearRuntimeContent_(); + + this.selectedError_ = null; + }, - if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) { - this.overlayDiv_.querySelector('.content-area').removeChild( + /** + * Clears the current content. + * @private + */ + clearRuntimeContent_: function() { + if (this.runtimeErrorContent_.parentNode) { + this.runtimeErrorContent_.parentNode.removeChild( this.runtimeErrorContent_); this.runtimeErrorContent_.clearError(); } - - this.error_ = null; + this.openDevtoolsButton_.hidden = true; }, /** - * Associate an error with the overlay. This will set the error for the - * overlay, and, if possible, will populate the code section of the overlay - * with the relevant file, load the stack trace, and generate links for - * opening devtools (the latter two only happen for runtime errors). - * @param {RuntimeError} error The error to show in the overlay. - * @param {string} extensionUrl The URL of the extension, in the form - * "chrome-extension://<extension_id>". + * Sets the active error for the overlay. + * @param {?(ManifestError|RuntimeError)} error The error to make active. + * TODO(dbeam): add URL externs and re-enable typechecking in this method. + * @suppress {missingProperties} + * @private */ - setErrorAndShowOverlay: function(error, extensionUrl) { - this.error_ = error; + setActiveError_: function(error) { + this.selectedError_ = error; + + // If there is no error (this can happen if, e.g., the user deleted all + // the errors), then clear the content. + if (!error) { + this.codeDiv_.populate( + null, loadTimeData.getString('extensionErrorNoErrorsCodeMessage')); + this.clearRuntimeContent_(); + return; + } - if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) { - this.runtimeErrorContent_.setError(this.error_, extensionUrl); + var extensionUrl = 'chrome-extension://' + error.extensionId + '/'; + // Set or hide runtime content. + if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) { + this.runtimeErrorContent_.setError(error, extensionUrl); this.overlayDiv_.querySelector('.content-area').insertBefore( this.runtimeErrorContent_, this.codeDiv_.nextSibling); this.openDevtoolsButton_.hidden = false; this.openDevtoolsButton_.disabled = !error.canInspect; + } else { + this.clearRuntimeContent_(); } + // Read the file source to populate the code section, or set it to null if + // the file is unreadable. if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) { - var relativeUrl = getRelativeUrl(error.source, extensionUrl); - + // Use pathname instead of relativeUrl. var requestFileSourceArgs = {extensionId: error.extensionId, - message: error.message, - pathSuffix: relativeUrl}; - - if (relativeUrl.toLowerCase() == - ExtensionErrorOverlay.MANIFEST_FILENAME_) { - requestFileSourceArgs.manifestKey = error.manifestKey; - requestFileSourceArgs.manifestSpecific = error.manifestSpecific; - } else { - requestFileSourceArgs.lineNumber = - error.stackTrace && error.stackTrace[0] ? - error.stackTrace[0].lineNumber : 0; + message: error.message}; + switch (error.type) { + case chrome.developerPrivate.ErrorType.MANIFEST: + requestFileSourceArgs.pathSuffix = error.source; + requestFileSourceArgs.manifestKey = error.manifestKey; + requestFileSourceArgs.manifestSpecific = error.manifestSpecific; + break; + case chrome.developerPrivate.ErrorType.RUNTIME: + // slice(1) because pathname starts with a /. + var pathname = new URL(error.source).pathname.slice(1); + requestFileSourceArgs.pathSuffix = pathname; + requestFileSourceArgs.lineNumber = + error.stackTrace && error.stackTrace[0] ? + error.stackTrace[0].lineNumber : 0; + break; + default: + assertNotReached(); } - ExtensionErrorOverlay.requestFileSource(requestFileSourceArgs); + this.requestFileSource(requestFileSourceArgs); } else { - ExtensionErrorOverlay.requestFileSourceResponse(null); + this.onFileSourceResponse_(null); } }, - /** - * Set the code to be displayed in the code portion of the overlay. - * @see ExtensionErrorOverlay.requestFileSourceResponse(). - * @param {?ExtensionHighlight} code The code to be displayed. If |code| is - * null, then - * a "Could not display code" message will be displayed instead. + * Associate an error with the overlay. This will set the error for the + * overlay, and, if possible, will populate the code section of the overlay + * with the relevant file, load the stack trace, and generate links for + * opening devtools (the latter two only happen for runtime errors). + * @param {Array<(RuntimeError|ManifestError)>} errors The error to show in + * the overlay. + * @param {string} extensionId The id of the extension. + * @param {string} extensionName The name of the extension. */ - setCode: function(code) { + setErrorsAndShowOverlay: function(errors, extensionId, extensionName) { document.querySelector( '#extension-error-overlay .extension-error-overlay-title'). - textContent = code.title; + textContent = extensionName; + var errorsDiv = this.overlayDiv_.querySelector('.extension-error-list'); + var extensionErrors = + new extensions.ExtensionErrorList(errors, extensionId); + errorsDiv.parentNode.replaceChild(extensionErrors, errorsDiv); + extensionErrors.addEventListener('activeExtensionErrorChanged', + function(e) { + this.setActiveError_(e.detail); + }.bind(this)); + + if (errors.length > 0) + this.setActiveError_(errors[0]); + this.setVisible(true); + }, + + /** + * Requests a file's source. + * @param {RequestFileSourceProperties} args The arguments for the call. + */ + requestFileSource: function(args) { + chrome.developerPrivate.requestFileSource( + args, this.onFileSourceResponse_.bind(this)); + }, + /** + * Set the code to be displayed in the code portion of the overlay. + * @see ExtensionErrorOverlay.requestFileSourceResponse(). + * @param {?RequestFileSourceResponse} response The response from the + * request file source call, which will be shown as code. If |response| + * is null, then a "Could not display code" message will be displayed + * instead. + */ + onFileSourceResponse_: function(response) { this.codeDiv_.populate( - code, + response, // ExtensionCode can handle a null response. loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay')); + this.setVisible(true); }, }; - /** - * Called by the ExtensionErrorHandler responding to the request for a file's - * source. Populate the content area of the overlay and display the overlay. - * @param {?ExtensionHighlight} result The three 'highlight' strings represent - * three portions of the file's content to display - the portion which is - * most relevant and should be emphasized (highlight), and the parts both - * before and after this portion. These may be empty. - */ - ExtensionErrorOverlay.requestFileSourceResponse = function(result) { - var overlay = extensions.ExtensionErrorOverlay.getInstance(); - overlay.setCode(result); - overlay.setVisible(true); - }; - // Export return { ExtensionErrorOverlay: ExtensionErrorOverlay diff --git a/chromium/chrome/browser/resources/extensions/extension_focus_manager.js b/chromium/chrome/browser/resources/extensions/extension_focus_manager.js index b2200245b6e..377744caaeb 100644 --- a/chromium/chrome/browser/resources/extensions/extension_focus_manager.js +++ b/chromium/chrome/browser/resources/extensions/extension_focus_manager.js @@ -3,16 +3,16 @@ // found in the LICENSE file. cr.define('extensions', function() { - var FocusManager = cr.ui.FocusManager; - - function ExtensionFocusManager() { - FocusManager.disableMouseFocusOnButtons(); - } + /** + * @constructor + * @extends {cr.ui.FocusManager} + */ + function ExtensionFocusManager() {} cr.addSingletonGetter(ExtensionFocusManager); ExtensionFocusManager.prototype = { - __proto__: FocusManager.prototype, + __proto__: cr.ui.FocusManager.prototype, /** @override */ getFocusParent: function() { diff --git a/chromium/chrome/browser/resources/extensions/extension_info.css b/chromium/chrome/browser/resources/extensions/extension_info.css deleted file mode 100644 index c5106095eca..00000000000 --- a/chromium/chrome/browser/resources/extensions/extension_info.css +++ /dev/null @@ -1,44 +0,0 @@ -/* Copyright (c) 2012 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. */ - -body { - font-family: 'DejaVu Sans', Arial, sans-serif; - margin: 8px; - max-width: 480px; - min-width: 360px; -} - -#extension-item { - background-repeat: no-repeat; - display: -webkit-box; - min-height: 48px; -} - -#extension-title-running { - -webkit-padding-end: 5px; - font-size: 1.2em; - font-weight: 500; - padding-bottom: 5px; -} - -#extension-last-updated, -#extension-update-time { - -webkit-padding-end: 7px; - color: rgb(78, 83, 86); - font-size: 1em; - font-weight: 400; -} - -#extension-description { - -webkit-padding-end: 5px; - color: rgb(121, 126, 130); - font-size: 1em; - margin: 5px 0; - white-space: normal; -} - -#extension-details { - -webkit-box-flex: 1; - -webkit-padding-start: 57px; -} diff --git a/chromium/chrome/browser/resources/extensions/extension_info.html b/chromium/chrome/browser/resources/extensions/extension_info.html deleted file mode 100644 index 43583f1ad39..00000000000 --- a/chromium/chrome/browser/resources/extensions/extension_info.html +++ /dev/null @@ -1,31 +0,0 @@ -<!DOCTYPE html> -<html i18n-values="dir:textdirection;"> -<head> -<meta charset="utf-8"> -<link rel="stylesheet" href="chrome://resources/css/chrome_shared.css"> -<link rel="stylesheet" href="extension_info.css"> - -<script src="chrome://resources/js/cr.js"></script> -<script src="chrome://resources/js/load_time_data.js"></script> -<script src="chrome://resources/js/util.js"></script> - -<script src="chrome://extension-info/extension_info.js"></script> -</head> - -<body i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> - -<div id="extension-item"> - <div id="extension-details"> - <div id="extension-title-running"></div> - <div> - <span id="extension-last-updated" i18n-content="lastUpdated"></span> - <span id="extension-update-time" i18n-content="installTime"></span> - </div> - <p id="extension-description" i18n-content="description"></p> - </div> -</div> - -<script src="chrome://extension-info/strings.js"></script> -<script src="chrome://resources/js/i18n_template2.js"></script> -</body> -</html> diff --git a/chromium/chrome/browser/resources/extensions/extension_info.js b/chromium/chrome/browser/resources/extensions/extension_info.js deleted file mode 100644 index 6bf0062b0f8..00000000000 --- a/chromium/chrome/browser/resources/extensions/extension_info.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2012 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('extension_info', function() { - 'use strict'; - - /** - * Initialize the info popup. - */ - function load() { - $('extension-item').style.backgroundImage = - 'url(' + loadTimeData.getString('icon') + ')'; - $('extension-title-running').textContent = - loadTimeData.getStringF('isRunning', loadTimeData.getString('name')); - } - - return { - load: load - }; -}); - -window.addEventListener('DOMContentLoaded', extension_info.load); diff --git a/chromium/chrome/browser/resources/extensions/extension_list.js b/chromium/chrome/browser/resources/extensions/extension_list.js index 7546db1fbb9..b3343913c9f 100644 --- a/chromium/chrome/browser/resources/extensions/extension_list.js +++ b/chromium/chrome/browser/resources/extensions/extension_list.js @@ -4,92 +4,197 @@ <include src="extension_error.js"> +/////////////////////////////////////////////////////////////////////////////// +// ExtensionFocusRow: + /** - * The type of the extension data object. The definition is based on - * chrome/browser/ui/webui/extensions/extension_basic_info.cc - * and - * chrome/browser/ui/webui/extensions/extension_settings_handler.cc - * ExtensionSettingsHandler::CreateExtensionDetailValue() - * @typedef {{allow_reload: boolean, - * allowAllUrls: boolean, - * allowFileAccess: boolean, - * blacklistText: string, - * corruptInstall: boolean, - * dependentExtensions: Array, - * description: string, - * detailsUrl: string, - * enableExtensionInfoDialog: boolean, - * enable_show_button: boolean, - * enabled: boolean, - * enabledIncognito: boolean, - * errorCollectionEnabled: (boolean|undefined), - * hasPopupAction: boolean, - * homepageProvided: boolean, - * homepageUrl: string, - * icon: string, - * id: string, - * incognitoCanBeEnabled: boolean, - * installWarnings: (Array|undefined), - * is_hosted_app: boolean, - * is_platform_app: boolean, - * isFromStore: boolean, - * isUnpacked: boolean, - * kioskEnabled: boolean, - * kioskOnly: boolean, - * locationText: string, - * managedInstall: boolean, - * manifestErrors: (Array.<RuntimeError>|undefined), - * name: string, - * offlineEnabled: boolean, - * optionsOpenInTab: boolean, - * optionsPageHref: string, - * optionsUrl: string, - * order: number, - * packagedApp: boolean, - * path: (string|undefined), - * prettifiedPath: (string|undefined), - * recommendedInstall: boolean, - * runtimeErrors: (Array.<RuntimeError>|undefined), - * suspiciousInstall: boolean, - * terminated: boolean, - * version: string, - * views: Array.<{renderViewId: number, renderProcessId: number, - * path: string, incognito: boolean, - * generatedBackgroundPage: boolean}>, - * wantsAllUrls: boolean, - * wantsErrorCollection: boolean, - * wantsFileAccess: boolean, - * warnings: (Array|undefined)}} + * Provides an implementation for a single column grid. + * @constructor + * @extends {cr.ui.FocusRow} */ -var ExtensionData; +function ExtensionFocusRow() {} -cr.define('options', function() { - 'use strict'; +/** + * Decorates |focusRow| so that it can be treated as a ExtensionFocusRow. + * @param {Element} focusRow The element that has all the columns. + * @param {Node} boundary Focus events are ignored outside of this node. + */ +ExtensionFocusRow.decorate = function(focusRow, boundary) { + focusRow.__proto__ = ExtensionFocusRow.prototype; + focusRow.decorate(boundary); +}; + +ExtensionFocusRow.prototype = { + __proto__: cr.ui.FocusRow.prototype, + + /** @override */ + getEquivalentElement: function(element) { + if (this.focusableElements.indexOf(element) > -1) + return element; + + // All elements default to another element with the same type. + var columnType = element.getAttribute('column-type'); + var equivalent = this.querySelector('[column-type=' + columnType + ']'); + + if (!equivalent || !this.canAddElement_(equivalent)) { + var actionLinks = ['options', 'website', 'launch', 'localReload']; + var optionalControls = ['showButton', 'incognito', 'dev-collectErrors', + 'allUrls', 'localUrls']; + var removeStyleButtons = ['trash', 'enterprise']; + var enableControls = ['terminatedReload', 'repair', 'enabled']; + + if (actionLinks.indexOf(columnType) > -1) + equivalent = this.getFirstFocusableByType_(actionLinks); + else if (optionalControls.indexOf(columnType) > -1) + equivalent = this.getFirstFocusableByType_(optionalControls); + else if (removeStyleButtons.indexOf(columnType) > -1) + equivalent = this.getFirstFocusableByType_(removeStyleButtons); + else if (enableControls.indexOf(columnType) > -1) + equivalent = this.getFirstFocusableByType_(enableControls); + } + + // Return the first focusable element if no equivalent type is found. + return equivalent || this.focusableElements[0]; + }, + + /** @override */ + makeActive: function(active) { + cr.ui.FocusRow.prototype.makeActive.call(this, active); + + // Only highlight if the row has focus. + this.classList.toggle('extension-highlight', + active && this.contains(document.activeElement)); + }, + + /** Updates the list of focusable elements. */ + updateFocusableElements: function() { + this.focusableElements.length = 0; + + var focusableCandidates = this.querySelectorAll('[column-type]'); + for (var i = 0; i < focusableCandidates.length; ++i) { + var element = focusableCandidates[i]; + if (this.canAddElement_(element)) + this.addFocusableElement(element); + } + }, /** - * Creates a new list of extensions. - * @param {Object=} opt_propertyBag Optional properties. - * @constructor - * @extends {HTMLDivElement} + * Get the first focusable element that matches a list of types. + * @param {Array<string>} types An array of types to match from. + * @return {?Element} Return the first element that matches a type in |types|. + * @private + */ + getFirstFocusableByType_: function(types) { + for (var i = 0; i < this.focusableElements.length; ++i) { + var element = this.focusableElements[i]; + if (types.indexOf(element.getAttribute('column-type')) > -1) + return element; + } + return null; + }, + + /** + * Setup a typical column in the ExtensionFocusRow. A column can be any + * element and should have an action when clicked/toggled. This function + * adds a listener and a handler for an event. Also adds the "column-type" + * attribute to make the element focusable in |updateFocusableElements|. + * @param {string} query A query to select the element to set up. + * @param {string} columnType A tag used to identify the column when + * changing focus. + * @param {string} eventType The type of event to listen to. + * @param {function(Event)} handler The function that should be called + * by the event. + * @private */ - var ExtensionsList = cr.ui.define('div'); + setupColumn: function(query, columnType, eventType, handler) { + var element = this.querySelector(query); + element.addEventListener(eventType, handler); + element.setAttribute('column-type', columnType); + }, + + /** + * @param {Element} element + * @return {boolean} + * @private + */ + canAddElement_: function(element) { + if (!element || element.disabled) + return false; + + var developerMode = $('extension-settings').classList.contains('dev-mode'); + if (this.isDeveloperOption_(element) && !developerMode) + return false; + + for (var el = element; el; el = el.parentElement) { + if (el.hidden) + return false; + } + + return true; + }, /** - * @type {Object.<string, boolean>} A map from extension id to a boolean - * indicating whether the incognito warning is showing. This persists - * between calls to decorate. + * Returns true if the element should only be shown in developer mode. + * @param {Element} element + * @return {boolean} + * @private */ - var butterBarVisibility = {}; + isDeveloperOption_: function(element) { + return /^dev-/.test(element.getAttribute('column-type')); + }, +}; + +cr.define('extensions', function() { + 'use strict'; /** - * @type {Object.<string, number>} A map from extension id to last reloaded - * timestamp. The timestamp is recorded when the user click the 'Reload' - * link. It is used to refresh the icon of an unpacked extension. - * This persists between calls to decorate. + * Compares two extensions for the order they should appear in the list. + * @param {ExtensionInfo} a The first extension. + * @param {ExtensionInfo} b The second extension. + * returns {number} -1 if A comes before B, 1 if A comes after B, 0 if equal. */ - var extensionReloadedTimestamp = {}; + function compareExtensions(a, b) { + function compare(x, y) { + return x < y ? -1 : (x > y ? 1 : 0); + } + function compareLocation(x, y) { + if (x.location == y.location) + return 0; + if (x.location == chrome.developerPrivate.Location.UNPACKED) + return -1; + if (y.location == chrome.developerPrivate.Location.UNPACKED) + return 1; + return 0; + } + return compareLocation(a, b) || + compare(a.name.toLowerCase(), b.name.toLowerCase()) || + compare(a.id, b.id); + } + + /** @interface */ + function ExtensionListDelegate() {} - ExtensionsList.prototype = { + ExtensionListDelegate.prototype = { + /** + * Called when the number of extensions in the list has changed. + */ + onExtensionCountChanged: assertNotReached, + }; + + /** + * Creates a new list of extensions. + * @param {extensions.ExtensionListDelegate} delegate + * @constructor + * @extends {HTMLDivElement} + */ + function ExtensionList(delegate) { + var div = document.createElement('div'); + div.__proto__ = ExtensionList.prototype; + div.initialize(delegate); + return div; + } + + ExtensionList.prototype = { __proto__: HTMLDivElement.prototype, /** @@ -101,11 +206,175 @@ cr.define('options', function() { */ optionsShown_: false, - /** @override */ - decorate: function() { - this.textContent = ''; + /** @private {!cr.ui.FocusGrid} */ + focusGrid_: new cr.ui.FocusGrid(), + + /** + * Indicates whether an uninstall dialog is being shown to prevent multiple + * dialogs from being displayed. + * @private {boolean} + */ + uninstallIsShowing_: false, + + /** + * Indicates whether a permissions prompt is showing. + * @private {boolean} + */ + permissionsPromptIsShowing_: false, + + /** + * Necessary to only show the butterbar once. + * @private {boolean} + */ + butterbarShown_: false, + + /** + * Whether or not incognito mode is available. + * @private {boolean} + */ + incognitoAvailable_: false, + + /** + * Whether or not the app info dialog is enabled. + * @private {boolean} + */ + enableAppInfoDialog_: false, + + /** + * Initializes the list. + * @param {!extensions.ExtensionListDelegate} delegate + */ + initialize: function(delegate) { + /** @private {!Array<ExtensionInfo>} */ + this.extensions_ = []; + + /** @private {!extensions.ExtensionListDelegate} */ + this.delegate_ = delegate; + + /** + * |loadFinished| should be used for testing purposes and will be + * fulfilled when this list has finished loading the first time. + * @type {Promise} + * */ + this.loadFinished = new Promise(function(resolve, reject) { + /** @private {function(?)} */ + this.resolveLoadFinished_ = resolve; + }.bind(this)); + + chrome.developerPrivate.onItemStateChanged.addListener( + function(eventData) { + var EventType = chrome.developerPrivate.EventType; + switch (eventData.event_type) { + case EventType.VIEW_REGISTERED: + case EventType.VIEW_UNREGISTERED: + case EventType.INSTALLED: + case EventType.LOADED: + case EventType.UNLOADED: + case EventType.ERROR_ADDED: + case EventType.ERRORS_REMOVED: + case EventType.PREFS_CHANGED: + if (eventData.extensionInfo) { + this.updateExtension_(eventData.extensionInfo); + this.focusGrid_.ensureRowActive(); + } + break; + case EventType.UNINSTALLED: + var index = this.getIndexOfExtension_(eventData.item_id); + this.extensions_.splice(index, 1); + this.removeNode_(getRequiredElement(eventData.item_id)); + break; + default: + assertNotReached(); + } + + if (eventData.event_type == EventType.UNLOADED) + this.hideEmbeddedExtensionOptions_(eventData.item_id); + + if (eventData.event_type == EventType.INSTALLED || + eventData.event_type == EventType.UNINSTALLED) { + this.delegate_.onExtensionCountChanged(); + } + }.bind(this)); + }, + + /** + * Updates the extensions on the page. + * @param {boolean} incognitoAvailable Whether or not incognito is allowed. + * @param {boolean} enableAppInfoDialog Whether or not the app info dialog + * is enabled. + * @return {Promise} A promise that is resolved once the extensions data is + * fully updated. + */ + updateExtensionsData: function(incognitoAvailable, enableAppInfoDialog) { + // If we start to need more information about the extension configuration, + // consider passing in the full object from the ExtensionSettings. + this.incognitoAvailable_ = incognitoAvailable; + this.enableAppInfoDialog_ = enableAppInfoDialog; + /** @private {Promise} */ + this.extensionsUpdated_ = new Promise(function(resolve, reject) { + chrome.developerPrivate.getExtensionsInfo( + {includeDisabled: true, includeTerminated: true}, + function(extensions) { + // Sort in order of unpacked vs. packed, followed by name, followed by + // id. + extensions.sort(compareExtensions); + this.extensions_ = extensions; + this.showExtensionNodes_(); + resolve(); + + // |resolve| is async so it's necessary to use |then| here in order to + // do work after other |then|s have finished. This is important so + // elements are visible when these updates happen. + this.extensionsUpdated_.then(function() { + this.onUpdateFinished_(); + this.resolveLoadFinished_(); + }.bind(this)); + }.bind(this)); + }.bind(this)); + return this.extensionsUpdated_; + }, + + /** + * Updates elements that need to be visible in order to update properly. + * @private + */ + onUpdateFinished_: function() { + // Cannot focus or highlight a extension if there are none. + if (this.extensions_.length == 0) + return; + + assert(!this.hidden); + assert(!this.parentElement.hidden); + + this.updateFocusableElements(); - this.showExtensionNodes_(); + var idToHighlight = this.getIdQueryParam_(); + if (idToHighlight && $(idToHighlight)) { + this.scrollToNode_(idToHighlight); + this.setInitialFocus_(idToHighlight); + } + + var idToOpenOptions = this.getOptionsQueryParam_(); + if (idToOpenOptions && $(idToOpenOptions)) + this.showEmbeddedExtensionOptions_(idToOpenOptions, true); + }, + + /** @return {number} The number of extensions being displayed. */ + getNumExtensions: function() { + return this.extensions_.length; + }, + + /** + * @param {string} id The id of the extension. + * @return {number} The index of the extension with the given id. + * @private + */ + getIndexOfExtension_: function(id) { + for (var i = 0; i < this.extensions_.length; ++i) { + if (this.extensions_[i].id == id) + return i; + } + return -1; }, getIdQueryParam_: function() { @@ -117,25 +386,58 @@ cr.define('options', function() { }, /** - * Creates all extension items from scratch. + * Creates or updates all extension items from scratch. * @private */ showExtensionNodes_: function() { + // Any node that is not updated will be removed. + var seenIds = []; + // Iterate over the extension data and add each item to the list. - this.data_.extensions.forEach(this.createNode_, this); + this.extensions_.forEach(function(extension) { + seenIds.push(extension.id); + this.updateExtension_(extension); + }, this); + this.focusGrid_.ensureRowActive(); - var idToHighlight = this.getIdQueryParam_(); - if (idToHighlight && $(idToHighlight)) - this.scrollToNode_(idToHighlight); + // Remove extensions that are no longer installed. + var nodes = document.querySelectorAll('.extension-list-item-wrapper[id]'); + Array.prototype.forEach.call(nodes, function(node) { + if (seenIds.indexOf(node.id) < 0) + this.removeNode_(node); + }, this); + }, - var idToOpenOptions = this.getOptionsQueryParam_(); - if (idToOpenOptions && $(idToOpenOptions)) - this.showEmbeddedExtensionOptions_(idToOpenOptions, true); + /** Updates each row's focusable elements without rebuilding the grid. */ + updateFocusableElements: function() { + var rows = document.querySelectorAll('.extension-list-item-wrapper[id]'); + for (var i = 0; i < rows.length; ++i) { + assertInstanceof(rows[i], ExtensionFocusRow).updateFocusableElements(); + } + }, - if (this.data_.extensions.length == 0) - this.classList.add('empty-extension-list'); - else - this.classList.remove('empty-extension-list'); + /** + * Removes the node from the DOM, and updates the focused element if needed. + * @param {!HTMLElement} node + * @private + */ + removeNode_: function(node) { + if (node.contains(document.activeElement)) { + var nodes = + document.querySelectorAll('.extension-list-item-wrapper[id]'); + var index = Array.prototype.indexOf.call(nodes, node); + assert(index != -1); + var focusableNode = nodes[index + 1] || nodes[index - 1]; + if (focusableNode) + focusableNode.getEquivalentElement(document.activeElement).focus(); + } + node.parentNode.removeChild(node); + this.focusGrid_.removeRow(assertInstanceof(node, ExtensionFocusRow)); + + // Unregister the removed node from events. + assertInstanceof(node, ExtensionFocusRow).destroy(); + + this.focusGrid_.ensureRowActive(); }, /** @@ -155,368 +457,619 @@ cr.define('options', function() { }, /** + * @param {string} extensionId The id of the extension that should have + * initial focus + * @private + */ + setInitialFocus_: function(extensionId) { + var focusRow = assertInstanceof($(extensionId), ExtensionFocusRow); + var columnTypePriority = ['enabled', 'enterprise', 'website', 'details']; + var elementToFocus = null; + var elementPriority = columnTypePriority.length; + + for (var i = 0; i < focusRow.focusableElements.length; ++i) { + var element = focusRow.focusableElements[i]; + var priority = + columnTypePriority.indexOf(element.getAttribute('column-type')); + if (priority > -1 && priority < elementPriority) { + elementToFocus = element; + elementPriority = priority; + } + } + + focusRow.getEquivalentElement(elementToFocus).focus(); + }, + + /** * Synthesizes and initializes an HTML element for the extension metadata * given in |extension|. - * @param {ExtensionData} extension A dictionary of extension metadata. + * @param {!ExtensionInfo} extension A dictionary of extension metadata. + * @param {?Element} nextNode |node| should be inserted before |nextNode|. + * |node| will be appended to the end if |nextNode| is null. * @private */ - createNode_: function(extension) { + createNode_: function(extension, nextNode) { var template = $('template-collection').querySelector( '.extension-list-item-wrapper'); var node = template.cloneNode(true); - node.id = extension.id; - - if (!extension.enabled || extension.terminated) - node.classList.add('inactive-extension'); - - if (extension.managedInstall || - extension.dependentExtensions.length > 0) { - node.classList.add('may-not-modify'); - node.classList.add('may-not-remove'); - } else if (extension.recommendedInstall) { - node.classList.add('may-not-remove'); - } else if (extension.suspiciousInstall || extension.corruptInstall) { - node.classList.add('may-not-modify'); - } + ExtensionFocusRow.decorate(node, $('extension-settings-list')); - var idToHighlight = this.getIdQueryParam_(); - if (node.id == idToHighlight) - node.classList.add('extension-highlight'); - - var item = node.querySelector('.extension-list-item'); - // Prevent the image cache of extension icon by using the reloaded - // timestamp as a query string. The timestamp is recorded when the user - // clicks the 'Reload' link. http://crbug.com/159302. - if (extensionReloadedTimestamp[extension.id]) { - item.style.backgroundImage = - 'url(' + extension.icon + '?' + - extensionReloadedTimestamp[extension.id] + ')'; - } else { - item.style.backgroundImage = 'url(' + extension.icon + ')'; - } - - var title = node.querySelector('.extension-title'); - title.textContent = extension.name; - - var version = node.querySelector('.extension-version'); - version.textContent = extension.version; - - var locationText = node.querySelector('.location-text'); - locationText.textContent = extension.locationText; - - var blacklistText = node.querySelector('.blacklist-text'); - blacklistText.textContent = extension.blacklistText; - - var description = document.createElement('span'); - description.textContent = extension.description; - node.querySelector('.extension-description').appendChild(description); + var row = assertInstanceof(node, ExtensionFocusRow); + row.id = extension.id; // The 'Show Browser Action' button. - if (extension.enable_show_button) { - var showButton = node.querySelector('.show-button'); - showButton.addEventListener('click', function(e) { - chrome.send('extensionSettingsShowButton', [extension.id]); + row.setupColumn('.show-button', 'showButton', 'click', function(e) { + chrome.developerPrivate.updateExtensionConfiguration({ + extensionId: extension.id, + showActionButton: true }); - showButton.hidden = false; - } + }); // The 'allow in incognito' checkbox. - node.querySelector('.incognito-control').hidden = - !this.data_.incognitoAvailable; - var incognito = node.querySelector('.incognito-control input'); - incognito.disabled = !extension.incognitoCanBeEnabled; - incognito.checked = extension.enabledIncognito; - if (!incognito.disabled) { - incognito.addEventListener('change', function(e) { - var checked = e.target.checked; - butterBarVisibility[extension.id] = checked; - butterBar.hidden = !checked || extension.is_hosted_app; - chrome.send('extensionSettingsEnableIncognito', - [extension.id, String(checked)]); + row.setupColumn('.incognito-control input', 'incognito', 'change', + function(e) { + var butterBar = row.querySelector('.butter-bar'); + var checked = e.target.checked; + if (!this.butterbarShown_) { + butterBar.hidden = !checked || + extension.type == + chrome.developerPrivate.ExtensionType.HOSTED_APP; + this.butterbarShown_ = !butterBar.hidden; + } else { + butterBar.hidden = true; + } + chrome.developerPrivate.updateExtensionConfiguration({ + extensionId: extension.id, + incognitoAccess: e.target.checked }); - } - var butterBar = node.querySelector('.butter-bar'); - butterBar.hidden = !butterBarVisibility[extension.id]; + }.bind(this)); // The 'collect errors' checkbox. This should only be visible if the // error console is enabled - we can detect this by the existence of the // |errorCollectionEnabled| property. - if (extension.wantsErrorCollection) { - node.querySelector('.error-collection-control').hidden = false; - var errorCollection = - node.querySelector('.error-collection-control input'); - errorCollection.checked = extension.errorCollectionEnabled; - errorCollection.addEventListener('change', function(e) { - chrome.send('extensionSettingsEnableErrorCollection', - [extension.id, String(e.target.checked)]); + row.setupColumn('.error-collection-control input', 'dev-collectErrors', + 'change', function(e) { + chrome.developerPrivate.updateExtensionConfiguration({ + extensionId: extension.id, + errorCollection: e.target.checked }); - } + }); // The 'allow on all urls' checkbox. This should only be visible if // active script restrictions are enabled. If they are not enabled, no // extensions should want all urls. - if (extension.wantsAllUrls) { - var allUrls = node.querySelector('.all-urls-control'); - allUrls.addEventListener('click', function(e) { - chrome.send('extensionSettingsAllowOnAllUrls', - [extension.id, String(e.target.checked)]); + row.setupColumn('.all-urls-control input', 'allUrls', 'click', + function(e) { + chrome.developerPrivate.updateExtensionConfiguration({ + extensionId: extension.id, + runOnAllUrls: e.target.checked }); - allUrls.querySelector('input').checked = extension.allowAllUrls; - allUrls.hidden = false; - } + }); // The 'allow file:// access' checkbox. - if (extension.wantsFileAccess) { - var fileAccess = node.querySelector('.file-access-control'); - fileAccess.addEventListener('click', function(e) { - chrome.send('extensionSettingsAllowFileAccess', - [extension.id, String(e.target.checked)]); + row.setupColumn('.file-access-control input', 'localUrls', 'click', + function(e) { + chrome.developerPrivate.updateExtensionConfiguration({ + extensionId: extension.id, + fileAccess: e.target.checked }); - fileAccess.querySelector('input').checked = extension.allowFileAccess; - fileAccess.hidden = false; - } + }); // The 'Options' button or link, depending on its behaviour. - if (extension.enabled && extension.optionsUrl) { - var options, optionsClickListener; - if (extension.optionsOpenInTab) { - options = node.querySelector('.options-link'); - // Set an href to get the correct mouse-over appearance (link, - // footer) - but the actual link opening is done through chrome.send - // with a preventDefault(). - options.setAttribute('href', extension.optionsPageHref); - optionsClickListener = function() { - chrome.send('extensionSettingsOptions', [extension.id]); - }; - } else { - options = node.querySelector('.options-button'); - optionsClickListener = function() { - this.showEmbeddedExtensionOptions_(extension.id, false); - }.bind(this); - } - options.addEventListener('click', function(e) { - optionsClickListener(); - e.preventDefault(); - }); - options.hidden = false; - } + // Set an href to get the correct mouse-over appearance (link, + // footer) - but the actual link opening is done through developerPrivate + // API with a preventDefault(). + row.querySelector('.options-link').href = + extension.optionsPage ? extension.optionsPage.url : ''; + row.setupColumn('.options-link', 'options', 'click', function(e) { + chrome.developerPrivate.showOptions(extension.id); + e.preventDefault(); + }); + + row.setupColumn('.options-button', 'options', 'click', function(e) { + this.showEmbeddedExtensionOptions_(extension.id, false); + e.preventDefault(); + }.bind(this)); + + // The 'View in Web Store/View Web Site' link. + row.querySelector('.site-link').setAttribute('column-type', 'website'); // The 'Permissions' link. - var permissions = node.querySelector('.permissions-link'); - permissions.addEventListener('click', function(e) { - chrome.send('extensionSettingsPermissions', [extension.id]); + row.setupColumn('.permissions-link', 'details', 'click', function(e) { + if (!this.permissionsPromptIsShowing_) { + chrome.developerPrivate.showPermissionsDialog(extension.id, + function() { + this.permissionsPromptIsShowing_ = false; + }.bind(this)); + this.permissionsPromptIsShowing_ = true; + } e.preventDefault(); }); - // The 'View in Web Store/View Web Site' link. - if (extension.homepageUrl && !extension.enableExtensionInfoDialog) { - var siteLink = node.querySelector('.site-link'); - siteLink.href = extension.homepageUrl; - siteLink.textContent = loadTimeData.getString( - extension.homepageProvided ? 'extensionSettingsVisitWebsite' : - 'extensionSettingsVisitWebStore'); - siteLink.hidden = false; - } + // The 'Reload' link. + row.setupColumn('.reload-link', 'localReload', 'click', function(e) { + chrome.developerPrivate.reload(extension.id, {failQuietly: true}); + }); - if (extension.allow_reload) { - // The 'Reload' link. - var reload = node.querySelector('.reload-link'); - reload.addEventListener('click', function(e) { - chrome.send('extensionSettingsReload', [extension.id]); - extensionReloadedTimestamp[extension.id] = Date.now(); - }); - reload.hidden = false; + // The 'Launch' link. + row.setupColumn('.launch-link', 'launch', 'click', function(e) { + chrome.management.launchApp(extension.id); + }); - if (extension.is_platform_app) { - // The 'Launch' link. - var launch = node.querySelector('.launch-link'); - launch.addEventListener('click', function(e) { - chrome.send('extensionSettingsLaunch', [extension.id]); - }); - launch.hidden = false; - } - } + row.setupColumn('.errors-link', 'errors', 'click', function(e) { + var extensionId = extension.id; + assert(this.extensions_.length > 0); + var newEx = this.extensions_.filter(function(e) { + return e.state == chrome.developerPrivate.ExtensionState.ENABLED && + e.id == extensionId; + })[0]; + var errors = newEx.manifestErrors.concat(newEx.runtimeErrors); + extensions.ExtensionErrorOverlay.getInstance().setErrorsAndShowOverlay( + errors, extensionId, newEx.name); + }.bind(this)); - if (extension.terminated) { - var terminatedReload = node.querySelector('.terminated-reload-link'); - terminatedReload.hidden = false; - terminatedReload.onclick = function() { - chrome.send('extensionSettingsReload', [extension.id]); - }; - } else if (extension.corruptInstall && extension.isFromStore) { - var repair = node.querySelector('.corrupted-repair-button'); - repair.hidden = false; - repair.onclick = function() { - chrome.send('extensionSettingsRepair', [extension.id]); - }; - } else { - // The 'Enabled' checkbox. - var enable = node.querySelector('.enable-checkbox'); - enable.hidden = false; - var enableCheckboxDisabled = extension.managedInstall || - extension.suspiciousInstall || - extension.corruptInstall || - extension.dependentExtensions.length > 0; - enable.querySelector('input').disabled = enableCheckboxDisabled; - - if (!enableCheckboxDisabled) { - enable.addEventListener('click', function(e) { - // When e.target is the label instead of the checkbox, it doesn't - // have the checked property and the state of the checkbox is - // left unchanged. - var checked = e.target.checked; - if (checked == undefined) - checked = !e.currentTarget.querySelector('input').checked; - chrome.send('extensionSettingsEnable', - [extension.id, checked ? 'true' : 'false']); - - // This may seem counter-intuitive (to not set/clear the checkmark) - // but this page will be updated asynchronously if the extension - // becomes enabled/disabled. It also might not become enabled or - // disabled, because the user might e.g. get prompted when enabling - // and choose not to. - e.preventDefault(); - }); - } + // The 'Reload' terminated link. + row.setupColumn('.terminated-reload-link', 'terminatedReload', 'click', + function(e) { + chrome.developerPrivate.reload(extension.id, {failQuietly: true}); + }); - enable.querySelector('input').checked = extension.enabled; - } + // The 'Repair' corrupted link. + row.setupColumn('.corrupted-repair-button', 'repair', 'click', + function(e) { + chrome.developerPrivate.repairExtension(extension.id); + }); + + // The 'Enabled' checkbox. + row.setupColumn('.enable-checkbox input', 'enabled', 'change', + function(e) { + var checked = e.target.checked; + // TODO(devlin): What should we do if this fails? + chrome.management.setEnabled(extension.id, checked); + + // This may seem counter-intuitive (to not set/clear the checkmark) + // but this page will be updated asynchronously if the extension + // becomes enabled/disabled. It also might not become enabled or + // disabled, because the user might e.g. get prompted when enabling + // and choose not to. + e.preventDefault(); + }); // 'Remove' button. var trashTemplate = $('template-collection').querySelector('.trash'); var trash = trashTemplate.cloneNode(true); trash.title = loadTimeData.getString('extensionUninstall'); + trash.hidden = !extension.userMayModify; + trash.setAttribute('column-type', 'trash'); trash.addEventListener('click', function(e) { - butterBarVisibility[extension.id] = false; - chrome.send('extensionSettingsUninstall', [extension.id]); + trash.classList.add('open'); + trash.classList.toggle('mouse-clicked', e.detail > 0); + if (this.uninstallIsShowing_) + return; + this.uninstallIsShowing_ = true; + chrome.management.uninstall(extension.id, + {showConfirmDialog: true}, + function() { + // TODO(devlin): What should we do if the uninstall fails? + this.uninstallIsShowing_ = false; + + if (trash.classList.contains('mouse-clicked')) + trash.blur(); + + if (chrome.runtime.lastError) { + // The uninstall failed (e.g. a cancel). Allow the trash to close. + trash.classList.remove('open'); + } else { + // Leave the trash open if the uninstall succeded. Otherwise it can + // half-close right before it's removed when the DOM is modified. + } + }.bind(this)); + }.bind(this)); + row.querySelector('.enable-controls').appendChild(trash); + + // Developer mode //////////////////////////////////////////////////////// + + // The path, if provided by unpacked extension. + row.setupColumn('.load-path a:first-of-type', 'dev-loadPath', 'click', + function(e) { + chrome.developerPrivate.showPath(extension.id); + e.preventDefault(); + }); + + // Maintain the order that nodes should be in when creating as well as + // when adding only one new row. + this.insertBefore(row, nextNode); + this.updateNode_(extension, row); + + var nextRow = null; + if (nextNode) + nextRow = assertInstanceof(nextNode, ExtensionFocusRow); + + this.focusGrid_.addRowBefore(row, nextRow); + }, + + /** + * Updates an HTML element for the extension metadata given in |extension|. + * @param {!ExtensionInfo} extension A dictionary of extension metadata. + * @param {!ExtensionFocusRow} row The node that is being updated. + * @private + */ + updateNode_: function(extension, row) { + var isActive = + extension.state == chrome.developerPrivate.ExtensionState.ENABLED; + row.classList.toggle('inactive-extension', !isActive); + + // Hack to keep the closure compiler happy about |remove|. + // TODO(hcarmona): Remove this hack when the closure compiler is updated. + var node = /** @type {Element} */ (row); + node.classList.remove('policy-controlled', 'may-not-modify', + 'may-not-remove'); + var classes = []; + if (!extension.userMayModify) { + classes.push('policy-controlled', 'may-not-modify'); + } else if (extension.dependentExtensions.length > 0) { + classes.push('may-not-remove', 'may-not-modify'); + } else if (extension.mustRemainInstalled) { + classes.push('may-not-remove'); + } else if (extension.disableReasons.suspiciousInstall || + extension.disableReasons.corruptInstall || + extension.disableReasons.updateRequired) { + classes.push('may-not-modify'); + } + row.classList.add.apply(row.classList, classes); + + var item = row.querySelector('.extension-list-item'); + item.style.backgroundImage = 'url(' + extension.iconUrl + ')'; + + this.setText_(row, '.extension-title', extension.name); + this.setText_(row, '.extension-version', extension.version); + this.setText_(row, '.location-text', extension.locationText || ''); + this.setText_(row, '.blacklist-text', extension.blacklistText || ''); + this.setText_(row, '.extension-description', extension.description); + + // The 'Show Browser Action' button. + this.updateVisibility_(row, '.show-button', + isActive && extension.actionButtonHidden); + + // The 'allow in incognito' checkbox. + this.updateVisibility_(row, '.incognito-control', + isActive && this.incognitoAvailable_, + function(item) { + var incognito = item.querySelector('input'); + incognito.disabled = !extension.incognitoAccess.isEnabled; + incognito.checked = extension.incognitoAccess.isActive; + }); + + // Hide butterBar if incognito is not enabled for the extension. + var butterBar = row.querySelector('.butter-bar'); + butterBar.hidden = + butterBar.hidden || !extension.incognitoAccess.isEnabled; + + // The 'collect errors' checkbox. This should only be visible if the + // error console is enabled - we can detect this by the existence of the + // |errorCollectionEnabled| property. + this.updateVisibility_( + row, '.error-collection-control', + isActive && extension.errorCollection.isEnabled, + function(item) { + item.querySelector('input').checked = + extension.errorCollection.isActive; + }); + + // The 'allow on all urls' checkbox. This should only be visible if + // active script restrictions are enabled. If they are not enabled, no + // extensions should want all urls. + this.updateVisibility_( + row, '.all-urls-control', + isActive && extension.runOnAllUrls.isEnabled, + function(item) { + item.querySelector('input').checked = extension.runOnAllUrls.isActive; + }); + + // The 'allow file:// access' checkbox. + this.updateVisibility_(row, '.file-access-control', + isActive && extension.fileAccess.isEnabled, + function(item) { + item.querySelector('input').checked = extension.fileAccess.isActive; + }); + + // The 'Options' button or link, depending on its behaviour. + var optionsEnabled = isActive && !!extension.optionsPage; + this.updateVisibility_(row, '.options-link', optionsEnabled && + extension.optionsPage.openInTab); + this.updateVisibility_(row, '.options-button', optionsEnabled && + !extension.optionsPage.openInTab); + + // The 'View in Web Store/View Web Site' link. + var siteLinkEnabled = !!extension.homePage.url && + !this.enableAppInfoDialog_; + this.updateVisibility_(row, '.site-link', siteLinkEnabled, + function(item) { + item.href = extension.homePage.url; + item.textContent = loadTimeData.getString( + extension.homePage.specified ? 'extensionSettingsVisitWebsite' : + 'extensionSettingsVisitWebStore'); + }); + + var isUnpacked = + extension.location == chrome.developerPrivate.Location.UNPACKED; + // The 'Reload' link. + this.updateVisibility_(row, '.reload-link', isUnpacked); + + // The 'Launch' link. + this.updateVisibility_( + row, '.launch-link', + isUnpacked && extension.type == + chrome.developerPrivate.ExtensionType.PLATFORM_APP); + + // The 'Errors' link. + var hasErrors = extension.runtimeErrors.length > 0 || + extension.manifestErrors.length > 0; + this.updateVisibility_(row, '.errors-link', hasErrors, function(item) { + var Level = chrome.developerPrivate.ErrorLevel; + + var map = {}; + map[Level.LOG] = {weight: 0, name: 'extension-error-info-icon'}; + map[Level.WARN] = {weight: 1, name: 'extension-error-warning-icon'}; + map[Level.ERROR] = {weight: 2, name: 'extension-error-fatal-icon'}; + + // Find the highest severity of all the errors; manifest errors all have + // a 'warning' level severity. + var highestSeverity = extension.runtimeErrors.reduce( + function(prev, error) { + return map[error.severity].weight > map[prev].weight ? + error.severity : prev; + }, extension.manifestErrors.length ? Level.WARN : Level.LOG); + + // Adjust the class on the icon. + var icon = item.querySelector('.extension-error-icon'); + // TODO(hcarmona): Populate alt text with a proper description since + // this icon conveys the severity of the error. (info, warning, fatal). + icon.alt = ''; + icon.className = 'extension-error-icon'; // Remove other classes. + icon.classList.add(map[highestSeverity].name); + }); + + // The 'Reload' terminated link. + var isTerminated = + extension.state == chrome.developerPrivate.ExtensionState.TERMINATED; + this.updateVisibility_(row, '.terminated-reload-link', isTerminated); + + // The 'Repair' corrupted link. + var canRepair = !isTerminated && + extension.disableReasons.corruptInstall && + extension.location == + chrome.developerPrivate.Location.FROM_STORE; + this.updateVisibility_(row, '.corrupted-repair-button', canRepair); + + // The 'Enabled' checkbox. + var isOK = !isTerminated && !canRepair; + this.updateVisibility_(row, '.enable-checkbox', isOK, function(item) { + var enableCheckboxDisabled = + !extension.userMayModify || + extension.disableReasons.suspiciousInstall || + extension.disableReasons.corruptInstall || + extension.disableReasons.updateRequired || + extension.installedByCustodian || + extension.dependentExtensions.length > 0; + item.querySelector('input').disabled = enableCheckboxDisabled; + item.querySelector('input').checked = isActive; }); - node.querySelector('.enable-controls').appendChild(trash); + + // Button for extensions controlled by policy. + var controlNode = row.querySelector('.enable-controls'); + var indicator = + controlNode.querySelector('.controlled-extension-indicator'); + var needsIndicator = isOK && + !extension.userMayModify && + extension.policyText; + // TODO(treib): If userMayModify is false, but policyText is empty, that + // indicates this extension is controlled by something else than + // enterprise policy (such as the profile being supervised). For now, just + // don't show the indicator in this case. We should really handle this + // better though (ie use a different text and icon). + + if (needsIndicator && !indicator) { + indicator = new cr.ui.ControlledIndicator(); + indicator.classList.add('controlled-extension-indicator'); + indicator.setAttribute('controlled-by', 'policy'); + var textPolicy = extension.policyText || ''; + indicator.setAttribute('textpolicy', textPolicy); + indicator.image.setAttribute('aria-label', textPolicy); + controlNode.appendChild(indicator); + indicator.querySelector('div').setAttribute('column-type', + 'enterprise'); + } else if (!needsIndicator && indicator) { + controlNode.removeChild(indicator); + } // Developer mode //////////////////////////////////////////////////////// // First we have the id. - var idLabel = node.querySelector('.extension-id'); + var idLabel = row.querySelector('.extension-id'); idLabel.textContent = ' ' + extension.id; // Then the path, if provided by unpacked extension. - if (extension.isUnpacked) { - var loadPath = node.querySelector('.load-path'); - loadPath.hidden = false; - var pathLink = loadPath.querySelector('a:nth-of-type(1)'); - pathLink.textContent = ' ' + extension.prettifiedPath; - pathLink.addEventListener('click', function(e) { - chrome.send('extensionSettingsShowPath', [String(extension.id)]); - e.preventDefault(); - }); - } + this.updateVisibility_(row, '.load-path', isUnpacked, + function(item) { + item.querySelector('a:first-of-type').textContent = + ' ' + extension.prettifiedPath; + }); // Then the 'managed, cannot uninstall/disable' message. - if (extension.managedInstall || extension.recommendedInstall) { - node.querySelector('.managed-message').hidden = false; - } else { - if (extension.suspiciousInstall) { - // Then the 'This isn't from the webstore, looks suspicious' message. - node.querySelector('.suspicious-install-message').hidden = false; - } - if (extension.corruptInstall) { - // Then the 'This is a corrupt extension' message. - node.querySelector('.corrupt-install-message').hidden = false; - } - } + // We would like to hide managed installed message since this + // extension is disabled. + var isRequired = + !extension.userMayModify || extension.mustRemainInstalled; + this.updateVisibility_(row, '.managed-message', isRequired && + !extension.disableReasons.updateRequired); + + // Then the 'This isn't from the webstore, looks suspicious' message. + this.updateVisibility_(row, '.suspicious-install-message', !isRequired && + extension.disableReasons.suspiciousInstall); + + // Then the 'This is a corrupt extension' message. + this.updateVisibility_(row, '.corrupt-install-message', !isRequired && + extension.disableReasons.corruptInstall); + + // Then the 'An update required by enterprise policy' message. Note that + // a force-installed extension might be disabled due to being outdated + // as well. + this.updateVisibility_(row, '.update-required-message', + extension.disableReasons.updateRequired); - if (extension.dependentExtensions.length > 0) { - var dependentMessage = - node.querySelector('.dependent-extensions-message'); - dependentMessage.hidden = false; - var dependentList = dependentMessage.querySelector('ul'); + // The 'following extensions depend on this extension' list. + var hasDependents = extension.dependentExtensions.length > 0; + row.classList.toggle('developer-extras', hasDependents); + this.updateVisibility_(row, '.dependent-extensions-message', + hasDependents, function(item) { + var dependentList = item.querySelector('ul'); + dependentList.textContent = ''; var dependentTemplate = $('template-collection').querySelector( '.dependent-list-item'); - extension.dependentExtensions.forEach(function(elem) { + extension.dependentExtensions.forEach(function(dependentId) { + var dependentExtension = null; + for (var i = 0; i < this.extensions_.length; ++i) { + if (this.extensions_[i].id == dependentId) { + dependentExtension = this.extensions_[i]; + break; + } + } + if (!dependentExtension) + return; + var depNode = dependentTemplate.cloneNode(true); - depNode.querySelector('.dep-extension-title').textContent = elem.name; - depNode.querySelector('.dep-extension-id').textContent = elem.id; + depNode.querySelector('.dep-extension-title').textContent = + dependentExtension.name; + depNode.querySelector('.dep-extension-id').textContent = + dependentExtension.id; dependentList.appendChild(depNode); - }); - } + }, this); + }.bind(this)); + + // The active views. + this.updateVisibility_(row, '.active-views', extension.views.length > 0, + function(item) { + var link = item.querySelector('a'); + + // Link needs to be an only child before the list is updated. + while (link.nextElementSibling) + item.removeChild(link.nextElementSibling); - // Then active views. - if (extension.views.length > 0) { - var activeViews = node.querySelector('.active-views'); - activeViews.hidden = false; - var link = activeViews.querySelector('a'); + // Link needs to be cleaned up if it was used before. + link.textContent = ''; + if (link.clickHandler) + link.removeEventListener('click', link.clickHandler); extension.views.forEach(function(view, i) { - var displayName = view.generatedBackgroundPage ? - loadTimeData.getString('backgroundPage') : view.path; + if (view.type == chrome.developerPrivate.ViewType.EXTENSION_DIALOG || + view.type == chrome.developerPrivate.ViewType.EXTENSION_POPUP) { + return; + } + var displayName; + if (view.url.indexOf('chrome-extension://') == 0) { + var pathOffset = 'chrome-extension://'.length + 32 + 1; + displayName = view.url.substring(pathOffset); + if (displayName == '_generated_background_page.html') + displayName = loadTimeData.getString('backgroundPage'); + } else { + displayName = view.url; + } var label = displayName + (view.incognito ? ' ' + loadTimeData.getString('viewIncognito') : '') + (view.renderProcessId == -1 ? ' ' + loadTimeData.getString('viewInactive') : ''); link.textContent = label; - link.addEventListener('click', function(e) { - // TODO(estade): remove conversion to string? - chrome.send('extensionSettingsInspect', [ - String(extension.id), - String(view.renderProcessId), - String(view.renderViewId), - view.incognito - ]); - }); + link.clickHandler = function(e) { + chrome.developerPrivate.openDevTools({ + extensionId: extension.id, + renderProcessId: view.renderProcessId, + renderViewId: view.renderViewId, + incognito: view.incognito + }); + }; + link.addEventListener('click', link.clickHandler); if (i < extension.views.length - 1) { link = link.cloneNode(true); - activeViews.appendChild(link); + item.appendChild(link); } }); - } - // The extension warnings (describing runtime issues). - if (extension.warnings) { - var panel = node.querySelector('.extension-warnings'); - panel.hidden = false; - var list = panel.querySelector('ul'); - extension.warnings.forEach(function(warning) { - list.appendChild(document.createElement('li')).innerText = warning; - }); - } + var allLinks = item.querySelectorAll('a'); + for (var i = 0; i < allLinks.length; ++i) { + allLinks[i].setAttribute('column-type', 'dev-activeViews' + i); + } + }); - // If the ErrorConsole is enabled, we should have manifest and/or runtime - // errors. Otherwise, we may have install warnings. We should not have - // both ErrorConsole errors and install warnings. - if (extension.manifestErrors) { - var panel = node.querySelector('.manifest-errors'); - panel.hidden = false; - panel.appendChild(new extensions.ExtensionErrorList( - extension.manifestErrors)); - } - if (extension.runtimeErrors) { - var panel = node.querySelector('.runtime-errors'); - panel.hidden = false; - panel.appendChild(new extensions.ExtensionErrorList( - extension.runtimeErrors)); - } - if (extension.installWarnings) { - var panel = node.querySelector('.install-warnings'); - panel.hidden = false; - var list = panel.querySelector('ul'); - extension.installWarnings.forEach(function(warning) { + // The extension warnings (describing runtime issues). + this.updateVisibility_(row, '.extension-warnings', + extension.runtimeWarnings.length > 0, + function(item) { + var warningList = item.querySelector('ul'); + warningList.textContent = ''; + extension.runtimeWarnings.forEach(function(warning) { var li = document.createElement('li'); - li.innerText = warning.message; - list.appendChild(li); + warningList.appendChild(li).innerText = warning; }); - } + }); + + // Install warnings. + this.updateVisibility_(row, '.install-warnings', + extension.installWarnings.length > 0, + function(item) { + var installWarningList = item.querySelector('ul'); + installWarningList.textContent = ''; + if (extension.installWarnings) { + extension.installWarnings.forEach(function(warning) { + var li = document.createElement('li'); + li.innerText = warning; + installWarningList.appendChild(li); + }); + } + }); - this.appendChild(node); if (location.hash.substr(1) == extension.id) { // Scroll beneath the fixed header so that the extension is not // obscured. - var topScroll = node.offsetTop - $('page-header').offsetHeight; - var pad = parseInt(window.getComputedStyle(node, null).marginTop, 10); + var topScroll = row.offsetTop - $('page-header').offsetHeight; + var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10); if (!isNaN(pad)) topScroll -= pad / 2; setScrollTopForDocument(document, topScroll); } + + row.updateFocusableElements(); + }, + + /** + * Updates an element's textContent. + * @param {Element} node Ancestor of the element specified by |query|. + * @param {string} query A query to select an element in |node|. + * @param {string} textContent + * @private + */ + setText_: function(node, query, textContent) { + node.querySelector(query).textContent = textContent; + }, + + /** + * Updates an element's visibility and calls |shownCallback| if it is + * visible. + * @param {Element} node Ancestor of the element specified by |query|. + * @param {string} query A query to select an element in |node|. + * @param {boolean} visible Whether the element should be visible or not. + * @param {function(Element)=} opt_shownCallback Callback if the element is + * visible. The element passed in will be the element specified by + * |query|. + * @private + */ + updateVisibility_: function(node, query, visible, opt_shownCallback) { + var item = assert(node.querySelector(query)); + item.hidden = !visible; + if (visible && opt_shownCallback) + opt_shownCallback(item); }, /** @@ -531,8 +1084,10 @@ cr.define('options', function() { return; // Get the extension from the given id. - var extension = this.data_.extensions.filter(function(extension) { - return extension.enabled && extension.id == extensionId; + var extension = this.extensions_.filter(function(extension) { + return extension.state == + chrome.developerPrivate.ExtensionState.ENABLED && + extension.id == extensionId; })[0]; if (!extension) @@ -540,24 +1095,85 @@ cr.define('options', function() { if (scroll) this.scrollToNode_(extensionId); + // Add the options query string. Corner case: the 'options' query string // will clobber the 'id' query string if the options link is clicked when // 'id' is in the URL, or if both query strings are in the URL. uber.replaceState({}, '?options=' + extensionId); - extensions.ExtensionOptionsOverlay.getInstance(). - setExtensionAndShowOverlay(extensionId, - extension.name, - extension.icon); - + var overlay = extensions.ExtensionOptionsOverlay.getInstance(); + var shownCallback = function() { + // This overlay doesn't get focused automatically as <extensionoptions> + // is created after the overlay is shown. + if (cr.ui.FocusOutlineManager.forDocument(document).visible) + overlay.setInitialFocus(); + }; + overlay.setExtensionAndShow(extensionId, extension.name, + extension.iconUrl, shownCallback); this.optionsShown_ = true; - $('overlay').addEventListener('cancelOverlay', function() { - this.optionsShown_ = false; - }.bind(this)); + + var self = this; + $('overlay').addEventListener('cancelOverlay', function f() { + self.optionsShown_ = false; + $('overlay').removeEventListener('cancelOverlay', f); + + // Remove the options query string. + uber.replaceState({}, ''); + }); + + // TODO(dbeam): why do we need to focus <extensionoptions> before and + // after its showing animation? Makes very little sense to me. + overlay.setInitialFocus(); + }, + + /** + * Hides the extension options overlay for the extension with id + * |extensionId|. If there is an overlay showing for a different extension, + * nothing happens. + * @param {string} extensionId ID of the extension to hide. + * @private + */ + hideEmbeddedExtensionOptions_: function(extensionId) { + if (!this.optionsShown_) + return; + + var overlay = extensions.ExtensionOptionsOverlay.getInstance(); + if (overlay.getExtensionId() == extensionId) + overlay.close(); }, + + /** + * Updates the node for the extension. + * @param {!ExtensionInfo} extension The information about the extension to + * update. + * @private + */ + updateExtension_: function(extension) { + var currIndex = this.getIndexOfExtension_(extension.id); + if (currIndex != -1) { + // If there is a current version of the extension, update it with the + // new version. + this.extensions_[currIndex] = extension; + } else { + // If the extension isn't found, push it back and sort. Technically, we + // could optimize by inserting it at the right location, but since this + // only happens on extension install, it's not worth it. + this.extensions_.push(extension); + this.extensions_.sort(compareExtensions); + } + + var node = /** @type {ExtensionFocusRow} */ ($(extension.id)); + if (node) { + this.updateNode_(extension, node); + } else { + var nextExt = this.extensions_[this.extensions_.indexOf(extension) + 1]; + this.createNode_(extension, nextExt ? $(nextExt.id) : null); + } + } }; return { - ExtensionsList: ExtensionsList + ExtensionList: ExtensionList, + ExtensionListDelegate: ExtensionListDelegate }; }); diff --git a/chromium/chrome/browser/resources/extensions/extension_load_error.html b/chromium/chrome/browser/resources/extensions/extension_load_error.html index 6feec95b289..9aed931ce17 100644 --- a/chromium/chrome/browser/resources/extensions/extension_load_error.html +++ b/chromium/chrome/browser/resources/extensions/extension_load_error.html @@ -1,4 +1,4 @@ -<!DOCTYPE html> +<!doctype html> <!-- Copyright 2014 The Chromium Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found diff --git a/chromium/chrome/browser/resources/extensions/extension_loader.js b/chromium/chrome/browser/resources/extensions/extension_loader.js index a71baafaec7..95def83d8bc 100644 --- a/chromium/chrome/browser/resources/extensions/extension_loader.js +++ b/chromium/chrome/browser/resources/extensions/extension_loader.js @@ -80,7 +80,7 @@ cr.define('extensions', function() { /** * An array of Failures for keeping track of multiple active failures. - * @type {Array.<Failure>} + * @type {Array<Failure>} * @private */ this.failures_ = []; @@ -103,7 +103,7 @@ cr.define('extensions', function() { /** * Add a failure to failures_ array. If there is already a displayed * failure, display the additional failures element. - * @param {Array.<Object>} failures Array of failures containing paths, + * @param {Array<Object>} failures Array of failures containing paths, * errors, and manifests. * @private */ @@ -186,17 +186,31 @@ cr.define('extensions', function() { ExtensionLoader.prototype = { /** + * Whether or not we are currently loading an unpacked extension. + * @private {boolean} + */ + isLoading_: false, + + /** * Begin the sequence of loading an unpacked extension. If an error is * encountered, this object will get notified via notifyFailed(). */ loadUnpacked: function() { - chrome.send('extensionLoaderLoadUnpacked'); + if (this.isLoading_) // Only one running load at a time. + return; + this.isLoading_ = true; + chrome.developerPrivate.loadUnpacked({failQuietly: true}, function() { + // Check lastError to avoid the log, but don't do anything with it - + // error-handling is done on the C++ side. + var lastError = chrome.runtime.lastError; + this.isLoading_ = false; + }.bind(this)); }, /** * Notify the ExtensionLoader that loading an unpacked extension failed. * Add the failure to failures_ and show the ExtensionLoadError. - * @param {Array.<Object>} failures Array of failures containing paths, + * @param {Array<Object>} failures Array of failures containing paths, * errors, and manifests. */ notifyFailed: function(failures) { @@ -206,7 +220,7 @@ cr.define('extensions', function() { /** * A static forwarding function for ExtensionLoader.notifyFailed. - * @param {Array.<Object>} failures Array of failures containing paths, + * @param {Array<Object>} failures Array of failures containing paths, * errors, and manifests. * @see ExtensionLoader.notifyFailed */ diff --git a/chromium/chrome/browser/resources/extensions/extension_options_overlay.js b/chromium/chrome/browser/resources/extensions/extension_options_overlay.js index 11f05637cca..8c09d153afc 100644 --- a/chromium/chrome/browser/resources/extensions/extension_options_overlay.js +++ b/chromium/chrome/browser/resources/extensions/extension_options_overlay.js @@ -24,6 +24,13 @@ cr.define('extensions', function() { showOverlay_: null, /** + * The id of the extension that this options page display. + * @type {string} + * @private + */ + extensionId_: '', + + /** * Initialize the page. * @param {function(HTMLDivElement)} showOverlay The function to show or * hide the ExtensionOptionsOverlay; this should take a single parameter @@ -40,6 +47,19 @@ cr.define('extensions', function() { this.showOverlay_ = showOverlay; }, + setInitialFocus: function() { + this.getExtensionOptions_().focus(); + }, + + /** + * @return {?Element} + * @private + */ + getExtensionOptions_: function() { + return $('extension-options-overlay-guest').querySelector( + 'extensionoptions'); + }, + /** * Handles a click on the close button. * @param {Event} event The click event. @@ -47,17 +67,11 @@ cr.define('extensions', function() { */ handleDismiss_: function(event) { this.setVisible_(false); - var extensionoptions = - $('extension-options-overlay-guest') - .querySelector('extensionoptions'); - + var extensionoptions = this.getExtensionOptions_(); if (extensionoptions) $('extension-options-overlay-guest').removeChild(extensionoptions); $('extension-options-overlay-icon').removeAttribute('src'); - - // Remove the options query string. - uber.replaceState({}, ''); }, /** @@ -67,14 +81,22 @@ cr.define('extensions', function() { * @param {string} extensionName The name of the extension, which is used * as the header of the overlay. * @param {string} extensionIcon The URL of the extension's icon. + * @param {function():void} shownCallback A function called when + * showing completes. * @suppress {checkTypes} * TODO(vitalyp): remove the suppression after adding * chrome/renderer/resources/extensions/extension_options.js * to dependencies. */ - setExtensionAndShowOverlay: function(extensionId, - extensionName, - extensionIcon) { + setExtensionAndShow: function(extensionId, + extensionName, + extensionIcon, + shownCallback) { + var overlay = $('extension-options-overlay'); + var overlayHeader = $('extension-options-overlay-header'); + var overlayGuest = $('extension-options-overlay-guest'); + var overlayStyle = window.getComputedStyle(overlay); + $('extension-options-overlay-title').textContent = extensionName; $('extension-options-overlay-icon').src = extensionIcon; @@ -82,76 +104,111 @@ cr.define('extensions', function() { var extensionoptions = new window.ExtensionOptions(); extensionoptions.extension = extensionId; - extensionoptions.autosize = 'on'; + this.extensionId_ = extensionId; // The <extensionoptions> content's size needs to be restricted to the - // bounds of the overlay window. The overlay gives a min width and - // max height, but the maxheight does not include our header height - // (title and close button), so we need to subtract that to get the - // max height for the extension options. - var headerHeight = $('extension-options-overlay-header').offsetHeight; - var overlayMaxHeight = - parseInt($('extension-options-overlay').style.maxHeight, 10); - extensionoptions.maxheight = overlayMaxHeight - headerHeight; - - extensionoptions.minwidth = - parseInt(window.getComputedStyle($('extension-options-overlay')) - .minWidth, 10); - - extensionoptions.setDeferAutoSize(true); + // bounds of the overlay window. The overlay gives a minWidth and + // maxHeight, but the maxHeight does not include our header height (title + // and close button), so we need to subtract that to get the maxHeight + // for the extension options. + var maxHeight = parseInt(overlayStyle.maxHeight, 10) - + overlayHeader.offsetHeight; + var minWidth = parseInt(overlayStyle.minWidth, 10); extensionoptions.onclose = function() { cr.dispatchSimpleEvent($('overlay'), 'cancelOverlay'); }.bind(this); + // Track the current animation (used to grow/shrink the overlay content), + // if any. Preferred size changes can fire more rapidly than the + // animation speed, and multiple animations running at the same time has + // undesirable effects. + var animation = null; + /** - * Resize the overlay if the <extensionoptions> changes size. - * @param {{newHeight: number, - * newWidth: number, - * oldHeight: number, - * oldWidth: number}} evt + * Resize the overlay if the <extensionoptions> changes preferred size. + * @param {{width: number, height: number}} evt */ - extensionoptions.onsizechanged = function(evt) { - var overlayStyle = - window.getComputedStyle($('extension-options-overlay')); - var oldWidth = parseInt(overlayStyle.width, 10); - var oldHeight = parseInt(overlayStyle.height, 10); + extensionoptions.onpreferredsizechanged = function(evt) { + var oldOverlayWidth = parseInt(overlayStyle.width, 10); + var oldOverlayHeight = parseInt(overlayStyle.height, 10); + var newOverlayWidth = Math.max(evt.width, minWidth); + // |evt.height| is just the new overlay guest height, and does not + // include the overlay header height, so it needs to be added. + var newOverlayHeight = + Math.min(evt.height + overlayHeader.offsetHeight, maxHeight); // animationTime is the amount of time in ms that will be used to resize // the overlay. It is calculated by multiplying the pythagorean distance // between old and the new size (in px) with a constant speed of // 0.25 ms/px. - var animationTime = 0.25 * Math.sqrt( - Math.pow(evt.newWidth - oldWidth, 2) + - Math.pow(evt.newHeight - oldHeight, 2)); - - var player = $('extension-options-overlay').animate([ - {width: oldWidth + 'px', height: oldHeight + 'px'}, - {width: evt.newWidth + 'px', height: evt.newHeight + 'px'} + var loading = document.documentElement.classList.contains('loading'); + var animationTime = loading ? 0 : + 0.25 * Math.sqrt(Math.pow(newOverlayWidth - oldOverlayWidth, 2) + + Math.pow(newOverlayHeight - oldOverlayHeight, 2)); + + if (animation) + animation.cancel(); + + // The header height must be added to the (old and new) preferred + // heights to get the full overlay heights. + animation = overlay.animate([ + {width: oldOverlayWidth + 'px', height: oldOverlayHeight + 'px'}, + {width: newOverlayWidth + 'px', height: newOverlayHeight + 'px'} ], { duration: animationTime, delay: 0 }); - player.onfinish = function(e) { - // Allow the <extensionoptions> to autosize now that the overlay - // has resized, and move it back on-screen. - extensionoptions.resumeDeferredAutoSize(); - $('extension-options-overlay-guest').style.position = 'static'; - $('extension-options-overlay-guest').style.left = 'auto'; + animation.onfinish = function(e) { + animation = null; + + // The <extensionoptions> element is ready to place back in the + // overlay. Make sure that it's sized to take up the full width/height + // of the overlay. + overlayGuest.style.position = ''; + overlayGuest.style.left = ''; + overlayGuest.style.width = newOverlayWidth + 'px'; + // |newOverlayHeight| includes the header height, so it needs to be + // subtracted to get the new guest height. + overlayGuest.style.height = + (newOverlayHeight - overlayHeader.offsetHeight) + 'px'; + + if (shownCallback) { + shownCallback(); + shownCallback = null; + } }; }.bind(this); - // Don't allow the <extensionoptions> to autosize until the overlay - // animation is complete. - extensionoptions.setDeferAutoSize(true); + // Move the <extensionoptions> off screen until the overlay is ready. + overlayGuest.style.position = 'fixed'; + overlayGuest.style.left = window.outerWidth + 'px'; + // Begin rendering at the default dimensions. This is also necessary to + // cancel any width/height set on a previous render. + // TODO(kalman): This causes a visual jag where the overlay guest shows + // up briefly. It would be better to render this off-screen first, then + // swap it in place. See crbug.com/408274. + // This may also solve crbug.com/431001 (width is 0 on initial render). + overlayGuest.style.width = ''; + overlayGuest.style.height = ''; + + overlayGuest.appendChild(extensionoptions); + }, - // Move the <extensionoptions> off screen until the overlay is ready - $('extension-options-overlay-guest').style.position = 'fixed'; - $('extension-options-overlay-guest').style.left = - window.outerWidth + 'px'; + /** + * Dispatches a 'cancelOverlay' event on the $('overlay') element. + */ + close: function() { + cr.dispatchSimpleEvent($('overlay'), 'cancelOverlay'); + }, - $('extension-options-overlay-guest').appendChild(extensionoptions); + /** + * Returns extension id that this options page set. + * @return {string} + */ + getExtensionId: function() { + return this.extensionId_; }, /** diff --git a/chromium/chrome/browser/resources/extensions/extensions.css b/chromium/chrome/browser/resources/extensions/extensions.css index 81d1d2137c1..47119b78412 100644 --- a/chromium/chrome/browser/resources/extensions/extensions.css +++ b/chromium/chrome/browser/resources/extensions/extensions.css @@ -3,7 +3,7 @@ * found in the LICENSE file. */ html.loading * { - -webkit-transition-duration: 0 !important; + transition-duration: 0ms !important; } html:not(.focus-outline-visible) @@ -16,85 +16,47 @@ html:not(.focus-outline-visible) overflow-y: hidden; } +#extension-settings.showing-banner { + margin-top: 45px; +} + /* Developer mode */ #dev-controls { - -webkit-margin-end: 20px; - -webkit-transition: padding 100ms, height 100ms, opacity 100ms; - border-bottom: 1px solid #eee; + -webkit-margin-end: 0; height: 0; - opacity: 0; overflow: hidden; } -#dev-controls .buttons-container { - -webkit-padding-end: 3px; - -webkit-padding-start: 4px; +#dev-controls.animated { + transition: height 150ms; } -#dev-controls .buttons-container { - display: -webkit-box; - height: 32px; /* height + padding-top matches #dev-controls height. */ - padding-top: 13px; +.dev-mode #dev-controls { + border-bottom: 1px solid #eee; } -#dev-controls button { - white-space: nowrap; +#dev-controls > * { + padding: 8px 3px; } -#apps-developer-tools-promo { - -webkit-padding-end: 3px; - align-items: center; - border-top: 1px solid #eee; +#dev-controls .button-container { + -webkit-padding-end: 12px; + -webkit-padding-start: 12px; display: flex; - font-size: 13px; - margin-top: 7px; /* This matches #dev-controls padding-bottom. */ - padding-top: 5px; -} - -#apps-developer-tools-promo img { - content: url(apps_developer_tools_promo_48.png); -} - -#apps-developer-tools-promo-text { - -webkit-margin-start: 5px; + flex-wrap: wrap; } -#apps-developer-tools-promo-close-wrapper { - display: flex; - flex-grow: 1; - justify-content: flex-end; -} - -#apps-developer-tools-promo .close-button { - background: url(chrome://theme/IDR_CLOSE_DIALOG) no-repeat center center; - border: 0; - height: 14px; - width: 14px; - z-index: 1; -} - -#apps-developer-tools-promo .close-button:hover { - background-image: url(chrome://theme/IDR_CLOSE_DIALOG_H); -} - -#apps-developer-tools-promo .close-button:active { - background-image: url(chrome://theme/IDR_CLOSE_DIALOG_P); -} - -#extension-settings.dev-mode #dev-controls { - -webkit-transition-duration: 250ms; - height: 45px; - opacity: 1; - padding-bottom: 7px; +#dev-controls button { + white-space: nowrap; } -#extension-settings.dev-mode.adt-promo #dev-controls { - height: 105px; /* Allow more height for the Apps Developer Tools promo. */ +#dev-controls .button-container button:not(:last-of-type) { + -webkit-margin-end: 5px; } #dev-controls-spacer { - -webkit-box-flex: 1; + flex: 1; } #dev-toggle { @@ -123,18 +85,44 @@ html:not(.focus-outline-visible) text-align: right; } -#extension-settings:not(.dev-mode) .developer-extras { - display: none; +.extension-code-empty { + background-color: #eee; + display: inline-block; + line-height: 100px; /* Vertically centers text and serves as min-height. */ + text-align: center; + width: 100%; } -.developer-extras > div, -.permanent-warnings > div { +.extension-details > .developer-extras > div, +.extension-details > .permanent-warnings > div { margin: 5px 0; } +.dependent-extensions-message, +.suspicious-install-message { + line-height: 150%; +} + +#page-header > .page-banner > .page-banner-gradient { + -webkit-margin-end: 0; +} + +#header-controls { + right: 13px; +} + +html[dir='rtl'] #header-controls { + left: 13px; + right: auto; +} + +#page-header > h1::after { + -webkit-margin-end: 0; +} + #extension-settings #page-header { /* These values match the .page values. */ - -webkit-margin-end: 24px; + -webkit-margin-end: 0; min-width: 576px; } @@ -148,6 +136,10 @@ html:not(.focus-outline-visible) font-weight: bold; } +#no-extensions { + margin-top: 3em; +} + #suggest-gallery { -webkit-padding-start: 10px; } @@ -156,6 +148,7 @@ html:not(.focus-outline-visible) background: url(chrome://theme/IDR_WEBSTORE_ICON_32) no-repeat left center; background-size: 32px 32px; font-size: 1.25em; + margin: 24px 12px 12px 12px; } html[dir=rtl] #footer-section { @@ -167,23 +160,14 @@ html[dir=rtl] #footer-section { line-height: 32px; } -.empty-extension-list { - height: 3em; -} - -.loading #no-extensions, -.loading #footer-section, -#extension-settings-list:not(.empty-extension-list) ~ #no-extensions, -.empty-extension-list ~ #footer-section { - display: none; -} - .extension-list-item-wrapper { - margin: 23px 0; + margin: 12px 0; + padding: 12px; } .extension-list-item { background-repeat: no-repeat; + background-size: 48px 48px; display: -webkit-box; min-height: 48px; } @@ -194,7 +178,7 @@ html[dir=rtl] #footer-section { } html[dir='rtl'] .extension-list-item { - background-position: right; + background-position-x: 100%; } .extension-title { @@ -235,10 +219,17 @@ html[dir='rtl'] .extension-list-item { -webkit-margin-start: 0; } +.action-links .errors-link { + align-items: center; + display: inline-flex; + vertical-align: bottom; +} + .extension-details { -webkit-box-flex: 1; -webkit-padding-end: 7px; - -webkit-padding-start: 55px; + -webkit-padding-start: 60px; + padding-top: 6px; } .extension-description, @@ -247,15 +238,24 @@ html[dir='rtl'] .extension-list-item { .location-text, .blacklist-text, .enable-checkbox input:disabled + .enable-checkbox-text { - color: rgb(151, 156, 160); + color: rgb(115, 119, 122); } .enable-controls { /* Matches right: position of dev controls toggle. */ - -webkit-margin-end: 20px; + -webkit-margin-end: 0; position: relative; } +.enable-controls > .controlled-setting-indicator { + width: 23px; +} + +.enable-controls > .controlled-setting-indicator > div { + left: 11px; + right: 11px; +} + /* We use x[is='action-link'] here so that we get higher specifity than the * action link rules without resorting to the Dark Side (!IMPORTANT). */ .terminated-reload-link[is='action-link'], @@ -279,6 +279,10 @@ html[dir='rtl'] .extension-list-item { display: none; } +.optional-controls .checkbox { + -webkit-margin-end: 12px; +} + .load-path > span { word-wrap: break-word; } @@ -297,8 +301,9 @@ html[dir='rtl'] .extension-list-item { .install-warnings, .extension-warnings { border-radius: 3px; - margin-top: 5px; - padding: 2px 5px; + line-height: 150%; + margin: 8px 0; + padding: 8px 12px; } .butter-bar { @@ -317,11 +322,11 @@ html[dir='rtl'] .extension-list-item { .error-collection-control { -webkit-margin-start: 5px; - display: none; } -#extension-settings.dev-mode .error-collection-control { - display: initial; +#extension-settings:not(.dev-mode) .developer-extras, +#extension-settings:not(.dev-mode) .error-collection-control { + display: none; } #font-measuring-div { @@ -367,16 +372,16 @@ html[dir=rtl] .extension-commands-config { /* Trash */ #extension-settings .trash { - -webkit-transition: opacity 200ms; height: 22px; opacity: 0.8; position: relative; - right: 0; + right: -8px; top: 6px; + transition: opacity 200ms; } html[dir='rtl'] #extension-settings .trash { - left: 0; + left: -8px; right: auto; } @@ -388,10 +393,13 @@ html[dir='rtl'] #extension-settings .trash { visibility: hidden; } -.extension-highlight { - background: rgb(250, 250, 250); - border-radius: 3px; - padding: 5px 0 5px 5px; +/* In case the extension is policy controlled the trash icon must be hidden by + * setting display:none rather than only setting visibility:hidden to completely + * remove it from the layout and make space for the controlled indicator. + * TODO(cschuet): rather than hide always replace it with something meaningful. + */ +.extension-list-item-wrapper.policy-controlled .trash { + display: none; } /* Supervised users */ @@ -415,3 +423,7 @@ html[dir='rtl'] #extension-settings .trash { -webkit-padding-start: 8px; background-image: none; } + +.extension-highlight { + background-color: rgba(0, 0, 0, .05); +} diff --git a/chromium/chrome/browser/resources/extensions/extensions.html b/chromium/chrome/browser/resources/extensions/extensions.html index b7b2ff88c83..fede5ebdc7a 100644 --- a/chromium/chrome/browser/resources/extensions/extensions.html +++ b/chromium/chrome/browser/resources/extensions/extensions.html @@ -1,5 +1,5 @@ -<!DOCTYPE html> -<html i18n-values="dir:textdirection;" class="loading"> +<!doctype html> +<html i18n-values="dir:textdirection;lang:language"> <head> <meta charset="utf-8"> @@ -11,6 +11,9 @@ <link rel="stylesheet" href="extension_options_overlay.css"> <link rel="stylesheet" href="pack_extension_overlay.css"> <link rel="stylesheet" href="chrome://resources/css/alert_overlay.css"> +<link rel="stylesheet" href="chrome://resources/css/bubble.css"> +<link rel="stylesheet" href="chrome://resources/css/bubble_button.css"> +<link rel="stylesheet" href="chrome://resources/css/controlled_indicator.css"> <link rel="stylesheet" href="chrome://resources/css/chrome_shared.css"> <link rel="stylesheet" href="chrome://resources/css/overlay.css"> <link rel="stylesheet" href="chrome://resources/css/trash.css"> @@ -18,13 +21,18 @@ <script src="chrome://resources/js/action_link.js"></script> <script src="chrome://resources/js/cr.js"></script> +<script src="chrome://resources/js/event_tracker.js"></script> <script src="chrome://resources/js/load_time_data.js"></script> <script src="chrome://resources/js/util.js"></script> <script src="chrome://resources/js/cr/ui.js"></script> <script src="chrome://resources/js/cr/ui/alert_overlay.js"></script> +<script src="chrome://resources/js/cr/ui/bubble.js"></script> +<script src="chrome://resources/js/cr/ui/bubble_button.js"></script> +<script src="chrome://resources/js/cr/ui/controlled_indicator.js"></script> <script src="chrome://resources/js/cr/ui/drag_wrapper.js"></script> <script src="chrome://resources/js/cr/ui/focus_manager.js"></script> <script src="chrome://resources/js/cr/ui/focus_outline_manager.js"></script> +<script src="chrome://resources/js/cr/ui/node_utils.js"></script> <script src="chrome://resources/js/cr/ui/overlay.js"></script> <if expr="chromeos"> @@ -43,8 +51,7 @@ <script src="chrome://extensions-frame/extensions.js"></script> </head> -<body class="uber-frame" - i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> +<body class="uber-frame"> <div id="overlay" class="overlay" hidden> <include src="extension_commands_overlay.html"> @@ -62,7 +69,8 @@ </div> <div class="page" id="extension-settings"> - <header id="page-header"><h1 i18n-content="extensionSettings"></h1> + <header id="page-header"> + <h1 i18n-content="extensionSettings"></h1> <div id="header-controls" class="header-extras"> <div id="dev-toggle" class="checkbox"> <label> @@ -78,8 +86,8 @@ </div> </div> </header> - <div id="dev-controls" hidden> - <div class="buttons-container"> + <div id="dev-controls"> + <div class="button-container"> <button id="load-unpacked" i18n-content="extensionSettingsLoadUnpackedButton"></button> <button id="pack-extension" @@ -92,33 +100,24 @@ <button id="update-extensions-now" i18n-content="extensionSettingsUpdateButton"></button> </div> - <div id="apps-developer-tools-promo"> - <img></img> - <span id="apps-developer-tools-promo-text" - i18n-values=".innerHTML:extensionSettingsAppsDevToolsPromoHTML"> - </span> - <div id="apps-developer-tools-promo-close-wrapper"> - <button i18n-values="title:extensionSettingsAppDevToolsPromoClose" - class="custom-appearance close-button"></button> - </div> - </div> </div> <include src="extension_load_error.html"> - <div id="extension-settings-list" class="empty-extension-list"></div> - <div id="no-extensions"> + <div id="extension-list-wrapper" hidden> + <div id="footer-section"> + <a target="_blank" class="more-extensions-link" + i18n-values="href:extensionSettingsGetMoreExtensionsUrl" + i18n-content="extensionSettingsGetMoreExtensions"></a> + <a is="action-link" class="extension-commands-config" + i18n-content="extensionSettingsCommandsLink" hidden></a> + </div> + </div> + <div id="no-extensions" hidden> <span id="no-extensions-message" i18n-content="extensionSettingsNoExtensions"></span> <span id="suggest-gallery" class="more-extensions-link" i18n-values=".innerHTML:extensionSettingsSuggestGallery"> </span> </div> - <div id="footer-section"> - <a target="_blank" class="more-extensions-link" - i18n-values="href:extensionSettingsGetMoreExtensionsUrl" - i18n-content="extensionSettingsGetMoreExtensions"></a> - <a is="action-link" class="extension-commands-config" - i18n-content="extensionSettingsCommandsLink" hidden></a> - </div> </div> <span id="font-measuring-div"></span> @@ -147,6 +146,10 @@ i18n-content="extensionSettingsLaunch" hidden></a> <a is="action-link" role="button" class="reload-link" i18n-content="extensionSettingsReloadUnpacked" hidden></a> + <a is="action-link" role="button" class="errors-link"> + <img class="extension-error-icon"></img> + <span i18n-content="extensionErrorHeading"></span> + </a> </div> <div class="permanent-warnings"> <div class="extension-warnings" hidden> @@ -178,12 +181,13 @@ <div class="managed-message" i18n-content="extensionSettingsPolicyControlled" hidden> </div> + <div class="update-required-message" + i18n-content="extensionSettingsUpdateRequiredBePolicy" hidden> + </div> <div class="active-views" hidden> <span i18n-content="extensionSettingsInspectViews"></span> <a is="action-link"></a> </div> - <div class="manifest-errors" hidden></div> - <div class="runtime-errors" hidden></div> <div class="install-warnings" hidden> <span i18n-content="extensionSettingsInstallWarnings"></span> <ul></ul> @@ -244,7 +248,7 @@ </div> <li class="dependent-list-item"> <span class="dep-extension-title"></span> - <ul class="developer-extras"> + <ul> <li> <span i18n-content="extensionSettingsExtensionId"></span> <span class="dep-extension-id"></span> @@ -259,7 +263,7 @@ </div> <script src="chrome://extensions-frame/strings.js"></script> -<script src="chrome://resources/js/i18n_template2.js"></script> +<script src="chrome://resources/js/i18n_template.js"></script> </body> </html> diff --git a/chromium/chrome/browser/resources/extensions/extensions.js b/chromium/chrome/browser/resources/extensions/extensions.js index 42ad4a43770..5e1812dbf25 100644 --- a/chromium/chrome/browser/resources/extensions/extensions.js +++ b/chromium/chrome/browser/resources/extensions/extensions.js @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +<include src="../../../../ui/webui/resources/js/cr/ui/focus_row.js"> +<include src="../../../../ui/webui/resources/js/cr/ui/focus_grid.js"> <include src="../uber/uber_utils.js"> <include src="extension_code.js"> <include src="extension_commands_overlay.js"> @@ -16,30 +18,22 @@ <include src="chromeos/kiosk_apps.js"> </if> -/** - * The type of the extension data object. The definition is based on - * chrome/browser/ui/webui/extensions/extension_settings_handler.cc: - * ExtensionSettingsHandler::HandleRequestExtensionsData() - * @typedef {{developerMode: boolean, - * extensions: Array, - * incognitoAvailable: boolean, - * loadUnpackedDisabled: boolean, - * profileIsSupervised: boolean, - * promoteAppsDevTools: boolean}} - */ -var ExtensionDataResponse; - // Used for observing function of the backend datasource for this page by // tests. var webuiResponded = false; cr.define('extensions', function() { - var ExtensionsList = options.ExtensionsList; + var ExtensionList = extensions.ExtensionList; // Implements the DragWrapper handler interface. var dragWrapperHandler = { /** @override */ shouldAcceptDrag: function(e) { + // External Extension installation can be disabled globally, e.g. while a + // different overlay is already showing. + if (!ExtensionSettings.getInstance().dragEnabled_) + return false; + // We can't access filenames during the 'dragenter' event, so we have to // wait until 'drop' to decide whether to do something with the file or // not. @@ -50,12 +44,11 @@ cr.define('extensions', function() { /** @override */ doDragEnter: function() { chrome.send('startDrag'); - ExtensionSettings.showOverlay(null); ExtensionSettings.showOverlay($('drop-target-overlay')); }, /** @override */ doDragLeave: function() { - ExtensionSettings.showOverlay(null); + this.hideDropTargetOverlay_(); chrome.send('stopDrag'); }, /** @override */ @@ -64,7 +57,7 @@ cr.define('extensions', function() { }, /** @override */ doDrop: function(e) { - ExtensionSettings.showOverlay(null); + this.hideDropTargetOverlay_(); if (e.dataTransfer.files.length != 1) return; @@ -89,12 +82,24 @@ cr.define('extensions', function() { e.preventDefault(); chrome.send(toSend); } + }, + + /** + * Hide the current overlay if it is the drop target overlay. + * @private + */ + hideDropTargetOverlay_: function() { + var currentOverlay = ExtensionSettings.getCurrentOverlay(); + if (currentOverlay && currentOverlay.id === 'drop-target-overlay') + ExtensionSettings.showOverlay(null); } }; /** * ExtensionSettings class * @class + * @constructor + * @implements {extensions.ExtensionListDelegate} */ function ExtensionSettings() {} @@ -104,11 +109,28 @@ cr.define('extensions', function() { __proto__: HTMLDivElement.prototype, /** - * Whether or not to try to display the Apps Developer Tools promotion. + * The drag-drop wrapper for installing external Extensions, if available. + * null if external Extension installation is not available. + * @type {cr.ui.DragWrapper} + * @private + */ + dragWrapper_: null, + + /** + * True if drag-drop is both available and currently enabled - it can be + * temporarily disabled while overlays are showing. * @type {boolean} * @private */ - displayPromo_: false, + dragEnabled_: false, + + /** + * Callback for testing purposes. This is called after the "Developer mode" + * checkbox is toggled and the div containing developer buttons' height has + * been set. + * @type {function()?} + */ + testingDeveloperModeCallback: null, /** * Perform initial setup. @@ -121,37 +143,47 @@ cr.define('extensions', function() { // Set the title. uber.setTitle(loadTimeData.getString('extensionSettings')); - // This will request the data to show on the page and will get a response - // back in returnExtensionsData. - chrome.send('extensionSettingsRequestExtensionsData'); + var extensionList = new ExtensionList(this); + extensionList.id = 'extension-settings-list'; + var wrapper = $('extension-list-wrapper'); + wrapper.insertBefore(extensionList, wrapper.firstChild); + + this.update_(); + // TODO(devlin): Remove this once all notifications are moved to events on + // the developerPrivate api. + chrome.send('extensionSettingsRegister'); var extensionLoader = extensions.ExtensionLoader.getInstance(); - $('toggle-dev-on').addEventListener('change', - this.handleToggleDevMode_.bind(this)); - $('dev-controls').addEventListener('webkitTransitionEnd', - this.handleDevControlsTransitionEnd_.bind(this)); + $('toggle-dev-on').addEventListener('change', function(e) { + this.updateDevControlsVisibility_(true); + extensionList.updateFocusableElements(); + chrome.developerPrivate.updateProfileConfiguration( + {inDeveloperMode: e.target.checked}); + var suffix = $('toggle-dev-on').checked ? 'Enabled' : 'Disabled'; + chrome.send('metricsHandler:recordAction', + ['Options_ToggleDeveloperMode_' + suffix]); + }.bind(this)); + + window.addEventListener('resize', function() { + this.updateDevControlsVisibility_(false); + }.bind(this)); // Set up the three dev mode buttons (load unpacked, pack and update). $('load-unpacked').addEventListener('click', function(e) { - extensionLoader.loadUnpacked(); + chrome.send('metricsHandler:recordAction', + ['Options_LoadUnpackedExtension']); + extensionLoader.loadUnpacked(); }); $('pack-extension').addEventListener('click', this.handlePackExtension_.bind(this)); $('update-extensions-now').addEventListener('click', this.handleUpdateExtensionNow_.bind(this)); - // Set up the close dialog for the apps developer tools promo. - $('apps-developer-tools-promo').querySelector('.close-button'). - addEventListener('click', function(e) { - this.displayPromo_ = false; - this.updatePromoVisibility_(); - chrome.send('extensionSettingsDismissADTPromo'); - }.bind(this)); - if (!loadTimeData.getBoolean('offStoreInstallEnabled')) { this.dragWrapper_ = new cr.ui.DragWrapper(document.documentElement, dragWrapperHandler); + this.dragEnabled_ = true; } extensions.PackExtensionOverlay.getInstance().initializePage(); @@ -170,6 +202,16 @@ cr.define('extensions', function() { extensions.ExtensionOptionsOverlay.getInstance().initializePage( extensions.ExtensionSettings.showOverlay); + // Add user action logging for bottom links. + var moreExtensionLink = + document.getElementsByClassName('more-extensions-link'); + for (var i = 0; i < moreExtensionLink.length; i++) { + moreExtensionLink[i].addEventListener('click', function(e) { + chrome.send('metricsHandler:recordAction', + ['Options_GetMoreExtensions']); + }); + } + // Initialize the kiosk overlay. if (cr.isChromeOS) { var kioskOverlay = extensions.KioskAppsOverlay.getInstance(); @@ -198,23 +240,52 @@ cr.define('extensions', function() { }, /** - * Updates the Chrome Apps and Extensions Developer Tools promotion's - * visibility. + * Updates the extensions page to the latest profile and extensions + * configuration. * @private */ - updatePromoVisibility_: function() { - var extensionSettings = $('extension-settings'); - var visible = extensionSettings.classList.contains('dev-mode') && - this.displayPromo_; - - var adtPromo = $('apps-developer-tools-promo'); - var controls = adtPromo.querySelectorAll('a, button'); - Array.prototype.forEach.call(controls, function(control) { - control[visible ? 'removeAttribute' : 'setAttribute']('tabindex', '-1'); - }); + update_: function() { + chrome.developerPrivate.getProfileConfiguration( + this.returnProfileConfiguration_.bind(this)); + }, + + /** + * [Re]-Populates the page with data representing the current state of + * installed extensions. + * @param {ProfileInfo} profileInfo + * @private + */ + returnProfileConfiguration_: function(profileInfo) { + webuiResponded = true; + /** @const */ + var supervised = profileInfo.isSupervised; + + var pageDiv = $('extension-settings'); + pageDiv.classList.toggle('profile-is-supervised', supervised); + pageDiv.classList.toggle('showing-banner', supervised); + + var devControlsCheckbox = $('toggle-dev-on'); + devControlsCheckbox.checked = profileInfo.inDeveloperMode; + devControlsCheckbox.disabled = supervised; + + this.updateDevControlsVisibility_(false); + + $('load-unpacked').disabled = !profileInfo.canLoadUnpacked; + var extensionList = $('extension-settings-list'); + extensionList.updateExtensionsData( + profileInfo.isIncognitoAvailable, + profileInfo.appInfoDialogEnabled).then(function() { + // We can get called many times in short order, thus we need to + // be careful to remove the 'finished loading' timeout. + if (this.loadingTimeout_) + window.clearTimeout(this.loadingTimeout_); + document.documentElement.classList.add('loading'); + this.loadingTimeout_ = window.setTimeout(function() { + document.documentElement.classList.remove('loading'); + }, 0); - adtPromo.setAttribute('aria-hidden', !visible); - extensionSettings.classList.toggle('adt-promo', visible); + this.onExtensionCountChanged(); + }.bind(this)); }, /** @@ -229,10 +300,9 @@ cr.define('extensions', function() { /** * Shows the Extension Commands configuration UI. - * @param {Event} e Change event. * @private */ - showExtensionCommandsConfigUi_: function(e) { + showExtensionCommandsConfigUi_: function() { ExtensionSettings.showOverlay($('extension-commands-overlay')); chrome.send('metricsHandler:recordAction', ['Options_ExtensionCommands']); @@ -253,130 +323,55 @@ cr.define('extensions', function() { * @private */ handleUpdateExtensionNow_: function(e) { - chrome.send('extensionSettingsAutoupdate'); + chrome.developerPrivate.autoUpdate(); + chrome.send('metricsHandler:recordAction', + ['Options_UpdateExtensions']); }, /** - * Handles the Toggle Dev Mode button. - * @param {Event} e Change event. + * Updates the visibility of the developer controls based on whether the + * [x] Developer mode checkbox is checked. + * @param {boolean} animated Whether to animate any updates. * @private */ - handleToggleDevMode_: function(e) { - if ($('toggle-dev-on').checked) { - $('dev-controls').hidden = false; - window.setTimeout(function() { - $('extension-settings').classList.add('dev-mode'); - }, 0); - } else { - $('extension-settings').classList.remove('dev-mode'); - } - window.setTimeout(this.updatePromoVisibility_.bind(this), 0); + updateDevControlsVisibility_: function(animated) { + var showDevControls = $('toggle-dev-on').checked; + $('extension-settings').classList.toggle('dev-mode', showDevControls); - chrome.send('extensionSettingsToggleDeveloperMode'); - }, + var devControls = $('dev-controls'); + devControls.classList.toggle('animated', animated); - /** - * Called when a transition has ended for #dev-controls. - * @param {Event} e webkitTransitionEnd event. - * @private - */ - handleDevControlsTransitionEnd_: function(e) { - if (e.propertyName == 'height' && - !$('extension-settings').classList.contains('dev-mode')) { - $('dev-controls').hidden = true; - } - }, - }; - - /** - * Called by the dom_ui_ to re-populate the page with data representing - * the current state of installed extensions. - * @param {ExtensionDataResponse} extensionsData - */ - ExtensionSettings.returnExtensionsData = function(extensionsData) { - // We can get called many times in short order, thus we need to - // be careful to remove the 'finished loading' timeout. - if (this.loadingTimeout_) - window.clearTimeout(this.loadingTimeout_); - document.documentElement.classList.add('loading'); - this.loadingTimeout_ = window.setTimeout(function() { - document.documentElement.classList.remove('loading'); - }, 0); - - webuiResponded = true; - - if (extensionsData.extensions.length > 0) { - // Enforce order specified in the data or (if equal) then sort by - // extension name (case-insensitive) followed by their ID (in the case - // where extensions have the same name). - extensionsData.extensions.sort(function(a, b) { - function compare(x, y) { - return x < y ? -1 : (x > y ? 1 : 0); - } - return compare(a.order, b.order) || - compare(a.name.toLowerCase(), b.name.toLowerCase()) || - compare(a.id, b.id); + var buttons = devControls.querySelector('.button-container'); + Array.prototype.forEach.call(buttons.querySelectorAll('a, button'), + function(control) { + control.tabIndex = showDevControls ? 0 : -1; }); - } - - var pageDiv = $('extension-settings'); - var marginTop = 0; - if (extensionsData.profileIsSupervised) { - pageDiv.classList.add('profile-is-supervised'); - } else { - pageDiv.classList.remove('profile-is-supervised'); - } - if (extensionsData.profileIsSupervised) { - pageDiv.classList.add('showing-banner'); - $('toggle-dev-on').disabled = true; - marginTop += 45; - } else { - pageDiv.classList.remove('showing-banner'); - $('toggle-dev-on').disabled = false; - } - - pageDiv.style.marginTop = marginTop + 'px'; - - if (extensionsData.developerMode) { - pageDiv.classList.add('dev-mode'); - $('toggle-dev-on').checked = true; - $('dev-controls').hidden = false; - } else { - pageDiv.classList.remove('dev-mode'); - $('toggle-dev-on').checked = false; - } + buttons.setAttribute('aria-hidden', !showDevControls); - ExtensionSettings.getInstance().displayPromo_ = - extensionsData.promoteAppsDevTools; - ExtensionSettings.getInstance().updatePromoVisibility_(); + window.requestAnimationFrame(function() { + devControls.style.height = !showDevControls ? '' : + buttons.offsetHeight + 'px'; - $('load-unpacked').disabled = extensionsData.loadUnpackedDisabled; + if (this.testingDeveloperModeCallback) + this.testingDeveloperModeCallback(); + }.bind(this)); + }, - ExtensionsList.prototype.data_ = extensionsData; - var extensionList = $('extension-settings-list'); - ExtensionsList.decorate(extensionList); + /** @override */ + onExtensionCountChanged: function() { + /** @const */ + var hasExtensions = $('extension-settings-list').getNumExtensions() != 0; + $('no-extensions').hidden = hasExtensions; + $('extension-list-wrapper').hidden = !hasExtensions; + }, }; - // Indicate that warning |message| has occured for pack of |crx_path| and - // |pem_path| files. Ask if user wants override the warning. Send - // |overrideFlags| to repeated 'pack' call to accomplish the override. - ExtensionSettings.askToOverrideWarning = - function(message, crx_path, pem_path, overrideFlags) { - var closeAlert = function() { - ExtensionSettings.showOverlay(null); - }; - - alertOverlay.setValues( - loadTimeData.getString('packExtensionWarningTitle'), - message, - loadTimeData.getString('packExtensionProceedAnyway'), - loadTimeData.getString('cancel'), - function() { - chrome.send('pack', [crx_path, pem_path, overrideFlags]); - closeAlert(); - }, - closeAlert); - ExtensionSettings.showOverlay($('alertOverlay')); + /** + * Called by the WebUI when something has changed and the extensions UI needs + * to be updated. + */ + ExtensionSettings.onExtensionsChanged = function() { + ExtensionSettings.getInstance().update_(); }; /** @@ -388,27 +383,31 @@ cr.define('extensions', function() { }; /** - * Sets the given overlay to show. This hides whatever overlay is currently - * showing, if any. - * @param {HTMLElement} node The overlay page to show. If falsey, all overlays + * Sets the given overlay to show. If the overlay is already showing, this is + * a no-op; otherwise, hides any currently-showing overlay. + * @param {HTMLElement} node The overlay page to show. If null, all overlays * are hidden. */ ExtensionSettings.showOverlay = function(node) { var pageDiv = $('extension-settings'); - if (node) { - pageDiv.style.width = window.getComputedStyle(pageDiv).width; - document.body.classList.add('no-scroll'); - } else { - document.body.classList.remove('no-scroll'); - pageDiv.style.width = ''; - } + pageDiv.style.width = node ? window.getComputedStyle(pageDiv).width : ''; + document.body.classList.toggle('no-scroll', !!node); var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay(); - if (currentlyShowingOverlay) + if (currentlyShowingOverlay) { + if (currentlyShowingOverlay == node) // Already displayed. + return; currentlyShowingOverlay.classList.remove('showing'); + } - if (node) + if (node) { + var lastFocused = document.activeElement; + $('overlay').addEventListener('cancelOverlay', function f() { + lastFocused.focus(); + $('overlay').removeEventListener('cancelOverlay', f); + }); node.classList.add('showing'); + } var pages = document.querySelectorAll('.page'); for (var i = 0; i < pages.length; i++) { @@ -416,10 +415,34 @@ cr.define('extensions', function() { } $('overlay').hidden = !node; + + if (node) + ExtensionSettings.focusOverlay(); + + // If drag-drop for external Extension installation is available, enable + // drag-drop when there is any overlay showing other than the usual overlay + // shown when drag-drop is started. + var settings = ExtensionSettings.getInstance(); + if (settings.dragWrapper_) + settings.dragEnabled_ = !node || node == $('drop-target-overlay'); + uber.invokeMethodOnParent(node ? 'beginInterceptingEvents' : 'stopInterceptingEvents'); }; + ExtensionSettings.focusOverlay = function() { + var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay(); + assert(currentlyShowingOverlay); + + if (cr.ui.FocusOutlineManager.forDocument(document).visible) + cr.ui.setInitialFocus(currentlyShowingOverlay); + + if (!currentlyShowingOverlay.contains(document.activeElement)) { + // Make sure focus isn't stuck behind the overlay. + document.activeElement.blur(); + } + }; + /** * Utility function to find the width of various UI strings and synchronize * the width of relevant spans. This is crucial for making sure the @@ -460,5 +483,6 @@ cr.define('extensions', function() { }); window.addEventListener('load', function(e) { + document.documentElement.classList.add('loading'); extensions.ExtensionSettings.getInstance().initialize(); }); diff --git a/chromium/chrome/browser/resources/extensions/pack_extension_overlay.html b/chromium/chrome/browser/resources/extensions/pack_extension_overlay.html index a7794bdfa57..be2394c69d3 100644 --- a/chromium/chrome/browser/resources/extensions/pack_extension_overlay.html +++ b/chromium/chrome/browser/resources/extensions/pack_extension_overlay.html @@ -1,4 +1,5 @@ <div id="pack-extension-overlay" class="page"> + <div class="close-button"></div> <h1 i18n-content="packExtensionOverlay"></h1> <div id="cbd-content-area" class="content-area"> <div class="pack-extension-heading" i18n-content="packExtensionHeading"> diff --git a/chromium/chrome/browser/resources/extensions/pack_extension_overlay.js b/chromium/chrome/browser/resources/extensions/pack_extension_overlay.js index 186f84102e6..03e6180a03d 100644 --- a/chromium/chrome/browser/resources/extensions/pack_extension_overlay.js +++ b/chromium/chrome/browser/resources/extensions/pack_extension_overlay.js @@ -48,24 +48,26 @@ cr.define('extensions', function() { handleCommit_: function(e) { var extensionPath = $('extension-root-dir').value; var privateKeyPath = $('extension-private-key').value; - chrome.send('pack', [extensionPath, privateKeyPath, 0]); + chrome.developerPrivate.packDirectory( + extensionPath, privateKeyPath, 0, this.onPackResponse_.bind(this)); }, /** * Utility function which asks the C++ to show a platform-specific file - * select dialog, and fire |callback| with the |filePath| that resulted. - * |selectType| can be either 'file' or 'folder'. |operation| can be 'load' - * or 'pem' which are signals to the C++ to do some operation-specific - * configuration. + * select dialog, and set the value property of |node| to the selected path. + * @param {chrome.developerPrivate.SelectType} selectType + * The type of selection to use. + * @param {chrome.developerPrivate.FileType} fileType + * The type of file to select. + * @param {HTMLInputElement} node The node to set the value of. * @private */ - showFileDialog_: function(selectType, operation, callback) { - window.handleFilePathSelected = function(filePath) { - callback(filePath); - window.handleFilePathSelected = function() {}; - }; - - chrome.send('packExtensionSelectFilePath', [selectType, operation]); + showFileDialog_: function(selectType, fileType, node) { + chrome.developerPrivate.choosePath(selectType, fileType, function(path) { + // Last error is set if the user canceled the dialog. + if (!chrome.runtime.lastError && path) + node.value = path; + }); }, /** @@ -74,9 +76,10 @@ cr.define('extensions', function() { * @private */ handleBrowseExtensionDir_: function(e) { - this.showFileDialog_('folder', 'load', function(filePath) { - $('extension-root-dir').value = filePath; - }); + this.showFileDialog_( + chrome.developerPrivate.SelectType.FOLDER, + chrome.developerPrivate.FileType.LOAD, + /** @type {HTMLInputElement} */ ($('extension-root-dir'))); }, /** @@ -85,44 +88,76 @@ cr.define('extensions', function() { * @private */ handleBrowsePrivateKey_: function(e) { - this.showFileDialog_('file', 'pem', function(filePath) { - $('extension-private-key').value = filePath; - }); + this.showFileDialog_( + chrome.developerPrivate.SelectType.FILE, + chrome.developerPrivate.FileType.PEM, + /** @type {HTMLInputElement} */ ($('extension-private-key'))); }, - }; - /** - * Wrap up the pack process by showing the success |message| and closing - * the overlay. - * @param {string} message The message to show to the user. - */ - PackExtensionOverlay.showSuccessMessage = function(message) { - alertOverlay.setValues( - loadTimeData.getString('packExtensionOverlay'), - message, - loadTimeData.getString('ok'), - '', - function() { - extensions.ExtensionSettings.showOverlay(null); - }); - extensions.ExtensionSettings.showOverlay($('alertOverlay')); - }; + /** + * Handles a response from a packDirectory call. + * @param {PackDirectoryResponse} response The response of the pack call. + * @private + */ + onPackResponse_: function(response) { + /** @type {string} */ + var alertTitle; + /** @type {string} */ + var alertOk; + /** @type {string} */ + var alertCancel; + /** @type {function()} */ + var alertOkCallback; + /** @type {function()} */ + var alertCancelCallback; - /** - * Post an alert overlay showing |message|, and upon acknowledgement, close - * the alert overlay and return to showing the PackExtensionOverlay. - * @param {string} message The error message. - */ - PackExtensionOverlay.showError = function(message) { - alertOverlay.setValues( - loadTimeData.getString('packExtensionErrorTitle'), - message, - loadTimeData.getString('ok'), - '', - function() { - extensions.ExtensionSettings.showOverlay($('pack-extension-overlay')); - }); - extensions.ExtensionSettings.showOverlay($('alertOverlay')); + var closeAlert = function() { + extensions.ExtensionSettings.showOverlay(null); + }; + + switch (response.status) { + case chrome.developerPrivate.PackStatus.SUCCESS: + alertTitle = loadTimeData.getString('packExtensionOverlay'); + alertOk = loadTimeData.getString('ok'); + alertOkCallback = closeAlert; + // No 'Cancel' option. + break; + case chrome.developerPrivate.PackStatus.WARNING: + alertTitle = loadTimeData.getString('packExtensionWarningTitle'); + alertOk = loadTimeData.getString('packExtensionProceedAnyway'); + alertCancel = loadTimeData.getString('cancel'); + alertOkCallback = function() { + chrome.developerPrivate.packDirectory( + response.item_path, + response.pem_path, + response.override_flags, + this.onPackResponse_.bind(this)); + closeAlert(); + }.bind(this); + alertCancelCallback = closeAlert; + break; + case chrome.developerPrivate.PackStatus.ERROR: + alertTitle = loadTimeData.getString('packExtensionErrorTitle'); + alertOk = loadTimeData.getString('ok'); + alertOkCallback = function() { + extensions.ExtensionSettings.showOverlay( + $('pack-extension-overlay')); + }; + // No 'Cancel' option. + break; + default: + assertNotReached(); + return; + } + + alertOverlay.setValues(alertTitle, + response.message, + alertOk, + alertCancel, + alertOkCallback, + alertCancelCallback); + extensions.ExtensionSettings.showOverlay($('alertOverlay')); + }, }; // Export |