summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/resources/extensions
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@theqtcompany.com>2015-06-18 14:10:49 +0200
committerOswald Buddenhagen <oswald.buddenhagen@theqtcompany.com>2015-06-18 13:53:24 +0000
commit813fbf95af77a531c57a8c497345ad2c61d475b3 (patch)
tree821b2c8de8365f21b6c9ba17a236fb3006a1d506 /chromium/chrome/browser/resources/extensions
parentaf6588f8d723931a298c995fa97259bb7f7deb55 (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')
-rw-r--r--chromium/chrome/browser/resources/extensions/chromeos/kiosk_app_list.js2
-rw-r--r--chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.css14
-rw-r--r--chromium/chrome/browser/resources/extensions/chromeos/kiosk_apps.js2
-rw-r--r--chromium/chrome/browser/resources/extensions/compiled_resources.gyp10
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_code.js13
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_command_list.js59
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_commands_overlay.js2
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_error.css100
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_error.html14
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_error.js382
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_error_overlay.css6
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_error_overlay.html1
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_error_overlay.js283
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_focus_manager.js12
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_info.css44
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_info.html31
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_info.js23
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_list.js1370
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_load_error.html2
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_loader.js24
-rw-r--r--chromium/chrome/browser/resources/extensions/extension_options_overlay.js169
-rw-r--r--chromium/chrome/browser/resources/extensions/extensions.css196
-rw-r--r--chromium/chrome/browser/resources/extensions/extensions.html64
-rw-r--r--chromium/chrome/browser/resources/extensions/extensions.js372
-rw-r--r--chromium/chrome/browser/resources/extensions/pack_extension_overlay.html1
-rw-r--r--chromium/chrome/browser/resources/extensions/pack_extension_overlay.js135
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