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