blob: 5e1812dbf250d7fd4b08858539e1d2b5230dca2b [file] [log] [blame]
// 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.
<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">
<include src="extension_error_overlay.js">
<include src="extension_focus_manager.js">
<include src="extension_list.js">
<include src="pack_extension_overlay.js">
<include src="extension_loader.js">
<include src="extension_options_overlay.js">
<if expr="chromeos">
<include src="chromeos/kiosk_apps.js">
</if>
// Used for observing function of the backend datasource for this page by
// tests.
var webuiResponded = false;
cr.define('extensions', function() {
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.
// See: http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p
return (e.dataTransfer.types &&
e.dataTransfer.types.indexOf('Files') > -1);
},
/** @override */
doDragEnter: function() {
chrome.send('startDrag');
ExtensionSettings.showOverlay($('drop-target-overlay'));
},
/** @override */
doDragLeave: function() {
this.hideDropTargetOverlay_();
chrome.send('stopDrag');
},
/** @override */
doDragOver: function(e) {
e.preventDefault();
},
/** @override */
doDrop: function(e) {
this.hideDropTargetOverlay_();
if (e.dataTransfer.files.length != 1)
return;
var toSend = null;
// Files lack a check if they're a directory, but we can find out through
// its item entry.
for (var i = 0; i < e.dataTransfer.items.length; ++i) {
if (e.dataTransfer.items[i].kind == 'file' &&
e.dataTransfer.items[i].webkitGetAsEntry().isDirectory) {
toSend = 'installDroppedDirectory';
break;
}
}
// Only process files that look like extensions. Other files should
// navigate the browser normally.
if (!toSend &&
/\.(crx|user\.js|zip)$/i.test(e.dataTransfer.files[0].name)) {
toSend = 'installDroppedFile';
}
if (toSend) {
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() {}
cr.addSingletonGetter(ExtensionSettings);
ExtensionSettings.prototype = {
__proto__: HTMLDivElement.prototype,
/**
* 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
*/
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.
*/
initialize: function() {
uber.onContentFrameLoaded();
cr.ui.FocusOutlineManager.forDocument(document);
measureCheckboxStrings();
// Set the title.
uber.setTitle(loadTimeData.getString('extensionSettings'));
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', 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) {
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));
if (!loadTimeData.getBoolean('offStoreInstallEnabled')) {
this.dragWrapper_ = new cr.ui.DragWrapper(document.documentElement,
dragWrapperHandler);
this.dragEnabled_ = true;
}
extensions.PackExtensionOverlay.getInstance().initializePage();
// Hook up the configure commands link to the overlay.
var link = document.querySelector('.extension-commands-config');
link.addEventListener('click',
this.handleExtensionCommandsConfig_.bind(this));
// Initialize the Commands overlay.
extensions.ExtensionCommandsOverlay.getInstance().initializePage();
extensions.ExtensionErrorOverlay.getInstance().initializePage(
extensions.ExtensionSettings.showOverlay);
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();
kioskOverlay.initialize();
$('add-kiosk-app').addEventListener('click', function() {
ExtensionSettings.showOverlay($('kiosk-apps-page'));
kioskOverlay.didShowPage();
});
extensions.KioskDisableBailoutConfirm.getInstance().initialize();
}
cr.ui.overlay.setupOverlay($('drop-target-overlay'));
cr.ui.overlay.globalInitialization();
extensions.ExtensionFocusManager.getInstance().initialize();
var path = document.location.pathname;
if (path.length > 1) {
// Skip starting slash and remove trailing slash (if any).
var overlayName = path.slice(1).replace(/\/$/, '');
if (overlayName == 'configureCommands')
this.showExtensionCommandsConfigUi_();
}
},
/**
* Updates the extensions page to the latest profile and extensions
* configuration.
* @private
*/
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);
this.onExtensionCountChanged();
}.bind(this));
},
/**
* Handles the Pack Extension button.
* @param {Event} e Change event.
* @private
*/
handlePackExtension_: function(e) {
ExtensionSettings.showOverlay($('pack-extension-overlay'));
chrome.send('metricsHandler:recordAction', ['Options_PackExtension']);
},
/**
* Shows the Extension Commands configuration UI.
* @private
*/
showExtensionCommandsConfigUi_: function() {
ExtensionSettings.showOverlay($('extension-commands-overlay'));
chrome.send('metricsHandler:recordAction',
['Options_ExtensionCommands']);
},
/**
* Handles the Configure (Extension) Commands link.
* @param {Event} e Change event.
* @private
*/
handleExtensionCommandsConfig_: function(e) {
this.showExtensionCommandsConfigUi_();
},
/**
* Handles the Update Extension Now button.
* @param {Event} e Change event.
* @private
*/
handleUpdateExtensionNow_: function(e) {
chrome.developerPrivate.autoUpdate();
chrome.send('metricsHandler:recordAction',
['Options_UpdateExtensions']);
},
/**
* 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
*/
updateDevControlsVisibility_: function(animated) {
var showDevControls = $('toggle-dev-on').checked;
$('extension-settings').classList.toggle('dev-mode', showDevControls);
var devControls = $('dev-controls');
devControls.classList.toggle('animated', animated);
var buttons = devControls.querySelector('.button-container');
Array.prototype.forEach.call(buttons.querySelectorAll('a, button'),
function(control) {
control.tabIndex = showDevControls ? 0 : -1;
});
buttons.setAttribute('aria-hidden', !showDevControls);
window.requestAnimationFrame(function() {
devControls.style.height = !showDevControls ? '' :
buttons.offsetHeight + 'px';
if (this.testingDeveloperModeCallback)
this.testingDeveloperModeCallback();
}.bind(this));
},
/** @override */
onExtensionCountChanged: function() {
/** @const */
var hasExtensions = $('extension-settings-list').getNumExtensions() != 0;
$('no-extensions').hidden = hasExtensions;
$('extension-list-wrapper').hidden = !hasExtensions;
},
};
/**
* Called by the WebUI when something has changed and the extensions UI needs
* to be updated.
*/
ExtensionSettings.onExtensionsChanged = function() {
ExtensionSettings.getInstance().update_();
};
/**
* Returns the current overlay or null if one does not exist.
* @return {Element} The overlay element.
*/
ExtensionSettings.getCurrentOverlay = function() {
return document.querySelector('#overlay .page.showing');
};
/**
* 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');
pageDiv.style.width = node ? window.getComputedStyle(pageDiv).width : '';
document.body.classList.toggle('no-scroll', !!node);
var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay();
if (currentlyShowingOverlay) {
if (currentlyShowingOverlay == node) // Already displayed.
return;
currentlyShowingOverlay.classList.remove('showing');
}
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++) {
pages[i].setAttribute('aria-hidden', node ? 'true' : 'false');
}
$('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
* Enable/Enabled checkboxes align, as well as the Developer Mode checkbox.
*/
function measureCheckboxStrings() {
var trashWidth = 30;
var measuringDiv = $('font-measuring-div');
measuringDiv.textContent =
loadTimeData.getString('extensionSettingsEnabled');
measuringDiv.className = 'enabled-text';
var pxWidth = measuringDiv.clientWidth + trashWidth;
measuringDiv.textContent =
loadTimeData.getString('extensionSettingsEnable');
measuringDiv.className = 'enable-text';
pxWidth = Math.max(measuringDiv.clientWidth + trashWidth, pxWidth);
measuringDiv.textContent =
loadTimeData.getString('extensionSettingsDeveloperMode');
measuringDiv.className = '';
pxWidth = Math.max(measuringDiv.clientWidth, pxWidth);
var style = document.createElement('style');
style.type = 'text/css';
style.textContent =
'.enable-checkbox-text {' +
' min-width: ' + (pxWidth - trashWidth) + 'px;' +
'}' +
'#dev-toggle span {' +
' min-width: ' + pxWidth + 'px;' +
'}';
document.querySelector('head').appendChild(style);
};
// Export
return {
ExtensionSettings: ExtensionSettings
};
});
window.addEventListener('load', function(e) {
document.documentElement.classList.add('loading');
extensions.ExtensionSettings.getInstance().initialize();
});