// 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. // 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(); });