Close embedded extension options page when the overlay is hidden
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extensions.js
blobc7f9bb5d37f7fc05f773247edf34ac48c81b737a
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.
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">
15 <if expr="chromeos">
16 <include src="chromeos/kiosk_apps.js">
17 </if>
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;
32 // Used for observing function of the backend datasource for this page by
33 // tests.
34 var webuiResponded = false;
36 cr.define('extensions', function() {
37   var ExtensionsList = options.ExtensionsList;
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       this.hideDropTargetOverlay_();
59       chrome.send('stopDrag');
60     },
61     /** @override */
62     doDragOver: function(e) {
63       e.preventDefault();
64     },
65     /** @override */
66     doDrop: function(e) {
67       this.hideDropTargetOverlay_();
68       if (e.dataTransfer.files.length != 1)
69         return;
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       }
88       if (toSend) {
89         e.preventDefault();
90         chrome.send(toSend);
91       }
92     },
94     /**
95      * Hide the current overlay if it is the drop target overlay.
96      * @private
97      */
98     hideDropTargetOverlay_: function() {
99       var currentOverlay = ExtensionSettings.getCurrentOverlay();
100       if (currentOverlay && currentOverlay.id === 'drop-target-overlay')
101         ExtensionSettings.showOverlay(null);
102     }
103   };
105   /**
106    * ExtensionSettings class
107    * @class
108    */
109   function ExtensionSettings() {}
111   cr.addSingletonGetter(ExtensionSettings);
113   ExtensionSettings.prototype = {
114     __proto__: HTMLDivElement.prototype,
116     /**
117      * Whether or not to try to display the Apps Developer Tools promotion.
118      * @type {boolean}
119      * @private
120      */
121     displayPromo_: false,
123     /**
124      * Perform initial setup.
125      */
126     initialize: function() {
127       uber.onContentFrameLoaded();
128       cr.ui.FocusOutlineManager.forDocument(document);
129       measureCheckboxStrings();
131       // Set the title.
132       uber.setTitle(loadTimeData.getString('extensionSettings'));
134       // This will request the data to show on the page and will get a response
135       // back in returnExtensionsData.
136       chrome.send('extensionSettingsRequestExtensionsData');
138       var extensionLoader = extensions.ExtensionLoader.getInstance();
140       $('toggle-dev-on').addEventListener('change',
141           this.handleToggleDevMode_.bind(this));
142       $('dev-controls').addEventListener('webkitTransitionEnd',
143           this.handleDevControlsTransitionEnd_.bind(this));
145       // Set up the three dev mode buttons (load unpacked, pack and update).
146       $('load-unpacked').addEventListener('click', function(e) {
147           extensionLoader.loadUnpacked();
148       });
149       $('pack-extension').addEventListener('click',
150           this.handlePackExtension_.bind(this));
151       $('update-extensions-now').addEventListener('click',
152           this.handleUpdateExtensionNow_.bind(this));
154       // Set up the close dialog for the apps developer tools promo.
155       $('apps-developer-tools-promo').querySelector('.close-button').
156           addEventListener('click', function(e) {
157         this.displayPromo_ = false;
158         this.updatePromoVisibility_();
159         chrome.send('extensionSettingsDismissADTPromo');
160       }.bind(this));
162       if (!loadTimeData.getBoolean('offStoreInstallEnabled')) {
163         this.dragWrapper_ = new cr.ui.DragWrapper(document.documentElement,
164                                                   dragWrapperHandler);
165       }
167       extensions.PackExtensionOverlay.getInstance().initializePage();
169       // Hook up the configure commands link to the overlay.
170       var link = document.querySelector('.extension-commands-config');
171       link.addEventListener('click',
172           this.handleExtensionCommandsConfig_.bind(this));
174       // Initialize the Commands overlay.
175       extensions.ExtensionCommandsOverlay.getInstance().initializePage();
177       extensions.ExtensionErrorOverlay.getInstance().initializePage(
178           extensions.ExtensionSettings.showOverlay);
180       extensions.ExtensionOptionsOverlay.getInstance().initializePage(
181           extensions.ExtensionSettings.showOverlay);
183       // Initialize the kiosk overlay.
184       if (cr.isChromeOS) {
185         var kioskOverlay = extensions.KioskAppsOverlay.getInstance();
186         kioskOverlay.initialize();
188         $('add-kiosk-app').addEventListener('click', function() {
189           ExtensionSettings.showOverlay($('kiosk-apps-page'));
190           kioskOverlay.didShowPage();
191         });
193         extensions.KioskDisableBailoutConfirm.getInstance().initialize();
194       }
196       cr.ui.overlay.setupOverlay($('drop-target-overlay'));
197       cr.ui.overlay.globalInitialization();
199       extensions.ExtensionFocusManager.getInstance().initialize();
201       var path = document.location.pathname;
202       if (path.length > 1) {
203         // Skip starting slash and remove trailing slash (if any).
204         var overlayName = path.slice(1).replace(/\/$/, '');
205         if (overlayName == 'configureCommands')
206           this.showExtensionCommandsConfigUi_();
207       }
208     },
210     /**
211      * Updates the Chrome Apps and Extensions Developer Tools promotion's
212      * visibility.
213      * @private
214      */
215     updatePromoVisibility_: function() {
216       var extensionSettings = $('extension-settings');
217       var visible = extensionSettings.classList.contains('dev-mode') &&
218                     this.displayPromo_;
220       var adtPromo = $('apps-developer-tools-promo');
221       var controls = adtPromo.querySelectorAll('a, button');
222       Array.prototype.forEach.call(controls, function(control) {
223         control[visible ? 'removeAttribute' : 'setAttribute']('tabindex', '-1');
224       });
226       adtPromo.setAttribute('aria-hidden', !visible);
227       extensionSettings.classList.toggle('adt-promo', visible);
228     },
230     /**
231      * Handles the Pack Extension button.
232      * @param {Event} e Change event.
233      * @private
234      */
235     handlePackExtension_: function(e) {
236       ExtensionSettings.showOverlay($('pack-extension-overlay'));
237       chrome.send('metricsHandler:recordAction', ['Options_PackExtension']);
238     },
240     /**
241      * Shows the Extension Commands configuration UI.
242      * @param {Event} e Change event.
243      * @private
244      */
245     showExtensionCommandsConfigUi_: function(e) {
246       ExtensionSettings.showOverlay($('extension-commands-overlay'));
247       chrome.send('metricsHandler:recordAction',
248                   ['Options_ExtensionCommands']);
249     },
251     /**
252      * Handles the Configure (Extension) Commands link.
253      * @param {Event} e Change event.
254      * @private
255      */
256     handleExtensionCommandsConfig_: function(e) {
257       this.showExtensionCommandsConfigUi_();
258     },
260     /**
261      * Handles the Update Extension Now button.
262      * @param {Event} e Change event.
263      * @private
264      */
265     handleUpdateExtensionNow_: function(e) {
266       chrome.send('extensionSettingsAutoupdate');
267     },
269     /**
270      * Handles the Toggle Dev Mode button.
271      * @param {Event} e Change event.
272      * @private
273      */
274     handleToggleDevMode_: function(e) {
275       if ($('toggle-dev-on').checked) {
276         $('dev-controls').hidden = false;
277         window.setTimeout(function() {
278           $('extension-settings').classList.add('dev-mode');
279         }, 0);
280       } else {
281         $('extension-settings').classList.remove('dev-mode');
282       }
283       window.setTimeout(this.updatePromoVisibility_.bind(this), 0);
285       chrome.send('extensionSettingsToggleDeveloperMode');
286     },
288     /**
289      * Called when a transition has ended for #dev-controls.
290      * @param {Event} e webkitTransitionEnd event.
291      * @private
292      */
293     handleDevControlsTransitionEnd_: function(e) {
294       if (e.propertyName == 'height' &&
295           !$('extension-settings').classList.contains('dev-mode')) {
296         $('dev-controls').hidden = true;
297       }
298     },
299   };
301   /**
302    * Called by the dom_ui_ to re-populate the page with data representing
303    * the current state of installed extensions.
304    * @param {ExtensionDataResponse} extensionsData
305    */
306   ExtensionSettings.returnExtensionsData = function(extensionsData) {
307     // We can get called many times in short order, thus we need to
308     // be careful to remove the 'finished loading' timeout.
309     if (this.loadingTimeout_)
310       window.clearTimeout(this.loadingTimeout_);
311     document.documentElement.classList.add('loading');
312     this.loadingTimeout_ = window.setTimeout(function() {
313       document.documentElement.classList.remove('loading');
314     }, 0);
316     webuiResponded = true;
318     if (extensionsData.extensions.length > 0) {
319       // Enforce order specified in the data or (if equal) then sort by
320       // extension name (case-insensitive) followed by their ID (in the case
321       // where extensions have the same name).
322       extensionsData.extensions.sort(function(a, b) {
323         function compare(x, y) {
324           return x < y ? -1 : (x > y ? 1 : 0);
325         }
326         return compare(a.order, b.order) ||
327                compare(a.name.toLowerCase(), b.name.toLowerCase()) ||
328                compare(a.id, b.id);
329       });
330     }
332     var pageDiv = $('extension-settings');
333     var marginTop = 0;
334     if (extensionsData.profileIsSupervised) {
335       pageDiv.classList.add('profile-is-supervised');
336     } else {
337       pageDiv.classList.remove('profile-is-supervised');
338     }
339     if (extensionsData.profileIsSupervised) {
340       pageDiv.classList.add('showing-banner');
341       $('toggle-dev-on').disabled = true;
342       marginTop += 45;
343     } else {
344       pageDiv.classList.remove('showing-banner');
345       $('toggle-dev-on').disabled = false;
346     }
348     pageDiv.style.marginTop = marginTop + 'px';
350     if (extensionsData.developerMode) {
351       pageDiv.classList.add('dev-mode');
352       $('toggle-dev-on').checked = true;
353       $('dev-controls').hidden = false;
354     } else {
355       pageDiv.classList.remove('dev-mode');
356       $('toggle-dev-on').checked = false;
357     }
359     ExtensionSettings.getInstance().displayPromo_ =
360         extensionsData.promoteAppsDevTools;
361     ExtensionSettings.getInstance().updatePromoVisibility_();
363     $('load-unpacked').disabled = extensionsData.loadUnpackedDisabled;
365     ExtensionsList.prototype.data_ = extensionsData;
366     var extensionList = $('extension-settings-list');
367     ExtensionsList.decorate(extensionList);
368   };
370   // Indicate that warning |message| has occured for pack of |crx_path| and
371   // |pem_path| files.  Ask if user wants override the warning.  Send
372   // |overrideFlags| to repeated 'pack' call to accomplish the override.
373   ExtensionSettings.askToOverrideWarning =
374       function(message, crx_path, pem_path, overrideFlags) {
375     var closeAlert = function() {
376       ExtensionSettings.showOverlay(null);
377     };
379     alertOverlay.setValues(
380         loadTimeData.getString('packExtensionWarningTitle'),
381         message,
382         loadTimeData.getString('packExtensionProceedAnyway'),
383         loadTimeData.getString('cancel'),
384         function() {
385           chrome.send('pack', [crx_path, pem_path, overrideFlags]);
386           closeAlert();
387         },
388         closeAlert);
389     ExtensionSettings.showOverlay($('alertOverlay'));
390   };
392   /**
393    * Returns the current overlay or null if one does not exist.
394    * @return {Element} The overlay element.
395    */
396   ExtensionSettings.getCurrentOverlay = function() {
397     return document.querySelector('#overlay .page.showing');
398   };
400   /**
401    * Sets the given overlay to show. This hides whatever overlay is currently
402    * showing, if any.
403    * @param {HTMLElement} node The overlay page to show. If falsey, all overlays
404    *     are hidden.
405    */
406   ExtensionSettings.showOverlay = function(node) {
407     var pageDiv = $('extension-settings');
408     if (node) {
409       pageDiv.style.width = window.getComputedStyle(pageDiv).width;
410       document.body.classList.add('no-scroll');
411     } else {
412       document.body.classList.remove('no-scroll');
413       pageDiv.style.width = '';
414     }
416     var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay();
417     if (currentlyShowingOverlay) {
418       currentlyShowingOverlay.classList.remove('showing');
419       cr.dispatchSimpleEvent($('overlay'), 'cancelOverlay');
420     }
422     if (node) {
423       if (document.activeElement != document.body)
424         document.activeElement.blur();
425       node.classList.add('showing');
426     }
428     var pages = document.querySelectorAll('.page');
429     for (var i = 0; i < pages.length; i++) {
430       pages[i].setAttribute('aria-hidden', node ? 'true' : 'false');
431     }
433     $('overlay').hidden = !node;
434     uber.invokeMethodOnParent(node ? 'beginInterceptingEvents' :
435                                      'stopInterceptingEvents');
436   };
438   /**
439    * Utility function to find the width of various UI strings and synchronize
440    * the width of relevant spans. This is crucial for making sure the
441    * Enable/Enabled checkboxes align, as well as the Developer Mode checkbox.
442    */
443   function measureCheckboxStrings() {
444     var trashWidth = 30;
445     var measuringDiv = $('font-measuring-div');
446     measuringDiv.textContent =
447         loadTimeData.getString('extensionSettingsEnabled');
448     measuringDiv.className = 'enabled-text';
449     var pxWidth = measuringDiv.clientWidth + trashWidth;
450     measuringDiv.textContent =
451         loadTimeData.getString('extensionSettingsEnable');
452     measuringDiv.className = 'enable-text';
453     pxWidth = Math.max(measuringDiv.clientWidth + trashWidth, pxWidth);
454     measuringDiv.textContent =
455         loadTimeData.getString('extensionSettingsDeveloperMode');
456     measuringDiv.className = '';
457     pxWidth = Math.max(measuringDiv.clientWidth, pxWidth);
459     var style = document.createElement('style');
460     style.type = 'text/css';
461     style.textContent =
462         '.enable-checkbox-text {' +
463         '  min-width: ' + (pxWidth - trashWidth) + 'px;' +
464         '}' +
465         '#dev-toggle span {' +
466         '  min-width: ' + pxWidth + 'px;' +
467         '}';
468     document.querySelector('head').appendChild(style);
469   };
471   // Export
472   return {
473     ExtensionSettings: ExtensionSettings
474   };
477 window.addEventListener('load', function(e) {
478   extensions.ExtensionSettings.getInstance().initialize();