Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extensions.js
blobe957033c9c80a9ecfbea7892e3bafb158e24415f
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="../../../../ui/webui/resources/js/cr/ui/focus_row.js">
6 <include src="../../../../ui/webui/resources/js/cr/ui/focus_grid.js">
7 <include src="../uber/uber_utils.js">
8 <include src="extension_code.js">
9 <include src="extension_commands_overlay.js">
10 <include src="extension_error_overlay.js">
11 <include src="extension_focus_manager.js">
12 <include src="focus_row.js">
13 <include src="extension_list.js">
14 <include src="pack_extension_overlay.js">
15 <include src="extension_loader.js">
16 <include src="extension_options_overlay.js">
18 <if expr="chromeos">
19 <include src="chromeos/kiosk_apps.js">
20 </if>
22 // Used for observing function of the backend datasource for this page by
23 // tests.
24 var webuiResponded = false;
26 cr.define('extensions', function() {
27   var ExtensionList = extensions.ExtensionList;
29   // Implements the DragWrapper handler interface.
30   var dragWrapperHandler = {
31     /** @override */
32     shouldAcceptDrag: function(e) {
33       // External Extension installation can be disabled globally, e.g. while a
34       // different overlay is already showing.
35       if (!ExtensionSettings.getInstance().dragEnabled_)
36         return false;
38       // We can't access filenames during the 'dragenter' event, so we have to
39       // wait until 'drop' to decide whether to do something with the file or
40       // not.
41       // See: http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p
42       return (e.dataTransfer.types &&
43               e.dataTransfer.types.indexOf('Files') > -1);
44     },
45     /** @override */
46     doDragEnter: function() {
47       chrome.send('startDrag');
48       ExtensionSettings.showOverlay($('drop-target-overlay'));
49     },
50     /** @override */
51     doDragLeave: function() {
52       this.hideDropTargetOverlay_();
53       chrome.send('stopDrag');
54     },
55     /** @override */
56     doDragOver: function(e) {
57       e.preventDefault();
58     },
59     /** @override */
60     doDrop: function(e) {
61       this.hideDropTargetOverlay_();
62       if (e.dataTransfer.files.length != 1)
63         return;
65       var toSend = null;
66       // Files lack a check if they're a directory, but we can find out through
67       // its item entry.
68       for (var i = 0; i < e.dataTransfer.items.length; ++i) {
69         if (e.dataTransfer.items[i].kind == 'file' &&
70             e.dataTransfer.items[i].webkitGetAsEntry().isDirectory) {
71           toSend = 'installDroppedDirectory';
72           break;
73         }
74       }
75       // Only process files that look like extensions. Other files should
76       // navigate the browser normally.
77       if (!toSend &&
78           /\.(crx|user\.js|zip)$/i.test(e.dataTransfer.files[0].name)) {
79         toSend = 'installDroppedFile';
80       }
82       if (toSend) {
83         e.preventDefault();
84         chrome.send(toSend);
85       }
86     },
88     /**
89      * Hide the current overlay if it is the drop target overlay.
90      * @private
91      */
92     hideDropTargetOverlay_: function() {
93       var currentOverlay = ExtensionSettings.getCurrentOverlay();
94       if (currentOverlay && currentOverlay.id === 'drop-target-overlay')
95         ExtensionSettings.showOverlay(null);
96     }
97   };
99   /**
100    * ExtensionSettings class
101    * @class
102    * @constructor
103    * @implements {extensions.ExtensionListDelegate}
104    */
105   function ExtensionSettings() {}
107   cr.addSingletonGetter(ExtensionSettings);
109   ExtensionSettings.prototype = {
110     __proto__: HTMLDivElement.prototype,
112     /**
113      * The drag-drop wrapper for installing external Extensions, if available.
114      * null if external Extension installation is not available.
115      * @type {cr.ui.DragWrapper}
116      * @private
117      */
118     dragWrapper_: null,
120     /**
121      * True if drag-drop is both available and currently enabled - it can be
122      * temporarily disabled while overlays are showing.
123      * @type {boolean}
124      * @private
125      */
126     dragEnabled_: false,
128     /**
129      * True if the page has finished the initial load.
130      * @private {boolean}
131      */
132     hasLoaded_: false,
134     /**
135      * Perform initial setup.
136      */
137     initialize: function() {
138       this.setLoading_(true);
139       uber.onContentFrameLoaded();
140       cr.ui.FocusOutlineManager.forDocument(document);
141       measureCheckboxStrings();
143       // Set the title.
144       uber.setTitle(loadTimeData.getString('extensionSettings'));
146       var extensionList = new ExtensionList(this);
147       extensionList.id = 'extension-settings-list';
148       var wrapper = $('extension-list-wrapper');
149       wrapper.insertBefore(extensionList, wrapper.firstChild);
151       // Get the initial profile state, and register to be notified of any
152       // future changes.
153       chrome.developerPrivate.getProfileConfiguration(
154           this.update_.bind(this));
155       chrome.developerPrivate.onProfileStateChanged.addListener(
156           this.update_.bind(this));
158       var extensionLoader = extensions.ExtensionLoader.getInstance();
160       $('toggle-dev-on').addEventListener('change', function(e) {
161         this.updateDevControlsVisibility_(true);
162         chrome.developerPrivate.updateProfileConfiguration(
163             {inDeveloperMode: e.target.checked});
164         var suffix = $('toggle-dev-on').checked ? 'Enabled' : 'Disabled';
165         chrome.send('metricsHandler:recordAction',
166                     ['Options_ToggleDeveloperMode_' + suffix]);
167       }.bind(this));
169       window.addEventListener('resize', function() {
170         this.updateDevControlsVisibility_(false);
171       }.bind(this));
173       // Set up the three dev mode buttons (load unpacked, pack and update).
174       $('load-unpacked').addEventListener('click', function(e) {
175         chrome.send('metricsHandler:recordAction',
176                     ['Options_LoadUnpackedExtension']);
177         extensionLoader.loadUnpacked();
178       });
179       $('pack-extension').addEventListener('click',
180           this.handlePackExtension_.bind(this));
181       $('update-extensions-now').addEventListener('click',
182           this.handleUpdateExtensionNow_.bind(this));
184       if (!loadTimeData.getBoolean('offStoreInstallEnabled')) {
185         this.dragWrapper_ = new cr.ui.DragWrapper(document.documentElement,
186                                                   dragWrapperHandler);
187         this.dragEnabled_ = true;
188       }
190       extensions.PackExtensionOverlay.getInstance().initializePage();
192       // Hook up the configure commands link to the overlay.
193       var link = document.querySelector('.extension-commands-config');
194       link.addEventListener('click',
195           this.handleExtensionCommandsConfig_.bind(this));
197       // Initialize the Commands overlay.
198       extensions.ExtensionCommandsOverlay.getInstance().initializePage();
200       extensions.ExtensionErrorOverlay.getInstance().initializePage(
201           extensions.ExtensionSettings.showOverlay);
203       extensions.ExtensionOptionsOverlay.getInstance().initializePage(
204           extensions.ExtensionSettings.showOverlay);
206       // Add user action logging for bottom links.
207       var moreExtensionLink =
208           document.getElementsByClassName('more-extensions-link');
209       for (var i = 0; i < moreExtensionLink.length; i++) {
210         moreExtensionLink[i].addEventListener('click', function(e) {
211           chrome.send('metricsHandler:recordAction',
212                       ['Options_GetMoreExtensions']);
213         });
214       }
216       // Initialize the kiosk overlay.
217       if (cr.isChromeOS) {
218         var kioskOverlay = extensions.KioskAppsOverlay.getInstance();
219         kioskOverlay.initialize();
221         $('add-kiosk-app').addEventListener('click', function() {
222           ExtensionSettings.showOverlay($('kiosk-apps-page'));
223           kioskOverlay.didShowPage();
224         });
226         extensions.KioskDisableBailoutConfirm.getInstance().initialize();
227       }
229       cr.ui.overlay.setupOverlay($('drop-target-overlay'));
230       cr.ui.overlay.globalInitialization();
232       extensions.ExtensionFocusManager.getInstance().initialize();
234       var path = document.location.pathname;
235       if (path.length > 1) {
236         // Skip starting slash and remove trailing slash (if any).
237         var overlayName = path.slice(1).replace(/\/$/, '');
238         if (overlayName == 'configureCommands')
239           this.showExtensionCommandsConfigUi_();
240       }
241     },
243     /**
244      * [Re]-Populates the page with data representing the current state of
245      * installed extensions.
246      * @param {ProfileInfo} profileInfo
247      * @private
248      */
249     update_: function(profileInfo) {
250       // We only set the page to be loading if we haven't already finished an
251       // initial load, because otherwise the updates are all incremental and
252       // don't need to display the interstitial spinner.
253       if (!this.hasLoaded_)
254         this.setLoading_(true);
255       webuiResponded = true;
257       /** @const */
258       var supervised = profileInfo.isSupervised;
260       var pageDiv = $('extension-settings');
261       pageDiv.classList.toggle('profile-is-supervised', supervised);
262       pageDiv.classList.toggle('showing-banner', supervised);
264       var devControlsCheckbox = $('toggle-dev-on');
265       devControlsCheckbox.checked = profileInfo.inDeveloperMode;
266       devControlsCheckbox.disabled = supervised;
268       $('load-unpacked').disabled = !profileInfo.canLoadUnpacked;
269       var extensionList = $('extension-settings-list');
270       extensionList.updateExtensionsData(
271           profileInfo.isIncognitoAvailable,
272           profileInfo.appInfoDialogEnabled).then(function() {
273         if (!this.hasLoaded_) {
274           this.hasLoaded_ = true;
275           this.setLoading_(false);
276         }
277         this.onExtensionCountChanged();
278       }.bind(this));
279     },
281     /**
282      * Shows the loading spinner and hides elements that shouldn't be visible
283      * while loading.
284      * @param {boolean} isLoading
285      * @private
286      */
287     setLoading_: function(isLoading) {
288       document.documentElement.classList.toggle('loading', isLoading);
289       $('loading-spinner').hidden = !isLoading;
290       $('dev-controls').hidden = isLoading;
291       this.updateDevControlsVisibility_(false);
293       // The extension list is already hidden/shown elsewhere and shouldn't be
294       // updated here because it can be hidden if there are no extensions.
295     },
297     /**
298      * Handles the Pack Extension button.
299      * @param {Event} e Change event.
300      * @private
301      */
302     handlePackExtension_: function(e) {
303       ExtensionSettings.showOverlay($('pack-extension-overlay'));
304       chrome.send('metricsHandler:recordAction', ['Options_PackExtension']);
305     },
307     /**
308      * Shows the Extension Commands configuration UI.
309      * @private
310      */
311     showExtensionCommandsConfigUi_: function() {
312       ExtensionSettings.showOverlay($('extension-commands-overlay'));
313       chrome.send('metricsHandler:recordAction',
314                   ['Options_ExtensionCommands']);
315     },
317     /**
318      * Handles the Configure (Extension) Commands link.
319      * @param {Event} e Change event.
320      * @private
321      */
322     handleExtensionCommandsConfig_: function(e) {
323       this.showExtensionCommandsConfigUi_();
324     },
326     /**
327      * Handles the Update Extension Now button.
328      * @param {Event} e Change event.
329      * @private
330      */
331     handleUpdateExtensionNow_: function(e) {
332       chrome.developerPrivate.autoUpdate();
333       chrome.send('metricsHandler:recordAction',
334                   ['Options_UpdateExtensions']);
335     },
337     /**
338      * Updates the visibility of the developer controls based on whether the
339      * [x] Developer mode checkbox is checked.
340      * @param {boolean} animated Whether to animate any updates.
341      * @private
342      */
343     updateDevControlsVisibility_: function(animated) {
344       var showDevControls = $('toggle-dev-on').checked;
345       $('extension-settings').classList.toggle('dev-mode', showDevControls);
347       var devControls = $('dev-controls');
348       devControls.classList.toggle('animated', animated);
350       var buttons = devControls.querySelector('.button-container');
351       Array.prototype.forEach.call(buttons.querySelectorAll('a, button'),
352                                    function(control) {
353         control.tabIndex = showDevControls ? 0 : -1;
354       });
355       buttons.setAttribute('aria-hidden', !showDevControls);
357       window.requestAnimationFrame(function() {
358         devControls.style.height = !showDevControls ? '' :
359             buttons.offsetHeight + 'px';
361         document.dispatchEvent(new Event('devControlsVisibilityUpdated'));
362       }.bind(this));
363     },
365     /** @override */
366     onExtensionCountChanged: function() {
367       /** @const */
368       var hasExtensions = $('extension-settings-list').getNumExtensions() != 0;
369       $('no-extensions').hidden = hasExtensions;
370       $('extension-list-wrapper').hidden = !hasExtensions;
371     },
372   };
374   /**
375    * Returns the current overlay or null if one does not exist.
376    * @return {Element} The overlay element.
377    */
378   ExtensionSettings.getCurrentOverlay = function() {
379     return document.querySelector('#overlay .page.showing');
380   };
382   /**
383    * Sets the given overlay to show. If the overlay is already showing, this is
384    * a no-op; otherwise, hides any currently-showing overlay.
385    * @param {HTMLElement} node The overlay page to show. If null, all overlays
386    *     are hidden.
387    */
388   ExtensionSettings.showOverlay = function(node) {
389     var pageDiv = $('extension-settings');
390     pageDiv.style.width = node ? window.getComputedStyle(pageDiv).width : '';
391     document.body.classList.toggle('no-scroll', !!node);
393     var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay();
394     if (currentlyShowingOverlay) {
395       if (currentlyShowingOverlay == node)  // Already displayed.
396         return;
397       currentlyShowingOverlay.classList.remove('showing');
398     }
400     if (node) {
401       var lastFocused;
403       var focusOutlineManager = cr.ui.FocusOutlineManager.forDocument(document);
404       if (focusOutlineManager.visible)
405         lastFocused = document.activeElement;
407       $('overlay').addEventListener('cancelOverlay', function f() {
408         if (lastFocused && focusOutlineManager.visible)
409           lastFocused.focus();
411         $('overlay').removeEventListener('cancelOverlay', f);
412         uber.replaceState({}, '');
413       });
414       node.classList.add('showing');
415     }
417     var pages = document.querySelectorAll('.page');
418     for (var i = 0; i < pages.length; i++) {
419       pages[i].setAttribute('aria-hidden', node ? 'true' : 'false');
420     }
422     $('overlay').hidden = !node;
424     if (node)
425       ExtensionSettings.focusOverlay();
427     // If drag-drop for external Extension installation is available, enable
428     // drag-drop when there is any overlay showing other than the usual overlay
429     // shown when drag-drop is started.
430     var settings = ExtensionSettings.getInstance();
431     if (settings.dragWrapper_)
432       settings.dragEnabled_ = !node || node == $('drop-target-overlay');
434     uber.invokeMethodOnParent(node ? 'beginInterceptingEvents' :
435                                      'stopInterceptingEvents');
436   };
438   ExtensionSettings.focusOverlay = function() {
439     var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay();
440     assert(currentlyShowingOverlay);
442     if (cr.ui.FocusOutlineManager.forDocument(document).visible)
443       cr.ui.setInitialFocus(currentlyShowingOverlay);
445     if (!currentlyShowingOverlay.contains(document.activeElement)) {
446       // Make sure focus isn't stuck behind the overlay.
447       document.activeElement.blur();
448     }
449   };
451   /**
452    * Utility function to find the width of various UI strings and synchronize
453    * the width of relevant spans. This is crucial for making sure the
454    * Enable/Enabled checkboxes align, as well as the Developer Mode checkbox.
455    */
456   function measureCheckboxStrings() {
457     var trashWidth = 30;
458     var measuringDiv = $('font-measuring-div');
459     measuringDiv.textContent =
460         loadTimeData.getString('extensionSettingsEnabled');
461     measuringDiv.className = 'enabled-text';
462     var pxWidth = measuringDiv.clientWidth + trashWidth;
463     measuringDiv.textContent =
464         loadTimeData.getString('extensionSettingsEnable');
465     measuringDiv.className = 'enable-text';
466     pxWidth = Math.max(measuringDiv.clientWidth + trashWidth, pxWidth);
467     measuringDiv.textContent =
468         loadTimeData.getString('extensionSettingsDeveloperMode');
469     measuringDiv.className = '';
470     pxWidth = Math.max(measuringDiv.clientWidth, pxWidth);
472     var style = document.createElement('style');
473     style.type = 'text/css';
474     style.textContent =
475         '.enable-checkbox-text {' +
476         '  min-width: ' + (pxWidth - trashWidth) + 'px;' +
477         '}' +
478         '#dev-toggle span {' +
479         '  min-width: ' + pxWidth + 'px;' +
480         '}';
481     document.querySelector('head').appendChild(style);
482   };
484   // Export
485   return {
486     ExtensionSettings: ExtensionSettings
487   };
490 window.addEventListener('load', function(e) {
491   extensions.ExtensionSettings.getInstance().initialize();