1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 <include src="../uber/uber_utils.js"> 6 <include src="extension_code.js"> 7 <include src="extension_commands_overlay.js"> 8 <include src="extension_error_overlay.js"> 9 <include src="extension_focus_manager.js"> 10 <include src="extension_list.js"> 11 <include src="pack_extension_overlay.js"> 12 <include src="extension_loader.js"> 13 <include src="extension_options_overlay.js"> 14 15 <if expr="chromeos"> 16 <include src="chromeos/kiosk_apps.js"> 17 </if> 18 19 /** 20 * The type of the extension data object. The definition is based on 21 * chrome/browser/ui/webui/extensions/extension_settings_handler.cc: 22 * ExtensionSettingsHandler::HandleRequestExtensionsData() 23 * @typedef {{developerMode: boolean, 24 * extensions: Array, 25 * incognitoAvailable: boolean, 26 * loadUnpackedDisabled: boolean, 27 * profileIsSupervised: boolean, 28 * promoteAppsDevTools: boolean}} 29 */ 30 var ExtensionDataResponse; 31 32 // Used for observing function of the backend datasource for this page by 33 // tests. 34 var webuiResponded = false; 35 36 cr.define('extensions', function() { 37 var ExtensionsList = options.ExtensionsList; 38 39 // Implements the DragWrapper handler interface. 40 var dragWrapperHandler = { 41 /** @override */ 42 shouldAcceptDrag: function(e) { 43 // We can't access filenames during the 'dragenter' event, so we have to 44 // wait until 'drop' to decide whether to do something with the file or 45 // not. 46 // See: http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p 47 return (e.dataTransfer.types && 48 e.dataTransfer.types.indexOf('Files') > -1); 49 }, 50 /** @override */ 51 doDragEnter: function() { 52 chrome.send('startDrag'); 53 ExtensionSettings.showOverlay(null); 54 ExtensionSettings.showOverlay($('drop-target-overlay')); 55 }, 56 /** @override */ 57 doDragLeave: function() { 58 ExtensionSettings.showOverlay(null); 59 chrome.send('stopDrag'); 60 }, 61 /** @override */ 62 doDragOver: function(e) { 63 e.preventDefault(); 64 }, 65 /** @override */ 66 doDrop: function(e) { 67 ExtensionSettings.showOverlay(null); 68 if (e.dataTransfer.files.length != 1) 69 return; 70 71 var toSend = null; 72 // Files lack a check if they're a directory, but we can find out through 73 // its item entry. 74 for (var i = 0; i < e.dataTransfer.items.length; ++i) { 75 if (e.dataTransfer.items[i].kind == 'file' && 76 e.dataTransfer.items[i].webkitGetAsEntry().isDirectory) { 77 toSend = 'installDroppedDirectory'; 78 break; 79 } 80 } 81 // Only process files that look like extensions. Other files should 82 // navigate the browser normally. 83 if (!toSend && 84 /\.(crx|user\.js|zip)$/i.test(e.dataTransfer.files[0].name)) { 85 toSend = 'installDroppedFile'; 86 } 87 88 if (toSend) { 89 e.preventDefault(); 90 chrome.send(toSend); 91 } 92 } 93 }; 94 95 /** 96 * ExtensionSettings class 97 * @class 98 */ 99 function ExtensionSettings() {} 100 101 cr.addSingletonGetter(ExtensionSettings); 102 103 ExtensionSettings.prototype = { 104 __proto__: HTMLDivElement.prototype, 105 106 /** 107 * Whether or not to try to display the Apps Developer Tools promotion. 108 * @type {boolean} 109 * @private 110 */ 111 displayPromo_: false, 112 113 /** 114 * Perform initial setup. 115 */ 116 initialize: function() { 117 uber.onContentFrameLoaded(); 118 cr.ui.FocusOutlineManager.forDocument(document); 119 measureCheckboxStrings(); 120 121 // Set the title. 122 uber.setTitle(loadTimeData.getString('extensionSettings')); 123 124 // This will request the data to show on the page and will get a response 125 // back in returnExtensionsData. 126 chrome.send('extensionSettingsRequestExtensionsData'); 127 128 var extensionLoader = extensions.ExtensionLoader.getInstance(); 129 130 $('toggle-dev-on').addEventListener('change', 131 this.handleToggleDevMode_.bind(this)); 132 $('dev-controls').addEventListener('webkitTransitionEnd', 133 this.handleDevControlsTransitionEnd_.bind(this)); 134 135 // Set up the three dev mode buttons (load unpacked, pack and update). 136 $('load-unpacked').addEventListener('click', function(e) { 137 extensionLoader.loadUnpacked(); 138 }); 139 $('pack-extension').addEventListener('click', 140 this.handlePackExtension_.bind(this)); 141 $('update-extensions-now').addEventListener('click', 142 this.handleUpdateExtensionNow_.bind(this)); 143 144 // Set up the close dialog for the apps developer tools promo. 145 $('apps-developer-tools-promo').querySelector('.close-button'). 146 addEventListener('click', function(e) { 147 this.displayPromo_ = false; 148 this.updatePromoVisibility_(); 149 chrome.send('extensionSettingsDismissADTPromo'); 150 }.bind(this)); 151 152 if (!loadTimeData.getBoolean('offStoreInstallEnabled')) { 153 this.dragWrapper_ = new cr.ui.DragWrapper(document.documentElement, 154 dragWrapperHandler); 155 } 156 157 extensions.PackExtensionOverlay.getInstance().initializePage(); 158 159 // Hook up the configure commands link to the overlay. 160 var link = document.querySelector('.extension-commands-config'); 161 link.addEventListener('click', 162 this.handleExtensionCommandsConfig_.bind(this)); 163 164 // Initialize the Commands overlay. 165 extensions.ExtensionCommandsOverlay.getInstance().initializePage(); 166 167 extensions.ExtensionErrorOverlay.getInstance().initializePage( 168 extensions.ExtensionSettings.showOverlay); 169 170 extensions.ExtensionOptionsOverlay.getInstance().initializePage( 171 extensions.ExtensionSettings.showOverlay); 172 173 // Initialize the kiosk overlay. 174 if (cr.isChromeOS) { 175 var kioskOverlay = extensions.KioskAppsOverlay.getInstance(); 176 kioskOverlay.initialize(); 177 178 $('add-kiosk-app').addEventListener('click', function() { 179 ExtensionSettings.showOverlay($('kiosk-apps-page')); 180 kioskOverlay.didShowPage(); 181 }); 182 183 extensions.KioskDisableBailoutConfirm.getInstance().initialize(); 184 } 185 186 cr.ui.overlay.setupOverlay($('drop-target-overlay')); 187 cr.ui.overlay.globalInitialization(); 188 189 extensions.ExtensionFocusManager.getInstance().initialize(); 190 191 var path = document.location.pathname; 192 if (path.length > 1) { 193 // Skip starting slash and remove trailing slash (if any). 194 var overlayName = path.slice(1).replace(/\/$/, ''); 195 if (overlayName == 'configureCommands') 196 this.showExtensionCommandsConfigUi_(); 197 } 198 199 preventDefaultOnPoundLinkClicks(); // From webui/js/util.js. 200 }, 201 202 /** 203 * Updates the Chrome Apps and Extensions Developer Tools promotion's 204 * visibility. 205 * @private 206 */ 207 updatePromoVisibility_: function() { 208 var extensionSettings = $('extension-settings'); 209 var visible = extensionSettings.classList.contains('dev-mode') && 210 this.displayPromo_; 211 212 var adtPromo = $('apps-developer-tools-promo'); 213 var controls = adtPromo.querySelectorAll('a, button'); 214 Array.prototype.forEach.call(controls, function(control) { 215 control[visible ? 'removeAttribute' : 'setAttribute']('tabindex', '-1'); 216 }); 217 218 adtPromo.setAttribute('aria-hidden', !visible); 219 extensionSettings.classList.toggle('adt-promo', visible); 220 }, 221 222 /** 223 * Handles the Pack Extension button. 224 * @param {Event} e Change event. 225 * @private 226 */ 227 handlePackExtension_: function(e) { 228 ExtensionSettings.showOverlay($('pack-extension-overlay')); 229 chrome.send('metricsHandler:recordAction', ['Options_PackExtension']); 230 }, 231 232 /** 233 * Shows the Extension Commands configuration UI. 234 * @param {Event} e Change event. 235 * @private 236 */ 237 showExtensionCommandsConfigUi_: function(e) { 238 ExtensionSettings.showOverlay($('extension-commands-overlay')); 239 chrome.send('metricsHandler:recordAction', 240 ['Options_ExtensionCommands']); 241 }, 242 243 /** 244 * Handles the Configure (Extension) Commands link. 245 * @param {Event} e Change event. 246 * @private 247 */ 248 handleExtensionCommandsConfig_: function(e) { 249 this.showExtensionCommandsConfigUi_(); 250 }, 251 252 /** 253 * Handles the Update Extension Now button. 254 * @param {Event} e Change event. 255 * @private 256 */ 257 handleUpdateExtensionNow_: function(e) { 258 chrome.send('extensionSettingsAutoupdate'); 259 }, 260 261 /** 262 * Handles the Toggle Dev Mode button. 263 * @param {Event} e Change event. 264 * @private 265 */ 266 handleToggleDevMode_: function(e) { 267 if ($('toggle-dev-on').checked) { 268 $('dev-controls').hidden = false; 269 window.setTimeout(function() { 270 $('extension-settings').classList.add('dev-mode'); 271 }, 0); 272 } else { 273 $('extension-settings').classList.remove('dev-mode'); 274 } 275 window.setTimeout(this.updatePromoVisibility_.bind(this), 0); 276 277 chrome.send('extensionSettingsToggleDeveloperMode'); 278 }, 279 280 /** 281 * Called when a transition has ended for #dev-controls. 282 * @param {Event} e webkitTransitionEnd event. 283 * @private 284 */ 285 handleDevControlsTransitionEnd_: function(e) { 286 if (e.propertyName == 'height' && 287 !$('extension-settings').classList.contains('dev-mode')) { 288 $('dev-controls').hidden = true; 289 } 290 }, 291 }; 292 293 /** 294 * Called by the dom_ui_ to re-populate the page with data representing 295 * the current state of installed extensions. 296 * @param {ExtensionDataResponse} extensionsData 297 */ 298 ExtensionSettings.returnExtensionsData = function(extensionsData) { 299 // We can get called many times in short order, thus we need to 300 // be careful to remove the 'finished loading' timeout. 301 if (this.loadingTimeout_) 302 window.clearTimeout(this.loadingTimeout_); 303 document.documentElement.classList.add('loading'); 304 this.loadingTimeout_ = window.setTimeout(function() { 305 document.documentElement.classList.remove('loading'); 306 }, 0); 307 308 webuiResponded = true; 309 310 if (extensionsData.extensions.length > 0) { 311 // Enforce order specified in the data or (if equal) then sort by 312 // extension name (case-insensitive) followed by their ID (in the case 313 // where extensions have the same name). 314 extensionsData.extensions.sort(function(a, b) { 315 function compare(x, y) { 316 return x < y ? -1 : (x > y ? 1 : 0); 317 } 318 return compare(a.order, b.order) || 319 compare(a.name.toLowerCase(), b.name.toLowerCase()) || 320 compare(a.id, b.id); 321 }); 322 } 323 324 var pageDiv = $('extension-settings'); 325 var marginTop = 0; 326 if (extensionsData.profileIsSupervised) { 327 pageDiv.classList.add('profile-is-supervised'); 328 } else { 329 pageDiv.classList.remove('profile-is-supervised'); 330 } 331 if (extensionsData.profileIsSupervised) { 332 pageDiv.classList.add('showing-banner'); 333 $('toggle-dev-on').disabled = true; 334 marginTop += 45; 335 } else { 336 pageDiv.classList.remove('showing-banner'); 337 $('toggle-dev-on').disabled = false; 338 } 339 340 pageDiv.style.marginTop = marginTop + 'px'; 341 342 if (extensionsData.developerMode) { 343 pageDiv.classList.add('dev-mode'); 344 $('toggle-dev-on').checked = true; 345 $('dev-controls').hidden = false; 346 } else { 347 pageDiv.classList.remove('dev-mode'); 348 $('toggle-dev-on').checked = false; 349 } 350 351 ExtensionSettings.getInstance().displayPromo_ = 352 extensionsData.promoteAppsDevTools; 353 ExtensionSettings.getInstance().updatePromoVisibility_(); 354 355 $('load-unpacked').disabled = extensionsData.loadUnpackedDisabled; 356 357 ExtensionsList.prototype.data_ = extensionsData; 358 var extensionList = $('extension-settings-list'); 359 ExtensionsList.decorate(extensionList); 360 }; 361 362 // Indicate that warning |message| has occured for pack of |crx_path| and 363 // |pem_path| files. Ask if user wants override the warning. Send 364 // |overrideFlags| to repeated 'pack' call to accomplish the override. 365 ExtensionSettings.askToOverrideWarning = 366 function(message, crx_path, pem_path, overrideFlags) { 367 var closeAlert = function() { 368 ExtensionSettings.showOverlay(null); 369 }; 370 371 alertOverlay.setValues( 372 loadTimeData.getString('packExtensionWarningTitle'), 373 message, 374 loadTimeData.getString('packExtensionProceedAnyway'), 375 loadTimeData.getString('cancel'), 376 function() { 377 chrome.send('pack', [crx_path, pem_path, overrideFlags]); 378 closeAlert(); 379 }, 380 closeAlert); 381 ExtensionSettings.showOverlay($('alertOverlay')); 382 }; 383 384 /** 385 * Returns the current overlay or null if one does not exist. 386 * @return {Element} The overlay element. 387 */ 388 ExtensionSettings.getCurrentOverlay = function() { 389 return document.querySelector('#overlay .page.showing'); 390 }; 391 392 /** 393 * Sets the given overlay to show. This hides whatever overlay is currently 394 * showing, if any. 395 * @param {HTMLElement} node The overlay page to show. If falsey, all overlays 396 * are hidden. 397 */ 398 ExtensionSettings.showOverlay = function(node) { 399 var pageDiv = $('extension-settings'); 400 if (node) { 401 pageDiv.style.width = window.getComputedStyle(pageDiv).width; 402 document.body.classList.add('no-scroll'); 403 } else { 404 document.body.classList.remove('no-scroll'); 405 pageDiv.style.width = ''; 406 } 407 408 var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay(); 409 if (currentlyShowingOverlay) 410 currentlyShowingOverlay.classList.remove('showing'); 411 412 if (node) 413 node.classList.add('showing'); 414 415 var pages = document.querySelectorAll('.page'); 416 for (var i = 0; i < pages.length; i++) { 417 pages[i].setAttribute('aria-hidden', node ? 'true' : 'false'); 418 } 419 420 $('overlay').hidden = !node; 421 uber.invokeMethodOnParent(node ? 'beginInterceptingEvents' : 422 'stopInterceptingEvents'); 423 }; 424 425 /** 426 * Utility function to find the width of various UI strings and synchronize 427 * the width of relevant spans. This is crucial for making sure the 428 * Enable/Enabled checkboxes align, as well as the Developer Mode checkbox. 429 */ 430 function measureCheckboxStrings() { 431 var trashWidth = 30; 432 var measuringDiv = $('font-measuring-div'); 433 measuringDiv.textContent = 434 loadTimeData.getString('extensionSettingsEnabled'); 435 measuringDiv.className = 'enabled-text'; 436 var pxWidth = measuringDiv.clientWidth + trashWidth; 437 measuringDiv.textContent = 438 loadTimeData.getString('extensionSettingsEnable'); 439 measuringDiv.className = 'enable-text'; 440 pxWidth = Math.max(measuringDiv.clientWidth + trashWidth, pxWidth); 441 measuringDiv.textContent = 442 loadTimeData.getString('extensionSettingsDeveloperMode'); 443 measuringDiv.className = ''; 444 pxWidth = Math.max(measuringDiv.clientWidth, pxWidth); 445 446 var style = document.createElement('style'); 447 style.type = 'text/css'; 448 style.textContent = 449 '.enable-checkbox-text {' + 450 ' min-width: ' + (pxWidth - trashWidth) + 'px;' + 451 '}' + 452 '#dev-toggle span {' + 453 ' min-width: ' + pxWidth + 'px;' + 454 '}'; 455 document.querySelector('head').appendChild(style); 456 }; 457 458 // Export 459 return { 460 ExtensionSettings: ExtensionSettings 461 }; 462 }); 463 464 window.addEventListener('load', function(e) { 465 extensions.ExtensionSettings.getInstance().initialize(); 466 }); 467