1 // Copyright 2015 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.
6 * CWSWidgetContainer contains a Chrome Web Store widget that displays list of
7 * apps that satisfy certain constraints (e.g. fileHandler apps that can handle
8 * files with specific file extension or MIME type) and enables the user to
9 * install apps directly from it.
10 * CWSWidgetContainer implements client side of the widget, which handles
11 * widget loading and app installation.
15 * The width of the widget (in pixels)
19 var WEBVIEW_WIDTH = 735;
22 * The height of the widget (in pixels).
26 var WEBVIEW_HEIGHT = 480;
29 * The URL of the widget showing suggested apps.
34 'https://clients5.google.com/webstore/wall/cros-widget-container';
37 * The origin of the widget.
41 var CWS_WIDGET_ORIGIN = 'https://clients5.google.com';
44 * Creates the widget container element in DOM tree.
46 * @param {!HTMLDocument} document The document to contain this container.
47 * @param {!HTMLElement} parentNode Node to be parent for this container.
48 * @param {!CWSWidgetContainer.PlatformDelegate} delegate Delegate for accessing
49 * Chrome platform APIs.
51 * overrideCwsContainerUrlForTest: (string|undefined),
52 * overrideCwsContainerOriginForTest: (string|undefined)
53 * }} params Overrides for container params.
56 function CWSWidgetContainer(document, parentNode, delegate, params) {
57 /** @private {!CWSWidgetContainer.PlatformDelegate} */
58 this.delegate_ = delegate;
60 /** @private {!CWSWidgetContainer.MetricsRecorder} */
61 this.metricsRecorder_ =
62 new CWSWidgetContainer.MetricsRecorder(delegate.metricsImpl);
65 * The document that will contain the container.
66 * @const {!HTMLDocument}
69 this.document_ = document;
72 * The element containing the widget webview.
76 this.webviewContainer_ = document.createElement('div');
77 this.webviewContainer_.classList.add('cws-widget-webview-container');
78 this.webviewContainer_.style.width = WEBVIEW_WIDTH + 'px';
79 this.webviewContainer_.style.height = WEBVIEW_HEIGHT + 'px';
80 parentNode.appendChild(this.webviewContainer_);
82 parentNode.classList.add('cws-widget-container-root');
85 * Element showing spinner layout in place of Web Store widget.
88 var spinnerLayer = document.createElement('div');
89 spinnerLayer.className = 'cws-widget-spinner-layer';
90 parentNode.appendChild(spinnerLayer);
92 /** @private {!CWSWidgetContainer.SpinnerLayerController} */
93 this.spinnerLayerController_ =
94 new CWSWidgetContainer.SpinnerLayerController(spinnerLayer);
97 * The widget container's button strip.
100 var buttons = document.createElement('div');
101 buttons.classList.add('cws-widget-buttons');
102 parentNode.appendChild(buttons);
105 * Button that opens the Webstore URL.
109 this.webstoreButton_ = document.createElement('div');
110 this.webstoreButton_.hidden = true;
111 this.webstoreButton_.setAttribute('role', 'button');
112 this.webstoreButton_.tabIndex = 0;
115 * Icon for the Webstore button.
118 var webstoreButtonIcon = this.document_.createElement('span');
119 webstoreButtonIcon.classList.add('cws-widget-webstore-button-icon');
120 this.webstoreButton_.appendChild(webstoreButtonIcon);
123 * The label for the Webstore button.
126 var webstoreButtonLabel = this.document_.createElement('span');
127 webstoreButtonLabel.classList.add('cws-widget-webstore-button-label');
128 webstoreButtonLabel.textContent = this.delegate_.strings.LINK_TO_WEBSTORE;
129 this.webstoreButton_.appendChild(webstoreButtonLabel);
131 this.webstoreButton_.addEventListener(
132 'click', this.onWebstoreLinkActivated_.bind(this));
133 this.webstoreButton_.addEventListener(
134 'keydown', this.onWebstoreLinkKeyDown_.bind(this));
136 buttons.appendChild(this.webstoreButton_);
139 * The webview element containing the Chrome Web Store widget.
143 this.webview_ = null;
146 * The Chrome Web Store widget URL.
150 this.widgetUrl_ = params.overrideCwsContainerUrlForTest || CWS_WIDGET_URL;
153 * The Chrome Web Store widget origin.
157 this.widgetOrigin_ = params.overrideCwsContainerOriginForTest ||
161 * Map of options for the widget.
165 this.options_ = null;
168 * The ID of the item being installed. Null if no items are being installed.
172 this.installingItemId_ = null;
175 * The ID of the the installed item. Null if no item was installed.
179 this.installedItemId_ = null;
182 * The current widget state.
183 * @type {CWSWidgetContainer.State}
186 this.state_ = CWSWidgetContainer.State.UNINITIALIZED;
189 * The Chrome Web Store access token to be used when communicating with the
190 * Chrome Web Store widget.
194 this.accessToken_ = null;
197 * Called when the Chrome Web Store widget is done. It resolves the promise
198 * returned by {@code this.start()}.
199 * @type {?function(CWSWidgetContainer.ResolveReason)}
202 this.resolveStart_ = null;
205 * Promise for retriving {@code this.accessToken_}.
206 * @type {Promise.<string>}
209 this.tokenGetter_ = this.createTokenGetter_();
212 * Dialog to be shown when an installation attempt fails.
213 * @type {CWSWidgetContainerErrorDialog}
216 this.errorDialog_ = new CWSWidgetContainerErrorDialog(parentNode);
220 * Strings required by the widget container.
223 * LINK_TO_WEBSTORE: string,
224 * INSTALLATION_FAILED_MESSAGE: string,
225 * LOADING_SPINNER_ALT: string,
226 * INSTALLING_SPINNER_ALT: string
229 CWSWidgetContainer.Strings;
232 * Functions for reporting metrics for the widget.
234 * recordEnum: function(string, number, number),
235 * recordUserAction: function(string),
236 * startInterval: function(string),
237 * recordInterval: function(string)
240 CWSWidgetContainer.MetricsImpl;
243 * Type for delegate used by CWSWidgetContainer component to access Chrome
246 * strings: !CWSWidgetContainer.Strings,
247 * metricsImpl: !CWSWidgetContainer.MetricsImpl,
248 * installWebstoreItem: function(string, function(?string)),
249 * getInstalledItems: function(function(?Array<!string>)),
250 * requestWebstoreAccessToken: function(function(?string))
253 CWSWidgetContainer.PlatformDelegate;
259 CWSWidgetContainer.State = {
260 UNINITIALIZED: 'CWSWidgetContainer.State.UNINITIALIZED',
261 GETTING_ACCESS_TOKEN: 'CWSWidgetContainer.State.GETTING_ACCESS_TOKEN',
262 ACCESS_TOKEN_READY: 'CWSWidgetContainer.State.ACCESS_TOKEN_READY',
263 INITIALIZING: 'CWSWidgetContainer.State.INITIALIZING',
264 INITIALIZE_FAILED_CLOSING:
265 'CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING',
266 INITIALIZED: 'CWSWidgetContainer.State.INITIALIZED',
267 INSTALLING: 'CWSWidgetContainer.State.INSTALLING',
268 WAITING_FOR_CONFIRMATION: 'CWSWidgetContainer.State.WAITING_FOR_CONFIRMATION',
269 INSTALLED_CLOSING: 'CWSWidgetContainer.State.INSTALLED_CLOSING',
270 OPENING_WEBSTORE_CLOSING: 'CWSWidgetContainer.State.OPENING_WEBSTORE_CLOSING',
271 CANCELED_CLOSING: 'CWSWidgetContainer.State.CANCELED_CLOSING'
273 Object.freeze(CWSWidgetContainer.State);
279 CWSWidgetContainer.Result = {
280 /** Install is done. The install app should be opened. */
281 INSTALL_SUCCESSFUL: 'CWSWidgetContainer.Result.INSTALL_SUCCESSFUL',
282 /** User cancelled the suggest app dialog. No message should be shown. */
283 USER_CANCEL: 'CWSWidgetContainer.Result.USER_CANCEL',
284 /** User clicked the link to web store so the dialog is closed. */
285 WEBSTORE_LINK_OPENED: 'CWSWidgetContainer.Result.WEBSTORE_LINK_OPENED',
286 /** Failed to load the widget. Error message should be shown. */
287 FAILED: 'CWSWidgetContainer.Result.FAILED'
289 Object.freeze(CWSWidgetContainer.Result);
292 * The reason due to which the container is resolving {@code this.start}
296 CWSWidgetContainer.ResolveReason = {
297 /** The widget container ended up in its final state. */
298 DONE: 'CWSWidgetContainer.ResolveReason.DONE',
299 /** The widget container is being reset. */
300 RESET: 'CWSWidgetContainer.CloserReason.RESET'
302 Object.freeze(CWSWidgetContainer.ResolveReason);
305 * @return {!Element} The element that should be focused initially.
307 CWSWidgetContainer.prototype.getInitiallyFocusedElement = function() {
308 return this.webviewContainer_;
312 * Injects headers into the passed request.
314 * @param {!Object} e Request event.
315 * @return {!BlockingResponse} Modified headers.
318 CWSWidgetContainer.prototype.authorizeRequest_ = function(e) {
319 e.requestHeaders.push({
320 name: 'Authorization',
321 value: 'Bearer ' + this.accessToken_
323 return /** @type {!BlockingResponse}*/ ({requestHeaders: e.requestHeaders});
327 * Retrieves the authorize token.
328 * @return {Promise.<string>} The promise with the retrived access token.
331 CWSWidgetContainer.prototype.createTokenGetter_ = function() {
332 return new Promise(function(resolve, reject) {
333 if (window.IN_TEST) {
334 // In test, use a dummy string as token. This must be a non-empty string.
335 resolve('DUMMY_ACCESS_TOKEN_FOR_TEST');
339 // Fetch or update the access token.
340 this.delegate_.requestWebstoreAccessToken(
341 /** @param {?string} accessToken The requested token. Null on error. */
342 function(accessToken) {
344 reject('Error retriveing Web Store access token.');
353 * @return {boolean} Whether the container is in initial state, i.e. inactive.
355 CWSWidgetContainer.prototype.isInInitialState = function() {
356 return this.state_ === CWSWidgetContainer.State.UNINITIALIZED;
360 * Ensures that the widget container is in the state where it can properly
361 * handle showing the Chrome Web Store webview.
362 * @return {Promise} Resolved when the container is ready to be used.
364 CWSWidgetContainer.prototype.ready = function() {
365 return new Promise(function(resolve, reject) {
366 if (this.state_ !== CWSWidgetContainer.State.UNINITIALIZED) {
367 reject('Invalid state.');
371 this.spinnerLayerController_.setAltText(
372 this.delegate_.strings.LOADING_SPINNER_ALT);
373 this.spinnerLayerController_.setVisible(true);
375 this.metricsRecorder_.recordShowDialog();
376 this.metricsRecorder_.startLoad();
378 this.state_ = CWSWidgetContainer.State.GETTING_ACCESS_TOKEN;
380 this.tokenGetter_.then(function(accessToken) {
381 this.state_ = CWSWidgetContainer.State.ACCESS_TOKEN_READY;
382 this.accessToken_ = accessToken;
384 }.bind(this), function(error) {
385 this.spinnerLayerController_.setVisible(false);
386 this.state_ = CWSWidgetContainer.State.UNINITIALIZED;
387 reject('Failed to get Web Store access token: ' + error);
393 * Initializes and starts loading the Chrome Web Store widget webview.
394 * Must not be called before {@code this.ready()} is resolved.
396 * @param {!Object<*>} options Map of options for the dialog.
397 * @param {?string} webStoreUrl Url for more results. Null if not supported.
398 * @return {!Promise.<CWSWidgetContainer.ResolveReason>} Resolved when app
399 * installation is done, or the installation is cancelled.
401 CWSWidgetContainer.prototype.start = function(options, webStoreUrl) {
402 return new Promise(function(resolve, reject) {
403 if (this.state_ !== CWSWidgetContainer.State.ACCESS_TOKEN_READY) {
404 this.state_ = CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING;
405 reject('Invalid state in |start|.');
409 if (!this.accessToken_) {
410 this.state_ = CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING;
411 reject('No access token.');
415 this.resolveStart_ = resolve;
417 this.state_ = CWSWidgetContainer.State.INITIALIZING;
419 this.webStoreUrl_ = webStoreUrl;
420 this.options_ = options;
422 this.webstoreButton_.hidden = !webStoreUrl;
423 this.webstoreButton_.classList.toggle('cws-widget-webstore-button',
427 /** @type {!WebView} */(this.document_.createElement('webview'));
428 this.webview_.id = 'cws-widget';
429 this.webview_.partition = 'persist:cwswidgets';
430 this.webview_.style.width = WEBVIEW_WIDTH + 'px';
431 this.webview_.style.height = WEBVIEW_HEIGHT + 'px';
432 this.webview_.request.onBeforeSendHeaders.addListener(
433 this.authorizeRequest_.bind(this),
434 /** @type {!RequestFilter}*/ ({urls: [this.widgetOrigin_ + '/*']}),
435 ['blocking', 'requestHeaders']);
436 this.webview_.addEventListener('newwindow', function(event) {
437 event = /** @type {NewWindowEvent} */ (event);
438 // Discard the window object and reopen in an external window.
439 event.window.discard();
440 window.open(event.targetUrl);
441 event.preventDefault();
443 this.webviewContainer_.appendChild(this.webview_);
445 this.spinnerLayerController_.setElementToFocusOnHide(this.webview_);
446 this.spinnerLayerController_.setAltText(
447 this.delegate_.strings.LOADING_SPINNER_ALT);
448 this.spinnerLayerController_.setVisible(true);
450 this.webviewClient_ = new CWSContainerClient(
458 this.webviewClient_.addEventListener(CWSContainerClient.Events.LOADED,
459 this.onWidgetLoaded_.bind(this));
460 this.webviewClient_.addEventListener(CWSContainerClient.Events.LOAD_FAILED,
461 this.onWidgetLoadFailed_.bind(this));
462 this.webviewClient_.addEventListener(
463 CWSContainerClient.Events.REQUEST_INSTALL,
464 this.onInstallRequest_.bind(this));
465 this.webviewClient_.addEventListener(
466 CWSContainerClient.Events.INSTALL_DONE,
467 this.onInstallDone_.bind(this));
468 this.webviewClient_.load();
473 * Called when the 'See more...' button is activated. It opens
474 * {@code this.webstoreUrl_}.
475 * @param {Event} e The event that activated the link. Either mouse click or
479 CWSWidgetContainer.prototype.onWebstoreLinkActivated_ = function(e) {
480 if (!this.webStoreUrl_)
482 window.open(this.webStoreUrl_);
483 this.state_ = CWSWidgetContainer.State.OPENING_WEBSTORE_CLOSING;
488 * Key down event handler for webstore button element. If the key is enter, it
489 * activates the button.
490 * @param {Event} e The event
493 CWSWidgetContainer.prototype.onWebstoreLinkKeyDown_ = function(e) {
494 if (e.keyCode !== 13 /* Enter */)
496 this.onWebstoreLinkActivated_(e);
500 * Called when the widget is loaded successfully.
501 * @param {Event} event Event.
504 CWSWidgetContainer.prototype.onWidgetLoaded_ = function(event) {
505 this.metricsRecorder_.finishLoad();
506 this.metricsRecorder_.recordLoad(
507 CWSWidgetContainer.MetricsRecorder.LOAD.SUCCEEDED);
509 this.state_ = CWSWidgetContainer.State.INITIALIZED;
511 this.spinnerLayerController_.setVisible(false);
512 this.webview_.focus();
516 * Called when the widget is failed to load.
517 * @param {Event} event Event.
520 CWSWidgetContainer.prototype.onWidgetLoadFailed_ = function(event) {
521 this.metricsRecorder_.recordLoad(
522 CWSWidgetContainer.MetricsRecorder.LOAD.FAILED);
524 this.spinnerLayerController_.setVisible(false);
525 this.state_ = CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING;
530 * Called when the connection status is changed to offline.
532 CWSWidgetContainer.prototype.onConnectionLost = function() {
533 if (this.state_ !== CWSWidgetContainer.State.UNINITIALIZED) {
534 this.state_ = CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING;
540 * Called when receiving the install request from the webview client.
541 * @param {Event} e Event.
544 CWSWidgetContainer.prototype.onInstallRequest_ = function(e) {
545 var itemId = e.itemId;
546 this.installingItemId_ = itemId;
548 this.appInstaller_ = new AppInstaller(itemId, this.delegate_);
549 this.appInstaller_.install(this.onItemInstalled_.bind(this));
551 this.spinnerLayerController_.setAltText(
552 this.delegate_.strings.INSTALLING_SPINNER_ALT);
553 this.spinnerLayerController_.setVisible(true);
554 this.state_ = CWSWidgetContainer.State.INSTALLING;
558 * Called when the webview client receives install confirmation from the
560 * @param {Event} e Event
563 CWSWidgetContainer.prototype.onInstallDone_ = function(e) {
564 this.spinnerLayerController_.setVisible(false);
565 this.state_ = CWSWidgetContainer.State.INSTALLED_CLOSING;
570 * Called when the installation is completed from the app installer.
571 * @param {AppInstaller.Result} result Result of the installation.
572 * @param {string} error Detail of the error.
575 CWSWidgetContainer.prototype.onItemInstalled_ = function(result, error) {
576 var success = (result === AppInstaller.Result.SUCCESS);
578 // If install succeeded, the spinner will be removed once
579 // |this.webviewClient_| dispatched INSTALL_DONE event.
581 this.spinnerLayerController_.setVisible(false);
583 this.state_ = success ?
584 CWSWidgetContainer.State.WAITING_FOR_CONFIRMATION :
585 CWSWidgetContainer.State.INITIALIZED; // Back to normal state.
586 this.webviewClient_.onInstallCompleted(success, this.installingItemId_);
587 this.installedItemId_ = this.installingItemId_;
588 this.installingItemId_ = null;
591 case AppInstaller.Result.SUCCESS:
592 this.metricsRecorder_.recordInstall(
593 CWSWidgetContainer.MetricsRecorder.INSTALL.SUCCEEDED);
594 // Wait for the widget webview container to dispatch INSTALL_DONE.
596 case AppInstaller.Result.CANCELLED:
597 this.metricsRecorder_.recordInstall(
598 CWSWidgetContainer.MetricsRecorder.INSTALL.CANCELLED);
599 // User cancelled the installation. Do nothing.
601 case AppInstaller.Result.ERROR:
602 this.metricsRecorder_.recordInstall(
603 CWSWidgetContainer.MetricsRecorder.INSTALL.FAILED);
604 this.errorDialog_.show(
605 this.delegate_.strings.INSTALLATION_FAILED_MESSAGE,
614 * Resolves the promise returned by {@code this.start} when widget is done with
618 CWSWidgetContainer.prototype.reportDone_ = function() {
619 if (this.resolveStart_)
620 this.resolveStart_(CWSWidgetContainer.ResolveReason.DONE);
621 this.resolveStart_ = null;
625 * Finalizes the widget container state and returns the final app instalation
626 * result. The widget should not be used after calling this. If called before
627 * promise returned by {@code this.start} is resolved, the reported result will
628 * be as if the widget was cancelled.
629 * @return {{result: CWSWidgetContainer.Result, installedItemId: ?string}}
631 CWSWidgetContainer.prototype.finalizeAndGetResult = function() {
632 switch (this.state_) {
633 case CWSWidgetContainer.State.INSTALLING:
634 // Install is being aborted. Send the failure result.
635 // Cancels the install.
636 if (this.webviewClient_)
637 this.webviewClient_.onInstallCompleted(false, this.installingItemId_);
638 this.installingItemId_ = null;
640 // Assumes closing the dialog as canceling the install.
641 this.state_ = CWSWidgetContainer.State.CANCELED_CLOSING;
643 case CWSWidgetContainer.State.GETTING_ACCESS_TOKEN:
644 case CWSWidgetContainer.State.ACCESS_TOKEN_READY:
645 case CWSWidgetContainer.State.INITIALIZING:
646 this.metricsRecorder_.recordLoad(
647 CWSWidgetContainer.MetricsRecorder.LOAD.CANCELLED);
648 this.state_ = CWSWidgetContainer.State.CANCELED_CLOSING;
650 case CWSWidgetContainer.State.WAITING_FOR_CONFIRMATION:
651 // This can happen if the dialog is closed by the user before Web Store
652 // widget replies with 'after_install'.
653 // Consider this success, as the app has actually been installed.
654 // TODO(tbarzic): Should the app be uninstalled in this case?
655 this.state_ = CWSWidgetContainer.State.INSTALLED_CLOSING;
657 case CWSWidgetContainer.State.INSTALLED_CLOSING:
658 case CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING:
659 case CWSWidgetContainer.State.OPENING_WEBSTORE_CLOSING:
662 case CWSWidgetContainer.State.INITIALIZED:
663 this.state_ = CWSWidgetContainer.State.CANCELED_CLOSING;
666 this.state_ = CWSWidgetContainer.State.CANCELED_CLOSING;
667 console.error('Invalid state.');
671 switch (this.state_) {
672 case CWSWidgetContainer.State.INSTALLED_CLOSING:
673 result = CWSWidgetContainer.Result.INSTALL_SUCCESSFUL;
674 this.metricsRecorder_.recordCloseDialog(
675 CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.ITEM_INSTALLED);
677 case CWSWidgetContainer.State.INITIALIZE_FAILED_CLOSING:
678 result = CWSWidgetContainer.Result.FAILED;
680 case CWSWidgetContainer.State.CANCELED_CLOSING:
681 result = CWSWidgetContainer.Result.USER_CANCEL;
682 this.metricsRecorder_.recordCloseDialog(
683 CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.USER_CANCELLED);
685 case CWSWidgetContainer.State.OPENING_WEBSTORE_CLOSING:
686 result = CWSWidgetContainer.Result.WEBSTORE_LINK_OPENED;
687 this.metricsRecorder_.recordCloseDialog(
688 CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.WEBSTORE_LINK_OPENED);
691 result = CWSWidgetContainer.Result.USER_CANCEL;
692 this.metricsRecorder_.recordCloseDialog(
693 CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.UNKNOWN_ERROR);
696 this.state_ = CWSWidgetContainer.State.UNINITIALIZED;
700 return {result: result, installedItemId: this.installedItemId_};
707 CWSWidgetContainer.prototype.reset_ = function () {
708 if (this.state_ !== CWSWidgetContainer.State.UNINITIALIZED)
709 console.error('Widget reset before its state was finalized.');
711 if (this.resolveStart_) {
712 this.resolveStart_(CWSWidgetContainer.ResolveReason.RESET);
713 this.resolveStart_ = null;
716 this.spinnerLayerController_.reset();
718 if (this.webviewClient_) {
719 this.webviewClient_.dispose();
720 this.webviewClient_ = null;
724 this.webviewContainer_.removeChild(this.webview_);
725 this.webview_ = null;
728 if (this.appInstaller_) {
729 this.appInstaller_.cancel();
730 this.appInstaller_ = null;
733 this.options_ = null;
735 if (this.errorDialog_.shown())
736 this.errorDialog_.hide();
740 * Controls showing and hiding spinner layer.
741 * @param {!Element} spinnerLayer The spinner layer element.
744 CWSWidgetContainer.SpinnerLayerController = function(spinnerLayer) {
745 /** @private {!Element} */
746 this.spinnerLayer_ = spinnerLayer;
748 /** @private {boolean} */
749 this.visible_ = false;
752 * Set only if spinner is transitioning between visible and hidden states.
753 * Calling the function clears event handlers set for handling the transition,
754 * and updates spinner layer class list to its final state.
755 * @type {?function()}
758 this.clearTransition_ = null;
761 * Reference to the timeout set to ensure {@code this.clearTransision_} gets
762 * called even if 'transitionend' event does not fire.
766 this.clearTransitionTimeout_ = null;
769 * Element to be focused when the layer is hidden.
773 this.focusOnHide_ = null;
775 spinnerLayer.tabIndex = -1;
777 // Prevent default Tab key handling in order to prevent the widget from
778 // taking the focus while the spinner layer is active.
779 // NOTE: This assumes that there are no elements allowed to become active
780 // while the spinner is shown. Something smarter would be needed if this
781 // assumption becomes invalid.
782 spinnerLayer.addEventListener('keydown', this.handleKeyDown_.bind(this));
786 * Sets element to be focused when the layer is hidden.
787 * @param {!Element} el
789 CWSWidgetContainer.SpinnerLayerController.prototype.setElementToFocusOnHide =
791 this.focusOnHide_ = el;
795 * Prevents default Tab key handling in order to prevent spinner layer from
797 * @param {Event} e The key down event.
800 CWSWidgetContainer.SpinnerLayerController.prototype.handleKeyDown_ =
804 if (e.keyCode === 9 /* Tab */)
809 * Resets the spinner layer controllers state, and makes sure the spinner
812 CWSWidgetContainer.SpinnerLayerController.prototype.reset = function() {
813 this.visible_ = false;
814 this.focusOnHide_ = null;
815 if (this.clearTransision_)
816 this.clearTransition_();
820 * Sets alt text for the spinner layer.
821 * @param {string} text
823 CWSWidgetContainer.SpinnerLayerController.prototype.setAltText = function(
825 this.spinnerLayer_.setAttribute('aria-label', text);
829 * Shows or hides the spinner layer and handles the layer's opacity transition.
830 * @param {boolean} visible Whether the layer should become visible.
832 CWSWidgetContainer.SpinnerLayerController.prototype.setVisible =
834 if (this.visible_ === visible)
837 if (this.clearTransition_)
838 this.clearTransition_();
840 this.visible_ = visible;
842 // Spinner should be shown during transition.
843 this.spinnerLayer_.classList.toggle('cws-widget-show-spinner', true);
846 this.spinnerLayer_.focus();
847 } else if (this.focusOnHide_) {
848 this.focusOnHide_.focus();
852 this.spinnerLayer_.classList.add('cws-widget-hiding-spinner');
854 this.clearTransition_ = function() {
855 if (this.clearTransitionTimeout_)
856 clearTimeout(this.clearTransitionTimeout_);
857 this.clearTransitionTimeout_ = null;
859 this.spinnerLayer_.removeEventListener(
860 'transitionend', this.clearTransition_);
861 this.clearTransition_ = null;
863 if (!this.visible_) {
864 this.spinnerLayer_.classList.remove('cws-widget-hiding-spinner');
865 this.spinnerLayer_.classList.remove('cws-widget-show-spinner');
869 this.spinnerLayer_.addEventListener('transitionend', this.clearTransition_);
871 // Ensure the transition state gets cleared, even if transitionend is not
873 this.clearTransitionTimeout_ = setTimeout(function() {
874 this.clearTransitionTimeout_ = null;
875 this.clearTransition_();
876 }.bind(this), 550 /* ms */);
880 * Utility methods and constants to record histograms.
881 * @param {!CWSWidgetContainer.MetricsImpl} metricsImpl
884 CWSWidgetContainer.MetricsRecorder = function(metricsImpl) {
885 /** @private {!CWSWidgetContainer.MetricsImpl} */
886 this.metricsImpl_ = metricsImpl;
893 CWSWidgetContainer.MetricsRecorder.LOAD = {
903 CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG = {
907 WEBSTORE_LINK_OPENED: 3,
914 CWSWidgetContainer.MetricsRecorder.INSTALL = {
921 * @param {number} result Result of load, which must be defined in
922 * CWSWidgetContainer.MetricsRecorder.LOAD.
924 CWSWidgetContainer.MetricsRecorder.prototype.recordLoad = function(result) {
925 if (0 <= result && result < 3)
926 this.metricsImpl_.recordEnum('Load', result, 3);
930 * @param {number} reason Reason of closing dialog, which must be defined in
931 * CWSWidgetContainer.MetricsRecorder.CLOSE_DIALOG.
933 CWSWidgetContainer.MetricsRecorder.prototype.recordCloseDialog = function(
935 if (0 <= reason && reason < 4)
936 this.metricsImpl_.recordEnum('CloseDialog', reason, 4);
940 * @param {number} result Result of installation, which must be defined in
941 * CWSWidgetContainer.MetricsRecorder.INSTALL.
943 CWSWidgetContainer.MetricsRecorder.prototype.recordInstall = function(result) {
944 if (0 <= result && result < 3)
945 this.metricsImpl_.recordEnum('Install', result, 3);
948 CWSWidgetContainer.MetricsRecorder.prototype.recordShowDialog = function() {
949 this.metricsImpl_.recordUserAction('ShowDialog');
952 CWSWidgetContainer.MetricsRecorder.prototype.startLoad = function() {
953 this.metricsImpl_.startInterval('LoadTime');
956 CWSWidgetContainer.MetricsRecorder.prototype.finishLoad = function() {
957 this.metricsImpl_.recordInterval('LoadTime');